@blamejs/core 0.4.1

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 (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
@@ -0,0 +1,604 @@
1
+ "use strict";
2
+ /**
3
+ * Framework state schema for cluster-mode external storage.
4
+ *
5
+ * When cluster mode is active, the framework's audit chain + consent
6
+ * log + audit checkpoints + audit tip live in the operator's external-db
7
+ * (configured via b.externalDb.init). This module owns the DDL for
8
+ * those tables and exposes a single idempotent ensureSchema() entry
9
+ * point that operators (or the framework's leader-acquire hook in a
10
+ * later release) call to create them.
11
+ *
12
+ * In external-db the framework tables are prefixed with `_blamejs_`
13
+ * to avoid collision with the operator's app data:
14
+ *
15
+ * audit_log local-SQLite name
16
+ * _blamejs_audit_log external-db name
17
+ *
18
+ * The mapping is exposed via tableName(local) so write-dispatch code
19
+ * (next release) can use a single name reference.
20
+ *
21
+ * Dialects: Postgres + SQLite. Both support CREATE TABLE IF NOT EXISTS,
22
+ * CREATE INDEX IF NOT EXISTS, and the same column types modulo
23
+ * INTEGER/BIGINT and BLOB/BYTEA differences. MySQL is not yet
24
+ * supported — operators on MySQL must wait for that adapter.
25
+ *
26
+ * What ensureSchema does NOT do:
27
+ * - Migrate existing audit_log rows from local SQLite into external-db.
28
+ * That migration belongs to a separate operator-driven tool.
29
+ * - Verify chain integrity in external-db. That happens at boot via
30
+ * the audit module's regular verify() path once dispatch lands.
31
+ * - Install append-only triggers. Trigger syntax differs across
32
+ * dialects and is being deferred to the next release.
33
+ *
34
+ * Public API:
35
+ * await frameworkSchema.ensureSchema({ externalDbBackend, dialect })
36
+ * frameworkSchema.tableName(localName) external table name lookup
37
+ * frameworkSchema.LOCAL_TO_EXTERNAL mapping (read-only)
38
+ * frameworkSchema.FrameworkSchemaError error class
39
+ */
40
+
41
+ var externalDb = require("./external-db");
42
+ var { FrameworkError } = require("./framework-error");
43
+
44
+ class FrameworkSchemaError extends FrameworkError {
45
+ constructor(message, code) {
46
+ super(message);
47
+ this.name = "FrameworkSchemaError";
48
+ this.code = code || "framework-schema/invalid";
49
+ this.isFrameworkSchemaError = true;
50
+ }
51
+ }
52
+
53
+ // Local-SQLite name → external-db name. The prefix protects against
54
+ // operator-app-table collision when the framework writes alongside
55
+ // app tables in the same database.
56
+ var LOCAL_TO_EXTERNAL = Object.freeze({
57
+ audit_log: "_blamejs_audit_log",
58
+ consent_log: "_blamejs_consent_log",
59
+ audit_checkpoints: "_blamejs_audit_checkpoints",
60
+ // No local equivalent — only exists in external-db. Coordinates with
61
+ // the cluster module's lease + fencing-token guard.
62
+ _blamejs_audit_tip: "_blamejs_audit_tip",
63
+ // Same shape and purpose as _blamejs_audit_tip but for consent_log.
64
+ // Single-row coordination state recording the tip of the consent
65
+ // chain so a new leader (or any boot) can detect external-db
66
+ // rollback against the consent chain too.
67
+ _blamejs_consent_tip: "_blamejs_consent_tip",
68
+ // Single-row anchor recording the boundary of the most recent
69
+ // audit-tools.purge(). After a purge, audit-chain.verifyChain reads
70
+ // this row to set its starting prevHash to lastPurgedRowHash and skip
71
+ // rows whose monotonicCounter ≤ lastPurgedCounter — without it the
72
+ // chain math breaks the moment the row referenced by survivors'
73
+ // prevHash is gone.
74
+ _blamejs_audit_purge_anchor: "_blamejs_audit_purge_anchor",
75
+ // Scheduler tick-claim table: closes the once-globally gap during
76
+ // cluster leader hand-offs (where two leaders briefly coexist) by
77
+ // making each fire claim a row before dispatching. UNIQUE on the
78
+ // composite tickKey (name + ":" + scheduledAtUnix) — loser of the
79
+ // INSERT race skips the tick.
80
+ _blamejs_scheduler_ticks: "_blamejs_scheduler_ticks",
81
+ // Rate-limit cluster-shared backend storage — fixed-window counter
82
+ // per key. The middleware atomically INSERT...ON CONFLICT increments
83
+ // count within the current window and rolls over when the window
84
+ // advances. Created in cluster mode by ensureSchema; mirrored in
85
+ // single-node SQLite by db.js's FRAMEWORK_SCHEMA so the same SQL
86
+ // works on either side of cluster-storage's dispatch.
87
+ _blamejs_rate_limit_counters: "_blamejs_rate_limit_counters",
88
+ // WebSocket channel-hub cluster fan-out — publish() writes a row,
89
+ // other nodes poll for new ids and dispatch to their local
90
+ // subscribers. Same dual-storage shape as sessions / jobs / etc.
91
+ _blamejs_ws_messages: "_blamejs_ws_messages",
92
+ _blamejs_api_encrypt_nonces: "_blamejs_api_encrypt_nonces",
93
+ // _blamejs_api_keys — operator-facing API-key registry table for the
94
+ // b.apiKey primitive. PRIMARY KEY is namespace-scoped id (so multiple
95
+ // namespaces can coexist in one table). Sealed columns: ownerId,
96
+ // scopes (JSON array), metadata (JSON object). Indexed lookup by
97
+ // ownerIdHash. The secret itself never lands here — only its
98
+ // SHA3-512 hash, constant-time-compared on verify.
99
+ _blamejs_api_keys: "_blamejs_api_keys",
100
+ // _blamejs_sessions exists in both local SQLite (single-node mode,
101
+ // created by db.js's FRAMEWORK_SCHEMA at boot) and external-db
102
+ // (cluster mode, created by ensureSchema below). Same name in both
103
+ // places — cluster-storage.execute routes the SQL to the right DB
104
+ // based on cluster.isClusterMode().
105
+ _blamejs_sessions: "_blamejs_sessions",
106
+ // _blamejs_jobs — same dual-storage pattern as sessions. The local-
107
+ // protocol queue (lib/queue-local.js) routes through cluster-storage
108
+ // so writes/reads land in the leader's external-db when cluster
109
+ // mode is active and any node can observe the queue state.
110
+ _blamejs_jobs: "_blamejs_jobs",
111
+ // _blamejs_cache — operator-facing cache primitive's cluster backend.
112
+ // Single shared table across all CacheInstance instances; the
113
+ // namespace prefix in cacheKey isolates instances. JSON-serialized
114
+ // values, BIGINT expiresAt for ttl. Indexed on expiresAt for the
115
+ // periodic prune query.
116
+ _blamejs_cache: "_blamejs_cache",
117
+ // _blamejs_seeders — registry of applied seed files for b.seeders
118
+ // (lib/seeders.js). Composite PK (env, name) lets the same filename
119
+ // apply per env. Mirrors the local-SQLite shape in db.js
120
+ // FRAMEWORK_SCHEMA so cluster-storage.execute routes to either side.
121
+ _blamejs_seeders: "_blamejs_seeders",
122
+ _blamejs_seeders_lock: "_blamejs_seeders_lock",
123
+ });
124
+
125
+ function tableName(localName) {
126
+ if (Object.prototype.hasOwnProperty.call(LOCAL_TO_EXTERNAL, localName)) {
127
+ return LOCAL_TO_EXTERNAL[localName];
128
+ }
129
+ // For framework-internal tables that are already prefixed locally
130
+ // (any name starting with _blamejs_), keep the same name.
131
+ return localName;
132
+ }
133
+
134
+ // ---- Dialect-specific column types ----
135
+ // TEXT and BOOLEAN are identical across both. INTEGER and BLOB diverge.
136
+
137
+ function _types(dialect) {
138
+ if (dialect === "postgres") {
139
+ return { INT: "BIGINT", BLOB: "BYTEA" };
140
+ }
141
+ if (dialect === "sqlite") {
142
+ return { INT: "INTEGER", BLOB: "BLOB" };
143
+ }
144
+ throw new FrameworkSchemaError(
145
+ "unsupported dialect '" + dialect + "' (postgres or sqlite)",
146
+ "framework-schema/unsupported-dialect"
147
+ );
148
+ }
149
+
150
+ // ---- Table DDL builders ----
151
+ //
152
+ // Each builder returns { create: <CREATE TABLE SQL>, indexes: [<CREATE INDEX SQL>, ...] }.
153
+ // All DDL uses IF NOT EXISTS so re-running is idempotent.
154
+
155
+ function _auditLogDDL(dialect) {
156
+ var t = _types(dialect);
157
+ var name = LOCAL_TO_EXTERNAL.audit_log;
158
+ return {
159
+ create:
160
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
161
+ " _id TEXT PRIMARY KEY," +
162
+ " recordedAt " + t.INT + " NOT NULL," +
163
+ " monotonicCounter " + t.INT + " NOT NULL," +
164
+ " actorUserId TEXT," +
165
+ " actorUserIdHash TEXT," +
166
+ " actorIp TEXT," +
167
+ " actorUserAgent TEXT," +
168
+ " actorSessionId TEXT," +
169
+ " action TEXT NOT NULL," +
170
+ " resourceKind TEXT," +
171
+ " resourceId TEXT," +
172
+ " resourceIdHash TEXT," +
173
+ " outcome TEXT NOT NULL," +
174
+ " reason TEXT," +
175
+ " metadata TEXT," +
176
+ " requestId TEXT," +
177
+ " prevHash TEXT NOT NULL," +
178
+ " rowHash TEXT NOT NULL," +
179
+ " nonce " + t.BLOB + " NOT NULL," +
180
+ " fencingToken " + t.INT + " NOT NULL DEFAULT 0" +
181
+ ")",
182
+ indexes: [
183
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_actorUserIdHash ON " + name + " (actorUserIdHash)",
184
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_resourceIdHash ON " + name + " (resourceIdHash)",
185
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_recordedAt ON " + name + " (recordedAt)",
186
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_action ON " + name + " (action)",
187
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_" + name + "_monotonic ON " + name + " (monotonicCounter)",
188
+ ],
189
+ };
190
+ }
191
+
192
+ function _consentLogDDL(dialect) {
193
+ var t = _types(dialect);
194
+ var name = LOCAL_TO_EXTERNAL.consent_log;
195
+ return {
196
+ create:
197
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
198
+ " _id TEXT PRIMARY KEY," +
199
+ " recordedAt " + t.INT + " NOT NULL," +
200
+ " monotonicCounter " + t.INT + " NOT NULL," +
201
+ " subjectId TEXT NOT NULL," +
202
+ " subjectIdHash TEXT NOT NULL," +
203
+ " purpose TEXT NOT NULL," +
204
+ " lawfulBasis TEXT NOT NULL," +
205
+ " action TEXT NOT NULL," +
206
+ " scope TEXT," +
207
+ " channel TEXT NOT NULL," +
208
+ " evidenceRef TEXT," +
209
+ " prevHash TEXT NOT NULL," +
210
+ " rowHash TEXT NOT NULL," +
211
+ " nonce " + t.BLOB + " NOT NULL," +
212
+ " fencingToken " + t.INT + " NOT NULL DEFAULT 0" +
213
+ ")",
214
+ indexes: [
215
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_subjectIdHash ON " + name + " (subjectIdHash)",
216
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_recordedAt ON " + name + " (recordedAt)",
217
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_purpose ON " + name + " (purpose)",
218
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_" + name + "_monotonic ON " + name + " (monotonicCounter)",
219
+ ],
220
+ };
221
+ }
222
+
223
+ function _auditCheckpointsDDL(dialect) {
224
+ var t = _types(dialect);
225
+ var name = LOCAL_TO_EXTERNAL.audit_checkpoints;
226
+ return {
227
+ create:
228
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
229
+ " _id TEXT PRIMARY KEY," +
230
+ " createdAt " + t.INT + " NOT NULL," +
231
+ " atMonotonicCounter " + t.INT + " NOT NULL," +
232
+ " atRowHash TEXT NOT NULL," +
233
+ " signature " + t.BLOB + " NOT NULL," +
234
+ " publicKeyFingerprint TEXT NOT NULL," +
235
+ " fencingToken " + t.INT + " NOT NULL DEFAULT 0" +
236
+ ")",
237
+ indexes: [
238
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_createdAt ON " + name + " (createdAt)",
239
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_" + name + "_chkpt_counter ON " + name + " (atMonotonicCounter)",
240
+ ],
241
+ };
242
+ }
243
+
244
+ // audit_tip is single-row coordination state for cluster-mode rollback
245
+ // detection. The CHECK constraint on fencingToken is the canonical
246
+ // fencing-token guard from the cluster spec — enforced at the DB
247
+ // level so a partitioned old leader can't insert rows behind a new
248
+ // leader's back regardless of application-layer state.
249
+ //
250
+ // Postgres and SQLite both honour CHECK constraints. The single-row
251
+ // invariant is enforced via PRIMARY KEY on the constant-valued
252
+ // `scope` column.
253
+ function _auditTipDDL(dialect) {
254
+ var t = _types(dialect);
255
+ var name = LOCAL_TO_EXTERNAL._blamejs_audit_tip;
256
+ return {
257
+ create:
258
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
259
+ " scope TEXT PRIMARY KEY," +
260
+ " atMonotonicCounter " + t.INT + " NOT NULL," +
261
+ " rowHash TEXT," +
262
+ " signedAt TEXT," +
263
+ " fencingToken " + t.INT + " NOT NULL DEFAULT 0," +
264
+ " CHECK (scope = 'audit')" +
265
+ ")",
266
+ indexes: [],
267
+ };
268
+ }
269
+
270
+ // Same shape + invariants as audit_tip but for the consent chain.
271
+ // Updated on every consent.grant / consent.withdraw write so the boot-
272
+ // time rollback check can detect external-db rollback against the
273
+ // consent chain (previously only the audit chain had this protection).
274
+ function _consentTipDDL(dialect) {
275
+ var t = _types(dialect);
276
+ var name = LOCAL_TO_EXTERNAL._blamejs_consent_tip;
277
+ return {
278
+ create:
279
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
280
+ " scope TEXT PRIMARY KEY," +
281
+ " atMonotonicCounter " + t.INT + " NOT NULL," +
282
+ " rowHash TEXT," +
283
+ " signedAt TEXT," +
284
+ " fencingToken " + t.INT + " NOT NULL DEFAULT 0," +
285
+ " CHECK (scope = 'consent')" +
286
+ ")",
287
+ indexes: [],
288
+ };
289
+ }
290
+
291
+ // _blamejs_audit_purge_anchor — single-row chain-origin anchor written
292
+ // by audit-tools.purge(). Holds the lastRowHash of the most recently
293
+ // purged range so verifyChain can ground its walk at the new origin.
294
+ // Single-row invariant via PRIMARY KEY on the constant-valued `scope`
295
+ // column (matches _blamejs_audit_tip pattern).
296
+ function _auditPurgeAnchorDDL(dialect) {
297
+ var t = _types(dialect);
298
+ var name = LOCAL_TO_EXTERNAL._blamejs_audit_purge_anchor;
299
+ return {
300
+ create:
301
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
302
+ " scope TEXT PRIMARY KEY," +
303
+ " lastPurgedCounter " + t.INT + " NOT NULL," +
304
+ " lastPurgedRowHash TEXT NOT NULL," +
305
+ " archiveBundleId TEXT NOT NULL," +
306
+ " purgedAt " + t.INT + " NOT NULL," +
307
+ " CHECK (scope = 'audit')" +
308
+ ")",
309
+ indexes: [],
310
+ };
311
+ }
312
+
313
+ // _blamejs_scheduler_ticks — exactly-once tick-claim table. PRIMARY KEY
314
+ // on composite tickKey makes concurrent INSERTs race; the loser skips
315
+ // the tick. claimedBy carries the node id for diagnostic.
316
+ function _schedulerTicksDDL(dialect) {
317
+ var t = _types(dialect);
318
+ var name = LOCAL_TO_EXTERNAL._blamejs_scheduler_ticks;
319
+ return {
320
+ create:
321
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
322
+ " tickKey TEXT PRIMARY KEY," +
323
+ " name TEXT NOT NULL," +
324
+ " scheduledAtUnix " + t.INT + " NOT NULL," +
325
+ " claimedAtUnix " + t.INT + " NOT NULL," +
326
+ " claimedBy TEXT" +
327
+ ")",
328
+ indexes: [
329
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_scheduledAt ON " + name + " (scheduledAtUnix)",
330
+ ],
331
+ };
332
+ }
333
+
334
+ // _blamejs_rate_limit_counters — fixed-window counter table for the
335
+ // cluster-shared rate-limit backend. PRIMARY KEY on the rate-limit
336
+ // key lets INSERT...ON CONFLICT atomically increment within a window
337
+ // and roll over on window advance. The windowStart index supports
338
+ // retention sweeps of expired windows.
339
+ function _rateLimitCountersDDL(dialect) {
340
+ var t = _types(dialect);
341
+ var name = LOCAL_TO_EXTERNAL._blamejs_rate_limit_counters;
342
+ return {
343
+ create:
344
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
345
+ " key TEXT PRIMARY KEY," +
346
+ " windowStart " + t.INT + " NOT NULL," +
347
+ " count " + t.INT + " NOT NULL DEFAULT 0" +
348
+ ")",
349
+ indexes: [
350
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_windowStart ON " + name + " (windowStart)",
351
+ ],
352
+ };
353
+ }
354
+
355
+ // _blamejs_ws_messages — cluster fan-out for the WebSocket channel
356
+ // hub. publish() on any node writes a row; the other nodes poll for
357
+ // new ids past their last seen and dispatch to local subscribers.
358
+ // Auto-incrementing id is essential — postgres needs BIGSERIAL,
359
+ // sqlite gets INTEGER PRIMARY KEY (which auto-increments implicitly).
360
+ function _wsMessagesDDL(dialect) {
361
+ var t = _types(dialect);
362
+ var name = LOCAL_TO_EXTERNAL._blamejs_ws_messages;
363
+ var idCol = dialect === "postgres"
364
+ ? "id BIGSERIAL PRIMARY KEY"
365
+ : "id INTEGER PRIMARY KEY AUTOINCREMENT";
366
+ return {
367
+ create:
368
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
369
+ " " + idCol + "," +
370
+ " channel TEXT NOT NULL," +
371
+ " payload TEXT NOT NULL," +
372
+ " publishedAt " + t.INT + " NOT NULL," +
373
+ " publishedBy TEXT NOT NULL" +
374
+ ")",
375
+ indexes: [
376
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_publishedAt ON " + name + " (publishedAt)",
377
+ ],
378
+ };
379
+ }
380
+
381
+ function _apiEncryptNoncesDDL(dialect) {
382
+ var t = _types(dialect);
383
+ var name = LOCAL_TO_EXTERNAL._blamejs_api_encrypt_nonces;
384
+ return {
385
+ create:
386
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
387
+ " nonceHash TEXT PRIMARY KEY," +
388
+ " expireAt " + t.INT + " NOT NULL" +
389
+ ")",
390
+ indexes: [
391
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_expireAt ON " + name + " (expireAt)",
392
+ ],
393
+ };
394
+ }
395
+
396
+ // _blamejs_api_keys — operator-facing API-key registry. PRIMARY KEY is
397
+ // the namespace-scoped id ("<namespace>:<idHex>"); ownerId/scopes/metadata
398
+ // are sealed by cryptoField. ownerIdHash supports indexed listForOwner
399
+ // lookups; expiresAt index supports purgeExpired sweeps.
400
+ function _apiKeysDDL(dialect) {
401
+ var t = _types(dialect);
402
+ var name = LOCAL_TO_EXTERNAL._blamejs_api_keys;
403
+ return {
404
+ create:
405
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
406
+ " id TEXT PRIMARY KEY," +
407
+ " namespace TEXT NOT NULL," +
408
+ " ownerId TEXT NOT NULL," +
409
+ " ownerIdHash TEXT NOT NULL," +
410
+ " secretHash TEXT NOT NULL," +
411
+ " secondarySecretHash TEXT," +
412
+ " secondaryExpiresAt " + t.INT + "," +
413
+ " scopes TEXT," +
414
+ " metadata TEXT," +
415
+ " createdAt " + t.INT + " NOT NULL," +
416
+ " expiresAt " + t.INT + "," +
417
+ " revokedAt " + t.INT + "," +
418
+ " lastUsedAt " + t.INT + "," +
419
+ " prefix TEXT NOT NULL" +
420
+ ")",
421
+ indexes: [
422
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_ownerIdHash ON " + name + " (ownerIdHash)",
423
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_namespace_owner ON " + name + " (namespace, ownerIdHash)",
424
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_expiresAt ON " + name + " (expiresAt)",
425
+ ],
426
+ };
427
+ }
428
+
429
+ // _blamejs_sessions — DB-backed session store. Mirrors the local-SQLite
430
+ // schema in db.js's FRAMEWORK_SCHEMA so single-node and cluster-mode
431
+ // behavior is identical at the column level. Sealed columns (userId,
432
+ // data) are stored vault-sealed; sidHash is the PRIMARY KEY (the raw
433
+ // session id never lands here).
434
+ function _sessionsDDL(dialect) {
435
+ var t = _types(dialect);
436
+ var name = LOCAL_TO_EXTERNAL._blamejs_sessions;
437
+ return {
438
+ create:
439
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
440
+ " sidHash TEXT PRIMARY KEY," +
441
+ " userId TEXT NOT NULL," +
442
+ " userIdHash TEXT NOT NULL," +
443
+ " data TEXT," +
444
+ " createdAt " + t.INT + " NOT NULL," +
445
+ " expiresAt " + t.INT + " NOT NULL," +
446
+ " lastActivity " + t.INT + " NOT NULL" +
447
+ ")",
448
+ indexes: [
449
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_userIdHash ON " + name + " (userIdHash)",
450
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_expiresAt ON " + name + " (expiresAt)",
451
+ ],
452
+ };
453
+ }
454
+
455
+ // _blamejs_jobs — local-protocol queue jobs. Mirrors db.js's
456
+ // FRAMEWORK_SCHEMA for the same table; sealed columns (payload,
457
+ // lastError) are stored vault-sealed. Indexes target the lease
458
+ // hot-path (queueName + status + availableAt) and lease-expiry
459
+ // sweep (leaseExpiresAt).
460
+ function _jobsDDL(dialect) {
461
+ var t = _types(dialect);
462
+ var name = LOCAL_TO_EXTERNAL._blamejs_jobs;
463
+ return {
464
+ create:
465
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
466
+ " _id TEXT PRIMARY KEY," +
467
+ " queueName TEXT NOT NULL," +
468
+ " payload TEXT," +
469
+ " status TEXT NOT NULL," +
470
+ " enqueuedAt " + t.INT + " NOT NULL," +
471
+ " availableAt " + t.INT + " NOT NULL," +
472
+ " leasedAt " + t.INT + "," +
473
+ " leaseExpiresAt " + t.INT + "," +
474
+ " attempts " + t.INT + " NOT NULL DEFAULT 0," +
475
+ " maxAttempts " + t.INT + " NOT NULL DEFAULT 5," +
476
+ " lastError TEXT," +
477
+ " finishedAt " + t.INT + "," +
478
+ " traceId TEXT," +
479
+ " classification TEXT" +
480
+ ")",
481
+ indexes: [
482
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_lease ON " + name + " (queueName, status, availableAt)",
483
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_leaseExpiresAt ON " + name + " (leaseExpiresAt)",
484
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_finishedAt ON " + name + " (finishedAt)",
485
+ ],
486
+ };
487
+ }
488
+
489
+ // _blamejs_seeders — registry of applied seed files for the b.seeders
490
+ // primitive (lib/seeders.js). Composite PK (env, name) so the same
491
+ // filename can apply per env without collision. rerunnable=1 entries
492
+ // have their appliedAt updated in place on each run; non-rerunnable
493
+ // entries are insert-once.
494
+ function _seedersDDL(dialect) {
495
+ var t = _types(dialect);
496
+ var name = LOCAL_TO_EXTERNAL._blamejs_seeders;
497
+ return {
498
+ create:
499
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
500
+ " env TEXT NOT NULL," +
501
+ " name TEXT NOT NULL," +
502
+ " description TEXT," +
503
+ " appliedAt TEXT NOT NULL," +
504
+ " rerunnable " + t.INT + " NOT NULL DEFAULT 0," +
505
+ " PRIMARY KEY (env, name)" +
506
+ ")",
507
+ indexes: [],
508
+ };
509
+ }
510
+
511
+ // _blamejs_seeders_lock — single-row advisory lock matching the
512
+ // _blamejs_migrations_lock pattern. CHECK enforces single row.
513
+ function _seedersLockDDL(dialect) {
514
+ var t = _types(dialect);
515
+ var name = LOCAL_TO_EXTERNAL._blamejs_seeders_lock;
516
+ return {
517
+ create:
518
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
519
+ " scope TEXT PRIMARY KEY CHECK (scope = 'lock')," +
520
+ " lockedAt " + t.INT + " NOT NULL," +
521
+ " lockedBy TEXT NOT NULL" +
522
+ ")",
523
+ indexes: [],
524
+ };
525
+ }
526
+
527
+ // _blamejs_cache — operator-facing cache primitive's cluster backend
528
+ // (lib/cache.js). PRIMARY KEY is the composite "<namespace>:<key>" so a
529
+ // single shared table serves every CacheInstance regardless of namespace
530
+ // without per-namespace table proliferation. valueJson is the
531
+ // JSON-serialized stored value; expiresAt is the unix-ms TTL boundary
532
+ // (Number.MAX_SAFE_INTEGER for never-expiring entries). Indexed on
533
+ // expiresAt for the periodic prune query.
534
+ function _cacheDDL(dialect) {
535
+ var t = _types(dialect);
536
+ var name = LOCAL_TO_EXTERNAL._blamejs_cache;
537
+ return {
538
+ create:
539
+ "CREATE TABLE IF NOT EXISTS " + name + " (" +
540
+ " cacheKey TEXT PRIMARY KEY," +
541
+ " valueJson TEXT NOT NULL," +
542
+ " expiresAt " + t.INT + " NOT NULL," +
543
+ " updatedAt " + t.INT + " NOT NULL" +
544
+ ")",
545
+ indexes: [
546
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_expiresAt ON " + name + " (expiresAt)",
547
+ ],
548
+ };
549
+ }
550
+
551
+ // ---- ensureSchema ----
552
+
553
+ async function ensureSchema(opts) {
554
+ if (!opts || !opts.externalDbBackend) {
555
+ throw new FrameworkSchemaError(
556
+ "ensureSchema requires { externalDbBackend: <name> }",
557
+ "framework-schema/invalid-config"
558
+ );
559
+ }
560
+ var dialect = (opts.dialect || "postgres").toLowerCase();
561
+ if (dialect !== "postgres" && dialect !== "sqlite") {
562
+ throw new FrameworkSchemaError(
563
+ "unsupported dialect '" + dialect + "' (postgres or sqlite)",
564
+ "framework-schema/unsupported-dialect"
565
+ );
566
+ }
567
+
568
+ var ddls = [
569
+ _auditLogDDL(dialect),
570
+ _consentLogDDL(dialect),
571
+ _auditCheckpointsDDL(dialect),
572
+ _auditTipDDL(dialect),
573
+ _consentTipDDL(dialect),
574
+ _auditPurgeAnchorDDL(dialect),
575
+ _schedulerTicksDDL(dialect),
576
+ _rateLimitCountersDDL(dialect),
577
+ _wsMessagesDDL(dialect),
578
+ _apiEncryptNoncesDDL(dialect),
579
+ _apiKeysDDL(dialect),
580
+ _sessionsDDL(dialect),
581
+ _jobsDDL(dialect),
582
+ _cacheDDL(dialect),
583
+ _seedersDDL(dialect),
584
+ _seedersLockDDL(dialect),
585
+ ];
586
+
587
+ var created = [];
588
+ for (var i = 0; i < ddls.length; i++) {
589
+ var d = ddls[i];
590
+ await externalDb.query(d.create, [], { backend: opts.externalDbBackend });
591
+ for (var j = 0; j < d.indexes.length; j++) {
592
+ await externalDb.query(d.indexes[j], [], { backend: opts.externalDbBackend });
593
+ }
594
+ created.push(d.create.match(/CREATE TABLE IF NOT EXISTS\s+(\S+)/)[1]);
595
+ }
596
+ return { tables: created };
597
+ }
598
+
599
+ module.exports = {
600
+ ensureSchema: ensureSchema,
601
+ tableName: tableName,
602
+ LOCAL_TO_EXTERNAL: LOCAL_TO_EXTERNAL,
603
+ FrameworkSchemaError: FrameworkSchemaError,
604
+ };