@blamejs/core 0.12.29 → 0.12.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/index.js +1 -0
- package/lib/auth/jar.js +168 -0
- package/lib/backup/index.js +96 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.31 (2026-05-24) — **`b.auth.jar.parse` — verify RFC 9101 JWT-Secured Authorization Requests (server side).** A plain OAuth authorization request carries its parameters in the URL query string, where a browser, proxy, or referer log can tamper with or leak them. RFC 9101 JAR packs those parameters into a JWT the client signs — the request object — so the authorization server can confirm they arrived exactly as sent. `b.auth.jar.parse(jar, opts)` is the server-side verifier and the request-side counterpart to the existing JARM response handling (`b.auth.oauth.parseJarmResponse`). It delegates the signature check to `b.auth.jwt.verifyExternal` — which already enforces a mandatory `algorithms` allowlist and refuses the alg-confusion (`alg: "none"`, HMAC-vs-RSA) and JWE-on-a-JWS-verifier shapes against a JWKS public-key trust source — then pins `iss` and the `client_id` claim to the expected client, pins `aud` to this server's issuer identifier, refuses a nested `request` / `request_uri` (RFC 9101 §6.3 recursion / confused-deputy vector), and returns the authorization parameters with the JWT envelope claims stripped. **Added:** *`b.auth.jar.parse(jar, opts)` — request-object verification* — `opts.clientId` (the expected client — pins `iss` + the `client_id` claim), `opts.audience` (this server's issuer identifier — pins `aud`), `opts.algorithms` (required signature allowlist — no defaults, the alg-confusion defense), and one of `opts.jwks` / `opts.jwksUri` / `opts.keyResolver` (the client's verification key). Returns `{ params, claims }` where `params` is the authorization parameters (`response_type`, `redirect_uri`, `scope`, `state`, `nonce`, …) with the JWT envelope claims (`iss`, `aud`, `exp`, `iat`, `nbf`, `jti`) removed. A request object whose `client_id` claim disagrees with `opts.clientId`, or that nests a `request` / `request_uri`, is refused. Emitting a request object (the client side) is deferred-with-condition: it requires signing with the client's key under a classical JWS algorithm, and the framework's own JWT signer is PQC-only for the tokens it issues — a PQC-signed request object would not interoperate with a standard authorization server; client-side emission re-opens when a classical JWS signer lands or operators surface the need. Until then clients sign request objects with their existing JOSE tooling.
|
|
12
|
+
|
|
13
|
+
- v0.12.30 (2026-05-24) — **`bundleAdapterStorage.keyRotation(opts)` — verified whole-repository envelope key rotation.** Rotating the key that wraps a backup repository is only safe if you can prove every bundle still reads under the new key — a rotation that silently corrupts one bundle is a time-bomb the operator discovers at restore time, exactly when they can least afford it. `storage.keyRotation(opts)` rotates every bundle's envelope from the old key to the new key (composing `rewrapAllBundles`) and then re-reads every bundle under the NEW key (composing `verifyAllBundles`), so a bad rotation surfaces as `verifyFailed > 0` immediately instead of at restore. It emits a `backup/key-rotated` audit event with the rotation id + per-status counts — a key-rotation event is a compliance record (SOC 2 CC6.1, PCI DSS 3.6.4) operators wire into their signed audit chain. Works for both `recipient` (hybrid PQC envelope) and `passphrase` (Argon2id) storage; refused cleanly on plaintext (`cryptoStrategy: "none"`) storage and when the new key is missing. **Added:** *`bundleAdapterStorage.keyRotation(opts)` — rotate then prove* — `opts.newRecipient` / `opts.newPassphrase` is the key bundles rotate TO (matched to the storage's `cryptoStrategy`); `opts.oldRecipient` / `opts.oldPassphrase` unwraps the current envelope when it differs from the configured key. Returns `{ rotationId, rotatedAt, total, rotated, skipped, failed, verified, verifyFailed, rotateResults, verifyResults }`. `opts.verify` (default true) runs the post-rotation read-back under the new key; `opts.concurrency` / `opts.stopOnFirstFailure` forward to the batch passes. Plaintext bundles + non-wrappable formats are skipped cleanly; a rotation that leaves any bundle unreadable reports `verifyFailed > 0` and emits the audit event with `outcome: "failure"`. A true overlap window where BOTH the old and new key decrypt a bundle (`dualWrap: true`) is refused with `backup/dual-wrap-unsupported` — it needs multi-recipient archive envelopes `b.archive.wrap` does not yet emit, and re-opens when the wrap layer gains them; until then stage a rotation by keeping the old key available to readers until `keyRotation` reports `failed: 0` + `verifyFailed: 0`, then retire it.
|
|
14
|
+
|
|
11
15
|
- v0.12.29 (2026-05-24) — **`b.ai.dp` — float-safe differential privacy: snapping-mechanism Laplace + discrete Gaussian + Rényi-DP budgets.** Differential privacy adds calibrated noise so an aggregate is provably insensitive to any single record — but the guarantee is fragile: Mironov (2012) showed that a Laplace mechanism sampled with naive double-precision floats lets an attacker distinguish neighbouring datasets with > 35% probability from a single output, silently destroying the promise. `b.ai.dp` ships only mechanisms whose sampling is hardened against that attack class: Laplace via the snapping mechanism (clamp + CSPRNG sign + full-mantissa uniform + power-of-two-grid rounding) and the discrete Gaussian (Canonne–Kamath–Steinke 2020) via integer-exact rejection sampling built from Bernoulli(exp(−γ)) over exact rationals — no floating-point noise at all. All randomness comes from `b.crypto.generateBytes` (SHAKE256 over the OS CSPRNG), never `Math.random`. `b.ai.dp.budget({ scope, epsilon, delta })` tracks a privacy budget per scope and refuses a `consume` that would exceed it, accounting composition either by basic summation (default) or a Rényi-DP accountant (Mironov 2017) for a much tighter bound under repeated Gaussian releases. NIST SP 800-226 (2025) is the evaluation standard; Dwork & Roth is the canonical reference. The exponential and sparse-vector mechanisms are deferred-with-condition — their float-safe constructions (base-2 / permute-and-flip; snapped SVT) re-open on operator demand, since shipping them float-unsafe would defeat the module's purpose. **Added:** *`b.ai.dp.mechanism({ type, sensitivity, epsilon, ... })` — float-safe noise mechanisms* — `type: "laplace"` is the snapping mechanism (pure ε-DP, real-valued, requires a clamp `bound` the guarantee depends on); `type: "gaussian"` is the discrete Gaussian (integer-valued, (ε, δ)-DP, requires `delta`). The Gaussian uses the classic calibration σ = √(2 ln(1.25/δ))·Δ/ε, proven for ε ≤ 1 — larger ε is refused with a pointer to splitting the release under an rdp budget. Descriptors are validated + frozen at construction so a malformed parameter fails fast. · *`b.ai.dp.budget({ scope, epsilon, delta, accounting })` — per-scope privacy budget* — Returns `{ consume, remaining, spent, reset }`. `consume(mechanism, value)` adds the mechanism's noise, charges the accountant, and throws `aiDp/budget-exhausted` if the release would push the scope past its (ε, δ). `accounting: "basic"` (default) sums per-release ε and δ; `accounting: "rdp"` runs a Rényi-DP accountant across a grid of orders and converts to (ε, δ) at the scope's δ for a tight composition bound under repeated Gaussian releases (requires `delta > 0`). The scope budget is enforced on both ε and δ independently. **Security:** *`b.crypto.generateBytes` uniformity fix at 1-byte length* — Node's SHAKE256 XOF is non-uniform at `outputLength: 1` — the byte values 0x00 and 0xff never occur and the low bit skews to ~0.54. `b.crypto.generateBytes(1)` (and the underlying `random(1)`) now draws at least 2 bytes and slices, so a single-byte CSPRNG request is uniform. Surfaced by `b.ai.dp` per-byte noise sampling; any per-byte consumer of `generateBytes` inherits the fix. A regression test asserts 0x00 / 0xff occur and the low bit is balanced.
|
|
12
16
|
|
|
13
17
|
- v0.12.28 (2026-05-24) — **`b.ai.capability` — model-capability registry + cheapest-satisfying-model router.** `b.ai.capability.create({ models })` turns a fleet of AI model descriptors into a routing decision: given a set of requirements (context window, input/output modalities, tool use, structured output, reasoning tier, citation support, prompt-caching size), it picks the cheapest model that satisfies all of them. NIST AI RMF (AI 100-1) MAP 2.x requires documenting each model's capabilities and limitations; the Model Cards convention (Mitchell et al., 2019) formalizes that descriptor — this primitive makes the descriptor actionable. Routing to the cheapest sufficient model is a front-line defense against over-provisioning spend and composes directly with `b.ai.quota`'s `cost-usd` dimension (the chosen descriptor's rate feeds the budget charge); refusing to route a request to a model that cannot satisfy it (missing modality, too-small context window, no tool use) catches a capability mismatch before the inference call burns tokens on a guaranteed-bad result. Cost ranking uses a supplied `costBasis` (`{ inputTokens, outputTokens }`) for real per-call spend, else the sum of the per-1k rates; ties break by model id so the choice is deterministic across calls and nodes. **Added:** *`b.ai.capability.create({ models })` — capability registry + router* — Returns `{ describe, list, register, satisfies, route }`. A descriptor carries `maxContextTokens`, `maxOutputTokens`, `modalitiesIn` / `modalitiesOut` (arrays), `toolUse`, `structuredOutput`, `fineTunable`, `reasoningTier` (`none` / `basic` / `standard` / `advanced`, ordered), `citationSupport`, `promptCachingMaxTokens`, and the cost rates `costPer1kInputTokens` / `costPer1kOutputTokens`. Descriptors are validated + frozen at registration so a typo (negative cost, unknown reasoning tier, non-array modality list) surfaces at config time rather than as a silent mis-route. `describe(modelId)` returns the frozen descriptor; `register(modelId, descriptor)` adds or replaces one at runtime. · *`route({ requirements, fallback?, costBasis? })` — cheapest-satisfying selection* — Collects every model whose descriptor satisfies all requirements, then returns the cheapest (`{ modelId, descriptor, estimatedCost, reason }`). Requirements: `minContextTokens`, `minOutputTokens`, `modalitiesIn` / `modalitiesOut` (model must support every listed modality), `toolUse`, `structuredOutput`, `fineTunable`, `minReasoningTier` (tier ordering — `standard` is met by `standard` or `advanced`), `citationSupport`, `minPromptCachingTokens`. When no model matches, `fallback` (a registered model id) is returned with `reason: "fallback"`, or the call refuses with `aiCapability/no-candidate` if no fallback was supplied. Routing decisions emit `ai/capability-routed` / `ai/capability-fallback` / `ai/capability-no-candidate` through the drop-silent audit chain. · *`satisfies(modelId, requirements)` — precise capability-mismatch reasons* — Returns `{ ok, failures }` where each failure names the `requirement`, the `need`, and what the model `have`s — so a caller surfaces a precise reason (e.g. `minReasoningTier need advanced have basic`) instead of a bare boolean. Use it to explain a routing miss or to gate a request against a specific model before calling it.
|
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
75
75
|
- RP-Initiated / Front-Channel / Back-Channel Logout 1.0 (`parseFrontchannelLogoutRequest` + `verifyBackchannelLogoutToken` with jti-replay defense)
|
|
76
76
|
- RFC 9207 AS Issuer Identifier validation on callbacks (`parseCallback` — refuses iss mismatch + OP `error=` redirect)
|
|
77
77
|
- OAuth 2.0 JARM signed-response decode (`parseJarmResponse`)
|
|
78
|
+
- RFC 9101 JWT-Secured Authorization Request verification — server-side request-object parse with mandatory alg allowlist + iss/client_id/aud binding + anti-nesting (`b.auth.jar.parse`)
|
|
78
79
|
- One-time-use refresh-token rotation with operator-supplied replay-defense callback (RFC 9700 §4.13 / OAuth 2.1 §6.1 — `refreshAccessToken({ seen })`)
|
|
79
80
|
- **Federation / VC** — CIBA Core 1.0 (`b.auth.ciba`, poll/ping/push); OpenID Federation 1.0 trust chain + metadata_policy (`b.auth.openidFederation`); SAML 2.0 SP with XMLDSig signature-wrapping defense + RFC 9525 server-identity (`b.auth.saml`); OpenID4VCI 1.0 issuer (`b.auth.oid4vci`); OpenID4VP 1.0 verifier with DCQL (`b.auth.oid4vp`); SD-JWT VC with `key_attestation` extension (`b.auth.sdJwtVc`)
|
|
80
81
|
- **Sessions** — `b.session`
|
package/index.js
CHANGED
|
@@ -243,6 +243,7 @@ var auth = {
|
|
|
243
243
|
require("./lib/auth/jwt"),
|
|
244
244
|
{ verifyExternal: require("./lib/auth/jwt-external").verifyExternal }),
|
|
245
245
|
oauth: require("./lib/auth/oauth"),
|
|
246
|
+
jar: require("./lib/auth/jar"),
|
|
246
247
|
lockout: require("./lib/auth/lockout"),
|
|
247
248
|
dpop: require("./lib/auth/dpop"),
|
|
248
249
|
aal: require("./lib/auth/aal"),
|
package/lib/auth/jar.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.auth.jar
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title JWT-Secured Authorization Request (JAR)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* RFC 9101 JWT-Secured Authorization Request — the authorization-
|
|
9
|
+
* server side of the request object, the counterpart to the JARM
|
|
10
|
+
* response handling in <code>b.auth.oauth</code>. A plain OAuth
|
|
11
|
+
* authorization request passes its parameters as URL query string,
|
|
12
|
+
* where they can be tampered with in the browser or leaked into
|
|
13
|
+
* proxy / referer logs. JAR packs the parameters into a JWT signed
|
|
14
|
+
* by the client (the "request object") so the authorization server
|
|
15
|
+
* can verify they arrived exactly as the client sent them.
|
|
16
|
+
*
|
|
17
|
+
* <code>b.auth.jar.parse(jar, opts)</code> verifies an incoming
|
|
18
|
+
* request object: the signature is checked through
|
|
19
|
+
* <code>b.auth.jwt.verifyExternal</code> (mandatory <code>algorithms</code>
|
|
20
|
+
* allowlist — no <code>alg: "none"</code>, no HMAC-vs-RSA confusion,
|
|
21
|
+
* no JWE-on-a-JWS-verifier), <code>iss</code> is pinned to the
|
|
22
|
+
* expected <code>clientId</code>, <code>aud</code> to this server's
|
|
23
|
+
* issuer identifier, the request object's <code>client_id</code>
|
|
24
|
+
* claim must match the client, and the authorization parameters are
|
|
25
|
+
* returned with the JWT envelope claims stripped.
|
|
26
|
+
*
|
|
27
|
+
* <strong>Anti-nesting (RFC 9101 §6.3):</strong> a request object
|
|
28
|
+
* may not itself carry a <code>request</code> or <code>request_uri</code>
|
|
29
|
+
* parameter — <code>parse</code> refuses it, closing the recursion /
|
|
30
|
+
* confused-deputy vector.
|
|
31
|
+
*
|
|
32
|
+
* The signature verification — the security-critical step — is
|
|
33
|
+
* delegated to <code>verifyExternal</code>, which already enforces
|
|
34
|
+
* the alg allowlist and refuses the alg-confusion / JWE-bypass
|
|
35
|
+
* shapes against a JWKS public-key trust source. JAR adds the
|
|
36
|
+
* request-object-specific bindings on top.
|
|
37
|
+
*
|
|
38
|
+
* <strong>Emitting</strong> a request object (the client side) is
|
|
39
|
+
* deferred-with-condition: it requires signing with the client's
|
|
40
|
+
* key under a classical JWS algorithm (RS256 / ES256 / EdDSA), and
|
|
41
|
+
* the framework's own JWT signer (<code>b.auth.jwt.sign</code>) is
|
|
42
|
+
* PQC-only (ML-DSA / SLH-DSA) for the tokens the framework itself
|
|
43
|
+
* issues — a PQC-signed request object would not interoperate with
|
|
44
|
+
* any standard authorization server today. blamejs sits on the
|
|
45
|
+
* authorization-server side here (it verifies client request
|
|
46
|
+
* objects); client-side emission re-opens when a classical
|
|
47
|
+
* <code>b.auth.jws.sign</code> primitive lands or operators surface
|
|
48
|
+
* the need. Until then clients sign their request objects with
|
|
49
|
+
* their existing JOSE tooling.
|
|
50
|
+
*
|
|
51
|
+
* @card
|
|
52
|
+
* RFC 9101 JWT-Secured Authorization Request (server side) — verify
|
|
53
|
+
* the OAuth request object with mandatory alg allowlist, iss +
|
|
54
|
+
* client_id binding, audience pinning, and anti-nesting.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
var jwtExternal = require("./jwt-external");
|
|
58
|
+
var validateOpts = require("../validate-opts");
|
|
59
|
+
var { defineClass } = require("../framework-error");
|
|
60
|
+
|
|
61
|
+
var AuthJarError = defineClass("AuthJarError", { alwaysPermanent: true });
|
|
62
|
+
|
|
63
|
+
var JAR_TYP = "oauth-authz-req+jwt";
|
|
64
|
+
|
|
65
|
+
// JWT-standard claims that are request-object envelope metadata, not
|
|
66
|
+
// OAuth authorization parameters — stripped from the returned params.
|
|
67
|
+
var ENVELOPE_CLAIMS = ["iss", "aud", "exp", "iat", "nbf", "jti"];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @primitive b.auth.jar.parse
|
|
71
|
+
* @signature b.auth.jar.parse(jar, opts)
|
|
72
|
+
* @since 0.12.31
|
|
73
|
+
* @status stable
|
|
74
|
+
* @compliance soc2
|
|
75
|
+
* @related b.auth.oauth.parseJarmResponse
|
|
76
|
+
*
|
|
77
|
+
* Verify an RFC 9101 request object and return its authorization
|
|
78
|
+
* parameters. The signature is checked via
|
|
79
|
+
* <code>b.auth.jwt.verifyExternal</code> (mandatory <code>algorithms</code>
|
|
80
|
+
* allowlist), <code>iss</code> is pinned to <code>opts.clientId</code>,
|
|
81
|
+
* <code>aud</code> to <code>opts.audience</code>, and the request
|
|
82
|
+
* object's <code>client_id</code> claim must equal
|
|
83
|
+
* <code>opts.clientId</code>. A request object carrying a nested
|
|
84
|
+
* <code>request</code> / <code>request_uri</code> is refused
|
|
85
|
+
* (RFC 9101 §6.3). Returns <code>{ params, claims }</code> where
|
|
86
|
+
* <code>params</code> is the authorization parameters with the JWT
|
|
87
|
+
* envelope claims removed.
|
|
88
|
+
*
|
|
89
|
+
* @opts
|
|
90
|
+
* {
|
|
91
|
+
* clientId: string, // required — expected client (iss + client_id pin)
|
|
92
|
+
* audience: string, // required — this server's issuer identifier (aud pin)
|
|
93
|
+
* algorithms: string[], // required — accepted signature algorithms (allowlist)
|
|
94
|
+
* jwks?: object, // one of jwks / jwksUri / keyResolver (the client's key)
|
|
95
|
+
* jwksUri?: string,
|
|
96
|
+
* keyResolver?: function,
|
|
97
|
+
* clockSkewMs?: number,
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* var out = await b.auth.jar.parse(jar, {
|
|
102
|
+
* clientId: "s6BhdRkqt3",
|
|
103
|
+
* audience: "https://as.example.com",
|
|
104
|
+
* algorithms: ["ES256"],
|
|
105
|
+
* jwks: clientJwks,
|
|
106
|
+
* });
|
|
107
|
+
* // → { params: { response_type: "code", redirect_uri: "...", ... }, claims: {...} }
|
|
108
|
+
*/
|
|
109
|
+
async function parse(jar, opts) {
|
|
110
|
+
if (typeof jar !== "string" || jar.length === 0) {
|
|
111
|
+
throw new AuthJarError("auth-jar/no-jar", "jar.parse: jar must be a non-empty string");
|
|
112
|
+
}
|
|
113
|
+
validateOpts.requireObject(opts, "jar.parse", AuthJarError);
|
|
114
|
+
validateOpts(opts, [
|
|
115
|
+
"clientId", "audience", "algorithms", "jwks", "jwksUri", "keyResolver", "clockSkewMs",
|
|
116
|
+
], "jar.parse");
|
|
117
|
+
validateOpts.requireNonEmptyString(opts.clientId, "jar.parse: clientId", AuthJarError, "auth-jar/bad-client-id");
|
|
118
|
+
validateOpts.requireNonEmptyString(opts.audience, "jar.parse: audience", AuthJarError, "auth-jar/bad-audience");
|
|
119
|
+
|
|
120
|
+
// Delegate signature + alg-allowlist + iss/aud/exp verification to
|
|
121
|
+
// verifyExternal (the hardened JWS verifier). It throws on alg
|
|
122
|
+
// confusion / none / JWE / bad signature / iss / aud / expiry and
|
|
123
|
+
// returns `{ header, claims }`.
|
|
124
|
+
var verified = await jwtExternal.verifyExternal(jar, {
|
|
125
|
+
algorithms: opts.algorithms,
|
|
126
|
+
jwks: opts.jwks,
|
|
127
|
+
jwksUri: opts.jwksUri,
|
|
128
|
+
keyResolver: opts.keyResolver,
|
|
129
|
+
issuer: opts.clientId,
|
|
130
|
+
audience: opts.audience,
|
|
131
|
+
clockSkewMs: opts.clockSkewMs,
|
|
132
|
+
});
|
|
133
|
+
var payload = verified.claims;
|
|
134
|
+
|
|
135
|
+
// RFC 9101 §5.2 — the request object MUST carry a client_id claim,
|
|
136
|
+
// and it MUST match the client. verifyExternal already pinned
|
|
137
|
+
// iss === clientId, but client_id is a distinct REQUIRED claim;
|
|
138
|
+
// accepting its absence would let a JAR pass on the strength of an
|
|
139
|
+
// outer (attacker-controllable) query-param client_id alone, so a
|
|
140
|
+
// missing client_id is refused rather than waved through.
|
|
141
|
+
if (payload.client_id === undefined) {
|
|
142
|
+
throw new AuthJarError("auth-jar/missing-client-id",
|
|
143
|
+
"jar.parse: request object is missing the required client_id claim (RFC 9101 §5.2)");
|
|
144
|
+
}
|
|
145
|
+
if (payload.client_id !== opts.clientId) {
|
|
146
|
+
throw new AuthJarError("auth-jar/client-id-mismatch",
|
|
147
|
+
"jar.parse: request object client_id does not match the expected client");
|
|
148
|
+
}
|
|
149
|
+
// RFC 9101 §6.3 — a request object must not nest another request /
|
|
150
|
+
// request_uri (recursion / confused-deputy vector).
|
|
151
|
+
if (payload.request !== undefined || payload.request_uri !== undefined) {
|
|
152
|
+
throw new AuthJarError("auth-jar/nested-request",
|
|
153
|
+
"jar.parse: request object must not carry `request` or `request_uri` (RFC 9101 §6.3)");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
var params = {};
|
|
157
|
+
var keys = Object.keys(payload);
|
|
158
|
+
for (var i = 0; i < keys.length; i++) {
|
|
159
|
+
if (ENVELOPE_CLAIMS.indexOf(keys[i]) === -1) params[keys[i]] = payload[keys[i]];
|
|
160
|
+
}
|
|
161
|
+
return { params: params, claims: payload };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
parse: parse,
|
|
166
|
+
JAR_TYP: JAR_TYP,
|
|
167
|
+
AuthJarError: AuthJarError,
|
|
168
|
+
};
|
package/lib/backup/index.js
CHANGED
|
@@ -1935,6 +1935,102 @@ function bundleAdapterStorage(opts) {
|
|
|
1935
1935
|
results: results,
|
|
1936
1936
|
};
|
|
1937
1937
|
},
|
|
1938
|
+
// keyRotation(opts) — orchestrate a whole-repository key rotation:
|
|
1939
|
+
// rotate every bundle's envelope from the old key to the new key
|
|
1940
|
+
// (composing rewrapAllBundles), then re-read every rotated bundle
|
|
1941
|
+
// under the NEW key (composing verifyAllBundles) so a rotation
|
|
1942
|
+
// that silently corrupted a bundle surfaces as a failure rather
|
|
1943
|
+
// than a time-bomb the operator discovers at restore time. Emits
|
|
1944
|
+
// a `backup/key-rotated` audit event with the rotation id + the
|
|
1945
|
+
// per-status counts — key-rotation events are a compliance record
|
|
1946
|
+
// (SOC 2 CC6.1 / PCI DSS 3.6.4) operators wire into their chain.
|
|
1947
|
+
//
|
|
1948
|
+
// opts.newRecipient / opts.newPassphrase is the key bundles are
|
|
1949
|
+
// rotated TO (required, matched to the storage's cryptoStrategy);
|
|
1950
|
+
// opts.oldRecipient / opts.oldPassphrase unwraps the current
|
|
1951
|
+
// envelope when it differs from the storage's configured key.
|
|
1952
|
+
// opts.verify (default true) runs the post-rotation read-back;
|
|
1953
|
+
// opts.concurrency / opts.stopOnFirstFailure forward to the
|
|
1954
|
+
// batch passes. opts.dualWrap is deferred-with-condition — a true
|
|
1955
|
+
// overlap window where BOTH the old and new key decrypt a bundle
|
|
1956
|
+
// needs multi-recipient envelopes (b.archive.wrap currently wraps
|
|
1957
|
+
// to a single recipient); it re-opens when the wrap layer gains
|
|
1958
|
+
// multi-recipient support. Until then operators stage a rotation
|
|
1959
|
+
// by keeping the old key available to readers until keyRotation
|
|
1960
|
+
// reports `failed: 0` + `verifyFailed: 0`, then retiring it.
|
|
1961
|
+
async keyRotation(opts) {
|
|
1962
|
+
opts = opts || {};
|
|
1963
|
+
if (opts.dualWrap === true) {
|
|
1964
|
+
throw new BackupError("backup/dual-wrap-unsupported",
|
|
1965
|
+
"keyRotation: dualWrap (simultaneous old+new key validity) requires multi-recipient " +
|
|
1966
|
+
"archive envelopes, which b.archive.wrap does not yet emit; rotate sequentially and " +
|
|
1967
|
+
"keep the old key available to readers until keyRotation reports failed: 0 + verifyFailed: 0");
|
|
1968
|
+
}
|
|
1969
|
+
if (cryptoStrategy === "none") {
|
|
1970
|
+
throw new BackupError("backup/no-envelope-to-rewrap",
|
|
1971
|
+
"keyRotation: storage cryptoStrategy is \"none\" — there is no envelope key to rotate");
|
|
1972
|
+
}
|
|
1973
|
+
if (cryptoStrategy === "recipient" &&
|
|
1974
|
+
(!opts.newRecipient || typeof opts.newRecipient !== "object")) {
|
|
1975
|
+
throw new BackupError("backup/no-recipient",
|
|
1976
|
+
"keyRotation: cryptoStrategy \"recipient\" requires opts.newRecipient (the key to rotate to)");
|
|
1977
|
+
}
|
|
1978
|
+
if (cryptoStrategy === "passphrase" &&
|
|
1979
|
+
!(typeof opts.newPassphrase === "string" || Buffer.isBuffer(opts.newPassphrase))) {
|
|
1980
|
+
throw new BackupError("backup/bad-passphrase",
|
|
1981
|
+
"keyRotation: cryptoStrategy \"passphrase\" requires opts.newPassphrase (string or Buffer)");
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
var rotatedAt = new Date().toISOString();
|
|
1985
|
+
var rotationId = "rotation-" + rotatedAt;
|
|
1986
|
+
|
|
1987
|
+
var rotate = await this.rewrapAllBundles(opts);
|
|
1988
|
+
|
|
1989
|
+
// Post-rotation read-back under the NEW key. Skip only when the
|
|
1990
|
+
// operator opts out; default proves the rotation landed.
|
|
1991
|
+
var verify = null;
|
|
1992
|
+
if (opts.verify !== false) {
|
|
1993
|
+
var verifyOpts = {
|
|
1994
|
+
concurrency: opts.concurrency,
|
|
1995
|
+
stopOnFirstFailure: opts.stopOnFirstFailure,
|
|
1996
|
+
};
|
|
1997
|
+
if (cryptoStrategy === "recipient") verifyOpts.recipient = opts.newRecipient;
|
|
1998
|
+
else verifyOpts.passphrase = opts.newPassphrase;
|
|
1999
|
+
verify = await this.verifyAllBundles(verifyOpts);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
var verifyFailed = verify ? verify.failed : 0;
|
|
2003
|
+
var outcome = (rotate.failed === 0 && verifyFailed === 0) ? "success" : "failure";
|
|
2004
|
+
try {
|
|
2005
|
+
audit().safeEmit({
|
|
2006
|
+
action: "backup/key-rotated",
|
|
2007
|
+
outcome: outcome,
|
|
2008
|
+
metadata: {
|
|
2009
|
+
rotationId: rotationId,
|
|
2010
|
+
cryptoStrategy: cryptoStrategy,
|
|
2011
|
+
total: rotate.total,
|
|
2012
|
+
rotated: rotate.rotated,
|
|
2013
|
+
skipped: rotate.skipped,
|
|
2014
|
+
failed: rotate.failed,
|
|
2015
|
+
verified: verify ? verify.ok : null,
|
|
2016
|
+
verifyFailed: verifyFailed,
|
|
2017
|
+
},
|
|
2018
|
+
});
|
|
2019
|
+
} catch (_e) { /* audit best-effort — drop-silent */ }
|
|
2020
|
+
|
|
2021
|
+
return {
|
|
2022
|
+
rotationId: rotationId,
|
|
2023
|
+
rotatedAt: rotatedAt,
|
|
2024
|
+
total: rotate.total,
|
|
2025
|
+
rotated: rotate.rotated,
|
|
2026
|
+
skipped: rotate.skipped,
|
|
2027
|
+
failed: rotate.failed,
|
|
2028
|
+
verified: verify ? verify.ok : null,
|
|
2029
|
+
verifyFailed: verifyFailed,
|
|
2030
|
+
rotateResults: rotate.results,
|
|
2031
|
+
verifyResults: verify ? verify.results : null,
|
|
2032
|
+
};
|
|
2033
|
+
},
|
|
1938
2034
|
// bundleInfo(bundleId) — v0.12.17 per-bundle introspection.
|
|
1939
2035
|
// Returns `{ bundleId, format, envelopeKind, sizeBytes }`.
|
|
1940
2036
|
// `format` is one of `"tar"` / `"tar.gz"` / `"directory"`
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:db45490d-0a47-4980-8047-83be6cb8f384",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T19:33:14.548Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.12.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.31",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.31",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.12.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.31",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.12.
|
|
57
|
+
"ref": "@blamejs/core@0.12.31",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|