mcpscraper-memory-db 1.0.0

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 (104) hide show
  1. package/README.md +40 -0
  2. package/dist/db/chat-link.d.ts +7 -0
  3. package/dist/db/chat-link.js +50 -0
  4. package/dist/db/chat-link.js.map +1 -0
  5. package/dist/db/facts.d.ts +60 -0
  6. package/dist/db/facts.js +186 -0
  7. package/dist/db/facts.js.map +1 -0
  8. package/dist/db/interlink-state.d.ts +34 -0
  9. package/dist/db/interlink-state.js +42 -0
  10. package/dist/db/interlink-state.js.map +1 -0
  11. package/dist/db/keys.d.ts +44 -0
  12. package/dist/db/keys.js +118 -0
  13. package/dist/db/keys.js.map +1 -0
  14. package/dist/db/messages.d.ts +77 -0
  15. package/dist/db/messages.js +198 -0
  16. package/dist/db/messages.js.map +1 -0
  17. package/dist/db/notes.d.ts +109 -0
  18. package/dist/db/notes.js +249 -0
  19. package/dist/db/notes.js.map +1 -0
  20. package/dist/db/pool.d.ts +7 -0
  21. package/dist/db/pool.js +37 -0
  22. package/dist/db/pool.js.map +1 -0
  23. package/dist/db/presence.d.ts +4 -0
  24. package/dist/db/presence.js +25 -0
  25. package/dist/db/presence.js.map +1 -0
  26. package/dist/db/props-snapshots.d.ts +18 -0
  27. package/dist/db/props-snapshots.js +37 -0
  28. package/dist/db/props-snapshots.js.map +1 -0
  29. package/dist/db/provenance.d.ts +26 -0
  30. package/dist/db/provenance.js +43 -0
  31. package/dist/db/provenance.js.map +1 -0
  32. package/dist/db/schedule-entitlements.d.ts +24 -0
  33. package/dist/db/schedule-entitlements.js +70 -0
  34. package/dist/db/schedule-entitlements.js.map +1 -0
  35. package/dist/db/schedule-link.d.ts +7 -0
  36. package/dist/db/schedule-link.js +50 -0
  37. package/dist/db/schedule-link.js.map +1 -0
  38. package/dist/db/scheduled-actions.d.ts +58 -0
  39. package/dist/db/scheduled-actions.js +139 -0
  40. package/dist/db/scheduled-actions.js.map +1 -0
  41. package/dist/db/schema.d.ts +3 -0
  42. package/dist/db/schema.js +340 -0
  43. package/dist/db/schema.js.map +1 -0
  44. package/dist/db/secure-notes.d.ts +26 -0
  45. package/dist/db/secure-notes.js +59 -0
  46. package/dist/db/secure-notes.js.map +1 -0
  47. package/dist/db/shares.d.ts +45 -0
  48. package/dist/db/shares.js +141 -0
  49. package/dist/db/shares.js.map +1 -0
  50. package/dist/db/tables.d.ts +50 -0
  51. package/dist/db/tables.js +226 -0
  52. package/dist/db/tables.js.map +1 -0
  53. package/dist/db/trust.d.ts +10 -0
  54. package/dist/db/trust.js +88 -0
  55. package/dist/db/trust.js.map +1 -0
  56. package/dist/db/usage.d.ts +30 -0
  57. package/dist/db/usage.js +106 -0
  58. package/dist/db/usage.js.map +1 -0
  59. package/dist/db/vaults.d.ts +98 -0
  60. package/dist/db/vaults.js +316 -0
  61. package/dist/db/vaults.js.map +1 -0
  62. package/dist/db/video-runs.d.ts +70 -0
  63. package/dist/db/video-runs.js +176 -0
  64. package/dist/db/video-runs.js.map +1 -0
  65. package/dist/db/webhooks.d.ts +20 -0
  66. package/dist/db/webhooks.js +43 -0
  67. package/dist/db/webhooks.js.map +1 -0
  68. package/dist/lib/auth.d.ts +75 -0
  69. package/dist/lib/auth.js +248 -0
  70. package/dist/lib/auth.js.map +1 -0
  71. package/dist/lib/cost-rates.d.ts +19 -0
  72. package/dist/lib/cost-rates.js +36 -0
  73. package/dist/lib/cost-rates.js.map +1 -0
  74. package/dist/lib/credit-debit.d.ts +15 -0
  75. package/dist/lib/credit-debit.js +21 -0
  76. package/dist/lib/credit-debit.js.map +1 -0
  77. package/dist/lib/operator.d.ts +5 -0
  78. package/dist/lib/operator.js +14 -0
  79. package/dist/lib/operator.js.map +1 -0
  80. package/dist/lib/schedule-cadence.d.ts +6 -0
  81. package/dist/lib/schedule-cadence.js +66 -0
  82. package/dist/lib/schedule-cadence.js.map +1 -0
  83. package/dist/lib/secret-crypto.d.ts +4 -0
  84. package/dist/lib/secret-crypto.js +28 -0
  85. package/dist/lib/secret-crypto.js.map +1 -0
  86. package/dist/lib/vault-contracts.d.ts +18 -0
  87. package/dist/lib/vault-contracts.js +248 -0
  88. package/dist/lib/vault-contracts.js.map +1 -0
  89. package/dist/lib/video-reaper.d.ts +13 -0
  90. package/dist/lib/video-reaper.js +19 -0
  91. package/dist/lib/video-reaper.js.map +1 -0
  92. package/dist/rag/embedder.d.ts +6 -0
  93. package/dist/rag/embedder.js +93 -0
  94. package/dist/rag/embedder.js.map +1 -0
  95. package/dist/rag/index-pipeline.d.ts +22 -0
  96. package/dist/rag/index-pipeline.js +62 -0
  97. package/dist/rag/index-pipeline.js.map +1 -0
  98. package/dist/rag/query-tool.d.ts +47 -0
  99. package/dist/rag/query-tool.js +47 -0
  100. package/dist/rag/query-tool.js.map +1 -0
  101. package/dist/rag/vector-store.d.ts +7 -0
  102. package/dist/rag/vector-store.js +25 -0
  103. package/dist/rag/vector-store.js.map +1 -0
  104. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # mcpscraper-memory-db
