@paydock/client-sdk 1.144.1-beta → 1.147.0

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 (82) hide show
  1. package/README.md +551 -32
  2. package/bundles/index.cjs +2683 -599
  3. package/bundles/index.cjs.d.ts +322 -21
  4. package/bundles/index.mjs +2683 -600
  5. package/bundles/index.mjs.d.ts +322 -21
  6. package/bundles/types/api/api-base.d.ts +6 -0
  7. package/bundles/types/api/api-base.d.ts.map +1 -1
  8. package/bundles/types/api/api-charge-internal.d.ts +1 -5
  9. package/bundles/types/api/api-charge-internal.d.ts.map +1 -1
  10. package/bundles/types/canvas-3ds/canvas-3ds-event-map.d.ts +50 -0
  11. package/bundles/types/canvas-3ds/canvas-3ds-event-map.d.ts.map +1 -1
  12. package/bundles/types/canvas-3ds/canvas-3ds.d.ts +7 -5
  13. package/bundles/types/canvas-3ds/canvas-3ds.d.ts.map +1 -1
  14. package/bundles/types/canvas-3ds/index.d.ts +1 -1
  15. package/bundles/types/canvas-3ds/index.d.ts.map +1 -1
  16. package/bundles/types/canvas-3ds/services/gpayments-service.d.ts +117 -2
  17. package/bundles/types/canvas-3ds/services/gpayments-service.d.ts.map +1 -1
  18. package/bundles/types/canvas-3ds/services/standalone3ds-service.d.ts +6 -0
  19. package/bundles/types/canvas-3ds/services/standalone3ds-service.d.ts.map +1 -1
  20. package/bundles/types/components/iframe-event.d.ts +4 -3
  21. package/bundles/types/components/iframe-event.d.ts.map +1 -1
  22. package/bundles/types/components/iframe.d.ts +5 -0
  23. package/bundles/types/components/iframe.d.ts.map +1 -1
  24. package/bundles/types/components/param.d.ts +19 -0
  25. package/bundles/types/components/param.d.ts.map +1 -1
  26. package/bundles/types/helper/browser.d.ts.map +1 -1
  27. package/bundles/types/helper/element-id.d.ts +3 -1
  28. package/bundles/types/helper/element-id.d.ts.map +1 -1
  29. package/bundles/types/helper/uuid.d.ts.map +1 -1
  30. package/bundles/types/index-cba.d.ts +1 -1
  31. package/bundles/types/index-cba.d.ts.map +1 -1
  32. package/bundles/types/index.d.ts +2 -2
  33. package/bundles/types/index.d.ts.map +1 -1
  34. package/bundles/types/open-wallets/apple-pay-open-wallet-button.d.ts +41 -0
  35. package/bundles/types/open-wallets/apple-pay-open-wallet-button.d.ts.map +1 -1
  36. package/bundles/types/open-wallets/base/open-wallet-buttons.d.ts +10 -0
  37. package/bundles/types/open-wallets/base/open-wallet-buttons.d.ts.map +1 -1
  38. package/bundles/types/open-wallets/base/service-config-cache.d.ts +23 -0
  39. package/bundles/types/open-wallets/base/service-config-cache.d.ts.map +1 -0
  40. package/bundles/types/open-wallets/google-pay-open-wallet-button.d.ts +33 -1
  41. package/bundles/types/open-wallets/google-pay-open-wallet-button.d.ts.map +1 -1
  42. package/bundles/types/open-wallets/index.d.ts +82 -1
  43. package/bundles/types/open-wallets/index.d.ts.map +1 -1
  44. package/bundles/types/open-wallets/interfaces/apple-pay/apple-pay-availability.interface.d.ts +41 -0
  45. package/bundles/types/open-wallets/interfaces/apple-pay/apple-pay-availability.interface.d.ts.map +1 -0
  46. package/bundles/types/open-wallets/interfaces/apple-pay/index.d.ts +1 -0
  47. package/bundles/types/open-wallets/interfaces/apple-pay/index.d.ts.map +1 -1
  48. package/bundles/types/open-wallets/interfaces/google-pay/google-pay-availability.interface.d.ts +25 -0
  49. package/bundles/types/open-wallets/interfaces/google-pay/google-pay-availability.interface.d.ts.map +1 -0
  50. package/bundles/types/open-wallets/interfaces/google-pay/index.d.ts +1 -0
  51. package/bundles/types/open-wallets/interfaces/google-pay/index.d.ts.map +1 -1
  52. package/bundles/types/open-wallets/interfaces/index.d.ts +3 -0
  53. package/bundles/types/open-wallets/interfaces/index.d.ts.map +1 -1
  54. package/bundles/types/open-wallets/interfaces/open-wallet-availability.interface.d.ts +31 -0
  55. package/bundles/types/open-wallets/interfaces/open-wallet-availability.interface.d.ts.map +1 -0
  56. package/bundles/types/open-wallets/services/apple-pay/apple-pay-availability.d.ts +58 -0
  57. package/bundles/types/open-wallets/services/apple-pay/apple-pay-availability.d.ts.map +1 -0
  58. package/bundles/types/open-wallets/services/apple-pay/apple-pay.open-wallet.service.d.ts +8 -10
  59. package/bundles/types/open-wallets/services/apple-pay/apple-pay.open-wallet.service.d.ts.map +1 -1
  60. package/bundles/types/open-wallets/services/apple-pay/index.d.ts +1 -0
  61. package/bundles/types/open-wallets/services/apple-pay/index.d.ts.map +1 -1
  62. package/bundles/types/open-wallets/services/google-pay/google-pay-availability.d.ts +53 -0
  63. package/bundles/types/open-wallets/services/google-pay/google-pay-availability.d.ts.map +1 -0
  64. package/bundles/types/open-wallets/services/google-pay/google-pay.open-wallet.service.d.ts.map +1 -1
  65. package/bundles/types/open-wallets/services/google-pay/index.d.ts +1 -0
  66. package/bundles/types/open-wallets/services/google-pay/index.d.ts.map +1 -1
  67. package/bundles/types/widget/multi-widget.d.ts +19 -0
  68. package/bundles/types/widget/multi-widget.d.ts.map +1 -1
  69. package/bundles/widget.umd.js +2683 -599
  70. package/bundles/widget.umd.js.d.ts +322 -21
  71. package/bundles/widget.umd.js.min.d.ts +322 -21
  72. package/bundles/widget.umd.min.js +1 -1
  73. package/docs/api-canvas3ds.md +10 -8
  74. package/docs/api-checkout-button.md +15 -0
  75. package/docs/api-ps-widget.md +15 -0
  76. package/docs/api-widget.md +15 -0
  77. package/docs/canvas3ds-examples.md +43 -1
  78. package/docs/open-wallet-buttons-examples.md +170 -2
  79. package/docs/open-wallet-buttons.md +252 -29
  80. package/docs/widget-examples.md +41 -0
  81. package/package.json +20 -11
  82. package/slate.md +254 -3
