@sapiom/browser-pay 0.0.0-stage → 0.1.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 (69) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -2
  4. package/dist/cjs/cdp.d.ts +24 -0
  5. package/dist/cjs/cdp.d.ts.map +1 -0
  6. package/dist/cjs/cdp.js +390 -0
  7. package/dist/cjs/cdp.js.map +1 -0
  8. package/dist/cjs/contract.d.ts +30 -0
  9. package/dist/cjs/contract.d.ts.map +1 -0
  10. package/dist/cjs/contract.js +57 -0
  11. package/dist/cjs/contract.js.map +1 -0
  12. package/dist/cjs/detect.d.ts +7 -0
  13. package/dist/cjs/detect.d.ts.map +1 -0
  14. package/dist/cjs/detect.js +111 -0
  15. package/dist/cjs/detect.js.map +1 -0
  16. package/dist/cjs/guards.d.ts +2 -0
  17. package/dist/cjs/guards.d.ts.map +1 -0
  18. package/dist/cjs/guards.js +7 -0
  19. package/dist/cjs/guards.js.map +1 -0
  20. package/dist/cjs/index.d.ts +8 -0
  21. package/dist/cjs/index.d.ts.map +1 -0
  22. package/dist/cjs/index.js +8 -0
  23. package/dist/cjs/index.js.map +1 -0
  24. package/dist/cjs/interceptor.d.ts +66 -0
  25. package/dist/cjs/interceptor.d.ts.map +1 -0
  26. package/dist/cjs/interceptor.js +949 -0
  27. package/dist/cjs/interceptor.js.map +1 -0
  28. package/dist/cjs/logger.d.ts +5 -0
  29. package/dist/cjs/logger.d.ts.map +1 -0
  30. package/dist/cjs/logger.js +7 -0
  31. package/dist/cjs/logger.js.map +1 -0
  32. package/dist/cjs/relay.d.ts +19 -0
  33. package/dist/cjs/relay.d.ts.map +1 -0
  34. package/dist/cjs/relay.js +130 -0
  35. package/dist/cjs/relay.js.map +1 -0
  36. package/dist/esm/cdp.d.ts +24 -0
  37. package/dist/esm/cdp.d.ts.map +1 -0
  38. package/dist/esm/cdp.js +383 -0
  39. package/dist/esm/cdp.js.map +1 -0
  40. package/dist/esm/contract.d.ts +30 -0
  41. package/dist/esm/contract.d.ts.map +1 -0
  42. package/dist/esm/contract.js +53 -0
  43. package/dist/esm/contract.js.map +1 -0
  44. package/dist/esm/detect.d.ts +7 -0
  45. package/dist/esm/detect.d.ts.map +1 -0
  46. package/dist/esm/detect.js +107 -0
  47. package/dist/esm/detect.js.map +1 -0
  48. package/dist/esm/guards.d.ts +2 -0
  49. package/dist/esm/guards.d.ts.map +1 -0
  50. package/dist/esm/guards.js +4 -0
  51. package/dist/esm/guards.js.map +1 -0
  52. package/dist/esm/index.d.ts +8 -0
  53. package/dist/esm/index.d.ts.map +1 -0
  54. package/dist/esm/index.js +3 -0
  55. package/dist/esm/index.js.map +1 -0
  56. package/dist/esm/interceptor.d.ts +66 -0
  57. package/dist/esm/interceptor.d.ts.map +1 -0
  58. package/dist/esm/interceptor.js +946 -0
  59. package/dist/esm/interceptor.js.map +1 -0
  60. package/dist/esm/logger.d.ts +5 -0
  61. package/dist/esm/logger.d.ts.map +1 -0
  62. package/dist/esm/logger.js +4 -0
  63. package/dist/esm/logger.js.map +1 -0
  64. package/dist/esm/package.json +1 -0
  65. package/dist/esm/relay.d.ts +19 -0
  66. package/dist/esm/relay.d.ts.map +1 -0
  67. package/dist/esm/relay.js +124 -0
  68. package/dist/esm/relay.js.map +1 -0
  69. package/package.json +72 -3
