@blamejs/core 0.14.21 → 0.14.24
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 +5 -1
- package/lib/auth/jar.js +190 -28
- package/lib/auth/jwt-external.js +213 -0
- package/lib/auth/oauth.js +115 -101
- package/lib/compliance.js +37 -0
- package/lib/crypto-field.js +111 -5
- package/lib/db-query.js +123 -0
- package/lib/external-db-migrate.js +19 -7
- package/lib/external-db.js +508 -20
- package/lib/framework-error.js +6 -0
- package/lib/http-client.js +3 -4
- package/lib/lro.js +3 -4
- 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/deny-response.js +2 -10
- package/lib/middleware/health.js +1 -4
- package/lib/middleware/trace-log-correlation.js +3 -6
- package/lib/validate-opts.js +34 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/crypto-field.js
CHANGED
|
@@ -132,6 +132,11 @@ var schemas = Object.create(null);
|
|
|
132
132
|
//
|
|
133
133
|
// { tableName: { columnName: "eu" | "us" | "global" | <tag> } }
|
|
134
134
|
var columnResidency = Object.create(null);
|
|
135
|
+
// Per-ROW residency registry — table → { residencyColumn, allowedTags }.
|
|
136
|
+
// The row-level sibling of columnResidency: one plaintext column on each
|
|
137
|
+
// row carries that row's residency tag; write gates refuse a tagged row
|
|
138
|
+
// landing on an incompatible backend.
|
|
139
|
+
var perRowResidency = Object.create(null);
|
|
135
140
|
|
|
136
141
|
// Per-row key declaration registry. For tables that opt
|
|
137
142
|
// into per-row keying, b.subject.eraseHard deletes the wrapped K_row
|
|
@@ -1105,7 +1110,7 @@ function getColumnResidency(table) {
|
|
|
1105
1110
|
* @signature b.cryptoField.assertColumnResidency(table, row, args)
|
|
1106
1111
|
* @since 0.7.27
|
|
1107
1112
|
* @compliance gdpr
|
|
1108
|
-
* @related b.cryptoField.declareColumnResidency
|
|
1113
|
+
* @related b.cryptoField.declareColumnResidency, b.cryptoField.declarePerRowResidency
|
|
1109
1114
|
*
|
|
1110
1115
|
* Storage-write gate. Storage backends call this with the proposed
|
|
1111
1116
|
* row before the SQL hits the wire; refusal under regulated postures
|
|
@@ -1161,6 +1166,104 @@ function assertColumnResidency(table, row, args) {
|
|
|
1161
1166
|
return null;
|
|
1162
1167
|
}
|
|
1163
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* @primitive b.cryptoField.declarePerRowResidency
|
|
1171
|
+
* @signature b.cryptoField.declarePerRowResidency(table, opts)
|
|
1172
|
+
* @since 0.14.24
|
|
1173
|
+
* @compliance gdpr
|
|
1174
|
+
* @related b.cryptoField.getPerRowResidency, b.cryptoField.declareColumnResidency
|
|
1175
|
+
*
|
|
1176
|
+
* Declares per-ROW data residency for `table`: one plaintext column on
|
|
1177
|
+
* each row carries that row's residency tag, and the write gates
|
|
1178
|
+
* refuse a tagged row landing on an incompatible backend. The sibling
|
|
1179
|
+
* of `declareColumnResidency` — columns answer "which fields are
|
|
1180
|
+
* region-bound", rows answer "which region does THIS record belong
|
|
1181
|
+
* to" (an EU user's row next to a US user's row in the same table).
|
|
1182
|
+
* Local writes (`b.db.from(...).insertOne` / `.update`) enforce the
|
|
1183
|
+
* tag against the deployment's `dataResidency` region set under
|
|
1184
|
+
* cross-border regulated postures; external writes
|
|
1185
|
+
* (`b.externalDb.query`) take the tag per call via
|
|
1186
|
+
* `opts.rowResidencyTag` because raw SQL carries no row object. Rows
|
|
1187
|
+
* tagged "global" or "unrestricted" pass any backend. Throws on bad
|
|
1188
|
+
* input (config-time fail-loud).
|
|
1189
|
+
*
|
|
1190
|
+
* @opts
|
|
1191
|
+
* residencyColumn: string, // plaintext column carrying the row's tag
|
|
1192
|
+
* allowedTags: string[], // whitelist of valid tag values ("eu", "us", "global", region names)
|
|
1193
|
+
*
|
|
1194
|
+
* @example
|
|
1195
|
+
* b.cryptoField.declarePerRowResidency("users", {
|
|
1196
|
+
* residencyColumn: "dataRegion",
|
|
1197
|
+
* allowedTags: ["eu-west-1", "us-east-1", "global"],
|
|
1198
|
+
* });
|
|
1199
|
+
* var spec = b.cryptoField.getPerRowResidency("users");
|
|
1200
|
+
* spec.residencyColumn; // → "dataRegion"
|
|
1201
|
+
*/
|
|
1202
|
+
function declarePerRowResidency(table, opts) {
|
|
1203
|
+
validateOpts.requireNonEmptyString(table, "declarePerRowResidency: table",
|
|
1204
|
+
CryptoFieldError, "crypto-field/per-row-residency-table-empty");
|
|
1205
|
+
validateOpts.requireObject(opts, "declarePerRowResidency",
|
|
1206
|
+
CryptoFieldError, "crypto-field/per-row-residency-opts-not-object");
|
|
1207
|
+
validateOpts(opts, ["residencyColumn", "allowedTags"], "cryptoField.declarePerRowResidency");
|
|
1208
|
+
validateOpts.requireNonEmptyString(opts.residencyColumn,
|
|
1209
|
+
"declarePerRowResidency: opts.residencyColumn",
|
|
1210
|
+
CryptoFieldError, "crypto-field/per-row-residency-column-invalid");
|
|
1211
|
+
if (!Array.isArray(opts.allowedTags) || opts.allowedTags.length === 0) {
|
|
1212
|
+
throw new CryptoFieldError("crypto-field/per-row-residency-tags-invalid",
|
|
1213
|
+
"declarePerRowResidency: opts.allowedTags must be a non-empty array of tag strings");
|
|
1214
|
+
}
|
|
1215
|
+
validateOpts.optionalNonEmptyStringArray(opts.allowedTags,
|
|
1216
|
+
"declarePerRowResidency: opts.allowedTags",
|
|
1217
|
+
CryptoFieldError, "crypto-field/per-row-residency-tag-empty");
|
|
1218
|
+
// The residency tag column MUST stay plaintext — the write gate reads
|
|
1219
|
+
// it on every INSERT / UPDATE before sealRow, and reads return it
|
|
1220
|
+
// verbatim. A sealed residency column would be ciphertext the gate
|
|
1221
|
+
// can't compare and reads can't surface. Refuse the misconfiguration
|
|
1222
|
+
// at declaration time when the table's sealed-field set is already
|
|
1223
|
+
// known (registration order permitting).
|
|
1224
|
+
var sealed = getSealedFields(table);
|
|
1225
|
+
if (Array.isArray(sealed) && sealed.indexOf(opts.residencyColumn) !== -1) {
|
|
1226
|
+
throw new CryptoFieldError("crypto-field/per-row-residency-sealed-conflict",
|
|
1227
|
+
"declarePerRowResidency: residencyColumn '" + opts.residencyColumn +
|
|
1228
|
+
"' is a sealed field on table '" + table + "' — the residency tag must " +
|
|
1229
|
+
"stay plaintext so the write gate can read it. Choose a non-sealed column");
|
|
1230
|
+
}
|
|
1231
|
+
perRowResidency[table] = {
|
|
1232
|
+
residencyColumn: opts.residencyColumn,
|
|
1233
|
+
allowedTags: opts.allowedTags.slice(),
|
|
1234
|
+
};
|
|
1235
|
+
return {
|
|
1236
|
+
table: table,
|
|
1237
|
+
residencyColumn: opts.residencyColumn,
|
|
1238
|
+
allowedTags: opts.allowedTags.slice(),
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* @primitive b.cryptoField.getPerRowResidency
|
|
1244
|
+
* @signature b.cryptoField.getPerRowResidency(table)
|
|
1245
|
+
* @since 0.14.24
|
|
1246
|
+
* @related b.cryptoField.declarePerRowResidency
|
|
1247
|
+
*
|
|
1248
|
+
* Returns the per-row residency spec declared for `table`
|
|
1249
|
+
* (`{ residencyColumn, allowedTags }`), or null when the table has no
|
|
1250
|
+
* declaration. Read-only — storage backends call this at the write
|
|
1251
|
+
* boundary to decide whether the row-residency gate applies.
|
|
1252
|
+
*
|
|
1253
|
+
* @example
|
|
1254
|
+
* b.cryptoField.declarePerRowResidency("users", {
|
|
1255
|
+
* residencyColumn: "dataRegion",
|
|
1256
|
+
* allowedTags: ["eu-west-1", "global"],
|
|
1257
|
+
* });
|
|
1258
|
+
* b.cryptoField.getPerRowResidency("users").allowedTags; // → ["eu-west-1", "global"]
|
|
1259
|
+
* b.cryptoField.getPerRowResidency("unknown"); // → null
|
|
1260
|
+
*/
|
|
1261
|
+
function getPerRowResidency(table) {
|
|
1262
|
+
var spec = perRowResidency[table];
|
|
1263
|
+
if (!spec) return null;
|
|
1264
|
+
return { residencyColumn: spec.residencyColumn, allowedTags: spec.allowedTags.slice() };
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1164
1267
|
/**
|
|
1165
1268
|
* @primitive b.cryptoField.declarePerRowKey
|
|
1166
1269
|
* @signature b.cryptoField.declarePerRowKey(table, opts)
|
|
@@ -1336,10 +1439,10 @@ function destroyPerRowKey(table, rowId, dbHandle) {
|
|
|
1336
1439
|
* @related b.cryptoField.declareColumnResidency, b.cryptoField.declarePerRowKey
|
|
1337
1440
|
*
|
|
1338
1441
|
* Test-only helper. Drops every entry from the per-column residency
|
|
1339
|
-
* registry
|
|
1340
|
-
* re-declare
|
|
1341
|
-
*
|
|
1342
|
-
* boot.
|
|
1442
|
+
* registry, the per-row residency registry, and the per-row-key
|
|
1443
|
+
* registry so a test fixture can re-declare them between cases.
|
|
1444
|
+
* Operator code never calls this — production declarations come from
|
|
1445
|
+
* `b.db.init({ schema })` once at boot.
|
|
1343
1446
|
*
|
|
1344
1447
|
* @example
|
|
1345
1448
|
* b.cryptoField.declareColumnResidency("users", {
|
|
@@ -1351,6 +1454,7 @@ function destroyPerRowKey(table, rowId, dbHandle) {
|
|
|
1351
1454
|
function clearResidencyForTest() {
|
|
1352
1455
|
for (var t in columnResidency) delete columnResidency[t];
|
|
1353
1456
|
for (var u in perRowKeyTables) delete perRowKeyTables[u];
|
|
1457
|
+
for (var v in perRowResidency) delete perRowResidency[v];
|
|
1354
1458
|
}
|
|
1355
1459
|
|
|
1356
1460
|
module.exports = {
|
|
@@ -1382,6 +1486,8 @@ module.exports = {
|
|
|
1382
1486
|
declareColumnResidency: declareColumnResidency,
|
|
1383
1487
|
getColumnResidency: getColumnResidency,
|
|
1384
1488
|
assertColumnResidency: assertColumnResidency,
|
|
1489
|
+
declarePerRowResidency: declarePerRowResidency,
|
|
1490
|
+
getPerRowResidency: getPerRowResidency,
|
|
1385
1491
|
declarePerRowKey: declarePerRowKey,
|
|
1386
1492
|
hasPerRowKey: hasPerRowKey,
|
|
1387
1493
|
materializePerRowKey: materializePerRowKey,
|
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
|
|
@@ -477,6 +593,9 @@ class Query {
|
|
|
477
593
|
if (withId._id === undefined || withId._id === null) {
|
|
478
594
|
withId._id = generateToken(C.BYTES.bytes(16));
|
|
479
595
|
}
|
|
596
|
+
// Residency gates read the PLAINTEXT row (the tag column must be
|
|
597
|
+
// inspectable even when sibling columns seal below).
|
|
598
|
+
_assertLocalResidency(this._cryptoFieldKey(), withId, "insert");
|
|
480
599
|
var sealed = cryptoField.sealRow(this._cryptoFieldKey(), withId);
|
|
481
600
|
var cols = Object.keys(sealed);
|
|
482
601
|
var placeholders = cols.map(function () { return "?"; }).join(", ");
|
|
@@ -514,6 +633,10 @@ class Query {
|
|
|
514
633
|
if (this._where.length === 0) {
|
|
515
634
|
throw new Error("refusing unconditional update — call where(...) first");
|
|
516
635
|
}
|
|
636
|
+
// Residency gates on the plaintext change set — an UPDATE that
|
|
637
|
+
// touches the residency tag (or a region-bound column) is a
|
|
638
|
+
// transfer and goes through the same refusal matrix as INSERT.
|
|
639
|
+
_assertLocalResidency(this._cryptoFieldKey(), changes, "update");
|
|
517
640
|
var sealed = cryptoField.sealRow(this._cryptoFieldKey(), changes);
|
|
518
641
|
var setKeys = Object.keys(sealed);
|
|
519
642
|
if (setKeys.length === 0) {
|
|
@@ -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
|