@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/external-db.js
CHANGED
|
@@ -57,7 +57,7 @@ function _emitMetric(name, value, labels) {
|
|
|
57
57
|
catch (_e) { /* hot-path observability sink — drop silent by design */ }
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Statement-class classifier for auth-failure forensics
|
|
60
|
+
// Statement-class classifier for auth-failure forensics. Inspects
|
|
61
61
|
// the leading keyword only so an attacker-controlled trailing fragment
|
|
62
62
|
// can't smuggle a false classification. Skips leading whitespace plus
|
|
63
63
|
// SQL line / block comments before reading the keyword.
|
|
@@ -83,8 +83,49 @@ function _classifyStatement(sql) {
|
|
|
83
83
|
return _STATEMENT_CLASS_MAP[m[1].toUpperCase()] || "OTHER";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Best-effort target-relation extractor for auth-failure forensics: the
|
|
87
|
+
// table the denied role attempted to touch, so the audit row records
|
|
88
|
+
// the OBJECT (SOC2 CC7.2 / NIST SP 800-53 AU-3 "what was accessed"),
|
|
89
|
+
// not just the statement class. Defensive reader — returns null on
|
|
90
|
+
// anything unparseable and NEVER throws: it runs in the live failure
|
|
91
|
+
// path and must not mask the real 28000 / 42501 error. The extracted
|
|
92
|
+
// identifier is captured into audit METADATA (a JSON string, never
|
|
93
|
+
// re-executed as SQL), so the only sink risk is log-injection: any
|
|
94
|
+
// segment carrying a control / NUL character is refused. Spaces and
|
|
95
|
+
// ordinary punctuation inside a quoted identifier are kept so a
|
|
96
|
+
// legitimately-quoted relation name still surfaces in the audit row.
|
|
97
|
+
var _RELATION_RE = /\b(?:FROM|INTO|UPDATE|JOIN|TABLE|COPY)\s+((?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*))?)/ig;
|
|
98
|
+
function _hasControlChar(s) {
|
|
99
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
100
|
+
var c = s.charCodeAt(i);
|
|
101
|
+
if (c < 0x20 || c === 0x7f) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function _extractTargetRelation(sql) {
|
|
106
|
+
if (typeof sql !== "string" || sql.length === 0) return null;
|
|
107
|
+
var clean = sql.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
108
|
+
_RELATION_RE.lastIndex = 0;
|
|
109
|
+
var m = _RELATION_RE.exec(clean);
|
|
110
|
+
if (!m) return null;
|
|
111
|
+
var segs = m[1].split(".").map(function (s) { return s.replace(/^["`]|["`]$/g, ""); });
|
|
112
|
+
for (var i = 0; i < segs.length; i += 1) {
|
|
113
|
+
if (segs[i].length === 0 || _hasControlChar(segs[i])) return null;
|
|
114
|
+
}
|
|
115
|
+
return segs.join(".");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _countTargetRelations(sql) {
|
|
119
|
+
if (typeof sql !== "string") return 0;
|
|
120
|
+
var clean = sql.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
121
|
+
_RELATION_RE.lastIndex = 0;
|
|
122
|
+
var n = 0;
|
|
123
|
+
while (_RELATION_RE.exec(clean) !== null) n += 1;
|
|
124
|
+
return n;
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
// Postgres SQLSTATE classes that indicate authentication / authorization
|
|
87
|
-
// failure at the DB level. SOC2 forensic gap
|
|
128
|
+
// failure at the DB level. SOC2 forensic gap — every match emits
|
|
88
129
|
// db.auth.failed with the SQL identity attempted, the database, and
|
|
89
130
|
// the statement class.
|
|
90
131
|
var _AUTH_FAILURE_CODES = Object.freeze({
|
|
@@ -97,18 +138,24 @@ function _emitAuthFailureAudit(backend, role, sql, e) {
|
|
|
97
138
|
if (!e || !e.code) return;
|
|
98
139
|
var kind = _AUTH_FAILURE_CODES[e.code];
|
|
99
140
|
if (!kind) return;
|
|
141
|
+
var attemptedTable = _extractTargetRelation(sql);
|
|
142
|
+
var relationCount = _countTargetRelations(sql);
|
|
143
|
+
var resource = { kind: "db.backend", id: backend.name };
|
|
144
|
+
if (attemptedTable !== null) resource.attemptedTable = attemptedTable;
|
|
100
145
|
audit().safeEmit({
|
|
101
146
|
action: "db.auth.failed",
|
|
102
147
|
actor: {},
|
|
103
|
-
resource:
|
|
148
|
+
resource: resource,
|
|
104
149
|
outcome: "denied",
|
|
105
150
|
reason: kind,
|
|
106
151
|
metadata: {
|
|
107
|
-
backend:
|
|
108
|
-
dialect:
|
|
109
|
-
sqlIdentity:
|
|
110
|
-
sqlstate:
|
|
111
|
-
statementClass:
|
|
152
|
+
backend: backend.name,
|
|
153
|
+
dialect: backend.dialect,
|
|
154
|
+
sqlIdentity: role || null,
|
|
155
|
+
sqlstate: e.code,
|
|
156
|
+
statementClass: _classifyStatement(sql),
|
|
157
|
+
attemptedTable: attemptedTable,
|
|
158
|
+
attemptedRelationCount: relationCount,
|
|
112
159
|
},
|
|
113
160
|
});
|
|
114
161
|
_emitMetric("db.auth.failed", 1, {
|
|
@@ -118,7 +165,7 @@ function _emitAuthFailureAudit(backend, role, sql, e) {
|
|
|
118
165
|
});
|
|
119
166
|
}
|
|
120
167
|
|
|
121
|
-
// Slow-query bucket emitter
|
|
168
|
+
// Slow-query bucket emitter. Single-shot per query — highest
|
|
122
169
|
// matched bucket wins. Operators dashboard on the `bucket` label
|
|
123
170
|
// rather than separate counters per threshold.
|
|
124
171
|
var _SLOW_QUERY_BUCKETS = Object.freeze([
|
|
@@ -628,7 +675,7 @@ async function query(sql, params, opts) {
|
|
|
628
675
|
_emitMetric("db.role.denied", 1,
|
|
629
676
|
{ backend: b.name, role: role || "(none)" });
|
|
630
677
|
}
|
|
631
|
-
//
|
|
678
|
+
// DB-auth audit visibility. Every 28000 / 28P01 / 42501
|
|
632
679
|
// surfaces an auditable db.auth.failed row tagged with the SQL
|
|
633
680
|
// identity and the statement class so SOC2 reviewers can
|
|
634
681
|
// reconstruct the denial timeline.
|
|
@@ -693,13 +740,13 @@ async function transaction(fn, opts) {
|
|
|
693
740
|
var prebuiltGucs = _buildSessionGucsStatements(opts.sessionGucs);
|
|
694
741
|
|
|
695
742
|
var t0 = Date.now();
|
|
696
|
-
//
|
|
697
|
-
// the query-cancel ceiling to this transaction;
|
|
743
|
+
// Per-statement timeout. SET LOCAL statement_timeout binds
|
|
744
|
+
// the query-cancel ceiling to this transaction; this wires
|
|
698
745
|
// idle_in_transaction_session_timeout from the same opt. Both
|
|
699
746
|
// emit at SET LOCAL scope so the next pool checkout starts clean.
|
|
700
747
|
var stmtTimeoutMs = opts.statementTimeoutMs;
|
|
701
748
|
var idleTimeoutMs = opts.idleInTransactionTimeoutMs;
|
|
702
|
-
//
|
|
749
|
+
// Deadlock-retry policy. 40P01 (deadlock_detected) and 40001
|
|
703
750
|
// (serialization_failure) are transient — retry with capped attempts
|
|
704
751
|
// and a small jittered backoff. Operators tune retries via opts.deadlockRetries (default 3).
|
|
705
752
|
// numeric-bounds doesn't have a non-negative-int helper; use a
|
|
@@ -756,7 +803,7 @@ async function transaction(fn, opts) {
|
|
|
756
803
|
_emitMetric("externaldb.transaction.retry", 1,
|
|
757
804
|
{ backend: b.name, code: txErr.code, attempt: String(attempt) });
|
|
758
805
|
var jitter = bCrypto.randomInt(0, 6); // 0-5ms jitter
|
|
759
|
-
await safeAsync.sleep(attempt * 5 + jitter);
|
|
806
|
+
await safeAsync.sleep(attempt * 5 + jitter);
|
|
760
807
|
continue;
|
|
761
808
|
}
|
|
762
809
|
var failureMs = Date.now() - t0;
|
|
@@ -771,7 +818,7 @@ async function transaction(fn, opts) {
|
|
|
771
818
|
_emitMetric("db.role.denied", 1,
|
|
772
819
|
{ backend: b.name, role: role || "(none)" });
|
|
773
820
|
}
|
|
774
|
-
//
|
|
821
|
+
// DB-auth audit visibility on transaction-shaped denials.
|
|
775
822
|
// Statement class always reads as "TX" since the failure
|
|
776
823
|
// surface inside a transaction body could be any statement;
|
|
777
824
|
// operators correlate via the transaction's audit row.
|
|
@@ -1017,7 +1064,7 @@ function _requireInit() {
|
|
|
1017
1064
|
|
|
1018
1065
|
var REPLICA_UNHEALTHY_COOLDOWN_MS = C.TIME.seconds(30);
|
|
1019
1066
|
|
|
1020
|
-
//
|
|
1067
|
+
// Replica residency-tag compatibility.
|
|
1021
1068
|
//
|
|
1022
1069
|
// A primary tagged "EU" replicating to a "US" replica is a GDPR
|
|
1023
1070
|
// Article 46 cross-border transfer; without an explicit operator
|
|
@@ -1194,7 +1241,7 @@ async function _readQuery(sql, params, opts) {
|
|
|
1194
1241
|
_emitMetric("db.role.denied", 1,
|
|
1195
1242
|
{ backend: b.name, role: role || "(none)" });
|
|
1196
1243
|
}
|
|
1197
|
-
//
|
|
1244
|
+
// DB-auth audit visibility for read-replica denials too.
|
|
1198
1245
|
_emitAuthFailureAudit(b, role, sql, e);
|
|
1199
1246
|
// Fallback to primary on a failed replica read when allowed.
|
|
1200
1247
|
if (b.replicaFallbackToPrimary) {
|
|
@@ -1874,4 +1921,5 @@ module.exports = {
|
|
|
1874
1921
|
migrate: externalDbMigrate,
|
|
1875
1922
|
Pool: Pool,
|
|
1876
1923
|
_resetForTest: _resetForTest,
|
|
1924
|
+
_extractTargetRelation: _extractTargetRelation,
|
|
1877
1925
|
};
|
package/lib/framework-schema.js
CHANGED
|
@@ -648,8 +648,8 @@ function _breakGlassPoliciesDDL(dialect) {
|
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
// _blamejs_break_glass_grants — issued grants. One row per successful
|
|
651
|
-
// step-up. Default maxRowsPerGrant=1 enforces row-by-row auth
|
|
652
|
-
//
|
|
651
|
+
// step-up. Default maxRowsPerGrant=1 enforces row-by-row auth
|
|
652
|
+
// ("each row access = its own grant").
|
|
653
653
|
function _breakGlassGrantsDDL(dialect) {
|
|
654
654
|
var t = _types(dialect);
|
|
655
655
|
var name = LOCAL_TO_EXTERNAL._blamejs_break_glass_grants;
|
|
@@ -766,7 +766,7 @@ async function ensureSchema(opts) {
|
|
|
766
766
|
created.push(d.create.match(/CREATE TABLE IF NOT EXISTS\s+(\S+)/)[1]);
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
-
//
|
|
769
|
+
// Append-only WORM enforcement on audit_log / consent_log /
|
|
770
770
|
// audit_checkpoints in cluster mode. Local-SQLite path already
|
|
771
771
|
// installs CREATE TRIGGER IF NOT EXISTS via lib/db.js's
|
|
772
772
|
// _installAppendOnlyTriggers; Postgres needs equivalent rules
|
|
@@ -779,7 +779,7 @@ async function ensureSchema(opts) {
|
|
|
779
779
|
return { tables: created };
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
-
//
|
|
782
|
+
// WORM enforcement helper. Idempotent: rebuilding triggers
|
|
783
783
|
// per boot is cheap and any operator-applied DROP TRIGGER is restored
|
|
784
784
|
// at the next ensureSchema pass.
|
|
785
785
|
async function _installWormTriggers(backend, dialect) {
|
package/lib/guard-cidr.js
CHANGED
|
@@ -73,7 +73,7 @@ var IPV4_RESERVED = Object.freeze([
|
|
|
73
73
|
{ net: _ipv4ToUint32([127, 0, 0, 0]), prefix: 8, label: "loopback" }, // IPv4 octets
|
|
74
74
|
{ net: _ipv4ToUint32([169, 254, 0, 0]), prefix: 16, label: "link-local" }, // IPv4 octets
|
|
75
75
|
{ net: _ipv4ToUint32([224, 0, 0, 0]), prefix: 4, label: "multicast" }, // IPv4 octets
|
|
76
|
-
{ net: _ipv4ToUint32([240, 0, 0, 0]), prefix: 4, label: "reserved-class-e" }, // allow:raw-time-literal —
|
|
76
|
+
{ net: _ipv4ToUint32([240, 0, 0, 0]), prefix: 4, label: "reserved-class-e" }, // allow:raw-time-literal — IPv4 octet 240; coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
77
77
|
{ net: _ipv4ToUint32([192, 0, 2, 0]), prefix: 24, label: "documentation-test-net-1" }, // IPv4 octets
|
|
78
78
|
{ net: _ipv4ToUint32([198, 51, 100, 0]), prefix: 24, label: "documentation-test-net-2" }, // IPv4 octets
|
|
79
79
|
{ net: _ipv4ToUint32([203, 0, 113, 0]), prefix: 24, label: "documentation-test-net-3" }, // IPv4 octets
|
package/lib/guard-image.js
CHANGED
|
@@ -111,7 +111,7 @@ var PROFILES = Object.freeze({
|
|
|
111
111
|
framesPolicy: "reject",
|
|
112
112
|
maxWidth: C.BYTES.bytes(8192), // pixel cap, repurposing bytes() for clarity
|
|
113
113
|
maxHeight: C.BYTES.bytes(8192),
|
|
114
|
-
maxFrames: 60, // allow:raw-time-literal —
|
|
114
|
+
maxFrames: 60, // allow:raw-time-literal — max-frame count 60; coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
115
115
|
maxBytes: C.BYTES.mib(32),
|
|
116
116
|
maxRuntimeMs: C.TIME.seconds(5),
|
|
117
117
|
},
|
package/lib/guard-list-id.js
CHANGED
|
@@ -203,8 +203,8 @@ function validate(headerValue, opts) {
|
|
|
203
203
|
// recover the boundary without Public Suffix List awareness
|
|
204
204
|
// (`team.example.com` could be label=team / ns=example.com OR
|
|
205
205
|
// label=team.example / ns=com). The earlier last-2-segment
|
|
206
|
-
// heuristic produced empty `label` for 2-label IDs
|
|
207
|
-
//
|
|
206
|
+
// heuristic produced empty `label` for 2-label IDs
|
|
207
|
+
// which violates RFC 2919 §2's required label "."
|
|
208
208
|
// namespace decomposition.
|
|
209
209
|
//
|
|
210
210
|
// Drop the heuristic split — surface only the raw `listId` (and
|
|
@@ -349,8 +349,7 @@ function _extractUris(raw, maxUris) {
|
|
|
349
349
|
// bracket pairs directly via String.matchAll so URIs containing
|
|
350
350
|
// commas (legitimate, e.g. `<https://x/u?tags=a,b>`) parse
|
|
351
351
|
// correctly. Earlier split(",")-based scan misclassified such
|
|
352
|
-
// URIs as "no <URI> elements" and refused legitimate mail
|
|
353
|
-
// (Codex P1 on PR #63).
|
|
352
|
+
// URIs as "no <URI> elements" and refused legitimate mail.
|
|
354
353
|
var matches = raw.matchAll(/<([^<>]*)>/g); // allow:regex-no-length-cap — input length-bounded by maxBytes check upstream
|
|
355
354
|
var uris = [];
|
|
356
355
|
for (var m of matches) {
|
|
@@ -372,7 +371,7 @@ function _hasControlChar(s) {
|
|
|
372
371
|
|
|
373
372
|
function _trunc(s) {
|
|
374
373
|
if (s.length <= 64) return s; // error-message truncation
|
|
375
|
-
return s.slice(0, 60) + "…"; // allow:raw-time-literal — char
|
|
374
|
+
return s.slice(0, 60) + "…"; // allow:raw-time-literal — truncation char-count 60; coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
376
375
|
}
|
|
377
376
|
|
|
378
377
|
function _verdict(action, reason, extra) {
|
package/lib/guard-time.js
CHANGED
|
@@ -254,7 +254,7 @@ function _detectIssues(input, opts) {
|
|
|
254
254
|
snippet: "minute " + minute + " > 59",
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
|
-
if (second > 60) { // allow:raw-time-literal — leap-second ceiling
|
|
257
|
+
if (second > 60) { // allow:raw-time-literal — leap-second ceiling literal 60 (RFC 3339 5.6); coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
258
258
|
issues.push({
|
|
259
259
|
kind: "second-range", severity: "high",
|
|
260
260
|
ruleId: "time.second-range",
|
package/lib/guard-xml.js
CHANGED
|
@@ -303,7 +303,7 @@ function _detectIssues(input, opts) {
|
|
|
303
303
|
// get the NCR cap disabled with them. The `maxNumericCharRefs` opt
|
|
304
304
|
// is validated by `numericBounds.requireAllPositiveFiniteIntIfPresent`
|
|
305
305
|
// at the public-surface boundary above.
|
|
306
|
-
var ncrCap = opts.maxNumericCharRefs;
|
|
306
|
+
var ncrCap = opts.maxNumericCharRefs;
|
|
307
307
|
if (ncrCap !== undefined && ncrCap !== null) {
|
|
308
308
|
var ncrMatches = input.match(NUMERIC_CHAR_REF_RE); // allow:regex-no-length-cap — input bounded by maxBytes above
|
|
309
309
|
var ncrCount = ncrMatches === null ? 0 : ncrMatches.length;
|
package/lib/http-client-cache.js
CHANGED
|
@@ -66,7 +66,7 @@ var HEURISTIC_MAX_AGE_MS = C.TIME.hours(24);
|
|
|
66
66
|
// Statuses RFC 9110 designates as heuristically cacheable. (Plus 200/206
|
|
67
67
|
// which are universally cacheable when a freshness lifetime is given.)
|
|
68
68
|
var CACHEABLE_STATUSES = new Set([
|
|
69
|
-
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501, // allow:raw-time-literal —
|
|
69
|
+
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501, // allow:raw-time-literal — RFC 9111 cacheable status-code set; coincidental multiple-of-60 entries, not durations, C.TIME N/A
|
|
70
70
|
]);
|
|
71
71
|
|
|
72
72
|
// Headers that MUST not be forwarded when serving a 304-updated entry.
|
package/lib/iab-tcf.js
CHANGED
|
@@ -108,7 +108,7 @@ var TCF_V23_POLICY_VERSION = 4;
|
|
|
108
108
|
var SEGMENT_TYPE_DISCLOSED_VENDORS = 1; // TCF segment-type marker, not bytes
|
|
109
109
|
var SEGMENT_TYPE_ALLOWED_VENDORS = 2; // TCF segment-type marker, not bytes
|
|
110
110
|
var SEGMENT_TYPE_PUBLISHER_TC = 3; // TCF segment-type marker, not bytes
|
|
111
|
-
var MAX_TC_STRING_BYTES = 64 * 1024;
|
|
111
|
+
var MAX_TC_STRING_BYTES = 64 * 1024;
|
|
112
112
|
|
|
113
113
|
// base64url decode (no padding) → Buffer.
|
|
114
114
|
function _b64urlDecode(s) {
|
|
@@ -186,8 +186,8 @@ function _parseCore(buf) {
|
|
|
186
186
|
var publisherRestrictions = _parsePublisherRestrictions(r);
|
|
187
187
|
return {
|
|
188
188
|
version: version,
|
|
189
|
-
createdAt: createdRaw * 100,
|
|
190
|
-
lastUpdatedAt: lastUpdatedRaw * 100,
|
|
189
|
+
createdAt: createdRaw * 100,
|
|
190
|
+
lastUpdatedAt: lastUpdatedRaw * 100,
|
|
191
191
|
cmpId: cmpId,
|
|
192
192
|
cmpVersion: cmpVersion,
|
|
193
193
|
consentScreen: consentScreen,
|
|
@@ -545,7 +545,7 @@ function _idRuns(ids) {
|
|
|
545
545
|
function _decisec(t) {
|
|
546
546
|
var ms = t instanceof Date ? t.getTime() : (t == null ? Date.now() : Number(t));
|
|
547
547
|
if (!isFinite(ms) || ms < 0) throw IabTcfError.factory("iab-tcf/bad-value", "iabTcf.encode: timestamp must be a Date or non-negative epoch-ms");
|
|
548
|
-
return Math.round(ms / 100);
|
|
548
|
+
return Math.round(ms / 100);
|
|
549
549
|
}
|
|
550
550
|
|
|
551
551
|
// Bit writer mirroring _bitReader: bits are packed high-bit-first, then the
|
package/lib/incident-report.js
CHANGED
|
@@ -305,8 +305,158 @@ function create(opts) {
|
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Breach detection -> notification running clock. The reporter
|
|
309
|
+
// (`create`) computes the static per-stage deadlines; this clock turns
|
|
310
|
+
// them into a live escalation loop: it tracks open incident records and,
|
|
311
|
+
// on each tick, fires "approaching" warnings as a stage's deadline nears
|
|
312
|
+
// and a "passed" alert when it elapses — once per (incident, stage,
|
|
313
|
+
// state) so a busy tick interval can't storm the operator. It re-uses
|
|
314
|
+
// the reporter's REGIME_DEADLINES / dueBy timestamps and re-encodes no
|
|
315
|
+
// jurisdiction hour-counts (GDPR Art.33 72h, NIS2 Art.23(4) 24h, DORA
|
|
316
|
+
// Art.19 + RTS 2024/1772 4h, CRA Art.14, HIPAA 45 CFR 164.404/408).
|
|
317
|
+
// `approachThresholds` are unitless proportions of detected-to-due.
|
|
318
|
+
function createDeadlineClock(opts) {
|
|
319
|
+
opts = opts || {};
|
|
320
|
+
validateOpts(opts, [
|
|
321
|
+
"audit", "notify", "approachThresholds", "intervalMs", "autoStart", "now",
|
|
322
|
+
], "incident.report.createDeadlineClock");
|
|
323
|
+
|
|
324
|
+
var auditOn = opts.audit !== false;
|
|
325
|
+
var notify = (opts.notify && typeof opts.notify.send === "function") ? opts.notify : null;
|
|
326
|
+
var thresholds = Array.isArray(opts.approachThresholds) ? opts.approachThresholds.slice() : [0.5, 0.75, 0.9];
|
|
327
|
+
for (var ti = 0; ti < thresholds.length; ti += 1) {
|
|
328
|
+
if (typeof thresholds[ti] !== "number" || !(thresholds[ti] > 0 && thresholds[ti] < 1)) {
|
|
329
|
+
throw new IncidentReportError("incident-report/bad-threshold",
|
|
330
|
+
"createDeadlineClock: approachThresholds must be numbers strictly between 0 and 1");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
thresholds.sort(function (a, b) { return a - b; });
|
|
334
|
+
var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
|
|
335
|
+
var intervalMs = (typeof opts.intervalMs === "number" && isFinite(opts.intervalMs) && opts.intervalMs > 0)
|
|
336
|
+
? opts.intervalMs : C.TIME.minutes(1);
|
|
337
|
+
var autoStart = opts.autoStart !== false;
|
|
338
|
+
|
|
339
|
+
var tracked = new Map(); // incidentId -> { detectedAt, dueBy, regime, acked, fired }
|
|
340
|
+
var timer = null;
|
|
341
|
+
|
|
342
|
+
function _emit(action, outcome, metadata) {
|
|
343
|
+
if (!auditOn) return;
|
|
344
|
+
try {
|
|
345
|
+
audit().safeEmit({ action: "incident.report.clock." + action, outcome: outcome, metadata: metadata || {} });
|
|
346
|
+
} catch (_e) { /* drop-silent — by design */ }
|
|
347
|
+
}
|
|
348
|
+
function _notify(payload) {
|
|
349
|
+
if (!notify) return;
|
|
350
|
+
try {
|
|
351
|
+
var r = notify.send(payload);
|
|
352
|
+
if (r && typeof r.then === "function") r.then(null, function () {});
|
|
353
|
+
} catch (_e) { /* drop-silent — escalation is best-effort, never crashes a tick */ }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function track(record) {
|
|
357
|
+
if (!record || typeof record !== "object" || typeof record.id !== "string" || record.id.length === 0) {
|
|
358
|
+
throw new IncidentReportError("incident-report/bad-record",
|
|
359
|
+
"createDeadlineClock.track: record must be an incident.report record with a string id");
|
|
360
|
+
}
|
|
361
|
+
if (!record.dueBy || typeof record.dueBy !== "object" ||
|
|
362
|
+
typeof record.detectedAt !== "number") {
|
|
363
|
+
throw new IncidentReportError("incident-report/bad-record",
|
|
364
|
+
"createDeadlineClock.track: record must carry detectedAt + dueBy { initial, intermediate, final }");
|
|
365
|
+
}
|
|
366
|
+
tracked.set(record.id, {
|
|
367
|
+
detectedAt: record.detectedAt,
|
|
368
|
+
dueBy: record.dueBy,
|
|
369
|
+
regime: record.regime || null,
|
|
370
|
+
acked: {},
|
|
371
|
+
fired: {},
|
|
372
|
+
});
|
|
373
|
+
return record.id;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function untrack(id) { return tracked.delete(id); }
|
|
377
|
+
|
|
378
|
+
function acknowledgeSubmission(id, stage, info) {
|
|
379
|
+
if (!VALID_STAGES[stage]) {
|
|
380
|
+
throw new IncidentReportError("incident-report/bad-stage",
|
|
381
|
+
"createDeadlineClock.acknowledgeSubmission: stage must be one of " + Object.keys(VALID_STAGES).join(", "));
|
|
382
|
+
}
|
|
383
|
+
var t = tracked.get(id);
|
|
384
|
+
if (!t) {
|
|
385
|
+
throw new IncidentReportError("incident-report/unknown-incident",
|
|
386
|
+
"createDeadlineClock.acknowledgeSubmission: no tracked incident '" + id + "'");
|
|
387
|
+
}
|
|
388
|
+
t.acked[stage] = true;
|
|
389
|
+
_emit("submission_acknowledged", "success", { incidentId: id, regime: t.regime, stage: stage, info: info || null });
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Pure evaluation seam — operators (and tests) can pass an explicit
|
|
394
|
+
// nowMs. Each (incident, stage, state) fires AT MOST once; a stage
|
|
395
|
+
// that has been acknowledged is skipped entirely.
|
|
396
|
+
function tick(nowMsArg) {
|
|
397
|
+
var nowMs = typeof nowMsArg === "number" ? nowMsArg : now();
|
|
398
|
+
tracked.forEach(function (t, id) {
|
|
399
|
+
var stages = ["initial", "intermediate", "final"];
|
|
400
|
+
for (var si = 0; si < stages.length; si += 1) {
|
|
401
|
+
var stage = stages[si];
|
|
402
|
+
if (t.acked[stage]) continue;
|
|
403
|
+
var due = t.dueBy[stage];
|
|
404
|
+
if (typeof due !== "number") continue;
|
|
405
|
+
var span = due - t.detectedAt;
|
|
406
|
+
if (span <= 0) continue;
|
|
407
|
+
if (nowMs >= due) {
|
|
408
|
+
var pk = stage + ":passed";
|
|
409
|
+
if (!t.fired[pk]) {
|
|
410
|
+
t.fired[pk] = true;
|
|
411
|
+
_emit("deadline_passed", "failure", { incidentId: id, regime: t.regime, stage: stage, dueBy: due });
|
|
412
|
+
_notify({ kind: "deadline_passed", incidentId: id, regime: t.regime, stage: stage, dueBy: due });
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
var proportion = (nowMs - t.detectedAt) / span;
|
|
417
|
+
for (var thi = thresholds.length - 1; thi >= 0; thi -= 1) {
|
|
418
|
+
if (proportion >= thresholds[thi]) {
|
|
419
|
+
var ak = stage + ":approaching:" + thresholds[thi];
|
|
420
|
+
if (!t.fired[ak]) {
|
|
421
|
+
t.fired[ak] = true;
|
|
422
|
+
_emit("deadline_approaching", "warning",
|
|
423
|
+
{ incidentId: id, regime: t.regime, stage: stage, dueBy: due, threshold: thresholds[thi] });
|
|
424
|
+
_notify({ kind: "deadline_approaching", incidentId: id, regime: t.regime, stage: stage, dueBy: due, threshold: thresholds[thi] });
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function start() {
|
|
434
|
+
if (timer) return;
|
|
435
|
+
timer = setInterval(function () { tick(); }, intervalMs);
|
|
436
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
437
|
+
}
|
|
438
|
+
function stop() {
|
|
439
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
440
|
+
}
|
|
441
|
+
function status() {
|
|
442
|
+
return { tracked: tracked.size, running: timer !== null, intervalMs: intervalMs };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (autoStart) start();
|
|
446
|
+
return {
|
|
447
|
+
track: track,
|
|
448
|
+
untrack: untrack,
|
|
449
|
+
acknowledgeSubmission: acknowledgeSubmission,
|
|
450
|
+
tick: tick,
|
|
451
|
+
start: start,
|
|
452
|
+
stop: stop,
|
|
453
|
+
status: status,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
308
457
|
module.exports = {
|
|
309
458
|
create: create,
|
|
459
|
+
createDeadlineClock: createDeadlineClock,
|
|
310
460
|
IncidentReportError: IncidentReportError,
|
|
311
461
|
REGIME_DEADLINES: REGIME_DEADLINES,
|
|
312
462
|
DEFAULT_DEADLINES: DEFAULT_DEADLINES,
|
package/lib/json-schema.js
CHANGED
|
@@ -487,7 +487,7 @@ function _typeMatches(typeKw, instance, actual) {
|
|
|
487
487
|
function _checkNumber(schema, n, emit) {
|
|
488
488
|
if (typeof schema.multipleOf === "number") {
|
|
489
489
|
var q = n / schema.multipleOf;
|
|
490
|
-
if (!isFinite(q) || Math.abs(q - Math.round(q)) > 1e-9 * Math.max(1, Math.abs(q))) {
|
|
490
|
+
if (!isFinite(q) || Math.abs(q - Math.round(q)) > 1e-9 * Math.max(1, Math.abs(q))) {
|
|
491
491
|
// Exact check for integers; tolerance only bridges float error.
|
|
492
492
|
if (n % schema.multipleOf !== 0) emit("multipleOf", "value is not a multiple of " + schema.multipleOf);
|
|
493
493
|
}
|
package/lib/jtd.js
CHANGED
|
@@ -33,7 +33,7 @@ var rfc3339 = require("./rfc3339");
|
|
|
33
33
|
|
|
34
34
|
var JtdError = defineClass("JtdError", { alwaysPermanent: true });
|
|
35
35
|
|
|
36
|
-
var MAX_DEPTH = 10000;
|
|
36
|
+
var MAX_DEPTH = 10000;
|
|
37
37
|
|
|
38
38
|
var TYPES = {
|
|
39
39
|
boolean: 1, string: 1, timestamp: 1, float32: 1, float64: 1,
|
package/lib/mail-auth.js
CHANGED
|
@@ -1819,7 +1819,7 @@ function authResultsEmit(opts) {
|
|
|
1819
1819
|
// // }
|
|
1820
1820
|
|
|
1821
1821
|
var DMARC_RUA_MAX_REPORT_BYTES = C.BYTES.mib(8);
|
|
1822
|
-
var DMARC_RUA_MAX_RECORDS_PER_REPORT = 10000;
|
|
1822
|
+
var DMARC_RUA_MAX_RECORDS_PER_REPORT = 10000;
|
|
1823
1823
|
|
|
1824
1824
|
function _arrayOf(value) {
|
|
1825
1825
|
if (value === undefined || value === null) return [];
|
package/lib/mail-bimi.js
CHANGED
|
@@ -444,7 +444,7 @@ function validateTinyPsSvg(svgBytes) {
|
|
|
444
444
|
if (lname === "href" || lname === "xlink:href") {
|
|
445
445
|
if (aval.length > 0 && aval.charAt(0) !== "#") {
|
|
446
446
|
_vio("external-ref-forbidden",
|
|
447
|
-
"external reference in `" + aname + "='" + aval.slice(0, 60) /* allow:raw-time-literal — display truncation
|
|
447
|
+
"external reference in `" + aname + "='" + aval.slice(0, 60) /* allow:raw-time-literal — display truncation char-count 60; coincidental multiple-of-60, not a duration, C.TIME N/A */ + "...'` " +
|
|
448
448
|
"is forbidden in Tiny-PS (only `#fragment` permitted)");
|
|
449
449
|
}
|
|
450
450
|
}
|
package/lib/mail-crypto-smime.js
CHANGED
|
@@ -754,7 +754,7 @@ function checkCert(opts) {
|
|
|
754
754
|
if (pub && pub.asymmetricKeyType === "rsa") {
|
|
755
755
|
var jwk = pub.export({ format: "jwk" });
|
|
756
756
|
var nBytes = Buffer.from(jwk.n, "base64url");
|
|
757
|
-
var bits = nBytes.length * 8; // allow:raw-time-literal —
|
|
757
|
+
var bits = nBytes.length * 8; // allow:raw-time-literal — byte-length*8 bit count; coincidental multiple-of-60 product, not a duration, C.TIME N/A
|
|
758
758
|
if (bits < RSA_MIN_BITS) {
|
|
759
759
|
throw new MailCryptoError("mail-crypto/smime/rsa-too-small",
|
|
760
760
|
"cert public key is " + bits + " RSA bits; minimum is " + RSA_MIN_BITS +
|
|
@@ -763,7 +763,7 @@ function checkCert(opts) {
|
|
|
763
763
|
}
|
|
764
764
|
|
|
765
765
|
// Validity window — refuse certs outside their notBefore / notAfter
|
|
766
|
-
// window.
|
|
766
|
+
// window. checkCert's docstring promises this throws
|
|
767
767
|
// `mail-crypto/smime/expired-cert` but the impl was missing, letting
|
|
768
768
|
// expired or not-yet-valid signing certs pass boot-time preflight
|
|
769
769
|
// and fail interop later when peers verify signatures against the
|
package/lib/mail-deploy.js
CHANGED
|
@@ -720,7 +720,7 @@ function _validateTlsRptReport(raw, ctx) {
|
|
|
720
720
|
"parseTlsRptReport: report has " + policies.length +
|
|
721
721
|
" policies (cap " + TLSRPT_MAX_POLICIES + ")");
|
|
722
722
|
}
|
|
723
|
-
//
|
|
723
|
+
// Validate summary counts as finite non-negative
|
|
724
724
|
// integers before summing. `Number(...) || 0` would accept
|
|
725
725
|
// `Infinity` (from JSON literal `1e309` or string "Infinity"),
|
|
726
726
|
// negative values, and arbitrary strings (coerced to NaN→0). Each
|
|
@@ -882,7 +882,7 @@ function tlsRptReportSchema() {
|
|
|
882
882
|
* posture-aware payload (organization-name, report-id,
|
|
883
883
|
* policy-domain set, session totals).
|
|
884
884
|
*
|
|
885
|
-
* Authentication discipline
|
|
885
|
+
* Authentication discipline:
|
|
886
886
|
* - `trustedReporters` is a CONTENT-SIDE soft filter — it compares
|
|
887
887
|
* the reporter's self-declared `organization-name` field (the
|
|
888
888
|
* report body, operator-untrusted) against the operator's
|
|
@@ -962,7 +962,7 @@ function tlsRptIngestHttp(opts) {
|
|
|
962
962
|
res.end("RFC 8460 §6.4-6.5 media types required\n");
|
|
963
963
|
return;
|
|
964
964
|
}
|
|
965
|
-
//
|
|
965
|
+
// Real-authentication boundary BEFORE body
|
|
966
966
|
// collection. The operator-supplied `authenticate(req)` hook
|
|
967
967
|
// routes to mTLS peer-cert / IP-allowlist / signed-header /
|
|
968
968
|
// reverse-proxy header inspection. Sync-or-async; falsy → 401.
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* `opts.auth.mechanisms`. The framework hardcodes no defaults; an
|
|
44
44
|
* operator who omits `mechanisms` gets a listener that refuses
|
|
45
45
|
* every AUTHENTICATE attempt with "mechanism not advertised"
|
|
46
|
-
* (
|
|
46
|
+
* (otherwise advertising AUTH=PLAIN
|
|
47
47
|
* when authConfig is null sets clients up to attempt PLAIN against
|
|
48
48
|
* a listener that hasn't wired the verifier).
|
|
49
49
|
*
|
|
@@ -482,7 +482,7 @@ function create(opts) {
|
|
|
482
482
|
}
|
|
483
483
|
socket.write('"SIEVE" "' + sieveCaps.join(" ") + '"\r\n');
|
|
484
484
|
// Advertise SASL mechanisms — ONLY the mechs the operator wired
|
|
485
|
-
// in opts.auth.mechanisms (
|
|
485
|
+
// in opts.auth.mechanisms (do not hardcode the advertised mechanisms).
|
|
486
486
|
if (authConfig && Array.isArray(authConfig.mechanisms) && authConfig.mechanisms.length > 0) {
|
|
487
487
|
var mechs = authConfig.mechanisms.map(function (m) {
|
|
488
488
|
return String(m).toUpperCase();
|
package/lib/mail-server-mx.js
CHANGED
|
@@ -998,7 +998,7 @@ function create(opts) {
|
|
|
998
998
|
});
|
|
999
999
|
var deadline = Date.now() + timeoutMs;
|
|
1000
1000
|
while (connections.size > 0 && Date.now() < deadline) {
|
|
1001
|
-
await safeAsync.sleep(100);
|
|
1001
|
+
await safeAsync.sleep(100);
|
|
1002
1002
|
}
|
|
1003
1003
|
connections.forEach(function (sock) {
|
|
1004
1004
|
try { sock.destroy(); } catch (_e) { /* best-effort */ }
|
package/lib/mail-server-pop3.js
CHANGED
|
@@ -368,8 +368,8 @@ function create(opts) {
|
|
|
368
368
|
socket.write("UIDL\r\n");
|
|
369
369
|
socket.write("RESP-CODES\r\n");
|
|
370
370
|
if (!state.tls) socket.write("STLS\r\n");
|
|
371
|
-
// Advertise AUTH mechanisms ONLY when wired
|
|
372
|
-
//
|
|
371
|
+
// Advertise AUTH mechanisms ONLY when wired
|
|
372
|
+
// (do not hardcode SASL mechs in caps).
|
|
373
373
|
if (authConfig && Array.isArray(authConfig.mechanisms) && authConfig.mechanisms.length > 0) {
|
|
374
374
|
var mechs = authConfig.mechanisms.map(function (m) {
|
|
375
375
|
return String(m).toUpperCase();
|
|
@@ -96,7 +96,7 @@ var MailServerRateLimitError = defineClass("MailServerRateLimitError", { alwaysP
|
|
|
96
96
|
|
|
97
97
|
var DEFAULTS = Object.freeze({
|
|
98
98
|
maxConcurrentConnectionsPerIp: 10,
|
|
99
|
-
connectionsPerIpPerMinute: 60, // allow:raw-time-literal — connection
|
|
99
|
+
connectionsPerIpPerMinute: 60, // allow:raw-time-literal — per-minute connection rate 60; time-derived (per-minute) single source of truth, C.TIME N/A as a count
|
|
100
100
|
authFailuresPerIpPer15Min: 10,
|
|
101
101
|
minBytesPerSecond: 100, // slow-loris byte-rate floor
|
|
102
102
|
// RCPT-TO recipient-failure cap defends against the 550-vs-250
|
|
@@ -1347,7 +1347,7 @@ function create(opts) {
|
|
|
1347
1347
|
});
|
|
1348
1348
|
var deadline = Date.now() + timeoutMs;
|
|
1349
1349
|
while (connections.size > 0 && Date.now() < deadline) {
|
|
1350
|
-
await safeAsync.sleep(100);
|
|
1350
|
+
await safeAsync.sleep(100);
|
|
1351
1351
|
}
|
|
1352
1352
|
connections.forEach(function (sock) {
|
|
1353
1353
|
try { sock.destroy(); } catch (_e) { /* best-effort */ }
|
package/lib/mail-store.js
CHANGED
|
@@ -753,7 +753,7 @@ function _setFlags(args) {
|
|
|
753
753
|
// Per-message modseq bump — without this, queryByModseq filters
|
|
754
754
|
// `messages.modseq > sinceModseq` and misses the flag change. CONDSTORE
|
|
755
755
|
// (RFC 7162) / JMAP Email/changes both depend on the per-message
|
|
756
|
-
// modseq being current.
|
|
756
|
+
// modseq being current.
|
|
757
757
|
if (args.objectids.length > 0 && (setFlags.length > 0 || unsetFlags.length > 0)) {
|
|
758
758
|
// Bulk-update via IN-clause. SQLite caps IN-clause at 32766 (max
|
|
759
759
|
// bound parameters); chunk for very large operands.
|