@blamejs/core 0.14.27 → 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 +4 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +29 -1
- 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 +218 -100
- 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 +22 -0
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +387 -91
- 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/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/framework-error.js +11 -0
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +596 -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 +14 -0
- 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-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- 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 +31 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/outbox.js +136 -82
- 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/retention.js +82 -39
- 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/static.js +45 -7
- 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 +16 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/break-glass.js
CHANGED
|
@@ -49,6 +49,7 @@ var requestHelpers = require("./request-helpers");
|
|
|
49
49
|
var safeAsync = require("./safe-async");
|
|
50
50
|
var safeJson = require("./safe-json");
|
|
51
51
|
var safeSql = require("./safe-sql");
|
|
52
|
+
var sql = require("./sql");
|
|
52
53
|
var totp = require("./totp");
|
|
53
54
|
var validateOpts = require("./validate-opts");
|
|
54
55
|
var { defineClass } = require("./framework-error");
|
|
@@ -86,6 +87,34 @@ var DEFAULT_AUDIT_REASON = "cleartext";
|
|
|
86
87
|
var ALLOWED_FACTORS = ["totp", "passkey"];
|
|
87
88
|
var ALLOWED_REASON_STORAGE = ["cleartext", "hmac", "both"];
|
|
88
89
|
|
|
90
|
+
// cryptoField REGISTRY KEYS for the two break-glass framework tables. These
|
|
91
|
+
// are the names db.js's FRAMEWORK_SCHEMA registered the tables under, so
|
|
92
|
+
// seal / unseal / computeDerived must key off the byte-identical literal —
|
|
93
|
+
// resolving them through frameworkSchema.tableName would diverge the seal-side
|
|
94
|
+
// key from the registration under a custom prefix and break decryption. (SQL
|
|
95
|
+
// composed via b.sql passes the SAME bare logical name so clusterStorage can
|
|
96
|
+
// rewrite the table reference; these constants cover only the cryptoField
|
|
97
|
+
// keying.) allow:hand-rolled-sql — cryptoField registry keys, not SQL text.
|
|
98
|
+
var POLICIES_TABLE = "_blamejs_break_glass_policies"; // allow:hand-rolled-sql
|
|
99
|
+
var GRANTS_TABLE = "_blamejs_break_glass_grants"; // allow:hand-rolled-sql
|
|
100
|
+
|
|
101
|
+
// b.sql opts for every statement break-glass dispatches through
|
|
102
|
+
// clusterStorage. Thread the ACTIVE backend dialect (clusterStorage.dialect()
|
|
103
|
+
// — "sqlite" single-node, "postgres" | "mysql" in cluster mode) so the
|
|
104
|
+
// emitted identifier quoting + dialect idioms (ON CONFLICT vs ON DUPLICATE
|
|
105
|
+
// KEY) match the backend the SQL dispatches to. Defaulting to "sqlite" works
|
|
106
|
+
// on Postgres only by accident (both double-quote identifiers) and emits the
|
|
107
|
+
// wrong quoting on MySQL. clusterStorage.execute still rewrites framework
|
|
108
|
+
// table names + translates `?` placeholders at dispatch; this controls only
|
|
109
|
+
// the builder-side quoting + idiom selection.
|
|
110
|
+
// _sqlOpts() — framework tables (policies / grants); name resolved bare,
|
|
111
|
+
// clusterStorage rewrites the prefix.
|
|
112
|
+
// _appSqlOpts() — the operator's glass-locked app table; quoteName so b.sql
|
|
113
|
+
// quotes the (validated) identifier, and it is NOT
|
|
114
|
+
// framework-rewritten.
|
|
115
|
+
function _sqlOpts() { return { dialect: clusterStorage.dialect() }; }
|
|
116
|
+
function _appSqlOpts() { return { dialect: clusterStorage.dialect(), quoteName: true }; }
|
|
117
|
+
|
|
89
118
|
// In-memory policy cache. Cluster-shared via the policies table; the
|
|
90
119
|
// cache short-circuits the DB roundtrip on the unsealRow hot path.
|
|
91
120
|
// Populated on first access per-table; invalidated on policy.set/delete.
|
|
@@ -157,10 +186,14 @@ async function _ensureDek(table) {
|
|
|
157
186
|
// DEK is vault-sealed and stored in the policy row's `dekSealed`
|
|
158
187
|
// column. Generated lazily on first use of cryptographic-mode for
|
|
159
188
|
// the table. Cached in-memory after first read.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
189
|
+
// The policy table is external-only; its LOGICAL name IS the
|
|
190
|
+
// `_blamejs_`-prefixed name (self-mapped in LOCAL_TO_EXTERNAL), passed
|
|
191
|
+
// bare to b.sql so clusterStorage rewrites + placeholderizes.
|
|
192
|
+
var dekReadBuilt = sql.select("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
|
|
193
|
+
.columns(["dekSealed"])
|
|
194
|
+
.where("tableName", table)
|
|
195
|
+
.toSql();
|
|
196
|
+
var rows = await clusterStorage.executeAll(dekReadBuilt.sql, dekReadBuilt.params);
|
|
164
197
|
if (!rows || rows.length === 0) {
|
|
165
198
|
throw new BreakGlassError("breakglass/policy-not-set",
|
|
166
199
|
"_ensureDek: no policy for table '" + table + "'", true);
|
|
@@ -172,10 +205,11 @@ async function _ensureDek(table) {
|
|
|
172
205
|
} else {
|
|
173
206
|
dek = generateBytes(DEK_BYTES);
|
|
174
207
|
var sealedDek = vault().seal(dek.toString("base64"));
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
208
|
+
var dekUpdBuilt = sql.update("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
|
|
209
|
+
.set({ dekSealed: sealedDek })
|
|
210
|
+
.where("tableName", table)
|
|
211
|
+
.toSql();
|
|
212
|
+
await clusterStorage.execute(dekUpdBuilt.sql, dekUpdBuilt.params);
|
|
179
213
|
}
|
|
180
214
|
dekCache.set(table, dek);
|
|
181
215
|
return dek;
|
|
@@ -347,14 +381,16 @@ async function migrate(table, opts) {
|
|
|
347
381
|
var lastId = "";
|
|
348
382
|
// Iterate via _id-keyset paging so we don't load the whole table into memory.
|
|
349
383
|
while (true) {
|
|
350
|
-
// table is already validated as a safe
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
var
|
|
355
|
-
"
|
|
356
|
-
|
|
357
|
-
|
|
384
|
+
// `table` is an operator app table (already validated as a safe
|
|
385
|
+
// identifier via _validatePolicySet). quoteName:true makes b.sql quote
|
|
386
|
+
// the name (reserved-word / case-sensitive safe); it is NOT a framework
|
|
387
|
+
// table, so clusterStorage's resolveTables leaves it untouched.
|
|
388
|
+
var pageBuilt = sql.select(table, _appSqlOpts())
|
|
389
|
+
.whereOp("_id", ">", lastId)
|
|
390
|
+
.orderBy("_id", "asc")
|
|
391
|
+
.limit(batchSize)
|
|
392
|
+
.toSql();
|
|
393
|
+
var rows = await clusterStorage.executeAll(pageBuilt.sql, pageBuilt.params);
|
|
358
394
|
if (!rows || rows.length === 0) break;
|
|
359
395
|
for (var i = 0; i < rows.length; i++) {
|
|
360
396
|
totalRows++;
|
|
@@ -378,16 +414,16 @@ async function migrate(table, opts) {
|
|
|
378
414
|
// the cell ciphertext stays as a literal string, not double-sealed.
|
|
379
415
|
var setCols = Object.keys(update).filter(function (k) { return k !== "_id"; });
|
|
380
416
|
if (setCols.length > 0) {
|
|
381
|
-
// Column names came from the validated policy.columns
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
var
|
|
385
|
-
var
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"
|
|
389
|
-
|
|
390
|
-
);
|
|
417
|
+
// Column names came from the validated policy.columns. b.sql
|
|
418
|
+
// quotes every SET target + binds every value; the operator app
|
|
419
|
+
// table is quoted (quoteName) and not framework-rewritten.
|
|
420
|
+
var setMap = {};
|
|
421
|
+
for (var sc = 0; sc < setCols.length; sc++) setMap[setCols[sc]] = update[setCols[sc]];
|
|
422
|
+
var updBuilt = sql.update(table, _appSqlOpts())
|
|
423
|
+
.set(setMap)
|
|
424
|
+
.where("_id", row._id)
|
|
425
|
+
.toSql();
|
|
426
|
+
await clusterStorage.execute(updBuilt.sql, updBuilt.params);
|
|
391
427
|
migratedRows++;
|
|
392
428
|
}
|
|
393
429
|
} else {
|
|
@@ -662,17 +698,20 @@ async function policySet(table, opts, callerOpts) {
|
|
|
662
698
|
auditReasonStorage: validated.auditReasonStorage,
|
|
663
699
|
updatedAt: Date.now(),
|
|
664
700
|
};
|
|
665
|
-
var sealed = cryptoField.sealRow(
|
|
666
|
-
// UPSERT
|
|
701
|
+
var sealed = cryptoField.sealRow(POLICIES_TABLE, policyRow);
|
|
702
|
+
// UPSERT via b.sql ON CONFLICT(tableName) DO UPDATE (Postgres + SQLite).
|
|
703
|
+
// BARE logical framework table — clusterStorage rewrites + placeholderizes;
|
|
704
|
+
// b.sql quotes every column + binds every sealed value. The conflict key
|
|
705
|
+
// (tableName) is excluded from the DO UPDATE set.
|
|
667
706
|
var keys = Object.keys(sealed);
|
|
668
|
-
var
|
|
669
|
-
var
|
|
670
|
-
|
|
671
|
-
.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
await clusterStorage.execute(sql,
|
|
707
|
+
var setCols = keys.filter(function (k) { return k !== "tableName"; });
|
|
708
|
+
var policyBuilt = sql.upsert("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
|
|
709
|
+
.columns(keys)
|
|
710
|
+
.values(sealed)
|
|
711
|
+
.onConflict(["tableName"])
|
|
712
|
+
.doUpdateFromExcluded(setCols)
|
|
713
|
+
.toSql();
|
|
714
|
+
await clusterStorage.execute(policyBuilt.sql, policyBuilt.params);
|
|
676
715
|
policyCache.delete(table);
|
|
677
716
|
|
|
678
717
|
audit.safeEmit({
|
|
@@ -715,15 +754,15 @@ async function policyGet(table) {
|
|
|
715
754
|
_requireInit();
|
|
716
755
|
if (typeof table !== "string" || table.length === 0) return null;
|
|
717
756
|
if (policyCache.has(table)) return policyCache.get(table);
|
|
718
|
-
var
|
|
719
|
-
"
|
|
720
|
-
|
|
721
|
-
);
|
|
757
|
+
var getBuilt = sql.select("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
|
|
758
|
+
.where("tableName", table)
|
|
759
|
+
.toSql();
|
|
760
|
+
var rows = await clusterStorage.executeAll(getBuilt.sql, getBuilt.params);
|
|
722
761
|
if (!rows || rows.length === 0) {
|
|
723
762
|
policyCache.set(table, null);
|
|
724
763
|
return null;
|
|
725
764
|
}
|
|
726
|
-
var unsealed = cryptoField.unsealRow(
|
|
765
|
+
var unsealed = cryptoField.unsealRow(POLICIES_TABLE, rows[0]);
|
|
727
766
|
var policy = {
|
|
728
767
|
table: unsealed.tableName,
|
|
729
768
|
columns: safeJson.parse(unsealed.columnsJson, { maxBytes: C.BYTES.kib(64) }),
|
|
@@ -767,9 +806,11 @@ async function policyGet(table) {
|
|
|
767
806
|
*/
|
|
768
807
|
async function policyList() {
|
|
769
808
|
_requireInit();
|
|
770
|
-
var
|
|
771
|
-
"
|
|
772
|
-
|
|
809
|
+
var listBuilt = sql.select("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
|
|
810
|
+
.columns(["tableName"])
|
|
811
|
+
.orderBy("tableName", "asc")
|
|
812
|
+
.toSql();
|
|
813
|
+
var rows = await clusterStorage.executeAll(listBuilt.sql, listBuilt.params);
|
|
773
814
|
var out = [];
|
|
774
815
|
for (var i = 0; i < (rows || []).length; i++) {
|
|
775
816
|
var p = await policyGet(rows[i].tableName);
|
|
@@ -801,10 +842,10 @@ async function policyDelete(table, callerOpts) {
|
|
|
801
842
|
throw new BreakGlassError("breakglass/bad-policy",
|
|
802
843
|
"policy.delete: table must be a non-empty string");
|
|
803
844
|
}
|
|
804
|
-
|
|
805
|
-
"
|
|
806
|
-
|
|
807
|
-
);
|
|
845
|
+
var delBuilt = sql.delete("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
|
|
846
|
+
.where("tableName", table)
|
|
847
|
+
.toSql();
|
|
848
|
+
await clusterStorage.execute(delBuilt.sql, delBuilt.params);
|
|
808
849
|
policyCache.delete(table);
|
|
809
850
|
audit.safeEmit({
|
|
810
851
|
action: "breakglass.policy.delete",
|
|
@@ -1100,14 +1141,13 @@ async function grant(opts) {
|
|
|
1100
1141
|
ip: ipFromReq,
|
|
1101
1142
|
kwGrantHalf: null,
|
|
1102
1143
|
};
|
|
1103
|
-
var sealed = cryptoField.sealRow(
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
var
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
);
|
|
1144
|
+
var sealed = cryptoField.sealRow(GRANTS_TABLE, grantRow);
|
|
1145
|
+
// BARE logical framework table — clusterStorage rewrites + placeholderizes;
|
|
1146
|
+
// b.sql quotes every column + binds every sealed value.
|
|
1147
|
+
var grantInsBuilt = sql.insert("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1148
|
+
.values(sealed)
|
|
1149
|
+
.toSql();
|
|
1150
|
+
await clusterStorage.execute(grantInsBuilt.sql, grantInsBuilt.params);
|
|
1111
1151
|
|
|
1112
1152
|
// Audit
|
|
1113
1153
|
var reasonForAudit = _reasonForAudit(reason, policy.auditReasonStorage);
|
|
@@ -1273,16 +1313,16 @@ async function unsealRow(grantHandle, table, rowId, opts) {
|
|
|
1273
1313
|
throw new BreakGlassError("breakglass/bad-grant-opts",
|
|
1274
1314
|
"unsealRow: rowId is required");
|
|
1275
1315
|
}
|
|
1276
|
-
var
|
|
1277
|
-
"
|
|
1278
|
-
|
|
1279
|
-
);
|
|
1316
|
+
var grantReadBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1317
|
+
.where("_id", grantHandle.id)
|
|
1318
|
+
.toSql();
|
|
1319
|
+
var grantRows = await clusterStorage.executeAll(grantReadBuilt.sql, grantReadBuilt.params);
|
|
1280
1320
|
if (!grantRows || grantRows.length === 0) {
|
|
1281
1321
|
throw new BreakGlassError("breakglass/grant-revoked",
|
|
1282
1322
|
"unsealRow: grant " + grantHandle.id + " not found (deleted or never issued)", true);
|
|
1283
1323
|
}
|
|
1284
1324
|
var sealedGrant = grantRows[0];
|
|
1285
|
-
var grantRow = cryptoField.unsealRow(
|
|
1325
|
+
var grantRow = cryptoField.unsealRow(GRANTS_TABLE, sealedGrant);
|
|
1286
1326
|
|
|
1287
1327
|
// Table mismatch
|
|
1288
1328
|
if (grantRow.scopeTable !== table) {
|
|
@@ -1343,30 +1383,41 @@ async function unsealRow(grantHandle, table, rowId, opts) {
|
|
|
1343
1383
|
// grant should not be consumed. Without this ordering, a single
|
|
1344
1384
|
// typo against `maxRowsPerGrant: 1` (the default) exhausts the
|
|
1345
1385
|
// grant and forces the operator to re-do the step-up ceremony.
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1386
|
+
// Operator app table (validated identifier) — quoteName quotes it; it is
|
|
1387
|
+
// not framework-rewritten.
|
|
1388
|
+
var rowReadBuilt = sql.select(table, _appSqlOpts())
|
|
1389
|
+
.where("_id", String(rowId))
|
|
1390
|
+
.toSql();
|
|
1391
|
+
var rows = await clusterStorage.executeAll(rowReadBuilt.sql, rowReadBuilt.params);
|
|
1350
1392
|
if (!rows || rows.length === 0) {
|
|
1351
1393
|
throw new BreakGlassError("breakglass/row-not-found",
|
|
1352
1394
|
"unsealRow: " + table + "[" + rowId + "] not found", true);
|
|
1353
1395
|
}
|
|
1354
1396
|
|
|
1355
|
-
// Increment rowsConsumed (atomic UPDATE with WHERE rowsConsumed < cap
|
|
1356
|
-
//
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
)
|
|
1397
|
+
// Increment rowsConsumed (atomic UPDATE with WHERE rowsConsumed < cap so
|
|
1398
|
+
// concurrent unseals can't both pass the runtime check above). The
|
|
1399
|
+
// rowsConsumed+1 RHS + the rowsConsumed<maxRowsPerGrant column comparison
|
|
1400
|
+
// are guarded raw fragments (b.guardSql + placeholder/literal scan). The
|
|
1401
|
+
// identifier quoting in those raw fragments is dialect-aware (backticks on
|
|
1402
|
+
// MySQL, double-quotes on PG/SQLite) so the column references resolve as
|
|
1403
|
+
// identifiers, not string literals, on the active backend.
|
|
1404
|
+
var incDialect = clusterStorage.dialect();
|
|
1405
|
+
var incBuilt = sql.update("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1406
|
+
.setRaw("rowsConsumed", safeSql.quoteIdentifier("rowsConsumed", incDialect) + " + 1", [])
|
|
1407
|
+
.where("_id", grantHandle.id)
|
|
1408
|
+
.whereRaw(safeSql.quoteIdentifier("rowsConsumed", incDialect) + " < " +
|
|
1409
|
+
safeSql.quoteIdentifier("maxRowsPerGrant", incDialect), [])
|
|
1410
|
+
.whereNull("revokedAt")
|
|
1411
|
+
.whereOp("expiresAt", ">", Date.now())
|
|
1412
|
+
.toSql();
|
|
1413
|
+
var updateRes = await clusterStorage.execute(incBuilt.sql, incBuilt.params);
|
|
1364
1414
|
// executeAll-style result; some backends return rowsAffected, others a count.
|
|
1365
1415
|
// Re-query to confirm the increment landed and get the post-increment counter.
|
|
1366
|
-
var
|
|
1367
|
-
"
|
|
1368
|
-
|
|
1369
|
-
|
|
1416
|
+
var postReadBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1417
|
+
.columns(["rowsConsumed", "revokedAt", "expiresAt"])
|
|
1418
|
+
.where("_id", grantHandle.id)
|
|
1419
|
+
.toSql();
|
|
1420
|
+
var postRows = await clusterStorage.executeAll(postReadBuilt.sql, postReadBuilt.params);
|
|
1370
1421
|
if (!postRows || postRows.length === 0) {
|
|
1371
1422
|
throw new BreakGlassError("breakglass/grant-revoked",
|
|
1372
1423
|
"unsealRow: grant " + grantHandle.id + " disappeared during unseal", true);
|
|
@@ -1469,11 +1520,14 @@ async function revoke(grantId, opts) {
|
|
|
1469
1520
|
}
|
|
1470
1521
|
opts = opts || {};
|
|
1471
1522
|
var nowMs = Date.now();
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1523
|
+
// revokedAt IS NULL keeps the revoke idempotent (already-revoked grants
|
|
1524
|
+
// keep their original timestamp).
|
|
1525
|
+
var revBuilt = sql.update("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1526
|
+
.set({ revokedAt: nowMs })
|
|
1527
|
+
.where("_id", grantId)
|
|
1528
|
+
.whereNull("revokedAt")
|
|
1529
|
+
.toSql();
|
|
1530
|
+
await clusterStorage.execute(revBuilt.sql, revBuilt.params);
|
|
1477
1531
|
audit.safeEmit({
|
|
1478
1532
|
action: "breakglass.grant.revoked",
|
|
1479
1533
|
outcome: "success",
|
|
@@ -1516,19 +1570,25 @@ async function listActive(opts) {
|
|
|
1516
1570
|
// Use cryptoField's computeDerived so the hash matches the table's
|
|
1517
1571
|
// hashNamespace prefix — raw sha3Hash would produce a different value.
|
|
1518
1572
|
var derived = cryptoField.computeDerived(
|
|
1519
|
-
|
|
1573
|
+
GRANTS_TABLE, "issuedToActorId", actorId
|
|
1520
1574
|
);
|
|
1521
1575
|
if (!derived) return [];
|
|
1522
1576
|
var nowMs = Date.now();
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1577
|
+
// rowsConsumed < maxRowsPerGrant is a column-to-column comparison (guarded
|
|
1578
|
+
// raw fragment); every other predicate is structured.
|
|
1579
|
+
var laDialect = clusterStorage.dialect();
|
|
1580
|
+
var laBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1581
|
+
.where("issuedToActorHash", derived.value)
|
|
1582
|
+
.whereNull("revokedAt")
|
|
1583
|
+
.whereOp("expiresAt", ">", nowMs)
|
|
1584
|
+
.whereRaw(safeSql.quoteIdentifier("rowsConsumed", laDialect) + " < " +
|
|
1585
|
+
safeSql.quoteIdentifier("maxRowsPerGrant", laDialect), [])
|
|
1586
|
+
.orderBy("issuedAt", "desc")
|
|
1587
|
+
.toSql();
|
|
1588
|
+
var rows = await clusterStorage.executeAll(laBuilt.sql, laBuilt.params);
|
|
1529
1589
|
var out = [];
|
|
1530
1590
|
for (var i = 0; i < (rows || []).length; i++) {
|
|
1531
|
-
var u = cryptoField.unsealRow(
|
|
1591
|
+
var u = cryptoField.unsealRow(GRANTS_TABLE, rows[i]);
|
|
1532
1592
|
out.push({
|
|
1533
1593
|
id: u._id,
|
|
1534
1594
|
scopeTable: u.scopeTable,
|
|
@@ -1644,11 +1704,12 @@ async function unsealRowAsService(req, table, rowId, opts) {
|
|
|
1644
1704
|
}
|
|
1645
1705
|
|
|
1646
1706
|
// Fetch + unseal the row (Model A or Model B path, same as
|
|
1647
|
-
// operator-initiated unsealRow).
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1707
|
+
// operator-initiated unsealRow). Operator app table — quoteName quotes it;
|
|
1708
|
+
// it is not framework-rewritten.
|
|
1709
|
+
var svcRowBuilt = sql.select(table, _appSqlOpts())
|
|
1710
|
+
.where("_id", String(rowId))
|
|
1711
|
+
.toSql();
|
|
1712
|
+
var rows = await clusterStorage.executeAll(svcRowBuilt.sql, svcRowBuilt.params);
|
|
1652
1713
|
if (!rows || rows.length === 0) {
|
|
1653
1714
|
throw new BreakGlassError("breakglass/row-not-found",
|
|
1654
1715
|
"unsealRowAsService: " + table + "[" + rowId + "] not found", true);
|
|
@@ -1722,24 +1783,22 @@ async function listActiveAll(opts) {
|
|
|
1722
1783
|
_requireInit();
|
|
1723
1784
|
opts = opts || {};
|
|
1724
1785
|
var nowMs = Date.now();
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
params
|
|
1739
|
-
);
|
|
1786
|
+
// rowsConsumed < maxRowsPerGrant is a column-to-column comparison (guarded
|
|
1787
|
+
// raw fragment); the rest are structured predicates.
|
|
1788
|
+
var laaDialect = clusterStorage.dialect();
|
|
1789
|
+
var laaQb = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1790
|
+
.whereNull("revokedAt")
|
|
1791
|
+
.whereOp("expiresAt", ">", nowMs)
|
|
1792
|
+
.whereRaw(safeSql.quoteIdentifier("rowsConsumed", laaDialect) + " < " +
|
|
1793
|
+
safeSql.quoteIdentifier("maxRowsPerGrant", laaDialect), []);
|
|
1794
|
+
if (opts.table) laaQb.where("scopeTable", opts.table);
|
|
1795
|
+
if (opts.since) laaQb.whereOp("issuedAt", ">=", opts.since);
|
|
1796
|
+
laaQb.orderBy("issuedAt", "desc");
|
|
1797
|
+
var laaBuilt = laaQb.toSql();
|
|
1798
|
+
var rows = await clusterStorage.executeAll(laaBuilt.sql, laaBuilt.params);
|
|
1740
1799
|
var out = [];
|
|
1741
1800
|
for (var i = 0; i < (rows || []).length; i++) {
|
|
1742
|
-
var u = cryptoField.unsealRow(
|
|
1801
|
+
var u = cryptoField.unsealRow(GRANTS_TABLE, rows[i]);
|
|
1743
1802
|
out.push({
|
|
1744
1803
|
id: u._id,
|
|
1745
1804
|
issuedToActorId: u.issuedToActorId,
|
|
@@ -1796,31 +1855,28 @@ async function revokeAll(criteria, opts) {
|
|
|
1796
1855
|
"revokeAll: at least one of { actorId, table } is required (refusing to mass-revoke without scope)");
|
|
1797
1856
|
}
|
|
1798
1857
|
opts = opts || {};
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
if (criteria.table) {
|
|
1811
|
-
clauses.push("scopeTable = ?");
|
|
1812
|
-
params.push(criteria.table);
|
|
1858
|
+
// The SELECT (snapshot ids) and UPDATE (apply revoke) share one predicate
|
|
1859
|
+
// set; applyRevokeCriteria replays it onto either builder so the WHERE can
|
|
1860
|
+
// never drift between the two.
|
|
1861
|
+
var derived = criteria.actorId
|
|
1862
|
+
? cryptoField.computeDerived(GRANTS_TABLE, "issuedToActorId", criteria.actorId)
|
|
1863
|
+
: null;
|
|
1864
|
+
function applyRevokeCriteria(qb) {
|
|
1865
|
+
qb.whereNull("revokedAt");
|
|
1866
|
+
if (criteria.actorId && derived) qb.where("issuedToActorHash", derived.value);
|
|
1867
|
+
if (criteria.table) qb.where("scopeTable", criteria.table);
|
|
1868
|
+
return qb;
|
|
1813
1869
|
}
|
|
1814
1870
|
// Snapshot the to-be-revoked grant ids first so audit captures specifics.
|
|
1815
|
-
var
|
|
1816
|
-
"
|
|
1817
|
-
|
|
1818
|
-
);
|
|
1871
|
+
var idSelBuilt = applyRevokeCriteria(
|
|
1872
|
+
sql.select("_blamejs_break_glass_grants", _sqlOpts()).columns(["_id"]) // allow:hand-rolled-sql
|
|
1873
|
+
).toSql();
|
|
1874
|
+
var ids = await clusterStorage.executeAll(idSelBuilt.sql, idSelBuilt.params);
|
|
1819
1875
|
var nowMs = Date.now();
|
|
1820
|
-
|
|
1821
|
-
"
|
|
1822
|
-
|
|
1823
|
-
);
|
|
1876
|
+
var revAllBuilt = applyRevokeCriteria(
|
|
1877
|
+
sql.update("_blamejs_break_glass_grants", _sqlOpts()).set({ revokedAt: nowMs }) // allow:hand-rolled-sql
|
|
1878
|
+
).toSql();
|
|
1879
|
+
await clusterStorage.execute(revAllBuilt.sql, revAllBuilt.params);
|
|
1824
1880
|
audit.safeEmit({
|
|
1825
1881
|
action: "breakglass.admin.revokeall",
|
|
1826
1882
|
outcome: "success",
|
|
@@ -1841,11 +1897,12 @@ async function revokeAll(criteria, opts) {
|
|
|
1841
1897
|
async function _sweepExpired(opts) {
|
|
1842
1898
|
opts = opts || {};
|
|
1843
1899
|
var nowMs = Date.now();
|
|
1844
|
-
var
|
|
1845
|
-
"
|
|
1846
|
-
"
|
|
1847
|
-
|
|
1848
|
-
|
|
1900
|
+
var expiredBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1901
|
+
.columns(["_id", "issuedToActorId", "scopeTable", "rowsConsumed"])
|
|
1902
|
+
.whereNull("revokedAt")
|
|
1903
|
+
.whereOp("expiresAt", "<=", nowMs)
|
|
1904
|
+
.toSql();
|
|
1905
|
+
var expired = await clusterStorage.executeAll(expiredBuilt.sql, expiredBuilt.params);
|
|
1849
1906
|
for (var i = 0; i < (expired || []).length; i++) {
|
|
1850
1907
|
var row = expired[i];
|
|
1851
1908
|
audit.safeEmit({
|
|
@@ -1855,11 +1912,12 @@ async function _sweepExpired(opts) {
|
|
|
1855
1912
|
metadata: { grantId: row._id, table: row.scopeTable, rowsConsumed: Number(row.rowsConsumed) },
|
|
1856
1913
|
});
|
|
1857
1914
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
"
|
|
1861
|
-
|
|
1862
|
-
|
|
1915
|
+
var sweepUpdBuilt = sql.update("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
|
|
1916
|
+
.set({ revokedAt: nowMs })
|
|
1917
|
+
.whereNull("revokedAt")
|
|
1918
|
+
.whereOp("expiresAt", "<=", nowMs)
|
|
1919
|
+
.toSql();
|
|
1920
|
+
await clusterStorage.execute(sweepUpdBuilt.sql, sweepUpdBuilt.params);
|
|
1863
1921
|
return { expired: (expired || []).length };
|
|
1864
1922
|
}
|
|
1865
1923
|
|