@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/gate-contract.js
CHANGED
|
@@ -1,61 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* composeHooks(hooks) — chain operator hooks
|
|
47
|
-
*
|
|
48
|
-
* Module-level constants:
|
|
49
|
-
*
|
|
50
|
-
* ACTIONS — allowed action enum
|
|
51
|
-
* MODES — allowed mode enum
|
|
52
|
-
* ISSUE_SEVERITIES — allowed severity enum
|
|
53
|
-
*
|
|
54
|
-
* The gate contract is the foundation of the guard-* family. Every
|
|
55
|
-
* content-safety primitive that ships in the family composes through it
|
|
56
|
-
* (b.guardCsv, future b.guardHtml / b.guardSvg / etc.). b.guardAll then
|
|
57
|
-
* aggregates the registered guards into a single security-on-by-default
|
|
58
|
-
* gate with operator opt-out via exceptFor.
|
|
3
|
+
* @module b.gateContract
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Gate Contract
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Shared substrate every `b.guard*` primitive composes against —
|
|
9
|
+
* `resolveProfileAndPosture`, `makeProfileBuilder`,
|
|
10
|
+
* `makeRulePackLoader`, `lookupCompliancePosture`, `buildGuardGate`,
|
|
11
|
+
* `aggregateIssues`, `extractBytesAsText`. The contract every guard
|
|
12
|
+
* implements; ensures the action vocabulary
|
|
13
|
+
* (`serve` / `sanitize` / `refuse` / `audit-only`) and the
|
|
14
|
+
* profile-and-posture resolution shape stay identical across the
|
|
15
|
+
* family.
|
|
16
|
+
*
|
|
17
|
+
* Every guard ships a `.gate(opts)` factory returning the shape
|
|
18
|
+
* defined here. Host primitives (`b.staticServe` / `b.fileUpload` /
|
|
19
|
+
* `b.mail` / `b.objectStore`) call `gate.check(ctx)` at their
|
|
20
|
+
* byte-boundary moment with a uniform context. The decision shape
|
|
21
|
+
* is `{ ok, action, sanitized?, issues, contentTypeOverride?,
|
|
22
|
+
* headers?, forensicHash, forensicSnapshot?, runtimeMs, cacheKey? }`.
|
|
23
|
+
*
|
|
24
|
+
* Operator extension surface inherited by every member:
|
|
25
|
+
*
|
|
26
|
+
* - Profile composition (extends + overrides + removes; cycle detection)
|
|
27
|
+
* - Hook system (beforeCheck / afterCheck / onIssue / onSanitize / onRefuse / onAudit)
|
|
28
|
+
* - Mode posture (enforce / warn-only / shadow / audit-only / log-only / canary)
|
|
29
|
+
* - Versioned policies (version + ruleHash) with policyDiff helper
|
|
30
|
+
* - Forensic snapshot store (operator-supplied evidence vault)
|
|
31
|
+
* - Decision cache (per-forensicHash memoization)
|
|
32
|
+
* - Runtime cap with timeout
|
|
33
|
+
* - Compliance-posture pre-sets (hipaa / pci-dss / gdpr / soc2)
|
|
34
|
+
*
|
|
35
|
+
* Module-level constants `ACTIONS` / `MODES` / `ISSUE_SEVERITIES`
|
|
36
|
+
* carry the frozen enums every guard validates against.
|
|
37
|
+
*
|
|
38
|
+
* Foundation for the guard-* family. Every content-safety primitive
|
|
39
|
+
* shipped under `b.guard*` composes through `buildGuardGate`,
|
|
40
|
+
* `makeProfileBuilder`, `makeRulePackLoader`, and
|
|
41
|
+
* `lookupCompliancePosture`; `b.guardAll` aggregates the registered
|
|
42
|
+
* guards into a single security-on-by-default gate.
|
|
43
|
+
*
|
|
44
|
+
* @card
|
|
45
|
+
* Shared substrate every `b.guard*` primitive composes against — `resolveProfileAndPosture`, `makeProfileBuilder`, `makeRulePackLoader`, `lookupCompliancePosture`, `buildGuardGate`, `aggregateIssues`, `extractBytesAsText`.
|
|
59
46
|
*/
|
|
60
47
|
|
|
61
48
|
var C = require("./constants");
|
|
@@ -77,10 +64,53 @@ var FINGERPRINT_HEX_LENGTH = C.BYTES.bytes(16);
|
|
|
77
64
|
// Default cachingGate TTL when operator doesn't supply one.
|
|
78
65
|
var DEFAULT_CACHE_TTL_MS = C.TIME.minutes(5);
|
|
79
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @primitive b.gateContract.GateContractError
|
|
69
|
+
* @signature b.gateContract.GateContractError
|
|
70
|
+
* @since 0.7.5
|
|
71
|
+
* @status stable
|
|
72
|
+
* @related b.gateContract.defineGate, b.gateContract.validateGateShape
|
|
73
|
+
*
|
|
74
|
+
* FrameworkError subclass thrown by gate-contract entry points on
|
|
75
|
+
* shape violations: `gate-contract/bad-shape` from
|
|
76
|
+
* `validateGateShape`, `gate-contract/bad-opt` from `defineGate` /
|
|
77
|
+
* `cachingGate` / `workerThreadGate`, `gate-contract/profile-cycle`
|
|
78
|
+
* and `gate-contract/unknown-profile` from `buildProfile`.
|
|
79
|
+
* `alwaysPermanent` — never retried by `b.retry`.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* try {
|
|
83
|
+
* b.gateContract.validateGateShape({}, "broken");
|
|
84
|
+
* } catch (e) {
|
|
85
|
+
* e instanceof b.gateContract.GateContractError; // → true
|
|
86
|
+
* e.code; // → "gate-contract/bad-shape"
|
|
87
|
+
* }
|
|
88
|
+
*/
|
|
80
89
|
var _err = GateContractError.factory;
|
|
81
90
|
|
|
82
91
|
// ---- Enumerations (module-level constants) ----
|
|
83
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @primitive b.gateContract.ACTIONS
|
|
95
|
+
* @signature b.gateContract.ACTIONS
|
|
96
|
+
* @since 0.7.5
|
|
97
|
+
* @status stable
|
|
98
|
+
* @related b.gateContract.MODES, b.gateContract.defineGate
|
|
99
|
+
*
|
|
100
|
+
* Frozen list of every action a gate decision is allowed to set.
|
|
101
|
+
* `serve` emits the bytes unchanged; `refuse` rejects with an
|
|
102
|
+
* operator-meaningful error; `sanitize` substitutes `decision.sanitized`
|
|
103
|
+
* for the original bytes; `strip` removes the offending content;
|
|
104
|
+
* `audit-only` serves but emits an audit entry; `warn` serves and
|
|
105
|
+
* emits a warning counter; `challenge-mfa` triggers step-up auth
|
|
106
|
+
* before serving; `deny-and-revoke` rejects and invalidates the
|
|
107
|
+
* actor's session.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* b.gateContract.ACTIONS.indexOf("serve"); // → 0
|
|
111
|
+
* b.gateContract.ACTIONS.indexOf("warp-speed"); // → -1
|
|
112
|
+
* Object.isFrozen(b.gateContract.ACTIONS); // → true
|
|
113
|
+
*/
|
|
84
114
|
var ACTIONS = Object.freeze([
|
|
85
115
|
"serve", // host emits the bytes as-is
|
|
86
116
|
"refuse", // host rejects with operator-meaningful error
|
|
@@ -92,6 +122,24 @@ var ACTIONS = Object.freeze([
|
|
|
92
122
|
"deny-and-revoke", // host rejects + invalidates the actor's session
|
|
93
123
|
]);
|
|
94
124
|
|
|
125
|
+
/**
|
|
126
|
+
* @primitive b.gateContract.MODES
|
|
127
|
+
* @signature b.gateContract.MODES
|
|
128
|
+
* @since 0.7.5
|
|
129
|
+
* @status stable
|
|
130
|
+
* @related b.gateContract.ACTIONS, b.gateContract.defineGate
|
|
131
|
+
*
|
|
132
|
+
* Frozen list of mode-posture values a gate can run in. `enforce`
|
|
133
|
+
* honors the decision; `warn-only` translates every `refuse` to
|
|
134
|
+
* `warn` for staged rollout; `shadow` runs alongside a primary and
|
|
135
|
+
* never refuses (observability-only); `audit-only` and `log-only`
|
|
136
|
+
* emit an audit entry but never block; `canary` enforces on a
|
|
137
|
+
* sampled subset and warns on the rest.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* b.gateContract.MODES.indexOf("enforce"); // → 0
|
|
141
|
+
* b.gateContract.MODES.indexOf("yolo"); // → -1
|
|
142
|
+
*/
|
|
95
143
|
var MODES = Object.freeze([
|
|
96
144
|
"enforce", // gate decision honored
|
|
97
145
|
"warn-only", // gate emits but never refuses (staged rollout)
|
|
@@ -101,6 +149,22 @@ var MODES = Object.freeze([
|
|
|
101
149
|
"canary", // enforce on N% of requests; warn on the rest
|
|
102
150
|
]);
|
|
103
151
|
|
|
152
|
+
/**
|
|
153
|
+
* @primitive b.gateContract.ISSUE_SEVERITIES
|
|
154
|
+
* @signature b.gateContract.ISSUE_SEVERITIES
|
|
155
|
+
* @since 0.7.5
|
|
156
|
+
* @status stable
|
|
157
|
+
* @related b.gateContract.aggregateIssues, b.gateContract.summarizeIssues
|
|
158
|
+
*
|
|
159
|
+
* Frozen list of severity levels a guard issue may carry. `info` and
|
|
160
|
+
* `warn` are observability-only — `aggregateIssues` keeps `ok: true`
|
|
161
|
+
* with them present. `high` and `critical` flip the result to
|
|
162
|
+
* `ok: false`, refusing the input.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* b.gateContract.ISSUE_SEVERITIES; // → ["info","warn","high","critical"]
|
|
166
|
+
* b.gateContract.ISSUE_SEVERITIES.indexOf("critical"); // → 3
|
|
167
|
+
*/
|
|
104
168
|
var ISSUE_SEVERITIES = Object.freeze([
|
|
105
169
|
"info",
|
|
106
170
|
"warn",
|
|
@@ -108,13 +172,33 @@ var ISSUE_SEVERITIES = Object.freeze([
|
|
|
108
172
|
"critical",
|
|
109
173
|
]);
|
|
110
174
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
175
|
+
/**
|
|
176
|
+
* @primitive b.gateContract.validateGateShape
|
|
177
|
+
* @signature b.gateContract.validateGateShape(gate, label, errorClass)
|
|
178
|
+
* @since 0.7.5
|
|
179
|
+
* @status stable
|
|
180
|
+
* @related b.gateContract.defineGate, b.gateContract.runGate
|
|
181
|
+
*
|
|
182
|
+
* Throws when `gate` does not satisfy the contract — `gate.check`
|
|
183
|
+
* must be a function, `gate.mode` (when present) must be one of the
|
|
184
|
+
* `MODES` enum values, and `gate.metrics` / `gate.close` (when
|
|
185
|
+
* present) must be functions. Operator-supplied gates (and
|
|
186
|
+
* framework-supplied gates with operator-toggled hooks) all flow
|
|
187
|
+
* through this check at host-primitive wire-up time. Shape errors at
|
|
188
|
+
* boot are cheaper than at request time. Returns `gate` unchanged on
|
|
189
|
+
* success.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* var gate = b.guardCsv.gate({ profile: "strict" });
|
|
193
|
+
* b.gateContract.validateGateShape(gate, "uploads.csv");
|
|
194
|
+
* // → returns the gate unchanged when shape is valid
|
|
195
|
+
*
|
|
196
|
+
* try {
|
|
197
|
+
* b.gateContract.validateGateShape({}, "broken");
|
|
198
|
+
* } catch (e) {
|
|
199
|
+
* e.code; // → "gate-contract/bad-shape"
|
|
200
|
+
* }
|
|
201
|
+
*/
|
|
118
202
|
function validateGateShape(gate, label, errorClass) {
|
|
119
203
|
errorClass = errorClass || GateContractError;
|
|
120
204
|
label = label || "gate";
|
|
@@ -142,23 +226,60 @@ function validateGateShape(gate, label, errorClass) {
|
|
|
142
226
|
return gate;
|
|
143
227
|
}
|
|
144
228
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
229
|
+
/**
|
|
230
|
+
* @primitive b.gateContract.defineGate
|
|
231
|
+
* @signature b.gateContract.defineGate(opts)
|
|
232
|
+
* @since 0.7.5
|
|
233
|
+
* @status stable
|
|
234
|
+
* @related b.gateContract.buildGuardGate, b.gateContract.validateGateShape
|
|
235
|
+
*
|
|
236
|
+
* Build a gate that satisfies the contract. Wraps the
|
|
237
|
+
* operator-supplied `check(ctx)` with the cross-cutting concerns
|
|
238
|
+
* (hooks, observability, forensic snapshot, runtime cap, decision
|
|
239
|
+
* cache, mode-posture translation) so guards only write the per-guard
|
|
240
|
+
* inspection logic. Returns a gate exposing
|
|
241
|
+
* `{ check, mode, audit, observability, metrics, reset, close, name,
|
|
242
|
+
* version, ruleHash, dryRun, policyDiff }`. Most guards forward through
|
|
243
|
+
* `b.gateContract.buildGuardGate` instead — `defineGate` is the lower-level
|
|
244
|
+
* factory used by host-side composers (`composeGates` / `byRoute` / etc.).
|
|
245
|
+
*
|
|
246
|
+
* @opts
|
|
247
|
+
* name: string, // identifier surfaced in audit / counters
|
|
248
|
+
* version: string, // semver default "1.0.0"
|
|
249
|
+
* mode: string, // one of MODES; default "enforce"
|
|
250
|
+
* check: function, // async (ctx) → decision
|
|
251
|
+
* beforeCheck: function|null, // (ctx) → { skip?, transform? }
|
|
252
|
+
* afterCheck: function|null, // (ctx, decision) → decision
|
|
253
|
+
* onIssue: function|null, // (issue, ctx) → issue|{suppress|promote}
|
|
254
|
+
* onSanitize: function|null, // (bytes, sanitized, ctx) → sanitized
|
|
255
|
+
* onRefuse: function|null, // (ctx, decision) → void
|
|
256
|
+
* onAudit: function|null, // (entry) → entry|false (false suppresses)
|
|
257
|
+
* audit: object|null, // b.audit handle
|
|
258
|
+
* observability: object|null, // b.observability handle
|
|
259
|
+
* forensicEvidenceStore: object|null, // { write({ ... }) }
|
|
260
|
+
* forensicSnippetBytes: number, // 0 = disabled
|
|
261
|
+
* cache: object|null, // b.cache shape
|
|
262
|
+
* cacheTtlMs: number,
|
|
263
|
+
* maxRuntimeMs: number, // 0 = uncapped
|
|
264
|
+
* ruleHash: string, // override fingerprint
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* var gate = b.gateContract.defineGate({
|
|
268
|
+
* name: "tenant:csv:strict",
|
|
269
|
+
* mode: "enforce",
|
|
270
|
+
* maxRuntimeMs: 250,
|
|
271
|
+
* check: async function (ctx) {
|
|
272
|
+
* var text = b.gateContract.extractBytesAsText(ctx);
|
|
273
|
+
* if (text.indexOf("=cmd|") === 0) {
|
|
274
|
+
* return { ok: false, action: "refuse",
|
|
275
|
+
* issues: [{ kind: "csv.formula-injection", severity: "high" }] };
|
|
276
|
+
* }
|
|
277
|
+
* return { ok: true, action: "serve" };
|
|
278
|
+
* },
|
|
279
|
+
* });
|
|
280
|
+
* var d = await gate.check({ bytes: Buffer.from("name,age\nada,36") });
|
|
281
|
+
* d.action; // → "serve"
|
|
282
|
+
*/
|
|
162
283
|
function defineGate(opts) {
|
|
163
284
|
validateOpts.requireObject(opts, "gateContract.defineGate", GateContractError);
|
|
164
285
|
validateOpts.requireNonEmptyString(opts.name, "gateContract.defineGate: name", GateContractError, "gate-contract/bad-opt");
|
|
@@ -439,13 +560,66 @@ function _hashFingerprint(obj) {
|
|
|
439
560
|
|
|
440
561
|
// ---- Host-side helpers ----
|
|
441
562
|
|
|
563
|
+
/**
|
|
564
|
+
* @primitive b.gateContract.runGate
|
|
565
|
+
* @signature b.gateContract.runGate(gate, ctx, opts?)
|
|
566
|
+
* @since 0.7.5
|
|
567
|
+
* @status stable
|
|
568
|
+
* @related b.gateContract.defineGate, b.gateContract.composeGates
|
|
569
|
+
*
|
|
570
|
+
* Execute a gate's `check(ctx)` and return its decision. When `gate`
|
|
571
|
+
* is null or has no `check` function, returns the canonical
|
|
572
|
+
* `{ ok: true, action: "serve" }` shape — host primitives invoke
|
|
573
|
+
* `runGate` with an operator-configurable gate that may legitimately
|
|
574
|
+
* be unset. The `opts` argument is reserved for future host-side
|
|
575
|
+
* cross-cutting concerns and is currently unused.
|
|
576
|
+
*
|
|
577
|
+
* @opts
|
|
578
|
+
* // reserved for future host-side cross-cutting concerns; pass `{}` today
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* var gate = b.guardCsv.gate({ profile: "strict" });
|
|
582
|
+
* var decision = await b.gateContract.runGate(gate, {
|
|
583
|
+
* bytes: Buffer.from("name,age\nada,36"),
|
|
584
|
+
* route: "/api/imports",
|
|
585
|
+
* filename: "people.csv",
|
|
586
|
+
* });
|
|
587
|
+
* decision.action; // → "serve"
|
|
588
|
+
*
|
|
589
|
+
* // Unset gate is a no-op serve.
|
|
590
|
+
* (await b.gateContract.runGate(null, {})).action; // → "serve"
|
|
591
|
+
*/
|
|
442
592
|
async function runGate(gate, ctx, opts) {
|
|
443
593
|
opts = opts || {};
|
|
444
594
|
if (!gate || typeof gate.check !== "function") return _build({ ok: true, action: "serve" });
|
|
445
595
|
return await gate.check(ctx);
|
|
446
596
|
}
|
|
447
597
|
|
|
448
|
-
|
|
598
|
+
/**
|
|
599
|
+
* @primitive b.gateContract.composeGates
|
|
600
|
+
* @signature b.gateContract.composeGates(gates, opts?)
|
|
601
|
+
* @since 0.7.5
|
|
602
|
+
* @status stable
|
|
603
|
+
* @related b.gateContract.multiplexGates, b.gateContract.contentTypeMux
|
|
604
|
+
*
|
|
605
|
+
* Chain a list of gates left-to-right. First refusal wins. When a
|
|
606
|
+
* gate returns `action: "sanitize"` and `firstRefusalWins` is true
|
|
607
|
+
* (default), the sanitized bytes feed into the next gate's context —
|
|
608
|
+
* letting an HTML sanitizer hand its scrubbed output to a downstream
|
|
609
|
+
* link-shape guard. Returns a wrapping gate that satisfies the
|
|
610
|
+
* contract (so the composition is itself composable).
|
|
611
|
+
*
|
|
612
|
+
* @opts
|
|
613
|
+
* name: string, // wrapper gate name (default "composed")
|
|
614
|
+
* firstRefusalWins: boolean, // default true
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* var bidi = b.guardCsv.gate({ profile: "strict" });
|
|
618
|
+
* var pii = b.guardCsv.gate({ compliancePosture: "hipaa" });
|
|
619
|
+
* var chain = b.gateContract.composeGates([bidi, pii], { name: "csv:chain" });
|
|
620
|
+
* var d = await chain.check({ bytes: Buffer.from("name,ssn\nada,123-45-6789") });
|
|
621
|
+
* d.action; // → "refuse"
|
|
622
|
+
*/
|
|
449
623
|
function composeGates(gates, opts) {
|
|
450
624
|
opts = opts || {};
|
|
451
625
|
var firstRefusalWins = opts.firstRefusalWins !== false;
|
|
@@ -464,7 +638,32 @@ function composeGates(gates, opts) {
|
|
|
464
638
|
});
|
|
465
639
|
}
|
|
466
640
|
|
|
467
|
-
|
|
641
|
+
/**
|
|
642
|
+
* @primitive b.gateContract.multiplexGates
|
|
643
|
+
* @signature b.gateContract.multiplexGates(gateMap, opts?)
|
|
644
|
+
* @since 0.7.5
|
|
645
|
+
* @status stable
|
|
646
|
+
* @related b.gateContract.contentTypeMux, b.gateContract.composeGates
|
|
647
|
+
*
|
|
648
|
+
* File-extension-keyed gate dispatch. Looks at `ctx.filename`,
|
|
649
|
+
* extracts the lowercased final extension (`.csv` / `.html` / etc.),
|
|
650
|
+
* and dispatches to the matching gate. The `"default"` key serves as
|
|
651
|
+
* the fallback; missing entries (no key match, no fallback) return
|
|
652
|
+
* the canonical serve decision so host primitives can wire a single
|
|
653
|
+
* mux gate without per-extension special-casing.
|
|
654
|
+
*
|
|
655
|
+
* @opts
|
|
656
|
+
* name: string, // wrapper gate name (default "multiplex")
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* var mux = b.gateContract.multiplexGates({
|
|
660
|
+
* ".csv": b.guardCsv.gate({ profile: "strict" }),
|
|
661
|
+
* ".html": b.guardHtml.gate({ profile: "strict" }),
|
|
662
|
+
* "default": b.guardCsv.gate({ profile: "permissive" }),
|
|
663
|
+
* });
|
|
664
|
+
* var d = await mux.check({ bytes: Buffer.from("a,b\n1,2"), filename: "x.csv" });
|
|
665
|
+
* d.action; // → "serve"
|
|
666
|
+
*/
|
|
468
667
|
function multiplexGates(gateMap, opts) {
|
|
469
668
|
opts = opts || {};
|
|
470
669
|
var lookup = Object.create(null);
|
|
@@ -484,8 +683,34 @@ function multiplexGates(gateMap, opts) {
|
|
|
484
683
|
});
|
|
485
684
|
}
|
|
486
685
|
|
|
487
|
-
|
|
488
|
-
|
|
686
|
+
/**
|
|
687
|
+
* @primitive b.gateContract.contentTypeMux
|
|
688
|
+
* @signature b.gateContract.contentTypeMux(gateMap, opts?)
|
|
689
|
+
* @since 0.7.5
|
|
690
|
+
* @status stable
|
|
691
|
+
* @related b.gateContract.multiplexGates, b.gateContract.composeGates
|
|
692
|
+
*
|
|
693
|
+
* Content-Type-keyed gate dispatch. Reads `ctx.contentType`, strips
|
|
694
|
+
* parameters (`; charset=utf-8`), lowercases, and routes to the
|
|
695
|
+
* matching gate. The `"default"` key is the fallback; unknown types
|
|
696
|
+
* (no key match, no fallback) serve uninspected. Useful when one
|
|
697
|
+
* route accepts multiple media types and each needs its own guard.
|
|
698
|
+
*
|
|
699
|
+
* @opts
|
|
700
|
+
* name: string, // wrapper gate name (default "contentTypeMux")
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* var mux = b.gateContract.contentTypeMux({
|
|
704
|
+
* "text/csv": b.guardCsv.gate({ profile: "strict" }),
|
|
705
|
+
* "text/html": b.guardHtml.gate({ profile: "strict" }),
|
|
706
|
+
* "default": b.guardCsv.gate({ profile: "permissive" }),
|
|
707
|
+
* });
|
|
708
|
+
* var d = await mux.check({
|
|
709
|
+
* bytes: Buffer.from("name,age\nada,36"),
|
|
710
|
+
* contentType: "text/csv; charset=utf-8",
|
|
711
|
+
* });
|
|
712
|
+
* d.action; // → "serve"
|
|
713
|
+
*/
|
|
489
714
|
function contentTypeMux(gateMap, opts) {
|
|
490
715
|
opts = opts || {};
|
|
491
716
|
var lookup = Object.create(null);
|
|
@@ -503,7 +728,35 @@ function contentTypeMux(gateMap, opts) {
|
|
|
503
728
|
});
|
|
504
729
|
}
|
|
505
730
|
|
|
506
|
-
|
|
731
|
+
/**
|
|
732
|
+
* @primitive b.gateContract.byActorTier
|
|
733
|
+
* @signature b.gateContract.byActorTier(gateMap, opts?)
|
|
734
|
+
* @since 0.7.5
|
|
735
|
+
* @status stable
|
|
736
|
+
* @related b.gateContract.byRoute, b.gateContract.byDirection
|
|
737
|
+
*
|
|
738
|
+
* Actor-tier-keyed gate dispatch. Reads `ctx.actor.tier` (e.g.
|
|
739
|
+
* `"free"` / `"paid"` / `"admin"`) and routes to the matching gate.
|
|
740
|
+
* Falls back to `gateMap["default"]` when the tier is missing or
|
|
741
|
+
* unmapped; missing fallback serves uninspected. Lets free-tier
|
|
742
|
+
* tenants run a stricter posture than paid customers without
|
|
743
|
+
* branching at every call site.
|
|
744
|
+
*
|
|
745
|
+
* @opts
|
|
746
|
+
* name: string, // wrapper gate name (default "byActorTier")
|
|
747
|
+
*
|
|
748
|
+
* @example
|
|
749
|
+
* var byTier = b.gateContract.byActorTier({
|
|
750
|
+
* free: b.guardCsv.gate({ profile: "strict" }),
|
|
751
|
+
* paid: b.guardCsv.gate({ profile: "balanced" }),
|
|
752
|
+
* default: b.guardCsv.gate({ profile: "strict" }),
|
|
753
|
+
* });
|
|
754
|
+
* var d = await byTier.check({
|
|
755
|
+
* bytes: Buffer.from("name,age\nada,36"),
|
|
756
|
+
* actor: { tier: "paid" },
|
|
757
|
+
* });
|
|
758
|
+
* d.action; // → "serve"
|
|
759
|
+
*/
|
|
507
760
|
function byActorTier(gateMap, opts) {
|
|
508
761
|
opts = opts || {};
|
|
509
762
|
return defineGate({
|
|
@@ -517,8 +770,35 @@ function byActorTier(gateMap, opts) {
|
|
|
517
770
|
});
|
|
518
771
|
}
|
|
519
772
|
|
|
520
|
-
|
|
521
|
-
|
|
773
|
+
/**
|
|
774
|
+
* @primitive b.gateContract.byRoute
|
|
775
|
+
* @signature b.gateContract.byRoute(gateMap, opts?)
|
|
776
|
+
* @since 0.7.5
|
|
777
|
+
* @status stable
|
|
778
|
+
* @related b.gateContract.byActorTier, b.gateContract.byDirection
|
|
779
|
+
*
|
|
780
|
+
* Route-pattern-keyed gate dispatch. Patterns are simple glob-prefix
|
|
781
|
+
* matches — `/admin/*` matches every path beginning with `/admin/`.
|
|
782
|
+
* Tries each entry in declaration order, falling back to `"*"` /
|
|
783
|
+
* `"default"`; missing fallback serves uninspected. Lets `/admin/*`
|
|
784
|
+
* routes apply a stricter guard than the public surface without
|
|
785
|
+
* threading per-route opts through every call site.
|
|
786
|
+
*
|
|
787
|
+
* @opts
|
|
788
|
+
* name: string, // wrapper gate name (default "byRoute")
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* var byPath = b.gateContract.byRoute({
|
|
792
|
+
* "/admin/*": b.guardCsv.gate({ profile: "strict" }),
|
|
793
|
+
* "/api/*": b.guardCsv.gate({ profile: "balanced" }),
|
|
794
|
+
* "*": b.guardCsv.gate({ profile: "permissive" }),
|
|
795
|
+
* });
|
|
796
|
+
* var d = await byPath.check({
|
|
797
|
+
* bytes: Buffer.from("name,age\nada,36"),
|
|
798
|
+
* route: "/admin/imports",
|
|
799
|
+
* });
|
|
800
|
+
* d.action; // → "serve"
|
|
801
|
+
*/
|
|
522
802
|
function byRoute(gateMap, opts) {
|
|
523
803
|
opts = opts || {};
|
|
524
804
|
var entries = Object.keys(gateMap).map(function (pattern) {
|
|
@@ -540,7 +820,33 @@ function byRoute(gateMap, opts) {
|
|
|
540
820
|
});
|
|
541
821
|
}
|
|
542
822
|
|
|
543
|
-
|
|
823
|
+
/**
|
|
824
|
+
* @primitive b.gateContract.byDirection
|
|
825
|
+
* @signature b.gateContract.byDirection(gateMap, opts?)
|
|
826
|
+
* @since 0.7.5
|
|
827
|
+
* @status stable
|
|
828
|
+
* @related b.gateContract.byRoute, b.gateContract.byActorTier
|
|
829
|
+
*
|
|
830
|
+
* Direction-aware gate dispatch. Reads `ctx.direction` (`"inbound"`
|
|
831
|
+
* or `"outbound"`; default `"outbound"`) and routes to the matching
|
|
832
|
+
* gate. Lets a single guard wiring run a stricter posture on bytes
|
|
833
|
+
* arriving from an external source than on bytes the framework is
|
|
834
|
+
* about to emit. Missing direction maps serve uninspected.
|
|
835
|
+
*
|
|
836
|
+
* @opts
|
|
837
|
+
* name: string, // wrapper gate name (default "byDirection")
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* var byDir = b.gateContract.byDirection({
|
|
841
|
+
* inbound: b.guardCsv.gate({ profile: "strict" }),
|
|
842
|
+
* outbound: b.guardCsv.gate({ profile: "balanced" }),
|
|
843
|
+
* });
|
|
844
|
+
* var d = await byDir.check({
|
|
845
|
+
* bytes: Buffer.from("name,age\nada,36"),
|
|
846
|
+
* direction: "inbound",
|
|
847
|
+
* });
|
|
848
|
+
* d.action; // → "serve"
|
|
849
|
+
*/
|
|
544
850
|
function byDirection(gateMap, opts) {
|
|
545
851
|
opts = opts || {};
|
|
546
852
|
return defineGate({
|
|
@@ -554,8 +860,30 @@ function byDirection(gateMap, opts) {
|
|
|
554
860
|
});
|
|
555
861
|
}
|
|
556
862
|
|
|
557
|
-
|
|
558
|
-
|
|
863
|
+
/**
|
|
864
|
+
* @primitive b.gateContract.shadowMode
|
|
865
|
+
* @signature b.gateContract.shadowMode(primary, candidate, opts?)
|
|
866
|
+
* @since 0.7.5
|
|
867
|
+
* @status stable
|
|
868
|
+
* @related b.gateContract.canaryGate, b.gateContract.composeGates
|
|
869
|
+
*
|
|
870
|
+
* Run a candidate gate alongside the primary; emit a divergence
|
|
871
|
+
* counter when their actions disagree. The primary's decision is the
|
|
872
|
+
* one honored — candidate runs are observability-only and don't
|
|
873
|
+
* block the request. Useful for staged rollout of a new profile
|
|
874
|
+
* (run it shadowed for a week, watch the divergence rate, then
|
|
875
|
+
* promote it to primary).
|
|
876
|
+
*
|
|
877
|
+
* @opts
|
|
878
|
+
* name: string, // wrapper gate name (default "shadow")
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* var primary = b.guardCsv.gate({ profile: "strict" });
|
|
882
|
+
* var candidate = b.guardCsv.gate({ profile: "balanced" });
|
|
883
|
+
* var staged = b.gateContract.shadowMode(primary, candidate, { name: "csv:staged" });
|
|
884
|
+
* var d = await staged.check({ bytes: Buffer.from("name,age\nada,36") });
|
|
885
|
+
* d.action; // → "serve" (primary's decision)
|
|
886
|
+
*/
|
|
559
887
|
function shadowMode(primary, candidate, opts) {
|
|
560
888
|
opts = opts || {};
|
|
561
889
|
return defineGate({
|
|
@@ -575,7 +903,28 @@ function shadowMode(primary, candidate, opts) {
|
|
|
575
903
|
});
|
|
576
904
|
}
|
|
577
905
|
|
|
578
|
-
|
|
906
|
+
/**
|
|
907
|
+
* @primitive b.gateContract.canaryGate
|
|
908
|
+
* @signature b.gateContract.canaryGate(gate, opts?)
|
|
909
|
+
* @since 0.7.5
|
|
910
|
+
* @status stable
|
|
911
|
+
* @related b.gateContract.shadowMode, b.gateContract.cachingGate
|
|
912
|
+
*
|
|
913
|
+
* Enforce the wrapped gate's refuse decisions on `rate` of requests;
|
|
914
|
+
* downgrade the rest to `warn`. Default rate is `0.1` (10% enforced,
|
|
915
|
+
* 90% warned). Sampling uses a non-cryptographic random source — fine
|
|
916
|
+
* for rollout shaping, never for security-critical sampling.
|
|
917
|
+
*
|
|
918
|
+
* @opts
|
|
919
|
+
* rate: number, // 0..1, default 0.1
|
|
920
|
+
* name: string, // wrapper gate name (default "canary")
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* var strict = b.guardCsv.gate({ profile: "strict" });
|
|
924
|
+
* var canary = b.gateContract.canaryGate(strict, { rate: 0.25 });
|
|
925
|
+
* var d = await canary.check({ bytes: Buffer.from("name,age\nada,36") });
|
|
926
|
+
* d.ok; // → true
|
|
927
|
+
*/
|
|
579
928
|
function canaryGate(gate, opts) {
|
|
580
929
|
opts = opts || {};
|
|
581
930
|
var rate = typeof opts.rate === "number" ? opts.rate : 0.1;
|
|
@@ -591,9 +940,34 @@ function canaryGate(gate, opts) {
|
|
|
591
940
|
});
|
|
592
941
|
}
|
|
593
942
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
943
|
+
/**
|
|
944
|
+
* @primitive b.gateContract.cachingGate
|
|
945
|
+
* @signature b.gateContract.cachingGate(gate, opts)
|
|
946
|
+
* @since 0.7.5
|
|
947
|
+
* @status stable
|
|
948
|
+
* @related b.gateContract.defineGate, b.gateContract.canaryGate
|
|
949
|
+
*
|
|
950
|
+
* Wrap a gate with an explicit shared cache backend. The per-gate
|
|
951
|
+
* built-in cache (configured via `defineGate({ cache, cacheTtlMs })`)
|
|
952
|
+
* is per-gate-instance; this wrapper is the operator-side variant
|
|
953
|
+
* for sharing one cache across multiple gates. `opts.backend` must
|
|
954
|
+
* expose the `b.cache` shape (`{ get(key), set(key, value, opts) }`).
|
|
955
|
+
*
|
|
956
|
+
* @opts
|
|
957
|
+
* backend: object, // b.cache-shaped { get, set } (required)
|
|
958
|
+
* ttlMs: number, // cache TTL
|
|
959
|
+
* name: string, // wrapper gate name (default "<gate>:cached")
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* var cache = b.cache.create({ backend: "memory", maxEntries: 10000 });
|
|
963
|
+
* var strict = b.guardCsv.gate({ profile: "strict" });
|
|
964
|
+
* var cached = b.gateContract.cachingGate(strict, {
|
|
965
|
+
* backend: cache,
|
|
966
|
+
* ttlMs: 60000,
|
|
967
|
+
* });
|
|
968
|
+
* var d = await cached.check({ bytes: Buffer.from("name,age\nada,36") });
|
|
969
|
+
* d.action; // → "serve"
|
|
970
|
+
*/
|
|
597
971
|
function cachingGate(gate, opts) {
|
|
598
972
|
opts = opts || {};
|
|
599
973
|
var backend = opts.backend;
|
|
@@ -617,7 +991,30 @@ function cachingGate(gate, opts) {
|
|
|
617
991
|
});
|
|
618
992
|
}
|
|
619
993
|
|
|
620
|
-
|
|
994
|
+
/**
|
|
995
|
+
* @primitive b.gateContract.workerThreadGate
|
|
996
|
+
* @signature b.gateContract.workerThreadGate(gate, opts)
|
|
997
|
+
* @since 0.7.5
|
|
998
|
+
* @status stable
|
|
999
|
+
* @related b.gateContract.defineGate, b.gateContract.cachingGate
|
|
1000
|
+
*
|
|
1001
|
+
* Offload a gate's `check(ctx)` to a worker. `opts.worker` must
|
|
1002
|
+
* expose `run({ gate, ctx })` returning the decision (matches the
|
|
1003
|
+
* `b.worker` shape). Useful when a guard's per-request CPU cost
|
|
1004
|
+
* (large-doc HTML parsing, archive entry inspection) is high enough
|
|
1005
|
+
* that running it on the request thread would impact throughput.
|
|
1006
|
+
*
|
|
1007
|
+
* @opts
|
|
1008
|
+
* worker: object, // b.worker-shaped { run } (required)
|
|
1009
|
+
* name: string, // wrapper gate name (default "<gate>:worker")
|
|
1010
|
+
*
|
|
1011
|
+
* @example
|
|
1012
|
+
* var worker = b.worker.create({ pool: 4, modulePath: "./guards/csv-worker.js" });
|
|
1013
|
+
* var strict = b.guardCsv.gate({ profile: "strict" });
|
|
1014
|
+
* var offloaded = b.gateContract.workerThreadGate(strict, { worker: worker });
|
|
1015
|
+
* var d = await offloaded.check({ bytes: Buffer.from("name,age\nada,36") });
|
|
1016
|
+
* d.action; // → "serve"
|
|
1017
|
+
*/
|
|
621
1018
|
function workerThreadGate(gate, opts) {
|
|
622
1019
|
opts = opts || {};
|
|
623
1020
|
if (!opts.worker) {
|
|
@@ -632,10 +1029,33 @@ function workerThreadGate(gate, opts) {
|
|
|
632
1029
|
});
|
|
633
1030
|
}
|
|
634
1031
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1032
|
+
/**
|
|
1033
|
+
* @primitive b.gateContract.makeProfileBuilder
|
|
1034
|
+
* @signature b.gateContract.makeProfileBuilder(profiles)
|
|
1035
|
+
* @since 0.7.5
|
|
1036
|
+
* @status stable
|
|
1037
|
+
* @related b.gateContract.buildProfile, b.gateContract.resolveProfileAndPosture
|
|
1038
|
+
*
|
|
1039
|
+
* Closes over a guard's `PROFILES` map and returns a `buildProfile(opts)`
|
|
1040
|
+
* function that delegates to the recursive composition entry point.
|
|
1041
|
+
* Every guard's `buildProfile` export is therefore a single binding,
|
|
1042
|
+
* not a duplicate forwarding wrapper. The returned function accepts
|
|
1043
|
+
* `{ baseProfile, extends, overrides, removes }` plus inline keys, and
|
|
1044
|
+
* resolves names through the closed-over profile table.
|
|
1045
|
+
*
|
|
1046
|
+
* @example
|
|
1047
|
+
* var PROFILES = {
|
|
1048
|
+
* strict: { formulaInjectionPolicy: "reject", bidiCharPolicy: "reject" },
|
|
1049
|
+
* balanced: { formulaInjectionPolicy: "prefix-tab", bidiCharPolicy: "strip" },
|
|
1050
|
+
* };
|
|
1051
|
+
* var buildProfile = b.gateContract.makeProfileBuilder(PROFILES);
|
|
1052
|
+
* var custom = buildProfile({
|
|
1053
|
+
* baseProfile: "strict",
|
|
1054
|
+
* overrides: { trailingWhitespacePolicy: "preserve" },
|
|
1055
|
+
* });
|
|
1056
|
+
* custom.formulaInjectionPolicy; // → "reject"
|
|
1057
|
+
* custom.trailingWhitespacePolicy; // → "preserve"
|
|
1058
|
+
*/
|
|
639
1059
|
function makeProfileBuilder(profiles) {
|
|
640
1060
|
return function (opts) {
|
|
641
1061
|
return buildProfile(Object.assign({}, opts, {
|
|
@@ -644,10 +1064,30 @@ function makeProfileBuilder(profiles) {
|
|
|
644
1064
|
};
|
|
645
1065
|
}
|
|
646
1066
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1067
|
+
/**
|
|
1068
|
+
* @primitive b.gateContract.lookupCompliancePosture
|
|
1069
|
+
* @signature b.gateContract.lookupCompliancePosture(name, postures, errorFactory, codePrefix)
|
|
1070
|
+
* @since 0.7.5
|
|
1071
|
+
* @status stable
|
|
1072
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
1073
|
+
* @related b.gateContract.resolveProfileAndPosture, b.gateContract.makeProfileBuilder
|
|
1074
|
+
*
|
|
1075
|
+
* Look up a compliance-posture overlay by name. Throws
|
|
1076
|
+
* `errorFactory(codePrefix + ".bad-posture")` when the name is not in
|
|
1077
|
+
* the posture map; returns a shallow clone of the posture object
|
|
1078
|
+
* otherwise. Every guard's `compliancePosture(name)` export forwards
|
|
1079
|
+
* here so the error code, error class, and clone semantics stay
|
|
1080
|
+
* identical across the family.
|
|
1081
|
+
*
|
|
1082
|
+
* @example
|
|
1083
|
+
* var POSTURES = {
|
|
1084
|
+
* hipaa: { piiPolicy: "redact", bidiCharPolicy: "reject" },
|
|
1085
|
+
* "pci-dss": { piiPolicy: "redact", bidiCharPolicy: "reject" },
|
|
1086
|
+
* };
|
|
1087
|
+
* var posture = b.gateContract.lookupCompliancePosture(
|
|
1088
|
+
* "hipaa", POSTURES, b.guardCsv.GuardCsvError.factory, "csv");
|
|
1089
|
+
* posture.piiPolicy; // → "redact"
|
|
1090
|
+
*/
|
|
651
1091
|
function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
|
|
652
1092
|
if (!postures || !postures[name]) {
|
|
653
1093
|
throw errorFactory(codePrefix + ".bad-posture",
|
|
@@ -656,10 +1096,30 @@ function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
|
|
|
656
1096
|
return Object.assign({}, postures[name]);
|
|
657
1097
|
}
|
|
658
1098
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* @primitive b.gateContract.makeRulePackLoader
|
|
1101
|
+
* @signature b.gateContract.makeRulePackLoader(errorClass, codePrefix)
|
|
1102
|
+
* @since 0.7.5
|
|
1103
|
+
* @status stable
|
|
1104
|
+
* @related b.gateContract.makeProfileBuilder, b.gateContract.lookupCompliancePosture
|
|
1105
|
+
*
|
|
1106
|
+
* Build a per-guard rule-pack registry. Returns
|
|
1107
|
+
* `{ load(pack), list(), get(id) }`. `load` validates that `pack`
|
|
1108
|
+
* is an object with a non-empty string `pack.id` (throwing
|
|
1109
|
+
* `errorClass(codePrefix + ".bad-opt")` when not) and stores it in
|
|
1110
|
+
* a closed-over map keyed by `pack.id`. `list` returns the stored
|
|
1111
|
+
* packs; `get(id)` returns one or `null`. Used so every guard's
|
|
1112
|
+
* `loadRulePack` export shares storage shape and validation.
|
|
1113
|
+
*
|
|
1114
|
+
* @example
|
|
1115
|
+
* var packs = b.gateContract.makeRulePackLoader(b.guardCsv.GuardCsvError, "csv");
|
|
1116
|
+
* packs.load({
|
|
1117
|
+
* id: "pii-extra",
|
|
1118
|
+
* rules: [{ id: "ssn", severity: "critical",
|
|
1119
|
+
* detect: function (cell) { return /^\d{3}-\d{2}-\d{4}$/.test(cell); } }],
|
|
1120
|
+
* });
|
|
1121
|
+
* packs.get("pii-extra").rules.length; // → 1
|
|
1122
|
+
*/
|
|
663
1123
|
function makeRulePackLoader(errorClass, codePrefix) {
|
|
664
1124
|
var store = Object.create(null);
|
|
665
1125
|
return {
|
|
@@ -677,15 +1137,26 @@ function makeRulePackLoader(errorClass, codePrefix) {
|
|
|
677
1137
|
};
|
|
678
1138
|
}
|
|
679
1139
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* @primitive b.gateContract.extractBytesAsText
|
|
1142
|
+
* @signature b.gateContract.extractBytesAsText(ctx)
|
|
1143
|
+
* @since 0.7.5
|
|
1144
|
+
* @status stable
|
|
1145
|
+
* @related b.gateContract.buildGuardGate, b.gateContract.aggregateIssues
|
|
1146
|
+
*
|
|
1147
|
+
* Read `ctx.bytes` and return a UTF-8 string for inspection. Centralizes
|
|
1148
|
+
* the string-or-Buffer-or-empty handling so each guard's `check(ctx)`
|
|
1149
|
+
* body deals with the inspection logic only. Returns `""` when
|
|
1150
|
+
* `ctx.bytes` is missing — callers treat empty as the serve case.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* var ctx = { bytes: Buffer.from("name,age\nada,36") };
|
|
1154
|
+
* var text = b.gateContract.extractBytesAsText(ctx);
|
|
1155
|
+
* text; // → "name,age\nada,36"
|
|
1156
|
+
*
|
|
1157
|
+
* b.gateContract.extractBytesAsText({}); // → ""
|
|
1158
|
+
* b.gateContract.extractBytesAsText({ bytes: "x,y" }); // → "x,y"
|
|
1159
|
+
*/
|
|
689
1160
|
function extractBytesAsText(ctx) {
|
|
690
1161
|
if (!ctx) return "";
|
|
691
1162
|
var bytes = ctx.bytes;
|
|
@@ -693,16 +1164,52 @@ function extractBytesAsText(ctx) {
|
|
|
693
1164
|
return Buffer.isBuffer(bytes) ? bytes.toString("utf8") : String(bytes);
|
|
694
1165
|
}
|
|
695
1166
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1167
|
+
/**
|
|
1168
|
+
* @primitive b.gateContract.buildGuardGate
|
|
1169
|
+
* @signature b.gateContract.buildGuardGate(name, opts, check)
|
|
1170
|
+
* @since 0.7.5
|
|
1171
|
+
* @status stable
|
|
1172
|
+
* @related b.gateContract.defineGate, b.gateContract.extractBytesAsText
|
|
1173
|
+
*
|
|
1174
|
+
* Gate-construction shorthand for guard-* primitives. Forwards the
|
|
1175
|
+
* uniform ~16-key opts bag (`mode`, `audit`, `observability`,
|
|
1176
|
+
* `forensicEvidenceStore`, `forensicSnippetBytes`, `cache`,
|
|
1177
|
+
* `cacheTtlMs`, `maxRuntimeMs`, all six lifecycle hooks) to
|
|
1178
|
+
* `defineGate`, so each guard's `gate(opts)` body is just the per-guard
|
|
1179
|
+
* `check` function plus a label. Result satisfies `validateGateShape`.
|
|
1180
|
+
*
|
|
1181
|
+
* @opts
|
|
1182
|
+
* mode: string, // one of MODES; default "enforce"
|
|
1183
|
+
* audit: object|null, // b.audit handle for emission
|
|
1184
|
+
* observability: object|null, // b.observability handle
|
|
1185
|
+
* forensicEvidenceStore: object|null, // { write({ ... }) }
|
|
1186
|
+
* forensicSnippetBytes: number, // 0 = disabled
|
|
1187
|
+
* cache: object|null, // b.cache shape
|
|
1188
|
+
* cacheTtlMs: number,
|
|
1189
|
+
* maxRuntimeMs: number, // 0 = uncapped
|
|
1190
|
+
* beforeCheck: function|null,
|
|
1191
|
+
* afterCheck: function|null,
|
|
1192
|
+
* onIssue: function|null,
|
|
1193
|
+
* onSanitize: function|null,
|
|
1194
|
+
* onRefuse: function|null,
|
|
1195
|
+
* onAudit: function|null,
|
|
1196
|
+
*
|
|
1197
|
+
* @example
|
|
1198
|
+
* var myGuardGate = b.gateContract.buildGuardGate(
|
|
1199
|
+
* "myGuard:strict",
|
|
1200
|
+
* { mode: "enforce", maxRuntimeMs: 250 },
|
|
1201
|
+
* async function (ctx) {
|
|
1202
|
+
* var text = b.gateContract.extractBytesAsText(ctx);
|
|
1203
|
+
* if (text.length === 0) return { ok: true, action: "serve" };
|
|
1204
|
+
* if (/\s/.test(text)) {
|
|
1205
|
+
* return { ok: false, action: "refuse",
|
|
1206
|
+
* issues: [{ kind: "whitespace", severity: "high" }] };
|
|
1207
|
+
* }
|
|
1208
|
+
* return { ok: true, action: "serve" };
|
|
1209
|
+
* });
|
|
1210
|
+
* var d = await myGuardGate.check({ bytes: Buffer.from("hello") });
|
|
1211
|
+
* d.action; // → "serve"
|
|
1212
|
+
*/
|
|
706
1213
|
function buildGuardGate(name, opts, check) {
|
|
707
1214
|
opts = opts || {};
|
|
708
1215
|
return defineGate({
|
|
@@ -726,10 +1233,28 @@ function buildGuardGate(name, opts, check) {
|
|
|
726
1233
|
});
|
|
727
1234
|
}
|
|
728
1235
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1236
|
+
/**
|
|
1237
|
+
* @primitive b.gateContract.aggregateIssues
|
|
1238
|
+
* @signature b.gateContract.aggregateIssues(issues)
|
|
1239
|
+
* @since 0.7.5
|
|
1240
|
+
* @status stable
|
|
1241
|
+
* @related b.gateContract.runIssueValidator, b.gateContract.summarizeIssues
|
|
1242
|
+
*
|
|
1243
|
+
* Wrap an issues array in the canonical `{ ok, issues }` validate-result
|
|
1244
|
+
* shape. `ok` is `true` only when no issue carries `critical` or `high`
|
|
1245
|
+
* severity — `info` and `warn` issues do not flip `ok`. Used by guards
|
|
1246
|
+
* whose validate path can't route through `runIssueValidator` (raw-Buffer
|
|
1247
|
+
* input cases such as svg magic detection or filename byte scans).
|
|
1248
|
+
*
|
|
1249
|
+
* @example
|
|
1250
|
+
* var result = b.gateContract.aggregateIssues([
|
|
1251
|
+
* { kind: "csv.bidi", severity: "high",
|
|
1252
|
+
* snippet: "U+202E embedded in cell" },
|
|
1253
|
+
* { kind: "csv.trailing-whitespace", severity: "info" },
|
|
1254
|
+
* ]);
|
|
1255
|
+
* result.ok; // → false
|
|
1256
|
+
* result.issues.length; // → 2
|
|
1257
|
+
*/
|
|
733
1258
|
function aggregateIssues(issues) {
|
|
734
1259
|
return {
|
|
735
1260
|
ok: !issues.some(function (i) {
|
|
@@ -739,12 +1264,27 @@ function aggregateIssues(issues) {
|
|
|
739
1264
|
};
|
|
740
1265
|
}
|
|
741
1266
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1267
|
+
/**
|
|
1268
|
+
* @primitive b.gateContract.badInputResultIfNotStringOrBuffer
|
|
1269
|
+
* @signature b.gateContract.badInputResultIfNotStringOrBuffer(input)
|
|
1270
|
+
* @since 0.7.5
|
|
1271
|
+
* @status stable
|
|
1272
|
+
* @related b.gateContract.runIssueValidator, b.gateContract.aggregateIssues
|
|
1273
|
+
*
|
|
1274
|
+
* Type-guard for guard-* validate entry points. Returns the canonical
|
|
1275
|
+
* `{ ok: false, issues: [{ kind: "bad-input", severity: "high", ... }] }`
|
|
1276
|
+
* result when `input` is neither a string nor a Buffer; `null`
|
|
1277
|
+
* otherwise. Used by guards whose validate path can't pre-convert —
|
|
1278
|
+
* `b.guardSvg` needs raw bytes for SVGZ magic detection,
|
|
1279
|
+
* `b.guardFilename` needs raw bytes for the overlong-UTF-8 byte scan.
|
|
1280
|
+
*
|
|
1281
|
+
* @example
|
|
1282
|
+
* b.gateContract.badInputResultIfNotStringOrBuffer("hello"); // → null
|
|
1283
|
+
* b.gateContract.badInputResultIfNotStringOrBuffer(Buffer.from("x")); // → null
|
|
1284
|
+
* var bad = b.gateContract.badInputResultIfNotStringOrBuffer(42);
|
|
1285
|
+
* bad.ok; // → false
|
|
1286
|
+
* bad.issues[0].kind; // → "bad-input"
|
|
1287
|
+
*/
|
|
748
1288
|
function badInputResultIfNotStringOrBuffer(input) {
|
|
749
1289
|
if (typeof input === "string" || Buffer.isBuffer(input)) return null;
|
|
750
1290
|
return {
|
|
@@ -754,17 +1294,40 @@ function badInputResultIfNotStringOrBuffer(input) {
|
|
|
754
1294
|
};
|
|
755
1295
|
}
|
|
756
1296
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1297
|
+
/**
|
|
1298
|
+
* @primitive b.gateContract.runIssueValidator
|
|
1299
|
+
* @signature b.gateContract.runIssueValidator(input, opts, detector)
|
|
1300
|
+
* @since 0.7.5
|
|
1301
|
+
* @status stable
|
|
1302
|
+
* @related b.gateContract.aggregateIssues, b.gateContract.badInputResultIfNotStringOrBuffer
|
|
1303
|
+
*
|
|
1304
|
+
* Boilerplate for guard-* `validate(input, opts)` entry points.
|
|
1305
|
+
* Normalizes string-or-Buffer input to a UTF-8 string, returns the
|
|
1306
|
+
* canonical `{ ok: false, issues: [{ kind: "bad-input", ... }] }` shape
|
|
1307
|
+
* on type mismatch, otherwise calls the operator-supplied `detector`
|
|
1308
|
+
* and aggregates its issues array. Result `ok` is `true` only when no
|
|
1309
|
+
* detected issue is `critical` / `high` severity. Lets every guard's
|
|
1310
|
+
* `validate()` body be identical scaffolding around the per-guard
|
|
1311
|
+
* detector. The `opts` argument is forwarded verbatim as the second
|
|
1312
|
+
* argument to `detector(text, opts)` — its shape is detector-defined,
|
|
1313
|
+
* not constrained by gate-contract.
|
|
1314
|
+
*
|
|
1315
|
+
* @opts
|
|
1316
|
+
* ...: any, // detector-defined; passed through to detector(text, opts)
|
|
1317
|
+
*
|
|
1318
|
+
* @example
|
|
1319
|
+
* function detectFormulaTrigger(text) {
|
|
1320
|
+
* if (/^[=+\-@]/.test(text)) {
|
|
1321
|
+
* return [{ kind: "csv.formula-injection", severity: "high",
|
|
1322
|
+
* snippet: text.slice(0, 16) }];
|
|
1323
|
+
* }
|
|
1324
|
+
* return [];
|
|
1325
|
+
* }
|
|
1326
|
+
* var bad = b.gateContract.runIssueValidator("=cmd|x", {}, detectFormulaTrigger);
|
|
1327
|
+
* bad.ok; // → false
|
|
1328
|
+
* var ok = b.gateContract.runIssueValidator("ada,36", {}, detectFormulaTrigger);
|
|
1329
|
+
* ok.ok; // → true
|
|
1330
|
+
*/
|
|
768
1331
|
function runIssueValidator(input, opts, detector) {
|
|
769
1332
|
var text = typeof input === "string"
|
|
770
1333
|
? input
|
|
@@ -779,18 +1342,52 @@ function runIssueValidator(input, opts, detector) {
|
|
|
779
1342
|
return aggregateIssues(detector(text, opts));
|
|
780
1343
|
}
|
|
781
1344
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1345
|
+
/**
|
|
1346
|
+
* @primitive b.gateContract.resolveProfileAndPosture
|
|
1347
|
+
* @signature b.gateContract.resolveProfileAndPosture(opts, cfg)
|
|
1348
|
+
* @since 0.7.5
|
|
1349
|
+
* @status stable
|
|
1350
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
1351
|
+
* @related b.gateContract.makeProfileBuilder, b.gateContract.lookupCompliancePosture
|
|
1352
|
+
*
|
|
1353
|
+
* Overlay `opts.profile` and `opts.compliancePosture` on top of a
|
|
1354
|
+
* defaults object using guard-supplied tables. Every guard primitive's
|
|
1355
|
+
* factory routes through this so the resolution shape — defaults
|
|
1356
|
+
* first, profile overlay, posture overlay, inline opts last — stays
|
|
1357
|
+
* identical across the family. When `opts.compliancePosture` is unset
|
|
1358
|
+
* and `b.compliance.set()` has declared a global posture, the global
|
|
1359
|
+
* posture takes effect (the value-add of the top-level coordinator).
|
|
1360
|
+
*
|
|
1361
|
+
* Throws `cfg.errorClass.factory(cfg.errCodePrefix + ".bad-profile")`
|
|
1362
|
+
* for unknown profile names and `... + ".bad-posture"` for unknown
|
|
1363
|
+
* postures.
|
|
1364
|
+
*
|
|
1365
|
+
* @opts
|
|
1366
|
+
* profiles: object, // PROFILES table (required)
|
|
1367
|
+
* compliancePostures: object, // COMPLIANCE_POSTURES table (required)
|
|
1368
|
+
* defaults: object, // baseline before overlay
|
|
1369
|
+
* errorClass: FrameworkError,// throws via .factory(code, msg)
|
|
1370
|
+
* errCodePrefix: string, // e.g. "csv" → "csv.bad-profile"
|
|
1371
|
+
*
|
|
1372
|
+
* @example
|
|
1373
|
+
* var PROFILES = {
|
|
1374
|
+
* strict: { formulaInjectionPolicy: "reject", bidiCharPolicy: "reject" },
|
|
1375
|
+
* balanced: { formulaInjectionPolicy: "prefix-tab", bidiCharPolicy: "strip" },
|
|
1376
|
+
* };
|
|
1377
|
+
* var POSTURES = { hipaa: { piiPolicy: "redact" } };
|
|
1378
|
+
* var resolved = b.gateContract.resolveProfileAndPosture(
|
|
1379
|
+
* { profile: "balanced", compliancePosture: "hipaa", maxCellBytes: 65536 },
|
|
1380
|
+
* {
|
|
1381
|
+
* profiles: PROFILES,
|
|
1382
|
+
* compliancePostures: POSTURES,
|
|
1383
|
+
* defaults: { maxCellBytes: 1024 },
|
|
1384
|
+
* errorClass: b.guardCsv.GuardCsvError,
|
|
1385
|
+
* errCodePrefix: "csv",
|
|
1386
|
+
* });
|
|
1387
|
+
* resolved.formulaInjectionPolicy; // → "prefix-tab"
|
|
1388
|
+
* resolved.piiPolicy; // → "redact"
|
|
1389
|
+
* resolved.maxCellBytes; // → 65536
|
|
1390
|
+
*/
|
|
794
1391
|
function resolveProfileAndPosture(opts, cfg) {
|
|
795
1392
|
opts = opts || {};
|
|
796
1393
|
validateOpts.requireObject(cfg, "gateContract.resolveProfileAndPosture",
|
|
@@ -830,15 +1427,47 @@ function resolveProfileAndPosture(opts, cfg) {
|
|
|
830
1427
|
return Object.assign({}, cfg.defaults || {}, overlay, opts);
|
|
831
1428
|
}
|
|
832
1429
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1430
|
+
/**
|
|
1431
|
+
* @primitive b.gateContract.buildProfile
|
|
1432
|
+
* @signature b.gateContract.buildProfile(opts)
|
|
1433
|
+
* @since 0.7.5
|
|
1434
|
+
* @status stable
|
|
1435
|
+
* @related b.gateContract.makeProfileBuilder, b.gateContract.resolveProfileAndPosture
|
|
1436
|
+
*
|
|
1437
|
+
* Recursive profile composition with cycle detection. Walks
|
|
1438
|
+
* `opts.baseProfile` and every name in `opts.extends` through the
|
|
1439
|
+
* caller-supplied `opts.resolveProfile` resolver, deep-merging arrays
|
|
1440
|
+
* (set-union, later-wins on duplicates) and objects (recursive). Then
|
|
1441
|
+
* applies inline `opts.overrides` and finally `opts.removes` (which
|
|
1442
|
+
* can drop array entries or whole keys). Cycles throw
|
|
1443
|
+
* `gate-contract/profile-cycle`; unknown names throw
|
|
1444
|
+
* `gate-contract/unknown-profile`. Most guards bind through
|
|
1445
|
+
* `makeProfileBuilder` and never call `buildProfile` directly.
|
|
1446
|
+
*
|
|
1447
|
+
* @opts
|
|
1448
|
+
* baseProfile: string, // start from this profile name
|
|
1449
|
+
* extends: string[], // additional bases (later-wins)
|
|
1450
|
+
* overrides: object, // inline merge after extends
|
|
1451
|
+
* removes: object, // drop array entries or keys
|
|
1452
|
+
* resolveProfile: function, // (name) → profile|null (required)
|
|
1453
|
+
*
|
|
1454
|
+
* @example
|
|
1455
|
+
* var PROFILES = {
|
|
1456
|
+
* "blog-post": {
|
|
1457
|
+
* allowedTags: ["p", "a", "strong"],
|
|
1458
|
+
* allowedAttrs: { a: ["href", "target"] },
|
|
1459
|
+
* },
|
|
1460
|
+
* "with-images": { extends: ["blog-post"], allowedTags: ["img"] },
|
|
1461
|
+
* };
|
|
1462
|
+
* var resolved = b.gateContract.buildProfile({
|
|
1463
|
+
* baseProfile: "with-images",
|
|
1464
|
+
* overrides: { allowedTags: ["em"] },
|
|
1465
|
+
* removes: { allowedAttrs: { a: ["target"] } },
|
|
1466
|
+
* resolveProfile: function (n) { return PROFILES[n] || null; },
|
|
1467
|
+
* });
|
|
1468
|
+
* resolved.allowedTags; // → ["p","a","strong","img","em"]
|
|
1469
|
+
* resolved.allowedAttrs.a; // → ["href"]
|
|
1470
|
+
*/
|
|
842
1471
|
function buildProfile(opts) {
|
|
843
1472
|
validateOpts.requireObject(opts, "gateContract.buildProfile", GateContractError);
|
|
844
1473
|
var resolve = opts.resolveProfile;
|
|
@@ -924,11 +1553,30 @@ function _applyRemoves(target, removes) {
|
|
|
924
1553
|
return out;
|
|
925
1554
|
}
|
|
926
1555
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1556
|
+
/**
|
|
1557
|
+
* @primitive b.gateContract.summarizeIssues
|
|
1558
|
+
* @signature b.gateContract.summarizeIssues(issues)
|
|
1559
|
+
* @since 0.7.5
|
|
1560
|
+
* @status stable
|
|
1561
|
+
* @related b.gateContract.aggregateIssues, b.gateContract.defineGate
|
|
1562
|
+
*
|
|
1563
|
+
* Project a gate decision's `issues` array down to the audit-friendly
|
|
1564
|
+
* shape — `{ kind, severity, ruleId }` only. Full snippets stay in the
|
|
1565
|
+
* forensic evidence store; the audit log records the classification
|
|
1566
|
+
* without the offending bytes. Replaces the inline
|
|
1567
|
+
* `(d.issues || []).map(...)` pattern host primitives previously
|
|
1568
|
+
* carried per emit site.
|
|
1569
|
+
*
|
|
1570
|
+
* @example
|
|
1571
|
+
* var summary = b.gateContract.summarizeIssues([
|
|
1572
|
+
* { kind: "csv.bidi", severity: "high", ruleId: "BIDI-OVERRIDE",
|
|
1573
|
+
* snippet: "<offending bytes redacted>" },
|
|
1574
|
+
* { kind: "csv.trailing-whitespace", severity: "info", ruleId: "TRIM" },
|
|
1575
|
+
* ]);
|
|
1576
|
+
* summary.length; // → 2
|
|
1577
|
+
* summary[0].snippet; // → undefined (stripped)
|
|
1578
|
+
* summary[0].ruleId; // → "BIDI-OVERRIDE"
|
|
1579
|
+
*/
|
|
932
1580
|
function summarizeIssues(issues) {
|
|
933
1581
|
if (!Array.isArray(issues)) return [];
|
|
934
1582
|
return issues.map(function (i) {
|
|
@@ -936,8 +1584,34 @@ function summarizeIssues(issues) {
|
|
|
936
1584
|
});
|
|
937
1585
|
}
|
|
938
1586
|
|
|
939
|
-
|
|
940
|
-
|
|
1587
|
+
/**
|
|
1588
|
+
* @primitive b.gateContract.composeHooks
|
|
1589
|
+
* @signature b.gateContract.composeHooks(hooks)
|
|
1590
|
+
* @since 0.7.5
|
|
1591
|
+
* @status stable
|
|
1592
|
+
* @related b.gateContract.defineGate, b.gateContract.buildGuardGate
|
|
1593
|
+
*
|
|
1594
|
+
* Chain a list of operator hooks into a single async hook. Empty
|
|
1595
|
+
* arrays return `null` (so `defineGate` can pass the result through
|
|
1596
|
+
* its `hooks.X || null` slot); single-element arrays return the
|
|
1597
|
+
* lone hook unchanged. Multi-element chains run sequentially —
|
|
1598
|
+
* `{ suppress: true }` or `{ skip: true }` from any hook short-
|
|
1599
|
+
* circuits and returns; otherwise the last non-null hook result wins.
|
|
1600
|
+
*
|
|
1601
|
+
* @example
|
|
1602
|
+
* var redactPii = function (issue) {
|
|
1603
|
+
* return Object.assign({}, issue, { snippet: "<redacted>" });
|
|
1604
|
+
* };
|
|
1605
|
+
* var dropInfo = function (issue) {
|
|
1606
|
+
* return issue.severity === "info" ? { suppress: true } : null;
|
|
1607
|
+
* };
|
|
1608
|
+
* var onIssue = b.gateContract.composeHooks([dropInfo, redactPii]);
|
|
1609
|
+
* var infoHit = await onIssue({ kind: "csv.trim", severity: "info" });
|
|
1610
|
+
* infoHit.suppress; // → true
|
|
1611
|
+
* var bidi = await onIssue({ kind: "csv.bidi", severity: "high",
|
|
1612
|
+
* snippet: "U+202E" });
|
|
1613
|
+
* bidi.snippet; // → "<redacted>"
|
|
1614
|
+
*/
|
|
941
1615
|
function composeHooks(hooks) {
|
|
942
1616
|
hooks = (hooks || []).filter(Boolean);
|
|
943
1617
|
if (hooks.length === 0) return null;
|