mcpscraper-memory-db 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/db/chat-link.js +3 -3
  2. package/dist/db/chat-link.js.map +1 -1
  3. package/dist/db/facts.js +1 -1
  4. package/dist/db/facts.js.map +1 -1
  5. package/dist/db/interlink-state.js +1 -1
  6. package/dist/db/interlink-state.js.map +1 -1
  7. package/dist/db/keys.js +2 -2
  8. package/dist/db/keys.js.map +1 -1
  9. package/dist/db/messages.js +2 -2
  10. package/dist/db/messages.js.map +1 -1
  11. package/dist/db/notes.js +4 -4
  12. package/dist/db/notes.js.map +1 -1
  13. package/dist/db/pool.js +1 -1
  14. package/dist/db/pool.js.map +1 -1
  15. package/dist/db/presence.js +1 -1
  16. package/dist/db/presence.js.map +1 -1
  17. package/dist/db/props-snapshots.js +1 -1
  18. package/dist/db/props-snapshots.js.map +1 -1
  19. package/dist/db/provenance.js +1 -1
  20. package/dist/db/provenance.js.map +1 -1
  21. package/dist/db/schedule-entitlements.js +2 -2
  22. package/dist/db/schedule-entitlements.js.map +1 -1
  23. package/dist/db/schedule-link.js +3 -3
  24. package/dist/db/schedule-link.js.map +1 -1
  25. package/dist/db/scheduled-actions.js +1 -1
  26. package/dist/db/scheduled-actions.js.map +1 -1
  27. package/dist/db/secure-notes.js +2 -2
  28. package/dist/db/secure-notes.js.map +1 -1
  29. package/dist/db/shares.js +1 -1
  30. package/dist/db/shares.js.map +1 -1
  31. package/dist/db/tables.js +1 -1
  32. package/dist/db/tables.js.map +1 -1
  33. package/dist/db/trust.js +2 -2
  34. package/dist/db/trust.js.map +1 -1
  35. package/dist/db/usage.js +3 -3
  36. package/dist/db/usage.js.map +1 -1
  37. package/dist/db/vaults.js +3 -3
  38. package/dist/db/vaults.js.map +1 -1
  39. package/dist/db/video-runs.js +2 -2
  40. package/dist/db/video-runs.js.map +1 -1
  41. package/dist/db/webhooks.js +2 -2
  42. package/dist/db/webhooks.js.map +1 -1
  43. package/dist/lib/auth.js +3 -3
  44. package/dist/lib/auth.js.map +1 -1
  45. package/dist/lib/video-reaper.js.map +1 -1
  46. package/dist/rag/index-pipeline.js +4 -4
  47. package/dist/rag/index-pipeline.js.map +1 -1
  48. package/dist/rag/query-tool.d.ts +5 -1
  49. package/dist/rag/query-tool.js +2 -2
  50. package/dist/rag/query-tool.js.map +1 -1
  51. package/dist/rag/vector-store.js +1 -1
  52. package/dist/rag/vector-store.js.map +1 -1
  53. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/secure-notes.ts"],"sourcesContent":["import { ensureSchema, query } from './pool'\nimport { encryptSecret, decryptSecret } from '../lib/secret-crypto'\n\nexport interface SecureNoteRecord {\n id: string\n vault: string\n path: string\n title: string\n content: string\n createdAt: string\n updatedAt: string\n updatedBy: string | null\n}\n\ninterface RawSecureNoteRow {\n id: string\n vault: string\n path: string\n title: string\n content_enc: string\n created_at: string\n updated_at: string\n updated_by: string | null\n}\n\nconst COLUMNS = 'id, vault, path, title, content_enc, created_at, updated_at, updated_by'\n\nfunction secureNoteId(vault: string, path: string): string {\n return `secure::${vault}::${path}`\n}\n\nfunction mapRow(r: RawSecureNoteRow): SecureNoteRecord {\n return {\n id: r.id,\n vault: r.vault,\n path: r.path,\n title: r.title,\n content: decryptSecret(r.content_enc),\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n updatedBy: r.updated_by,\n }\n}\n\nexport async function putSecureNote(args: { vault: string; path: string; title?: string; content: string; updatedBy?: string }): Promise<SecureNoteRecord> {\n await ensureSchema()\n const id = secureNoteId(args.vault, args.path)\n const res = await query<RawSecureNoteRow>(\n `INSERT INTO mem_secure_notes (id, vault, path, title, content_enc, updated_at, updated_by)\n VALUES ($1, $2, $3, $4, $5, now(), $6)\n ON CONFLICT (vault, path) DO UPDATE SET\n title = EXCLUDED.title, content_enc = EXCLUDED.content_enc, updated_at = now(), updated_by = EXCLUDED.updated_by\n RETURNING ${COLUMNS}`,\n [id, args.vault, args.path, args.title ?? args.path, encryptSecret(args.content), args.updatedBy ?? null],\n )\n return mapRow(res.rows[0])\n}\n\nexport async function getSecureNote(vault: string, path: string): Promise<SecureNoteRecord | null> {\n await ensureSchema()\n const res = await query<RawSecureNoteRow>(\n `SELECT ${COLUMNS} FROM mem_secure_notes WHERE vault = $1 AND path = $2 LIMIT 1`,\n [vault, path],\n )\n return res.rows[0] ? mapRow(res.rows[0]) : null\n}\n\nexport async function listSecureNotes(vault: string): Promise<Array<{ path: string; title: string; updatedAt: string }>> {\n await ensureSchema()\n const res = await query<{ path: string; title: string; updated_at: string }>(\n `SELECT path, title, updated_at FROM mem_secure_notes WHERE vault = $1 ORDER BY updated_at DESC`,\n [vault],\n )\n return res.rows.map(r => ({ path: r.path, title: r.title, updatedAt: r.updated_at }))\n}\n\nexport async function deleteSecureNote(vault: string, path: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_secure_notes WHERE vault = $1 AND path = $2`, [vault, path])\n return (res.rowCount ?? 0) > 0\n}\n"],"mappings":"AAAA,SAAS,cAAc,aAAa;AACpC,SAAS,eAAe,qBAAqB;AAwB7C,MAAM,UAAU;AAEhB,SAAS,aAAa,OAAe,MAAsB;AACzD,SAAO,WAAW,KAAK,KAAK,IAAI;AAClC;AAEA,SAAS,OAAO,GAAuC;AACrD,SAAO;AAAA,IACL,IAAI,EAAE;AAAA,IACN,OAAO,EAAE;AAAA,IACT,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,IACT,SAAS,cAAc,EAAE,WAAW;AAAA,IACpC,WAAW,EAAE;AAAA,IACb,WAAW,EAAE;AAAA,IACb,WAAW,EAAE;AAAA,EACf;AACF;AAEA,eAAsB,cAAc,MAAuH;AACzJ,QAAM,aAAa;AACnB,QAAM,KAAK,aAAa,KAAK,OAAO,KAAK,IAAI;AAC7C,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA,iBAIa,OAAO;AAAA,IACpB,CAAC,IAAI,KAAK,OAAO,KAAK,MAAM,KAAK,SAAS,KAAK,MAAM,cAAc,KAAK,OAAO,GAAG,KAAK,aAAa,IAAI;AAAA,EAC1G;AACA,SAAO,OAAO,IAAI,KAAK,CAAC,CAAC;AAC3B;AAEA,eAAsB,cAAc,OAAe,MAAgD;AACjG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB,UAAU,OAAO;AAAA,IACjB,CAAC,OAAO,IAAI;AAAA,EACd;AACA,SAAO,IAAI,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC,IAAI;AAC7C;AAEA,eAAsB,gBAAgB,OAAmF;AACvH,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AACA,SAAO,IAAI,KAAK,IAAI,QAAM,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,WAAW,EAAE,WAAW,EAAE;AACtF;AAEA,eAAsB,iBAAiB,OAAe,MAAgC;AACpF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM,MAAM,+DAA+D,CAAC,OAAO,IAAI,CAAC;AACpG,UAAQ,IAAI,YAAY,KAAK;AAC/B;","names":[]}
1
+ {"version":3,"sources":["../../src/db/secure-notes.ts"],"sourcesContent":["import { ensureSchema, query } from './pool.js'\nimport { encryptSecret, decryptSecret } from '../lib/secret-crypto.js'\n\nexport interface SecureNoteRecord {\n id: string\n vault: string\n path: string\n title: string\n content: string\n createdAt: string\n updatedAt: string\n updatedBy: string | null\n}\n\ninterface RawSecureNoteRow {\n id: string\n vault: string\n path: string\n title: string\n content_enc: string\n created_at: string\n updated_at: string\n updated_by: string | null\n}\n\nconst COLUMNS = 'id, vault, path, title, content_enc, created_at, updated_at, updated_by'\n\nfunction secureNoteId(vault: string, path: string): string {\n return `secure::${vault}::${path}`\n}\n\nfunction mapRow(r: RawSecureNoteRow): SecureNoteRecord {\n return {\n id: r.id,\n vault: r.vault,\n path: r.path,\n title: r.title,\n content: decryptSecret(r.content_enc),\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n updatedBy: r.updated_by,\n }\n}\n\nexport async function putSecureNote(args: { vault: string; path: string; title?: string; content: string; updatedBy?: string }): Promise<SecureNoteRecord> {\n await ensureSchema()\n const id = secureNoteId(args.vault, args.path)\n const res = await query<RawSecureNoteRow>(\n `INSERT INTO mem_secure_notes (id, vault, path, title, content_enc, updated_at, updated_by)\n VALUES ($1, $2, $3, $4, $5, now(), $6)\n ON CONFLICT (vault, path) DO UPDATE SET\n title = EXCLUDED.title, content_enc = EXCLUDED.content_enc, updated_at = now(), updated_by = EXCLUDED.updated_by\n RETURNING ${COLUMNS}`,\n [id, args.vault, args.path, args.title ?? args.path, encryptSecret(args.content), args.updatedBy ?? null],\n )\n return mapRow(res.rows[0])\n}\n\nexport async function getSecureNote(vault: string, path: string): Promise<SecureNoteRecord | null> {\n await ensureSchema()\n const res = await query<RawSecureNoteRow>(\n `SELECT ${COLUMNS} FROM mem_secure_notes WHERE vault = $1 AND path = $2 LIMIT 1`,\n [vault, path],\n )\n return res.rows[0] ? mapRow(res.rows[0]) : null\n}\n\nexport async function listSecureNotes(vault: string): Promise<Array<{ path: string; title: string; updatedAt: string }>> {\n await ensureSchema()\n const res = await query<{ path: string; title: string; updated_at: string }>(\n `SELECT path, title, updated_at FROM mem_secure_notes WHERE vault = $1 ORDER BY updated_at DESC`,\n [vault],\n )\n return res.rows.map(r => ({ path: r.path, title: r.title, updatedAt: r.updated_at }))\n}\n\nexport async function deleteSecureNote(vault: string, path: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_secure_notes WHERE vault = $1 AND path = $2`, [vault, path])\n return (res.rowCount ?? 0) > 0\n}\n"],"mappings":"AAAA,SAAS,cAAc,aAAa;AACpC,SAAS,eAAe,qBAAqB;AAwB7C,MAAM,UAAU;AAEhB,SAAS,aAAa,OAAe,MAAsB;AACzD,SAAO,WAAW,KAAK,KAAK,IAAI;AAClC;AAEA,SAAS,OAAO,GAAuC;AACrD,SAAO;AAAA,IACL,IAAI,EAAE;AAAA,IACN,OAAO,EAAE;AAAA,IACT,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,IACT,SAAS,cAAc,EAAE,WAAW;AAAA,IACpC,WAAW,EAAE;AAAA,IACb,WAAW,EAAE;AAAA,IACb,WAAW,EAAE;AAAA,EACf;AACF;AAEA,eAAsB,cAAc,MAAuH;AACzJ,QAAM,aAAa;AACnB,QAAM,KAAK,aAAa,KAAK,OAAO,KAAK,IAAI;AAC7C,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA,iBAIa,OAAO;AAAA,IACpB,CAAC,IAAI,KAAK,OAAO,KAAK,MAAM,KAAK,SAAS,KAAK,MAAM,cAAc,KAAK,OAAO,GAAG,KAAK,aAAa,IAAI;AAAA,EAC1G;AACA,SAAO,OAAO,IAAI,KAAK,CAAC,CAAC;AAC3B;AAEA,eAAsB,cAAc,OAAe,MAAgD;AACjG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB,UAAU,OAAO;AAAA,IACjB,CAAC,OAAO,IAAI;AAAA,EACd;AACA,SAAO,IAAI,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC,IAAI;AAC7C;AAEA,eAAsB,gBAAgB,OAAmF;AACvH,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AACA,SAAO,IAAI,KAAK,IAAI,QAAM,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,WAAW,EAAE,WAAW,EAAE;AACtF;AAEA,eAAsB,iBAAiB,OAAe,MAAgC;AACpF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM,MAAM,+DAA+D,CAAC,OAAO,IAAI,CAAC;AACpG,UAAQ,IAAI,YAAY,KAAK;AAC/B;","names":[]}
package/dist/db/shares.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { ensureSchema, query } from "./pool";
2
+ import { ensureSchema, query } from "./pool.js";
3
3
  function defaultNotePermissions() {
4
4
  return { read: true, edit: false, delete: false, reshare: false };
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/shares.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { ensureSchema, query } from './pool'\n\nexport interface NotePermissions {\n read: boolean\n edit: boolean\n delete: boolean\n reshare: boolean\n}\n\nexport function defaultNotePermissions(): NotePermissions {\n return { read: true, edit: false, delete: false, reshare: false }\n}\n\nexport function normalizeNotePermissions(input?: Partial<Record<keyof NotePermissions, boolean>>): NotePermissions {\n if (!input) return defaultNotePermissions()\n return { read: true, edit: input.edit === true, delete: input.delete === true, reshare: input.reshare === true }\n}\n\nexport type ShareStatus = 'pending' | 'accepted' | 'declined' | 'revoked'\n\nexport interface NoteShare {\n shareId: string\n noteId: string\n ownerIdentity: string\n srcVault: string\n srcPath: string\n grantee: string\n permissions: NotePermissions\n status: ShareStatus\n bundleRoot: string | null\n createdAt: string\n updatedAt: string\n}\n\ninterface RawShareRow {\n share_id: string\n note_id: string\n owner_identity: string\n src_vault: string\n src_path: string\n grantee: string\n permissions: unknown\n status: string\n bundle_root: string | null\n created_at: string\n updated_at: string\n}\n\nfunction mapShare(r: RawShareRow): NoteShare {\n return {\n shareId: r.share_id,\n noteId: r.note_id,\n ownerIdentity: r.owner_identity,\n srcVault: r.src_vault,\n srcPath: r.src_path,\n grantee: r.grantee,\n permissions: normalizeNotePermissions(r.permissions as Record<string, boolean>),\n status: r.status as ShareStatus,\n bundleRoot: r.bundle_root,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n }\n}\n\nconst SHARE_COLUMNS = 'share_id, note_id, owner_identity, src_vault, src_path, grantee, permissions, status, bundle_root, created_at, updated_at'\n\nexport async function createShare(args: {\n noteId: string\n ownerIdentity: string\n srcVault: string\n srcPath: string\n grantee: string\n permissions: NotePermissions\n bundleRoot?: string\n autoAccept?: boolean\n}): Promise<NoteShare> {\n await ensureSchema()\n const shareId = `share_${randomBytes(12).toString('base64url')}`\n const status = args.autoAccept ? 'accepted' : 'pending'\n const res = await query<RawShareRow>(\n `INSERT INTO mem_note_shares (share_id, note_id, owner_identity, src_vault, src_path, grantee, permissions, status, bundle_root, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, now())\n ON CONFLICT (note_id, grantee) DO UPDATE SET\n permissions = EXCLUDED.permissions, status = $8, bundle_root = EXCLUDED.bundle_root, updated_at = now()\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, args.noteId, args.ownerIdentity, args.srcVault, args.srcPath, args.grantee, JSON.stringify(args.permissions), status, args.bundleRoot ?? null],\n )\n return mapShare(res.rows[0])\n}\n\nexport async function getShare(shareId: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(`SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE share_id = $1 LIMIT 1`, [shareId])\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function getShareByNoteAndGrantee(noteId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(`SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE note_id = $1 AND grantee = $2 LIMIT 1`, [noteId, grantee])\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function getAcceptedShareByNoteAndGrantee(noteId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE note_id = $1 AND grantee = $2 AND status = 'accepted' LIMIT 1`,\n [noteId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function listPendingForGrantee(grantee: string): Promise<NoteShare[]> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE grantee = $1 AND status = 'pending' ORDER BY created_at ASC`,\n [grantee],\n )\n return res.rows.map(mapShare)\n}\n\nexport async function listAcceptedForGrantee(grantee: string): Promise<NoteShare[]> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE grantee = $1 AND status = 'accepted' ORDER BY created_at ASC`,\n [grantee],\n )\n return res.rows.map(mapShare)\n}\n\nexport async function acceptShareRow(shareId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `UPDATE mem_note_shares SET status = 'accepted', updated_at = now()\n WHERE share_id = $1 AND grantee = $2 AND status = 'pending'\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function declineShareRow(shareId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `UPDATE mem_note_shares SET status = 'declined', updated_at = now()\n WHERE share_id = $1 AND grantee = $2 AND status = 'pending'\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function unlinkShareRow(shareId: string, grantee: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `DELETE FROM mem_note_shares WHERE share_id = $1 AND grantee = $2 AND status = 'accepted'`,\n [shareId, grantee],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function revokeShareRow(shareId: string, ownerIdentity: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `UPDATE mem_note_shares SET status = 'revoked', updated_at = now()\n WHERE share_id = $1 AND owner_identity = $2 AND status IN ('pending', 'accepted')\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, ownerIdentity],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function deleteSharesForNote(noteId: string): Promise<number> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_note_shares WHERE note_id = $1`, [noteId])\n return res.rowCount ?? 0\n}\n\nexport async function resolveAcceptedShare(shareId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE share_id = $1 AND grantee = $2 AND status = 'accepted' LIMIT 1`,\n [shareId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/db/shares.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { ensureSchema, query } from './pool.js'\n\nexport interface NotePermissions {\n read: boolean\n edit: boolean\n delete: boolean\n reshare: boolean\n}\n\nexport function defaultNotePermissions(): NotePermissions {\n return { read: true, edit: false, delete: false, reshare: false }\n}\n\nexport function normalizeNotePermissions(input?: Partial<Record<keyof NotePermissions, boolean>>): NotePermissions {\n if (!input) return defaultNotePermissions()\n return { read: true, edit: input.edit === true, delete: input.delete === true, reshare: input.reshare === true }\n}\n\nexport type ShareStatus = 'pending' | 'accepted' | 'declined' | 'revoked'\n\nexport interface NoteShare {\n shareId: string\n noteId: string\n ownerIdentity: string\n srcVault: string\n srcPath: string\n grantee: string\n permissions: NotePermissions\n status: ShareStatus\n bundleRoot: string | null\n createdAt: string\n updatedAt: string\n}\n\ninterface RawShareRow {\n share_id: string\n note_id: string\n owner_identity: string\n src_vault: string\n src_path: string\n grantee: string\n permissions: unknown\n status: string\n bundle_root: string | null\n created_at: string\n updated_at: string\n}\n\nfunction mapShare(r: RawShareRow): NoteShare {\n return {\n shareId: r.share_id,\n noteId: r.note_id,\n ownerIdentity: r.owner_identity,\n srcVault: r.src_vault,\n srcPath: r.src_path,\n grantee: r.grantee,\n permissions: normalizeNotePermissions(r.permissions as Record<string, boolean>),\n status: r.status as ShareStatus,\n bundleRoot: r.bundle_root,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n }\n}\n\nconst SHARE_COLUMNS = 'share_id, note_id, owner_identity, src_vault, src_path, grantee, permissions, status, bundle_root, created_at, updated_at'\n\nexport async function createShare(args: {\n noteId: string\n ownerIdentity: string\n srcVault: string\n srcPath: string\n grantee: string\n permissions: NotePermissions\n bundleRoot?: string\n autoAccept?: boolean\n}): Promise<NoteShare> {\n await ensureSchema()\n const shareId = `share_${randomBytes(12).toString('base64url')}`\n const status = args.autoAccept ? 'accepted' : 'pending'\n const res = await query<RawShareRow>(\n `INSERT INTO mem_note_shares (share_id, note_id, owner_identity, src_vault, src_path, grantee, permissions, status, bundle_root, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, now())\n ON CONFLICT (note_id, grantee) DO UPDATE SET\n permissions = EXCLUDED.permissions, status = $8, bundle_root = EXCLUDED.bundle_root, updated_at = now()\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, args.noteId, args.ownerIdentity, args.srcVault, args.srcPath, args.grantee, JSON.stringify(args.permissions), status, args.bundleRoot ?? null],\n )\n return mapShare(res.rows[0])\n}\n\nexport async function getShare(shareId: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(`SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE share_id = $1 LIMIT 1`, [shareId])\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function getShareByNoteAndGrantee(noteId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(`SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE note_id = $1 AND grantee = $2 LIMIT 1`, [noteId, grantee])\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function getAcceptedShareByNoteAndGrantee(noteId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE note_id = $1 AND grantee = $2 AND status = 'accepted' LIMIT 1`,\n [noteId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function listPendingForGrantee(grantee: string): Promise<NoteShare[]> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE grantee = $1 AND status = 'pending' ORDER BY created_at ASC`,\n [grantee],\n )\n return res.rows.map(mapShare)\n}\n\nexport async function listAcceptedForGrantee(grantee: string): Promise<NoteShare[]> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE grantee = $1 AND status = 'accepted' ORDER BY created_at ASC`,\n [grantee],\n )\n return res.rows.map(mapShare)\n}\n\nexport async function acceptShareRow(shareId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `UPDATE mem_note_shares SET status = 'accepted', updated_at = now()\n WHERE share_id = $1 AND grantee = $2 AND status = 'pending'\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function declineShareRow(shareId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `UPDATE mem_note_shares SET status = 'declined', updated_at = now()\n WHERE share_id = $1 AND grantee = $2 AND status = 'pending'\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function unlinkShareRow(shareId: string, grantee: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `DELETE FROM mem_note_shares WHERE share_id = $1 AND grantee = $2 AND status = 'accepted'`,\n [shareId, grantee],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function revokeShareRow(shareId: string, ownerIdentity: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `UPDATE mem_note_shares SET status = 'revoked', updated_at = now()\n WHERE share_id = $1 AND owner_identity = $2 AND status IN ('pending', 'accepted')\n RETURNING ${SHARE_COLUMNS}`,\n [shareId, ownerIdentity],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n\nexport async function deleteSharesForNote(noteId: string): Promise<number> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_note_shares WHERE note_id = $1`, [noteId])\n return res.rowCount ?? 0\n}\n\nexport async function resolveAcceptedShare(shareId: string, grantee: string): Promise<NoteShare | null> {\n await ensureSchema()\n const res = await query<RawShareRow>(\n `SELECT ${SHARE_COLUMNS} FROM mem_note_shares WHERE share_id = $1 AND grantee = $2 AND status = 'accepted' LIMIT 1`,\n [shareId, grantee],\n )\n return res.rows[0] ? mapShare(res.rows[0]) : null\n}\n"],"mappings":"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","names":[]}
package/dist/db/tables.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { ensureSchema as ensureNotesSchema, query } from "./pool";
2
+ import { ensureSchema as ensureNotesSchema, query } from "./pool.js";
3
3
  const TABLE_NAME_RE = /^[a-z][a-z0-9_]{0,62}$/;
4
4
  const COLUMN_NAME_RE = /^[a-z][a-z0-9_]{0,62}$/;
5
5
  const COLUMN_TYPE_SQL = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/tables.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { ensureSchema as ensureNotesSchema, query } from './pool'\n\nexport const TABLE_NAME_RE = /^[a-z][a-z0-9_]{0,62}$/\nexport const COLUMN_NAME_RE = /^[a-z][a-z0-9_]{0,62}$/\n\nexport type ColumnType = 'text' | 'number' | 'integer' | 'boolean' | 'date' | 'timestamp' | 'json'\n\nconst COLUMN_TYPE_SQL: Record<ColumnType, string> = {\n text: 'TEXT',\n number: 'DOUBLE PRECISION',\n integer: 'BIGINT',\n boolean: 'BOOLEAN',\n date: 'DATE',\n timestamp: 'TIMESTAMPTZ',\n json: 'JSONB',\n}\n\nconst RESERVED_COLUMN_NAMES = new Set(['id', 'created_at', 'updated_at'])\nconst MAX_COLUMNS_PER_TABLE = 40\nconst MAX_TABLES_PER_USER = 50\nconst MAX_ROWS_PER_INSERT = 500\nconst MAX_ROWS_PER_QUERY = 2000\nconst DEFAULT_ROWS_PER_QUERY = 100\n\nexport class TableValidationError extends Error {}\n\nexport interface ColumnDef {\n name: string\n type: ColumnType\n}\n\nexport type FilterOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'in'\n\nconst FILTER_OP_SQL: Record<FilterOp, string> = {\n eq: '=',\n neq: '!=',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n like: 'ILIKE',\n in: '= ANY',\n}\n\nexport interface TableFilter {\n column: string\n op: FilterOp\n value?: unknown\n}\n\nexport interface TableSort {\n column: string\n direction?: 'asc' | 'desc'\n}\n\nfunction assertSafeIdentifier(name: string, re: RegExp, kind: string): void {\n if (!re.test(name)) {\n throw new TableValidationError(`invalid ${kind} name \"${name}\" — use lowercase letters, numbers, underscores, starting with a letter`)\n }\n}\n\nexport function schemaNameFor(identity: string): string {\n const hash = createHash('sha256').update(identity).digest('hex').slice(0, 32)\n return `tbl_${hash}`\n}\n\nasync function ensureUserSchema(identity: string): Promise<string> {\n await ensureNotesSchema()\n await query(\n `CREATE TABLE IF NOT EXISTS mem_table_schemas (\n identity TEXT PRIMARY KEY,\n schema_name TEXT UNIQUE NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n )`,\n )\n const schemaName = schemaNameFor(identity)\n await query(`CREATE SCHEMA IF NOT EXISTS \"${schemaName}\"`)\n await query(\n `INSERT INTO mem_table_schemas (identity, schema_name) VALUES ($1, $2)\n ON CONFLICT (identity) DO NOTHING`,\n [identity, schemaName],\n )\n return schemaName\n}\n\nexport async function listUserTables(identity: string): Promise<string[]> {\n const schemaName = await ensureUserSchema(identity)\n const res = await query<{ table_name: string }>(\n `SELECT table_name FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name`,\n [schemaName],\n )\n return res.rows.map(r => r.table_name)\n}\n\nexport async function tableExists(identity: string, tableName: string): Promise<boolean> {\n const schemaName = await ensureUserSchema(identity)\n const res = await query(\n `SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 LIMIT 1`,\n [schemaName, tableName],\n )\n return res.rows.length > 0\n}\n\nexport interface TableColumnInfo {\n name: string\n type: string\n}\n\nexport async function describeUserTable(identity: string, tableName: string): Promise<TableColumnInfo[]> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n const schemaName = await ensureUserSchema(identity)\n const res = await query<{ column_name: string; data_type: string }>(\n `SELECT column_name, data_type FROM information_schema.columns\n WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position`,\n [schemaName, tableName],\n )\n return res.rows.map(r => ({ name: r.column_name, type: r.data_type }))\n}\n\nexport async function createUserTable(identity: string, tableName: string, columns: ColumnDef[]): Promise<{ created: boolean; columns: ColumnDef[] }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n if (columns.length === 0) throw new TableValidationError('at least one column is required')\n if (columns.length > MAX_COLUMNS_PER_TABLE) throw new TableValidationError(`at most ${MAX_COLUMNS_PER_TABLE} columns per table`)\n const seen = new Set<string>()\n for (const col of columns) {\n assertSafeIdentifier(col.name, COLUMN_NAME_RE, 'column')\n if (RESERVED_COLUMN_NAMES.has(col.name)) throw new TableValidationError(`column name \"${col.name}\" is reserved`)\n if (seen.has(col.name)) throw new TableValidationError(`duplicate column name \"${col.name}\"`)\n seen.add(col.name)\n if (!(col.type in COLUMN_TYPE_SQL)) throw new TableValidationError(`unsupported column type \"${col.type}\"`)\n }\n\n const schemaName = await ensureUserSchema(identity)\n const existing = await listUserTables(identity)\n if (!existing.includes(tableName) && existing.length >= MAX_TABLES_PER_USER) {\n throw new TableValidationError(`at most ${MAX_TABLES_PER_USER} tables per account`)\n }\n\n const columnSql = columns.map(c => `\"${c.name}\" ${COLUMN_TYPE_SQL[c.type]}`).join(', ')\n await query(\n `CREATE TABLE IF NOT EXISTS \"${schemaName}\".\"${tableName}\" (\n id BIGSERIAL PRIMARY KEY,\n ${columnSql},\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\n )`,\n )\n return { created: !existing.includes(tableName), columns }\n}\n\nexport async function dropUserTable(identity: string, tableName: string): Promise<boolean> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n const schemaName = await ensureUserSchema(identity)\n if (!(await tableExists(identity, tableName))) return false\n await query(`DROP TABLE IF EXISTS \"${schemaName}\".\"${tableName}\"`)\n return true\n}\n\nasync function knownColumns(identity: string, tableName: string): Promise<Set<string>> {\n const cols = await describeUserTable(identity, tableName)\n if (cols.length === 0) throw new TableValidationError(`table \"${tableName}\" does not exist`)\n return new Set(cols.map(c => c.name))\n}\n\nfunction assertKnownColumns(names: string[], known: Set<string>): void {\n for (const name of names) {\n if (name === 'id' || name === 'created_at' || name === 'updated_at') continue\n if (!known.has(name)) throw new TableValidationError(`unknown column \"${name}\"`)\n }\n}\n\nexport async function insertUserTableRows(identity: string, tableName: string, rows: Array<Record<string, unknown>>): Promise<{ inserted: number }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n if (rows.length === 0) throw new TableValidationError('at least one row is required')\n if (rows.length > MAX_ROWS_PER_INSERT) throw new TableValidationError(`at most ${MAX_ROWS_PER_INSERT} rows per insert`)\n const schemaName = await ensureUserSchema(identity)\n const known = await knownColumns(identity, tableName)\n\n const columnNames = [...new Set(rows.flatMap(r => Object.keys(r)))]\n assertKnownColumns(columnNames, known)\n if (columnNames.length === 0) throw new TableValidationError('rows must have at least one column value')\n\n const valueRows: string[] = []\n const params: unknown[] = []\n let i = 1\n for (const row of rows) {\n const placeholders = columnNames.map(col => {\n params.push(Object.prototype.hasOwnProperty.call(row, col) ? row[col] : null)\n return `$${i++}`\n })\n valueRows.push(`(${placeholders.join(', ')})`)\n }\n\n const columnSql = columnNames.map(c => `\"${c}\"`).join(', ')\n const res = await query(\n `INSERT INTO \"${schemaName}\".\"${tableName}\" (${columnSql}) VALUES ${valueRows.join(', ')}`,\n params,\n )\n return { inserted: res.rowCount ?? 0 }\n}\n\nfunction buildWhereClause(filters: TableFilter[], known: Set<string>, startParamIndex: number): { sql: string; params: unknown[] } {\n if (filters.length === 0) return { sql: '', params: [] }\n const parts: string[] = []\n const params: unknown[] = []\n let i = startParamIndex\n for (const f of filters) {\n assertKnownColumns([f.column], known)\n const opSql = FILTER_OP_SQL[f.op]\n if (!opSql) throw new TableValidationError(`unsupported filter op \"${f.op}\"`)\n if (f.op === 'in') {\n if (!Array.isArray(f.value)) throw new TableValidationError(`\"in\" filter requires an array value`)\n parts.push(`\"${f.column}\" = ANY($${i++})`)\n params.push(f.value)\n } else {\n parts.push(`\"${f.column}\" ${opSql} $${i++}`)\n params.push(f.op === 'like' ? `%${f.value}%` : f.value)\n }\n }\n return { sql: `WHERE ${parts.join(' AND ')}`, params }\n}\n\nexport interface TableQueryOptions {\n filters?: TableFilter[]\n sort?: TableSort\n limit?: number\n offset?: number\n}\n\nexport async function queryUserTable(identity: string, tableName: string, opts: TableQueryOptions = {}): Promise<{ rows: Record<string, unknown>[]; count: number }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n const schemaName = await ensureUserSchema(identity)\n const known = await knownColumns(identity, tableName)\n\n const filters = opts.filters ?? []\n const { sql: whereSql, params: whereParams } = buildWhereClause(filters, known, 1)\n\n let orderSql = 'ORDER BY id ASC'\n if (opts.sort) {\n assertKnownColumns([opts.sort.column], known)\n const dir = opts.sort.direction === 'desc' ? 'DESC' : 'ASC'\n orderSql = `ORDER BY \"${opts.sort.column}\" ${dir}`\n }\n\n const limit = Math.min(Math.max(1, opts.limit ?? DEFAULT_ROWS_PER_QUERY), MAX_ROWS_PER_QUERY)\n const offset = Math.max(0, opts.offset ?? 0)\n const limitParamIndex = whereParams.length + 1\n const offsetParamIndex = whereParams.length + 2\n\n const res = await query(\n `SELECT * FROM \"${schemaName}\".\"${tableName}\" ${whereSql} ${orderSql} LIMIT $${limitParamIndex} OFFSET $${offsetParamIndex}`,\n [...whereParams, limit, offset],\n )\n\n const countRes = await query<{ count: string }>(\n `SELECT COUNT(*) AS count FROM \"${schemaName}\".\"${tableName}\" ${whereSql}`,\n whereParams,\n )\n\n return { rows: res.rows as Record<string, unknown>[], count: Number(countRes.rows[0]?.count ?? 0) }\n}\n\nexport async function deleteUserTableRows(identity: string, tableName: string, filters: TableFilter[]): Promise<{ deleted: number }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n if (filters.length === 0) throw new TableValidationError('at least one filter is required — refusing an unfiltered delete')\n const schemaName = await ensureUserSchema(identity)\n const known = await knownColumns(identity, tableName)\n const { sql: whereSql, params } = buildWhereClause(filters, known, 1)\n const res = await query(`DELETE FROM \"${schemaName}\".\"${tableName}\" ${whereSql}`, params)\n return { deleted: res.rowCount ?? 0 }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/db/tables.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { ensureSchema as ensureNotesSchema, query } from './pool.js'\n\nexport const TABLE_NAME_RE = /^[a-z][a-z0-9_]{0,62}$/\nexport const COLUMN_NAME_RE = /^[a-z][a-z0-9_]{0,62}$/\n\nexport type ColumnType = 'text' | 'number' | 'integer' | 'boolean' | 'date' | 'timestamp' | 'json'\n\nconst COLUMN_TYPE_SQL: Record<ColumnType, string> = {\n text: 'TEXT',\n number: 'DOUBLE PRECISION',\n integer: 'BIGINT',\n boolean: 'BOOLEAN',\n date: 'DATE',\n timestamp: 'TIMESTAMPTZ',\n json: 'JSONB',\n}\n\nconst RESERVED_COLUMN_NAMES = new Set(['id', 'created_at', 'updated_at'])\nconst MAX_COLUMNS_PER_TABLE = 40\nconst MAX_TABLES_PER_USER = 50\nconst MAX_ROWS_PER_INSERT = 500\nconst MAX_ROWS_PER_QUERY = 2000\nconst DEFAULT_ROWS_PER_QUERY = 100\n\nexport class TableValidationError extends Error {}\n\nexport interface ColumnDef {\n name: string\n type: ColumnType\n}\n\nexport type FilterOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'in'\n\nconst FILTER_OP_SQL: Record<FilterOp, string> = {\n eq: '=',\n neq: '!=',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n like: 'ILIKE',\n in: '= ANY',\n}\n\nexport interface TableFilter {\n column: string\n op: FilterOp\n value?: unknown\n}\n\nexport interface TableSort {\n column: string\n direction?: 'asc' | 'desc'\n}\n\nfunction assertSafeIdentifier(name: string, re: RegExp, kind: string): void {\n if (!re.test(name)) {\n throw new TableValidationError(`invalid ${kind} name \"${name}\" — use lowercase letters, numbers, underscores, starting with a letter`)\n }\n}\n\nexport function schemaNameFor(identity: string): string {\n const hash = createHash('sha256').update(identity).digest('hex').slice(0, 32)\n return `tbl_${hash}`\n}\n\nasync function ensureUserSchema(identity: string): Promise<string> {\n await ensureNotesSchema()\n await query(\n `CREATE TABLE IF NOT EXISTS mem_table_schemas (\n identity TEXT PRIMARY KEY,\n schema_name TEXT UNIQUE NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n )`,\n )\n const schemaName = schemaNameFor(identity)\n await query(`CREATE SCHEMA IF NOT EXISTS \"${schemaName}\"`)\n await query(\n `INSERT INTO mem_table_schemas (identity, schema_name) VALUES ($1, $2)\n ON CONFLICT (identity) DO NOTHING`,\n [identity, schemaName],\n )\n return schemaName\n}\n\nexport async function listUserTables(identity: string): Promise<string[]> {\n const schemaName = await ensureUserSchema(identity)\n const res = await query<{ table_name: string }>(\n `SELECT table_name FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name`,\n [schemaName],\n )\n return res.rows.map(r => r.table_name)\n}\n\nexport async function tableExists(identity: string, tableName: string): Promise<boolean> {\n const schemaName = await ensureUserSchema(identity)\n const res = await query(\n `SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 LIMIT 1`,\n [schemaName, tableName],\n )\n return res.rows.length > 0\n}\n\nexport interface TableColumnInfo {\n name: string\n type: string\n}\n\nexport async function describeUserTable(identity: string, tableName: string): Promise<TableColumnInfo[]> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n const schemaName = await ensureUserSchema(identity)\n const res = await query<{ column_name: string; data_type: string }>(\n `SELECT column_name, data_type FROM information_schema.columns\n WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position`,\n [schemaName, tableName],\n )\n return res.rows.map(r => ({ name: r.column_name, type: r.data_type }))\n}\n\nexport async function createUserTable(identity: string, tableName: string, columns: ColumnDef[]): Promise<{ created: boolean; columns: ColumnDef[] }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n if (columns.length === 0) throw new TableValidationError('at least one column is required')\n if (columns.length > MAX_COLUMNS_PER_TABLE) throw new TableValidationError(`at most ${MAX_COLUMNS_PER_TABLE} columns per table`)\n const seen = new Set<string>()\n for (const col of columns) {\n assertSafeIdentifier(col.name, COLUMN_NAME_RE, 'column')\n if (RESERVED_COLUMN_NAMES.has(col.name)) throw new TableValidationError(`column name \"${col.name}\" is reserved`)\n if (seen.has(col.name)) throw new TableValidationError(`duplicate column name \"${col.name}\"`)\n seen.add(col.name)\n if (!(col.type in COLUMN_TYPE_SQL)) throw new TableValidationError(`unsupported column type \"${col.type}\"`)\n }\n\n const schemaName = await ensureUserSchema(identity)\n const existing = await listUserTables(identity)\n if (!existing.includes(tableName) && existing.length >= MAX_TABLES_PER_USER) {\n throw new TableValidationError(`at most ${MAX_TABLES_PER_USER} tables per account`)\n }\n\n const columnSql = columns.map(c => `\"${c.name}\" ${COLUMN_TYPE_SQL[c.type]}`).join(', ')\n await query(\n `CREATE TABLE IF NOT EXISTS \"${schemaName}\".\"${tableName}\" (\n id BIGSERIAL PRIMARY KEY,\n ${columnSql},\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\n )`,\n )\n return { created: !existing.includes(tableName), columns }\n}\n\nexport async function dropUserTable(identity: string, tableName: string): Promise<boolean> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n const schemaName = await ensureUserSchema(identity)\n if (!(await tableExists(identity, tableName))) return false\n await query(`DROP TABLE IF EXISTS \"${schemaName}\".\"${tableName}\"`)\n return true\n}\n\nasync function knownColumns(identity: string, tableName: string): Promise<Set<string>> {\n const cols = await describeUserTable(identity, tableName)\n if (cols.length === 0) throw new TableValidationError(`table \"${tableName}\" does not exist`)\n return new Set(cols.map(c => c.name))\n}\n\nfunction assertKnownColumns(names: string[], known: Set<string>): void {\n for (const name of names) {\n if (name === 'id' || name === 'created_at' || name === 'updated_at') continue\n if (!known.has(name)) throw new TableValidationError(`unknown column \"${name}\"`)\n }\n}\n\nexport async function insertUserTableRows(identity: string, tableName: string, rows: Array<Record<string, unknown>>): Promise<{ inserted: number }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n if (rows.length === 0) throw new TableValidationError('at least one row is required')\n if (rows.length > MAX_ROWS_PER_INSERT) throw new TableValidationError(`at most ${MAX_ROWS_PER_INSERT} rows per insert`)\n const schemaName = await ensureUserSchema(identity)\n const known = await knownColumns(identity, tableName)\n\n const columnNames = [...new Set(rows.flatMap(r => Object.keys(r)))]\n assertKnownColumns(columnNames, known)\n if (columnNames.length === 0) throw new TableValidationError('rows must have at least one column value')\n\n const valueRows: string[] = []\n const params: unknown[] = []\n let i = 1\n for (const row of rows) {\n const placeholders = columnNames.map(col => {\n params.push(Object.prototype.hasOwnProperty.call(row, col) ? row[col] : null)\n return `$${i++}`\n })\n valueRows.push(`(${placeholders.join(', ')})`)\n }\n\n const columnSql = columnNames.map(c => `\"${c}\"`).join(', ')\n const res = await query(\n `INSERT INTO \"${schemaName}\".\"${tableName}\" (${columnSql}) VALUES ${valueRows.join(', ')}`,\n params,\n )\n return { inserted: res.rowCount ?? 0 }\n}\n\nfunction buildWhereClause(filters: TableFilter[], known: Set<string>, startParamIndex: number): { sql: string; params: unknown[] } {\n if (filters.length === 0) return { sql: '', params: [] }\n const parts: string[] = []\n const params: unknown[] = []\n let i = startParamIndex\n for (const f of filters) {\n assertKnownColumns([f.column], known)\n const opSql = FILTER_OP_SQL[f.op]\n if (!opSql) throw new TableValidationError(`unsupported filter op \"${f.op}\"`)\n if (f.op === 'in') {\n if (!Array.isArray(f.value)) throw new TableValidationError(`\"in\" filter requires an array value`)\n parts.push(`\"${f.column}\" = ANY($${i++})`)\n params.push(f.value)\n } else {\n parts.push(`\"${f.column}\" ${opSql} $${i++}`)\n params.push(f.op === 'like' ? `%${f.value}%` : f.value)\n }\n }\n return { sql: `WHERE ${parts.join(' AND ')}`, params }\n}\n\nexport interface TableQueryOptions {\n filters?: TableFilter[]\n sort?: TableSort\n limit?: number\n offset?: number\n}\n\nexport async function queryUserTable(identity: string, tableName: string, opts: TableQueryOptions = {}): Promise<{ rows: Record<string, unknown>[]; count: number }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n const schemaName = await ensureUserSchema(identity)\n const known = await knownColumns(identity, tableName)\n\n const filters = opts.filters ?? []\n const { sql: whereSql, params: whereParams } = buildWhereClause(filters, known, 1)\n\n let orderSql = 'ORDER BY id ASC'\n if (opts.sort) {\n assertKnownColumns([opts.sort.column], known)\n const dir = opts.sort.direction === 'desc' ? 'DESC' : 'ASC'\n orderSql = `ORDER BY \"${opts.sort.column}\" ${dir}`\n }\n\n const limit = Math.min(Math.max(1, opts.limit ?? DEFAULT_ROWS_PER_QUERY), MAX_ROWS_PER_QUERY)\n const offset = Math.max(0, opts.offset ?? 0)\n const limitParamIndex = whereParams.length + 1\n const offsetParamIndex = whereParams.length + 2\n\n const res = await query(\n `SELECT * FROM \"${schemaName}\".\"${tableName}\" ${whereSql} ${orderSql} LIMIT $${limitParamIndex} OFFSET $${offsetParamIndex}`,\n [...whereParams, limit, offset],\n )\n\n const countRes = await query<{ count: string }>(\n `SELECT COUNT(*) AS count FROM \"${schemaName}\".\"${tableName}\" ${whereSql}`,\n whereParams,\n )\n\n return { rows: res.rows as Record<string, unknown>[], count: Number(countRes.rows[0]?.count ?? 0) }\n}\n\nexport async function deleteUserTableRows(identity: string, tableName: string, filters: TableFilter[]): Promise<{ deleted: number }> {\n assertSafeIdentifier(tableName, TABLE_NAME_RE, 'table')\n if (filters.length === 0) throw new TableValidationError('at least one filter is required — refusing an unfiltered delete')\n const schemaName = await ensureUserSchema(identity)\n const known = await knownColumns(identity, tableName)\n const { sql: whereSql, params } = buildWhereClause(filters, known, 1)\n const res = await query(`DELETE FROM \"${schemaName}\".\"${tableName}\" ${whereSql}`, params)\n return { deleted: res.rowCount ?? 0 }\n}\n"],"mappings":"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","names":[]}