package/bundles/index.mjs CHANGED
@@ -828,16 +828,28 @@ var Browser = /*#__PURE__*/function () {
828
828
  value: function getBrowserName() {
829
829
  var sUsrAg = navigator.userAgent;
830
830
  // "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"
831
- if (sUsrAg.indexOf('Firefox') > -1) return 'Mozilla Firefox';
832
- if (sUsrAg.indexOf('Opera') > -1) return 'Opera';
831
+ if (sUsrAg.indexOf('Firefox') > -1) {
832
+ return 'Mozilla Firefox';
833
+ }
834
+ if (sUsrAg.indexOf('Opera') > -1) {
835
+ return 'Opera';
836
+ }
833
837
  // "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; Zoom 3.6.0; wbx 1.0.0; rv:11.0) like Gecko"
834
- if (sUsrAg.indexOf('Trident') > -1) return 'Microsoft Internet Explorer';
838
+ if (sUsrAg.indexOf('Trident') > -1) {
839
+ return 'Microsoft Internet Explorer';
840
+ }
835
841
  // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"
836
- if (sUsrAg.indexOf('Edge') > -1) return 'Microsoft Edge';
842
+ if (sUsrAg.indexOf('Edge') > -1) {
843
+ return 'Microsoft Edge';
844
+ }
837
845
  // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/66.0.3359.181 Chrome/66.0.3359.181 Safari/537.36"
838
- if (sUsrAg.indexOf('Chrome') > -1) return 'Google Chrome or Chromium';
846
+ if (sUsrAg.indexOf('Chrome') > -1) {
847
+ return 'Google Chrome or Chromium';
848
+ }
839
849
  // "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1 980x1306"
840
- if (sUsrAg.indexOf('Safari') > -1) return 'Apple Safari';
850
+ if (sUsrAg.indexOf('Safari') > -1) {
851
+ return 'Apple Safari';
852
+ }
841
853
  return 'unknown';
842
854
  }
843
855
  }, {
@@ -868,11 +880,13 @@ var Browser = /*#__PURE__*/function () {
868
880
  }, {
869
881
  key: "getBrowserInfo",
870
882
  value: function getBrowserInfo() {
871
- var ua = navigator.userAgent;
883
+ // Bound the user-agent length before running any regex to avoid
884
+ // pathological backtracking (ReDoS) on maliciously long inputs.
885
+ var ua = (navigator.userAgent || '').slice(0, 256);
872
886
  var M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
873
- var tem;
874
- if (/trident/i.test(M[1])) {
875
- tem = /\brv[ :](\d+)/.exec(ua) || [];
887
+ var tem = null;
888
+ if (/trident/i.test(M[1] || '')) {
889
+ tem = ua.match(/\brv[ :](\d+)/) || [];
876
890
  return {
877
891
  name: 'IE',
878
892
  version: tem[1] || ''
@@ -888,8 +902,8 @@ var Browser = /*#__PURE__*/function () {
888
902
  }
889
903
  }
890
904
  M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
891
- // tslint:disable-next-line:no-conditional-assignment
892
- if ((tem = ua.match(/version\/(\d+)/i)) != null) {
905
+ tem = ua.match(/version\/(\d+)/i);
906
+ if (tem != null) {
893
907
  M.splice(1, 1, tem[1]);
894
908
  }
895
909
  return {
@@ -1849,7 +1863,7 @@ SDK.headerKeys = Object.freeze({
1849
1863
  version: 'x-sdk-version',
1850
1864
  type: 'x-sdk-type'
1851
1865
  });
1852
- SDK._version = 'v1.144.1-beta';
1866
+ SDK._version = 'v1.147.0';
1853
1867
 
1854
1868
  var ACCESS_HEADER_NAME = {
1855
1869
  accessToken: 'x-access-token',
@@ -4705,6 +4719,15 @@ var ApiBase = /*#__PURE__*/function () {
4705
4719
  this.env.setEnv(env, alias);
4706
4720
  return this;
4707
4721
  }
4722
+ /**
4723
+ * Origin (`scheme://host[:port]`) of the API this client talks to, for the current env/alias.
4724
+ * Used to validate the `origin` of postMessages the API itself sends to the page.
4725
+ */
4726
+ }, {
4727
+ key: "getApiOrigin",
4728
+ value: function getApiOrigin() {
4729
+ return ApiBase.toOrigin(this.env.getConf().url);
4730
+ }
4708
4731
  }, {
4709
4732
  key: "requestObservable",
4710
4733
  value: function requestObservable(method, url, requestBody) {
@@ -4917,6 +4940,18 @@ var ApiBase = /*#__PURE__*/function () {
4917
4940
  environment: this.env.getEnv()
4918
4941
  };
4919
4942
  }
4943
+ }], [{
4944
+ key: "toOrigin",
4945
+ value: function toOrigin(url) {
4946
+ try {
4947
+ return new URL(url).origin;
4948
+ } catch (_a) {
4949
+ // Not a parsable URL: the returned string is not an origin and will never match a
4950
+ // MessageEvent.origin — surface the misconfiguration instead of failing silently.
4951
+ console.warn("[paydock/client-sdk] cannot derive an origin from configured URL \"".concat(url, "\""));
4952
+ return url.replace(/\/+$/, '');
4953
+ }
4954
+ }
4920
4955
  }]);
4921
4956
  }();
4922
4957
 
@@ -4977,7 +5012,6 @@ var Api = /*#__PURE__*/function (_ApiBase) {
4977
5012
 
4978
5013
  var WALLET_CAPTURE_LINK = '/v1/charges/wallet/capture';
4979
5014
  var WALLET_CALLBACK_LINK = '/v1/charges/wallet/callback';
4980
- var STANDALONE_3DS_PROCESS_LINK = '/v1/charges/standalone-3ds/process';
4981
5015
  var STANDALONE_3DS_HANDLE_LINK = '/v1/charges/standalone-3ds/handle';
4982
5016
  var ApiChargeInternal = /*#__PURE__*/function () {
4983
5017
  function ApiChargeInternal(api) {
@@ -4994,11 +5028,6 @@ var ApiChargeInternal = /*#__PURE__*/function () {
4994
5028
  value: function walletCallback(payload) {
4995
5029
  return this.api.getClientPromise('POST', WALLET_CALLBACK_LINK).send(payload);
4996
5030
  }
4997
- }, {
4998
- key: "standalone3dsProcess",
4999
- value: function standalone3dsProcess(payload) {
5000
- return this.api.getClientPromise('POST', STANDALONE_3DS_PROCESS_LINK).send(payload);
5001
- }
5002
5031
  }, {
5003
5032
  key: "standalone3dsHandle",
5004
5033
  value: function standalone3dsHandle() {
@@ -5137,6 +5166,10 @@ var IFrame = /*#__PURE__*/function () {
5137
5166
  _classCallCheck(this, IFrame);
5138
5167
  this.container = container;
5139
5168
  }
5169
+ /**
5170
+ * @param options.onLoad Called on every `load` of the iframe (initial document and each
5171
+ * subsequent navigation). Attached before `src` is set so a fast load is never missed.
5172
+ */
5140
5173
  return _createClass(IFrame, [{
5141
5174
  key: "load",
5142
5175
  value: function load(link) {
@@ -5145,6 +5178,9 @@ var IFrame = /*#__PURE__*/function () {
5145
5178
  if (!this.container.isExist() || this.isExist()) return;
5146
5179
  var iFrame = document.createElement('iframe');
5147
5180
  if (options.title) iFrame.title = options.title;
5181
+ if (options.onLoad) {
5182
+ iFrame.addEventListener('load', options.onLoad);
5183
+ }
5148
5184
  if (iframeClass) iFrame.classList.add(iframeClass);
5149
5185
  // Set additional attributes before src and DOM insertion so that
5150
5186
  // Permissions-Policy (e.g. allow="payment") is in place when the
@@ -5285,14 +5321,14 @@ var IFrameEvent = /*#__PURE__*/function () {
5285
5321
  data = JSON.parse(event.data);
5286
5322
  } catch (error) {}
5287
5323
  if (!data) return;
5288
- _this.emit(data);
5324
+ _this.emit(data, event.origin);
5289
5325
  });
5290
5326
  }
5291
5327
  return _createClass(IFrameEvent, [{
5292
5328
  key: "emit",
5293
- value: function emit(data) {
5329
+ value: function emit(data, origin) {
5294
5330
  for (var key in this.listeners) {
5295
- if (this.listeners[key].event === data.event && data.widget_id === this.listeners[key].widget_id) this.listeners[key].listener.apply(this, [data]);
5331
+ if (this.listeners[key].event === data.event && data.widget_id === this.listeners[key].widget_id) this.listeners[key].listener.apply(this, [data, origin]);
5296
5332
  }
5297
5333
  }
5298
5334
  }, {
@@ -5365,6 +5401,20 @@ var GPAYMENTS_EVENT = {
5365
5401
  DECOUPLED: 'chargeAuthDecoupled',
5366
5402
  /** Challenge required — issuer demands user interaction. Payload: `Canvas3dsAuthPayload`. */
5367
5403
  CHALLENGE: 'chargeAuthChallenge',
5404
+ /**
5405
+ * The challenge iframe fired its FIRST `load` after `chargeAuthChallenge` (the ACS page has painted),
5406
+ * or the `CHALLENGE_LOADED_FALLBACK_MS` safety fallback elapsed without one. Emitted exactly once per
5407
+ * challenge; never emitted for a decoupled challenge (no challenge iframe). Payload: `Canvas3dsChallengeLoadedPayload`.
5408
+ */
5409
+ CHALLENGE_LOADED: 'chargeAuthChallengeLoaded',
5410
+ /**
5411
+ * The issuer delivered the final authentication result to the 3DS Server (the shopper finished the
5412
+ * challenge, or approved a decoupled authentication) and the SDK is now obtaining the outcome. Emitted at
5413
+ * most once per authentication, always before the terminal event. Detected through the `secondary_url`
5414
+ * result monitor (`source: 'poll'`) or, when the callback message arrives first, right before the outcome
5415
+ * it carries (`source: 'callback'`). Payload: `Canvas3dsChallengeCompletedPayload`.
5416
+ */
5417
+ CHALLENGE_COMPLETED: 'chargeAuthChallengeCompleted',
5368
5418
  /** Informational signal emitted by the GPayments flow. Internal — not part of the public typed surface. */
5369
5419
  INFO: 'chargeAuthInfo',
5370
5420
  /** Error during the flow (network, parsing, etc.). Payload: `Canvas3dsErrorPayload`. */
@@ -5376,13 +5426,56 @@ var GPAYMENTS_EVENT = {
5376
5426
  * backwards-compatible internal usage.
5377
5427
  */
5378
5428
  var CANVAS_3DS_EVENT = GPAYMENTS_EVENT;
5429
+ /**
5430
+ * Upper bound on how long `chargeAuthChallengeLoaded` waits for the challenge iframe's first `load`.
5431
+ * When it elapses the event is emitted with `reason: 'timeout'` so a merchant loader is never held
5432
+ * forever; the real `load` arriving afterwards is the paint and is simply ignored.
5433
+ */
5434
+ var CHALLENGE_LOADED_FALLBACK_MS = 2500;
5435
+ /**
5436
+ * Interval of the `secondary_url` (GPayments `resultMonUrl`) poll while an ACS challenge is on screen.
5437
+ * The monitor flips to `AuthResultReady` when the issuer's RReq reaches the 3DS Server — before the ACS has
5438
+ * even returned its final page to the browser — so this poll is what makes `chargeAuthChallengeCompleted`
5439
+ * land before the shopper sees any post-submit page. GPayments recommends 2 s and leaves the interval to the
5440
+ * requestor; 1 s matches the cadence of GPayments' own monitoring frame.
5441
+ */
5442
+ var CHALLENGE_RESULT_POLL_INTERVAL_MS = 1000;
5443
+ /**
5444
+ * Interval of the `secondary_url` poll for a decoupled challenge (shopper approves out-of-band, possibly
5445
+ * minutes later; nothing to cover on screen) — the GPayments-recommended 2 s.
5446
+ */
5447
+ var DECOUPLED_RESULT_POLL_INTERVAL_MS = 2000;
5448
+ /** The result-monitor request failed at transport level (network, abort, timeout) — retried, not fatal. */
5449
+ var ResultMonitorTransportError = /*#__PURE__*/function (_Error) {
5450
+ function ResultMonitorTransportError() {
5451
+ var _this;
5452
+ _classCallCheck(this, ResultMonitorTransportError);
5453
+ _this = _callSuper(this, ResultMonitorTransportError, ['Standalone 3DS result monitor request failed']);
5454
+ _this.name = 'ResultMonitorTransportError';
5455
+ return _this;
5456
+ }
5457
+ _inherits(ResultMonitorTransportError, _Error);
5458
+ return _createClass(ResultMonitorTransportError);
5459
+ }(/*#__PURE__*/_wrapNativeSuper(Error));
5379
5460
  var GPaymentsService = /*#__PURE__*/function () {
5380
5461
  function GPaymentsService(container, api, eventEmitter) {
5462
+ var trustedOrigins = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
5381
5463
  _classCallCheck(this, GPaymentsService);
5382
5464
  this.container = container;
5383
5465
  this.api = api;
5384
5466
  this.eventEmitter = eventEmitter;
5467
+ this.trustedOrigins = trustedOrigins;
5385
5468
  this.resultRead = false;
5469
+ /** `chargeAuthChallengeLoaded` already emitted for the current challenge (by `load` or by the fallback). */
5470
+ this.challengeLoadedEmitted = false;
5471
+ /** `chargeAuthChallengeCompleted` already emitted for the current authentication (poll or callback). */
5472
+ this.challengeCompletedEmitted = false;
5473
+ /** Set once a terminal event was emitted: no lifecycle event may follow (reset when a new challenge starts). */
5474
+ this.challengeLifecycleClosed = false;
5475
+ this.challengeLoadedTimer = null;
5476
+ this.resultPollTimer = null;
5477
+ /** One-shot guard for the `chargeAuth` leg — the callback message may be delivered more than once. */
5478
+ this.authHandled = false;
5386
5479
  this.iFrameEvent = new IFrameEvent(window);
5387
5480
  }
5388
5481
  return _createClass(GPaymentsService, [{
@@ -5402,6 +5495,7 @@ var GPaymentsService = /*#__PURE__*/function () {
5402
5495
  key: "initializeIFrames",
5403
5496
  value: function initializeIFrames(initializationUrl, secondaryUrl, title) {
5404
5497
  var hideAuthorization = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
5498
+ var onLoad = arguments.length > 4 ? arguments[4] : undefined;
5405
5499
  var container = this.container.getElement();
5406
5500
  if (!container) {
5407
5501
  return;
@@ -5412,7 +5506,8 @@ var GPaymentsService = /*#__PURE__*/function () {
5412
5506
  this.browserAndChallengeContainer = new Container('#paydock_authorization_iframe');
5413
5507
  this.iFrameAuthorization = new IFrame(this.browserAndChallengeContainer);
5414
5508
  this.iFrameAuthorization.load(initializationUrl, {
5415
- title: title
5509
+ title: title,
5510
+ onLoad: onLoad
5416
5511
  });
5417
5512
  if (secondaryUrl) {
5418
5513
  var divSecondaryURL = document.createElement('div');
@@ -5446,25 +5541,192 @@ var GPaymentsService = /*#__PURE__*/function () {
5446
5541
  }, {
5447
5542
  key: "setupIFrameEvents",
5448
5543
  value: function setupIFrameEvents(widgetId) {
5449
- var _this = this;
5450
- this.iFrameEvent.on(EVENT$5.CHARGE_AUTH, widgetId, function (data) {
5544
+ var _this2 = this;
5545
+ this.iFrameEvent.on(EVENT$5.CHARGE_AUTH, widgetId, function (data, origin) {
5451
5546
  if (data.status === IFRAME_STATUS.SKIPPED || data.status === IFRAME_STATUS.FINISHED) {
5547
+ // this channel is spoofable — any frame that knows the charge_3ds_id can post to it.
5548
+ // A message from an unknown sender must never terminate a live authentication: ignore it.
5549
+ if (!_this2.isTrustedOrigin(origin)) {
5550
+ return;
5551
+ }
5552
+ // one-shot: the callback message can be delivered more than once (it is posted to both
5553
+ // `parent` and `top`) — a duplicate must not re-emit events or duplicate challenge iframes
5554
+ if (_this2.authHandled) {
5555
+ return;
5556
+ }
5557
+ _this2.authHandled = true;
5452
5558
  if (data.info) {
5453
- _this.eventEmitter.emit(GPAYMENTS_EVENT.INFO, {
5559
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.INFO, {
5454
5560
  info: data.info
5455
5561
  });
5456
5562
  }
5457
- _this.performAuthentication(data);
5563
+ _this2.removeIframes();
5564
+ if (typeof data.process_status === 'string') {
5565
+ // the API already ran the authentication inside the callback: no /process call needed
5566
+ _this2.handleAuthenticationOutcome({
5567
+ status: data.process_status,
5568
+ result: data.result,
5569
+ error: data.error
5570
+ }, data.charge_3ds_id);
5571
+ } else {
5572
+ // The API always runs the authentication inside the callback (PAYRAC-2480). A trusted
5573
+ // message without the outcome means an outdated API — fail, never verify via /process.
5574
+ _this2.finishChallengeLifecycle();
5575
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this2.parseError({
5576
+ message: 'Standalone 3DS authentication outcome missing from the callback message'
5577
+ }, data.charge_3ds_id));
5578
+ }
5458
5579
  } else if (data.status === IFRAME_STATUS.TIMEOUT || data.status === IFRAME_STATUS.INVALID) {
5459
- _this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this.parseHandleResponse({
5580
+ _this2.finishChallengeLifecycle();
5581
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this2.parseHandleResponse({
5460
5582
  status: data.status
5461
5583
  }, data.charge_3ds_id));
5462
5584
  }
5463
5585
  });
5464
- this.iFrameEvent.on(EVENT$5.CHARGE_AUTH_SUCCESS, widgetId, function (data) {
5465
- _this.processResult(data.charge_3ds_id);
5586
+ this.iFrameEvent.on(EVENT$5.CHARGE_AUTH_SUCCESS, widgetId, function (data, origin) {
5587
+ // the callback iframe reached Paydock with `AuthResultReady`: the issuer has answered. Announce it
5588
+ // before the outcome so merchants always see completed → terminal, whichever transport won.
5589
+ _this2.emitChallengeCompleted(data.charge_3ds_id, 'callback');
5590
+ if (_this2.hasChainedOutcome(data, origin)) {
5591
+ // the API already fetched the challenge result inside the callback: no GET /handle needed
5592
+ _this2.handleResultOutcome({
5593
+ status: data.process_status,
5594
+ result: data.result,
5595
+ error: data.error
5596
+ }, data.charge_3ds_id);
5597
+ } else {
5598
+ _this2.processResult(data.charge_3ds_id);
5599
+ }
5600
+ });
5601
+ }
5602
+ /**
5603
+ * A message carries a processed outcome only when it has `process_status` AND comes from a trusted
5604
+ * origin. Used by the result leg: an untrusted `process_status` is never acted upon — the SDK falls
5605
+ * back to `GET /handle` (still needed for decoupled challenges / polling). The authentication leg
5606
+ * ignores untrusted senders outright, before this check.
5607
+ */
5608
+ }, {
5609
+ key: "hasChainedOutcome",
5610
+ value: function hasChainedOutcome(data, origin) {
5611
+ return typeof data.process_status === 'string' && this.isTrustedOrigin(origin);
5612
+ }
5613
+ }, {
5614
+ key: "isTrustedOrigin",
5615
+ value: function isTrustedOrigin(origin) {
5616
+ // 'null' is the serialized opaque origin (e.g. a sandboxed iframe) — never trust it, even if a
5617
+ // misconfigured env URL were ever to put the literal string 'null' into the trusted list
5618
+ return typeof origin === 'string' && origin !== 'null' && this.trustedOrigins.includes(origin);
5619
+ }
5620
+ }, {
5621
+ key: "removeIframes",
5622
+ value: function removeIframes() {
5623
+ var _a, _b;
5624
+ this.clearChallengeLoadedTimer();
5625
+ (_a = this.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.remove();
5626
+ (_b = this.iFrameSecondaryUrl) === null || _b === void 0 ? void 0 : _b.remove();
5627
+ }
5628
+ }, {
5629
+ key: "clearChallengeLoadedTimer",
5630
+ value: function clearChallengeLoadedTimer() {
5631
+ if (this.challengeLoadedTimer !== null) {
5632
+ clearTimeout(this.challengeLoadedTimer);
5633
+ this.challengeLoadedTimer = null;
5634
+ }
5635
+ }
5636
+ /**
5637
+ * Challenge iframe lifecycle (`chargeAuthChallengeLoaded`).
5638
+ *
5639
+ * The challenge iframe is cross-origin, but its `load` event still fires when the ACS page finishes loading:
5640
+ * the first one means the page painted (merchants can drop their loader). A fallback timer
5641
+ * (`armChallengeLoadedFallback`) guarantees `chargeAuthChallengeLoaded` within `CHALLENGE_LOADED_FALLBACK_MS`
5642
+ * even if no `load` ever fires; a late paint after that is simply ignored. Later `load`s are NOT a usable
5643
+ * signal for "the shopper submitted": the post-submit hops are HTTP redirects and self-submitting pages whose
5644
+ * loads abort, and the final callback page posts its message before its own `load` — so the post-submit
5645
+ * moment is `chargeAuthChallengeCompleted`, driven by the `secondary_url` result monitor instead.
5646
+ *
5647
+ * Nothing is emitted once the flow reached a terminal outcome (`finishChallengeLifecycle`), and a
5648
+ * decoupled challenge (no challenge iframe) never emits `chargeAuthChallengeLoaded`.
5649
+ *
5650
+ * Resets the per-challenge state and returns the `load` handler to attach to the challenge iframe.
5651
+ */
5652
+ }, {
5653
+ key: "startChallengeLifecycle",
5654
+ value: function startChallengeLifecycle(charge3dsId) {
5655
+ var _this3 = this;
5656
+ this.clearChallengeLoadedTimer();
5657
+ this.challengeLoadedEmitted = false;
5658
+ this.challengeCompletedEmitted = false;
5659
+ this.challengeLifecycleClosed = false;
5660
+ return function () {
5661
+ var _a;
5662
+ if (_this3.challengeLifecycleClosed || _this3.challengeLoadedEmitted || !((_a = _this3.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.isExist())) {
5663
+ return;
5664
+ }
5665
+ _this3.clearChallengeLoadedTimer();
5666
+ _this3.emitChallengeLoaded(charge3dsId, 'load');
5667
+ };
5668
+ }
5669
+ /**
5670
+ * Arms the `chargeAuthChallengeLoaded` fallback — only when a challenge iframe actually exists (the
5671
+ * container may be gone, or `IFrame.load` may have no-op'd on an already-present iframe).
5672
+ */
5673
+ }, {
5674
+ key: "armChallengeLoadedFallback",
5675
+ value: function armChallengeLoadedFallback(charge3dsId) {
5676
+ var _this4 = this;
5677
+ var _a;
5678
+ if (!((_a = this.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.isExist())) {
5679
+ return;
5680
+ }
5681
+ this.challengeLoadedTimer = setTimeout(function () {
5682
+ _this4.challengeLoadedTimer = null;
5683
+ if (_this4.challengeLifecycleClosed || _this4.challengeLoadedEmitted) {
5684
+ return;
5685
+ }
5686
+ _this4.emitChallengeLoaded(charge3dsId, 'timeout');
5687
+ }, CHALLENGE_LOADED_FALLBACK_MS);
5688
+ }
5689
+ }, {
5690
+ key: "emitChallengeLoaded",
5691
+ value: function emitChallengeLoaded(charge3dsId, reason) {
5692
+ this.challengeLoadedEmitted = true;
5693
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE_LOADED, {
5694
+ charge_3ds_id: charge3dsId,
5695
+ reason: reason
5696
+ });
5697
+ }
5698
+ /**
5699
+ * `chargeAuthChallengeCompleted` — once per authentication, never after a terminal event. Idempotent so the
5700
+ * poll and the callback message can both call it: the first transport to notice the issuer's result wins.
5701
+ */
5702
+ }, {
5703
+ key: "emitChallengeCompleted",
5704
+ value: function emitChallengeCompleted(charge3dsId, source) {
5705
+ if (this.challengeLifecycleClosed || this.challengeCompletedEmitted) {
5706
+ return;
5707
+ }
5708
+ this.challengeCompletedEmitted = true;
5709
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE_COMPLETED, {
5710
+ charge_3ds_id: charge3dsId,
5711
+ source: source
5466
5712
  });
5467
5713
  }
5714
+ /** Call before emitting any terminal event: no lifecycle event may follow it, and the result poll stops. */
5715
+ }, {
5716
+ key: "finishChallengeLifecycle",
5717
+ value: function finishChallengeLifecycle() {
5718
+ this.clearChallengeLoadedTimer();
5719
+ this.stopResultPolling();
5720
+ this.challengeLifecycleClosed = true;
5721
+ }
5722
+ }, {
5723
+ key: "stopResultPolling",
5724
+ value: function stopResultPolling() {
5725
+ if (this.resultPollTimer !== null) {
5726
+ clearTimeout(this.resultPollTimer);
5727
+ this.resultPollTimer = null;
5728
+ }
5729
+ }
5468
5730
  }, {
5469
5731
  key: "parseResultData",
5470
5732
  value: function parseResultData(_ref2, charge3dsId) {
@@ -5498,24 +5760,49 @@ var GPaymentsService = /*#__PURE__*/function () {
5498
5760
  }, {
5499
5761
  key: "processResult",
5500
5762
  value: function processResult(charge3dsId) {
5501
- var _this2 = this;
5763
+ var _this5 = this;
5502
5764
  if (this.resultRead) {
5503
5765
  return;
5504
5766
  }
5505
5767
  this.resultRead = true;
5768
+ this.finishChallengeLifecycle();
5506
5769
  this.api.charge().standalone3dsHandle().then(function (result) {
5507
5770
  var _a;
5508
- _this2.iFrameAuthorization.remove();
5509
- (_a = _this2.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5771
+ _this5.iFrameAuthorization.remove();
5772
+ (_a = _this5.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5510
5773
  if (result.status === PROCESS_STANDALONE_3DS_STATUS.SUCCESS) {
5511
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this2.parseResultData(result, charge3dsId));
5774
+ _this5.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this5.parseResultData(result, charge3dsId));
5512
5775
  } else {
5513
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this2.parseResultData(result, charge3dsId));
5776
+ _this5.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this5.parseResultData(result, charge3dsId));
5514
5777
  }
5515
5778
  }, function (err) {
5516
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this2.parseError(err, charge3dsId));
5779
+ _this5.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this5.parseError(err, charge3dsId));
5517
5780
  });
5518
5781
  }
5782
+ /** Final (post-challenge) outcome delivered by the API through the callback message. */
5783
+ }, {
5784
+ key: "handleResultOutcome",
5785
+ value: function handleResultOutcome(_ref4, charge3dsId) {
5786
+ var status = _ref4.status,
5787
+ error = _ref4.error;
5788
+ if (this.resultRead) {
5789
+ return;
5790
+ }
5791
+ this.resultRead = true;
5792
+ this.finishChallengeLifecycle();
5793
+ this.removeIframes();
5794
+ if (status === PROCESS_STANDALONE_3DS_STATUS.SUCCESS) {
5795
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, this.parseResultData({
5796
+ status: status
5797
+ }, charge3dsId));
5798
+ } else if (status === PROCESS_STANDALONE_3DS_STATUS.ERROR) {
5799
+ this.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, this.parseError(error, charge3dsId));
5800
+ } else {
5801
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseResultData({
5802
+ status: status
5803
+ }, charge3dsId));
5804
+ }
5805
+ }
5519
5806
  }, {
5520
5807
  key: "externalAPI",
5521
5808
  value: function externalAPI(method, url) {
@@ -5529,60 +5816,106 @@ var GPaymentsService = /*#__PURE__*/function () {
5529
5816
  reject(error);
5530
5817
  }
5531
5818
  };
5819
+ // without these a failed request left the promise pending forever and the poll silently dead
5820
+ var fail = function fail() {
5821
+ return reject(new ResultMonitorTransportError());
5822
+ };
5823
+ request.onerror = fail;
5824
+ request.onabort = fail;
5825
+ request.ontimeout = fail;
5532
5826
  request.send();
5533
5827
  });
5534
5828
  }
5829
+ /**
5830
+ * Polls the GPayments result monitor (`secondary_url`) until `AuthResultReady`. It is the primary source of
5831
+ * `chargeAuthChallengeCompleted` (the monitor flips as soon as the issuer's RReq reaches the 3DS Server) and
5832
+ * the fallback transport for the outcome itself (`GET /handle`) when the callback message never arrives —
5833
+ * the only transport for decoupled challenges. Stops once an outcome was obtained or the lifecycle closed.
5834
+ * Transport failures are retried on the same cadence; an unparsable body or an unknown event is fatal.
5835
+ */
5535
5836
  }, {
5536
5837
  key: "doPolling",
5537
- value: function doPolling(url, charge3dsId) {
5538
- var _this3 = this;
5838
+ value: function doPolling(url, charge3dsId, intervalMs) {
5839
+ var _this6 = this;
5840
+ this.stopResultPolling();
5841
+ if (this.resultRead || this.challengeLifecycleClosed) {
5842
+ return;
5843
+ }
5844
+ var schedule = function schedule() {
5845
+ _this6.resultPollTimer = setTimeout(function () {
5846
+ _this6.resultPollTimer = null;
5847
+ _this6.doPolling(url, charge3dsId, intervalMs);
5848
+ }, intervalMs);
5849
+ };
5539
5850
  this.externalAPI('GET', url).then(function (data) {
5851
+ if (_this6.resultRead || _this6.challengeLifecycleClosed) {
5852
+ return;
5853
+ }
5540
5854
  if (!data.event || data.event === 'AuthResultNotReady') {
5541
- setTimeout(function () {
5542
- _this3.doPolling(url, charge3dsId);
5543
- }, 2000);
5855
+ schedule();
5544
5856
  } else if (data.event === 'AuthResultReady') {
5545
- _this3.processResult(charge3dsId);
5857
+ _this6.emitChallengeCompleted(charge3dsId, 'poll');
5858
+ _this6.processResult(charge3dsId);
5546
5859
  } else {
5547
5860
  throw new Error('Event not supported');
5548
5861
  }
5549
5862
  })["catch"](function (err) {
5550
- return _this3.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this3.parseError(err, charge3dsId));
5863
+ if (err instanceof ResultMonitorTransportError) {
5864
+ if (!_this6.resultRead && !_this6.challengeLifecycleClosed) {
5865
+ schedule();
5866
+ }
5867
+ return;
5868
+ }
5869
+ _this6.finishChallengeLifecycle();
5870
+ _this6.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this6.parseError(err, charge3dsId));
5551
5871
  });
5552
5872
  }
5873
+ /**
5874
+ * Branches on the authentication outcome delivered by the API through the callback message
5875
+ * (`process_status` → `status`; same shape as the `/process` response body).
5876
+ */
5553
5877
  }, {
5554
- key: "performAuthentication",
5555
- value: function performAuthentication(_ref4) {
5556
- var _this4 = this;
5557
- var charge_3ds_id = _ref4.charge_3ds_id;
5558
- var _a;
5559
- this.iFrameAuthorization.remove();
5560
- (_a = this.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5561
- this.api.charge().standalone3dsProcess({
5562
- charge_3ds_id: charge_3ds_id
5563
- }).then(function (authenticationResult) {
5564
- var _a, _b;
5565
- if (authenticationResult.status === AUTHENTICATION_STATUS.SUCCESS) {
5566
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5567
- } else if (authenticationResult.status === AUTHENTICATION_STATUS.PENDING) {
5568
- if ((_a = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _a === void 0 ? void 0 : _a.challenge) {
5569
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5570
- _this4.initializeIFrames(authenticationResult.result.challenge_url, undefined, 'Authentication Challenge', false);
5571
- if (authenticationResult.result.secondary_url) {
5572
- _this4.doPolling(authenticationResult.result.secondary_url, charge_3ds_id);
5573
- }
5574
- } else if ((_b = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _b === void 0 ? void 0 : _b.decoupled_challenge) {
5575
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.DECOUPLED, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5576
- if (authenticationResult.result.secondary_url) {
5577
- _this4.doPolling(authenticationResult.result.secondary_url, charge_3ds_id);
5578
- }
5878
+ key: "handleAuthenticationOutcome",
5879
+ value: function handleAuthenticationOutcome(authenticationResult, charge_3ds_id) {
5880
+ var _a, _b, _c;
5881
+ if (authenticationResult.status !== AUTHENTICATION_STATUS.PENDING) {
5882
+ // terminal outcome — a later `chargeAuthSuccess` message or poll must not emit a second result
5883
+ this.resultRead = true;
5884
+ }
5885
+ if (authenticationResult.status === AUTHENTICATION_STATUS.SUCCESS) {
5886
+ this.finishChallengeLifecycle();
5887
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5888
+ } else if (authenticationResult.status === AUTHENTICATION_STATUS.PENDING) {
5889
+ if ((_a = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _a === void 0 ? void 0 : _a.challenge) {
5890
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5891
+ var onChallengeLoad = this.startChallengeLifecycle(charge_3ds_id);
5892
+ this.initializeIFrames(authenticationResult.result.challenge_url, undefined, 'Authentication Challenge', false, onChallengeLoad);
5893
+ this.armChallengeLoadedFallback(charge_3ds_id);
5894
+ if (authenticationResult.result.secondary_url) {
5895
+ this.doPolling(authenticationResult.result.secondary_url, charge_3ds_id, CHALLENGE_RESULT_POLL_INTERVAL_MS);
5896
+ }
5897
+ } else if ((_b = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _b === void 0 ? void 0 : _b.decoupled_challenge) {
5898
+ this.challengeCompletedEmitted = false;
5899
+ this.challengeLifecycleClosed = false;
5900
+ this.eventEmitter.emit(GPAYMENTS_EVENT.DECOUPLED, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5901
+ if (authenticationResult.result.secondary_url) {
5902
+ this.doPolling(authenticationResult.result.secondary_url, charge_3ds_id, DECOUPLED_RESULT_POLL_INTERVAL_MS);
5579
5903
  }
5580
5904
  } else {
5581
- return _this4.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5582
- }
5583
- }, function (err) {
5584
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this4.parseError(err, charge_3ds_id));
5585
- });
5905
+ // pending but nothing to run (no challenge delivered) — fail rather than hang silently
5906
+ // with the iframes already removed and no further message ever coming
5907
+ this.resultRead = true;
5908
+ this.finishChallengeLifecycle();
5909
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5910
+ }
5911
+ } else if (authenticationResult.status === AUTHENTICATION_STATUS.ERROR) {
5912
+ // the API rejected the authentication, or could not run it inside the callback
5913
+ this.finishChallengeLifecycle();
5914
+ this.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, this.parseError((_c = authenticationResult.error) !== null && _c !== void 0 ? _c : authenticationResult, charge_3ds_id));
5915
+ } else {
5916
+ this.finishChallengeLifecycle();
5917
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5918
+ }
5586
5919
  }
5587
5920
  }]);
5588
5921
  }();
@@ -5609,10 +5942,27 @@ var Standalone3dsService = /*#__PURE__*/function () {
5609
5942
  api.setEnv(this.env, this.alias);
5610
5943
  switch (tokenData.service_type) {
5611
5944
  case STANDALONE_3DS_GATEWAYS.GPAYMENTS:
5612
- new GPaymentsService(this.container, api, this.eventEmitter).load(tokenData, options.title);
5945
+ new GPaymentsService(this.container, api, this.eventEmitter, this.getTrustedMessageOrigins(api)).load(tokenData, options.title);
5613
5946
  break;
5614
5947
  }
5615
5948
  }
5949
+ /**
5950
+ * Origins allowed to deliver a processed 3DS outcome to the page via postMessage: the Paydock API
5951
+ * (inline callback page) and the Paydock widget (remote-app landing page), for the current env/alias.
5952
+ */
5953
+ }, {
5954
+ key: "getTrustedMessageOrigins",
5955
+ value: function getTrustedMessageOrigins(api) {
5956
+ var origins = [api.getApiOrigin()];
5957
+ try {
5958
+ var widgetEnv = new Env(WIDGET_URL, this.env);
5959
+ widgetEnv.setEnv(this.env, this.alias);
5960
+ origins.push(ApiBase.toOrigin(widgetEnv.getConf().url));
5961
+ } catch (_a) {
5962
+ // widget origin unknown for this env — the API origin is enough
5963
+ }
5964
+ return origins;
5965
+ }
5616
5966
  }, {
5617
5967
  key: "setEnv",
5618
5968
  value: function setEnv(env, alias) {
@@ -5648,12 +5998,12 @@ var Uuid = /*#__PURE__*/function () {
5648
5998
  return _createClass(Uuid, null, [{
5649
5999
  key: "generate",
5650
6000
  value: function generate() {
5651
- if (typeof window !== "undefined" && typeof window.crypto !== "undefined" && typeof window.crypto.getRandomValues !== "undefined") {
6001
+ if (typeof window !== 'undefined' && typeof window.crypto !== 'undefined' && typeof window.crypto.getRandomValues !== 'undefined') {
5652
6002
  var buf = new Uint16Array(8);
5653
6003
  window.crypto.getRandomValues(buf);
5654
- return this.hash(buf[0]) + this.hash(buf[1]) + "-" + this.hash(buf[2]) + "-" + this.hash(buf[3]) + "-" + this.hash(buf[4]) + "-" + this.hash(buf[5]) + this.hash(buf[6]) + this.hash(buf[7]);
6004
+ return Uuid.hash(buf[0]) + Uuid.hash(buf[1]) + '-' + Uuid.hash(buf[2]) + '-' + Uuid.hash(buf[3]) + '-' + Uuid.hash(buf[4]) + '-' + Uuid.hash(buf[5]) + Uuid.hash(buf[6]) + Uuid.hash(buf[7]);
5655
6005
  } else {
5656
- return this.random() + this.random() + "-" + this.random() + "-" + this.random() + "-" + this.random() + "-" + this.random() + this.random() + this.random();
6006
+ return Uuid.random() + Uuid.random() + '-' + Uuid.random() + '-' + Uuid.random() + '-' + Uuid.random() + '-' + Uuid.random() + Uuid.random() + Uuid.random();
5657
6007
  }
5658
6008
  }
5659
6009
  }, {
@@ -5661,14 +6011,22 @@ var Uuid = /*#__PURE__*/function () {
5661
6011
  value: function hash(number) {
5662
6012
  var hash = number.toString(16);
5663
6013
  while (hash.length < 4) {
5664
- hash = "0" + hash;
6014
+ hash = '0' + hash;
5665
6015
  }
5666
6016
  return hash;
5667
6017
  }
5668
6018
  }, {
5669
6019
  key: "random",
5670
6020
  value: function random() {
5671
- return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
6021
+ var value;
6022
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
6023
+ var buf = new Uint16Array(1);
6024
+ crypto.getRandomValues(buf);
6025
+ value = buf[0] / 0x10000;
6026
+ } else {
6027
+ value = Math.random();
6028
+ }
6029
+ return Math.floor((1 + value) * 0x10000).toString(16).substring(1);
5672
6030
  }
5673
6031
  }]);
5674
6032
  }();
@@ -5773,11 +6131,13 @@ var TOKEN_FORMAT$2;
5773
6131
  * @const STANDALONE_3DS_EVENT
5774
6132
  *
5775
6133
  * @type {object}
5776
- * @param {string} CHARGE_AUTH_SUCCESS=chargeAuthSuccess
5777
- * @param {string} CHARGE_AUTH_REJECT=chargeAuthReject
5778
- * @param {string} CHARGE_AUTH_DECOUPLED=chargeAuthDecoupled
5779
- * @param {string} CHARGE_AUTH_CHALLENGE=chargeAuthChallenge
5780
- * @param {string} CHARGE_AUTH_INFO=chargeAuthInfo
6134
+ * @param {string} AUTH_SUCCESS=chargeAuthSuccess
6135
+ * @param {string} AUTH_ERROR=chargeAuthReject
6136
+ * @param {string} DECOUPLED=chargeAuthDecoupled
6137
+ * @param {string} CHALLENGE=chargeAuthChallenge
6138
+ * @param {string} CHALLENGE_LOADED=chargeAuthChallengeLoaded - challenge iframe painted (first `load`, or 2.5 s fallback); reveal your challenge UI here
6139
+ * @param {string} CHALLENGE_COMPLETED=chargeAuthChallengeCompleted - the shopper finished the challenge (issuer result delivered to the 3DS Server); cover the challenge iframe with your own loader until the terminal event
6140
+ * @param {string} INFO=chargeAuthInfo
5781
6141
  * @param {string} ERROR=error
5782
6142
  */
5783
6143
  /**
@@ -6159,6 +6519,24 @@ var SUPPORTED_CARD_TYPES = {
6159
6519
  EFTPOS_WHITE: 'eftpos_white',
6160
6520
  UNIONPAY: 'unionpay'
6161
6521
  };
6522
+ /**
6523
+ * Card schemes the widget is able to resolve from a card number, and therefore the only values
6524
+ * `setAllowedCardSchemes` accepts. This is a separate vocabulary from SUPPORTED_CARD_TYPES, which drives the
6525
+ * card icons: it omits the icon-only variants `visa_white` / `eftpos_white`, and the `laser` / `maestro`
6526
+ * identifiers the widget can never resolve — allowing those would be configuration that can never match.
6527
+ */
6528
+ var CARD_SCHEME = {
6529
+ AMEX: 'amex',
6530
+ AUSBC: 'ausbc',
6531
+ DINERS: 'diners',
6532
+ DISCOVER: 'discover',
6533
+ EFTPOS: 'eftpos',
6534
+ JAPCB: 'japcb',
6535
+ MASTERCARD: 'mastercard',
6536
+ SOLO: 'solo',
6537
+ UNIONPAY: 'unionpay',
6538
+ VISA: 'visa'
6539
+ };
6162
6540
  var SUPPORTED_CHECKOUT_META_COLLECTION = [].concat([
6163
6541
  // Paypal
6164
6542
  'brand_name', 'cart_border_color', 'reference', 'email', 'hdr_img', 'logo_img', 'pay_flow_color', 'first_name', 'last_name', 'address_line', 'address_line2', 'address_city', 'address_state', 'address_postcode', 'address_country', 'phone', 'hide_shipping_address'], [
@@ -8343,7 +8721,7 @@ var AfterPayWalletService = /*#__PURE__*/function (_WalletService) {
8343
8721
  }]);
8344
8722
  }(WalletService);
8345
8723
 
8346
- /*! @license DOMPurify 3.4.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.0/LICENSE */
8724
+ /*! @license DOMPurify 3.4.13 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.13/LICENSE */
8347
8725
 
8348
8726
  var purify_cjs;
8349
8727
  var hasRequiredPurify_cjs;
@@ -8352,22 +8730,63 @@ function requirePurify_cjs () {
8352
8730
  if (hasRequiredPurify_cjs) return purify_cjs;
8353
8731
  hasRequiredPurify_cjs = 1;
8354
8732
 
8355
- const {
8356
- entries,
8357
- setPrototypeOf,
8358
- isFrozen,
8359
- getPrototypeOf,
8360
- getOwnPropertyDescriptor
8361
- } = Object;
8362
- let {
8363
- freeze,
8364
- seal,
8365
- create
8366
- } = Object; // eslint-disable-line import/no-mutable-exports
8367
- let {
8368
- apply,
8369
- construct
8370
- } = typeof Reflect !== 'undefined' && Reflect;
8733
+ function _arrayLikeToArray(r, a) {
8734
+ (null == a || a > r.length) && (a = r.length);
8735
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
8736
+ return n;
8737
+ }
8738
+ function _arrayWithHoles(r) {
8739
+ if (Array.isArray(r)) return r;
8740
+ }
8741
+ function _iterableToArrayLimit(r, l) {
8742
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
8743
+ if (null != t) {
8744
+ var e,
8745
+ n,
8746
+ i,
8747
+ u,
8748
+ a = [],
8749
+ f = true,
8750
+ o = false;
8751
+ try {
8752
+ if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
8753
+ } catch (r) {
8754
+ o = true, n = r;
8755
+ } finally {
8756
+ try {
8757
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
8758
+ } finally {
8759
+ if (o) throw n;
8760
+ }
8761
+ }
8762
+ return a;
8763
+ }
8764
+ }
8765
+ function _nonIterableRest() {
8766
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
8767
+ }
8768
+ function _slicedToArray(r, e) {
8769
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
8770
+ }
8771
+ function _unsupportedIterableToArray(r, a) {
8772
+ if (r) {
8773
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
8774
+ var t = {}.toString.call(r).slice(8, -1);
8775
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
8776
+ }
8777
+ }
8778
+
8779
+ const entries = Object.entries,
8780
+ setPrototypeOf = Object.setPrototypeOf,
8781
+ isFrozen = Object.isFrozen,
8782
+ getPrototypeOf = Object.getPrototypeOf,
8783
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
8784
+ let freeze = Object.freeze,
8785
+ seal = Object.seal,
8786
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
8787
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
8788
+ apply = _ref.apply,
8789
+ construct = _ref.construct;
8371
8790
  if (!freeze) {
8372
8791
  freeze = function freeze(x) {
8373
8792
  return x;
@@ -8399,13 +8818,19 @@ function requirePurify_cjs () {
8399
8818
  const arrayPop = unapply(Array.prototype.pop);
8400
8819
  const arrayPush = unapply(Array.prototype.push);
8401
8820
  const arraySplice = unapply(Array.prototype.splice);
8821
+ const arrayIsArray = Array.isArray;
8402
8822
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
8403
8823
  const stringToString = unapply(String.prototype.toString);
8404
8824
  const stringMatch = unapply(String.prototype.match);
8405
8825
  const stringReplace = unapply(String.prototype.replace);
8406
8826
  const stringIndexOf = unapply(String.prototype.indexOf);
8407
8827
  const stringTrim = unapply(String.prototype.trim);
8828
+ const numberToString = unapply(Number.prototype.toString);
8829
+ const booleanToString = unapply(Boolean.prototype.toString);
8830
+ const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
8831
+ const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
8408
8832
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
8833
+ const objectToString = unapply(Object.prototype.toString);
8409
8834
  const regExpTest = unapply(RegExp.prototype.test);
8410
8835
  const typeErrorCreate = unconstruct(TypeError);
8411
8836
  /**
@@ -8455,6 +8880,9 @@ function requirePurify_cjs () {
8455
8880
  // Prevent prototype setters from intercepting set as a this value.
8456
8881
  setPrototypeOf(set, null);
8457
8882
  }
8883
+ if (!arrayIsArray(array)) {
8884
+ return set;
8885
+ }
8458
8886
  let l = array.length;
8459
8887
  while (l--) {
8460
8888
  let element = array[l];
@@ -8495,10 +8923,13 @@ function requirePurify_cjs () {
8495
8923
  */
8496
8924
  function clone(object) {
8497
8925
  const newObject = create(null);
8498
- for (const [property, value] of entries(object)) {
8926
+ for (const _ref2 of entries(object)) {
8927
+ var _ref3 = _slicedToArray(_ref2, 2);
8928
+ const property = _ref3[0];
8929
+ const value = _ref3[1];
8499
8930
  const isPropertyExist = objectHasOwnProperty(object, property);
8500
8931
  if (isPropertyExist) {
8501
- if (Array.isArray(value)) {
8932
+ if (arrayIsArray(value)) {
8502
8933
  newObject[property] = cleanArray(value);
8503
8934
  } else if (value && typeof value === 'object' && value.constructor === Object) {
8504
8935
  newObject[property] = clone(value);
@@ -8509,6 +8940,58 @@ function requirePurify_cjs () {
8509
8940
  }
8510
8941
  return newObject;
8511
8942
  }
8943
+ /**
8944
+ * Convert non-node values into strings without depending on direct property access.
8945
+ *
8946
+ * @param value - The value to stringify.
8947
+ * @returns A string representation of the provided value.
8948
+ */
8949
+ function stringifyValue(value) {
8950
+ switch (typeof value) {
8951
+ case 'string':
8952
+ {
8953
+ return value;
8954
+ }
8955
+ case 'number':
8956
+ {
8957
+ return numberToString(value);
8958
+ }
8959
+ case 'boolean':
8960
+ {
8961
+ return booleanToString(value);
8962
+ }
8963
+ case 'bigint':
8964
+ {
8965
+ return bigintToString ? bigintToString(value) : '0';
8966
+ }
8967
+ case 'symbol':
8968
+ {
8969
+ return symbolToString ? symbolToString(value) : 'Symbol()';
8970
+ }
8971
+ case 'undefined':
8972
+ {
8973
+ return objectToString(value);
8974
+ }
8975
+ case 'function':
8976
+ case 'object':
8977
+ {
8978
+ if (value === null) {
8979
+ return objectToString(value);
8980
+ }
8981
+ const valueAsRecord = value;
8982
+ const valueToString = lookupGetter(valueAsRecord, 'toString');
8983
+ if (typeof valueToString === 'function') {
8984
+ const stringified = valueToString(valueAsRecord);
8985
+ return typeof stringified === 'string' ? stringified : objectToString(stringified);
8986
+ }
8987
+ return objectToString(value);
8988
+ }
8989
+ default:
8990
+ {
8991
+ return objectToString(value);
8992
+ }
8993
+ }
8994
+ }
8512
8995
  /**
8513
8996
  * This method automatically checks if the prop is function or getter and behaves accordingly.
8514
8997
  *
@@ -8534,6 +9017,14 @@ function requirePurify_cjs () {
8534
9017
  }
8535
9018
  return fallbackValue;
8536
9019
  }
9020
+ function isRegex(value) {
9021
+ try {
9022
+ regExpTest(value, '');
9023
+ return true;
9024
+ } catch (_unused) {
9025
+ return false;
9026
+ }
9027
+ }
8537
9028
 
8538
9029
  const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
8539
9030
  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
@@ -8549,15 +9040,14 @@ function requirePurify_cjs () {
8549
9040
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
8550
9041
  const text = freeze(['#text']);
8551
9042
 
8552
- const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
8553
- const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
9043
+ const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
9044
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
8554
9045
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
8555
9046
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
8556
9047
 
8557
- // eslint-disable-next-line unicorn/better-regex
8558
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
8559
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
8560
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
9048
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
9049
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
9050
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
8561
9051
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
8562
9052
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
8563
9053
  const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
@@ -8567,30 +9057,31 @@ function requirePurify_cjs () {
8567
9057
  );
8568
9058
  const DOCTYPE_NAME = seal(/^html$/i);
8569
9059
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
9060
+ // Markup-significant character probes used by _sanitizeElements.
9061
+ // Shared module-level instances are safe despite the sticky /g flags:
9062
+ // unapply() resets lastIndex for RegExp receivers before every call.
9063
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
9064
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
9065
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
9066
+ const SELF_CLOSING_TAG = seal(/\/>/i);
8570
9067
 
8571
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
8572
- __proto__: null,
8573
- ARIA_ATTR: ARIA_ATTR,
8574
- ATTR_WHITESPACE: ATTR_WHITESPACE,
8575
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
8576
- DATA_ATTR: DATA_ATTR,
8577
- DOCTYPE_NAME: DOCTYPE_NAME,
8578
- ERB_EXPR: ERB_EXPR,
8579
- IS_ALLOWED_URI: IS_ALLOWED_URI,
8580
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
8581
- MUSTACHE_EXPR: MUSTACHE_EXPR,
8582
- TMPLIT_EXPR: TMPLIT_EXPR
8583
- });
8584
-
8585
- /* eslint-disable @typescript-eslint/indent */
8586
9068
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
8587
9069
  const NODE_TYPE = {
8588
9070
  element: 1,
9071
+ attribute: 2,
8589
9072
  text: 3,
9073
+ cdataSection: 4,
9074
+ entityReference: 5,
8590
9075
  // Deprecated
8591
- progressingInstruction: 7,
9076
+ entityNode: 6,
9077
+ // Deprecated
9078
+ processingInstruction: 7,
8592
9079
  comment: 8,
8593
- document: 9};
9080
+ document: 9,
9081
+ documentType: 10,
9082
+ documentFragment: 11,
9083
+ notation: 12 // Deprecated
9084
+ };
8594
9085
  const getGlobal = function getGlobal() {
8595
9086
  return typeof window === 'undefined' ? null : window;
8596
9087
  };
@@ -8645,10 +9136,25 @@ function requirePurify_cjs () {
8645
9136
  uponSanitizeShadowNode: []
8646
9137
  };
8647
9138
  };
9139
+ /**
9140
+ * Resolve a set-valued configuration option: a fresh set built from
9141
+ * cfg[key] when it is an own array property (seeded with a clone of
9142
+ * options.base when given, case-normalized via options.transform),
9143
+ * the fallback set otherwise.
9144
+ *
9145
+ * @param cfg the cloned, prototype-free configuration object
9146
+ * @param key the configuration property to read
9147
+ * @param fallback the set to use when the option is absent or not an array
9148
+ * @param options transform and optional base set to merge into
9149
+ * @returns the resolved set
9150
+ */
9151
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
9152
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
9153
+ };
8648
9154
  function createDOMPurify() {
8649
9155
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
8650
9156
  const DOMPurify = root => createDOMPurify(root);
8651
- DOMPurify.version = '3.4.0';
9157
+ DOMPurify.version = '3.4.13';
8652
9158
  DOMPurify.removed = [];
8653
9159
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
8654
9160
  // Not running in a browser, provide a factory function
@@ -8656,28 +9162,30 @@ function requirePurify_cjs () {
8656
9162
  DOMPurify.isSupported = false;
8657
9163
  return DOMPurify;
8658
9164
  }
8659
- let {
8660
- document
8661
- } = window;
9165
+ let document = window.document;
8662
9166
  const originalDocument = document;
8663
9167
  const currentScript = originalDocument.currentScript;
8664
- const {
8665
- DocumentFragment,
8666
- HTMLTemplateElement,
8667
- Node,
8668
- Element,
8669
- NodeFilter,
8670
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
8671
- HTMLFormElement,
8672
- DOMParser,
8673
- trustedTypes
8674
- } = window;
9168
+ window.DocumentFragment;
9169
+ const HTMLTemplateElement = window.HTMLTemplateElement,
9170
+ Node = window.Node,
9171
+ Element = window.Element,
9172
+ NodeFilter = window.NodeFilter,
9173
+ _window$NamedNodeMap = window.NamedNodeMap;
9174
+ _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap;
9175
+ window.HTMLFormElement;
9176
+ const DOMParser = window.DOMParser,
9177
+ trustedTypes = window.trustedTypes;
8675
9178
  const ElementPrototype = Element.prototype;
8676
9179
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
8677
9180
  const remove = lookupGetter(ElementPrototype, 'remove');
8678
9181
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
8679
9182
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
8680
9183
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
9184
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
9185
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
9186
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
9187
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
9188
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
8681
9189
  // As per issue #47, the web-components registry is inherited by a
8682
9190
  // new document created via createHTMLDocument. As per the spec
8683
9191
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -8692,33 +9200,74 @@ function requirePurify_cjs () {
8692
9200
  }
8693
9201
  let trustedTypesPolicy;
8694
9202
  let emptyHTML = '';
8695
- const {
8696
- implementation,
8697
- createNodeIterator,
8698
- createDocumentFragment,
8699
- getElementsByTagName
8700
- } = document;
8701
- const {
8702
- importNode
8703
- } = originalDocument;
9203
+ // The instance's own internal Trusted Types policy. Unlike a caller-supplied
9204
+ // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
9205
+ // on duplicate policy names — and is the only policy allowed to persist
9206
+ // across configurations and survive `clearConfig()`.
9207
+ let defaultTrustedTypesPolicy;
9208
+ let defaultTrustedTypesPolicyResolved = false;
9209
+ // Tracks whether we are already inside a call to the configured Trusted Types
9210
+ // policy (`createHTML` or `createScriptURL`). If a supplied policy callback
9211
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
9212
+ // re-enter the policy and recurse until the stack overflows. We detect that
9213
+ // re-entry and throw a clear, actionable error instead. The guard is shared
9214
+ // across both callbacks, because either one re-entering `sanitize` triggers
9215
+ // the same unbounded recursion.
9216
+ let IN_TRUSTED_TYPES_POLICY = 0;
9217
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
9218
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
9219
+ throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or ' + 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' + 'infinite recursion. Do not pass a policy whose callbacks wrap ' + 'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' + 'Types" section of the README.');
9220
+ }
9221
+ };
9222
+ const _createTrustedHTML = function _createTrustedHTML(html) {
9223
+ _assertNotInTrustedTypesPolicy();
9224
+ IN_TRUSTED_TYPES_POLICY++;
9225
+ try {
9226
+ return trustedTypesPolicy.createHTML(html);
9227
+ } finally {
9228
+ IN_TRUSTED_TYPES_POLICY--;
9229
+ }
9230
+ };
9231
+ const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
9232
+ _assertNotInTrustedTypesPolicy();
9233
+ IN_TRUSTED_TYPES_POLICY++;
9234
+ try {
9235
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
9236
+ } finally {
9237
+ IN_TRUSTED_TYPES_POLICY--;
9238
+ }
9239
+ };
9240
+ // Lazily resolve (and cache) the instance's internal default policy.
9241
+ // Resolution is attempted at most once: a successful `createPolicy` cannot be
9242
+ // repeated (Trusted Types throws on duplicate names), and a failed or
9243
+ // unsupported attempt must not be retried on every parse.
9244
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
9245
+ if (!defaultTrustedTypesPolicyResolved) {
9246
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
9247
+ defaultTrustedTypesPolicyResolved = true;
9248
+ }
9249
+ return defaultTrustedTypesPolicy;
9250
+ };
9251
+ const _document = document,
9252
+ implementation = _document.implementation,
9253
+ createNodeIterator = _document.createNodeIterator,
9254
+ createDocumentFragment = _document.createDocumentFragment,
9255
+ getElementsByTagName = _document.getElementsByTagName;
9256
+ const importNode = originalDocument.importNode;
8704
9257
  let hooks = _createHooksMap();
8705
9258
  /**
8706
9259
  * Expose whether this browser supports running the full DOMPurify.
8707
9260
  */
8708
9261
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
8709
- const {
8710
- MUSTACHE_EXPR,
8711
- ERB_EXPR,
8712
- TMPLIT_EXPR,
8713
- DATA_ATTR,
8714
- ARIA_ATTR,
8715
- IS_SCRIPT_OR_DATA,
8716
- ATTR_WHITESPACE,
8717
- CUSTOM_ELEMENT
8718
- } = EXPRESSIONS;
8719
- let {
8720
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
8721
- } = EXPRESSIONS;
9262
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
9263
+ ERB_EXPR$1 = ERB_EXPR,
9264
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
9265
+ DATA_ATTR$1 = DATA_ATTR,
9266
+ ARIA_ATTR$1 = ARIA_ATTR,
9267
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
9268
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
9269
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
9270
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
8722
9271
  /**
8723
9272
  * We consider the elements and attributes below to be safe. Ideally
8724
9273
  * don't add any new ones but feel free to remove unwanted ones.
@@ -8795,6 +9344,13 @@ function requirePurify_cjs () {
8795
9344
  let WHOLE_DOCUMENT = false;
8796
9345
  /* Track whether config is already set on this instance of DOMPurify. */
8797
9346
  let SET_CONFIG = false;
9347
+ /* Pristine allowlist bindings captured at setConfig() time. On the
9348
+ * persistent-config path sanitize() restores the sets from these before
9349
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
9350
+ * carry across calls. Null until setConfig() is called; reset by
9351
+ * clearConfig(). */
9352
+ let SET_CONFIG_ALLOWED_TAGS = null;
9353
+ let SET_CONFIG_ALLOWED_ATTR = null;
8798
9354
  /* Decide if all elements (e.g. style, script) must be children of
8799
9355
  * document.body. By default, browsers might move them to document.head */
8800
9356
  let FORCE_BODY = false;
@@ -8837,7 +9393,17 @@ function requirePurify_cjs () {
8837
9393
  let USE_PROFILES = {};
8838
9394
  /* Tags to ignore content of when KEEP_CONTENT is true */
8839
9395
  let FORBID_CONTENTS = null;
8840
- const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
9396
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script',
9397
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
9398
+ // the UA (customizable <select>) — including any on* handlers — and the
9399
+ // engine re-mirrors synchronously whenever a removal changes which
9400
+ // option/selectedcontent is current, even inside DOMPurify's inert
9401
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
9402
+ // mirror target ahead of the walk, which the engine refills, looping
9403
+ // forever (DoS) and amplifying output. Dropping its content on removal
9404
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
9405
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
9406
+ 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
8841
9407
  /* Tags that are safe for data: URIs */
8842
9408
  let DATA_URI_TAGS = null;
8843
9409
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -8853,8 +9419,10 @@ function requirePurify_cjs () {
8853
9419
  /* Allowed XHTML+XML namespaces */
8854
9420
  let ALLOWED_NAMESPACES = null;
8855
9421
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
8856
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
8857
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
9422
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
9423
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
9424
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
9425
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
8858
9426
  // Certain elements are allowed in both SVG and HTML
8859
9427
  // namespace. We need to specify them explicitly
8860
9428
  // so that they don't get erroneously deleted from
@@ -8896,15 +9464,33 @@ function requirePurify_cjs () {
8896
9464
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
8897
9465
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
8898
9466
  /* Set configuration parameters */
8899
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
8900
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
8901
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
8902
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
8903
- DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
8904
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
8905
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
8906
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
8907
- USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
9467
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
9468
+ transform: transformCaseFunc
9469
+ });
9470
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
9471
+ transform: transformCaseFunc
9472
+ });
9473
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
9474
+ transform: stringToString
9475
+ });
9476
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
9477
+ transform: transformCaseFunc,
9478
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
9479
+ });
9480
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
9481
+ transform: transformCaseFunc,
9482
+ base: DEFAULT_DATA_URI_TAGS
9483
+ });
9484
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
9485
+ transform: transformCaseFunc
9486
+ });
9487
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
9488
+ transform: transformCaseFunc
9489
+ });
9490
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
9491
+ transform: transformCaseFunc
9492
+ });
9493
+ USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
8908
9494
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
8909
9495
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
8910
9496
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -8920,20 +9506,22 @@ function requirePurify_cjs () {
8920
9506
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
8921
9507
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
8922
9508
  IN_PLACE = cfg.IN_PLACE || false; // Default false
8923
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
8924
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
8925
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
8926
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
8927
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);
8928
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
8929
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
9509
+ IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
9510
+ NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
9511
+ MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
9512
+ HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
9513
+ const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
9514
+ CUSTOM_ELEMENT_HANDLING = create(null);
9515
+ if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
9516
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
8930
9517
  }
8931
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
8932
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
9518
+ if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) {
9519
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined
8933
9520
  }
8934
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
8935
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
9521
+ if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
9522
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
8936
9523
  }
9524
+ seal(CUSTOM_ELEMENT_HANDLING);
8937
9525
  if (SAFE_FOR_TEMPLATES) {
8938
9526
  ALLOW_DATA_ATTR = false;
8939
9527
  }
@@ -8969,36 +9557,36 @@ function requirePurify_cjs () {
8969
9557
  EXTRA_ELEMENT_HANDLING.tagCheck = null;
8970
9558
  EXTRA_ELEMENT_HANDLING.attributeCheck = null;
8971
9559
  /* Merge configuration parameters */
8972
- if (cfg.ADD_TAGS) {
9560
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
8973
9561
  if (typeof cfg.ADD_TAGS === 'function') {
8974
9562
  EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
8975
- } else {
9563
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
8976
9564
  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
8977
9565
  ALLOWED_TAGS = clone(ALLOWED_TAGS);
8978
9566
  }
8979
9567
  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
8980
9568
  }
8981
9569
  }
8982
- if (cfg.ADD_ATTR) {
9570
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
8983
9571
  if (typeof cfg.ADD_ATTR === 'function') {
8984
9572
  EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
8985
- } else {
9573
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
8986
9574
  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
8987
9575
  ALLOWED_ATTR = clone(ALLOWED_ATTR);
8988
9576
  }
8989
9577
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
8990
9578
  }
8991
9579
  }
8992
- if (cfg.ADD_URI_SAFE_ATTR) {
9580
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
8993
9581
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
8994
9582
  }
8995
- if (cfg.FORBID_CONTENTS) {
9583
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
8996
9584
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
8997
9585
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
8998
9586
  }
8999
9587
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
9000
9588
  }
9001
- if (cfg.ADD_FORBID_CONTENTS) {
9589
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
9002
9590
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
9003
9591
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
9004
9592
  }
@@ -9017,6 +9605,13 @@ function requirePurify_cjs () {
9017
9605
  addToSet(ALLOWED_TAGS, ['tbody']);
9018
9606
  delete FORBID_TAGS.tbody;
9019
9607
  }
9608
+ // Re-derive the active Trusted Types policy from this configuration on
9609
+ // every parse. The active policy must never be sticky closure state that
9610
+ // outlives the config that set it: a caller-supplied policy left in place
9611
+ // after `clearConfig()` — or after a later call that supplied none, or
9612
+ // `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
9613
+ // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
9614
+ // See GHSA-vxr8-fq34-vvx9.
9020
9615
  if (cfg.TRUSTED_TYPES_POLICY) {
9021
9616
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
9022
9617
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -9024,18 +9619,45 @@ function requirePurify_cjs () {
9024
9619
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
9025
9620
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
9026
9621
  }
9027
- // Overwrite existing TrustedTypes policy.
9622
+ // A caller-supplied policy applies to this configuration only.
9623
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
9028
9624
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
9029
- // Sign local variables required by `sanitize`.
9030
- emptyHTML = trustedTypesPolicy.createHTML('');
9625
+ // Sign local variables required by `sanitize`. If the supplied policy's
9626
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
9627
+ // throws via the re-entrancy guard. Restore the previous policy first so
9628
+ // the instance is not left in a poisoned state. See #1422.
9629
+ try {
9630
+ emptyHTML = _createTrustedHTML('');
9631
+ } catch (error) {
9632
+ trustedTypesPolicy = previousTrustedTypesPolicy;
9633
+ throw error;
9634
+ }
9635
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
9636
+ // Explicit opt-out for this call: perform no Trusted Types signing and
9637
+ // create nothing (so a strict `trusted-types` CSP that disallows a
9638
+ // `dompurify` policy can still call `sanitize` from inside its own
9639
+ // policy — see #1422). Resetting to `undefined` rather than a sticky
9640
+ // `null` also drops any previously retained caller policy, so it cannot
9641
+ // resurface on a later call, while still allowing the next config-less
9642
+ // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
9643
+ trustedTypesPolicy = undefined;
9644
+ emptyHTML = '';
9031
9645
  } else {
9032
- // Uninitialized policy, attempt to initialize the internal dompurify policy.
9646
+ // No policy supplied: keep the currently active policy if one is set — a
9647
+ // previously supplied policy is intentionally sticky across config-less
9648
+ // calls — otherwise fall back to the instance's own internal policy,
9649
+ // created at most once. (A policy supplied for a *single* call still
9650
+ // lingers by design; what must not linger is a policy whose configuration
9651
+ // has been torn down via `clearConfig()`, which restores the default.)
9033
9652
  if (trustedTypesPolicy === undefined) {
9034
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
9653
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
9035
9654
  }
9036
- // If creating the internal policy succeeded sign internal variables.
9037
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
9038
- emptyHTML = trustedTypesPolicy.createHTML('');
9655
+ // Sign internal variables only when a policy is active. A falsy policy
9656
+ // (Trusted Types unsupported, creation failed, or an explicit opt-out)
9657
+ // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
9658
+ // a non-policy and throw. See #1422.
9659
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
9660
+ emptyHTML = _createTrustedHTML('');
9039
9661
  }
9040
9662
  }
9041
9663
  // Prevent further manipulation of configuration.
@@ -9050,6 +9672,77 @@ function requirePurify_cjs () {
9050
9672
  * correctly. */
9051
9673
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
9052
9674
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
9675
+ /**
9676
+ * Namespace rules for an element in the SVG namespace.
9677
+ *
9678
+ * @param tagName the element's lowercase tag name
9679
+ * @param parent the (possibly simulated) parent node
9680
+ * @param parentTagName the parent's lowercase tag name
9681
+ * @returns true if a spec-compliant parser could produce this element
9682
+ */
9683
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
9684
+ // The only way to switch from HTML namespace to SVG
9685
+ // is via <svg>. If it happens via any other tag, then
9686
+ // it should be killed.
9687
+ if (parent.namespaceURI === HTML_NAMESPACE) {
9688
+ return tagName === 'svg';
9689
+ }
9690
+ // The only way to switch from MathML to SVG is via <svg>
9691
+ // if the parent is either <annotation-xml> or a MathML
9692
+ // text integration point.
9693
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
9694
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
9695
+ }
9696
+ // We only allow elements that are defined in SVG
9697
+ // spec. All others are disallowed in SVG namespace.
9698
+ return Boolean(ALL_SVG_TAGS[tagName]);
9699
+ };
9700
+ /**
9701
+ * Namespace rules for an element in the MathML namespace.
9702
+ *
9703
+ * @param tagName the element's lowercase tag name
9704
+ * @param parent the (possibly simulated) parent node
9705
+ * @param parentTagName the parent's lowercase tag name
9706
+ * @returns true if a spec-compliant parser could produce this element
9707
+ */
9708
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
9709
+ // The only way to switch from HTML namespace to MathML
9710
+ // is via <math>. If it happens via any other tag, then
9711
+ // it should be killed.
9712
+ if (parent.namespaceURI === HTML_NAMESPACE) {
9713
+ return tagName === 'math';
9714
+ }
9715
+ // The only way to switch from SVG to MathML is via
9716
+ // <math> and HTML integration points
9717
+ if (parent.namespaceURI === SVG_NAMESPACE) {
9718
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
9719
+ }
9720
+ // We only allow elements that are defined in MathML
9721
+ // spec. All others are disallowed in MathML namespace.
9722
+ return Boolean(ALL_MATHML_TAGS[tagName]);
9723
+ };
9724
+ /**
9725
+ * Namespace rules for an element in the HTML namespace.
9726
+ *
9727
+ * @param tagName the element's lowercase tag name
9728
+ * @param parent the (possibly simulated) parent node
9729
+ * @param parentTagName the parent's lowercase tag name
9730
+ * @returns true if a spec-compliant parser could produce this element
9731
+ */
9732
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
9733
+ // The only way to switch from SVG to HTML is via
9734
+ // HTML integration points, and from MathML to HTML
9735
+ // is via MathML text integration points
9736
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
9737
+ return false;
9738
+ }
9739
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
9740
+ return false;
9741
+ }
9742
+ // We disallow tags that are specific for MathML
9743
+ // or SVG and should never appear in HTML namespace
9744
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
9745
+ };
9053
9746
  /**
9054
9747
  * @param element a DOM element whose namespace is being checked
9055
9748
  * @returns Return false if the element has a
@@ -9072,51 +9765,13 @@ function requirePurify_cjs () {
9072
9765
  return false;
9073
9766
  }
9074
9767
  if (element.namespaceURI === SVG_NAMESPACE) {
9075
- // The only way to switch from HTML namespace to SVG
9076
- // is via <svg>. If it happens via any other tag, then
9077
- // it should be killed.
9078
- if (parent.namespaceURI === HTML_NAMESPACE) {
9079
- return tagName === 'svg';
9080
- }
9081
- // The only way to switch from MathML to SVG is via`
9082
- // svg if parent is either <annotation-xml> or MathML
9083
- // text integration points.
9084
- if (parent.namespaceURI === MATHML_NAMESPACE) {
9085
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
9086
- }
9087
- // We only allow elements that are defined in SVG
9088
- // spec. All others are disallowed in SVG namespace.
9089
- return Boolean(ALL_SVG_TAGS[tagName]);
9768
+ return _checkSvgNamespace(tagName, parent, parentTagName);
9090
9769
  }
9091
9770
  if (element.namespaceURI === MATHML_NAMESPACE) {
9092
- // The only way to switch from HTML namespace to MathML
9093
- // is via <math>. If it happens via any other tag, then
9094
- // it should be killed.
9095
- if (parent.namespaceURI === HTML_NAMESPACE) {
9096
- return tagName === 'math';
9097
- }
9098
- // The only way to switch from SVG to MathML is via
9099
- // <math> and HTML integration points
9100
- if (parent.namespaceURI === SVG_NAMESPACE) {
9101
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
9102
- }
9103
- // We only allow elements that are defined in MathML
9104
- // spec. All others are disallowed in MathML namespace.
9105
- return Boolean(ALL_MATHML_TAGS[tagName]);
9771
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
9106
9772
  }
9107
9773
  if (element.namespaceURI === HTML_NAMESPACE) {
9108
- // The only way to switch from SVG to HTML is via
9109
- // HTML integration points, and from MathML to HTML
9110
- // is via MathML text integration points
9111
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
9112
- return false;
9113
- }
9114
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
9115
- return false;
9116
- }
9117
- // We disallow tags that are specific for MathML
9118
- // or SVG and should never appear in HTML namespace
9119
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
9774
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
9120
9775
  }
9121
9776
  // For XHTML and XML documents that support custom namespaces
9122
9777
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -9141,7 +9796,81 @@ function requirePurify_cjs () {
9141
9796
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
9142
9797
  getParentNode(node).removeChild(node);
9143
9798
  } catch (_) {
9799
+ /* The normal detach failed — this is reached for a parentless node
9800
+ (getParentNode() is null, so .removeChild throws). Element.prototype
9801
+ .remove() is itself a spec no-op on a parentless node, so a recorded
9802
+ "removal" would otherwise hand the caller back an intact,
9803
+ payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
9804
+ the style-with-element-child rule decided to kill). Fail closed by
9805
+ throwing — exactly as a clobbered root does at the IN_PLACE entry —
9806
+ rather than trying to "neutralize" the node via its own methods.
9807
+ Neutralizing would mean calling getAttributeNames()/removeAttribute()
9808
+ on the node, both of which a <form> root can clobber via a named child
9809
+ (and _isClobbered does not even probe getAttributeNames), so the
9810
+ neutralize step could itself be silently defeated, leaving the payload
9811
+ intact. A throw touches only the cached, clobber-safe remove() and
9812
+ getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
9813
+ to every root-kill reason. REPORT-3.
9814
+ This lives inside the catch, so it never fires for a normally-removed
9815
+ in-tree node: those have a parent, removeChild() succeeds, and the
9816
+ catch is not entered. Only a kept (parentless) root reaches here. */
9144
9817
  remove(node);
9818
+ if (!getParentNode(node)) {
9819
+ throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
9820
+ }
9821
+ }
9822
+ };
9823
+ /**
9824
+ * _neutralizeRoot
9825
+ *
9826
+ * Fail-closed teardown of an in-place root after the sanitize walk aborts
9827
+ * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
9828
+ * custom element's reaction detaches a node so `_forceRemove`'s deliberate
9829
+ * parentless guard throws, or any other re-entrant engine mutation — would
9830
+ * otherwise leave the caller's *live* tree half-sanitized, with everything
9831
+ * after the abort point still carrying its handlers. There is no safe way
9832
+ * to resume the walk (the tree mutated under us), so we strip the root bare:
9833
+ * remove every child and every attribute, then let the caller's catch see
9834
+ * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
9835
+ * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
9836
+ *
9837
+ * @param root the in-place root to empty
9838
+ */
9839
+ const _neutralizeRoot = function _neutralizeRoot(root) {
9840
+ /* Strip every disallowed attribute (on* handlers included) off the whole
9841
+ subtree BEFORE detaching anything. Detaching first would hand back
9842
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
9843
+ whose queued resource event still fires in page scope after we throw.
9844
+ Clobber-safe reads; a doomed clobbered node's own attributes are
9845
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
9846
+ _neutralizeSubtree(root);
9847
+ const childNodes = getChildNodes(root);
9848
+ if (childNodes) {
9849
+ const snapshot = [];
9850
+ arrayForEach(childNodes, child => {
9851
+ arrayPush(snapshot, child);
9852
+ });
9853
+ arrayForEach(snapshot, child => {
9854
+ try {
9855
+ remove(child);
9856
+ } catch (_) {
9857
+ /* Best-effort teardown; a still-attached child is handled below */
9858
+ }
9859
+ });
9860
+ }
9861
+ const attributes = getAttributes(root);
9862
+ if (attributes) {
9863
+ for (let i = attributes.length - 1; i >= 0; --i) {
9864
+ const attribute = attributes[i];
9865
+ const name = attribute && attribute.name;
9866
+ if (typeof name === 'string') {
9867
+ try {
9868
+ root.removeAttribute(name);
9869
+ } catch (_) {
9870
+ /* Clobbered removeAttribute — ignore (fail-closed best effort) */
9871
+ }
9872
+ }
9873
+ }
9145
9874
  }
