@paydock/client-sdk 1.145.0-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 (43) hide show
  1. package/README.md +129 -1
  2. package/bundles/index.cjs +746 -102
  3. package/bundles/index.cjs.d.ts +131 -14
  4. package/bundles/index.mjs +746 -103
  5. package/bundles/index.mjs.d.ts +131 -14
  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/index-cba.d.ts +1 -1
  27. package/bundles/types/index-cba.d.ts.map +1 -1
  28. package/bundles/types/index.d.ts +2 -2
  29. package/bundles/types/index.d.ts.map +1 -1
  30. package/bundles/types/widget/multi-widget.d.ts +19 -0
  31. package/bundles/types/widget/multi-widget.d.ts.map +1 -1
  32. package/bundles/widget.umd.js +746 -102
  33. package/bundles/widget.umd.js.d.ts +131 -14
  34. package/bundles/widget.umd.js.min.d.ts +131 -14
  35. package/bundles/widget.umd.min.js +1 -1
  36. package/docs/api-canvas3ds.md +10 -8
  37. package/docs/api-checkout-button.md +15 -0
  38. package/docs/api-ps-widget.md +15 -0
  39. package/docs/api-widget.md +15 -0
  40. package/docs/canvas3ds-examples.md +43 -1
  41. package/docs/widget-examples.md +41 -0
  42. package/package.json +15 -9
  43. package/slate.md +84 -1
package/bundles/index.mjs CHANGED
@@ -1863,7 +1863,7 @@ SDK.headerKeys = Object.freeze({
1863
1863
  version: 'x-sdk-version',
1864
1864
  type: 'x-sdk-type'
1865
1865
  });
1866
- SDK._version = 'v1.145.0-beta';
1866
+ SDK._version = 'v1.147.0';
1867
1867
 
1868
1868
  var ACCESS_HEADER_NAME = {
1869
1869
  accessToken: 'x-access-token',
@@ -4719,6 +4719,15 @@ var ApiBase = /*#__PURE__*/function () {
4719
4719
  this.env.setEnv(env, alias);
4720
4720
  return this;
4721
4721
  }
4722
+ /**
4723
+ * Origin (`scheme://host[:port]`) of the API this client talks to, for the current env/alias.
4724
+ * Used to validate the `origin` of postMessages the API itself sends to the page.
4725
+ */
4726
+ }, {
4727
+ key: "getApiOrigin",
4728
+ value: function getApiOrigin() {
4729
+ return ApiBase.toOrigin(this.env.getConf().url);
4730
+ }
4722
4731
  }, {
4723
4732
  key: "requestObservable",
4724
4733
  value: function requestObservable(method, url, requestBody) {
@@ -4931,6 +4940,18 @@ var ApiBase = /*#__PURE__*/function () {
4931
4940
  environment: this.env.getEnv()
4932
4941
  };
4933
4942
  }
4943
+ }], [{
4944
+ key: "toOrigin",
4945
+ value: function toOrigin(url) {
4946
+ try {
4947
+ return new URL(url).origin;
4948
+ } catch (_a) {
4949
+ // Not a parsable URL: the returned string is not an origin and will never match a
4950
+ // MessageEvent.origin — surface the misconfiguration instead of failing silently.
4951
+ console.warn("[paydock/client-sdk] cannot derive an origin from configured URL \"".concat(url, "\""));
4952
+ return url.replace(/\/+$/, '');
4953
+ }
4954
+ }
4934
4955
  }]);
4935
4956
  }();
4936
4957
 
