@blamejs/blamejs-shop 0.4.2 → 0.4.4
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 +8 -7
- package/lib/admin.js +376 -0
- package/lib/asset-manifest.json +3 -3
- package/lib/security-middleware.js +81 -30
- package/lib/storefront.js +271 -6
- package/lib/vendor/MANIFEST.json +23 -23
- package/lib/vendor/blamejs/.pinact.yaml +1 -1
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/SECURITY.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +15 -2
- package/lib/vendor/blamejs/index.js +5 -1
- package/lib/vendor/blamejs/lib/auth/jar.js +190 -28
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +213 -0
- package/lib/vendor/blamejs/lib/auth/oauth.js +115 -101
- package/lib/vendor/blamejs/lib/http-client.js +3 -4
- package/lib/vendor/blamejs/lib/lro.js +3 -4
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -10
- package/lib/vendor/blamejs/lib/middleware/health.js +1 -4
- package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +3 -6
- package/lib/vendor/blamejs/lib/validate-opts.js +34 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jar.test.js +226 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +122 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/jwt-external.test.js +104 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +127 -0
- package/package.json +1 -1
- package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +0 -165
|
@@ -122,6 +122,11 @@ var lazyRequire = require("../lazy-require");
|
|
|
122
122
|
// convention §3; no circular load — jwt-external requires nothing from
|
|
123
123
|
// oauth.
|
|
124
124
|
var jwtExternal = require("./jwt-external");
|
|
125
|
+
// RFC 9101 request-object builder — composed by pushAuthorizationRequest
|
|
126
|
+
// when the operator opts into sending a signed request object. Top-of-file
|
|
127
|
+
// per convention §3; no circular load — jar requires jwt-external +
|
|
128
|
+
// validate-opts only, nothing from oauth.
|
|
129
|
+
var jar = require("./jar");
|
|
125
130
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
126
131
|
|
|
127
132
|
// Cap on responses parsed from upstream OAuth providers. Token /
|
|
@@ -517,97 +522,62 @@ var MAX_ATTESTATION_JWT_BYTES = C.BYTES.kib(16);
|
|
|
517
522
|
var DEFAULT_POP_MAX_AGE_SEC = C.TIME.minutes(5) / C.TIME.seconds(1);
|
|
518
523
|
|
|
519
524
|
// Sign/verify params keyed by alg — superset of _verifyParamsForAlg that
|
|
520
|
-
// also covers EdDSA (used only on the attestation path; the
|
|
521
|
-
// verifier keeps its own narrower table untouched).
|
|
525
|
+
// also covers EdDSA (used only on the attestation verify path; the
|
|
526
|
+
// ID-token verifier keeps its own narrower table untouched).
|
|
522
527
|
function _attestationCryptoParams(alg) {
|
|
523
528
|
if (alg === "EdDSA") return { hash: null };
|
|
524
529
|
return _verifyParamsForAlg(alg);
|
|
525
530
|
}
|
|
526
531
|
|
|
532
|
+
// _toAttestationPrivateKey / _resolveAttestationAlg / _signAttestationJws —
|
|
533
|
+
// thin wrappers over the classical-JWS signer that the jwt-external module
|
|
534
|
+
// owns (b.auth.jws.sign internals). The attestation path keeps its own
|
|
535
|
+
// `auth-oauth/attestation-*` error codes so operators routing alerts on
|
|
536
|
+
// that class see no change; the signer BODIES (alg-from-key derivation,
|
|
537
|
+
// compact-JWS assembly) live in exactly one place — the classical-JOSE
|
|
538
|
+
// domain owner — rather than duplicated here. RFC 7518 §3.1 alg↔key
|
|
539
|
+
// binding and the self-invalid-alg defenses are enforced by the composed
|
|
540
|
+
// primitive.
|
|
541
|
+
|
|
527
542
|
function _toAttestationPrivateKey(value, label) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (typeof value === "string" || Buffer.isBuffer(value)) {
|
|
534
|
-
return nodeCrypto.createPrivateKey({ key: value, format: "pem" });
|
|
535
|
-
}
|
|
536
|
-
if (typeof value === "object" && value.kty) {
|
|
537
|
-
return nodeCrypto.createPrivateKey({ key: value, format: "jwk" });
|
|
538
|
-
}
|
|
539
|
-
} catch (e) {
|
|
540
|
-
throw new OAuthError("auth-oauth/attestation-bad-key",
|
|
541
|
-
label + ": private key parse failed: " + ((e && e.message) || String(e)));
|
|
543
|
+
try { return jwtExternal._toPrivateKey(value, label); }
|
|
544
|
+
catch (e) {
|
|
545
|
+
var code = (e && e.code) === "auth-jwt-external/sign-no-key"
|
|
546
|
+
? "auth-oauth/attestation-no-key" : "auth-oauth/attestation-bad-key";
|
|
547
|
+
throw new OAuthError(code, (e && e.message) || String(e));
|
|
542
548
|
}
|
|
543
|
-
throw new OAuthError("auth-oauth/attestation-bad-key",
|
|
544
|
-
label + ": privateKey must be a PEM string/Buffer, JWK object, or KeyObject");
|
|
545
549
|
}
|
|
546
550
|
|
|
547
|
-
// EC curve → the one ES* alg whose hash matches it (RFC 7518 §3.4).
|
|
548
|
-
var _EC_CURVE_ALG = { prime256v1: "ES256", secp384r1: "ES384", secp521r1: "ES512" };
|
|
549
|
-
|
|
550
551
|
// Resolve the JWS alg for an attestation / PoP signature. When the caller
|
|
551
|
-
// gives no `algorithm`,
|
|
552
|
-
// non-EC attester key (RSA, Ed25519) yields a
|
|
553
|
-
// alg ⇄ signature key — instead of a fixed
|
|
554
|
-
// real key, which `verifyClientAttestation`'s
|
|
555
|
-
// then reject. An explicit alg incompatible with
|
|
556
|
-
// signing
|
|
552
|
+
// gives no `algorithm`, the composed signer infers the default that matches
|
|
553
|
+
// the key type so a non-EC attester key (RSA, Ed25519) yields a
|
|
554
|
+
// self-consistent JWS — header alg ⇄ signature key — instead of a fixed
|
|
555
|
+
// `ES256` header signed with the real key, which `verifyClientAttestation`'s
|
|
556
|
+
// alg/kty cross-check would then reject. An explicit alg incompatible with
|
|
557
|
+
// the key is refused BEFORE signing. The draft additionally floors the
|
|
558
|
+
// accepted set to ATTESTATION_ALGS (no HMAC / none); the composed resolver
|
|
559
|
+
// already refuses those, surfaced here as the attestation-specific code.
|
|
557
560
|
function _resolveAttestationAlg(explicitAlg, privateKey, label) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
var
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
compatible = ["PS256", "PS384", "PS512"]; // an RSA-PSS key cannot produce an RS* signature
|
|
574
|
-
} else if (kty === "ed25519" || kty === "ed448") {
|
|
575
|
-
defaultAlg = "EdDSA";
|
|
576
|
-
compatible = ["EdDSA"];
|
|
577
|
-
} else {
|
|
578
|
-
throw new OAuthError("auth-oauth/attestation-key-unsupported",
|
|
579
|
-
label + ": key type '" + String(kty) + "' is not a supported attestation key (EC / RSA / Ed25519 / Ed448)");
|
|
580
|
-
}
|
|
581
|
-
if (explicitAlg === undefined || explicitAlg === null) return defaultAlg;
|
|
582
|
-
if (ATTESTATION_ALGS.indexOf(explicitAlg) === -1) {
|
|
583
|
-
throw new OAuthError("auth-oauth/attestation-alg-not-accepted",
|
|
584
|
-
label + ": alg '" + explicitAlg + "' is not an accepted attestation algorithm");
|
|
585
|
-
}
|
|
586
|
-
if (compatible.indexOf(explicitAlg) === -1) {
|
|
587
|
-
throw new OAuthError("auth-oauth/attestation-alg-key-mismatch",
|
|
588
|
-
label + ": alg '" + explicitAlg + "' is incompatible with the " + kty +
|
|
589
|
-
" key (compatible: " + compatible.join(", ") + ")");
|
|
561
|
+
try {
|
|
562
|
+
return jwtExternal._resolveSignAlg(explicitAlg, privateKey, label);
|
|
563
|
+
} catch (e) {
|
|
564
|
+
var ec = (e && e.code) || "";
|
|
565
|
+
if (ec === "auth-jwt-external/sign-alg-key-mismatch") {
|
|
566
|
+
throw new OAuthError("auth-oauth/attestation-alg-key-mismatch", (e && e.message) || String(e));
|
|
567
|
+
}
|
|
568
|
+
if (ec === "auth-jwt-external/sign-alg-refused" || ec === "auth-jwt-external/sign-alg-unsupported") {
|
|
569
|
+
throw new OAuthError("auth-oauth/attestation-alg-not-accepted",
|
|
570
|
+
label + ": alg '" + explicitAlg + "' is not an accepted attestation algorithm");
|
|
571
|
+
}
|
|
572
|
+
if (ec === "auth-jwt-external/sign-key-unsupported") {
|
|
573
|
+
throw new OAuthError("auth-oauth/attestation-key-unsupported", (e && e.message) || String(e));
|
|
574
|
+
}
|
|
575
|
+
throw new OAuthError("auth-oauth/attestation-bad-key", (e && e.message) || String(e));
|
|
590
576
|
}
|
|
591
|
-
return explicitAlg;
|
|
592
577
|
}
|
|
593
578
|
|
|
594
579
|
function _signAttestationJws(header, payload, privateKey, alg) {
|
|
595
|
-
|
|
596
|
-
var headerB64 = _b64urlEncode(Buffer.from(JSON.stringify(header), "utf8"));
|
|
597
|
-
var payloadB64 = _b64urlEncode(Buffer.from(JSON.stringify(payload), "utf8"));
|
|
598
|
-
var signingInput = headerB64 + "." + payloadB64;
|
|
599
|
-
var sig;
|
|
600
|
-
var input = Buffer.from(signingInput, "ascii");
|
|
601
|
-
if (params.hash === null) {
|
|
602
|
-
sig = nodeCrypto.sign(null, input, privateKey); // EdDSA — no prehash
|
|
603
|
-
} else {
|
|
604
|
-
var keyParam = { key: privateKey };
|
|
605
|
-
if (params.padding !== undefined) keyParam.padding = params.padding;
|
|
606
|
-
if (params.saltLength !== undefined) keyParam.saltLength = params.saltLength;
|
|
607
|
-
if (params.dsaEncoding !== undefined) keyParam.dsaEncoding = params.dsaEncoding;
|
|
608
|
-
sig = nodeCrypto.sign(params.hash, input, keyParam);
|
|
609
|
-
}
|
|
610
|
-
return signingInput + "." + _b64urlEncode(sig);
|
|
580
|
+
return jwtExternal._signCompactJws(header, payload, privateKey, alg);
|
|
611
581
|
}
|
|
612
582
|
|
|
613
583
|
// Verify a compact JWS against an already-imported public KeyObject. The
|
|
@@ -751,16 +721,9 @@ function buildClientAttestation(aopts) {
|
|
|
751
721
|
};
|
|
752
722
|
if (typeof aopts.nbf === "number") payload.nbf = aopts.nbf;
|
|
753
723
|
// Operator extra claims merged WITHOUT overriding the spec-required
|
|
754
|
-
// fields (
|
|
755
|
-
// names rejected).
|
|
724
|
+
// fields (proto-pollution sentinels skipped, the spec keys reserved).
|
|
756
725
|
if (aopts.extraClaims && typeof aopts.extraClaims === "object" && !Array.isArray(aopts.extraClaims)) {
|
|
757
|
-
|
|
758
|
-
for (var i = 0; i < ck.length; i += 1) {
|
|
759
|
-
var k = ck[i];
|
|
760
|
-
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
761
|
-
if (Object.prototype.hasOwnProperty.call(payload, k)) continue; // never override spec fields
|
|
762
|
-
payload[k] = aopts.extraClaims[k];
|
|
763
|
-
}
|
|
726
|
+
validateOpts.assignOwnEnumerable(payload, aopts.extraClaims, Object.keys(payload));
|
|
764
727
|
}
|
|
765
728
|
return _signAttestationJws(
|
|
766
729
|
{ typ: "oauth-client-attestation+jwt", alg: alg }, payload, key, alg);
|
|
@@ -1996,6 +1959,13 @@ function create(opts) {
|
|
|
1996
1959
|
// browser-side redirect to /authorize. Defends against parameter
|
|
1997
1960
|
// tampering by an MITM at the user-agent + against URL-length
|
|
1998
1961
|
// overflow on long authorization requests.
|
|
1962
|
+
//
|
|
1963
|
+
// RFC 9101 signed request object: pass `signedRequestObject: { key,
|
|
1964
|
+
// alg?, kid?, audience?, expiresInMs? }` to push a JAR request object
|
|
1965
|
+
// instead of plain form params. The authorization parameters then
|
|
1966
|
+
// travel as signed claims (RFC 9126 §3 — form body carries only
|
|
1967
|
+
// `request` + client auth), so the PAR endpoint can verify they
|
|
1968
|
+
// arrived exactly as the client signed them. Absent → plain-form PAR.
|
|
1999
1969
|
async function pushAuthorizationRequest(uopts) {
|
|
2000
1970
|
uopts = uopts || {};
|
|
2001
1971
|
var endpoint;
|
|
@@ -2006,6 +1976,20 @@ function create(opts) {
|
|
|
2006
1976
|
"pushed_authorization_request_endpoint (set opts.pushedAuthorizationRequestEndpoint " +
|
|
2007
1977
|
"on create() if the IdP doesn't publish it)");
|
|
2008
1978
|
}
|
|
1979
|
+
// RFC 9101 signed-request-object opt: when the operator supplies
|
|
1980
|
+
// `signedRequestObject` (a config object carrying the client's signing
|
|
1981
|
+
// key), the authorization parameters travel as claims of a JAR request
|
|
1982
|
+
// object rather than as bare form params. Validated config-time; absent
|
|
1983
|
+
// → the existing plain-form path sends the same key/value set
|
|
1984
|
+
// (form-encoded params are unordered per the media type).
|
|
1985
|
+
var sro = uopts.signedRequestObject || null;
|
|
1986
|
+
if (sro) {
|
|
1987
|
+
validateOpts.optionalPlainObject(sro, "pushAuthorizationRequest: signedRequestObject",
|
|
1988
|
+
OAuthError, "auth-oauth/par-bad-request-object-opt",
|
|
1989
|
+
"must be an object { key, alg?, kid?, audience?, expiresInMs? }");
|
|
1990
|
+
validateOpts(sro, ["key", "alg", "kid", "audience", "expiresInMs"],
|
|
1991
|
+
"pushAuthorizationRequest.signedRequestObject");
|
|
1992
|
+
}
|
|
2009
1993
|
// Same PKCE-downgrade gate as authorizationUrl (RFC 9700 §4.13):
|
|
2010
1994
|
// PAR pushes the identical S256 challenge, so an OP advertising
|
|
2011
1995
|
// code_challenge_methods_supported without S256 is refused here too.
|
|
@@ -2015,31 +1999,60 @@ function create(opts) {
|
|
|
2015
1999
|
var state = uopts.state || _generateRandomToken(STATE_NONCE_BYTES);
|
|
2016
2000
|
var nonce = uopts.nonce || (isOidc ? _generateRandomToken(STATE_NONCE_BYTES) : null);
|
|
2017
2001
|
var pkceVals = _generatePkce();
|
|
2018
|
-
|
|
2019
|
-
body
|
|
2020
|
-
body.
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
if (
|
|
2031
|
-
if (
|
|
2002
|
+
// The authorization-request parameters. On the plain path these are set
|
|
2003
|
+
// on the form body directly; on the JAR path they become request-object
|
|
2004
|
+
// claims and the form body carries only `request` + client auth.
|
|
2005
|
+
var authzParams = {
|
|
2006
|
+
response_type: "code",
|
|
2007
|
+
client_id: clientId,
|
|
2008
|
+
redirect_uri: redirectUri,
|
|
2009
|
+
scope: scope.join(" "),
|
|
2010
|
+
state: state,
|
|
2011
|
+
code_challenge: pkceVals.challenge,
|
|
2012
|
+
code_challenge_method: "S256",
|
|
2013
|
+
};
|
|
2014
|
+
if (nonce) authzParams.nonce = nonce;
|
|
2015
|
+
if (responseMode) authzParams.response_mode = responseMode;
|
|
2016
|
+
if (uopts.prompt) authzParams.prompt = uopts.prompt;
|
|
2017
|
+
if (uopts.loginHint) authzParams.login_hint = uopts.loginHint;
|
|
2018
|
+
if (uopts.maxAge != null) authzParams.max_age = String(uopts.maxAge);
|
|
2032
2019
|
// RFC 9396 — push the fine-grained authorization request through PAR
|
|
2033
2020
|
// identically to the redirect path (validated, then JSON-serialized).
|
|
2034
2021
|
var requestedAuthzDetails = null;
|
|
2035
2022
|
if (uopts.authorizationDetails !== undefined) {
|
|
2036
2023
|
requestedAuthzDetails = _validateAuthorizationDetailsArray(
|
|
2037
2024
|
uopts.authorizationDetails, "pushAuthorizationRequest");
|
|
2038
|
-
|
|
2025
|
+
authzParams.authorization_details = JSON.stringify(requestedAuthzDetails);
|
|
2039
2026
|
}
|
|
2040
2027
|
if (uopts.extraParams && typeof uopts.extraParams === "object") {
|
|
2041
2028
|
var ek = Object.keys(uopts.extraParams);
|
|
2042
|
-
for (var i = 0; i < ek.length; i++)
|
|
2029
|
+
for (var i = 0; i < ek.length; i++) authzParams[ek[i]] = String(uopts.extraParams[ek[i]]);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
var body = new URLSearchParams();
|
|
2033
|
+
if (sro) {
|
|
2034
|
+
// RFC 9126 §3 — when a signed request object is pushed, the
|
|
2035
|
+
// authorization parameters MUST appear ONLY as claims of the JWT;
|
|
2036
|
+
// the form body carries `request` plus the parameters a client
|
|
2037
|
+
// authentication method requires (client_id, and client_secret for
|
|
2038
|
+
// the secret-based methods) and nothing else. The JAR `aud` is the
|
|
2039
|
+
// AS issuer identifier (RFC 9101 §5) — the operator may override but
|
|
2040
|
+
// it defaults to the configured `issuer`.
|
|
2041
|
+
var requestJwt = jar.build(authzParams, {
|
|
2042
|
+
clientId: clientId,
|
|
2043
|
+
audience: sro.audience || issuer,
|
|
2044
|
+
key: sro.key,
|
|
2045
|
+
alg: sro.alg,
|
|
2046
|
+
kid: sro.kid,
|
|
2047
|
+
expiresInMs: sro.expiresInMs,
|
|
2048
|
+
});
|
|
2049
|
+
body.set("request", requestJwt);
|
|
2050
|
+
body.set("client_id", clientId); // RFC 9126 §3 — client identification
|
|
2051
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
2052
|
+
} else {
|
|
2053
|
+
var ak = Object.keys(authzParams);
|
|
2054
|
+
for (var ap = 0; ap < ak.length; ap++) body.set(ak[ap], authzParams[ak[ap]]);
|
|
2055
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
2043
2056
|
}
|
|
2044
2057
|
var rv = await _postForm(endpoint, body);
|
|
2045
2058
|
if (!rv || typeof rv.request_uri !== "string" || rv.request_uri.length === 0) {
|
|
@@ -2062,6 +2075,7 @@ function create(opts) {
|
|
|
2062
2075
|
requestUri: rv.request_uri,
|
|
2063
2076
|
expiresIn: typeof rv.expires_in === "number" ? rv.expires_in : null,
|
|
2064
2077
|
authorizationDetails: requestedAuthzDetails,
|
|
2078
|
+
requestObjectSent: !!sro,
|
|
2065
2079
|
};
|
|
2066
2080
|
}
|
|
2067
2081
|
|
|
@@ -673,13 +673,12 @@ function _buildMultipartBody(spec) {
|
|
|
673
673
|
var SENSITIVE_HEADERS_LC = ["authorization", "cookie", "proxy-authorization"];
|
|
674
674
|
|
|
675
675
|
function _stripCrossOriginAuth(headers) {
|
|
676
|
-
var out = {};
|
|
677
676
|
var keys = Object.keys(headers);
|
|
677
|
+
var strip = [];
|
|
678
678
|
for (var i = 0; i < keys.length; i++) {
|
|
679
|
-
if (SENSITIVE_HEADERS_LC.indexOf(keys[i].toLowerCase()) !== -1)
|
|
680
|
-
out[keys[i]] = headers[keys[i]];
|
|
679
|
+
if (SENSITIVE_HEADERS_LC.indexOf(keys[i].toLowerCase()) !== -1) strip.push(keys[i]);
|
|
681
680
|
}
|
|
682
|
-
return
|
|
681
|
+
return validateOpts.assignOwnEnumerable({}, headers, strip);
|
|
683
682
|
}
|
|
684
683
|
|
|
685
684
|
/**
|
|
@@ -185,13 +185,12 @@ function create(opts) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
function _stripPrivate(op) {
|
|
188
|
-
var out = {};
|
|
189
188
|
var keys = Object.keys(op);
|
|
189
|
+
var priv = [];
|
|
190
190
|
for (var i = 0; i < keys.length; i += 1) {
|
|
191
|
-
if (keys[i].charAt(0) === "_")
|
|
192
|
-
out[keys[i]] = op[keys[i]];
|
|
191
|
+
if (keys[i].charAt(0) === "_") priv.push(keys[i]);
|
|
193
192
|
}
|
|
194
|
-
return
|
|
193
|
+
return validateOpts.assignOwnEnumerable({}, op, priv);
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
module.exports = {
|
|
@@ -34,18 +34,10 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
var problemDetails = require("../problem-details");
|
|
37
|
+
var validateOpts = require("../validate-opts");
|
|
37
38
|
|
|
38
39
|
function _isFn(x) { return typeof x === "function"; }
|
|
39
40
|
|
|
40
|
-
function _mergeInto(target, extra) {
|
|
41
|
-
if (!extra || typeof extra !== "object") return target;
|
|
42
|
-
var keys = Object.keys(extra);
|
|
43
|
-
for (var i = 0; i < keys.length; i += 1) {
|
|
44
|
-
target[keys[i]] = extra[keys[i]];
|
|
45
|
-
}
|
|
46
|
-
return target;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
/**
|
|
50
42
|
* Resolve a deny-path refusal through the uniform hook / problem+json
|
|
51
43
|
* / default chain. Returns whatever the `onDeny` hook returns when it
|
|
@@ -144,7 +136,7 @@ function denyResponse(req, res, ctx) {
|
|
|
144
136
|
return undefined;
|
|
145
137
|
}
|
|
146
138
|
|
|
147
|
-
var head =
|
|
139
|
+
var head = validateOpts.assignOwnEnumerable({ "Content-Type": ctx.contentType }, extra);
|
|
148
140
|
var denyOut = (ctx.body === undefined || ctx.body === null) ? ""
|
|
149
141
|
: (typeof ctx.body === "string" ? ctx.body : JSON.stringify(ctx.body));
|
|
150
142
|
if (ctx.body !== undefined && ctx.body !== null && req && typeof req.apiEncryptEncode === "function") {
|
|
@@ -317,10 +317,7 @@ function create(opts) {
|
|
|
317
317
|
var entry = { ok: r.ok, ms: r.ms };
|
|
318
318
|
if (r.detail) {
|
|
319
319
|
// Merge detail keys other than `ok` into the entry.
|
|
320
|
-
|
|
321
|
-
for (var n = 0; n < keys.length; n++) {
|
|
322
|
-
if (keys[n] !== "ok") entry[keys[n]] = r.detail[keys[n]];
|
|
323
|
-
}
|
|
320
|
+
validateOpts.assignOwnEnumerable(entry, r.detail, ["ok"]);
|
|
324
321
|
}
|
|
325
322
|
if (r.error) entry.error = r.error;
|
|
326
323
|
if (!r.critical) entry.critical = false;
|
|
@@ -52,13 +52,10 @@ function _baggageToObject(entries) {
|
|
|
52
52
|
|
|
53
53
|
function _wrapLogger(baseLogger, req, opts) {
|
|
54
54
|
if (!baseLogger || typeof baseLogger !== "object") return baseLogger;
|
|
55
|
-
var wrapped = Object.create(null);
|
|
56
55
|
// Preserve any non-level properties the operator put on the
|
|
57
|
-
// logger (e.g. boot context, child-logger metadata)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (LOG_LEVELS.indexOf(keys[i]) === -1) wrapped[keys[i]] = baseLogger[keys[i]];
|
|
61
|
-
}
|
|
56
|
+
// logger (e.g. boot context, child-logger metadata); the level
|
|
57
|
+
// methods themselves are re-wrapped below.
|
|
58
|
+
var wrapped = validateOpts.assignOwnEnumerable(Object.create(null), baseLogger, LOG_LEVELS);
|
|
62
59
|
|
|
63
60
|
function _enrichMeta(meta) {
|
|
64
61
|
var enriched = Object.assign({}, meta || {});
|
|
@@ -393,6 +393,39 @@ function makeNamespacedEmitters(prefix, deps) {
|
|
|
393
393
|
return { audit: audit, metric: metric };
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
// assignOwnEnumerable — copy a source object's own enumerable keys onto a
|
|
397
|
+
// target, skipping the prototype-pollution sentinels (__proto__ /
|
|
398
|
+
// constructor / prototype) and any caller-named reserved keys. Several
|
|
399
|
+
// primitives that merge operator-supplied free-form fields onto a
|
|
400
|
+
// spec-built object (JOSE claim sets, JWS protected headers, attestation
|
|
401
|
+
// extra-claims) previously open-coded the identical
|
|
402
|
+
// `for (k of Object.keys(src)) { if (sentinel) continue; if (reserved)
|
|
403
|
+
// continue; dst[k] = src[k]; }` loop. Centralizing the proto-safe walk
|
|
404
|
+
// keeps the merge contract in one place. Reserved keys win — they are NOT
|
|
405
|
+
// overwritten — so the caller's spec-built fields can never be shadowed by
|
|
406
|
+
// a same-named operator key. Returns the target.
|
|
407
|
+
function assignOwnEnumerable(target, source, reservedKeys) {
|
|
408
|
+
if (!source || typeof source !== "object") return target;
|
|
409
|
+
var reserved = Object.create(null);
|
|
410
|
+
if (reservedKeys) for (var r = 0; r < reservedKeys.length; r += 1) reserved[reservedKeys[r]] = true;
|
|
411
|
+
var keys = Object.keys(source);
|
|
412
|
+
var entries = [];
|
|
413
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
414
|
+
var k = keys[i];
|
|
415
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
416
|
+
if (reserved[k]) continue;
|
|
417
|
+
entries.push([k, source[k]]);
|
|
418
|
+
}
|
|
419
|
+
// Staged through entries + Object.assign so the copy contains no
|
|
420
|
+
// computed-name property write at all: Object.fromEntries creates own
|
|
421
|
+
// data properties (it cannot walk the prototype chain), and the
|
|
422
|
+
// sentinel skip above means the staging object carries no
|
|
423
|
+
// __proto__/constructor/prototype key for Object.assign's [[Set]] to
|
|
424
|
+
// trip over. Same observable result as a key-by-key copy, with the
|
|
425
|
+
// arbitrary-property-write shape removed instead of merely guarded.
|
|
426
|
+
return Object.assign(target, Object.fromEntries(entries));
|
|
427
|
+
}
|
|
428
|
+
|
|
396
429
|
// observabilityShape — operator-supplied `opts.observability` must
|
|
397
430
|
// expose an `event` function. Parallel to auditShape; the n=1 catalog
|
|
398
431
|
// tracks both inline-shape regexes.
|
|
@@ -426,3 +459,4 @@ module.exports.requireMethods = requireMethods;
|
|
|
426
459
|
module.exports.applyDefaults = applyDefaults;
|
|
427
460
|
module.exports.makeAuditEmitter = makeAuditEmitter;
|
|
428
461
|
module.exports.makeNamespacedEmitters = makeNamespacedEmitters;
|
|
462
|
+
module.exports.assignOwnEnumerable = assignOwnEnumerable;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.14.22",
|
|
4
|
+
"date": "2026-06-04",
|
|
5
|
+
"headline": "RFC 9101 signed request objects: a JAR request-object builder, a classical JWS signer for external interop, and pushed authorization requests that carry `request=`",
|
|
6
|
+
"summary": "The framework can now mint JWT-Secured Authorization Requests, completing the JAR surface whose verify side (`b.auth.jar.parse`) shipped in v0.12.31 with the builder documented as waiting on a classical signer. `b.auth.jws.sign` is that signer — a compact-JWS producer for RS / PS / ES / EdDSA keys that exists strictly for interop with external ecosystems (authorization servers and relying parties that require classical algorithms); the framework's own tokens stay on the PQC-first signer. `b.auth.jar.build` mints the RFC 9101 request object on top of it, and `pushAuthorizationRequest` composes both so a pushed authorization request can carry the signed `request=` parameter — the FAPI 2.0 message-signing client shape. The OAuth client-attestation builder now composes the same promoted signer internally, with identical wire output.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.auth.jar.build` — RFC 9101 request-object builder",
|
|
13
|
+
"body": "Mints the JWT-Secured Authorization Request: header `typ: oauth-authz-req+jwt` (RFC 9101 §10.8), `iss` = the client_id and `aud` = the authorization server (§5), `response_type` and `client_id` required as claims (§4), and every authorization parameter carried as a claim. A params object containing `request` or `request_uri` is refused (§4 forbids nesting), reserved-claim collisions are refused, and a `params.client_id` that disagrees with `opts.clientId` is refused. The JWT carries a short `exp` (default 5 minutes, `expiresInMs`-overridable), `nbf`, and a random `jti` for FAPI 2.0 message signing. The signing algorithm derives from the supplied key; `none` is impossible. Round-trips against the existing `b.auth.jar.parse` verifier."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "`b.auth.jws.sign` — classical compact-JWS signer for external interop",
|
|
17
|
+
"body": "Signs a compact JWS with an RS / PS / ES (P-256/P-384/P-521) / EdDSA key, deriving the algorithm from the key per RFC 7518 §3.1 and refusing `none`, HMAC, and algorithm/key mismatches; a caller-supplied `header.alg` cannot override the derived algorithm (algorithm-substitution closed). This primitive exists for interop with external ecosystems that require classical JOSE — JAR request objects, attestation JWTs, and similar cross-vendor surfaces. It is never the framework-internal token default: `b.auth.jwt` remains the PQC-first signer for the framework's own tokens."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "Pushed authorization requests can carry a signed request object (RFC 9126 §3)",
|
|
21
|
+
"body": "`pushAuthorizationRequest` accepts a `signedRequestObject` option (`{ key, alg?, kid?, audience?, expiresInMs? }`). When present, the authorization parameters are minted into a JAR request object and the PAR body carries `request=<jwt>` plus only the client-authentication material RFC 9126 allows alongside it; the bare authorization parameters are not duplicated in the form. Absent, the existing plain-form path sends the same key/value set as before."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"title": "`validateOpts.assignOwnEnumerable` — shared prototype-safe claim merge",
|
|
25
|
+
"body": "Consolidates the own-enumerable key merge with prototype-pollution and reserved-key guards that the request-object builder, the classical signer, and the client-attestation builder all need. Existing call sites compose it; behavior is unchanged."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"heading": "Changed",
|
|
31
|
+
"items": [
|
|
32
|
+
{
|
|
33
|
+
"title": "OAuth client-attestation signing composes the promoted classical signer",
|
|
34
|
+
"body": "The attestation builder's private JWS assembly moved to the shared `b.auth.jws.sign` internals. Wire output is identical — same headers, claim order, algorithm selection, and accepted-algorithm set — and the `auth-oauth/attestation-*` error codes are preserved, so operators routing alerts on those codes see no change."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"title": "Object key-copy sites compose the prototype-safe merge",
|
|
38
|
+
"body": "The long-running-operation status reader, the deny-path response-header merge, the HTTP client's cross-origin redirect header strip, and the trace-log logger wrapper now copy keys through `validateOpts.assignOwnEnumerable` instead of raw bracket-assign loops, so a `__proto__`/`constructor`/`prototype` key in the source object can never graft onto the copy. Behavior is otherwise unchanged."
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"heading": "Security",
|
|
44
|
+
"items": [
|
|
45
|
+
{
|
|
46
|
+
"title": "`jar.parse` returns prototype-safe authorization params (CWE-1321)",
|
|
47
|
+
"body": "A verified request object whose payload carried a `__proto__` claim (JSON.parse materializes it as an own key) previously grafted that claim's value onto the returned `params` object's prototype chain — a signature from a registered-but-malicious client was sufficient. The params object is now built through the prototype-safe merge; `__proto__`/`constructor`/`prototype` claim names are inert and are not copied."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"title": "`jws.sign` refuses `b64` and `crit` protected-header members",
|
|
51
|
+
"body": "RFC 7797 `b64: false` changes the JWS signing input (the payload is signed raw, not base64url-encoded) and RFC 7515 §4.1.11 `crit` promises the producer implements every extension it names. The signer always base64url-encodes the payload and implements no header extensions, so passing either member through minted a JWS whose header advertised semantics its signature was not computed under — a compliant verifier derives a different signing input or refuses the critical header. Both members are now refused with `auth-jwt-external/sign-unsupported-header`; unencoded-payload support would land as an explicit feature, not a header pass-through."
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"heading": "Removed",
|
|
57
|
+
"items": [
|
|
58
|
+
{
|
|
59
|
+
"title": "Maintainer planning note removed from the repository",
|
|
60
|
+
"body": "`memory/specs/node-26-map-getorinsert-migration.md` — a maintainer-local planning note that had been committed since v0.11.2 — is gone from the repository (it was never part of the npm package). The Node 26 detector allowlists in the pattern catalog now carry their per-site annotations standalone, and `SECURITY.md` / `.pinact.yaml` no longer reference maintainer-local note paths."
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"heading": "Detectors",
|
|
66
|
+
"items": [
|
|
67
|
+
{
|
|
68
|
+
"title": "raw-key-copy-loop-bypasses-assign-own-enumerable",
|
|
69
|
+
"body": "Refuses raw `out[keys[i]] = src[keys[i]]` bracket-assign copy loops in `lib/` — the shape behind the `jar.parse` finding. Key-copy sites compose `validateOpts.assignOwnEnumerable`; the two genuinely-different bodies (audit-chain hash canonicalization, schema-shape transforms) carry allowlist entries with structural reasons."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"title": "jose-header-passthrough-without-b64-crit-refusal",
|
|
73
|
+
"body": "Any caller-supplied JOSE protected-header pass-through must name-refuse `b64`/`crit` before signing."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"title": "no-tracked-internal-notes gate",
|
|
77
|
+
"body": "The pattern catalog now refuses any tracked file under `memory/`, `notes/`, or `.scratch*` paths at commit time."
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"heading": "Migration",
|
|
83
|
+
"items": [
|
|
84
|
+
{
|
|
85
|
+
"title": "No action required; everything is additive",
|
|
86
|
+
"body": "The JAR builder, the classical signer, the PAR `signedRequestObject` option, and the shared merge helper are new surface. Existing `jar.parse` callers, attestation flows, and plain PAR requests behave exactly as before."
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|