@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
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
* - parseJarmResponse (OAuth 2.0 JARM signed authorization response)
|
|
6
6
|
* - refreshAccessToken seen() callback (RFC 9700 §4.13 / OAuth 2.1 §6.1)
|
|
7
7
|
* - authorizationUrl PKCE-downgrade refusal (RFC 9700 §4.13 / RFC 7636)
|
|
8
|
+
* - authorizationUrl / exchangeCode authorization_details (RFC 9396 RAR)
|
|
9
|
+
* - buildClientAttestation / buildClientAttestationPop /
|
|
10
|
+
* verifyClientAttestation (draft-ietf-oauth-attestation-based-client-auth)
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
var http = require("node:http");
|
|
14
|
+
var crypto = require("node:crypto");
|
|
11
15
|
var helpers = require("../helpers");
|
|
12
16
|
var b = helpers.b;
|
|
13
17
|
var check = helpers.check;
|
|
@@ -140,6 +144,271 @@ async function run() {
|
|
|
140
144
|
try { await oaStatic.authorizationUrl(); } catch (e) { staticErr = e; }
|
|
141
145
|
check("oauth.authorizationUrl: static endpoints skip discovery (no fetch, no refusal)",
|
|
142
146
|
staticErr === null);
|
|
147
|
+
|
|
148
|
+
// ---- RFC 9396 Rich Authorization Requests (RAR) ----
|
|
149
|
+
var oaRar = b.auth.oauth.create({
|
|
150
|
+
issuer: "https://static.example",
|
|
151
|
+
clientId: "rp-rar",
|
|
152
|
+
redirectUri: "https://rp.example/cb",
|
|
153
|
+
isOidc: true,
|
|
154
|
+
authorizationEndpoint: "https://static.example/auth",
|
|
155
|
+
tokenEndpoint: "https://static.example/token",
|
|
156
|
+
});
|
|
157
|
+
var requested = [
|
|
158
|
+
{ type: "payment_initiation", actions: ["initiate", "status"],
|
|
159
|
+
locations: ["https://rs.example/pay"] },
|
|
160
|
+
];
|
|
161
|
+
var rar = await oaRar.authorizationUrl({ authorizationDetails: requested });
|
|
162
|
+
check("oauth.authorizationUrl: serializes authorization_details (RFC 9396)",
|
|
163
|
+
/[?&]authorization_details=/.test(rar.url));
|
|
164
|
+
check("oauth.authorizationUrl: returns validated authorizationDetails",
|
|
165
|
+
Array.isArray(rar.authorizationDetails) && rar.authorizationDetails.length === 1);
|
|
166
|
+
// back-compat — a client that omits the opt emits no authorization_details
|
|
167
|
+
var noRar = await oaRar.authorizationUrl();
|
|
168
|
+
check("oauth.authorizationUrl: omitted authorizationDetails → no param (back-compat)",
|
|
169
|
+
!/authorization_details=/.test(noRar.url) && noRar.authorizationDetails === null);
|
|
170
|
+
// config-time refusal on malformed shape
|
|
171
|
+
var rarThrew = null;
|
|
172
|
+
try { await oaRar.authorizationUrl({ authorizationDetails: [{ noType: 1 }] }); }
|
|
173
|
+
catch (e) { rarThrew = e; }
|
|
174
|
+
check("oauth.authorizationUrl: authorization_details missing type refused",
|
|
175
|
+
rarThrew && rarThrew.code === "auth-oauth/bad-authorization-details");
|
|
176
|
+
rarThrew = null;
|
|
177
|
+
try { await oaRar.authorizationUrl({ authorizationDetails: "not-an-array" }); }
|
|
178
|
+
catch (e) { rarThrew = e; }
|
|
179
|
+
check("oauth.authorizationUrl: non-array authorization_details refused",
|
|
180
|
+
rarThrew && rarThrew.code === "auth-oauth/bad-authorization-details");
|
|
181
|
+
|
|
182
|
+
// granted-vs-requested cross-check (the security-relevant subset rule)
|
|
183
|
+
var X = b.auth.oauth;
|
|
184
|
+
var subset = X._crossCheckGrantedAuthorizationDetails(
|
|
185
|
+
[{ type: "payment_initiation", actions: ["status"], locations: ["https://rs.example/pay"] }],
|
|
186
|
+
requested, true);
|
|
187
|
+
check("oauth: granted authorization_details subset accepted",
|
|
188
|
+
Array.isArray(subset) && subset.length === 1);
|
|
189
|
+
var overThrew = null;
|
|
190
|
+
try {
|
|
191
|
+
X._crossCheckGrantedAuthorizationDetails(
|
|
192
|
+
[{ type: "payment_initiation", actions: ["initiate", "transfer"] }], requested, true);
|
|
193
|
+
} catch (e) { overThrew = e; }
|
|
194
|
+
check("oauth: granted action beyond request refused (RFC 9396 over-grant)",
|
|
195
|
+
overThrew && overThrew.code === "auth-oauth/authorization-details-over-grant");
|
|
196
|
+
overThrew = null;
|
|
197
|
+
try {
|
|
198
|
+
X._crossCheckGrantedAuthorizationDetails([{ type: "account_access" }], requested, true);
|
|
199
|
+
} catch (e) { overThrew = e; }
|
|
200
|
+
check("oauth: granted unrequested type refused (RFC 9396 over-grant)",
|
|
201
|
+
overThrew && overThrew.code === "auth-oauth/authorization-details-over-grant");
|
|
202
|
+
// non-strict mode surfaces over-grant without throwing
|
|
203
|
+
var surfaced = X._crossCheckGrantedAuthorizationDetails([{ type: "account_access" }], requested, false);
|
|
204
|
+
check("oauth: non-strict cross-check surfaces granted without refusal",
|
|
205
|
+
Array.isArray(surfaced) && surfaced.length === 1);
|
|
206
|
+
// location over-grant
|
|
207
|
+
overThrew = null;
|
|
208
|
+
try {
|
|
209
|
+
X._crossCheckGrantedAuthorizationDetails(
|
|
210
|
+
[{ type: "payment_initiation", locations: ["https://rs.example/pay", "https://evil.example"] }],
|
|
211
|
+
requested, true);
|
|
212
|
+
} catch (e) { overThrew = e; }
|
|
213
|
+
check("oauth: granted location beyond request refused (RFC 9396 over-grant)",
|
|
214
|
+
overThrew && overThrew.code === "auth-oauth/authorization-details-over-grant");
|
|
215
|
+
// privileges over-grant — `privileges` is a registered array-valued common
|
|
216
|
+
// data field (RFC 9396 §2.1); a granted privileges array the request never
|
|
217
|
+
// constrained is the sharpest escalation and must be refused.
|
|
218
|
+
overThrew = null;
|
|
219
|
+
try {
|
|
220
|
+
X._crossCheckGrantedAuthorizationDetails(
|
|
221
|
+
[{ type: "payment_initiation", privileges: ["admin"] }], requested, true);
|
|
222
|
+
} catch (e) { overThrew = e; }
|
|
223
|
+
check("oauth: granted privilege beyond request refused (RFC 9396 over-grant)",
|
|
224
|
+
overThrew && overThrew.code === "auth-oauth/authorization-details-over-grant");
|
|
225
|
+
|
|
226
|
+
// ---- draft-ietf-oauth-attestation-based-client-auth ----
|
|
227
|
+
var attesterKp = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
228
|
+
var instanceKp = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
229
|
+
var instancePubJwk = instanceKp.publicKey.export({ format: "jwk" });
|
|
230
|
+
var attesterPubJwk = attesterKp.publicKey.export({ format: "jwk" });
|
|
231
|
+
|
|
232
|
+
var attestation = b.auth.oauth.buildClientAttestation({
|
|
233
|
+
clientId: "wallet-app",
|
|
234
|
+
attesterPrivateKey: attesterKp.privateKey,
|
|
235
|
+
instanceKeyJwk: instancePubJwk,
|
|
236
|
+
});
|
|
237
|
+
var pop = b.auth.oauth.buildClientAttestationPop({
|
|
238
|
+
instancePrivateKey: instanceKp.privateKey,
|
|
239
|
+
audience: "https://as.example.com",
|
|
240
|
+
});
|
|
241
|
+
// header typ values per draft §4 / §5
|
|
242
|
+
var attHeader = JSON.parse(Buffer.from(attestation.split(".")[0], "base64url").toString("utf8"));
|
|
243
|
+
var popHeader = JSON.parse(Buffer.from(pop.split(".")[0], "base64url").toString("utf8"));
|
|
244
|
+
check("oauth.attestation: attestation typ is oauth-client-attestation+jwt",
|
|
245
|
+
attHeader.typ === "oauth-client-attestation+jwt");
|
|
246
|
+
check("oauth.attestation: pop typ is oauth-client-attestation-pop+jwt",
|
|
247
|
+
popHeader.typ === "oauth-client-attestation-pop+jwt");
|
|
248
|
+
|
|
249
|
+
var jtiSeen = {};
|
|
250
|
+
var seenJti = function (jti) { if (jtiSeen[jti]) { return false; } jtiSeen[jti] = 1; return true; };
|
|
251
|
+
var verified = await b.auth.oauth.verifyClientAttestation(attestation, pop, {
|
|
252
|
+
attesterJwk: attesterPubJwk,
|
|
253
|
+
expectedAudience: "https://as.example.com",
|
|
254
|
+
expectedClientId: "wallet-app",
|
|
255
|
+
seenJti: seenJti,
|
|
256
|
+
});
|
|
257
|
+
check("oauth.verifyClientAttestation: valid pair → clientId from sub",
|
|
258
|
+
verified.clientId === "wallet-app");
|
|
259
|
+
check("oauth.verifyClientAttestation: returns the cnf key",
|
|
260
|
+
verified.cnfJwk && verified.cnfJwk.kty === "EC");
|
|
261
|
+
|
|
262
|
+
// replay (jti already seen) refused — draft §12.1
|
|
263
|
+
var attThrew = null;
|
|
264
|
+
try {
|
|
265
|
+
await b.auth.oauth.verifyClientAttestation(attestation, pop, {
|
|
266
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com", seenJti: seenJti });
|
|
267
|
+
} catch (e) { attThrew = e; }
|
|
268
|
+
check("oauth.verifyClientAttestation: jti replay refused (draft §12.1)",
|
|
269
|
+
attThrew && attThrew.code === "auth-oauth/attestation-pop-replay");
|
|
270
|
+
|
|
271
|
+
// wrong audience refused — draft §8 step 7
|
|
272
|
+
attThrew = null;
|
|
273
|
+
try {
|
|
274
|
+
await b.auth.oauth.verifyClientAttestation(attestation, pop, {
|
|
275
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://other.example.com" });
|
|
276
|
+
} catch (e) { attThrew = e; }
|
|
277
|
+
check("oauth.verifyClientAttestation: PoP aud mismatch refused",
|
|
278
|
+
attThrew && attThrew.code === "auth-oauth/attestation-pop-aud-mismatch");
|
|
279
|
+
|
|
280
|
+
// PoP forged with a key not in the cnf claim refused — possession proof
|
|
281
|
+
var attackerKp = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
282
|
+
var forgedPop = b.auth.oauth.buildClientAttestationPop({
|
|
283
|
+
instancePrivateKey: attackerKp.privateKey, audience: "https://as.example.com" });
|
|
284
|
+
attThrew = null;
|
|
285
|
+
try {
|
|
286
|
+
await b.auth.oauth.verifyClientAttestation(attestation, forgedPop, {
|
|
287
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com" });
|
|
288
|
+
} catch (e) { attThrew = e; }
|
|
289
|
+
check("oauth.verifyClientAttestation: PoP not signed by cnf key refused",
|
|
290
|
+
attThrew && attThrew.code === "auth-oauth/attestation-bad-signature");
|
|
291
|
+
|
|
292
|
+
// attestation forged by an untrusted attester refused
|
|
293
|
+
attThrew = null;
|
|
294
|
+
var rogueAtt = b.auth.oauth.buildClientAttestation({
|
|
295
|
+
clientId: "wallet-app", attesterPrivateKey: attackerKp.privateKey, instanceKeyJwk: instancePubJwk });
|
|
296
|
+
try {
|
|
297
|
+
await b.auth.oauth.verifyClientAttestation(rogueAtt, b.auth.oauth.buildClientAttestationPop({
|
|
298
|
+
instancePrivateKey: instanceKp.privateKey, audience: "https://as.example.com" }), {
|
|
299
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com" });
|
|
300
|
+
} catch (e) { attThrew = e; }
|
|
301
|
+
check("oauth.verifyClientAttestation: untrusted attester signature refused",
|
|
302
|
+
attThrew && attThrew.code === "auth-oauth/attestation-bad-signature");
|
|
303
|
+
|
|
304
|
+
// client_id mismatch refused — draft §8 step 10
|
|
305
|
+
attThrew = null;
|
|
306
|
+
try {
|
|
307
|
+
await b.auth.oauth.verifyClientAttestation(attestation, b.auth.oauth.buildClientAttestationPop({
|
|
308
|
+
instancePrivateKey: instanceKp.privateKey, audience: "https://as.example.com" }), {
|
|
309
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com",
|
|
310
|
+
expectedClientId: "different-client" });
|
|
311
|
+
} catch (e) { attThrew = e; }
|
|
312
|
+
check("oauth.verifyClientAttestation: client_id != attestation sub refused",
|
|
313
|
+
attThrew && attThrew.code === "auth-oauth/attestation-client-id-mismatch");
|
|
314
|
+
|
|
315
|
+
// challenge binding — draft §8 step 5/6
|
|
316
|
+
var popChal = b.auth.oauth.buildClientAttestationPop({
|
|
317
|
+
instancePrivateKey: instanceKp.privateKey, audience: "https://as.example.com", challenge: "srv-nonce-1" });
|
|
318
|
+
var vChal = await b.auth.oauth.verifyClientAttestation(attestation, popChal, {
|
|
319
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com", challenge: "srv-nonce-1" });
|
|
320
|
+
check("oauth.verifyClientAttestation: matching challenge accepted", vChal.clientId === "wallet-app");
|
|
321
|
+
attThrew = null;
|
|
322
|
+
try {
|
|
323
|
+
await b.auth.oauth.verifyClientAttestation(attestation, popChal, {
|
|
324
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com", challenge: "WRONG" });
|
|
325
|
+
} catch (e) { attThrew = e; }
|
|
326
|
+
check("oauth.verifyClientAttestation: challenge mismatch refused",
|
|
327
|
+
attThrew && attThrew.code === "auth-oauth/attestation-pop-challenge-mismatch");
|
|
328
|
+
|
|
329
|
+
// HMAC alg refused at build (no symmetric attestation)
|
|
330
|
+
attThrew = null;
|
|
331
|
+
try {
|
|
332
|
+
b.auth.oauth.buildClientAttestation({ clientId: "w", attesterPrivateKey: attesterKp.privateKey,
|
|
333
|
+
instanceKeyJwk: instancePubJwk, algorithm: "HS256" });
|
|
334
|
+
} catch (e) { attThrew = e; }
|
|
335
|
+
check("oauth.buildClientAttestation: HMAC alg refused",
|
|
336
|
+
attThrew && attThrew.code === "auth-oauth/attestation-alg-not-accepted");
|
|
337
|
+
|
|
338
|
+
// 5-segment JWE refused on the verifier path
|
|
339
|
+
attThrew = null;
|
|
340
|
+
try {
|
|
341
|
+
await b.auth.oauth.verifyClientAttestation("a.b.c.d.e", pop, {
|
|
342
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com" });
|
|
343
|
+
} catch (e) { attThrew = e; }
|
|
344
|
+
check("oauth.verifyClientAttestation: 5-segment JWE attestation refused",
|
|
345
|
+
attThrew && attThrew.code === "auth-oauth/attestation-jwe-refused");
|
|
346
|
+
|
|
347
|
+
// builder infers the JWS alg from the key type — a non-EC attester key
|
|
348
|
+
// with no explicit `algorithm` must produce a self-consistent JWS (header
|
|
349
|
+
// alg matches the signing key), not a fixed ES256 header the verifier's
|
|
350
|
+
// alg/kty cross-check would reject.
|
|
351
|
+
var edAttKp = crypto.generateKeyPairSync("ed25519");
|
|
352
|
+
var edAtt = b.auth.oauth.buildClientAttestation({
|
|
353
|
+
clientId: "wallet-app", attesterPrivateKey: edAttKp.privateKey, instanceKeyJwk: instancePubJwk });
|
|
354
|
+
var edAttHdr = JSON.parse(Buffer.from(edAtt.split(".")[0], "base64url").toString("utf8"));
|
|
355
|
+
check("oauth.buildClientAttestation: Ed25519 key infers EdDSA alg", edAttHdr.alg === "EdDSA");
|
|
356
|
+
var edVerified = await b.auth.oauth.verifyClientAttestation(edAtt, b.auth.oauth.buildClientAttestationPop({
|
|
357
|
+
instancePrivateKey: instanceKp.privateKey, audience: "https://as.example.com" }), {
|
|
358
|
+
attesterJwk: edAttKp.publicKey.export({ format: "jwk" }), expectedAudience: "https://as.example.com" });
|
|
359
|
+
check("oauth.verifyClientAttestation: Ed25519-signed attestation verifies", edVerified.clientId === "wallet-app");
|
|
360
|
+
|
|
361
|
+
var rsaAttKp = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
|
|
362
|
+
var rsaAttHdr = JSON.parse(Buffer.from(b.auth.oauth.buildClientAttestation({
|
|
363
|
+
clientId: "wallet-app", attesterPrivateKey: rsaAttKp.privateKey, instanceKeyJwk: instancePubJwk
|
|
364
|
+
}).split(".")[0], "base64url").toString("utf8"));
|
|
365
|
+
check("oauth.buildClientAttestation: RSA key infers RS256 alg", rsaAttHdr.alg === "RS256");
|
|
366
|
+
|
|
367
|
+
// an explicit alg incompatible with the key is refused BEFORE signing
|
|
368
|
+
// (a P-256 key cannot produce an RS256 signature).
|
|
369
|
+
attThrew = null;
|
|
370
|
+
try {
|
|
371
|
+
b.auth.oauth.buildClientAttestation({ clientId: "w", attesterPrivateKey: attesterKp.privateKey,
|
|
372
|
+
instanceKeyJwk: instancePubJwk, algorithm: "RS256" });
|
|
373
|
+
} catch (e) { attThrew = e; }
|
|
374
|
+
check("oauth.buildClientAttestation: explicit alg incompatible with key refused",
|
|
375
|
+
attThrew && attThrew.code === "auth-oauth/attestation-alg-key-mismatch");
|
|
376
|
+
|
|
377
|
+
// async (Promise) replay store is awaited — a Redis/DB seenJti returns a
|
|
378
|
+
// Promise; the verifier must await it so a resolved `false` (replayed
|
|
379
|
+
// jti) refuses instead of comparing a never-`false` Promise object.
|
|
380
|
+
var asyncSeen = {};
|
|
381
|
+
var asyncSeenJti = function (jti) {
|
|
382
|
+
return Promise.resolve().then(function () {
|
|
383
|
+
if (asyncSeen[jti]) { return false; }
|
|
384
|
+
asyncSeen[jti] = 1;
|
|
385
|
+
return true;
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
var popAsync = b.auth.oauth.buildClientAttestationPop({
|
|
389
|
+
instancePrivateKey: instanceKp.privateKey, audience: "https://as.example.com" });
|
|
390
|
+
var asyncOk = await b.auth.oauth.verifyClientAttestation(attestation, popAsync, {
|
|
391
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com", seenJti: asyncSeenJti });
|
|
392
|
+
check("oauth.verifyClientAttestation: async seenJti first sighting accepted",
|
|
393
|
+
asyncOk.clientId === "wallet-app");
|
|
394
|
+
attThrew = null;
|
|
395
|
+
try {
|
|
396
|
+
await b.auth.oauth.verifyClientAttestation(attestation, popAsync, {
|
|
397
|
+
attesterJwk: attesterPubJwk, expectedAudience: "https://as.example.com", seenJti: asyncSeenJti });
|
|
398
|
+
} catch (e) { attThrew = e; }
|
|
399
|
+
check("oauth.verifyClientAttestation: async seenJti replay refused (Promise awaited)",
|
|
400
|
+
attThrew && attThrew.code === "auth-oauth/attestation-pop-replay");
|
|
401
|
+
|
|
402
|
+
// instance-bound convenience builder produces both headers
|
|
403
|
+
var hdrs = oaRar.clientAttestationHeaders({
|
|
404
|
+
attesterPrivateKey: attesterKp.privateKey,
|
|
405
|
+
instanceKeyJwk: instancePubJwk,
|
|
406
|
+
instancePrivateKey: instanceKp.privateKey,
|
|
407
|
+
audience: "https://as.example.com",
|
|
408
|
+
});
|
|
409
|
+
check("oauth.clientAttestationHeaders: emits both header fields",
|
|
410
|
+
typeof hdrs.headers["OAuth-Client-Attestation"] === "string" &&
|
|
411
|
+
typeof hdrs.headers["OAuth-Client-Attestation-PoP"] === "string");
|
|
143
412
|
}
|
|
144
413
|
|
|
145
414
|
module.exports = { run: run };
|
|
@@ -607,6 +607,32 @@ async function testOtlpExporterRetryOn5xx() {
|
|
|
607
607
|
await exporter.shutdown();
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
+
function testTraceLogCorrelationRejectsAuditOpt() {
|
|
611
|
+
// The `audit` opt was an accepted-but-unread no-op (the middleware
|
|
612
|
+
// only wraps a logger — no audit-worthy event). De-advertised: it is
|
|
613
|
+
// no longer in the allowlist, so passing it throws at config time.
|
|
614
|
+
var threw = false;
|
|
615
|
+
try {
|
|
616
|
+
b.middleware.traceLogCorrelation({ logger: { info: function () {} }, audit: true });
|
|
617
|
+
} catch (_e) { threw = true; }
|
|
618
|
+
check("traceLogCorrelation: unknown 'audit' opt rejected", threw);
|
|
619
|
+
// Default surface still constructs without the key.
|
|
620
|
+
var mw = b.middleware.traceLogCorrelation({ logger: { info: function () {} } });
|
|
621
|
+
check("traceLogCorrelation: constructs without audit opt", typeof mw === "function");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function testTracerRejectsAuditOpt() {
|
|
625
|
+
// `audit` was accepted by tracer.create but never read (the tracer is
|
|
626
|
+
// a pure observability primitive — span lifecycle goes to observability
|
|
627
|
+
// counters, not the audit log). De-advertised.
|
|
628
|
+
var threw = false;
|
|
629
|
+
try { b.observability.tracer.create({ service: "test", audit: true }); }
|
|
630
|
+
catch (_e) { threw = true; }
|
|
631
|
+
check("tracer.create: unknown 'audit' opt rejected", threw);
|
|
632
|
+
var tracer = b.observability.tracer.create({ service: "test" });
|
|
633
|
+
check("tracer.create: constructs without audit opt", typeof tracer.start === "function");
|
|
634
|
+
}
|
|
635
|
+
|
|
610
636
|
function testTracerToJSONIsImmutable() {
|
|
611
637
|
var tracer = b.observability.tracer.create({ service: "test" });
|
|
612
638
|
var span = tracer.start("op");
|
|
@@ -649,4 +675,6 @@ function testTracerToJSONIsImmutable() {
|
|
|
649
675
|
testTracerStatusCodeValidation();
|
|
650
676
|
await testOtlpExporterRetryOn5xx();
|
|
651
677
|
testTracerToJSONIsImmutable();
|
|
678
|
+
testTraceLogCorrelationRejectsAuditOpt();
|
|
679
|
+
testTracerRejectsAuditOpt();
|
|
652
680
|
})().catch(function (e) { console.error(e); process.exit(1); });
|
|
@@ -164,6 +164,43 @@ function testObservabilityEventDropsBadName() {
|
|
|
164
164
|
m.deactivate();
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
function testObservabilitySemconvResourceAttributes() {
|
|
168
|
+
var S = b.observability.SEMCONV;
|
|
169
|
+
check("SEMCONV is frozen", Object.isFrozen(S));
|
|
170
|
+
// Resource / general additions (OTel semconv).
|
|
171
|
+
check("SEMCONV.PEER_SERVICE", S.PEER_SERVICE === "peer.service");
|
|
172
|
+
check("SEMCONV.DEPLOYMENT_ENVIRONMENT_NAME",
|
|
173
|
+
S.DEPLOYMENT_ENVIRONMENT_NAME === "deployment.environment.name");
|
|
174
|
+
check("SEMCONV.TELEMETRY_DISTRO_NAME",
|
|
175
|
+
S.TELEMETRY_DISTRO_NAME === "telemetry.distro.name");
|
|
176
|
+
check("SEMCONV.TELEMETRY_DISTRO_VERSION",
|
|
177
|
+
S.TELEMETRY_DISTRO_VERSION === "telemetry.distro.version");
|
|
178
|
+
check("SEMCONV.OTEL_SCOPE_NAME", S.OTEL_SCOPE_NAME === "otel.scope.name");
|
|
179
|
+
check("SEMCONV.OTEL_SCOPE_VERSION", S.OTEL_SCOPE_VERSION === "otel.scope.version");
|
|
180
|
+
// FaaS (serverless).
|
|
181
|
+
check("SEMCONV.FAAS_NAME", S.FAAS_NAME === "faas.name");
|
|
182
|
+
check("SEMCONV.FAAS_VERSION", S.FAAS_VERSION === "faas.version");
|
|
183
|
+
check("SEMCONV.FAAS_INSTANCE", S.FAAS_INSTANCE === "faas.instance");
|
|
184
|
+
check("SEMCONV.FAAS_TRIGGER", S.FAAS_TRIGGER === "faas.trigger");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function testObservabilitySemconvK8sAttributes() {
|
|
188
|
+
var S = b.observability.SEMCONV;
|
|
189
|
+
// Pre-existing trio kept.
|
|
190
|
+
check("SEMCONV.K8S_NAMESPACE_NAME", S.K8S_NAMESPACE_NAME === "k8s.namespace.name");
|
|
191
|
+
check("SEMCONV.K8S_POD_NAME", S.K8S_POD_NAME === "k8s.pod.name");
|
|
192
|
+
check("SEMCONV.K8S_DEPLOYMENT_NAME", S.K8S_DEPLOYMENT_NAME === "k8s.deployment.name");
|
|
193
|
+
// New workload + node + cluster subset.
|
|
194
|
+
check("SEMCONV.K8S_NODE_NAME", S.K8S_NODE_NAME === "k8s.node.name");
|
|
195
|
+
check("SEMCONV.K8S_CLUSTER_NAME", S.K8S_CLUSTER_NAME === "k8s.cluster.name");
|
|
196
|
+
check("SEMCONV.K8S_CONTAINER_NAME", S.K8S_CONTAINER_NAME === "k8s.container.name");
|
|
197
|
+
check("SEMCONV.K8S_STATEFULSET_NAME", S.K8S_STATEFULSET_NAME === "k8s.statefulset.name");
|
|
198
|
+
check("SEMCONV.K8S_DAEMONSET_NAME", S.K8S_DAEMONSET_NAME === "k8s.daemonset.name");
|
|
199
|
+
check("SEMCONV.K8S_JOB_NAME", S.K8S_JOB_NAME === "k8s.job.name");
|
|
200
|
+
check("SEMCONV.K8S_CRONJOB_NAME", S.K8S_CRONJOB_NAME === "k8s.cronjob.name");
|
|
201
|
+
check("SEMCONV.K8S_REPLICASET_NAME", S.K8S_REPLICASET_NAME === "k8s.replicaset.name");
|
|
202
|
+
}
|
|
203
|
+
|
|
167
204
|
async function run() {
|
|
168
205
|
testObservabilitySurface();
|
|
169
206
|
testObservabilityTapRunsFnWithoutRegistries();
|
|
@@ -178,6 +215,8 @@ async function run() {
|
|
|
178
215
|
testObservabilityTapRejectsBadFn();
|
|
179
216
|
testObservabilityTapRejectsBadName();
|
|
180
217
|
testObservabilityEventDropsBadName();
|
|
218
|
+
testObservabilitySemconvResourceAttributes();
|
|
219
|
+
testObservabilitySemconvK8sAttributes();
|
|
181
220
|
}
|
|
182
221
|
|
|
183
222
|
module.exports = { run: run };
|
|
@@ -733,6 +733,43 @@ function run() {
|
|
|
733
733
|
|
|
734
734
|
check("openapiServe.forceRebuild is fn", typeof serve.forceRebuild === "function");
|
|
735
735
|
|
|
736
|
+
// HEAD carries the GET response headers (incl. Content-Length) with an
|
|
737
|
+
// EMPTY body (RFC 9110 §9.3.2). GET unchanged: it still returns the
|
|
738
|
+
// body. Asserts the head-suppression against both JSON and YAML mounts.
|
|
739
|
+
var sentHeadJson = { status: null, headers: null, body: undefined, ended: false };
|
|
740
|
+
serve({ method: "HEAD", url: "/openapi.json", pathname: "/openapi.json", headers: {} },
|
|
741
|
+
{ writeHead: function (s, h) { sentHeadJson.status = s; sentHeadJson.headers = h; },
|
|
742
|
+
end: function (bdy) { sentHeadJson.body = bdy; sentHeadJson.ended = true; } },
|
|
743
|
+
function () {});
|
|
744
|
+
check("openapiServe HEAD JSON: 200", sentHeadJson.status === 200);
|
|
745
|
+
check("openapiServe HEAD JSON: Content-Length set like GET",
|
|
746
|
+
sentHeadJson.headers["Content-Length"] === sentJson.headers["Content-Length"]);
|
|
747
|
+
check("openapiServe HEAD JSON: Content-Type set like GET",
|
|
748
|
+
sentHeadJson.headers["Content-Type"] === sentJson.headers["Content-Type"]);
|
|
749
|
+
check("openapiServe HEAD JSON: empty body",
|
|
750
|
+
sentHeadJson.ended === true && (sentHeadJson.body === undefined || sentHeadJson.body == null));
|
|
751
|
+
|
|
752
|
+
var sentHeadYaml = { status: null, headers: null, body: undefined, ended: false };
|
|
753
|
+
serve({ method: "HEAD", url: "/openapi.yaml", pathname: "/openapi.yaml", headers: {} },
|
|
754
|
+
{ writeHead: function (s, h) { sentHeadYaml.status = s; sentHeadYaml.headers = h; },
|
|
755
|
+
end: function (bdy) { sentHeadYaml.body = bdy; sentHeadYaml.ended = true; } },
|
|
756
|
+
function () {});
|
|
757
|
+
check("openapiServe HEAD YAML: 200", sentHeadYaml.status === 200);
|
|
758
|
+
check("openapiServe HEAD YAML: Content-Length set like GET",
|
|
759
|
+
sentHeadYaml.headers["Content-Length"] === sentYaml.headers["Content-Length"]);
|
|
760
|
+
check("openapiServe HEAD YAML: empty body",
|
|
761
|
+
sentHeadYaml.ended === true && (sentHeadYaml.body === undefined || sentHeadYaml.body == null));
|
|
762
|
+
|
|
763
|
+
// GET still returns the body after the HEAD path was added.
|
|
764
|
+
var sentGetAfter = { status: null, headers: null, body: null };
|
|
765
|
+
serve({ method: "GET", url: "/openapi.json", pathname: "/openapi.json", headers: {} },
|
|
766
|
+
{ writeHead: function (s, h) { sentGetAfter.status = s; sentGetAfter.headers = h; },
|
|
767
|
+
end: function (bdy) { sentGetAfter.body = bdy; } },
|
|
768
|
+
function () {});
|
|
769
|
+
check("openapiServe GET still returns body",
|
|
770
|
+
sentGetAfter.status === 200 && typeof sentGetAfter.body === "string" &&
|
|
771
|
+
sentGetAfter.body.length > 0);
|
|
772
|
+
|
|
736
773
|
// ---- bigger schema-walk coverage ----
|
|
737
774
|
var arrSchema = b.openapi.schemaWalk({
|
|
738
775
|
type: "array",
|
|
@@ -63,6 +63,83 @@ function testCreateRefusesBadShape() {
|
|
|
63
63
|
"problem-details/reserved-extension");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function testCreateExtensions() {
|
|
67
|
+
// RFC 9457 §3.2 — `extensions` keys are spread as top-level siblings;
|
|
68
|
+
// the literal `extensions` member is never emitted.
|
|
69
|
+
var p = b.problemDetails.create({
|
|
70
|
+
status: 400, title: "t", extensions: { balance: 30, accounts: ["/a"] },
|
|
71
|
+
});
|
|
72
|
+
check("create: extensions spread balance to top level", p.balance === 30);
|
|
73
|
+
check("create: extensions spread accounts to top level",
|
|
74
|
+
Array.isArray(p.accounts) && p.accounts[0] === "/a");
|
|
75
|
+
check("create: no literal extensions member", !("extensions" in p));
|
|
76
|
+
check("create: extensions output frozen", Object.isFrozen(p));
|
|
77
|
+
|
|
78
|
+
// Reserved fields can't be overridden by an extension key.
|
|
79
|
+
var p2 = b.problemDetails.create({
|
|
80
|
+
status: 400, title: "real-title",
|
|
81
|
+
extensions: { title: "spoofed", status: 999, type: "evil", detail: "x", instance: "y" },
|
|
82
|
+
});
|
|
83
|
+
check("create: extensions cannot override title", p2.title === "real-title");
|
|
84
|
+
check("create: extensions cannot override status", p2.status === 400);
|
|
85
|
+
check("create: extensions cannot override type", p2.type === "about:blank");
|
|
86
|
+
check("create: extensions cannot inject detail", !("detail" in p2));
|
|
87
|
+
check("create: extensions cannot inject instance", !("instance" in p2));
|
|
88
|
+
|
|
89
|
+
// Direct top-level key wins over the same name nested under extensions.
|
|
90
|
+
var p3 = b.problemDetails.create({
|
|
91
|
+
status: 400, balance: 99, extensions: { balance: 30 },
|
|
92
|
+
});
|
|
93
|
+
check("create: direct top-level key wins over extensions", p3.balance === 99);
|
|
94
|
+
|
|
95
|
+
// Poisoned keys inside extensions are dropped silently (not thrown).
|
|
96
|
+
var p4 = b.problemDetails.create({
|
|
97
|
+
status: 400, extensions: JSON.parse('{"__proto__":{"x":1},"safe":7}'),
|
|
98
|
+
});
|
|
99
|
+
check("create: poisoned extension key dropped, no throw", p4.safe === 7);
|
|
100
|
+
check("create: poisoned key did not pollute prototype", ({}).x === undefined);
|
|
101
|
+
|
|
102
|
+
// Non-plain-object extensions throw at config time.
|
|
103
|
+
function expectCode(label, fn, code) {
|
|
104
|
+
var threw = null;
|
|
105
|
+
try { fn(); } catch (e) { threw = e; }
|
|
106
|
+
check(label, threw && (threw.code || "").indexOf(code) !== -1);
|
|
107
|
+
}
|
|
108
|
+
expectCode("create: extensions array refused",
|
|
109
|
+
function () { b.problemDetails.create({ status: 400, extensions: [] }); },
|
|
110
|
+
"problem-details/bad-extensions");
|
|
111
|
+
expectCode("create: extensions string refused",
|
|
112
|
+
function () { b.problemDetails.create({ status: 400, extensions: "x" }); },
|
|
113
|
+
"problem-details/bad-extensions");
|
|
114
|
+
|
|
115
|
+
// null / undefined extensions are a no-op (no literal member, no throw).
|
|
116
|
+
var p5 = b.problemDetails.create({ status: 400, extensions: null });
|
|
117
|
+
check("create: null extensions is a no-op", !("extensions" in p5) && p5.status === 400);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function testSendExtensions() {
|
|
121
|
+
// Success path end-to-end through send(): extensions spread as
|
|
122
|
+
// siblings, no nested `extensions` member emitted.
|
|
123
|
+
var headers = {};
|
|
124
|
+
var statusCode = null;
|
|
125
|
+
var body = null;
|
|
126
|
+
var fakeRes = {
|
|
127
|
+
setHeader: function (k, v) { headers[k.toLowerCase()] = v; },
|
|
128
|
+
end: function (b2) { body = b2; },
|
|
129
|
+
};
|
|
130
|
+
Object.defineProperty(fakeRes, "statusCode", {
|
|
131
|
+
get: function () { return statusCode; },
|
|
132
|
+
set: function (v) { statusCode = v; },
|
|
133
|
+
});
|
|
134
|
+
b.problemDetails.send(fakeRes, { status: 400, extensions: { balance: 30 } });
|
|
135
|
+
var doc = JSON.parse(body);
|
|
136
|
+
check("send+extensions: statusCode 400", statusCode === 400);
|
|
137
|
+
check("send+extensions: problem+json type", headers["content-type"] === "application/problem+json");
|
|
138
|
+
check("send+extensions: balance is a top-level sibling", doc.balance === 30);
|
|
139
|
+
check("send+extensions: status emitted", doc.status === 400);
|
|
140
|
+
check("send+extensions: NO nested extensions member", !("extensions" in doc));
|
|
141
|
+
}
|
|
142
|
+
|
|
66
143
|
function testFromError() {
|
|
67
144
|
b.problemDetails._resetForTest();
|
|
68
145
|
var err = new (b.frameworkError.ComplianceError)("compliance/unknown-posture", "bad posture", true);
|
|
@@ -179,6 +256,8 @@ async function run() {
|
|
|
179
256
|
testCreateDefaults();
|
|
180
257
|
testCreateFullShape();
|
|
181
258
|
testCreateRefusesBadShape();
|
|
259
|
+
testCreateExtensions();
|
|
260
|
+
testSendExtensions();
|
|
182
261
|
testFromError();
|
|
183
262
|
testFromErrorWithStatusCode();
|
|
184
263
|
testSetBase();
|
|
@@ -106,6 +106,54 @@ async function testPatternSubscribe() {
|
|
|
106
106
|
await ps.close();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
async function testClusterNumericConfigValidation() {
|
|
110
|
+
// pollIntervalMs / retentionMs / pruneEveryMs are config-time numeric
|
|
111
|
+
// knobs on the cluster backend. A typo (NaN-coercing string / negative /
|
|
112
|
+
// fractional) must THROW at create rather than silently coercing to the
|
|
113
|
+
// default and shipping a mis-tuned poll loop. The throw happens before
|
|
114
|
+
// any DB is touched, so no test-db setup is needed.
|
|
115
|
+
var nodeA = { currentNodeId: function () { return "node-A"; } };
|
|
116
|
+
function shouldThrow(label, overrides) {
|
|
117
|
+
var threw = null;
|
|
118
|
+
try {
|
|
119
|
+
b.pubsub.create(Object.assign(
|
|
120
|
+
{ backend: "cluster", cluster: nodeA }, overrides));
|
|
121
|
+
} catch (e) { threw = e; }
|
|
122
|
+
check("cluster-config: " + label,
|
|
123
|
+
threw && (threw.code === "BAD_OPT" || /BAD_OPT/.test(threw.code || "")));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
shouldThrow("rejects NaN-coercing pollIntervalMs", { pollIntervalMs: "30ms" });
|
|
127
|
+
shouldThrow("rejects negative pollIntervalMs", { pollIntervalMs: -1 });
|
|
128
|
+
shouldThrow("rejects fractional pollIntervalMs", { pollIntervalMs: 1.5 });
|
|
129
|
+
shouldThrow("rejects zero pollIntervalMs", { pollIntervalMs: 0 });
|
|
130
|
+
shouldThrow("rejects NaN-coercing retentionMs", { retentionMs: "1m" });
|
|
131
|
+
shouldThrow("rejects negative retentionMs", { retentionMs: -100 });
|
|
132
|
+
shouldThrow("rejects NaN-coercing pruneEveryMs", { pruneEveryMs: {} });
|
|
133
|
+
shouldThrow("rejects negative pruneEveryMs", { pruneEveryMs: -5 });
|
|
134
|
+
|
|
135
|
+
// Absent keeps the default — create succeeds (returns a live instance).
|
|
136
|
+
var ok = null;
|
|
137
|
+
try {
|
|
138
|
+
ok = b.pubsub.create({ backend: "cluster", cluster: nodeA });
|
|
139
|
+
} catch (e) { ok = e; }
|
|
140
|
+
check("cluster-config: absent numeric knobs keep defaults",
|
|
141
|
+
ok && typeof ok.publish === "function");
|
|
142
|
+
if (ok && typeof ok.close === "function") { await ok.close(); }
|
|
143
|
+
|
|
144
|
+
// Valid positive integers flow through.
|
|
145
|
+
var ok2 = null;
|
|
146
|
+
try {
|
|
147
|
+
ok2 = b.pubsub.create({
|
|
148
|
+
backend: "cluster", cluster: nodeA,
|
|
149
|
+
pollIntervalMs: 50, retentionMs: 120000, pruneEveryMs: 300000,
|
|
150
|
+
});
|
|
151
|
+
} catch (e) { ok2 = e; }
|
|
152
|
+
check("cluster-config: valid numeric knobs accepted",
|
|
153
|
+
ok2 && typeof ok2.publish === "function");
|
|
154
|
+
if (ok2 && typeof ok2.close === "function") { await ok2.close(); }
|
|
155
|
+
}
|
|
156
|
+
|
|
109
157
|
async function testClusterFanOut() {
|
|
110
158
|
// Two pubsub instances sharing the same cluster DB simulate two
|
|
111
159
|
// nodes. A publish on instance A is observed by instance B's poll
|
|
@@ -218,6 +266,7 @@ async function run() {
|
|
|
218
266
|
await testTopicPrefixIsolation();
|
|
219
267
|
await testPatternSubscribe();
|
|
220
268
|
await testClosedRejectsPublishSubscribe();
|
|
269
|
+
await testClusterNumericConfigValidation();
|
|
221
270
|
await testClusterFanOut();
|
|
222
271
|
await testCacheInvalidationFanOut();
|
|
223
272
|
}
|
|
@@ -277,8 +277,56 @@ async function testSessionTokenHeader() {
|
|
|
277
277
|
} finally { await srv.close(); }
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
async function testNumericConfigValidation() {
|
|
281
|
+
// visibilityTimeoutSec / waitTimeSec are config-time numeric knobs.
|
|
282
|
+
// A typo (NaN-coercing string / negative / fractional) must THROW at
|
|
283
|
+
// create rather than silently coercing to the default and shipping a
|
|
284
|
+
// mis-tuned lease loop.
|
|
285
|
+
function shouldThrow(label, overrides, codeRe) {
|
|
286
|
+
var threw = null;
|
|
287
|
+
try {
|
|
288
|
+
sqs.create(Object.assign({}, _baseConfig(9999), overrides));
|
|
289
|
+
} catch (e) { threw = e; }
|
|
290
|
+
check("numeric-config: " + label, threw && codeRe.test(threw.code || ""));
|
|
291
|
+
}
|
|
292
|
+
function shouldPass(label, overrides) {
|
|
293
|
+
var ok = null;
|
|
294
|
+
try {
|
|
295
|
+
ok = sqs.create(Object.assign({}, _baseConfig(9999), overrides));
|
|
296
|
+
} catch (e) { ok = e; }
|
|
297
|
+
check("numeric-config: " + label, ok && typeof ok.enqueue === "function");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Present-but-bad visibilityTimeoutSec throws.
|
|
301
|
+
shouldThrow("rejects NaN-coercing visibilityTimeoutSec",
|
|
302
|
+
{ visibilityTimeoutSec: "30s" }, /INVALID_CONFIG/);
|
|
303
|
+
shouldThrow("rejects negative visibilityTimeoutSec",
|
|
304
|
+
{ visibilityTimeoutSec: -1 }, /INVALID_CONFIG/);
|
|
305
|
+
shouldThrow("rejects fractional visibilityTimeoutSec",
|
|
306
|
+
{ visibilityTimeoutSec: 1.5 }, /INVALID_CONFIG/);
|
|
307
|
+
shouldThrow("rejects zero visibilityTimeoutSec (not a positive int)",
|
|
308
|
+
{ visibilityTimeoutSec: 0 }, /INVALID_CONFIG/);
|
|
309
|
+
|
|
310
|
+
// Present-but-bad waitTimeSec throws — but 0 (short-poll) stays valid.
|
|
311
|
+
shouldThrow("rejects NaN-coercing waitTimeSec",
|
|
312
|
+
{ waitTimeSec: "10s" }, /INVALID_CONFIG/);
|
|
313
|
+
shouldThrow("rejects negative waitTimeSec",
|
|
314
|
+
{ waitTimeSec: -1 }, /INVALID_CONFIG/);
|
|
315
|
+
shouldThrow("rejects fractional waitTimeSec",
|
|
316
|
+
{ waitTimeSec: 2.5 }, /INVALID_CONFIG/);
|
|
317
|
+
|
|
318
|
+
// Absent keeps the default (create succeeds, returns a live adapter).
|
|
319
|
+
shouldPass("absent numeric knobs keep defaults", {});
|
|
320
|
+
// Valid values flow through.
|
|
321
|
+
shouldPass("accepts valid visibilityTimeoutSec", { visibilityTimeoutSec: 60 });
|
|
322
|
+
shouldPass("accepts valid waitTimeSec", { waitTimeSec: 20 });
|
|
323
|
+
// waitTimeSec=0 is the valid SQS short-poll sentinel — must NOT throw.
|
|
324
|
+
shouldPass("accepts waitTimeSec=0 (short-poll sentinel)", { waitTimeSec: 0 });
|
|
325
|
+
}
|
|
326
|
+
|
|
280
327
|
async function run() {
|
|
281
328
|
await testFactoryValidation();
|
|
329
|
+
await testNumericConfigValidation();
|
|
282
330
|
await testEnqueueWireShape();
|
|
283
331
|
await testDelaySecondsClampedAt900();
|
|
284
332
|
await testLeaseRoundTrip();
|