@blamejs/core 0.8.66 → 0.8.67

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 CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.8.x
10
10
 
11
+ - v0.8.67 (2026-05-10) — SAML XMLDSig Reference Transforms (`enveloped-signature` + per-Reference c14n) + full IdP-emitted SAML round-trip in the federation-auth integration test. Pre-v0.8.67 `b.auth.saml.sp.verifyResponse` only honored the SignedInfo's `CanonicalizationMethod`; it didn't process the `<ds:Transforms>` block on the Reference. Real-world IdP-signed responses (Keycloak, ADFS, Okta) attach `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (strip the `<Signature>` child of the referenced element before c14n) and a per-Reference `xml-exc-c14n#` Transform — without them, the digest computed over the assertion-including-signature never matches the signed-then-signature-injected reality and verifyResponse rejected legitimate IdP responses. `_verifyXmldsig` now reads the Transforms list, applies enveloped-signature by filtering the parsed-tree's `<Signature>` element children before canonicalization, and honors the per-Reference c14n choice (with vs without comments). The single-match-by-ID invariant + signature-wrapping defense moves into the saml.js path directly so the modified subtree (signature stripped) is the one that gets canonicalized + digested. `test/integration/federation-auth.test.js` now drives Keycloak's HTML login form via cookie-jar curl-equivalent (no headless browser needed), captures the IdP-signed SAMLResponse, fetches the IdP signing certificate from `/protocol/saml/descriptor`, hands the response to `sp.verifyResponse(b64, { expectedInResponseTo })`, and asserts the extracted `nameId` / `issuer` / `audience` / `inResponseTo` match the realm's signed claims. Verified end-to-end: Keycloak alice user → SAML AuthnRequest → login form POST → signed Response → `sp.verifyResponse()` → `{ nameId: "alice", issuer: <realm>, audience: <SP entityID>, attributes: { Role: "default-roles-..." } }`. Unsupported Transform algorithms refuse loudly via `auth-saml/unsupported-transform`. No primitive surface change versus v0.8.66 (the Transforms processing is internal to `_verifyXmldsig`).
11
12
  - v0.8.66 (2026-05-10) — `b.session.updateData(token, data, opts?)`. Update the sealed `data` payload on a session WITHOUT rotating the sid. Pre-v0.8.66 the only path to mutate session data was `b.session.rotate(token, { data })` which forces an sid rotation — appropriate for security-boundary transitions (login, MFA, role escalation) but heavyweight for cart-state writes / preference flips / step-up-completion flags. Default semantics: full payload replace, `lastActivity` bumped (idle-timeout reset), reserved `__bj_fingerprint` binding preserved automatically so verify() still surfaces drift correctly. `opts.merge: true` does a one-level deep merge into the existing payload; `opts.touchLastActivity: false` skips the idle-timeout bump. Returns `false` for unknown / expired / pre-v0.8.61-raw-format tokens (no throw). Anonymous-session userIds work the same as named userIds. Leader-only. New Layer 0 tests: 13 checks covering replace / merge / null-clear / fingerprint preservation / unknown-token returns / array refusal.
12
13
  - v0.8.65 (2026-05-10) — federated-authentication integration test fixture (Keycloak as OIDC OP + SAML IdP). Adds `quay.io/keycloak/keycloak:26.0` to `docker-compose.test.yml` running with realm-import on port `:18080` (HTTP) + `:18081` (Quarkus health). Realm `blamejs-test` boots with one OIDC client (`blamejs-rp-oidc`, secret `blamejs-test-rp-secret`, frontchannel + backchannel logout enabled), one SAML SP client (entityID `https://sp.blamejs-test.example`, RSA-SHA256 assertion signature), and a test user (`alice` / `blamejs-test-password`). New `test/integration/federation-auth.test.js` exercises end-to-end against the live Keycloak: OIDC discovery, `b.auth.oauth.authorizationUrl` (state + nonce + PKCE), password-grant token retrieval, `verifyIdToken` against the realm JWKS, `fetchUserInfo` with `idTokenSub` cross-check, RP-Initiated Logout URL build, `parseFrontchannelLogoutRequest` iss-mismatch refusal, `verifyBackchannelLogoutToken` JWKS-lookup + signature + typ-check failure paths, SAML `buildAuthnRequest` POST to the IdP's `/protocol/saml` endpoint, SP `metadata()` XML emit, and CIBA `startAuthentication` wire-format check (Keycloak's `backchannel_authentication_endpoint` is at `/protocol/openid-connect/ext/ciba/auth`). `b.auth.ciba._postForm` now sets `responseMode: "always-resolve"` so deterministic OAuth-shape error JSON (`{ error, error_description }`) reaches the AuthError-mapping path instead of being rejected as a generic HTTP error. `b.auth.ciba._postForm` URL validation switched from a non-existent `safeUrl.assertHttpUrl` to `safeUrl.parse({ allowedProtocols })`. `scripts/check-services.js` registers `keycloak` + `keycloak-health` (both v4 + v6); `test/helpers/services.js` exposes `URLS.keycloak`. CLAUDE.md release-workflow §6 documents the federation-auth integration test path. OID4VCI / OID4VP / OpenID Federation deferred — Keycloak's `oid4vc-issuer` SPI is preview-only and there's no entity-statement publisher in the base image.