9146
9875
  };
9147
9876
  /**
@@ -9176,6 +9905,148 @@ function requirePurify_cjs () {
9176
9905
  }
9177
9906
  }
9178
9907
  };
9908
+ /**
9909
+ * _stripDisallowedAttributes
9910
+ *
9911
+ * Removes every attribute the active configuration does not allow from a
9912
+ * single element, using the same allowlist as the main attribute pass (so
9913
+ * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
9914
+ * neutralise nodes that are being discarded from an in-place tree.
9915
+ *
9916
+ * @param element the element to strip
9917
+ */
9918
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
9919
+ const attributes = getAttributes(element);
9920
+ if (!attributes) {
9921
+ return;
9922
+ }
9923
+ for (let i = attributes.length - 1; i >= 0; --i) {
9924
+ const attribute = attributes[i];
9925
+ const name = attribute && attribute.name;
9926
+ if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
9927
+ continue;
9928
+ }
9929
+ try {
9930
+ element.removeAttribute(name);
9931
+ } catch (_) {
9932
+ /* Clobbered removeAttribute on a doomed node — ignore */
9933
+ }
9934
+ }
9935
+ };
9936
+ /**
9937
+ * _neutralizeSubtree
9938
+ *
9939
+ * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
9940
+ * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
9941
+ * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
9942
+ * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
9943
+ * those dropped nodes are detached from the caller's LIVE tree but a
9944
+ * handler-bearing original among them (an `<img onerror>`/`<video>` that was
9945
+ * loading) keeps its queued resource event, which fires in page scope after
9946
+ * sanitize returns. This walks a removed subtree and strips every attribute
9947
+ * the active configuration does not allow — so `on*` handlers are cancelled
9948
+ * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
9949
+ * blocklist. Run synchronously before sanitize returns, i.e. before any
9950
+ * queued event can fire. Hook-free by design: these nodes leave the output,
9951
+ * so firing attribute hooks for them would be surprising. Clobber-safe reads;
9952
+ * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
9953
+ * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
9954
+ * the `<img>`, are reached and scrubbed).
9955
+ *
9956
+ * @param root the root of a removed subtree to neutralise
9957
+ */
9958
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
9959
+ const stack = [root];
9960
+ while (stack.length > 0) {
9961
+ const node = stack.pop();
9962
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
9963
+ if (nodeType === NODE_TYPE.element) {
9964
+ _stripDisallowedAttributes(node);
9965
+ }
9966
+ const childNodes = getChildNodes(node);
9967
+ if (childNodes) {
9968
+ for (let i = childNodes.length - 1; i >= 0; --i) {
9969
+ stack.push(childNodes[i]);
9970
+ }
9971
+ }
9972
+ }
9973
+ };
9974
+ /**
9975
+ * _neutralizePatchLinkage
9976
+ *
9977
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
9978
+ * hardening, https://github.com/WICG/declarative-partial-updates).
9979
+ *
9980
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
9981
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
9982
+ * reaches each node. On a live in-place root that leaves a window: from the
9983
+ * moment the root is connected until the walk arrives at a given node, that
9984
+ * node's linkage is live. A patch applied on connection/stream can fire as
9985
+ * a microtask during the walk and inject or teleport an unsanitized DOM
9986
+ * range into a region the iterator has already passed and will not revisit,
9987
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
9988
+ * whole tree once up front and sever every linkage before the walk begins,
9989
+ * closing that window.
9990
+ *
9991
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
9992
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
9993
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
9994
+ * to group with the rest of the declarative-partial-updates handling and
9995
+ * stay overridable, consistent with the codebase.
9996
+ *
9997
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
9998
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
9999
+ *
10000
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
10001
+ * .html Q1): this mirrors the existing policy of keeping `for` on
10002
+ * <label>/<output>. If the shipping feature can drive a patch through a
10003
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
10004
+ * attribute check at _isBasicCustomElement's caller must additionally drop
10005
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
10006
+ *
10007
+ * @param root the in-place root to sweep
10008
+ */
10009
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
10010
+ if (!SAFE_FOR_XML) {
10011
+ return;
10012
+ }
10013
+ const stack = [root];
10014
+ while (stack.length > 0) {
10015
+ const node = stack.pop();
10016
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
10017
+ /* Remove range markers (the target side of a patch linkage): every
10018
+ processing instruction, and any markup-bearing comment. */
10019
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
10020
+ try {
10021
+ remove(node);
10022
+ } catch (_) {
10023
+ /* Best-effort */
10024
+ }
10025
+ continue;
10026
+ }
10027
+ /* Strip patch-source attributes (the source side) off elements. */
10028
+ if (nodeType === NODE_TYPE.element) {
10029
+ const element = node;
10030
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
10031
+ try {
10032
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
10033
+ element.removeAttribute('patchsrc');
10034
+ }
10035
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
10036
+ element.removeAttribute('for');
10037
+ }
10038
+ } catch (_) {
10039
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
10040
+ }
10041
+ }
10042
+ const childNodes = getChildNodes(node);
10043
+ if (childNodes) {
10044
+ for (let i = childNodes.length - 1; i >= 0; --i) {
10045
+ stack.push(childNodes[i]);
10046
+ }
10047
+ }
10048
+ }
10049
+ };
9179
10050
  /**
9180
10051
  * _initDocument
9181
10052
  *
@@ -9197,7 +10068,7 @@ function requirePurify_cjs () {
9197
10068
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
9198
10069
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
9199
10070
  }
9200
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
10071
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
9201
10072
  /*
9202
10073
  * Use the DOMParser API by default, fallback later if needs be
9203
10074
  * DOMParser not work for svg when has multiple root element.
@@ -9233,33 +10104,297 @@ function requirePurify_cjs () {
9233
10104
  * @return The created NodeIterator
9234
10105
  */
