@paydock/client-sdk 1.145.0-beta → 1.148.0-beta

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 +935 -103
  3. package/bundles/index.cjs.d.ts +629 -336
  4. package/bundles/index.mjs +935 -104
  5. package/bundles/index.mjs.d.ts +629 -336
  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 +4 -3
  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 +65 -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 +34 -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 +151 -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 +16 -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 +935 -103
  33. package/bundles/widget.umd.js.d.ts +629 -336
  34. package/bundles/widget.umd.js.min.d.ts +629 -336
  35. package/bundles/widget.umd.min.js +1 -1
  36. package/docs/api-canvas3ds.md +41 -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 +20 -11
  43. package/slate.md +84 -1
@@ -1869,7 +1869,7 @@
1869
1869
  version: 'x-sdk-version',
1870
1870
  type: 'x-sdk-type'
1871
1871
  });
1872
- SDK._version = 'v1.145.0-beta';
1872
+ SDK._version = 'v1.148.0-beta';
1873
1873
 
1874
1874
  var ACCESS_HEADER_NAME = {
1875
1875
  accessToken: 'x-access-token',
@@ -4725,6 +4725,15 @@
4725
4725
  this.env.setEnv(env, alias);
4726
4726
  return this;
4727
4727
  }
4728
+ /**
4729
+ * Origin (`scheme://host[:port]`) of the API this client talks to, for the current env/alias.
4730
+ * Used to validate the `origin` of postMessages the API itself sends to the page.
4731
+ */
4732
+ }, {
4733
+ key: "getApiOrigin",
4734
+ value: function getApiOrigin() {
4735
+ return ApiBase.toOrigin(this.env.getConf().url);
4736
+ }
4728
4737
  }, {
4729
4738
  key: "requestObservable",
4730
4739
  value: function requestObservable(method, url, requestBody) {
@@ -4937,6 +4946,18 @@
4937
4946
  environment: this.env.getEnv()
4938
4947
  };
4939
4948
  }
4949
+ }], [{
4950
+ key: "toOrigin",
4951
+ value: function toOrigin(url) {
4952
+ try {
4953
+ return new URL(url).origin;
4954
+ } catch (_a) {
4955
+ // Not a parsable URL: the returned string is not an origin and will never match a
4956
+ // MessageEvent.origin — surface the misconfiguration instead of failing silently.
4957
+ console.warn("[paydock/client-sdk] cannot derive an origin from configured URL \"".concat(url, "\""));
4958
+ return url.replace(/\/+$/, '');
4959
+ }
4960
+ }
4940
4961
  }]);
4941
4962
  }();
4942
4963
 
@@ -5014,6 +5035,7 @@
5014
5035
  value: function walletCallback(payload) {
5015
5036
  return this.api.getClientPromise('POST', WALLET_CALLBACK_LINK).send(payload);
5016
5037
  }
