@blamejs/blamejs-shop 0.3.70 → 0.3.72

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 (93) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +254 -2
  4. package/lib/asset-manifest.json +1 -1
  5. package/lib/customer-segments.js +150 -0
  6. package/lib/vendor/MANIFEST.json +95 -83
  7. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
  8. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +1 -1
  9. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +1 -1
  10. package/lib/vendor/blamejs/.github/workflows/ci.yml +10 -10
  11. package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
  12. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +2 -2
  13. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  14. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +2 -2
  15. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
  16. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  17. package/lib/vendor/blamejs/README.md +1 -1
  18. package/lib/vendor/blamejs/SECURITY.md +2 -0
  19. package/lib/vendor/blamejs/api-snapshot.json +108 -4
  20. package/lib/vendor/blamejs/lib/auth/oauth.js +736 -1
  21. package/lib/vendor/blamejs/lib/auth/oid4vci.js +124 -5
  22. package/lib/vendor/blamejs/lib/auth/oid4vp.js +14 -4
  23. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +46 -1
  24. package/lib/vendor/blamejs/lib/break-glass.js +1 -2
  25. package/lib/vendor/blamejs/lib/config.js +28 -31
  26. package/lib/vendor/blamejs/lib/crypto-field.js +274 -17
  27. package/lib/vendor/blamejs/lib/dora.js +8 -5
  28. package/lib/vendor/blamejs/lib/dsr.js +2 -2
  29. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +7 -0
  30. package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +4 -2
  31. package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +4 -2
  32. package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +4 -2
  33. package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +20 -0
  34. package/lib/vendor/blamejs/lib/guard-html-wcag.js +1 -1
  35. package/lib/vendor/blamejs/lib/honeytoken.js +27 -20
  36. package/lib/vendor/blamejs/lib/mail-auth.js +333 -0
  37. package/lib/vendor/blamejs/lib/mail-deploy.js +1 -1
  38. package/lib/vendor/blamejs/lib/mail-send-deliver.js +13 -4
  39. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +140 -13
  40. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -0
  41. package/lib/vendor/blamejs/lib/middleware/csp-report.js +13 -9
  42. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +115 -14
  43. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -0
  44. package/lib/vendor/blamejs/lib/middleware/scim-server.js +297 -19
  45. package/lib/vendor/blamejs/lib/middleware/security-headers.js +47 -0
  46. package/lib/vendor/blamejs/lib/middleware/security-txt.js +1 -2
  47. package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +1 -2
  48. package/lib/vendor/blamejs/lib/network-smtp-policy.js +4 -4
  49. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +11 -2
  50. package/lib/vendor/blamejs/lib/observability-tracer.js +1 -1
  51. package/lib/vendor/blamejs/lib/observability.js +39 -1
  52. package/lib/vendor/blamejs/lib/problem-details.js +56 -11
  53. package/lib/vendor/blamejs/lib/pubsub-cluster.js +16 -3
  54. package/lib/vendor/blamejs/lib/queue-sqs.js +20 -2
  55. package/lib/vendor/blamejs/lib/redis-client.js +32 -4
  56. package/lib/vendor/blamejs/lib/safe-redirect.js +16 -2
  57. package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +3 -2
  58. package/lib/vendor/blamejs/package.json +1 -1
  59. package/lib/vendor/blamejs/release-notes/v0.14.20.json +73 -0
  60. package/lib/vendor/blamejs/release-notes/v0.14.21.json +98 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +339 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +37 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +22 -0
  64. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +315 -5
  65. package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +46 -0
  66. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +176 -0
  67. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +86 -0
  68. package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +38 -0
  69. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +29 -0
  70. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +236 -1
  71. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +190 -0
  72. package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +23 -0
  73. package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +59 -0
  74. package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +26 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -0
  76. package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +16 -0
  77. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +108 -0
  78. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +269 -0
  79. package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +28 -0
  80. package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +39 -0
  81. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +37 -0
  82. package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +79 -0
  83. package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +49 -0
  84. package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +48 -0
  85. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +60 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/safe-redirect.test.js +118 -0
  87. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +259 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +46 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +113 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/security-txt.test.js +111 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +62 -0
  92. package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +39 -0
  93. package/package.json +1 -1