9235
10106
  const _createNodeIterator = function _createNodeIterator(root) {
9236
- return createNodeIterator.call(root.ownerDocument || root, root,
10107
+ /* Read ownerDocument through the cached Node.prototype getter, never the
10108
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
10109
+ clobbering child (<input name="ownerDocument"> or a form-associated
10110
+ external input) shadows the prototype getter and makes a direct read
10111
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
10112
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
10113
+ the walk's fail-closed barrier - leaving the caller's live tree, with
10114
+ any already-armed handler in it, un-neutralized. The cached getter
10115
+ returns the real Document regardless of the clobber. */
10116
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
10117
+ return createNodeIterator.call(doc || root, root,
9237
10118
  // eslint-disable-next-line no-bitwise
9238
10119
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
9239
10120
  };
10121
+ /**
10122
+ * Replace template expression syntax (mustache, ERB, template
10123
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
10124
+ * sites. Order matters: mustache, then ERB, then template literal.
10125
+ *
10126
+ * @param value the string to scrub
10127
+ * @returns the scrubbed string
10128
+ */
10129
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
10130
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
10131
+ value = stringReplace(value, ERB_EXPR$1, ' ');
10132
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
10133
+ return value;
10134
+ };
10135
+ /**
10136
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
10137
+ * character data of an element subtree. Used as the final safety net for
10138
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
10139
+ * which only form after text-node normalization (e.g. fragments split across
10140
+ * stripped elements) cannot survive into a template-evaluating framework.
10141
+ *
10142
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
10143
+ * in place rather than round-tripping through innerHTML. This preserves
10144
+ * descendant node references (important for IN_PLACE callers), avoids a
10145
+ * serialize/reparse cycle, and reads literal character data — which means
10146
+ * `<%...%>` in text content matches the ERB regex against its real bytes
10147
+ * instead of the HTML-entity-escaped form innerHTML would produce.
10148
+ *
10149
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
10150
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
10151
+ *
10152
+ * @param node The root element whose character data should be scrubbed.
10153
+ */
10154
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
10155
+ var _node$querySelectorAl;
10156
+ node.normalize();
10157
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
10158
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
10159
+ carry a form-named-getter override of ownerDocument. */
10160
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
10161
+ const walker = createNodeIterator.call(doc || node, node,
10162
+ // eslint-disable-next-line no-bitwise
10163
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
10164
+ let currentNode = walker.nextNode();
10165
+ while (currentNode) {
10166
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
10167
+ currentNode = walker.nextNode();
10168
+ }
10169
+ // NodeIterator does not descend into <template>.content per the DOM spec,
10170
+ // so we must explicitly recurse into each template's content fragment,
10171
+ // mirroring the approach used by _sanitizeShadowDOM.
10172
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
10173
+ if (templates) {
10174
+ arrayForEach(templates, tmpl => {
10175
+ if (_isDocumentFragment(tmpl.content)) {
10176
+ _scrubTemplateExpressions2(tmpl.content);
10177
+ }
10178
+ });
10179
+ }
10180
+ };
9240
10181
  /**
9241
10182
  * _isClobbered
9242
10183
  *
10184
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
10185
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
10186
+ * `name` attribute matching a prototype property shadows that property
10187
+ * on direct reads. We use this check at the IN_PLACE entry-point and
10188
+ * during attribute sanitization to refuse clobbered forms.
10189
+ *
9243
10190
  * @param element element to check for clobbering attacks
9244
10191
  * @return true if clobbered, false if safe
9245
10192
  */
9246
10193
  const _isClobbered = function _isClobbered(element) {
9247
- return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
10194
+ // Realm-independent tag-name probe. If we can't determine the tag
10195
+ // name at all, we can't reason about clobbering — return false
10196
+ // (the caller's other defences still apply).
10197
+ const realTagName = getNodeName ? getNodeName(element) : null;
10198
+ if (typeof realTagName !== 'string') {
10199
+ return false;
10200
+ }
10201
+ if (transformCaseFunc(realTagName) !== 'form') {
10202
+ return false;
10203
+ }
10204
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
10205
+ // Realm-safe NamedNodeMap detection: equality against the cached
10206
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
10207
+ // makes the direct read diverge from the cached read; a clean form
10208
+ // (same-realm OR foreign-realm) has both reads pointing at the same
10209
+ // canonical NamedNodeMap.
10210
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
10211
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
10212
+ // returns the integer 1 for any Element regardless of realm; direct
10213
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
10214
+ // the named child element. Cheap addition — nodeType is read from
10215
+ // an internal slot, no serialization cost — and removes a residual
10216
+ // clobbering surface used by several mXSS / PI / comment branches
10217
+ // in _sanitizeElements that compare currentNode.nodeType directly.
10218
+ element.nodeType !== getNodeType(element) ||
10219
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
10220
+ // "childNodes" shadows the prototype getter. Direct reads of
10221
+ // form.childNodes from a clobbered form return the named child
10222
+ // instead of the real NodeList, so any walk that reads it directly
10223
+ // skips the form's real children. Compare the direct read to the
10224
+ // cached Node.prototype getter — when the form's named-property
10225
+ // getter intercepts the read, the two values differ and we flag
10226
+ // the form. This catches every clobbering child type (input,
10227
+ // select, etc.) regardless of whether the named child happens to
10228
+ // carry a numeric .length, which a typeof-based probe would miss
10229
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
10230
+ element.childNodes !== getChildNodes(element);
9248
10231
  };
9249
10232
  /**
9250
- * Checks whether the given object is a DOM node.
10233
+ * Checks whether the given value is a DocumentFragment from any realm.
10234
+ *
10235
+ * The realm-independent replacement reads `nodeType` through the cached
10236
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
10237
+ * constant (11). nodeType is a numeric value resolved from the node's
10238
+ * internal slot, identical across realms for the same kind of node.
10239
+ *
10240
+ * @param value object to check
10241
+ * @return true if value is a DocumentFragment-shaped node from any realm
10242
+ */
10243
+ const _isDocumentFragment = function _isDocumentFragment(value) {
10244
+ if (!getNodeType || typeof value !== 'object' || value === null) {
10245
+ return false;
10246
+ }
10247
+ try {
10248
+ return getNodeType(value) === NODE_TYPE.documentFragment;
10249
+ } catch (_) {
10250
+ return false;
10251
+ }
10252
+ };
10253
+ /**
10254
+ * Checks whether the given object is a DOM node, including nodes that
10255
+ * originate from a different window/realm (e.g. an iframe's
10256
+ * contentDocument). The previous `value instanceof Node` check was
10257
+ * realm-bound: nodes from a different window failed it, causing
10258
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
10259
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
9251
10260
  *
9252
10261
  * @param value object to check whether it's a DOM node
9253
- * @return true is object is a DOM node
10262
+ * @return true if value is a DOM node from any realm
9254
10263
  */
9255
10264
  const _isNode = function _isNode(value) {
9256
- return typeof Node === 'function' && value instanceof Node;
10265
+ if (!getNodeType || typeof value !== 'object' || value === null) {
10266
+ return false;
10267
+ }
10268
+ try {
10269
+ return typeof getNodeType(value) === 'number';
10270
+ } catch (_) {
10271
+ return false;
10272
+ }
9257
10273
  };
9258
10274
  function _executeHooks(hooks, currentNode, data) {
10275
+ if (hooks.length === 0) {
10276
+ return;
10277
+ }
9259
10278
  arrayForEach(hooks, hook => {
9260
10279
  hook.call(DOMPurify, currentNode, data, CONFIG);
9261
10280
  });
9262
10281
  }
10282
+ /**
10283
+ * Structural-threat checks that condemn a node regardless of the
10284
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
10285
+ * processing instructions, markup-bearing comments. Pure predicate;
10286
+ * the caller removes. Check order is load-bearing.
10287
+ *
10288
+ * @param currentNode the node to inspect
10289
+ * @param tagName the node's transformCaseFunc'd tag name
10290
+ * @return true if the node must be removed
10291
+ */
10292
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
10293
+ /* Detect mXSS attempts abusing namespace confusion */
10294
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
10295
+ return true;
10296
+ }
10297
+ /* Remove risky CSS construction leading to mXSS */
10298
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
10299
+ return true;
10300
+ }
10301
+ /* Remove any occurrence of processing instructions */
10302
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
10303
+ return true;
10304
+ }
10305
+ /* Remove any kind of possibly harmful comments */
10306
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
10307
+ return true;
10308
+ }
10309
+ return false;
10310
+ };
10311
+ /**
10312
+ * Handle a node whose tag is forbidden or not allowlisted: keep
10313
+ * allowed custom elements (false return exits _sanitizeElements
10314
+ * early - the namespace and fallback-tag removal checks are
10315
+ * intentionally skipped for kept custom elements), else hoist
10316
+ * content per KEEP_CONTENT and remove.
10317
+ *
10318
+ * A kept custom element is the ONLY case in which this function
10319
+ * returns false, so the caller uses that return value to run the
10320
+ * afterSanitizeElements hook on the kept element and keep the
10321
+ * element-hook lifecycle consistent with normal allowlisted
10322
+ * elements (GHSA-c2j3-45gr-mqc4).
10323
+ *
10324
+ * @param currentNode the disallowed node
10325
+ * @param tagName the node's transformCaseFunc'd tag name
10326
+ * @return true if the node was removed, false if kept
10327
+ */
10328
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
10329
+ /* Check if we have a custom element to handle */
10330
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
10331
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
10332
+ return false;
10333
+ }
10334
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
10335
+ return false;
10336
+ }
10337
+ }
10338
+ /* Keep content except for bad-listed elements.
10339
+ Use the cached prototype getters exclusively — the previous code
10340
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
10341
+ fallbacks, but the cached getters always return the canonical
10342
+ value (or null for a real parent-less node), so the fallback
10343
+ path was dead in safe cases and a clobbering surface in unsafe
10344
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
10345
+ parentNode)` check already gates correctly. */
10346
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
10347
+ const parentNode = getParentNode(currentNode);
10348
+ const childNodes = getChildNodes(currentNode);
10349
+ if (childNodes && parentNode) {
10350
+ const childCount = childNodes.length;
10351
+ /* Hoist by moving each child up one level rather than deep-cloning
10352
+ it. Moving transfers every descendant exactly once, so a chain of
10353
+ nested disallowed elements costs O(n) instead of the O(n^2) that
10354
+ re-cloning the shrinking subtree at each level produced; it also
10355
+ empties the removed original, so `DOMPurify.removed` no longer
10356
+ pins whole subtrees. Moving preserves the in-place guarantee too:
10357
+ an original carrying already-queued resource events (`<img
10358
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
10359
+ relocated and sanitised rather than left detached but still armed.
10360
+ The sole case that must clone is removing the walk root itself.
10361
+ The result is serialised from the root's subtree, so a restrictive
10362
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
10363
+ leave the content inside it, which only cloning does. In IN_PLACE
10364
+ the root is pre-validated as an allowed tag and so is never removed
10365
+ here, so that path always takes the move branch.
10366
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
10367
+ valid whether we move (drops the trailing entry) or clone (leaves
10368
+ the list intact). */
10369
+ for (let i = childCount - 1; i >= 0; --i) {
10370
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
10371
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
10372
+ }
10373
+ }
10374
+ }
10375
+ _forceRemove(currentNode);
10376
+ return true;
10377
+ };
10378
+ /**
10379
+ * Fork a hook-mutable allowlist off its shared binding the first time a
10380
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
10381
+ * hook cannot widen the per-instance default or the setConfig binding by
10382
+ * reference and leak past the call. Returns the set unchanged once it is
10383
+ * already call-local, so repeated calls across elements are idempotent.
10384
+ *
10385
+ * @param hookList the uponSanitize* hook array for this event
10386
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
10387
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
10388
+ * @param setConfigSet the captured setConfig() binding, or null
10389
+ * @return a call-local clone if a hook is present and set is still shared,
10390
+ * else set unchanged
10391
+ */
10392
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
10393
+ if (hookList.length === 0) {
10394
+ return set;
10395
+ }
10396
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
10397
+ };
9263
10398
  /**
9264
10399
  * _sanitizeElements
9265
10400
  *
@@ -9269,86 +10404,112 @@ function requirePurify_cjs () {
9269
10404
  * @param currentNode to check for permission to exist
9270
10405
  * @return true if node was killed, false if left alive
9271
10406
  */
