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