@blamejs/core 0.14.6 → 0.14.8
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 +3 -2
- package/lib/a2a-tasks.js +6 -6
- package/lib/agent-event-bus.js +4 -4
- package/lib/agent-idempotency.js +6 -6
- package/lib/agent-orchestrator.js +9 -9
- package/lib/agent-posture-chain.js +10 -10
- package/lib/agent-saga.js +6 -7
- package/lib/agent-snapshot.js +8 -8
- package/lib/agent-stream.js +3 -3
- package/lib/agent-tenant.js +4 -4
- package/lib/agent-trace.js +5 -5
- package/lib/ai-disclosure.js +3 -3
- package/lib/ai-input.js +1 -1
- package/lib/app.js +2 -2
- package/lib/archive-read.js +1 -1
- package/lib/archive-tar-read.js +1 -1
- package/lib/archive-wrap.js +5 -5
- package/lib/audit-tools.js +65 -5
- package/lib/audit.js +2 -2
- package/lib/auth/acr-vocabulary.js +1 -1
- package/lib/auth/ciba.js +4 -4
- package/lib/auth/dpop.js +1 -1
- package/lib/auth/fal.js +1 -1
- package/lib/auth/fido-mds3.js +2 -3
- package/lib/auth/jwt-external.js +2 -2
- package/lib/auth/oauth.js +10 -10
- package/lib/auth/oid4vci.js +8 -8
- package/lib/auth/oid4vp.js +1 -1
- package/lib/auth/openid-federation.js +6 -6
- package/lib/auth/passkey.js +6 -6
- package/lib/auth/saml.js +1 -1
- package/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/backup/index.js +18 -18
- package/lib/breach-deadline.js +3 -3
- package/lib/cache.js +4 -4
- package/lib/calendar.js +7 -7
- package/lib/circuit-breaker.js +1 -1
- package/lib/cms-codec.js +2 -2
- package/lib/compliance.js +14 -14
- package/lib/content-credentials.js +3 -3
- package/lib/crypto-field.js +58 -21
- package/lib/crypto.js +5 -6
- package/lib/db-query.js +131 -9
- package/lib/db.js +106 -22
- package/lib/ddl-change-control.js +2 -2
- package/lib/did.js +2 -2
- package/lib/dsr.js +4 -4
- package/lib/external-db.js +65 -17
- package/lib/framework-schema.js +4 -4
- package/lib/guard-cidr.js +1 -1
- package/lib/guard-image.js +1 -1
- package/lib/guard-list-id.js +2 -2
- package/lib/guard-list-unsubscribe.js +2 -3
- package/lib/guard-time.js +1 -1
- package/lib/guard-xml.js +1 -1
- package/lib/http-client-cache.js +1 -1
- package/lib/iab-tcf.js +4 -4
- package/lib/incident-report.js +150 -0
- package/lib/json-schema.js +1 -1
- package/lib/jtd.js +1 -1
- package/lib/mail-auth.js +1 -1
- package/lib/mail-bimi.js +1 -1
- package/lib/mail-crypto-smime.js +2 -2
- package/lib/mail-deploy.js +3 -3
- package/lib/mail-server-managesieve.js +2 -2
- package/lib/mail-server-mx.js +1 -1
- package/lib/mail-server-pop3.js +2 -2
- package/lib/mail-server-rate-limit.js +1 -1
- package/lib/mail-server-submission.js +1 -1
- package/lib/mail-store.js +1 -1
- package/lib/mcp.js +7 -7
- package/lib/mdoc.js +1 -1
- package/lib/metrics.js +10 -10
- package/lib/middleware/compose-pipeline.js +1 -1
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/dpop.js +5 -5
- package/lib/middleware/idempotency-key.js +21 -22
- package/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/network-dns-resolver.js +2 -2
- package/lib/network-dns.js +1 -2
- package/lib/network-dnssec.js +2 -2
- package/lib/network-smtp-policy.js +1 -1
- package/lib/network-tls.js +1 -2
- package/lib/network-tsig.js +3 -3
- package/lib/outbox.js +1 -1
- package/lib/pqc-agent.js +1 -1
- package/lib/retention.js +1 -1
- package/lib/retry.js +1 -1
- package/lib/rfc3339.js +2 -2
- package/lib/safe-archive.js +2 -2
- package/lib/safe-decompress.js +1 -1
- package/lib/safe-ical.js +2 -2
- package/lib/safe-mime.js +1 -1
- package/lib/self-update-standalone-verifier.js +1 -1
- package/lib/self-update.js +2 -2
- package/lib/standard-webhooks.js +3 -3
- package/lib/static.js +1 -1
- package/lib/stream-throttle.js +2 -2
- package/lib/structured-fields.js +1 -1
- package/lib/subject.js +2 -2
- package/lib/vault/index.js +64 -1
- package/lib/vault/rotate.js +19 -0
- package/lib/vault/seal-pem-file.js +1 -1
- package/lib/vendor-data.js +1 -1
- package/lib/web-push-vapid.js +1 -1
- package/lib/webhook.js +1 -1
- package/lib/websocket.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/crypto.js
CHANGED
|
@@ -168,7 +168,7 @@ function hashFile(filePath, algorithm) {
|
|
|
168
168
|
// `algorithms` entry. Used by hashFilesParallel below; not exported
|
|
169
169
|
// directly because the common case is the parallel-many shape.
|
|
170
170
|
//
|
|
171
|
-
//
|
|
171
|
+
// Hardening (v0.9.58):
|
|
172
172
|
// - lstat-then-stat so symlinks are detected before open; refused
|
|
173
173
|
// unless opts.followSymlinks === true (default false — a symlink-
|
|
174
174
|
// in-input-list attack lets a write-restricted caller hash files
|
|
@@ -269,9 +269,8 @@ function _hashFileMulti(filePath, algorithms, opts) {
|
|
|
269
269
|
* SBOM regeneration / vendor-data integrity sweeps / release-asset
|
|
270
270
|
* bundling — situations where N files each need both SHA-256 (legacy
|
|
271
271
|
* compat) and SHA-3-512 (PQC-first) digests and rolling a worker
|
|
272
|
-
* pool by hand
|
|
273
|
-
*
|
|
274
|
-
* every release.
|
|
272
|
+
* pool by hand means the same two-loop, capture-N-promises, settle-Q
|
|
273
|
+
* boilerplate every release.
|
|
275
274
|
*
|
|
276
275
|
* @opts
|
|
277
276
|
* algorithms?: string[], // default ["sha256", "sha3-512"]; any node:crypto-known digest
|
|
@@ -333,7 +332,7 @@ function hashFilesParallel(filePaths, opts) {
|
|
|
333
332
|
"crypto.hashFilesParallel: opts.onProgress must be a function when supplied"
|
|
334
333
|
));
|
|
335
334
|
}
|
|
336
|
-
//
|
|
335
|
+
// DoS cap. Default 1 GiB per file; operators with larger
|
|
337
336
|
// legitimate hashing workloads (firmware images, vendor packs)
|
|
338
337
|
// override per-call.
|
|
339
338
|
var maxBytesPerFile = opts.maxBytesPerFile !== undefined
|
|
@@ -1202,7 +1201,7 @@ function decrypt(ciphertext, privateKeys, opts) {
|
|
|
1202
1201
|
}
|
|
1203
1202
|
// Audit-emit every legacy decrypt so the migration window is
|
|
1204
1203
|
// visible. Emit success ONLY on actual decrypt success; emit
|
|
1205
|
-
// failure on throw.
|
|
1204
|
+
// failure on throw. Before this fix, the audit fired
|
|
1206
1205
|
// before decryptEnvelope() ran, so corrupted 0xE1 blobs / wrong
|
|
1207
1206
|
// private keys / unsupported KEMs got logged as successful legacy
|
|
1208
1207
|
// decrypts when the call actually threw, inflating real success
|
package/lib/db-query.js
CHANGED
|
@@ -33,6 +33,7 @@ var { generateToken } = require("./crypto");
|
|
|
33
33
|
var safeJson = require("./safe-json");
|
|
34
34
|
var safeJsonPath = require("./safe-jsonpath");
|
|
35
35
|
var safeSql = require("./safe-sql");
|
|
36
|
+
var audit = require("./audit");
|
|
36
37
|
|
|
37
38
|
// "@>" / "?" / "?|" / "?&" are JSONB containment + key-existence
|
|
38
39
|
// operators. Routed through safeJsonPath validation before binding so
|
|
@@ -46,7 +47,7 @@ var JSONB_CONTAINMENT_OPS = new Set(["@>"]);
|
|
|
46
47
|
var JSONB_KEY_OPS = new Set(["?", "?|", "?&"]);
|
|
47
48
|
|
|
48
49
|
class Query {
|
|
49
|
-
constructor(database, tableName) {
|
|
50
|
+
constructor(database, tableName, opts) {
|
|
50
51
|
// Identifier safety: tableName flows into SQL via interpolation
|
|
51
52
|
// (parameter placeholders only bind values, not names). Validate at
|
|
52
53
|
// construction so an attacker-controlled name with embedded `"` or
|
|
@@ -84,6 +85,62 @@ class Query {
|
|
|
84
85
|
this._orderBy = null;
|
|
85
86
|
this._limit = null;
|
|
86
87
|
this._offset = null;
|
|
88
|
+
|
|
89
|
+
// Column-membership gate. `db.from()` passes the table's
|
|
90
|
+
// declared columns + the configured gate mode so an operator-
|
|
91
|
+
// supplied column name that isn't a real column of the table is
|
|
92
|
+
// refused before it interpolates into SQL as an identifier
|
|
93
|
+
// (ORDER-BY / sealed-column-disclosure injection — CWE-89 /
|
|
94
|
+
// CWE-1336). A bare `new Query(db, name)` with no opts leaves the
|
|
95
|
+
// gate disabled (declaredColumns null), so direct/internal
|
|
96
|
+
// construction is unaffected.
|
|
97
|
+
opts = opts || {};
|
|
98
|
+
this._declaredColumns = (opts.declaredColumns instanceof Set) ? opts.declaredColumns
|
|
99
|
+
: (Array.isArray(opts.declaredColumns) ? new Set(opts.declaredColumns) : null);
|
|
100
|
+
this._columnGateMode = opts.columnGateMode || "reject";
|
|
101
|
+
this._allowedColumns = null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Restrict the operator-allowable columns to an explicit subset
|
|
105
|
+
// (tighter than the schema-declared set). Use when a query is built
|
|
106
|
+
// from request input and must only ever touch a known-safe list.
|
|
107
|
+
// Throws on a non-array or an invalid identifier.
|
|
108
|
+
allowedColumns(cols) {
|
|
109
|
+
if (!Array.isArray(cols) || cols.length === 0) {
|
|
110
|
+
throw new TypeError("allowedColumns(cols): expected a non-empty array of column names");
|
|
111
|
+
}
|
|
112
|
+
cols.forEach(_validateField);
|
|
113
|
+
this._allowedColumns = new Set(cols);
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Assert `field` is a member of the allowed/declared column set
|
|
118
|
+
// before it is interpolated into SQL as an identifier. The operator
|
|
119
|
+
// `allowedColumns()` set (when present) is ALWAYS enforced; the
|
|
120
|
+
// schema gate respects the configured mode ("reject" default
|
|
121
|
+
// throws | "warn" drop-silent audits + allows | "off" / no declared
|
|
122
|
+
// set skips).
|
|
123
|
+
_assertColumnMember(field, where) {
|
|
124
|
+
if (this._allowedColumns && !this._allowedColumns.has(field)) {
|
|
125
|
+
throw new Error("column '" + field + "' is not in the allowedColumns() set" +
|
|
126
|
+
(where ? " (" + where + ")" : ""));
|
|
127
|
+
}
|
|
128
|
+
if (this._declaredColumns === null || this._columnGateMode === "off") return;
|
|
129
|
+
if (this._declaredColumns.has(field)) return;
|
|
130
|
+
if (this._columnGateMode === "warn") {
|
|
131
|
+
try {
|
|
132
|
+
audit.safeEmit({
|
|
133
|
+
action: "db.query.unknown_column",
|
|
134
|
+
outcome: "failure",
|
|
135
|
+
metadata: { table: this._qualifiedKey, column: field, where: where || null },
|
|
136
|
+
});
|
|
137
|
+
} catch (_e) { /* drop-silent — observability sink, by design */ }
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw new Error("column '" + field + "' is not a declared column of '" +
|
|
141
|
+
this._qualifiedKey + "'" + (where ? " (" + where + ")" : "") +
|
|
142
|
+
". Declared columns: " + Array.from(this._declaredColumns).join(", ") +
|
|
143
|
+
". Use .allowedColumns([...]) or db.init({ columnGate: 'off' }) to bypass.");
|
|
87
144
|
}
|
|
88
145
|
|
|
89
146
|
// Quoted SQL form: `"schema"."table"` if schema-qualified, else `"table"`.
|
|
@@ -114,7 +171,7 @@ class Query {
|
|
|
114
171
|
if (!ALLOWED_OPS.has(op)) {
|
|
115
172
|
throw new Error("invalid where operator: " + op);
|
|
116
173
|
}
|
|
117
|
-
//
|
|
174
|
+
// JSONB / JSON-path injection guard. Routes operator-
|
|
118
175
|
// supplied JSONB containment + key-existence values through
|
|
119
176
|
// safe-jsonpath before they reach the engine. Bound via `?`
|
|
120
177
|
// placeholder so the value still doesn't interpolate; this is
|
|
@@ -171,6 +228,10 @@ class Query {
|
|
|
171
228
|
value = lookup.value;
|
|
172
229
|
}
|
|
173
230
|
cryptoField && _validateField(field);
|
|
231
|
+
// Gate the post-sealed-rewrite physical column (derived-hash
|
|
232
|
+
// columns are declared physical columns, so the rewrite target
|
|
233
|
+
// passes membership).
|
|
234
|
+
this._assertColumnMember(field, "where");
|
|
174
235
|
if (op === "IN") {
|
|
175
236
|
// node:sqlite ? does not support array-binding. Pre-v0.8.18
|
|
176
237
|
// `where(field, "IN", [1,2,3])` silently bound the entire
|
|
@@ -228,10 +289,11 @@ class Query {
|
|
|
228
289
|
// text used to build expressions the chainable .where() can't express
|
|
229
290
|
// (compound OR, row-value comparison for cursor pagination, etc.).
|
|
230
291
|
// Placeholder count must match params.length.
|
|
231
|
-
whereRaw(sql, params) {
|
|
292
|
+
whereRaw(sql, params, opts) {
|
|
232
293
|
if (typeof sql !== "string" || sql.length === 0) {
|
|
233
294
|
throw new Error("whereRaw: sql must be a non-empty string");
|
|
234
295
|
}
|
|
296
|
+
if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql, "whereRaw");
|
|
235
297
|
var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
|
|
236
298
|
// Count `?` placeholders, but skip occurrences inside string
|
|
237
299
|
// literals ('...' or "..."), line comments (-- to EOL), and
|
|
@@ -255,12 +317,15 @@ class Query {
|
|
|
255
317
|
throw new Error("select() expects an array of column names");
|
|
256
318
|
}
|
|
257
319
|
columns.forEach(_validateField);
|
|
320
|
+
var self = this;
|
|
321
|
+
columns.forEach(function (c) { self._assertColumnMember(c, "select"); });
|
|
258
322
|
this._select = columns.slice();
|
|
259
323
|
return this;
|
|
260
324
|
}
|
|
261
325
|
|
|
262
326
|
orderBy(field, direction) {
|
|
263
327
|
_validateField(field);
|
|
328
|
+
this._assertColumnMember(field, "orderBy");
|
|
264
329
|
direction = (direction || "asc").toLowerCase();
|
|
265
330
|
if (direction !== "asc" && direction !== "desc") {
|
|
266
331
|
throw new Error("orderBy direction must be 'asc' or 'desc'");
|
|
@@ -348,7 +413,7 @@ class Query {
|
|
|
348
413
|
// the bound table's sealedFields registration before it lands in the
|
|
349
414
|
// operator's pipeline. For large result sets (audit exports, backup
|
|
350
415
|
// table dumps) this avoids materializing the full rowset in memory.
|
|
351
|
-
//
|
|
416
|
+
// StreamLimit ceiling enforced from the module-level db
|
|
352
417
|
// config; per-call opts.streamLimit overrides for one-off bumps.
|
|
353
418
|
stream(opts) {
|
|
354
419
|
var sql = "SELECT " + this._projection() + " FROM " + this._quotedTable() +
|
|
@@ -455,6 +520,8 @@ class Query {
|
|
|
455
520
|
throw new Error("update changes object is empty");
|
|
456
521
|
}
|
|
457
522
|
setKeys.forEach(_validateField);
|
|
523
|
+
var selfUpd = this;
|
|
524
|
+
setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
|
|
458
525
|
var setClause = setKeys.map(function (k) { return '"' + k + '" = ?'; }).join(", ");
|
|
459
526
|
var setValues = setKeys.map(function (k) { return sealed[k]; });
|
|
460
527
|
|
|
@@ -498,6 +565,7 @@ class Query {
|
|
|
498
565
|
throw new Error("increment(column, delta): column must be a non-empty string");
|
|
499
566
|
}
|
|
500
567
|
_validateField(column);
|
|
568
|
+
this._assertColumnMember(column, "increment");
|
|
501
569
|
if (delta === undefined) delta = 1;
|
|
502
570
|
if (typeof delta !== "number" || !Number.isFinite(delta) || !Number.isInteger(delta)) {
|
|
503
571
|
throw new Error("increment(column, delta): delta must be a finite integer (default 1)");
|
|
@@ -532,7 +600,7 @@ class Query {
|
|
|
532
600
|
if (typeof closure !== "function") {
|
|
533
601
|
throw new Error("whereGroup(closure): expected function (qb) => ...");
|
|
534
602
|
}
|
|
535
|
-
var sub = new WhereBuilder();
|
|
603
|
+
var sub = new WhereBuilder(this);
|
|
536
604
|
closure(sub);
|
|
537
605
|
var built = sub.build();
|
|
538
606
|
if (!built.sql) return this;
|
|
@@ -551,7 +619,7 @@ class Query {
|
|
|
551
619
|
throw new Error("orWhere(...): no prior where(...) — start the chain with where(...)");
|
|
552
620
|
}
|
|
553
621
|
if (typeof fieldOrObjOrFn === "function") {
|
|
554
|
-
var sub = new WhereBuilder();
|
|
622
|
+
var sub = new WhereBuilder(this);
|
|
555
623
|
fieldOrObjOrFn(sub);
|
|
556
624
|
var built = sub.build();
|
|
557
625
|
if (!built.sql) return this;
|
|
@@ -562,7 +630,7 @@ class Query {
|
|
|
562
630
|
}
|
|
563
631
|
// For non-closure shapes, build a transient single-leaf Query and
|
|
564
632
|
// splice it. We compile to a `WhereBuilder` for symmetry.
|
|
565
|
-
var sub2 = new WhereBuilder();
|
|
633
|
+
var sub2 = new WhereBuilder(this);
|
|
566
634
|
if (fieldOrObjOrFn !== null && typeof fieldOrObjOrFn === "object" && !Array.isArray(fieldOrObjOrFn)) {
|
|
567
635
|
Object.keys(fieldOrObjOrFn).forEach(function (k) { sub2.eq(k, fieldOrObjOrFn[k]); });
|
|
568
636
|
} else if (op === undefined) {
|
|
@@ -592,6 +660,8 @@ class Query {
|
|
|
592
660
|
throw new Error("search(fields, term): fields must be a non-empty array of column names");
|
|
593
661
|
}
|
|
594
662
|
fields.forEach(_validateField);
|
|
663
|
+
var selfS = this;
|
|
664
|
+
fields.forEach(function (f) { selfS._assertColumnMember(f, "search"); });
|
|
595
665
|
if (term === undefined || term === null) return this;
|
|
596
666
|
if (typeof term !== "string") {
|
|
597
667
|
throw new Error("search(fields, term): term must be a string");
|
|
@@ -684,14 +754,18 @@ class Query {
|
|
|
684
754
|
// `.build()` returns `{ sql, params }`. Empty builder → `{ sql: "",
|
|
685
755
|
// params: [] }`.
|
|
686
756
|
class WhereBuilder {
|
|
687
|
-
constructor() {
|
|
757
|
+
constructor(gate) {
|
|
688
758
|
this._parts = []; // [{ joiner: "AND"|"OR", sql: "...", params: [...] }]
|
|
759
|
+
// The owning Query, so grouped/OR sub-expressions enforce the
|
|
760
|
+
// same column-membership gate as the top-level chain.
|
|
761
|
+
this._gate = gate || null;
|
|
689
762
|
}
|
|
690
763
|
_push(joiner, field, op, value) {
|
|
691
764
|
if (typeof field !== "string" || field.length === 0) {
|
|
692
765
|
throw new Error("WhereBuilder: field must be a non-empty string");
|
|
693
766
|
}
|
|
694
767
|
_validateField(field);
|
|
768
|
+
if (this._gate) this._gate._assertColumnMember(field, "whereGroup");
|
|
695
769
|
var qf = '"' + field + '"';
|
|
696
770
|
if (op === "IN" || op === "NOT IN") {
|
|
697
771
|
if (!Array.isArray(value) || value.length === 0) {
|
|
@@ -723,10 +797,11 @@ class WhereBuilder {
|
|
|
723
797
|
orLte(f, v) { return this._push("OR", f, "<=", v); }
|
|
724
798
|
orIn(f, vs) { return this._push("OR", f, "IN", vs); }
|
|
725
799
|
orLike(f, v) { return this._push("OR", f, "LIKE", v); }
|
|
726
|
-
raw(sql, params) {
|
|
800
|
+
raw(sql, params, opts) {
|
|
727
801
|
if (typeof sql !== "string" || sql.length === 0) {
|
|
728
802
|
throw new Error("WhereBuilder.raw: sql must be a non-empty string");
|
|
729
803
|
}
|
|
804
|
+
if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql, "WhereBuilder.raw");
|
|
730
805
|
var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
|
|
731
806
|
if (_countPlaceholders(sql) !== p.length) {
|
|
732
807
|
throw new Error("WhereBuilder.raw: placeholder count mismatch");
|
|
@@ -752,6 +827,53 @@ class WhereBuilder {
|
|
|
752
827
|
// Tracks SQL single-quoted, double-quoted, line-comment, and block-
|
|
753
828
|
// comment state to avoid counting `?` characters that are part of
|
|
754
829
|
// literal text the SQL engine never interprets as a binding marker.
|
|
830
|
+
// Refuse raw SQL fragments that embed a single-quoted string
|
|
831
|
+
// literal. A whereRaw / WhereBuilder.raw fragment is meant to be a
|
|
832
|
+
// STATIC template whose every value is bound through a `?` placeholder;
|
|
833
|
+
// an embedded `'...'` literal is the signature of operator input
|
|
834
|
+
// concatenated into the query (CWE-89 / CWE-564 — concat into a
|
|
835
|
+
// query builder). Double-quoted identifiers (`"col"`), line comments,
|
|
836
|
+
// and block comments are skipped. Operators with a deliberate static
|
|
837
|
+
// literal pass `{ allowLiterals: true }`. Shares the quote/comment
|
|
838
|
+
// scanning shape with _countPlaceholders.
|
|
839
|
+
function _assertRawNoStringLiteral(sql, where) {
|
|
840
|
+
var i = 0;
|
|
841
|
+
var len = sql.length;
|
|
842
|
+
while (i < len) {
|
|
843
|
+
var ch = sql.charAt(i);
|
|
844
|
+
var next = i + 1 < len ? sql.charAt(i + 1) : "";
|
|
845
|
+
if (ch === '"') {
|
|
846
|
+
i += 1;
|
|
847
|
+
while (i < len) {
|
|
848
|
+
if (sql.charAt(i) === '"') {
|
|
849
|
+
if (sql.charAt(i + 1) === '"') { i += 2; continue; }
|
|
850
|
+
i += 1; break;
|
|
851
|
+
}
|
|
852
|
+
i += 1;
|
|
853
|
+
}
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (ch === "-" && next === "-") {
|
|
857
|
+
while (i < len && sql.charAt(i) !== "\n") i += 1;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (ch === "/" && next === "*") {
|
|
861
|
+
i += 2;
|
|
862
|
+
while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
|
|
863
|
+
i += 2;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (ch === "'") {
|
|
867
|
+
throw new safeSql.SafeSqlError(
|
|
868
|
+
where + ": raw SQL must not contain a string literal ('...') — bind every " +
|
|
869
|
+
"value with a ? placeholder, or pass { allowLiterals: true } when the literal " +
|
|
870
|
+
"is static and operator-controlled.",
|
|
871
|
+
"sql/raw-literal");
|
|
872
|
+
}
|
|
873
|
+
i += 1;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
755
877
|
function _countPlaceholders(sql) {
|
|
756
878
|
var count = 0;
|
|
757
879
|
var i = 0;
|
package/lib/db.js
CHANGED
|
@@ -70,6 +70,7 @@ var safeJson = require("./safe-json");
|
|
|
70
70
|
var safeSql = require("./safe-sql");
|
|
71
71
|
var validateOpts = require("./validate-opts");
|
|
72
72
|
var vault = require("./vault");
|
|
73
|
+
var vaultAad = require("./vault-aad");
|
|
73
74
|
|
|
74
75
|
var DbError = defineClass("DbError", { alwaysPermanent: true });
|
|
75
76
|
var WormViolationError = require("./framework-error").WormViolationError;
|
|
@@ -153,12 +154,18 @@ var initialized = false;
|
|
|
153
154
|
var dataResidency = null; // operator's declared region config (validated by storage backends)
|
|
154
155
|
var subjectTables = []; // [{ name, subjectField, personalDataCategories }] — for subject.export/erase
|
|
155
156
|
var tableMetadata = {}; // table name → metadata snapshot (PK/FK/sealed/derived) for getTableMetadata
|
|
156
|
-
//
|
|
157
|
+
// StreamLimit ceiling. db.stream() / Query.stream() consult this
|
|
157
158
|
// (overridden per-call via opts.streamLimit). Default cap matches a
|
|
158
159
|
// generous-but-bounded 1M rows so an accidentally-unbounded export
|
|
159
160
|
// surfaces a thrown error instead of OOM. v0.7.67's maxRowsPerQuery
|
|
160
161
|
// bounds .all() / .first() — this is its streaming counterpart.
|
|
161
162
|
var streamLimit = C.BYTES.bytes(1000000); // row-count ceiling, not bytes
|
|
163
|
+
// Column-membership gate mode, set by db.init({ columnGate }). Default
|
|
164
|
+
// "reject" (security-on): a query that orders / selects / filters on a
|
|
165
|
+
// column that is not declared in the table's schema is refused before
|
|
166
|
+
// the identifier interpolates into SQL. "warn" audits + allows; "off"
|
|
167
|
+
// disables the gate.
|
|
168
|
+
var columnGateMode = "reject";
|
|
162
169
|
|
|
163
170
|
// ---- Framework-baked tables ----
|
|
164
171
|
//
|
|
@@ -287,7 +294,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
287
294
|
indexes: ["placedAt"],
|
|
288
295
|
},
|
|
289
296
|
{
|
|
290
|
-
// Per-row crypto-erasure key registry —
|
|
297
|
+
// Per-row crypto-erasure key registry — per-row keys.
|
|
291
298
|
// Each entry holds a sealed wrapped K_row keyed by (table,
|
|
292
299
|
// rowId). b.subject.eraseHard deletes the entry, leaving WAL /
|
|
293
300
|
// replica residuals undecryptable.
|
|
@@ -614,8 +621,8 @@ var FRAMEWORK_SCHEMA = [
|
|
|
614
621
|
{
|
|
615
622
|
// _blamejs_break_glass_grants — issued grants. Each successful
|
|
616
623
|
// step-up creates one row; each row read decrements rowsRemaining.
|
|
617
|
-
// Default maxRowsPerGrant=1 enforces "row by row" auth
|
|
618
|
-
//
|
|
624
|
+
// Default maxRowsPerGrant=1 enforces "row by row" auth
|
|
625
|
+
// (each row access = its own grant).
|
|
619
626
|
// Sealed columns hold reason + scopeColumns so audit-readable
|
|
620
627
|
// metadata doesn't leak in cleartext.
|
|
621
628
|
name: "_blamejs_break_glass_grants",
|
|
@@ -661,25 +668,58 @@ function resolveTmpDir(optsTmpDir) {
|
|
|
661
668
|
|
|
662
669
|
// ---- DB encryption key management ----
|
|
663
670
|
|
|
671
|
+
// AAD binds the sealed DB encryption key to the deployment's dataDir
|
|
672
|
+
// + key-file path, so a key file substituted from another deployment
|
|
673
|
+
// fails the AEAD tag check on unseal (cross-deployment ciphertext
|
|
674
|
+
// substitution / silent re-key — CWE-345 / CWE-441; the AAD itself is
|
|
675
|
+
// NIST SP 800-38D additional-authenticated-data over the XChaCha20-
|
|
676
|
+
// Poly1305 seal). nodePath.resolve (not realpathSync) — the key file
|
|
677
|
+
// may not exist yet at first-run seal.
|
|
678
|
+
function _dbKeyAad(dataDirPath, keyPath) {
|
|
679
|
+
return vaultAad.buildContextAad({
|
|
680
|
+
purpose: "blamejs/db-encryption-key/v1",
|
|
681
|
+
dataDir: nodePath.resolve(dataDirPath),
|
|
682
|
+
keyPath: nodePath.resolve(keyPath),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
664
686
|
function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
|
|
665
687
|
// Operator opt: `opts.dbKeyPath` — useful when the encryption key
|
|
666
688
|
// needs to live outside `dataDir` (e.g. a separate volume mounted
|
|
667
689
|
// from a KMS-fronted secret store). Default places it next to the
|
|
668
690
|
// encrypted DB so backup capture is one-tarball.
|
|
669
691
|
var keyPath = keyPathOverride || nodePath.join(dataDirPath, "db.key.enc");
|
|
692
|
+
var aad = _dbKeyAad(dataDirPath, keyPath);
|
|
670
693
|
if (nodeFs.existsSync(keyPath)) {
|
|
671
694
|
var sealed = atomicFile.readSync(keyPath, { encoding: "utf8" }).trim();
|
|
672
|
-
var b64
|
|
695
|
+
var b64;
|
|
696
|
+
// isAadSealed is checked FIRST and is load-bearing: AAD_PREFIX
|
|
697
|
+
// ("vault.aad:") is NOT a prefix of VAULT_PREFIX ("vault:"), so a
|
|
698
|
+
// plain vault.unseal would silently pass an AAD-sealed value
|
|
699
|
+
// through unchanged. AAD-bound keys verify the deployment context;
|
|
700
|
+
// a key file lifted from another deployment fails the tag check.
|
|
701
|
+
if (vaultAad.isAadSealed(sealed)) {
|
|
702
|
+
b64 = vaultAad.unseal(sealed, aad);
|
|
703
|
+
} else {
|
|
704
|
+
// Legacy plain-sealed key (pre-AAD): unseal with the classic
|
|
705
|
+
// path, then re-seal in place with the deployment-path binding so
|
|
706
|
+
// the next boot is AAD-verified. Read-migration preserves the key
|
|
707
|
+
// bytes — no re-key, no operator action.
|
|
708
|
+
b64 = vault.unseal(sealed);
|
|
709
|
+
if (b64) {
|
|
710
|
+
atomicFile.writeSync(keyPath, vaultAad.seal(b64, aad), { fileMode: 0o600 });
|
|
711
|
+
log("re-sealed DB encryption key with deployment-path binding at " + keyPath);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
673
714
|
if (!b64) {
|
|
674
715
|
throw _dbErr("db/key-unseal-empty",
|
|
675
716
|
"FATAL: db.key.enc unseal returned empty — vault may not be initialized or key file corrupted");
|
|
676
717
|
}
|
|
677
718
|
return Buffer.from(b64, "base64");
|
|
678
719
|
}
|
|
679
|
-
// First run — generate, seal, persist (atomic)
|
|
720
|
+
// First run — generate, AAD-seal, persist (atomic).
|
|
680
721
|
var raw = generateBytes(C.BYTES.bytes(32));
|
|
681
|
-
|
|
682
|
-
var sealedKey = vault.seal(raw.toString("base64"));
|
|
722
|
+
var sealedKey = vaultAad.seal(raw.toString("base64"), aad);
|
|
683
723
|
atomicFile.writeSync(keyPath, sealedKey, { fileMode: 0o600 });
|
|
684
724
|
log("generated DB encryption key at " + keyPath);
|
|
685
725
|
return raw;
|
|
@@ -918,6 +958,7 @@ function cleanStaleTmpDbs(tmpDir) {
|
|
|
918
958
|
* tmpDir: string, // override the encrypted-mode tmpfs path (default /dev/shm or BLAMEJS_TMPDIR)
|
|
919
959
|
* migrationDir: string, // optional — path to ./migrations/ (run-once each)
|
|
920
960
|
* streamLimit: number, // default 1_000_000 — db.stream row ceiling
|
|
961
|
+
* columnGate: "reject"|"warn"|"off", // default "reject" — refuse queries on columns not declared in the table schema
|
|
921
962
|
* skipBootIntegrityCheck: boolean, // default false — skip PRAGMA integrity_check
|
|
922
963
|
* skipIntegrityCheck: boolean, // default false — alias
|
|
923
964
|
* auditSigning: { mode, algorithm }, // default { mode: "wrapped" }
|
|
@@ -966,7 +1007,7 @@ async function init(opts) {
|
|
|
966
1007
|
throw new DbError("db/bad-at-rest",
|
|
967
1008
|
"db.init: atRest must be 'encrypted' or 'plain', got: " + opts.atRest);
|
|
968
1009
|
}
|
|
969
|
-
//
|
|
1010
|
+
// Operator-tunable streamLimit ceiling. Throw at config-time
|
|
970
1011
|
// on bad shape so a typo surfaces at boot rather than as an
|
|
971
1012
|
// unbounded stream at first export.
|
|
972
1013
|
if (opts.streamLimit !== undefined) {
|
|
@@ -978,6 +1019,15 @@ async function init(opts) {
|
|
|
978
1019
|
}
|
|
979
1020
|
streamLimit = opts.streamLimit;
|
|
980
1021
|
}
|
|
1022
|
+
// Column-membership gate mode — throw at config-time on a typo so it
|
|
1023
|
+
// surfaces at boot, not as a query that silently bypasses the gate.
|
|
1024
|
+
if (opts.columnGate !== undefined &&
|
|
1025
|
+
opts.columnGate !== "reject" && opts.columnGate !== "warn" && opts.columnGate !== "off") {
|
|
1026
|
+
throw new DbError("db/bad-init",
|
|
1027
|
+
"db.init: columnGate must be 'reject' (default), 'warn', or 'off'; got " +
|
|
1028
|
+
JSON.stringify(opts.columnGate));
|
|
1029
|
+
}
|
|
1030
|
+
columnGateMode = opts.columnGate || "reject";
|
|
981
1031
|
dataDir = opts.dataDir;
|
|
982
1032
|
if (!nodeFs.existsSync(dataDir)) nodeFs.mkdirSync(dataDir, { recursive: true });
|
|
983
1033
|
|
|
@@ -990,7 +1040,7 @@ async function init(opts) {
|
|
|
990
1040
|
}
|
|
991
1041
|
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
|
|
992
1042
|
|
|
993
|
-
//
|
|
1043
|
+
// If the resolved tmpDir is NOT actually tmpfs, the
|
|
994
1044
|
// plaintext DB file lives on persistent storage. We check that tmpDir
|
|
995
1045
|
// resolves under /dev/shm or /run/shm on Linux as a heuristic; on other
|
|
996
1046
|
// platforms we warn that the operator must verify tmpfs binding
|
|
@@ -1264,9 +1314,10 @@ async function init(opts) {
|
|
|
1264
1314
|
for (var i = 0; i < fullSchema.length; i++) {
|
|
1265
1315
|
var t = fullSchema[i];
|
|
1266
1316
|
cryptoField.registerTable(t.name, {
|
|
1267
|
-
sealedFields:
|
|
1268
|
-
derivedHashes:
|
|
1269
|
-
hashNamespaces:
|
|
1317
|
+
sealedFields: t.sealedFields,
|
|
1318
|
+
derivedHashes: t.derivedHashes,
|
|
1319
|
+
hashNamespaces: t.hashNamespaces,
|
|
1320
|
+
derivedHashMode: t.derivedHashMode,
|
|
1270
1321
|
});
|
|
1271
1322
|
tableMetadata[t.name] = {
|
|
1272
1323
|
primaryKey: _normalizePk(t),
|
|
@@ -1349,7 +1400,7 @@ async function init(opts) {
|
|
|
1349
1400
|
// is BELOW tip — the DB was rolled back to an older snapshot. Refuse boot.
|
|
1350
1401
|
_checkRollback(dataDir);
|
|
1351
1402
|
|
|
1352
|
-
// ----
|
|
1403
|
+
// ---- WORM posture assertion ----
|
|
1353
1404
|
// Under sec-17a-4 / finra-4511 / fda-21cfr11 postures the operator
|
|
1354
1405
|
// MUST have declared row-level WORM on at least one business-record
|
|
1355
1406
|
// table. Refuse boot otherwise so missing-declaration drift is
|
|
@@ -1491,10 +1542,41 @@ async function init(opts) {
|
|
|
1491
1542
|
*/
|
|
1492
1543
|
function from(tableName) {
|
|
1493
1544
|
_requireInit();
|
|
1494
|
-
return new Query(database, tableName
|
|
1545
|
+
return new Query(database, tableName, {
|
|
1546
|
+
declaredColumns: getDeclaredColumns(tableName),
|
|
1547
|
+
columnGateMode: columnGateMode,
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* @primitive b.db.getDeclaredColumns
|
|
1553
|
+
* @signature b.db.getDeclaredColumns(tableName)
|
|
1554
|
+
* @since 0.14.7
|
|
1555
|
+
* @status stable
|
|
1556
|
+
* @related b.db.from, b.db.getTableMetadata
|
|
1557
|
+
*
|
|
1558
|
+
* Returns the declared column names for a table as an array, or `null`
|
|
1559
|
+
* when the table has no registered schema metadata (a cross- or
|
|
1560
|
+
* attached-schema table — the column-membership gate is a no-op for
|
|
1561
|
+
* those). The declared set includes `_id` and any derived-hash columns,
|
|
1562
|
+
* so sealed-field queries (which rewrite to the hash column) and `_id`
|
|
1563
|
+
* lookups pass the gate. Backs the `db.init({ columnGate })` gate that
|
|
1564
|
+
* refuses queries ordering / selecting / filtering on an undeclared
|
|
1565
|
+
* column before the identifier interpolates into SQL.
|
|
1566
|
+
*
|
|
1567
|
+
* @example
|
|
1568
|
+
* b.db.getDeclaredColumns("orders");
|
|
1569
|
+
* // → ["_id", "customerId", "total", "createdAt"]
|
|
1570
|
+
*/
|
|
1571
|
+
// The declared set includes `_id` and any derived-hash columns, so
|
|
1572
|
+
// sealed-field queries (which rewrite to the hash column) and `_id`
|
|
1573
|
+
// lookups pass membership.
|
|
1574
|
+
function getDeclaredColumns(tableName) {
|
|
1575
|
+
var md = tableMetadata[tableName];
|
|
1576
|
+
return (md && md.columns) ? Object.keys(md.columns) : null;
|
|
1495
1577
|
}
|
|
1496
1578
|
|
|
1497
|
-
//
|
|
1579
|
+
// Bounded prepared-statement cache for SQLite. Long-running
|
|
1498
1580
|
// daemons with diverse query shapes accumulate node:sqlite Statement
|
|
1499
1581
|
// handles indefinitely; the LRU here caps at PREPARE_CACHE_MAX (256)
|
|
1500
1582
|
// distinct SQL strings and finalizes the oldest when over. Reuse of
|
|
@@ -1611,7 +1693,7 @@ function stream(sql) {
|
|
|
1611
1693
|
var table = opts && typeof opts.table === "string" ? opts.table : null;
|
|
1612
1694
|
var unseal = table ? cryptoField : null;
|
|
1613
1695
|
|
|
1614
|
-
//
|
|
1696
|
+
// StreamLimit ceiling. Per-call opts.streamLimit overrides
|
|
1615
1697
|
// the module-level default; bad shape throws at call time so the
|
|
1616
1698
|
// typo surfaces instead of an unbounded stream.
|
|
1617
1699
|
var perCallLimit = streamLimit;
|
|
@@ -1661,10 +1743,10 @@ function stream(sql) {
|
|
|
1661
1743
|
|
|
1662
1744
|
// DDL_RE — case-insensitive prefix match for the eight statement
|
|
1663
1745
|
// shapes that MUTATE schema. Audited individually so a forensic
|
|
1664
|
-
// review can reconstruct schema evolution from the chain alone
|
|
1746
|
+
// review can reconstruct schema evolution from the chain alone.
|
|
1665
1747
|
var DDL_RE = /^\s*(CREATE|DROP|ALTER|TRUNCATE|RENAME|ATTACH|DETACH|REINDEX)\b/i;
|
|
1666
1748
|
|
|
1667
|
-
//
|
|
1749
|
+
// Slow-query observability buckets for the local SQLite nodePath.
|
|
1668
1750
|
// Highest matched bucket wins so the per-query emit is single-shot;
|
|
1669
1751
|
// operators dashboard on the `bucket` label.
|
|
1670
1752
|
var _SLOW_QUERY_BUCKETS_LOCAL = Object.freeze([
|
|
@@ -1712,7 +1794,7 @@ function execRaw(sql) {
|
|
|
1712
1794
|
action: "db.ddl.executed",
|
|
1713
1795
|
outcome: "success",
|
|
1714
1796
|
metadata: {
|
|
1715
|
-
// OTel db.* semconv
|
|
1797
|
+
// OTel db.* semconv — emit framework-conventional
|
|
1716
1798
|
// attributes alongside the audit row so dashboards built on
|
|
1717
1799
|
// OTel can correlate without an adapter.
|
|
1718
1800
|
"db.system": "sqlite",
|
|
@@ -2701,7 +2783,7 @@ function vacuumAfterErase(opts) {
|
|
|
2701
2783
|
} catch (_e) { /* audit best-effort */ }
|
|
2702
2784
|
}
|
|
2703
2785
|
|
|
2704
|
-
//
|
|
2786
|
+
// Cascade-installed posture name. b.compliance.set(p)
|
|
2705
2787
|
// calls applyPosture(p) which records the posture; the downstream
|
|
2706
2788
|
// cryptoField.eraseRow path consults this via getActivePosture() to
|
|
2707
2789
|
// auto-vacuum under postures whose POSTURE_DEFAULTS sets
|
|
@@ -3054,10 +3136,12 @@ module.exports = {
|
|
|
3054
3136
|
getActivePosture: getActivePosture,
|
|
3055
3137
|
vacuumAfterErase: vacuumAfterErase,
|
|
3056
3138
|
from: from,
|
|
3139
|
+
getDeclaredColumns: getDeclaredColumns,
|
|
3140
|
+
_checkDualControlGate: _checkDualControlGate,
|
|
3057
3141
|
collection: require("./db-collection").collection, // allow:inline-require — db-collection lazy-requires db.js back; the inline require here breaks the cycle without needing a stub
|
|
3058
3142
|
prepare: prepare,
|
|
3059
3143
|
stream: stream,
|
|
3060
|
-
//
|
|
3144
|
+
// Runtime read-only accessor so Query.stream picks up the
|
|
3061
3145
|
// configured ceiling without re-importing module state.
|
|
3062
3146
|
getStreamLimit: function () { return streamLimit; },
|
|
3063
3147
|
runSql: execRaw,
|
|
@@ -132,7 +132,7 @@ function _parseHHMM(s) {
|
|
|
132
132
|
throw new DdlChangeControlError("ddl-change-control/bad-window",
|
|
133
133
|
"windowSpec time out of range - got " + s);
|
|
134
134
|
}
|
|
135
|
-
return hh * 60 + mm; // allow:raw-time-literal —
|
|
135
|
+
return hh * 60 + mm; // allow:raw-time-literal — HH*60+MM minute-of-day conversion; coincidental multiple-of-60 factor, not a duration, C.TIME N/A
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function _isInWindow(window, nowMs) {
|
|
@@ -141,7 +141,7 @@ function _isInWindow(window, nowMs) {
|
|
|
141
141
|
var d = new Date(nowMs);
|
|
142
142
|
var dayIdx = d.getUTCDay();
|
|
143
143
|
if (!window.days.has(dayIdx)) return false;
|
|
144
|
-
var min = d.getUTCHours() * 60 + d.getUTCMinutes(); // allow:raw-time-literal —
|
|
144
|
+
var min = d.getUTCHours() * 60 + d.getUTCMinutes(); // allow:raw-time-literal — HH*60+MM minute-of-day conversion; coincidental multiple-of-60 factor, not a duration, C.TIME N/A
|
|
145
145
|
return min >= window.startMin && min < window.endMin;
|
|
146
146
|
}
|
|
147
147
|
|
package/lib/did.js
CHANGED
|
@@ -65,7 +65,7 @@ var MAX_JWK_B64_CHARS = 8192; // bounded did:jwk encode
|
|
|
65
65
|
// keyLen is the multicodec payload: Ed25519 raw 32; EC compressed point.
|
|
66
66
|
var MULTICODEC = {
|
|
67
67
|
0xed: { name: "Ed25519", kind: "okp" }, // ed25519-pub
|
|
68
|
-
0x1200: { name: "P-256", kind: "ec", curveOid: "1.2.840.10045.3.1.7" },
|
|
68
|
+
0x1200: { name: "P-256", kind: "ec", curveOid: "1.2.840.10045.3.1.7" },
|
|
69
69
|
0x1201: { name: "P-384", kind: "ec", curveOid: "1.3.132.0.34" }, // p384-pub multicodec code
|
|
70
70
|
0xe7: { name: "secp256k1", kind: "ec", curveOid: "1.3.132.0.10" }, // secp256k1-pub
|
|
71
71
|
};
|
|
@@ -159,7 +159,7 @@ function _keyObjectFromMulticodec(code, keyBytes) {
|
|
|
159
159
|
|
|
160
160
|
// AlgorithmIdentifier SEQUENCE { id-ecPublicKey, namedCurve OID }.
|
|
161
161
|
function _ecAlgId(curveOid) {
|
|
162
|
-
var idEcPublicKey = Buffer.from("06072a8648ce3d0201", "hex");
|
|
162
|
+
var idEcPublicKey = Buffer.from("06072a8648ce3d0201", "hex");
|
|
163
163
|
var curve = _oidDer(curveOid);
|
|
164
164
|
var inner = Buffer.concat([idEcPublicKey, curve]);
|
|
165
165
|
return Buffer.concat([Buffer.from([0x30, inner.length]), inner]);
|
package/lib/dsr.js
CHANGED
|
@@ -1094,7 +1094,7 @@ function dbTicketStore(opts) {
|
|
|
1094
1094
|
|
|
1095
1095
|
// State DSR rule table — `responseDays` / `extensionDays` / `cureDays`
|
|
1096
1096
|
// are integer day-counts from per-state statutes (not durations in
|
|
1097
|
-
// seconds/ms).
|
|
1097
|
+
// seconds/ms).
|
|
1098
1098
|
var STATE_RULES = Object.freeze({
|
|
1099
1099
|
"vcdpa": { posture: "vcdpa", state: "VA", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01" }, // allow:raw-time-literal
|
|
1100
1100
|
"co-cpa": { posture: "co-cpa", state: "CO", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01; UOOM (GPC) mandatory" }, // allow:raw-time-literal
|
|
@@ -1110,7 +1110,7 @@ var STATE_RULES = Object.freeze({
|
|
|
1110
1110
|
"nj-njdpa": { posture: "nj-njdpa", state: "NJ", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 17, notes: "Under-17 opt-in default" }, // allow:raw-time-literal
|
|
1111
1111
|
"ky-kcdpa": { posture: "ky-kcdpa", state: "KY", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
|
|
1112
1112
|
"tn-tipa": { posture: "tn-tipa", state: "TN", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "NIST CSF safe-harbor available" }, // allow:raw-time-literal
|
|
1113
|
-
"mn-mncdpa": { posture: "mn-mncdpa", state: "MN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-07-31; profiling opt-out for consequential decisions" },
|
|
1113
|
+
"mn-mncdpa": { posture: "mn-mncdpa", state: "MN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-07-31; profiling opt-out for consequential decisions" },
|
|
1114
1114
|
"ri-ricpa": { posture: "ri-ricpa", state: "RI", responseDays: 45, extensionDays: 45, cureDays: 0, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01; no cure period" }, // allow:raw-time-literal
|
|
1115
1115
|
"ne-dpa": { posture: "ne-dpa", state: "NE", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2025-01-01" }, // allow:raw-time-literal
|
|
1116
1116
|
"nv-sb370": { posture: "nv-sb370", state: "NV", responseDays: 60, extensionDays: 30, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
|
|
@@ -1118,8 +1118,8 @@ var STATE_RULES = Object.freeze({
|
|
|
1118
1118
|
"ct-sb3": { posture: "ct-sb3", state: "CT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
|
|
1119
1119
|
"tx-cubi": { posture: "tx-cubi", state: "TX", responseDays: 0, extensionDays: 0, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Biometric-only; private-right-of-action absent" }, // allow:raw-time-literal
|
|
1120
1120
|
"modpa": { posture: "modpa", state: "MD", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Strict data-minimization; effective 2026-10-01" }, // allow:raw-time-literal
|
|
1121
|
-
"quebec-25": { posture: "quebec-25", state: "QC", responseDays: 30, extensionDays: 30, cureDays: 0, profilingOptOut: true, minorOptIn: 14, notes: "DPIA + automated-decision opt-out; FR-language obligations" },
|
|
1122
|
-
"fl-fdbr": { posture: "fl-fdbr", state: "FL", responseDays: 45, extensionDays: 15, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Narrow scope ($1B+ revenue threshold); effective 2024-07-01; AG-only enforcement" },
|
|
1121
|
+
"quebec-25": { posture: "quebec-25", state: "QC", responseDays: 30, extensionDays: 30, cureDays: 0, profilingOptOut: true, minorOptIn: 14, notes: "DPIA + automated-decision opt-out; FR-language obligations" },
|
|
1122
|
+
"fl-fdbr": { posture: "fl-fdbr", state: "FL", responseDays: 45, extensionDays: 15, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Narrow scope ($1B+ revenue threshold); effective 2024-07-01; AG-only enforcement" },
|
|
1123
1123
|
});
|
|
1124
1124
|
|
|
1125
1125
|
/**
|