9272
- const _sanitizeElements = function _sanitizeElements(currentNode) {
9273
- let content = null;
10407
+ // eslint-disable-next-line complexity
10408
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
9274
10409
  /* Execute a hook if present */
9275
10410
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
10411
+ /* A hook may have detached the node - treat it as removed (see the
10412
+ detached-node comment after the uponSanitizeElement hook below). On
10413
+ the IN_PLACE path, neutralize the detached subtree first so a queued
10414
+ resource handler on it cannot fire in page scope after we return. */
10415
+ if (currentNode !== root && getParentNode(currentNode) === null) {
10416
+ if (IN_PLACE) {
10417
+ _neutralizeSubtree(currentNode);
10418
+ }
10419
+ return true;
10420
+ }
9276
10421
  /* Check if element is clobbered or can clobber */
9277
10422
  if (_isClobbered(currentNode)) {
9278
10423
  _forceRemove(currentNode);
9279
10424
  return true;
9280
10425
  }
9281
10426
  /* Now let's check the element's type and name */
9282
- const tagName = transformCaseFunc(currentNode.nodeName);
10427
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
10428
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
10429
+ hook may have been installed after that guard sampled the hook arrays
10430
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
10431
+ aliasing a shared binding that a widening hook would mutate by
10432
+ reference. Fork it before exposing it to the hook. */
10433
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
9283
10434
  /* Execute a hook if present */
9284
10435
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
9285
10436
  tagName,
9286
10437
  allowedTags: ALLOWED_TAGS
9287
10438
  });
9288
- /* Detect mXSS attempts abusing namespace confusion */
9289
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
9290
- _forceRemove(currentNode);
9291
- return true;
9292
- }
9293
- /* Remove risky CSS construction leading to mXSS */
9294
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
9295
- _forceRemove(currentNode);
9296
- return true;
9297
- }
9298
- /* Remove any occurrence of processing instructions */
9299
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
9300
- _forceRemove(currentNode);
10439
+ /* A hook may have detached the node from the tree — a long-standing
10440
+ user pattern (issue #469; draw.io-style foreignObject filtering).
10441
+ Per the cached, unclobberable parentNode getter the node is
10442
+ genuinely out of the tree, so it can reach neither the serialized
10443
+ output nor an IN_PLACE live tree; treat it as removed and stop
10444
+ processing it. Without this guard, the unsafe-node / namespace
10445
+ checks below would call _forceRemove on a parentless node and hit
10446
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
10447
+ wants gone but *cannot* detach (clobbered / parentless roots), the
10448
+ opposite of a node that is already safely gone. The walk root is
10449
+ exempt: a detached IN_PLACE root is legitimate input and must still
10450
+ be fully sanitized, and a kill-decision on it must keep hitting the
10451
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
10452
+ responsibility for placement: they are not recorded in
10453
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
10454
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
10455
+ can still hold a queued resource-event handler - e.g. an <img onload>
10456
+ that began loading when the caller built the live tree - which fires
10457
+ in page scope after sanitize returns even though the handler never
10458
+ reached the returned tree. That is the audit-5 F1 hazard, and the
10459
+ documented node.remove() hook pattern walks straight into it. So on
10460
+ the IN_PLACE path we neutralize the detached subtree inline here,
10461
+ stripping its non-allow-listed attributes before returning, exactly
10462
+ as the post-walk pass does for _forceRemove'd subtrees. */
10463
+ if (currentNode !== root && getParentNode(currentNode) === null) {
10464
+ if (IN_PLACE) {
10465
+ _neutralizeSubtree(currentNode);
10466
+ }
9301
10467
  return true;
9302
10468
  }
9303
- /* Remove any kind of possibly harmful comments */
9304
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
10469
+ /* Remove mXSS vectors, processing instructions and risky comments */
10470
+ if (_isUnsafeNode(currentNode, tagName)) {
9305
10471
  _forceRemove(currentNode);
9306
10472
  return true;
9307
10473
  }
9308
10474
  /* Remove element if anything forbids its presence */
9309
10475
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
9310
- /* Check if we have a custom element to handle */
9311
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
9312
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
9313
- return false;
9314
- }
9315
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
9316
- return false;
9317
- }
9318
- }
9319
- /* Keep content except for bad-listed elements */
9320
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
9321
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
9322
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
9323
- if (childNodes && parentNode) {
9324
- const childCount = childNodes.length;
9325
- for (let i = childCount - 1; i >= 0; --i) {
9326
- const childClone = cloneNode(childNodes[i], true);
9327
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
9328
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
9329
- }
9330
- }
10476
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
10477
+ /* A false return means the node is a custom element kept via
10478
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
10479
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
10480
+ element-hook lifecycle matches normal allowlisted elements: a
10481
+ security policy applied in this hook (e.g. stripping an attribute
10482
+ from every surviving element) must not silently skip kept custom
10483
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
10484
+ tail below - the hook runs, then the walker's subsequent
10485
+ _sanitizeAttributes pass sanitizes the element's attributes. The
10486
+ deliberately skipped namespace and fallback-tag removal checks stay
10487
+ skipped; they are removal decisions, not the hook contract. */
10488
+ if (removed === false) {
10489
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
9331
10490
  }
9332
- _forceRemove(currentNode);
9333
- return true;
10491
+ return removed;
9334
10492
  }
9335
- /* Check whether element has a valid namespace */
9336
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
10493
+ /* Check whether element has a valid namespace.
10494
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
10495
+ nodeType getter rather than `instanceof Element`, which is realm-
10496
+ bound and short-circuits to false for any node minted in a different
10497
+ realm — letting a foreign-realm element with a forbidden namespace
10498
+ slip past the namespace check entirely. */
10499
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
10500
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
9337
10501
  _forceRemove(currentNode);
9338
10502
  return true;
9339
10503
  }
9340
10504
  /* Make sure that older browsers don't get fallback-tag mXSS */
9341
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
10505
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
9342
10506
  _forceRemove(currentNode);
9343
10507
  return true;
9344
10508
  }
9345
10509
  /* Sanitize element content to be template-safe */
9346
10510
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
9347
10511
  /* Get the element's text content */
9348
- content = currentNode.textContent;
9349
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
9350
- content = stringReplace(content, expr, ' ');
9351
- });
10512
+ const content = _stripTemplateExpressions(currentNode.textContent);
9352
10513
  if (currentNode.textContent !== content) {
9353
10514
  arrayPush(DOMPurify.removed, {
9354
10515
  element: currentNode.cloneNode()
@@ -9374,15 +10535,43 @@ function requirePurify_cjs () {
9374
10535
  if (FORBID_ATTR[lcName]) {
9375
10536
  return false;
9376
10537
  }
10538
+ /* Reject declarative-partial-updates patch-linkage attributes
10539
+ (https://github.com/WICG/declarative-partial-updates).
10540
+ Empirical note (Chrome 150, verified — see
10541
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
10542
+ sanitization. For the string path it fires during sanitize()'s own
10543
+ parse, so the walk sees and sanitizes the fully materialized expanded
10544
+ tree — teleports into MathML/SVG integration points included; a
10545
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
10546
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
10547
+ Either way DOMPurify is NOT blind to the patch.
10548
+ This removal is therefore defense-in-depth rather than the sole barrier:
10549
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
10550
+ in the caller's context, and keeps behaviour deterministic if a future
10551
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
10552
+ anywhere else (notably <template for>) it links the element to a patch
10553
+ target and teleports or removes an arbitrary DOM range by id/marker name.
10554
+ `patchsrc` fetches remote markup and is treated as a script-loading
10555
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
10556
+ other structural-threat checks and stays overridable, consistent with
10557
+ the rest of the codebase. PI range markers are already removed by
10558
+ _isUnsafeNode. */
10559
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
10560
+ return false;
10561
+ }
10562
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
10563
+ return false;
10564
+ }
9377
10565
  /* Make sure attribute cannot clobber */
9378
10566
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
9379
10567
  return false;
9380
10568
  }
10569
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
9381
10570
  /* Allow valid data-* attributes: At least one character after "-"
9382
10571
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
9383
10572
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
9384
10573
  We don't need to check the value; it's always URI safe. */
9385
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
10574
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
9386
10575
  if (
9387
10576
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
9388
10577
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -9394,11 +10583,15 @@ function requirePurify_cjs () {
9394
10583
  return false;
9395
10584
  }
9396
10585
  /* Check value is safe. First, is attr inert? If so, is safe */
9397
- } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
10586
+ } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if (value) {
9398
10587
  return false;
9399
10588
  } else ;
9400
10589
  return true;
9401
10590
  };
10591
+ /* Names the HTML spec reserves from valid-custom-element-name; these must
10592
+ * never be treated as basic custom elements even when a permissive
10593
+ * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */
10594
+ const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ['annotation-xml', 'color-profile', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'missing-glyph']);
9402
10595
  /**
9403
10596
  * _isBasicCustomElement
9404
10597
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -9408,7 +10601,64 @@ function requirePurify_cjs () {
9408
10601
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
9409
10602
  */
9410
10603
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
9411
- return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
10604
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
10605
+ };
10606
+ /**
10607
+ * Wrap an attribute value in the matching Trusted Types object when
10608
+ * the active policy requires it. Namespaced attributes pass through
10609
+ * unchanged (no TT support yet, see
10610
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
10611
+ *
10612
+ * @param lcTag lowercase tag name of the containing element
10613
+ * @param lcName lowercase attribute name
10614
+ * @param namespaceURI the attribute's namespace, if any
10615
+ * @param value the attribute value to wrap
10616
+ * @return the value, wrapped when Trusted Types demand it
10617
+ */
10618
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
10619
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
10620
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
10621
+ case 'TrustedHTML':
10622
+ {
10623
+ return _createTrustedHTML(value);
10624
+ }
10625
+ case 'TrustedScriptURL':
10626
+ {
10627
+ return _createTrustedScriptURL(value);
10628
+ }
10629
+ }
10630
+ }
10631
+ return value;
10632
+ };
10633
+ /**
10634
+ * Write a modified attribute value back onto the element. On
10635
+ * success, re-probe for clobbering introduced by the new value and
10636
+ * remove the element when found; otherwise pop the removal entry
10637
+ * recorded by the earlier _removeAttribute (long-standing pairing
10638
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
10639
+ * failure, remove the attribute instead.
10640
+ *
10641
+ * @param currentNode the element carrying the attribute
10642
+ * @param name the attribute name as present on the element
10643
+ * @param namespaceURI the attribute's namespace, if any
10644
+ * @param value the new attribute value
10645
+ */
10646
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
10647
+ try {
10648
+ if (namespaceURI) {
10649
+ currentNode.setAttributeNS(namespaceURI, name, value);
10650
+ } else {
10651
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
10652
+ currentNode.setAttribute(name, value);
10653
+ }
10654
+ if (_isClobbered(currentNode)) {
10655
+ _forceRemove(currentNode);
10656
+ } else {
10657
+ arrayPop(DOMPurify.removed);
10658
+ }
10659
+ } catch (_) {
10660
+ _removeAttribute(name, currentNode);
10661
+ }
9412
10662
  };
9413
10663
  /**
9414
10664
  * _sanitizeAttributes
@@ -9423,13 +10673,14 @@ function requirePurify_cjs () {
9423
10673
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
9424
10674
  /* Execute a hook if present */
9425
10675
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
9426
- const {
9427
- attributes
9428
- } = currentNode;
10676
+ const attributes = currentNode.attributes;
9429
10677
  /* Check if we have attributes; if not we might have a text node */
9430
10678
  if (!attributes || _isClobbered(currentNode)) {
9431
10679
  return;
9432
10680
  }
10681
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
10682
+ attribute allowlist off its shared binding before a hook can see it. */
10683
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
9433
10684
  const hookEvent = {
9434
10685
  attrName: '',
9435
10686
  attrValue: '',
@@ -9438,14 +10689,13 @@ function requirePurify_cjs () {
9438
10689
  forceKeepAttr: undefined
9439
10690
  };
9440
10691
  let l = attributes.length;
10692
+ const lcTag = transformCaseFunc(currentNode.nodeName);
9441
10693
  /* Go backwards over all attributes; safely remove bad ones */
9442
10694
  while (l--) {
9443
10695
  const attr = attributes[l];
9444
- const {
9445
- name,
9446
- namespaceURI,
9447
- value: attrValue
9448
- } = attr;
10696
+ const name = attr.name,
10697
+ namespaceURI = attr.namespaceURI,
10698
+ attrValue = attr.value;
9449
10699
  const lcName = transformCaseFunc(name);
9450
10700
  const initValue = attrValue;
9451
10701
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -9459,12 +10709,14 @@ function requirePurify_cjs () {
9459
10709
  /* Full DOM Clobbering protection via namespace isolation,
9460
10710
  * Prefix id and name attributes with `user-content-`
9461
10711
  */
9462
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
10712
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
9463
10713
  // Remove the attribute with this value
9464
10714
  _removeAttribute(name, currentNode);
9465
10715
  // Prefix the value and later re-create the attribute with the sanitized value
9466
10716
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
9467
10717
  }
10718
+ // Else: already prefixed, leave the attribute alone — the prefix is
10719
+ // itself the clobbering protection, and re-applying it is incorrect.
9468
10720
  /* Work around a security issue with comments inside attributes */
9469
10721
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
9470
10722
  _removeAttribute(name, currentNode);
@@ -9475,7 +10727,7 @@ function requirePurify_cjs () {
9475
10727
  _removeAttribute(name, currentNode);
9476
10728
  continue;
9477
10729
  }
9478
- /* Did the hooks approve of the attribute? */
10730
+ /* Did the hooks force-keep the attribute? */
9479
10731
  if (hookEvent.forceKeepAttr) {
9480
10732
  continue;
9481
10733
  }
@@ -9485,56 +10737,24 @@ function requirePurify_cjs () {
9485
10737
  continue;
9486
10738
  }
9487
10739
  /* Work around a security issue in jQuery 3.0 */
9488
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
10740
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
9489
10741
  _removeAttribute(name, currentNode);
9490
10742
  continue;
9491
10743
  }
9492
10744
  /* Sanitize attribute content to be template-safe */
9493
10745
  if (SAFE_FOR_TEMPLATES) {
9494
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
9495
- value = stringReplace(value, expr, ' ');
9496
- });
10746
+ value = _stripTemplateExpressions(value);
9497
10747
  }
9498
10748
  /* Is `value` valid for this attribute? */
9499
- const lcTag = transformCaseFunc(currentNode.nodeName);
9500
10749
  if (!_isValidAttribute(lcTag, lcName, value)) {
9501
10750
  _removeAttribute(name, currentNode);
9502
10751
  continue;
9503
10752
  }
9504
10753
  /* Handle attributes that require Trusted Types */
9505
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
9506
- if (namespaceURI) ; else {
9507
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
9508
- case 'TrustedHTML':
9509
- {
9510
- value = trustedTypesPolicy.createHTML(value);
9511
- break;
9512
- }
9513
- case 'TrustedScriptURL':
9514
- {
9515
- value = trustedTypesPolicy.createScriptURL(value);
9516
- break;
9517
- }
9518
- }
9519
- }
9520
- }
10754
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
9521
10755
  /* Handle invalid data-* attribute set by try-catching it */
9522
10756
  if (value !== initValue) {
9523
- try {
9524
- if (namespaceURI) {
9525
- currentNode.setAttributeNS(namespaceURI, name, value);
9526
- } else {
9527
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
9528
- currentNode.setAttribute(name, value);
9529
- }
9530
- if (_isClobbered(currentNode)) {
9531
- _forceRemove(currentNode);
9532
- } else {
9533
- arrayPop(DOMPurify.removed);
9534
- }
9535
- } catch (_) {
9536
- _removeAttribute(name, currentNode);
9537
- }
10757
+ _setAttributeValue(currentNode, name, namespaceURI, value);
9538
10758
  }
9539
10759
  }
9540
10760
  /* Execute a hook if present */
@@ -9554,17 +10774,136 @@ function requirePurify_cjs () {
9554
10774
  /* Execute a hook if present */
9555
10775
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
9556
10776
  /* Sanitize tags and elements */
9557
- _sanitizeElements(shadowNode);
10777
+ _sanitizeElements(shadowNode, fragment);
9558
10778
  /* Check attributes next */
9559
10779
  _sanitizeAttributes(shadowNode);
9560
- /* Deep shadow DOM detected */
9561
- if (shadowNode.content instanceof DocumentFragment) {
10780
+ /* Deep shadow DOM detected.
10781
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
10782
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
10783
+ recurse into <template>.content from foreign realms too. */
10784
+ if (_isDocumentFragment(shadowNode.content)) {
9562
10785
  _sanitizeShadowDOM2(shadowNode.content);
9563
10786
  }
10787
+ /* An element iterated here may itself host an attached
10788
+ shadow root. The default NodeIterator does not enter shadow
10789
+ trees, so a shadow root nested inside template.content was
10790
+ previously reached by no walk at all (the pre-pass at
10791
+ _sanitizeAttachedShadowRoots descends via childNodes, which
10792
+ doesn't enter template.content; the template-content recursion
10793
+ above iterates the content but never inspected shadowRoot).
10794
+ Walk it explicitly. The nodeType guard avoids reading
10795
+ shadowRoot off text / comment / CDATA / PI nodes that the
10796
+ iterator also surfaces. */
10797
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
10798
+ if (shadowNodeType === NODE_TYPE.element) {
10799
+ const innerSr = getShadowRoot(shadowNode);
10800
+ if (_isDocumentFragment(innerSr)) {
10801
+ _sanitizeAttachedShadowRoots(innerSr);
10802
+ _sanitizeShadowDOM2(innerSr);
10803
+ }
10804
+ }
9564
10805
  }
9565
10806
  /* Execute a hook if present */
9566
10807
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
9567
10808
  };
10809
+ /**
10810
+ * _sanitizeAttachedShadowRoots
10811
+ *
10812
+ * Walks `root` and feeds every attached shadow root we encounter into
10813
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
10814
+ * does not descend into shadow trees, so nodes inside an attached
10815
+ * shadow root would otherwise be skipped entirely.
10816
+ *
10817
+ * Two real input paths put attached shadow roots in front of us:
10818
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
10819
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
10820
+ * shadow root because it was created with `clonable: true`.
10821
+ *
10822
+ * This pass runs once, up front, so the main iteration loop (and the
10823
+ * existing _sanitizeShadowDOM template-content recursion) stay
10824
+ * untouched — string-input paths are not affected.
10825
+ *
10826
+ * @param root the subtree root to walk for attached shadow roots
10827
+ */
10828
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
10829
+ /* Iterative (explicit stack) rather than per-child recursion. DOM APIs
10830
+ impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
10831
+ built straight into the DOM — the IN_PLACE surface) deeper than the JS
10832
+ call-stack budget would otherwise overflow native recursion here and
10833
+ throw at the IN_PLACE entry pre-pass, before a single node is
10834
+ sanitized, leaving the caller's live tree untouched (fail-open). See
10835
+ campaign-3 F4. A heap stack keeps depth off the call stack.
10836
+ Each work item is either a node to descend into, or a deferred
10837
+ `_sanitizeShadowDOM` for an already-walked shadow root. The deferred
10838
+ form preserves the original post-order discipline: a shadow root's
10839
+ nested shadow roots are discovered before the outer shadow is
10840
+ sanitized (which may remove hosts). Pushes are in reverse of the
10841
+ desired processing order (LIFO): template content, then children, then
10842
+ the shadow-sanitize, then the shadow walk — so the order matches the
10843
+ previous recursion exactly. */
10844
+ const stack = [{
10845
+ node: root,
10846
+ shadow: null
10847
+ }];
10848
+ while (stack.length > 0) {
10849
+ const item = stack.pop();
10850
+ /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
10851
+ if (item.shadow) {
10852
+ _sanitizeShadowDOM2(item.shadow);
10853
+ continue;
10854
+ }
10855
+ const node = item.node;
10856
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
10857
+ const isElement = nodeType === NODE_TYPE.element;
10858
+ /* (pushed last → processed first) Children, snapshotted in reverse so
10859
+ the first child is processed first. Snapshotting matters because a
10860
+ hook may detach siblings mid-walk. */
10861
+ const childNodes = getChildNodes(node);
10862
+ if (childNodes) {
10863
+ for (let i = childNodes.length - 1; i >= 0; --i) {
10864
+ stack.push({
10865
+ node: childNodes[i],
10866
+ shadow: null
10867
+ });
10868
+ }
10869
+ }
10870
+ /* (pushed before children → processed after them, matching the old
10871
+ "template content last" order) When the node is a <template>,
10872
+ descend into its content. */
10873
+ if (isElement) {
10874
+ const rootName = getNodeName ? getNodeName(node) : null;
10875
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
10876
+ const content = node.content;
10877
+ if (_isDocumentFragment(content)) {
10878
+ stack.push({
10879
+ node: content,
10880
+ shadow: null
10881
+ });
10882
+ }
10883
+ }
10884
+ }
10885
+ /* Shadow root (processed first): walk its subtree, then sanitise it.
10886
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
10887
+ rather than `instanceof DocumentFragment`, which is realm-bound and
10888
+ silently skipped foreign-realm shadow roots (e.g.
10889
+ iframe.contentDocument attachShadow). */
10890
+ if (isElement) {
10891
+ const sr = getShadowRoot(node);
10892
+ if (_isDocumentFragment(sr)) {
10893
+ /* Push the deferred sanitise first so it pops after the shadow
10894
+ walk we push next, i.e. nested shadow roots are discovered
10895
+ before this one is sanitised. */
10896
+ stack.push({
10897
+ node: null,
10898
+ shadow: sr
10899
+ }, {
10900
+ node: sr,
10901
+ shadow: null
10902
+ });
10903
+ }
10904
+ }
10905
+ }
10906
+ };
9568
10907
  // eslint-disable-next-line complexity
