@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 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.assertHttpUrl(url, opts.allowHttp === true);
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: url,
250
- method: "POST",
251
- headers: hdrs,
252
- body: body.toString(),
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
- // 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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.64",
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:23c254f3-eb31-48e9-84e5-285c0cd30ffe",
5
+ "serialNumber": "urn:uuid:7b747b8f-4f21-448f-a73e-50b8615f6937",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-10T06:15:43.727Z",
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.64",
22
+ "bom-ref": "@blamejs/core@0.8.67",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.64",
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.64",
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.64",
57
+ "ref": "@blamejs/core@0.8.67",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]