@blamejs/blamejs-shop 0.0.124 → 0.0.126

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.
@@ -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
+ };
@@ -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"`