9569
10908
  DOMPurify.sanitize = function (dirty) {
9570
10909
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -9581,13 +10920,9 @@ function requirePurify_cjs () {
9581
10920
  }
9582
10921
  /* Stringify, in case dirty is an object */
9583
10922
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
9584
- if (typeof dirty.toString === 'function') {
9585
- dirty = dirty.toString();
9586
- if (typeof dirty !== 'string') {
9587
- throw typeErrorCreate('dirty is not a string, aborting');
9588
- }
9589
- } else {
9590
- throw typeErrorCreate('toString is not a function');
10923
+ dirty = stringifyValue(dirty);
10924
+ if (typeof dirty !== 'string') {
10925
+ throw typeErrorCreate('dirty is not a string, aborting');
9591
10926
  }
9592
10927
  }
9593
10928
  /* Return dirty HTML if DOMPurify cannot run */
@@ -9595,24 +10930,90 @@ function requirePurify_cjs () {
9595
10930
  return dirty;
9596
10931
  }
9597
10932
  /* Assign config vars */
9598
- if (!SET_CONFIG) {
10933
+ if (SET_CONFIG) {
10934
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
10935
+ * not re-derived per call. Restore them from the pristine bindings
10936
+ * captured at setConfig() time so a previous call's hook clone (mutated
10937
+ * below) does not carry over. */
10938
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
10939
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
10940
+ } else {
9599
10941
  _parseConfig(cfg);
9600
10942
  }
10943
+ /* Clone the hook-mutable allowlists before the walk whenever an
10944
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
10945
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
10946
+ * a hook that widens them would otherwise mutate the shared set
10947
+ * permanently: across later calls and across every element. Cloning per
10948
+ * walk keeps documented in-call widening working while scoping it to the
10949
+ * call. A single guard for both config paths - the per-call path rebinds
10950
+ * the sets in _parseConfig each call, the persistent path restores them
10951
+ * from the captured bindings just above - so the two cannot diverge. */
10952
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
10953
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
10954
+ }
10955
+ if (hooks.uponSanitizeAttribute.length > 0) {
10956
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
10957
+ }
9601
10958
  /* Clean up removed elements */
9602
10959
  DOMPurify.removed = [];
9603
- /* Check if dirty is correctly typed for IN_PLACE */
9604
- if (typeof dirty === 'string') {
9605
- IN_PLACE = false;
9606
- }
9607
- if (IN_PLACE) {
9608
- /* Do some early pre-sanitization to avoid unsafe root nodes */
9609
- if (dirty.nodeName) {
9610
- const tagName = transformCaseFunc(dirty.nodeName);
10960
+ /* Resolve IN_PLACE for this call without mutating persistent config.
10961
+ Writing the IN_PLACE closure variable here leaks under setConfig(),
10962
+ where _parseConfig is skipped on later calls: a single string call would
10963
+ disable in-place mode for every subsequent node call, returning a
10964
+ sanitized copy while leaving the caller's node — which in-place callers
10965
+ keep using and whose return value they ignore — unsanitized. REPORT-2. */
10966
+ const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
10967
+ if (inPlace) {
10968
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
10969
+ linkage across the live tree BEFORE the walk, so no patch can fire
10970
+ mid-walk and inject into an already-processed region. Runs first, so
10971
+ it also covers the forbidden/clobbered roots that throw below. */
10972
+ _neutralizePatchLinkage(dirty);
10973
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
10974
+ Read nodeName through the cached prototype getter — a clobbering
10975
+ child named "nodeName" on the form root would otherwise shadow
10976
+ the property and let this check skip the root-allowlist
10977
+ validation entirely. */
10978
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
10979
+ if (typeof nn === 'string') {
10980
+ const tagName = transformCaseFunc(nn);
9611
10981
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
10982
+ /* Fail closed on a live root: neutralize handlers/children before
10983
+ throwing, exactly as the mid-walk abort path does. */
10984
+ _neutralizeRoot(dirty);
9612
10985
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
9613
10986
  }
9614
10987
  }
9615
- } else if (dirty instanceof Node) {
10988
+ /* Pre-flight the root through _isClobbered. The iterator-driven
10989
+ removal path can not detach a parent-less root: _forceRemove
10990
+ falls through to Element.prototype.remove(), which per spec
10991
+ is a no-op on a node with no parent. A clobbered root would
10992
+ then survive the main loop with its attributes uninspected,
10993
+ because _sanitizeAttributes early-returns on _isClobbered. The
10994
+ result would be an attacker-controlled form, complete with any
10995
+ event-handler attributes the caller passed in, handed back to
10996
+ the application unsanitized. Refuse to sanitize such a root
10997
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
10998
+ if (_isClobbered(dirty)) {
10999
+ /* Fail closed on a live clobbered root before throwing.
11000
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
11001
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
11002
+ _neutralizeRoot(dirty);
11003
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
11004
+ }
11005
+ /* Sanitize attached shadow roots before the main iterator runs.
11006
+ The iterator does not descend into shadow trees. Same fail-closed
11007
+ barrier as the main walk (campaign-3 F2): a custom-element reaction
11008
+ inside a shadow root could abort this pre-pass before the walk runs,
11009
+ which would otherwise leave the entire live tree unsanitized. */
11010
+ try {
11011
+ _sanitizeAttachedShadowRoots(dirty);
11012
+ } catch (error) {
11013
+ _neutralizeRoot(dirty);
11014
+ throw error;
11015
+ }
11016
+ } else if (_isNode(dirty)) {
9616
11017
  /* If dirty is a DOM element, append to an empty document to avoid
9617
11018
  elements being stripped by the parser */
9618
11019
  body = _initDocument('<!---->');
@@ -9626,12 +11027,18 @@ function requirePurify_cjs () {
9626
11027
  // eslint-disable-next-line unicorn/prefer-dom-node-append
9627
11028
  body.appendChild(importedNode);
9628
11029
  }
11030
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
11031
+ them before the main iterator runs, since the iterator does not
11032
+ descend into shadow trees. The walk routes every read through a
11033
+ cached prototype getter so clobbering descendants on a form root
11034
+ cannot hide a shadow host from this pass. */
11035
+ _sanitizeAttachedShadowRoots(importedNode);
9629
11036
  } else {
9630
11037
  /* Exit directly if we have nothing to do */
9631
11038
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
9632
11039
  // eslint-disable-next-line unicorn/prefer-includes
9633
11040
  dirty.indexOf('<') === -1) {
9634
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
11041
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
9635
11042
  }
9636
11043
  /* Initialize the document to work on */
9637
11044
  body = _initDocument(dirty);
@@ -9645,31 +11052,71 @@ function requirePurify_cjs () {
9645
11052
  _forceRemove(body.firstChild);
9646
11053
  }
9647
11054
  /* Get node iterator */
9648
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
9649
- /* Now start iterating over the created document */
9650
- while (currentNode = nodeIterator.nextNode()) {
9651
- /* Sanitize tags and elements */
9652
- _sanitizeElements(currentNode);
9653
- /* Check attributes next */
9654
- _sanitizeAttributes(currentNode);
9655
- /* Shadow DOM detected, sanitize it */
9656
- if (currentNode.content instanceof DocumentFragment) {
9657
- _sanitizeShadowDOM2(currentNode.content);
11055
+ const walkRoot = inPlace ? dirty : body;
11056
+ /* Now start iterating over the created document.
11057
+ The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
11058
+ engine/custom-element mutation can detach a node mid-walk so
11059
+ `_forceRemove`'s parentless guard throws, aborting the loop. Without the
11060
+ barrier the caller's in-place tree would be left half-sanitized with the
11061
+ unvisited tail still armed. _createNodeIterator itself is inside the
11062
+ barrier too: constructing the iterator dereferences the root's document,
11063
+ and any failure there (e.g. an exotic/clobbered root) must still fail
11064
+ closed rather than skip the neutralize. On any throw we fail closed -
11065
+ strip the in-place root bare - then rethrow so the existing throw
11066
+ contract is preserved. (String/DOM-copy paths never return the partial
11067
+ body, so the propagating throw is already fail-closed there.) */
11068
+ try {
11069
+ const nodeIterator = _createNodeIterator(walkRoot);
11070
+ while (currentNode = nodeIterator.nextNode()) {
11071
+ /* Sanitize tags and elements */
11072
+ _sanitizeElements(currentNode, walkRoot);
11073
+ /* Check attributes next */
11074
+ _sanitizeAttributes(currentNode);
11075
+ /* Shadow DOM detected, sanitize it.
11076
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
11077
+ instead of instanceof, so foreign-realm <template>.content is
11078
+ walked correctly. */
11079
+ if (_isDocumentFragment(currentNode.content)) {
11080
+ _sanitizeShadowDOM2(currentNode.content);
11081
+ }
11082
+ }
11083
+ } catch (error) {
11084
+ if (inPlace) {
11085
+ _neutralizeRoot(dirty);
11086
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
11087
+ detached from the root, so _neutralizeRoot's subtree pass does not
11088
+ reach them. Defuse them too, mirroring the success-path loop below. */
11089
+ arrayForEach(DOMPurify.removed, entry => {
11090
+ if (entry.element) {
11091
+ _neutralizeSubtree(entry.element);
11092
+ }
11093
+ });
9658
11094
  }
11095
+ throw error;
9659
11096
  }
9660
11097
  /* If we sanitized `dirty` in-place, return it. */
9661
- if (IN_PLACE) {
11098
+ if (inPlace) {
11099
+ /* Fail-closed completion of the audit-5 F1 fix: every node removed from
11100
+ the caller's live tree is detached but may still hold a queued
11101
+ resource-event handler that fires in page scope after we return. The
11102
+ move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
11103
+ non-allow-listed attributes off every other removed subtree (clobber,
11104
+ mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
11105
+ cancelled before any event can fire. Runs synchronously, pre-return. */
11106
+ arrayForEach(DOMPurify.removed, entry => {
11107
+ if (entry.element) {
11108
+ _neutralizeSubtree(entry.element);
11109
+ }
11110
+ });
11111
+ if (SAFE_FOR_TEMPLATES) {
11112
+ _scrubTemplateExpressions2(dirty);
11113
+ }
9662
11114
  return dirty;
9663
11115
  }
9664
11116
  /* Return sanitized string or DOM */
9665
11117
  if (RETURN_DOM) {
9666
11118
  if (SAFE_FOR_TEMPLATES) {
9667
- body.normalize();
9668
- let html = body.innerHTML;
9669
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
9670
- html = stringReplace(html, expr, ' ');
9671
- });
9672
- body.innerHTML = html;
11119
+ _scrubTemplateExpressions2(body);
9673
11120
  }
9674
11121
  if (RETURN_DOM_FRAGMENT) {
9675
11122
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -9699,20 +11146,28 @@ function requirePurify_cjs () {
9699
11146
  }
9700
11147
  /* Sanitize final string template-safe */
9701
11148
  if (SAFE_FOR_TEMPLATES) {
9702
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
9703
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
9704
- });
11149
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
9705
11150
  }
9706
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
11151
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
9707
11152
  };
9708
11153
  DOMPurify.setConfig = function () {
9709
11154
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
9710
11155
  _parseConfig(cfg);
9711
11156
  SET_CONFIG = true;
11157
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
11158
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
9712
11159
  };
9713
11160
  DOMPurify.clearConfig = function () {
9714
11161
  CONFIG = null;
9715
11162
  SET_CONFIG = false;
11163
+ SET_CONFIG_ALLOWED_TAGS = null;
11164
+ SET_CONFIG_ALLOWED_ATTR = null;
11165
+ // Drop any caller-supplied Trusted Types policy so it cannot poison later
11166
+ // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
11167
+ // never recreated — Trusted Types throws on duplicate names) is restored by
11168
+ // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
11169
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
11170
+ emptyHTML = '';
9716
11171
  };
9717
11172
  DOMPurify.isValidAttribute = function (tag, attr, value) {
9718
11173
  /* Initialize shared config vars if necessary. */
@@ -9727,9 +11182,19 @@ function requirePurify_cjs () {
9727
11182
  if (typeof hookFunction !== 'function') {
9728
11183
  return;
9729
11184
  }
11185
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
11186
+ * '__proto__') indexes off the prototype chain rather than a real
11187
+ * hook array, and arrayPush then writes to Object.prototype. Guard
11188
+ * with an own-property check against the known hook names. */
11189
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
11190
+ return;
11191
+ }
9730
11192
  arrayPush(hooks[entryPoint], hookFunction);
9731
11193
  };
9732
11194
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
11195
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
11196
+ return undefined;
11197
+ }
9733
11198
  if (hookFunction !== undefined) {
9734
11199
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
9735
11200
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -9737,6 +11202,9 @@ function requirePurify_cjs () {
9737
11202
  return arrayPop(hooks[entryPoint]);
9738
11203
  };
9739
11204
  DOMPurify.removeHooks = function (entryPoint) {
11205
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
11206
+ return;
11207
+ }
9740
11208
  hooks[entryPoint] = [];
9741
11209
  };
9742
11210
  DOMPurify.removeAllHooks = function () {
@@ -9997,10 +11465,26 @@ var WalletBackground = /*#__PURE__*/function (_Background) {
9997
11465
  }(Background);
9998
11466
 
9999
11467
  /**
10000
- * Generate a unique id compatible with HTML element id
11468
+ * Generate a unique id compatible with HTML element id.
11469
+ * Uses the Web Crypto API for cryptographically strong randomness, with a
11470
+ * deterministic fallback only when crypto is unavailable.
10001
11471
  */
10002
11472
  function getUniqueId() {
10003
- return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
11473
+ var letters = 'abcdefghijklmnopqrstuvwxyz';
11474
+ var length = 10;
11475
+ var bytes = new Uint8Array(length);
11476
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
11477
+ crypto.getRandomValues(bytes);
11478
+ } else {
11479
+ for (var i = 0; i < length; i++) {
11480
+ bytes[i] = (i * 31 + 7) % 256;
11481
+ }
11482
+ }
11483
+ var id = '';
11484
+ for (var _i = 0; _i < length; _i++) {
11485
+ id += letters[bytes[_i] % letters.length];
11486
+ }
11487
+ return id;
10004
11488
  }
10005
11489
 
10006
11490
  function getFlypayV2Button(widgetId) {
@@ -14033,6 +15517,54 @@ var MultiWidget = /*#__PURE__*/function () {
14033
15517
  validate_card_types: validateCardNumberInput
14034
15518
  });
14035
15519
  }
