@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +254 -1
  4. package/lib/asset-manifest.json +1 -1
  5. package/lib/vendor/MANIFEST.json +95 -83
  6. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
  7. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +1 -1
  8. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +1 -1
  9. package/lib/vendor/blamejs/.github/workflows/ci.yml +10 -10
  10. package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
  11. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +2 -2
  12. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  13. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +2 -2
  14. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
  15. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  16. package/lib/vendor/blamejs/README.md +1 -1
  17. package/lib/vendor/blamejs/SECURITY.md +2 -0
  18. package/lib/vendor/blamejs/api-snapshot.json +108 -4
  19. package/lib/vendor/blamejs/lib/auth/oauth.js +736 -1
  20. package/lib/vendor/blamejs/lib/auth/oid4vci.js +124 -5
  21. package/lib/vendor/blamejs/lib/auth/oid4vp.js +14 -4
  22. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +46 -1
  23. package/lib/vendor/blamejs/lib/break-glass.js +1 -2
  24. package/lib/vendor/blamejs/lib/config.js +28 -31
  25. package/lib/vendor/blamejs/lib/crypto-field.js +274 -17
  26. package/lib/vendor/blamejs/lib/dora.js +8 -5
  27. package/lib/vendor/blamejs/lib/dsr.js +2 -2
  28. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +7 -0
  29. package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +4 -2
  30. package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +4 -2
  31. package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +4 -2
  32. package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +20 -0
  33. package/lib/vendor/blamejs/lib/guard-html-wcag.js +1 -1
  34. package/lib/vendor/blamejs/lib/honeytoken.js +27 -20
  35. package/lib/vendor/blamejs/lib/mail-auth.js +333 -0
  36. package/lib/vendor/blamejs/lib/mail-deploy.js +1 -1
  37. package/lib/vendor/blamejs/lib/mail-send-deliver.js +13 -4
  38. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +140 -13
  39. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -0
  40. package/lib/vendor/blamejs/lib/middleware/csp-report.js +13 -9
  41. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +115 -14
  42. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -0
  43. package/lib/vendor/blamejs/lib/middleware/scim-server.js +297 -19
  44. package/lib/vendor/blamejs/lib/middleware/security-headers.js +47 -0
  45. package/lib/vendor/blamejs/lib/middleware/security-txt.js +1 -2
  46. package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +1 -2
  47. package/lib/vendor/blamejs/lib/network-smtp-policy.js +4 -4
  48. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +11 -2
  49. package/lib/vendor/blamejs/lib/observability-tracer.js +1 -1
  50. package/lib/vendor/blamejs/lib/observability.js +39 -1
  51. package/lib/vendor/blamejs/lib/problem-details.js +56 -11
  52. package/lib/vendor/blamejs/lib/pubsub-cluster.js +16 -3
  53. package/lib/vendor/blamejs/lib/queue-sqs.js +20 -2
  54. package/lib/vendor/blamejs/lib/redis-client.js +32 -4
  55. package/lib/vendor/blamejs/lib/safe-redirect.js +16 -2
  56. package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +3 -2
  57. package/lib/vendor/blamejs/package.json +1 -1
  58. package/lib/vendor/blamejs/release-notes/v0.14.20.json +73 -0
  59. package/lib/vendor/blamejs/release-notes/v0.14.21.json +98 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +339 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +37 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +22 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +315 -5
  64. package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +46 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +176 -0
  66. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +86 -0
  67. package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +38 -0
  68. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +29 -0
  69. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +236 -1
  70. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +190 -0
  71. package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +23 -0
  72. package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +59 -0
  73. package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +26 -0
  74. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +16 -0
  76. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +108 -0
  77. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +269 -0
  78. package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +28 -0
  79. package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +39 -0
  80. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +37 -0
  81. package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +79 -0
  82. package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +49 -0
  83. package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +48 -0
  84. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +60 -0
  85. package/lib/vendor/blamejs/test/layer-0-primitives/safe-redirect.test.js +118 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +259 -0
  87. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +46 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +113 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/security-txt.test.js +111 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +62 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +39 -0
  92. 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();