@@ -0,0 +1,946 @@
1
+ import { CdpConnection } from "./cdp.js";
2
+ import { paymentIdempotencyKey, } from "./contract.js";
3
+ import { redactHeld, textHoldsPlaceholderNumber, } from "./detect.js";
4
+ import { isRecord } from "./guards.js";
5
+ import { silentLogger } from "./logger.js";
6
+ import { isRelayTimeout, RelayTimeoutError, } from "./relay.js";
7
+ const DEFAULT_TIMEOUT_MS = 20000;
8
+ const TARGET_FILTER = [{ type: "page" }, { type: "iframe" }];
9
+ const CHILD_FILTER = [{ type: "iframe" }];
10
+ /** GET/HEAD with no body and no held number in the URL. Safe to continue immediately. */
11
+ function canContinueWithoutReading(method, url, hasBody, placeholderNumber) {
12
+ const verb = method.toUpperCase();
13
+ if (verb !== "GET" && verb !== "HEAD")
14
+ return false;
15
+ if (hasBody)
16
+ return false;
17
+ return !textHoldsPlaceholderNumber(url, placeholderNumber);
18
+ }
19
+ /**
20
+ * Captcha widgets and the session solver. Holding them on Fetch or the
21
+ * debugger stalls the solver past its own timeout.
22
+ */
23
+ function isSolverTraffic(url) {
24
+ let parsed;
25
+ try {
26
+ parsed = new URL(url);
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ const host = parsed.hostname.toLowerCase();
32
+ if (host === "hcaptcha.com" ||
33
+ host.endsWith(".hcaptcha.com") ||
34
+ host === "recaptcha.net" ||
35
+ host.endsWith(".recaptcha.net") ||
36
+ host === "challenges.cloudflare.com" ||
37
+ host === "anchorbrowser.io" ||
38
+ host.endsWith(".anchorbrowser.io")) {
39
+ return true;
40
+ }
41
+ return parsed.pathname.toLowerCase().includes("/recaptcha/");
42
+ }
43
+ // CDP writes the body bytes itself. These headers would describe a different
44
+ // representation (gzip, a length from the processor hop) than the bytes we fulfill.
45
+ const RESPONSE_HOP = new Set([
46
+ "connection",
47
+ "keep-alive",
48
+ "transfer-encoding",
49
+ "te",
50
+ "trailer",
51
+ "upgrade",
52
+ "content-encoding",
53
+ "content-length",
54
+ ]);
55
+ // Blank frames stay armed. Disarming about:blank and arming again on the
56
+ // next URL drops the navigation that leaves the blank document.
57
+ function shouldArm(session) {
58
+ return !isHandsOffTarget(session);
59
+ }
60
+ function isHandsOffTarget(session) {
61
+ if (session.type === "worker" ||
62
+ session.type === "shared_worker" ||
63
+ session.type === "service_worker") {
64
+ return true;
65
+ }
66
+ if (session.url.startsWith("chrome-extension:"))
67
+ return true;
68
+ return isSolverTraffic(session.url);
69
+ }
70
+ export async function attachInterceptor(target, options) {
71
+ assertAttachOptions(options);
72
+ const connection = await CdpConnection.connect(resolveCdpUrl(target));
73
+ try {
74
+ return await new PaymentInterceptor(connection, options).start();
75
+ }
76
+ catch (error) {
77
+ connection.close();
78
+ throw error;
79
+ }
80
+ }
81
+ class PaymentInterceptor {
82
+ constructor(connection, options) {
83
+ this.connection = connection;
84
+ this.options = options;
85
+ this.sessions = new Map();
86
+ this.byTarget = new Map();
87
+ this.enabledSessions = new Set();
88
+ this.listeners = new Set();
89
+ this.inflight = new Map();
90
+ this.relayFlights = new Map();
91
+ this.unknownKeys = new Set();
92
+ this.loggedRefusals = new Set();
93
+ this.pendingArms = new Set();
94
+ this.backlog = [];
95
+ this.buffering = true;
96
+ this.closed = false;
97
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
98
+ this.logger = options.logger ?? silentLogger;
99
+ }
100
+ get number() {
101
+ return this.options.placeholder.number;
102
+ }
103
+ async start() {
104
+ this.connection.onEvent((event) => this.dispatch(event));
105
+ await this.connection.send("Target.setAutoAttach", {
106
+ autoAttach: true,
107
+ waitForDebuggerOnStart: true,
108
+ flatten: true,
109
+ filter: TARGET_FILTER,
110
+ });
111
+ await this.connection
112
+ .send("Target.setDiscoverTargets", { discover: true })
113
+ .catch(() => { });
114
+ await this.attachExistingFrames();
115
+ await Promise.all([...this.pendingArms]);
116
+ this.buffering = false;
117
+ return {
118
+ on: (listener) => this.subscribe(listener),
119
+ detach: () => this.detach(),
120
+ };
121
+ }
122
+ dispatch(event) {
123
+ if (event.method === "Fetch.requestPaused") {
124
+ this.onPaused(event);
125
+ return;
126
+ }
127
+ if (this.closed)
128
+ return;
129
+ if (event.method === "Target.attachedToTarget")
130
+ this.onAttached(event);
131
+ else if (event.method === "Target.detachedFromTarget")
132
+ this.onDetached(event);
133
+ else if (event.method === "Target.targetInfoChanged") {
134
+ this.onTargetInfoChanged(event);
135
+ }
136
+ }
137
+ onAttached(event) {
138
+ const params = parseAttachedTarget(event.params);
139
+ if (!params)
140
+ return;
141
+ const sessionId = params.sessionId;
142
+ const info = params.targetInfo;
143
+ if (this.byTarget.has(info.targetId)) {
144
+ // setAutoAttach already owns this target. A second session would pause
145
+ // every request twice.
146
+ void this.resumeIfWaiting(sessionId, params.waitingForDebugger).then(() => this.connection
147
+ .send("Target.detachFromTarget", { sessionId })
148
+ .catch(() => { }));
149
+ return;
150
+ }
151
+ const session = this.trackSession({
152
+ sessionId,
153
+ targetId: info.targetId,
154
+ type: info.type,
155
+ url: info.url,
156
+ fetchEnabled: false,
157
+ ...(event.sessionId ? { parentSessionId: event.sessionId } : {}),
158
+ });
159
+ const arm = (async () => {
160
+ if (info.type === "page" || info.type === "iframe") {
161
+ await this.autoAttachChildren(sessionId);
162
+ }
163
+ await this.enableSession(session.sessionId, params.waitingForDebugger);
164
+ })();
165
+ this.pendingArms.add(arm);
166
+ void arm.finally(() => {
167
+ this.pendingArms.delete(arm);
168
+ });
169
+ }
170
+ onDetached(event) {
171
+ const sessionId = readStringField(event.params, "sessionId");
172
+ if (!sessionId)
173
+ return;
174
+ const session = this.sessions.get(sessionId);
175
+ if (!session)
176
+ return;
177
+ this.sessions.delete(sessionId);
178
+ this.enabledSessions.delete(sessionId);
179
+ if (this.byTarget.get(session.targetId) === session) {
180
+ this.byTarget.delete(session.targetId);
181
+ }
182
+ }
183
+ onTargetInfoChanged(event) {
184
+ const info = parseTargetInfoChange(event.params);
185
+ if (!info)
186
+ return;
187
+ const session = this.byTarget.get(info.targetId);
188
+ if (!session)
189
+ return;
190
+ session.url = info.url;
191
+ if (!shouldArm(session)) {
192
+ this.disarm(session);
193
+ return;
194
+ }
195
+ if (!session.fetchEnabled)
196
+ void this.armOrClose(session);
197
+ }
198
+ disarm(session) {
199
+ if (session.fetchEnabled) {
200
+ session.fetchEnabled = false;
201
+ void this.connection
202
+ .send("Fetch.disable", {}, session.sessionId)
203
+ .catch(() => { });
204
+ }
205
+ void this.resumeIfWaiting(session.sessionId, true);
206
+ }
207
+ async enableSession(sessionId, waitingForDebugger) {
208
+ if (this.enabledSessions.has(sessionId)) {
209
+ await this.resumeIfWaiting(sessionId, waitingForDebugger);
210
+ return;
211
+ }
212
+ this.enabledSessions.add(sessionId);
213
+ const session = this.sessions.get(sessionId);
214
+ if (!session || !shouldArm(session)) {
215
+ await this.resumeIfWaiting(sessionId, waitingForDebugger);
216
+ return;
217
+ }
218
+ // Fetch has to be on before the target runs, or the card iframe can post
219
+ // before we are listening.
220
+ const armed = await this.armOrClose(session);
221
+ if (!armed)
222
+ return;
223
+ await this.resumeIfWaiting(sessionId, waitingForDebugger);
224
+ }
225
+ async armOrClose(session) {
226
+ const armed = await armFetch(this.connection, session);
227
+ if (armed)
228
+ return true;
229
+ if (this.closed)
230
+ return false;
231
+ this.emit({
232
+ type: "failed",
233
+ url: this.safe(session.url || session.targetId),
234
+ reason: "intercept_unavailable",
235
+ });
236
+ await this.connection
237
+ .send("Target.closeTarget", { targetId: session.targetId })
238
+ .catch(() => { });
239
+ return false;
240
+ }
241
+ async autoAttachChildren(sessionId) {
242
+ await this.connection
243
+ .send("Target.setAutoAttach", {
244
+ autoAttach: true,
245
+ waitForDebuggerOnStart: true,
246
+ flatten: true,
247
+ filter: CHILD_FILTER,
248
+ }, sessionId)
249
+ .catch(() => { });
250
+ }
251
+ async resumeIfWaiting(sessionId, waitingForDebugger) {
252
+ if (!waitingForDebugger)
253
+ return;
254
+ await this.connection
255
+ .send("Runtime.runIfWaitingForDebugger", {}, sessionId)
256
+ .catch(() => { });
257
+ }
258
+ async attachExistingFrames() {
259
+ const listed = await this.connection
260
+ .send("Target.getTargets")
261
+ .catch(() => undefined);
262
+ for (const info of parseTargetInfos(listed)) {
263
+ if (info.type !== "iframe")
264
+ continue;
265
+ if (this.byTarget.has(info.targetId))
266
+ continue;
267
+ const attached = await this.connection
268
+ .send("Target.attachToTarget", {
269
+ targetId: info.targetId,
270
+ flatten: true,
271
+ })
272
+ .catch(() => undefined);
273
+ const sessionId = readAttachedSessionId(attached);
274
+ if (!sessionId || this.sessions.has(sessionId))
275
+ continue;
276
+ const session = this.trackSession({
277
+ sessionId,
278
+ targetId: info.targetId,
279
+ type: "iframe",
280
+ url: info.url,
281
+ fetchEnabled: false,
282
+ });
283
+ await this.autoAttachChildren(sessionId);
284
+ await this.enableSession(session.sessionId, false);
285
+ }
286
+ }
287
+ trackSession(session) {
288
+ this.sessions.set(session.sessionId, session);
289
+ this.byTarget.set(session.targetId, session);
290
+ return session;
291
+ }
292
+ onPaused(event) {
293
+ const sessionId = event.sessionId;
294
+ const parsed = parsePaused(event.params);
295
+ if (!sessionId || !parsed)
296
+ return;
297
+ const { requestId, request } = parsed;
298
+ if (!this.sessions.has(sessionId)) {
299
+ void failRequest(this.connection, sessionId, requestId);
300
+ this.emit({
301
+ type: "failed",
302
+ url: this.safe(request.url),
303
+ reason: "unknown_session",
304
+ });
305
+ return;
306
+ }
307
+ const key = inflightKey(sessionId, requestId);
308
+ if (this.inflight.has(key))
309
+ return;
310
+ const entry = {
311
+ key,
312
+ sessionId,
313
+ requestId,
314
+ phase: { status: "open" },
315
+ };
316
+ this.inflight.set(key, entry);
317
+ if (canContinueWithoutReading(request.method, request.url, postDataPlan(request) !== "empty", this.number)) {
318
+ void this.settle(entry, "continue");
319
+ return;
320
+ }
321
+ void this.handlePaused(entry, request);
322
+ }
323
+ async handlePaused(entry, request) {
324
+ const safeUrl = this.safe(request.url);
325
+ try {
326
+ const decision = await this.decide(entry, request, safeUrl);
327
+ await this.apply(entry, safeUrl, decision);
328
+ }
329
+ catch {
330
+ await this.settle(entry, "fail");
331
+ this.emit({ type: "failed", url: safeUrl, reason: "network" });
332
+ }
333
+ }
334
+ async decide(entry, request, safeUrl) {
335
+ const read = await this.readHeld(entry.sessionId, request);
336
+ if (read.kind === "unreadable") {
337
+ return { action: "fail", reason: "unreadable_body" };
338
+ }
339
+ if (read.kind === "miss")
340
+ return { action: "continue" };
341
+ if (this.closed)
342
+ return { action: "fail", reason: "detached" };
343
+ const targetType = this.sessions.get(entry.sessionId)?.type ?? "";
344
+ this.emit({
345
+ type: "paused",
346
+ url: safeUrl,
347
+ method: request.method,
348
+ targetType,
349
+ });
350
+ const pageOrigin = await readPageOrigin(this.connection, this.sessions, entry.sessionId);
351
+ if (!pageOrigin || this.closed) {
352
+ return {
353
+ action: "fail",
354
+ reason: pageOrigin ? "detached" : "page_origin",
355
+ };
356
+ }
357
+ const idempotencyKey = paymentIdempotencyKey(request.method, request.url, read.body);
358
+ if (this.unknownKeys.has(idempotencyKey)) {
359
+ return { action: "fail", reason: "unknown" };
360
+ }
361
+ const relayed = await this.callRelay(request, read.body, pageOrigin, idempotencyKey);
362
+ if (relayed.action === "fail")
363
+ return relayed;
364
+ return this.decisionForOutcome(relayed.response, idempotencyKey);
365
+ }
366
+ async readHeld(sessionId, request) {
367
+ const urlHeld = textHoldsPlaceholderNumber(request.url, this.number);
368
+ if (urlHeld && this.closed)
369
+ return { kind: "held", body: "" };
370
+ if (postDataPlan(request) === "empty") {
371
+ return urlHeld ? { kind: "held", body: "" } : { kind: "miss" };
372
+ }
373
+ const body = await readBody(this.connection, sessionId, request);
374
+ if (body === undefined)
375
+ return { kind: "unreadable" };
376
+ if (urlHeld || textHoldsPlaceholderNumber(body, this.number)) {
377
+ return { kind: "held", body };
378
+ }
379
+ return { kind: "miss" };
380
+ }
381
+ async callRelay(request, body, pageOrigin, idempotencyKey) {
382
+ const flight = this.relayOnce(idempotencyKey, (signal) => this.options.relay.relay({
383
+ injectionRef: this.options.placeholder.injectionRef,
384
+ profileId: this.options.profileId,
385
+ pageOrigin,
386
+ request: {
387
+ method: request.method,
388
+ url: request.url,
389
+ headers: forwardedHeaders(request.headers),
390
+ body,
391
+ },
392
+ idempotencyKey,
393
+ }, { signal }));
394
+ try {
395
+ const response = await failAfter(flight.promise, this.timeoutMs);
396
+ this.releaseWaiter(flight);
397
+ return { action: "ok", response };
398
+ }
399
+ catch (error) {
400
+ this.releaseWaiter(flight);
401
+ return {
402
+ action: "fail",
403
+ reason: this.closed ? "detached" : failureReason(error),
404
+ };
405
+ }
406
+ }
407
+ decisionForOutcome(response, idempotencyKey) {
408
+ if (response.outcome === "unknown") {
409
+ this.unknownKeys.add(idempotencyKey);
410
+ return { action: "fail", reason: "unknown" };
411
+ }
412
+ if (response.outcome === "refused") {
413
+ this.logRefusalOnce(idempotencyKey, response.reason);
414
+ return { action: "refused", reason: response.reason };
415
+ }
416
+ if (textHoldsPlaceholderNumber(response.response.body, this.number)) {
417
+ return { action: "fail", reason: "response_credential" };
418
+ }
419
+ return { action: "relayed", response: response.response };
420
+ }
421
+ logRefusalOnce(idempotencyKey, reason) {
422
+ if (this.loggedRefusals.has(idempotencyKey))
423
+ return;
424
+ this.loggedRefusals.add(idempotencyKey);
425
+ this.logger.warn("payment relay refused the paused request", { reason });
426
+ }
427
+ async apply(entry, safeUrl, decision) {
428
+ if (decision.action === "continue") {
429
+ await this.settle(entry, "continue");
430
+ return;
431
+ }
432
+ if (decision.action === "fail") {
433
+ await this.settle(entry, "fail");
434
+ this.emit({ type: "failed", url: safeUrl, reason: decision.reason });
435
+ return;
436
+ }
437
+ if (decision.action === "refused") {
438
+ await this.settle(entry, "fail");
439
+ this.emit({ type: "refused", url: safeUrl, reason: decision.reason });
440
+ return;
441
+ }
442
+ const fulfilled = await this.settle(entry, { fulfill: decision.response });
443
+ this.emit(fulfilled
444
+ ? { type: "relayed", url: safeUrl, status: decision.response.status }
445
+ : {
446
+ type: "relayed_unfulfilled",
447
+ url: safeUrl,
448
+ status: decision.response.status,
449
+ });
450
+ }
451
+ async settle(entry, action) {
452
+ if (entry.phase.status !== "open")
453
+ return false;
454
+ if (action === "continue") {
455
+ const done = continueRequest(this.connection, entry.sessionId, entry.requestId).finally(() => {
456
+ entry.phase = { status: "done" };
457
+ this.inflight.delete(entry.key);
458
+ });
459
+ entry.phase = { status: "continuing", done };
460
+ await done;
461
+ return true;
462
+ }
463
+ entry.phase = { status: "done" };
464
+ try {
465
+ if (action === "fail") {
466
+ await failRequest(this.connection, entry.sessionId, entry.requestId);
467
+ return true;
468
+ }
469
+ await fulfillRequest(this.connection, entry.sessionId, entry.requestId, action.fulfill);
470
+ return true;
471
+ }
472
+ catch {
473
+ await failRequest(this.connection, entry.sessionId, entry.requestId);
474
+ return false;
475
+ }
476
+ finally {
477
+ this.inflight.delete(entry.key);
478
+ }
479
+ }
480
+ async drainInflight() {
481
+ const entries = [...this.inflight.values()];
482
+ await Promise.all(entries.map(async (entry) => {
483
+ if (entry.phase.status === "continuing") {
484
+ await entry.phase.done;
485
+ return;
486
+ }
487
+ if (entry.phase.status === "open")
488
+ await this.settle(entry, "fail");
489
+ }));
490
+ }
491
+ relayOnce(key, start) {
492
+ const existing = this.relayFlights.get(key);
493
+ if (existing && !existing.controller.signal.aborted) {
494
+ existing.waiters += 1;
495
+ return existing;
496
+ }
497
+ const controller = new AbortController();
498
+ const promise = start(controller.signal).finally(() => {
499
+ const current = this.relayFlights.get(key);
500
+ if (current?.controller === controller)
501
+ this.relayFlights.delete(key);
502
+ });
503
+ void promise.catch(() => { });
504
+ const flight = { controller, waiters: 1, promise };
505
+ this.relayFlights.set(key, flight);
506
+ return flight;
507
+ }
508
+ releaseWaiter(flight) {
509
+ flight.waiters -= 1;
510
+ if (flight.waiters > 0)
511
+ return;
512
+ flight.controller.abort();
513
+ }
514
+ async detach() {
515
+ if (this.closed)
516
+ return;
517
+ this.closed = true;
518
+ this.buffering = false;
519
+ this.backlog = null;
520
+ for (const flight of this.relayFlights.values())
521
+ flight.controller.abort();
522
+ await this.drainInflight();
523
+ for (const sessionId of this.sessions.keys()) {
524
+ await this.connection
525
+ .send("Fetch.disable", {}, sessionId)
526
+ .catch(() => { });
527
+ }
528
+ // A pause delivered while disable was in flight is still failed or
529
+ // continued here, before the socket goes away.
530
+ await this.drainInflight();
531
+ await this.connection
532
+ .send("Target.setAutoAttach", {
533
+ autoAttach: false,
534
+ waitForDebuggerOnStart: false,
535
+ flatten: true,
536
+ })
537
+ .catch(() => { });
538
+ this.connection.close();
539
+ }
540
+ subscribe(listener) {
541
+ const missed = this.backlog;
542
+ this.backlog = null;
543
+ this.listeners.add(listener);
544
+ if (missed) {
545
+ for (const event of missed)
546
+ this.deliver(listener, event);
547
+ }
548
+ return () => {
549
+ this.listeners.delete(listener);
550
+ };
551
+ }
552
+ emit(event) {
553
+ if (this.buffering && this.backlog) {
554
+ this.backlog.push(event);
555
+ return;
556
+ }
557
+ for (const listener of this.listeners)
558
+ this.deliver(listener, event);
559
+ }
560
+ deliver(listener, event) {
561
+ try {
562
+ listener(event);
563
+ }
564
+ catch {
565
+ // A listener must not break interception.
566
+ }
567
+ }
568
+ safe(value) {
569
+ return redactHeld(value, this.number);
570
+ }
571
+ }
572
+ function inflightKey(sessionId, requestId) {
573
+ return `${sessionId}\n${requestId}`;
574
+ }
575
+ function failureReason(error) {
576
+ return isRelayTimeout(error) ? "timeout" : "network";
577
+ }
578
+ function failAfter(promise, ms) {
579
+ return new Promise((resolve, reject) => {
580
+ const timer = setTimeout(() => {
581
+ reject(new RelayTimeoutError());
582
+ }, ms);
583
+ promise.then((value) => {
584
+ clearTimeout(timer);
585
+ resolve(value);
586
+ }, (error) => {
587
+ clearTimeout(timer);
588
+ reject(error instanceof Error ? error : new Error("Payment relay failed"));
589
+ });
590
+ });
591
+ }
592
+ function postDataPlan(request) {
593
+ if (typeof request.postData === "string" && request.postData.length > 0) {
594
+ return "inline";
595
+ }
596
+ if (request.postDataEntries && request.postDataEntries.length > 0) {
597
+ const missing = request.postDataEntries.some((entry) => typeof entry.bytes !== "string" || entry.bytes.length === 0);
598
+ return missing ? "fetch" : "entries";
599
+ }
600
+ if (request.hasPostData)
601
+ return "fetch";
602
+ return "empty";
603
+ }
604
+ async function readBody(connection, sessionId, request) {
605
+ const plan = postDataPlan(request);
606
+ if (plan === "empty")
607
+ return "";
608
+ if (plan === "inline")
609
+ return request.postData ?? "";
610
+ if (plan === "entries") {
611
+ return Buffer.concat((request.postDataEntries ?? []).map((entry) => Buffer.from(entry.bytes ?? "", "base64"))).toString("utf8");
612
+ }
613
+ if (!request.networkId)
614
+ return undefined;
615
+ try {
616
+ const result = await connection.send("Network.getRequestPostData", { requestId: request.networkId }, sessionId);
617
+ const postData = readStringField(result, "postData");
618
+ if (!postData)
619
+ return undefined;
620
+ const body = isRecord(result) && result.base64Encoded === true
621
+ ? Buffer.from(postData, "base64").toString("utf8")
622
+ : postData;
623
+ if (isMultipart(request.headers) && multipartOmitsFileBytes(body)) {
624
+ return undefined;
625
+ }
626
+ return body;
627
+ }
628
+ catch {
629
+ return undefined;
630
+ }
631
+ }
632
+ async function readPageOrigin(connection, sessions, sessionId) {
633
+ const pageSessionId = topPageSession(sessions, sessionId);
634
+ if (pageSessionId) {
635
+ const fromPage = await originFromSession(connection, sessions, pageSessionId);
636
+ if (fromPage)
637
+ return fromPage;
638
+ }
639
+ const ancestors = await connection
640
+ .send("Runtime.evaluate", {
641
+ expression: "location.ancestorOrigins[location.ancestorOrigins.length - 1]",
642
+ returnByValue: true,
643
+ }, sessionId)
644
+ .catch(() => null);
645
+ const value = evaluatedString(ancestors);
646
+ if (value && /^https?:\/\//.test(value))
647
+ return value;
648
+ return undefined;
649
+ }
650
+ async function originFromSession(connection, sessions, sessionId) {
651
+ const evaluated = await connection
652
+ .send("Runtime.evaluate", { expression: "location.origin", returnByValue: true }, sessionId)
653
+ .catch(() => null);
654
+ const value = evaluatedString(evaluated);
655
+ if (value && /^https?:\/\//.test(value))
656
+ return value;
657
+ const url = sessions.get(sessionId)?.url;
658
+ if (!url)
659
+ return undefined;
660
+ try {
661
+ const origin = new URL(url).origin;
662
+ return /^https?:\/\//.test(origin) ? origin : undefined;
663
+ }
664
+ catch {
665
+ return undefined;
666
+ }
667
+ }
668
+ function topPageSession(sessions, sessionId) {
669
+ let current = sessionId;
670
+ const visited = new Set();
671
+ while (current && !visited.has(current)) {
672
+ visited.add(current);
673
+ const session = sessions.get(current);
674
+ if (!session)
675
+ break;
676
+ if (session.type === "page")
677
+ return current;
678
+ if (!session.parentSessionId)
679
+ break;
680
+ current = session.parentSessionId;
681
+ }
682
+ const pages = [...sessions.values()].filter((session) => session.type === "page");
683
+ if (pages.length === 1)
684
+ return pages[0].sessionId;
685
+ return undefined;
686
+ }
687
+ const FORWARDED_HEADERS = new Set([
688
+ "accept",
689
+ "content-type",
690
+ "shopify-identification-signature",
691
+ ]);
692
+ function forwardedHeaders(raw) {
693
+ const out = {};
694
+ const take = (name, value) => {
695
+ const key = name.toLowerCase();
696
+ if (!FORWARDED_HEADERS.has(key) || value.length > 8192)
697
+ return;
698
+ out[key] = value;
699
+ };
700
+ if (Array.isArray(raw)) {
701
+ for (const entry of raw) {
702
+ if (Array.isArray(entry) &&
703
+ entry.length === 2 &&
704
+ typeof entry[0] === "string" &&
705
+ typeof entry[1] === "string") {
706
+ take(entry[0], entry[1]);
707
+ continue;
708
+ }
709
+ if (isRecord(entry) && typeof entry.name === "string") {
710
+ take(entry.name, typeof entry.value === "string" ? entry.value : "");
711
+ }
712
+ }
713
+ return out;
714
+ }
715
+ if (isRecord(raw)) {
716
+ for (const [key, value] of Object.entries(raw)) {
717
+ if (typeof value === "string")
718
+ take(key, value);
719
+ }
720
+ }
721
+ return out;
722
+ }
723
+ function parseAttachedTarget(value) {
724
+ if (!isRecord(value) || typeof value.sessionId !== "string")
725
+ return undefined;
726
+ if (!isRecord(value.targetInfo))
727
+ return undefined;
728
+ const info = value.targetInfo;
729
+ if (typeof info.targetId !== "string" || typeof info.type !== "string") {
730
+ return undefined;
731
+ }
732
+ return {
733
+ sessionId: value.sessionId,
734
+ waitingForDebugger: value.waitingForDebugger === true,
735
+ targetInfo: {
736
+ targetId: info.targetId,
737
+ type: info.type,
738
+ url: typeof info.url === "string" ? info.url : "",
739
+ },
740
+ };
741
+ }
742
+ function parsePaused(value) {
743
+ if (!isRecord(value) || typeof value.requestId !== "string")
744
+ return undefined;
745
+ if (!isRecord(value.request))
746
+ return undefined;
747
+ const request = value.request;
748
+ return {
749
+ requestId: value.requestId,
750
+ request: {
751
+ url: typeof request.url === "string" ? request.url : "",
752
+ method: typeof request.method === "string" ? request.method : "GET",
753
+ headers: request.headers,
754
+ postData: typeof request.postData === "string" ? request.postData : undefined,
755
+ hasPostData: request.hasPostData === true,
756
+ postDataEntries: parsePostDataEntries(request.postDataEntries),
757
+ ...(typeof value.networkId === "string" && value.networkId.length > 0
758
+ ? { networkId: value.networkId }
759
+ : {}),
760
+ },
761
+ };
762
+ }
763
+ function parsePostDataEntries(value) {
764
+ if (!Array.isArray(value))
765
+ return undefined;
766
+ const entries = [];
767
+ for (const entry of value) {
768
+ if (!isRecord(entry)) {
769
+ entries.push({});
770
+ continue;
771
+ }
772
+ entries.push(typeof entry.bytes === "string" ? { bytes: entry.bytes } : {});
773
+ }
774
+ return entries;
775
+ }
776
+ function parseTargetInfoChange(value) {
777
+ if (!isRecord(value) || !isRecord(value.targetInfo))
778
+ return undefined;
779
+ const info = value.targetInfo;
780
+ if (typeof info.targetId !== "string" || typeof info.url !== "string") {
781
+ return undefined;
782
+ }
783
+ return { targetId: info.targetId, url: info.url };
784
+ }
785
+ function evaluatedString(value) {
786
+ if (!isRecord(value) || !isRecord(value.result))
787
+ return undefined;
788
+ return typeof value.result.value === "string"
789
+ ? value.result.value
790
+ : undefined;
791
+ }
792
+ function readStringField(value, key) {
793
+ if (!isRecord(value))
794
+ return undefined;
795
+ const field = value[key];
796
+ return typeof field === "string" ? field : undefined;
797
+ }
798
+ function headerValue(headers, name) {
799
+ const wanted = name.toLowerCase();
800
+ if (Array.isArray(headers)) {
801
+ for (const entry of headers) {
802
+ if (Array.isArray(entry) &&
803
+ entry.length === 2 &&
804
+ typeof entry[0] === "string" &&
805
+ typeof entry[1] === "string" &&
806
+ entry[0].toLowerCase() === wanted) {
807
+ return entry[1];
808
+ }
809
+ if (isRecord(entry) &&
810
+ typeof entry.name === "string" &&
811
+ entry.name.toLowerCase() === wanted &&
812
+ typeof entry.value === "string") {
813
+ return entry.value;
814
+ }
815
+ }
816
+ return "";
817
+ }
818
+ if (isRecord(headers)) {
819
+ for (const [key, value] of Object.entries(headers)) {
820
+ if (key.toLowerCase() === wanted && typeof value === "string") {
821
+ return value;
822
+ }
823
+ }
824
+ }
825
+ return "";
826
+ }
827
+ function isMultipart(headers) {
828
+ return headerValue(headers, "content-type")
829
+ .toLowerCase()
830
+ .includes("multipart/");
831
+ }
832
+ // The retrieved body keeps a file part's filename header and drops its bytes.
833
+ function multipartOmitsFileBytes(body) {
834
+ return /content-disposition:[^\r\n]*[;\s]filename\s*\*?=/i.test(body);
835
+ }
836
+ async function armFetch(connection, session) {
837
+ if (session.fetchEnabled)
838
+ return true;
839
+ try {
840
+ await connection
841
+ .send("Network.enable", {}, session.sessionId)
842
+ .catch(() => { });
843
+ await connection.send("Fetch.enable", { patterns: [{ urlPattern: "*", requestStage: "Request" }] }, session.sessionId);
844
+ }
845
+ catch {
846
+ return false;
847
+ }
848
+ session.fetchEnabled = true;
849
+ return true;
850
+ }
851
+ async function continueRequest(connection, sessionId, requestId) {
852
+ try {
853
+ await connection.send("Fetch.continueRequest", { requestId }, sessionId);
854
+ }
855
+ catch {
856
+ await failRequest(connection, sessionId, requestId);
857
+ }
858
+ }
859
+ async function failRequest(connection, sessionId, requestId) {
860
+ await connection
861
+ .send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" }, sessionId)
862
+ .catch(() => { });
863
+ }
864
+ async function fulfillRequest(connection, sessionId, requestId, response) {
865
+ const responseHeaders = Object.entries(response.headers).flatMap(([name, value]) => {
866
+ if (RESPONSE_HOP.has(name.toLowerCase()))
867
+ return [];
868
+ return [{ name, value }];
869
+ });
870
+ await connection.send("Fetch.fulfillRequest", {
871
+ requestId,
872
+ responseCode: response.status,
873
+ responseHeaders,
874
+ body: Buffer.from(response.body, "utf8").toString("base64"),
875
+ }, sessionId);
876
+ }
877
+ function parseTargetInfos(value) {
878
+ if (!isRecord(value) || !Array.isArray(value.targetInfos))
879
+ return [];
880
+ const infos = [];
881
+ for (const entry of value.targetInfos) {
882
+ if (!isRecord(entry))
883
+ continue;
884
+ if (typeof entry.targetId !== "string" || typeof entry.type !== "string") {
885
+ continue;
886
+ }
887
+ infos.push({
888
+ targetId: entry.targetId,
889
+ type: entry.type,
890
+ url: typeof entry.url === "string" ? entry.url : "",
891
+ });
892
+ }
893
+ return infos;
894
+ }
895
+ function readAttachedSessionId(value) {
896
+ if (!isRecord(value) || typeof value.sessionId !== "string")
897
+ return undefined;
898
+ return value.sessionId.length > 0 ? value.sessionId : undefined;
899
+ }
900
+ function assertAttachOptions(options) {
901
+ if (typeof options.relay?.relay !== "function") {
902
+ throw new Error("A payment relay is required");
903
+ }
904
+ if (!options.profileId) {
905
+ throw new Error("profileId is required");
906
+ }
907
+ const digits = options.placeholder?.number?.replace(/\D/g, "") ?? "";
908
+ if (!options.placeholder?.injectionRef || digits.length === 0) {
909
+ throw new Error("placeholder.injectionRef and placeholder.number are required");
910
+ }
911
+ }
912
+ function resolveCdpUrl(target) {
913
+ if (typeof target === "string") {
914
+ if (!target)
915
+ throw new Error("A CDP websocket URL is required");
916
+ return target;
917
+ }
918
+ const direct = callString(target, "wsEndpoint");
919
+ if (direct)
920
+ return direct;
921
+ const browser = callObject(target, "browser");
922
+ const fromBrowser = browser ? callString(browser, "wsEndpoint") : undefined;
923
+ if (fromBrowser)
924
+ return fromBrowser;
925
+ const context = callObject(target, "context");
926
+ const nested = context ? callObject(context, "browser") : undefined;
927
+ const fromContext = nested ? callString(nested, "wsEndpoint") : undefined;
928
+ if (fromContext)
929
+ return fromContext;
930
+ throw new Error("Pass a CDP websocket URL, or a Playwright/Puppeteer browser or page whose browser exposes wsEndpoint()");
931
+ }
932
+ function callString(target, key) {
933
+ const method = Reflect.get(target, key);
934
+ if (typeof method !== "function")
935
+ return undefined;
936
+ const value = method.call(target);
937
+ return typeof value === "string" && value.length > 0 ? value : undefined;
938
+ }
939
+ function callObject(target, key) {
940
+ const method = Reflect.get(target, key);
941
+ if (typeof method !== "function")
942
+ return undefined;
943
+ const value = method.call(target);
944
+ return typeof value === "object" && value !== null ? value : undefined;
945
+ }
946
+ //# sourceMappingURL=interceptor.js.map