2
+
3
+ Shared data-access layer for the mcpscraper memory product: Postgres `db/*.ts` modules, the
4
+ `lib/*.ts` utilities they depend on (auth, secret encryption, vault contracts, cost rates, the
5
+ already-HTTP-only credit-debit client), and the `rag/*.ts` embedding/indexing layer (Jina
6
+ embeddings over `@mastra/pg`'s `PgVector`).
7
+
8
+ This package exists because `mcp-memory` and `mcp-scraper-scheduler` both need this exact logic —
9
+ before this package, it was hand-copied between the two repos, which is exactly the kind of drift
10
+ this package is meant to prevent. Each consumer's own `src/mastra/db/*.ts`, `lib/*.ts`, and
11
+ `rag/*.ts` files are one-line re-export shims pointing here; the real implementation lives only in
12
+ this package.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { query, ensureSchema } from 'mcpscraper-memory-db/db/pool'
18
+ import { resolveToolAuth } from 'mcpscraper-memory-db/lib/auth'
19
+ ```
20
+
21
+ Every subpath under `db/`, `lib/`, and `rag/` is exported individually — there is no single barrel
22
+ export, to keep tree-shaking simple and match how the original files were imported.
23
+
24
+ ## Requires
25
+
26
+ `DATABASE_URL` (Postgres, used by `db/pool.ts`), `JINA_API_KEY`/`JINA_EMBED_MODEL`/`JINA_EMBED_DIM`
27
+ (used by `rag/embedder.ts`), `SCHEDULE_SECRET_ENC_KEY` (used by `lib/secret-crypto.ts`),
28
+ `MCP_SCRAPER_INTERNAL_URL`/`INTERNAL_MEMORY_DEBIT_SECRET` (used by `lib/credit-debit.ts`'s HTTP
29
+ call into mcp-scraper's real ledger) — same env vars either consuming repo already needs.
30
+
31
+ ## Publishing
32
+
33
+ ```
34
+ npm run build
35
+ npm publish
36
+ ```
37
+
38
+ Bump the version in `package.json` first. Both consumers pin an exact or caret version and need a
39
+ `npm install mcpscraper-memory-db@latest` + redeploy to pick up a new release — there is no
40
+ auto-sync.
@@ -0,0 +1,7 @@
1
+ declare function getOrCreateChatLinkSecret(identity: string): Promise<string>;
2
+ declare class ChatLinkSecretNotRetrievable extends Error {
3
+ constructor();
4
+ }
5
+ declare function revokeChatLink(identity: string): Promise<boolean>;
6
+
7
+ export { ChatLinkSecretNotRetrievable, getOrCreateChatLinkSecret, revokeChatLink };
@@ -0,0 +1,50 @@
1
+ import { ensureSchema, query } from "./pool";
2
+ import { issueKeyRow, revokeKeyRow, getKeyById } from "./keys";
3
+ import { fullScope } from "../lib/auth";
4
+ async function getChatLinkKeyId(identity) {
5
+ await ensureSchema();
6
+ const res = await query(
7
+ `SELECT chat_link_key_id FROM mem_settings WHERE identity = $1 LIMIT 1`,
8
+ [identity]
9
+ );
10
+ return res.rows[0]?.chat_link_key_id ?? null;
11
+ }
12
+ async function setChatLinkKeyId(identity, keyId) {
13
+ await ensureSchema();
14
+ await query(
15
+ `INSERT INTO mem_settings (identity, chat_link_key_id, updated_at)
16
+ VALUES ($1, $2, now())
17
+ ON CONFLICT (identity) DO UPDATE SET chat_link_key_id = EXCLUDED.chat_link_key_id, updated_at = now()`,
18
+ [identity, keyId]
19
+ );
20
+ }
21
+ async function getOrCreateChatLinkSecret(identity) {
22
+ const existingId = await getChatLinkKeyId(identity);
23
+ if (existingId) {
24
+ const existing = await getKeyById(existingId);
25
+ if (existing && !existing.revoked) {
26
+ throw new ChatLinkSecretNotRetrievable();
27
+ }
28
+ }
29
+ const issued = await issueKeyRow({ identity, vaults: [], scope: fullScope(), plan: "free", expiresAt: null });
30
+ await setChatLinkKeyId(identity, issued.keyId);
31
+ return issued.secret;
32
+ }
33
+ class ChatLinkSecretNotRetrievable extends Error {
34
+ constructor() {
35
+ super("A chat link already exists for this identity; its secret cannot be re-displayed (only revoked and reissued).");
36
+ }
37
+ }
38
+ async function revokeChatLink(identity) {
39
+ const existingId = await getChatLinkKeyId(identity);
40
+ if (!existingId) return false;
41
+ await revokeKeyRow(existingId);
42
+ await setChatLinkKeyId(identity, null);
43
+ return true;
44
+ }
45
+ export {
46
+ ChatLinkSecretNotRetrievable,
47
+ getOrCreateChatLinkSecret,
48
+ revokeChatLink
49
+ };
50
+ //# sourceMappingURL=chat-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/chat-link.ts"],"sourcesContent":["import { ensureSchema, query } from './pool'\nimport { issueKeyRow, revokeKeyRow, getKeyById } from './keys'\nimport { fullScope } from '../lib/auth'\n\nasync function getChatLinkKeyId(identity: string): Promise<string | null> {\n await ensureSchema()\n const res = await query<{ chat_link_key_id: string | null }>(\n `SELECT chat_link_key_id FROM mem_settings WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n return res.rows[0]?.chat_link_key_id ?? null\n}\n\nasync function setChatLinkKeyId(identity: string, keyId: string | null): Promise<void> {\n await ensureSchema()\n await query(\n `INSERT INTO mem_settings (identity, chat_link_key_id, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (identity) DO UPDATE SET chat_link_key_id = EXCLUDED.chat_link_key_id, updated_at = now()`,\n [identity, keyId],\n )\n}\n\nexport async function getOrCreateChatLinkSecret(identity: string): Promise<string> {\n const existingId = await getChatLinkKeyId(identity)\n if (existingId) {\n const existing = await getKeyById(existingId)\n if (existing && !existing.revoked) {\n throw new ChatLinkSecretNotRetrievable()\n }\n }\n const issued = await issueKeyRow({ identity, vaults: [], scope: fullScope(), plan: 'free', expiresAt: null })\n await setChatLinkKeyId(identity, issued.keyId)\n return issued.secret\n}\n\nexport class ChatLinkSecretNotRetrievable extends Error {\n constructor() {\n super('A chat link already exists for this identity; its secret cannot be re-displayed (only revoked and reissued).')\n }\n}\n\nexport async function revokeChatLink(identity: string): Promise<boolean> {\n const existingId = await getChatLinkKeyId(identity)\n if (!existingId) return false\n await revokeKeyRow(existingId)\n await setChatLinkKeyId(identity, null)\n return true\n}\n"],"mappings":"AAAA,SAAS,cAAc,aAAa;AACpC,SAAS,aAAa,cAAc,kBAAkB;AACtD,SAAS,iBAAiB;AAE1B,eAAe,iBAAiB,UAA0C;AACxE,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AACA,SAAO,IAAI,KAAK,CAAC,GAAG,oBAAoB;AAC1C;AAEA,eAAe,iBAAiB,UAAkB,OAAqC;AACrF,QAAM,aAAa;AACnB,QAAM;AAAA,IACJ;AAAA;AAAA;AAAA,IAGA,CAAC,UAAU,KAAK;AAAA,EAClB;AACF;AAEA,eAAsB,0BAA0B,UAAmC;AACjF,QAAM,aAAa,MAAM,iBAAiB,QAAQ;AAClD,MAAI,YAAY;AACd,UAAM,WAAW,MAAM,WAAW,UAAU;AAC5C,QAAI,YAAY,CAAC,SAAS,SAAS;AACjC,YAAM,IAAI,6BAA6B;AAAA,IACzC;AAAA,EACF;AACA,QAAM,SAAS,MAAM,YAAY,EAAE,UAAU,QAAQ,CAAC,GAAG,OAAO,UAAU,GAAG,MAAM,QAAQ,WAAW,KAAK,CAAC;AAC5G,QAAM,iBAAiB,UAAU,OAAO,KAAK;AAC7C,SAAO,OAAO;AAChB;AAEO,MAAM,qCAAqC,MAAM;AAAA,EACtD,cAAc;AACZ,UAAM,8GAA8G;AAAA,EACtH;AACF;AAEA,eAAsB,eAAe,UAAoC;AACvE,QAAM,aAAa,MAAM,iBAAiB,QAAQ;AAClD,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,aAAa,UAAU;AAC7B,QAAM,iBAAiB,UAAU,IAAI;AACrC,SAAO;AACT;","names":[]}
@@ -0,0 +1,60 @@
1
+ type FactStatus = 'active' | 'superseded';
2
+ type PrecedencePolicy = 'recency' | 'source-priority' | 'intent-aware';
3
+ interface Fact {
4
+ id: string;
5
+ vault: string;
6
+ subject: string;
7
+ value: string;
8
+ source: string;
9
+ confidence: number;
10
+ status: FactStatus;
11
+ supersededBy: string | null;
12
+ resolution: string | null;
13
+ precedence: string | null;
14
+ identity: string;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ }
18
+ declare function canonicalSubject(subject: string): string;
19
+ declare function factId(vault: string, subject: string, createdAtIso: string): string;
20
+ declare function sourceRank(source: string): number;
21
+ interface PrecedenceResult {
22
+ winner: Fact;
23
+ losers: Fact[];
24
+ reason: string;
25
+ ambiguous: boolean;
26
+ }
27
+ declare function applyPrecedence(facts: Fact[], policy: PrecedencePolicy): PrecedenceResult;
28
+ declare function vaultPrecedenceDefault(vault: string): Promise<PrecedencePolicy>;
29
+ interface RecordFactArgs {
30
+ vault: string;
31
+ subject: string;
32
+ value: string;
33
+ source?: string;
34
+ confidence?: number;
35
+ identity: string;
36
+ reason?: string;
37
+ precedence?: PrecedencePolicy;
38
+ }
39
+ declare function recordFact(args: RecordFactArgs): Promise<{
40
+ fact: Fact;
41
+ superseded?: Fact;
42
+ }>;
43
+ declare function factHistory(vault: string, subject: string): Promise<{
44
+ current: Fact | null;
45
+ history: Fact[];
46
+ }>;
47
+ declare function activeFacts(vault: string): Promise<Fact[]>;
48
+ declare function contradictions(vault: string): Promise<{
49
+ subject: string;
50
+ facts: Fact[];
51
+ }[]>;
52
+ declare function supersedeFact(args: {
53
+ loserId: string;
54
+ winnerId: string;
55
+ reason: string;
56
+ precedence: PrecedencePolicy;
57
+ }): Promise<Fact | null>;
58
+ declare function deleteFactsForVault(vault: string): Promise<number>;
59
+
60
+ export { type Fact, type FactStatus, type PrecedencePolicy, type PrecedenceResult, type RecordFactArgs, activeFacts, applyPrecedence, canonicalSubject, contradictions, deleteFactsForVault, factHistory, factId, recordFact, sourceRank, supersedeFact, vaultPrecedenceDefault };
@@ -0,0 +1,186 @@
1
+ import { createHash } from "node:crypto";
2
+ import { ensureSchema, query } from "./pool";
3
+ function mapFact(r) {
4
+ return {
5
+ id: r.id,
6
+ vault: r.vault,
7
+ subject: r.subject,
8
+ value: r.value,
9
+ source: r.source,
10
+ confidence: Number(r.confidence),
11
+ status: r.status,
12
+ supersededBy: r.superseded_by,
13
+ resolution: r.resolution,
14
+ precedence: r.precedence,
15
+ identity: r.identity,
16
+ createdAt: r.created_at,
17
+ updatedAt: r.updated_at
18
+ };
19
+ }
20
+ const FACT_COLS = "id, vault, subject, value, source, confidence, status, superseded_by, resolution, precedence, identity, created_at, updated_at";
21
+ function canonicalSubject(subject) {
22
+ return subject.trim().toLowerCase().replace(/\s+/g, " ");
23
+ }
24
+ function factId(vault, subject, createdAtIso) {
25
+ return `fact_${createHash("sha256").update(`${vault}::${canonicalSubject(subject)}::${createdAtIso}`).digest("hex").slice(0, 24)}`;
26
+ }
27
+ function sourceRank(source) {
28
+ const s = (source || "").toLowerCase();
29
+ if (s.startsWith("user")) return 4;
30
+ if (s.startsWith("tool")) return 3;
31
+ if (s.startsWith("chat")) return 2;
32
+ if (s.startsWith("library")) return 1;
33
+ return 0;
34
+ }
35
+ function applyPrecedence(facts, policy) {
36
+ if (facts.length === 0) {
37
+ throw new Error("applyPrecedence requires at least one fact");
38
+ }
39
+ const byRecency = (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt);
40
+ const sorted = [...facts];
41
+ let winner;
42
+ let reason;
43
+ let ambiguous = false;
44
+ if (policy === "source-priority") {
45
+ sorted.sort((a, b) => {
46
+ const r = sourceRank(b.source) - sourceRank(a.source);
47
+ return r !== 0 ? r : byRecency(a, b);
48
+ });
49
+ winner = sorted[0];
50
+ reason = `source-priority: kept "${winner.source || "unknown"}" (rank ${sourceRank(winner.source)}), most recent on tie`;
51
+ if (sorted.length > 1 && sourceRank(sorted[1].source) === sourceRank(winner.source) && Date.parse(sorted[1].createdAt) === Date.parse(winner.createdAt)) {
52
+ ambiguous = true;
53
+ }
54
+ } else {
55
+ sorted.sort(byRecency);
56
+ winner = sorted[0];
57
+ reason = policy === "intent-aware" ? "intent-aware: newest assertion kept (v2: recency; flagged for review when history may be referenced intentionally)" : "recency: newest assertion kept";
58
+ if (policy === "intent-aware" && sorted.length > 1) {
59
+ ambiguous = true;
60
+ }
61
+ if (sorted.length > 1 && Date.parse(sorted[1].createdAt) === Date.parse(winner.createdAt)) {
62
+ ambiguous = true;
63
+ }
64
+ }
65
+ const losers = sorted.filter((f) => f.id !== winner.id);
66
+ return { winner, losers, reason, ambiguous };
67
+ }
68
+ async function vaultPrecedenceDefault(vault) {
69
+ await ensureSchema();
70
+ const res = await query(
71
+ `SELECT precedence_default FROM mem_vault_registry WHERE vault = $1 AND precedence_default IS NOT NULL LIMIT 1`,
72
+ [vault]
73
+ );
74
+ const p = res.rows[0]?.precedence_default;
75
+ if (p === "recency" || p === "source-priority" || p === "intent-aware") return p;
76
+ return "recency";
77
+ }
78
+ async function recordFact(args) {
79
+ await ensureSchema();
80
+ const subject = canonicalSubject(args.subject);
81
+ const value = args.value;
82
+ const source = args.source ?? "";
83
+ const confidence = typeof args.confidence === "number" ? args.confidence : 0.5;
84
+ const precedence = args.precedence ?? "recency";
85
+ const existingRes = await query(
86
+ `SELECT ${FACT_COLS} FROM mem_facts
87
+ WHERE vault = $1 AND subject = $2 AND status = 'active' AND identity = $3
88
+ ORDER BY created_at DESC LIMIT 1`,
89
+ [args.vault, subject, args.identity]
90
+ );
91
+ const existing = existingRes.rows[0] ? mapFact(existingRes.rows[0]) : null;
92
+ if (existing && existing.value === value) {
93
+ const bumped = await query(
94
+ `UPDATE mem_facts SET updated_at = now(), confidence = $2 WHERE id = $1 RETURNING ${FACT_COLS}`,
95
+ [existing.id, confidence]
96
+ );
97
+ return { fact: mapFact(bumped.rows[0]) };
98
+ }
99
+ const createdAtIso = (/* @__PURE__ */ new Date()).toISOString();
100
+ let id = factId(args.vault, subject, createdAtIso);
101
+ const clash = await query(`SELECT 1 FROM mem_facts WHERE id = $1 LIMIT 1`, [id]);
102
+ if ((clash.rowCount ?? 0) > 0) {
103
+ id = `${id}_${Math.random().toString(36).slice(2, 8)}`;
104
+ }
105
+ let superseded;
106
+ if (existing) {
107
+ const sup = await query(
108
+ `UPDATE mem_facts
109
+ SET status = 'superseded', superseded_by = $2, resolution = $3, precedence = $4, updated_at = now()
110
+ WHERE id = $1 RETURNING ${FACT_COLS}`,
111
+ [existing.id, id, args.reason ?? "replaced by newer assertion", precedence]
112
+ );
113
+ superseded = mapFact(sup.rows[0]);
114
+ }
115
+ const inserted = await query(
116
+ `INSERT INTO mem_facts (id, vault, subject, value, source, confidence, status, identity, created_at, updated_at)
117
+ VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, $8::timestamptz, $8::timestamptz)
118
+ RETURNING ${FACT_COLS}`,
119
+ [id, args.vault, subject, value, source, confidence, args.identity, createdAtIso]
120
+ );
121
+ return { fact: mapFact(inserted.rows[0]), superseded };
122
+ }
123
+ async function factHistory(vault, subject) {
124
+ await ensureSchema();
125
+ const subj = canonicalSubject(subject);
126
+ const res = await query(
127
+ `SELECT ${FACT_COLS} FROM mem_facts WHERE vault = $1 AND subject = $2 ORDER BY created_at DESC`,
128
+ [vault, subj]
129
+ );
130
+ const all = res.rows.map(mapFact);
131
+ const current = all.find((f) => f.status === "active") ?? null;
132
+ const history = all.filter((f) => f.status === "superseded");
133
+ return { current, history };
134
+ }
135
+ async function activeFacts(vault) {
136
+ await ensureSchema();
137
+ const res = await query(
138
+ `SELECT ${FACT_COLS} FROM mem_facts WHERE vault = $1 AND status = 'active' ORDER BY subject ASC, created_at DESC`,
139
+ [vault]
140
+ );
141
+ return res.rows.map(mapFact);
142
+ }
143
+ async function contradictions(vault) {
144
+ const active = await activeFacts(vault);
145
+ const bySubject = /* @__PURE__ */ new Map();
146
+ for (const f of active) {
147
+ const arr = bySubject.get(f.subject) ?? [];
148
+ arr.push(f);
149
+ bySubject.set(f.subject, arr);
150
+ }
151
+ const out = [];
152
+ for (const [subject, facts] of bySubject) {
153
+ if (facts.length > 1) out.push({ subject, facts });
154
+ }
155
+ return out;
156
+ }
157
+ async function supersedeFact(args) {
158
+ await ensureSchema();
159
+ const res = await query(
160
+ `UPDATE mem_facts
161
+ SET status = 'superseded', superseded_by = $2, resolution = $3, precedence = $4, updated_at = now()
162
+ WHERE id = $1 AND status = 'active'
163
+ RETURNING ${FACT_COLS}`,
164
+ [args.loserId, args.winnerId, args.reason, args.precedence]
165
+ );
166
+ return res.rows[0] ? mapFact(res.rows[0]) : null;
167
+ }
168
+ async function deleteFactsForVault(vault) {
169
+ await ensureSchema();
170
+ const res = await query(`DELETE FROM mem_facts WHERE vault = $1`, [vault]);
171
+ return res.rowCount ?? 0;
172
+ }
173
+ export {
174
+ activeFacts,
175
+ applyPrecedence,
176
+ canonicalSubject,
177
+ contradictions,
178
+ deleteFactsForVault,
179
+ factHistory,
180
+ factId,
181
+ recordFact,
182
+ sourceRank,
183
+ supersedeFact,
184
+ vaultPrecedenceDefault
185
+ };
186
+ //# sourceMappingURL=facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/facts.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { ensureSchema, query } from './pool'\n\nexport type FactStatus = 'active' | 'superseded'\nexport type PrecedencePolicy = 'recency' | 'source-priority' | 'intent-aware'\n\nexport interface Fact {\n id: string\n vault: string\n subject: string\n value: string\n source: string\n confidence: number\n status: FactStatus\n supersededBy: string | null\n resolution: string | null\n precedence: string | null\n identity: string\n createdAt: string\n updatedAt: string\n}\n\ninterface RawFactRow {\n id: string\n vault: string\n subject: string\n value: string\n source: string\n confidence: number\n status: string\n superseded_by: string | null\n resolution: string | null\n precedence: string | null\n identity: string\n created_at: string\n updated_at: string\n}\n\nfunction mapFact(r: RawFactRow): Fact {\n return {\n id: r.id,\n vault: r.vault,\n subject: r.subject,\n value: r.value,\n source: r.source,\n confidence: Number(r.confidence),\n status: r.status as FactStatus,\n supersededBy: r.superseded_by,\n resolution: r.resolution,\n precedence: r.precedence,\n identity: r.identity,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n }\n}\n\nconst FACT_COLS =\n 'id, vault, subject, value, source, confidence, status, superseded_by, resolution, precedence, identity, created_at, updated_at'\n\nexport function canonicalSubject(subject: string): string {\n return subject.trim().toLowerCase().replace(/\\s+/g, ' ')\n}\n\nexport function factId(vault: string, subject: string, createdAtIso: string): string {\n return `fact_${createHash('sha256').update(`${vault}::${canonicalSubject(subject)}::${createdAtIso}`).digest('hex').slice(0, 24)}`\n}\n\nexport function sourceRank(source: string): number {\n const s = (source || '').toLowerCase()\n if (s.startsWith('user')) return 4\n if (s.startsWith('tool')) return 3\n if (s.startsWith('chat')) return 2\n if (s.startsWith('library')) return 1\n return 0\n}\n\nexport interface PrecedenceResult {\n winner: Fact\n losers: Fact[]\n reason: string\n ambiguous: boolean\n}\n\nexport function applyPrecedence(facts: Fact[], policy: PrecedencePolicy): PrecedenceResult {\n if (facts.length === 0) {\n throw new Error('applyPrecedence requires at least one fact')\n }\n const byRecency = (a: Fact, b: Fact): number => Date.parse(b.createdAt) - Date.parse(a.createdAt)\n const sorted = [...facts]\n let winner: Fact\n let reason: string\n let ambiguous = false\n\n if (policy === 'source-priority') {\n sorted.sort((a, b) => {\n const r = sourceRank(b.source) - sourceRank(a.source)\n return r !== 0 ? r : byRecency(a, b)\n })\n winner = sorted[0]\n reason = `source-priority: kept \"${winner.source || 'unknown'}\" (rank ${sourceRank(winner.source)}), most recent on tie`\n if (\n sorted.length > 1 &&\n sourceRank(sorted[1].source) === sourceRank(winner.source) &&\n Date.parse(sorted[1].createdAt) === Date.parse(winner.createdAt)\n ) {\n ambiguous = true\n }\n } else {\n sorted.sort(byRecency)\n winner = sorted[0]\n reason =\n policy === 'intent-aware'\n ? 'intent-aware: newest assertion kept (v2: recency; flagged for review when history may be referenced intentionally)'\n : 'recency: newest assertion kept'\n if (policy === 'intent-aware' && sorted.length > 1) {\n ambiguous = true\n }\n if (sorted.length > 1 && Date.parse(sorted[1].createdAt) === Date.parse(winner.createdAt)) {\n ambiguous = true\n }\n }\n\n const losers = sorted.filter(f => f.id !== winner.id)\n return { winner, losers, reason, ambiguous }\n}\n\nexport async function vaultPrecedenceDefault(vault: string): Promise<PrecedencePolicy> {\n await ensureSchema()\n const res = await query<{ precedence_default: string | null }>(\n `SELECT precedence_default FROM mem_vault_registry WHERE vault = $1 AND precedence_default IS NOT NULL LIMIT 1`,\n [vault],\n )\n const p = res.rows[0]?.precedence_default\n if (p === 'recency' || p === 'source-priority' || p === 'intent-aware') return p\n return 'recency'\n}\n\nexport interface RecordFactArgs {\n vault: string\n subject: string\n value: string\n source?: string\n confidence?: number\n identity: string\n reason?: string\n precedence?: PrecedencePolicy\n}\n\nexport async function recordFact(args: RecordFactArgs): Promise<{ fact: Fact; superseded?: Fact }> {\n await ensureSchema()\n const subject = canonicalSubject(args.subject)\n const value = args.value\n const source = args.source ?? ''\n const confidence = typeof args.confidence === 'number' ? args.confidence : 0.5\n const precedence = args.precedence ?? 'recency'\n\n const existingRes = await query<RawFactRow>(\n `SELECT ${FACT_COLS} FROM mem_facts\n WHERE vault = $1 AND subject = $2 AND status = 'active' AND identity = $3\n ORDER BY created_at DESC LIMIT 1`,\n [args.vault, subject, args.identity],\n )\n const existing = existingRes.rows[0] ? mapFact(existingRes.rows[0]) : null\n\n if (existing && existing.value === value) {\n const bumped = await query<RawFactRow>(\n `UPDATE mem_facts SET updated_at = now(), confidence = $2 WHERE id = $1 RETURNING ${FACT_COLS}`,\n [existing.id, confidence],\n )\n return { fact: mapFact(bumped.rows[0]) }\n }\n\n const createdAtIso = new Date().toISOString()\n let id = factId(args.vault, subject, createdAtIso)\n const clash = await query(`SELECT 1 FROM mem_facts WHERE id = $1 LIMIT 1`, [id])\n if ((clash.rowCount ?? 0) > 0) {\n id = `${id}_${Math.random().toString(36).slice(2, 8)}`\n }\n\n let superseded: Fact | undefined\n if (existing) {\n const sup = await query<RawFactRow>(\n `UPDATE mem_facts\n SET status = 'superseded', superseded_by = $2, resolution = $3, precedence = $4, updated_at = now()\n WHERE id = $1 RETURNING ${FACT_COLS}`,\n [existing.id, id, args.reason ?? 'replaced by newer assertion', precedence],\n )\n superseded = mapFact(sup.rows[0])\n }\n\n const inserted = await query<RawFactRow>(\n `INSERT INTO mem_facts (id, vault, subject, value, source, confidence, status, identity, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, $8::timestamptz, $8::timestamptz)\n RETURNING ${FACT_COLS}`,\n [id, args.vault, subject, value, source, confidence, args.identity, createdAtIso],\n )\n\n return { fact: mapFact(inserted.rows[0]), superseded }\n}\n\nexport async function factHistory(\n vault: string,\n subject: string,\n): Promise<{ current: Fact | null; history: Fact[] }> {\n await ensureSchema()\n const subj = canonicalSubject(subject)\n const res = await query<RawFactRow>(\n `SELECT ${FACT_COLS} FROM mem_facts WHERE vault = $1 AND subject = $2 ORDER BY created_at DESC`,\n [vault, subj],\n )\n const all = res.rows.map(mapFact)\n const current = all.find(f => f.status === 'active') ?? null\n const history = all.filter(f => f.status === 'superseded')\n return { current, history }\n}\n\nexport async function activeFacts(vault: string): Promise<Fact[]> {\n await ensureSchema()\n const res = await query<RawFactRow>(\n `SELECT ${FACT_COLS} FROM mem_facts WHERE vault = $1 AND status = 'active' ORDER BY subject ASC, created_at DESC`,\n [vault],\n )\n return res.rows.map(mapFact)\n}\n\nexport async function contradictions(vault: string): Promise<{ subject: string; facts: Fact[] }[]> {\n const active = await activeFacts(vault)\n const bySubject = new Map<string, Fact[]>()\n for (const f of active) {\n const arr = bySubject.get(f.subject) ?? []\n arr.push(f)\n bySubject.set(f.subject, arr)\n }\n const out: { subject: string; facts: Fact[] }[] = []\n for (const [subject, facts] of bySubject) {\n if (facts.length > 1) out.push({ subject, facts })\n }\n return out\n}\n\nexport async function supersedeFact(args: {\n loserId: string\n winnerId: string\n reason: string\n precedence: PrecedencePolicy\n}): Promise<Fact | null> {\n await ensureSchema()\n const res = await query<RawFactRow>(\n `UPDATE mem_facts\n SET status = 'superseded', superseded_by = $2, resolution = $3, precedence = $4, updated_at = now()\n WHERE id = $1 AND status = 'active'\n RETURNING ${FACT_COLS}`,\n [args.loserId, args.winnerId, args.reason, args.precedence],\n )\n return res.rows[0] ? mapFact(res.rows[0]) : null\n}\n\nexport async function deleteFactsForVault(vault: string): Promise<number> {\n await ensureSchema()\n const res = await query(`DELETE FROM mem_facts WHERE vault = $1`, [vault])\n return res.rowCount ?? 0\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,34 @@
1
+ type InterlinkPhase = 'orphans' | 'springboard' | 'walkthrough';
2
+ interface InterlinkCounts {
3
+ orphansFound: number;
4
+ orphansLinked: number;
5
+ springboardLinks: number;
6
+ contractLinks: number;
7
+ hubsCreated: number;
8
+ removals: number;
9
+ budgetRemaining: number;
10
+ }
11
+ interface InterlinkCursor {
12
+ phase: InterlinkPhase;
13
+ vaultIndex: number;
14
+ noteOffset: number;
15
+ edgeIndex?: number;
16
+ runDate: string;
17
+ counts?: InterlinkCounts;
18
+ }
19
+ interface InterlinkStateRecord {
20
+ identity: string;
21
+ cursor: InterlinkCursor | null;
22
+ lastRunAt: string | null;
23
+ lastSummary: string | null;
24
+ }
25
+ declare function getInterlinkState(identity: string): Promise<InterlinkStateRecord | null>;
26
+ declare function upsertInterlinkState(args: {
27
+ identity: string;
28
+ cursor?: InterlinkCursor | null;
29
+ lastRunAt?: string;
30
+ lastSummary?: string;
31
+ }): Promise<void>;
32
+ declare function clearInterlinkCursor(identity: string): Promise<void>;
33
+
34
+ export { type InterlinkCounts, type InterlinkCursor, type InterlinkPhase, type InterlinkStateRecord, clearInterlinkCursor, getInterlinkState, upsertInterlinkState };
@@ -0,0 +1,42 @@
1
+ import { ensureSchema, query } from "./pool";
2
+ function mapRow(r) {
3
+ return {
4
+ identity: r.identity,
5
+ cursor: r.cursor && typeof r.cursor === "object" ? r.cursor : null,
6
+ lastRunAt: r.last_run_at,
7
+ lastSummary: r.last_summary
8
+ };
9
+ }
10
+ async function getInterlinkState(identity) {
11
+ await ensureSchema();
12
+ const res = await query(
13
+ `SELECT identity, cursor, last_run_at, last_summary FROM mem_interlink_state WHERE identity = $1 LIMIT 1`,
14
+ [identity]
15
+ );
16
+ return res.rows[0] ? mapRow(res.rows[0]) : null;
17
+ }
18
+ async function upsertInterlinkState(args) {
19
+ await ensureSchema();
20
+ const touchesCursor = "cursor" in args;
21
+ const cursorJson = touchesCursor && args.cursor ? JSON.stringify(args.cursor) : null;
22
+ await query(
23
+ `INSERT INTO mem_interlink_state (identity, cursor, last_run_at, last_summary, updated_at)
24
+ VALUES ($1, $2, $3, $4, now())
25
+ ON CONFLICT (identity) DO UPDATE SET
26
+ cursor = CASE WHEN $5 THEN $2::jsonb ELSE mem_interlink_state.cursor END,
27
+ last_run_at = COALESCE($3, mem_interlink_state.last_run_at),
28
+ last_summary = COALESCE($4, mem_interlink_state.last_summary),
29
+ updated_at = now()`,
30
+ [args.identity, cursorJson, args.lastRunAt ?? null, args.lastSummary ?? null, touchesCursor]
31
+ );
32
+ }
33
+ async function clearInterlinkCursor(identity) {
34
+ await ensureSchema();
35
+ await query(`UPDATE mem_interlink_state SET cursor = NULL, updated_at = now() WHERE identity = $1`, [identity]);
36
+ }
37
+ export {
38
+ clearInterlinkCursor,
39
+ getInterlinkState,
40
+ upsertInterlinkState
41
+ };
42
+ //# sourceMappingURL=interlink-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/interlink-state.ts"],"sourcesContent":["import { ensureSchema, query } from './pool'\n\nexport type InterlinkPhase = 'orphans' | 'springboard' | 'walkthrough'\n\nexport interface InterlinkCounts {\n orphansFound: number\n orphansLinked: number\n springboardLinks: number\n contractLinks: number\n hubsCreated: number\n removals: number\n budgetRemaining: number\n}\n\nexport interface InterlinkCursor {\n phase: InterlinkPhase\n vaultIndex: number\n noteOffset: number\n edgeIndex?: number\n runDate: string\n counts?: InterlinkCounts\n}\n\nexport interface InterlinkStateRecord {\n identity: string\n cursor: InterlinkCursor | null\n lastRunAt: string | null\n lastSummary: string | null\n}\n\ninterface RawRow {\n identity: string\n cursor: unknown\n last_run_at: string | null\n last_summary: string | null\n}\n\nfunction mapRow(r: RawRow): InterlinkStateRecord {\n return {\n identity: r.identity,\n cursor: (r.cursor && typeof r.cursor === 'object' ? (r.cursor as InterlinkCursor) : null),\n lastRunAt: r.last_run_at,\n lastSummary: r.last_summary,\n }\n}\n\nexport async function getInterlinkState(identity: string): Promise<InterlinkStateRecord | null> {\n await ensureSchema()\n const res = await query<RawRow>(\n `SELECT identity, cursor, last_run_at, last_summary FROM mem_interlink_state WHERE identity = $1 LIMIT 1`,\n [identity],\n )\n return res.rows[0] ? mapRow(res.rows[0]) : null\n}\n\nexport async function upsertInterlinkState(args: {\n identity: string\n cursor?: InterlinkCursor | null\n lastRunAt?: string\n lastSummary?: string\n}): Promise<void> {\n await ensureSchema()\n const touchesCursor = 'cursor' in args\n const cursorJson = touchesCursor && args.cursor ? JSON.stringify(args.cursor) : null\n await query(\n `INSERT INTO mem_interlink_state (identity, cursor, last_run_at, last_summary, updated_at)\n VALUES ($1, $2, $3, $4, now())\n ON CONFLICT (identity) DO UPDATE SET\n cursor = CASE WHEN $5 THEN $2::jsonb ELSE mem_interlink_state.cursor END,\n last_run_at = COALESCE($3, mem_interlink_state.last_run_at),\n last_summary = COALESCE($4, mem_interlink_state.last_summary),\n updated_at = now()`,\n [args.identity, cursorJson, args.lastRunAt ?? null, args.lastSummary ?? null, touchesCursor],\n )\n}\n\nexport async function clearInterlinkCursor(identity: string): Promise<void> {\n await ensureSchema()\n await query(`UPDATE mem_interlink_state SET cursor = NULL, updated_at = now() WHERE identity = $1`, [identity])\n}\n"],"mappings":"AAAA,SAAS,cAAc,aAAa;AAqCpC,SAAS,OAAO,GAAiC;AAC/C,SAAO;AAAA,IACL,UAAU,EAAE;AAAA,IACZ,QAAS,EAAE,UAAU,OAAO,EAAE,WAAW,WAAY,EAAE,SAA6B;AAAA,IACpF,WAAW,EAAE;AAAA,IACb,aAAa,EAAE;AAAA,EACjB;AACF;AAEA,eAAsB,kBAAkB,UAAwD;AAC9F,QAAM,aAAa;AACnB,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AACA,SAAO,IAAI,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC,IAAI;AAC7C;AAEA,eAAsB,qBAAqB,MAKzB;AAChB,QAAM,aAAa;AACnB,QAAM,gBAAgB,YAAY;AAClC,QAAM,aAAa,iBAAiB,KAAK,SAAS,KAAK,UAAU,KAAK,MAAM,IAAI;AAChF,QAAM;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,CAAC,KAAK,UAAU,YAAY,KAAK,aAAa,MAAM,KAAK,eAAe,MAAM,aAAa;AAAA,EAC7F;AACF;AAEA,eAAsB,qBAAqB,UAAiC;AAC1E,QAAM,aAAa;AACnB,QAAM,MAAM,wFAAwF,CAAC,QAAQ,CAAC;AAChH;","names":[]}
@@ -0,0 +1,44 @@
1
+ import { Scope } from '../lib/auth.js';
2
+
3
+ declare const PLAN_QUOTA_BYTES: Record<string, number>;
4
+ declare function planQuotaBytes(plan: string): number;
5
+ declare function isUnlimitedPlan(plan: string): boolean;
6
+ interface IssueKeyArgs {
7
+ identity: string;
8
+ vaults: string[];
9
+ scope: Scope;
10
+ plan: string;
11
+ expiresAt: string | null;
12
+ }
13
+ interface IssuedKey {
14
+ keyId: string;
15
+ secret: string;
16
+ identity: string;
17
+ vaults: string[];
18
+ scope: Scope;
19
+ plan: string;
20
+ expiresAt: string | null;
21
+ }
22
+ declare function issueKeyRow(args: IssueKeyArgs): Promise<IssuedKey>;
23
+ interface KeySummary {
24
+ keyId: string;
25
+ identity: string;
26
+ vaults: string[];
27
+ scope: Scope;
28
+ plan: string;
29
+ expiresAt: string | null;
30
+ revoked: boolean;
31
+ usageCount: number;
32
+ lastUsedAt: string | null;
33
+ }
34
+ declare function listKeyRows(filter: {
35
+ identity?: string;
36
+ vault?: string;
37
+ plan?: string;
38
+ }): Promise<KeySummary[]>;
39
+ declare function revokeKeyRow(keyId: string): Promise<boolean>;
40
+ declare function setScopeRow(keyId: string, scope: Scope | undefined, plan: string | undefined): Promise<boolean>;
41
+ declare function planForIdentity(identity: string): Promise<string>;
42
+ declare function getKeyById(keyId: string): Promise<KeySummary | null>;
43
+
44
+ export { type IssueKeyArgs, type IssuedKey, type KeySummary, PLAN_QUOTA_BYTES, getKeyById, isUnlimitedPlan, issueKeyRow, listKeyRows, planForIdentity, planQuotaBytes, revokeKeyRow, setScopeRow };
@@ -0,0 +1,118 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { ensureSchema, query } from "./pool";
3
+ import { generateSecret, hashSecret, normalizeScope } from "../lib/auth";
4
+ const PLAN_QUOTA_BYTES = {
5
+ free: 1e7,
6
+ pro: 5e9,
7
+ team: 5e10,
8
+ enterprise: 5e11,
9
+ unlimited: Number.MAX_SAFE_INTEGER
10
+ };
11
+ function planQuotaBytes(plan) {
12
+ return PLAN_QUOTA_BYTES[plan] ?? PLAN_QUOTA_BYTES.free;
13
+ }
14
+ function isUnlimitedPlan(plan) {
15
+ return plan === "unlimited";
16
+ }
17
+ async function issueKeyRow(args) {
18
+ await ensureSchema();
19
+ const keyId = `key_${randomBytes(8).toString("hex")}`;
20
+ const secret = generateSecret();
21
+ await query(
22
+ `INSERT INTO mem_keys (key_id, secret_hash, identity, vaults, scope, plan, expires_at)
23
+ VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, $6, $7)`,
24
+ [keyId, hashSecret(secret), args.identity, JSON.stringify(args.vaults), JSON.stringify(args.scope), args.plan, args.expiresAt]
25
+ );
26
+ return { keyId, secret, identity: args.identity, vaults: args.vaults, scope: args.scope, plan: args.plan, expiresAt: args.expiresAt };
27
+ }
28
+ function toArr(v) {
29
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
30
+ }
31
+ function mapKey(row) {
32
+ return {
33
+ keyId: row.key_id,
34
+ identity: row.identity,
35
+ vaults: toArr(row.vaults),
36
+ scope: normalizeScope(row.scope),
37
+ plan: row.plan,
38
+ expiresAt: row.expires_at,
39
+ revoked: row.revoked,
40
+ usageCount: row.usage_count,
41
+ lastUsedAt: row.last_used_at
42
+ };
43
+ }
44
+ async function listKeyRows(filter) {
45
+ await ensureSchema();
46
+ const clauses = [];
47
+ const params = [];
48
+ if (filter.identity) {
49
+ params.push(filter.identity);
50
+ clauses.push(`identity = $${params.length}`);
51
+ }
52
+ if (filter.plan) {
53
+ params.push(filter.plan);
54
+ clauses.push(`plan = $${params.length}`);
55
+ }
56
+ if (filter.vault) {
57
+ params.push(JSON.stringify([filter.vault]));
58
+ clauses.push(`vaults @> $${params.length}::jsonb`);
59
+ }
60
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
61
+ const res = await query(
62
+ `SELECT key_id, identity, vaults, scope, plan, expires_at, revoked, usage_count, last_used_at
63
+ FROM mem_keys ${where} ORDER BY created_at DESC`,
64
+ params
65
+ );
66
+ return res.rows.map(mapKey);
67
+ }
68
+ async function revokeKeyRow(keyId) {
69
+ await ensureSchema();
70
+ const res = await query("UPDATE mem_keys SET revoked = TRUE WHERE key_id = $1", [keyId]);
71
+ return (res.rowCount ?? 0) > 0;
72
+ }
73
+ async function setScopeRow(keyId, scope, plan) {
74
+ await ensureSchema();
75
+ const sets = [];
76
+ const params = [];
77
+ if (scope) {
78
+ params.push(JSON.stringify(scope));
79
+ sets.push(`scope = $${params.length}::jsonb`);
80
+ }
81
+ if (plan) {
82
+ params.push(plan);
83
+ sets.push(`plan = $${params.length}`);
84
+ }
85
+ if (!sets.length) return false;
86
+ params.push(keyId);
87
+ const res = await query(`UPDATE mem_keys SET ${sets.join(", ")} WHERE key_id = $${params.length}`, params);
88
+ return (res.rowCount ?? 0) > 0;
89
+ }
90
+ async function planForIdentity(identity) {
91
+ await ensureSchema();
92
+ const res = await query(
93
+ `SELECT plan FROM mem_keys WHERE identity = $1 ORDER BY created_at DESC LIMIT 1`,
94
+ [identity]
95
+ );
96
+ return res.rows[0]?.plan ?? "free";
97
+ }
98
+ async function getKeyById(keyId) {
99
+ await ensureSchema();
100
+ const res = await query(
101
+ `SELECT key_id, identity, vaults, scope, plan, expires_at, revoked, usage_count, last_used_at
102
+ FROM mem_keys WHERE key_id = $1 LIMIT 1`,
103
+ [keyId]
104
+ );
105
+ return res.rows[0] ? mapKey(res.rows[0]) : null;
106
+ }
107
+ export {
108
+ PLAN_QUOTA_BYTES,
109
+ getKeyById,
110
+ isUnlimitedPlan,
111
+ issueKeyRow,
112
+ listKeyRows,
113
+ planForIdentity,
114
+ planQuotaBytes,
115
+ revokeKeyRow,
116
+ setScopeRow
117
+ };
118
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/keys.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { ensureSchema, query } from './pool'\nimport { generateSecret, hashSecret, normalizeScope } from '../lib/auth'\nimport type { Scope } from '../lib/auth'\n\nexport const PLAN_QUOTA_BYTES: Record<string, number> = {\n free: 10_000_000,\n pro: 5_000_000_000,\n team: 50_000_000_000,\n enterprise: 500_000_000_000,\n unlimited: Number.MAX_SAFE_INTEGER,\n}\n\nexport function planQuotaBytes(plan: string): number {\n return PLAN_QUOTA_BYTES[plan] ?? PLAN_QUOTA_BYTES.free\n}\n\nexport function isUnlimitedPlan(plan: string): boolean {\n return plan === 'unlimited'\n}\n\nexport interface IssueKeyArgs {\n identity: string\n vaults: string[]\n scope: Scope\n plan: string\n expiresAt: string | null\n}\n\nexport interface IssuedKey {\n keyId: string\n secret: string\n identity: string\n vaults: string[]\n scope: Scope\n plan: string\n expiresAt: string | null\n}\n\nexport async function issueKeyRow(args: IssueKeyArgs): Promise<IssuedKey> {\n await ensureSchema()\n const keyId = `key_${randomBytes(8).toString('hex')}`\n const secret = generateSecret()\n await query(\n `INSERT INTO mem_keys (key_id, secret_hash, identity, vaults, scope, plan, expires_at)\n VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, $6, $7)`,\n [keyId, hashSecret(secret), args.identity, JSON.stringify(args.vaults), JSON.stringify(args.scope), args.plan, args.expiresAt],\n )\n return { keyId, secret, identity: args.identity, vaults: args.vaults, scope: args.scope, plan: args.plan, expiresAt: args.expiresAt }\n}\n\nexport interface KeySummary {\n keyId: string\n identity: string\n vaults: string[]\n scope: Scope\n plan: string\n expiresAt: string | null\n revoked: boolean\n usageCount: number\n lastUsedAt: string | null\n}\n\ninterface KeyRow {\n key_id: string\n identity: string\n vaults: unknown\n scope: unknown\n plan: string\n expires_at: string | null\n revoked: boolean\n usage_count: number\n last_used_at: string | null\n}\n\nfunction toArr(v: unknown): string[] {\n return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []\n}\n\nfunction mapKey(row: KeyRow): KeySummary {\n return {\n keyId: row.key_id,\n identity: row.identity,\n vaults: toArr(row.vaults),\n scope: normalizeScope(row.scope as Record<string, boolean>),\n plan: row.plan,\n expiresAt: row.expires_at,\n revoked: row.revoked,\n usageCount: row.usage_count,\n lastUsedAt: row.last_used_at,\n }\n}\n\nexport async function listKeyRows(filter: { identity?: string; vault?: string; plan?: string }): Promise<KeySummary[]> {\n await ensureSchema()\n const clauses: string[] = []\n const params: unknown[] = []\n if (filter.identity) {\n params.push(filter.identity)\n clauses.push(`identity = $${params.length}`)\n }\n if (filter.plan) {\n params.push(filter.plan)\n clauses.push(`plan = $${params.length}`)\n }\n if (filter.vault) {\n params.push(JSON.stringify([filter.vault]))\n clauses.push(`vaults @> $${params.length}::jsonb`)\n }\n const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''\n const res = await query<KeyRow>(\n `SELECT key_id, identity, vaults, scope, plan, expires_at, revoked, usage_count, last_used_at\n FROM mem_keys ${where} ORDER BY created_at DESC`,\n params,\n )\n return res.rows.map(mapKey)\n}\n\nexport async function revokeKeyRow(keyId: string): Promise<boolean> {\n await ensureSchema()\n const res = await query('UPDATE mem_keys SET revoked = TRUE WHERE key_id = $1', [keyId])\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function setScopeRow(keyId: string, scope: Scope | undefined, plan: string | undefined): Promise<boolean> {\n await ensureSchema()\n const sets: string[] = []\n const params: unknown[] = []\n if (scope) {\n params.push(JSON.stringify(scope))\n sets.push(`scope = $${params.length}::jsonb`)\n }\n if (plan) {\n params.push(plan)\n sets.push(`plan = $${params.length}`)\n }\n if (!sets.length) return false\n params.push(keyId)\n const res = await query(`UPDATE mem_keys SET ${sets.join(', ')} WHERE key_id = $${params.length}`, params)\n return (res.rowCount ?? 0) > 0\n}\n\nexport async function planForIdentity(identity: string): Promise<string> {\n await ensureSchema()\n const res = await query<{ plan: string }>(\n `SELECT plan FROM mem_keys WHERE identity = $1 ORDER BY created_at DESC LIMIT 1`,\n [identity],\n )\n return res.rows[0]?.plan ?? 'free'\n}\n\nexport async function getKeyById(keyId: string): Promise<KeySummary | null> {\n await ensureSchema()\n const res = await query<KeyRow>(\n `SELECT key_id, identity, vaults, scope, plan, expires_at, revoked, usage_count, last_used_at\n FROM mem_keys WHERE key_id = $1 LIMIT 1`,\n [keyId],\n )\n return res.rows[0] ? mapKey(res.rows[0]) : null\n}\n"],"mappings":"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","names":[]}