15520
+ /**
15521
+ * Current method restricts which card schemes the card number input accepts, without changing which card
15522
+ * icons the widget displays. Scheme identifiers are matched case-insensitively and ignoring surrounding
15523
+ * whitespace; unsupported identifiers are ignored with a warning, so a list in which every identifier is
15524
+ * unsupported applies no restriction at all.
15525
+ *
15526
+ * Takes precedence over setSupportedCardIcons: when a non-empty list is supplied here, it is the only list
15527
+ * used for validation and the validateCardNumberInput flag of setSupportedCardIcons no longer affects it.
15528
+ * Calling this method more than once replaces the previous list rather than merging with it.
15529
+ *
15530
+ * Must be called before load().
15531
+ *
15532
+ * @example
15533
+ *
15534
+ * widget.setAllowedCardSchemes(['visa', 'mastercard']);
15535
+ *
15536
+ * @param {string[]} schemes - [CARD_SCHEME]{@link CARD_SCHEME}
15537
+ */
15538
+ }, {
15539
+ key: "setAllowedCardSchemes",
15540
+ value: function setAllowedCardSchemes(schemes) {
15541
+ var vocabulary = ObjectHelper.values(CARD_SCHEME);
15542
+ var allowedSchemes = [];
15543
+ var _iterator = _createForOfIteratorHelper(Array.isArray(schemes) ? schemes : []),
15544
+ _step;
15545
+ try {
15546
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
15547
+ var entry = _step.value;
15548
+ var scheme = typeof entry === 'string' ? entry.trim().toLowerCase() : '';
15549
+ if (vocabulary.indexOf(scheme) === -1) {
15550
+ console.warn("Widget::setAllowedCardSchemes: unsupported card scheme ".concat(entry));
15551
+ continue;
15552
+ }
15553
+ if (allowedSchemes.indexOf(scheme) === -1) {
15554
+ allowedSchemes.push(scheme);
15555
+ }
15556
+ }
15557
+ } catch (err) {
15558
+ _iterator.e(err);
15559
+ } finally {
15560
+ _iterator.f();
15561
+ }
15562
+ if (allowedSchemes.length) {
15563
+ this.link.setParams({
15564
+ allowed_card_schemes: allowedSchemes.join(',')
15565
+ });
15566
+ }
15567
+ }
14036
15568
  /**
14037
15569
  * Current method can hide prevent the widget from showing the error messages
14038
15570
  *
@@ -19132,6 +20664,90 @@ var EVENT$2;
19132
20664
  EVENT["ON_SHIPPING_OPTIONS_CHANGE"] = "onShippingOptionsChange";
19133
20665
  })(EVENT$2 || (EVENT$2 = {}));
19134
20666
 
20667
+ /** TTL for a cached service configuration: 1 day, in milliseconds. */
20668
+ var CACHE_TTL_MS = 86400000;
20669
+ /** Builds the `localStorage` key used to cache a service's configuration. */
20670
+ function serviceConfigStorageKey(serviceId) {
20671
+ return "paydock-serviceConfig-".concat(serviceId);
20672
+ }
20673
+ /**
20674
+ * Removes the cached service configuration for the given service ID, forcing the next
20675
+ * fetch to go to the network. Called on error / unavailable flows so a potentially
20676
+ * stale cached config is not reused after something changed server-side. No-throw when
20677
+ * `localStorage` is unavailable.
20678
+ *
20679
+ * @param serviceId - The service ID whose cached configuration should be cleared.
20680
+ */
20681
+ function clearServiceConfigCache(serviceId) {
20682
+ try {
20683
+ localStorage.removeItem(serviceConfigStorageKey(serviceId));
20684
+ } catch (_error) {
20685
+ // localStorage may be unavailable; nothing to clear
20686
+ }
20687
+ }
20688
+ /**
20689
+ * Fetches the wallet service configuration, backed by a 1-day `localStorage` cache
20690
+ * keyed on the service ID. Falls back gracefully (no caching) when `localStorage`
20691
+ * is unavailable (e.g. private browsing). Shared by the instance `load()` flow and
20692
+ * the static availability checks so both reuse the same cached fetch.
20693
+ *
20694
+ * @param api - The configured API client (env must already be set).
20695
+ * @param serviceId - The service ID to fetch the configuration for.
20696
+ * @returns The service configuration response.
20697
+ */
20698
+ function getServiceConfigCached(api, serviceId) {
20699
+ return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee() {
20700
+ var storageKey, cachedData, _JSON$parse, data, timestamp, serviceConfigResponse, encodedData;
20701
+ return _regenerator().w(function (_context) {
20702
+ while (1) switch (_context.p = _context.n) {
20703
+ case 0:
20704
+ storageKey = serviceConfigStorageKey(serviceId);
20705
+ _context.p = 1;
20706
+ cachedData = localStorage.getItem(storageKey);
20707
+ if (!cachedData) {
20708
+ _context.n = 5;
20709
+ break;
20710
+ }
20711
+ _context.p = 2;
20712
+ _JSON$parse = JSON.parse(atob(cachedData)), data = _JSON$parse.data, timestamp = _JSON$parse.timestamp;
20713
+ if (!(Date.now() - timestamp < CACHE_TTL_MS)) {
20714
+ _context.n = 3;
20715
+ break;
20716
+ }
20717
+ return _context.a(2, JSON.parse(data));
20718
+ case 3:
20719
+ _context.n = 5;
20720
+ break;
20721
+ case 4:
20722
+ _context.p = 4;
20723
+ _context.v;
20724
+ localStorage.removeItem(storageKey);
20725
+ case 5:
20726
+ _context.n = 7;
20727
+ break;
20728
+ case 6:
20729
+ _context.p = 6;
20730
+ _context.v;
20731
+ case 7:
20732
+ _context.n = 8;
20733
+ return api.service().getConfig(serviceId);
20734
+ case 8:
20735
+ serviceConfigResponse = _context.v;
20736
+ try {
20737
+ encodedData = btoa(JSON.stringify({
20738
+ data: JSON.stringify(serviceConfigResponse),
20739
+ timestamp: Date.now()
20740
+ }));
20741
+ localStorage.setItem(storageKey, encodedData);
20742
+ } catch (_error) {
20743
+ // localStorage may be unavailable; proceed without caching
20744
+ }
20745
+ return _context.a(2, serviceConfigResponse);
20746
+ }
20747
+ }, _callee, null, [[2, 4], [1, 6]]);
20748
+ }));
20749
+ }
20750
+
19135
20751
  /**
19136
20752
  * @classdesc Abstract base class for Open Wallet buttons. **Do not instantiate directly.**
19137
20753
  * Use {@link ApplePayOpenWalletButton} or {@link GooglePayOpenWalletButton} instead.
@@ -19165,10 +20781,30 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19165
20781
  this.api = new ApiInternal(publicKeyOrAccessToken, this.getApiAuthType(publicKeyOrAccessToken));
19166
20782
  this.serviceId = serviceId;
19167
20783
  this.meta = meta;
20784
+ this.registerCacheInvalidationOnError();
19168
20785
  this.validateBaseMeta();
19169
20786
  this.validateWalletMeta();
19170
20787
  }
20788
+ /**
20789
+ * Invalidates the cached service configuration whenever an error or unavailable
20790
+ * event fires, so the next `load()` re-fetches a fresh config. The cache is keyed
20791
+ * on the service ID with a 1-day TTL; clearing it on failure means a config that
20792
+ * changed server-side (and caused the failure) is not reused until the TTL expires.
20793
+ * Registered once per button, independently of any merchant-registered handlers.
20794
+ *
20795
+ * @private
20796
+ */
19171
20797
  return _createClass(OpenWalletButtons, [{
20798
+ key: "registerCacheInvalidationOnError",
20799
+ value: function registerCacheInvalidationOnError() {
20800
+ var _this = this;
20801
+ var invalidate = function invalidate() {
20802
+ return clearServiceConfigCache(_this.serviceId);
20803
+ };
20804
+ this.eventEmitter.subscribe(EVENT$2.ERROR, invalidate);
20805
+ this.eventEmitter.subscribe(EVENT$2.UNAVAILABLE, invalidate);
20806
+ }
20807
+ }, {
19172
20808
  key: "getApiAuthType",
19173
20809
  value: function getApiAuthType(publicKeyOrAccessToken) {
19174
20810
  if (AccessToken.validateJWT(publicKeyOrAccessToken)) {
@@ -19202,57 +20838,7 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19202
20838
  }, {
19203
20839
  key: "getOpenWalletServiceConfig",
19204
20840
  value: function getOpenWalletServiceConfig() {
19205
- return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee() {
19206
- var storageKey, cachedData, _JSON$parse, data, timestamp, oneDay, serviceConfigResponse, encodedData;
19207
- return _regenerator().w(function (_context) {
19208
- while (1) switch (_context.p = _context.n) {
19209
- case 0:
19210
- storageKey = "serviceConfig-".concat(this.serviceId);
19211
- _context.p = 1;
19212
- cachedData = localStorage.getItem(storageKey);
19213
- if (!cachedData) {
19214
- _context.n = 5;
19215
- break;
19216
- }
19217
- _context.p = 2;
19218
- _JSON$parse = JSON.parse(atob(cachedData)), data = _JSON$parse.data, timestamp = _JSON$parse.timestamp;
19219
- oneDay = 86400000;
19220
- if (!(Date.now() - timestamp < oneDay)) {
19221
- _context.n = 3;
19222
- break;
19223
- }
19224
- return _context.a(2, JSON.parse(data));
19225
- case 3:
19226
- _context.n = 5;
19227
- break;
19228
- case 4:
19229
- _context.p = 4;
19230
- _context.v;
19231
- localStorage.removeItem(storageKey);
19232
- case 5:
19233
- _context.n = 7;
19234
- break;
19235
- case 6:
19236
- _context.p = 6;
19237
- _context.v;
19238
- case 7:
19239
- _context.n = 8;
19240
- return this.api.service().getConfig(this.serviceId);
19241
- case 8:
19242
- serviceConfigResponse = _context.v;
19243
- try {
19244
- encodedData = btoa(JSON.stringify({
19245
- data: JSON.stringify(serviceConfigResponse),
19246
- timestamp: Date.now()
19247
- }));
19248
- localStorage.setItem(storageKey, encodedData);
19249
- } catch (_error) {
19250
- // localStorage may be unavailable; proceed without caching
19251
- }
19252
- return _context.a(2, serviceConfigResponse);
19253
- }
19254
- }, _callee, this, [[2, 4], [1, 6]]);
19255
- }));
20841
+ return getServiceConfigCached(this.api, this.serviceId);
19256
20842
  }
19257
20843
  }, {
19258
20844
  key: "handleOnError",
@@ -19287,13 +20873,13 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19287
20873
  }, {
19288
20874
  key: "load",
19289
20875
  value: function load() {
19290
- var _this = this;
20876
+ var _this2 = this;
19291
20877
  this.getOpenWalletServiceConfig().then(function (serviceConfig) {
19292
- _this.validateServiceType(serviceConfig);
19293
- _this.walletService = _this.createWalletService(serviceConfig);
19294
- _this.walletService.load();
20878
+ _this2.validateServiceType(serviceConfig);
20879
+ _this2.walletService = _this2.createWalletService(serviceConfig);
20880
+ _this2.walletService.load();
19295
20881
  })["catch"](function (error) {
19296
- _this.handleOnError(error instanceof Error ? error : new Error(String(error)), ERROR_OPERATION.CONFIGURATION);
20882
+ _this2.handleOnError(error instanceof Error ? error : new Error(String(error)), ERROR_OPERATION.CONFIGURATION);
19297
20883
  });
19298
20884
  }
19299
20885
  /**
@@ -19491,28 +21077,28 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19491
21077
  }, {
19492
21078
  key: "onSuccess",
19493
21079
  value: function onSuccess(handler) {
19494
- var _this2 = this;
21080
+ var _this3 = this;
19495
21081
  if (typeof handler === 'function') {
19496
21082
  this.eventEmitter.subscribe(EVENT$2.SUCCESS, function (data) {
19497
- return __awaiter(_this2, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee2() {
19498
- var _t3;
19499
- return _regenerator().w(function (_context2) {
19500
- while (1) switch (_context2.p = _context2.n) {
21083
+ return __awaiter(_this3, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee() {
21084
+ var _t;
21085
+ return _regenerator().w(function (_context) {
21086
+ while (1) switch (_context.p = _context.n) {
19501
21087
  case 0:
19502
- _context2.p = 0;
19503
- _context2.n = 1;
21088
+ _context.p = 0;
21089
+ _context.n = 1;
19504
21090
  return handler(data);
19505
21091
  case 1:
19506
- _context2.n = 3;
21092
+ _context.n = 3;
19507
21093
  break;
19508
21094
  case 2:
19509
- _context2.p = 2;
19510
- _t3 = _context2.v;
19511
- console.warn('Merchant onSuccess handler threw (swallowed):', _t3);
21095
+ _context.p = 2;
21096
+ _t = _context.v;
21097
+ console.warn('Merchant onSuccess handler threw (swallowed):', _t);
19512
21098
  case 3:
19513
- return _context2.a(2);
21099
+ return _context.a(2);
19514
21100
  }
19515
- }, _callee2, null, [[0, 2]]);
21101
+ }, _callee, null, [[0, 2]]);
19516
21102
  }));
19517
21103
  });
19518
21104
  return;
@@ -19543,28 +21129,28 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19543
21129
  }, {
19544
21130
  key: "onUnavailable",
19545
21131
  value: function onUnavailable(handler) {
19546
- var _this3 = this;
21132
+ var _this4 = this;
19547
21133
  if (typeof handler === 'function') {
19548
21134
  this.eventEmitter.subscribe(EVENT$2.UNAVAILABLE, function (data) {
19549
- return __awaiter(_this3, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee3() {
19550
- var _t4;
19551
- return _regenerator().w(function (_context3) {
19552
- while (1) switch (_context3.p = _context3.n) {
21135
+ return __awaiter(_this4, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee2() {
21136
+ var _t2;
21137
+ return _regenerator().w(function (_context2) {
21138
+ while (1) switch (_context2.p = _context2.n) {
19553
21139
  case 0:
19554
- _context3.p = 0;
19555
- _context3.n = 1;
21140
+ _context2.p = 0;
21141
+ _context2.n = 1;
19556
21142
  return handler(data);
19557
21143
  case 1:
19558
- _context3.n = 3;
21144
+ _context2.n = 3;
19559
21145
  break;
19560
21146
  case 2:
19561
- _context3.p = 2;
19562
- _t4 = _context3.v;
19563
- console.warn('Merchant onUnavailable handler threw (swallowed):', _t4);
21147
+ _context2.p = 2;
21148
+ _t2 = _context2.v;
21149
+ console.warn('Merchant onUnavailable handler threw (swallowed):', _t2);
19564
21150
  case 3:
19565
- return _context3.a(2);
21151
+ return _context2.a(2);
19566
21152
  }
19567
- }, _callee3, null, [[0, 2]]);
21153
+ }, _callee2, null, [[0, 2]]);
19568
21154
  }));
19569
21155
  });
19570
21156
  return;
@@ -19596,28 +21182,28 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19596
21182
  }, {
19597
21183
  key: "onError",
19598
21184
  value: function onError(handler) {
19599
- var _this4 = this;
21185
+ var _this5 = this;
19600
21186
  if (typeof handler === 'function') {
19601
21187
  this.eventEmitter.subscribe(EVENT$2.ERROR, function (data) {
19602
- return __awaiter(_this4, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee4() {
19603
- var _t5;
19604
- return _regenerator().w(function (_context4) {
19605
- while (1) switch (_context4.p = _context4.n) {
21188
+ return __awaiter(_this5, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee3() {
21189
+ var _t3;
21190
+ return _regenerator().w(function (_context3) {
21191
+ while (1) switch (_context3.p = _context3.n) {
19606
21192
  case 0:
19607
- _context4.p = 0;
19608
- _context4.n = 1;
21193
+ _context3.p = 0;
21194
+ _context3.n = 1;
19609
21195
  return handler(data);
19610
21196
  case 1:
19611
- _context4.n = 3;
21197
+ _context3.n = 3;
19612
21198
  break;
19613
21199
  case 2:
19614
- _context4.p = 2;
19615
- _t5 = _context4.v;
19616
- console.warn('Merchant onError handler threw (swallowed):', _t5);
21200
+ _context3.p = 2;
21201
+ _t3 = _context3.v;
21202
+ console.warn('Merchant onError handler threw (swallowed):', _t3);
19617
21203
  case 3:
19618
- return _context4.a(2);
21204
+ return _context3.a(2);
19619
21205
  }
19620
- }, _callee4, null, [[0, 2]]);
21206
+ }, _callee3, null, [[0, 2]]);
19621
21207
  }));
19622
21208
  });
19623
21209
  return;
@@ -19648,28 +21234,28 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19648
21234
  }, {
19649
21235
  key: "onCancel",
19650
21236
  value: function onCancel(handler) {
19651
- var _this5 = this;
21237
+ var _this6 = this;
19652
21238
  if (typeof handler === 'function') {
19653
21239
  this.eventEmitter.subscribe(EVENT$2.CHECKOUT_CLOSE, function (data) {
19654
- return __awaiter(_this5, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee5() {
19655
- var _t6;
19656
- return _regenerator().w(function (_context5) {
19657
- while (1) switch (_context5.p = _context5.n) {
21240
+ return __awaiter(_this6, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee4() {
21241
+ var _t4;
21242
+ return _regenerator().w(function (_context4) {
21243
+ while (1) switch (_context4.p = _context4.n) {
19658
21244
  case 0:
19659
- _context5.p = 0;
19660
- _context5.n = 1;
21245
+ _context4.p = 0;
21246
+ _context4.n = 1;
19661
21247
  return handler(data);
19662
21248
  case 1:
19663
- _context5.n = 3;
21249
+ _context4.n = 3;
19664
21250
  break;
19665
21251
  case 2:
19666
- _context5.p = 2;
19667
- _t6 = _context5.v;
19668
- console.warn('Merchant onCancel handler threw (swallowed):', _t6);
21252
+ _context4.p = 2;
21253
+ _t4 = _context4.v;
21254
+ console.warn('Merchant onCancel handler threw (swallowed):', _t4);
19669
21255
  case 3:
19670
- return _context5.a(2);
21256
+ return _context4.a(2);
19671
21257
  }
19672
- }, _callee5, null, [[0, 2]]);
21258
+ }, _callee4, null, [[0, 2]]);
19673
21259
  }));
19674
21260
  });
19675
21261
  return;
@@ -19693,28 +21279,28 @@ var OpenWalletButtons = /*#__PURE__*/function () {
19693
21279
  }, {
19694
21280
  key: "onLoaded",
19695
21281
  value: function onLoaded(handler) {
19696
- var _this6 = this;
21282
+ var _this7 = this;
19697
21283
  if (typeof handler === 'function') {
19698
21284
  this.eventEmitter.subscribe(EVENT$2.LOADED, function (data) {
19699
- return __awaiter(_this6, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee6() {
19700
- var _t7;
19701
- return _regenerator().w(function (_context6) {
19702
- while (1) switch (_context6.p = _context6.n) {
21285
+ return __awaiter(_this7, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee5() {
21286
+ var _t5;
21287
+ return _regenerator().w(function (_context5) {
21288
+ while (1) switch (_context5.p = _context5.n) {
19703
21289
  case 0:
19704
- _context6.p = 0;
19705
- _context6.n = 1;
21290
+ _context5.p = 0;
21291
+ _context5.n = 1;
19706
21292
  return handler(data);
19707
21293
  case 1:
19708
- _context6.n = 3;
21294
+ _context5.n = 3;
19709
21295
  break;
19710
21296
  case 2:
19711
- _context6.p = 2;
19712
- _t7 = _context6.v;
19713
- console.warn('Merchant onLoaded handler threw (swallowed):', _t7);
21297
+ _context5.p = 2;
21298
+ _t5 = _context5.v;
21299
+ console.warn('Merchant onLoaded handler threw (swallowed):', _t5);
19714
21300
  case 3:
19715
- return _context6.a(2);
21301
+ return _context5.a(2);
19716
21302
  }
19717
- }, _callee6, null, [[0, 2]]);
21303
+ }, _callee5, null, [[0, 2]]);
19718
21304
  }));
19719
21305
  });
19720
21306
  return;
@@ -20062,7 +21648,143 @@ var WALLET_TYPES;
20062
21648
  WALLET_TYPES["GOOGLE_PAY"] = "google";
20063
21649
  })(WALLET_TYPES || (WALLET_TYPES = {}));
20064
21650
 
20065
- var DEFAULT_APPLE_PAY_CAPABILITIES$1 = ['paymentCredentialsAvailable'];
21651
+ /** CDN location of Apple's web SDK (provides the `<apple-pay-button>` web component). */
21652
+ var APPLE_PAY_SDK_URL = 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js';
21653
+ /**
21654
+ * Statuses that count as available when the merchant does not override them.
21655
+ * Includes `paymentCredentialStatusUnknown` so the button is shown in third-party /
21656
+ * cross-device (QR) browsers, per Apple's guidance.
21657
+ */
21658
+ var DEFAULT_APPLE_PAY_CAPABILITIES$1 = ['paymentCredentialsAvailable', 'paymentCredentialStatusUnknown'];
21659
+ /**
21660
+ * Maps the SDK's friendly capability tokens to Apple's `PaymentCredentialStatus` values.
21661
+ * Unknown tokens are passed through unchanged.
21662
+ */
21663
+ function formatCapabilities(capabilities) {
21664
+ var fieldMap = {
21665
+ credentials_available: 'paymentCredentialsAvailable',
21666
+ credentials_status_unknown: 'paymentCredentialStatusUnknown',
21667
+ credentials_unavailable: 'paymentCredentialsUnavailable'
21668
+ };
21669
+ return capabilities.map(function (capability) {
21670
+ return fieldMap[capability] || capability;
21671
+ });
21672
+ }
21673
+ /**
21674
+ * Synchronous, network-free check for a **native, same-device** Apple Pay session
21675
+ * (`window.ApplePaySession` exists and `canMakePayments()` is `true`). Safe to call in
21676
+ * any environment (never throws).
21677
+ *
21678
+ * Note: this is a legacy/Safari-oriented signal. It is **not** authoritative for
21679
+ * cross-browser / cross-device (QR) availability — in Chrome/Edge/Firefox
21680
+ * `window.ApplePaySession` is supplied by the CDN SDK and may be absent until it loads,
21681
+ * and the cross-device flow is reflected by `applePayCapabilities()`, not this call.
21682
+ * Use {@link evaluateApplePayAvailability} for the authoritative, cross-browser answer.
21683
+ */
21684
+ function hasNativeApplePaySession() {
21685
+ try {
21686
+ return typeof window !== 'undefined' && !!window.ApplePaySession && typeof ApplePaySession !== 'undefined' && ApplePaySession.canMakePayments();
21687
+ } catch (_a) {
21688
+ return false;
21689
+ }
21690
+ }
21691
+ var sdkLoadPromise = null;
21692
+ /**
21693
+ * Idempotently loads Apple's web SDK from the CDN.
21694
+ * Resolves immediately if the `<apple-pay-button>` custom element is already
21695
+ * registered, and de-duplicates concurrent loads. Rejects if the script fails to load.
21696
+ */
21697
+ function loadApplePaySdk() {
21698
+ if (typeof customElements !== 'undefined' && customElements.get('apple-pay-button')) {
21699
+ return Promise.resolve();
21700
+ }
21701
+ if (sdkLoadPromise) {
21702
+ return sdkLoadPromise;
21703
+ }
21704
+ sdkLoadPromise = new Promise(function (resolve, reject) {
21705
+ var script = document.createElement('script');
21706
+ script.src = APPLE_PAY_SDK_URL;
21707
+ script.type = 'text/javascript';
21708
+ script.crossOrigin = 'anonymous';
21709
+ script.onload = function () {
21710
+ return resolve();
21711
+ };
21712
+ script.onerror = function () {
21713
+ // Allow a future retry after a transient failure.
21714
+ sdkLoadPromise = null;
21715
+ reject(new Error('Failed to load Apple Pay script'));
21716
+ };
21717
+ document.head.appendChild(script);
21718
+ });
21719
+ return sdkLoadPromise;
21720
+ }
21721
+ /**
21722
+ * Core Apple Pay availability evaluation, shared by the instance flow
21723
+ * (`ApplePayOpenWalletService`) and the static `ApplePayOpenWalletButton.checkAvailability`.
21724
+ *
21725
+ * Assumes the Apple Pay SDK is already loaded (call {@link loadApplePaySdk} first).
21726
+ */
21727
+ function evaluateApplePayAvailability(params) {
21728
+ return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee() {
21729
+ var _a, hasNativeSession, allowed, capabilities, paymentCredentialStatus, available;
21730
+ return _regenerator().w(function (_context) {
21731
+ while (1) switch (_context.p = _context.n) {
21732
+ case 0:
21733
+ if (!(typeof window === 'undefined' || !window.ApplePaySession)) {
21734
+ _context.n = 1;
21735
+ break;
21736
+ }
21737
+ return _context.a(2, {
21738
+ available: false,
21739
+ supported: false,
21740
+ has_native_session: false,
21741
+ reason: 'unsupported_browser'
21742
+ });
21743
+ case 1:
21744
+ hasNativeSession = hasNativeApplePaySession();
21745
+ allowed = ((_a = params.apple_pay_capabilities) === null || _a === void 0 ? void 0 : _a.length) ? formatCapabilities(params.apple_pay_capabilities) : DEFAULT_APPLE_PAY_CAPABILITIES$1;
21746
+ _context.p = 2;
21747
+ _context.n = 3;
21748
+ return ApplePaySession.applePayCapabilities(params.merchantId);
21749
+ case 3:
21750
+ capabilities = _context.v;
21751
+ paymentCredentialStatus = capabilities === null || capabilities === void 0 ? void 0 : capabilities.paymentCredentialStatus; // `applePayCapabilities` is authoritative: trust an explicit `applePayUnsupported`
21752
+ // (e.g. an unregistered/misconfigured merchant) rather than overriding it with the
21753
+ // coarse `canMakePayments()` signal.
21754
+ if (!(paymentCredentialStatus === 'applePayUnsupported')) {
21755
+ _context.n = 4;
21756
+ break;
21757
+ }
21758
+ return _context.a(2, {
21759
+ available: false,
21760
+ supported: false,
21761
+ has_native_session: hasNativeSession,
21762
+ payment_credential_status: paymentCredentialStatus,
21763
+ reason: 'unsupported_browser'
21764
+ });
21765
+ case 4:
21766
+ available = allowed.includes(paymentCredentialStatus);
21767
+ return _context.a(2, {
21768
+ available: available,
21769
+ supported: true,
21770
+ has_native_session: hasNativeSession,
21771
+ payment_credential_status: paymentCredentialStatus,
21772
+ reason: available ? undefined : 'no_credentials'
21773
+ });
21774
+ case 5:
21775
+ _context.p = 5;
21776
+ _context.v;
21777
+ return _context.a(2, {
21778
+ available: false,
21779
+ supported: hasNativeSession,
21780
+ has_native_session: hasNativeSession,
21781
+ reason: 'query_failed'
21782
+ });
21783
+ }
21784
+ }, _callee, null, [[2, 5]]);
21785
+ }));
21786
+ }
21787
+
20066
21788
  /**
20067
21789
  * Apple Pay implementation of the OpenWallet service.
20068
21790
  *
@@ -20338,7 +22060,6 @@ var ApplePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
20338
22060
  value: function load() {
20339
22061
  return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee3() {
20340
22062
  var _this4 = this;
20341
- var applePayScript;
20342
22063
  return _regenerator().w(function (_context3) {
20343
22064
  while (1) switch (_context3.n) {
20344
22065
  case 0:
@@ -20349,26 +22070,12 @@ var ApplePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
20349
22070
  this.handleOnUnavailable('Promises not supported');
20350
22071
  return _context3.a(2);
20351
22072
  case 1:
20352
- if (!(customElements === null || customElements === void 0 ? void 0 : customElements.get('apple-pay-button'))) {
20353
- _context3.n = 2;
20354
- break;
20355
- }
20356
- this.onScriptLoaded();
20357
- return _context3.a(2);
20358
- case 2:
20359
- applePayScript = document.createElement('script');
20360
- applePayScript.src = 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js';
20361
- applePayScript.type = 'text/javascript';
20362
- applePayScript.crossOrigin = 'anonymous';
20363
- applePayScript.onload = function () {
22073
+ loadApplePaySdk().then(function () {
20364
22074
  _this4.onScriptLoaded();
20365
- };
20366
- applePayScript.onerror = function () {
22075
+ })["catch"](function () {
20367
22076
  _this4.handleOnUnavailable('Failed to load Apple Pay script');
20368
- return;
20369
- };
20370
- document.head.appendChild(applePayScript);
20371
- case 3:
22077
+ });
22078
+ case 2:
20372
22079
  return _context3.a(2);
20373
22080
  }
20374
22081
  }, _callee3, this);
@@ -20396,26 +22103,22 @@ var ApplePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
20396
22103
  }, {
20397
22104
  key: "checkAvailability",
20398
22105
  value: function checkAvailability() {
20399
- var _this6 = this;
20400
- return new Promise(function (resolve) {
20401
- var _a;
20402
- if (!window.ApplePaySession || !ApplePaySession) {
20403
- return resolve(false);
20404
- }
20405
- var formattedCapabilities = DEFAULT_APPLE_PAY_CAPABILITIES$1;
20406
- if (((_a = _this6.meta.apple_pay_capabilities) === null || _a === void 0 ? void 0 : _a.length) > 0) {
20407
- formattedCapabilities = _this6.formatCapabilities(_this6.meta.apple_pay_capabilities);
20408
- }
20409
- ApplePaySession.applePayCapabilities(_this6.getMerchantId()).then(function (capabilities) {
20410
- if (_this6.isUnsuportedBrowser(capabilities)) {
20411
- return resolve(ApplePaySession.canMakePayments());
22106
+ return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee4() {
22107
+ var result;
22108
+ return _regenerator().w(function (_context4) {
22109
+ while (1) switch (_context4.n) {
22110
+ case 0:
22111
+ _context4.n = 1;
22112
+ return evaluateApplePayAvailability({
22113
+ merchantId: this.getMerchantId(),
22114
+ apple_pay_capabilities: this.meta.apple_pay_capabilities
22115
+ });
22116
+ case 1:
22117
+ result = _context4.v;
22118
+ return _context4.a(2, result.available);
20412
22119
  }
20413
- var allowedCapability = formattedCapabilities.includes(capabilities === null || capabilities === void 0 ? void 0 : capabilities.paymentCredentialStatus);
20414
- return resolve(allowedCapability);
20415
- })["catch"](function () {
20416
- resolve(false);
20417
- });
20418
- });
22120
+ }, _callee4, this);
22121
+ }));
20419
22122
  }
20420
22123
  }, {
20421
22124
  key: "mount",
@@ -20441,7 +22144,7 @@ var ApplePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
20441
22144
  }, {
20442
22145
  key: "onApplePayButtonClicked",
20443
22146
  value: function onApplePayButtonClicked() {
20444
- var _this7 = this;
22147
+ var _this6 = this;
20445
22148
  if (this.isButtonDisabled()) {
20446
22149
  return;
20447
22150
  }
@@ -20460,26 +22163,27 @@ var ApplePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
20460
22163
  // As part of PAYCA-2065 we detect that the paymentSession.onvalidatemerchant is not
20461
22164
  // executed if the browser is not safari. In that case, we handle it outside the
20462
22165
  // actual merchant validation function.
20463
- var isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent);
20464
- var isDesktopRequestIOS = navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent);
22166
+ var userAgent = navigator.userAgent || '';
22167
+ var isIOS = userAgent.includes('iPhone') || userAgent.includes('iPad') || userAgent.includes('iPod');
22168
+ var isDesktopRequestIOS = navigator.maxTouchPoints > 1 && userAgent.includes('Macintosh');
20465
22169
  if (!window.safari && !isIOS && !isDesktopRequestIOS) {
20466
22170
  this.handleMerchantOnButtonClickEvent()["catch"](function (err) {
20467
22171
  console.error('Error while processing merchant callback', err);
20468
- _this7.paymentSession.abort();
22172
+ _this6.paymentSession.abort();
20469
22173
  });
20470
22174
  }
20471
22175
  }
20472
22176
  }, {
20473
22177
  key: "getMerchantSession",
20474
22178
  value: function getMerchantSession() {
20475
- var _this8 = this;
22179
+ var _this7 = this;
20476
22180
  return new Promise(function (resolve, reject) {
20477
- return _this8.api.paymentSource().createSession(_extends({
20478
- service_id: _this8.serviceId,
22181
+ return _this7.api.paymentSource().createSession(_extends({
22182
+ service_id: _this7.serviceId,
20479
22183
  session_id: window.location.hostname,
20480
- store_name: _this8.meta.store_name
20481
- }, _this8.isShippingRequired() && {
20482
- request_shipping: _this8.meta.request_shipping
22184
+ store_name: _this7.meta.store_name
22185
+ }, _this7.isShippingRequired() && {
22186
+ request_shipping: _this7.meta.request_shipping
20483
22187
  })).then(function (res) {
20484
22188
  resolve(res);
20485
22189
  })["catch"](function (err) {
@@ -20490,42 +22194,30 @@ var ApplePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
20490
22194
  }, {
20491
22195
  key: "handleMerchantOnButtonClickEvent",
20492
22196
  value: function handleMerchantOnButtonClickEvent() {
20493
- return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee4() {
22197
+ return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee5() {
20494
22198
  var results;
20495
- return _regenerator().w(function (_context4) {
20496
- while (1) switch (_context4.n) {
22199
+ return _regenerator().w(function (_context5) {
22200
+ while (1) switch (_context5.n) {
20497
22201
  case 0:
20498
- _context4.n = 1;
22202
+ _context5.n = 1;
20499
22203
  return this.eventEmitter.emitWithResult(EVENT$2.ON_CLICK, {
20500
22204
  event: EVENT$2.ON_CLICK
20501
22205
  });
20502
22206
  case 1:
20503
- results = _context4.v;
22207
+ results = _context5.v;
20504
22208
  if (!results.some(function (r) {
20505
22209
  return r === false;
20506
22210
  })) {
20507
- _context4.n = 2;
22211
+ _context5.n = 2;
20508
22212
  break;
20509
22213
  }
20510
22214
  throw new Error('onClick handler aborted the payment flow');
20511
22215
  case 2:
20512
- return _context4.a(2);
22216
+ return _context5.a(2);
20513
22217
  }
20514
- }, _callee4, this);
22218
+ }, _callee5, this);
20515
22219
  }));
20516
22220
  }
20517
- }, {
20518
- key: "formatCapabilities",
20519
- value: function formatCapabilities(capabilities) {
20520
- var fieldMap = {
20521
- credentials_available: 'paymentCredentialsAvailable',
20522
- credentials_status_unknown: 'paymentCredentialStatusUnknown',
20523
- credentials_unavailable: 'paymentCredentialsUnavailable'
20524
- };
20525
- return capabilities.map(function (capability) {
20526
- return fieldMap[capability] || capability;
20527
- });
20528
- }
20529
22221
  }, {
20530
22222
  key: "formatErrorFields",
20531
22223
  value: function formatErrorFields(error) {
@@ -20692,11 +22384,6 @@ var ApplePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
20692
22384
  applePayLaterAvailability: this.meta.apple_pay_later_availability
20693
22385
  });
20694
22386
  }
20695
- }, {
20696
- key: "isUnsuportedBrowser",
20697
- value: function isUnsuportedBrowser(capabilities) {
20698
- return (capabilities === null || capabilities === void 0 ? void 0 : capabilities.paymentCredentialStatus) === 'applePayUnsupported';
20699
- }
20700
22387
  }]);
20701
22388
  }(OpenWalletService);
20702
22389
 
@@ -20745,17 +22432,57 @@ var ApplePayOpenWalletButton = /*#__PURE__*/function (_OpenWalletButtons) {
20745
22432
  return _this;
20746
22433
  }
20747
22434
  /**
20748
- * Validates Apple Pay-specific required metadata fields.
20749
- * Apple Pay requires `amount_label` and `store_name` to be present and strings.
22435
+ * Checks Apple Pay availability **without instantiating the button** — the single,
22436
+ * authoritative, cross-browser-aware availability entry point.
20750
22437
  *
20751
- * @private
20752
- * @throws {Error} If `amount_label` is missing or not a string.
20753
- * @throws {Error} If `store_name` is missing or not a string.
22438
+ * Resolves the merchant identifier (either supplied directly or fetched from the
22439
+ * service configuration), loads Apple's web SDK if needed, then queries
22440
+ * `ApplePaySession.applePayCapabilities()`. Returns a convenience `available` verdict
22441
+ * (computed against `apple_pay_capabilities`), the raw device-reported
22442
+ * `payment_credential_status`, and `has_native_session` so callers can infer the transport:
22443
+ * `available && has_native_session` ⇒ native sheet likely; `available && !has_native_session`
22444
+ * ⇒ cross-device / QR handoff likely.
22445
+ *
22446
+ * Pass `{ sync: true }` for a cheap, network-free native same-device pre-check that
22447
+ * resolves immediately (see {@link ApplePayAvailabilityOptions.sync}).
22448
+ *
22449
+ * @param {ApplePayAvailabilityOptions} [options] - Merchant context, allowed capabilities, and mode.
22450
+ * @returns {Promise<ApplePayAvailabilityResult>} The availability result.
22451
+ *
22452
+ * @example
22453
+ * // Lightest path — no network, when you already know the merchant identifier:
22454
+ * const result = await ApplePayOpenWalletButton.checkAvailability({ merchant_id });
22455
+ *
22456
+ * @example
22457
+ * // Resolve the merchant identifier from the service configuration:
22458
+ * const result = await ApplePayOpenWalletButton.checkAvailability({
22459
+ * public_key_or_access_token,
22460
+ * service_id,
22461
+ * env: 'sandbox',
22462
+ * });
22463
+ * if (result.available) {
22464
+ * // Render the Apple Pay button (native sheet or cross-device QR, per has_native_session).
22465
+ * } else {
22466
+ * console.log('Unavailable:', result.reason, result.payment_credential_status);
22467
+ * }
22468
+ *
22469
+ * @example
22470
+ * // Synchronous native-only fast check (no network / SDK load):
22471
+ * const { has_native_session } = await ApplePayOpenWalletButton.checkAvailability({ sync: true });
20754
22472
  */
20755
22473
  _inherits(ApplePayOpenWalletButton, _OpenWalletButtons);
20756
22474
  return _createClass(ApplePayOpenWalletButton, [{
20757
22475
  key: "validateWalletMeta",
20758
- value: function validateWalletMeta() {
22476
+ value:
22477
+ /**
22478
+ * Validates Apple Pay-specific required metadata fields.
22479
+ * Apple Pay requires `amount_label` and `store_name` to be present and strings.
22480
+ *
22481
+ * @private
22482
+ * @throws {Error} If `amount_label` is missing or not a string.
22483
+ * @throws {Error} If `store_name` is missing or not a string.
22484
+ */
22485
+ function validateWalletMeta() {
20759
22486
  if (typeof this.meta.amount_label !== 'string' || this.meta.amount_label.trim() === '') {
20760
22487
  throw new Error("meta.amount_label is required for Apple Pay and must be a non-empty string (e.g. 'TOTAL').");
20761
22488
  }
@@ -20789,6 +22516,114 @@ var ApplePayOpenWalletButton = /*#__PURE__*/function (_OpenWalletButtons) {
20789
22516
  value: function createWalletService(serviceConfig) {
20790
22517
  return new ApplePayOpenWalletService(this.api, this.eventEmitter, this.serviceId, this.container, this.meta, serviceConfig);
20791
22518
  }
22519
+ }], [{
22520
+ key: "checkAvailability",
22521
+ value: function checkAvailability() {
22522
+ return __awaiter(this, arguments, void 0, function () {
22523
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
22524
+ return /*#__PURE__*/_regenerator().m(function _callee() {
22525
+ var hasNativeSession, merchantId, authType, api, serviceConfig;
22526
+ return _regenerator().w(function (_context) {
22527
+ while (1) switch (_context.p = _context.n) {
22528
+ case 0:
22529
+ hasNativeSession = hasNativeApplePaySession();
22530
+ if (!options.sync) {
22531
+ _context.n = 1;
22532
+ break;
22533
+ }
22534
+ return _context.a(2, {
22535
+ available: hasNativeSession,
22536
+ supported: hasNativeSession,
22537
+ has_native_session: hasNativeSession,
22538
+ reason: hasNativeSession ? undefined : 'no_native_session'
22539
+ });
22540
+ case 1:
22541
+ merchantId = options.merchant_id;
22542
+ if (merchantId) {
22543
+ _context.n = 6;
22544
+ break;
22545
+ }
22546
+ if (!(!options.public_key_or_access_token || !options.service_id)) {
22547
+ _context.n = 2;
22548
+ break;
22549
+ }
22550
+ return _context.a(2, {
22551
+ available: false,
22552
+ supported: hasNativeSession,
22553
+ has_native_session: hasNativeSession,
22554
+ reason: 'missing_merchant_id'
22555
+ });
22556
+ case 2:
22557
+ _context.p = 2;
22558
+ authType = AccessToken.validateJWT(options.public_key_or_access_token) ? API_AUTH_TYPE.TOKEN : API_AUTH_TYPE.PUBLIC_KEY;
22559
+ api = new ApiInternal(options.public_key_or_access_token, authType);
22560
+ if (options.env) {
22561
+ api.setEnv(options.env);
22562
+ }
22563
+ _context.n = 3;
22564
+ return getServiceConfigCached(api, options.service_id);
22565
+ case 3:
22566
+ serviceConfig = _context.v;
22567
+ if (!(serviceConfig.type !== SERVICE_TYPES.APPLE_PAY)) {
22568
+ _context.n = 4;
22569
+ break;
22570
+ }
22571
+ return _context.a(2, {
22572
+ available: false,
22573
+ supported: hasNativeSession,
22574
+ has_native_session: hasNativeSession,
22575
+ reason: 'service_type_mismatch'
22576
+ });
22577
+ case 4:
22578
+ merchantId = serviceConfig.merchant;
22579
+ _context.n = 6;
22580
+ break;
22581
+ case 5:
22582
+ _context.p = 5;
22583
+ _context.v;
22584
+ return _context.a(2, {
22585
+ available: false,
22586
+ supported: hasNativeSession,
22587
+ has_native_session: hasNativeSession,
22588
+ reason: 'config_fetch_failed'
22589
+ });
22590
+ case 6:
22591
+ if (merchantId) {
22592
+ _context.n = 7;
22593
+ break;
22594
+ }
22595
+ return _context.a(2, {
22596
+ available: false,
22597
+ supported: hasNativeSession,
22598
+ has_native_session: hasNativeSession,
22599
+ reason: 'missing_merchant_id'
22600
+ });
22601
+ case 7:
22602
+ _context.p = 7;
22603
+ _context.n = 8;
22604
+ return loadApplePaySdk();
22605
+ case 8:
22606
+ _context.n = 10;
22607
+ break;
22608
+ case 9:
22609
+ _context.p = 9;
22610
+ _context.v;
22611
+ return _context.a(2, {
22612
+ available: false,
22613
+ supported: hasNativeSession,
22614
+ has_native_session: hasNativeSession,
22615
+ reason: 'sdk_load_failed'
22616
+ });
22617
+ case 10:
22618
+ return _context.a(2, evaluateApplePayAvailability({
22619
+ merchantId: merchantId,
22620
+ apple_pay_capabilities: options.apple_pay_capabilities
22621
+ }));
22622
+ }
22623
+ }, _callee, null, [[7, 9], [2, 5]]);
22624
+ })();
22625
+ });
22626
+ }
20792
22627
  }]);
20793
22628
  }(OpenWalletButtons);
20794
22629
 
@@ -20825,6 +22660,137 @@ var GOOGLE_PAY_CARD_NETWORKS = ['AMEX', 'DISCOVER', 'INTERAC', 'JCB', 'MASTERCAR
20825
22660
  SHIPPING_OPTION: EVENT$2.ON_SHIPPING_OPTIONS_CHANGE
20826
22661
  });
20827
22662
 
22663
+ /**
22664
+ * Synchronous, network-free check for whether the Google Pay JS API is already present
22665
+ * in the page (`window.google.payments.api.PaymentsClient`). Safe to call in any
22666
+ * environment (never throws).
22667
+ *
22668
+ * Note: Google Pay has no synchronous device/card capability API — this only reports
22669
+ * whether the SDK script has loaded. The authoritative readiness signal comes from
22670
+ * {@link evaluateGooglePayAvailability} (which runs `isReadyToPay`).
22671
+ */
22672
+ function isGooglePayApiPresent() {
22673
+ var _a, _b, _c;
22674
+ try {
22675
+ return typeof window !== 'undefined' && !!((_c = (_b = (_a = window.google) === null || _a === void 0 ? void 0 : _a.payments) === null || _b === void 0 ? void 0 : _b.api) === null || _c === void 0 ? void 0 : _c.PaymentsClient);
22676
+ } catch (_d) {
22677
+ return false;
22678
+ }
22679
+ }
22680
+ var scriptLoadPromise = null;
22681
+ /**
22682
+ * Idempotently loads the Google Pay JS SDK from the CDN.
22683
+ * Resolves immediately if the API is already present, de-duplicates concurrent loads,
22684
+ * and rejects if the script fails to load.
22685
+ */
22686
+ function loadGooglePayScript() {
22687
+ if (isGooglePayApiPresent()) {
22688
+ return Promise.resolve();
22689
+ }
22690
+ if (scriptLoadPromise) {
22691
+ return scriptLoadPromise;
22692
+ }
22693
+ scriptLoadPromise = new Promise(function (resolve, reject) {
22694
+ var script = document.createElement('script');
22695
+ script.type = 'text/javascript';
22696
+ script.src = GOOGLE_PAY_SCRIPT_URL;
22697
+ script.async = true;
22698
+ script.onload = function () {
22699
+ if (!isGooglePayApiPresent()) {
22700
+ scriptLoadPromise = null;
22701
+ reject(new Error('Google Pay not available'));
22702
+ return;
22703
+ }
22704
+ resolve();
22705
+ };
22706
+ script.onerror = function () {
22707
+ // Allow a future retry after a transient failure.
22708
+ scriptLoadPromise = null;
22709
+ reject(new Error('Failed to load Google Pay script'));
22710
+ };
22711
+ document.head.appendChild(script);
22712
+ });
22713
+ return scriptLoadPromise;
22714
+ }
22715
+ /**
22716
+ * Builds the base `isReadyToPay` request using the default card configuration
22717
+ * (standard auth methods + card networks). Mirrors the instance service's request:
22718
+ * `existingPaymentMethodRequired` is `false` on Safari (where Google Pay surfaces a
22719
+ * different flow) and `true` elsewhere.
22720
+ */
22721
+ function buildIsReadyToPayRequest() {
22722
+ var isSafariBrowser = Browser.getBrowserName() === 'Apple Safari';
22723
+ return _extends(_extends({}, GOOGLE_PAY_API_CONFIG), {
22724
+ allowedPaymentMethods: [{
22725
+ type: 'CARD',
22726
+ parameters: {
22727
+ allowedAuthMethods: GOOGLE_PAY_AUTH_METHODS,
22728
+ allowedCardNetworks: GOOGLE_PAY_CARD_NETWORKS
22729
+ }
22730
+ }],
22731
+ existingPaymentMethodRequired: !isSafariBrowser
22732
+ });
22733
+ }
22734
+ /**
22735
+ * Core Google Pay availability evaluation, shared by the instance flow
22736
+ * (`GooglePayOpenWalletService`) and the static `GooglePayOpenWalletButton.checkAvailability`.
22737
+ *
22738
+ * The static check passes no `request`, so it uses the **default** card configuration
22739
+ * (a button instance with a custom `card_config` may produce a different result). The
22740
+ * instance flow passes its own `paymentsClient` and `request` so the readiness check
22741
+ * honours the merchant's `card_config` and runs against the same client it pays with.
22742
+ *
22743
+ * Assumes the Google Pay SDK is already loaded (call {@link loadGooglePayScript} first).
22744
+ */
22745
+ function evaluateGooglePayAvailability() {
22746
+ return __awaiter(this, arguments, void 0, function () {
22747
+ var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
22748
+ return /*#__PURE__*/_regenerator().m(function _callee() {
22749
+ var _a, _b, paymentsClient, response, available;
22750
+ return _regenerator().w(function (_context) {
22751
+ while (1) switch (_context.p = _context.n) {
22752
+ case 0:
22753
+ if (isGooglePayApiPresent()) {
22754
+ _context.n = 1;
22755
+ break;
22756
+ }
22757
+ return _context.a(2, {
22758
+ available: false,
22759
+ supported: false,
22760
+ reason: 'unsupported_browser'
22761
+ });
22762
+ case 1:
22763
+ _context.p = 1;
22764
+ paymentsClient = (_a = params.paymentsClient) !== null && _a !== void 0 ? _a : new google.payments.api.PaymentsClient({
22765
+ merchantInfo: {
22766
+ merchantId: params.merchantId || ''
22767
+ }
22768
+ });
22769
+ _context.n = 2;
22770
+ return paymentsClient.isReadyToPay((_b = params.request) !== null && _b !== void 0 ? _b : buildIsReadyToPayRequest());
22771
+ case 2:
22772
+ response = _context.v;
22773
+ available = !!response.result;
22774
+ return _context.a(2, {
22775
+ available: available,
22776
+ supported: true,
22777
+ payment_method_present: response.paymentMethodPresent,
22778
+ reason: available ? undefined : 'no_payment_method'
22779
+ });
22780
+ case 3:
22781
+ _context.p = 3;
22782
+ _context.v;
22783
+ return _context.a(2, {
22784
+ available: false,
22785
+ supported: isGooglePayApiPresent(),
22786
+ reason: 'query_failed'
22787
+ });
22788
+ }
22789
+ }, _callee, null, [[1, 3]]);
22790
+ })();
22791
+ });
22792
+ }
22793
+
20828
22794
  /**
20829
22795
  * Parses intermediate payment data from Google Pay into a normalized format
20830
22796
  * for use in shipping address/option change events.
@@ -21160,14 +23126,18 @@ var GooglePayOpenWalletService = /*#__PURE__*/function (_OpenWalletService) {
21160
23126
  key: "checkAvailability",
21161
23127
  value: function checkAvailability() {
21162
23128
  return __awaiter(this, void 0, void 0, /*#__PURE__*/_regenerator().m(function _callee2() {
23129
+ var result;
21163
23130
  return _regenerator().w(function (_context2) {
21164
23131
  while (1) switch (_context2.n) {
21165
23132
  case 0:
21166
- return _context2.a(2, this.paymentsClient.isReadyToPay(this.createRequest()).then(function (response) {
21167
- return !!response.result;
21168
- })["catch"](function () {
21169
- return false;
21170
- }));
23133
+ _context2.n = 1;
23134
+ return evaluateGooglePayAvailability({
23135
+ paymentsClient: this.paymentsClient,
23136
+ request: this.createRequest()
23137
+ });
23138
+ case 1:
23139
+ result = _context2.v;
23140
+ return _context2.a(2, result.available);
21171
23141
  }
21172
23142
  }, _callee2, this);
21173
23143
  }));
@@ -21625,14 +23595,46 @@ var GooglePayOpenWalletButton = /*#__PURE__*/function (_OpenWalletButtons) {
21625
23595
  return _this;
21626
23596
  }
21627
23597
  /**
21628
- * Validates Google Pay-specific required metadata fields.
21629
- * Google Pay has no additional required fields beyond the base (amount, currency, country).
21630
- * @private
23598
+ * Checks Google Pay availability **without instantiating the button** — the static
23599
+ * availability entry point.
23600
+ *
23601
+ * Loads the Google Pay JS SDK if needed, then runs `PaymentsClient.isReadyToPay()` using
23602
+ * the **default** card configuration (a button instance with a custom `card_config` may
23603
+ * produce a different result). All options are optional — unlike Apple Pay, Google Pay's
23604
+ * readiness check does not require a merchant identifier; supplying `service_id` +
23605
+ * `public_key_or_access_token` additionally validates that the service is a Google Pay service.
23606
+ *
23607
+ * Pass `{ sync: true }` for a cheap, network-free check that only reports whether the
23608
+ * Google Pay JS API is already present (see {@link GooglePayAvailabilityOptions.sync}).
23609
+ *
23610
+ * @param {GooglePayAvailabilityOptions} [options] - Optional merchant context and mode.
23611
+ * @returns {Promise<GooglePayAvailabilityResult>} The availability result.
23612
+ *
23613
+ * @example
23614
+ * // Simplest form — no merchant context needed:
23615
+ * const result = await GooglePayOpenWalletButton.checkAvailability();
23616
+ * if (result.available) {
23617
+ * // Render the Google Pay button.
23618
+ * }
23619
+ *
23620
+ * @example
23621
+ * // Validate the service is a Google Pay service while checking:
23622
+ * const result = await GooglePayOpenWalletButton.checkAvailability({
23623
+ * public_key_or_access_token,
23624
+ * service_id,
23625
+ * env: 'sandbox',
23626
+ * });
21631
23627
  */
21632
23628
  _inherits(GooglePayOpenWalletButton, _OpenWalletButtons);
21633
23629
  return _createClass(GooglePayOpenWalletButton, [{
21634
23630
  key: "validateWalletMeta",
21635
- value: function validateWalletMeta() {
23631
+ value:
23632
+ /**
23633
+ * Validates Google Pay-specific required metadata fields.
23634
+ * Google Pay has no additional required fields beyond the base (amount, currency, country).
23635
+ * @private
23636
+ */
23637
+ function validateWalletMeta() {
21636
23638
  // Google Pay has no additional required meta fields beyond the base.
21637
23639
  // Optional fields (merchant_name, card_config, etc.) are validated
21638
23640
  // at runtime by the Google Pay SDK.
@@ -21663,6 +23665,87 @@ var GooglePayOpenWalletButton = /*#__PURE__*/function (_OpenWalletButtons) {
21663
23665
  value: function createWalletService(serviceConfig) {
21664
23666
  return new GooglePayOpenWalletService(this.api, this.eventEmitter, this.serviceId, this.container, this.meta, serviceConfig);
21665
23667
  }
23668
+ }], [{
23669
+ key: "checkAvailability",
23670
+ value: function checkAvailability() {
23671
+ return __awaiter(this, arguments, void 0, function () {
23672
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
23673
+ return /*#__PURE__*/_regenerator().m(function _callee() {
23674
+ var present, merchantId, authType, api, serviceConfig;
23675
+ return _regenerator().w(function (_context) {
23676
+ while (1) switch (_context.p = _context.n) {
23677
+ case 0:
23678
+ if (!options.sync) {
23679
+ _context.n = 1;
23680
+ break;
23681
+ }
23682
+ present = isGooglePayApiPresent();
23683
+ return _context.a(2, {
23684
+ available: present,
23685
+ supported: present,
23686
+ reason: present ? undefined : 'api_not_loaded'
23687
+ });
23688
+ case 1:
23689
+ merchantId = options.merchant_id;
23690
+ if (!(!merchantId && options.public_key_or_access_token && options.service_id)) {
23691
+ _context.n = 6;
23692
+ break;
23693
+ }
23694
+ _context.p = 2;
23695
+ authType = AccessToken.validateJWT(options.public_key_or_access_token) ? API_AUTH_TYPE.TOKEN : API_AUTH_TYPE.PUBLIC_KEY;
23696
+ api = new ApiInternal(options.public_key_or_access_token, authType);
23697
+ if (options.env) {
23698
+ api.setEnv(options.env);
23699
+ }
23700
+ _context.n = 3;
23701
+ return getServiceConfigCached(api, options.service_id);
23702
+ case 3:
23703
+ serviceConfig = _context.v;
23704
+ if (!(serviceConfig.type !== SERVICE_TYPES.GOOGLE_PAY)) {
23705
+ _context.n = 4;
23706
+ break;
23707
+ }
23708
+ return _context.a(2, {
23709
+ available: false,
23710
+ supported: isGooglePayApiPresent(),
23711
+ reason: 'service_type_mismatch'
23712
+ });
23713
+ case 4:
23714
+ merchantId = serviceConfig.merchant;
23715
+ _context.n = 6;
23716
+ break;
23717
+ case 5:
23718
+ _context.p = 5;
23719
+ _context.v;
23720
+ return _context.a(2, {
23721
+ available: false,
23722
+ supported: isGooglePayApiPresent(),
23723
+ reason: 'config_fetch_failed'
23724
+ });
23725
+ case 6:
23726
+ _context.p = 6;
23727
+ _context.n = 7;
23728
+ return loadGooglePayScript();
23729
+ case 7:
23730
+ _context.n = 9;
23731
+ break;
23732
+ case 8:
23733
+ _context.p = 8;
23734
+ _context.v;
23735
+ return _context.a(2, {
23736
+ available: false,
23737
+ supported: isGooglePayApiPresent(),
23738
+ reason: 'sdk_load_failed'
23739
+ });
23740
+ case 9:
23741
+ return _context.a(2, evaluateGooglePayAvailability({
23742
+ merchantId: merchantId
23743
+ }));
23744
+ }
23745
+ }, _callee, null, [[6, 8], [2, 5]]);
23746
+ })();
23747
+ });
23748
+ }
21666
23749
  }]);
21667
23750
  }(OpenWalletButtons);
21668
23751
 
@@ -44727,4 +46810,4 @@ var browser = /*#__PURE__*/Object.freeze({
44727
46810
  zlibSync: zlibSync
44728
46811
  });
44729
46812
 
44730
- export { AfterpayCheckoutButton, AfterpayOnSiteMessaging, Api, ApplePayOpenWalletButton, ApplePayWalletButtonExpress, CANVAS_3DS_EVENT, CHECKOUT_BUTTON_EVENT, EVENT$3 as CHECKOUT_EVENT, Canvas3ds, Canvas3dsTokenFormat, Checkout, ClickToPay, Configuration, ContactShippingEditingMode, ELEMENT, ERROR_OPERATION, EVENT$5 as EVENT, Builder$1 as ExternalCheckoutBuilder, Checker as ExternalCheckoutChecker, FORM_FIELD$1 as FORM_FIELD, FRAUD_PREVENTION_EVENTS, FraudPreventionService, GooglePayOpenWalletButton, HtmlMultiWidget, HtmlPaymentSourceWidget, HtmlWidget, InstructionDebugger, MultiWidget, PAYMENT_EXPIRED, PAYMENT_FAILURE, PAYMENT_SUCCESSFUL, PAYMENT_TYPE, PURPOSE, PayPalDataCollector, PayPalSavePaymentSourceWidget, Builder as PaymentSourceBuilder, PaymentSourceWidget, PaypalCheckoutButton, PaypalWalletButtonExpress, STYLABLE_ELEMENT, STYLABLE_ELEMENT_STATE, STYLE$2 as STYLE, SUPPORTED_CARD_TYPES, TEXT, TOKEN_TYPE, TRIGGER$1 as TRIGGER, TYPE, VAULT_DISPLAY_STYLE, VaultDisplayWidget, EVENT as WALLET_BUTTONS_EXPRESS_EVENT, WALLET_TYPES, WalletButtons, ZipmoneyCheckoutButton };
46813
+ export { AfterpayCheckoutButton, AfterpayOnSiteMessaging, Api, ApplePayOpenWalletButton, ApplePayWalletButtonExpress, CANVAS_3DS_EVENT, CARD_SCHEME, CHECKOUT_BUTTON_EVENT, EVENT$3 as CHECKOUT_EVENT, Canvas3ds, Canvas3dsTokenFormat, Checkout, ClickToPay, Configuration, ContactShippingEditingMode, ELEMENT, ERROR_OPERATION, EVENT$5 as EVENT, Builder$1 as ExternalCheckoutBuilder, Checker as ExternalCheckoutChecker, FORM_FIELD$1 as FORM_FIELD, FRAUD_PREVENTION_EVENTS, FraudPreventionService, GooglePayOpenWalletButton, HtmlMultiWidget, HtmlPaymentSourceWidget, HtmlWidget, InstructionDebugger, MultiWidget, PAYMENT_EXPIRED, PAYMENT_FAILURE, PAYMENT_SUCCESSFUL, PAYMENT_TYPE, PURPOSE, PayPalDataCollector, PayPalSavePaymentSourceWidget, Builder as PaymentSourceBuilder, PaymentSourceWidget, PaypalCheckoutButton, PaypalWalletButtonExpress, STYLABLE_ELEMENT, STYLABLE_ELEMENT_STATE, STYLE$2 as STYLE, SUPPORTED_CARD_TYPES, TEXT, TOKEN_TYPE, TRIGGER$1 as TRIGGER, TYPE, VAULT_DISPLAY_STYLE, VaultDisplayWidget, EVENT as WALLET_BUTTONS_EXPRESS_EVENT, WALLET_TYPES, WalletButtons, ZipmoneyCheckoutButton };