package/dist/db/trust.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ensureSchema, query } from "./pool";
2
- import { getAccountGrant } from "./vaults";
1
+ import { ensureSchema, query } from "./pool.js";
2
+ import { getAccountGrant } from "./vaults.js";
3
3
  async function addApprovedSender(ownerIdentity, senderIdentity) {
4
4
  await ensureSchema();
5
5
  await query(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/trust.ts"],"sourcesContent":["import { ensureSchema, query } from './pool'\nimport { getAccountGrant } from './vaults'\n\nexport async function addApprovedSender(ownerIdentity: string, senderIdentity: string): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_approved_senders (owner_identity, sender_identity) VALUES ($1, $2)\n ON CONFLICT (owner_identity, sender_identity) DO NOTHING`,\n [ownerIdentity, senderIdentity],\n )\n}\n\nexport async function removeApprovedSender(ownerIdentity: string, senderIdentity: string): Promise<number> {\n await ensureSchema()\n const res = await query(\n `DELETE FROM mem_approved_senders WHERE owner_identity = $1 AND sender_identity = $2`,\n [ownerIdentity, senderIdentity],\n )\n return res.rowCount ?? 0\n}\n\nexport async function listApprovedSenders(ownerIdentity: string): Promise<string[]> {\n await ensureSchema()\n const res = await query<{ sender_identity: string }>(\n `SELECT sender_identity FROM mem_approved_senders WHERE owner_identity = $1 ORDER BY created_at ASC`,\n [ownerIdentity],\n )\n return res.rows.map(r => r.sender_identity)\n}\n\nasync function isApprovedSender(ownerIdentity: string, senderIdentity: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `SELECT 1 FROM mem_approved_senders WHERE owner_identity = $1 AND sender_identity = $2 LIMIT 1`,\n [ownerIdentity, senderIdentity],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function allowsUnapprovedSenders(identity: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ allow_unapproved_senders: boolean }>(\n `SELECT allow_unapproved_senders FROM mem_settings WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n return res.rows[0]?.allow_unapproved_senders ?? false\n}\n\nexport async function setAllowUnapprovedSenders(identity: string, allow: boolean): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_settings (identity, allow_unapproved_senders, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (identity) DO UPDATE SET allow_unapproved_senders = EXCLUDED.allow_unapproved_senders, updated_at = now()`,\n [identity, allow],\n )\n}\n\nexport async function setIsAgent(identity: string, isAgent: boolean): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_settings (identity, is_agent, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (identity) DO UPDATE SET is_agent = EXCLUDED.is_agent, updated_at = now()`,\n [identity, isAgent],\n )\n}\n\nexport async function isAgentIdentities(identities: string[]): Promise<Set<string>> {\n await ensureSchema()\n if (identities.length === 0) return new Set()\n const res = await query<{ identity: string }>(\n `SELECT identity FROM mem_settings WHERE identity = ANY($1) AND is_agent = TRUE`,\n [identities],\n )\n return new Set(res.rows.map(r => r.identity))\n}\n\nexport async function maySend(sender: string, recipient: string): Promise<boolean> {\n if (sender === recipient) return true\n if (await isApprovedSender(recipient, sender)) return true\n if (await allowsUnapprovedSenders(recipient)) return true\n if ((await getAccountGrant(recipient, sender)) !== null) return true\n if ((await getAccountGrant(sender, recipient)) !== null) return true\n return false\n}\n"],"mappings":"AAAA,SAAS,cAAc,aAAa;AACpC,SAAS,uBAAuB;AAEhC,eAAsB,kBAAkB,eAAuB,gBAAuC;AACpG,QAAM,aAAa;AACnB,QAAM;AAAA,IACJ;AAAA;AAAA,IAEA,CAAC,eAAe,cAAc;AAAA,EAChC;AACF;AAEA,eAAsB,qBAAqB,eAAuB,gBAAyC;AACzG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,eAAe,cAAc;AAAA,EAChC;AACA,SAAO,IAAI,YAAY;AACzB;AAEA,eAAsB,oBAAoB,eAA0C;AAClF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AACA,SAAO,IAAI,KAAK,IAAI,OAAK,EAAE,eAAe;AAC5C;AAEA,eAAe,iBAAiB,eAAuB,gBAA0C;AAC/F,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,eAAe,cAAc;AAAA,EAChC;AACA,UAAQ,IAAI,YAAY,KAAK;AAC/B;AAEA,eAAsB,wBAAwB,UAAoC;AAChF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AACA,SAAO,IAAI,KAAK,CAAC,GAAG,4BAA4B;AAClD;AAEA,eAAsB,0BAA0B,UAAkB,OAA+B;AAC/F,QAAM,aAAa;AACnB,QAAM;AAAA,IACJ;AAAA;AAAA;AAAA,IAGA,CAAC,UAAU,KAAK;AAAA,EAClB;AACF;AAEA,eAAsB,WAAW,UAAkB,SAAiC;AAClF,QAAM,aAAa;AACnB,QAAM;AAAA,IACJ;AAAA;AAAA;AAAA,IAGA,CAAC,UAAU,OAAO;AAAA,EACpB;AACF;AAEA,eAAsB,kBAAkB,YAA4C;AAClF,QAAM,aAAa;AACnB,MAAI,WAAW,WAAW,EAAG,QAAO,oBAAI,IAAI;AAC5C,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AACA,SAAO,IAAI,IAAI,IAAI,KAAK,IAAI,OAAK,EAAE,QAAQ,CAAC;AAC9C;AAEA,eAAsB,QAAQ,QAAgB,WAAqC;AACjF,MAAI,WAAW,UAAW,QAAO;AACjC,MAAI,MAAM,iBAAiB,WAAW,MAAM,EAAG,QAAO;AACtD,MAAI,MAAM,wBAAwB,SAAS,EAAG,QAAO;AACrD,MAAK,MAAM,gBAAgB,WAAW,MAAM,MAAO,KAAM,QAAO;AAChE,MAAK,MAAM,gBAAgB,QAAQ,SAAS,MAAO,KAAM,QAAO;AAChE,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/db/trust.ts"],"sourcesContent":["import { ensureSchema, query } from './pool.js'\nimport { getAccountGrant } from './vaults.js'\n\nexport async function addApprovedSender(ownerIdentity: string, senderIdentity: string): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_approved_senders (owner_identity, sender_identity) VALUES ($1, $2)\n ON CONFLICT (owner_identity, sender_identity) DO NOTHING`,\n [ownerIdentity, senderIdentity],\n )\n}\n\nexport async function removeApprovedSender(ownerIdentity: string, senderIdentity: string): Promise<number> {\n await ensureSchema()\n const res = await query(\n `DELETE FROM mem_approved_senders WHERE owner_identity = $1 AND sender_identity = $2`,\n [ownerIdentity, senderIdentity],\n )\n return res.rowCount ?? 0\n}\n\nexport async function listApprovedSenders(ownerIdentity: string): Promise<string[]> {\n await ensureSchema()\n const res = await query<{ sender_identity: string }>(\n `SELECT sender_identity FROM mem_approved_senders WHERE owner_identity = $1 ORDER BY created_at ASC`,\n [ownerIdentity],\n )\n return res.rows.map(r => r.sender_identity)\n}\n\nasync function isApprovedSender(ownerIdentity: string, senderIdentity: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `SELECT 1 FROM mem_approved_senders WHERE owner_identity = $1 AND sender_identity = $2 LIMIT 1`,\n [ownerIdentity, senderIdentity],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function allowsUnapprovedSenders(identity: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ allow_unapproved_senders: boolean }>(\n `SELECT allow_unapproved_senders FROM mem_settings WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n return res.rows[0]?.allow_unapproved_senders ?? false\n}\n\nexport async function setAllowUnapprovedSenders(identity: string, allow: boolean): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_settings (identity, allow_unapproved_senders, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (identity) DO UPDATE SET allow_unapproved_senders = EXCLUDED.allow_unapproved_senders, updated_at = now()`,\n [identity, allow],\n )\n}\n\nexport async function setIsAgent(identity: string, isAgent: boolean): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_settings (identity, is_agent, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (identity) DO UPDATE SET is_agent = EXCLUDED.is_agent, updated_at = now()`,\n [identity, isAgent],\n )\n}\n\nexport async function isAgentIdentities(identities: string[]): Promise<Set<string>> {\n await ensureSchema()\n if (identities.length === 0) return new Set()\n const res = await query<{ identity: string }>(\n `SELECT identity FROM mem_settings WHERE identity = ANY($1) AND is_agent = TRUE`,\n [identities],\n )\n return new Set(res.rows.map(r => r.identity))\n}\n\nexport async function maySend(sender: string, recipient: string): Promise<boolean> {\n if (sender === recipient) return true\n if (await isApprovedSender(recipient, sender)) return true\n if (await allowsUnapprovedSenders(recipient)) return true\n if ((await getAccountGrant(recipient, sender)) !== null) return true\n if ((await getAccountGrant(sender, recipient)) !== null) return true\n return false\n}\n"],"mappings":"AAAA,SAAS,cAAc,aAAa;AACpC,SAAS,uBAAuB;AAEhC,eAAsB,kBAAkB,eAAuB,gBAAuC;AACpG,QAAM,aAAa;AACnB,QAAM;AAAA,IACJ;AAAA;AAAA,IAEA,CAAC,eAAe,cAAc;AAAA,EAChC;AACF;AAEA,eAAsB,qBAAqB,eAAuB,gBAAyC;AACzG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,eAAe,cAAc;AAAA,EAChC;AACA,SAAO,IAAI,YAAY;AACzB;AAEA,eAAsB,oBAAoB,eAA0C;AAClF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AACA,SAAO,IAAI,KAAK,IAAI,OAAK,EAAE,eAAe;AAC5C;AAEA,eAAe,iBAAiB,eAAuB,gBAA0C;AAC/F,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,eAAe,cAAc;AAAA,EAChC;AACA,UAAQ,IAAI,YAAY,KAAK;AAC/B;AAEA,eAAsB,wBAAwB,UAAoC;AAChF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AACA,SAAO,IAAI,KAAK,CAAC,GAAG,4BAA4B;AAClD;AAEA,eAAsB,0BAA0B,UAAkB,OAA+B;AAC/F,QAAM,aAAa;AACnB,QAAM;AAAA,IACJ;AAAA;AAAA;AAAA,IAGA,CAAC,UAAU,KAAK;AAAA,EAClB;AACF;AAEA,eAAsB,WAAW,UAAkB,SAAiC;AAClF,QAAM,aAAa;AACnB,QAAM;AAAA,IACJ;AAAA;AAAA;AAAA,IAGA,CAAC,UAAU,OAAO;AAAA,EACpB;AACF;AAEA,eAAsB,kBAAkB,YAA4C;AAClF,QAAM,aAAa;AACnB,MAAI,WAAW,WAAW,EAAG,QAAO,oBAAI,IAAI;AAC5C,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AACA,SAAO,IAAI,IAAI,IAAI,KAAK,IAAI,OAAK,EAAE,QAAQ,CAAC;AAC9C;AAEA,eAAsB,QAAQ,QAAgB,WAAqC;AACjF,MAAI,WAAW,UAAW,QAAO;AACjC,MAAI,MAAM,iBAAiB,WAAW,MAAM,EAAG,QAAO;AACtD,MAAI,MAAM,wBAAwB,SAAS,EAAG,QAAO;AACrD,MAAK,MAAM,gBAAgB,WAAW,MAAM,MAAO,KAAM,QAAO;AAChE,MAAK,MAAM,gBAAgB,QAAQ,SAAS,MAAO,KAAM,QAAO;AAChE,SAAO;AACT;","names":[]}