5038
+ /** Two-step mode: runs the deferred Standalone 3DS authentication (AReq). Charge-token auth, like `standalone3dsHandle`. */
5017
5039
  }, {
5018
5040
  key: "standalone3dsProcess",
5019
5041
  value: function standalone3dsProcess(payload) {
@@ -5157,6 +5179,10 @@
5157
5179
  _classCallCheck(this, IFrame);
5158
5180
  this.container = container;
5159
5181
  }
5182
+ /**
5183
+ * @param options.onLoad Called on every `load` of the iframe (initial document and each
5184
+ * subsequent navigation). Attached before `src` is set so a fast load is never missed.
5185
+ */
5160
5186
  return _createClass(IFrame, [{
5161
5187
  key: "load",
5162
5188
  value: function load(link) {
@@ -5165,6 +5191,9 @@
5165
5191
  if (!this.container.isExist() || this.isExist()) return;
5166
5192
  var iFrame = document.createElement('iframe');
5167
5193
  if (options.title) iFrame.title = options.title;
5194
+ if (options.onLoad) {
5195
+ iFrame.addEventListener('load', options.onLoad);
5196
+ }
5168
5197
  if (iframeClass) iFrame.classList.add(iframeClass);
5169
5198
  // Set additional attributes before src and DOM insertion so that
5170
5199
  // Permissions-Policy (e.g. allow="payment") is in place when the
@@ -5305,14 +5334,14 @@
5305
5334
  data = JSON.parse(event.data);
5306
5335
  } catch (error) {}
5307
5336
  if (!data) return;
5308
- _this.emit(data);
5337
+ _this.emit(data, event.origin);
5309
5338
  });
5310
5339
  }
5311
5340
  return _createClass(IFrameEvent, [{
5312
5341
  key: "emit",
5313
- value: function emit(data) {
5342
+ value: function emit(data, origin) {
5314
5343
  for (var key in this.listeners) {
5315
- if (this.listeners[key].event === data.event && data.widget_id === this.listeners[key].widget_id) this.listeners[key].listener.apply(this, [data]);
5344
+ if (this.listeners[key].event === data.event && data.widget_id === this.listeners[key].widget_id) this.listeners[key].listener.apply(this, [data, origin]);
5316
5345
  }
5317
5346
  }
5318
5347
  }, {
@@ -5372,6 +5401,51 @@
5372
5401
  IFRAME_STATUS["TIMEOUT"] = "AuthTimedOut";
5373
5402
  IFRAME_STATUS["INVALID"] = "invalid_event";
5374
5403
  })(IFRAME_STATUS || (IFRAME_STATUS = {}));
5404
+ /**
5405
+ * Maps a `/process` rejection onto the error object the one-step callback delivers for the same outcome
5406
+ * (`{ error_code, error_description, gateway_specific_description, process_error_code }`), so `error`
5407
+ * carries the same payload in both flows. Absent fields are left out.
5408
+ */
5409
+ function toOutcomeError(err) {
5410
+ var _a, _b;
5411
+ if (err instanceof Error) {
5412
+ // no API envelope to map (the body was not the API's JSON): report it as unreachable, so the
5413
+ // merchant can tell this retryable case apart from a final API answer by `error_code` alone
5414
+ return {
5415
+ error_code: ERROR_CAUSE.SERVICE_UNREACHABLE,
5416
+ error_description: 'Network request failed: the service did not answer with a valid response.'
5417
+ };
5418
+ }
5419
+ var apiError = err !== null && err !== void 0 ? err : {};
5420
+ var gateway = (_b = (_a = apiError.details) === null || _a === void 0 ? void 0 : _a.messages) === null || _b === void 0 ? void 0 : _b[0];
5421
+ var fields = {
5422
+ error_code: apiError.code,
5423
+ error_description: apiError.message,
5424
+ gateway_specific_description: gateway === null || gateway === void 0 ? void 0 : gateway.gateway_specific_description,
5425
+ process_error_code: gateway === null || gateway === void 0 ? void 0 : gateway.gateway_specific_code
5426
+ };
5427
+ var outcomeError = {};
5428
+ for (var _i = 0, _Object$keys = Object.keys(fields); _i < _Object$keys.length; _i++) {
5429
+ var key = _Object$keys[_i];
5430
+ if (fields[key]) {
5431
+ outcomeError[key] = fields[key];
5432
+ }
5433
+ }
5434
+ return outcomeError;
5435
+ }
5436
+ /**
5437
+ * `true` when `/process` produced no API answer: the request never got a response (network, timeout, abort)
5438
+ * or the response body was not the API's JSON (e.g. an HTML page from a proxy in between).
5439
+ */
5440
+ function isTransportFailure(err) {
5441
+ if (err instanceof Error) {
5442
+ return true;
5443
+ }
5444
+ var code = err === null || err === void 0 ? void 0 : err.code;
5445
+ return code === ERROR_CAUSE.SERVICE_UNREACHABLE || code === ERROR_CAUSE.ABORTED;
5446
+ }
5447
+ /** Callback `process_status` of a two-step charge: fingerprint done, authentication not started. */
5448
+ var PROCESS_STATUS_DEFERRED = 'deferred';
5375
5449
  /**
5376
5450
  * Event name constants emitted during the Standalone 3DS (GPayments) flow.
5377
5451
  * Used internally by `GPaymentsService` and re-exported publicly as `CANVAS_3DS_EVENT`.
@@ -5385,6 +5459,25 @@
5385
5459
  DECOUPLED: 'chargeAuthDecoupled',
5386
5460
  /** Challenge required — issuer demands user interaction. Payload: `Canvas3dsAuthPayload`. */
5387
5461
  CHALLENGE: 'chargeAuthChallenge',
5462
+ /**
5463
+ * Two-step mode only: fingerprint done, SDK waiting for `Canvas3ds.authenticate()`. Emitted once, before any
5464
+ * outcome event (`INFO` may precede it); never for a one-step charge. Payload: `Canvas3dsReadyPayload`.
5465
+ */
5466
+ READY: 'chargeAuthReady',
5467
+ /**
5468
+ * The challenge iframe fired its FIRST `load` after `chargeAuthChallenge` (the ACS page has painted),
5469
+ * or the `CHALLENGE_LOADED_FALLBACK_MS` safety fallback elapsed without one. Emitted exactly once per
5470
+ * challenge; never emitted for a decoupled challenge (no challenge iframe). Payload: `Canvas3dsChallengeLoadedPayload`.
5471
+ */
5472
+ CHALLENGE_LOADED: 'chargeAuthChallengeLoaded',
5473
+ /**
5474
+ * The issuer delivered the final authentication result to the 3DS Server (the shopper finished the
5475
+ * challenge, or approved a decoupled authentication) and the SDK is now obtaining the outcome. Emitted at
5476
+ * most once per authentication, always before the terminal event. Detected through the `secondary_url`
5477
+ * result monitor (`source: 'poll'`) or, when the callback message arrives first, right before the outcome
5478
+ * it carries (`source: 'callback'`). Payload: `Canvas3dsChallengeCompletedPayload`.
5479
+ */
5480
+ CHALLENGE_COMPLETED: 'chargeAuthChallengeCompleted',
5388
5481
  /** Informational signal emitted by the GPayments flow. Internal — not part of the public typed surface. */
5389
5482
  INFO: 'chargeAuthInfo',
5390
5483
  /** Error during the flow (network, parsing, etc.). Payload: `Canvas3dsErrorPayload`. */
@@ -5396,13 +5489,64 @@
5396
5489
  * backwards-compatible internal usage.
5397
5490
  */
5398
5491
  var CANVAS_3DS_EVENT = GPAYMENTS_EVENT;
5492
+ /**
5493
+ * Upper bound on how long `chargeAuthChallengeLoaded` waits for the challenge iframe's first `load`.
5494
+ * When it elapses the event is emitted with `reason: 'timeout'` so a merchant loader is never held
5495
+ * forever; the real `load` arriving afterwards is the paint and is simply ignored.
5496
+ */
5497
+ var CHALLENGE_LOADED_FALLBACK_MS = 2500;
5498
+ /**
5499
+ * Interval of the `secondary_url` (GPayments `resultMonUrl`) poll while an ACS challenge is on screen.
5500
+ * The monitor flips to `AuthResultReady` when the issuer's RReq reaches the 3DS Server — before the ACS has
5501
+ * even returned its final page to the browser — so this poll is what makes `chargeAuthChallengeCompleted`
5502
+ * land before the shopper sees any post-submit page. GPayments recommends 2 s and leaves the interval to the
5503
+ * requestor; 1 s matches the cadence of GPayments' own monitoring frame.
5504
+ */
5505
+ var CHALLENGE_RESULT_POLL_INTERVAL_MS = 1000;
5506
+ /**
5507
+ * Interval of the `secondary_url` poll for a decoupled challenge (shopper approves out-of-band, possibly
5508
+ * minutes later; nothing to cover on screen) — the GPayments-recommended 2 s.
5509
+ */
5510
+ var DECOUPLED_RESULT_POLL_INTERVAL_MS = 2000;
5511
+ /** The result-monitor request failed at transport level (network, abort, timeout) — retried, not fatal. */
5512
+ var ResultMonitorTransportError = /*#__PURE__*/function (_Error) {
5513
+ function ResultMonitorTransportError() {
5514
+ var _this;
5515
+ _classCallCheck(this, ResultMonitorTransportError);
5516
+ _this = _callSuper(this, ResultMonitorTransportError, ['Standalone 3DS result monitor request failed']);
5517
+ _this.name = 'ResultMonitorTransportError';
5518
+ return _this;
5519
+ }
5520
+ _inherits(ResultMonitorTransportError, _Error);
5521
+ return _createClass(ResultMonitorTransportError);
5522
+ }(/*#__PURE__*/_wrapNativeSuper(Error));
5399
5523
  var GPaymentsService = /*#__PURE__*/function () {
5400
5524
  function GPaymentsService(container, api, eventEmitter) {
5525
+ var trustedOrigins = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
5401
5526
  _classCallCheck(this, GPaymentsService);
5402
5527
  this.container = container;
5403
5528
  this.api = api;
5404
5529
  this.eventEmitter = eventEmitter;
5530
+ this.trustedOrigins = trustedOrigins;
5405
5531
  this.resultRead = false;
5532
+ /** `chargeAuthChallengeLoaded` already emitted for the current challenge (by `load` or by the fallback). */
5533
+ this.challengeLoadedEmitted = false;
5534
+ /** `chargeAuthChallengeCompleted` already emitted for the current authentication (poll or callback). */
5535
+ this.challengeCompletedEmitted = false;
5536
+ /** Set once a terminal event was emitted: no lifecycle event may follow (reset when a new challenge starts). */
5537
+ this.challengeLifecycleClosed = false;
5538
+ this.challengeLoadedTimer = null;
5539
+ this.resultPollTimer = null;
5540
+ /** One-shot guard for the `chargeAuth` leg — the callback message may be delivered more than once. */
5541
+ this.authHandled = false;
5542
+ /** Two-step mode: `charge_3ds_id` of the deferred callback message; `null` until it arrives (always for one-step). */
5543
+ this.deferredCharge3dsId = null;
5544
+ /** `authenticate()` latch; consumed when the deferred message arrives, reset only after a `/process` transport failure. */
5545
+ this.authenticateRequested = false;
5546
+ /** One `/process` call per widget instance (a transport failure allows one more `authenticate()`). */
5547
+ this.authenticationStarted = false;
5548
+ /** The single `/process` retry after a transport failure has been granted; the next failure is final. */
5549
+ this.authenticationRetryUsed = false;
5406
5550
  this.iFrameEvent = new IFrameEvent(window);
5407
5551
  }
5408
5552
  return _createClass(GPaymentsService, [{
@@ -5422,6 +5566,7 @@
5422
5566
  key: "initializeIFrames",
5423
5567
  value: function initializeIFrames(initializationUrl, secondaryUrl, title) {
5424
5568
  var hideAuthorization = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
5569
+ var onLoad = arguments.length > 4 ? arguments[4] : undefined;
5425
5570
  var container = this.container.getElement();
5426
5571
  if (!container) {
5427
5572
  return;
@@ -5432,7 +5577,8 @@
5432
5577
  this.browserAndChallengeContainer = new Container('#paydock_authorization_iframe');
5433
5578
  this.iFrameAuthorization = new IFrame(this.browserAndChallengeContainer);
5434
5579
  this.iFrameAuthorization.load(initializationUrl, {
5435
- title: title
5580
+ title: title,
5581
+ onLoad: onLoad
5436
5582
  });
5437
5583
  if (secondaryUrl) {
5438
5584
  var divSecondaryURL = document.createElement('div');
@@ -5466,25 +5612,250 @@
5466
5612
  }, {
5467
5613
  key: "setupIFrameEvents",
5468
5614
  value: function setupIFrameEvents(widgetId) {
5469
- var _this = this;
5470
- this.iFrameEvent.on(EVENT$4.CHARGE_AUTH, widgetId, function (data) {
5615
+ var _this2 = this;
5616
+ this.iFrameEvent.on(EVENT$4.CHARGE_AUTH, widgetId, function (data, origin) {
5471
5617
  if (data.status === IFRAME_STATUS.SKIPPED || data.status === IFRAME_STATUS.FINISHED) {
5618
+ // this channel is spoofable — any frame that knows the charge_3ds_id can post to it.
5619
+ // A message from an unknown sender must never terminate a live authentication: ignore it.
5620
+ if (!_this2.isTrustedOrigin(origin)) {
5621
+ return;
5622
+ }
5623
+ // one-shot: the callback message can be delivered more than once (it is posted to both
5624
+ // `parent` and `top`) — a duplicate must not re-emit events or duplicate challenge iframes
5625
+ if (_this2.authHandled) {
5626
+ return;
5627
+ }
5628
+ _this2.authHandled = true;
5472
5629
  if (data.info) {
5473
- _this.eventEmitter.emit(GPAYMENTS_EVENT.INFO, {
5630
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.INFO, {
5474
5631
  info: data.info
5475
5632
  });
5476
5633
  }
5477
- _this.performAuthentication(data);
5634
+ _this2.removeIframes();
5635
+ if (data.process_status === PROCESS_STATUS_DEFERRED) {
5636
+ // two-step: announce readiness; authenticate now only if `authenticate()` was already called
5637
+ _this2.deferredCharge3dsId = data.charge_3ds_id;
5638
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.READY, {
5639
+ charge_3ds_id: data.charge_3ds_id
5640
+ });
5641
+ if (_this2.authenticateRequested) {
5642
+ _this2.performAuthentication(data.charge_3ds_id);
5643
+ }
5644
+ } else if (typeof data.process_status === 'string') {
5645
+ // the API already ran the authentication inside the callback: no /process call needed
5646
+ _this2.handleAuthenticationOutcome({
5647
+ status: data.process_status,
5648
+ result: data.result,
5649
+ error: data.error
5650
+ }, data.charge_3ds_id);
5651
+ } else {
5652
+ // The API always runs the authentication inside the callback (PAYRAC-2480). A trusted
5653
+ // message without the outcome means an outdated API — fail, never verify via /process.
5654
+ _this2.finishChallengeLifecycle();
5655
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this2.parseError({
5656
+ message: 'Standalone 3DS authentication outcome missing from the callback message'
5657
+ }, data.charge_3ds_id));
5658
+ }
5478
5659
  } else if (data.status === IFRAME_STATUS.TIMEOUT || data.status === IFRAME_STATUS.INVALID) {
5479
- _this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this.parseHandleResponse({
5660
+ _this2.finishChallengeLifecycle();
5661
+ _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this2.parseHandleResponse({
5480
5662
  status: data.status
5481
5663
  }, data.charge_3ds_id));
5482
5664
  }
5483
5665
  });
5484
- this.iFrameEvent.on(EVENT$4.CHARGE_AUTH_SUCCESS, widgetId, function (data) {
5485
- _this.processResult(data.charge_3ds_id);
5666
+ this.iFrameEvent.on(EVENT$4.CHARGE_AUTH_SUCCESS, widgetId, function (data, origin) {
5667
+ // the callback iframe reached Paydock with `AuthResultReady`: the issuer has answered. Announce it
5668
+ // before the outcome so merchants always see completed → terminal, whichever transport won.
5669
+ _this2.emitChallengeCompleted(data.charge_3ds_id, 'callback');
5670
+ if (_this2.hasChainedOutcome(data, origin)) {
5671
+ // the API already fetched the challenge result inside the callback: no GET /handle needed
5672
+ _this2.handleResultOutcome({
5673
+ status: data.process_status,
5674
+ result: data.result,
5675
+ error: data.error
5676
+ }, data.charge_3ds_id);
5677
+ } else {
5678
+ _this2.processResult(data.charge_3ds_id);
5679
+ }
5486
5680
  });
5487
5681
  }
5682
+ /**
5683
+ * Two-step mode: run the deferred authentication (`POST /standalone-3ds/process`). Never throws: runs now if
5684
+ * the deferred message already arrived, otherwise as soon as it does; a no-op on one-step charges, repeated
5685
+ * calls or after a terminal outcome. The outcome flows through the usual events.
5686
+ */
5687
+ }, {
5688
+ key: "authenticate",
5689
+ value: function authenticate() {
5690
+ if (this.authenticateRequested) {
5691
+ return;
5692
+ }
5693
+ this.authenticateRequested = true;
5694
+ if (this.deferredCharge3dsId !== null) {
5695
+ this.performAuthentication(this.deferredCharge3dsId);
5696
+ }
5697
+ }
5698
+ /**
5699
+ * Calls `/process` once per instance (reachable from both the deferred message and `authenticate()`). The
5700
+ * response has the callback-outcome shape, so the challenge / decoupled / result legs are shared with
5701
+ * one-step. A rejection surfaces as `error`, with the same error object shape as the one-step callback
5702
+ * (`toOutcomeError`). An API answer (stale session, rejected authentication…) is terminal. The first transport
5703
+ * failure is not: the AReq may have run with its response lost, so the latches are reset and the merchant may
5704
+ * call `authenticate()` once more — `/process` is idempotent and replays the stored outcome. A failure of that
5705
+ * retry, of any kind, is final.
5706
+ */
5707
+ }, {
5708
+ key: "performAuthentication",
5709
+ value: function performAuthentication(charge3dsId) {
5710
+ var _this3 = this;
5711
+ if (this.authenticationStarted) {
5712
+ return;
5713
+ }
5714
+ this.authenticationStarted = true;
5715
+ this.api.charge().standalone3dsProcess({
5716
+ charge_3ds_id: charge3dsId
5717
+ }).then(function (authenticationResult) {
5718
+ return _this3.handleAuthenticationOutcome(authenticationResult, charge3dsId);
5719
+ }, function (err) {
5720
+ if (isTransportFailure(err) && !_this3.authenticationRetryUsed) {
5721
+ _this3.authenticationRetryUsed = true;
5722
+ _this3.authenticateRequested = false;
5723
+ _this3.authenticationStarted = false;
5724
+ } else {
5725
+ _this3.resultRead = true;
5726
+ _this3.finishChallengeLifecycle();
5727
+ }
5728
+ _this3.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this3.parseError(toOutcomeError(err), charge3dsId));
5729
+ });
5730
+ }
5731
+ /**
5732
+ * A message carries a processed outcome only when it has `process_status` AND comes from a trusted
5733
+ * origin. Used by the result leg: an untrusted `process_status` is never acted upon — the SDK falls
5734
+ * back to `GET /handle` (still needed for decoupled challenges / polling). The authentication leg
5735
+ * ignores untrusted senders outright, before this check.
5736
+ */
5737
+ }, {
5738
+ key: "hasChainedOutcome",
5739
+ value: function hasChainedOutcome(data, origin) {
5740
+ return typeof data.process_status === 'string' && this.isTrustedOrigin(origin);
5741
+ }
5742
+ }, {
5743
+ key: "isTrustedOrigin",
5744
+ value: function isTrustedOrigin(origin) {
5745
+ // 'null' is the serialized opaque origin (e.g. a sandboxed iframe) — never trust it, even if a
5746
+ // misconfigured env URL were ever to put the literal string 'null' into the trusted list
5747
+ return typeof origin === 'string' && origin !== 'null' && this.trustedOrigins.includes(origin);
5748
+ }
5749
+ }, {
5750
+ key: "removeIframes",
5751
+ value: function removeIframes() {
5752
+ var _a, _b;
5753
+ this.clearChallengeLoadedTimer();
5754
+ (_a = this.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.remove();
5755
+ (_b = this.iFrameSecondaryUrl) === null || _b === void 0 ? void 0 : _b.remove();
5756
+ }
5757
+ }, {
5758
+ key: "clearChallengeLoadedTimer",
5759
+ value: function clearChallengeLoadedTimer() {
5760
+ if (this.challengeLoadedTimer !== null) {
5761
+ clearTimeout(this.challengeLoadedTimer);
5762
+ this.challengeLoadedTimer = null;
5763
+ }
5764
+ }
5765
+ /**
5766
+ * Challenge iframe lifecycle (`chargeAuthChallengeLoaded`).
5767
+ *
5768
+ * The challenge iframe is cross-origin, but its `load` event still fires when the ACS page finishes loading:
5769
+ * the first one means the page painted (merchants can drop their loader). A fallback timer
5770
+ * (`armChallengeLoadedFallback`) guarantees `chargeAuthChallengeLoaded` within `CHALLENGE_LOADED_FALLBACK_MS`
5771
+ * even if no `load` ever fires; a late paint after that is simply ignored. Later `load`s are NOT a usable
5772
+ * signal for "the shopper submitted": the post-submit hops are HTTP redirects and self-submitting pages whose
5773
+ * loads abort, and the final callback page posts its message before its own `load` — so the post-submit
5774
+ * moment is `chargeAuthChallengeCompleted`, driven by the `secondary_url` result monitor instead.
5775
+ *
5776
+ * Nothing is emitted once the flow reached a terminal outcome (`finishChallengeLifecycle`), and a
5777
+ * decoupled challenge (no challenge iframe) never emits `chargeAuthChallengeLoaded`.
5778
+ *
5779
+ * Resets the per-challenge state and returns the `load` handler to attach to the challenge iframe.
5780
+ */
5781
+ }, {
5782
+ key: "startChallengeLifecycle",
5783
+ value: function startChallengeLifecycle(charge3dsId) {
5784
+ var _this4 = this;
5785
+ this.clearChallengeLoadedTimer();
5786
+ this.challengeLoadedEmitted = false;
5787
+ this.challengeCompletedEmitted = false;
5788
+ this.challengeLifecycleClosed = false;
5789
+ return function () {
5790
+ var _a;
5791
+ if (_this4.challengeLifecycleClosed || _this4.challengeLoadedEmitted || !((_a = _this4.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.isExist())) {
5792
+ return;
5793
+ }
5794
+ _this4.clearChallengeLoadedTimer();
5795
+ _this4.emitChallengeLoaded(charge3dsId, 'load');
5796
+ };
5797
+ }
5798
+ /**
5799
+ * Arms the `chargeAuthChallengeLoaded` fallback — only when a challenge iframe actually exists (the
5800
+ * container may be gone, or `IFrame.load` may have no-op'd on an already-present iframe).
5801
+ */
5802
+ }, {
5803
+ key: "armChallengeLoadedFallback",
5804
+ value: function armChallengeLoadedFallback(charge3dsId) {
5805
+ var _this5 = this;
5806
+ var _a;
5807
+ if (!((_a = this.iFrameAuthorization) === null || _a === void 0 ? void 0 : _a.isExist())) {
5808
+ return;
5809
+ }
5810
+ this.challengeLoadedTimer = setTimeout(function () {
5811
+ _this5.challengeLoadedTimer = null;
5812
+ if (_this5.challengeLifecycleClosed || _this5.challengeLoadedEmitted) {
5813
+ return;
5814
+ }
5815
+ _this5.emitChallengeLoaded(charge3dsId, 'timeout');
5816
+ }, CHALLENGE_LOADED_FALLBACK_MS);
5817
+ }
5818
+ }, {
5819
+ key: "emitChallengeLoaded",
5820
+ value: function emitChallengeLoaded(charge3dsId, reason) {
5821
+ this.challengeLoadedEmitted = true;
5822
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE_LOADED, {
5823
+ charge_3ds_id: charge3dsId,
5824
+ reason: reason
5825
+ });
5826
+ }
5827
+ /**
5828
+ * `chargeAuthChallengeCompleted` — once per authentication, never after a terminal event. Idempotent so the
5829
+ * poll and the callback message can both call it: the first transport to notice the issuer's result wins.
5830
+ */
5831
+ }, {
5832
+ key: "emitChallengeCompleted",
5833
+ value: function emitChallengeCompleted(charge3dsId, source) {
5834
+ if (this.challengeLifecycleClosed || this.challengeCompletedEmitted) {
5835
+ return;
5836
+ }
5837
+ this.challengeCompletedEmitted = true;
5838
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE_COMPLETED, {
5839
+ charge_3ds_id: charge3dsId,
5840
+ source: source
5841
+ });
5842
+ }
5843
+ /** Call before emitting any terminal event: no lifecycle event may follow it, and the result poll stops. */
5844
+ }, {
5845
+ key: "finishChallengeLifecycle",
5846
+ value: function finishChallengeLifecycle() {
5847
+ this.clearChallengeLoadedTimer();
5848
+ this.stopResultPolling();
5849
+ this.challengeLifecycleClosed = true;
5850
+ }
5851
+ }, {
5852
+ key: "stopResultPolling",
5853
+ value: function stopResultPolling() {
5854
+ if (this.resultPollTimer !== null) {
5855
+ clearTimeout(this.resultPollTimer);
5856
+ this.resultPollTimer = null;
5857
+ }
5858
+ }
5488
5859
  }, {
5489
5860
  key: "parseResultData",
5490
5861
  value: function parseResultData(_ref2, charge3dsId) {
@@ -5518,24 +5889,49 @@
5518
5889
  }, {
5519
5890
  key: "processResult",
5520
5891
  value: function processResult(charge3dsId) {
5521
- var _this2 = this;
5892
+ var _this6 = this;
5522
5893
  if (this.resultRead) {
5523
5894
  return;
5524
5895
  }
5525
5896
  this.resultRead = true;
5897
+ this.finishChallengeLifecycle();
5526
5898
  this.api.charge().standalone3dsHandle().then(function (result) {
5527
5899
  var _a;
5528
- _this2.iFrameAuthorization.remove();
5529
- (_a = _this2.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5900
+ _this6.iFrameAuthorization.remove();
5901
+ (_a = _this6.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5530
5902
  if (result.status === PROCESS_STANDALONE_3DS_STATUS.SUCCESS) {
5531
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this2.parseResultData(result, charge3dsId));
5903
+ _this6.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this6.parseResultData(result, charge3dsId));
5532
5904
  } else {
5533
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this2.parseResultData(result, charge3dsId));
5905
+ _this6.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this6.parseResultData(result, charge3dsId));
5534
5906
  }
5535
5907
  }, function (err) {
5536
- _this2.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this2.parseError(err, charge3dsId));
5908
+ _this6.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this6.parseError(err, charge3dsId));
5537
5909
  });
5538
5910
  }
5911
+ /** Final (post-challenge) outcome delivered by the API through the callback message. */
5912
+ }, {
5913
+ key: "handleResultOutcome",
5914
+ value: function handleResultOutcome(_ref4, charge3dsId) {
5915
+ var status = _ref4.status,
5916
+ error = _ref4.error;
5917
+ if (this.resultRead) {
5918
+ return;
5919
+ }
5920
+ this.resultRead = true;
5921
+ this.finishChallengeLifecycle();
5922
+ this.removeIframes();
5923
+ if (status === PROCESS_STANDALONE_3DS_STATUS.SUCCESS) {
5924
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, this.parseResultData({
5925
+ status: status
5926
+ }, charge3dsId));
5927
+ } else if (status === PROCESS_STANDALONE_3DS_STATUS.ERROR) {
5928
+ this.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, this.parseError(error, charge3dsId));
5929
+ } else {
5930
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseResultData({
5931
+ status: status
5932
+ }, charge3dsId));
5933
+ }
5934
+ }
5539
5935
  }, {
5540
5936
  key: "externalAPI",
5541
5937
  value: function externalAPI(method, url) {
@@ -5549,60 +5945,106 @@
5549
5945
  reject(error);
5550
5946
  }
5551
5947
  };
5948
+ // without these a failed request left the promise pending forever and the poll silently dead
5949
+ var fail = function fail() {
5950
+ return reject(new ResultMonitorTransportError());
5951
+ };
5952
+ request.onerror = fail;
5953
+ request.onabort = fail;
5954
+ request.ontimeout = fail;
5552
5955
  request.send();
5553
5956
  });
5554
5957
  }
5958
+ /**
5959
+ * Polls the GPayments result monitor (`secondary_url`) until `AuthResultReady`. It is the primary source of
5960
+ * `chargeAuthChallengeCompleted` (the monitor flips as soon as the issuer's RReq reaches the 3DS Server) and
5961
+ * the fallback transport for the outcome itself (`GET /handle`) when the callback message never arrives —
5962
+ * the only transport for decoupled challenges. Stops once an outcome was obtained or the lifecycle closed.
5963
+ * Transport failures are retried on the same cadence; an unparsable body or an unknown event is fatal.
5964
+ */
5555
5965
  }, {
5556
5966
  key: "doPolling",
5557
- value: function doPolling(url, charge3dsId) {
5558
- var _this3 = this;
5967
+ value: function doPolling(url, charge3dsId, intervalMs) {
5968
+ var _this7 = this;
5969
+ this.stopResultPolling();
5970
+ if (this.resultRead || this.challengeLifecycleClosed) {
5971
+ return;
5972
+ }
5973
+ var schedule = function schedule() {
5974
+ _this7.resultPollTimer = setTimeout(function () {
5975
+ _this7.resultPollTimer = null;
5976
+ _this7.doPolling(url, charge3dsId, intervalMs);
5977
+ }, intervalMs);
5978
+ };
5559
5979
  this.externalAPI('GET', url).then(function (data) {
5980
+ if (_this7.resultRead || _this7.challengeLifecycleClosed) {
5981
+ return;
5982
+ }
5560
5983
  if (!data.event || data.event === 'AuthResultNotReady') {
5561
- setTimeout(function () {
5562
- _this3.doPolling(url, charge3dsId);
5563
- }, 2000);
5984
+ schedule();
5564
5985
  } else if (data.event === 'AuthResultReady') {
5565
- _this3.processResult(charge3dsId);
5986
+ _this7.emitChallengeCompleted(charge3dsId, 'poll');
5987
+ _this7.processResult(charge3dsId);
5566
5988
  } else {
5567
5989
  throw new Error('Event not supported');
5568
5990
  }
5569
5991
  })["catch"](function (err) {
5570
- return _this3.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this3.parseError(err, charge3dsId));
5992
+ if (err instanceof ResultMonitorTransportError) {
5993
+ if (!_this7.resultRead && !_this7.challengeLifecycleClosed) {
5994
+ schedule();
5995
+ }
5996
+ return;
5997
+ }
5998
+ _this7.finishChallengeLifecycle();
5999
+ _this7.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this7.parseError(err, charge3dsId));
5571
6000
  });
5572
6001
  }
6002
+ /**
6003
+ * Branches on the authentication outcome — delivered by the API through the callback message
6004
+ * (`process_status` → `status`) or, in two-step mode, returned by `/process` (same shape).
6005
+ */
5573
6006
  }, {
5574
- key: "performAuthentication",
5575
- value: function performAuthentication(_ref4) {
5576
- var _this4 = this;
5577
- var charge_3ds_id = _ref4.charge_3ds_id;
5578
- var _a;
5579
- this.iFrameAuthorization.remove();
5580
- (_a = this.iFrameSecondaryUrl) === null || _a === void 0 ? void 0 : _a.remove();
5581
- this.api.charge().standalone3dsProcess({
5582
- charge_3ds_id: charge_3ds_id
5583
- }).then(function (authenticationResult) {
5584
- var _a, _b;
5585
- if (authenticationResult.status === AUTHENTICATION_STATUS.SUCCESS) {
5586
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5587
- } else if (authenticationResult.status === AUTHENTICATION_STATUS.PENDING) {
5588
- if ((_a = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _a === void 0 ? void 0 : _a.challenge) {
5589
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5590
- _this4.initializeIFrames(authenticationResult.result.challenge_url, undefined, 'Authentication Challenge', false);
5591
- if (authenticationResult.result.secondary_url) {
5592
- _this4.doPolling(authenticationResult.result.secondary_url, charge_3ds_id);
5593
- }
5594
- } else if ((_b = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _b === void 0 ? void 0 : _b.decoupled_challenge) {
5595
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.DECOUPLED, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5596
- if (authenticationResult.result.secondary_url) {
5597
- _this4.doPolling(authenticationResult.result.secondary_url, charge_3ds_id);
5598
- }
6007
+ key: "handleAuthenticationOutcome",
6008
+ value: function handleAuthenticationOutcome(authenticationResult, charge_3ds_id) {
6009
+ var _a, _b, _c;
6010
+ if (authenticationResult.status !== AUTHENTICATION_STATUS.PENDING) {
6011
+ // terminal outcome — a later `chargeAuthSuccess` message or poll must not emit a second result
6012
+ this.resultRead = true;
6013
+ }
6014
+ if (authenticationResult.status === AUTHENTICATION_STATUS.SUCCESS) {
6015
+ this.finishChallengeLifecycle();
6016
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_SUCCESS, this.parseHandleResponse(authenticationResult, charge_3ds_id));
6017
+ } else if (authenticationResult.status === AUTHENTICATION_STATUS.PENDING) {
6018
+ if ((_a = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _a === void 0 ? void 0 : _a.challenge) {
6019
+ this.eventEmitter.emit(GPAYMENTS_EVENT.CHALLENGE, this.parseHandleResponse(authenticationResult, charge_3ds_id));
6020
+ var onChallengeLoad = this.startChallengeLifecycle(charge_3ds_id);
6021
+ this.initializeIFrames(authenticationResult.result.challenge_url, undefined, 'Authentication Challenge', false, onChallengeLoad);
6022
+ this.armChallengeLoadedFallback(charge_3ds_id);
6023
+ if (authenticationResult.result.secondary_url) {
6024
+ this.doPolling(authenticationResult.result.secondary_url, charge_3ds_id, CHALLENGE_RESULT_POLL_INTERVAL_MS);
6025
+ }
6026
+ } else if ((_b = authenticationResult === null || authenticationResult === void 0 ? void 0 : authenticationResult.result) === null || _b === void 0 ? void 0 : _b.decoupled_challenge) {
6027
+ this.challengeCompletedEmitted = false;
6028
+ this.challengeLifecycleClosed = false;
6029
+ this.eventEmitter.emit(GPAYMENTS_EVENT.DECOUPLED, this.parseHandleResponse(authenticationResult, charge_3ds_id));
6030
+ if (authenticationResult.result.secondary_url) {
6031
+ this.doPolling(authenticationResult.result.secondary_url, charge_3ds_id, DECOUPLED_RESULT_POLL_INTERVAL_MS);
5599
6032
  }
5600
6033
  } else {
5601
- return _this4.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, _this4.parseHandleResponse(authenticationResult, charge_3ds_id));
5602
- }
5603
- }, function (err) {
5604
- _this4.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, _this4.parseError(err, charge_3ds_id));
5605
- });
6034
+ // pending but nothing to run (no challenge delivered) — fail rather than hang silently
6035
+ // with the iframes already removed and no further message ever coming
6036
+ this.resultRead = true;
6037
+ this.finishChallengeLifecycle();
6038
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseHandleResponse(authenticationResult, charge_3ds_id));
6039
+ }
6040
+ } else if (authenticationResult.status === AUTHENTICATION_STATUS.ERROR) {
6041
+ // the API rejected the authentication, or could not run it inside the callback
6042
+ this.finishChallengeLifecycle();
6043
+ this.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, this.parseError((_c = authenticationResult.error) !== null && _c !== void 0 ? _c : authenticationResult, charge_3ds_id));
6044
+ } else {
6045
+ this.finishChallengeLifecycle();
6046
+ this.eventEmitter.emit(GPAYMENTS_EVENT.AUTH_ERROR, this.parseHandleResponse(authenticationResult, charge_3ds_id));
6047
+ }
5606
6048
  }
5607
6049
  }]);
