import pg from 'pg'; export const schema=` CREATE TABLE IF NOT EXISTS character_sheets(owner text NOT NULL,id text NOT NULL,character jsonb,revision integer NOT NULL DEFAULT 1,deleted boolean NOT NULL DEFAULT false,PRIMARY KEY(owner,id)); CREATE TABLE IF NOT EXISTS mcp_clients(id text PRIMARY KEY,metadata jsonb NOT NULL); CREATE TABLE IF NOT EXISTS mcp_grants(id text PRIMARY KEY,payload jsonb NOT NULL,expires_at bigint NOT NULL,revoked boolean NOT NULL DEFAULT false); CREATE TABLE IF NOT EXISTS mcp_codes(id text PRIMARY KEY,payload jsonb NOT NULL,expires_at bigint NOT NULL); `; export class Conflict extends Error{constructor(){super('Character changed elsewhere. Reload its current revision before saving.');this.status=409}} export class Storage{ constructor(pool){this.pool=pool} async init(){await this.pool.query(schema)} async list(owner){return (await this.pool.query('SELECT id,character,revision,deleted FROM character_sheets WHERE owner=$1 ORDER BY id',[owner])).rows} async get(owner,id){return (await this.pool.query('SELECT id,character,revision,deleted FROM character_sheets WHERE owner=$1 AND id=$2',[owner,id])).rows[0]} async put(owner,id,character,revision,deleted=false){ // Preserve additive collections omitted by older clients, under the same account/revision guard. if(character&&!deleted&&revision>0&&(character.inventoryItems===undefined||character.currency===undefined||character.companions===undefined)){const prior=await this.get(owner,id);for(const key of ['inventoryItems','currency','companions'])if(character[key]===undefined&&prior?.character?.[key]!==undefined)character={...character,[key]:prior.character[key]}} const result=revision===0?await this.pool.query('INSERT INTO character_sheets(owner,id,character,deleted) VALUES($1,$2,$3,$4) ON CONFLICT(owner,id) DO NOTHING RETURNING id,character,revision,deleted',[owner,id,character,deleted]):await this.pool.query('UPDATE character_sheets SET character=$3,deleted=$4,revision=revision+1 WHERE owner=$1 AND id=$2 AND revision=$5 RETURNING id,character,revision,deleted',[owner,id,character,deleted,revision]); if(!result.rows[0])throw new Conflict();return result.rows[0]; } async client(id){return (await this.pool.query('SELECT metadata FROM mcp_clients WHERE id=$1',[id])).rows[0]?.metadata} async register(id,metadata){await this.pool.query('INSERT INTO mcp_clients(id,metadata) VALUES($1,$2)',[id,metadata]);return metadata} async grant(id,payload,expires){await this.pool.query('INSERT INTO mcp_grants(id,payload,expires_at) VALUES($1,$2,$3)',[id,payload,expires])} async readGrant(id){const r=(await this.pool.query('SELECT payload,expires_at,revoked FROM mcp_grants WHERE id=$1',[id])).rows[0];return r&&!r.revoked&&Number(r.expires_at)>Date.now()?r.payload:undefined} async revoke(id){await this.pool.query('UPDATE mcp_grants SET revoked=true WHERE id=$1',[id])} async code(id,payload,seconds=300){await this.pool.query('DELETE FROM mcp_codes WHERE expires_at<$1',[Date.now()]);await this.pool.query('INSERT INTO mcp_codes(id,payload,expires_at) VALUES($1,$2,$3)',[id,payload,Date.now()+seconds*1000])} async consume(id){const r=await this.pool.query('DELETE FROM mcp_codes WHERE id=$1 RETURNING payload,expires_at',[id]);const row=r.rows[0];return row&&Number(row.expires_at)>Date.now()?row.payload:undefined} } export async function productionStorage(){if(!process.env.DATABASE_URL)throw new Error('DATABASE_URL is required');const storage=new Storage(new pg.Pool({connectionString:process.env.DATABASE_URL,max:5}));await storage.init();return storage}