@blamejs/core 0.14.26 → 0.15.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.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/agent-envelope-mac.js +104 -0
- package/lib/agent-event-bus.js +105 -4
- package/lib/agent-posture-chain.js +8 -42
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +62 -4
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +249 -123
- package/lib/auth/openid-federation.js +108 -47
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +169 -4
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +474 -92
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +497 -255
- package/lib/db-schema.js +209 -44
- package/lib/db.js +176 -95
- package/lib/error-page.js +14 -1
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/file-upload.js +52 -7
- package/lib/framework-error.js +14 -1
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +649 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +37 -9
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-server-jmap.js +117 -12
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- package/lib/middleware/body-parser.js +71 -25
- package/lib/middleware/csrf-protect.js +19 -8
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +57 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/observability.js +87 -0
- package/lib/otel-export.js +25 -1
- package/lib/outbox.js +136 -82
- package/lib/parsers/safe-xml.js +47 -7
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/redact.js +68 -11
- package/lib/redis-client.js +160 -31
- package/lib/retention.js +82 -39
- package/lib/router.js +212 -5
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +109 -72
- package/lib/sql.js +3885 -0
- package/lib/ssrf-guard.js +51 -4
- package/lib/static.js +177 -34
- package/lib/subject.js +55 -17
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +35 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -63,17 +63,59 @@ var auditSign = lazyRequire(function () { return require("./audit-sign"); });
|
|
|
63
63
|
var ExternalDbMigrateError = defineClass("ExternalDbMigrateError", { alwaysPermanent: true });
|
|
64
64
|
|
|
65
65
|
// Lazy require — external-db imports back into this module via its
|
|
66
|
-
// public `migrate` namespace; load-order would cycle without lazy.
|
|
66
|
+
// public `migrate` namespace; load-order would cycle without lazy. The
|
|
67
|
+
// same cycle (external-db -> external-db-migrate -> cluster-storage ->
|
|
68
|
+
// cluster -> cluster-provider-db -> external-db) means clusterStorage +
|
|
69
|
+
// frameworkSchema must be lazy here too, and the table-name constants
|
|
70
|
+
// resolved on first use rather than at module load (frameworkSchema's
|
|
71
|
+
// tableName export is not yet bound while this module evaluates).
|
|
67
72
|
var externalDb = lazyRequire(function () { return require("./external-db"); });
|
|
73
|
+
var clusterStorage = lazyRequire(function () { return require("./cluster-storage"); });
|
|
74
|
+
var frameworkSchema = lazyRequire(function () { return require("./framework-schema"); });
|
|
75
|
+
var sql = lazyRequire(function () { return require("./sql"); });
|
|
76
|
+
|
|
77
|
+
// The migration runner's own bookkeeping tables, resolved through
|
|
78
|
+
// frameworkSchema so the configurable framework-table prefix is honored
|
|
79
|
+
// (these names are not in the LOCAL_TO_EXTERNAL map, so the resolve only
|
|
80
|
+
// swaps the leading prefix; default prefix is a no-op). b.sql quotes
|
|
81
|
+
// every identifier by construction in the backend's own dialect
|
|
82
|
+
// (double-quote on Postgres / SQLite, backtick on MySQL), with the
|
|
83
|
+
// placeholder form (`$N` on Postgres, `?` on SQLite / MySQL) selected by
|
|
84
|
+
// the resolved backend dialect — see _backendDialect / _bind below.
|
|
85
|
+
function _trackingTable() { return frameworkSchema().tableName("_blamejs_externaldb_migrations"); } // allow:hand-rolled-sql — single canonical logical-name reference
|
|
86
|
+
function _lockTable() { return frameworkSchema().tableName("_blamejs_externaldb_migrations_lock"); } // allow:hand-rolled-sql — single canonical logical-name reference
|
|
87
|
+
function _historyTable() { return frameworkSchema().tableName("_blamejs_schema_version_history"); } // allow:hand-rolled-sql — single canonical logical-name reference
|
|
88
|
+
|
|
89
|
+
// Resolve the SQL dialect of the backend this migration wave targets.
|
|
90
|
+
// The runner emits Postgres / SQLite / MySQL — they diverge on identifier
|
|
91
|
+
// quoting (double-quote vs backtick), the ON CONFLICT / ON DUPLICATE KEY
|
|
92
|
+
// upsert idiom, and placeholder syntax (`$N` vs `?`). Reading the dialect
|
|
93
|
+
// off the backend itself (set at b.externalDb.init) is what keeps the
|
|
94
|
+
// bookkeeping DDL + tracking statements valid on each. Falls back to
|
|
95
|
+
// "postgres" when the backend can't be resolved (uninitialized externalDb
|
|
96
|
+
// surfaces a clearer error upstream at _resolveBackendName); the bare
|
|
97
|
+
// fallback never reaches a real query.
|
|
98
|
+
function _backendDialect(backendName) {
|
|
99
|
+
var listed;
|
|
100
|
+
try { listed = externalDb().listBackends(); }
|
|
101
|
+
catch (_e) { return "postgres"; }
|
|
102
|
+
for (var i = 0; i < listed.length; i++) {
|
|
103
|
+
if (listed[i].name === backendName) {
|
|
104
|
+
return (listed[i].dialect || "postgres").toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return "postgres";
|
|
108
|
+
}
|
|
68
109
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
var
|
|
76
|
-
|
|
110
|
+
// b.sql emits `?` placeholders; the externalDb driver receives SQL
|
|
111
|
+
// verbatim, so translate to the Postgres `$N` form on a Postgres backend
|
|
112
|
+
// (placeholderize is a passthrough for SQLite / MySQL, which keep `?`).
|
|
113
|
+
// dialect is the resolved backend dialect so the placeholder form matches
|
|
114
|
+
// the backend the SQL dispatches to.
|
|
115
|
+
function _bind(builder, dialect) {
|
|
116
|
+
var built = builder.toSql();
|
|
117
|
+
return { sql: clusterStorage().placeholderize(built.sql, dialect), params: built.params };
|
|
118
|
+
}
|
|
77
119
|
|
|
78
120
|
// The migration tracking / history / lock tables hold framework
|
|
79
121
|
// bookkeeping ("migration X ran at time T"), not region-bound personal
|
|
@@ -110,12 +152,11 @@ function _historyPayload(row) {
|
|
|
110
152
|
// default introspect just returns the migration name list as a JSON
|
|
111
153
|
// array, which is enough to detect "someone manually altered the
|
|
112
154
|
// migrations table."
|
|
113
|
-
async function _defaultSchemaIntrospect(xdb) {
|
|
114
|
-
var
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
|
|
118
|
-
);
|
|
155
|
+
async function _defaultSchemaIntrospect(xdb, dialect) {
|
|
156
|
+
var q = _bind(sql().select(_trackingTable(), { dialect: dialect })
|
|
157
|
+
.columns(["name", "appliedAt"])
|
|
158
|
+
.orderBy("appliedAt", "asc").orderBy("name", "asc"), dialect);
|
|
159
|
+
var res = await xdb.query(q.sql, q.params);
|
|
119
160
|
var rows = (res && res.rows) || [];
|
|
120
161
|
return sha3Hash(Buffer.from(canonicalJson.stringify(rows), "utf8"));
|
|
121
162
|
}
|
|
@@ -149,75 +190,73 @@ function _lockHolderId() {
|
|
|
149
190
|
(require("node:os").hostname() || "unknown") + "@" + _BOOT_TOKEN;
|
|
150
191
|
}
|
|
151
192
|
|
|
152
|
-
async function _ensureTrackingTable(xdb) {
|
|
193
|
+
async function _ensureTrackingTable(xdb, dialect) {
|
|
153
194
|
// Tracking table holds the migration history. ISO-8601 timestamp
|
|
154
|
-
// strings
|
|
155
|
-
// Postgres/SQLite without dialect-specific type juggling —
|
|
156
|
-
// who want strict TIMESTAMPTZ for their own ad-hoc queries
|
|
157
|
-
// the table ALTER it post-creation.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
")",
|
|
164
|
-
|
|
165
|
-
|
|
195
|
+
// strings keep the framework's tracking table portable across
|
|
196
|
+
// Postgres/SQLite/MySQL without dialect-specific type juggling —
|
|
197
|
+
// operators who want strict TIMESTAMPTZ for their own ad-hoc queries
|
|
198
|
+
// against the table ALTER it post-creation. The `name` PK is a bounded
|
|
199
|
+
// VARCHAR, not TEXT: MySQL refuses an unbounded TEXT/BLOB in a key
|
|
200
|
+
// (ER 1170), and a migration filename is length-capped at FILE_NAME_MAX
|
|
201
|
+
// so 255 covers every valid value. Postgres / SQLite treat VARCHAR(255)
|
|
202
|
+
// identically to TEXT for storage.
|
|
203
|
+
await xdb.query(sql().createTable(_trackingTable(), [
|
|
204
|
+
{ name: "name", type: "VARCHAR(255)", primaryKey: true },
|
|
205
|
+
{ name: "description", type: "TEXT" },
|
|
206
|
+
{ name: "appliedAt", type: "TEXT", notNull: true },
|
|
207
|
+
], { dialect: dialect }).sql, []);
|
|
166
208
|
}
|
|
167
209
|
|
|
168
|
-
async function _ensureHistoryTable(xdb) {
|
|
210
|
+
async function _ensureHistoryTable(xdb, dialect) {
|
|
169
211
|
// Schema-version history table: append-only record of every migrate.up
|
|
170
212
|
// wave + signature over (version, ranAt, ranBy, schemaIntrospectionHash).
|
|
171
213
|
// Signature uses ML-DSA-87 / SLH-DSA-SHAKE-256f via b.auditSign — an
|
|
172
214
|
// attacker tampering with rows after-the-fact cannot forge a matching
|
|
173
|
-
// signature without the audit-signing private key.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
|
|
183
|
-
"
|
|
184
|
-
|
|
185
|
-
);
|
|
215
|
+
// signature without the audit-signing private key. version + ranAt form
|
|
216
|
+
// the composite PK, so both are bounded VARCHARs (MySQL refuses an
|
|
217
|
+
// unbounded TEXT/BLOB in a key, ER 1170); version is a filename
|
|
218
|
+
// (length-capped) and ranAt an ISO-8601 string, both within bound.
|
|
219
|
+
await xdb.query(sql().createTable(_historyTable(), [
|
|
220
|
+
{ name: "version", type: "VARCHAR(255)", notNull: true },
|
|
221
|
+
{ name: "ranAt", type: "VARCHAR(64)", notNull: true },
|
|
222
|
+
{ name: "ranBy", type: "TEXT", notNull: true },
|
|
223
|
+
{ name: "schemaIntrospectionHash", type: "TEXT", notNull: true },
|
|
224
|
+
{ name: "signature", type: "TEXT" },
|
|
225
|
+
{ name: "publicKeyFingerprint", type: "TEXT" },
|
|
226
|
+
], { dialect: dialect, primaryKey: ["version", "ranAt"] }).sql, []);
|
|
186
227
|
}
|
|
187
228
|
|
|
188
|
-
async function _writeHistoryRow(xdb, row) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
row.signature,
|
|
199
|
-
row.publicKeyFingerprint,
|
|
200
|
-
],
|
|
201
|
-
FRAMEWORK_METADATA_OPTS
|
|
202
|
-
);
|
|
229
|
+
async function _writeHistoryRow(xdb, row, dialect) {
|
|
230
|
+
var q = _bind(sql().insert(_historyTable(), { dialect: dialect }).values({
|
|
231
|
+
version: row.version,
|
|
232
|
+
ranAt: row.ranAt,
|
|
233
|
+
ranBy: row.ranBy,
|
|
234
|
+
schemaIntrospectionHash: row.schemaIntrospectionHash,
|
|
235
|
+
signature: row.signature,
|
|
236
|
+
publicKeyFingerprint: row.publicKeyFingerprint,
|
|
237
|
+
}), dialect);
|
|
238
|
+
await xdb.query(q.sql, q.params, FRAMEWORK_METADATA_OPTS);
|
|
203
239
|
}
|
|
204
240
|
|
|
205
|
-
async function _ensureLockTable(xdb) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
241
|
+
async function _ensureLockTable(xdb, dialect) {
|
|
242
|
+
// The scope CHECK is a static operator-controlled literal, carried as
|
|
243
|
+
// the last column's verbatim constraint (b.sql guards it via
|
|
244
|
+
// allowLiterals). lockedAt holds a ms-epoch value, so the framework INT
|
|
245
|
+
// type (BIGINT on Postgres/MySQL) is required — a 32-bit INTEGER
|
|
246
|
+
// overflows. The scope PK is a bounded VARCHAR (only ever 'lock'): MySQL
|
|
247
|
+
// refuses an unbounded TEXT/BLOB in a key (ER 1170).
|
|
248
|
+
await xdb.query(sql().createTable(_lockTable(), [
|
|
249
|
+
{ name: "scope", type: "VARCHAR(64)", primaryKey: true },
|
|
250
|
+
{ name: "lockedAt", type: "INTEGER", notNull: true },
|
|
251
|
+
{ name: "lockedBy", type: "TEXT", notNull: true,
|
|
252
|
+
constraints: ", CHECK (scope = 'lock')" }, // allow:hand-rolled-sql — static DDL CHECK literal
|
|
253
|
+
], { dialect: dialect }).sql, []);
|
|
215
254
|
}
|
|
216
255
|
|
|
217
256
|
// ---- Lock acquire / release ----
|
|
218
257
|
|
|
219
|
-
async function _acquireLock(xdb, opts) {
|
|
220
|
-
await _ensureLockTable(xdb);
|
|
258
|
+
async function _acquireLock(xdb, opts, dialect) {
|
|
259
|
+
await _ensureLockTable(xdb, dialect);
|
|
221
260
|
var holder = _lockHolderId();
|
|
222
261
|
var nowMs = Date.now();
|
|
223
262
|
// See migrations.acquireLock for the same fix — Infinity was
|
|
@@ -229,27 +268,67 @@ async function _acquireLock(xdb, opts) {
|
|
|
229
268
|
ExternalDbMigrateError, "externalDb-migrate/bad-opt");
|
|
230
269
|
if (opts.staleAfterMs !== undefined) staleAfterMs = opts.staleAfterMs;
|
|
231
270
|
}
|
|
271
|
+
// Conflict-safe lock acquire. The INSERT runs inside
|
|
272
|
+
// externalDb.transaction(_acquireLock); on Postgres a plain INSERT that
|
|
273
|
+
// hits the PRIMARY KEY conflict raises SQLSTATE 23505 which ABORTS the
|
|
274
|
+
// surrounding transaction (every later statement then fails with 25P02,
|
|
275
|
+
// "current transaction is aborted"), so the holder-naming SELECT could
|
|
276
|
+
// not run and the operator got a raw aborted-transaction error instead of
|
|
277
|
+
// the documented "migration lock is held by <holder>" message. Emitting
|
|
278
|
+
// `INSERT ... ON CONFLICT (scope) DO NOTHING` (Postgres/SQLite) /
|
|
279
|
+
// `INSERT ... ON DUPLICATE KEY UPDATE scope=scope` (MySQL — a no-op)
|
|
280
|
+
// turns the conflict into a 0-row result rather than a transaction-
|
|
281
|
+
// aborting error, so the inspect SELECT below runs cleanly and names the
|
|
282
|
+
// holder. rowCount === 1 means we won the lock; 0 means it is held.
|
|
283
|
+
function _insertLock() {
|
|
284
|
+
return _bind(sql().upsert(_lockTable(), { dialect: dialect })
|
|
285
|
+
.values({ scope: "lock", lockedAt: nowMs, lockedBy: holder })
|
|
286
|
+
.onConflict(["scope"]).doNothing(), dialect);
|
|
287
|
+
}
|
|
288
|
+
var insRes;
|
|
232
289
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
290
|
+
var ins = _insertLock();
|
|
291
|
+
insRes = await xdb.query(ins.sql, ins.params, FRAMEWORK_METADATA_OPTS);
|
|
292
|
+
} catch (e0) {
|
|
293
|
+
// A genuine driver/connection fault (not a conflict — the conflict is now
|
|
294
|
+
// a 0-row no-op, never a throw). Surface as lock-busy.
|
|
295
|
+
throw _err("externaldb-migrate/lock-busy",
|
|
296
|
+
"could not acquire migration lock: " + ((e0 && e0.message) || String(e0)));
|
|
297
|
+
}
|
|
298
|
+
if (insRes && insRes.rowCount >= 1) {
|
|
237
299
|
return { holder: holder, takeoverFrom: null, takeoverAgeMs: 0 };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
300
|
+
}
|
|
301
|
+
{
|
|
302
|
+
// 0 rows inserted → the lock IS held. Inspect it to name the holder. The
|
|
303
|
+
// conflict was a clean no-op (DO NOTHING), so the transaction is NOT
|
|
304
|
+
// aborted and this SELECT runs.
|
|
305
|
+
var selExisting = _bind(sql().select(_lockTable(), { dialect: dialect })
|
|
306
|
+
.columns(["lockedAt", "lockedBy"]).where("scope", "lock"), dialect);
|
|
307
|
+
var existingRes;
|
|
308
|
+
try {
|
|
309
|
+
existingRes = await xdb.query(selExisting.sql, selExisting.params);
|
|
310
|
+
} catch (_inspectErr) {
|
|
311
|
+
throw _err("externaldb-migrate/lock-held",
|
|
312
|
+
"migration lock is held — another process is running migrations " +
|
|
313
|
+
"(the lock row could not be inspected). Wait for it to finish, or " +
|
|
314
|
+
"pass staleAfterMs to force-replace stale locks.");
|
|
315
|
+
}
|
|
244
316
|
var existing = existingRes && existingRes.rows && existingRes.rows[0];
|
|
245
317
|
if (!existing) {
|
|
318
|
+
// Lock row vanished between the no-op insert and the inspect (the
|
|
319
|
+
// holder released concurrently). Retry the acquire once.
|
|
246
320
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
321
|
+
var insRetry = _insertLock();
|
|
322
|
+
var retryRes = await xdb.query(insRetry.sql, insRetry.params, FRAMEWORK_METADATA_OPTS);
|
|
323
|
+
if (retryRes && retryRes.rowCount >= 1) {
|
|
324
|
+
return { holder: holder, takeoverFrom: null, takeoverAgeMs: 0 };
|
|
325
|
+
}
|
|
326
|
+
throw _err("externaldb-migrate/lock-held",
|
|
327
|
+
"migration lock is held — another process re-acquired it during " +
|
|
328
|
+
"the acquire race. Wait for it to finish, or pass staleAfterMs to " +
|
|
329
|
+
"force-replace stale locks.");
|
|
252
330
|
} catch (e2) {
|
|
331
|
+
if (e2 && e2.isExternalDbMigrateError) throw e2;
|
|
253
332
|
throw _err("externaldb-migrate/lock-busy",
|
|
254
333
|
"could not acquire migration lock: " + ((e2 && e2.message) || String(e2)));
|
|
255
334
|
}
|
|
@@ -259,14 +338,19 @@ async function _acquireLock(xdb, opts) {
|
|
|
259
338
|
// Force-replace the stale lock atomically. Stale-takeover is a
|
|
260
339
|
// SOC2 evidence event — caller emits an audit row.
|
|
261
340
|
var prevHolder = existing.lockedby || existing.lockedBy;
|
|
262
|
-
|
|
263
|
-
"
|
|
264
|
-
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
341
|
+
var delStale = _bind(sql().delete(_lockTable(), { dialect: dialect })
|
|
342
|
+
.where("scope", "lock")
|
|
343
|
+
.where("lockedAt", Number(existing.lockedat || existing.lockedAt)), dialect);
|
|
344
|
+
await xdb.query(delStale.sql, delStale.params, FRAMEWORK_METADATA_OPTS);
|
|
345
|
+
var insTakeover = _insertLock();
|
|
346
|
+
var takeoverRes = await xdb.query(insTakeover.sql, insTakeover.params, FRAMEWORK_METADATA_OPTS);
|
|
347
|
+
if (!takeoverRes || takeoverRes.rowCount < 1) {
|
|
348
|
+
// Another process slipped a fresh lock in between our DELETE and
|
|
349
|
+
// INSERT (the conflict is a DO NOTHING no-op, so 0 rows = lost race).
|
|
350
|
+
throw _err("externaldb-migrate/lock-held",
|
|
351
|
+
"migration lock was re-acquired by another process during the " +
|
|
352
|
+
"stale-lock takeover. Wait for it to finish, or retry.");
|
|
353
|
+
}
|
|
270
354
|
return { holder: holder, takeoverFrom: prevHolder, takeoverAgeMs: ageMs };
|
|
271
355
|
}
|
|
272
356
|
throw _err("externaldb-migrate/lock-held",
|
|
@@ -276,12 +360,11 @@ async function _acquireLock(xdb, opts) {
|
|
|
276
360
|
}
|
|
277
361
|
}
|
|
278
362
|
|
|
279
|
-
async function _releaseLock(xdb, holder) {
|
|
363
|
+
async function _releaseLock(xdb, holder, dialect) {
|
|
280
364
|
try {
|
|
281
|
-
|
|
282
|
-
"
|
|
283
|
-
|
|
284
|
-
);
|
|
365
|
+
var del = _bind(sql().delete(_lockTable(), { dialect: dialect })
|
|
366
|
+
.where("scope", "lock").where("lockedBy", holder), dialect);
|
|
367
|
+
await xdb.query(del.sql, del.params, FRAMEWORK_METADATA_OPTS);
|
|
285
368
|
} catch (_e) {
|
|
286
369
|
// best-effort release; operator can DELETE manually.
|
|
287
370
|
}
|
|
@@ -381,13 +464,13 @@ function create(opts) {
|
|
|
381
464
|
|
|
382
465
|
async function status() {
|
|
383
466
|
var backendName = _resolveBackendName(opts);
|
|
467
|
+
var dialect = _backendDialect(backendName);
|
|
384
468
|
return await externalDb().transaction(async function (xdb) {
|
|
385
|
-
await _ensureTrackingTable(xdb);
|
|
386
|
-
var
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
|
|
390
|
-
);
|
|
469
|
+
await _ensureTrackingTable(xdb, dialect);
|
|
470
|
+
var q = _bind(sql().select(_trackingTable(), { dialect: dialect })
|
|
471
|
+
.columns(["name", "description", "appliedAt"])
|
|
472
|
+
.orderBy("appliedAt", "asc").orderBy("name", "asc"), dialect);
|
|
473
|
+
var res = await xdb.query(q.sql, q.params);
|
|
391
474
|
var applied = (res && res.rows) || [];
|
|
392
475
|
var appliedNames = new Set(applied.map(function (r) { return r.name; }));
|
|
393
476
|
var files = _list(dir);
|
|
@@ -403,12 +486,13 @@ function create(opts) {
|
|
|
403
486
|
|
|
404
487
|
async function up() {
|
|
405
488
|
var backendName = _resolveBackendName(opts);
|
|
489
|
+
var dialect = _backendDialect(backendName);
|
|
406
490
|
var ctx = _ctx(backendName);
|
|
407
491
|
|
|
408
492
|
return await externalDb().transaction(async function (xdb) {
|
|
409
|
-
await _ensureTrackingTable(xdb);
|
|
410
|
-
await _ensureLockTable(xdb);
|
|
411
|
-
await _ensureHistoryTable(xdb);
|
|
493
|
+
await _ensureTrackingTable(xdb, dialect);
|
|
494
|
+
await _ensureLockTable(xdb, dialect);
|
|
495
|
+
await _ensureHistoryTable(xdb, dialect);
|
|
412
496
|
}, { backend: backendName }).then(async function () {
|
|
413
497
|
// Acquire the lock OUTSIDE the per-migration transaction so the
|
|
414
498
|
// lock survives across migration boundaries. We use a separate
|
|
@@ -416,7 +500,7 @@ function create(opts) {
|
|
|
416
500
|
// serializes apply order, so this single-connection lock is
|
|
417
501
|
// sufficient.
|
|
418
502
|
var lockResult = await externalDb().transaction(async function (xdb) {
|
|
419
|
-
return await _acquireLock(xdb, opts);
|
|
503
|
+
return await _acquireLock(xdb, opts, dialect);
|
|
420
504
|
}, { backend: backendName });
|
|
421
505
|
var lockHolder = lockResult.holder;
|
|
422
506
|
|
|
@@ -432,9 +516,9 @@ function create(opts) {
|
|
|
432
516
|
}
|
|
433
517
|
|
|
434
518
|
try {
|
|
435
|
-
var
|
|
436
|
-
"
|
|
437
|
-
);
|
|
519
|
+
var appliedQ = _bind(sql().select(_trackingTable(), { dialect: dialect })
|
|
520
|
+
.columns(["name"]), dialect);
|
|
521
|
+
var appliedRes = await externalDb().query(appliedQ.sql, appliedQ.params, { backend: backendName });
|
|
438
522
|
var appliedSet = new Set(((appliedRes && appliedRes.rows) || []).map(function (r) { return r.name; }));
|
|
439
523
|
var files = _list(dir);
|
|
440
524
|
var applied = [];
|
|
@@ -449,12 +533,9 @@ function create(opts) {
|
|
|
449
533
|
await externalDb().transaction(async function (xdb) {
|
|
450
534
|
await mod.up(xdb, ctx);
|
|
451
535
|
var ranAt = new Date().toISOString();
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
[file, mod.description || "", ranAt],
|
|
456
|
-
FRAMEWORK_METADATA_OPTS
|
|
457
|
-
);
|
|
536
|
+
var insTrack = _bind(sql().insert(_trackingTable(), { dialect: dialect })
|
|
537
|
+
.values({ name: file, description: mod.description || "", appliedAt: ranAt }), dialect);
|
|
538
|
+
await xdb.query(insTrack.sql, insTrack.params, FRAMEWORK_METADATA_OPTS);
|
|
458
539
|
// Schema-version history with signature. Sign post-INSERT
|
|
459
540
|
// so the introspection hash reflects the row that just
|
|
460
541
|
// landed. Sign-failure is non-fatal for the migration but
|
|
@@ -463,7 +544,7 @@ function create(opts) {
|
|
|
463
544
|
version: file,
|
|
464
545
|
ranAt: ranAt,
|
|
465
546
|
ranBy: ranBy,
|
|
466
|
-
schemaIntrospectionHash: await schemaIntrospect(xdb),
|
|
547
|
+
schemaIntrospectionHash: await schemaIntrospect(xdb, dialect),
|
|
467
548
|
signature: null,
|
|
468
549
|
publicKeyFingerprint: null,
|
|
469
550
|
};
|
|
@@ -479,7 +560,7 @@ function create(opts) {
|
|
|
479
560
|
(sigErr && sigErr.message) || String(sigErr));
|
|
480
561
|
}
|
|
481
562
|
}
|
|
482
|
-
await _writeHistoryRow(xdb, historyRow);
|
|
563
|
+
await _writeHistoryRow(xdb, historyRow, dialect);
|
|
483
564
|
_emit(audit, "migrations.history.appended", "success", {
|
|
484
565
|
migration: file,
|
|
485
566
|
schemaIntrospectionHash: historyRow.schemaIntrospectionHash,
|
|
@@ -502,7 +583,7 @@ function create(opts) {
|
|
|
502
583
|
} finally {
|
|
503
584
|
try {
|
|
504
585
|
await externalDb().transaction(async function (xdb) {
|
|
505
|
-
await _releaseLock(xdb, lockHolder);
|
|
586
|
+
await _releaseLock(xdb, lockHolder, dialect);
|
|
506
587
|
}, { backend: backendName });
|
|
507
588
|
_emit(audit, "externaldb.migrate.lock.released", "success",
|
|
508
589
|
{ holder: lockHolder, backend: backendName }, null);
|
|
@@ -519,15 +600,16 @@ function create(opts) {
|
|
|
519
600
|
var steps = (typeof downOpts.steps === "number" && downOpts.steps > 0)
|
|
520
601
|
? Math.floor(downOpts.steps) : 1;
|
|
521
602
|
var backendName = _resolveBackendName(opts);
|
|
603
|
+
var dialect = _backendDialect(backendName);
|
|
522
604
|
var ctx = _ctx(backendName);
|
|
523
605
|
|
|
524
606
|
await externalDb().transaction(async function (xdb) {
|
|
525
|
-
await _ensureTrackingTable(xdb);
|
|
526
|
-
await _ensureLockTable(xdb);
|
|
607
|
+
await _ensureTrackingTable(xdb, dialect);
|
|
608
|
+
await _ensureLockTable(xdb, dialect);
|
|
527
609
|
}, { backend: backendName });
|
|
528
610
|
|
|
529
611
|
var lockResultDown = await externalDb().transaction(async function (xdb) {
|
|
530
|
-
return await _acquireLock(xdb, opts);
|
|
612
|
+
return await _acquireLock(xdb, opts, dialect);
|
|
531
613
|
}, { backend: backendName });
|
|
532
614
|
var lockHolder = lockResultDown.holder;
|
|
533
615
|
|
|
@@ -540,10 +622,10 @@ function create(opts) {
|
|
|
540
622
|
}
|
|
541
623
|
|
|
542
624
|
try {
|
|
543
|
-
var
|
|
544
|
-
"
|
|
545
|
-
|
|
546
|
-
);
|
|
625
|
+
var downQ = _bind(sql().select(_trackingTable(), { dialect: dialect })
|
|
626
|
+
.columns(["name"])
|
|
627
|
+
.orderBy("appliedAt", "desc").orderBy("name", "desc").limit(steps), dialect);
|
|
628
|
+
var appliedRes = await externalDb().query(downQ.sql, downQ.params, { backend: backendName });
|
|
547
629
|
var rows = (appliedRes && appliedRes.rows) || [];
|
|
548
630
|
var reverted = [];
|
|
549
631
|
for (var i = 0; i < rows.length; i++) {
|
|
@@ -557,10 +639,9 @@ function create(opts) {
|
|
|
557
639
|
try {
|
|
558
640
|
await externalDb().transaction(async function (xdb) {
|
|
559
641
|
await mod.down(xdb, ctx);
|
|
560
|
-
|
|
561
|
-
"
|
|
562
|
-
|
|
563
|
-
);
|
|
642
|
+
var delTrack = _bind(sql().delete(_trackingTable(), { dialect: dialect })
|
|
643
|
+
.where("name", file), dialect);
|
|
644
|
+
await xdb.query(delTrack.sql, delTrack.params);
|
|
564
645
|
}, { backend: backendName });
|
|
565
646
|
_emit(audit, "externaldb.migrate.down", "success",
|
|
566
647
|
{ migration: file, durationMs: Date.now() - t0, backend: backendName }, null);
|
|
@@ -577,7 +658,7 @@ function create(opts) {
|
|
|
577
658
|
} finally {
|
|
578
659
|
try {
|
|
579
660
|
await externalDb().transaction(async function (xdb) {
|
|
580
|
-
await _releaseLock(xdb, lockHolder);
|
|
661
|
+
await _releaseLock(xdb, lockHolder, dialect);
|
|
581
662
|
}, { backend: backendName });
|
|
582
663
|
_emit(audit, "externaldb.migrate.lock.released", "success",
|
|
583
664
|
{ holder: lockHolder, backend: backendName }, null);
|
|
@@ -600,13 +681,13 @@ function create(opts) {
|
|
|
600
681
|
async function history(historyOpts) {
|
|
601
682
|
historyOpts = historyOpts || {};
|
|
602
683
|
var backendName = _resolveBackendName(opts);
|
|
684
|
+
var dialect = _backendDialect(backendName);
|
|
603
685
|
return await externalDb().transaction(async function (xdb) {
|
|
604
|
-
await _ensureHistoryTable(xdb);
|
|
605
|
-
var
|
|
606
|
-
"
|
|
607
|
-
"
|
|
608
|
-
|
|
609
|
-
);
|
|
686
|
+
await _ensureHistoryTable(xdb, dialect);
|
|
687
|
+
var histQ = _bind(sql().select(_historyTable(), { dialect: dialect })
|
|
688
|
+
.columns(["version", "ranAt", "ranBy", "schemaIntrospectionHash", "signature", "publicKeyFingerprint"])
|
|
689
|
+
.orderBy("ranAt", "asc").orderBy("version", "asc"), dialect);
|
|
690
|
+
var res = await xdb.query(histQ.sql, histQ.params);
|
|
610
691
|
var out = [];
|
|
611
692
|
var rows = (res && res.rows) || [];
|
|
612
693
|
for (var i = 0; i < rows.length; i++) {
|
|
@@ -665,7 +746,16 @@ function create(opts) {
|
|
|
665
746
|
module.exports = {
|
|
666
747
|
create: create,
|
|
667
748
|
ExternalDbMigrateError: ExternalDbMigrateError,
|
|
668
|
-
TRACKING_TABLE: TRACKING_TABLE,
|
|
669
|
-
HISTORY_TABLE: HISTORY_TABLE,
|
|
670
749
|
HISTORY_SIGNATURE_FORMAT: HISTORY_SIGNATURE_FORMAT,
|
|
671
750
|
};
|
|
751
|
+
|
|
752
|
+
// The resolved table names are exposed as lazy getters: frameworkSchema's
|
|
753
|
+
// tableName export is not bound while this module evaluates (the
|
|
754
|
+
// external-db require cycle), so resolving at access time gives the
|
|
755
|
+
// configurable-prefix-aware concrete name without a load-order trap.
|
|
756
|
+
Object.defineProperty(module.exports, "TRACKING_TABLE", {
|
|
757
|
+
enumerable: true, get: function () { return _trackingTable(); },
|
|
758
|
+
});
|
|
759
|
+
Object.defineProperty(module.exports, "HISTORY_TABLE", {
|
|
760
|
+
enumerable: true, get: function () { return _historyTable(); },
|
|
761
|
+
});
|