5608
6050
  }();
@@ -5629,16 +6071,61 @@
5629
6071
  api.setEnv(this.env, this.alias);
5630
6072
  switch (tokenData.service_type) {
5631
6073
  case STANDALONE_3DS_GATEWAYS.GPAYMENTS:
5632
- new GPaymentsService(this.container, api, this.eventEmitter).load(tokenData, options.title);
6074
+ this.gpaymentsService = new GPaymentsService(this.container, api, this.eventEmitter, this.getTrustedMessageOrigins(api));
6075
+ this.gpaymentsService.load(tokenData, options.title);
5633
6076
  break;
5634
6077
  }
5635
6078
  }
6079
+ /**
6080
+ * Two-step mode: forwards `Canvas3ds.authenticate()` to the provider flow. Calling it before `load()` is an
6081
+ * integration bug (nothing to authenticate yet): it emits `error` and is not remembered.
6082
+ */
6083
+ }, {
6084
+ key: "authenticate",
6085
+ value: function authenticate(token) {
6086
+ if (!this.gpaymentsService) {
6087
+ this.eventEmitter.emit(GPAYMENTS_EVENT.ERROR, {
6088
+ charge_3ds_id: Standalone3dsService.readCharge3dsId(token),
6089
+ error: {
6090
+ message: 'Canvas3ds.authenticate() called before load()'
6091
+ }
6092
+ });
6093
+ return;
6094
+ }
6095
+ this.gpaymentsService.authenticate();
6096
+ }
6097
+ /** `charge_3ds_id` from the standalone 3DS charge token, `undefined` when it cannot be decoded. */
6098
+ }, {
6099
+ key: "getTrustedMessageOrigins",
6100
+ value:
6101
+ /**
6102
+ * Origins allowed to deliver a processed 3DS outcome to the page via postMessage: the Paydock API
6103
+ * (inline callback page) and the Paydock widget (remote-app landing page), for the current env/alias.
6104
+ */
6105
+ function getTrustedMessageOrigins(api) {
6106
+ var origins = [api.getApiOrigin()];
6107
+ try {
6108
+ var widgetEnv = new Env(WIDGET_URL, this.env);
6109
+ widgetEnv.setEnv(this.env, this.alias);
6110
+ origins.push(ApiBase.toOrigin(widgetEnv.getConf().url));
6111
+ } catch (_a) {
6112
+ // widget origin unknown for this env — the API origin is enough
6113
+ }
6114
+ return origins;
6115
+ }
5636
6116
  }, {
5637
6117
  key: "setEnv",
5638
6118
  value: function setEnv(env, alias) {
5639
6119
  this.env = env;
5640
6120
  this.alias = alias;
5641
6121
  }
6122
+ }], [{
6123
+ key: "readCharge3dsId",
6124
+ value: function readCharge3dsId(token) {
6125
+ var _a;
6126
+ var parsedToken = AccessToken.validateJWT(token);
6127
+ return parsedToken ? (_a = AccessToken.extractData(parsedToken.body)) === null || _a === void 0 ? void 0 : _a.charge_3ds_id : undefined;
6128
+ }
5642
6129
  }]);
