@blamejs/blamejs-shop 0.3.69 → 0.3.71
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 +4 -0
- package/README.md +1 -1
- package/lib/admin.js +254 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +95 -83
- package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +1 -1
- package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +1 -1
- package/lib/vendor/blamejs/.github/workflows/ci.yml +10 -10
- package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +2 -2
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
- package/lib/vendor/blamejs/.github/workflows/scorecard.yml +2 -2
- package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +108 -4
- package/lib/vendor/blamejs/lib/auth/oauth.js +736 -1
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +124 -5
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +14 -4
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +46 -1
- package/lib/vendor/blamejs/lib/break-glass.js +1 -2
- package/lib/vendor/blamejs/lib/config.js +28 -31
- package/lib/vendor/blamejs/lib/crypto-field.js +274 -17
- package/lib/vendor/blamejs/lib/dora.js +8 -5
- package/lib/vendor/blamejs/lib/dsr.js +2 -2
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +7 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +4 -2
- package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +4 -2
- package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +4 -2
- package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +20 -0
- package/lib/vendor/blamejs/lib/guard-html-wcag.js +1 -1
- package/lib/vendor/blamejs/lib/honeytoken.js +27 -20
- package/lib/vendor/blamejs/lib/mail-auth.js +333 -0
- package/lib/vendor/blamejs/lib/mail-deploy.js +1 -1
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +13 -4
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +140 -13
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -0
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +13 -9
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +115 -14
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -0
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +297 -19
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +47 -0
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +1 -2
- package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +1 -2
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +4 -4
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +11 -2
- package/lib/vendor/blamejs/lib/observability-tracer.js +1 -1
- package/lib/vendor/blamejs/lib/observability.js +39 -1
- package/lib/vendor/blamejs/lib/problem-details.js +56 -11
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +16 -3
- package/lib/vendor/blamejs/lib/queue-sqs.js +20 -2
- package/lib/vendor/blamejs/lib/redis-client.js +32 -4
- package/lib/vendor/blamejs/lib/safe-redirect.js +16 -2
- package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +3 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.20.json +73 -0
- package/lib/vendor/blamejs/release-notes/v0.14.21.json +98 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +339 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +315 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +176 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +38 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +236 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +190 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +23 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +16 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +108 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +269 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +39 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +49 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +60 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-redirect.test.js +118 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +259 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-txt.test.js +111 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +62 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +39 -0
- 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 };
|