@blamejs/blamejs-shop 0.4.85 → 0.4.86

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/order.js +4 -2
  4. package/lib/security-middleware.js +1 -0
  5. package/lib/vendor/MANIFEST.json +71 -39
  6. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  7. package/lib/vendor/blamejs/api-snapshot.json +11 -3
  8. package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
  9. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
  10. package/lib/vendor/blamejs/lib/auth/password.js +1 -0
  11. package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
  12. package/lib/vendor/blamejs/lib/daemon.js +4 -1
  13. package/lib/vendor/blamejs/lib/external-db.js +131 -0
  14. package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
  15. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
  16. package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
  17. package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
  18. package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
  19. package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
  20. package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
  21. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
  22. package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
  23. package/lib/vendor/blamejs/lib/mail.js +39 -0
  24. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
  25. package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
  26. package/lib/vendor/blamejs/lib/network-dns.js +47 -2
  27. package/lib/vendor/blamejs/lib/network-nts.js +16 -0
  28. package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
  29. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
  30. package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
  31. package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
  32. package/lib/vendor/blamejs/lib/outbox.js +29 -0
  33. package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
  34. package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
  35. package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
  36. package/lib/vendor/blamejs/lib/session.js +85 -28
  37. package/lib/vendor/blamejs/package.json +1 -1
  38. package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
  45. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
  57. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
  58. package/lib/webhooks.js +38 -9
  59. package/package.json +1 -1
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * b.* NTS-KE handshake byte-cap.
4
+ *
5
+ * The NTS-KE (RFC 8915) key-establishment handshake reader accumulates
6
+ * received bytes until a REC_END record terminates the exchange. A
7
+ * wall-clock timer bounds the handshake by time, but NOT by memory: a
8
+ * server that streams non-END records fast enough OOMs the process
9
+ * before the timer fires. The reader must cap the accumulated buffer at
10
+ * a sane ceiling (64 KiB) and fail closed with an nts/* typed error.
11
+ *
12
+ * This drives the real consumer path — network-nts.performKeHandshake
13
+ * against a live loopback TLS server negotiating ALPN "ntske/1" that
14
+ * streams > 64 KiB of non-END records.
15
+ */
16
+
17
+ var helpers = require("../helpers");
18
+ var check = helpers.check;
19
+ var nts = require("../../lib/network-nts");
20
+ var nodeTls = require("node:tls");
21
+
22
+ var REC_NEW_COOKIE = 5;
23
+
24
+ // Encode an NTS-KE record: u16 type (top bit = critical) || u16 length
25
+ // || body. We emit non-critical NEW_COOKIE records that never include a
26
+ // REC_END (type 0), so a conformant reader keeps accumulating.
27
+ function _encodeRecord(type, body) {
28
+ var hdr = Buffer.alloc(4);
29
+ hdr.writeUInt16BE(type & 0x7fff, 0);
30
+ hdr.writeUInt16BE(body.length, 2);
31
+ return Buffer.concat([hdr, body]);
32
+ }
33
+
34
+ function testHandshakeByteCapFailsClosed() {
35
+ return new Promise(function (resolve, reject) {
36
+ (async function () {
37
+ var server = null;
38
+ try {
39
+ var ca = await helpers.b.mtlsEngine.generateCa({ name: "nts-test-ca" });
40
+ var leaf = await helpers.b.mtlsEngine.signClientCert({
41
+ cn: "localhost",
42
+ caCertPem: ca.caCertPem,
43
+ caKeyPem: ca.caKeyPem,
44
+ usage: "server",
45
+ sans: ["DNS:localhost", "IP:127.0.0.1"],
46
+ validityDays: 1,
47
+ });
48
+
49
+ // One ~1 KiB non-END record; streamed enough times to exceed the
50
+ // 64 KiB ceiling without ever sending REC_END.
51
+ var oneRecord = _encodeRecord(REC_NEW_COOKIE, Buffer.alloc(1020, 0x41));
52
+ var TOTAL_BYTES = 128 * 1024; // 2x the 64 KiB cap
53
+
54
+ server = nodeTls.createServer({
55
+ key: leaf.key,
56
+ cert: leaf.cert,
57
+ minVersion: "TLSv1.3",
58
+ maxVersion: "TLSv1.3",
59
+ ALPNProtocols: ["ntske/1"],
60
+ }, function (sock) {
61
+ sock.on("error", function () { /* client tears down mid-stream */ });
62
+ var sent = 0;
63
+ function pump() {
64
+ while (sent < TOTAL_BYTES) {
65
+ sent += oneRecord.length;
66
+ if (!sock.write(oneRecord)) {
67
+ sock.once("drain", pump);
68
+ return;
69
+ }
70
+ }
71
+ }
72
+ pump();
73
+ });
74
+
75
+ server.listen(0, "127.0.0.1", function () {
76
+ var port = server.address().port;
77
+ var startedAt = Date.now();
78
+ nts.performKeHandshake({
79
+ host: "127.0.0.1",
80
+ port: port,
81
+ servername: "localhost",
82
+ ca: ca.caCertPem,
83
+ // Generous timeout: a passing run must reject on the byte cap
84
+ // FAST (well under this), proving the cap — not the timer —
85
+ // bounded the read.
86
+ timeoutMs: 30000,
87
+ }).then(function () {
88
+ server.close();
89
+ reject(new Error("handshake resolved despite > 64 KiB of non-END records"));
90
+ }).catch(function (e) {
91
+ var elapsed = Date.now() - startedAt;
92
+ try { server.close(); } catch (_e) { /* best-effort */ }
93
+ try {
94
+ check("handshake rejects on oversized stream",
95
+ e && e.code === "nts/ke-too-large");
96
+ check("rejection is NtsError typed",
97
+ e instanceof nts.NtsError);
98
+ check("rejection is bounded, not the wall-clock timeout",
99
+ e.code !== "nts/ke-timeout");
100
+ check("bounded fast (well under the 30s timer)",
101
+ elapsed < 15000);
102
+ resolve();
103
+ } catch (assertErr) {
104
+ reject(assertErr);
105
+ }
106
+ });
107
+ });
108
+
109
+ server.on("error", function (e) { reject(e); });
110
+ } catch (e) {
111
+ if (server) { try { server.close(); } catch (_e) { /* best-effort */ } }
112
+ reject(e);
113
+ }
114
+ })();
115
+ });
116
+ }
117
+
118
+ async function run() {
119
+ await testHandshakeByteCapFailsClosed();
120
+ }
121
+
122
+ module.exports = { run: run };
123
+
124
+ if (require.main === module) {
125
+ run().then(function () { console.log("OK"); })
126
+ .catch(function (e) { console.error(e.stack || e); process.exit(1); });
127
+ }
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ /**
3
+ * b.auth.saml.sp.verifyResponse — Bearer SubjectConfirmationData
4
+ * NotBefore must fail closed when present-but-unparseable.
5
+ *
6
+ * SAML 2.0 Web Browser SSO Profile §4.1.4.2: a Bearer
7
+ * SubjectConfirmationData MAY carry a `NotBefore` bounding the start of
8
+ * its validity window. When NotBefore is PRESENT, the verifier must
9
+ * honor it. A NotBefore that cannot be parsed (Date.parse → NaN) is an
10
+ * IdP error or a tampered attribute — it cannot establish that the
11
+ * confirmation has begun, so it must NOT be silently dropped, leaving
12
+ * the SCD accepted. It must fail closed (the confirmation is skipped,
13
+ * the verifier tries the next one, and an assertion with no other valid
14
+ * confirmation is refused) — mirroring the NotOnOrAfter line just above
15
+ * and the already-hardened Conditions block.
16
+ *
17
+ * These tests drive the shipped consumer path:
18
+ * sp = b.auth.saml.sp.create({ ... }); sp.verifyResponse(b64, vopts).
19
+ * They mint a self-signed RSA IdP cert, build a genuinely XMLDSig-
20
+ * signed SAML Response (digest + SignatureValue computed through the
21
+ * framework's own b.xmlC14n so the verifier's recomputation matches),
22
+ * and vary ONLY the SubjectConfirmationData's NotBefore between the
23
+ * accepted and refused cases.
24
+ */
25
+
26
+ var helpers = require("../helpers");
27
+ var check = helpers.check;
28
+ var b = helpers.b;
29
+ var nodeCrypto = require("node:crypto");
30
+ var c14n = require("../../lib/xml-c14n");
31
+
32
+ var DS = "http://www.w3.org/2000/09/xmldsig#";
33
+ var EXC = "http://www.w3.org/2001/10/xml-exc-c14n#";
34
+
35
+ var IDP_ENTITY_ID = "https://idp.example";
36
+ var SP_ENTITY_ID = "https://sp.example";
37
+ var ACS_URL = "https://sp.example/saml/acs";
38
+
39
+ // Mint a self-signed RSA cert via the vendored @peculiar/x509 bundle.
40
+ // verifyResponse parses idpCertPem with nodeCrypto.createPublicKey and
41
+ // verifies an rsa-sha256 PKCS1 signature, so a real RSA cert + matching
42
+ // private key is required — there is no test bypass of the signature
43
+ // check.
44
+ async function _mintRsaCert(cn) {
45
+ var pki = require("../../lib/vendor/pki.cjs");
46
+ var x509 = pki.x509;
47
+ var keys = await nodeCrypto.webcrypto.subtle.generateKey(
48
+ { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, // allow:raw-byte-literal — RFC 8301 §3.1 RSA bit floor
49
+ publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
50
+ true, ["sign", "verify"]);
51
+ var now = new Date();
52
+ var cert = await x509.X509CertificateGenerator.createSelfSigned({
53
+ serialNumber: "01",
54
+ name: "CN=" + cn,
55
+ notBefore: now,
56
+ notAfter: new Date(now.getTime() + 365 * 24 * 3600 * 1000), // allow:raw-time-literal — 1y fixture validity
57
+ signingAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
58
+ keys: keys,
59
+ });
60
+ var pkcs8 = await nodeCrypto.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
61
+ var keyPem = "-----BEGIN PRIVATE KEY-----\n" +
62
+ Buffer.from(pkcs8).toString("base64").match(/.{1,64}/g).join("\n") +
63
+ "\n-----END PRIVATE KEY-----\n";
64
+ return { certPem: cert.toString("pem"), keyPem: keyPem };
65
+ }
66
+
67
+ function _isoFromNow(ms) { return new Date(Date.now() + ms).toISOString(); }
68
+
69
+ // Build a SAML Response with an Assertion-level enveloped XMLDSig
70
+ // signature. The Assertion's Signature child is stripped before the
71
+ // digest (the enveloped-signature transform), and both the digest and
72
+ // the SignedInfo are canonicalized through b.xmlC14n so the values
73
+ // match exactly what verifyResponse recomputes.
74
+ function _buildSignedResponse(idp, parts) {
75
+ var assertionId = "_assertion-" + parts.tag;
76
+ var responseId = "_response-" + parts.tag;
77
+ var issueInstant = _isoFromNow(0);
78
+ var subjectConfirmation =
79
+ "<saml:SubjectConfirmation Method=\"" + parts.method + "\">" +
80
+ parts.scd +
81
+ "</saml:SubjectConfirmation>";
82
+
83
+ var assertionInner =
84
+ "<saml:Issuer>" + IDP_ENTITY_ID + "</saml:Issuer>" +
85
+ "SIGNATURE_PLACEHOLDER" +
86
+ "<saml:Subject>" +
87
+ "<saml:NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">" +
88
+ parts.nameId + "</saml:NameID>" +
89
+ subjectConfirmation +
90
+ "</saml:Subject>" +
91
+ (parts.conditions !== undefined ? parts.conditions :
92
+ "<saml:Conditions NotBefore=\"" + _isoFromNow(-5 * 60 * 1000) + // allow:raw-time-literal — 5m skew window
93
+ "\" NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\">" + // allow:raw-time-literal — 5m skew window
94
+ "<saml:AudienceRestriction><saml:Audience>" + SP_ENTITY_ID +
95
+ "</saml:Audience></saml:AudienceRestriction>" +
96
+ "</saml:Conditions>") +
97
+ "<saml:AuthnStatement SessionIndex=\"_sess-1\" AuthnInstant=\"" + issueInstant + "\">" +
98
+ "<saml:AuthnContext><saml:AuthnContextClassRef>" +
99
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" +
100
+ "</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>";
101
+
102
+ var assertionOpen =
103
+ "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
104
+ "ID=\"" + assertionId + "\" Version=\"2.0\" IssueInstant=\"" + issueInstant + "\">";
105
+ var assertionClose = "</saml:Assertion>";
106
+
107
+ // Digest the assertion with no Signature child (enveloped-signature
108
+ // transform output), canonicalized through b.xmlC14n.
109
+ var assertionNoSig = assertionOpen +
110
+ assertionInner.replace("SIGNATURE_PLACEHOLDER", "") + assertionClose;
111
+ var digest = nodeCrypto.createHash("sha256")
112
+ .update(c14n.canonicalize(assertionNoSig)).digest("base64");
113
+
114
+ var signedInfo =
115
+ "<ds:SignedInfo xmlns:ds=\"" + DS + "\">" +
116
+ "<ds:CanonicalizationMethod Algorithm=\"" + EXC + "\"></ds:CanonicalizationMethod>" +
117
+ "<ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"></ds:SignatureMethod>" +
118
+ "<ds:Reference URI=\"#" + assertionId + "\">" +
119
+ "<ds:Transforms>" +
120
+ "<ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"></ds:Transform>" +
121
+ "<ds:Transform Algorithm=\"" + EXC + "\"></ds:Transform>" +
122
+ "</ds:Transforms>" +
123
+ "<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"></ds:DigestMethod>" +
124
+ "<ds:DigestValue>" + digest + "</ds:DigestValue>" +
125
+ "</ds:Reference>" +
126
+ "</ds:SignedInfo>";
127
+ var priv = nodeCrypto.createPrivateKey({ key: idp.keyPem, format: "pem" });
128
+ var sigValue = nodeCrypto.sign("sha256", c14n.canonicalize(signedInfo),
129
+ { key: priv, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }).toString("base64");
130
+
131
+ var signatureXml =
132
+ "<ds:Signature xmlns:ds=\"" + DS + "\">" + signedInfo +
133
+ "<ds:SignatureValue>" + sigValue + "</ds:SignatureValue></ds:Signature>";
134
+ var assertionFull = assertionOpen +
135
+ assertionInner.replace("SIGNATURE_PLACEHOLDER", signatureXml) + assertionClose;
136
+
137
+ var response =
138
+ "<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
139
+ "xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
140
+ "ID=\"" + responseId + "\" Version=\"2.0\" IssueInstant=\"" + issueInstant + "\" " +
141
+ "Destination=\"" + ACS_URL + "\">" +
142
+ "<saml:Issuer>" + IDP_ENTITY_ID + "</saml:Issuer>" +
143
+ "<samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status>" +
144
+ assertionFull + "</samlp:Response>";
145
+
146
+ return Buffer.from(response, "utf8").toString("base64");
147
+ }
148
+
149
+ function _bearerScd(notBeforeAttr, inResponseTo) {
150
+ return "<saml:SubjectConfirmationData" +
151
+ " NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\"" + // allow:raw-time-literal — 5m future, otherwise-valid window
152
+ notBeforeAttr +
153
+ " Recipient=\"" + ACS_URL + "\"" +
154
+ " InResponseTo=\"" + inResponseTo + "\"/>";
155
+ }
156
+
157
+ function _newSp(idp) {
158
+ return b.auth.saml.sp.create({
159
+ entityId: SP_ENTITY_ID,
160
+ assertionConsumerServiceUrl: ACS_URL,
161
+ idpEntityId: IDP_ENTITY_ID,
162
+ idpSsoUrl: "https://idp.example/sso",
163
+ idpCertPem: idp.certPem,
164
+ });
165
+ }
166
+
167
+ function _verifyThrows(sp, b64, vopts) {
168
+ try { sp.verifyResponse(b64, vopts); return null; }
169
+ catch (e) { return e.code || e.message; }
170
+ }
171
+
172
+ // Case 1 — a past (already-valid) NotBefore succeeds. Proves the signer
173
+ // + harness are correct and the fail-closed fix does not break the happy
174
+ // path (an otherwise-acceptable confirmation that has begun).
175
+ async function testBearerPastNotBeforeAccepted() {
176
+ var idp = await _mintRsaCert("idp.example");
177
+ var sp = _newSp(idp);
178
+ var inResponseTo = "_req-past-nb";
179
+ var b64 = _buildSignedResponse(idp, {
180
+ tag: "bearer-past-nb",
181
+ method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
182
+ nameId: "alice@example.com",
183
+ scd: _bearerScd(" NotBefore=\"" + _isoFromNow(-5 * 60 * 1000) + "\"", inResponseTo), // allow:raw-time-literal — 5m past, already valid
184
+ });
185
+ var info = sp.verifyResponse(b64, { expectedInResponseTo: inResponseTo });
186
+ check("Bearer with past NotBefore verifies", info && typeof info === "object");
187
+ check("Bearer past NotBefore: nameId returned", info.nameId === "alice@example.com");
188
+ }
189
+
190
+ // Case 2 — a NotBefore in the FUTURE (not yet valid) must be refused.
191
+ // This path already fails closed on the unfixed tree (isFinite(nb) is
192
+ // true and nb > now), so it is the harness-correctness control proving
193
+ // the not-yet-valid axis is enforced at all.
194
+ async function testBearerFutureNotBeforeRefused() {
195
+ var idp = await _mintRsaCert("idp.example");
196
+ var sp = _newSp(idp);
197
+ var inResponseTo = "_req-future-nb";
198
+ var b64 = _buildSignedResponse(idp, {
199
+ tag: "bearer-future-nb",
200
+ method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
201
+ nameId: "alice@example.com",
202
+ scd: _bearerScd(" NotBefore=\"" + _isoFromNow(60 * 60 * 1000) + "\"", inResponseTo), // allow:raw-time-literal — 1h future, not yet valid
203
+ });
204
+ check("Bearer with future NotBefore is refused (not yet valid)",
205
+ _verifyThrows(sp, b64, { expectedInResponseTo: inResponseTo }) === "auth-saml/no-valid-confirmation");
206
+ }
207
+
208
+ // Case 3 — the RED case: a Bearer SubjectConfirmationData with a
209
+ // present-but-UNPARSEABLE NotBefore. On the unfixed tree `isFinite(nb)`
210
+ // is false, so the `&&`-guarded not-yet-valid check is skipped and the
211
+ // SCD is wrongly ACCEPTED. It must fail closed: an unparseable NotBefore
212
+ // cannot establish that the confirmation has begun, so the SCD is
213
+ // skipped and the assertion (with no other valid confirmation) refused.
214
+ async function testBearerUnparseableNotBeforeRefused() {
215
+ var idp = await _mintRsaCert("idp.example");
216
+ var sp = _newSp(idp);
217
+ var inResponseTo = "_req-bad-nb";
218
+ var b64 = _buildSignedResponse(idp, {
219
+ tag: "bearer-bad-nb",
220
+ method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
221
+ nameId: "alice@example.com",
222
+ scd: _bearerScd(" NotBefore=\"not-a-date\"", inResponseTo),
223
+ });
224
+ check("Bearer unparseable NotBefore is refused (fail-closed)",
225
+ _verifyThrows(sp, b64, { expectedInResponseTo: inResponseTo }) === "auth-saml/no-valid-confirmation");
226
+ }
227
+
228
+ async function run() {
229
+ await testBearerPastNotBeforeAccepted();
230
+ await testBearerFutureNotBeforeRefused();
231
+ await testBearerUnparseableNotBeforeRefused();
232
+ }
233
+
234
+ if (require.main === module) {
235
+ run().then(function () { process.exit(0); })
236
+ .catch(function (e) { console.error(e); process.exit(1); });
237
+ }
238
+ module.exports = { run: run };
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * b.session.destroyAllForUser must succeed for a store-backed-only consumer
4
+ * (b.session.useStore) that never called b.db.init() (#340).
5
+ *
6
+ * A store-backed-only deployment points session data at an isolated localDbThin
7
+ * file and registers the _blamejs_sessions sealed/derived-hash schema directly
8
+ * (so sealRow / lookupHash work) WITHOUT initializing the framework db. Every
9
+ * revoke-all path — logout-everywhere, suspend, delete, role change — calls
10
+ * destroyAllForUser, which deletes the store rows and then raises the stateless
11
+ * valid-from boundary via bump(). The boundary table lives in the framework db,
12
+ * so an uninitialized db threw db/not-initialized (a regression: worked in
13
+ * 0.15.11, broke when bump() was added) and 500'd the revoke even though the
14
+ * store rows were already deleted.
15
+ *
16
+ * RED on the pre-fix tree: destroyAllForUser throws (db/not-initialized rewrapped
17
+ * to MISCONFIGURED). GREEN: it resolves AND the stateless valid-from boundary is
18
+ * honored (routed through the configured store), so b.session.check revokes a
19
+ * token issued before the boundary.
20
+ *
21
+ * Drives the real consumer path through the public b.session surface — no db.init.
22
+ */
23
+
24
+ var helpers = require("../helpers");
25
+ var b = helpers.b;
26
+ var check = helpers.check;
27
+ var setupVaultOnly = helpers.setupVaultOnly;
28
+ var teardownVaultOnly = helpers.teardownVaultOnly;
29
+ var fs = require("fs");
30
+ var os = require("os");
31
+ var path = require("path");
32
+
33
+ // Mirror the framework's _blamejs_sessions sealed/derived schema (db.js
34
+ // FRAMEWORK_SCHEMA) so sealRow / lookupHash work without b.db.init().
35
+ function _registerSessionSchema() {
36
+ b.cryptoField.registerTable("_blamejs_sessions", {
37
+ sealedFields: ["userId", "data"],
38
+ derivedHashes: { userIdHash: { from: "userId" } },
39
+ });
40
+ }
41
+
42
+ async function run() {
43
+ var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-das-"));
44
+ var storeFile = path.join(tmpDir, "sessions.db");
45
+ var store = null;
46
+ try {
47
+ // Vault only — NO b.db.init(). This is the store-backed-only deployment.
48
+ await setupVaultOnly(tmpDir);
49
+ b.cluster._resetForTest();
50
+ b.session._resetForTest();
51
+ b.cryptoField.clearForTest();
52
+ _registerSessionSchema();
53
+
54
+ store = b.session.stores.localDbThin({ file: storeFile, audit: false });
55
+ b.session.useStore(store);
56
+
57
+ var s = b.session;
58
+ var uid = "user-340";
59
+
60
+ // Create a real store-backed session so destroyAllForUser has a row to
61
+ // delete (drives sealRow + the store INSERT — the shipped consumer path).
62
+ var created = await s.create({ userId: uid });
63
+ check("create returns a sealed cookie token", typeof created.token === "string" && created.token.length > 0);
64
+
65
+ // A token issued NOW (before any revoke) is valid against the boundary.
66
+ var iatBefore = Date.now();
67
+ check("a token issued before any revoke passes session.check",
68
+ (await s.check(uid, iatBefore)) === true);
69
+ check("a fresh subject's valid-from boundary is 0", (await s.validFrom(uid)) === 0);
70
+
71
+ // The bug: destroyAllForUser deletes the store rows, then bump() routes the
72
+ // valid-from upsert to the framework db (uninitialized) and throws. With the
73
+ // fix it falls back to the configured store and resolves.
74
+ var revoked = null;
75
+ var threw = null;
76
+ try {
77
+ revoked = await s.destroyAllForUser(uid);
78
+ } catch (e) {
79
+ threw = e;
80
+ }
81
+ check("destroyAllForUser resolves for a store-backed-only consumer (no db.init)",
82
+ threw === null);
83
+ if (threw) {
84
+ check(" (pre-fix would throw db/not-initialized → MISCONFIGURED): " +
85
+ (threw.code || threw.message), false);
86
+ }
87
+ check("destroyAllForUser reports the deleted store-row count",
88
+ typeof revoked === "number" && revoked >= 1);
89
+
90
+ // The stateless valid-from boundary was actually raised (routed through the
91
+ // store), not silently dropped — a token issued before it is now revoked.
92
+ var boundary = await s.validFrom(uid);
93
+ check("the stateless valid-from boundary was raised (honored, not dropped)",
94
+ typeof boundary === "number" && boundary >= iatBefore);
95
+ check("a token issued BEFORE the revoke now fails session.check",
96
+ (await s.check(uid, iatBefore - 1000)) === false);
97
+ check("a token issued AFTER the boundary still passes session.check",
98
+ (await s.check(uid, boundary + 5000)) === true);
99
+
100
+ // Monotonicity still holds through the store backend.
101
+ var lower = await s.bump(uid, { epochMs: boundary - 5000 });
102
+ check("a lower-epoch bump is a monotonic no-op through the store",
103
+ lower === boundary);
104
+
105
+ // With NEITHER a framework db NOR a store, bump still fails closed (the
106
+ // boundary is never silently dropped — a real misconfiguration propagates).
107
+ b.session.useStore(null);
108
+ var threwNoStorage = null;
109
+ try {
110
+ await s.bump("user-no-storage");
111
+ } catch (e) { threwNoStorage = e; }
112
+ check("bump fails closed when neither a framework db nor a store exists",
113
+ threwNoStorage !== null);
114
+ } finally {
115
+ try { if (store && store.close) store.close(); } catch (_e) { /* best-effort */ }
116
+ b.session.useStore(null);
117
+ b.session._resetForTest();
118
+ b.cryptoField.clearForTest();
119
+ try { teardownVaultOnly(tmpDir); } catch (_e) { /* best-effort */ }
120
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
121
+ }
122
+ }
123
+
124
+ module.exports = { run: run };
125
+ if (require.main === module) {
126
+ run().then(function () { console.log("OK"); })
127
+ .catch(function (e) { console.error(e && e.stack ? e.stack : e); process.exit(1); });
128
+ }