5643
6130
  }();
5644
6131
 
@@ -5801,11 +6288,14 @@
5801
6288
  * @const STANDALONE_3DS_EVENT
5802
6289
  *
5803
6290
  * @type {object}
5804
- * @param {string} CHARGE_AUTH_SUCCESS=chargeAuthSuccess
5805
- * @param {string} CHARGE_AUTH_REJECT=chargeAuthReject
5806
- * @param {string} CHARGE_AUTH_DECOUPLED=chargeAuthDecoupled
5807
- * @param {string} CHARGE_AUTH_CHALLENGE=chargeAuthChallenge
5808
- * @param {string} CHARGE_AUTH_INFO=chargeAuthInfo
6291
+ * @param {string} AUTH_SUCCESS=chargeAuthSuccess
6292
+ * @param {string} AUTH_ERROR=chargeAuthReject
6293
+ * @param {string} DECOUPLED=chargeAuthDecoupled
6294
+ * @param {string} CHALLENGE=chargeAuthChallenge
6295
+ * @param {string} READY=chargeAuthReady - two-step mode only: fingerprint done, waiting for `authenticate()`
6296
+ * @param {string} CHALLENGE_LOADED=chargeAuthChallengeLoaded - challenge iframe painted (first `load`, or 2.5 s fallback); reveal your challenge UI here
6297
+ * @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
6298
+ * @param {string} INFO=chargeAuthInfo
5809
6299
  * @param {string} ERROR=error
