@blamejs/blamejs-shop 0.5.19 → 0.5.21

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 (194) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -0
  3. package/SECURITY.md +17 -0
  4. package/lib/addresses.js +2 -2
  5. package/lib/admin.js +49 -45
  6. package/lib/affiliates.js +7 -23
  7. package/lib/announcement-bar.js +5 -22
  8. package/lib/api-keys.js +4 -13
  9. package/lib/asset-manifest.json +1 -1
  10. package/lib/banner-ab-tests.js +5 -9
  11. package/lib/blog-articles.js +8 -26
  12. package/lib/captcha-gate.js +7 -17
  13. package/lib/carrier-accounts.js +3 -10
  14. package/lib/catalog-drafts.js +5 -12
  15. package/lib/category-navigation.js +4 -20
  16. package/lib/cms-blocks.js +4 -7
  17. package/lib/compliance-export.js +4 -11
  18. package/lib/consent-ledger.js +2 -4
  19. package/lib/cost-layers.js +56 -21
  20. package/lib/customer-import.js +2 -2
  21. package/lib/customer-notes.js +7 -23
  22. package/lib/customer-roles.js +3 -5
  23. package/lib/customer-segments.js +17 -14
  24. package/lib/customer-surveys.js +7 -14
  25. package/lib/customers.js +2 -4
  26. package/lib/dispute-resolution.js +5 -13
  27. package/lib/dropship-forwarding.js +3 -5
  28. package/lib/dunning.js +2 -7
  29. package/lib/email-ab-tests.js +4 -8
  30. package/lib/email-campaigns.js +25 -14
  31. package/lib/email-warmup.js +2 -7
  32. package/lib/experiments.js +6 -10
  33. package/lib/externaldb-d1.js +78 -44
  34. package/lib/geolocation.js +18 -28
  35. package/lib/gift-options.js +5 -23
  36. package/lib/knowledge-base.js +7 -22
  37. package/lib/line-gift-wrap.js +3 -17
  38. package/lib/live-chat.js +5 -18
  39. package/lib/metered-usage.js +3 -8
  40. package/lib/notifications.js +3 -7
  41. package/lib/operator-accounts.js +3 -5
  42. package/lib/operator-approvals.js +6 -8
  43. package/lib/operator-audit-log.js +2 -7
  44. package/lib/operator-help-center.js +5 -21
  45. package/lib/operator-inbox.js +4 -11
  46. package/lib/operator-roles.js +5 -7
  47. package/lib/order-escalation.js +2 -2
  48. package/lib/order-export.js +24 -20
  49. package/lib/order-notes.js +4 -14
  50. package/lib/order-ratings.js +4 -10
  51. package/lib/payment-methods.js +5 -13
  52. package/lib/payment-retries.js +2 -7
  53. package/lib/payment.js +103 -58
  54. package/lib/pixel-events.js +8 -23
  55. package/lib/plan-changes.js +2 -7
  56. package/lib/product-qa.js +4 -8
  57. package/lib/promo-banners.js +7 -15
  58. package/lib/purchase-orders.js +3 -6
  59. package/lib/push-notifications.js +7 -32
  60. package/lib/pwa-manifest.js +6 -11
  61. package/lib/quotes.js +5 -9
  62. package/lib/referrals.js +10 -6
  63. package/lib/reviews.js +4 -10
  64. package/lib/robots-config.js +6 -14
  65. package/lib/sales-tax-filings.js +2 -2
  66. package/lib/search-facets.js +5 -5
  67. package/lib/search-ranking.js +4 -7
  68. package/lib/search-synonyms.js +7 -21
  69. package/lib/security-middleware.js +125 -70
  70. package/lib/seller-signup.js +10 -26
  71. package/lib/sidebar-widgets.js +6 -15
  72. package/lib/site-redirects.js +49 -137
  73. package/lib/sitemap-generator.js +3 -7
  74. package/lib/sms-dispatcher.js +7 -23
  75. package/lib/stock-receipts.js +4 -10
  76. package/lib/storefront-dashboards.js +2 -10
  77. package/lib/storefront-forms.js +9 -22
  78. package/lib/storefront-pages.js +6 -20
  79. package/lib/storefront.js +21 -23
  80. package/lib/subscription-billing.js +3 -8
  81. package/lib/subscription-controls.js +2 -7
  82. package/lib/suggestion-box.js +9 -26
  83. package/lib/support-tickets.js +6 -24
  84. package/lib/tax-remittance.js +2 -2
  85. package/lib/tenants.js +2 -7
  86. package/lib/text-guard.js +137 -35
  87. package/lib/theme-assets.js +4 -16
  88. package/lib/translations.js +3 -6
  89. package/lib/trust-badges.js +4 -7
  90. package/lib/vendor/MANIFEST.json +107 -97
  91. package/lib/vendor/blamejs/CHANGELOG.md +180 -0
  92. package/lib/vendor/blamejs/NOTICE +14 -10
  93. package/lib/vendor/blamejs/README.md +17 -5
  94. package/lib/vendor/blamejs/index.js +2 -0
  95. package/lib/vendor/blamejs/lib/acme.js +9 -9
  96. package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
  97. package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
  98. package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
  99. package/lib/vendor/blamejs/lib/archive.js +13 -24
  100. package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
  101. package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
  102. package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
  103. package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
  104. package/lib/vendor/blamejs/lib/auth/password.js +1 -1
  105. package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
  106. package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
  107. package/lib/vendor/blamejs/lib/backup/index.js +5 -5
  108. package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
  109. package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
  110. package/lib/vendor/blamejs/lib/constants.js +79 -0
  111. package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
  112. package/lib/vendor/blamejs/lib/error-page.js +20 -1
  113. package/lib/vendor/blamejs/lib/external-db.js +5 -1
  114. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
  115. package/lib/vendor/blamejs/lib/flag.js +28 -2
  116. package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
  117. package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
  118. package/lib/vendor/blamejs/lib/http-client.js +217 -40
  119. package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
  120. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
  121. package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
  122. package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
  123. package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
  124. package/lib/vendor/blamejs/lib/mail-store.js +7 -1
  125. package/lib/vendor/blamejs/lib/mail.js +5 -1
  126. package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
  127. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
  128. package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
  129. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
  130. package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
  131. package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
  132. package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
  133. package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
  134. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
  135. package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
  136. package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
  137. package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
  138. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
  139. package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
  140. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
  141. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
  142. package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
  143. package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
  144. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
  145. package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
  146. package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
  147. package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
  148. package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
  149. package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
  150. package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
  151. package/lib/vendor/blamejs/lib/network-dns.js +3 -3
  152. package/lib/vendor/blamejs/lib/network-nts.js +35 -14
  153. package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
  154. package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
  155. package/lib/vendor/blamejs/lib/network-tls.js +341 -11
  156. package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
  157. package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
  158. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
  159. package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
  160. package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
  161. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
  162. package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
  163. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
  164. package/lib/vendor/blamejs/lib/openapi.js +1 -1
  165. package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
  166. package/lib/vendor/blamejs/lib/problem-details.js +2 -1
  167. package/lib/vendor/blamejs/lib/redis-client.js +12 -1
  168. package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
  169. package/lib/vendor/blamejs/lib/render.js +531 -7
  170. package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
  171. package/lib/vendor/blamejs/lib/router.js +37 -13
  172. package/lib/vendor/blamejs/lib/safe-async.js +142 -0
  173. package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
  174. package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
  175. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  176. package/lib/vendor/blamejs/lib/testing.js +54 -7
  177. package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
  178. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
  179. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
  180. package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
  181. package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
  182. package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
  183. package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
  184. package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
  185. package/lib/vendor/blamejs/lib/webhook.js +1 -1
  186. package/lib/vendor/blamejs/lib/ws-client.js +24 -2
  187. package/lib/vendor/blamejs/package.json +1 -1
  188. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  189. package/lib/vendor-invoices.js +3 -6
  190. package/lib/vendors.js +5 -19
  191. package/lib/webhook-receiver.js +46 -83
  192. package/lib/webhook-subscriptions.js +3 -13
  193. package/lib/wishlist-sharing.js +5 -10
  194. package/package.json +1 -1
