@blamejs/core 0.14.22 → 0.14.25
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/lib/compliance.js +37 -0
- package/lib/constants.js +11 -0
- package/lib/crypto-field.js +418 -83
- package/lib/db-query.js +188 -5
- package/lib/db.js +17 -3
- package/lib/external-db-migrate.js +19 -7
- package/lib/external-db.js +508 -20
- package/lib/framework-error.js +6 -0
- package/lib/mail-auth.js +236 -0
- package/lib/mail-dkim.js +1 -0
- package/lib/mail-server-mx.js +276 -7
- package/lib/mail.js +8 -4
- package/lib/middleware/idempotency-key.js +21 -13
- package/lib/retention.js +11 -1
- package/lib/vault-aad.js +6 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/db-query.js
CHANGED
|
@@ -34,6 +34,122 @@ var safeJson = require("./safe-json");
|
|
|
34
34
|
var safeJsonPath = require("./safe-jsonpath");
|
|
35
35
|
var safeSql = require("./safe-sql");
|
|
36
36
|
var audit = require("./audit");
|
|
37
|
+
var lazyRequire = require("./lazy-require");
|
|
38
|
+
var { DbQueryError } = require("./framework-error");
|
|
39
|
+
|
|
40
|
+
// Circular load — db.js requires db-query at module scope, so the
|
|
41
|
+
// residency gate reaches back for getDataResidency() lazily.
|
|
42
|
+
var db = lazyRequire(function () { return require("./db"); });
|
|
43
|
+
|
|
44
|
+
// Cross-border regulated postures live on b.compliance
|
|
45
|
+
// (CROSS_BORDER_REGULATED_POSTURES — one vocabulary shared with
|
|
46
|
+
// external-db's gate): under these, a residency mismatch REFUSES the
|
|
47
|
+
// write; under anything else the gates emit an advisory audit and
|
|
48
|
+
// pass (backward-compatible).
|
|
49
|
+
function _postureState() {
|
|
50
|
+
try {
|
|
51
|
+
var compliance = require("./compliance"); // allow:inline-require — defensive against optional load
|
|
52
|
+
var posture = compliance.current();
|
|
53
|
+
return { posture: posture, regulated: compliance.isCrossBorderRegulated(posture) };
|
|
54
|
+
} catch (_e) { return { posture: null, regulated: false }; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Local-SQLite write-residency gate (GDPR Art 44-46 / PIPL Art 38 /
|
|
58
|
+
// DPDP §16 cross-border transfer restrictions). Runs on the PLAINTEXT
|
|
59
|
+
// row before sealRow so the tag column is readable even when other
|
|
60
|
+
// columns seal. Two layers:
|
|
61
|
+
//
|
|
62
|
+
// 1. Per-ROW tag (declarePerRowResidency): on INSERT the declared
|
|
63
|
+
// column must be present and within allowedTags; under a
|
|
64
|
+
// regulated posture a tag outside the deployment's region set
|
|
65
|
+
// ({ region } + allowedStorageRegions from db.init's
|
|
66
|
+
// dataResidency) refuses the write. UPDATEs gate only when the
|
|
67
|
+
// change set touches the residency column (an update that does
|
|
68
|
+
// not move residency is not a transfer).
|
|
69
|
+
// 2. Per-COLUMN tags (declareColumnResidency): the long-advertised
|
|
70
|
+
// assertColumnResidency gate, enforced here against the
|
|
71
|
+
// deployment region. Operators tag columns with the region
|
|
72
|
+
// value their dataResidency declares.
|
|
73
|
+
//
|
|
74
|
+
// Unregulated postures audit (drop-silent) and pass; tables with no
|
|
75
|
+
// declaration are untouched.
|
|
76
|
+
function _assertLocalResidency(table, plaintextRow, op) {
|
|
77
|
+
var spec = cryptoField.getPerRowResidency(table);
|
|
78
|
+
var colMap = cryptoField.getColumnResidency(table);
|
|
79
|
+
if (!spec && !colMap) return;
|
|
80
|
+
|
|
81
|
+
var residency = null;
|
|
82
|
+
try { residency = db().getDataResidency(); } catch (_e) { residency = null; }
|
|
83
|
+
var region = residency && residency.region ? residency.region : null;
|
|
84
|
+
var allowedRegions = region
|
|
85
|
+
? [region].concat(Array.isArray(residency.allowedStorageRegions)
|
|
86
|
+
? residency.allowedStorageRegions : [])
|
|
87
|
+
: null;
|
|
88
|
+
var state = _postureState();
|
|
89
|
+
var posture = state.posture;
|
|
90
|
+
var regulated = state.regulated;
|
|
91
|
+
|
|
92
|
+
if (spec) {
|
|
93
|
+
var tag = plaintextRow[spec.residencyColumn];
|
|
94
|
+
var tagPresent = tag !== undefined && tag !== null;
|
|
95
|
+
var colInChangeSet = Object.prototype.hasOwnProperty.call(plaintextRow, spec.residencyColumn);
|
|
96
|
+
if (op === "insert" && !tagPresent) {
|
|
97
|
+
throw new DbQueryError("db-query/row-residency-tag-missing",
|
|
98
|
+
op + ": table '" + table + "' declares per-row residency on column '" +
|
|
99
|
+
spec.residencyColumn + "' — every inserted row must carry a tag from [" +
|
|
100
|
+
spec.allowedTags.join(", ") + "]", true);
|
|
101
|
+
}
|
|
102
|
+
// An UPDATE that explicitly sets the residency column to null /
|
|
103
|
+
// undefined would clear the row's region binding (INSERT refuses a
|
|
104
|
+
// missing tag; the same row must not be nullable into an untagged
|
|
105
|
+
// state on update). UPDATEs that don't touch the column pass.
|
|
106
|
+
if (op === "update" && colInChangeSet && !tagPresent) {
|
|
107
|
+
throw new DbQueryError("db-query/row-residency-tag-missing",
|
|
108
|
+
op + ": table '" + table + "' residency column '" + spec.residencyColumn +
|
|
109
|
+
"' cannot be cleared — set a tag from [" + spec.allowedTags.join(", ") + "]", true);
|
|
110
|
+
}
|
|
111
|
+
if (tagPresent) {
|
|
112
|
+
if (typeof tag !== "string" || spec.allowedTags.indexOf(tag) === -1) {
|
|
113
|
+
throw new DbQueryError("db-query/row-residency-tag-invalid",
|
|
114
|
+
op + ": table '" + table + "' residency tag '" + tag +
|
|
115
|
+
"' is not in allowedTags [" + spec.allowedTags.join(", ") + "]", true);
|
|
116
|
+
}
|
|
117
|
+
if (tag !== "global" && tag !== "unrestricted" && allowedRegions &&
|
|
118
|
+
allowedRegions.indexOf(tag) === -1) {
|
|
119
|
+
if (regulated) {
|
|
120
|
+
audit.safeEmit({ action: "db.residency.gate.rejected", outcome: "denied",
|
|
121
|
+
metadata: { table: table, rowTag: tag, region: region, posture: posture,
|
|
122
|
+
operation: op, scope: "local" } });
|
|
123
|
+
throw new DbQueryError("db-query/row-residency-local-mismatch",
|
|
124
|
+
op + ": row residency tag '" + tag + "' is outside this deployment's " +
|
|
125
|
+
"region set [" + allowedRegions.join(", ") + "] under '" + posture +
|
|
126
|
+
"' posture (cross-border transfer refused)", true);
|
|
127
|
+
}
|
|
128
|
+
audit.safeEmit({ action: "db.residency.gate.advisory", outcome: "info",
|
|
129
|
+
metadata: { table: table, rowTag: tag, region: region, posture: posture || null,
|
|
130
|
+
operation: op, scope: "local" } });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (colMap && region) {
|
|
136
|
+
var refusal = cryptoField.assertColumnResidency(table, plaintextRow, { backendTag: region });
|
|
137
|
+
if (refusal) {
|
|
138
|
+
if (regulated) {
|
|
139
|
+
audit.safeEmit({ action: "db.column_residency.gate.rejected", outcome: "denied",
|
|
140
|
+
metadata: { table: refusal.table, column: refusal.column, want: refusal.want,
|
|
141
|
+
got: refusal.got, posture: posture, operation: op, scope: "local" } });
|
|
142
|
+
throw new DbQueryError("db-query/column-residency-mismatch",
|
|
143
|
+
op + ": column '" + refusal.column + "' on table '" + refusal.table +
|
|
144
|
+
"' is bound to residency '" + refusal.want + "' but this deployment's " +
|
|
145
|
+
"region is '" + refusal.got + "' under '" + posture + "' posture", true);
|
|
146
|
+
}
|
|
147
|
+
audit.safeEmit({ action: "db.residency.gate.advisory", outcome: "info",
|
|
148
|
+
metadata: { table: refusal.table, column: refusal.column, want: refusal.want,
|
|
149
|
+
got: refusal.got, posture: posture || null, operation: op, scope: "local" } });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
37
153
|
|
|
38
154
|
// "@>" / "?" / "?|" / "?&" are JSONB containment + key-existence
|
|
39
155
|
// operators. Routed through safeJsonPath validation before binding so
|
|
@@ -393,7 +509,9 @@ class Query {
|
|
|
393
509
|
this._whereClause() + this._orderLimitOffset() + " LIMIT 1";
|
|
394
510
|
var stmt = this._db.prepare(sql);
|
|
395
511
|
var row = stmt.get.apply(stmt, this._whereParams);
|
|
396
|
-
|
|
512
|
+
// 4th arg (dbHandle) lets unsealRow fetch + unwrap the row-scoped
|
|
513
|
+
// K_row for vault.row: cells (declarePerRowKey tables).
|
|
514
|
+
return row ? cryptoField.unsealRow(this._cryptoFieldKey(), row, undefined, this._db) : null;
|
|
397
515
|
}
|
|
398
516
|
|
|
399
517
|
all() {
|
|
@@ -403,8 +521,9 @@ class Query {
|
|
|
403
521
|
var rows = stmt.all.apply(stmt, this._whereParams);
|
|
404
522
|
var out = new Array(rows.length);
|
|
405
523
|
var key = this._cryptoFieldKey();
|
|
524
|
+
var dbHandle = this._db;
|
|
406
525
|
for (var i = 0; i < rows.length; i++) {
|
|
407
|
-
out[i] = cryptoField.unsealRow(key, rows[i]);
|
|
526
|
+
out[i] = cryptoField.unsealRow(key, rows[i], undefined, dbHandle);
|
|
408
527
|
}
|
|
409
528
|
return out;
|
|
410
529
|
}
|
|
@@ -433,6 +552,7 @@ class Query {
|
|
|
433
552
|
}
|
|
434
553
|
var stmt = this._db.prepare(sql);
|
|
435
554
|
var key = this._cryptoFieldKey();
|
|
555
|
+
var dbHandle = this._db;
|
|
436
556
|
var iter;
|
|
437
557
|
try { iter = stmt.iterate.apply(stmt, this._whereParams); }
|
|
438
558
|
catch (e) {
|
|
@@ -454,7 +574,7 @@ class Query {
|
|
|
454
574
|
var step = iter.next();
|
|
455
575
|
if (step.done) { this.push(null); return; }
|
|
456
576
|
emitted += 1;
|
|
457
|
-
this.push(cryptoField.unsealRow(key, step.value));
|
|
577
|
+
this.push(cryptoField.unsealRow(key, step.value, undefined, dbHandle));
|
|
458
578
|
} catch (e) {
|
|
459
579
|
this.destroy(e);
|
|
460
580
|
}
|
|
@@ -477,7 +597,22 @@ class Query {
|
|
|
477
597
|
if (withId._id === undefined || withId._id === null) {
|
|
478
598
|
withId._id = generateToken(C.BYTES.bytes(16));
|
|
479
599
|
}
|
|
480
|
-
|
|
600
|
+
// Residency gates read the PLAINTEXT row (the tag column must be
|
|
601
|
+
// inspectable even when sibling columns seal below).
|
|
602
|
+
_assertLocalResidency(this._cryptoFieldKey(), withId, "insert");
|
|
603
|
+
// Per-row-key tables (declarePerRowKey): materialize a fresh K_row
|
|
604
|
+
// BEFORE sealRow so sealed columns encrypt under the row-scoped key
|
|
605
|
+
// (vault.row: cells). rowId MUST be withId._id — the same value
|
|
606
|
+
// b.subject.eraseHard / b.retention destroy on, so a later shred
|
|
607
|
+
// makes these cells undecryptable. Materialize stores the random
|
|
608
|
+
// row-secret AAD-sealed in _blamejs_per_row_keys.
|
|
609
|
+
var sealOpts;
|
|
610
|
+
var cfKey = this._cryptoFieldKey();
|
|
611
|
+
if (cryptoField.hasPerRowKey(cfKey)) {
|
|
612
|
+
var kRow = cryptoField.materializePerRowKey(cfKey, withId._id, this._db);
|
|
613
|
+
sealOpts = { kRow: kRow, rowId: withId._id };
|
|
614
|
+
}
|
|
615
|
+
var sealed = cryptoField.sealRow(cfKey, withId, sealOpts);
|
|
481
616
|
var cols = Object.keys(sealed);
|
|
482
617
|
var placeholders = cols.map(function () { return "?"; }).join(", ");
|
|
483
618
|
var quotedCols = cols.map(function (c) { return '"' + c + '"'; }).join(", ");
|
|
@@ -514,7 +649,20 @@ class Query {
|
|
|
514
649
|
if (this._where.length === 0) {
|
|
515
650
|
throw new Error("refusing unconditional update — call where(...) first");
|
|
516
651
|
}
|
|
517
|
-
|
|
652
|
+
// Residency gates on the plaintext change set — an UPDATE that
|
|
653
|
+
// touches the residency tag (or a region-bound column) is a
|
|
654
|
+
// transfer and goes through the same refusal matrix as INSERT.
|
|
655
|
+
_assertLocalResidency(this._cryptoFieldKey(), changes, "update");
|
|
656
|
+
var cfKey = this._cryptoFieldKey();
|
|
657
|
+
// Per-row-key tables: sealed columns must re-encrypt under EACH
|
|
658
|
+
// affected row's own K_row, so a single set-based UPDATE can't seal
|
|
659
|
+
// one value across rows. Resolve the affected _id set, then seal +
|
|
660
|
+
// write each row under its row-scoped key. Idempotent materialize
|
|
661
|
+
// re-derives the existing K_row (created on INSERT).
|
|
662
|
+
if (cryptoField.hasPerRowKey(cfKey)) {
|
|
663
|
+
return this._updatePerRowKey(cfKey, changes, single);
|
|
664
|
+
}
|
|
665
|
+
var sealed = cryptoField.sealRow(cfKey, changes);
|
|
518
666
|
var setKeys = Object.keys(sealed);
|
|
519
667
|
if (setKeys.length === 0) {
|
|
520
668
|
throw new Error("update changes object is empty");
|
|
@@ -543,6 +691,41 @@ class Query {
|
|
|
543
691
|
return info.changes;
|
|
544
692
|
}
|
|
545
693
|
|
|
694
|
+
// Per-row-key UPDATE. Sealed columns on a declarePerRowKey table are
|
|
695
|
+
// K_row cells (vault.row:), so each affected row must be re-sealed
|
|
696
|
+
// under its OWN K_row — a single set-based UPDATE can't carry per-row
|
|
697
|
+
// ciphertext. Resolve the affected _id set via the WHERE, then for
|
|
698
|
+
// each row: materialize (idempotent) its K_row, seal the change set
|
|
699
|
+
// under it (derived hashes computed from plaintext as usual), and
|
|
700
|
+
// UPDATE that single row by _id. `single` stops after the first row.
|
|
701
|
+
_updatePerRowKey(cfKey, changes, single) {
|
|
702
|
+
var whereSql = this._where.join(" AND ");
|
|
703
|
+
var qt = this._quotedTable();
|
|
704
|
+
var idStmt = this._db.prepare(
|
|
705
|
+
"SELECT _id FROM " + qt + " WHERE " + whereSql + (single ? " LIMIT 1" : ""));
|
|
706
|
+
var idRows = idStmt.all.apply(idStmt, this._whereParams);
|
|
707
|
+
var changed = 0;
|
|
708
|
+
for (var r = 0; r < idRows.length; r++) {
|
|
709
|
+
var rowId = idRows[r]._id;
|
|
710
|
+
if (rowId === undefined || rowId === null) continue;
|
|
711
|
+
var kRow = cryptoField.materializePerRowKey(cfKey, rowId, this._db);
|
|
712
|
+
var sealed = cryptoField.sealRow(cfKey, changes, { kRow: kRow, rowId: rowId });
|
|
713
|
+
var setKeys = Object.keys(sealed);
|
|
714
|
+
if (setKeys.length === 0) {
|
|
715
|
+
throw new Error("update changes object is empty");
|
|
716
|
+
}
|
|
717
|
+
setKeys.forEach(_validateField);
|
|
718
|
+
var selfUpd = this;
|
|
719
|
+
setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
|
|
720
|
+
var setClause = setKeys.map(function (k) { return '"' + k + '" = ?'; }).join(", ");
|
|
721
|
+
var setValues = setKeys.map(function (k) { return sealed[k]; });
|
|
722
|
+
var updStmt = this._db.prepare("UPDATE " + qt + " SET " + setClause + " WHERE _id = ?");
|
|
723
|
+
var info = updStmt.run.apply(updStmt, setValues.concat([rowId]));
|
|
724
|
+
changed += (info && info.changes) || 0;
|
|
725
|
+
}
|
|
726
|
+
return changed;
|
|
727
|
+
}
|
|
728
|
+
|
|
546
729
|
deleteOne() {
|
|
547
730
|
return this._delete(true) > 0;
|
|
548
731
|
}
|
package/lib/db.js
CHANGED
|
@@ -295,11 +295,21 @@ var FRAMEWORK_SCHEMA = [
|
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
// Per-row crypto-erasure key registry — per-row keys.
|
|
298
|
-
// Each entry holds
|
|
299
|
-
// rowId)
|
|
300
|
-
//
|
|
298
|
+
// Each entry holds the AAD-sealed random row-secret keyed by
|
|
299
|
+
// (tableName, rowId); the row-scoped K_row is derived from it.
|
|
300
|
+
// b.subject.eraseHard / b.retention destroy the entry, leaving WAL /
|
|
301
|
+
// replica residuals undecryptable. wrappedKey is registered as an
|
|
302
|
+
// AAD-bound sealed field (aad:true, rowIdField:"rowId") so a vault
|
|
303
|
+
// keypair rotation auto-reseals it old-root -> new-root via
|
|
304
|
+
// rotate._rotateColumn — without this a rotation would orphan every
|
|
305
|
+
// wrapped secret and brick every keyed row.
|
|
301
306
|
name: "_blamejs_per_row_keys",
|
|
302
307
|
columns: {
|
|
308
|
+
// _id is the rotation pipeline's keyset-pagination + UPDATE key
|
|
309
|
+
// (rotate._rotateColumn SELECTs _id and orders by it); the natural
|
|
310
|
+
// identity stays the composite (tableName, rowId). materialize
|
|
311
|
+
// populates _id with a fresh token.
|
|
312
|
+
_id: "TEXT",
|
|
303
313
|
tableName: "TEXT NOT NULL",
|
|
304
314
|
rowId: "TEXT NOT NULL",
|
|
305
315
|
wrappedKey: "BLOB NOT NULL",
|
|
@@ -307,6 +317,10 @@ var FRAMEWORK_SCHEMA = [
|
|
|
307
317
|
},
|
|
308
318
|
primaryKey: ["tableName", "rowId"],
|
|
309
319
|
indexes: [],
|
|
320
|
+
sealedFields: ["wrappedKey"],
|
|
321
|
+
aad: true,
|
|
322
|
+
rowIdField: "rowId",
|
|
323
|
+
schemaVersion: "1",
|
|
310
324
|
},
|
|
311
325
|
{
|
|
312
326
|
// Operator-declared WORM (write-once-read-many) registry. Each
|
|
@@ -75,6 +75,16 @@ var Q_TRACKING = '"' + TRACKING_TABLE + '"';
|
|
|
75
75
|
var Q_LOCK = '"' + LOCK_TABLE + '"';
|
|
76
76
|
var Q_HISTORY = '"' + HISTORY_TABLE + '"';
|
|
77
77
|
|
|
78
|
+
// The migration tracking / history / lock tables hold framework
|
|
79
|
+
// bookkeeping ("migration X ran at time T"), not region-bound personal
|
|
80
|
+
// data, so their writes carry the residency-neutral "unrestricted" tag
|
|
81
|
+
// — the per-row residency write gate (b.externalDb.query) refuses DML
|
|
82
|
+
// to a residency-tagged backend under a cross-border regulated posture
|
|
83
|
+
// unless a compatible rowResidencyTag is supplied. Operator migration
|
|
84
|
+
// DML (mod.up) stays subject to the gate; only these internal writes
|
|
85
|
+
// are exempt. Passed as the per-statement opts override on the txClient.
|
|
86
|
+
var FRAMEWORK_METADATA_OPTS = Object.freeze({ rowResidencyTag: "unrestricted" });
|
|
87
|
+
|
|
78
88
|
// Bytes that get signed for one history row. Stable forever — changing
|
|
79
89
|
// it invalidates every prior signature.
|
|
80
90
|
var HISTORY_SIGNATURE_FORMAT = "blamejs-schema-history-v1";
|
|
@@ -187,7 +197,8 @@ async function _writeHistoryRow(xdb, row) {
|
|
|
187
197
|
row.schemaIntrospectionHash,
|
|
188
198
|
row.signature,
|
|
189
199
|
row.publicKeyFingerprint,
|
|
190
|
-
]
|
|
200
|
+
],
|
|
201
|
+
FRAMEWORK_METADATA_OPTS
|
|
191
202
|
);
|
|
192
203
|
}
|
|
193
204
|
|
|
@@ -221,7 +232,7 @@ async function _acquireLock(xdb, opts) {
|
|
|
221
232
|
try {
|
|
222
233
|
await xdb.query(
|
|
223
234
|
"INSERT INTO " + Q_LOCK + " (scope, lockedAt, lockedBy) VALUES ('lock', $1, $2)",
|
|
224
|
-
[nowMs, holder]
|
|
235
|
+
[nowMs, holder], FRAMEWORK_METADATA_OPTS
|
|
225
236
|
);
|
|
226
237
|
return { holder: holder, takeoverFrom: null, takeoverAgeMs: 0 };
|
|
227
238
|
} catch (_e) {
|
|
@@ -235,7 +246,7 @@ async function _acquireLock(xdb, opts) {
|
|
|
235
246
|
try {
|
|
236
247
|
await xdb.query(
|
|
237
248
|
"INSERT INTO " + Q_LOCK + " (scope, lockedAt, lockedBy) VALUES ('lock', $1, $2)",
|
|
238
|
-
[nowMs, holder]
|
|
249
|
+
[nowMs, holder], FRAMEWORK_METADATA_OPTS
|
|
239
250
|
);
|
|
240
251
|
return { holder: holder, takeoverFrom: null, takeoverAgeMs: 0 };
|
|
241
252
|
} catch (e2) {
|
|
@@ -250,11 +261,11 @@ async function _acquireLock(xdb, opts) {
|
|
|
250
261
|
var prevHolder = existing.lockedby || existing.lockedBy;
|
|
251
262
|
await xdb.query(
|
|
252
263
|
"DELETE FROM " + Q_LOCK + " WHERE scope = 'lock' AND lockedAt = $1",
|
|
253
|
-
[Number(existing.lockedat || existing.lockedAt)]
|
|
264
|
+
[Number(existing.lockedat || existing.lockedAt)], FRAMEWORK_METADATA_OPTS
|
|
254
265
|
);
|
|
255
266
|
await xdb.query(
|
|
256
267
|
"INSERT INTO " + Q_LOCK + " (scope, lockedAt, lockedBy) VALUES ('lock', $1, $2)",
|
|
257
|
-
[nowMs, holder]
|
|
268
|
+
[nowMs, holder], FRAMEWORK_METADATA_OPTS
|
|
258
269
|
);
|
|
259
270
|
return { holder: holder, takeoverFrom: prevHolder, takeoverAgeMs: ageMs };
|
|
260
271
|
}
|
|
@@ -269,7 +280,7 @@ async function _releaseLock(xdb, holder) {
|
|
|
269
280
|
try {
|
|
270
281
|
await xdb.query(
|
|
271
282
|
"DELETE FROM " + Q_LOCK + " WHERE scope = 'lock' AND lockedBy = $1",
|
|
272
|
-
[holder]
|
|
283
|
+
[holder], FRAMEWORK_METADATA_OPTS
|
|
273
284
|
);
|
|
274
285
|
} catch (_e) {
|
|
275
286
|
// best-effort release; operator can DELETE manually.
|
|
@@ -441,7 +452,8 @@ function create(opts) {
|
|
|
441
452
|
await xdb.query(
|
|
442
453
|
"INSERT INTO " + Q_TRACKING +
|
|
443
454
|
" (name, description, appliedAt) VALUES ($1, $2, $3)",
|
|
444
|
-
[file, mod.description || "", ranAt]
|
|
455
|
+
[file, mod.description || "", ranAt],
|
|
456
|
+
FRAMEWORK_METADATA_OPTS
|
|
445
457
|
);
|
|
446
458
|
// Schema-version history with signature. Sign post-INSERT
|
|
447
459
|
// so the introspection hash reflects the row that just
|