@blamejs/core 0.8.43 → 0.8.50
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/retention.js
CHANGED
|
@@ -1,59 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.retention
|
|
4
|
+
* @nav Compliance
|
|
5
|
+
* @title Retention
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Row-level retention floors per regulatory regime. GDPR Art. 17,
|
|
9
|
+
* HIPAA 45 CFR §164.530(j), PCI-DSS Req. 3.1, SOX §802 and friends
|
|
10
|
+
* all share the shape: "data of class X stored beyond TTL Y must be
|
|
11
|
+
* either deleted or anonymized". `b.retention` ties the framework's
|
|
12
|
+
* building blocks (`b.cryptoField.eraseRow` for crypto-erasure of
|
|
13
|
+
* sealed columns, `b.scheduler` for cadence, `b.audit` for the
|
|
14
|
+
* chain, `b.legalHold` for per-subject holds) into one
|
|
15
|
+
* operator-facing primitive that emits delete / erase / soft-delete
|
|
16
|
+
* jobs at expiry.
|
|
11
17
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* action: "erase", // "erase" (b.cryptoField.eraseRow) | "delete"
|
|
23
|
-
* batchSize: 500, // rows-per-sweep iteration; default 500
|
|
24
|
-
* });
|
|
25
|
-
*
|
|
26
|
-
* // Operator wires the sweep cadence:
|
|
27
|
-
* scheduler.schedule({
|
|
28
|
-
* name: "retention.sweep",
|
|
29
|
-
* every: C.TIME.hours(1),
|
|
30
|
-
* run: function () { return rules.runAll(); },
|
|
31
|
-
* });
|
|
32
|
-
*
|
|
33
|
-
* // Or run on demand (operator CLI / one-shot):
|
|
34
|
-
* var summary = await rules.run("users.notes-ttl");
|
|
35
|
-
* // → { name, scanned, processed, action, durationMs, errors: [] }
|
|
18
|
+
* Action vocabulary per row: `"erase"` (sealed columns + derived
|
|
19
|
+
* hashes go to NULL, `__erasedAt` set, row remains for FK / audit
|
|
20
|
+
* reference — cleartext is unrecoverable even with a vault key);
|
|
21
|
+
* `"delete"` (full row DELETE — for tables with no FK / audit
|
|
22
|
+
* reference); `"soft-delete"` (writes a deletion timestamp into
|
|
23
|
+
* `softDeleteField` — typical "trash bin" pattern); `"warn"` (audit
|
|
24
|
+
* only, no row write — used as an early stage in multi-stage
|
|
25
|
+
* schedules); `function(row)` (escape hatch for joined / conditional
|
|
26
|
+
* retention). Cascades follow `rule.cascade[]` foreign-key edges so
|
|
27
|
+
* a parent erase fans out into child rows in the same sweep.
|
|
36
28
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - retention.sweep.failed — when the rule's SQL throws
|
|
29
|
+
* `b.compliance.set(posture)` cascades into `applyPosture` here, so
|
|
30
|
+
* the active posture's `audit_log` minimum-retention floor becomes
|
|
31
|
+
* the default `ttlMs` for any rule the operator declares without an
|
|
32
|
+
* explicit value. `complianceFloor(posture, candidateTtlMs)`
|
|
33
|
+
* surfaces those minimums for app-side conditional logic.
|
|
43
34
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* blocks the row from going.
|
|
35
|
+
* Audit events (namespace `retention`): `rule.declared`,
|
|
36
|
+
* `sweep.started`, `row.processed` (with `action`), `row.warned`,
|
|
37
|
+
* `row.legal_hold_skipped`, `sweep.completed`, `sweep.failed`,
|
|
38
|
+
* `sweep.skipped_concurrent`. Each sweep is single-flighted per
|
|
39
|
+
* rule name so a slow run cannot be re-entered by the next
|
|
40
|
+
* scheduler tick.
|
|
51
41
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* with each candidate row and the operator's function performs the
|
|
55
|
-
* write. This is the escape hatch; the table+ageField+ttlMs shape
|
|
56
|
-
* covers the common case.
|
|
42
|
+
* @card
|
|
43
|
+
* Row-level retention floors per regulatory regime.
|
|
57
44
|
*/
|
|
58
45
|
var C = require("./constants");
|
|
59
46
|
var lazyRequire = require("./lazy-require");
|
|
@@ -63,6 +50,7 @@ var { defineClass } = require("./framework-error");
|
|
|
63
50
|
|
|
64
51
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
65
52
|
var cryptoField = require("./crypto-field");
|
|
53
|
+
var legalHold = lazyRequire(function () { return require("./legal-hold"); });
|
|
66
54
|
|
|
67
55
|
var RetentionError = defineClass("RetentionError", { alwaysPermanent: true });
|
|
68
56
|
var _err = RetentionError.factory;
|
|
@@ -127,6 +115,13 @@ function _validateRule(rule) {
|
|
|
127
115
|
if (rule.legalHoldField !== undefined) {
|
|
128
116
|
_validateRuleIdentifier(rule.legalHoldField, "rule.legalHoldField");
|
|
129
117
|
}
|
|
118
|
+
if (rule.subjectField !== undefined &&
|
|
119
|
+
(typeof rule.subjectField !== "string" || rule.subjectField.length === 0)) {
|
|
120
|
+
throw _err("BAD_RULE", "rule.subjectField must be a non-empty string");
|
|
121
|
+
}
|
|
122
|
+
if (rule.subjectField !== undefined) {
|
|
123
|
+
_validateRuleIdentifier(rule.subjectField, "rule.subjectField");
|
|
124
|
+
}
|
|
130
125
|
if (rule.cascade !== undefined) {
|
|
131
126
|
if (!Array.isArray(rule.cascade) || rule.cascade.length === 0) {
|
|
132
127
|
throw _err("BAD_RULE", "rule.cascade must be a non-empty array of { table, foreignKey } entries");
|
|
@@ -163,6 +158,37 @@ function _validateRule(rule) {
|
|
|
163
158
|
}
|
|
164
159
|
}
|
|
165
160
|
|
|
161
|
+
/**
|
|
162
|
+
* @primitive b.retention.create
|
|
163
|
+
* @signature b.retention.create(opts)
|
|
164
|
+
* @since 0.6.14
|
|
165
|
+
* @status stable
|
|
166
|
+
* @compliance gdpr, hipaa, pci-dss, sox-404, soc2, dora, nis2
|
|
167
|
+
* @related b.retention.complianceFloor, b.retention.applyPosture, b.cryptoField.eraseRow, b.legalHold
|
|
168
|
+
*
|
|
169
|
+
* Build a retention controller bound to a database handle. Returns an
|
|
170
|
+
* object with `declare(rule)`, `run(name, runOpts?)`, `runAll(runOpts?)`,
|
|
171
|
+
* `preview(name)`, and `list()`. Audit emit is on by default; pass
|
|
172
|
+
* `audit: false` for a quiet controller in tests.
|
|
173
|
+
*
|
|
174
|
+
* @opts
|
|
175
|
+
* db: object, // b.db handle, must expose .prepare(sql)
|
|
176
|
+
* audit: boolean | object, // true | false | a b.audit instance
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* var rules = b.retention.create({ db: b.db, audit: true });
|
|
180
|
+
* rules.declare({
|
|
181
|
+
* name: "users.notes-ttl",
|
|
182
|
+
* table: "users",
|
|
183
|
+
* ageField: "createdAt",
|
|
184
|
+
* ttlMs: C.TIME.days(90),
|
|
185
|
+
* action: "erase",
|
|
186
|
+
* batchSize: 500,
|
|
187
|
+
* legalHoldField: "__legalHold",
|
|
188
|
+
* });
|
|
189
|
+
* var summary = await rules.run("users.notes-ttl");
|
|
190
|
+
* // → { name, scanned, processed, action: "erase", durationMs, errors: [] }
|
|
191
|
+
*/
|
|
166
192
|
function create(opts) {
|
|
167
193
|
opts = opts || {};
|
|
168
194
|
validateOpts(opts, ["db", "audit"], "retention");
|
|
@@ -384,9 +410,25 @@ function create(opts) {
|
|
|
384
410
|
if (rule.legalHoldField && row[rule.legalHoldField]) {
|
|
385
411
|
summary.legalHoldsHonored++;
|
|
386
412
|
_emit("retention.row.legal_hold_skipped",
|
|
387
|
-
{ name: name, table: rule.table, rowId: row._id
|
|
413
|
+
{ name: name, table: rule.table, rowId: row._id,
|
|
414
|
+
source: "per-row-field" }, "warning");
|
|
388
415
|
continue;
|
|
389
416
|
}
|
|
417
|
+
// Subject-level legal-hold registry consult. When the rule
|
|
418
|
+
// names a subjectField (typical for user-keyed retention
|
|
419
|
+
// tables), the central registry is authoritative. Honors
|
|
420
|
+
// the same skip semantics as the per-row field.
|
|
421
|
+
if (rule.subjectField && row[rule.subjectField]) {
|
|
422
|
+
var holdsRegistry = legalHold._getSingleton();
|
|
423
|
+
if (holdsRegistry && holdsRegistry.isHeld(row[rule.subjectField])) {
|
|
424
|
+
summary.legalHoldsHonored++;
|
|
425
|
+
_emit("retention.row.legal_hold_skipped",
|
|
426
|
+
{ name: name, table: rule.table, rowId: row._id,
|
|
427
|
+
source: "subject-registry",
|
|
428
|
+
subjectId: row[rule.subjectField] }, "warning");
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
390
432
|
var action = _stageForRow(rule, row, startedAt);
|
|
391
433
|
if (!action) { summary.skipped++; continue; }
|
|
392
434
|
var actionLabel = typeof action === "function" ? "custom" : action;
|
|
@@ -487,6 +529,29 @@ var COMPLIANCE_RETENTION_FLOOR_MS = Object.freeze({
|
|
|
487
529
|
// Operator passes a posture name + a candidate ttlMs; returns the
|
|
488
530
|
// effective ttl that meets-or-exceeds the floor. Throws if posture is
|
|
489
531
|
// unknown so typos surface at config time.
|
|
532
|
+
/**
|
|
533
|
+
* @primitive b.retention.complianceFloor
|
|
534
|
+
* @signature b.retention.complianceFloor(posture, candidateTtlMs)
|
|
535
|
+
* @since 0.7.24
|
|
536
|
+
* @status stable
|
|
537
|
+
* @compliance pci-dss, hipaa, sox-404, soc2, dora, nis2, cra
|
|
538
|
+
* @related b.retention.applyPosture, b.retention.create, b.compliance
|
|
539
|
+
*
|
|
540
|
+
* Take a regulatory posture name and a candidate TTL; return the
|
|
541
|
+
* effective TTL that meets-or-exceeds the regime's minimum-retention
|
|
542
|
+
* floor. Floors come from `COMPLIANCE_RETENTION_FLOOR_MS` (PCI-DSS
|
|
543
|
+
* §10.7.1: 12 months online; HIPAA 45 CFR §164.316(b)(2)(i): 6 years;
|
|
544
|
+
* SOX §802: 7 years; DORA Art. 17: 5 years; NIS2 Art. 23: 3 years;
|
|
545
|
+
* CRA Art. 14: 5 years; LGPD-BR / APPI-JP / PDPA-SG / UK-GDPR variants
|
|
546
|
+
* matched). Throws on an unknown posture so config-time typos surface.
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* var ttl = b.retention.complianceFloor("hipaa", b.C.TIME.days(180));
|
|
550
|
+
* // → 189216000000 (HIPAA's 6-year floor wins over the 180-day candidate)
|
|
551
|
+
*
|
|
552
|
+
* var sox = b.retention.complianceFloor("sox", 0);
|
|
553
|
+
* // → 220752000000 (Sarbanes-Oxley §802 — 7 years)
|
|
554
|
+
*/
|
|
490
555
|
function complianceFloor(posture, candidateTtlMs) {
|
|
491
556
|
if (typeof posture !== "string") {
|
|
492
557
|
throw new RetentionError("retention/bad-posture",
|
|
@@ -504,9 +569,72 @@ function complianceFloor(posture, candidateTtlMs) {
|
|
|
504
569
|
return candidateTtlMs > floor ? candidateTtlMs : floor;
|
|
505
570
|
}
|
|
506
571
|
|
|
572
|
+
// applyPosture — F-POSTURE-1 cascade hook. b.compliance.set(posture)
|
|
573
|
+
// calls this to merge posture defaults into retention's state. The
|
|
574
|
+
// retention module itself doesn't carry per-instance global defaults;
|
|
575
|
+
// the cascade's job here is to surface the posture's audit-log
|
|
576
|
+
// retention floor as the value rules.declare() uses when an operator
|
|
577
|
+
// hasn't passed an explicit ttlMs. Returns the recognized floor (ms)
|
|
578
|
+
// or null when the posture has no retention floor.
|
|
579
|
+
/**
|
|
580
|
+
* @primitive b.retention.applyPosture
|
|
581
|
+
* @signature b.retention.applyPosture(posture)
|
|
582
|
+
* @since 0.7.24
|
|
583
|
+
* @status stable
|
|
584
|
+
* @compliance pci-dss, hipaa, sox-404, soc2, dora, nis2, cra
|
|
585
|
+
* @related b.retention.complianceFloor, b.retention.activePosture, b.compliance
|
|
586
|
+
*
|
|
587
|
+
* Cascade hook called by `b.compliance.set(posture)`. Records the
|
|
588
|
+
* posture name and its `audit_log` retention floor as module state so
|
|
589
|
+
* subsequent `complianceFloor` callers without an explicit posture
|
|
590
|
+
* argument inherit the active value. Returns `null` for an empty
|
|
591
|
+
* input or a posture with no retention floor; otherwise returns
|
|
592
|
+
* `{ posture, floorMs }`.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* b.compliance.set("hipaa");
|
|
596
|
+
* b.retention.applyPosture("hipaa");
|
|
597
|
+
* // → { posture: "hipaa", floorMs: 189216000000 }
|
|
598
|
+
* b.retention.activePosture();
|
|
599
|
+
* // → "hipaa"
|
|
600
|
+
*/
|
|
601
|
+
function applyPosture(posture) {
|
|
602
|
+
if (typeof posture !== "string" || posture.length === 0) return null;
|
|
603
|
+
var floor = COMPLIANCE_RETENTION_FLOOR_MS[posture];
|
|
604
|
+
STATE.activePosture = posture;
|
|
605
|
+
STATE.activeFloorMs = (typeof floor === "number") ? floor : null;
|
|
606
|
+
return { posture: posture, floorMs: STATE.activeFloorMs };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Module-level state — read by complianceFloor() callers that omit the
|
|
610
|
+
// posture argument (lookup falls back to the active cascade-set value).
|
|
611
|
+
var STATE = { activePosture: null, activeFloorMs: null };
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* @primitive b.retention.activePosture
|
|
615
|
+
* @signature b.retention.activePosture()
|
|
616
|
+
* @since 0.7.24
|
|
617
|
+
* @status stable
|
|
618
|
+
* @related b.retention.applyPosture, b.compliance.current
|
|
619
|
+
*
|
|
620
|
+
* Read the posture name set by the most recent `applyPosture` call,
|
|
621
|
+
* or `null` if `b.compliance.set` has never run on this process.
|
|
622
|
+
* Used by audit-dashboard tooling to surface "this deployment is
|
|
623
|
+
* pinned to <posture>" without crossing into `b.compliance` directly.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* var p = b.retention.activePosture();
|
|
627
|
+
* if (p === null) console.log("no compliance posture pinned");
|
|
628
|
+
* else console.log("active posture:", p);
|
|
629
|
+
* // → "hipaa"
|
|
630
|
+
*/
|
|
631
|
+
function activePosture() { return STATE.activePosture; }
|
|
632
|
+
|
|
507
633
|
module.exports = {
|
|
508
634
|
create: create,
|
|
509
635
|
complianceFloor: complianceFloor,
|
|
636
|
+
applyPosture: applyPosture,
|
|
637
|
+
activePosture: activePosture,
|
|
510
638
|
COMPLIANCE_RETENTION_FLOOR_MS: COMPLIANCE_RETENTION_FLOOR_MS,
|
|
511
639
|
RetentionError: RetentionError,
|
|
512
640
|
};
|
package/lib/retry.js
CHANGED
|
@@ -1,35 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.retry
|
|
3
|
+
* @module b.retry
|
|
4
|
+
* @nav Production
|
|
5
|
+
* @title Retry
|
|
4
6
|
*
|
|
5
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Retry plus circuit-breaker primitives — exponential backoff with
|
|
9
|
+
* jitter, half-open probe, and built-in classification of OS network
|
|
10
|
+
* error codes plus retryable HTTP status codes.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* `b.retry.withRetry(fn, opts)` wraps a single call: exponential
|
|
13
|
+
* backoff (`baseDelayMs * 2^(attempt-1)`, capped at `maxDelayMs`)
|
|
14
|
+
* with cryptographic jitter so a thundering-herd of retrying clients
|
|
15
|
+
* does not realign on the same boundary. The default classifier
|
|
16
|
+
* targets HTTP 408 / 425 / 429 / 5xx and the Node net-layer codes
|
|
17
|
+
* (`ECONNRESET`, `ECONNREFUSED`, `ECONNABORTED`, `ETIMEDOUT`,
|
|
18
|
+
* `EPIPE`, `EAGAIN`, `ENOTFOUND`, `ENETUNREACH`); callers with
|
|
19
|
+
* non-network semantics override via `opts.isRetryable`. The retry
|
|
20
|
+
* loop honors `opts.signal` (AbortSignal) so a caller who aborts
|
|
21
|
+
* mid-retry is unblocked immediately rather than waiting out the
|
|
22
|
+
* backoff.
|
|
11
23
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
24
|
+
* `b.retry.CircuitBreaker` is the per-target sibling: N consecutive
|
|
25
|
+
* failures opens the circuit, opening fast-fails subsequent calls
|
|
26
|
+
* for `cooldownMs`, then a half-open state lets one probe call
|
|
27
|
+
* through. M consecutive probe successes close the circuit; one
|
|
28
|
+
* probe failure reopens it. Permanent errors (`err.permanent`) do
|
|
29
|
+
* not trip the breaker — those are caller bugs, not backend health
|
|
30
|
+
* issues. Both primitives are side-effect-free on success and
|
|
31
|
+
* compose with `b.safeAsync.withTimeout` and any caller-side
|
|
32
|
+
* instrumentation. HTTP-client auto-retry is intentionally not
|
|
33
|
+
* provided here so timeout, idempotency, and body-replay decisions
|
|
34
|
+
* stay explicit at the call site.
|
|
16
35
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* caller-side instrumentation.
|
|
20
|
-
*
|
|
21
|
-
* Validation policy:
|
|
22
|
-
*
|
|
23
|
-
* - withRetry opts at first call → throw at call site
|
|
24
|
-
* - CircuitBreaker constructor opts → throw at call site
|
|
25
|
-
* - backoffDelay(attempt) attempt argument → throw at call site
|
|
26
|
-
* - isRetryable(err) defensive read → tolerant (return defaults)
|
|
27
|
-
* - onRetry callback throw → drop silent (hot-path sink)
|
|
28
|
-
* - breaker internal _onSuccess/_onFailure → drop silent (hot-path sink)
|
|
29
|
-
*
|
|
30
|
-
* HTTP-client auto-retry is intentionally NOT provided here. Callers
|
|
31
|
-
* wrap their own outbound calls in `b.retry.withRetry(...)` to keep
|
|
32
|
-
* timeout/idempotency/body-replay decisions explicit.
|
|
36
|
+
* @card
|
|
37
|
+
* Retry plus circuit-breaker primitives — exponential backoff with jitter, half-open probe, and built-in classification of OS network error codes plus retryable HTTP status codes.
|
|
33
38
|
*/
|
|
34
39
|
|
|
35
40
|
var C = require("./constants");
|
|
@@ -187,6 +192,30 @@ function _validateBreakerOpts(name, opts) {
|
|
|
187
192
|
|
|
188
193
|
// ---- Public surface ----
|
|
189
194
|
|
|
195
|
+
/**
|
|
196
|
+
* @primitive b.retry.isRetryable
|
|
197
|
+
* @signature b.retry.isRetryable(err)
|
|
198
|
+
* @since 0.5.0
|
|
199
|
+
* @related b.retry.withRetry, b.retry.backoffDelay
|
|
200
|
+
*
|
|
201
|
+
* Default classifier — returns `true` when an error looks transient
|
|
202
|
+
* and worth retrying, `false` otherwise. Honors `err.permanent` and
|
|
203
|
+
* `err.isObjectStoreError && err.permanent`; recognizes retryable HTTP
|
|
204
|
+
* status codes (408 / 425 / 429 / 5xx) and Node net-layer codes
|
|
205
|
+
* (`ECONNRESET`, `ECONNREFUSED`, `ECONNABORTED`, `ETIMEDOUT`, `EPIPE`,
|
|
206
|
+
* `EAGAIN`, `ENOTFOUND`, `ENETUNREACH`). Defensive read — missing
|
|
207
|
+
* fields return `false` rather than throwing, so a malformed error
|
|
208
|
+
* never crashes the retry loop.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* var transient = new Error("timeout");
|
|
212
|
+
* transient.code = "ETIMEDOUT";
|
|
213
|
+
* b.retry.isRetryable(transient); // → true
|
|
214
|
+
*
|
|
215
|
+
* var fatal = new Error("bad request");
|
|
216
|
+
* fatal.statusCode = 400;
|
|
217
|
+
* b.retry.isRetryable(fatal); // → false
|
|
218
|
+
*/
|
|
190
219
|
// Tolerant read of err shape; missing fields → false.
|
|
191
220
|
function isRetryable(err) {
|
|
192
221
|
if (!err) return false;
|
|
@@ -202,13 +231,30 @@ function isRetryable(err) {
|
|
|
202
231
|
return false; // default: not retryable (avoid masking bugs)
|
|
203
232
|
}
|
|
204
233
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
234
|
+
/**
|
|
235
|
+
* @primitive b.retry.backoffDelay
|
|
236
|
+
* @signature b.retry.backoffDelay(attempt, opts)
|
|
237
|
+
* @since 0.5.0
|
|
238
|
+
* @related b.retry.withRetry, b.retry.isRetryable
|
|
239
|
+
*
|
|
240
|
+
* Compute the backoff in milliseconds for a given (1-based) `attempt`
|
|
241
|
+
* number. Exponential growth `baseDelayMs * 2^(attempt-1)` capped at
|
|
242
|
+
* `maxDelayMs`, then subtract a cryptographic jitter sample scaled by
|
|
243
|
+
* `jitterFactor` so retrying clients do not realign on the same
|
|
244
|
+
* boundary. Throws TypeError when `attempt` is not a positive integer.
|
|
245
|
+
* `opts` defaults to `b.retry.DEFAULT_RETRY` when absent.
|
|
246
|
+
*
|
|
247
|
+
* @opts
|
|
248
|
+
* baseDelayMs: number, // initial backoff (default 100)
|
|
249
|
+
* maxDelayMs: number, // cap between attempts (default 10s)
|
|
250
|
+
* jitterFactor: number, // 0..1; 0 = no jitter, 1 = full jitter (default 0.5)
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* var d1 = b.retry.backoffDelay(1, { baseDelayMs: 100, maxDelayMs: 1000, jitterFactor: 0 });
|
|
254
|
+
* d1; // → 100
|
|
255
|
+
* var d3 = b.retry.backoffDelay(3, { baseDelayMs: 100, maxDelayMs: 1000, jitterFactor: 0 });
|
|
256
|
+
* d3; // → 400
|
|
257
|
+
*/
|
|
212
258
|
function backoffDelay(attempt, opts) {
|
|
213
259
|
if (!_isPositiveInt(attempt)) {
|
|
214
260
|
throw new TypeError("retry.backoffDelay: attempt must be a positive integer, got " +
|
|
@@ -224,6 +270,43 @@ function backoffDelay(attempt, opts) {
|
|
|
224
270
|
return Math.floor(capped - jitter);
|
|
225
271
|
}
|
|
226
272
|
|
|
273
|
+
/**
|
|
274
|
+
* @primitive b.retry.withRetry
|
|
275
|
+
* @signature b.retry.withRetry(fn, opts)
|
|
276
|
+
* @since 0.5.0
|
|
277
|
+
* @related b.retry.isRetryable, b.retry.backoffDelay
|
|
278
|
+
*
|
|
279
|
+
* Run `fn(attempt)` with exponential backoff plus jitter. Retries up
|
|
280
|
+
* to `maxAttempts` while the classifier reports the failure transient;
|
|
281
|
+
* on a non-retryable error or after the final attempt the underlying
|
|
282
|
+
* error rethrows. Honors `opts.signal` so an AbortSignal cancels the
|
|
283
|
+
* backoff sleep. The `opts.onRetry` hook is invoked between attempts
|
|
284
|
+
* with `{ attempt, delay, error }`; throws inside the hook are
|
|
285
|
+
* captured and surfaced as `retry.onRetry.threw` observability events
|
|
286
|
+
* — the retry loop itself never crashes.
|
|
287
|
+
*
|
|
288
|
+
* @opts
|
|
289
|
+
* maxAttempts: number, // total tries incl. first (default 5)
|
|
290
|
+
* baseDelayMs: number, // initial backoff (default 100)
|
|
291
|
+
* maxDelayMs: number, // cap between attempts (default 10s)
|
|
292
|
+
* jitterFactor: number, // 0..1 (default 0.5)
|
|
293
|
+
* isRetryable: function, // override classifier (default b.retry.isRetryable)
|
|
294
|
+
* onRetry: function, // ({ attempt, delay, error }) -> void
|
|
295
|
+
* signal: object, // AbortSignal — cancels the backoff sleep
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* var attempts = 0;
|
|
299
|
+
* var result = await b.retry.withRetry(async function () {
|
|
300
|
+
* attempts += 1;
|
|
301
|
+
* if (attempts < 2) {
|
|
302
|
+
* var err = new Error("nope");
|
|
303
|
+
* err.code = "ECONNRESET";
|
|
304
|
+
* throw err;
|
|
305
|
+
* }
|
|
306
|
+
* return "ok";
|
|
307
|
+
* }, { maxAttempts: 3, baseDelayMs: 1, maxDelayMs: 1, jitterFactor: 0 });
|
|
308
|
+
* result; // → "ok"
|
|
309
|
+
*/
|
|
227
310
|
async function withRetry(fn, opts) {
|
|
228
311
|
if (typeof fn !== "function") {
|
|
229
312
|
throw new TypeError("retry.withRetry: fn must be a function, got " + typeof fn);
|