@@ -38,9 +38,11 @@
38
38
  * and internal helpers.
39
39
  *
40
40
  * Function-arity changes: a DECREASE in `fn.length` is breaking
41
- * (the operator removed a required parameter). An INCREASE is not
42
- * flagged because adding an optional trailing parameter is additive
43
- * to existing callers.
41
+ * (the operator removed a required parameter). An INCREASE is
42
+ * additive rather than breaking a trailing optional parameter
43
+ * cannot break an existing caller — but it IS reported, because the
44
+ * baseline records arity: leaving it silent means the committed
45
+ * snapshot goes stale while the gate prints "no changes".
44
46
  *
45
47
  * On-disk format: stable canonical JSON ordered as
46
48
  * `{ version, frameworkVersion, createdAt, exports }`. The format
@@ -310,18 +312,30 @@ function _walkCompare(oldNode, newNode, prefix, breaking, additive, typeChanged)
310
312
  return;
311
313
  }
312
314
 
313
- // For function nodes, arity DROPS are flagged (operator removed a
314
- // required parameter). Arity INCREASES are not flagged (added
315
- // optional param at the end is additive).
315
+ // For function nodes, arity DROPS are breaking (a required parameter was
316
+ // removed). An INCREASE cannot break a caller — the added parameter is
317
+ // trailing and optional but the baseline records arity, so a silent pass
318
+ // leaves it stale while the gate prints "no changes": exactly how a changed
319
+ // signature reaches a release untracked. It is reported as additive, the
320
+ // list that already means "not a failure, refresh the baseline".
316
321
  if (oldNode.type === "function") {
317
- if (typeof oldNode.arity === "number" && typeof newNode.arity === "number" &&
318
- newNode.arity < oldNode.arity) {
319
- breaking.push({
320
- path: prefix,
321
- kind: "arity-decreased",
322
- was: "function/" + oldNode.arity,
323
- is: "function/" + newNode.arity,
324
- });
322
+ if (typeof oldNode.arity === "number" && typeof newNode.arity === "number") {
323
+ if (newNode.arity < oldNode.arity) {
324
+ breaking.push({
325
+ path: prefix,
326
+ kind: "arity-decreased",
327
+ was: "function/" + oldNode.arity,
328
+ is: "function/" + newNode.arity,
329
+ });
330
+ } else if (newNode.arity > oldNode.arity) {
331
+ additive.push({
332
+ path: prefix,
333
+ type: "function",
334
+ kind: "arity-increased",
335
+ was: "function/" + oldNode.arity,
336
+ is: "function/" + newNode.arity,
337
+ });
338
+ }
325
339
  }
326
340
  return;
327
341
  }
@@ -367,7 +381,9 @@ function _walkCompare(oldNode, newNode, prefix, breaking, additive, typeChanged)
367
381
  * member that was removed, retyped, lost arity, swapped its
368
382
  * constructor name, or changed primitive `valueType`; the release
369
383
  * workflow exits non-zero when this list is non-empty. `additive`
370
- * lists new members (informational operator should rerun
384
+ * lists new members AND functions that gained a trailing parameter
385
+ * (informational — neither breaks a caller, but both move the
386
+ * surface the baseline records, so the operator should rerun
371
387
  * `capture` and commit the refreshed baseline). `typeChanged` is a
372
388
  * subset of `breaking` surfaced separately for easier triage.
373
389
  *
@@ -446,7 +462,10 @@ function formatDiff(diff) {
446
462
  lines.push("[api-snapshot] additive (" + diff.additive.length + ", informational):");
447
463
  for (var j = 0; j < diff.additive.length; j++) {
448
464
  var a = diff.additive[j];
449
- lines.push(" + " + a.path + " (" + a.type + ")");
465
+ var aline = " + " + a.path + " (" + (a.kind || a.type) + ")";
466
+ if (a.was !== undefined) aline += " was=" + JSON.stringify(a.was);
467
+ if (a.is !== undefined) aline += " is=" + JSON.stringify(a.is);
468
+ lines.push(aline);
450
469
  }
451
470
  }
452
471
  return lines.join("\n");
@@ -162,7 +162,7 @@ function create(opts) {
162
162
  // don't get a stale 503 served from a CDN.
163
163
  if (typeof res.writeHead === "function") {
164
164
  var body = JSON.stringify({ error: "service-shutting-down" });
165
- res.writeHead(503, {
165
+ res.writeHead(C.HTTP.STATUS.SERVICE_UNAVAILABLE, {
166
166
  "Content-Type": "application/json; charset=utf-8",
167
167
  "Content-Length": Buffer.byteLength(body),
168
168
  "Cache-Control": "no-store",
@@ -472,7 +472,7 @@ function http(url, opts) {
472
472
  signal: signal,
473
473
  audit: opts.audit,
474
474
  });
475
- if (!res || (res.status !== 206 && res.status !== 200)) {
475
+ if (!res || (res.status !== C.HTTP.STATUS.PARTIAL_CONTENT && res.status !== C.HTTP.STATUS.OK)) {
476
476
  throw new AdapterError("adapter/bad-response",
477
477
  "http.range: expected 206 Partial Content, got " + (res && res.status));
478
478
  }
@@ -61,6 +61,7 @@ var C = require("./constants");
61
61
  var { defineClass } = require("./framework-error");
62
62
  var auditEmit = require("./audit-emit");
63
63
  var atomicFile = require("./atomic-file");
64
+ var safeAsync = require("./safe-async");
64
65
  var safeBuffer = require("./safe-buffer");
65
66
 
66
67
  var ArchiveError = defineClass("ArchiveError", { alwaysPermanent: true });
@@ -492,25 +493,12 @@ function zip() {
492
493
 
493
494
  var _emitAudit = auditEmit.emitToSink; // operator-sink audit emit (opts.audit)
494
495
 
496
+ // The drain-aware write is b.safeAsync.writeChunk — the same one
497
+ // b.render.stream uses. It was private here first; a second copy would be a
498
+ // second chance to get the closed-peer case wrong, which is the half that
499
+ // hangs a request rather than failing it.
495
500
  function _writeChunk(writable, chunk) {
496
- return new Promise(function (resolve, reject) {
497
- function onError(e) {
498
- writable.removeListener("drain", onDrain);
499
- reject(e);
500
- }
501
- function onDrain() {
502
- writable.removeListener("error", onError);
503
- resolve();
504
- }
505
- var ok = writable.write(chunk);
506
- if (ok) {
507
- // Already flushed — no need to wait for drain.
508
- resolve();
509
- return;
510
- }
511
- writable.once("drain", onDrain);
512
- writable.once("error", onError);
513
- });
501
+ return safeAsync.writeChunk(writable, chunk);
514
502
  }
515
503
 
516
504
  async function _streamEntry(entry, writable) {
@@ -552,9 +540,12 @@ function zip() {
552
540
  var sinkWritable = new nodeStream.Writable({
553
541
  write: function (chunk, enc, cb) {
554
542
  csize += chunk.length;
555
- var ok = writable.write(chunk);
556
- if (ok) cb();
557
- else writable.once("drain", function () { cb(); });
543
+ // Through the shared writer, so a destination that goes away
544
+ // mid-entry fails the pipeline instead of leaving `cb` uncalled.
545
+ // Waiting only for `drain` here meant a destroyed sink never called
546
+ // back at all: the pipeline never settled, toStream() never settled,
547
+ // and the listener stayed attached to the dead stream.
548
+ safeAsync.writeChunk(writable, chunk).then(function () { cb(); }, cb);
558
549
  },
559
550
  });
560
551
  try {
@@ -568,9 +559,7 @@ function zip() {
568
559
  var storeCollect = new nodeStream.Writable({
569
560
  write: function (chunk, enc, cb) {
570
561
  csize += chunk.length;
571
- var ok = writable.write(chunk);
572
- if (ok) cb();
573
- else writable.once("drain", function () { cb(); });
562
+ safeAsync.writeChunk(writable, chunk).then(function () { cb(); }, cb);
574
563
  },
575
564
  });
576
565
  try {
@@ -40,6 +40,7 @@
40
40
  * });
41
41
  */
42
42
 
43
+ var C = require("../constants");
43
44
  var defineClass = require("../framework-error").defineClass;
44
45
  var lazyRequire = require("../lazy-require");
45
46
  var validateOpts = require("../validate-opts");
@@ -147,7 +148,7 @@ function create(opts) {
147
148
 
148
149
  function _refuse(res, reason) {
149
150
  if (!res.writableEnded && typeof res.writeHead === "function") {
150
- res.writeHead(503, {
151
+ res.writeHead(C.HTTP.STATUS.SERVICE_UNAVAILABLE, {
151
152
  "Content-Type": "application/json; charset=utf-8",
152
153
  "Retry-After": "60",
153
154
  "Cache-Control": "no-store",
@@ -227,12 +227,9 @@ function _normaliseAllowlist(name, val) {
227
227
  }
228
228
 
229
229
  function _httpClientShape(client, callerLabel) {
230
- if (client === undefined || client === null) return null;
231
- if (typeof client !== "object" || typeof client.request !== "function") {
232
- throw new BotChallengeError("bot-challenge/bad-opt",
233
- callerLabel + ": httpClient must be a b.httpClient-shaped object (request fn)");
234
- }
235
- return client;
230
+ return validateOpts.optionalObjectWithMethod(client, "request",
231
+ callerLabel + ": httpClient", BotChallengeError, "bot-challenge/bad-opt",
232
+ "must be a b.httpClient-shaped object (request fn)") || null;
236
233
  }
237
234
 
238
235
  function _normaliseTimeoutMs(val) {
@@ -103,7 +103,8 @@ var _emitMetric = emit.metric;
103
103
  * scope?: string|string[],
104
104
  * deliveryMode: "poll"|"ping"|"push",
105
105
  * clientNotificationToken?: string, // fixed token RP mints once + registers with IdP
106
- * httpClientOpts?: object,
106
+ * httpClientOpts?: object, // options merged INTO each request
107
+ * http?: { client?: object, allowedHosts?: string[] }, // the client each request is made THROUGH, and its host pin
107
108
  * allowHttp?: boolean,
108
109
  * }
109
110
  *
@@ -144,6 +145,13 @@ function create(opts) {
144
145
  "auth.ciba.client.create: clientAssertionSigner required for clientAuth='jwt'");
145
146
  }
146
147
 
148
+ // The client this flow dials with, and the host pin every dial carries —
149
+ // shared with the inner OAuth client below so a CIBA flow does not split
150
+ // across two clients, with the backchannel POST inside an operator's
151
+ // instrumentation and discovery outside it.
152
+ var httpOpts = validateOpts.outboundHttpOpts(opts.http, "auth.ciba.client.create",
153
+ AuthError, "auth-ciba");
154
+
147
155
  var deliveryMode = opts.deliveryMode || "poll";
148
156
  if (["poll", "ping", "push"].indexOf(deliveryMode) === -1) {
149
157
  throw new AuthError("auth-ciba/bad-delivery-mode",
@@ -162,7 +170,13 @@ function create(opts) {
162
170
  scope: opts.scope,
163
171
  backchannelAuthenticationEndpoint: opts.backchannelAuthenticationEndpoint,
164
172
  tokenEndpoint: opts.tokenEndpoint,
165
- httpClientOpts: opts.httpClientOpts,
173
+ // The inner client reads this bag as `httpClient`; handing it over under
174
+ // the name it is called here meant an operator's configuration applied to
175
+ // the backchannel and token POSTs and was silently dropped on discovery
176
+ // and JWKS — the same accepted-but-ignored split the allowInternal note
177
+ // below records.
178
+ httpClient: opts.httpClientOpts,
179
+ http: opts.http,
166
180
  allowHttp: opts.allowHttp === true,
167
181
  // Thread the SSRF opt-in through to the inner client's discovery / JWKS /
168
182
  // token fetches — an operator with an internal-network IdP needs it, and
@@ -260,7 +274,10 @@ function create(opts) {
260
274
  safeUrl.parse(url, {
261
275
  allowedProtocols: opts.allowHttp === true ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
262
276
  });
263
- var hc = httpClient();
277
+ // A supplied client is wrapped in the pin — see the note in oauth.create.
278
+ var hc = httpOpts.client
279
+ ? httpClient().pinnedClient(httpOpts.client, httpOpts.allowedHosts)
280
+ : httpClient();
264
281
  var basic = _basicAuthHeader();
265
282
  var hdrs = Object.assign({
266
283
  "Content-Type": "application/x-www-form-urlencoded",
@@ -280,6 +297,18 @@ function create(opts) {
280
297
  responseMode: "always-resolve",
281
298
  };
282
299
  Object.assign(req, opts.httpClientOpts || {});
300
+ // Applied after the options bag so a stray allowedHosts there cannot widen
301
+ // the pin the operator named explicitly.
302
+ if (httpOpts.allowedHosts) req.allowedHosts = httpOpts.allowedHosts.slice();
303
+ // Re-checked HERE, not only on the endpoint above: the options bag is
304
+ // merged in after that check and can carry a `url` of its own, so the
305
+ // address actually dialed need not be the one that was validated. These
306
+ // are the requests holding the Basic credentials or the client assertion,
307
+ // and a host pin does not stand in for the scheme — it deliberately admits
308
+ // both. This is the last point before the wire.
309
+ safeUrl.parse(req.url, {
310
+ allowedProtocols: opts.allowHttp === true ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
311
+ });
283
312
  if (opts.allowHttp === true) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
284
313
  // Thread the operator's explicit SSRF waiver onto the backchannel-auth
285
314
  // and token POSTs too — create() already hands allowInternal to the
@@ -104,6 +104,32 @@
104
104
  * through `b.httpClient` which enforces the framework's PQC-locked
105
105
  * posture by default (operators with mixed-protocol setups pass
106
106
  * `allowedProtocols`).
107
+ *
108
+ * `http` names the client those fetches are made THROUGH, and the
109
+ * hosts they may reach:
110
+ *
111
+ * var oauth = b.auth.oauth.create({
112
+ * issuer, clientId, clientSecret,
113
+ * http: {
114
+ * client: myHttpClient, // any { request(opts) }
115
+ * allowedHosts: ["api-m.paypal.com"], // pin every dial
116
+ * },
117
+ * });
118
+ *
119
+ * Both default to the framework's behaviour. `client` lets a caller
120
+ * that already routes its outbound calls through one circuit breaker
121
+ * and one retry policy keep the token exchange inside them, rather
122
+ * than leaving the endpoint whose failure should open the circuit as
123
+ * the one endpoint the circuit cannot see. `allowedHosts` is passed
124
+ * to `b.httpClient.request`, which enforces it; it covers discovery
125
+ * and JWKS as well as the token endpoint, since a mangled or
126
+ * attacker-influenced issuer is read at discovery — pinning only the
127
+ * token dial would leave the egress open where it starts.
128
+ *
129
+ * Note `httpClient` (options merged INTO each request) and
130
+ * `http.client` (the client each request is made through) are
131
+ * different options; the host pin is applied after the options bag,
132
+ * so it cannot be widened from there.
107
133
  */
108
134
 
109
135
  var nodeCrypto = require("node:crypto");
@@ -1116,6 +1142,21 @@ function create(opts) {
1116
1142
  var allowHttp = !!opts.allowHttp; // localhost dev opt-in (scheme)
1117
1143
  var allowInternal = opts.allowInternal != null ? opts.allowInternal : null; // localhost dev opt-in (SSRF gate)
1118
1144
  var httpClientOpts = opts.httpClient || {};
1145
+ // The client this module dials with, and the host pin every dial carries.
1146
+ // `opts.httpClient` is options merged INTO each request; `opts.http.client`
1147
+ // is the client the request is made through. A consumer that instruments its
1148
+ // outbound calls — one circuit breaker, one retry policy, one recorded
1149
+ // transcript — needs the token exchange inside that, or the endpoint whose
1150
+ // failure should open the circuit is the one endpoint the circuit cannot see.
1151
+ // A supplied client is wrapped in the host pin rather than handed it as a
1152
+ // request field: its contract is a `request` method, so it need not know
1153
+ // what allowedHosts means, and an advisory pin a client ignores is no pin.
1154
+ // b.httpClient enforces the field itself, so the unwrapped default is
1155
+ // already covered.
1156
+ var httpOpts = validateOpts.outboundHttpOpts(opts.http, "oauth.create", OAuthError, "auth-oauth");
1157
+ var httpDialer = httpOpts.client
1158
+ ? httpClient.pinnedClient(httpOpts.client, httpOpts.allowedHosts)
1159
+ : httpClient;
1119
1160
  var responseMode = opts.responseMode || null;
1120
1161
  // v0.9.5 — client-level opt-out for the kid-less JWKS-of-one
1121
1162
  // refusal added in v0.9.4. Surfaced at the create() level (not
@@ -1218,7 +1259,7 @@ function create(opts) {
1218
1259
 
1219
1260
  async function _fetchJson(url, fetchOpts) {
1220
1261
  fetchOpts = fetchOpts || {};
1221
- var hc = httpClient;
1262
+ var hc = httpDialer;
1222
1263
  var req = Object.assign({
1223
1264
  url: url,
1224
1265
  method: "GET",
@@ -1789,7 +1830,7 @@ function create(opts) {
1789
1830
  // Revocation endpoint (RFC 7009) — authenticate with client_secret_post (the
1790
1831
  // credentials stay in the body, universally accepted). tokenEndpointAuthMethod
1791
1832
  // is scoped to the token endpoint; this endpoint may advertise a different one.
1792
- var hc = httpClient;
1833
+ var hc = httpDialer;
1793
1834
  var req = {
1794
1835
  url: endpoint,
1795
1836
  method: "POST",
@@ -1816,6 +1857,22 @@ function create(opts) {
1816
1857
  var ownHeaders = req.headers || {};
1817
1858
  Object.assign(req, httpClientOpts); // may set req.headers from the operator config (always an object)
1818
1859
  req.headers = Object.assign({}, req.headers, ownHeaders);
1860
+ // The host pin is applied AFTER the options bag, so a stray allowedHosts
1861
+ // in the loose `httpClient` config cannot widen the pin the operator named
1862
+ // explicitly. Every request this module builds passes through here, which
1863
+ // is what makes the pin cover discovery and JWKS as well as the token
1864
+ // endpoint — a mangled issuer is read at discovery, so pinning only the
1865
+ // token dial would leave the SSRF egress open where it actually starts.
1866
+ if (httpOpts.allowedHosts) req.allowedHosts = httpOpts.allowedHosts.slice();
1867
+ // The scheme is this module's requirement, not a side effect of dialing
1868
+ // through b.httpClient. That client refuses a non-TLS destination itself,
1869
+ // so while it was the only dialer the check came for free; a supplied
1870
+ // client carries no such promise, and these requests are the ones holding
1871
+ // the client secret, the authorization code and the access token. An
1872
+ // endpoint reaches here from static config OR from a discovery document,
1873
+ // so it is checked here rather than at either source — this is the last
1874
+ // point before the wire, and after the options bag has had its say.
1875
+ _validateUrl(req.url, allowHttp, "outbound endpoint");
1819
1876
  }
1820
1877
 
1821
1878
  // RFC 7231 §7.1.3 Retry-After → milliseconds. Accepts delta-seconds or an
@@ -1847,7 +1904,7 @@ function create(opts) {
1847
1904
  }
1848
1905
 
1849
1906
  async function _postForm(endpoint, body, extraHeaders, applyTokenClientAuth) {
1850
- var hc = httpClient;
1907
+ var hc = httpDialer;
1851
1908
  // tokenEndpointAuthMethod (client_secret_basic) is the TOKEN endpoint's client
1852
1909
  // authentication — apply it only to token-endpoint requests. Other endpoints
1853
1910
  // (introspection / revocation / PAR / device-authorization) may advertise a
@@ -2783,7 +2840,7 @@ function create(opts) {
2783
2840
  throw new OAuthError("auth-oauth/no-registration-endpoint",
2784
2841
  "registerClient: AS does not advertise registration_endpoint");
2785
2842
  }
2786
- var hc = httpClient;
2843
+ var hc = httpDialer;
2787
2844
  var headers = {
2788
2845
  "Content-Type": "application/json",
2789
2846
  "Accept": "application/json",
@@ -2923,10 +2980,10 @@ function create(opts) {
2923
2980
  if (allowHttp) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
2924
2981
  if (allowInternal !== null) req.allowInternal = allowInternal;
2925
2982
  _mergeHttpClientOpts(req);
2926
- var res = await httpClient.request(req);
2983
+ var res = await httpDialer.request(req);
2927
2984
  if (method === "DELETE") {
2928
- if (res.statusCode === 204 || res.statusCode === 200) return null;
2929
- if (res.statusCode === 404) {
2985
+ if (res.statusCode === C.HTTP.STATUS.NO_CONTENT || res.statusCode === C.HTTP.STATUS.OK) return null;
2986
+ if (res.statusCode === C.HTTP.STATUS.NOT_FOUND) {
2930
2987
  throw new OAuthError("auth-oauth/dcr-not-found",
2931
2988
  "deleteClient: 404 — registrationClientUri does not resolve to a client");
2932
2989
  }
@@ -3052,7 +3109,7 @@ function create(opts) {
3052
3109
  // its own request rather than routing through _postForm because it must
3053
3110
  // read the pending/slow_down 400 bodies, so it composes the shared helper).
3054
3111
  var authHeaders = _clientBasicAuthHeaders(body);
3055
- var hc = httpClient;
3112
+ var hc = httpDialer;
3056
3113
  var req = {
3057
3114
  url: endpoint,
3058
3115
  method: "POST",
@@ -448,7 +448,7 @@ function policy(opts) {
448
448
  return _ok({ breachCheckSkipped: true,
449
449
  breachCheckSkipReason: (e && e.message) || String(e) });
450
450
  }
451
- if (resp.statusCode !== 200 || !resp.body) {
451
+ if (resp.statusCode !== C.HTTP.STATUS.OK || !resp.body) {
452
452
  if (p.failClosed) {
453
453
  return _fail("breach-check-failed",
454
454
  "HIBP returned status " + resp.statusCode + " with no body");
@@ -57,6 +57,7 @@ var bCrypto = require("../crypto");
57
57
  var C = require("../constants");
58
58
  var { generateToken, timingSafeEqual } = bCrypto;
59
59
  var { AuthError } = require("../framework-error");
60
+ var safeUrl = require("../safe-url");
60
61
 
61
62
  var xmlC14n = lazyRequire(function () { return require("../xml-c14n"); });
62
63
  var httpClient = lazyRequire(function () { return require("../http-client"); });
@@ -2138,6 +2139,7 @@ function _sigAlgUrn(alg) {
2138
2139
  * baseUrl: string,
2139
2140
  * entityId: string,
2140
2141
  * trustCertPem?: string, // PEM of the federation operator's signing cert
2142
+ * http?: { client?: object, allowedHosts?: string[] }, // the client the fetch is made through, and its host pin
2141
2143
  * }
2142
2144
  *
2143
2145
  * @example
@@ -2151,14 +2153,30 @@ async function fetchMdq(opts) {
2151
2153
  validateOpts.requireObject(opts, "auth.saml.fetchMdq", AuthError);
2152
2154
  validateOpts.requireNonEmptyString(opts.baseUrl, "baseUrl", AuthError, "auth-saml/no-mdq-base");
2153
2155
  validateOpts.requireNonEmptyString(opts.entityId, "entityId", AuthError, "auth-saml/no-mdq-entity");
2156
+ var httpOpts = validateOpts.outboundHttpOpts(opts.http, "auth.saml.fetchMdq",
2157
+ AuthError, "auth-saml");
2154
2158
  var hash = nodeCrypto.createHash("sha1").update(opts.entityId, "utf8").digest("hex");
2155
2159
  var url = opts.baseUrl.replace(/\/$/, "") + "/entities/%7Bsha1%7D" + hash;
2156
- var hc = httpClient();
2157
- var res = await hc.request({
2160
+ // The scheme is checked HERE, not left to whichever client dials. b.httpClient
2161
+ // refuses a non-TLS destination itself, so routing through it was enough while
2162
+ // it was the only client; an injected one carries no such promise, and
2163
+ // trustCertPem is optional — an http:// metadata fetch with no signature to
2164
+ // check lets anyone on the path substitute the federation's signing keys.
2165
+ safeUrl.parse(url, {
2166
+ allowedProtocols: safeUrl.ALLOW_HTTP_TLS,
2167
+ errorClass: AuthError,
2168
+ });
2169
+ // A supplied client is wrapped in the pin — see the note in oauth.create.
2170
+ var hc = httpOpts.client
2171
+ ? httpClient().pinnedClient(httpOpts.client, httpOpts.allowedHosts)
2172
+ : httpClient();
2173
+ var req = {
2158
2174
  url: url,
2159
2175
  method: "GET",
2160
2176
  headers: { Accept: "application/samlmetadata+xml" },
2161
- });
2177
+ };
2178
+ if (httpOpts.allowedHosts) req.allowedHosts = httpOpts.allowedHosts.slice();
2179
+ var res = await hc.request(req);
2162
2180
  if (res.statusCode < 200 || res.statusCode >= 300) {
2163
2181
  throw new AuthError("auth-saml/mdq-fetch-failed",
2164
2182
  "fetchMdq " + url + " returned " + res.statusCode);
@@ -243,7 +243,7 @@ function create(opts) {
243
243
  function _runBotGuardCheck(req) {
244
244
  return new Promise(function (resolve) {
245
245
  var capturedRes = {
246
- statusCode: 200, // HTTP 200 status code, not bytes
246
+ statusCode: C.HTTP.STATUS.OK, // HTTP 200 status code, not bytes
247
247
  writableEnded: false,
248
248
  writeHead: function (status) { capturedRes.statusCode = status; },
249
249
  end: function () { capturedRes.writableEnded = true; },
@@ -400,7 +400,7 @@ function create(opts) {
400
400
  function _writeChallengeRequired(res) {
401
401
  if (!res || res.writableEnded) return;
402
402
  if (typeof res.writeHead === "function") {
403
- res.writeHead(401, {
403
+ res.writeHead(C.HTTP.STATUS.UNAUTHORIZED, {
404
404
  "Content-Type": "text/plain",
405
405
  "WWW-Authenticate": 'Bearer error="bot_challenge_required"',
406
406
  });
@@ -413,7 +413,7 @@ function create(opts) {
413
413
  function _writeLocked(res) {
414
414
  if (!res || res.writableEnded) return;
415
415
  if (typeof res.writeHead === "function") {
416
- res.writeHead(423, { "Content-Type": "text/plain" });
416
+ res.writeHead(C.HTTP.STATUS.LOCKED, { "Content-Type": "text/plain" });
417
417
  } else if (typeof res.statusCode !== "undefined") {
418
418
  res.statusCode = 423;
419
419
  }
@@ -2416,7 +2416,7 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
2416
2416
  // b.objectStore surfaces NOT_FOUND via the framework's
2417
2417
  // err.code === "NOT_FOUND" convention — translate to the
2418
2418
  // backup adapter contract's no-key error.
2419
- if (e && (e.code === "NOT_FOUND" || e.statusCode === 404 || /NOT_FOUND|not found/i.test(e.message || ""))) {
2419
+ if (e && (e.code === "NOT_FOUND" || e.statusCode === C.HTTP.STATUS.NOT_FOUND || /NOT_FOUND|not found/i.test(e.message || ""))) {
2420
2420
  throw new BackupError("backup/no-key",
2421
2421
  "objectStoreAdapter: key not found: " + JSON.stringify(key));
2422
2422
  }
@@ -2473,7 +2473,7 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
2473
2473
  } catch (e) {
2474
2474
  // drop-silent on NOT_FOUND — adapter contract is idempotent
2475
2475
  // delete (fsAdapter same shape).
2476
- if (e && (e.code === "NOT_FOUND" || e.statusCode === 404 || /NOT_FOUND|not found/i.test(e.message || ""))) {
2476
+ if (e && (e.code === "NOT_FOUND" || e.statusCode === C.HTTP.STATUS.NOT_FOUND || /NOT_FOUND|not found/i.test(e.message || ""))) {
2477
2477
  return;
2478
2478
  }
2479
2479
  throw e;
@@ -2484,7 +2484,7 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
2484
2484
  await client.head(_scopedKey(key));
2485
2485
  return true;
2486
2486
  } catch (e) {
2487
- if (e && (e.code === "NOT_FOUND" || e.statusCode === 404 || /NOT_FOUND|not found/i.test(e.message || ""))) {
2487
+ if (e && (e.code === "NOT_FOUND" || e.statusCode === C.HTTP.STATUS.NOT_FOUND || /NOT_FOUND|not found/i.test(e.message || ""))) {
2488
2488
  return false;
2489
2489
  }
2490
2490
  throw e;
@@ -2501,7 +2501,7 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
2501
2501
  var buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
2502
2502
  return buf.slice(0, length);
2503
2503
  } catch (e) {
2504
- if (e && (e.code === "NOT_FOUND" || e.statusCode === 404 || /NOT_FOUND|not found/i.test(e.message || ""))) {
2504
+ if (e && (e.code === "NOT_FOUND" || e.statusCode === C.HTTP.STATUS.NOT_FOUND || /NOT_FOUND|not found/i.test(e.message || ""))) {
2505
2505
  throw new BackupError("backup/no-key",
2506
2506
  "objectStoreAdapter.readPartial: key not found: " + JSON.stringify(key));
2507
2507
  }
@@ -2514,7 +2514,7 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
2514
2514
  if (!meta || typeof meta.size !== "number") return null;
2515
2515
  return { size: meta.size, mtimeMs: meta.lastModified || null };
2516
2516
  } catch (e) {
2517
- if (e && (e.code === "NOT_FOUND" || e.statusCode === 404 || /NOT_FOUND|not found/i.test(e.message || ""))) {
2517
+ if (e && (e.code === "NOT_FOUND" || e.statusCode === C.HTTP.STATUS.NOT_FOUND || /NOT_FOUND|not found/i.test(e.message || ""))) {
2518
2518
  return null;
2519
2519
  }
2520
2520
  throw e;