@@ -119,6 +119,66 @@ async function run() {
119
119
  check("frameToValue: error becomes _redisError marker",
120
120
  efv && efv._redisError === true && efv.message === "ERR boom");
121
121
 
122
+ // ---- create() config-time opt validation ----
123
+ // db / connectTimeoutMs / commandTimeoutMs / maxReconnectAttempts were
124
+ // coerced with bare Number() + falsy fallback: a bad type silently became
125
+ // the default (or, for a negative timeout, sailed into setTimeout; for a
126
+ // non-numeric maxReconnectAttempts, NaN made the `>= 0` reconnect cap
127
+ // false and disabled the bound entirely). They now throw at the entry
128
+ // point. db and maxReconnectAttempts must still allow 0.
129
+ function _createThrows(label, badOpts) {
130
+ var threw = false;
131
+ var msg = "";
132
+ try { redis.create(Object.assign({ url: "redis://127.0.0.1:1/0" }, badOpts)); }
133
+ catch (e) { threw = true; msg = (e && e.message) || ""; }
134
+ check("create rejects " + label, threw);
135
+ check("create rejects " + label + " with a clear message",
136
+ threw && msg.length > 0 && /must be/.test(msg));
137
+ }
138
+
139
+ _createThrows("connectTimeoutMs:\"abc\"", { connectTimeoutMs: "abc" });
140
+ _createThrows("connectTimeoutMs:-1", { connectTimeoutMs: -1 });
141
+ _createThrows("connectTimeoutMs:0", { connectTimeoutMs: 0 });
142
+ _createThrows("commandTimeoutMs:\"abc\"", { commandTimeoutMs: "abc" });
143
+ _createThrows("commandTimeoutMs:-5", { commandTimeoutMs: -5 });
144
+ _createThrows("db:\"3\"", { db: "3" });
145
+ _createThrows("db:-1", { db: -1 });
146
+ _createThrows("db:1.5", { db: 1.5 });
147
+ _createThrows("maxReconnectAttempts:\"abc\"", { maxReconnectAttempts: "abc" });
148
+ _createThrows("maxReconnectAttempts:-1", { maxReconnectAttempts: -1 });
149
+ _createThrows("maxReconnectAttempts:2.5", { maxReconnectAttempts: 2.5 });
150
+
151
+ // Absent opts keep the documented defaults.
152
+ var cDefaults = redis.create({ url: "redis://127.0.0.1:6379/0" });
153
+ var sDefaults = cDefaults._state();
154
+ check("create default connectTimeoutMs is 5000", sDefaults.connectTimeoutMs === 5000);
155
+ check("create default commandTimeoutMs is 10000", sDefaults.commandTimeoutMs === 10000);
156
+ check("create default maxReconnectAttempts is 10", sDefaults.maxReconnectAttempts === 10);
157
+ check("create default db comes from url (0)", sDefaults.db === 0);
158
+
159
+ // Valid values flow through unchanged.
160
+ var cValid = redis.create({
161
+ url: "redis://127.0.0.1:6379/0",
162
+ db: 7, connectTimeoutMs: 3000, commandTimeoutMs: 8000, maxReconnectAttempts: 3,
163
+ });
164
+ var sValid = cValid._state();
165
+ check("create accepts valid db", sValid.db === 7);
166
+ check("create accepts valid connectTimeoutMs", sValid.connectTimeoutMs === 3000);
167
+ check("create accepts valid commandTimeoutMs", sValid.commandTimeoutMs === 8000);
168
+ check("create accepts valid maxReconnectAttempts", sValid.maxReconnectAttempts === 3);
169
+
170
+ // 0 is a legitimate value for both db and maxReconnectAttempts and must
171
+ // NOT throw. db:0 = no SELECT on connect; maxReconnectAttempts:0 = give
172
+ // up immediately (the `reconnectAttempt >= maxReconnectAttempts` gate is
173
+ // true on the first reconnect call) — both preserved from prior behavior.
174
+ var cZero = redis.create({
175
+ url: "redis://127.0.0.1:6379/0", db: 0, maxReconnectAttempts: 0,
176
+ });
177
+ var sZero = cZero._state();
178
+ check("create accepts db:0", sZero.db === 0);
179
+ check("create accepts maxReconnectAttempts:0 (give-up-immediately bound)",
180
+ sZero.maxReconnectAttempts === 0);
181
+
122
182
  // ---- close()/reconnect leak guard (v0.13.40) ----
123
183
  // After close(), a reconnect timer scheduled during backoff must be
124
184
  // cancelled and _connect() must refuse to re-open — otherwise a post-
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * b.safeRedirect — open-redirect (CWE-601) defense for operator-supplied
4
+ * post-login redirect targets.
5
+ *
6
+ * Run standalone: `node test/layer-0-primitives/safe-redirect.test.js`
7
+ * Or via smoke: `node test/smoke.js`
8
+ */
9
+
10
+ var helpers = require("../helpers");
11
+ var b = helpers.b;
12
+ var check = helpers.check;
13
+
14
+ function testSurface() {
15
+ check("b.safeRedirect is object", typeof b.safeRedirect === "object");
16
+ check("b.safeRedirect.resolve is fn", typeof b.safeRedirect.resolve === "function");
17
+ check("b.safeRedirect.DEFAULT_FALLBACK", b.safeRedirect.DEFAULT_FALLBACK === "/");
18
+ }
19
+
20
+ function testRelativeAndFragmentTargets() {
21
+ check("relative path is same-origin safe",
22
+ b.safeRedirect.resolve("/dashboard", { fallback: "/x" }) === "/dashboard");
23
+ check("query-only target is safe",
24
+ b.safeRedirect.resolve("?q=1", { fallback: "/x" }) === "?q=1");
25
+ check("fragment target is safe",
26
+ b.safeRedirect.resolve("#section", { fallback: "/x" }) === "#section");
27
+ }
28
+
29
+ function testHostileTargetsFallBack() {
30
+ check("protocol-relative // → fallback",
31
+ b.safeRedirect.resolve("//attacker.example.com", { fallback: "/safe" }) === "/safe");
32
+ check("backslash-relative \\\\ → fallback",
33
+ b.safeRedirect.resolve("\\\\attacker.example.com", { fallback: "/safe" }) === "/safe");
34
+ check("control char → fallback",
35
+ b.safeRedirect.resolve("/a\nb", { fallback: "/safe" }) === "/safe");
36
+ check("empty target → fallback",
37
+ b.safeRedirect.resolve("", { fallback: "/safe" }) === "/safe");
38
+ check("full URL with no allowlist → fallback",
39
+ b.safeRedirect.resolve("https://attacker.example.com/x", { fallback: "/safe" }) === "/safe");
40
+ }
41
+
42
+ function testAllowedOriginsAndHosts() {
43
+ check("full URL matching allowedOrigins passes",
44
+ b.safeRedirect.resolve("https://app.example.com/next",
45
+ { allowedOrigins: ["https://app.example.com"], fallback: "/safe" })
46
+ === "https://app.example.com/next");
47
+ check("full URL not in allowedOrigins → fallback",
48
+ b.safeRedirect.resolve("https://evil.example.com/x",
49
+ { allowedOrigins: ["https://app.example.com"], fallback: "/safe" })
50
+ === "/safe");
51
+ check("full URL matching allowedHosts passes",
52
+ b.safeRedirect.resolve("https://app.example.com/next",
53
+ { allowedHosts: ["app.example.com"], fallback: "/safe" })
54
+ === "https://app.example.com/next");
55
+ }
56
+
57
+ // ---- base wiring (opts.base is the app's own origin) ----
58
+
59
+ function testBaseOriginImplicitlyAllowed() {
60
+ // A full URL on the same origin as base is same-origin by definition,
61
+ // so it passes even without an explicit allowedOrigins / allowedHosts.
62
+ check("full URL on base origin allowed via base alone",
63
+ b.safeRedirect.resolve("https://app.example.com/dashboard",
64
+ { base: "https://app.example.com", fallback: "/safe" })
65
+ === "https://app.example.com/dashboard");
66
+
67
+ // A cross-origin full URL is still refused when only base is supplied.
68
+ check("cross-origin full URL refused with base only",
69
+ b.safeRedirect.resolve("https://attacker.example.com/x",
70
+ { base: "https://app.example.com", fallback: "/safe" })
71
+ === "/safe");
72
+
73
+ // base combines with allowedOrigins (both are accepted).
74
+ check("base origin allowed alongside allowedOrigins",
75
+ b.safeRedirect.resolve("https://app.example.com/y",
76
+ { base: "https://app.example.com",
77
+ allowedOrigins: ["https://other.example.com"], fallback: "/safe" })
78
+ === "https://app.example.com/y");
79
+
80
+ // A different port is a different origin → refused.
81
+ check("base origin port mismatch refused",
82
+ b.safeRedirect.resolve("https://app.example.com:8443/x",
83
+ { base: "https://app.example.com", fallback: "/safe" })
84
+ === "/safe");
85
+ }
86
+
87
+ function testBaseDefaultBehaviorUnchanged() {
88
+ // No base + no allowlist → full URLs still refused (default safe).
89
+ check("no base, no allowlist → full URL fallback",
90
+ b.safeRedirect.resolve("https://app.example.com/x", { fallback: "/safe" }) === "/safe");
91
+
92
+ // Relative paths are unaffected by base.
93
+ check("relative path safe regardless of base",
94
+ b.safeRedirect.resolve("/home", { base: "https://app.example.com", fallback: "/safe" })
95
+ === "/home");
96
+
97
+ // A malformed / non-http(s) base is ignored (no crash, falls through
98
+ // to refuse the full URL with no other allowlist).
99
+ check("malformed base ignored → full URL fallback",
100
+ b.safeRedirect.resolve("https://app.example.com/x",
101
+ { base: "not a url", fallback: "/safe" })
102
+ === "/safe");
103
+ }
104
+
105
+ function run() {
106
+ testSurface();
107
+ testRelativeAndFragmentTargets();
108
+ testHostileTargetsFallBack();
109
+ testAllowedOriginsAndHosts();
110
+ testBaseOriginImplicitlyAllowed();
111
+ testBaseDefaultBehaviorUnchanged();
112
+ console.log("OK — safe-redirect tests");
113
+ }
114
+
115
+ module.exports = { run: run };
116
+ if (require.main === module) {
117
+ try { run(); process.exit(0); } catch (e) { console.error(e); process.exit(1); }
118
+ }
@@ -92,6 +92,7 @@ async function run() {
92
92
  check("BulkResponse URN exported", scimMod.SCIM_MESSAGE_BULK_RESPONSE === "urn:ietf:params:scim:api:messages:2.0:BulkResponse");
93
93
 
94
94
  await _runBulkTests();
95
+ await _runBulkRefOrderingTests();
95
96
  }
96
97
 
97
98
  // RFC 7644 §3.7 — /Bulk operations.
@@ -247,6 +248,264 @@ async function _runBulkTests() {
247
248
  check("bulk: bad maxOperations refused", threwCap);
248
249
  }
249
250
 
251
+ // Deep-walk a value asserting no string carries an unresolved
252
+ // "bulkId:<id>" cross-reference token (RFC 7644 §3.7.2 — a literal token
253
+ // must never reach the adapter as if it were a real resource id).
254
+ function _containsBulkIdToken(value) {
255
+ if (typeof value === "string") return /^bulkId:/.test(value);
256
+ if (Array.isArray(value)) return value.some(_containsBulkIdToken);
257
+ if (value && typeof value === "object") {
258
+ return Object.keys(value).some(function (k) { return _containsBulkIdToken(value[k]); });
259
+ }
260
+ return false;
261
+ }
262
+
263
+ // A user adapter that records every data object it is handed so a test
264
+ // can assert the adapter NEVER received a literal "bulkId:<id>" token.
265
+ function _mkRecordingUsers() {
266
+ return {
267
+ _records: new Map(),
268
+ _seq: 0,
269
+ _received: [],
270
+ async create(rec) {
271
+ this._received.push(rec);
272
+ var id = "u-" + (++this._seq);
273
+ var out = Object.assign({ id: id, meta: { resourceType: "User" } }, rec);
274
+ this._records.set(id, out);
275
+ return out;
276
+ },
277
+ async read(id) { return this._records.get(id) || null; },
278
+ async update(id, rec) { this._received.push(rec); var r = Object.assign({}, rec, { id: id }); this._records.set(id, r); return r; },
279
+ async patch(id, ops) { this._received.push(ops); var r = this._records.get(id); return r; },
280
+ async remove(id) { this._records.delete(id); },
281
+ async list() { return { totalResults: this._records.size, Resources: Array.from(this._records.values()) }; },
282
+ };
283
+ }
284
+
285
+ function _mkRecordingGroups() {
286
+ return {
287
+ _records: new Map(),
288
+ _seq: 0,
289
+ _received: [],
290
+ _lastCreate: null,
291
+ async create(rec) {
292
+ this._received.push(rec);
293
+ var id = "g-" + (++this._seq);
294
+ var out = Object.assign({ id: id, meta: { resourceType: "Group" } }, rec);
295
+ this._records.set(id, out);
296
+ this._lastCreate = out;
297
+ return out;
298
+ },
299
+ async read(id) { return this._records.get(id) || null; },
300
+ async update(id, rec) { this._received.push(rec); var r = Object.assign({}, rec, { id: id }); this._records.set(id, r); return r; },
301
+ async patch(id, ops) { this._received.push(ops); var r = this._records.get(id); return r; },
302
+ async remove(id) { this._records.delete(id); },
303
+ async list() { return { totalResults: this._records.size, Resources: Array.from(this._records.values()) }; },
304
+ };
305
+ }
306
+
307
+ // RFC 7644 §3.7.2 — forward references, circular references, undeclared
308
+ // references, and failed-dependency references.
309
+ async function _runBulkRefOrderingTests() {
310
+ // --- Forward reference: the GROUP op references the USER op that comes
311
+ // AFTER it in request order. Both must succeed and the token must be
312
+ // substituted with the real id (success path, end-to-end).
313
+ var fwdUsers = _mkRecordingUsers();
314
+ var fwdGroups = _mkRecordingGroups();
315
+ var fwdMw = b.middleware.scimServer({
316
+ basePath: "/scim/v2", users: fwdUsers, groups: fwdGroups, bulk: { maxOperations: 10 },
317
+ });
318
+ var fwd = await _bulkBody(fwdMw, [
319
+ { method: "POST", path: "/Groups", bulkId: "admins",
320
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], displayName: "Admins",
321
+ members: [{ value: "bulkId:carol" }] } },
322
+ { method: "POST", path: "/Users", bulkId: "carol",
323
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "carol" } },
324
+ ]);
325
+ var fwdResp = JSON.parse(fwd.res._body());
326
+ check("bulk fwd-ref: 200 envelope", fwd.res._sc() === 200);
327
+ check("bulk fwd-ref: both succeed", fwdResp.Operations[0].status === "201" && fwdResp.Operations[1].status === "201");
328
+ // Response array stays in ORIGINAL request order (group first, user second).
329
+ check("bulk fwd-ref: response in request order",
330
+ fwdResp.Operations[0].bulkId === "admins" && fwdResp.Operations[1].bulkId === "carol");
331
+ var fwdUserId = Array.from(fwdUsers._records.keys())[0];
332
+ var fwdMembers = fwdGroups._lastCreate && fwdGroups._lastCreate.members;
333
+ check("bulk fwd-ref: token resolved to real id",
334
+ Array.isArray(fwdMembers) && fwdMembers[0].value === fwdUserId);
335
+ check("bulk fwd-ref: adapter never saw a literal token",
336
+ !fwdGroups._received.some(_containsBulkIdToken) && !fwdUsers._received.some(_containsBulkIdToken));
337
+
338
+ // --- Circular reference: two POSTs each reference the other's bulkId.
339
+ // Both fail with HTTP 409 (RFC 7644 §3.7.1) and the adapter is NEVER
340
+ // called with a literal token (in fact never called at all here).
341
+ var cycUsers = _mkRecordingUsers();
342
+ var cycGroups = _mkRecordingGroups();
343
+ var cycMw = b.middleware.scimServer({
344
+ basePath: "/scim/v2", users: cycUsers, groups: cycGroups, bulk: { maxOperations: 10 },
345
+ });
346
+ var cyc = await _bulkBody(cycMw, [
347
+ { method: "POST", path: "/Users", bulkId: "ua",
348
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "ua",
349
+ manager: { value: "bulkId:ub" } } },
350
+ { method: "POST", path: "/Users", bulkId: "ub",
351
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "ub",
352
+ manager: { value: "bulkId:ua" } } },
353
+ ]);
354
+ var cycResp = JSON.parse(cyc.res._body());
355
+ check("bulk circular: 200 envelope", cyc.res._sc() === 200);
356
+ check("bulk circular: 2 results in order", cycResp.Operations.length === 2 &&
357
+ cycResp.Operations[0].bulkId === "ua" && cycResp.Operations[1].bulkId === "ub");
358
+ check("bulk circular: both fail 409", cycResp.Operations[0].status === "409" && cycResp.Operations[1].status === "409");
359
+ check("bulk circular: adapter never called with token",
360
+ !cycUsers._received.some(_containsBulkIdToken));
361
+ check("bulk circular: nothing persisted", cycUsers._records.size === 0);
362
+
363
+ // --- Undeclared reference: a member points at a bulkId no operation
364
+ // declares — that op fails invalidValue; the well-formed op succeeds.
365
+ var undUsers = _mkRecordingUsers();
366
+ var undGroups = _mkRecordingGroups();
367
+ var undMw = b.middleware.scimServer({
368
+ basePath: "/scim/v2", users: undUsers, groups: undGroups, bulk: { maxOperations: 10 },
369
+ });
370
+ var und = await _bulkBody(undMw, [
371
+ { method: "POST", path: "/Users", bulkId: "real",
372
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "real" } },
373
+ { method: "POST", path: "/Groups", bulkId: "grp",
374
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], displayName: "G",
375
+ members: [{ value: "bulkId:ghost" }] } },
376
+ ]);
377
+ var undResp = JSON.parse(und.res._body());
378
+ check("bulk undeclared: real op succeeds", undResp.Operations[0].status === "201");
379
+ check("bulk undeclared: ref op fails 400", undResp.Operations[1].status === "400" &&
380
+ undResp.Operations[1].response.scimType === "invalidValue");
381
+ check("bulk undeclared: group adapter never saw token",
382
+ !undGroups._received.some(_containsBulkIdToken));
383
+ check("bulk undeclared: no group persisted", undGroups._records.size === 0);
384
+
385
+ // --- Failed dependency: the creating op fails, so the op that
386
+ // references it must fail invalidValue rather than receive the token.
387
+ var depUsersBase = _mkRecordingUsers();
388
+ var depUsers = {
389
+ _records: depUsersBase._records,
390
+ _received: depUsersBase._received,
391
+ create: async function (rec) {
392
+ depUsersBase._received.push(rec);
393
+ var e = new Error("user create rejected"); e.statusCode = 409; e.scimType = "uniqueness"; throw e;
394
+ },
395
+ read: depUsersBase.read.bind(depUsersBase), update: depUsersBase.update.bind(depUsersBase),
396
+ patch: depUsersBase.patch.bind(depUsersBase), remove: depUsersBase.remove.bind(depUsersBase),
397
+ list: depUsersBase.list.bind(depUsersBase),
398
+ };
399
+ var depGroups = _mkRecordingGroups();
400
+ var depMw = b.middleware.scimServer({
401
+ basePath: "/scim/v2", users: depUsers, groups: depGroups, bulk: { maxOperations: 10 },
402
+ });
403
+ var dep = await _bulkBody(depMw, [
404
+ { method: "POST", path: "/Users", bulkId: "willfail",
405
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "willfail" } },
406
+ { method: "POST", path: "/Groups", bulkId: "grp",
407
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], displayName: "G",
408
+ members: [{ value: "bulkId:willfail" }] } },
409
+ ]);
410
+ var depResp = JSON.parse(dep.res._body());
411
+ check("bulk failed-dep: creator fails 409", depResp.Operations[0].status === "409");
412
+ check("bulk failed-dep: dependent fails 400 invalidValue",
413
+ depResp.Operations[1].status === "400" && depResp.Operations[1].response.scimType === "invalidValue");
414
+ check("bulk failed-dep: group adapter never saw token",
415
+ !depGroups._received.some(_containsBulkIdToken));
416
+ check("bulk failed-dep: no group persisted", depGroups._records.size === 0);
417
+
418
+ // --- Backward reference unchanged: a member references an EARLIER POST.
419
+ var bwUsers = _mkRecordingUsers();
420
+ var bwGroups = _mkRecordingGroups();
421
+ var bwMw = b.middleware.scimServer({
422
+ basePath: "/scim/v2", users: bwUsers, groups: bwGroups, bulk: { maxOperations: 10 },
423
+ });
424
+ var bw = await _bulkBody(bwMw, [
425
+ { method: "POST", path: "/Users", bulkId: "dave",
426
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "dave" } },
427
+ { method: "POST", path: "/Groups", bulkId: "team",
428
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], displayName: "Team",
429
+ members: [{ value: "bulkId:dave" }] } },
430
+ ]);
431
+ var bwResp = JSON.parse(bw.res._body());
432
+ check("bulk back-ref: both succeed", bwResp.Operations[0].status === "201" && bwResp.Operations[1].status === "201");
433
+ var bwUserId = Array.from(bwUsers._records.keys())[0];
434
+ var bwMembers = bwGroups._lastCreate && bwGroups._lastCreate.members;
435
+ check("bulk back-ref: token resolved", Array.isArray(bwMembers) && bwMembers[0].value === bwUserId);
436
+ check("bulk back-ref: adapter never saw token",
437
+ !bwGroups._received.some(_containsBulkIdToken));
438
+
439
+ // --- PATH references (RFC 7644 §3.7.2) — a bulkId can appear as the
440
+ // resource id in an operation's path ("PATCH /Users/bulkId:u1"), not
441
+ // just in operation data. Forward path refs order like data refs and
442
+ // the path token is substituted with the real id before dispatch.
443
+ var pthUsers = _mkRecordingUsers();
444
+ var pthPatchedIds = [];
445
+ var pthPatchInner = pthUsers.patch.bind(pthUsers);
446
+ pthUsers.patch = async function (id, ops) { pthPatchedIds.push(id); return pthPatchInner(id, ops); };
447
+ var pthMw = b.middleware.scimServer({
448
+ basePath: "/scim/v2", users: pthUsers, bulk: { maxOperations: 10 },
449
+ });
450
+ var pth = await _bulkBody(pthMw, [
451
+ { method: "PATCH", path: "/Users/bulkId:newbie",
452
+ data: { Operations: [{ op: "replace", path: "active", value: true }] } },
453
+ { method: "POST", path: "/Users", bulkId: "newbie",
454
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "newbie" } },
455
+ ]);
456
+ var pthResp = JSON.parse(pth.res._body());
457
+ check("bulk path-ref: 200 envelope", pth.res._sc() === 200);
458
+ check("bulk path-ref: both succeed",
459
+ pthResp.Operations[0].status === "200" && pthResp.Operations[1].status === "201");
460
+ check("bulk path-ref: response in request order",
461
+ pthResp.Operations[1].bulkId === "newbie");
462
+ var pthUserId = Array.from(pthUsers._records.keys())[0];
463
+ check("bulk path-ref: patch received the real id, not the token",
464
+ pthPatchedIds.length === 1 && pthPatchedIds[0] === pthUserId);
465
+
466
+ // Backward path ref on DELETE: the created resource is removable via
467
+ // its bulkId path token in the same request.
468
+ var delUsers = _mkRecordingUsers();
469
+ var delMw = b.middleware.scimServer({
470
+ basePath: "/scim/v2", users: delUsers, bulk: { maxOperations: 10 },
471
+ });
472
+ var del = await _bulkBody(delMw, [
473
+ { method: "POST", path: "/Users", bulkId: "gone",
474
+ data: { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: "gone" } },
475
+ { method: "DELETE", path: "/Users/bulkId:gone" },
476
+ ]);
477
+ var delResp = JSON.parse(del.res._body());
478
+ check("bulk path-ref delete: both succeed",
479
+ delResp.Operations[0].status === "201" && delResp.Operations[1].status === "204");
480
+ check("bulk path-ref delete: record removed", delUsers._records.size === 0);
481
+
482
+ // Undeclared path ref: fails per-op invalidValue, adapter never called.
483
+ var updUsers = _mkRecordingUsers();
484
+ var updMw = b.middleware.scimServer({
485
+ basePath: "/scim/v2", users: updUsers, bulk: { maxOperations: 10 },
486
+ });
487
+ var upd = await _bulkBody(updMw, [
488
+ { method: "DELETE", path: "/Users/bulkId:phantom" },
489
+ ]);
490
+ var updResp = JSON.parse(upd.res._body());
491
+ check("bulk path-ref undeclared: fails 400 invalidValue",
492
+ updResp.Operations[0].status === "400" &&
493
+ updResp.Operations[0].response.scimType === "invalidValue");
494
+ check("bulk path-ref undeclared: adapter untouched",
495
+ updUsers._received.length === 0 && updUsers._records.size === 0);
496
+
497
+ // --- maxPageSize garbage throws at create() (entry-point tier).
498
+ var threwPage = false;
499
+ try { b.middleware.scimServer({ basePath: "/scim/v2", users: _mkUsers(), maxPageSize: "lots" }); }
500
+ catch (e) { threwPage = /bad-max-page-size/.test(e.code || ""); }
501
+ check("maxPageSize garbage refused at create()", threwPage);
502
+
503
+ var threwPage2 = false;
504
+ try { b.middleware.scimServer({ basePath: "/scim/v2", users: _mkUsers(), maxPageSize: -3 }); }
505
+ catch (e) { threwPage2 = /bad-max-page-size/.test(e.code || ""); }
506
+ check("maxPageSize negative refused at create()", threwPage2);
507
+ }
508
+
250
509
  module.exports = { run: run };