5810
6300
  */
5811
6301
  /**
@@ -5932,6 +6422,39 @@
5932
6422
  });
5933
6423
  }
5934
6424
  }
6425
+ /**
6426
+ * Two-step mode only (`_3ds.authentication_flow: 'two_step'`): runs the authentication the API deferred after the
6427
+ * fingerprint. Call it when the shopper confirms (e.g. presses "Pay"); the outcome arrives through the
6428
+ * usual events (`chargeAuthChallenge`, `chargeAuthSuccess`, `chargeAuthReject`, `error`).
6429
+ *
6430
+ * Never throws. After `chargeAuthReady` the authentication starts at once; before it, the call is
6431
+ * remembered and runs as soon as the fingerprint completes. One-step charges, repeated calls and calls after
6432
+ * a terminal outcome are no-ops. For inbuilt 3DS (`html` / `url`) tokens it is a silent no-op: no event is
6433
+ * emitted at all, so do not wait for `error` there. Calling it before `load()` is an integration error: it emits `error` and
6434
+ * is not remembered. If `/process` could not be reached (`error` with `error_code` `service_unreachable` /
6435
+ * `aborted`), `authenticate()` may be called once more; any other `error`, and any failure of that retry,
6436
+ * is final.
6437
+ * No client-side deadline: a stale 3DS Server session surfaces as `error` / `chargeAuthReject`.
6438
+ *
6439
+ * @example
6440
+ * const widget = new Canvas3ds('#widget', token); // charge created with _3ds.authentication_flow: 'two_step'
6441
+ * widget.on('chargeAuthReady', () => payButton.disabled = false);
6442
+ * widget.on('chargeAuthChallenge', () => showChallengeModal());
6443
+ * widget.on('chargeAuthSuccess', (data) => completePayment(data.charge_3ds_id));
6444
+ * widget.on('chargeAuthReject', () => showDeclined());
6445
+ * widget.on('error', () => showError());
6446
+ * widget.load(); // fingerprint runs while the shopper reviews the order
6447
+ *
6448
+ * payButton.addEventListener('click', () => widget.authenticate());
6449
+ */
6450
+ }, {
6451
+ key: "authenticate",
6452
+ value: function authenticate() {
6453
+ if (this.token.format !== TOKEN_FORMAT$2.STANDALONE_3DS) {
6454
+ return;
6455
+ }
6456
+ this.standalone3dsService.authenticate(this.token.content);
6457
+ }
5935
6458
  /**
5936
6459
  * Using this method you can hide widget after load
5937
6460
  * @param {boolean} [saveSize=false] - using this param you can save iframe's size
@@ -6187,6 +6710,24 @@
6187
6710
  EFTPOS_WHITE: 'eftpos_white',
6188
6711
  UNIONPAY: 'unionpay'
6189
6712
  };
6713
+ /**
6714
+ * Card schemes the widget is able to resolve from a card number, and therefore the only values
6715
+ * `setAllowedCardSchemes` accepts. This is a separate vocabulary from SUPPORTED_CARD_TYPES, which drives the
6716
+ * card icons: it omits the icon-only variants `visa_white` / `eftpos_white`, and the `laser` / `maestro`
6717
+ * identifiers the widget can never resolve — allowing those would be configuration that can never match.
6718
+ */
6719
+ var CARD_SCHEME = {
6720
+ AMEX: 'amex',
6721
+ AUSBC: 'ausbc',
6722
+ DINERS: 'diners',
6723
+ DISCOVER: 'discover',
6724
+ EFTPOS: 'eftpos',
6725
+ JAPCB: 'japcb',
6726
+ MASTERCARD: 'mastercard',
6727
+ SOLO: 'solo',
6728
+ UNIONPAY: 'unionpay',
6729
+ VISA: 'visa'
6730
+ };
6190
6731
  var SUPPORTED_CHECKOUT_META_COLLECTION = [].concat([
6191
6732
  // Paypal
6192
6733
  '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'], [
@@ -8371,7 +8912,7 @@
8371
8912
  }]);
