@blamejs/core 0.8.64 → 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 +3 -0
- package/lib/auth/ciba.js +13 -5
- package/lib/auth/saml.js +69 -4
- package/lib/session.js +130 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,9 @@ 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`).
|
|
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.
|
|
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.
|
|
11
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.
|
|
12
15
|
- v0.8.63 (2026-05-10) — CI green-up for v0.8.62. The v0.8.62 npm-publish workflow's lint gate flagged ten ESLint findings the local pre-ship audit missed (eslint not run before commit): unused vars (`openTag` in xml-c14n; `cache` + unused `C` import + `DEFAULT_CHAIN_TTL_MS` in openid-federation; `safeJson` in oid4vp; `sdJwtVcCore` + `SUPPORTED_PROOF_TYPES` in oid4vci), an unnecessary `\-` escape in xml-c14n's name-character regex, and a control-character regex in ciba's binding_message validator (eslint's `no-control-regex` refuses control-char ranges in regex literals regardless of `\u` escaping). Fixed by removing the dead vars + dead escape, and replacing the regex with an explicit codepoint scan in `_validateBindingMessage` that walks `msg.charCodeAt(i)` and refuses C0 / DEL+C1 / zero-width / bidi-mark / bidi-isolate / BOM ranges. No primitive surface change versus v0.8.62.
|
|
13
16
|
- v0.8.62 (2026-05-10) — federation / VC primitive family + standalone DB-file lifecycle + anonymous-session ergonomics. **OpenID Connect Front-Channel + Back-Channel Logout 1.0** on `b.auth.oauth`: `parseFrontchannelLogoutRequest(req)` validates iss + sid query params; `verifyBackchannelLogoutToken(jwt, vopts)` verifies the JWS (typ=logout+jwt), validates the events claim per OIDC §2.6, refuses logout-tokens carrying nonce, requires sub OR sid, and runs an operator-supplied `seen({jti,iss,iat})` callback for jti-replay defense. Discovery surface extended to `check_session_iframe` + `backchannel_authentication_endpoint`. **CIBA Core 1.0** at `b.auth.ciba.client.create({ deliveryMode: "poll"|"ping"|"push", ... })` with `startAuthentication` / `pollToken` / `parseNotification`; supports JWT-bearer / mTLS / shared-secret client auth, binding_message + acr_values + requested_expiry, ping/push notification token (timing-safe compared via sha3 hash), and the urn:openid:params:grant-type:ciba grant. **OpenID4VCI 1.0** at `b.auth.oid4vci.issuer.create({ sdJwtIssuer, supportedCredentials, ... })` — issuer-initiated credential_offer with pre-authorized_code + tx_code, /token grant exchange, /credential endpoint with proof-JWT verification (typ=openid4vci-proof+jwt, iat freshness, nonce-replay defense, holder-key binding via header.jwk + JWS verify), c_nonce rotation per request, /.well-known/openid-credential-issuer metadata. Composes `b.auth.sdJwtVc.issuer` for SD-JWT VC minting. **OpenID4VP 1.0 + DCQL** at `b.auth.oid4vp.verifier.create({ ... })` — `createRequest` builds a vp_token authz request with a DCQL query; `verifyResponse` parses the wallet's vp_token, runs each presentation through `b.auth.sdJwtVc.verify` (audience + nonce + KB-JWT bound, optional key_attestation verifier), then runs the DCQL matcher. DCQL implementation covers credentials[] (id / format / meta vct_values / meta issuer_values / claims with path + values) and credential_sets[] (options + required) per OID4VP §6. **OpenID Federation 1.0** at `b.auth.openidFederation.{parseEntityStatement, verifyEntityStatement, buildTrustChain, applyMetadataPolicy, resolveLeaf}` — fetches entity configs from `<entity>/.well-known/openid-federation`, walks `authority_hints` up to a trust anchor (operator-pinned JWKS), verifies each subordinate-statement JWS, and applies the federation's metadata_policy (value / default / add / one_of / subset_of / superset_of / essential — unknown operators refuse). **SAML 2.0 SP** at `b.auth.saml.sp.create({ entityId, idpEntityId, idpCertPem, ... })` — AuthnRequest builder for HTTP-Redirect/POST bindings, Response parser that verifies XMLDSig (Response-level OR Assertion-level signature), defends against signature-wrapping via `b.xmlC14n.canonicalizeElementById`'s single-match invariant, validates SubjectConfirmation Bearer (NotOnOrAfter / NotBefore / Recipient / InResponseTo) + Conditions audience + Status. Plus `b.auth.saml.fetchMdq({ baseUrl, entityId, trustCertPem? })` for MDQ-style metadata fetch with optional XMLDSig verification. **`b.xmlC14n`** — RFC 3741 Exclusive XML Canonicalization 1.0 (SAML/XMLDSig subset): `canonicalize(input, opts?)` and `canonicalizeElementById(xml, id, opts?)`. The `canonicalizeElementById` single-match invariant is the core defense against XML signature-wrapping attacks (refuses ID collisions + zero-match references). Doctype + ENTITY refused at parse time. **SD-JWT VC `key_attestation` extension** — `b.auth.sdJwtVc.holder.store({..., keyAttestation })` persists a holder-side attestation JWT alongside the credential; `b.auth.sdJwtVc.present({..., keyAttestation })` embeds it in the KB-JWT header; `b.auth.sdJwtVc.verify(presentation, { keyAttestationVerifier, requireKeyAttestation })` surfaces the attestation token to an operator-supplied verifier so trust-anchor decisions (TEE / FIDO MDS3 / App Attest / Play Integrity) stay operator-side. **`b.db.fileLifecycle({ dataDir, vault, ... })`** — standalone encrypted-DB-file lifecycle for consumers that own their own SQLite handle (own schema, own migrations, own connection). Decrypts `<dataDir>/db.enc` to a tmpfs path (`/dev/shm` on Linux), exposes `dbPath` for the operator to open, runs a periodic re-encrypt flush via `startFlushTimer(db)`, returns an in-memory snapshot via `snapshot(db)`, and runs a graceful flush + cleanup via `flushAndCleanup(db, opts)`. Same envelope shape as `b.db`; no schema / audit-chain coupling. **`b.session.create({ anonymous: true })`** auto-mints `userId = "anon:" + crypto.randomUUID()` so operators running pre-login flows keep the full sealed-cookie + sealed-userId + sidHash + idle/absolute-timeout posture without rolling their own opaque-id pattern. `b.session.isAnonymous(userId)` helper for post-auth gates. `destroyAllForUser` refuses anon-prefix ids (per-session, not portable). **`validateOpts.makeNamespacedEmitters(prefix, { audit, observability })`** — collapses the recurring per-primitive `_emitAudit / _emitMetric` boilerplate into one call; new federation/VC primitives consume it. Wiki e2e regex updated for the v0.8.61 sealed-cookie format (`vault:<base64>` instead of pre-v0.8.61 hex).
|
package/lib/auth/ciba.js
CHANGED
|
@@ -237,7 +237,9 @@ function create(opts) {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
async function _postForm(url, body, headers) {
|
|
240
|
-
safeUrl.
|
|
240
|
+
safeUrl.parse(url, {
|
|
241
|
+
allowedProtocols: opts.allowHttp === true ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
|
|
242
|
+
});
|
|
241
243
|
var hc = httpClient();
|
|
242
244
|
var basic = _basicAuthHeader();
|
|
243
245
|
var hdrs = Object.assign({
|
|
@@ -246,10 +248,16 @@ function create(opts) {
|
|
|
246
248
|
}, headers || {});
|
|
247
249
|
if (basic) hdrs["Authorization"] = basic;
|
|
248
250
|
var req = {
|
|
249
|
-
url:
|
|
250
|
-
method:
|
|
251
|
-
headers:
|
|
252
|
-
body:
|
|
251
|
+
url: url,
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: hdrs,
|
|
254
|
+
body: body.toString(),
|
|
255
|
+
// OAuth / CIBA 4xx responses carry structured error JSON
|
|
256
|
+
// (`{ error, error_description }`) the framework inspects to
|
|
257
|
+
// raise the right `auth-ciba/<code>` AuthError below. Default
|
|
258
|
+
// http-client behavior throws on >=400 — opt into resolve-and-
|
|
259
|
+
// surface so the body reaches us.
|
|
260
|
+
responseMode: "always-resolve",
|
|
253
261
|
};
|
|
254
262
|
Object.assign(req, opts.httpClientOpts || {});
|
|
255
263
|
if (opts.allowHttp === true) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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/lib/session.js
CHANGED
|
@@ -885,6 +885,135 @@ async function rotate(oldToken, opts) {
|
|
|
885
885
|
return { token: _sealCookieToken(newSid), expiresAt: expiresAt };
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
+
/**
|
|
889
|
+
* @primitive b.session.updateData
|
|
890
|
+
* @signature b.session.updateData(token, data, opts?)
|
|
891
|
+
* @since 0.8.66
|
|
892
|
+
* @related b.session.verify, b.session.rotate
|
|
893
|
+
*
|
|
894
|
+
* Update the sealed `data` payload on a session WITHOUT rotating the
|
|
895
|
+
* sid. Use cases: cart-state writes, user-preference flips, step-up-
|
|
896
|
+
* auth completion flags, fingerprint-anomaly score updates. Anything
|
|
897
|
+
* that doesn't change the security boundary (login transition, role
|
|
898
|
+
* escalation, multifactor verified) — those still go through
|
|
899
|
+
* `b.session.rotate({ data })` so the sid moves and any pre-login
|
|
900
|
+
* tokens an attacker may have planted become invalid.
|
|
901
|
+
*
|
|
902
|
+
* Default semantics:
|
|
903
|
+
* - `data` REPLACES the existing payload (full overwrite). The
|
|
904
|
+
* reserved `__bj_fingerprint` key is preserved automatically so
|
|
905
|
+
* fingerprint-binding survives the update.
|
|
906
|
+
* - `lastActivity` is bumped (idle-timeout reset) unless
|
|
907
|
+
* `opts.touchLastActivity: false`.
|
|
908
|
+
* - The session must be live (not expired) for the write to land;
|
|
909
|
+
* returns `false` for unknown / expired tokens.
|
|
910
|
+
*
|
|
911
|
+
* Pass `opts.merge: true` to deep-merge top-level keys into the
|
|
912
|
+
* existing payload instead of replacing — useful for incremental
|
|
913
|
+
* writes where the operator doesn't want to round-trip read+merge
|
|
914
|
+
* themselves. Inner objects merge ONE LEVEL DEEP; arrays REPLACE.
|
|
915
|
+
*
|
|
916
|
+
* Leader-only.
|
|
917
|
+
*
|
|
918
|
+
* @opts
|
|
919
|
+
* {
|
|
920
|
+
* merge?: boolean, // default false (full replace)
|
|
921
|
+
* touchLastActivity?: boolean, // default true
|
|
922
|
+
* }
|
|
923
|
+
*
|
|
924
|
+
* @example
|
|
925
|
+
* // Replace the data payload entirely.
|
|
926
|
+
* await b.session.updateData(req.cookies.sid, { roles: ["admin"], theme: "dark" });
|
|
927
|
+
*
|
|
928
|
+
* // Merge a single field without disturbing the rest of the payload.
|
|
929
|
+
* await b.session.updateData(req.cookies.sid,
|
|
930
|
+
* { stepUpAt: Date.now() }, { merge: true });
|
|
931
|
+
* // → true
|
|
932
|
+
*/
|
|
933
|
+
async function updateData(token, data, opts) {
|
|
934
|
+
cluster.requireLeader();
|
|
935
|
+
opts = opts || {};
|
|
936
|
+
if (typeof token !== "string" || token.length === 0) return false;
|
|
937
|
+
if (data !== null && (typeof data !== "object" || Array.isArray(data))) {
|
|
938
|
+
throw _err("INVALID_ARG",
|
|
939
|
+
"session.updateData: data must be a plain object or null", true);
|
|
940
|
+
}
|
|
941
|
+
var sid = _unsealCookieToken(token);
|
|
942
|
+
if (sid === null) return false;
|
|
943
|
+
var sidHash = _hashSid(sid);
|
|
944
|
+
var nowMs = Date.now();
|
|
945
|
+
|
|
946
|
+
// Read the live row so we can preserve __bj_fingerprint and (in
|
|
947
|
+
// merge mode) carry forward existing keys. Single SELECT + UPDATE
|
|
948
|
+
// — racing concurrent updateData calls fall through to last-write-
|
|
949
|
+
// wins on the same sid, which is the right shape for cart-style
|
|
950
|
+
// writes; operators needing strict serialization wrap with
|
|
951
|
+
// b.resourceAccessLock.
|
|
952
|
+
var row = await _currentStore().executeOne(
|
|
953
|
+
'SELECT "userId", "userIdHash", "data", "createdAt", "expiresAt", "lastActivity" ' +
|
|
954
|
+
'FROM _blamejs_sessions WHERE sidHash = ? AND expiresAt >= ?',
|
|
955
|
+
[sidHash, nowMs]
|
|
956
|
+
);
|
|
957
|
+
if (!row) return false;
|
|
958
|
+
|
|
959
|
+
// Recover the existing data + reserved fingerprint key (vault-
|
|
960
|
+
// sealed at rest). Operators that want a fresh fingerprint also
|
|
961
|
+
// call b.session.rotate; updateData preserves the binding.
|
|
962
|
+
var unsealed = cryptoField.unsealRow("_blamejs_sessions", row);
|
|
963
|
+
var existing = null;
|
|
964
|
+
var storedFingerprint = null;
|
|
965
|
+
if (unsealed.data) {
|
|
966
|
+
try {
|
|
967
|
+
existing = safeJson.parse(unsealed.data);
|
|
968
|
+
if (existing && typeof existing === "object" &&
|
|
969
|
+
typeof existing.__bj_fingerprint === "string") {
|
|
970
|
+
storedFingerprint = existing.__bj_fingerprint;
|
|
971
|
+
}
|
|
972
|
+
} catch (_e) {
|
|
973
|
+
// Decrypt-then-parse failure mirrors verify() — drop existing
|
|
974
|
+
// and proceed with the new payload only. Operator gets the
|
|
975
|
+
// same auth.session.data_unparseable signal next verify().
|
|
976
|
+
existing = null;
|
|
977
|
+
storedFingerprint = null;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Build the next payload. merge:true does a one-level deep merge
|
|
982
|
+
// into the existing object (arrays at the top level REPLACE);
|
|
983
|
+
// default replaces wholesale.
|
|
984
|
+
var next;
|
|
985
|
+
if (opts.merge === true && existing && typeof existing === "object") {
|
|
986
|
+
next = Object.assign({}, existing);
|
|
987
|
+
if (data && typeof data === "object") {
|
|
988
|
+
Object.keys(data).forEach(function (k) {
|
|
989
|
+
if (k === "__bj_fingerprint") return; // reserved — only fingerprint binding writes this
|
|
990
|
+
next[k] = data[k];
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
} else {
|
|
994
|
+
next = (data && typeof data === "object") ? Object.assign({}, data) : null;
|
|
995
|
+
if (next) delete next.__bj_fingerprint; // reserved — operator can't overwrite the binding
|
|
996
|
+
}
|
|
997
|
+
if (storedFingerprint && next) next.__bj_fingerprint = storedFingerprint;
|
|
998
|
+
|
|
999
|
+
// Re-seal the data column. cryptoField.sealRow handles the AAD
|
|
1000
|
+
// binding + sealedFields registration automatically.
|
|
1001
|
+
var sealedRow = cryptoField.sealRow("_blamejs_sessions", {
|
|
1002
|
+
data: next ? JSON.stringify(next) : null,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
var setParts = ['"data" = ?'];
|
|
1006
|
+
var setParams = [sealedRow.data];
|
|
1007
|
+
if (opts.touchLastActivity !== false) {
|
|
1008
|
+
setParts.push('"lastActivity" = ?');
|
|
1009
|
+
setParams.push(nowMs);
|
|
1010
|
+
}
|
|
1011
|
+
var sql = "UPDATE _blamejs_sessions SET " + setParts.join(", ") +
|
|
1012
|
+
" WHERE sidHash = ? AND expiresAt >= ?";
|
|
1013
|
+
var result = await _currentStore().execute(sql, setParams.concat([sidHash, nowMs]));
|
|
1014
|
+
return (result.rowCount || 0) > 0;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
888
1017
|
/**
|
|
889
1018
|
* @primitive b.session.purgeExpired
|
|
890
1019
|
* @signature b.session.purgeExpired()
|
|
@@ -1021,6 +1150,7 @@ module.exports = {
|
|
|
1021
1150
|
destroyAllForUser: destroyAllForUser,
|
|
1022
1151
|
touch: touch,
|
|
1023
1152
|
rotate: rotate,
|
|
1153
|
+
updateData: updateData,
|
|
1024
1154
|
purgeExpired: purgeExpired,
|
|
1025
1155
|
count: count,
|
|
1026
1156
|
useStore: useStore,
|
package/package.json
CHANGED
package/sbom.cyclonedx.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:7b747b8f-4f21-448f-a73e-50b8615f6937",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.67",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.8.67",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|