package/dist/db/usage.js CHANGED
@@ -1,6 +1,6 @@
1
- import { ensureSchema, query } from "./pool";
2
- import { storageCostUsdForDay } from "../lib/cost-rates";
3
- import { debitAiCredits } from "../lib/credit-debit";
1
+ import { ensureSchema, query } from "./pool.js";
2
+ import { storageCostUsdForDay } from "../lib/cost-rates.js";
3
+ import { debitAiCredits } from "../lib/credit-debit.js";
4
4
  const AI_SOURCES = /* @__PURE__ */ new Set(["embed", "compute", "llm"]);
5
5
  async function totalBytesForVaults(vaults) {
6
6
  if (vaults.length === 0) return 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/usage.ts"],"sourcesContent":["import { ensureSchema, query } from './pool'\nimport { storageCostUsdForDay } from '../lib/cost-rates'\nimport { debitAiCredits } from '../lib/credit-debit'\n\nconst AI_SOURCES = new Set(['embed', 'compute', 'llm']) // NOT 'storage'\n\nasync function totalBytesForVaults(vaults: string[]): Promise<number> {\n if (vaults.length === 0) return 0\n const res = await query<{ bytes: string }>(\n `SELECT COALESCE(SUM(octet_length(content)), 0)::bigint AS bytes FROM mem_notes WHERE vault = ANY($1)`,\n [vaults],\n )\n return Number(res.rows[0]?.bytes ?? 0)\n}\n\nexport type CostSource = 'llm' | 'embed' | 'storage' | 'compute'\n\nfunction periodOf(ts: Date): string {\n return `${ts.getUTCFullYear()}-${String(ts.getUTCMonth() + 1).padStart(2, '0')}`\n}\n\nexport interface RecordCostArgs {\n identity: string\n source: CostSource\n op: string\n tokens?: number\n costUsd: number\n vault?: string\n rawCostUsd?: number\n}\n\nexport async function recordCost(args: RecordCostArgs): Promise<void> {\n await ensureSchema()\n const now = new Date()\n await query(\n `INSERT INTO mem_usage_ledger (identity, ts, source, op, tokens, cost_usd, vault, period)\n VALUES ($1, now(), $2, $3, $4, $5, $6, $7)`,\n [args.identity, args.source, args.op, Math.round(args.tokens ?? 0), args.costUsd, args.vault ?? null, periodOf(now)],\n )\n const debitBasisUsd = args.rawCostUsd ?? args.costUsd\n if (AI_SOURCES.has(args.source) && debitBasisUsd > 0) {\n const res = await debitAiCredits({ identity: args.identity, costUsd: debitBasisUsd, op: args.op, source: args.source })\n if (!res.ok) {\n console.warn(JSON.stringify({ event: 'ai_debit_failed', identity: args.identity, source: args.source, op: args.op, costUsd: debitBasisUsd, reason: res.reason }))\n }\n }\n}\n\nexport interface CostSummary {\n total: number\n bySource: Record<string, number>\n}\n\nexport async function userCostThisPeriod(identity: string, period?: string): Promise<CostSummary> {\n await ensureSchema()\n const p = period ?? periodOf(new Date())\n const res = await query<{ source: string; total: string }>(\n `SELECT source, COALESCE(SUM(cost_usd), 0)::float8 AS total\n FROM mem_usage_ledger WHERE identity = $1 AND period = $2 GROUP BY source`,\n [identity, p],\n )\n const bySource: Record<string, number> = {}\n let total = 0\n for (const r of res.rows) {\n const v = Number(r.total)\n bySource[r.source] = v\n total += v\n }\n return { total, bySource }\n}\n\nexport interface TierCost {\n plan: string\n users: number\n totalUsd: number\n avgUsdPerUser: number\n}\n\nexport async function costByTier(period?: string): Promise<TierCost[]> {\n await ensureSchema()\n const p = period ?? periodOf(new Date())\n const res = await query<{ plan: string | null; users: string; total: string }>(\n `SELECT k.plan AS plan, COUNT(DISTINCT l.identity)::int AS users, COALESCE(SUM(l.cost_usd), 0)::float8 AS total\n FROM mem_usage_ledger l\n LEFT JOIN LATERAL (\n SELECT plan FROM mem_keys WHERE identity = l.identity ORDER BY created_at DESC LIMIT 1\n ) k ON true\n WHERE l.period = $1\n GROUP BY k.plan\n ORDER BY k.plan`,\n [p],\n )\n return res.rows.map(r => {\n const users = Number(r.users) || 0\n const totalUsd = Number(r.total) || 0\n return {\n plan: r.plan ?? 'unknown',\n users,\n totalUsd,\n avgUsdPerUser: users > 0 ? totalUsd / users : 0,\n }\n })\n}\n\nexport async function snapshotStorageCostForIdentity(identity: string, vaults: string[]): Promise<number> {\n const totalBytes = await totalBytesForVaults(vaults)\n const costUsd = storageCostUsdForDay(totalBytes)\n if (totalBytes > 0) {\n await recordCost({ identity, source: 'storage', op: 'daily-snapshot', costUsd, tokens: 0 })\n }\n return costUsd\n}\n\nexport async function snapshotAllStorageCost(): Promise<{ identities: number; totalUsd: number }> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; vault: string }>(\n `SELECT DISTINCT owner_identity, vault FROM mem_vault_registry`,\n )\n const byIdentity = new Map<string, string[]>()\n for (const r of res.rows) {\n const arr = byIdentity.get(r.owner_identity) ?? []\n arr.push(r.vault)\n byIdentity.set(r.owner_identity, arr)\n }\n let totalUsd = 0\n for (const [identity, vaults] of byIdentity) {\n totalUsd += await snapshotStorageCostForIdentity(identity, vaults)\n }\n return { identities: byIdentity.size, totalUsd }\n}\n"],"mappings":"AAAA,SAAS,cAAc,aAAa;AACpC,SAAS,4BAA4B;AACrC,SAAS,sBAAsB;AAE/B,MAAM,aAAa,oBAAI,IAAI,CAAC,SAAS,WAAW,KAAK,CAAC;AAEtD,eAAe,oBAAoB,QAAmC;AACpE,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,SAAO,OAAO,IAAI,KAAK,CAAC,GAAG,SAAS,CAAC;AACvC;AAIA,SAAS,SAAS,IAAkB;AAClC,SAAO,GAAG,GAAG,eAAe,CAAC,IAAI,OAAO,GAAG,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAChF;AAYA,eAAsB,WAAW,MAAqC;AACpE,QAAM,aAAa;AACnB,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM;AAAA,IACJ;AAAA;AAAA,IAEA,CAAC,KAAK,UAAU,KAAK,QAAQ,KAAK,IAAI,KAAK,MAAM,KAAK,UAAU,CAAC,GAAG,KAAK,SAAS,KAAK,SAAS,MAAM,SAAS,GAAG,CAAC;AAAA,EACrH;AACA,QAAM,gBAAgB,KAAK,cAAc,KAAK;AAC9C,MAAI,WAAW,IAAI,KAAK,MAAM,KAAK,gBAAgB,GAAG;AACpD,UAAM,MAAM,MAAM,eAAe,EAAE,UAAU,KAAK,UAAU,SAAS,eAAe,IAAI,KAAK,IAAI,QAAQ,KAAK,OAAO,CAAC;AACtH,QAAI,CAAC,IAAI,IAAI;AACX,cAAQ,KAAK,KAAK,UAAU,EAAE,OAAO,mBAAmB,UAAU,KAAK,UAAU,QAAQ,KAAK,QAAQ,IAAI,KAAK,IAAI,SAAS,eAAe,QAAQ,IAAI,OAAO,CAAC,CAAC;AAAA,IAClK;AAAA,EACF;AACF;AAOA,eAAsB,mBAAmB,UAAkB,QAAuC;AAChG,QAAM,aAAa;AACnB,QAAM,IAAI,UAAU,SAAS,oBAAI,KAAK,CAAC;AACvC,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA;AAAA,IAEA,CAAC,UAAU,CAAC;AAAA,EACd;AACA,QAAM,WAAmC,CAAC;AAC1C,MAAI,QAAQ;AACZ,aAAW,KAAK,IAAI,MAAM;AACxB,UAAM,IAAI,OAAO,EAAE,KAAK;AACxB,aAAS,EAAE,MAAM,IAAI;AACrB,aAAS;AAAA,EACX;AACA,SAAO,EAAE,OAAO,SAAS;AAC3B;AASA,eAAsB,WAAW,QAAsC;AACrE,QAAM,aAAa;AACnB,QAAM,IAAI,UAAU,SAAS,oBAAI,KAAK,CAAC;AACvC,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,CAAC,CAAC;AAAA,EACJ;AACA,SAAO,IAAI,KAAK,IAAI,OAAK;AACvB,UAAM,QAAQ,OAAO,EAAE,KAAK,KAAK;AACjC,UAAM,WAAW,OAAO,EAAE,KAAK,KAAK;AACpC,WAAO;AAAA,MACL,MAAM,EAAE,QAAQ;AAAA,MAChB;AAAA,MACA;AAAA,MACA,eAAe,QAAQ,IAAI,WAAW,QAAQ;AAAA,IAChD;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,+BAA+B,UAAkB,QAAmC;AACxG,QAAM,aAAa,MAAM,oBAAoB,MAAM;AACnD,QAAM,UAAU,qBAAqB,UAAU;AAC/C,MAAI,aAAa,GAAG;AAClB,UAAM,WAAW,EAAE,UAAU,QAAQ,WAAW,IAAI,kBAAkB,SAAS,QAAQ,EAAE,CAAC;AAAA,EAC5F;AACA,SAAO;AACT;AAEA,eAAsB,yBAA4E;AAChG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,EACF;AACA,QAAM,aAAa,oBAAI,IAAsB;AAC7C,aAAW,KAAK,IAAI,MAAM;AACxB,UAAM,MAAM,WAAW,IAAI,EAAE,cAAc,KAAK,CAAC;AACjD,QAAI,KAAK,EAAE,KAAK;AAChB,eAAW,IAAI,EAAE,gBAAgB,GAAG;AAAA,EACtC;AACA,MAAI,WAAW;AACf,aAAW,CAAC,UAAU,MAAM,KAAK,YAAY;AAC3C,gBAAY,MAAM,+BAA+B,UAAU,MAAM;AAAA,EACnE;AACA,SAAO,EAAE,YAAY,WAAW,MAAM,SAAS;AACjD;","names":[]}
1
+ {"version":3,"sources":["../../src/db/usage.ts"],"sourcesContent":["import { ensureSchema, query } from './pool.js'\nimport { storageCostUsdForDay } from '../lib/cost-rates.js'\nimport { debitAiCredits } from '../lib/credit-debit.js'\n\nconst AI_SOURCES = new Set(['embed', 'compute', 'llm']) // NOT 'storage'\n\nasync function totalBytesForVaults(vaults: string[]): Promise<number> {\n if (vaults.length === 0) return 0\n const res = await query<{ bytes: string }>(\n `SELECT COALESCE(SUM(octet_length(content)), 0)::bigint AS bytes FROM mem_notes WHERE vault = ANY($1)`,\n [vaults],\n )\n return Number(res.rows[0]?.bytes ?? 0)\n}\n\nexport type CostSource = 'llm' | 'embed' | 'storage' | 'compute'\n\nfunction periodOf(ts: Date): string {\n return `${ts.getUTCFullYear()}-${String(ts.getUTCMonth() + 1).padStart(2, '0')}`\n}\n\nexport interface RecordCostArgs {\n identity: string\n source: CostSource\n op: string\n tokens?: number\n costUsd: number\n vault?: string\n rawCostUsd?: number\n}\n\nexport async function recordCost(args: RecordCostArgs): Promise<void> {\n await ensureSchema()\n const now = new Date()\n await query(\n `INSERT INTO mem_usage_ledger (identity, ts, source, op, tokens, cost_usd, vault, period)\n VALUES ($1, now(), $2, $3, $4, $5, $6, $7)`,\n [args.identity, args.source, args.op, Math.round(args.tokens ?? 0), args.costUsd, args.vault ?? null, periodOf(now)],\n )\n const debitBasisUsd = args.rawCostUsd ?? args.costUsd\n if (AI_SOURCES.has(args.source) && debitBasisUsd > 0) {\n const res = await debitAiCredits({ identity: args.identity, costUsd: debitBasisUsd, op: args.op, source: args.source })\n if (!res.ok) {\n console.warn(JSON.stringify({ event: 'ai_debit_failed', identity: args.identity, source: args.source, op: args.op, costUsd: debitBasisUsd, reason: res.reason }))\n }\n }\n}\n\nexport interface CostSummary {\n total: number\n bySource: Record<string, number>\n}\n\nexport async function userCostThisPeriod(identity: string, period?: string): Promise<CostSummary> {\n await ensureSchema()\n const p = period ?? periodOf(new Date())\n const res = await query<{ source: string; total: string }>(\n `SELECT source, COALESCE(SUM(cost_usd), 0)::float8 AS total\n FROM mem_usage_ledger WHERE identity = $1 AND period = $2 GROUP BY source`,\n [identity, p],\n )\n const bySource: Record<string, number> = {}\n let total = 0\n for (const r of res.rows) {\n const v = Number(r.total)\n bySource[r.source] = v\n total += v\n }\n return { total, bySource }\n}\n\nexport interface TierCost {\n plan: string\n users: number\n totalUsd: number\n avgUsdPerUser: number\n}\n\nexport async function costByTier(period?: string): Promise<TierCost[]> {\n await ensureSchema()\n const p = period ?? periodOf(new Date())\n const res = await query<{ plan: string | null; users: string; total: string }>(\n `SELECT k.plan AS plan, COUNT(DISTINCT l.identity)::int AS users, COALESCE(SUM(l.cost_usd), 0)::float8 AS total\n FROM mem_usage_ledger l\n LEFT JOIN LATERAL (\n SELECT plan FROM mem_keys WHERE identity = l.identity ORDER BY created_at DESC LIMIT 1\n ) k ON true\n WHERE l.period = $1\n GROUP BY k.plan\n ORDER BY k.plan`,\n [p],\n )\n return res.rows.map(r => {\n const users = Number(r.users) || 0\n const totalUsd = Number(r.total) || 0\n return {\n plan: r.plan ?? 'unknown',\n users,\n totalUsd,\n avgUsdPerUser: users > 0 ? totalUsd / users : 0,\n }\n })\n}\n\nexport async function snapshotStorageCostForIdentity(identity: string, vaults: string[]): Promise<number> {\n const totalBytes = await totalBytesForVaults(vaults)\n const costUsd = storageCostUsdForDay(totalBytes)\n if (totalBytes > 0) {\n await recordCost({ identity, source: 'storage', op: 'daily-snapshot', costUsd, tokens: 0 })\n }\n return costUsd\n}\n\nexport async function snapshotAllStorageCost(): Promise<{ identities: number; totalUsd: number }> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; vault: string }>(\n `SELECT DISTINCT owner_identity, vault FROM mem_vault_registry`,\n )\n const byIdentity = new Map<string, string[]>()\n for (const r of res.rows) {\n const arr = byIdentity.get(r.owner_identity) ?? []\n arr.push(r.vault)\n byIdentity.set(r.owner_identity, arr)\n }\n let totalUsd = 0\n for (const [identity, vaults] of byIdentity) {\n totalUsd += await snapshotStorageCostForIdentity(identity, vaults)\n }\n return { identities: byIdentity.size, totalUsd }\n}\n"],"mappings":"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","names":[]}
package/dist/db/vaults.js CHANGED
@@ -1,6 +1,6 @@
1
- import { ensureSchema, query } from "./pool";
2
- import { normalizeScope } from "../lib/auth";
3
- import { DEFAULT_VAULTS, VAULT_CONTRACTS } from "../lib/vault-contracts";
1
+ import { ensureSchema, query } from "./pool.js";
2
+ import { normalizeScope } from "../lib/auth.js";
3
+ import { DEFAULT_VAULTS, VAULT_CONTRACTS } from "../lib/vault-contracts.js";
4
4
  function sharedHandle(ownerIdentity, vault) {
5
5
  return `${ownerIdentity}/${vault}`;
6
6
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/vaults.ts"],"sourcesContent":["import { ensureSchema, query } from './pool'\nimport { normalizeScope } from '../lib/auth'\nimport type { Scope } from '../lib/auth'\nimport { DEFAULT_VAULTS, VAULT_CONTRACTS } from '../lib/vault-contracts'\nimport type { VaultContract } from '../lib/vault-contracts'\n\nexport interface VaultEntitlement {\n handle: string\n vault: string\n ownerIdentity: string\n entitlement: Scope\n shared: boolean\n kind: 'notes' | 'channel' | 'secure'\n}\n\ninterface VaultRow {\n handle: string\n vault: string\n owner_identity: string\n entitlement: unknown\n shared: boolean\n contract: unknown\n}\n\nexport function sharedHandle(ownerIdentity: string, vault: string): string {\n return `${ownerIdentity}/${vault}`\n}\n\nfunction contractKind(contract: unknown): 'notes' | 'channel' | 'secure' {\n const c = contract as { kind?: string } | null\n if (c?.kind === 'channel') return 'channel'\n if (c?.kind === 'secure') return 'secure'\n return 'notes'\n}\n\nexport async function entitledVaults(identity: string): Promise<VaultEntitlement[]> {\n await ensureSchema()\n const res = await query<VaultRow>(\n `SELECT COALESCE(r.handle, r.vault) AS handle, r.vault, r.owner_identity, r.entitlement, r.shared,\n (SELECT owner.contract FROM mem_vault_registry owner\n WHERE owner.owner_identity = r.owner_identity AND owner.vault = r.vault AND owner.grantee = owner.owner_identity\n LIMIT 1) AS contract\n FROM mem_vault_registry r WHERE r.grantee = $1 ORDER BY r.created_at ASC`,\n [identity],\n )\n return res.rows.map(r => ({\n handle: r.handle,\n vault: r.vault,\n ownerIdentity: r.owner_identity,\n entitlement: normalizeScope(r.entitlement as Record<string, boolean>),\n shared: r.shared,\n kind: contractKind(r.contract),\n }))\n}\n\nexport async function addEntitlement(args: {\n grantee: string\n handle: string\n vault: string\n ownerIdentity: string\n entitlement: Scope\n shared: boolean\n}): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_vault_registry (grantee, handle, vault, owner_identity, entitlement, shared)\n VALUES ($1, $2, $3, $4, $5::jsonb, $6)\n ON CONFLICT (grantee, handle) DO UPDATE SET entitlement = EXCLUDED.entitlement, shared = EXCLUDED.shared`,\n [args.grantee, args.handle, args.vault, args.ownerIdentity, JSON.stringify(args.entitlement), args.shared],\n )\n}\n\nexport async function shareVaultRow(args: {\n grantee: string\n vault: string\n ownerIdentity: string\n entitlement: Scope\n}): Promise<void> {\n await addEntitlement({ ...args, handle: sharedHandle(args.ownerIdentity, args.vault), shared: true })\n}\n\nexport interface VaultMember {\n grantee: string\n entitlement: Scope\n createdAt: string\n}\n\nexport async function isChannelVault(ownerIdentity: string, vault: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ contract: unknown }>(\n `SELECT contract FROM mem_vault_registry WHERE owner_identity = $1 AND vault = $2 LIMIT 1`,\n [ownerIdentity, vault],\n )\n const contract = res.rows[0]?.contract as { kind?: string } | null\n return contract?.kind === 'channel'\n}\n\nexport async function isSecureVault(ownerIdentity: string, vault: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ contract: unknown }>(\n `SELECT contract FROM mem_vault_registry WHERE owner_identity = $1 AND vault = $2 LIMIT 1`,\n [ownerIdentity, vault],\n )\n const contract = res.rows[0]?.contract as { kind?: string } | null\n return contract?.kind === 'secure'\n}\n\nexport async function listVaultMembers(ownerIdentity: string, vault: string): Promise<VaultMember[]> {\n await ensureSchema()\n const res = await query<{ grantee: string; entitlement: unknown; created_at: string }>(\n `SELECT grantee, entitlement, created_at FROM mem_vault_registry\n WHERE owner_identity = $1 AND vault = $2 AND grantee != $1 ORDER BY created_at ASC`,\n [ownerIdentity, vault],\n )\n return res.rows.map(r => ({ grantee: r.grantee, entitlement: normalizeScope(r.entitlement as Record<string, boolean>), createdAt: r.created_at }))\n}\n\nexport async function revokeVaultMember(ownerIdentity: string, vault: string, grantee: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `DELETE FROM mem_vault_registry WHERE owner_identity = $1 AND vault = $2 AND grantee = $3`,\n [ownerIdentity, vault, grantee],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport interface IdentityVault {\n handle: string\n vault: string\n role: 'owner' | 'shared'\n sharedBy?: string\n entitlement: Scope\n kind: 'notes' | 'channel' | 'secure'\n}\n\nexport async function listVaultsForIdentity(identity: string): Promise<IdentityVault[]> {\n const ents = await entitledVaults(identity)\n const seen = new Map<string, IdentityVault>()\n for (const e of ents) {\n if (seen.has(e.handle)) continue\n const isOwner = e.ownerIdentity === identity\n seen.set(e.handle, {\n handle: e.handle,\n vault: e.vault,\n role: isOwner ? 'owner' : 'shared',\n sharedBy: isOwner ? undefined : e.ownerIdentity,\n entitlement: e.entitlement,\n kind: e.kind,\n })\n }\n return [...seen.values()]\n}\n\nexport async function physicalVaultsForIdentity(identity: string, opts?: { ownedOnly?: boolean }): Promise<string[]> {\n const vaults = await listVaultsForIdentity(identity)\n return vaults\n .filter(v => (opts?.ownedOnly ? v.role === 'owner' : true))\n .map(v => scopedVault(v.role === 'owner' ? identity : (v.sharedBy ?? identity), v.vault))\n}\n\nexport async function isVaultOwner(identity: string, handle: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ owner_identity: string }>(\n `SELECT owner_identity FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2 LIMIT 1`,\n [identity, handle],\n )\n return res.rows[0]?.owner_identity === identity\n}\n\nexport async function vaultOwnerIdentity(vault: string): Promise<string | null> {\n await ensureSchema()\n const res = await query<{ owner_identity: string }>(\n `SELECT owner_identity FROM mem_vault_registry\n WHERE grantee = owner_identity AND (owner_identity || ' ' || vault) = $1\n LIMIT 1`,\n [vault],\n )\n return res.rows[0]?.owner_identity ?? null\n}\n\nexport async function deleteVaultRegistry(grantee: string, handle: string): Promise<number> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2`, [grantee, handle])\n return res.rowCount ?? 0\n}\n\nexport function scopedVault(owner: string, vault: string): string {\n return `${owner} ${vault}`\n}\n\nexport async function vaultOwnerAndName(grantee: string, handle: string): Promise<{ ownerIdentity: string; vault: string } | null> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; vault: string }>(\n `SELECT owner_identity, vault FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2 LIMIT 1`,\n [grantee, handle],\n )\n return res.rows[0] ? { ownerIdentity: res.rows[0].owner_identity, vault: res.rows[0].vault } : null\n}\n\nexport async function physicalVaultFor(grantee: string, handle: string): Promise<string> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; vault: string }>(\n `SELECT owner_identity, vault FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2 LIMIT 1`,\n [grantee, handle],\n )\n const row = res.rows[0]\n if (row) return scopedVault(row.owner_identity, row.vault)\n const ctx = await getActiveContext(grantee)\n if (ctx.activeOwner && ctx.activeOwner !== grantee) return scopedVault(ctx.activeOwner, handle)\n return scopedVault(grantee, handle)\n}\n\nexport const VAULT_NAME_RE = /^[A-Za-z0-9 _-]{1,48}$/\n\nexport async function createVault(args: { identity: string; vault: string; owner?: string }): Promise<{ vault: string; created: boolean; owner: string }> {\n await ensureSchema()\n if (!VAULT_NAME_RE.test(args.vault)) {\n throw new Error(`invalid vault name \"${args.vault}\" (allowed: letters, digits, space, _ or -, 1-48 chars)`)\n }\n const owner = args.owner ?? args.identity\n const existing = await query(\n `SELECT 1 FROM mem_vault_registry WHERE grantee = $1 AND vault = $2 LIMIT 1`,\n [owner, args.vault],\n )\n if ((existing.rowCount ?? 0) > 0) {\n return { vault: args.vault, created: false, owner }\n }\n await addEntitlement({\n grantee: owner,\n handle: args.vault,\n vault: args.vault,\n ownerIdentity: owner,\n entitlement: normalizeScope({ read: true, write: true, export: true, index: true, admin: true, swap: true }),\n shared: false,\n })\n return { vault: args.vault, created: true, owner }\n}\n\nexport async function setVaultContract(grantee: string, vault: string, contract: VaultContract): Promise<void> {\n await ensureSchema()\n await query(\n `UPDATE mem_vault_registry SET contract = $3::jsonb WHERE grantee = $1 AND vault = $2`,\n [grantee, vault, JSON.stringify(contract)],\n )\n}\n\nexport async function provisionDefaultVaults(identity: string): Promise<{ created: string[]; vaults: string[] }> {\n await ensureSchema()\n const created: string[] = []\n for (const vault of DEFAULT_VAULTS) {\n const result = await createVault({ identity, vault, owner: identity })\n if (result.created) created.push(vault)\n const contract = VAULT_CONTRACTS[vault]\n if (contract) await setVaultContract(identity, vault, contract)\n }\n return { created, vaults: [...DEFAULT_VAULTS] }\n}\n\nexport async function setKeyVaults(keyId: string, vaults: string[]): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `UPDATE mem_keys SET vaults = $2::jsonb WHERE key_id = $1`,\n [keyId, JSON.stringify(vaults)],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function getActiveVault(_sessionId: string, identity: string): Promise<string | null> {\n await ensureSchema()\n const res = await query<{ active_vault: string | null }>(\n `SELECT active_vault FROM mem_active_context WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n return res.rows[0]?.active_vault ?? null\n}\n\nexport async function setActiveVault(_sessionId: string, identity: string, vault: string): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_active_context (identity, active_vault, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (identity) DO UPDATE SET active_vault = EXCLUDED.active_vault, updated_at = now()`,\n [identity, vault],\n )\n}\n\nexport interface ActiveContext {\n activeOwner: string | null\n activeVault: string | null\n}\n\nexport async function getActiveContext(identity: string): Promise<ActiveContext> {\n await ensureSchema()\n const res = await query<{ active_owner: string | null; active_vault: string | null }>(\n `SELECT active_owner, active_vault FROM mem_active_context WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n const r = res.rows[0]\n return { activeOwner: r?.active_owner ?? null, activeVault: r?.active_vault ?? null }\n}\n\nexport async function setActiveOwner(identity: string, owner: string | null, vault: string | null): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_active_context (identity, active_owner, active_vault, updated_at)\n VALUES ($1, $2, $3, now())\n ON CONFLICT (identity) DO UPDATE SET active_owner = EXCLUDED.active_owner, active_vault = EXCLUDED.active_vault, updated_at = now()`,\n [identity, owner, vault],\n )\n}\n\nexport interface AccountGrant {\n ownerIdentity: string\n grantee: string\n entitlement: Scope\n}\n\nexport async function addAccountGrant(args: { ownerIdentity: string; grantee: string; entitlement: Scope }): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_account_grants (owner_identity, grantee, entitlement)\n VALUES ($1, $2, $3::jsonb)\n ON CONFLICT (owner_identity, grantee) DO UPDATE SET entitlement = EXCLUDED.entitlement`,\n [args.ownerIdentity, args.grantee, JSON.stringify(args.entitlement)],\n )\n}\n\nexport async function getAccountGrant(ownerIdentity: string, grantee: string): Promise<Scope | null> {\n await ensureSchema()\n const res = await query<{ entitlement: unknown }>(\n `SELECT entitlement FROM mem_account_grants WHERE owner_identity = $1 AND grantee = $2 LIMIT 1`,\n [ownerIdentity, grantee],\n )\n const r = res.rows[0]\n return r ? normalizeScope(r.entitlement as Record<string, boolean>) : null\n}\n\nexport async function accountGrantsForGrantee(grantee: string): Promise<AccountGrant[]> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; entitlement: unknown }>(\n `SELECT owner_identity, entitlement FROM mem_account_grants WHERE grantee = $1 ORDER BY created_at ASC`,\n [grantee],\n )\n return res.rows.map(r => ({\n ownerIdentity: r.owner_identity,\n grantee,\n entitlement: normalizeScope(r.entitlement as Record<string, boolean>),\n }))\n}\n\nexport async function revokeAccountGrant(ownerIdentity: string, grantee: string): Promise<number> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_account_grants WHERE owner_identity = $1 AND grantee = $2`, [ownerIdentity, grantee])\n return res.rowCount ?? 0\n}\n\nexport async function ownedVaultNames(owner: string): Promise<string[]> {\n await ensureSchema()\n const res = await query<{ vault: string }>(\n `SELECT vault FROM mem_vault_registry WHERE grantee = $1 AND owner_identity = $1 ORDER BY created_at ASC`,\n [owner],\n )\n return res.rows.map(r => r.vault)\n}\n\nexport async function listAllNoteVaults(): Promise<string[]> {\n await ensureSchema()\n const res = await query<{ physical: string }>(\n `SELECT (owner_identity || ' ' || vault) AS physical FROM mem_vault_registry\n WHERE grantee = owner_identity AND COALESCE(contract->>'kind', '') != 'channel'\n ORDER BY owner_identity, vault`,\n )\n return res.rows.map(r => r.physical)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/db/vaults.ts"],"sourcesContent":["import { ensureSchema, query } from './pool.js'\nimport { normalizeScope } from '../lib/auth.js'\nimport type { Scope } from '../lib/auth.js'\nimport { DEFAULT_VAULTS, VAULT_CONTRACTS } from '../lib/vault-contracts.js'\nimport type { VaultContract } from '../lib/vault-contracts.js'\n\nexport interface VaultEntitlement {\n handle: string\n vault: string\n ownerIdentity: string\n entitlement: Scope\n shared: boolean\n kind: 'notes' | 'channel' | 'secure'\n}\n\ninterface VaultRow {\n handle: string\n vault: string\n owner_identity: string\n entitlement: unknown\n shared: boolean\n contract: unknown\n}\n\nexport function sharedHandle(ownerIdentity: string, vault: string): string {\n return `${ownerIdentity}/${vault}`\n}\n\nfunction contractKind(contract: unknown): 'notes' | 'channel' | 'secure' {\n const c = contract as { kind?: string } | null\n if (c?.kind === 'channel') return 'channel'\n if (c?.kind === 'secure') return 'secure'\n return 'notes'\n}\n\nexport async function entitledVaults(identity: string): Promise<VaultEntitlement[]> {\n await ensureSchema()\n const res = await query<VaultRow>(\n `SELECT COALESCE(r.handle, r.vault) AS handle, r.vault, r.owner_identity, r.entitlement, r.shared,\n (SELECT owner.contract FROM mem_vault_registry owner\n WHERE owner.owner_identity = r.owner_identity AND owner.vault = r.vault AND owner.grantee = owner.owner_identity\n LIMIT 1) AS contract\n FROM mem_vault_registry r WHERE r.grantee = $1 ORDER BY r.created_at ASC`,\n [identity],\n )\n return res.rows.map(r => ({\n handle: r.handle,\n vault: r.vault,\n ownerIdentity: r.owner_identity,\n entitlement: normalizeScope(r.entitlement as Record<string, boolean>),\n shared: r.shared,\n kind: contractKind(r.contract),\n }))\n}\n\nexport async function addEntitlement(args: {\n grantee: string\n handle: string\n vault: string\n ownerIdentity: string\n entitlement: Scope\n shared: boolean\n}): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_vault_registry (grantee, handle, vault, owner_identity, entitlement, shared)\n VALUES ($1, $2, $3, $4, $5::jsonb, $6)\n ON CONFLICT (grantee, handle) DO UPDATE SET entitlement = EXCLUDED.entitlement, shared = EXCLUDED.shared`,\n [args.grantee, args.handle, args.vault, args.ownerIdentity, JSON.stringify(args.entitlement), args.shared],\n )\n}\n\nexport async function shareVaultRow(args: {\n grantee: string\n vault: string\n ownerIdentity: string\n entitlement: Scope\n}): Promise<void> {\n await addEntitlement({ ...args, handle: sharedHandle(args.ownerIdentity, args.vault), shared: true })\n}\n\nexport interface VaultMember {\n grantee: string\n entitlement: Scope\n createdAt: string\n}\n\nexport async function isChannelVault(ownerIdentity: string, vault: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ contract: unknown }>(\n `SELECT contract FROM mem_vault_registry WHERE owner_identity = $1 AND vault = $2 LIMIT 1`,\n [ownerIdentity, vault],\n )\n const contract = res.rows[0]?.contract as { kind?: string } | null\n return contract?.kind === 'channel'\n}\n\nexport async function isSecureVault(ownerIdentity: string, vault: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ contract: unknown }>(\n `SELECT contract FROM mem_vault_registry WHERE owner_identity = $1 AND vault = $2 LIMIT 1`,\n [ownerIdentity, vault],\n )\n const contract = res.rows[0]?.contract as { kind?: string } | null\n return contract?.kind === 'secure'\n}\n\nexport async function listVaultMembers(ownerIdentity: string, vault: string): Promise<VaultMember[]> {\n await ensureSchema()\n const res = await query<{ grantee: string; entitlement: unknown; created_at: string }>(\n `SELECT grantee, entitlement, created_at FROM mem_vault_registry\n WHERE owner_identity = $1 AND vault = $2 AND grantee != $1 ORDER BY created_at ASC`,\n [ownerIdentity, vault],\n )\n return res.rows.map(r => ({ grantee: r.grantee, entitlement: normalizeScope(r.entitlement as Record<string, boolean>), createdAt: r.created_at }))\n}\n\nexport async function revokeVaultMember(ownerIdentity: string, vault: string, grantee: string): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `DELETE FROM mem_vault_registry WHERE owner_identity = $1 AND vault = $2 AND grantee = $3`,\n [ownerIdentity, vault, grantee],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport interface IdentityVault {\n handle: string\n vault: string\n role: 'owner' | 'shared'\n sharedBy?: string\n entitlement: Scope\n kind: 'notes' | 'channel' | 'secure'\n}\n\nexport async function listVaultsForIdentity(identity: string): Promise<IdentityVault[]> {\n const ents = await entitledVaults(identity)\n const seen = new Map<string, IdentityVault>()\n for (const e of ents) {\n if (seen.has(e.handle)) continue\n const isOwner = e.ownerIdentity === identity\n seen.set(e.handle, {\n handle: e.handle,\n vault: e.vault,\n role: isOwner ? 'owner' : 'shared',\n sharedBy: isOwner ? undefined : e.ownerIdentity,\n entitlement: e.entitlement,\n kind: e.kind,\n })\n }\n return [...seen.values()]\n}\n\nexport async function physicalVaultsForIdentity(identity: string, opts?: { ownedOnly?: boolean }): Promise<string[]> {\n const vaults = await listVaultsForIdentity(identity)\n return vaults\n .filter(v => (opts?.ownedOnly ? v.role === 'owner' : true))\n .map(v => scopedVault(v.role === 'owner' ? identity : (v.sharedBy ?? identity), v.vault))\n}\n\nexport async function isVaultOwner(identity: string, handle: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<{ owner_identity: string }>(\n `SELECT owner_identity FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2 LIMIT 1`,\n [identity, handle],\n )\n return res.rows[0]?.owner_identity === identity\n}\n\nexport async function vaultOwnerIdentity(vault: string): Promise<string | null> {\n await ensureSchema()\n const res = await query<{ owner_identity: string }>(\n `SELECT owner_identity FROM mem_vault_registry\n WHERE grantee = owner_identity AND (owner_identity || ' ' || vault) = $1\n LIMIT 1`,\n [vault],\n )\n return res.rows[0]?.owner_identity ?? null\n}\n\nexport async function deleteVaultRegistry(grantee: string, handle: string): Promise<number> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2`, [grantee, handle])\n return res.rowCount ?? 0\n}\n\nexport function scopedVault(owner: string, vault: string): string {\n return `${owner} ${vault}`\n}\n\nexport async function vaultOwnerAndName(grantee: string, handle: string): Promise<{ ownerIdentity: string; vault: string } | null> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; vault: string }>(\n `SELECT owner_identity, vault FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2 LIMIT 1`,\n [grantee, handle],\n )\n return res.rows[0] ? { ownerIdentity: res.rows[0].owner_identity, vault: res.rows[0].vault } : null\n}\n\nexport async function physicalVaultFor(grantee: string, handle: string): Promise<string> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; vault: string }>(\n `SELECT owner_identity, vault FROM mem_vault_registry WHERE grantee = $1 AND COALESCE(handle, vault) = $2 LIMIT 1`,\n [grantee, handle],\n )\n const row = res.rows[0]\n if (row) return scopedVault(row.owner_identity, row.vault)\n const ctx = await getActiveContext(grantee)\n if (ctx.activeOwner && ctx.activeOwner !== grantee) return scopedVault(ctx.activeOwner, handle)\n return scopedVault(grantee, handle)\n}\n\nexport const VAULT_NAME_RE = /^[A-Za-z0-9 _-]{1,48}$/\n\nexport async function createVault(args: { identity: string; vault: string; owner?: string }): Promise<{ vault: string; created: boolean; owner: string }> {\n await ensureSchema()\n if (!VAULT_NAME_RE.test(args.vault)) {\n throw new Error(`invalid vault name \"${args.vault}\" (allowed: letters, digits, space, _ or -, 1-48 chars)`)\n }\n const owner = args.owner ?? args.identity\n const existing = await query(\n `SELECT 1 FROM mem_vault_registry WHERE grantee = $1 AND vault = $2 LIMIT 1`,\n [owner, args.vault],\n )\n if ((existing.rowCount ?? 0) > 0) {\n return { vault: args.vault, created: false, owner }\n }\n await addEntitlement({\n grantee: owner,\n handle: args.vault,\n vault: args.vault,\n ownerIdentity: owner,\n entitlement: normalizeScope({ read: true, write: true, export: true, index: true, admin: true, swap: true }),\n shared: false,\n })\n return { vault: args.vault, created: true, owner }\n}\n\nexport async function setVaultContract(grantee: string, vault: string, contract: VaultContract): Promise<void> {\n await ensureSchema()\n await query(\n `UPDATE mem_vault_registry SET contract = $3::jsonb WHERE grantee = $1 AND vault = $2`,\n [grantee, vault, JSON.stringify(contract)],\n )\n}\n\nexport async function provisionDefaultVaults(identity: string): Promise<{ created: string[]; vaults: string[] }> {\n await ensureSchema()\n const created: string[] = []\n for (const vault of DEFAULT_VAULTS) {\n const result = await createVault({ identity, vault, owner: identity })\n if (result.created) created.push(vault)\n const contract = VAULT_CONTRACTS[vault]\n if (contract) await setVaultContract(identity, vault, contract)\n }\n return { created, vaults: [...DEFAULT_VAULTS] }\n}\n\nexport async function setKeyVaults(keyId: string, vaults: string[]): Promise<boolean> {\n await ensureSchema()\n const res = await query(\n `UPDATE mem_keys SET vaults = $2::jsonb WHERE key_id = $1`,\n [keyId, JSON.stringify(vaults)],\n )\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function getActiveVault(_sessionId: string, identity: string): Promise<string | null> {\n await ensureSchema()\n const res = await query<{ active_vault: string | null }>(\n `SELECT active_vault FROM mem_active_context WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n return res.rows[0]?.active_vault ?? null\n}\n\nexport async function setActiveVault(_sessionId: string, identity: string, vault: string): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_active_context (identity, active_vault, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (identity) DO UPDATE SET active_vault = EXCLUDED.active_vault, updated_at = now()`,\n [identity, vault],\n )\n}\n\nexport interface ActiveContext {\n activeOwner: string | null\n activeVault: string | null\n}\n\nexport async function getActiveContext(identity: string): Promise<ActiveContext> {\n await ensureSchema()\n const res = await query<{ active_owner: string | null; active_vault: string | null }>(\n `SELECT active_owner, active_vault FROM mem_active_context WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n const r = res.rows[0]\n return { activeOwner: r?.active_owner ?? null, activeVault: r?.active_vault ?? null }\n}\n\nexport async function setActiveOwner(identity: string, owner: string | null, vault: string | null): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_active_context (identity, active_owner, active_vault, updated_at)\n VALUES ($1, $2, $3, now())\n ON CONFLICT (identity) DO UPDATE SET active_owner = EXCLUDED.active_owner, active_vault = EXCLUDED.active_vault, updated_at = now()`,\n [identity, owner, vault],\n )\n}\n\nexport interface AccountGrant {\n ownerIdentity: string\n grantee: string\n entitlement: Scope\n}\n\nexport async function addAccountGrant(args: { ownerIdentity: string; grantee: string; entitlement: Scope }): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_account_grants (owner_identity, grantee, entitlement)\n VALUES ($1, $2, $3::jsonb)\n ON CONFLICT (owner_identity, grantee) DO UPDATE SET entitlement = EXCLUDED.entitlement`,\n [args.ownerIdentity, args.grantee, JSON.stringify(args.entitlement)],\n )\n}\n\nexport async function getAccountGrant(ownerIdentity: string, grantee: string): Promise<Scope | null> {\n await ensureSchema()\n const res = await query<{ entitlement: unknown }>(\n `SELECT entitlement FROM mem_account_grants WHERE owner_identity = $1 AND grantee = $2 LIMIT 1`,\n [ownerIdentity, grantee],\n )\n const r = res.rows[0]\n return r ? normalizeScope(r.entitlement as Record<string, boolean>) : null\n}\n\nexport async function accountGrantsForGrantee(grantee: string): Promise<AccountGrant[]> {\n await ensureSchema()\n const res = await query<{ owner_identity: string; entitlement: unknown }>(\n `SELECT owner_identity, entitlement FROM mem_account_grants WHERE grantee = $1 ORDER BY created_at ASC`,\n [grantee],\n )\n return res.rows.map(r => ({\n ownerIdentity: r.owner_identity,\n grantee,\n entitlement: normalizeScope(r.entitlement as Record<string, boolean>),\n }))\n}\n\nexport async function revokeAccountGrant(ownerIdentity: string, grantee: string): Promise<number> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_account_grants WHERE owner_identity = $1 AND grantee = $2`, [ownerIdentity, grantee])\n return res.rowCount ?? 0\n}\n\nexport async function ownedVaultNames(owner: string): Promise<string[]> {\n await ensureSchema()\n const res = await query<{ vault: string }>(\n `SELECT vault FROM mem_vault_registry WHERE grantee = $1 AND owner_identity = $1 ORDER BY created_at ASC`,\n [owner],\n )\n return res.rows.map(r => r.vault)\n}\n\nexport async function listAllNoteVaults(): Promise<string[]> {\n await ensureSchema()\n const res = await query<{ physical: string }>(\n `SELECT (owner_identity || ' ' || vault) AS physical FROM mem_vault_registry\n WHERE grantee = owner_identity AND COALESCE(contract->>'kind', '') != 'channel'\n ORDER BY owner_identity, vault`,\n )\n return res.rows.map(r => r.physical)\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { ensureSchema, query } from "./pool";
3
- import { VIDEO_RUN_TIMEOUT_ERROR } from "../lib/video-reaper";
2
+ import { ensureSchema, query } from "./pool.js";
3
+ import { VIDEO_RUN_TIMEOUT_ERROR } from "../lib/video-reaper.js";
4
4
  function mapRun(r) {
5
5
  return {
6
6
  id: r.id,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/video-runs.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { ensureSchema, query } from './pool'\nimport { VIDEO_RUN_TIMEOUT_ERROR } from '../lib/video-reaper'\n\nexport type VideoRunStatus =\n | 'queued' | 'resolving' | 'analyzing' | 'synthesizing' | 'detail' | 'done' | 'failed'\n\nexport interface VideoRunRow {\n id: string\n identity: string\n vault: string\n sourceUrl: string\n mediaUrl: string | null\n scraperApiKey: string | null\n intervalS: number\n maxFrames: number\n detail: string\n durationS: number | null\n frameCount: number\n status: VideoRunStatus\n error: string | null\n transcript: unknown\n metrics: unknown\n lenses: unknown\n qc: unknown\n reportMd: string | null\n artifactPath: string | null\n costUsd: number\n createdAt: Date\n updatedAt: Date\n}\n\ninterface RawRun {\n id: string; identity: string; vault: string; source_url: string; media_url: string | null\n scraper_api_key: string | null; interval_s: number; max_frames: number; detail: string\n duration_s: number | null; frame_count: number; status: string; error: string | null\n transcript: unknown; metrics: unknown; lenses: unknown; qc: unknown\n report_md: string | null; artifact_path: string | null; cost_usd: number\n created_at: string | Date; updated_at: string | Date\n}\n\nfunction mapRun(r: RawRun): VideoRunRow {\n return {\n id: r.id, identity: r.identity, vault: r.vault, sourceUrl: r.source_url, mediaUrl: r.media_url,\n scraperApiKey: r.scraper_api_key, intervalS: Number(r.interval_s), maxFrames: r.max_frames,\n detail: r.detail, durationS: r.duration_s == null ? null : Number(r.duration_s), frameCount: r.frame_count,\n status: r.status as VideoRunStatus, error: r.error,\n transcript: r.transcript, metrics: r.metrics, lenses: r.lenses, qc: r.qc, reportMd: r.report_md,\n artifactPath: r.artifact_path, costUsd: Number(r.cost_usd ?? 0),\n createdAt: new Date(r.created_at), updatedAt: new Date(r.updated_at),\n }\n}\n\nconst RUN_COLS = `id, identity, vault, source_url, media_url, scraper_api_key, interval_s, max_frames,\n detail, duration_s, frame_count, status, error, transcript, metrics, lenses, qc, report_md, artifact_path, cost_usd,\n created_at, updated_at`\n\nexport async function createVideoRun(args: {\n identity: string; vault: string; sourceUrl: string; intervalS: number; maxFrames: number;\n detail: string; scraperApiKey: string | null\n}): Promise<string> {\n await ensureSchema()\n const id = `vid_${randomBytes(8).toString('hex')}`\n await query(\n `INSERT INTO mem_video_runs (id, identity, vault, source_url, interval_s, max_frames, detail, scraper_api_key, status)\n VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'queued')`,\n [id, args.identity, args.vault, args.sourceUrl, args.intervalS, args.maxFrames, args.detail, args.scraperApiKey],\n )\n return id\n}\n\nexport async function getVideoRun(id: string, identity: string): Promise<VideoRunRow | null> {\n await ensureSchema()\n const res = await query<RawRun>(`SELECT ${RUN_COLS} FROM mem_video_runs WHERE id = $1 AND identity = $2 LIMIT 1`, [id, identity])\n return res.rows[0] ? mapRun(res.rows[0]) : null\n}\n\nconst PATCH_COLS: Record<string, string> = {\n mediaUrl: 'media_url', durationS: 'duration_s', frameCount: 'frame_count', status: 'status',\n error: 'error', transcript: 'transcript', metrics: 'metrics', lenses: 'lenses', qc: 'qc',\n reportMd: 'report_md', artifactPath: 'artifact_path', costUsd: 'cost_usd',\n}\nconst JSONB_KEYS = new Set(['transcript', 'metrics', 'lenses', 'qc'])\n\nexport async function updateVideoRun(id: string, patch: Partial<Record<keyof typeof PATCH_COLS, unknown>>): Promise<void> {\n await ensureSchema()\n const sets: string[] = []\n const params: unknown[] = []\n for (const [k, col] of Object.entries(PATCH_COLS)) {\n if (k in patch) {\n const isJson = JSONB_KEYS.has(k)\n params.push(isJson ? JSON.stringify((patch as Record<string, unknown>)[k]) : (patch as Record<string, unknown>)[k])\n sets.push(`${col} = $${params.length}${isJson ? '::jsonb' : ''}`)\n }\n }\n if (!sets.length) return\n params.push(id)\n await query(`UPDATE mem_video_runs SET ${sets.join(', ')}, updated_at = now() WHERE id = $${params.length}`, params)\n}\n\nexport async function insertFrames(runId: string, frames: Array<{ idx: number; tSec: number; imgB64?: string }>): Promise<void> {\n if (!frames.length) return\n await ensureSchema()\n const values: string[] = []\n const params: unknown[] = []\n for (const f of frames) {\n params.push(runId, f.idx, f.tSec, f.imgB64 ?? null)\n values.push(`($${params.length - 3}, $${params.length - 2}, $${params.length - 1}, $${params.length})`)\n }\n await query(\n `INSERT INTO mem_video_frames (run_id, idx, t_sec, img_b64) VALUES ${values.join(', ')}\n ON CONFLICT (run_id, idx) DO UPDATE SET img_b64 = COALESCE(mem_video_frames.img_b64, EXCLUDED.img_b64)`,\n params,\n )\n}\n\nexport async function setFrameObservation(runId: string, idx: number, observation: unknown, status: 'ok' | 'error'): Promise<void> {\n await query(\n `UPDATE mem_video_frames SET observation = $3::jsonb, status = $4, img_b64 = NULL WHERE run_id = $1 AND idx = $2`,\n [runId, idx, JSON.stringify(observation), status],\n )\n}\n\nexport interface PendingFrame { idx: number; tSec: number; imgB64: string }\n\nexport async function listPendingImageFrames(runId: string, limit: number): Promise<PendingFrame[]> {\n await ensureSchema()\n const res = await query<{ idx: number; t_sec: number; img_b64: string }>(\n `SELECT idx, t_sec, img_b64 FROM mem_video_frames\n WHERE run_id = $1 AND status = 'pending' AND img_b64 IS NOT NULL\n ORDER BY idx ASC LIMIT $2`,\n [runId, limit],\n )\n return res.rows.map(r => ({ idx: r.idx, tSec: Number(r.t_sec), imgB64: r.img_b64 }))\n}\n\nexport async function countPendingFrames(runId: string): Promise<number> {\n await ensureSchema()\n const res = await query<{ n: string }>(\n `SELECT COUNT(*)::int AS n FROM mem_video_frames WHERE run_id = $1 AND status = 'pending'`,\n [runId],\n )\n return Number(res.rows[0]?.n ?? 0)\n}\n\nexport async function setFrameDetail(runId: string, idx: number, detail: unknown): Promise<void> {\n await query(\n `UPDATE mem_video_frames SET detail = $3::jsonb WHERE run_id = $1 AND idx = $2`,\n [runId, idx, JSON.stringify(detail)],\n )\n}\n\nexport interface FrameRow { idx: number; tSec: number; observation: unknown; detail: unknown; status: string }\n\nexport async function listFrames(runId: string): Promise<FrameRow[]> {\n await ensureSchema()\n const res = await query<{ idx: number; t_sec: number; observation: unknown; detail: unknown; status: string }>(\n `SELECT idx, t_sec, observation, detail, status FROM mem_video_frames WHERE run_id = $1 ORDER BY idx ASC`,\n [runId],\n )\n return res.rows.map(r => ({ idx: r.idx, tSec: Number(r.t_sec), observation: r.observation, detail: r.detail, status: r.status }))\n}\n\nexport async function listQueuedVideoRuns(limit: number): Promise<Array<{ id: string; identity: string }>> {\n await ensureSchema()\n const res = await query<{ id: string; identity: string }>(\n `UPDATE mem_video_runs SET status = 'resolving', updated_at = now()\n WHERE id IN (SELECT id FROM mem_video_runs WHERE status = 'queued' ORDER BY created_at ASC LIMIT $1)\n RETURNING id, identity`,\n [limit],\n )\n return res.rows.map(r => ({ id: r.id, identity: r.identity }))\n}\n\nexport async function reapStaleVideoRun(id: string, reapMinutes: number): Promise<VideoRunRow | null> {\n await ensureSchema()\n const res = await query<RawRun>(\n `UPDATE mem_video_runs\n SET status = 'failed', error = $2, updated_at = now()\n WHERE id = $1\n AND status NOT IN ('done', 'failed')\n AND updated_at < now() - ($3 || ' minutes')::interval\n RETURNING ${RUN_COLS}`,\n [id, VIDEO_RUN_TIMEOUT_ERROR, reapMinutes],\n )\n return res.rows[0] ? mapRun(res.rows[0]) : null\n}\n\nexport async function runProgress(runId: string): Promise<{ total: number; analyzed: number }> {\n await ensureSchema()\n const res = await query<{ total: string; analyzed: string }>(\n `SELECT COUNT(*)::int AS total, COUNT(observation)::int AS analyzed FROM mem_video_frames WHERE run_id = $1`,\n [runId],\n )\n return { total: Number(res.rows[0]?.total ?? 0), analyzed: Number(res.rows[0]?.analyzed ?? 0) }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/db/video-runs.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { ensureSchema, query } from './pool.js'\nimport { VIDEO_RUN_TIMEOUT_ERROR } from '../lib/video-reaper.js'\n\nexport type VideoRunStatus =\n | 'queued' | 'resolving' | 'analyzing' | 'synthesizing' | 'detail' | 'done' | 'failed'\n\nexport interface VideoRunRow {\n id: string\n identity: string\n vault: string\n sourceUrl: string\n mediaUrl: string | null\n scraperApiKey: string | null\n intervalS: number\n maxFrames: number\n detail: string\n durationS: number | null\n frameCount: number\n status: VideoRunStatus\n error: string | null\n transcript: unknown\n metrics: unknown\n lenses: unknown\n qc: unknown\n reportMd: string | null\n artifactPath: string | null\n costUsd: number\n createdAt: Date\n updatedAt: Date\n}\n\ninterface RawRun {\n id: string; identity: string; vault: string; source_url: string; media_url: string | null\n scraper_api_key: string | null; interval_s: number; max_frames: number; detail: string\n duration_s: number | null; frame_count: number; status: string; error: string | null\n transcript: unknown; metrics: unknown; lenses: unknown; qc: unknown\n report_md: string | null; artifact_path: string | null; cost_usd: number\n created_at: string | Date; updated_at: string | Date\n}\n\nfunction mapRun(r: RawRun): VideoRunRow {\n return {\n id: r.id, identity: r.identity, vault: r.vault, sourceUrl: r.source_url, mediaUrl: r.media_url,\n scraperApiKey: r.scraper_api_key, intervalS: Number(r.interval_s), maxFrames: r.max_frames,\n detail: r.detail, durationS: r.duration_s == null ? null : Number(r.duration_s), frameCount: r.frame_count,\n status: r.status as VideoRunStatus, error: r.error,\n transcript: r.transcript, metrics: r.metrics, lenses: r.lenses, qc: r.qc, reportMd: r.report_md,\n artifactPath: r.artifact_path, costUsd: Number(r.cost_usd ?? 0),\n createdAt: new Date(r.created_at), updatedAt: new Date(r.updated_at),\n }\n}\n\nconst RUN_COLS = `id, identity, vault, source_url, media_url, scraper_api_key, interval_s, max_frames,\n detail, duration_s, frame_count, status, error, transcript, metrics, lenses, qc, report_md, artifact_path, cost_usd,\n created_at, updated_at`\n\nexport async function createVideoRun(args: {\n identity: string; vault: string; sourceUrl: string; intervalS: number; maxFrames: number;\n detail: string; scraperApiKey: string | null\n}): Promise<string> {\n await ensureSchema()\n const id = `vid_${randomBytes(8).toString('hex')}`\n await query(\n `INSERT INTO mem_video_runs (id, identity, vault, source_url, interval_s, max_frames, detail, scraper_api_key, status)\n VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'queued')`,\n [id, args.identity, args.vault, args.sourceUrl, args.intervalS, args.maxFrames, args.detail, args.scraperApiKey],\n )\n return id\n}\n\nexport async function getVideoRun(id: string, identity: string): Promise<VideoRunRow | null> {\n await ensureSchema()\n const res = await query<RawRun>(`SELECT ${RUN_COLS} FROM mem_video_runs WHERE id = $1 AND identity = $2 LIMIT 1`, [id, identity])\n return res.rows[0] ? mapRun(res.rows[0]) : null\n}\n\nconst PATCH_COLS: Record<string, string> = {\n mediaUrl: 'media_url', durationS: 'duration_s', frameCount: 'frame_count', status: 'status',\n error: 'error', transcript: 'transcript', metrics: 'metrics', lenses: 'lenses', qc: 'qc',\n reportMd: 'report_md', artifactPath: 'artifact_path', costUsd: 'cost_usd',\n}\nconst JSONB_KEYS = new Set(['transcript', 'metrics', 'lenses', 'qc'])\n\nexport async function updateVideoRun(id: string, patch: Partial<Record<keyof typeof PATCH_COLS, unknown>>): Promise<void> {\n await ensureSchema()\n const sets: string[] = []\n const params: unknown[] = []\n for (const [k, col] of Object.entries(PATCH_COLS)) {\n if (k in patch) {\n const isJson = JSONB_KEYS.has(k)\n params.push(isJson ? JSON.stringify((patch as Record<string, unknown>)[k]) : (patch as Record<string, unknown>)[k])\n sets.push(`${col} = $${params.length}${isJson ? '::jsonb' : ''}`)\n }\n }\n if (!sets.length) return\n params.push(id)\n await query(`UPDATE mem_video_runs SET ${sets.join(', ')}, updated_at = now() WHERE id = $${params.length}`, params)\n}\n\nexport async function insertFrames(runId: string, frames: Array<{ idx: number; tSec: number; imgB64?: string }>): Promise<void> {\n if (!frames.length) return\n await ensureSchema()\n const values: string[] = []\n const params: unknown[] = []\n for (const f of frames) {\n params.push(runId, f.idx, f.tSec, f.imgB64 ?? null)\n values.push(`($${params.length - 3}, $${params.length - 2}, $${params.length - 1}, $${params.length})`)\n }\n await query(\n `INSERT INTO mem_video_frames (run_id, idx, t_sec, img_b64) VALUES ${values.join(', ')}\n ON CONFLICT (run_id, idx) DO UPDATE SET img_b64 = COALESCE(mem_video_frames.img_b64, EXCLUDED.img_b64)`,\n params,\n )\n}\n\nexport async function setFrameObservation(runId: string, idx: number, observation: unknown, status: 'ok' | 'error'): Promise<void> {\n await query(\n `UPDATE mem_video_frames SET observation = $3::jsonb, status = $4, img_b64 = NULL WHERE run_id = $1 AND idx = $2`,\n [runId, idx, JSON.stringify(observation), status],\n )\n}\n\nexport interface PendingFrame { idx: number; tSec: number; imgB64: string }\n\nexport async function listPendingImageFrames(runId: string, limit: number): Promise<PendingFrame[]> {\n await ensureSchema()\n const res = await query<{ idx: number; t_sec: number; img_b64: string }>(\n `SELECT idx, t_sec, img_b64 FROM mem_video_frames\n WHERE run_id = $1 AND status = 'pending' AND img_b64 IS NOT NULL\n ORDER BY idx ASC LIMIT $2`,\n [runId, limit],\n )\n return res.rows.map(r => ({ idx: r.idx, tSec: Number(r.t_sec), imgB64: r.img_b64 }))\n}\n\nexport async function countPendingFrames(runId: string): Promise<number> {\n await ensureSchema()\n const res = await query<{ n: string }>(\n `SELECT COUNT(*)::int AS n FROM mem_video_frames WHERE run_id = $1 AND status = 'pending'`,\n [runId],\n )\n return Number(res.rows[0]?.n ?? 0)\n}\n\nexport async function setFrameDetail(runId: string, idx: number, detail: unknown): Promise<void> {\n await query(\n `UPDATE mem_video_frames SET detail = $3::jsonb WHERE run_id = $1 AND idx = $2`,\n [runId, idx, JSON.stringify(detail)],\n )\n}\n\nexport interface FrameRow { idx: number; tSec: number; observation: unknown; detail: unknown; status: string }\n\nexport async function listFrames(runId: string): Promise<FrameRow[]> {\n await ensureSchema()\n const res = await query<{ idx: number; t_sec: number; observation: unknown; detail: unknown; status: string }>(\n `SELECT idx, t_sec, observation, detail, status FROM mem_video_frames WHERE run_id = $1 ORDER BY idx ASC`,\n [runId],\n )\n return res.rows.map(r => ({ idx: r.idx, tSec: Number(r.t_sec), observation: r.observation, detail: r.detail, status: r.status }))\n}\n\nexport async function listQueuedVideoRuns(limit: number): Promise<Array<{ id: string; identity: string }>> {\n await ensureSchema()\n const res = await query<{ id: string; identity: string }>(\n `UPDATE mem_video_runs SET status = 'resolving', updated_at = now()\n WHERE id IN (SELECT id FROM mem_video_runs WHERE status = 'queued' ORDER BY created_at ASC LIMIT $1)\n RETURNING id, identity`,\n [limit],\n )\n return res.rows.map(r => ({ id: r.id, identity: r.identity }))\n}\n\nexport async function reapStaleVideoRun(id: string, reapMinutes: number): Promise<VideoRunRow | null> {\n await ensureSchema()\n const res = await query<RawRun>(\n `UPDATE mem_video_runs\n SET status = 'failed', error = $2, updated_at = now()\n WHERE id = $1\n AND status NOT IN ('done', 'failed')\n AND updated_at < now() - ($3 || ' minutes')::interval\n RETURNING ${RUN_COLS}`,\n [id, VIDEO_RUN_TIMEOUT_ERROR, reapMinutes],\n )\n return res.rows[0] ? mapRun(res.rows[0]) : null\n}\n\nexport async function runProgress(runId: string): Promise<{ total: number; analyzed: number }> {\n await ensureSchema()\n const res = await query<{ total: string; analyzed: string }>(\n `SELECT COUNT(*)::int AS total, COUNT(observation)::int AS analyzed FROM mem_video_frames WHERE run_id = $1`,\n [runId],\n )\n return { total: Number(res.rows[0]?.total ?? 0), analyzed: Number(res.rows[0]?.analyzed ?? 0) }\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAC5B,SAAS,cAAc,aAAa;AACpC,SAAS,+BAA+B;AAuCxC,SAAS,OAAO,GAAwB;AACtC,SAAO;AAAA,IACL,IAAI,EAAE;AAAA,IAAI,UAAU,EAAE;AAAA,IAAU,OAAO,EAAE;AAAA,IAAO,WAAW,EAAE;AAAA,IAAY,UAAU,EAAE;AAAA,IACrF,eAAe,EAAE;AAAA,IAAiB,WAAW,OAAO,EAAE,UAAU;AAAA,IAAG,WAAW,EAAE;AAAA,IAChF,QAAQ,EAAE;AAAA,IAAQ,WAAW,EAAE,cAAc,OAAO,OAAO,OAAO,EAAE,UAAU;AAAA,IAAG,YAAY,EAAE;AAAA,IAC/F,QAAQ,EAAE;AAAA,IAA0B,OAAO,EAAE;AAAA,IAC7C,YAAY,EAAE;AAAA,IAAY,SAAS,EAAE;AAAA,IAAS,QAAQ,EAAE;AAAA,IAAQ,IAAI,EAAE;AAAA,IAAI,UAAU,EAAE;AAAA,IACtF,cAAc,EAAE;AAAA,IAAe,SAAS,OAAO,EAAE,YAAY,CAAC;AAAA,IAC9D,WAAW,IAAI,KAAK,EAAE,UAAU;AAAA,IAAG,WAAW,IAAI,KAAK,EAAE,UAAU;AAAA,EACrE;AACF;AAEA,MAAM,WAAW;AAAA;AAAA;AAIjB,eAAsB,eAAe,MAGjB;AAClB,QAAM,aAAa;AACnB,QAAM,KAAK,OAAO,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;AAChD,QAAM;AAAA,IACJ;AAAA;AAAA,IAEA,CAAC,IAAI,KAAK,UAAU,KAAK,OAAO,KAAK,WAAW,KAAK,WAAW,KAAK,WAAW,KAAK,QAAQ,KAAK,aAAa;AAAA,EACjH;AACA,SAAO;AACT;AAEA,eAAsB,YAAY,IAAY,UAA+C;AAC3F,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM,MAAc,UAAU,QAAQ,gEAAgE,CAAC,IAAI,QAAQ,CAAC;AAChI,SAAO,IAAI,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC,IAAI;AAC7C;AAEA,MAAM,aAAqC;AAAA,EACzC,UAAU;AAAA,EAAa,WAAW;AAAA,EAAc,YAAY;AAAA,EAAe,QAAQ;AAAA,EACnF,OAAO;AAAA,EAAS,YAAY;AAAA,EAAc,SAAS;AAAA,EAAW,QAAQ;AAAA,EAAU,IAAI;AAAA,EACpF,UAAU;AAAA,EAAa,cAAc;AAAA,EAAiB,SAAS;AACjE;AACA,MAAM,aAAa,oBAAI,IAAI,CAAC,cAAc,WAAW,UAAU,IAAI,CAAC;AAEpE,eAAsB,eAAe,IAAY,OAAyE;AACxH,QAAM,aAAa;AACnB,QAAM,OAAiB,CAAC;AACxB,QAAM,SAAoB,CAAC;AAC3B,aAAW,CAAC,GAAG,GAAG,KAAK,OAAO,QAAQ,UAAU,GAAG;AACjD,QAAI,KAAK,OAAO;AACd,YAAM,SAAS,WAAW,IAAI,CAAC;AAC/B,aAAO,KAAK,SAAS,KAAK,UAAW,MAAkC,CAAC,CAAC,IAAK,MAAkC,CAAC,CAAC;AAClH,WAAK,KAAK,GAAG,GAAG,OAAO,OAAO,MAAM,GAAG,SAAS,YAAY,EAAE,EAAE;AAAA,IAClE;AAAA,EACF;AACA,MAAI,CAAC,KAAK,OAAQ;AAClB,SAAO,KAAK,EAAE;AACd,QAAM,MAAM,6BAA6B,KAAK,KAAK,IAAI,CAAC,oCAAoC,OAAO,MAAM,IAAI,MAAM;AACrH;AAEA,eAAsB,aAAa,OAAe,QAA8E;AAC9H,MAAI,CAAC,OAAO,OAAQ;AACpB,QAAM,aAAa;AACnB,QAAM,SAAmB,CAAC;AAC1B,QAAM,SAAoB,CAAC;AAC3B,aAAW,KAAK,QAAQ;AACtB,WAAO,KAAK,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,IAAI;AAClD,WAAO,KAAK,KAAK,OAAO,SAAS,CAAC,MAAM,OAAO,SAAS,CAAC,MAAM,OAAO,SAAS,CAAC,MAAM,OAAO,MAAM,GAAG;AAAA,EACxG;AACA,QAAM;AAAA,IACJ,qEAAqE,OAAO,KAAK,IAAI,CAAC;AAAA;AAAA,IAEtF;AAAA,EACF;AACF;AAEA,eAAsB,oBAAoB,OAAe,KAAa,aAAsB,QAAuC;AACjI,QAAM;AAAA,IACJ;AAAA,IACA,CAAC,OAAO,KAAK,KAAK,UAAU,WAAW,GAAG,MAAM;AAAA,EAClD;AACF;AAIA,eAAsB,uBAAuB,OAAe,OAAwC;AAClG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA;AAAA;AAAA,IAGA,CAAC,OAAO,KAAK;AAAA,EACf;AACA,SAAO,IAAI,KAAK,IAAI,QAAM,EAAE,KAAK,EAAE,KAAK,MAAM,OAAO,EAAE,KAAK,GAAG,QAAQ,EAAE,QAAQ,EAAE;AACrF;AAEA,eAAsB,mBAAmB,OAAgC;AACvE,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AACA,SAAO,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,CAAC;AACnC;AAEA,eAAsB,eAAe,OAAe,KAAa,QAAgC;AAC/F,QAAM;AAAA,IACJ;AAAA,IACA,CAAC,OAAO,KAAK,KAAK,UAAU,MAAM,CAAC;AAAA,EACrC;AACF;AAIA,eAAsB,WAAW,OAAoC;AACnE,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AACA,SAAO,IAAI,KAAK,IAAI,QAAM,EAAE,KAAK,EAAE,KAAK,MAAM,OAAO,EAAE,KAAK,GAAG,aAAa,EAAE,aAAa,QAAQ,EAAE,QAAQ,QAAQ,EAAE,OAAO,EAAE;AAClI;AAEA,eAAsB,oBAAoB,OAAiE;AACzG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA;AAAA;AAAA,IAGA,CAAC,KAAK;AAAA,EACR;AACA,SAAO,IAAI,KAAK,IAAI,QAAM,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,SAAS,EAAE;AAC/D;AAEA,eAAsB,kBAAkB,IAAY,aAAkD;AACpG,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA,iBAKa,QAAQ;AAAA,IACrB,CAAC,IAAI,yBAAyB,WAAW;AAAA,EAC3C;AACA,SAAO,IAAI,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC,IAAI;AAC7C;AAEA,eAAsB,YAAY,OAA6D;AAC7F,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AACA,SAAO,EAAE,OAAO,OAAO,IAAI,KAAK,CAAC,GAAG,SAAS,CAAC,GAAG,UAAU,OAAO,IAAI,KAAK,CAAC,GAAG,YAAY,CAAC,EAAE;AAChG;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { ensureSchema, query } from "./pool";
3
- import { issueKeyRow, revokeKeyRow } from "./keys";
2
+ import { ensureSchema, query } from "./pool.js";
3
+ import { issueKeyRow, revokeKeyRow } from "./keys.js";
4
4
  function mapRow(r) {
5
5
  return { id: r.id, identity: r.identity, keyId: r.key_id, vault: r.vault, label: r.label, createdAt: r.created_at };
6
6
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/webhooks.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { ensureSchema, query } from './pool'\nimport { issueKeyRow, revokeKeyRow } from './keys'\n\nexport interface WebhookRecord {\n id: string\n identity: string\n keyId: string\n vault: string\n label: string | null\n createdAt: string\n}\n\ninterface RawRow {\n id: string\n identity: string\n key_id: string\n vault: string\n label: string | null\n created_at: string\n}\n\nfunction mapRow(r: RawRow): WebhookRecord {\n return { id: r.id, identity: r.identity, keyId: r.key_id, vault: r.vault, label: r.label, createdAt: r.created_at }\n}\n\nconst COLUMNS = 'id, identity, key_id, vault, label, created_at'\n\nexport async function createWebhook(args: { identity: string; vault: string; label?: string }): Promise<{ webhook: WebhookRecord; secret: string }> {\n await ensureSchema()\n const issued = await issueKeyRow({\n identity: args.identity,\n vaults: [args.vault],\n scope: { read: false, write: true, export: false, index: false, admin: false, swap: false },\n plan: 'free',\n expiresAt: null,\n })\n const id = randomUUID()\n const res = await query<RawRow>(\n `INSERT INTO mem_webhooks (id, identity, key_id, vault, label) VALUES ($1, $2, $3, $4, $5) RETURNING ${COLUMNS}`,\n [id, args.identity, issued.keyId, args.vault, args.label ?? null],\n )\n return { webhook: mapRow(res.rows[0]), secret: issued.secret }\n}\n\nexport async function listWebhooksForIdentity(identity: string): Promise<WebhookRecord[]> {\n await ensureSchema()\n const res = await query<RawRow>(`SELECT ${COLUMNS} FROM mem_webhooks WHERE identity = $1 ORDER BY created_at DESC`, [identity])\n return res.rows.map(mapRow)\n}\n\nexport async function revokeWebhook(id: string, identity: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<RawRow>(`SELECT ${COLUMNS} FROM mem_webhooks WHERE id = $1 AND identity = $2`, [id, identity])\n const row = res.rows[0]\n if (!row) return false\n await revokeKeyRow(row.key_id)\n await query(`DELETE FROM mem_webhooks WHERE id = $1`, [id])\n return true\n}\n"],"mappings":"AAAA,SAAS,kBAAkB;AAC3B,SAAS,cAAc,aAAa;AACpC,SAAS,aAAa,oBAAoB;AAoB1C,SAAS,OAAO,GAA0B;AACxC,SAAO,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,UAAU,OAAO,EAAE,QAAQ,OAAO,EAAE,OAAO,OAAO,EAAE,OAAO,WAAW,EAAE,WAAW;AACpH;AAEA,MAAM,UAAU;AAEhB,eAAsB,cAAc,MAAgH;AAClJ,QAAM,aAAa;AACnB,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B,UAAU,KAAK;AAAA,IACf,QAAQ,CAAC,KAAK,KAAK;AAAA,IACnB,OAAO,EAAE,MAAM,OAAO,OAAO,MAAM,QAAQ,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM,MAAM;AAAA,IAC1F,MAAM;AAAA,IACN,WAAW;AAAA,EACb,CAAC;AACD,QAAM,KAAK,WAAW;AACtB,QAAM,MAAM,MAAM;AAAA,IAChB,uGAAuG,OAAO;AAAA,IAC9G,CAAC,IAAI,KAAK,UAAU,OAAO,OAAO,KAAK,OAAO,KAAK,SAAS,IAAI;AAAA,EAClE;AACA,SAAO,EAAE,SAAS,OAAO,IAAI,KAAK,CAAC,CAAC,GAAG,QAAQ,OAAO,OAAO;AAC/D;AAEA,eAAsB,wBAAwB,UAA4C;AACxF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM,MAAc,UAAU,OAAO,mEAAmE,CAAC,QAAQ,CAAC;AAC9H,SAAO,IAAI,KAAK,IAAI,MAAM;AAC5B;AAEA,eAAsB,cAAc,IAAY,UAAoC;AAClF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM,MAAc,UAAU,OAAO,sDAAsD,CAAC,IAAI,QAAQ,CAAC;AACrH,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,aAAa,IAAI,MAAM;AAC7B,QAAM,MAAM,0CAA0C,CAAC,EAAE,CAAC;AAC1D,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/db/webhooks.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { ensureSchema, query } from './pool.js'\nimport { issueKeyRow, revokeKeyRow } from './keys.js'\n\nexport interface WebhookRecord {\n id: string\n identity: string\n keyId: string\n vault: string\n label: string | null\n createdAt: string\n}\n\ninterface RawRow {\n id: string\n identity: string\n key_id: string\n vault: string\n label: string | null\n created_at: string\n}\n\nfunction mapRow(r: RawRow): WebhookRecord {\n return { id: r.id, identity: r.identity, keyId: r.key_id, vault: r.vault, label: r.label, createdAt: r.created_at }\n}\n\nconst COLUMNS = 'id, identity, key_id, vault, label, created_at'\n\nexport async function createWebhook(args: { identity: string; vault: string; label?: string }): Promise<{ webhook: WebhookRecord; secret: string }> {\n await ensureSchema()\n const issued = await issueKeyRow({\n identity: args.identity,\n vaults: [args.vault],\n scope: { read: false, write: true, export: false, index: false, admin: false, swap: false },\n plan: 'free',\n expiresAt: null,\n })\n const id = randomUUID()\n const res = await query<RawRow>(\n `INSERT INTO mem_webhooks (id, identity, key_id, vault, label) VALUES ($1, $2, $3, $4, $5) RETURNING ${COLUMNS}`,\n [id, args.identity, issued.keyId, args.vault, args.label ?? null],\n )\n return { webhook: mapRow(res.rows[0]), secret: issued.secret }\n}\n\nexport async function listWebhooksForIdentity(identity: string): Promise<WebhookRecord[]> {\n await ensureSchema()\n const res = await query<RawRow>(`SELECT ${COLUMNS} FROM mem_webhooks WHERE identity = $1 ORDER BY created_at DESC`, [identity])\n return res.rows.map(mapRow)\n}\n\nexport async function revokeWebhook(id: string, identity: string): Promise<boolean> {\n await ensureSchema()\n const res = await query<RawRow>(`SELECT ${COLUMNS} FROM mem_webhooks WHERE id = $1 AND identity = $2`, [id, identity])\n const row = res.rows[0]\n if (!row) return false\n await revokeKeyRow(row.key_id)\n await query(`DELETE FROM mem_webhooks WHERE id = $1`, [id])\n return true\n}\n"],"mappings":"AAAA,SAAS,kBAAkB;AAC3B,SAAS,cAAc,aAAa;AACpC,SAAS,aAAa,oBAAoB;AAoB1C,SAAS,OAAO,GAA0B;AACxC,SAAO,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,UAAU,OAAO,EAAE,QAAQ,OAAO,EAAE,OAAO,OAAO,EAAE,OAAO,WAAW,EAAE,WAAW;AACpH;AAEA,MAAM,UAAU;AAEhB,eAAsB,cAAc,MAAgH;AAClJ,QAAM,aAAa;AACnB,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B,UAAU,KAAK;AAAA,IACf,QAAQ,CAAC,KAAK,KAAK;AAAA,IACnB,OAAO,EAAE,MAAM,OAAO,OAAO,MAAM,QAAQ,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM,MAAM;AAAA,IAC1F,MAAM;AAAA,IACN,WAAW;AAAA,EACb,CAAC;AACD,QAAM,KAAK,WAAW;AACtB,QAAM,MAAM,MAAM;AAAA,IAChB,uGAAuG,OAAO;AAAA,IAC9G,CAAC,IAAI,KAAK,UAAU,OAAO,OAAO,KAAK,OAAO,KAAK,SAAS,IAAI;AAAA,EAClE;AACA,SAAO,EAAE,SAAS,OAAO,IAAI,KAAK,CAAC,CAAC,GAAG,QAAQ,OAAO,OAAO;AAC/D;AAEA,eAAsB,wBAAwB,UAA4C;AACxF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM,MAAc,UAAU,OAAO,mEAAmE,CAAC,QAAQ,CAAC;AAC9H,SAAO,IAAI,KAAK,IAAI,MAAM;AAC5B;AAEA,eAAsB,cAAc,IAAY,UAAoC;AAClF,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM,MAAc,UAAU,OAAO,sDAAsD,CAAC,IAAI,QAAQ,CAAC;AACrH,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,aAAa,IAAI,MAAM;AAC7B,QAAM,MAAM,0CAA0C,CAAC,EAAE,CAAC;AAC1D,SAAO;AACT;","names":[]}
package/dist/lib/auth.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { createRemoteJWKSet, jwtVerify } from "jose";
3
- import { ensureSchema, query } from "../db/pool";
4
- import { isOperatorIdentity, OPERATOR_PLAN } from "./operator";
5
- import { listVaultsForIdentity, provisionDefaultVaults, getActiveContext, getAccountGrant, ownedVaultNames } from "../db/vaults";
3
+ import { ensureSchema, query } from "../db/pool.js";
4
+ import { isOperatorIdentity, OPERATOR_PLAN } from "./operator.js";
5
+ import { listVaultsForIdentity, provisionDefaultVaults, getActiveContext, getAccountGrant, ownedVaultNames } from "../db/vaults.js";
6
6
  const SCOPE_KEYS = ["read", "write", "export", "index", "admin", "swap"];
7
7
  class AuthError extends Error {
8
8
  code = "auth_error";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/auth.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { createRemoteJWKSet, jwtVerify } from 'jose'\nimport { ensureSchema, query } from '../db/pool'\nimport { isOperatorIdentity, OPERATOR_PLAN } from './operator'\nimport { listVaultsForIdentity, provisionDefaultVaults, getActiveContext, getAccountGrant, ownedVaultNames } from '../db/vaults'\n\nexport const SCOPE_KEYS = ['read', 'write', 'export', 'index', 'admin', 'swap'] as const\nexport type ScopeKey = (typeof SCOPE_KEYS)[number]\nexport type Scope = Record<ScopeKey, boolean>\n\nexport type Plan = 'free' | 'pro' | 'team' | 'enterprise'\n\nexport interface AuthUser {\n keyId: string\n identity: string\n storageOwner: string\n vaults: string[]\n vaultScopes: Record<string, Scope>\n scope: Scope\n plan: string\n session: string\n}\n\nexport class AuthError extends Error {\n readonly code = 'auth_error'\n}\nexport class ScopeError extends Error {\n readonly code = 'scope_error'\n}\n\nexport function emptyScope(): Scope {\n return { read: false, write: false, export: false, index: false, admin: false, swap: false }\n}\n\nexport function leastPrivilegeScope(): Scope {\n return { ...emptyScope(), read: true }\n}\n\nexport function fullScope(): Scope {\n return { read: true, write: true, export: true, index: true, admin: true, swap: true }\n}\n\nexport function normalizeScope(input: Partial<Record<string, boolean>> | undefined): Scope {\n const out = emptyScope()\n if (!input) return out\n for (const k of SCOPE_KEYS) {\n if (input[k] === true) out[k] = true\n }\n return out\n}\n\nexport function generateSecret(): string {\n return `mk_${randomBytes(24).toString('base64url')}`\n}\n\nexport function hashSecret(secret: string): string {\n return createHash('sha256').update(secret).digest('hex')\n}\n\ninterface KeyRecord {\n key_id: string\n secret_hash: string\n identity: string\n vaults: unknown\n scope: unknown\n plan: string\n expires_at: string | null\n revoked: boolean\n}\n\nfunction toStringArray(value: unknown): string[] {\n if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')\n return []\n}\n\nexport async function resolveSecret(secret: string, session: string): Promise<AuthUser> {\n await ensureSchema()\n const secretHash = hashSecret(secret)\n const res = await query<KeyRecord>(\n 'SELECT key_id, secret_hash, identity, vaults, scope, plan, expires_at, revoked FROM mem_keys WHERE secret_hash = $1 LIMIT 1',\n [secretHash],\n )\n const row = res.rows[0]\n if (!row) throw new AuthError('Unknown API key.')\n if (row.revoked) throw new AuthError('API key has been revoked.')\n if (row.expires_at && new Date(row.expires_at).getTime() < Date.now()) {\n throw new AuthError('API key has expired.')\n }\n await query(\n 'UPDATE mem_keys SET last_used_at = now(), usage_count = usage_count + 1 WHERE key_id = $1',\n [row.key_id],\n )\n const operator = isOperatorIdentity(row.identity)\n const baseScope = operator ? fullScope() : normalizeScope(row.scope as Record<string, boolean>)\n const live = await listVaultsForIdentity(row.identity)\n const selfVaults = [...new Set([...toStringArray(row.vaults), ...live.map(v => v.handle)])]\n const selfVaultScopes: Record<string, Scope> = {}\n for (const v of live) if (v.role === 'shared') selfVaultScopes[v.handle] = v.entitlement\n const ctx = await resolveVaultContext({\n identity: row.identity,\n baseScope,\n operator,\n selfVaults,\n selfVaultScopes,\n })\n return {\n keyId: row.key_id,\n identity: row.identity,\n storageOwner: ctx.storageOwner,\n vaults: ctx.vaults,\n vaultScopes: ctx.vaultScopes,\n scope: ctx.scope,\n plan: operator ? OPERATOR_PLAN : row.plan,\n session,\n }\n}\n\nexport interface ToolAuthInput {\n apiKey?: string\n sessionId?: string\n}\n\ninterface ToolExecContext {\n runtimeContext?: { get?: (key: string) => unknown }\n requestContext?: { get?: (key: string) => unknown }\n}\n\nexport async function resolveToolAuth(input: ToolAuthInput, context?: ToolExecContext): Promise<AuthUser> {\n const fromCtx =\n context?.runtimeContext?.get?.('user') ?? context?.requestContext?.get?.('user')\n if (fromCtx && typeof fromCtx === 'object' && 'keyId' in (fromCtx as object)) {\n return fromCtx as AuthUser\n }\n const secret = input.apiKey\n if (!secret) throw new AuthError('No API key supplied (set apiKey or authenticate via the MCP transport).')\n const session = input.sessionId ?? `sess_${hashSecret(secret).slice(0, 12)}`\n return resolveSecret(secret, session)\n}\n\nexport function intersectScope(a: Scope, b: Scope): Scope {\n const out = emptyScope()\n for (const k of SCOPE_KEYS) out[k] = a[k] === true && b[k] === true\n return out\n}\n\nexport interface VaultContext {\n storageOwner: string\n vaults: string[]\n vaultScopes: Record<string, Scope>\n scope: Scope\n}\n\nexport async function resolveVaultContext(args: {\n identity: string\n baseScope: Scope\n operator: boolean\n selfVaults: string[]\n selfVaultScopes: Record<string, Scope>\n}): Promise<VaultContext> {\n const ctx = await getActiveContext(args.identity)\n if (ctx.activeOwner && ctx.activeOwner !== args.identity) {\n const grant = args.operator ? fullScope() : await getAccountGrant(ctx.activeOwner, args.identity)\n if (grant) {\n return {\n storageOwner: ctx.activeOwner,\n vaults: await ownedVaultNames(ctx.activeOwner),\n vaultScopes: {},\n scope: args.operator ? args.baseScope : intersectScope(args.baseScope, grant),\n }\n }\n }\n return { storageOwner: args.identity, vaults: args.selfVaults, vaultScopes: args.selfVaultScopes, scope: args.baseScope }\n}\n\nexport const fga = {\n assertVaultAccess(user: AuthUser, vault: string, scopeKey: ScopeKey): void {\n if (!user.vaults.includes(vault)) {\n throw new ScopeError(`Key ${user.keyId} is not entitled to vault \"${vault}\".`)\n }\n if (!user.scope[scopeKey]) {\n throw new ScopeError(`Key ${user.keyId} lacks \"${scopeKey}\" scope.`)\n }\n const vs = user.vaultScopes[vault]\n if (vs && vs[scopeKey] !== true) {\n throw new ScopeError(`Vault \"${vault}\" is shared without \"${scopeKey}\" permission.`)\n }\n },\n hasVaultAccess(user: AuthUser, vault: string, scopeKey: ScopeKey): boolean {\n if (!user.vaults.includes(vault) || user.scope[scopeKey] !== true) return false\n const vs = user.vaultScopes[vault]\n return !vs || vs[scopeKey] === true\n },\n}\n\nexport function oauthIssuer(): string {\n return (process.env.OAUTH_ISSUER ?? 'https://mcpscraper.dev').replace(/\\/$/, '')\n}\n\nexport function oauthResourceUrl(): string {\n return (\n process.env.OAUTH_RESOURCE ??\n 'https://memory.mcpscraper.dev/mcp'\n )\n}\n\nexport function oauthBaseUrl(): string {\n return (process.env.OAUTH_RS_BASE_URL ?? 'https://memory.mcpscraper.dev').replace(/\\/$/, '')\n}\n\nexport function protectedResourceMetadata(): Record<string, unknown> {\n return {\n resource: oauthResourceUrl(),\n authorization_servers: [oauthIssuer()],\n scopes_supported: ['memory.read', 'memory.write', 'memory.admin'],\n bearer_methods_supported: ['header'],\n }\n}\n\nexport function wwwAuthenticateValue(): string {\n return `Bearer resource_metadata=\"${oauthBaseUrl()}/.well-known/oauth-protected-resource\"`\n}\n\nfunction looksLikeJwt(token: string): boolean {\n return !token.startsWith('mk_') && token.split('.').length === 3\n}\n\nlet jwksSingleton: ReturnType<typeof createRemoteJWKSet> | null = null\n\nfunction getJwks(): ReturnType<typeof createRemoteJWKSet> {\n if (jwksSingleton) return jwksSingleton\n jwksSingleton = createRemoteJWKSet(new URL(`${oauthIssuer()}/.well-known/jwks.json`))\n return jwksSingleton\n}\n\nfunction scopeFromOAuth(scopeClaim: string | undefined, operator: boolean): Scope {\n if (operator) return fullScope()\n const scopes = (scopeClaim ?? '').split(/\\s+/).map(s => s.trim()).filter(Boolean)\n const out = emptyScope()\n if (scopes.includes('memory.admin')) return fullScope()\n if (scopes.includes('memory.read')) out.read = true\n if (scopes.includes('memory.write')) {\n out.read = true\n out.write = true\n out.export = true\n out.index = true\n out.swap = true\n }\n if (!out.read && !out.write) out.read = true\n return out\n}\n\nexport function isBearerJwt(token: string): boolean {\n return looksLikeJwt(token)\n}\n\nexport async function verifyOAuthJwt(token: string): Promise<Record<string, unknown>> {\n const { payload } = await jwtVerify(token, getJwks(), {\n issuer: oauthIssuer(),\n audience: oauthResourceUrl(),\n })\n return payload as Record<string, unknown>\n}\n\nexport async function resolveBearerJwt(token: string, session: string): Promise<AuthUser> {\n await ensureSchema()\n const payload = await verifyOAuthJwt(token)\n const identity = typeof payload.sub === 'string' ? payload.sub : ''\n if (!identity) throw new AuthError('Bearer token has no subject.')\n const operator = isOperatorIdentity(identity)\n const scopeClaim = typeof payload.scope === 'string' ? payload.scope : undefined\n let self = await listVaultsForIdentity(identity)\n if (self.length === 0) {\n await provisionDefaultVaults(identity)\n self = await listVaultsForIdentity(identity)\n }\n const selfVaults = self.map(v => v.handle)\n const selfVaultScopes: Record<string, Scope> = {}\n for (const v of self) if (v.role === 'shared') selfVaultScopes[v.handle] = v.entitlement\n const baseScope = scopeFromOAuth(scopeClaim, operator)\n const ctx = await resolveVaultContext({ identity, baseScope, operator, selfVaults, selfVaultScopes })\n const planClaim = typeof payload.plan === 'string' ? payload.plan : undefined\n return {\n keyId: `oauth:${identity}`,\n identity,\n storageOwner: ctx.storageOwner,\n vaults: ctx.vaults,\n vaultScopes: ctx.vaultScopes,\n scope: ctx.scope,\n plan: operator ? OPERATOR_PLAN : planClaim ?? 'free',\n session,\n }\n}\n\nexport const mapAuthInfoToUser = async (args: {\n authInfo: unknown\n extra: Record<string, unknown>\n requestContext?: { set?: (key: string, value: unknown) => void }\n}): Promise<AuthUser | undefined> => {\n const info = args.authInfo as { token?: string; apiKey?: string; extra?: { apiKey?: string } } | undefined\n const token = info?.token ?? info?.apiKey ?? info?.extra?.apiKey\n if (!token) return undefined\n const sessionId = typeof args.extra?.sessionId === 'string' ? args.extra.sessionId : `sess_${hashSecret(token).slice(0, 12)}`\n const user = looksLikeJwt(token)\n ? await resolveBearerJwt(token, sessionId)\n : await resolveSecret(token, sessionId)\n args.requestContext?.set?.('user', user)\n return user\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB;AACxC,SAAS,oBAAoB,iBAAiB;AAC9C,SAAS,cAAc,aAAa;AACpC,SAAS,oBAAoB,qBAAqB;AAClD,SAAS,uBAAuB,wBAAwB,kBAAkB,iBAAiB,uBAAuB;AAE3G,MAAM,aAAa,CAAC,QAAQ,SAAS,UAAU,SAAS,SAAS,MAAM;AAiBvE,MAAM,kBAAkB,MAAM;AAAA,EAC1B,OAAO;AAClB;AACO,MAAM,mBAAmB,MAAM;AAAA,EAC3B,OAAO;AAClB;AAEO,SAAS,aAAoB;AAClC,SAAO,EAAE,MAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM,MAAM;AAC7F;AAEO,SAAS,sBAA6B;AAC3C,SAAO,EAAE,GAAG,WAAW,GAAG,MAAM,KAAK;AACvC;AAEO,SAAS,YAAmB;AACjC,SAAO,EAAE,MAAM,MAAM,OAAO,MAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM,KAAK;AACvF;AAEO,SAAS,eAAe,OAA4D;AACzF,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,KAAK,YAAY;AAC1B,QAAI,MAAM,CAAC,MAAM,KAAM,KAAI,CAAC,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAEO,SAAS,iBAAyB;AACvC,SAAO,MAAM,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;AACpD;AAEO,SAAS,WAAW,QAAwB;AACjD,SAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AACzD;AAaA,SAAS,cAAc,OAA0B;AAC/C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AACvF,SAAO,CAAC;AACV;AAEA,eAAsB,cAAc,QAAgB,SAAoC;AACtF,QAAM,aAAa;AACnB,QAAM,aAAa,WAAW,MAAM;AACpC,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AACA,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,MAAI,CAAC,IAAK,OAAM,IAAI,UAAU,kBAAkB;AAChD,MAAI,IAAI,QAAS,OAAM,IAAI,UAAU,2BAA2B;AAChE,MAAI,IAAI,cAAc,IAAI,KAAK,IAAI,UAAU,EAAE,QAAQ,IAAI,KAAK,IAAI,GAAG;AACrE,UAAM,IAAI,UAAU,sBAAsB;AAAA,EAC5C;AACA,QAAM;AAAA,IACJ;AAAA,IACA,CAAC,IAAI,MAAM;AAAA,EACb;AACA,QAAM,WAAW,mBAAmB,IAAI,QAAQ;AAChD,QAAM,YAAY,WAAW,UAAU,IAAI,eAAe,IAAI,KAAgC;AAC9F,QAAM,OAAO,MAAM,sBAAsB,IAAI,QAAQ;AACrD,QAAM,aAAa,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,cAAc,IAAI,MAAM,GAAG,GAAG,KAAK,IAAI,OAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AAC1F,QAAM,kBAAyC,CAAC;AAChD,aAAW,KAAK,KAAM,KAAI,EAAE,SAAS,SAAU,iBAAgB,EAAE,MAAM,IAAI,EAAE;AAC7E,QAAM,MAAM,MAAM,oBAAoB;AAAA,IACpC,UAAU,IAAI;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,SAAO;AAAA,IACL,OAAO,IAAI;AAAA,IACX,UAAU,IAAI;AAAA,IACd,cAAc,IAAI;AAAA,IAClB,QAAQ,IAAI;AAAA,IACZ,aAAa,IAAI;AAAA,IACjB,OAAO,IAAI;AAAA,IACX,MAAM,WAAW,gBAAgB,IAAI;AAAA,IACrC;AAAA,EACF;AACF;AAYA,eAAsB,gBAAgB,OAAsB,SAA8C;AACxG,QAAM,UACJ,SAAS,gBAAgB,MAAM,MAAM,KAAK,SAAS,gBAAgB,MAAM,MAAM;AACjF,MAAI,WAAW,OAAO,YAAY,YAAY,WAAY,SAAoB;AAC5E,WAAO;AAAA,EACT;AACA,QAAM,SAAS,MAAM;AACrB,MAAI,CAAC,OAAQ,OAAM,IAAI,UAAU,yEAAyE;AAC1G,QAAM,UAAU,MAAM,aAAa,QAAQ,WAAW,MAAM,EAAE,MAAM,GAAG,EAAE,CAAC;AAC1E,SAAO,cAAc,QAAQ,OAAO;AACtC;AAEO,SAAS,eAAe,GAAU,GAAiB;AACxD,QAAM,MAAM,WAAW;AACvB,aAAW,KAAK,WAAY,KAAI,CAAC,IAAI,EAAE,CAAC,MAAM,QAAQ,EAAE,CAAC,MAAM;AAC/D,SAAO;AACT;AASA,eAAsB,oBAAoB,MAMhB;AACxB,QAAM,MAAM,MAAM,iBAAiB,KAAK,QAAQ;AAChD,MAAI,IAAI,eAAe,IAAI,gBAAgB,KAAK,UAAU;AACxD,UAAM,QAAQ,KAAK,WAAW,UAAU,IAAI,MAAM,gBAAgB,IAAI,aAAa,KAAK,QAAQ;AAChG,QAAI,OAAO;AACT,aAAO;AAAA,QACL,cAAc,IAAI;AAAA,QAClB,QAAQ,MAAM,gBAAgB,IAAI,WAAW;AAAA,QAC7C,aAAa,CAAC;AAAA,QACd,OAAO,KAAK,WAAW,KAAK,YAAY,eAAe,KAAK,WAAW,KAAK;AAAA,MAC9E;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,cAAc,KAAK,UAAU,QAAQ,KAAK,YAAY,aAAa,KAAK,iBAAiB,OAAO,KAAK,UAAU;AAC1H;AAEO,MAAM,MAAM;AAAA,EACjB,kBAAkB,MAAgB,OAAe,UAA0B;AACzE,QAAI,CAAC,KAAK,OAAO,SAAS,KAAK,GAAG;AAChC,YAAM,IAAI,WAAW,OAAO,KAAK,KAAK,8BAA8B,KAAK,IAAI;AAAA,IAC/E;AACA,QAAI,CAAC,KAAK,MAAM,QAAQ,GAAG;AACzB,YAAM,IAAI,WAAW,OAAO,KAAK,KAAK,WAAW,QAAQ,UAAU;AAAA,IACrE;AACA,UAAM,KAAK,KAAK,YAAY,KAAK;AACjC,QAAI,MAAM,GAAG,QAAQ,MAAM,MAAM;AAC/B,YAAM,IAAI,WAAW,UAAU,KAAK,wBAAwB,QAAQ,eAAe;AAAA,IACrF;AAAA,EACF;AAAA,EACA,eAAe,MAAgB,OAAe,UAA6B;AACzE,QAAI,CAAC,KAAK,OAAO,SAAS,KAAK,KAAK,KAAK,MAAM,QAAQ,MAAM,KAAM,QAAO;AAC1E,UAAM,KAAK,KAAK,YAAY,KAAK;AACjC,WAAO,CAAC,MAAM,GAAG,QAAQ,MAAM;AAAA,EACjC;AACF;AAEO,SAAS,cAAsB;AACpC,UAAQ,QAAQ,IAAI,gBAAgB,0BAA0B,QAAQ,OAAO,EAAE;AACjF;AAEO,SAAS,mBAA2B;AACzC,SACE,QAAQ,IAAI,kBACZ;AAEJ;AAEO,SAAS,eAAuB;AACrC,UAAQ,QAAQ,IAAI,qBAAqB,iCAAiC,QAAQ,OAAO,EAAE;AAC7F;AAEO,SAAS,4BAAqD;AACnE,SAAO;AAAA,IACL,UAAU,iBAAiB;AAAA,IAC3B,uBAAuB,CAAC,YAAY,CAAC;AAAA,IACrC,kBAAkB,CAAC,eAAe,gBAAgB,cAAc;AAAA,IAChE,0BAA0B,CAAC,QAAQ;AAAA,EACrC;AACF;AAEO,SAAS,uBAA+B;AAC7C,SAAO,6BAA6B,aAAa,CAAC;AACpD;AAEA,SAAS,aAAa,OAAwB;AAC5C,SAAO,CAAC,MAAM,WAAW,KAAK,KAAK,MAAM,MAAM,GAAG,EAAE,WAAW;AACjE;AAEA,IAAI,gBAA8D;AAElE,SAAS,UAAiD;AACxD,MAAI,cAAe,QAAO;AAC1B,kBAAgB,mBAAmB,IAAI,IAAI,GAAG,YAAY,CAAC,wBAAwB,CAAC;AACpF,SAAO;AACT;AAEA,SAAS,eAAe,YAAgC,UAA0B;AAChF,MAAI,SAAU,QAAO,UAAU;AAC/B,QAAM,UAAU,cAAc,IAAI,MAAM,KAAK,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAChF,QAAM,MAAM,WAAW;AACvB,MAAI,OAAO,SAAS,cAAc,EAAG,QAAO,UAAU;AACtD,MAAI,OAAO,SAAS,aAAa,EAAG,KAAI,OAAO;AAC/C,MAAI,OAAO,SAAS,cAAc,GAAG;AACnC,QAAI,OAAO;AACX,QAAI,QAAQ;AACZ,QAAI,SAAS;AACb,QAAI,QAAQ;AACZ,QAAI,OAAO;AAAA,EACb;AACA,MAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,MAAO,KAAI,OAAO;AACxC,SAAO;AACT;AAEO,SAAS,YAAY,OAAwB;AAClD,SAAO,aAAa,KAAK;AAC3B;AAEA,eAAsB,eAAe,OAAiD;AACpF,QAAM,EAAE,QAAQ,IAAI,MAAM,UAAU,OAAO,QAAQ,GAAG;AAAA,IACpD,QAAQ,YAAY;AAAA,IACpB,UAAU,iBAAiB;AAAA,EAC7B,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,iBAAiB,OAAe,SAAoC;AACxF,QAAM,aAAa;AACnB,QAAM,UAAU,MAAM,eAAe,KAAK;AAC1C,QAAM,WAAW,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AACjE,MAAI,CAAC,SAAU,OAAM,IAAI,UAAU,8BAA8B;AACjE,QAAM,WAAW,mBAAmB,QAAQ;AAC5C,QAAM,aAAa,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AACvE,MAAI,OAAO,MAAM,sBAAsB,QAAQ;AAC/C,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,uBAAuB,QAAQ;AACrC,WAAO,MAAM,sBAAsB,QAAQ;AAAA,EAC7C;AACA,QAAM,aAAa,KAAK,IAAI,OAAK,EAAE,MAAM;AACzC,QAAM,kBAAyC,CAAC;AAChD,aAAW,KAAK,KAAM,KAAI,EAAE,SAAS,SAAU,iBAAgB,EAAE,MAAM,IAAI,EAAE;AAC7E,QAAM,YAAY,eAAe,YAAY,QAAQ;AACrD,QAAM,MAAM,MAAM,oBAAoB,EAAE,UAAU,WAAW,UAAU,YAAY,gBAAgB,CAAC;AACpG,QAAM,YAAY,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AACpE,SAAO;AAAA,IACL,OAAO,SAAS,QAAQ;AAAA,IACxB;AAAA,IACA,cAAc,IAAI;AAAA,IAClB,QAAQ,IAAI;AAAA,IACZ,aAAa,IAAI;AAAA,IACjB,OAAO,IAAI;AAAA,IACX,MAAM,WAAW,gBAAgB,aAAa;AAAA,IAC9C;AAAA,EACF;AACF;AAEO,MAAM,oBAAoB,OAAO,SAIH;AACnC,QAAM,OAAO,KAAK;AAClB,QAAM,QAAQ,MAAM,SAAS,MAAM,UAAU,MAAM,OAAO;AAC1D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,OAAO,KAAK,OAAO,cAAc,WAAW,KAAK,MAAM,YAAY,QAAQ,WAAW,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC;AAC3H,QAAM,OAAO,aAAa,KAAK,IAC3B,MAAM,iBAAiB,OAAO,SAAS,IACvC,MAAM,cAAc,OAAO,SAAS;AACxC,OAAK,gBAAgB,MAAM,QAAQ,IAAI;AACvC,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/auth.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { createRemoteJWKSet, jwtVerify } from 'jose'\nimport { ensureSchema, query } from '../db/pool.js'\nimport { isOperatorIdentity, OPERATOR_PLAN } from './operator.js'\nimport { listVaultsForIdentity, provisionDefaultVaults, getActiveContext, getAccountGrant, ownedVaultNames } from '../db/vaults.js'\n\nexport const SCOPE_KEYS = ['read', 'write', 'export', 'index', 'admin', 'swap'] as const\nexport type ScopeKey = (typeof SCOPE_KEYS)[number]\nexport type Scope = Record<ScopeKey, boolean>\n\nexport type Plan = 'free' | 'pro' | 'team' | 'enterprise'\n\nexport interface AuthUser {\n keyId: string\n identity: string\n storageOwner: string\n vaults: string[]\n vaultScopes: Record<string, Scope>\n scope: Scope\n plan: string\n session: string\n}\n\nexport class AuthError extends Error {\n readonly code = 'auth_error'\n}\nexport class ScopeError extends Error {\n readonly code = 'scope_error'\n}\n\nexport function emptyScope(): Scope {\n return { read: false, write: false, export: false, index: false, admin: false, swap: false }\n}\n\nexport function leastPrivilegeScope(): Scope {\n return { ...emptyScope(), read: true }\n}\n\nexport function fullScope(): Scope {\n return { read: true, write: true, export: true, index: true, admin: true, swap: true }\n}\n\nexport function normalizeScope(input: Partial<Record<string, boolean>> | undefined): Scope {\n const out = emptyScope()\n if (!input) return out\n for (const k of SCOPE_KEYS) {\n if (input[k] === true) out[k] = true\n }\n return out\n}\n\nexport function generateSecret(): string {\n return `mk_${randomBytes(24).toString('base64url')}`\n}\n\nexport function hashSecret(secret: string): string {\n return createHash('sha256').update(secret).digest('hex')\n}\n\ninterface KeyRecord {\n key_id: string\n secret_hash: string\n identity: string\n vaults: unknown\n scope: unknown\n plan: string\n expires_at: string | null\n revoked: boolean\n}\n\nfunction toStringArray(value: unknown): string[] {\n if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')\n return []\n}\n\nexport async function resolveSecret(secret: string, session: string): Promise<AuthUser> {\n await ensureSchema()\n const secretHash = hashSecret(secret)\n const res = await query<KeyRecord>(\n 'SELECT key_id, secret_hash, identity, vaults, scope, plan, expires_at, revoked FROM mem_keys WHERE secret_hash = $1 LIMIT 1',\n [secretHash],\n )\n const row = res.rows[0]\n if (!row) throw new AuthError('Unknown API key.')\n if (row.revoked) throw new AuthError('API key has been revoked.')\n if (row.expires_at && new Date(row.expires_at).getTime() < Date.now()) {\n throw new AuthError('API key has expired.')\n }\n await query(\n 'UPDATE mem_keys SET last_used_at = now(), usage_count = usage_count + 1 WHERE key_id = $1',\n [row.key_id],\n )\n const operator = isOperatorIdentity(row.identity)\n const baseScope = operator ? fullScope() : normalizeScope(row.scope as Record<string, boolean>)\n const live = await listVaultsForIdentity(row.identity)\n const selfVaults = [...new Set([...toStringArray(row.vaults), ...live.map(v => v.handle)])]\n const selfVaultScopes: Record<string, Scope> = {}\n for (const v of live) if (v.role === 'shared') selfVaultScopes[v.handle] = v.entitlement\n const ctx = await resolveVaultContext({\n identity: row.identity,\n baseScope,\n operator,\n selfVaults,\n selfVaultScopes,\n })\n return {\n keyId: row.key_id,\n identity: row.identity,\n storageOwner: ctx.storageOwner,\n vaults: ctx.vaults,\n vaultScopes: ctx.vaultScopes,\n scope: ctx.scope,\n plan: operator ? OPERATOR_PLAN : row.plan,\n session,\n }\n}\n\nexport interface ToolAuthInput {\n apiKey?: string\n sessionId?: string\n}\n\ninterface ToolExecContext {\n runtimeContext?: { get?: (key: string) => unknown }\n requestContext?: { get?: (key: string) => unknown }\n}\n\nexport async function resolveToolAuth(input: ToolAuthInput, context?: ToolExecContext): Promise<AuthUser> {\n const fromCtx =\n context?.runtimeContext?.get?.('user') ?? context?.requestContext?.get?.('user')\n if (fromCtx && typeof fromCtx === 'object' && 'keyId' in (fromCtx as object)) {\n return fromCtx as AuthUser\n }\n const secret = input.apiKey\n if (!secret) throw new AuthError('No API key supplied (set apiKey or authenticate via the MCP transport).')\n const session = input.sessionId ?? `sess_${hashSecret(secret).slice(0, 12)}`\n return resolveSecret(secret, session)\n}\n\nexport function intersectScope(a: Scope, b: Scope): Scope {\n const out = emptyScope()\n for (const k of SCOPE_KEYS) out[k] = a[k] === true && b[k] === true\n return out\n}\n\nexport interface VaultContext {\n storageOwner: string\n vaults: string[]\n vaultScopes: Record<string, Scope>\n scope: Scope\n}\n\nexport async function resolveVaultContext(args: {\n identity: string\n baseScope: Scope\n operator: boolean\n selfVaults: string[]\n selfVaultScopes: Record<string, Scope>\n}): Promise<VaultContext> {\n const ctx = await getActiveContext(args.identity)\n if (ctx.activeOwner && ctx.activeOwner !== args.identity) {\n const grant = args.operator ? fullScope() : await getAccountGrant(ctx.activeOwner, args.identity)\n if (grant) {\n return {\n storageOwner: ctx.activeOwner,\n vaults: await ownedVaultNames(ctx.activeOwner),\n vaultScopes: {},\n scope: args.operator ? args.baseScope : intersectScope(args.baseScope, grant),\n }\n }\n }\n return { storageOwner: args.identity, vaults: args.selfVaults, vaultScopes: args.selfVaultScopes, scope: args.baseScope }\n}\n\nexport const fga = {\n assertVaultAccess(user: AuthUser, vault: string, scopeKey: ScopeKey): void {\n if (!user.vaults.includes(vault)) {\n throw new ScopeError(`Key ${user.keyId} is not entitled to vault \"${vault}\".`)\n }\n if (!user.scope[scopeKey]) {\n throw new ScopeError(`Key ${user.keyId} lacks \"${scopeKey}\" scope.`)\n }\n const vs = user.vaultScopes[vault]\n if (vs && vs[scopeKey] !== true) {\n throw new ScopeError(`Vault \"${vault}\" is shared without \"${scopeKey}\" permission.`)\n }\n },\n hasVaultAccess(user: AuthUser, vault: string, scopeKey: ScopeKey): boolean {\n if (!user.vaults.includes(vault) || user.scope[scopeKey] !== true) return false\n const vs = user.vaultScopes[vault]\n return !vs || vs[scopeKey] === true\n },\n}\n\nexport function oauthIssuer(): string {\n return (process.env.OAUTH_ISSUER ?? 'https://mcpscraper.dev').replace(/\\/$/, '')\n}\n\nexport function oauthResourceUrl(): string {\n return (\n process.env.OAUTH_RESOURCE ??\n 'https://memory.mcpscraper.dev/mcp'\n )\n}\n\nexport function oauthBaseUrl(): string {\n return (process.env.OAUTH_RS_BASE_URL ?? 'https://memory.mcpscraper.dev').replace(/\\/$/, '')\n}\n\nexport function protectedResourceMetadata(): Record<string, unknown> {\n return {\n resource: oauthResourceUrl(),\n authorization_servers: [oauthIssuer()],\n scopes_supported: ['memory.read', 'memory.write', 'memory.admin'],\n bearer_methods_supported: ['header'],\n }\n}\n\nexport function wwwAuthenticateValue(): string {\n return `Bearer resource_metadata=\"${oauthBaseUrl()}/.well-known/oauth-protected-resource\"`\n}\n\nfunction looksLikeJwt(token: string): boolean {\n return !token.startsWith('mk_') && token.split('.').length === 3\n}\n\nlet jwksSingleton: ReturnType<typeof createRemoteJWKSet> | null = null\n\nfunction getJwks(): ReturnType<typeof createRemoteJWKSet> {\n if (jwksSingleton) return jwksSingleton\n jwksSingleton = createRemoteJWKSet(new URL(`${oauthIssuer()}/.well-known/jwks.json`))\n return jwksSingleton\n}\n\nfunction scopeFromOAuth(scopeClaim: string | undefined, operator: boolean): Scope {\n if (operator) return fullScope()\n const scopes = (scopeClaim ?? '').split(/\\s+/).map(s => s.trim()).filter(Boolean)\n const out = emptyScope()\n if (scopes.includes('memory.admin')) return fullScope()\n if (scopes.includes('memory.read')) out.read = true\n if (scopes.includes('memory.write')) {\n out.read = true\n out.write = true\n out.export = true\n out.index = true\n out.swap = true\n }\n if (!out.read && !out.write) out.read = true\n return out\n}\n\nexport function isBearerJwt(token: string): boolean {\n return looksLikeJwt(token)\n}\n\nexport async function verifyOAuthJwt(token: string): Promise<Record<string, unknown>> {\n const { payload } = await jwtVerify(token, getJwks(), {\n issuer: oauthIssuer(),\n audience: oauthResourceUrl(),\n })\n return payload as Record<string, unknown>\n}\n\nexport async function resolveBearerJwt(token: string, session: string): Promise<AuthUser> {\n await ensureSchema()\n const payload = await verifyOAuthJwt(token)\n const identity = typeof payload.sub === 'string' ? payload.sub : ''\n if (!identity) throw new AuthError('Bearer token has no subject.')\n const operator = isOperatorIdentity(identity)\n const scopeClaim = typeof payload.scope === 'string' ? payload.scope : undefined\n let self = await listVaultsForIdentity(identity)\n if (self.length === 0) {\n await provisionDefaultVaults(identity)\n self = await listVaultsForIdentity(identity)\n }\n const selfVaults = self.map(v => v.handle)\n const selfVaultScopes: Record<string, Scope> = {}\n for (const v of self) if (v.role === 'shared') selfVaultScopes[v.handle] = v.entitlement\n const baseScope = scopeFromOAuth(scopeClaim, operator)\n const ctx = await resolveVaultContext({ identity, baseScope, operator, selfVaults, selfVaultScopes })\n const planClaim = typeof payload.plan === 'string' ? payload.plan : undefined\n return {\n keyId: `oauth:${identity}`,\n identity,\n storageOwner: ctx.storageOwner,\n vaults: ctx.vaults,\n vaultScopes: ctx.vaultScopes,\n scope: ctx.scope,\n plan: operator ? OPERATOR_PLAN : planClaim ?? 'free',\n session,\n }\n}\n\nexport const mapAuthInfoToUser = async (args: {\n authInfo: unknown\n extra: Record<string, unknown>\n requestContext?: { set?: (key: string, value: unknown) => void }\n}): Promise<AuthUser | undefined> => {\n const info = args.authInfo as { token?: string; apiKey?: string; extra?: { apiKey?: string } } | undefined\n const token = info?.token ?? info?.apiKey ?? info?.extra?.apiKey\n if (!token) return undefined\n const sessionId = typeof args.extra?.sessionId === 'string' ? args.extra.sessionId : `sess_${hashSecret(token).slice(0, 12)}`\n const user = looksLikeJwt(token)\n ? await resolveBearerJwt(token, sessionId)\n : await resolveSecret(token, sessionId)\n args.requestContext?.set?.('user', user)\n return user\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB;AACxC,SAAS,oBAAoB,iBAAiB;AAC9C,SAAS,cAAc,aAAa;AACpC,SAAS,oBAAoB,qBAAqB;AAClD,SAAS,uBAAuB,wBAAwB,kBAAkB,iBAAiB,uBAAuB;AAE3G,MAAM,aAAa,CAAC,QAAQ,SAAS,UAAU,SAAS,SAAS,MAAM;AAiBvE,MAAM,kBAAkB,MAAM;AAAA,EAC1B,OAAO;AAClB;AACO,MAAM,mBAAmB,MAAM;AAAA,EAC3B,OAAO;AAClB;AAEO,SAAS,aAAoB;AAClC,SAAO,EAAE,MAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM,MAAM;AAC7F;AAEO,SAAS,sBAA6B;AAC3C,SAAO,EAAE,GAAG,WAAW,GAAG,MAAM,KAAK;AACvC;AAEO,SAAS,YAAmB;AACjC,SAAO,EAAE,MAAM,MAAM,OAAO,MAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM,KAAK;AACvF;AAEO,SAAS,eAAe,OAA4D;AACzF,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,KAAK,YAAY;AAC1B,QAAI,MAAM,CAAC,MAAM,KAAM,KAAI,CAAC,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAEO,SAAS,iBAAyB;AACvC,SAAO,MAAM,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;AACpD;AAEO,SAAS,WAAW,QAAwB;AACjD,SAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AACzD;AAaA,SAAS,cAAc,OAA0B;AAC/C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AACvF,SAAO,CAAC;AACV;AAEA,eAAsB,cAAc,QAAgB,SAAoC;AACtF,QAAM,aAAa;AACnB,QAAM,aAAa,WAAW,MAAM;AACpC,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AACA,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,MAAI,CAAC,IAAK,OAAM,IAAI,UAAU,kBAAkB;AAChD,MAAI,IAAI,QAAS,OAAM,IAAI,UAAU,2BAA2B;AAChE,MAAI,IAAI,cAAc,IAAI,KAAK,IAAI,UAAU,EAAE,QAAQ,IAAI,KAAK,IAAI,GAAG;AACrE,UAAM,IAAI,UAAU,sBAAsB;AAAA,EAC5C;AACA,QAAM;AAAA,IACJ;AAAA,IACA,CAAC,IAAI,MAAM;AAAA,EACb;AACA,QAAM,WAAW,mBAAmB,IAAI,QAAQ;AAChD,QAAM,YAAY,WAAW,UAAU,IAAI,eAAe,IAAI,KAAgC;AAC9F,QAAM,OAAO,MAAM,sBAAsB,IAAI,QAAQ;AACrD,QAAM,aAAa,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,cAAc,IAAI,MAAM,GAAG,GAAG,KAAK,IAAI,OAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AAC1F,QAAM,kBAAyC,CAAC;AAChD,aAAW,KAAK,KAAM,KAAI,EAAE,SAAS,SAAU,iBAAgB,EAAE,MAAM,IAAI,EAAE;AAC7E,QAAM,MAAM,MAAM,oBAAoB;AAAA,IACpC,UAAU,IAAI;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,SAAO;AAAA,IACL,OAAO,IAAI;AAAA,IACX,UAAU,IAAI;AAAA,IACd,cAAc,IAAI;AAAA,IAClB,QAAQ,IAAI;AAAA,IACZ,aAAa,IAAI;AAAA,IACjB,OAAO,IAAI;AAAA,IACX,MAAM,WAAW,gBAAgB,IAAI;AAAA,IACrC;AAAA,EACF;AACF;AAYA,eAAsB,gBAAgB,OAAsB,SAA8C;AACxG,QAAM,UACJ,SAAS,gBAAgB,MAAM,MAAM,KAAK,SAAS,gBAAgB,MAAM,MAAM;AACjF,MAAI,WAAW,OAAO,YAAY,YAAY,WAAY,SAAoB;AAC5E,WAAO;AAAA,EACT;AACA,QAAM,SAAS,MAAM;AACrB,MAAI,CAAC,OAAQ,OAAM,IAAI,UAAU,yEAAyE;AAC1G,QAAM,UAAU,MAAM,aAAa,QAAQ,WAAW,MAAM,EAAE,MAAM,GAAG,EAAE,CAAC;AAC1E,SAAO,cAAc,QAAQ,OAAO;AACtC;AAEO,SAAS,eAAe,GAAU,GAAiB;AACxD,QAAM,MAAM,WAAW;AACvB,aAAW,KAAK,WAAY,KAAI,CAAC,IAAI,EAAE,CAAC,MAAM,QAAQ,EAAE,CAAC,MAAM;AAC/D,SAAO;AACT;AASA,eAAsB,oBAAoB,MAMhB;AACxB,QAAM,MAAM,MAAM,iBAAiB,KAAK,QAAQ;AAChD,MAAI,IAAI,eAAe,IAAI,gBAAgB,KAAK,UAAU;AACxD,UAAM,QAAQ,KAAK,WAAW,UAAU,IAAI,MAAM,gBAAgB,IAAI,aAAa,KAAK,QAAQ;AAChG,QAAI,OAAO;AACT,aAAO;AAAA,QACL,cAAc,IAAI;AAAA,QAClB,QAAQ,MAAM,gBAAgB,IAAI,WAAW;AAAA,QAC7C,aAAa,CAAC;AAAA,QACd,OAAO,KAAK,WAAW,KAAK,YAAY,eAAe,KAAK,WAAW,KAAK;AAAA,MAC9E;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,cAAc,KAAK,UAAU,QAAQ,KAAK,YAAY,aAAa,KAAK,iBAAiB,OAAO,KAAK,UAAU;AAC1H;AAEO,MAAM,MAAM;AAAA,EACjB,kBAAkB,MAAgB,OAAe,UAA0B;AACzE,QAAI,CAAC,KAAK,OAAO,SAAS,KAAK,GAAG;AAChC,YAAM,IAAI,WAAW,OAAO,KAAK,KAAK,8BAA8B,KAAK,IAAI;AAAA,IAC/E;AACA,QAAI,CAAC,KAAK,MAAM,QAAQ,GAAG;AACzB,YAAM,IAAI,WAAW,OAAO,KAAK,KAAK,WAAW,QAAQ,UAAU;AAAA,IACrE;AACA,UAAM,KAAK,KAAK,YAAY,KAAK;AACjC,QAAI,MAAM,GAAG,QAAQ,MAAM,MAAM;AAC/B,YAAM,IAAI,WAAW,UAAU,KAAK,wBAAwB,QAAQ,eAAe;AAAA,IACrF;AAAA,EACF;AAAA,EACA,eAAe,MAAgB,OAAe,UAA6B;AACzE,QAAI,CAAC,KAAK,OAAO,SAAS,KAAK,KAAK,KAAK,MAAM,QAAQ,MAAM,KAAM,QAAO;AAC1E,UAAM,KAAK,KAAK,YAAY,KAAK;AACjC,WAAO,CAAC,MAAM,GAAG,QAAQ,MAAM;AAAA,EACjC;AACF;AAEO,SAAS,cAAsB;AACpC,UAAQ,QAAQ,IAAI,gBAAgB,0BAA0B,QAAQ,OAAO,EAAE;AACjF;AAEO,SAAS,mBAA2B;AACzC,SACE,QAAQ,IAAI,kBACZ;AAEJ;AAEO,SAAS,eAAuB;AACrC,UAAQ,QAAQ,IAAI,qBAAqB,iCAAiC,QAAQ,OAAO,EAAE;AAC7F;AAEO,SAAS,4BAAqD;AACnE,SAAO;AAAA,IACL,UAAU,iBAAiB;AAAA,IAC3B,uBAAuB,CAAC,YAAY,CAAC;AAAA,IACrC,kBAAkB,CAAC,eAAe,gBAAgB,cAAc;AAAA,IAChE,0BAA0B,CAAC,QAAQ;AAAA,EACrC;AACF;AAEO,SAAS,uBAA+B;AAC7C,SAAO,6BAA6B,aAAa,CAAC;AACpD;AAEA,SAAS,aAAa,OAAwB;AAC5C,SAAO,CAAC,MAAM,WAAW,KAAK,KAAK,MAAM,MAAM,GAAG,EAAE,WAAW;AACjE;AAEA,IAAI,gBAA8D;AAElE,SAAS,UAAiD;AACxD,MAAI,cAAe,QAAO;AAC1B,kBAAgB,mBAAmB,IAAI,IAAI,GAAG,YAAY,CAAC,wBAAwB,CAAC;AACpF,SAAO;AACT;AAEA,SAAS,eAAe,YAAgC,UAA0B;AAChF,MAAI,SAAU,QAAO,UAAU;AAC/B,QAAM,UAAU,cAAc,IAAI,MAAM,KAAK,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAChF,QAAM,MAAM,WAAW;AACvB,MAAI,OAAO,SAAS,cAAc,EAAG,QAAO,UAAU;AACtD,MAAI,OAAO,SAAS,aAAa,EAAG,KAAI,OAAO;AAC/C,MAAI,OAAO,SAAS,cAAc,GAAG;AACnC,QAAI,OAAO;AACX,QAAI,QAAQ;AACZ,QAAI,SAAS;AACb,QAAI,QAAQ;AACZ,QAAI,OAAO;AAAA,EACb;AACA,MAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,MAAO,KAAI,OAAO;AACxC,SAAO;AACT;AAEO,SAAS,YAAY,OAAwB;AAClD,SAAO,aAAa,KAAK;AAC3B;AAEA,eAAsB,eAAe,OAAiD;AACpF,QAAM,EAAE,QAAQ,IAAI,MAAM,UAAU,OAAO,QAAQ,GAAG;AAAA,IACpD,QAAQ,YAAY;AAAA,IACpB,UAAU,iBAAiB;AAAA,EAC7B,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,iBAAiB,OAAe,SAAoC;AACxF,QAAM,aAAa;AACnB,QAAM,UAAU,MAAM,eAAe,KAAK;AAC1C,QAAM,WAAW,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AACjE,MAAI,CAAC,SAAU,OAAM,IAAI,UAAU,8BAA8B;AACjE,QAAM,WAAW,mBAAmB,QAAQ;AAC5C,QAAM,aAAa,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AACvE,MAAI,OAAO,MAAM,sBAAsB,QAAQ;AAC/C,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,uBAAuB,QAAQ;AACrC,WAAO,MAAM,sBAAsB,QAAQ;AAAA,EAC7C;AACA,QAAM,aAAa,KAAK,IAAI,OAAK,EAAE,MAAM;AACzC,QAAM,kBAAyC,CAAC;AAChD,aAAW,KAAK,KAAM,KAAI,EAAE,SAAS,SAAU,iBAAgB,EAAE,MAAM,IAAI,EAAE;AAC7E,QAAM,YAAY,eAAe,YAAY,QAAQ;AACrD,QAAM,MAAM,MAAM,oBAAoB,EAAE,UAAU,WAAW,UAAU,YAAY,gBAAgB,CAAC;AACpG,QAAM,YAAY,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AACpE,SAAO;AAAA,IACL,OAAO,SAAS,QAAQ;AAAA,IACxB;AAAA,IACA,cAAc,IAAI;AAAA,IAClB,QAAQ,IAAI;AAAA,IACZ,aAAa,IAAI;AAAA,IACjB,OAAO,IAAI;AAAA,IACX,MAAM,WAAW,gBAAgB,aAAa;AAAA,IAC9C;AAAA,EACF;AACF;AAEO,MAAM,oBAAoB,OAAO,SAIH;AACnC,QAAM,OAAO,KAAK;AAClB,QAAM,QAAQ,MAAM,SAAS,MAAM,UAAU,MAAM,OAAO;AAC1D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,OAAO,KAAK,OAAO,cAAc,WAAW,KAAK,MAAM,YAAY,QAAQ,WAAW,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC;AAC3H,QAAM,OAAO,aAAa,KAAK,IAC3B,MAAM,iBAAiB,OAAO,SAAS,IACvC,MAAM,cAAc,OAAO,SAAS;AACxC,OAAK,gBAAgB,MAAM,QAAQ,IAAI;AACvC,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/video-reaper.ts"],"sourcesContent":["import type { VideoRunStatus } from '../db/video-runs'\n\nexport const VIDEO_RUN_REAP_MINUTES = Number(process.env.VIDEO_RUN_REAP_MINUTES ?? 45)\n\nexport const VIDEO_RUN_TIMEOUT_ERROR = 'processing timed out'\n\nconst TERMINAL_VIDEO_STATUSES: ReadonlySet<VideoRunStatus> = new Set(['done', 'failed'])\n\nexport function isTerminalVideoStatus(status: VideoRunStatus): boolean {\n return TERMINAL_VIDEO_STATUSES.has(status)\n}\n\nexport interface ReapableRun {\n status: VideoRunStatus\n createdAt: Date\n updatedAt: Date | null\n}\n\nexport function isVideoRunStale(run: ReapableRun, now: Date, reapMinutes: number = VIDEO_RUN_REAP_MINUTES): boolean {\n if (isTerminalVideoStatus(run.status)) return false\n const last = run.updatedAt ?? run.createdAt\n const ageMs = now.getTime() - last.getTime()\n return ageMs > reapMinutes * 60_000\n}\n"],"mappings":"AAEO,MAAM,yBAAyB,OAAO,QAAQ,IAAI,0BAA0B,EAAE;AAE9E,MAAM,0BAA0B;AAEvC,MAAM,0BAAuD,oBAAI,IAAI,CAAC,QAAQ,QAAQ,CAAC;AAEhF,SAAS,sBAAsB,QAAiC;AACrE,SAAO,wBAAwB,IAAI,MAAM;AAC3C;AAQO,SAAS,gBAAgB,KAAkB,KAAW,cAAsB,wBAAiC;AAClH,MAAI,sBAAsB,IAAI,MAAM,EAAG,QAAO;AAC9C,QAAM,OAAO,IAAI,aAAa,IAAI;AAClC,QAAM,QAAQ,IAAI,QAAQ,IAAI,KAAK,QAAQ;AAC3C,SAAO,QAAQ,cAAc;AAC/B;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/video-reaper.ts"],"sourcesContent":["import type { VideoRunStatus } from '../db/video-runs.js'\n\nexport const VIDEO_RUN_REAP_MINUTES = Number(process.env.VIDEO_RUN_REAP_MINUTES ?? 45)\n\nexport const VIDEO_RUN_TIMEOUT_ERROR = 'processing timed out'\n\nconst TERMINAL_VIDEO_STATUSES: ReadonlySet<VideoRunStatus> = new Set(['done', 'failed'])\n\nexport function isTerminalVideoStatus(status: VideoRunStatus): boolean {\n return TERMINAL_VIDEO_STATUSES.has(status)\n}\n\nexport interface ReapableRun {\n status: VideoRunStatus\n createdAt: Date\n updatedAt: Date | null\n}\n\nexport function isVideoRunStale(run: ReapableRun, now: Date, reapMinutes: number = VIDEO_RUN_REAP_MINUTES): boolean {\n if (isTerminalVideoStatus(run.status)) return false\n const last = run.updatedAt ?? run.createdAt\n const ageMs = now.getTime() - last.getTime()\n return ageMs > reapMinutes * 60_000\n}\n"],"mappings":"AAEO,MAAM,yBAAyB,OAAO,QAAQ,IAAI,0BAA0B,EAAE;AAE9E,MAAM,0BAA0B;AAEvC,MAAM,0BAAuD,oBAAI,IAAI,CAAC,QAAQ,QAAQ,CAAC;AAEhF,SAAS,sBAAsB,QAAiC;AACrE,SAAO,wBAAwB,IAAI,MAAM;AAC3C;AAQO,SAAS,gBAAgB,KAAkB,KAAW,cAAsB,wBAAiC;AAClH,MAAI,sBAAsB,IAAI,MAAM,EAAG,QAAO;AAC9C,QAAM,OAAO,IAAI,aAAa,IAAI;AAClC,QAAM,QAAQ,IAAI,QAAQ,IAAI,KAAK,QAAQ;AAC3C,SAAO,QAAQ,cAAc;AAC/B;","names":[]}