8372
8913
  }(WalletService);
8373
8914
 
8374
- /*! @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 */
8915
+ /*! @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 */
8375
8916
 
8376
8917
  var purify_cjs;
8377
8918
  var hasRequiredPurify_cjs;
@@ -8691,7 +9232,7 @@
8691
9232
  const text = freeze(['#text']);
8692
9233
 
8693
9234
  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']);
8694
- 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']);
9235
+ 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']);
8695
9236
  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']);
8696
9237
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
8697
9238
 
@@ -8804,7 +9345,7 @@
8804
9345
  function createDOMPurify() {
8805
9346
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
8806
9347
  const DOMPurify = root => createDOMPurify(root);
8807
- DOMPurify.version = '3.4.11';
9348
+ DOMPurify.version = '3.4.13';
8808
9349
  DOMPurify.removed = [];
8809
9350
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
8810
9351
  // Not running in a browser, provide a factory function
@@ -8835,6 +9376,7 @@
8835
9376
  const getAttributes = lookupGetter(ElementPrototype, 'attributes');
8836
9377
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
8837
9378
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
9379
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
8838
9380
  // As per issue #47, the web-components registry is inherited by a
8839
9381
  // new document created via createHTMLDocument. As per the spec
8840
9382
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -9486,6 +10028,13 @@
9486
10028
  * @param root the in-place root to empty
9487
10029
  */
9488
10030
  const _neutralizeRoot = function _neutralizeRoot(root) {
10031
+ /* Strip every disallowed attribute (on* handlers included) off the whole
10032
+ subtree BEFORE detaching anything. Detaching first would hand back
10033
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
10034
+ whose queued resource event still fires in page scope after we throw.
10035
+ Clobber-safe reads; a doomed clobbered node's own attributes are
10036
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
10037
+ _neutralizeSubtree(root);
9489
10038
  const childNodes = getChildNodes(root);
9490
10039
  if (childNodes) {
9491
10040
  const snapshot = [];
@@ -9613,6 +10162,82 @@
9613
10162
  }
9614
10163
  }
9615
10164
  };
10165
+ /**
10166
+ * _neutralizePatchLinkage
10167
+ *
10168
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
10169
+ * hardening, https://github.com/WICG/declarative-partial-updates).
10170
+ *
10171
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
10172
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
10173
+ * reaches each node. On a live in-place root that leaves a window: from the
10174
+ * moment the root is connected until the walk arrives at a given node, that
10175
+ * node's linkage is live. A patch applied on connection/stream can fire as
10176
+ * a microtask during the walk and inject or teleport an unsanitized DOM
10177
+ * range into a region the iterator has already passed and will not revisit,
10178
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
10179
+ * whole tree once up front and sever every linkage before the walk begins,
10180
+ * closing that window.
10181
+ *
10182
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
10183
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
10184
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
10185
+ * to group with the rest of the declarative-partial-updates handling and
10186
+ * stay overridable, consistent with the codebase.
10187
+ *
10188
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
10189
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
10190
+ *
10191
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
10192
+ * .html Q1): this mirrors the existing policy of keeping `for` on
10193
+ * <label>/<output>. If the shipping feature can drive a patch through a
10194
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
10195
+ * attribute check at _isBasicCustomElement's caller must additionally drop
10196
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
10197
+ *
10198
+ * @param root the in-place root to sweep
10199
+ */
10200
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
10201
+ if (!SAFE_FOR_XML) {
10202
+ return;
10203
+ }
10204
+ const stack = [root];
10205
+ while (stack.length > 0) {
10206
+ const node = stack.pop();
10207
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
10208
+ /* Remove range markers (the target side of a patch linkage): every
10209
+ processing instruction, and any markup-bearing comment. */
10210
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
10211
+ try {
10212
+ remove(node);
10213
+ } catch (_) {
10214
+ /* Best-effort */
10215
+ }
10216
+ continue;
10217
+ }
10218
+ /* Strip patch-source attributes (the source side) off elements. */
10219
+ if (nodeType === NODE_TYPE.element) {
10220
+ const element = node;
10221
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
10222
+ try {
10223
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
10224
+ element.removeAttribute('patchsrc');
10225
+ }
10226
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
10227
+ element.removeAttribute('for');
10228
+ }
10229
+ } catch (_) {
10230
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
10231
+ }
10232
+ }
10233
+ const childNodes = getChildNodes(node);
10234
+ if (childNodes) {
10235
+ for (let i = childNodes.length - 1; i >= 0; --i) {
10236
+ stack.push(childNodes[i]);
10237
+ }
10238
+ }
10239
+ }
10240
+ };
9616
10241
  /**
9617
10242
  * _initDocument
9618
10243
  *
@@ -9670,7 +10295,17 @@
9670
10295
  * @return The created NodeIterator
9671
10296
  */
