@blamejs/core 0.8.42 → 0.8.49
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/ai-pref.js
CHANGED
|
@@ -1,48 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.aiPref
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* per-user-agent rules in robots.txt rather
|
|
28
|
-
* than the catch-all default.
|
|
29
|
-
*
|
|
30
|
-
* b.aiPref.robotsBlock(opts) -> string
|
|
31
|
-
* Returns a robots.txt block per AIPREF §3 grammar:
|
|
32
|
-
*
|
|
33
|
-
* User-agent: GPTBot
|
|
34
|
-
* Content-Usage: train=deny, infer=allow, snippet=allow
|
|
35
|
-
*
|
|
36
|
-
* b.aiPref.serializeHeader(opts) -> string
|
|
37
|
-
* Returns the Content-Usage HTTP response header value.
|
|
38
|
-
*
|
|
39
|
-
* b.aiPref.parseHeader(value) -> { train, infer, snippet, price? }
|
|
40
|
-
* Parses an inbound Content-Usage header (used when the framework
|
|
41
|
-
* plays the role of crawler: respect declared preferences).
|
|
42
|
-
*
|
|
43
|
-
* b.aiPref.refusePaidCrawl(req, res, opts)
|
|
44
|
-
* Convenience: emits HTTP 402 Payment Required with the price
|
|
45
|
-
* manifest in the Cloudflare-compatible JSON body.
|
|
3
|
+
* @module b.aiPref
|
|
4
|
+
* @nav AI
|
|
5
|
+
* @title Ai Pref
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* AIPREF (RFC draft) signal — operators publish a machine-readable
|
|
9
|
+
* preference about AI training / agent crawling / etc.
|
|
10
|
+
*
|
|
11
|
+
* Wires three coordinating surfaces into one primitive: the IETF
|
|
12
|
+
* AIPREF `Content-Usage` HTTP response header
|
|
13
|
+
* (draft-ietf-aipref-attach-04, deadline 2026-08), the matching
|
|
14
|
+
* robots.txt grammar, and Cloudflare's Content Signals Policy +
|
|
15
|
+
* Pay-Per-Crawl (HTTP 402). Operators declare train / infer /
|
|
16
|
+
* snippet preferences once; the middleware emits both the
|
|
17
|
+
* `Content-Usage` header and Cloudflare's `CF-Content-Signals`
|
|
18
|
+
* alongside.
|
|
19
|
+
*
|
|
20
|
+
* Inbound parsing closes the loop when the framework plays the role
|
|
21
|
+
* of crawler — `parseHeader` decodes a peer's preferences so the
|
|
22
|
+
* caller can refuse training / pay the per-crawl price / respect a
|
|
23
|
+
* snippet=deny.
|
|
24
|
+
*
|
|
25
|
+
* @card
|
|
26
|
+
* AIPREF (RFC draft) signal — operators publish a machine-readable preference about AI training / agent crawling / etc.
|
|
46
27
|
*/
|
|
47
28
|
|
|
48
29
|
var audit = require("./audit");
|
|
@@ -80,6 +61,38 @@ function _validate(opts) {
|
|
|
80
61
|
return { train: train, infer: infer, snippet: snippet, price: opts.price || null };
|
|
81
62
|
}
|
|
82
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @primitive b.aiPref.serializeHeader
|
|
66
|
+
* @signature b.aiPref.serializeHeader(opts)
|
|
67
|
+
* @since 0.8.44
|
|
68
|
+
* @related b.aiPref.middleware, b.aiPref.parseHeader, b.aiPref.robotsBlock
|
|
69
|
+
*
|
|
70
|
+
* Render the AIPREF `Content-Usage` HTTP response header value from
|
|
71
|
+
* an operator preference object. Output is an RFC 8941 structured-
|
|
72
|
+
* fields list of `train=...`, `infer=...`, `snippet=...` pairs, plus
|
|
73
|
+
* `price-usd` / `per-tokens` when any axis is `paid`. Throws when the
|
|
74
|
+
* preferences are inconsistent (e.g. `train=paid` with no price).
|
|
75
|
+
*
|
|
76
|
+
* @opts
|
|
77
|
+
* train: "allow" | "deny" | "paid", // default "deny"
|
|
78
|
+
* infer: "allow" | "deny" | "paid", // default "allow"
|
|
79
|
+
* snippet: "allow" | "deny", // default "allow"
|
|
80
|
+
* price: { amountUsd: number, perTokens?: number },
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* var v = b.aiPref.serializeHeader({
|
|
84
|
+
* train: "deny",
|
|
85
|
+
* infer: "allow",
|
|
86
|
+
* snippet: "allow",
|
|
87
|
+
* });
|
|
88
|
+
* // → "train=deny, infer=allow, snippet=allow"
|
|
89
|
+
*
|
|
90
|
+
* var paid = b.aiPref.serializeHeader({
|
|
91
|
+
* train: "paid", infer: "paid", snippet: "allow",
|
|
92
|
+
* price: { amountUsd: 0.001, perTokens: 1000 },
|
|
93
|
+
* });
|
|
94
|
+
* // → "train=paid, infer=paid, snippet=allow, price-usd=0.001000, per-tokens=1000"
|
|
95
|
+
*/
|
|
83
96
|
function serializeHeader(opts) {
|
|
84
97
|
var v = _validate(opts);
|
|
85
98
|
// RFC 8941 structured-fields list of token=token pairs. AIPREF §4.2.
|
|
@@ -97,6 +110,33 @@ function serializeHeader(opts) {
|
|
|
97
110
|
return parts.join(", ");
|
|
98
111
|
}
|
|
99
112
|
|
|
113
|
+
/**
|
|
114
|
+
* @primitive b.aiPref.parseHeader
|
|
115
|
+
* @signature b.aiPref.parseHeader(value)
|
|
116
|
+
* @since 0.8.44
|
|
117
|
+
* @related b.aiPref.serializeHeader, b.aiPref.middleware
|
|
118
|
+
*
|
|
119
|
+
* Parse an inbound `Content-Usage` header value into the typed
|
|
120
|
+
* preference shape. Used when the framework acts as a crawler and
|
|
121
|
+
* must respect a publisher's declared preferences. Unknown axes are
|
|
122
|
+
* dropped silently so a forward-compatible publisher can advertise
|
|
123
|
+
* future fields without breaking older clients. Throws when the
|
|
124
|
+
* value is missing or exceeds the 1024-char defensive cap.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* var p = b.aiPref.parseHeader(
|
|
128
|
+
* "train=deny, infer=allow, snippet=allow"
|
|
129
|
+
* );
|
|
130
|
+
* p.train; // → "deny"
|
|
131
|
+
* p.infer; // → "allow"
|
|
132
|
+
* p.snippet; // → "allow"
|
|
133
|
+
*
|
|
134
|
+
* var paid = b.aiPref.parseHeader(
|
|
135
|
+
* "train=paid, infer=allow, snippet=allow, price-usd=0.001000, per-tokens=1000"
|
|
136
|
+
* );
|
|
137
|
+
* paid.price.amountUsd; // → 0.001
|
|
138
|
+
* paid.price.perTokens; // → 1000
|
|
139
|
+
*/
|
|
100
140
|
function parseHeader(value) {
|
|
101
141
|
if (typeof value !== "string" || value.length === 0) {
|
|
102
142
|
throw AiPrefError.factory("BAD_HEADER", "aiPref.parseHeader: value required");
|
|
@@ -127,6 +167,35 @@ function parseHeader(value) {
|
|
|
127
167
|
return out;
|
|
128
168
|
}
|
|
129
169
|
|
|
170
|
+
/**
|
|
171
|
+
* @primitive b.aiPref.robotsBlock
|
|
172
|
+
* @signature b.aiPref.robotsBlock(opts)
|
|
173
|
+
* @since 0.8.44
|
|
174
|
+
* @related b.aiPref.serializeHeader, b.aiPref.middleware
|
|
175
|
+
*
|
|
176
|
+
* Render an AIPREF §3 robots.txt block: a `User-agent:` line followed
|
|
177
|
+
* by a `Content-Usage:` line carrying the same grammar as the HTTP
|
|
178
|
+
* header. Authors who serve robots.txt as a static file paste the
|
|
179
|
+
* output verbatim. The `userAgent` opt defaults to the catch-all `*`;
|
|
180
|
+
* pass `"GPTBot"` / `"ClaudeBot"` / etc. for per-crawler rules. UA
|
|
181
|
+
* strings are capped at 256 chars.
|
|
182
|
+
*
|
|
183
|
+
* @opts
|
|
184
|
+
* train: "allow" | "deny" | "paid",
|
|
185
|
+
* infer: "allow" | "deny" | "paid",
|
|
186
|
+
* snippet: "allow" | "deny",
|
|
187
|
+
* price: { amountUsd: number, perTokens?: number },
|
|
188
|
+
* userAgent: string, // default "*"
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* var block = b.aiPref.robotsBlock({
|
|
192
|
+
* userAgent: "GPTBot",
|
|
193
|
+
* train: "deny",
|
|
194
|
+
* infer: "allow",
|
|
195
|
+
* snippet: "allow",
|
|
196
|
+
* });
|
|
197
|
+
* // → "User-agent: GPTBot\nContent-Usage: train=deny, infer=allow, snippet=allow\n"
|
|
198
|
+
*/
|
|
130
199
|
function robotsBlock(opts) {
|
|
131
200
|
var v = _validate(opts);
|
|
132
201
|
var ua = opts.userAgent || "*";
|
|
@@ -152,6 +221,34 @@ function _cfSignalsHeader(v) {
|
|
|
152
221
|
return parts.join("; ");
|
|
153
222
|
}
|
|
154
223
|
|
|
224
|
+
/**
|
|
225
|
+
* @primitive b.aiPref.middleware
|
|
226
|
+
* @signature b.aiPref.middleware(opts)
|
|
227
|
+
* @since 0.8.44
|
|
228
|
+
* @related b.aiPref.serializeHeader, b.aiPref.refusePaidCrawl, b.aiPref.robotsBlock
|
|
229
|
+
*
|
|
230
|
+
* Build an HTTP middleware that emits `Content-Usage` (and, by
|
|
231
|
+
* default, the Cloudflare `CF-Content-Signals` mirror) on every
|
|
232
|
+
* response. Wires the operator's AI-training / inference / snippet
|
|
233
|
+
* preferences into the request lifecycle so every page advertises
|
|
234
|
+
* the same posture without per-route plumbing.
|
|
235
|
+
*
|
|
236
|
+
* @opts
|
|
237
|
+
* train: "allow" | "deny" | "paid",
|
|
238
|
+
* infer: "allow" | "deny" | "paid",
|
|
239
|
+
* snippet: "allow" | "deny",
|
|
240
|
+
* price: { amountUsd: number, perTokens?: number },
|
|
241
|
+
* cloudflareSignals: boolean, // default true
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* var aiPrefMw = b.aiPref.middleware({
|
|
245
|
+
* train: "deny",
|
|
246
|
+
* infer: "allow",
|
|
247
|
+
* snippet: "allow",
|
|
248
|
+
* });
|
|
249
|
+
* // mount aiPrefMw on every public route — emits Content-Usage +
|
|
250
|
+
* // CF-Content-Signals headers on each response.
|
|
251
|
+
*/
|
|
155
252
|
function middleware(opts) {
|
|
156
253
|
var v = _validate(opts);
|
|
157
254
|
var emitCf = opts.cloudflareSignals !== false;
|
|
@@ -167,6 +264,32 @@ function middleware(opts) {
|
|
|
167
264
|
};
|
|
168
265
|
}
|
|
169
266
|
|
|
267
|
+
/**
|
|
268
|
+
* @primitive b.aiPref.refusePaidCrawl
|
|
269
|
+
* @signature b.aiPref.refusePaidCrawl(req, res, opts)
|
|
270
|
+
* @since 0.8.44
|
|
271
|
+
* @related b.aiPref.middleware, b.aiPref.serializeHeader
|
|
272
|
+
*
|
|
273
|
+
* Emit HTTP 402 Payment Required with the price manifest in the
|
|
274
|
+
* Cloudflare-compatible JSON body. Operator route handlers detect
|
|
275
|
+
* an unmonetized AI crawler (via UA / signed-token absence / etc.)
|
|
276
|
+
* and call this helper to surface the price + contact channel
|
|
277
|
+
* uniformly. Audits the refusal under
|
|
278
|
+
* `aipref.paid_crawl_refused`.
|
|
279
|
+
*
|
|
280
|
+
* @opts
|
|
281
|
+
* price: { amountUsd: number, perTokens?: number },
|
|
282
|
+
* contact: string, // optional pricing contact
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* function handler(req, res) {
|
|
286
|
+
* b.aiPref.refusePaidCrawl(req, res, {
|
|
287
|
+
* price: { amountUsd: 0.005, perTokens: 1000 },
|
|
288
|
+
* contact: "https://example.test/ai-licensing",
|
|
289
|
+
* });
|
|
290
|
+
* }
|
|
291
|
+
* // → res.statusCode === 402; body is JSON { error: "payment_required", ... }
|
|
292
|
+
*/
|
|
170
293
|
function refusePaidCrawl(req, res, opts) {
|
|
171
294
|
if (!opts || !opts.price || typeof opts.price.amountUsd !== "number") {
|
|
172
295
|
throw AiPrefError.factory("BAD_PRICE",
|
package/lib/api-key.js
CHANGED
|
@@ -1,56 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.apiKey
|
|
4
|
-
*
|
|
3
|
+
* @module b.apiKey
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title API Keys
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* ownerId: "user-42",
|
|
14
|
-
* scopes: ["read:users", "write:posts"],
|
|
15
|
-
* metadata: { name: "Mobile app v3" },
|
|
16
|
-
* expiresAt: Date.now() + b.constants.TIME.days(90),
|
|
17
|
-
* });
|
|
18
|
-
* // issued.key — "bk_live_<idHex>_<secretHex>" (returned ONCE)
|
|
19
|
-
* // issued.id — "<idHex>"
|
|
20
|
-
*
|
|
21
|
-
* var record = await keys.verify(req.headers["x-api-key"]);
|
|
22
|
-
* // → { id, ownerId, scopes, metadata, ... } or null
|
|
23
|
-
*
|
|
24
|
-
* await keys.revoke(id);
|
|
25
|
-
* var rotated = await keys.rotate(id); // new secret; old stops working
|
|
26
|
-
* var owned = await keys.listForOwner("user-42");
|
|
27
|
-
*
|
|
28
|
-
* Token format (Stripe-style, prefix-recognizable):
|
|
29
|
-
*
|
|
30
|
-
* <prefix>_<namespace>_<idHex>_<secretHex>
|
|
31
|
-
*
|
|
32
|
-
* Example: `bk_live_5b9e7c8a4f2d1e3a_8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d`
|
|
7
|
+
* @intro
|
|
8
|
+
* Long-lived API token primitives — generate / verify / revoke /
|
|
9
|
+
* rotate; sealed at rest; per-key scope + rate-limit. Tokens are
|
|
10
|
+
* Stripe-style prefix-recognizable strings of the form
|
|
11
|
+
* `<prefix>_<namespace>_<idHex>_<secretHex>` so a leaked credential
|
|
12
|
+
* is identifiable on sight (secret-scanner allowlists, log-grep
|
|
13
|
+
* for `bk_live_`).
|
|
33
14
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* -
|
|
39
|
-
*
|
|
15
|
+
* Storage: framework table `_blamejs_api_keys` with sealed columns
|
|
16
|
+
* (ownerId / scopes / metadata via cryptoField), `ownerIdHash` for
|
|
17
|
+
* indexed `listForOwner`. Same dual-storage pattern as sessions —
|
|
18
|
+
* local SQLite in single-node mode, external-db in cluster mode,
|
|
19
|
+
* dispatched via cluster-storage. Hash algorithm is operator-
|
|
20
|
+
* selectable (SHAKE256 default for high-entropy random secrets;
|
|
21
|
+
* Argon2id available for low-entropy deployments). Visibility
|
|
22
|
+
* defaults are ON: `auditFailures`, `auditSuccess`, and
|
|
23
|
+
* `trackLastUsedAt` all default true so HIPAA §164.312(b) /
|
|
24
|
+
* PCI-DSS 10.2.1 / GDPR Art. 32 trails are complete out of the
|
|
25
|
+
* box. Operators with extreme verify-rate volume opt OUT
|
|
26
|
+
* explicitly.
|
|
40
27
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
28
|
+
* Graceful rotation moves the prior secret hash into a
|
|
29
|
+
* `secondarySecretHash` slot with a TTL (default 7 days) so
|
|
30
|
+
* in-flight clients survive the rotation window without coordinated
|
|
31
|
+
* redeploy.
|
|
45
32
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* - apiKey.create opts → throw at config time
|
|
49
|
-
* - registry.issue opts → throw ApiKeyError at call site
|
|
50
|
-
* - registry.rotate(id) on missing/revoked → throw ApiKeyError at call site
|
|
51
|
-
* - registry.verify(token) on any failure → return null (tolerant read)
|
|
52
|
-
* - registry.revoke(id) on missing → return false (tolerant read)
|
|
53
|
-
* - registry.getById(id) on missing → return null (tolerant read)
|
|
33
|
+
* @card
|
|
34
|
+
* Long-lived API token primitives — generate / verify / revoke / rotate; sealed at rest; per-key scope + rate-limit.
|
|
54
35
|
*/
|
|
55
36
|
|
|
56
37
|
var crypto = require("./crypto");
|
|
@@ -179,6 +160,30 @@ function _validateIssueOpts(opts) {
|
|
|
179
160
|
// Each part is alphanumeric so split-by-underscore is unambiguous as long
|
|
180
161
|
// as prefix/namespace are validated to contain no underscores. We verify
|
|
181
162
|
// that during create.
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @primitive b.apiKey.parseFormat
|
|
166
|
+
* @signature b.apiKey.parseFormat(token)
|
|
167
|
+
* @since 0.4.9
|
|
168
|
+
* @status stable
|
|
169
|
+
* @related b.apiKey.create
|
|
170
|
+
*
|
|
171
|
+
* Pure parser for the framework's `<prefix>_<namespace>_<idHex>_<secretHex>`
|
|
172
|
+
* token format. Returns `{ prefix, namespace, idHex, secretHex }` on
|
|
173
|
+
* a structurally-valid token, `null` otherwise. Never touches the
|
|
174
|
+
* registry — used by routing code that wants to dispatch a request
|
|
175
|
+
* to the correct registry (multi-namespace deployments) before
|
|
176
|
+
* calling `verify()`. Hex parts are not constant-time-compared here;
|
|
177
|
+
* that happens inside `verify()` against the stored hash.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* var parts = b.apiKey.parseFormat("bk_live_5b9e7c8a4f2d1e3a_8a7b6c5d4e3f2a1b");
|
|
181
|
+
* // → { prefix: "bk", namespace: "live", idHex: "5b9e7c8a4f2d1e3a",
|
|
182
|
+
* // secretHex: "8a7b6c5d4e3f2a1b" }
|
|
183
|
+
*
|
|
184
|
+
* b.apiKey.parseFormat("not-a-token"); // → null
|
|
185
|
+
* b.apiKey.parseFormat("bk_live_xyz_zzz"); // → null (non-hex)
|
|
186
|
+
*/
|
|
182
187
|
function parseFormat(token) {
|
|
183
188
|
if (typeof token !== "string" || token.length === 0) return null;
|
|
184
189
|
var parts = token.split("_");
|
|
@@ -209,6 +214,62 @@ function _sealForInsert(row) {
|
|
|
209
214
|
|
|
210
215
|
// ---- Registry factory ----
|
|
211
216
|
|
|
217
|
+
/**
|
|
218
|
+
* @primitive b.apiKey.create
|
|
219
|
+
* @signature b.apiKey.create(opts)
|
|
220
|
+
* @since 0.4.9
|
|
221
|
+
* @status stable
|
|
222
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
223
|
+
* @related b.apiKey.parseFormat, b.permissions.create, b.session
|
|
224
|
+
*
|
|
225
|
+
* Build an API-key registry bound to a single `namespace`. Returns a
|
|
226
|
+
* handle exposing async `issue` / `verify` / `revoke` / `rotate` /
|
|
227
|
+
* `listForOwner` / `getById` / `purgeExpired`. State changes
|
|
228
|
+
* (`issue` / `revoke` / `rotate` / `purgeExpired`) require leader in
|
|
229
|
+
* cluster mode; reads (`verify` / `getById` / `listForOwner`) run on
|
|
230
|
+
* any node. Issued tokens contain the secret material exactly once —
|
|
231
|
+
* the registry persists only the SHAKE256 / Argon2id hash and a
|
|
232
|
+
* scrub-safe record without secrets. Operators with multiple key
|
|
233
|
+
* lifecycles (e.g. `live` / `test`) instantiate one registry per
|
|
234
|
+
* namespace.
|
|
235
|
+
*
|
|
236
|
+
* @opts
|
|
237
|
+
* namespace: string, // registry namespace (required, no underscores / whitespace)
|
|
238
|
+
* prefix: string, // token prefix (default "bk", no underscores)
|
|
239
|
+
* idBytes: number, // bytes of id randomness (default 8 → 16 hex chars)
|
|
240
|
+
* secretBytes: number, // bytes of secret randomness (default 16 → 32 hex chars)
|
|
241
|
+
* trackLastUsedAt: boolean, // update lastUsedAt on verify success (default true)
|
|
242
|
+
* auditFailures: boolean, // emit verify-failure audits (default true)
|
|
243
|
+
* auditSuccess: boolean, // emit verify/list/get-success audits (default true)
|
|
244
|
+
* purgeAfterMs: number, // age threshold for purgeExpired (default 90 days)
|
|
245
|
+
* hashAlgo: string, // "shake256" (default) or "argon2id"
|
|
246
|
+
* audit: b.audit, // optional audit sink
|
|
247
|
+
* clock: function, // () → unix ms (test override)
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* var keys = b.apiKey.create({
|
|
251
|
+
* namespace: "live",
|
|
252
|
+
* audit: b.audit,
|
|
253
|
+
* });
|
|
254
|
+
*
|
|
255
|
+
* var issued = await keys.issue({
|
|
256
|
+
* ownerId: "user-42",
|
|
257
|
+
* scopes: ["read:users", "write:posts"],
|
|
258
|
+
* metadata: { name: "Mobile app v3" },
|
|
259
|
+
* expiresAt: Date.now() + b.constants.TIME.days(90),
|
|
260
|
+
* });
|
|
261
|
+
* // issued.key — "bk_live_5b9e7c8a4f2d1e3a_8a7b6c5d4e3f2a1b" (returned ONCE)
|
|
262
|
+
*
|
|
263
|
+
* var record = await keys.verify(req.headers["x-api-key"]);
|
|
264
|
+
* if (!record) return res.writeHead(401).end();
|
|
265
|
+
* // → { id, ownerId, scopes, metadata, lastUsedAt, ... }
|
|
266
|
+
*
|
|
267
|
+
* // Graceful rotation — old secret keeps working for 7 days:
|
|
268
|
+
* var rotated = await keys.rotate(issued.id, { graceful: true });
|
|
269
|
+
*
|
|
270
|
+
* await keys.revoke(issued.id); // immediate cutover
|
|
271
|
+
* var owned = await keys.listForOwner("user-42");
|
|
272
|
+
*/
|
|
212
273
|
function create(opts) {
|
|
213
274
|
opts = opts || {};
|
|
214
275
|
validateOpts(opts, [
|
package/lib/api-snapshot.js
CHANGED
|
@@ -1,52 +1,53 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.apiSnapshot
|
|
4
|
+
* @nav Other
|
|
5
|
+
* @title API Snapshot
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Public-API surface walker plus breaking-change detector — the
|
|
9
|
+
* framework's LTS-contract enforcement at the type level. Operators
|
|
10
|
+
* capture a snapshot of the framework's `module.exports` tree,
|
|
11
|
+
* commit it alongside the version bump, and the release workflow's
|
|
12
|
+
* `check-api-snapshot.js` gate fails CI when any subsequent change
|
|
13
|
+
* removes or retypes a previously-shipped public member.
|
|
8
14
|
*
|
|
9
|
-
*
|
|
10
|
-
* (BREAKING — fails CI)
|
|
11
|
-
* - typeChanged a member's category flipped (function → object, etc.)
|
|
12
|
-
* (BREAKING — fails CI)
|
|
13
|
-
* - added a new member that wasn't in the old snapshot
|
|
14
|
-
* (ADDITIVE — does not fail; signals the snapshot is
|
|
15
|
-
* out-of-date and the operator should rerun capture)
|
|
15
|
+
* Three diff classes:
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* - `removed` a member present in the old snapshot but not
|
|
18
|
+
* the new (BREAKING — fails CI)
|
|
19
|
+
* - `typeChanged` a member's category flipped, e.g. function →
|
|
20
|
+
* object, primitive → instance (BREAKING — fails
|
|
21
|
+
* CI)
|
|
22
|
+
* - `additive` a new member that wasn't in the old snapshot
|
|
23
|
+
* (informational — signals the snapshot is out
|
|
24
|
+
* of date and the operator should rerun capture)
|
|
20
25
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
26
|
+
* Walker rules: functions record as
|
|
27
|
+
* `{ type: "function", arity: fn.length }`; plain objects recurse
|
|
28
|
+
* into enumerable string keys; primitives record as
|
|
29
|
+
* `{ type: "primitive", valueType }` without capturing the literal
|
|
30
|
+
* value (so a version-string change in `b.version` doesn't fail
|
|
31
|
+
* CI); non-plain objects (Map, Set, Buffer, Date, RegExp, Error
|
|
32
|
+
* instances) record as `{ type: "instance", ctorName }` without
|
|
33
|
+
* recursion; cycles short-circuit as `{ type: "cycle" }`; depth is
|
|
34
|
+
* capped at `opts.maxDepth` (default 8). Members whose key starts
|
|
35
|
+
* with `_` are skipped — the framework convention for test seams
|
|
36
|
+
* and internal helpers.
|
|
23
37
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
38
|
+
* Function-arity changes: a DECREASE in `fn.length` is breaking
|
|
39
|
+
* (the operator removed a required parameter). An INCREASE is not
|
|
40
|
+
* flagged because adding an optional trailing parameter is additive
|
|
41
|
+
* to existing callers.
|
|
28
42
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
43
|
+
* On-disk format: stable canonical JSON ordered as
|
|
44
|
+
* `{ version, frameworkVersion, createdAt, exports }`. The format
|
|
45
|
+
* version (`b.apiSnapshot.SNAPSHOT_FORMAT_VERSION`) is checked on
|
|
46
|
+
* read so a future schema bump can't silently mis-compare against
|
|
47
|
+
* an older baseline.
|
|
33
48
|
*
|
|
34
|
-
*
|
|
35
|
-
* -
|
|
36
|
-
* Class constructors are still 'function' — recursive scope walks
|
|
37
|
-
* prototype only when the operator explicitly opts in via
|
|
38
|
-
* opts.includeClassPrototypes.
|
|
39
|
-
* - Plain objects recurse into their own enumerable string keys.
|
|
40
|
-
* - Primitives (string, number, boolean, null, undefined) record as
|
|
41
|
-
* { type: 'primitive', valueType: typeof v }. Specific values are
|
|
42
|
-
* NOT captured — only the type — so a version-string change in
|
|
43
|
-
* constants doesn't fail CI.
|
|
44
|
-
* - Members whose key starts with '_' are skipped (test seams,
|
|
45
|
-
* internal helpers).
|
|
46
|
-
* - Cycles are detected and short-circuit as { type: 'cycle' }.
|
|
47
|
-
* - Non-plain objects (Map, Set, Buffer, Date, RegExp, Error, etc.)
|
|
48
|
-
* are recorded as { type: 'instance', constructor: name } without
|
|
49
|
-
* recursion — they're terminal nodes.
|
|
49
|
+
* @card
|
|
50
|
+
* Public-API surface walker plus breaking-change detector — the framework's LTS-contract enforcement at the type level.
|
|
50
51
|
*/
|
|
51
52
|
|
|
52
53
|
var fs = require("fs");
|
|
@@ -112,6 +113,34 @@ function _walkNode(value, depth, maxDepth, seen, skipUnderscore) {
|
|
|
112
113
|
return { type: "object", members: members };
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @primitive b.apiSnapshot.capture
|
|
118
|
+
* @signature b.apiSnapshot.capture(target, opts)
|
|
119
|
+
* @since 0.1.91
|
|
120
|
+
* @status stable
|
|
121
|
+
* @related b.apiSnapshot.compare, b.apiSnapshot.write
|
|
122
|
+
*
|
|
123
|
+
* Walk a module's exports tree and produce a snapshot object
|
|
124
|
+
* `{ version, frameworkVersion, createdAt, exports }` suitable for
|
|
125
|
+
* round-tripping through `write` / `read`. The walk is recursive
|
|
126
|
+
* with cycle detection and a depth cap; underscore-prefixed keys
|
|
127
|
+
* are skipped by default (override with `skipUnderscore: false`).
|
|
128
|
+
* Throws `ApiSnapshotError` when the top-level target is not a plain
|
|
129
|
+
* object — class instances and runtime-built exports can't be walked
|
|
130
|
+
* by category.
|
|
131
|
+
*
|
|
132
|
+
* @opts
|
|
133
|
+
* maxDepth: 8, // recursion ceiling
|
|
134
|
+
* skipUnderscore: true, // skip `_internal` keys
|
|
135
|
+
* frameworkVersion: "0.8.48", // override target.version
|
|
136
|
+
* createdAt: "2026-05-09T...", // pin for deterministic snapshots
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* var snap = b.apiSnapshot.capture(require("@blamejs/core"));
|
|
140
|
+
* // → { version: 1, frameworkVersion: "0.8.48",
|
|
141
|
+
* // createdAt: "2026-05-09T12:00:00.000Z",
|
|
142
|
+
* // exports: { uuid: { type: "object", members: {...} }, ... } }
|
|
143
|
+
*/
|
|
115
144
|
function capture(target, opts) {
|
|
116
145
|
opts = opts || {};
|
|
117
146
|
if (!target || typeof target !== "object") {
|
|
@@ -135,6 +164,25 @@ function capture(target, opts) {
|
|
|
135
164
|
};
|
|
136
165
|
}
|
|
137
166
|
|
|
167
|
+
/**
|
|
168
|
+
* @primitive b.apiSnapshot.write
|
|
169
|
+
* @signature b.apiSnapshot.write(snapshot, filePath)
|
|
170
|
+
* @since 0.1.91
|
|
171
|
+
* @status stable
|
|
172
|
+
* @related b.apiSnapshot.read, b.apiSnapshot.capture
|
|
173
|
+
*
|
|
174
|
+
* Serialize a snapshot to disk in canonical JSON form (stable
|
|
175
|
+
* `{ version, frameworkVersion, createdAt, exports }` ordering, mode
|
|
176
|
+
* 0o644). Returns the filePath written. Throws `ApiSnapshotError`
|
|
177
|
+
* when the snapshot or path is missing — the release workflow
|
|
178
|
+
* surfaces typos at commit time instead of writing to an unintended
|
|
179
|
+
* location.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* var snap = b.apiSnapshot.capture(require("@blamejs/core"));
|
|
183
|
+
* var written = b.apiSnapshot.write(snap, "./api-snapshot.json");
|
|
184
|
+
* // → "./api-snapshot.json"
|
|
185
|
+
*/
|
|
138
186
|
function write(snapshot, filePath) {
|
|
139
187
|
if (!snapshot || typeof snapshot !== "object") {
|
|
140
188
|
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
@@ -155,6 +203,28 @@ function write(snapshot, filePath) {
|
|
|
155
203
|
return filePath;
|
|
156
204
|
}
|
|
157
205
|
|
|
206
|
+
/**
|
|
207
|
+
* @primitive b.apiSnapshot.read
|
|
208
|
+
* @signature b.apiSnapshot.read(filePath)
|
|
209
|
+
* @since 0.1.91
|
|
210
|
+
* @status stable
|
|
211
|
+
* @related b.apiSnapshot.write, b.apiSnapshot.compare
|
|
212
|
+
*
|
|
213
|
+
* Load a snapshot from disk and validate its envelope. Throws
|
|
214
|
+
* `ApiSnapshotError` with a specific code on each failure mode —
|
|
215
|
+
* `api-snapshot/missing` (no file), `api-snapshot/read-failed`
|
|
216
|
+
* (I/O error), `api-snapshot/bad-json` (parse failure), or
|
|
217
|
+
* `api-snapshot/bad-version` (format-version mismatch — the
|
|
218
|
+
* baseline was written by a different snapshot major) — so the
|
|
219
|
+
* release workflow can surface a precise reason instead of a
|
|
220
|
+
* generic "snapshot broken" message.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* var loaded = b.apiSnapshot.read("./api-snapshot.json");
|
|
224
|
+
* var current = b.apiSnapshot.capture(require("@blamejs/core"));
|
|
225
|
+
* var diff = b.apiSnapshot.compare(loaded, current);
|
|
226
|
+
* // → { breaking: [], additive: [], typeChanged: [] }
|
|
227
|
+
*/
|
|
158
228
|
function read(filePath) {
|
|
159
229
|
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
160
230
|
throw new ApiSnapshotError("api-snapshot/bad-path",
|
|
@@ -277,6 +347,31 @@ function _walkCompare(oldNode, newNode, prefix, breaking, additive, typeChanged)
|
|
|
277
347
|
// cycle / deep — terminal, nothing more to compare
|
|
278
348
|
}
|
|
279
349
|
|
|
350
|
+
/**
|
|
351
|
+
* @primitive b.apiSnapshot.compare
|
|
352
|
+
* @signature b.apiSnapshot.compare(oldSnapshot, newSnapshot)
|
|
353
|
+
* @since 0.1.91
|
|
354
|
+
* @status stable
|
|
355
|
+
* @related b.apiSnapshot.formatDiff, b.apiSnapshot.capture
|
|
356
|
+
*
|
|
357
|
+
* Diff two snapshots and return
|
|
358
|
+
* `{ breaking, additive, typeChanged }`. `breaking` carries every
|
|
359
|
+
* member that was removed, retyped, lost arity, swapped its
|
|
360
|
+
* constructor name, or changed primitive `valueType`; the release
|
|
361
|
+
* workflow exits non-zero when this list is non-empty. `additive`
|
|
362
|
+
* lists new members (informational — operator should rerun
|
|
363
|
+
* `capture` and commit the refreshed baseline). `typeChanged` is a
|
|
364
|
+
* subset of `breaking` surfaced separately for easier triage.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* var loaded = b.apiSnapshot.read("./api-snapshot.json");
|
|
368
|
+
* var current = b.apiSnapshot.capture(require("@blamejs/core"));
|
|
369
|
+
* var diff = b.apiSnapshot.compare(loaded, current);
|
|
370
|
+
* if (diff.breaking.length > 0) {
|
|
371
|
+
* console.error(b.apiSnapshot.formatDiff(diff));
|
|
372
|
+
* process.exit(1);
|
|
373
|
+
* }
|
|
374
|
+
*/
|
|
280
375
|
function compare(oldSnapshot, newSnapshot) {
|
|
281
376
|
if (!oldSnapshot || !oldSnapshot.exports) {
|
|
282
377
|
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
@@ -298,6 +393,28 @@ function compare(oldSnapshot, newSnapshot) {
|
|
|
298
393
|
return { breaking: breaking, additive: additive, typeChanged: typeChanged };
|
|
299
394
|
}
|
|
300
395
|
|
|
396
|
+
/**
|
|
397
|
+
* @primitive b.apiSnapshot.formatDiff
|
|
398
|
+
* @signature b.apiSnapshot.formatDiff(diff)
|
|
399
|
+
* @since 0.1.91
|
|
400
|
+
* @status stable
|
|
401
|
+
* @related b.apiSnapshot.compare
|
|
402
|
+
*
|
|
403
|
+
* Render a diff result from `compare` into a human-readable
|
|
404
|
+
* multi-line string suitable for `console.error` in a CI script.
|
|
405
|
+
* Breaking entries are flagged with `-`, additive entries with `+`,
|
|
406
|
+
* and the `was` / `is` types are JSON-quoted so the operator can
|
|
407
|
+
* paste the line verbatim into the migration notes.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* var diff = {
|
|
411
|
+
* breaking: [{ path: "uuid.v3", kind: "removed", was: "function" }],
|
|
412
|
+
* additive: [{ path: "uuid.v8", type: "function" }],
|
|
413
|
+
* typeChanged: [],
|
|
414
|
+
* };
|
|
415
|
+
* var rendered = b.apiSnapshot.formatDiff(diff);
|
|
416
|
+
* // → "[api-snapshot] BREAKING (1):\n - uuid.v3 (removed) was=\"function\"\n..."
|
|
417
|
+
*/
|
|
301
418
|
function formatDiff(diff) {
|
|
302
419
|
if (!diff || typeof diff !== "object") {
|
|
303
420
|
throw new ApiSnapshotError("api-snapshot/bad-diff",
|