@@ -4991,7 +5012,6 @@ var Api = /*#__PURE__*/function (_ApiBase) {
4991
5012
 
4992
5013
  var WALLET_CAPTURE_LINK = '/v1/charges/wallet/capture';
4993
5014
  var WALLET_CALLBACK_LINK = '/v1/charges/wallet/callback';
4994
- var STANDALONE_3DS_PROCESS_LINK = '/v1/charges/standalone-3ds/process';
4995
5015
  var STANDALONE_3DS_HANDLE_LINK = '/v1/charges/standalone-3ds/handle';
4996
5016
  var ApiChargeInternal = /*#__PURE__*/function () {
4997
5017
  function ApiChargeInternal(api) {
@@ -5008,11 +5028,6 @@ var ApiChargeInternal = /*#__PURE__*/function () {
5008
5028
  value: function walletCallback(payload) {
5009
5029
  return this.api.getClientPromise('POST', WALLET_CALLBACK_LINK).send(payload);
5010
5030
  }
5011
- }, {
5012
- key: "standalone3dsProcess",
5013
- value: function standalone3dsProcess(payload) {
5014
- return this.api.getClientPromise('POST', STANDALONE_3DS_PROCESS_LINK).send(payload);
5015
- }
5016
5031
  }, {
5017
5032
  key: "standalone3dsHandle",
5018
5033
  value: function standalone3dsHandle() {
@@ -5151,6 +5166,10 @@ var IFrame = /*#__PURE__*/function () {
5151
5166
  _classCallCheck(this, IFrame);
5152
5167
  this.container = container;
5153
5168
  }
5169
+ /**
5170
+ * @param options.onLoad Called on every `load` of the iframe (initial document and each
5171
+ * subsequent navigation). Attached before `src` is set so a fast load is never missed.
5172
+ */
5154
5173
  return _createClass(IFrame, [{
5155
5174
  key: "load",
5156
5175
  value: function load(link) {
@@ -5159,6 +5178,9 @@ var IFrame = /*#__PURE__*/function () {
5159
5178
  if (!this.container.isExist() || this.isExist()) return;
5160
5179
  var iFrame = document.createElement('iframe');
5161
5180
  if (options.title) iFrame.title = options.title;
5181
+ if (options.onLoad) {
5182
+ iFrame.addEventListener('load', options.onLoad);
5183
+ }
5162
5184
  if (iframeClass) iFrame.classList.add(iframeClass);
5163
5185
  // Set additional attributes before src and DOM insertion so that
5164
5186
  // Permissions-Policy (e.g. allow="payment") is in place when the
@@ -5299,14 +5321,14 @@ var IFrameEvent = /*#__PURE__*/function () {
5299
5321
  data = JSON.parse(event.data);
5300
5322
  } catch (error) {}
5301
5323
  if (!data) return;
5302
- _this.emit(data);
5324
+ _this.emit(data, event.origin);
5303
5325
  });
5304
5326
  }
5305
5327
  return _createClass(IFrameEvent, [{
5306
5328
  key: "emit",
5307
- value: function emit(data) {
5329
+ value: function emit(data, origin) {
5308
5330
  for (var key in this.listeners) {
5309
- if (this.listeners[key].event === data.event && data.widget_id === this.listeners[key].widget_id) this.listeners[key].listener.apply(this, [data]);
5331
+ if (this.listeners[key].event === data.event && data.widget_id === this.listeners[key].widget_id) this.listeners[key].listener.apply(this, [data, origin]);
5310
5332
  }
5311
5333
  }
5312
5334
  }, {
@@ -5379,6 +5401,20 @@ var GPAYMENTS_EVENT = {
5379
5401
  DECOUPLED: 'chargeAuthDecoupled',
5380
5402
  /** Challenge required — issuer demands user interaction. Payload: `Canvas3dsAuthPayload`. */
5381
5403
  CHALLENGE: 'chargeAuthChallenge',
5404
+ /**
5405
+ * The challenge iframe fired its FIRST `load` after `chargeAuthChallenge` (the ACS page has painted),
5406
+ * or the `CHALLENGE_LOADED_FALLBACK_MS` safety fallback elapsed without one. Emitted exactly once per
5407
+ * challenge; never emitted for a decoupled challenge (no challenge iframe). Payload: `Canvas3dsChallengeLoadedPayload`.
5408
+ */
5409
+ CHALLENGE_LOADED: 'chargeAuthChallengeLoaded',
5410
+ /**
5411
+ * The issuer delivered the final authentication result to the 3DS Server (the shopper finished the
5412
+ * challenge, or approved a decoupled authentication) and the SDK is now obtaining the outcome. Emitted at
5413
+ * most once per authentication, always before the terminal event. Detected through the `secondary_url`
5414
+ * result monitor (`source: 'poll'`) or, when the callback message arrives first, right before the outcome
5415
+ * it carries (`source: 'callback'`). Payload: `Canvas3dsChallengeCompletedPayload`.
5416
+ */
5417
+ CHALLENGE_COMPLETED: 'chargeAuthChallengeCompleted',
5382
5418
  /** Informational signal emitted by the GPayments flow. Internal — not part of the public typed surface. */
5383
5419
  INFO: 'chargeAuthInfo',
5384
5420
  /** Error during the flow (network, parsing, etc.). Payload: `Canvas3dsErrorPayload`. */
@@ -5390,13 +5426,56 @@ var GPAYMENTS_EVENT = {
5390
5426
  * backwards-compatible internal usage.
5391
5427
  */
5392
5428
  var CANVAS_3DS_EVENT = GPAYMENTS_EVENT;
5429
+ /**
5430
+ * Upper bound on how long `chargeAuthChallengeLoaded` waits for the challenge iframe's first `load`.
5431
+ * When it elapses the event is emitted with `reason: 'timeout'` so a merchant loader is never held
5432
+ * forever; the real `load` arriving afterwards is the paint and is simply ignored.
5433
+ */
5434
+ var CHALLENGE_LOADED_FALLBACK_MS = 2500;
5435
+ /**
5436
+ * Interval of the `secondary_url` (GPayments `resultMonUrl`) poll while an ACS challenge is on screen.
5437
+ * The monitor flips to `AuthResultReady` when the issuer's RReq reaches the 3DS Server — before the ACS has
5438
+ * even returned its final page to the browser — so this poll is what makes `chargeAuthChallengeCompleted`
5439
+ * land before the shopper sees any post-submit page. GPayments recommends 2 s and leaves the interval to the
5440
+ * requestor; 1 s matches the cadence of GPayments' own monitoring frame.
5441
+ */
5442
+ var CHALLENGE_RESULT_POLL_INTERVAL_MS = 1000;
5443
+ /**
5444
+ * Interval of the `secondary_url` poll for a decoupled challenge (shopper approves out-of-band, possibly
5445
+ * minutes later; nothing to cover on screen) — the GPayments-recommended 2 s.
5446
+ */
5447
+ var DECOUPLED_RESULT_POLL_INTERVAL_MS = 2000;
5448
+ /** The result-monitor request failed at transport level (network, abort, timeout) — retried, not fatal. */
5449
+ var ResultMonitorTransportError = /*#__PURE__*/function (_Error) {
5450
+ function ResultMonitorTransportError() {
5451
+ var _this;
5452
+ _classCallCheck(this, ResultMonitorTransportError);
5453
+ _this = _callSuper(this, ResultMonitorTransportError, ['Standalone 3DS result monitor request failed']);
5454
+ _this.name = 'ResultMonitorTransportError';
5455
+ return _this;
5456
+ }
5457
+ _inherits(ResultMonitorTransportError, _Error);
5458
+ return _createClass(ResultMonitorTransportError);
5459
+ }(/*#__PURE__*/_wrapNativeSuper(Error));
5393
5460
  var GPaymentsService = /*#__PURE__*/function () {
5394
5461
  function GPaymentsService(container, api, eventEmitter) {
5462
+ var trustedOrigins = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
5395
5463
  _classCallCheck(this, GPaymentsService);
5396
5464
  this.container = container;
5397
5465
  this.api = api;
5398
5466
  this.eventEmitter = eventEmitter;
5467
+ this.trustedOrigins = trustedOrigins;
5399
5468
  this.resultRead = false;
5469
+ /** `chargeAuthChallengeLoaded` already emitted for the current challenge (by `load` or by the fallback). */
5470
+ this.challengeLoadedEmitted = false;
5471
+ /** `chargeAuthChallengeCompleted` already emitted for the current authentication (poll or callback). */
5472
+ this.challengeCompletedEmitted = false;
5473
+ /** Set once a terminal event was emitted: no lifecycle event may follow (reset when a new challenge starts). */
5474
+ this.challengeLifecycleClosed = false;
5475
+ this.challengeLoadedTimer = null;
5476
+ this.resultPollTimer = null;
5477
+ /** One-shot guard for the `chargeAuth` leg — the callback message may be delivered more than once. */
5478
+ this.authHandled = false;
5400
5479
  this.iFrameEvent = new IFrameEvent(window);
5401
5480
  }
5402
5481
  return _createClass(GPaymentsService, [{
@@ -5416,6 +5495,7 @@ var GPaymentsService = /*#__PURE__*/function () {
5416
5495
  key: "initializeIFrames",
5417
5496
  value: function initializeIFrames(initializationUrl, secondaryUrl, title) {
5418
5497
  var hideAuthorization = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
5498
+ var onLoad = arguments.length > 4 ? arguments[4] : undefined;
5419
5499
  var container = this.container.getElement();
5420
5500
  if (!container) {
5421
5501
  return;
@@ -5426,7 +5506,8 @@ var GPaymentsService = /*#__PURE__*/function () {
5426
5506
  this.browserAndChallengeContainer = new Container('#paydock_authorization_iframe');
5427
5507
  this.iFrameAuthorization = new IFrame(this.browserAndChallengeContainer);
5428
5508
  this.iFrameAuthorization.load(initializationUrl, {
5429
- title: title
5509
+ title: title,
5510
+ onLoad: onLoad
5430
5511
  });
5431
5512
  if (secondaryUrl) {
5432
5513
  var divSecondaryURL = document.createElement('div');
@@ -5460,25 +5541,192 @@ var GPaymentsService = /*#__PURE__*/function () {
5460
5541
  }, {
5461
5542
  key: "setupIFrameEvents",
5462
5543
  value: function setupIFrameEvents(widgetId) {
5463
- var _this = this;
5464
- this.iFrameEvent.on(EVENT$5.CHARGE_AUTH, widgetId, function (data) {
5544
+ var _this2 = this;
5545
+ this.iFrameEvent.on(EVENT$5.CHARGE_AUTH, widgetId, function (data, origin) {
5465
5546
  if (data.status === IFRAME_STATUS.SKIPPED || data.status === IFRAME_STATUS.FINISHED) {
5547
+ // this channel is spoofable — any frame that knows the charge_3ds_id can post to it.
5548
+ // A message from an unknown sender must never terminate a live authentication: ignore it.
5549
+ if (!_this2.isTrustedOrigin(origin)) {
5550
+ return;
5551
+ }
5552
+ // one-shot: the callback message can be delivered more than once (it is posted to both
5553
+ // `parent` and `top`) — a duplicate must not re-emit events or duplicate challenge iframes
5554
+ if (_this2.authHandled) {
5555
+ return;
5556
+ }
5557
+ _this2.authHandled = true;
5466
5558
  if (data.info) {
5467
- _this.eventEmitter.emit(GPAYMENTS_EVENT.INFO, {
5559
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.INFO, {
5468
5560
  info: data.info
5469
5561
  });
5470
5562
  }
5471
- _this.performAuthentication(data);
5563
+ _this2.removeIframes();
5564
+ if (typeof data.process_status === 'string') {
5565
+ // the API already ran the authentication inside the callback: no /process call needed
5566
+ _this2.handleAuthenticationOutcome({
5567
+ status: data.process_status,
5568
+ result: data.result,
5569
+ error: data.error
5570
+ }, data.charge_3ds_id);
5571
+ } else {
5572
+ // The API always runs the authentication inside the callback (PAYRAC-2480). A trusted
5573
+ // message without the outcome means an outdated API — fail, never verify via /process.
5574
+ _this2.finishChallengeLifecycle();
5575
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this2.parseError({
5576
+ message: 'Standalone 3DS authentication outcome missing from the callback message'
5577
+ }, data.charge_3ds_id));
5578
+ }
5472
5579
  } else if (data.status === IFRAME_STATUS.TIMEOUT || data.status === IFRAME_STATUS.INVALID) {
5473
- _this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this.parseHandleResponse({
5580
+ _this2.finishChallengeLifecycle();
5581
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this2.parseHandleResponse({
5474
5582
  status: data.status
5475
5583
  }, data.charge_3ds_id));
5476
5584
  }
5477
5585
  });
5478
- this.iFrameEvent.on(EVENT$5.CHARGE_AUTH_SUCCESS, widgetId, function (data) {
5479
- _this.processResult(data.charge_3ds_id);
5586
+ this.iFrameEvent.on(EVENT$5.CHARGE_AUTH_SUCCESS, widgetId, function (data, origin) {
5587
+ // the callback iframe reached Paydock with `AuthResultReady`: the issuer has answered. Announce it
5588
+ // before the outcome so merchants always see completed → terminal, whichever transport won.
5589
+ _this2.emitChallengeCompleted(data.charge_3ds_id, 'callback');
5590
+ if (_this2.hasChainedOutcome(data, origin)) {
5591
+ // the API already fetched the challenge result inside the callback: no GET /handle needed
5592
+ _this2.handleResultOutcome({
5593
+ status: data.process_status,
5594
+ result: data.result,
5595
+ error: data.error
5596
+ }, data.charge_3ds_id);
5597
+ } else {
5598
+ _this2.processResult(data.charge_3ds_id);
5599
+ }
5600
+ });
5601
+ }
5602
+ /**
5603
+ * A message carries a processed outcome only when it has `process_status` AND comes from a trusted
5604
+ * origin. Used by the result leg: an untrusted `process_status` is never acted upon — the SDK falls
5605
+ * back to `GET /handle` (still needed for decoupled challenges / polling). The authentication leg
5606
+ * ignores untrusted senders outright, before this check.
5607
+ */
5608
+ }, {
5609
+ key: "hasChainedOutcome",
5610
+ value: function hasChainedOutcome(data, origin) {
5611
+ return typeof data.process_status === 'string' && this.isTrustedOrigin(origin);
5612
+ }
5613
+ }, {
5614
+ key: "isTrustedOrigin",
5615
+ value: function isTrustedOrigin(origin) {
5616
+ // 'null' is the serialized opaque origin (e.g. a sandboxed iframe) — never trust it, even if a
5617
+ // misconfigured env URL were ever to put the literal string 'null' into the trusted list
5618
+ return typeof origin === 'string' && origin !== 'null' && this.trustedOrigins.includes(origin);
5619
+ }
5620
+ }, {
5621
+ key: "removeIframes",
5622
+ value: function removeIframes() {
5623
+ var _a, _b;
5624
+ this.clearChallengeLoadedTimer();
5625
+ (_a = this.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.remove();
5626
+ (_b = this.iFrameSecondaryUrl) === null || _b === void 0 ? void 0 : _b.remove();
5627
+ }
5628
+ }, {
5629
+ key: "clearChallengeLoadedTimer",
5630
+ value: function clearChallengeLoadedTimer() {
5631
+ if (this.challengeLoadedTimer !== null) {
5632
+ clearTimeout(this.challengeLoadedTimer);
5633
+ this.challengeLoadedTimer = null;
5634
+ }
5635
+ }
5636
+ /**
5637
+ * Challenge iframe lifecycle (`chargeAuthChallengeLoaded`).
5638
+ *
5639
+ * The challenge iframe is cross-origin, but its `load` event still fires when the ACS page finishes loading:
5640
+ * the first one means the page painted (merchants can drop their loader). A fallback timer
5641
+ * (`armChallengeLoadedFallback`) guarantees `chargeAuthChallengeLoaded` within `CHALLENGE_LOADED_FALLBACK_MS`
5642
+ * even if no `load` ever fires; a late paint after that is simply ignored. Later `load`s are NOT a usable
5643
+ * signal for "the shopper submitted": the post-submit hops are HTTP redirects and self-submitting pages whose
5644
+ * loads abort, and the final callback page posts its message before its own `load` — so the post-submit
5645
+ * moment is `chargeAuthChallengeCompleted`, driven by the `secondary_url` result monitor instead.
5646
+ *
5647
+ * Nothing is emitted once the flow reached a terminal outcome (`finishChallengeLifecycle`), and a
5648
+ * decoupled challenge (no challenge iframe) never emits `chargeAuthChallengeLoaded`.
5649
+ *
5650
+ * Resets the per-challenge state and returns the `load` handler to attach to the challenge iframe.
5651
+ */
5652
+ }, {
5653
+ key: "startChallengeLifecycle",
5654
+ value: function startChallengeLifecycle(charge3dsId) {
5655
+ var _this3 = this;
5656
+ this.clearChallengeLoadedTimer();
5657
+ this.challengeLoadedEmitted = false;
5658
+ this.challengeCompletedEmitted = false;
5659
+ this.challengeLifecycleClosed = false;
5660
+ return function () {
5661
+ var _a;
5662
+ if (_this3.challengeLifecycleClosed || _this3.challengeLoadedEmitted || !((_a = _this3.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.isExist())) {
5663
+ return;
5664
+ }
5665
+ _this3.clearChallengeLoadedTimer();
5666
+ _this3.emitChallengeLoaded(charge3dsId, 'load');
5667
+ };
5668
+ }
5669
+ /**
5670
+ * Arms the `chargeAuthChallengeLoaded` fallback — only when a challenge iframe actually exists (the
5671
+ * container may be gone, or `IFrame.load` may have no-op'd on an already-present iframe).
5672
+ */
5673
+ }, {
5674
+ key: "armChallengeLoadedFallback",
5675
+ value: function armChallengeLoadedFallback(charge3dsId) {
5676
+ var _this4 = this;
5677
+ var _a;
5678
+ if (!((_a = this.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.isExist())) {
5679
+ return;
5680
+ }
5681
+ this.challengeLoadedTimer = setTimeout(function () {
5682
+ _this4.challengeLoadedTimer = null;
5683
+ if (_this4.challengeLifecycleClosed || _this4.challengeLoadedEmitted) {
5684
+ return;
5685
+ }
5686
+ _this4.emitChallengeLoaded(charge3dsId, 'timeout');
5687
+ }, CHALLENGE_LOADED_FALLBACK_MS);
5688
+ }
5689
+ }, {
5690
+ key: "emitChallengeLoaded",
5691
+ value: function emitChallengeLoaded(charge3dsId, reason) {
5692
+ this.challengeLoadedEmitted = true;
5693
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE_LOADED, {
5694
+ charge_3ds_id: charge3dsId,
5695
+ reason: reason
5696
+ });
5697
+ }
5698
+ /**
5699
+ * `chargeAuthChallengeCompleted` — once per authentication, never after a terminal event. Idempotent so the
5700
+ * poll and the callback message can both call it: the first transport to notice the issuer's result wins.
5701
+ */
5702
+ }, {
5703
+ key: "emitChallengeCompleted",
5704
+ value: function emitChallengeCompleted(charge3dsId, source) {
5705
+ if (this.challengeLifecycleClosed || this.challengeCompletedEmitted) {
5706
+ return;
5707
+ }
5708
+ this.challengeCompletedEmitted = true;
5709
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE_COMPLETED, {
5710
+ charge_3ds_id: charge3dsId,
5711
+ source: source
5480
5712
  });
5481
5713
  }
5714
+ /** Call before emitting any terminal event: no lifecycle event may follow it, and the result poll stops. */
5715
+ }, {
5716
+ key: "finishChallengeLifecycle",
5717
+ value: function finishChallengeLifecycle() {
5718
+ this.clearChallengeLoadedTimer();
5719
+ this.stopResultPolling();
5720
+ this.challengeLifecycleClosed = true;
5721
+ }
5722
+ }, {
5723
+ key: "stopResultPolling",
5724
+ value: function stopResultPolling() {
5725
+ if (this.resultPollTimer !== null) {
5726
+ clearTimeout(this.resultPollTimer);
5727
+ this.resultPollTimer = null;
5728
+ }
5729
+ }
5482
5730
  }, {
5483
5731
  key: "parseResultData",
5484
5732
  value: function parseResultData(_ref2, charge3dsId) {
@@ -5512,24 +5760,49 @@ var GPaymentsService = /*#__PURE__*/function () {
5512
5760
  }, {
5513
5761
  key: "processResult",
5514
5762
  value: function processResult(charge3dsId) {
5515
- var _this2 = this;
5763
+ var _this5 = this;
5516
5764
  if (this.resultRead) {
5517
5765
  return;
5518
5766
  }
5519
5767
  this.resultRead = true;
5768
+ this.finishChallengeLifecycle();
5520
5769
  this.api.charge().standalone3dsHandle().then(function (result) {
5521
5770
  var _a;
5522
- _this2.iFrameAuthorization.remove();
5523
- (_a = _this2.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5771
+ _this5.iFrameAuthorization.remove();
5772
+ (_a = _this5.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5524
5773
  if (result.status === PROCESS_STANDALONE_3DS_STATUS.SUCCESS) {
5525
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this2.parseResultData(result, charge3dsId));
5774
+ _this5.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this5.parseResultData(result, charge3dsId));
5526
5775
  } else {
5527
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this2.parseResultData(result, charge3dsId));
5776
+ _this5.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this5.parseResultData(result, charge3dsId));
5528
5777
  }
5529
5778
  }, function (err) {
5530
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this2.parseError(err, charge3dsId));
5779
+ _this5.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this5.parseError(err, charge3dsId));
5531
5780
  });
5532
5781
  }
5782
+ /** Final (post-challenge) outcome delivered by the API through the callback message. */
5783
+ }, {
5784
+ key: "handleResultOutcome",
5785
+ value: function handleResultOutcome(_ref4, charge3dsId) {
5786
+ var status = _ref4.status,
5787
+ error = _ref4.error;
5788
+ if (this.resultRead) {
5789
+ return;
5790
+ }
5791
+ this.resultRead = true;
5792
+ this.finishChallengeLifecycle();
5793
+ this.removeIframes();
5794
+ if (status === PROCESS_STANDALONE_3DS_STATUS.SUCCESS) {
5795
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, this.parseResultData({
5796
+ status: status
5797
+ }, charge3dsId));
5798
+ } else if (status === PROCESS_STANDALONE_3DS_STATUS.ERROR) {
5799
+ this.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, this.parseError(error, charge3dsId));
5800
+ } else {
5801
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseResultData({
5802
+ status: status
5803
+ }, charge3dsId));
5804
+ }
5805
+ }
5533
5806
  }, {
5534
5807
  key: "externalAPI",
5535
5808
  value: function externalAPI(method, url) {
@@ -5543,60 +5816,106 @@ var GPaymentsService = /*#__PURE__*/function () {
5543
5816
  reject(error);
5544
5817
  }
5545
5818
  };
5819
+ // without these a failed request left the promise pending forever and the poll silently dead
5820
+ var fail = function fail() {
5821
+ return reject(new ResultMonitorTransportError());
5822
+ };
5823
+ request.onerror = fail;
5824
+ request.onabort = fail;
5825
+ request.ontimeout = fail;
5546
5826
  request.send();
5547
5827
  });
5548
5828
  }
5829
+ /**
5830
+ * Polls the GPayments result monitor (`secondary_url`) until `AuthResultReady`. It is the primary source of
5831
+ * `chargeAuthChallengeCompleted` (the monitor flips as soon as the issuer's RReq reaches the 3DS Server) and
5832
+ * the fallback transport for the outcome itself (`GET /handle`) when the callback message never arrives —
5833
+ * the only transport for decoupled challenges. Stops once an outcome was obtained or the lifecycle closed.
5834
+ * Transport failures are retried on the same cadence; an unparsable body or an unknown event is fatal.
5835
+ */
5549
5836
  }, {
5550
5837
  key: "doPolling",
5551
- value: function doPolling(url, charge3dsId) {
5552
- var _this3 = this;
5838
+ value: function doPolling(url, charge3dsId, intervalMs) {
5839
+ var _this6 = this;
5840
+ this.stopResultPolling();
5841
+ if (this.resultRead || this.challengeLifecycleClosed) {
5842
+ return;
5843
+ }
5844
+ var schedule = function schedule() {
5845
+ _this6.resultPollTimer = setTimeout(function () {
5846
+ _this6.resultPollTimer = null;
5847
+ _this6.doPolling(url, charge3dsId, intervalMs);
5848
+ }, intervalMs);
5849
+ };
5553
5850
  this.externalAPI('GET', url).then(function (data) {
5851
+ if (_this6.resultRead || _this6.challengeLifecycleClosed) {
5852
+ return;
5853
+ }
5554
5854
  if (!data.event || data.event === 'AuthResultNotReady') {
5555
- setTimeout(function () {
5556
- _this3.doPolling(url, charge3dsId);
5557
- }, 2000);
5855
+ schedule();
5558
5856
  } else if (data.event === 'AuthResultReady') {
5559
- _this3.processResult(charge3dsId);
5857
+ _this6.emitChallengeCompleted(charge3dsId, 'poll');
5858
+ _this6.processResult(charge3dsId);
5560
5859
  } else {
5561
5860
  throw new Error('Event not supported');
5562
5861
  }
5563
5862
  })["catch"](function (err) {
5564
- return _this3.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this3.parseError(err, charge3dsId));
5863
+ if (err instanceof ResultMonitorTransportError) {
5864
+ if (!_this6.resultRead && !_this6.challengeLifecycleClosed) {
5865
+ schedule();
5866
+ }
5867
+ return;
5868
+ }
5869
+ _this6.finishChallengeLifecycle();
5870
+ _this6.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this6.parseError(err, charge3dsId));
5565
5871
  });
5566
5872
  }
5873
+ /**
5874
+ * Branches on the authentication outcome delivered by the API through the callback message
5875
+ * (`process_status` → `status`; same shape as the `/process` response body).
5876
+ */
5567
5877
  }, {
5568
- key: "performAuthentication",
5569
- value: function performAuthentication(_ref4) {
5570
- var _this4 = this;
5571
- var charge_3ds_id = _ref4.charge_3ds_id;
5572
- var _a;
5573
- this.iFrameAuthorization.remove();
5574
- (_a = this.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5575
- this.api.charge().standalone3dsProcess({
5576
- charge_3ds_id: charge_3ds_id
5577
- }).then(function (authenticationResult) {
5578
- var _a, _b;
5579
- if (authenticationResult.status === AUTHENTICATION_STATUS.SUCCESS) {
5580
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5581
- } else if (authenticationResult.status === AUTHENTICATION_STATUS.PENDING) {
5582
- if ((_a = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _a === void 0 ? void 0 : _a.challenge) {
5583
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5584
- _this4.initializeIFrames(authenticationResult.result.challenge_url, undefined, 'Authentication Challenge', false);
5585
- if (authenticationResult.result.secondary_url) {
5586
- _this4.doPolling(authenticationResult.result.secondary_url, charge_3ds_id);
5587
- }
5588
- } else if ((_b = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _b === void 0 ? void 0 : _b.decoupled_challenge) {
5589
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.DECOUPLED, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5590
- if (authenticationResult.result.secondary_url) {
5591
- _this4.doPolling(authenticationResult.result.secondary_url, charge_3ds_id);
5592
- }
5878
+ key: "handleAuthenticationOutcome",
5879
+ value: function handleAuthenticationOutcome(authenticationResult, charge_3ds_id) {
5880
+ var _a, _b, _c;
5881
+ if (authenticationResult.status !== AUTHENTICATION_STATUS.PENDING) {
5882
+ // terminal outcome — a later `chargeAuthSuccess` message or poll must not emit a second result
5883
+ this.resultRead = true;
5884
+ }
5885
+ if (authenticationResult.status === AUTHENTICATION_STATUS.SUCCESS) {
5886
+ this.finishChallengeLifecycle();
5887
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5888
+ } else if (authenticationResult.status === AUTHENTICATION_STATUS.PENDING) {
5889
+ if ((_a = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _a === void 0 ? void 0 : _a.challenge) {
5890
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5891
+ var onChallengeLoad = this.startChallengeLifecycle(charge_3ds_id);
5892
+ this.initializeIFrames(authenticationResult.result.challenge_url, undefined, 'Authentication Challenge', false, onChallengeLoad);
5893
+ this.armChallengeLoadedFallback(charge_3ds_id);
5894
+ if (authenticationResult.result.secondary_url) {
5895
+ this.doPolling(authenticationResult.result.secondary_url, charge_3ds_id, CHALLENGE_RESULT_POLL_INTERVAL_MS);
5896
+ }
5897
+ } else if ((_b = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _b === void 0 ? void 0 : _b.decoupled_challenge) {
5898
+ this.challengeCompletedEmitted = false;
5899
+ this.challengeLifecycleClosed = false;
5900
+ this.eventEmitter.emit(GPAYMENTS_EVENT.DECOUPLED, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5901
+ if (authenticationResult.result.secondary_url) {
5902
+ this.doPolling(authenticationResult.result.secondary_url, charge_3ds_id, DECOUPLED_RESULT_POLL_INTERVAL_MS);
5593
5903
  }
5594
5904
  } else {
5595
- return _this4.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5596
- }
5597
- }, function (err) {
5598
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this4.parseError(err, charge_3ds_id));
5599
- });
5905
+ // pending but nothing to run (no challenge delivered) — fail rather than hang silently
5906
+ // with the iframes already removed and no further message ever coming
5907
+ this.resultRead = true;
5908
+ this.finishChallengeLifecycle();
5909
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5910
+ }
5911
+ } else if (authenticationResult.status === AUTHENTICATION_STATUS.ERROR) {
5912
+ // the API rejected the authentication, or could not run it inside the callback
5913
+ this.finishChallengeLifecycle();
5914
+ this.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, this.parseError((_c = authenticationResult.error) !== null && _c !== void 0 ? _c : authenticationResult, charge_3ds_id));
5915
+ } else {
5916
+ this.finishChallengeLifecycle();
5917
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseHandleResponse(authenticationResult, charge_3ds_id));
5918
+ }
5600
5919
  }
5601
5920
  }]);
5602
5921
  }();
@@ -5623,10 +5942,27 @@ var Standalone3dsService = /*#__PURE__*/function () {
5623
5942
  api.setEnv(this.env, this.alias);
5624
5943
  switch (tokenData.service_type) {
5625
5944
  case STANDALONE_3DS_GATEWAYS.GPAYMENTS:
5626
- new GPaymentsService(this.container, api, this.eventEmitter).load(tokenData, options.title);
5945
+ new GPaymentsService(this.container, api, this.eventEmitter, this.getTrustedMessageOrigins(api)).load(tokenData, options.title);
5627
5946
  break;
5628
5947
  }
5629
5948
  }
5949
+ /**
5950
+ * Origins allowed to deliver a processed 3DS outcome to the page via postMessage: the Paydock API
5951
+ * (inline callback page) and the Paydock widget (remote-app landing page), for the current env/alias.
5952
+ */
5953
+ }, {
5954
+ key: "getTrustedMessageOrigins",
5955
+ value: function getTrustedMessageOrigins(api) {
5956
+ var origins = [api.getApiOrigin()];
5957
+ try {
5958
+ var widgetEnv = new Env(WIDGET_URL, this.env);
5959
+ widgetEnv.setEnv(this.env, this.alias);
5960
+ origins.push(ApiBase.toOrigin(widgetEnv.getConf().url));
5961
+ } catch (_a) {
5962
+ // widget origin unknown for this env — the API origin is enough
5963
+ }
5964
+ return origins;
5965
+ }
5630
5966
  }, {
5631
5967
  key: "setEnv",
5632
5968
  value: function setEnv(env, alias) {
@@ -5795,11 +6131,13 @@ var TOKEN_FORMAT$2;
5795
6131
  * @const STANDALONE_3DS_EVENT
5796
6132
  *
5797
6133
  * @type {object}
5798
- * @param {string} CHARGE_AUTH_SUCCESS=chargeAuthSuccess
5799
- * @param {string} CHARGE_AUTH_REJECT=chargeAuthReject
5800
- * @param {string} CHARGE_AUTH_DECOUPLED=chargeAuthDecoupled
5801
- * @param {string} CHARGE_AUTH_CHALLENGE=chargeAuthChallenge
5802
- * @param {string} CHARGE_AUTH_INFO=chargeAuthInfo
6134
+ * @param {string} AUTH_SUCCESS=chargeAuthSuccess
6135
+ * @param {string} AUTH_ERROR=chargeAuthReject
6136
+ * @param {string} DECOUPLED=chargeAuthDecoupled
6137
+ * @param {string} CHALLENGE=chargeAuthChallenge
6138
+ * @param {string} CHALLENGE_LOADED=chargeAuthChallengeLoaded - challenge iframe painted (first `load`, or 2.5 s fallback); reveal your challenge UI here
6139
+ * @param {string} CHALLENGE_COMPLETED=chargeAuthChallengeCompleted - the shopper finished the challenge (issuer result delivered to the 3DS Server); cover the challenge iframe with your own loader until the terminal event
6140
+ * @param {string} INFO=chargeAuthInfo
5803
6141
  * @param {string} ERROR=error
5804
6142
  */
5805
6143
  /**
@@ -6181,6 +6519,24 @@ var SUPPORTED_CARD_TYPES = {
6181
6519
  EFTPOS_WHITE: 'eftpos_white',
6182
6520
  UNIONPAY: 'unionpay'
6183
6521
  };
6522
+ /**
6523
+ * Card schemes the widget is able to resolve from a card number, and therefore the only values
6524
+ * `setAllowedCardSchemes` accepts. This is a separate vocabulary from SUPPORTED_CARD_TYPES, which drives the
6525
+ * card icons: it omits the icon-only variants `visa_white` / `eftpos_white`, and the `laser` / `maestro`
6526
+ * identifiers the widget can never resolve — allowing those would be configuration that can never match.
6527
+ */
6528
+ var CARD_SCHEME = {
6529
+ AMEX: 'amex',
6530
+ AUSBC: 'ausbc',
6531
+ DINERS: 'diners',
6532
+ DISCOVER: 'discover',
6533
+ EFTPOS: 'eftpos',
6534
+ JAPCB: 'japcb',
6535
+ MASTERCARD: 'mastercard',
6536
+ SOLO: 'solo',
6537
+ UNIONPAY: 'unionpay',
6538
+ VISA: 'visa'
6539
+ };
6184
6540
  var SUPPORTED_CHECKOUT_META_COLLECTION = [].concat([
6185
6541
  // Paypal
6186
6542
  'brand_name', 'cart_border_color', 'reference', 'email', 'hdr_img', 'logo_img', 'pay_flow_color', 'first_name', 'last_name', 'address_line', 'address_line2', 'address_city', 'address_state', 'address_postcode', 'address_country', 'phone', 'hide_shipping_address'], [
@@ -8365,7 +8721,7 @@ var AfterPayWalletService = /*#__PURE__*/function (_WalletService) {
8365
8721
  }]);
8366
8722
  }(WalletService);
8367
8723
 
8368
- /*! @license DOMPurify 3.4.11 | (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.11/LICENSE */
8724
+ /*! @license DOMPurify 3.4.13 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.13/LICENSE */
8369
8725
 
8370
8726
  var purify_cjs;
8371
8727
  var hasRequiredPurify_cjs;
@@ -8685,7 +9041,7 @@ function requirePurify_cjs () {
8685
9041
  const text = freeze(['#text']);
8686
9042
 
8687
9043
  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
8688
- 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']);
9044
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
8689
9045
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
8690
9046
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
8691
9047
 
@@ -8798,7 +9154,7 @@ function requirePurify_cjs () {
8798
9154
  function createDOMPurify() {
8799
9155
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
8800
9156
  const DOMPurify = root => createDOMPurify(root);
8801
- DOMPurify.version = '3.4.11';
9157
+ DOMPurify.version = '3.4.13';
8802
9158
  DOMPurify.removed = [];
8803
9159
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
8804
9160
  // Not running in a browser, provide a factory function
@@ -8829,6 +9185,7 @@ function requirePurify_cjs () {
8829
9185
  const getAttributes = lookupGetter(ElementPrototype, 'attributes');
8830
9186
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
8831
9187
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
9188
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
8832
9189
  // As per issue #47, the web-components registry is inherited by a
8833
9190
  // new document created via createHTMLDocument. As per the spec
8834
9191
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -9480,6 +9837,13 @@ function requirePurify_cjs () {
9480
9837
  * @param root the in-place root to empty
9481
9838
  */
9482
9839
  const _neutralizeRoot = function _neutralizeRoot(root) {
9840
+ /* Strip every disallowed attribute (on* handlers included) off the whole
9841
+ subtree BEFORE detaching anything. Detaching first would hand back
9842
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
9843
+ whose queued resource event still fires in page scope after we throw.
9844
+ Clobber-safe reads; a doomed clobbered node's own attributes are
9845
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
9846
+ _neutralizeSubtree(root);
9483
9847
  const childNodes = getChildNodes(root);
9484
9848
  if (childNodes) {
9485
9849
  const snapshot = [];
@@ -9607,6 +9971,82 @@ function requirePurify_cjs () {
9607
9971
  }
9608
9972
  }
9609
9973
  };
9974
+ /**
9975
+ * _neutralizePatchLinkage
9976
+ *
9977
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
9978
+ * hardening, https://github.com/WICG/declarative-partial-updates).
9979
+ *
9980
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
9981
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
9982
+ * reaches each node. On a live in-place root that leaves a window: from the
9983
+ * moment the root is connected until the walk arrives at a given node, that
9984
+ * node's linkage is live. A patch applied on connection/stream can fire as
9985
+ * a microtask during the walk and inject or teleport an unsanitized DOM
9986
+ * range into a region the iterator has already passed and will not revisit,
9987
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
9988
+ * whole tree once up front and sever every linkage before the walk begins,
9989
+ * closing that window.
9990
+ *
9991
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
9992
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
9993
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
9994
+ * to group with the rest of the declarative-partial-updates handling and
9995
+ * stay overridable, consistent with the codebase.
9996
+ *
9997
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
9998
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
9999
+ *
10000
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
10001
+ * .html Q1): this mirrors the existing policy of keeping `for` on
10002
+ * <label>/<output>. If the shipping feature can drive a patch through a
10003
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
10004
+ * attribute check at _isBasicCustomElement's caller must additionally drop
10005
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
10006
+ *
10007
+ * @param root the in-place root to sweep
10008
+ */
10009
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
10010
+ if (!SAFE_FOR_XML) {
10011
+ return;
10012
+ }
10013
+ const stack = [root];
10014
+ while (stack.length > 0) {
10015
+ const node = stack.pop();
10016
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
10017
+ /* Remove range markers (the target side of a patch linkage): every
10018
+ processing instruction, and any markup-bearing comment. */
10019
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
10020
+ try {
10021
+ remove(node);
10022
+ } catch (_) {
10023
+ /* Best-effort */
10024
+ }
10025
+ continue;
10026
+ }
10027
+ /* Strip patch-source attributes (the source side) off elements. */
10028
+ if (nodeType === NODE_TYPE.element) {
10029
+ const element = node;
10030
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
10031
+ try {
10032
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
10033
+ element.removeAttribute('patchsrc');
10034
+ }
10035
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
10036
+ element.removeAttribute('for');
10037
+ }
10038
+ } catch (_) {
10039
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
10040
+ }
10041
+ }
10042
+ const childNodes = getChildNodes(node);
10043
+ if (childNodes) {
10044
+ for (let i = childNodes.length - 1; i >= 0; --i) {
10045
+ stack.push(childNodes[i]);
10046
+ }
10047
+ }
10048
+ }
10049
+ };
9610
10050
  /**
9611
10051
  * _initDocument
9612
10052
  *
@@ -9664,7 +10104,17 @@ function requirePurify_cjs () {
9664
10104
  * @return The created NodeIterator
9665
10105
  */
9666
10106
  const _createNodeIterator = function _createNodeIterator(root) {
9667
- return createNodeIterator.call(root.ownerDocument || root, root,
10107
+ /* Read ownerDocument through the cached Node.prototype getter, never the
10108
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
10109
+ clobbering child (<input name="ownerDocument"> or a form-associated
10110
+ external input) shadows the prototype getter and makes a direct read
10111
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
10112
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
10113
+ the walk's fail-closed barrier - leaving the caller's live tree, with
10114
+ any already-armed handler in it, un-neutralized. The cached getter
10115
+ returns the real Document regardless of the clobber. */
10116
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
10117
+ return createNodeIterator.call(doc || root, root,
9668
10118
  // eslint-disable-next-line no-bitwise
9669
10119
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
9670
10120
  };
@@ -9704,7 +10154,11 @@ function requirePurify_cjs () {
9704
10154
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
9705
10155
  var _node$querySelectorAl;
9706
10156
  node.normalize();
9707
- const walker = createNodeIterator.call(node.ownerDocument || node, node,
10157
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
10158
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
10159
+ carry a form-named-getter override of ownerDocument. */
10160
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
10161
+ const walker = createNodeIterator.call(doc || node, node,
9708
10162
  // eslint-disable-next-line no-bitwise
9709
10163
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
9710
10164
  let currentNode = walker.nextNode();
@@ -9857,15 +10311,21 @@ function requirePurify_cjs () {
9857
10311
  /**
9858
10312
  * Handle a node whose tag is forbidden or not allowlisted: keep
9859
10313
  * allowed custom elements (false return exits _sanitizeElements
9860
- * early - namespace/fallback checks and the afterSanitizeElements
9861
- * hook are intentionally skipped for kept custom elements), else
9862
- * hoist content per KEEP_CONTENT and remove.
10314
+ * early - the namespace and fallback-tag removal checks are
10315
+ * intentionally skipped for kept custom elements), else hoist
10316
+ * content per KEEP_CONTENT and remove.
10317
+ *
10318
+ * A kept custom element is the ONLY case in which this function
10319
+ * returns false, so the caller uses that return value to run the
10320
+ * afterSanitizeElements hook on the kept element and keep the
10321
+ * element-hook lifecycle consistent with normal allowlisted
10322
+ * elements (GHSA-c2j3-45gr-mqc4).
9863
10323
  *
9864
10324
  * @param currentNode the disallowed node
9865
10325
  * @param tagName the node's transformCaseFunc'd tag name
9866
10326
  * @return true if the node was removed, false if kept
9867
10327
  */
9868
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
10328
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
9869
10329
  /* Check if we have a custom element to handle */
9870
10330
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
9871
10331
  if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
@@ -9888,27 +10348,26 @@ function requirePurify_cjs () {
9888
10348
  const childNodes = getChildNodes(currentNode);
9889
10349
  if (childNodes && parentNode) {
9890
10350
  const childCount = childNodes.length;
9891
- /* In-place: hoist the *original* children so the iterator visits
9892
- and sanitises them through the same allowlist pass as every other
9893
- node. The caller built the tree in the live document, so the
9894
- originals carry already-queued resource events (`<img onerror>`,
9895
- `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
9896
- those originals detached but still armed, firing in page scope
9897
- while the returned tree looked clean. Moving is safe in-place: the
9898
- root is pre-validated as an allowed tag and so is never the node
9899
- being removed, which keeps `parentNode` inside the iterator root
9900
- and the relocated child inside the serialised tree.
9901
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
9902
- at — and the result serialised from — `body`, so a restrictive
9903
- ALLOWED_TAGS that removes `body` itself must leave its content in
9904
- place, which only cloning does; and those paths parse into an
9905
- inert document, so their discarded originals never had a queued
9906
- event to neutralise.
10351
+ /* Hoist by moving each child up one level rather than deep-cloning
10352
+ it. Moving transfers every descendant exactly once, so a chain of
10353
+ nested disallowed elements costs O(n) instead of the O(n^2) that
10354
+ re-cloning the shrinking subtree at each level produced; it also
10355
+ empties the removed original, so `DOMPurify.removed` no longer
10356
+ pins whole subtrees. Moving preserves the in-place guarantee too:
10357
+ an original carrying already-queued resource events (`<img
10358
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
10359
+ relocated and sanitised rather than left detached but still armed.
10360
+ The sole case that must clone is removing the walk root itself.
10361
+ The result is serialised from the root's subtree, so a restrictive
10362
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
10363
+ leave the content inside it, which only cloning does. In IN_PLACE
10364
+ the root is pre-validated as an allowed tag and so is never removed
10365
+ here, so that path always takes the move branch.
9907
10366
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
9908
10367
  valid whether we move (drops the trailing entry) or clone (leaves
9909
10368
  the list intact). */
9910
10369
  for (let i = childCount - 1; i >= 0; --i) {
9911
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
10370
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
9912
10371
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
9913
10372
  }
9914
10373
  }
@@ -9916,6 +10375,26 @@ function requirePurify_cjs () {
9916
10375
  _forceRemove(currentNode);
9917
10376
  return true;
9918
10377
  };
10378
+ /**
10379
+ * Fork a hook-mutable allowlist off its shared binding the first time a
10380
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
10381
+ * hook cannot widen the per-instance default or the setConfig binding by
10382
+ * reference and leak past the call. Returns the set unchanged once it is
10383
+ * already call-local, so repeated calls across elements are idempotent.
10384
+ *
10385
+ * @param hookList the uponSanitize* hook array for this event
10386
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
10387
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
10388
+ * @param setConfigSet the captured setConfig() binding, or null
10389
+ * @return a call-local clone if a hook is present and set is still shared,
10390
+ * else set unchanged
10391
+ */
10392
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
10393
+ if (hookList.length === 0) {
10394
+ return set;
10395
+ }
10396
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
10397
+ };
9919
10398
  /**
9920
10399
  * _sanitizeElements
9921
10400
  *
@@ -9925,9 +10404,20 @@ function requirePurify_cjs () {
9925
10404
  * @param currentNode to check for permission to exist
9926
10405
  * @return true if node was killed, false if left alive
9927
10406
  */
9928
- const _sanitizeElements = function _sanitizeElements(currentNode) {
10407
+ // eslint-disable-next-line complexity
10408
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
9929
10409
  /* Execute a hook if present */
9930
10410
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
10411
+ /* A hook may have detached the node - treat it as removed (see the
10412
+ detached-node comment after the uponSanitizeElement hook below). On
10413
+ the IN_PLACE path, neutralize the detached subtree first so a queued
10414
+ resource handler on it cannot fire in page scope after we return. */
10415
+ if (currentNode !== root && getParentNode(currentNode) === null) {
10416
+ if (IN_PLACE) {
10417
+ _neutralizeSubtree(currentNode);
10418
+ }
10419
+ return true;
10420
+ }
9931
10421
  /* Check if element is clobbered or can clobber */
9932
10422
  if (_isClobbered(currentNode)) {
9933
10423
  _forceRemove(currentNode);
@@ -9935,11 +10425,47 @@ function requirePurify_cjs () {
9935
10425
  }
9936
10426
  /* Now let's check the element's type and name */
9937
10427
  const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
10428
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
10429
+ hook may have been installed after that guard sampled the hook arrays
10430
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
10431
+ aliasing a shared binding that a widening hook would mutate by
10432
+ reference. Fork it before exposing it to the hook. */
10433
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
9938
10434
  /* Execute a hook if present */
9939
10435
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
9940
10436
  tagName,
9941
10437
  allowedTags: ALLOWED_TAGS
9942
10438
  });
10439
+ /* A hook may have detached the node from the tree — a long-standing
10440
+ user pattern (issue #469; draw.io-style foreignObject filtering).
10441
+ Per the cached, unclobberable parentNode getter the node is
10442
+ genuinely out of the tree, so it can reach neither the serialized
10443
+ output nor an IN_PLACE live tree; treat it as removed and stop
10444
+ processing it. Without this guard, the unsafe-node / namespace
10445
+ checks below would call _forceRemove on a parentless node and hit
10446
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
10447
+ wants gone but *cannot* detach (clobbered / parentless roots), the
10448
+ opposite of a node that is already safely gone. The walk root is
10449
+ exempt: a detached IN_PLACE root is legitimate input and must still
10450
+ be fully sanitized, and a kill-decision on it must keep hitting the
10451
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
10452
+ responsibility for placement: they are not recorded in
10453
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
10454
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
10455
+ can still hold a queued resource-event handler - e.g. an <img onload>
10456
+ that began loading when the caller built the live tree - which fires
10457
+ in page scope after sanitize returns even though the handler never
10458
+ reached the returned tree. That is the audit-5 F1 hazard, and the
10459
+ documented node.remove() hook pattern walks straight into it. So on
10460
+ the IN_PLACE path we neutralize the detached subtree inline here,
10461
+ stripping its non-allow-listed attributes before returning, exactly
10462
+ as the post-walk pass does for _forceRemove'd subtrees. */
10463
+ if (currentNode !== root && getParentNode(currentNode) === null) {
10464
+ if (IN_PLACE) {
10465
+ _neutralizeSubtree(currentNode);
10466
+ }
10467
+ return true;
10468
+ }
9943
10469
  /* Remove mXSS vectors, processing instructions and risky comments */
9944
10470
  if (_isUnsafeNode(currentNode, tagName)) {
9945
10471
  _forceRemove(currentNode);
@@ -9947,7 +10473,22 @@ function requirePurify_cjs () {
9947
10473
  }
9948
10474
  /* Remove element if anything forbids its presence */
9949
10475
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
9950
- return _sanitizeDisallowedNode(currentNode, tagName);
10476
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
10477
+ /* A false return means the node is a custom element kept via
10478
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
10479
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
10480
+ element-hook lifecycle matches normal allowlisted elements: a
10481
+ security policy applied in this hook (e.g. stripping an attribute
10482
+ from every surviving element) must not silently skip kept custom
10483
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
10484
+ tail below - the hook runs, then the walker's subsequent
10485
+ _sanitizeAttributes pass sanitizes the element's attributes. The
10486
+ deliberately skipped namespace and fallback-tag removal checks stay
10487
+ skipped; they are removal decisions, not the hook contract. */
10488
+ if (removed === false) {
10489
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
10490
+ }
10491
+ return removed;
9951
10492
  }
9952
10493
  /* Check whether element has a valid namespace.
9953
10494
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -9994,6 +10535,33 @@ function requirePurify_cjs () {
9994
10535
  if (FORBID_ATTR[lcName]) {
9995
10536
  return false;
9996
10537
  }
10538
+ /* Reject declarative-partial-updates patch-linkage attributes
10539
+ (https://github.com/WICG/declarative-partial-updates).
10540
+ Empirical note (Chrome 150, verified — see
10541
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
10542
+ sanitization. For the string path it fires during sanitize()'s own
10543
+ parse, so the walk sees and sanitizes the fully materialized expanded
10544
+ tree — teleports into MathML/SVG integration points included; a
10545
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
10546
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
10547
+ Either way DOMPurify is NOT blind to the patch.
10548
+ This removal is therefore defense-in-depth rather than the sole barrier:
10549
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
10550
+ in the caller's context, and keeps behaviour deterministic if a future
10551
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
10552
+ anywhere else (notably <template for>) it links the element to a patch
10553
+ target and teleports or removes an arbitrary DOM range by id/marker name.
10554
+ `patchsrc` fetches remote markup and is treated as a script-loading
10555
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
10556
+ other structural-threat checks and stays overridable, consistent with
10557
+ the rest of the codebase. PI range markers are already removed by
10558
+ _isUnsafeNode. */
10559
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
10560
+ return false;
10561
+ }
10562
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
10563
+ return false;
10564
+ }
9997
10565
  /* Make sure attribute cannot clobber */
9998
10566
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
9999
10567
  return false;
@@ -10110,6 +10678,9 @@ function requirePurify_cjs () {
10110
10678
  if (!attributes || _isClobbered(currentNode)) {
10111
10679
  return;
10112
10680
  }
10681
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
10682
+ attribute allowlist off its shared binding before a hook can see it. */
10683
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
10113
10684
  const hookEvent = {
10114
10685
  attrName: '',
10115
10686
  attrValue: '',
@@ -10203,7 +10774,7 @@ function requirePurify_cjs () {
10203
10774
  /* Execute a hook if present */
10204
10775
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
10205
10776
  /* Sanitize tags and elements */
10206
- _sanitizeElements(shadowNode);
10777
+ _sanitizeElements(shadowNode, fragment);
10207
10778
  /* Check attributes next */
10208
10779
  _sanitizeAttributes(shadowNode);
10209
10780
  /* Deep shadow DOM detected.
@@ -10394,6 +10965,11 @@ function requirePurify_cjs () {
10394
10965
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
10395
10966
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
10396
10967
  if (inPlace) {
10968
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
10969
+ linkage across the live tree BEFORE the walk, so no patch can fire
10970
+ mid-walk and inject into an already-processed region. Runs first, so
10971
+ it also covers the forbidden/clobbered roots that throw below. */
10972
+ _neutralizePatchLinkage(dirty);
10397
10973
  /* Do some early pre-sanitization to avoid unsafe root nodes.
10398
10974
  Read nodeName through the cached prototype getter — a clobbering
10399
10975
  child named "nodeName" on the form root would otherwise shadow
@@ -10403,6 +10979,9 @@ function requirePurify_cjs () {
10403
10979
  if (typeof nn === 'string') {
10404
10980
  const tagName = transformCaseFunc(nn);
10405
10981
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
10982
+ /* Fail closed on a live root: neutralize handlers/children before
10983
+ throwing, exactly as the mid-walk abort path does. */
10984
+ _neutralizeRoot(dirty);
10406
10985
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
10407
10986
  }
10408
10987
  }
@@ -10417,6 +10996,10 @@ function requirePurify_cjs () {
10417
10996
  the application unsanitized. Refuse to sanitize such a root
10418
10997
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
10419
10998
  if (_isClobbered(dirty)) {
10999
+ /* Fail closed on a live clobbered root before throwing.
11000
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
11001
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
11002
+ _neutralizeRoot(dirty);
10420
11003
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
10421
11004
  }
10422
11005
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -10469,20 +11052,24 @@ function requirePurify_cjs () {
10469
11052
  _forceRemove(body.firstChild);
10470
11053
  }
10471
11054
  /* Get node iterator */
10472
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
11055
+ const walkRoot = inPlace ? dirty : body;
10473
11056
  /* Now start iterating over the created document.
10474
11057
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
10475
11058
  engine/custom-element mutation can detach a node mid-walk so
10476
11059
  `_forceRemove`'s parentless guard throws, aborting the loop. Without the
10477
11060
  barrier the caller's in-place tree would be left half-sanitized with the
10478
- unvisited tail still armed. On any throw we fail closed — strip the
10479
- in-place root bare — then rethrow so the existing throw contract is
10480
- preserved. (String/DOM-copy paths never return the partial body, so the
10481
- propagating throw is already fail-closed there.) */
11061
+ unvisited tail still armed. _createNodeIterator itself is inside the
11062
+ barrier too: constructing the iterator dereferences the root's document,
11063
+ and any failure there (e.g. an exotic/clobbered root) must still fail
11064
+ closed rather than skip the neutralize. On any throw we fail closed -
11065
+ strip the in-place root bare - then rethrow so the existing throw
11066
+ contract is preserved. (String/DOM-copy paths never return the partial
11067
+ body, so the propagating throw is already fail-closed there.) */
10482
11068
  try {
11069
+ const nodeIterator = _createNodeIterator(walkRoot);
10483
11070
  while (currentNode = nodeIterator.nextNode()) {
10484
11071
  /* Sanitize tags and elements */
10485
- _sanitizeElements(currentNode);
11072
+ _sanitizeElements(currentNode, walkRoot);
10486
11073
  /* Check attributes next */
10487
11074
  _sanitizeAttributes(currentNode);
10488
11075
  /* Shadow DOM detected, sanitize it.
@@ -10496,6 +11083,14 @@ function requirePurify_cjs () {
10496
11083
  } catch (error) {
10497
11084
  if (inPlace) {
10498
11085
  _neutralizeRoot(dirty);
11086
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
11087
+ detached from the root, so _neutralizeRoot's subtree pass does not
11088
+ reach them. Defuse them too, mirroring the success-path loop below. */
11089
+ arrayForEach(DOMPurify.removed, entry => {
11090
+ if (entry.element) {
11091
+ _neutralizeSubtree(entry.element);
11092
+ }
11093
+ });
10499
11094
  }
10500
11095
  throw error;
10501
11096
  }
@@ -14922,6 +15517,54 @@ var MultiWidget = /*#__PURE__*/function () {
14922
15517
  validate_card_types: validateCardNumberInput
14923
15518
  });
14924
15519
  }
15520
+ /**
15521
+ * Current method restricts which card schemes the card number input accepts, without changing which card
15522
+ * icons the widget displays. Scheme identifiers are matched case-insensitively and ignoring surrounding
15523
+ * whitespace; unsupported identifiers are ignored with a warning, so a list in which every identifier is
15524
+ * unsupported applies no restriction at all.
15525
+ *
15526
+ * Takes precedence over setSupportedCardIcons: when a non-empty list is supplied here, it is the only list
15527
+ * used for validation and the validateCardNumberInput flag of setSupportedCardIcons no longer affects it.
15528
+ * Calling this method more than once replaces the previous list rather than merging with it.
15529
+ *
15530
+ * Must be called before load().
15531
+ *
15532
+ * @example
15533
+ *
15534
+ * widget.setAllowedCardSchemes(['visa', 'mastercard']);
15535
+ *
15536
+ * @param {string[]} schemes - [CARD_SCHEME]{@link CARD_SCHEME}
15537
+ */
15538
+ }, {
15539
+ key: "setAllowedCardSchemes",
15540
+ value: function setAllowedCardSchemes(schemes) {
15541
+ var vocabulary = ObjectHelper.values(CARD_SCHEME);
15542
+ var allowedSchemes = [];
15543
+ var _iterator = _createForOfIteratorHelper(Array.isArray(schemes) ? schemes : []),
15544
+ _step;
15545
+ try {
15546
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
15547
+ var entry = _step.value;
15548
+ var scheme = typeof entry === 'string' ? entry.trim().toLowerCase() : '';
15549
+ if (vocabulary.indexOf(scheme) === -1) {
15550
+ console.warn("Widget::setAllowedCardSchemes: unsupported card scheme ".concat(entry));
15551
+ continue;
15552
+ }
15553
+ if (allowedSchemes.indexOf(scheme) === -1) {
15554
+ allowedSchemes.push(scheme);
15555
+ }
15556
+ }
15557
+ } catch (err) {
15558
+ _iterator.e(err);
15559
+ } finally {
15560
+ _iterator.f();
15561
+ }
15562
+ if (allowedSchemes.length) {
15563
+ this.link.setParams({
15564
+ allowed_card_schemes: allowedSchemes.join(',')
15565
+ });
15566
+ }
15567
+ }
14925
15568
  /**
14926
15569
  * Current method can hide prevent the widget from showing the error messages
14927
15570
  *
@@ -46167,4 +46810,4 @@ var browser = /*#__PURE__*/Object.freeze({
46167
46810
  zlibSync: zlibSync
46168
46811
  });
46169
46812
 
46170
- export { AfterpayCheckoutButton, AfterpayOnSiteMessaging, Api, ApplePayOpenWalletButton, ApplePayWalletButtonExpress, CANVAS_3DS_EVENT, CHECKOUT_BUTTON_EVENT, EVENT$3 as CHECKOUT_EVENT, Canvas3ds, Canvas3dsTokenFormat, Checkout, ClickToPay, Configuration, ContactShippingEditingMode, ELEMENT, ERROR_OPERATION, EVENT$5 as EVENT, Builder$1 as ExternalCheckoutBuilder, Checker as ExternalCheckoutChecker, FORM_FIELD$1 as FORM_FIELD, FRAUD_PREVENTION_EVENTS, FraudPreventionService, GooglePayOpenWalletButton, HtmlMultiWidget, HtmlPaymentSourceWidget, HtmlWidget, InstructionDebugger, MultiWidget, PAYMENT_EXPIRED, PAYMENT_FAILURE, PAYMENT_SUCCESSFUL, PAYMENT_TYPE, PURPOSE, PayPalDataCollector, PayPalSavePaymentSourceWidget, Builder as PaymentSourceBuilder, PaymentSourceWidget, PaypalCheckoutButton, PaypalWalletButtonExpress, STYLABLE_ELEMENT, STYLABLE_ELEMENT_STATE, STYLE$2 as STYLE, SUPPORTED_CARD_TYPES, TEXT, TOKEN_TYPE, TRIGGER$1 as TRIGGER, TYPE, VAULT_DISPLAY_STYLE, VaultDisplayWidget, EVENT as WALLET_BUTTONS_EXPRESS_EVENT, WALLET_TYPES, WalletButtons, ZipmoneyCheckoutButton };
46813
+ export { AfterpayCheckoutButton, AfterpayOnSiteMessaging, Api, ApplePayOpenWalletButton, ApplePayWalletButtonExpress, CANVAS_3DS_EVENT, CARD_SCHEME, CHECKOUT_BUTTON_EVENT, EVENT$3 as CHECKOUT_EVENT, Canvas3ds, Canvas3dsTokenFormat, Checkout, ClickToPay, Configuration, ContactShippingEditingMode, ELEMENT, ERROR_OPERATION, EVENT$5 as EVENT, Builder$1 as ExternalCheckoutBuilder, Checker as ExternalCheckoutChecker, FORM_FIELD$1 as FORM_FIELD, FRAUD_PREVENTION_EVENTS, FraudPreventionService, GooglePayOpenWalletButton, HtmlMultiWidget, HtmlPaymentSourceWidget, HtmlWidget, InstructionDebugger, MultiWidget, PAYMENT_EXPIRED, PAYMENT_FAILURE, PAYMENT_SUCCESSFUL, PAYMENT_TYPE, PURPOSE, PayPalDataCollector, PayPalSavePaymentSourceWidget, Builder as PaymentSourceBuilder, PaymentSourceWidget, PaypalCheckoutButton, PaypalWalletButtonExpress, STYLABLE_ELEMENT, STYLABLE_ELEMENT_STATE, STYLE$2 as STYLE, SUPPORTED_CARD_TYPES, TEXT, TOKEN_TYPE, TRIGGER$1 as TRIGGER, TYPE, VAULT_DISPLAY_STYLE, VaultDisplayWidget, EVENT as WALLET_BUTTONS_EXPRESS_EVENT, WALLET_TYPES, WalletButtons, ZipmoneyCheckoutButton };