9672
10297
  const _createNodeIterator = function _createNodeIterator(root) {
9673
- return createNodeIterator.call(root.ownerDocument || root, root,
10298
+ /* Read ownerDocument through the cached Node.prototype getter, never the
10299
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
10300
+ clobbering child (<input name="ownerDocument"> or a form-associated
10301
+ external input) shadows the prototype getter and makes a direct read
10302
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
10303
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
10304
+ the walk's fail-closed barrier - leaving the caller's live tree, with
10305
+ any already-armed handler in it, un-neutralized. The cached getter
10306
+ returns the real Document regardless of the clobber. */
10307
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
10308
+ return createNodeIterator.call(doc || root, root,
9674
10309
  // eslint-disable-next-line no-bitwise
9675
10310
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
9676
10311
  };
@@ -9710,7 +10345,11 @@
9710
10345
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
9711
10346
  var _node$querySelectorAl;
9712
10347
  node.normalize();
9713
- const walker = createNodeIterator.call(node.ownerDocument || node, node,
10348
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
10349
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
10350
+ carry a form-named-getter override of ownerDocument. */
10351
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
10352
+ const walker = createNodeIterator.call(doc || node, node,
9714
10353
  // eslint-disable-next-line no-bitwise
9715
10354
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
9716
10355
  let currentNode = walker.nextNode();
@@ -9863,15 +10502,21 @@
9863
10502
  /**
9864
10503
  * Handle a node whose tag is forbidden or not allowlisted: keep
9865
10504
  * allowed custom elements (false return exits _sanitizeElements
9866
- * early - namespace/fallback checks and the afterSanitizeElements
9867
- * hook are intentionally skipped for kept custom elements), else
9868
- * hoist content per KEEP_CONTENT and remove.
10505
+ * early - the namespace and fallback-tag removal checks are
10506
+ * intentionally skipped for kept custom elements), else hoist
10507
+ * content per KEEP_CONTENT and remove.
10508
+ *
10509
+ * A kept custom element is the ONLY case in which this function
10510
+ * returns false, so the caller uses that return value to run the
10511
+ * afterSanitizeElements hook on the kept element and keep the
10512
+ * element-hook lifecycle consistent with normal allowlisted
10513
+ * elements (GHSA-c2j3-45gr-mqc4).
9869
10514
  *
9870
10515
  * @param currentNode the disallowed node
9871
10516
  * @param tagName the node's transformCaseFunc'd tag name
9872
10517
  * @return true if the node was removed, false if kept
9873
10518
  */
9874
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
10519
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
9875
10520
  /* Check if we have a custom element to handle */
9876
10521
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
9877
10522
  if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
@@ -9894,27 +10539,26 @@
9894
10539
  const childNodes = getChildNodes(currentNode);
9895
10540
  if (childNodes && parentNode) {
9896
10541
  const childCount = childNodes.length;
9897
- /* In-place: hoist the *original* children so the iterator visits
9898
- and sanitises them through the same allowlist pass as every other
9899
- node. The caller built the tree in the live document, so the
9900
- originals carry already-queued resource events (`<img onerror>`,
9901
- `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
9902
- those originals detached but still armed, firing in page scope
9903
- while the returned tree looked clean. Moving is safe in-place: the
9904
- root is pre-validated as an allowed tag and so is never the node
9905
- being removed, which keeps `parentNode` inside the iterator root
9906
- and the relocated child inside the serialised tree.
9907
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
9908
- at — and the result serialised from — `body`, so a restrictive
9909
- ALLOWED_TAGS that removes `body` itself must leave its content in
9910
- place, which only cloning does; and those paths parse into an
9911
- inert document, so their discarded originals never had a queued
9912
- event to neutralise.
10542
+ /* Hoist by moving each child up one level rather than deep-cloning
10543
+ it. Moving transfers every descendant exactly once, so a chain of
10544
+ nested disallowed elements costs O(n) instead of the O(n^2) that
10545
+ re-cloning the shrinking subtree at each level produced; it also
10546
+ empties the removed original, so `DOMPurify.removed` no longer
10547
+ pins whole subtrees. Moving preserves the in-place guarantee too:
10548
+ an original carrying already-queued resource events (`<img
10549
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
10550
+ relocated and sanitised rather than left detached but still armed.
10551
+ The sole case that must clone is removing the walk root itself.
10552
+ The result is serialised from the root's subtree, so a restrictive
10553
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
10554
+ leave the content inside it, which only cloning does. In IN_PLACE
10555
+ the root is pre-validated as an allowed tag and so is never removed
10556
+ here, so that path always takes the move branch.
9913
10557
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
9914
10558
  valid whether we move (drops the trailing entry) or clone (leaves
9915
10559
  the list intact). */
9916
10560
  for (let i = childCount - 1; i >= 0; --i) {
9917
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
10561
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
9918
10562
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
9919
10563
  }
9920
10564
  }
@@ -9922,6 +10566,26 @@
9922
10566
  _forceRemove(currentNode);
9923
10567
  return true;
9924
10568
  };
10569
+ /**
10570
+ * Fork a hook-mutable allowlist off its shared binding the first time a
10571
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
10572
+ * hook cannot widen the per-instance default or the setConfig binding by
10573
+ * reference and leak past the call. Returns the set unchanged once it is
10574
+ * already call-local, so repeated calls across elements are idempotent.
10575
+ *
10576
+ * @param hookList the uponSanitize* hook array for this event
10577
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
10578
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
10579
+ * @param setConfigSet the captured setConfig() binding, or null
10580
+ * @return a call-local clone if a hook is present and set is still shared,
10581
+ * else set unchanged
10582
+ */
10583
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
10584
+ if (hookList.length === 0) {
10585
+ return set;
10586
+ }
10587
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
10588
+ };
9925
10589
  /**
9926
10590
  * _sanitizeElements
9927
10591
  *
@@ -9931,9 +10595,20 @@
9931
10595
  * @param currentNode to check for permission to exist
9932
10596
  * @return true if node was killed, false if left alive
9933
10597
  */
9934
- const _sanitizeElements = function _sanitizeElements(currentNode) {
10598
+ // eslint-disable-next-line complexity
10599
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
9935
10600
  /* Execute a hook if present */
9936
10601
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
10602
+ /* A hook may have detached the node - treat it as removed (see the
10603
+ detached-node comment after the uponSanitizeElement hook below). On
10604
+ the IN_PLACE path, neutralize the detached subtree first so a queued
10605
+ resource handler on it cannot fire in page scope after we return. */
10606
+ if (currentNode !== root && getParentNode(currentNode) === null) {
10607
+ if (IN_PLACE) {
10608
+ _neutralizeSubtree(currentNode);
10609
+ }
10610
+ return true;
10611
+ }
9937
10612
  /* Check if element is clobbered or can clobber */
9938
10613
  if (_isClobbered(currentNode)) {
9939
10614
  _forceRemove(currentNode);
@@ -9941,11 +10616,47 @@
9941
10616
  }
9942
10617
  /* Now let's check the element's type and name */
9943
10618
  const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
10619
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
10620
+ hook may have been installed after that guard sampled the hook arrays
10621
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
10622
+ aliasing a shared binding that a widening hook would mutate by
10623
+ reference. Fork it before exposing it to the hook. */
10624
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
9944
10625
  /* Execute a hook if present */
9945
10626
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
9946
10627
  tagName,
9947
10628
  allowedTags: ALLOWED_TAGS
9948
10629
  });
10630
+ /* A hook may have detached the node from the tree — a long-standing
10631
+ user pattern (issue #469; draw.io-style foreignObject filtering).
10632
+ Per the cached, unclobberable parentNode getter the node is
10633
+ genuinely out of the tree, so it can reach neither the serialized
10634
+ output nor an IN_PLACE live tree; treat it as removed and stop
10635
+ processing it. Without this guard, the unsafe-node / namespace
10636
+ checks below would call _forceRemove on a parentless node and hit
10637
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
10638
+ wants gone but *cannot* detach (clobbered / parentless roots), the
10639
+ opposite of a node that is already safely gone. The walk root is
10640
+ exempt: a detached IN_PLACE root is legitimate input and must still
10641
+ be fully sanitized, and a kill-decision on it must keep hitting the
10642
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
10643
+ responsibility for placement: they are not recorded in
10644
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
10645
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
10646
+ can still hold a queued resource-event handler - e.g. an <img onload>
10647
+ that began loading when the caller built the live tree - which fires
10648
+ in page scope after sanitize returns even though the handler never
10649
+ reached the returned tree. That is the audit-5 F1 hazard, and the
10650
+ documented node.remove() hook pattern walks straight into it. So on
10651
+ the IN_PLACE path we neutralize the detached subtree inline here,
10652
+ stripping its non-allow-listed attributes before returning, exactly
10653
+ as the post-walk pass does for _forceRemove'd subtrees. */
10654
+ if (currentNode !== root && getParentNode(currentNode) === null) {
10655
+ if (IN_PLACE) {
10656
+ _neutralizeSubtree(currentNode);
10657
+ }
10658
+ return true;
10659
+ }
9949
10660
  /* Remove mXSS vectors, processing instructions and risky comments */
9950
10661
  if (_isUnsafeNode(currentNode, tagName)) {
9951
10662
  _forceRemove(currentNode);
@@ -9953,7 +10664,22 @@
9953
10664
  }
9954
10665
  /* Remove element if anything forbids its presence */
9955
10666
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
9956
- return _sanitizeDisallowedNode(currentNode, tagName);
10667
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
10668
+ /* A false return means the node is a custom element kept via
10669
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
10670
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
10671
+ element-hook lifecycle matches normal allowlisted elements: a
10672
+ security policy applied in this hook (e.g. stripping an attribute
10673
+ from every surviving element) must not silently skip kept custom
10674
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
10675
+ tail below - the hook runs, then the walker's subsequent
10676
+ _sanitizeAttributes pass sanitizes the element's attributes. The
10677
+ deliberately skipped namespace and fallback-tag removal checks stay
10678
+ skipped; they are removal decisions, not the hook contract. */
10679
+ if (removed === false) {
10680
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
10681
+ }
10682
+ return removed;
9957
10683
  }
9958
10684
  /* Check whether element has a valid namespace.
9959
10685
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -10000,6 +10726,33 @@
10000
10726
  if (FORBID_ATTR[lcName]) {
10001
10727
  return false;
10002
10728
  }
10729
+ /* Reject declarative-partial-updates patch-linkage attributes
10730
+ (https://github.com/WICG/declarative-partial-updates).
10731
+ Empirical note (Chrome 150, verified — see
10732
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
10733
+ sanitization. For the string path it fires during sanitize()'s own
10734
+ parse, so the walk sees and sanitizes the fully materialized expanded
10735
+ tree — teleports into MathML/SVG integration points included; a
10736
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
10737
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
10738
+ Either way DOMPurify is NOT blind to the patch.
10739
+ This removal is therefore defense-in-depth rather than the sole barrier:
10740
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
10741
+ in the caller's context, and keeps behaviour deterministic if a future
10742
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
10743
+ anywhere else (notably <template for>) it links the element to a patch
10744
+ target and teleports or removes an arbitrary DOM range by id/marker name.
10745
+ `patchsrc` fetches remote markup and is treated as a script-loading
10746
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
10747
+ other structural-threat checks and stays overridable, consistent with
10748
+ the rest of the codebase. PI range markers are already removed by
10749
+ _isUnsafeNode. */
10750
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
10751
+ return false;
10752
+ }
10753
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
10754
+ return false;
10755
+ }
10003
10756
  /* Make sure attribute cannot clobber */
10004
10757
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
10005
10758
  return false;
@@ -10116,6 +10869,9 @@
10116
10869
  if (!attributes || _isClobbered(currentNode)) {
10117
10870
  return;
10118
10871
  }