251
510
 
252
511
  if (require.main === module) {
@@ -650,6 +650,51 @@ function testHolderValidation() {
650
650
  check("holder.create: missing holderKey throws", threwNoKey);
651
651
  }
652
652
 
653
+ // The holder must derive the KB-JWT alg from the holder key type when no
654
+ // explicit `algorithm` is given — a fixed "ES256" default signed a non-EC
655
+ // holder key under a header alg that disagreed with the key (un-signable
656
+ // or a self-invalid KB-JWT a verifier rejects).
657
+ async function testHolderAlgFromKeyType() {
658
+ var issuerKey = _newKeyPair();
659
+ var edHolder = nodeCrypto.generateKeyPairSync("ed25519");
660
+ var sd = sdJwtVc.issue({
661
+ issuer: "https://issuer", vct: "https://example/identity",
662
+ claims: { given_name: "Alice" },
663
+ selectivelyDisclosed: ["given_name"],
664
+ issuerKey: issuerKey.privateKey,
665
+ holderKey: _jwk(edHolder.publicKey),
666
+ });
667
+ var holder = sdJwtVc.holder.create({
668
+ storage: sdJwtVc.holder.memoryStorage(),
669
+ holderKey: edHolder.privateKey, // Ed25519, no explicit algorithm
670
+ });
671
+ await holder.store({ id: "c", sdJwt: sd.token });
672
+ var pres = await holder.present({
673
+ credentialId: "c", disclosedClaimNames: ["given_name"],
674
+ audience: "https://verifier", nonce: "n-1",
675
+ });
676
+ var kbJwt = pres.presentation.split("~").pop();
677
+ var kbAlg = JSON.parse(Buffer.from(kbJwt.split(".")[0], "base64url").toString("utf8")).alg;
678
+ check("holder: Ed25519 key infers EdDSA KB-JWT alg (not fixed ES256)", kbAlg === "EdDSA");
679
+ var verified = await sdJwtVc.verify(pres.presentation, {
680
+ issuerKeyResolver: async function () { return issuerKey.publicKey; },
681
+ audience: "https://verifier", nonce: "n-1", requireKeyBinding: true,
682
+ });
683
+ check("holder: Ed25519-bound presentation round-trips", verified.valid && verified.kbValidated);
684
+
685
+ // An RSA holder key has no KB-JWT alg in SUPPORTED_ALGS — refuse at
686
+ // create time rather than emit a self-invalid ES256-headed token.
687
+ var rsaThrew = null;
688
+ try {
689
+ sdJwtVc.holder.create({
690
+ storage: sdJwtVc.holder.memoryStorage(),
691
+ holderKey: nodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 }).privateKey,
692
+ });
693
+ } catch (e) { rsaThrew = e; }
694
+ check("holder.create: RSA holder key refused with a clear error",
695
+ rsaThrew && rsaThrew.code === "auth-sd-jwt-vc/holder-key-unsupported");
696
+ }
697
+
653
698
  // ---- Module exports ----
