@blamejs/core 0.14.21 → 0.14.24

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.
@@ -109,6 +109,14 @@ function _b64urlDecode(s) {
109
109
  return Buffer.from(padded, "base64");
110
110
  }
111
111
 
112
+ // EC named-curve → the one ES* alg whose hash matches it (RFC 7518 §3.4).
113
+ // A P-256 key signs ES256 and only ES256; the curve fixes the hash, so a
114
+ // header alg of ES384 over a P-256 signature is self-inconsistent and a
115
+ // conforming verifier rejects it. Naming the binding here lets the signer
116
+ // derive the right header alg from the key instead of trusting a caller-
117
+ // supplied alg the key can't actually produce.
118
+ var _EC_CURVE_ALG = { prime256v1: "ES256", secp384r1: "ES384", secp521r1: "ES512" };
119
+
112
120
  function _verifyParamsForAlg(alg) {
113
121
  if (alg === "RS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
114
122
  if (alg === "RS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
@@ -124,6 +132,104 @@ function _verifyParamsForAlg(alg) {
124
132
  "alg '" + alg + "' is not supported by verifyExternal");
125
133
  }
126
134
 
135
+ // _toPrivateKey — import the operator's classical signing key from any of
136
+ // the three shapes node:crypto understands (KeyObject / PEM string|Buffer /
137
+ // private JWK). The PQC framework signer (lib/auth/jwt.js) never travels
138
+ // this path; this is the classical-interop importer only.
139
+ function _toPrivateKey(value, label) {
140
+ if (!value) {
141
+ throw new AuthError("auth-jwt-external/sign-no-key", label + ": privateKey is required");
142
+ }
143
+ if (value instanceof nodeCrypto.KeyObject) return value;
144
+ try {
145
+ if (typeof value === "string" || Buffer.isBuffer(value)) {
146
+ return nodeCrypto.createPrivateKey({ key: value, format: "pem" });
147
+ }
148
+ if (typeof value === "object" && value.kty) {
149
+ return nodeCrypto.createPrivateKey({ key: value, format: "jwk" });
150
+ }
151
+ } catch (e) {
152
+ throw new AuthError("auth-jwt-external/sign-bad-key",
153
+ label + ": private key parse failed: " + ((e && e.message) || String(e)));
154
+ }
155
+ throw new AuthError("auth-jwt-external/sign-bad-key",
156
+ label + ": privateKey must be a PEM string/Buffer, private JWK object, or KeyObject");
157
+ }
158
+
159
+ // _resolveSignAlg — derive the JWS `alg` for a private key, validating any
160
+ // explicit override against what the key can actually produce. A signer
161
+ // that emitted a fixed `alg` header while signing with an incompatible key
162
+ // (e.g. an `ES256` header over an Ed25519 signature) would mint a token no
163
+ // conforming verifier accepts; deriving the alg from the key — or refusing
164
+ // an incompatible explicit alg BEFORE signing — closes that self-invalid
165
+ // shape. RFC 7518 §3.1 maps each `alg` to the key type it requires.
166
+ function _resolveSignAlg(explicitAlg, privateKey, label) {
167
+ var kty = privateKey.asymmetricKeyType;
168
+ var defaultAlg, compatible;
169
+ if (kty === "ec") {
170
+ var curve = (privateKey.asymmetricKeyDetails && privateKey.asymmetricKeyDetails.namedCurve) || "";
171
+ defaultAlg = _EC_CURVE_ALG[curve];
172
+ if (!defaultAlg) {
173
+ throw new AuthError("auth-jwt-external/sign-key-unsupported",
174
+ label + ": EC curve '" + curve + "' has no JWS alg (use P-256 / P-384 / P-521)");
175
+ }
176
+ compatible = [defaultAlg]; // an EC curve pins exactly one ES alg
177
+ } else if (kty === "rsa") {
178
+ defaultAlg = "RS256";
179
+ compatible = ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"];
180
+ } else if (kty === "rsa-pss") {
181
+ defaultAlg = "PS256";
182
+ compatible = ["PS256", "PS384", "PS512"]; // an RSA-PSS key cannot produce an RS* signature
183
+ } else if (kty === "ed25519" || kty === "ed448") {
184
+ defaultAlg = "EdDSA";
185
+ compatible = ["EdDSA"];
186
+ } else {
187
+ throw new AuthError("auth-jwt-external/sign-key-unsupported",
188
+ label + ": key type '" + String(kty) + "' is not a supported JWS signing key (EC / RSA / Ed25519 / Ed448)");
189
+ }
190
+ if (explicitAlg === undefined || explicitAlg === null) return defaultAlg;
191
+ if (explicitAlg === "none" || REFUSED_ALGS.indexOf(explicitAlg) !== -1) {
192
+ throw new AuthError("auth-jwt-external/sign-alg-refused",
193
+ label + ": alg '" + explicitAlg + "' is refused (HMAC / none are never valid for an asymmetric signer)");
194
+ }
195
+ if (SUPPORTED_CLASSICAL_ALGS.indexOf(explicitAlg) === -1) {
196
+ throw new AuthError("auth-jwt-external/sign-alg-unsupported",
197
+ label + ": alg '" + explicitAlg + "' is not a supported classical JWS algorithm (" +
198
+ SUPPORTED_CLASSICAL_ALGS.join(", ") + ")");
199
+ }
200
+ if (compatible.indexOf(explicitAlg) === -1) {
201
+ throw new AuthError("auth-jwt-external/sign-alg-key-mismatch",
202
+ label + ": alg '" + explicitAlg + "' is incompatible with the " + kty +
203
+ " key (compatible: " + compatible.join(", ") + ")");
204
+ }
205
+ return explicitAlg;
206
+ }
207
+
208
+ // _signCompactJws — produce the compact JWS serialization (protected
209
+ // header . payload . signature) for an already-resolved alg + imported
210
+ // private key. Header and payload are JCS-independent here: they are
211
+ // serialized exactly once by the signer, base64url-encoded, and that byte
212
+ // string IS the signing input, so there is no canonicalization gap a
213
+ // verifier could diverge on.
214
+ function _signCompactJws(header, payload, privateKey, alg) {
215
+ var params = _verifyParamsForAlg(alg);
216
+ var headerB64 = bCrypto.toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
217
+ var payloadB64 = bCrypto.toBase64Url(Buffer.from(JSON.stringify(payload), "utf8"));
218
+ var signingInput = headerB64 + "." + payloadB64;
219
+ var input = Buffer.from(signingInput, "ascii");
220
+ var sig;
221
+ if (params.hash === null) {
222
+ sig = nodeCrypto.sign(null, input, privateKey); // EdDSA — no prehash
223
+ } else {
224
+ var keyParam = { key: privateKey };
225
+ if (params.padding !== undefined) keyParam.padding = params.padding;
226
+ if (params.saltLength !== undefined) keyParam.saltLength = params.saltLength;
227
+ if (params.dsaEncoding !== undefined) keyParam.dsaEncoding = params.dsaEncoding;
228
+ sig = nodeCrypto.sign(params.hash, input, keyParam);
229
+ }
230
+ return signingInput + "." + bCrypto.toBase64Url(sig);
231
+ }
232
+
127
233
  function _jwkToKey(jwk) {
128
234
  try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
129
235
  catch (e) {
@@ -512,12 +618,119 @@ async function verifyExternal(token, opts) {
512
618
  return { header: header, claims: payload };
513
619
  }
514
620
 
621
+ /**
622
+ * @primitive b.auth.jws.sign
623
+ * @signature b.auth.jws.sign(claims, opts)
624
+ * @since 0.14.22
625
+ * @status stable
626
+ * @compliance soc2
627
+ * @related b.auth.jar.build, b.auth.jar.parse
628
+ *
629
+ * Mint a compact JWS (RFC 7515) over <code>claims</code> using a classical
630
+ * asymmetric algorithm — RS/PS256/384/512, ES256/384/512, or EdDSA. This
631
+ * primitive exists strictly for <strong>interop with external ecosystems</strong>:
632
+ * OAuth/OIDC OPs and RPs (and the wallet / FAPI profiles built on them)
633
+ * require a request object / assertion signed with a classical JWS alg, and
634
+ * the framework's own token signer (<code>b.auth.jwt.sign</code>) is
635
+ * PQC-only (ML-DSA / SLH-DSA). It is <strong>never the framework-internal
636
+ * token default</strong>; <code>lib/jwt.js</code> remains the signer for
637
+ * tokens blamejs itself issues. The verify counterpart is
638
+ * <code>b.auth.jwt.verifyExternal</code>; this is its inverse for the cases
639
+ * where blamejs is the client emitting a signed object to a third party.
640
+ *
641
+ * The signing <code>alg</code> is derived from the key type (RFC 7518 §3.1)
642
+ * so the header alg always matches the signature the key can actually
643
+ * produce; an explicit <code>opts.alg</code> is validated against the key
644
+ * and refused if incompatible. <code>alg: "none"</code> and HMAC algs are
645
+ * refused outright — an asymmetric signer never emits them. The protected
646
+ * header always carries <code>alg</code>; <code>typ</code> and <code>kid</code>
647
+ * are set from <code>opts</code> when supplied (callers minting a typed
648
+ * object such as a JAR request object pass <code>typ</code>). Extra
649
+ * <code>opts.header</code> members pass through with two refusals:
650
+ * <code>b64</code> (RFC 7797 unencoded payload — it changes the signing
651
+ * input, which this signer always base64url-encodes) and <code>crit</code>
652
+ * (RFC 7515 §4.1.11 — it promises extension semantics the signer does not
653
+ * implement). Emitting either would produce a JWS whose header claims
654
+ * semantics its signature was not computed under.
655
+ *
656
+ * @opts
657
+ * {
658
+ * privateKey: KeyObject|PEM|JWK, // required — classical signing key
659
+ * alg?: string, // override; default inferred from the key (RS256 / ES256/384/512 / PS256 / EdDSA)
660
+ * typ?: string, // protected-header typ (e.g. "oauth-authz-req+jwt")
661
+ * kid?: string, // protected-header kid (JWKS key selection)
662
+ * header?: object, // extra protected-header members (alg/typ/kid reserved; b64/crit refused)
663
+ * }
664
+ *
665
+ * @example
666
+ * var jws = b.auth.jws.sign(
667
+ * { iss: "client", aud: "https://as.example.com", response_type: "code" },
668
+ * { privateKey: clientKey, typ: "oauth-authz-req+jwt", kid: "c1" });
669
+ * // → "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..."
670
+ */
671
+ function signExternal(claims, opts) {
672
+ if (claims === null || typeof claims !== "object" || Array.isArray(claims)) {
673
+ throw new AuthError("auth-jwt-external/sign-bad-claims",
674
+ "jws.sign: claims must be a plain object");
675
+ }
676
+ validateOpts.requireObject(opts, "jws.sign", AuthError, "auth-jwt-external/sign-bad-opts");
677
+ validateOpts(opts, ["privateKey", "alg", "typ", "kid", "header"], "auth.jws.sign");
678
+ if (opts.alg !== undefined && opts.alg !== null) {
679
+ validateOpts.requireNonEmptyString(opts.alg, "jws.sign: alg", AuthError, "auth-jwt-external/sign-bad-alg");
680
+ }
681
+ if (opts.typ !== undefined && opts.typ !== null) {
682
+ validateOpts.requireNonEmptyString(opts.typ, "jws.sign: typ", AuthError, "auth-jwt-external/sign-bad-typ");
683
+ }
684
+ if (opts.kid !== undefined && opts.kid !== null) {
685
+ validateOpts.requireNonEmptyString(opts.kid, "jws.sign: kid", AuthError, "auth-jwt-external/sign-bad-kid");
686
+ }
687
+ validateOpts.optionalPlainObject(opts.header, "jws.sign: header", AuthError, "auth-jwt-external/sign-bad-header",
688
+ "must be a plain object of extra protected-header members");
689
+ // RFC 7797 `b64: false` changes the JWS signing input (the payload is
690
+ // signed raw, not base64url-encoded) and RFC 7515 §4.1.11 `crit`
691
+ // promises the producer implements every extension it names.
692
+ // _signCompactJws always base64url-encodes the payload and implements
693
+ // no header extensions, so passing either member through would mint a
694
+ // JWS whose header advertises semantics its signature was not computed
695
+ // under — a compliant verifier derives a different signing input (or
696
+ // refuses the critical header). Refused until those semantics are
697
+ // actually implemented.
698
+ if (opts.header !== undefined && opts.header !== null &&
699
+ (Object.prototype.hasOwnProperty.call(opts.header, "b64") ||
700
+ Object.prototype.hasOwnProperty.call(opts.header, "crit"))) {
701
+ throw new AuthError("auth-jwt-external/sign-unsupported-header",
702
+ "jws.sign: header members 'b64' (RFC 7797 unencoded payload) and 'crit' " +
703
+ "(RFC 7515 §4.1.11 critical extensions) are not supported — the signer " +
704
+ "always base64url-encodes the payload and implements no critical extensions");
705
+ }
706
+
707
+ var key = _toPrivateKey(opts.privateKey, "jws.sign");
708
+ var alg = _resolveSignAlg(opts.alg, key, "jws.sign");
709
+
710
+ // Extra protected-header members first (alg/typ/kid reserved so a
711
+ // caller-supplied header object can never override the signer-set alg —
712
+ // the canonical alg-substitution shape), then the reserved members.
713
+ var header = validateOpts.assignOwnEnumerable({}, opts.header, ["alg", "typ", "kid"]);
714
+ header.alg = alg;
715
+ if (opts.typ !== undefined && opts.typ !== null) header.typ = opts.typ;
716
+ if (opts.kid !== undefined && opts.kid !== null) header.kid = opts.kid;
717
+
718
+ return _signCompactJws(header, claims, key, alg);
719
+ }
720
+
515
721
  module.exports = {
516
722
  verifyExternal: verifyExternal,
723
+ signExternal: signExternal,
517
724
  SUPPORTED_CLASSICAL_ALGS: SUPPORTED_CLASSICAL_ALGS,
518
725
  REFUSED_ALGS: REFUSED_ALGS,
519
726
  // Shared JOSE defenses — routed from oauth.verifyIdToken /
520
727
  // oid4vci proof verify / sd-jwt-vc.verify / openid-federation.
521
728
  _assertAlgKtyMatch: _assertAlgKtyMatch,
522
729
  _issuerMatches: _issuerMatches,
730
+ // Classical-JWS signer internals — composed by oauth.js's attestation
731
+ // builders so the alg-from-key + compact-JWS bodies live in exactly one
732
+ // place (the classical-JOSE domain owner).
733
+ _toPrivateKey: _toPrivateKey,
734
+ _resolveSignAlg: _resolveSignAlg,
735
+ _signCompactJws: _signCompactJws,
523
736
  };
package/lib/auth/oauth.js CHANGED
@@ -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 ID-token
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
- if (!value) {
529
- throw new OAuthError("auth-oauth/attestation-no-key", label + ": privateKey is required");
530
- }
531
- if (value instanceof nodeCrypto.KeyObject) return value;
532
- try {
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`, infer the default that matches the key type so a
552
- // non-EC attester key (RSA, Ed25519) yields a self-consistent JWS — header
553
- // alg ⇄ signature key — instead of a fixed `ES256` header signed with the
554
- // real key, which `verifyClientAttestation`'s alg/kty cross-check would
555
- // then reject. An explicit alg incompatible with the key is refused BEFORE
556
- // signing rather than producing a self-invalid attestation.
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
- var kty = privateKey.asymmetricKeyType;
559
- var defaultAlg, compatible;
560
- if (kty === "ec") {
561
- var curve = (privateKey.asymmetricKeyDetails && privateKey.asymmetricKeyDetails.namedCurve) || "";
562
- defaultAlg = _EC_CURVE_ALG[curve];
563
- if (!defaultAlg) {
564
- throw new OAuthError("auth-oauth/attestation-key-unsupported",
565
- label + ": EC curve '" + curve + "' has no attestation JWS alg (use P-256 / P-384 / P-521)");
566
- }
567
- compatible = [defaultAlg]; // an EC curve pins exactly one ES alg
568
- } else if (kty === "rsa") {
569
- defaultAlg = "RS256";
570
- compatible = ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"];
571
- } else if (kty === "rsa-pss") {
572
- defaultAlg = "PS256";
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
- var params = _attestationCryptoParams(alg);
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 (no prototype-pollution: only own enumerable keys, reserved
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
- var ck = Object.keys(aopts.extraClaims);
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
- var body = new URLSearchParams();
2019
- body.set("response_type", "code");
2020
- body.set("client_id", clientId);
2021
- body.set("redirect_uri", redirectUri);
2022
- body.set("scope", scope.join(" "));
2023
- body.set("state", state);
2024
- if (nonce) body.set("nonce", nonce);
2025
- body.set("code_challenge", pkceVals.challenge);
2026
- body.set("code_challenge_method", "S256");
2027
- if (responseMode) body.set("response_mode", responseMode);
2028
- if (uopts.prompt) body.set("prompt", uopts.prompt);
2029
- if (uopts.loginHint) body.set("login_hint", uopts.loginHint);
2030
- if (uopts.maxAge != null) body.set("max_age", String(uopts.maxAge));
2031
- if (clientSecret) body.set("client_secret", clientSecret);
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
- body.set("authorization_details", JSON.stringify(requestedAuthzDetails));
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++) body.set(ek[i], String(uopts.extraParams[ek[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
 
package/lib/compliance.js CHANGED
@@ -1580,9 +1580,46 @@ function fipsMode(enable) {
1580
1580
  return STATE.fipsMode;
1581
1581
  }
1582
1582
 
1583
+ // Postures whose jurisdictions restrict cross-border data transfer
1584
+ // (GDPR Art 44-46 / UK-GDPR / DPDP §16 / PIPL Art 38 / LGPD Art 33 /
1585
+ // APPI Art 28 / PDPA §26). The residency write gates (db-query local,
1586
+ // external-db backend/replica) refuse mismatched writes under these;
1587
+ // other postures observe-and-audit only.
1588
+ var CROSS_BORDER_REGULATED_POSTURES = Object.freeze([
1589
+ "gdpr", "uk-gdpr", "dpdp", "pipl-cn", "lgpd-br", "appi-jp", "pdpa-sg",
1590
+ ]);
1591
+
1592
+ /**
1593
+ * @primitive b.compliance.isCrossBorderRegulated
1594
+ * @signature b.compliance.isCrossBorderRegulated(posture)
1595
+ * @since 0.14.24
1596
+ * @compliance gdpr
1597
+ * @related b.compliance.current, b.cryptoField.declarePerRowResidency
1598
+ *
1599
+ * Returns true when `posture` is one of the cross-border regulated
1600
+ * postures (gdpr / uk-gdpr / dpdp / pipl-cn / lgpd-br / appi-jp /
1601
+ * pdpa-sg) — the jurisdictions whose transfer restrictions flip the
1602
+ * data-residency write gates from advisory to refusing. The set
1603
+ * itself is exported as `CROSS_BORDER_REGULATED_POSTURES`; this
1604
+ * helper is the membership test the local (`b.db.from`) and external
1605
+ * (`b.externalDb.query`) gates share. Non-string and unknown postures
1606
+ * return false.
1607
+ *
1608
+ * @example
1609
+ * b.compliance.isCrossBorderRegulated("gdpr"); // → true
1610
+ * b.compliance.isCrossBorderRegulated("soc2"); // → false
1611
+ * b.compliance.isCrossBorderRegulated(null); // → false
1612
+ */
1613
+ function isCrossBorderRegulated(posture) {
1614
+ if (typeof posture !== "string" || posture.length === 0) return false;
1615
+ return CROSS_BORDER_REGULATED_POSTURES.indexOf(posture) !== -1;
1616
+ }
1617
+
1583
1618
  module.exports = {
1584
1619
  set: set,
1585
1620
  current: current,
1621
+ isCrossBorderRegulated: isCrossBorderRegulated,
1622
+ CROSS_BORDER_REGULATED_POSTURES: CROSS_BORDER_REGULATED_POSTURES,
1586
1623
  assert: assert,
1587
1624
  clear: clear,
1588
1625
  describe: describe,