10872
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
10873
+ attribute allowlist off its shared binding before a hook can see it. */
10874
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
10119
10875
  const hookEvent = {
10120
10876
  attrName: '',
10121
10877
  attrValue: '',
@@ -10209,7 +10965,7 @@
10209
10965
  /* Execute a hook if present */
10210
10966
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
10211
10967
  /* Sanitize tags and elements */
10212
- _sanitizeElements(shadowNode);
10968
+ _sanitizeElements(shadowNode, fragment);
10213
10969
  /* Check attributes next */
10214
10970
  _sanitizeAttributes(shadowNode);
10215
10971
  /* Deep shadow DOM detected.
@@ -10400,6 +11156,11 @@
10400
11156
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
10401
11157
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
10402
11158
  if (inPlace) {
11159
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
11160
+ linkage across the live tree BEFORE the walk, so no patch can fire
11161
+ mid-walk and inject into an already-processed region. Runs first, so
11162
+ it also covers the forbidden/clobbered roots that throw below. */
11163
+ _neutralizePatchLinkage(dirty);
10403
11164
  /* Do some early pre-sanitization to avoid unsafe root nodes.
10404
11165
  Read nodeName through the cached prototype getter — a clobbering
10405
11166
  child named "nodeName" on the form root would otherwise shadow
@@ -10409,6 +11170,9 @@
10409
11170
  if (typeof nn === 'string') {
10410
11171
  const tagName = transformCaseFunc(nn);
10411
11172
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
11173
+ /* Fail closed on a live root: neutralize handlers/children before
11174
+ throwing, exactly as the mid-walk abort path does. */
11175
+ _neutralizeRoot(dirty);
10412
11176
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
10413
11177
  }
10414
11178
  }
@@ -10423,6 +11187,10 @@
10423
11187
  the application unsanitized. Refuse to sanitize such a root
10424
11188
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
10425
11189
  if (_isClobbered(dirty)) {
11190
+ /* Fail closed on a live clobbered root before throwing.
11191
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
11192
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
11193
+ _neutralizeRoot(dirty);
10426
11194
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
10427
11195
  }
10428
11196
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -10475,20 +11243,24 @@
10475
11243
  _forceRemove(body.firstChild);
10476
11244
  }
10477
11245
  /* Get node iterator */
10478
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
11246
+ const walkRoot = inPlace ? dirty : body;
10479
11247
  /* Now start iterating over the created document.
10480
11248
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
10481
11249
  engine/custom-element mutation can detach a node mid-walk so
10482
11250
  `_forceRemove`'s parentless guard throws, aborting the loop. Without the
10483
11251
  barrier the caller's in-place tree would be left half-sanitized with the
10484
- unvisited tail still armed. On any throw we fail closed — strip the
10485
- in-place root bare — then rethrow so the existing throw contract is
10486
- preserved. (String/DOM-copy paths never return the partial body, so the
10487
- propagating throw is already fail-closed there.) */
11252
+ unvisited tail still armed. _createNodeIterator itself is inside the
11253
+ barrier too: constructing the iterator dereferences the root's document,
11254
+ and any failure there (e.g. an exotic/clobbered root) must still fail
11255
+ closed rather than skip the neutralize. On any throw we fail closed -
11256
+ strip the in-place root bare - then rethrow so the existing throw
11257
+ contract is preserved. (String/DOM-copy paths never return the partial
11258
+ body, so the propagating throw is already fail-closed there.) */
10488
11259
  try {
11260
+ const nodeIterator = _createNodeIterator(walkRoot);
10489
11261
  while (currentNode = nodeIterator.nextNode()) {
10490
11262
  /* Sanitize tags and elements */
10491
- _sanitizeElements(currentNode);
11263
+ _sanitizeElements(currentNode, walkRoot);
10492
11264
  /* Check attributes next */
10493
11265
  _sanitizeAttributes(currentNode);
10494
11266
  /* Shadow DOM detected, sanitize it.
@@ -10502,6 +11274,14 @@
10502
11274
  } catch (error) {
10503
11275
  if (inPlace) {
10504
11276
  _neutralizeRoot(dirty);
11277
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
11278
+ detached from the root, so _neutralizeRoot's subtree pass does not
11279
+ reach them. Defuse them too, mirroring the success-path loop below. */
11280
+ arrayForEach(DOMPurify.removed, entry => {
11281
+ if (entry.element) {
11282
+ _neutralizeSubtree(entry.element);
11283
+ }
11284
+ });
10505
11285
  }
10506
11286
  throw error;
10507
11287
  }
@@ -14928,6 +15708,54 @@
14928
15708
  validate_card_types: validateCardNumberInput
14929
15709
  });
14930
15710
  }
15711
+ /**
15712
+ * Current method restricts which card schemes the card number input accepts, without changing which card
15713
+ * icons the widget displays. Scheme identifiers are matched case-insensitively and ignoring surrounding
15714
+ * whitespace; unsupported identifiers are ignored with a warning, so a list in which every identifier is
15715
+ * unsupported applies no restriction at all.
15716
+ *
15717
+ * Takes precedence over setSupportedCardIcons: when a non-empty list is supplied here, it is the only list
15718
+ * used for validation and the validateCardNumberInput flag of setSupportedCardIcons no longer affects it.
15719
+ * Calling this method more than once replaces the previous list rather than merging with it.
15720
+ *
15721
+ * Must be called before load().
15722
+ *
15723
+ * @example
15724
+ *
15725
+ * widget.setAllowedCardSchemes(['visa', 'mastercard']);
15726
+ *
15727
+ * @param {string[]} schemes - [CARD_SCHEME]{@link CARD_SCHEME}
15728
+ */
15729
+ }, {
15730
+ key: "setAllowedCardSchemes",
15731
+ value: function setAllowedCardSchemes(schemes) {
15732
+ var vocabulary = ObjectHelper.values(CARD_SCHEME);
15733
+ var allowedSchemes = [];
15734
+ var _iterator = _createForOfIteratorHelper(Array.isArray(schemes) ? schemes : []),
15735
+ _step;
15736
+ try {
15737
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
15738
+ var entry = _step.value;
15739
+ var scheme = typeof entry === 'string' ? entry.trim().toLowerCase() : '';
15740
+ if (vocabulary.indexOf(scheme) === -1) {
15741
+ console.warn("Widget::setAllowedCardSchemes: unsupported card scheme ".concat(entry));
15742
+ continue;
15743
+ }
15744
+ if (allowedSchemes.indexOf(scheme) === -1) {
15745
+ allowedSchemes.push(scheme);
15746
+ }
15747
+ }
15748
+ } catch (err) {
15749
+ _iterator.e(err);
15750
+ } finally {
15751
+ _iterator.f();
15752
+ }
15753
+ if (allowedSchemes.length) {
15754
+ this.link.setParams({
15755
+ allowed_card_schemes: allowedSchemes.join(',')
15756
+ });
15757
+ }
15758
+ }
14931
15759
  /**
14932
15760
  * Current method can hide prevent the widget from showing the error messages
14933
15761
  *
@@ -45288,14 +46116,17 @@
45288
46116
  var slzh = function (d, b) { return b + 30 + b2(d, b + 26) + b2(d, b + 28); };
45289
46117
  // read zip header
45290
46118
  var zh = function (d, b, z) {
45291
- var fnl = b2(d, b + 28), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl, bs = b4(d, b + 20);
45292
- var _a = z && bs == 4294967295 ? z64e(d, es) : [bs, b4(d, b + 24), b4(d, b + 42)], sc = _a[0], su = _a[1], off = _a[2];
45293
- return [b2(d, b + 10), sc, su, fn, es + b2(d, b + 30) + b2(d, b + 32), off];
46119
+ var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl, bs = b4(d, b + 20);
46120
+ var _a = z && bs == 4294967295 ? z64e(d, es, efl) : [bs, b4(d, b + 24), b4(d, b + 42)], sc = _a[0], su = _a[1], off = _a[2];
46121
+ return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
45294
46122
  };
45295
46123
  // read zip64 extra field
45296
- var z64e = function (d, b) {
45297
- for (; b2(d, b) != 1; b += 4 + b2(d, b + 2))
45298
- ;
46124
+ var z64e = function (d, b, l) {
46125
+ var e = b + l;
46126
+ for (; b2(d, b) != 1; b += 4 + b2(d, b + 2)) {
46127
+ if (b + 4 > e)
46128
+ err(13);
46129
+ }
45299
46130
  return [b8(d, b + 12), b8(d, b + 4), b8(d, b + 20)];
45300
46131
  };
45301
46132
  // extra field length
@@ -45898,7 +46729,7 @@
45898
46729
  var sc_1 = b4(buf, i + 18), su_1 = b4(buf, i + 22);
45899
46730
  var fn_1 = strFromU8(buf.subarray(i + 30, i += 30 + fnl), !u);
45900
46731
  if (sc_1 == 4294967295) {
45901
- _a = dd ? [-2] : z64e(buf, i), sc_1 = _a[0], su_1 = _a[1];
46732
+ _a = dd ? [-2] : z64e(buf, i, es), sc_1 = _a[0], su_1 = _a[1];
45902
46733
  }
45903
46734
  else if (dd)
45904
46735
  sc_1 = -1;
@@ -46179,6 +47010,7 @@
46179
47010
  exports.ApplePayOpenWalletButton = ApplePayOpenWalletButton;
46180
47011
  exports.ApplePayWalletButtonExpress = ApplePayWalletButtonExpress;
46181
47012
  exports.CANVAS_3DS_EVENT = CANVAS_3DS_EVENT;
47013
+ exports.CARD_SCHEME = CARD_SCHEME;
46182
47014
  exports.CHECKOUT_BUTTON_EVENT = CHECKOUT_BUTTON_EVENT;
46183
47015
  exports.CHECKOUT_EVENT = EVENT$2;
46184
47016
  exports.Canvas3ds = Canvas3ds;