654
699
 
655
700
  function testExports() {
@@ -696,5 +741,6 @@ function testExports() {
696
741
  await testHolderDelete();
697
742
  await testHolderPresentNonexistent();
698
743
  testHolderValidation();
744
+ await testHolderAlgFromKeyType();
699
745
  testExports();
700
746
  })().catch(function (e) { console.error(e); process.exit(1); });
@@ -150,6 +150,110 @@ function testDefaultDocumentPolicyExportedConstant() {
150
150
  typeof m.DEFAULT_DOCUMENT_POLICY === "string" && m.DEFAULT_DOCUMENT_POLICY.length > 0);
151
151
  }
152
152
 
153
+ function testReportOnlyHeadersEmittedWhenOptedIn() {
154
+ var mw = b.middleware.securityHeaders({
155
+ coopReportOnly: 'same-origin; report-to="coop"',
156
+ coepReportOnly: 'require-corp; report-to="coep"',
157
+ documentPolicyReportOnly: 'document-write=?0; report-to="docpol"',
158
+ });
159
+ var res = _mkRes();
160
+ mw({ headers: {} }, res, function () {});
161
+ check("Cross-Origin-Opener-Policy-Report-Only emitted when coopReportOnly set",
162
+ res._hdrs["Cross-Origin-Opener-Policy-Report-Only"] === 'same-origin; report-to="coop"');
163
+ check("Cross-Origin-Embedder-Policy-Report-Only emitted when coepReportOnly set",
164
+ res._hdrs["Cross-Origin-Embedder-Policy-Report-Only"] === 'require-corp; report-to="coep"');
165
+ check("Document-Policy-Report-Only emitted when documentPolicyReportOnly set",
166
+ res._hdrs["Document-Policy-Report-Only"] === 'document-write=?0; report-to="docpol"');
167
+ }
168
+
169
+ function testReportOnlyDoesNotTouchEnforcingHeaders() {
170
+ // Monitor-mode opt-ins must not alter the enforcing COOP / COEP /
171
+ // Document-Policy headers: COOP stays same-origin, COEP stays off by
172
+ // default, Document-Policy keeps its enforcing default.
173
+ var mw = b.middleware.securityHeaders({
174
+ coopReportOnly: "same-origin",
175
+ coepReportOnly: "require-corp",
176
+ });
177
+ var res = _mkRes();
178
+ mw({ headers: {} }, res, function () {});
179
+ check("enforcing COOP unchanged by coopReportOnly",
180
+ res._hdrs["Cross-Origin-Opener-Policy"] === "same-origin");
181
+ check("COEP stays off by default despite coepReportOnly",
182
+ res._hdrs["Cross-Origin-Embedder-Policy"] === undefined);
183
+ check("enforcing Document-Policy unchanged by report-only opts",
184
+ res._hdrs["Document-Policy"] === b.middleware._modules.securityHeaders.DEFAULT_DOCUMENT_POLICY);
185
+ }
186
+
187
+ function testReportOnlyDefaultOff() {
188
+ var mw = b.middleware.securityHeaders();
189
+ var res = _mkRes();
190
+ mw({ headers: {} }, res, function () {});
191
+ check("Cross-Origin-Opener-Policy-Report-Only default off",
192
+ res._hdrs["Cross-Origin-Opener-Policy-Report-Only"] === undefined);
193
+ check("Cross-Origin-Embedder-Policy-Report-Only default off",
194
+ res._hdrs["Cross-Origin-Embedder-Policy-Report-Only"] === undefined);
195
+ check("Document-Policy-Report-Only default off",
196
+ res._hdrs["Document-Policy-Report-Only"] === undefined);
197
+ }
198
+
199
+ function testRequireDocumentPolicyEmittedWhenOptedIn() {
200
+ var mw = b.middleware.securityHeaders({ requireDocumentPolicy: "unsized-media=?0" });
201
+ var res = _mkRes();
202
+ mw({ headers: {} }, res, function () {});
203
+ check("Require-Document-Policy emitted when opted in",
204
+ res._hdrs["Require-Document-Policy"] === "unsized-media=?0");
205
+ }
206
+
207
+ function testRequireDocumentPolicyDefaultOff() {
208
+ var mw = b.middleware.securityHeaders();
209
+ var res = _mkRes();
210
+ mw({ headers: {} }, res, function () {});
211
+ check("Require-Document-Policy default off",
212
+ res._hdrs["Require-Document-Policy"] === undefined);
213
+ }
214
+
215
+ function testServiceWorkerAllowedEmittedWhenOptedIn() {
216
+ var mw = b.middleware.securityHeaders({ serviceWorkerAllowed: "/" });
217
+ var res = _mkRes();
218
+ mw({ headers: {} }, res, function () {});
219
+ check("Service-Worker-Allowed emitted when opted in",
220
+ res._hdrs["Service-Worker-Allowed"] === "/");
221
+ }
222
+
223
+ function testServiceWorkerAllowedDefaultOff() {
224
+ var mw = b.middleware.securityHeaders();
225
+ var res = _mkRes();
226
+ mw({ headers: {} }, res, function () {});
227
+ check("Service-Worker-Allowed default off",
228
+ res._hdrs["Service-Worker-Allowed"] === undefined);
229
+ }
230
+
231
+ function testNewOptsNonStringIgnored() {
232
+ // Defensive reader: a non-string (truthy-but-wrong) value emits no
233
+ // header rather than serializing an object into the response.
234
+ var mw = b.middleware.securityHeaders({
235
+ coopReportOnly: { not: "a string" },
236
+ serviceWorkerAllowed: 123,
237
+ requireDocumentPolicy: [],
238
+ });
239
+ var res = _mkRes();
240
+ mw({ headers: {} }, res, function () {});
241
+ check("non-string coopReportOnly emits no header",
242
+ res._hdrs["Cross-Origin-Opener-Policy-Report-Only"] === undefined);
243
+ check("non-string serviceWorkerAllowed emits no header",
244
+ res._hdrs["Service-Worker-Allowed"] === undefined);
245
+ check("non-string requireDocumentPolicy emits no header",
246
+ res._hdrs["Require-Document-Policy"] === undefined);
247
+ }
248
+
249
+ function testUnknownOptStillRefused() {
250
+ var threw = false;
251
+ try {
252
+ b.middleware.securityHeaders({ coopReportOnlyy: "same-origin" });
253
+ } catch (_e) { threw = true; }
254
+ check("typo'd report-only opt refused at config-time", threw);
255
+ }
256
+
153
257
  async function run() {
154
258
  testFencedFrameSrcInDefaultCsp();
155
259
  testDocumentPolicyDefault();
@@ -163,6 +267,15 @@ async function run() {
163
267
  testPermissionsPolicyMultiEntryAccepted();
164
268
  testV0870PermissionsPolicyDefaults();
165
269
  testDefaultDocumentPolicyExportedConstant();
270
+ testReportOnlyHeadersEmittedWhenOptedIn();
271
+ testReportOnlyDoesNotTouchEnforcingHeaders();
272
+ testReportOnlyDefaultOff();
273
+ testRequireDocumentPolicyEmittedWhenOptedIn();
274
+ testRequireDocumentPolicyDefaultOff();
275
+ testServiceWorkerAllowedEmittedWhenOptedIn();
276
+ testServiceWorkerAllowedDefaultOff();
277
+ testNewOptsNonStringIgnored();
278
+ testUnknownOptStillRefused();
166
279
  }
167
280
 
168
281
  module.exports = { run: run };