13
14
  - v0.8.64 (2026-05-10) — CI green-up for v0.8.63 (wiki source-comment validator). The v0.8.62 push missed `examples/wiki/test/validate-source-comment-blocks.js` — a wiki-side validator that enforces every `@primitive` block has `@signature` starting with `b.`, matching function arity, an `@opts` declaration when the function takes opts, an `@example` block, and resolvable `@related` cross-refs. 50 findings across the new federation/VC primitives. Fixed: every nested-namespace `@signature` (`b.auth.ciba.client.X`, `b.auth.oid4vci.issuer.X`, `b.auth.oid4vp.verifier.X`, `b.auth.saml.sp.X`) re-qualified with the full `b.*` prefix instead of the bare `client.X` shorthand; arity collapsed to `(opts)` where the function takes a single opts arg; `@example` block added to every primitive; `@opts` block added to ciba.client.startAuthentication / parseNotification + openidFederation.resolveLeaf; `@primitive` block added to xmlC14n.parse; `@related` cross-refs scrubbed of dangling references to undocumented primitives (oauth.create / sdJwtVc.issuer aren't @primitive-documented yet); the parse-as-JS check on oid4vci's @example caught a `{...}` literal with no target — replaced with a concrete object spread. No primitive surface change versus v0.8.63.
package/lib/auth/saml.js CHANGED
@@ -191,10 +191,75 @@ function _verifyXmldsig(envelope, signatureNode, certPem) {
191
191
  throw new AuthError("auth-saml/no-digest-value", "Reference missing DigestValue");
192
192
  }
193
193
  var withComments = canonAlgo.indexOf("#WithComments") !== -1;
194
- // Single-match invariant — anti-wrapping defense
195
- var canonical = xmlC14n().canonicalizeElementById(envelope, refId, {
196
- withComments: withComments,
197
- });
194
+
195
+ // XMLDSig Reference Transforms applied in order before the digest.
196
+ // SAML responses commonly use:
197
+ // 1. http://www.w3.org/2000/09/xmldsig#enveloped-signature (strip
198
+ // the <Signature> child of the referenced element)
199
+ // 2. http://www.w3.org/2001/10/xml-exc-c14n# (canonicalize)
200
+ // Without the enveloped-signature transform, the digest is computed
201
+ // over the assertion-including-signature, which never matches the
202
+ // signed-then-signature-injected reality.
203
+ var transformsNode = _findChild(refNode, "Transforms");
204
+ var transformList = transformsNode ? _findAllChildren(transformsNode, "Transform") : [];
205
+ var stripSignature = false;
206
+ var refC14nWithComments = withComments;
207
+ for (var ti = 0; ti < transformList.length; ti++) {
208
+ var algo = _attr(transformList[ti], "Algorithm");
209
+ switch (algo) {
210
+ case "http://www.w3.org/2000/09/xmldsig#enveloped-signature":
211
+ stripSignature = true;
212
+ break;
213
+ case "http://www.w3.org/2001/10/xml-exc-c14n#":
214
+ refC14nWithComments = false;
215
+ break;
216
+ case "http://www.w3.org/2001/10/xml-exc-c14n#WithComments":
217
+ refC14nWithComments = true;
218
+ break;
219
+ default:
220
+ throw new AuthError("auth-saml/unsupported-transform",
221
+ "Unsupported Transform: " + algo + " (supported: enveloped-signature, xml-exc-c14n)");
222
+ }
223
+ }
224
+
225
+ // Locate the referenced element with the single-match invariant
226
+ // (anti-wrapping defense) — if zero or duplicate IDs match, refuse.
227
+ // We then optionally strip its <Signature> child(ren) per the
228
+ // enveloped-signature transform and canonicalize the result.
229
+ var c14n = xmlC14n();
230
+ var rootForRef = c14n.parse(envelope);
231
+ var matches = [];
232
+ (function _walk(node) {
233
+ if (node.type !== "element") return;
234
+ if (node.attrs) {
235
+ for (var ai = 0; ai < node.attrs.length; ai++) {
236
+ if (node.attrs[ai].name === "ID" && node.attrs[ai].value === refId) {
237
+ matches.push(node);
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ for (var ci = 0; ci < node.children.length; ci++) _walk(node.children[ci]);
243
+ })(rootForRef);
244
+ if (matches.length === 0) {
245
+ throw new AuthError("auth-saml/no-id-match",
246
+ "Reference URI #" + refId + " resolves to no element");
247
+ }
248
+ if (matches.length > 1) {
249
+ throw new AuthError("auth-saml/duplicate-id",
250
+ "Reference URI #" + refId + " matches " + matches.length +
251
+ " elements — refused (signature-wrapping defense)");
252
+ }
253
+ var refTarget = matches[0];
254
+ if (stripSignature) {
255
+ refTarget.children = refTarget.children.filter(function (c) {
256
+ if (c.type !== "element") return true;
257
+ var colon = c.name.indexOf(":");
258
+ var local = colon !== -1 ? c.name.substring(colon + 1) : c.name;
259
+ return local !== "Signature";
260
+ });
261
+ }
262
+ var canonical = c14n.canonicalize(refTarget, { withComments: refC14nWithComments });
198
263
  var actualDigest = nodeCrypto.createHash(SUPPORTED_DIGEST[digestAlgo]).update(canonical).digest();
199
264
  if (Buffer.from(expectedDigestB64, "base64").compare(actualDigest) !== 0) {
200
265
  throw new AuthError("auth-saml/digest-mismatch",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.66",
3
+ "version": "0.8.67",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:af468756-ee7e-474b-aa3e-0e24106e1792",
5
+ "serialNumber": "urn:uuid:7b747b8f-4f21-448f-a73e-50b8615f6937",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-10T06:52:11.439Z",
8
+ "timestamp": "2026-05-10T14:43:14.112Z",
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.8.66",
22
+ "bom-ref": "@blamejs/core@0.8.67",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.66",
25
+ "version": "0.8.67",
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.8.66",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.67",
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.8.66",
57
+ "ref": "@blamejs/core@0.8.67",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]