site-agent-pro 1.0.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 (81) hide show
  1. package/README.md +689 -0
  2. package/dist/auth/credentialStore.js +62 -0
  3. package/dist/auth/inbox.js +193 -0
  4. package/dist/auth/profile.js +379 -0
  5. package/dist/auth/runner.js +1124 -0
  6. package/dist/backend/dashboardData.js +194 -0
  7. package/dist/backend/runArtifacts.js +48 -0
  8. package/dist/backend/runRepository.js +93 -0
  9. package/dist/bin.js +2 -0
  10. package/dist/cli/backfillSiteChecks.js +143 -0
  11. package/dist/cli/run.js +309 -0
  12. package/dist/cli/trade.js +69 -0
  13. package/dist/config.js +199 -0
  14. package/dist/core/agentProfiles.js +55 -0
  15. package/dist/core/aggregateReport.js +382 -0
  16. package/dist/core/audit.js +30 -0
  17. package/dist/core/customTaskSuite.js +148 -0
  18. package/dist/core/evaluator.js +217 -0
  19. package/dist/core/executor.js +788 -0
  20. package/dist/core/fallbackReport.js +335 -0
  21. package/dist/core/formHeuristics.js +411 -0
  22. package/dist/core/gameplaySummary.js +164 -0
  23. package/dist/core/interaction.js +202 -0
  24. package/dist/core/pageState.js +201 -0
  25. package/dist/core/planner.js +1669 -0
  26. package/dist/core/processSubmissionBatch.js +204 -0
  27. package/dist/core/runAuditJob.js +170 -0
  28. package/dist/core/runner.js +2352 -0
  29. package/dist/core/siteBrief.js +107 -0
  30. package/dist/core/siteChecks.js +1526 -0
  31. package/dist/core/taskDirectives.js +279 -0
  32. package/dist/core/taskHeuristics.js +263 -0
  33. package/dist/dashboard/client.js +1256 -0
  34. package/dist/dashboard/contracts.js +95 -0
  35. package/dist/dashboard/narrative.js +277 -0
  36. package/dist/dashboard/server.js +458 -0
  37. package/dist/dashboard/theme.js +888 -0
  38. package/dist/index.js +84 -0
  39. package/dist/llm/client.js +188 -0
  40. package/dist/paystack/account.js +123 -0
  41. package/dist/paystack/client.js +100 -0
  42. package/dist/paystack/index.js +13 -0
  43. package/dist/paystack/test-paystack.js +83 -0
  44. package/dist/paystack/transfer.js +138 -0
  45. package/dist/paystack/types.js +74 -0
  46. package/dist/paystack/webhook.js +121 -0
  47. package/dist/prompts/browserAgent.js +124 -0
  48. package/dist/prompts/reviewer.js +71 -0
  49. package/dist/reporting/clickReplay.js +290 -0
  50. package/dist/reporting/html.js +930 -0
  51. package/dist/reporting/markdown.js +238 -0
  52. package/dist/reporting/template.js +1141 -0
  53. package/dist/schemas/types.js +361 -0
  54. package/dist/submissions/customTasks.js +196 -0
  55. package/dist/submissions/html.js +770 -0
  56. package/dist/submissions/model.js +56 -0
  57. package/dist/submissions/publicUrl.js +76 -0
  58. package/dist/submissions/service.js +74 -0
  59. package/dist/submissions/store.js +37 -0
  60. package/dist/submissions/types.js +65 -0
  61. package/dist/trade/engine.js +241 -0
  62. package/dist/trade/evm/erc20.js +44 -0
  63. package/dist/trade/extractor.js +148 -0
  64. package/dist/trade/policy.js +35 -0
  65. package/dist/trade/session.js +31 -0
  66. package/dist/trade/types.js +107 -0
  67. package/dist/trade/validator.js +148 -0
  68. package/dist/utils/files.js +59 -0
  69. package/dist/utils/log.js +24 -0
  70. package/dist/utils/playwrightCompat.js +14 -0
  71. package/dist/utils/time.js +3 -0
  72. package/dist/wallet/provider.js +345 -0
  73. package/dist/wallet/relay.js +129 -0
  74. package/dist/wallet/wallet.js +178 -0
  75. package/docs/01-installation.md +134 -0
  76. package/docs/02-running-your-first-audit.md +136 -0
  77. package/docs/03-configuration.md +233 -0
  78. package/docs/04-how-the-agent-thinks.md +41 -0
  79. package/docs/05-extending-personas-and-tasks.md +42 -0
  80. package/docs/06-hardening-for-production.md +92 -0
  81. package/package.json +60 -0
@@ -0,0 +1,1124 @@
1
+ import path from "node:path";
2
+ import { chromium, devices } from "playwright";
3
+ import { clampRunDurationMs, config } from "../config.js";
4
+ import { buildFormFieldKey, buildSupplementalAccessProfile, findMatchingSelectOption, fitValueToField } from "../core/formHeuristics.js";
5
+ import { buildLooseAccessiblePattern, prepareLocatorForInteraction, resolvePreferredFieldLocator, typeLikeHuman } from "../core/interaction.js";
6
+ import { ensureDir, writeJson } from "../utils/files.js";
7
+ import { captureInboxCheckpoint, waitForVerificationEmail } from "./inbox.js";
8
+ import { saveStoredIdentity } from "./credentialStore.js";
9
+ import { createAuthIdentityPlan, getMailboxConfig, resolveAuthSessionStatePath } from "./profile.js";
10
+ import { installPlaywrightPageCompat } from "../utils/playwrightCompat.js";
11
+ const SIGNUP_ENTRY_LABELS = [
12
+ "sign up",
13
+ "signup",
14
+ "register",
15
+ "create account",
16
+ "get started",
17
+ "join now",
18
+ "start free trial",
19
+ "start trial",
20
+ "try free"
21
+ ];
22
+ const LOGIN_ENTRY_LABELS = [
23
+ "log in",
24
+ "login",
25
+ "sign in",
26
+ "signin",
27
+ "existing account",
28
+ "already have an account"
29
+ ];
30
+ const SIGNUP_SUBMIT_LABELS = [
31
+ "create account",
32
+ "sign up",
33
+ "signup",
34
+ "register",
35
+ "continue",
36
+ "next",
37
+ "get started",
38
+ "submit",
39
+ "verify email",
40
+ "send code"
41
+ ];
42
+ const LOGIN_SUBMIT_LABELS = [
43
+ "log in",
44
+ "login",
45
+ "sign in",
46
+ "signin",
47
+ "continue",
48
+ "submit",
49
+ "verify",
50
+ "next"
51
+ ];
52
+ const VERIFY_SUBMIT_LABELS = [
53
+ "verify",
54
+ "confirm",
55
+ "continue",
56
+ "submit",
57
+ "finish",
58
+ "complete",
59
+ "activate"
60
+ ];
61
+ const AUTHENTICATED_SIGNAL_PATTERNS = [
62
+ /log out/i,
63
+ /logout/i,
64
+ /sign out/i,
65
+ /my account/i,
66
+ /account settings/i,
67
+ /profile/i,
68
+ /dashboard/i,
69
+ /billing/i
70
+ ];
71
+ const VERIFICATION_SIGNAL_PATTERNS = [
72
+ /check your email/i,
73
+ /verify your email/i,
74
+ /verification code/i,
75
+ /one[- ]time passcode/i,
76
+ /one[- ]time code/i,
77
+ /enter the code/i,
78
+ /confirm your email/i,
79
+ /activation link/i
80
+ ];
81
+ const EXISTING_ACCOUNT_PATTERNS = [
82
+ /already (?:have|has) an account/i,
83
+ /already exists/i,
84
+ /email.*already.*use/i,
85
+ /account already/i,
86
+ /user already/i
87
+ ];
88
+ const AUTH_WALL_TEXT_PATTERNS = [
89
+ /log in/i,
90
+ /login/i,
91
+ /sign in/i,
92
+ /signin/i,
93
+ /sign up/i,
94
+ /signup/i,
95
+ /register/i,
96
+ /create account/i,
97
+ /already have an account/i
98
+ ];
99
+ const AUTH_WALL_EXCLUSION_PATTERNS = [/newsletter/i, /subscribe/i, /marketing emails?/i];
100
+ function shouldUseServerlessChromium() {
101
+ return process.env.USE_SERVERLESS_CHROMIUM === "true";
102
+ }
103
+ async function resolveLaunchOptions(options) {
104
+ const explicitExecutablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH?.trim();
105
+ if (explicitExecutablePath) {
106
+ return {
107
+ executablePath: explicitExecutablePath,
108
+ headless: options.headed ? false : config.headless
109
+ };
110
+ }
111
+ if (!shouldUseServerlessChromium()) {
112
+ return {
113
+ headless: options.headed ? false : config.headless
114
+ };
115
+ }
116
+ const imported = (await import("@sparticuz/chromium"));
117
+ const serverlessChromium = imported.default ?? imported;
118
+ const location = process.env.SPARTICUZ_CHROMIUM_LOCATION?.trim() || undefined;
119
+ if ("setGraphicsMode" in serverlessChromium) {
120
+ serverlessChromium.setGraphicsMode = false;
121
+ }
122
+ return {
123
+ args: serverlessChromium.args,
124
+ executablePath: await serverlessChromium.executablePath(location),
125
+ headless: true
126
+ };
127
+ }
128
+ function cleanErrorMessage(error) {
129
+ const message = error instanceof Error ? error.message : String(error);
130
+ return message.replace(/\u001b\[[0-9;]*m/g, "").replace(/\s+/g, " ").trim() || "Unknown error";
131
+ }
132
+ function normalizeText(value) {
133
+ return value.replace(/\s+/g, " ").trim();
134
+ }
135
+ function summarizeLocalPath(filePath) {
136
+ const relativePath = path.relative(process.cwd(), filePath);
137
+ return relativePath && relativePath !== "" && !relativePath.startsWith("..") ? relativePath : filePath;
138
+ }
139
+ function pushEvent(events, type, note, extra = {}) {
140
+ events.push({
141
+ type,
142
+ time: new Date().toISOString(),
143
+ note,
144
+ ...extra
145
+ });
146
+ }
147
+ async function readVisibleSnapshot(page) {
148
+ return {
149
+ url: page.url(),
150
+ title: await page.title().catch(() => ""),
151
+ textSnippet: normalizeText(await page.locator("body").innerText().catch(() => "")).slice(0, 1200)
152
+ };
153
+ }
154
+ function visibleStateChanged(before, after) {
155
+ return before.url !== after.url || before.title !== after.title || before.textSnippet !== after.textSnippet;
156
+ }
157
+ async function firstVisible(locators) {
158
+ for (const entry of locators) {
159
+ try {
160
+ if (await entry.locator.first().isVisible({ timeout: 1200 })) {
161
+ return { locator: entry.locator.first(), label: entry.label, strategy: entry.strategy };
162
+ }
163
+ }
164
+ catch {
165
+ // continue
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ function buildActionLocators(page, labels) {
171
+ return labels.flatMap((label) => {
172
+ const pattern = buildLooseAccessiblePattern(label);
173
+ if (!pattern) {
174
+ return [];
175
+ }
176
+ return [
177
+ { locator: page.getByRole("button", { name: pattern }), label, strategy: "getByRole(button)" },
178
+ { locator: page.getByRole("link", { name: pattern }), label, strategy: "getByRole(link)" },
179
+ { locator: page.getByText(pattern), label, strategy: "getByText" }
180
+ ];
181
+ });
182
+ }
183
+ async function clickFirstMatchingAction(args) {
184
+ const match = await firstVisible(buildActionLocators(args.page, args.labels));
185
+ if (!match) {
186
+ if (!args.optional) {
187
+ pushEvent(args.events, `${args.eventType}_missing`, `Could not find any visible action matching: ${args.labels.join(", ")}`);
188
+ }
189
+ return false;
190
+ }
191
+ const before = await readVisibleSnapshot(args.page);
192
+ const startedAt = Date.now();
193
+ const preparedLocator = await prepareLocatorForInteraction(match.locator);
194
+ await preparedLocator.click({ timeout: 5000 }).catch(async () => {
195
+ await preparedLocator.click({ force: true, timeout: 5000 });
196
+ });
197
+ await args.page.waitForLoadState("domcontentloaded").catch(() => undefined);
198
+ await args.page.waitForTimeout(config.actionDelayMs);
199
+ const after = await readVisibleSnapshot(args.page);
200
+ pushEvent(args.events, args.eventType, `Clicked '${match.label}' using ${match.strategy}.`, {
201
+ label: match.label,
202
+ strategy: match.strategy,
203
+ elapsedMs: Date.now() - startedAt,
204
+ stateChanged: visibleStateChanged(before, after),
205
+ destinationUrl: after.url,
206
+ destinationTitle: after.title
207
+ });
208
+ return true;
209
+ }
210
+ async function gotoUrl(page, url, events, eventType) {
211
+ await page.goto(url, { waitUntil: "domcontentloaded" });
212
+ await page.waitForTimeout(config.actionDelayMs);
213
+ const snapshot = await readVisibleSnapshot(page);
214
+ pushEvent(events, eventType, `Navigated to ${url}.`, {
215
+ destinationUrl: snapshot.url,
216
+ destinationTitle: snapshot.title
217
+ });
218
+ }
219
+ async function collectVisibleFields(page) {
220
+ return page.evaluate(() => {
221
+ const fields = [];
222
+ const elements = Array.from(document.querySelectorAll("input, textarea, select"));
223
+ let visibleIndex = 0;
224
+ for (const element of elements) {
225
+ const rect = element.getBoundingClientRect();
226
+ const style = window.getComputedStyle(element);
227
+ const inputType = element instanceof HTMLInputElement ? (element.type || "text").replace(/\s+/g, " ").trim().toLowerCase() : "";
228
+ if (rect.width <= 0 ||
229
+ rect.height <= 0 ||
230
+ style.visibility === "hidden" ||
231
+ style.display === "none" ||
232
+ element.disabled ||
233
+ inputType === "hidden" ||
234
+ inputType === "submit" ||
235
+ inputType === "button" ||
236
+ inputType === "image") {
237
+ continue;
238
+ }
239
+ visibleIndex += 1;
240
+ const marker = String(visibleIndex);
241
+ element.setAttribute("data-site-agent-auth-field", marker);
242
+ const labelParts = [];
243
+ if ("labels" in element && element.labels) {
244
+ for (const label of Array.from(element.labels)) {
245
+ const labelText = (label.innerText || label.textContent || "").replace(/\s+/g, " ").trim();
246
+ if (labelText) {
247
+ labelParts.push(labelText);
248
+ }
249
+ }
250
+ }
251
+ const ariaLabelledBy = element.getAttribute("aria-labelledby");
252
+ if (ariaLabelledBy) {
253
+ for (const id of ariaLabelledBy.split(/\s+/)) {
254
+ const labelElement = document.getElementById(id);
255
+ if (!labelElement) {
256
+ continue;
257
+ }
258
+ const labelText = (labelElement.textContent || "").replace(/\s+/g, " ").trim();
259
+ if (labelText) {
260
+ labelParts.push(labelText);
261
+ }
262
+ }
263
+ }
264
+ const closestLabel = element.closest("label");
265
+ if (closestLabel) {
266
+ const labelText = (closestLabel.innerText || closestLabel.textContent || "").replace(/\s+/g, " ").trim();
267
+ if (labelText) {
268
+ labelParts.push(labelText);
269
+ }
270
+ }
271
+ const ariaLabel = (element.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim();
272
+ if (ariaLabel) {
273
+ labelParts.push(ariaLabel);
274
+ }
275
+ const options = [];
276
+ if (element instanceof HTMLSelectElement) {
277
+ for (const option of Array.from(element.options)) {
278
+ const optionText = (option.textContent || "").replace(/\s+/g, " ").trim();
279
+ if (optionText && options.length < 120) {
280
+ options.push(optionText);
281
+ }
282
+ }
283
+ }
284
+ fields.push({
285
+ marker,
286
+ tag: element.tagName.toLowerCase(),
287
+ inputType: inputType || element.tagName.toLowerCase(),
288
+ label: labelParts.join(" ").replace(/\s+/g, " ").trim(),
289
+ placeholder: (element.getAttribute("placeholder") || "").replace(/\s+/g, " ").trim(),
290
+ name: (element.getAttribute("name") || "").replace(/\s+/g, " ").trim(),
291
+ id: (element.id || "").replace(/\s+/g, " ").trim(),
292
+ autocomplete: (element.getAttribute("autocomplete") || "").replace(/\s+/g, " ").trim().toLowerCase(),
293
+ inputMode: (element.getAttribute("inputmode") || "").replace(/\s+/g, " ").trim().toLowerCase(),
294
+ required: element.required || element.getAttribute("aria-required") === "true",
295
+ disabled: element.disabled,
296
+ value: element instanceof HTMLSelectElement
297
+ ? (element.selectedOptions[0]?.textContent || "").replace(/\s+/g, " ").trim()
298
+ : (element.value || "").replace(/\s+/g, " ").trim(),
299
+ maxLength: element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
300
+ ? element.maxLength > 0
301
+ ? element.maxLength
302
+ : null
303
+ : null,
304
+ options
305
+ });
306
+ }
307
+ return fields;
308
+ });
309
+ }
310
+ async function collectVisibleCheckboxes(page) {
311
+ return page.evaluate(() => {
312
+ const checkboxes = [];
313
+ const elements = Array.from(document.querySelectorAll('input[type="checkbox"]'));
314
+ let visibleIndex = 0;
315
+ for (const element of elements) {
316
+ const rect = element.getBoundingClientRect();
317
+ const style = window.getComputedStyle(element);
318
+ if (rect.width <= 0 ||
319
+ rect.height <= 0 ||
320
+ style.visibility === "hidden" ||
321
+ style.display === "none" ||
322
+ element.disabled) {
323
+ continue;
324
+ }
325
+ visibleIndex += 1;
326
+ const marker = String(visibleIndex);
327
+ element.setAttribute("data-site-agent-auth-checkbox", marker);
328
+ const labels = [];
329
+ if (element.labels) {
330
+ for (const label of Array.from(element.labels)) {
331
+ const labelText = (label.innerText || label.textContent || "").replace(/\s+/g, " ").trim();
332
+ if (labelText) {
333
+ labels.push(labelText);
334
+ }
335
+ }
336
+ }
337
+ const closestLabel = element.closest("label");
338
+ if (closestLabel) {
339
+ const labelText = (closestLabel.innerText || closestLabel.textContent || "").replace(/\s+/g, " ").trim();
340
+ if (labelText) {
341
+ labels.push(labelText);
342
+ }
343
+ }
344
+ const ariaLabel = (element.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim();
345
+ if (ariaLabel) {
346
+ labels.push(ariaLabel);
347
+ }
348
+ checkboxes.push({
349
+ marker,
350
+ label: labels.join(" ").replace(/\s+/g, " ").trim(),
351
+ checked: element.checked,
352
+ required: element.required || element.getAttribute("aria-required") === "true",
353
+ disabled: element.disabled
354
+ });
355
+ }
356
+ return checkboxes;
357
+ });
358
+ }
359
+ function buildFieldKey(field) {
360
+ return buildFormFieldKey(field);
361
+ }
362
+ function inferFieldKind(field) {
363
+ const key = buildFieldKey(field);
364
+ if (/credit card|cardholder|card number|\bcvv\b|\bcvc\b|expiry|expiration|mm\/yy|mm-yy/.test(key)) {
365
+ return null;
366
+ }
367
+ if (field.autocomplete === "one-time-code" || /otp|one[- ]?time|verification|passcode|security code|auth code/.test(key)) {
368
+ return "otp";
369
+ }
370
+ if (field.inputType === "email" || /\bemail\b|e-mail/.test(key)) {
371
+ return "email";
372
+ }
373
+ if (/user.?name/.test(key)) {
374
+ return "username";
375
+ }
376
+ if (field.inputType === "password" && /confirm|repeat|again/.test(key)) {
377
+ return "confirmPassword";
378
+ }
379
+ if (field.inputType === "password") {
380
+ return "password";
381
+ }
382
+ if (/first.?name|given.?name/.test(key)) {
383
+ return "firstName";
384
+ }
385
+ if (/last.?name|family.?name|surname/.test(key)) {
386
+ return "lastName";
387
+ }
388
+ if (/full.?name/.test(key) || (/\bname\b/.test(key) && !/company|business|organization/.test(key))) {
389
+ return "fullName";
390
+ }
391
+ if (field.inputType === "tel" || /phone|mobile|telephone|tel\b/.test(key)) {
392
+ return "phone";
393
+ }
394
+ if (/address.*line.*2|address 2|suite|unit|apt|apartment/.test(key)) {
395
+ return "addressLine2";
396
+ }
397
+ if (/street|address/.test(key)) {
398
+ return "addressLine1";
399
+ }
400
+ if (/city|town/.test(key)) {
401
+ return "city";
402
+ }
403
+ if (/state|province|region/.test(key)) {
404
+ return "state";
405
+ }
406
+ if (/zip|postal/.test(key)) {
407
+ return "postalCode";
408
+ }
409
+ if (/country/.test(key)) {
410
+ return "country";
411
+ }
412
+ if (/company|organization|business/.test(key)) {
413
+ return "company";
414
+ }
415
+ if (field.autocomplete === "bday" ||
416
+ /date of birth|dob|birthday/.test(key) ||
417
+ (field.inputType === "date" && !/arrival|departure|check[- ]?in|check[- ]?out|appointment|delivery|pickup|schedule/.test(key))) {
418
+ return "dateOfBirth";
419
+ }
420
+ if (field.autocomplete === "bday-day" || /(?:birth|dob|bday).*(?:day)|day of birth/.test(key)) {
421
+ return "birthDay";
422
+ }
423
+ if (field.autocomplete === "bday-month" || /(?:birth|dob|bday).*(?:month)|month of birth/.test(key)) {
424
+ return "birthMonth";
425
+ }
426
+ if (field.autocomplete === "bday-year" || /(?:birth|dob|bday).*(?:year)|year of birth/.test(key)) {
427
+ return "birthYear";
428
+ }
429
+ if (/\bage\b|years?\b|how old/.test(key)) {
430
+ return "age";
431
+ }
432
+ if (field.inputType === "url" || /website|url|portfolio|linkedin|homepage/.test(key)) {
433
+ return "website";
434
+ }
435
+ if (/occupation|job title|profession|role|what do you do/.test(key)) {
436
+ return "occupation";
437
+ }
438
+ if (/bio|about|summary|description|introduce yourself|tell us about yourself/.test(key)) {
439
+ return "bio";
440
+ }
441
+ if (/message|why are you interested|why do you want|comment|notes?/.test(key)) {
442
+ return "message";
443
+ }
444
+ if (/pronouns?/.test(key)) {
445
+ return "pronouns";
446
+ }
447
+ if (/gender|sex/.test(key)) {
448
+ return "gender";
449
+ }
450
+ return null;
451
+ }
452
+ function fieldValueForKind(kind, field, identity, otpCode) {
453
+ const supplemental = buildSupplementalAccessProfile(identity);
454
+ switch (kind) {
455
+ case "email":
456
+ return identity.email;
457
+ case "username":
458
+ return supplemental.username;
459
+ case "password":
460
+ case "confirmPassword":
461
+ return identity.password;
462
+ case "fullName":
463
+ return identity.fullName;
464
+ case "firstName":
465
+ return identity.firstName;
466
+ case "lastName":
467
+ return identity.lastName;
468
+ case "phone":
469
+ return identity.phone;
470
+ case "addressLine1":
471
+ return identity.addressLine1;
472
+ case "addressLine2":
473
+ return identity.addressLine2;
474
+ case "city":
475
+ return identity.city;
476
+ case "state":
477
+ return identity.state;
478
+ case "postalCode":
479
+ return identity.postalCode;
480
+ case "country":
481
+ return identity.country;
482
+ case "company":
483
+ return identity.company;
484
+ case "dateOfBirth":
485
+ return supplemental.birthDateIso;
486
+ case "birthDay":
487
+ return supplemental.birthDay;
488
+ case "birthMonth":
489
+ return field.tag === "select" ? supplemental.birthMonthName : supplemental.birthMonthNumber;
490
+ case "birthYear":
491
+ return supplemental.birthYear;
492
+ case "age":
493
+ return supplemental.age;
494
+ case "website":
495
+ return supplemental.website;
496
+ case "occupation":
497
+ return supplemental.occupation;
498
+ case "bio":
499
+ return fitValueToField(field, supplemental.bio);
500
+ case "message":
501
+ return fitValueToField(field, supplemental.message);
502
+ case "pronouns":
503
+ return supplemental.pronouns;
504
+ case "gender":
505
+ return supplemental.gender;
506
+ case "otp":
507
+ return otpCode;
508
+ default:
509
+ return undefined;
510
+ }
511
+ }
512
+ function fieldAllowedInPhase(kind, phase) {
513
+ if (phase === "otp") {
514
+ return kind === "otp";
515
+ }
516
+ if (phase === "login") {
517
+ return kind === "email" || kind === "username" || kind === "password";
518
+ }
519
+ return kind !== "otp";
520
+ }
521
+ async function selectOption(locator, field, value) {
522
+ const preparedLocator = await prepareLocatorForInteraction(locator);
523
+ const desiredOptions = [value];
524
+ if (field.options.some((option) => option.toLowerCase() === "united states of america")) {
525
+ desiredOptions.push("United States of America");
526
+ }
527
+ for (const desired of desiredOptions) {
528
+ try {
529
+ await preparedLocator.selectOption({ label: desired });
530
+ return;
531
+ }
532
+ catch {
533
+ // continue
534
+ }
535
+ }
536
+ const matchingOption = findMatchingSelectOption(field.options, desiredOptions);
537
+ if (matchingOption) {
538
+ await preparedLocator.selectOption({ label: matchingOption });
539
+ return;
540
+ }
541
+ throw new Error(`No select option matched '${value}'.`);
542
+ }
543
+ async function fillFields(args) {
544
+ const fields = await collectVisibleFields(args.page);
545
+ let filledCount = 0;
546
+ for (const field of fields) {
547
+ const kind = inferFieldKind(field);
548
+ if (!kind || !fieldAllowedInPhase(kind, args.phase)) {
549
+ continue;
550
+ }
551
+ const value = fieldValueForKind(kind, field, args.identity, args.otpCode);
552
+ if (!value) {
553
+ continue;
554
+ }
555
+ const currentValue = normalizeText(field.value).toLowerCase();
556
+ if (currentValue === normalizeText(value).toLowerCase()) {
557
+ continue;
558
+ }
559
+ const fallbackLocator = args.page.locator(`[data-site-agent-auth-field="${field.marker}"]`).first();
560
+ try {
561
+ const resolvedField = await resolvePreferredFieldLocator({
562
+ page: args.page,
563
+ field,
564
+ fallbackLocator,
565
+ preferredNames: [field.label, field.placeholder, kind]
566
+ });
567
+ if (field.tag === "select") {
568
+ await selectOption(resolvedField.locator, field, value);
569
+ }
570
+ else {
571
+ await typeLikeHuman(resolvedField.locator, fitValueToField(field, value));
572
+ }
573
+ filledCount += 1;
574
+ pushEvent(args.events, `${args.phase}_field_filled`, `Filled ${kind} field '${field.label || field.name || field.id || field.placeholder}'.`, {
575
+ fieldKind: kind,
576
+ fieldLabel: field.label || field.name || field.id || field.placeholder,
577
+ strategy: resolvedField.strategy
578
+ });
579
+ }
580
+ catch (error) {
581
+ pushEvent(args.events, `${args.phase}_field_fill_error`, `Failed to fill ${kind} field '${field.label || field.name || field.id || field.placeholder}': ${cleanErrorMessage(error)}.`, {
582
+ fieldKind: kind,
583
+ fieldLabel: field.label || field.name || field.id || field.placeholder
584
+ });
585
+ }
586
+ }
587
+ return filledCount;
588
+ }
589
+ async function checkRequiredBoxes(page, events) {
590
+ const checkboxes = await collectVisibleCheckboxes(page);
591
+ let checkedCount = 0;
592
+ for (const checkbox of checkboxes) {
593
+ if (checkbox.checked || checkbox.disabled) {
594
+ continue;
595
+ }
596
+ const key = checkbox.label.toLowerCase();
597
+ const isTermsBox = /agree|accept|terms|privacy|conditions|policy/.test(key);
598
+ if (!checkbox.required && !isTermsBox) {
599
+ continue;
600
+ }
601
+ const locator = page.locator(`[data-site-agent-auth-checkbox="${checkbox.marker}"]`).first();
602
+ const preparedLocator = await prepareLocatorForInteraction(locator).catch(() => locator);
603
+ await preparedLocator.check({ force: true }).catch(() => undefined);
604
+ checkedCount += 1;
605
+ pushEvent(events, "signup_checkbox_checked", `Checked '${checkbox.label || "required checkbox"}'.`, {
606
+ label: checkbox.label
607
+ });
608
+ }
609
+ return checkedCount;
610
+ }
611
+ async function pageContainsPattern(page, patterns) {
612
+ const snapshot = await readVisibleSnapshot(page);
613
+ return patterns.some((pattern) => pattern.test(`${snapshot.title} ${snapshot.textSnippet}`));
614
+ }
615
+ async function hasFieldKind(page, kinds) {
616
+ const fields = await collectVisibleFields(page);
617
+ return fields.some((field) => {
618
+ const kind = inferFieldKind(field);
619
+ return kind ? kinds.includes(kind) : false;
620
+ });
621
+ }
622
+ async function hasLoginForm(page) {
623
+ return (await hasFieldKind(page, ["email", "username"])) && (await hasFieldKind(page, ["password"]));
624
+ }
625
+ async function hasOtpField(page) {
626
+ return hasFieldKind(page, ["otp"]);
627
+ }
628
+ async function isProbablyAuthenticated(page) {
629
+ if (await hasLoginForm(page)) {
630
+ return false;
631
+ }
632
+ const snapshot = await readVisibleSnapshot(page);
633
+ const haystack = `${snapshot.title} ${snapshot.textSnippet}`;
634
+ if (AUTHENTICATED_SIGNAL_PATTERNS.some((pattern) => pattern.test(haystack))) {
635
+ return true;
636
+ }
637
+ return !/login|sign in|sign up|register|verify/i.test(new URL(snapshot.url).pathname);
638
+ }
639
+ function isAuthGateUrl(url) {
640
+ try {
641
+ const { pathname } = new URL(url);
642
+ return /login|sign-in|signin|register|signup|sign-up|verify|confirmation|activate/i.test(pathname);
643
+ }
644
+ catch {
645
+ return false;
646
+ }
647
+ }
648
+ export async function detectAuthWall(page) {
649
+ if (await hasOtpField(page)) {
650
+ return {
651
+ required: true,
652
+ reason: "The page is asking for a verification or one-time code.",
653
+ kind: "verification"
654
+ };
655
+ }
656
+ if (await hasLoginForm(page)) {
657
+ return {
658
+ required: true,
659
+ reason: "A visible login form is blocking access to the destination.",
660
+ kind: "login"
661
+ };
662
+ }
663
+ if (await pageContainsPattern(page, VERIFICATION_SIGNAL_PATTERNS)) {
664
+ return {
665
+ required: true,
666
+ reason: "The page is asking the visitor to verify an email before continuing.",
667
+ kind: "verification"
668
+ };
669
+ }
670
+ const hasEntryFields = (await hasFieldKind(page, ["email", "username"])) || (await hasFieldKind(page, ["password"]));
671
+ const snapshot = await readVisibleSnapshot(page);
672
+ const haystack = `${snapshot.url} ${snapshot.title} ${snapshot.textSnippet}`;
673
+ if (hasEntryFields &&
674
+ AUTH_WALL_TEXT_PATTERNS.some((pattern) => pattern.test(haystack)) &&
675
+ !AUTH_WALL_EXCLUSION_PATTERNS.some((pattern) => pattern.test(haystack))) {
676
+ return {
677
+ required: true,
678
+ reason: "The page is presenting visible login or registration copy instead of the requested content.",
679
+ kind: /log in|login|sign in|signin/i.test(haystack) ? "login" : "signup"
680
+ };
681
+ }
682
+ if (isAuthGateUrl(snapshot.url)) {
683
+ return {
684
+ required: true,
685
+ reason: "The current URL path looks like an auth gate.",
686
+ kind: "unknown"
687
+ };
688
+ }
689
+ return {
690
+ required: false,
691
+ reason: "No obvious auth wall was detected.",
692
+ kind: "unknown"
693
+ };
694
+ }
695
+ async function ensureSignupEntry(args) {
696
+ await gotoUrl(args.page, args.signupUrl ?? args.baseUrl, args.events, "signup_navigation");
697
+ if ((await hasFieldKind(args.page, ["email"])) || (await pageContainsPattern(args.page, [/sign up|register|create account/i]))) {
698
+ return;
699
+ }
700
+ await clickFirstMatchingAction({
701
+ page: args.page,
702
+ labels: SIGNUP_ENTRY_LABELS,
703
+ events: args.events,
704
+ eventType: "signup_entry_click",
705
+ optional: true
706
+ });
707
+ }
708
+ async function ensureLoginEntry(args) {
709
+ if (args.loginUrl) {
710
+ await gotoUrl(args.page, args.loginUrl, args.events, "login_navigation");
711
+ }
712
+ if (await hasLoginForm(args.page)) {
713
+ return;
714
+ }
715
+ await clickFirstMatchingAction({
716
+ page: args.page,
717
+ labels: LOGIN_ENTRY_LABELS,
718
+ events: args.events,
719
+ eventType: "login_entry_click",
720
+ optional: true
721
+ });
722
+ }
723
+ async function submitPhase(args) {
724
+ const labels = args.phase === "signup" ? SIGNUP_SUBMIT_LABELS : args.phase === "login" ? LOGIN_SUBMIT_LABELS : VERIFY_SUBMIT_LABELS;
725
+ return clickFirstMatchingAction({
726
+ page: args.page,
727
+ labels,
728
+ events: args.events,
729
+ eventType: `${args.phase}_submit_click`,
730
+ optional: false
731
+ });
732
+ }
733
+ async function attemptSignupWithIdentity(args) {
734
+ let checkpoint = args.mailboxCheckpoint;
735
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
736
+ const filledCount = await fillFields({
737
+ page: args.page,
738
+ identity: args.identity,
739
+ phase: "signup",
740
+ events: args.events
741
+ });
742
+ await checkRequiredBoxes(args.page, args.events);
743
+ if (await hasOtpField(args.page)) {
744
+ pushEvent(args.events, "signup_requires_otp", "Signup flow reached an on-page OTP step.");
745
+ return { checkpoint, outcome: "verification" };
746
+ }
747
+ if (await pageContainsPattern(args.page, EXISTING_ACCOUNT_PATTERNS)) {
748
+ pushEvent(args.events, "signup_existing_account", "Signup page indicates the account already exists.", {
749
+ accountEmail: args.identity.email
750
+ });
751
+ return { checkpoint, outcome: "existing_account" };
752
+ }
753
+ if (await pageContainsPattern(args.page, VERIFICATION_SIGNAL_PATTERNS)) {
754
+ pushEvent(args.events, "signup_verification_prompt", "Signup page is asking for email verification.");
755
+ return { checkpoint, outcome: "verification" };
756
+ }
757
+ if (await isProbablyAuthenticated(args.page)) {
758
+ pushEvent(args.events, "signup_authenticated", "Signup flow appears to have already produced an authenticated session.");
759
+ return { checkpoint, outcome: "authenticated" };
760
+ }
761
+ if (!checkpoint) {
762
+ const mailbox = getMailboxConfig();
763
+ if (mailbox) {
764
+ checkpoint = await captureInboxCheckpoint(mailbox);
765
+ pushEvent(args.events, "mailbox_checkpoint", `Captured mailbox checkpoint at UID ${checkpoint.uidNext}.`, {
766
+ uidNext: checkpoint.uidNext
767
+ });
768
+ }
769
+ }
770
+ const submitted = await submitPhase({
771
+ page: args.page,
772
+ phase: "signup",
773
+ events: args.events
774
+ });
775
+ if (!submitted && filledCount === 0) {
776
+ break;
777
+ }
778
+ if (await pageContainsPattern(args.page, EXISTING_ACCOUNT_PATTERNS)) {
779
+ pushEvent(args.events, "signup_existing_account", "Signup submission indicates the account already exists.", {
780
+ accountEmail: args.identity.email
781
+ });
782
+ return { checkpoint, outcome: "existing_account" };
783
+ }
784
+ if ((await hasOtpField(args.page)) || (await pageContainsPattern(args.page, VERIFICATION_SIGNAL_PATTERNS))) {
785
+ return { checkpoint, outcome: "verification" };
786
+ }
787
+ if (await isProbablyAuthenticated(args.page)) {
788
+ return { checkpoint, outcome: "authenticated" };
789
+ }
790
+ }
791
+ return { checkpoint, outcome: "stalled" };
792
+ }
793
+ async function driveSignup(args) {
794
+ const identityPlan = createAuthIdentityPlan(args.baseUrl);
795
+ let checkpoint = args.mailboxCheckpoint;
796
+ let activeIdentity = identityPlan.identities[0];
797
+ if (identityPlan.source === "stored") {
798
+ pushEvent(args.events, "stored_identity_loaded", "Loaded saved credentials for this target origin and will reuse them during auth.", {
799
+ credentialScope: new URL(args.baseUrl).origin,
800
+ accountEmail: activeIdentity.email,
801
+ accountUsername: activeIdentity.username ?? null
802
+ });
803
+ }
804
+ for (const [index, identity] of identityPlan.identities.entries()) {
805
+ activeIdentity = identity;
806
+ pushEvent(args.events, "signup_identity_attempt", `Attempting signup with test identity ${index + 1}/${identityPlan.maxAttempts}.`, {
807
+ accountEmail: identity.email,
808
+ attempt: index + 1,
809
+ maxAttempts: identityPlan.maxAttempts
810
+ });
811
+ await ensureSignupEntry({
812
+ page: args.page,
813
+ signupUrl: args.signupUrl,
814
+ baseUrl: args.baseUrl,
815
+ events: args.events
816
+ });
817
+ const result = await attemptSignupWithIdentity({
818
+ page: args.page,
819
+ identity,
820
+ events: args.events,
821
+ mailboxCheckpoint: checkpoint
822
+ });
823
+ checkpoint = result.checkpoint;
824
+ if (result.outcome !== "existing_account") {
825
+ return { checkpoint, identity };
826
+ }
827
+ if (index < identityPlan.identities.length - 1) {
828
+ checkpoint = undefined;
829
+ pushEvent(args.events, "signup_identity_retry", `Signup rejected '${identity.email}' as an existing account, so the runner will retry with a fresh generated identity.`, {
830
+ accountEmail: identity.email,
831
+ nextAttempt: index + 2
832
+ });
833
+ }
834
+ }
835
+ return { checkpoint, identity: activeIdentity };
836
+ }
837
+ async function handleVerification(args) {
838
+ const needsVerification = (await hasOtpField(args.page)) || (await pageContainsPattern(args.page, VERIFICATION_SIGNAL_PATTERNS));
839
+ if (!needsVerification) {
840
+ return "none";
841
+ }
842
+ const mailbox = getMailboxConfig();
843
+ if (!mailbox) {
844
+ throw new Error("The page is asking for email verification, but AUTH_IMAP_* mailbox settings are not configured.");
845
+ }
846
+ const effectiveCheckpoint = args.checkpoint ?? (await captureInboxCheckpoint(mailbox));
847
+ const siteHost = new URL(args.baseUrl).hostname;
848
+ const message = await waitForVerificationEmail({
849
+ mailbox,
850
+ checkpoint: effectiveCheckpoint,
851
+ siteHost,
852
+ recipientEmail: args.identity.email
853
+ });
854
+ pushEvent(args.events, "verification_email_received", `Received verification email '${message.subject}'.`, {
855
+ receivedAt: message.receivedAt,
856
+ from: message.from,
857
+ hasOtpCode: Boolean(message.otpCode),
858
+ hasVerificationLink: Boolean(message.verificationLink)
859
+ });
860
+ if ((await hasOtpField(args.page)) && message.otpCode) {
861
+ await fillFields({
862
+ page: args.page,
863
+ identity: args.identity,
864
+ phase: "otp",
865
+ events: args.events,
866
+ otpCode: message.otpCode
867
+ });
868
+ await submitPhase({
869
+ page: args.page,
870
+ phase: "verify",
871
+ events: args.events
872
+ });
873
+ pushEvent(args.events, "otp_submitted", "Submitted OTP from verification email.");
874
+ return "otp";
875
+ }
876
+ if (message.verificationLink) {
877
+ await gotoUrl(args.page, message.verificationLink, args.events, "verification_link_navigation");
878
+ return "link";
879
+ }
880
+ throw new Error("A verification email arrived, but no usable OTP code or verification link could be extracted.");
881
+ }
882
+ async function driveLogin(args) {
883
+ if (await isProbablyAuthenticated(args.page)) {
884
+ return;
885
+ }
886
+ await ensureLoginEntry({
887
+ page: args.page,
888
+ loginUrl: args.loginUrl,
889
+ baseUrl: args.baseUrl,
890
+ events: args.events
891
+ });
892
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
893
+ const filledCount = await fillFields({
894
+ page: args.page,
895
+ identity: args.identity,
896
+ phase: "login",
897
+ events: args.events
898
+ });
899
+ if (filledCount === 0 && !(await hasLoginForm(args.page))) {
900
+ break;
901
+ }
902
+ await submitPhase({
903
+ page: args.page,
904
+ phase: "login",
905
+ events: args.events
906
+ });
907
+ if (await isProbablyAuthenticated(args.page)) {
908
+ pushEvent(args.events, "login_authenticated", "Login flow appears to have produced an authenticated session.");
909
+ return;
910
+ }
911
+ }
912
+ }
913
+ async function confirmAccess(args) {
914
+ if (args.accessUrl) {
915
+ await gotoUrl(args.page, args.accessUrl, args.events, "access_navigation");
916
+ }
917
+ const snapshot = await readVisibleSnapshot(args.page);
918
+ const authenticated = await isProbablyAuthenticated(args.page);
919
+ const accessConfirmed = authenticated && !isAuthGateUrl(snapshot.url);
920
+ pushEvent(args.events, "access_check", accessConfirmed
921
+ ? "Protected content appears accessible with the authenticated session."
922
+ : "Protected content still appears gated or redirected to auth.", {
923
+ destinationUrl: snapshot.url,
924
+ destinationTitle: snapshot.title
925
+ });
926
+ return { snapshot, authenticated, accessConfirmed };
927
+ }
928
+ async function saveStorageState(args) {
929
+ if (!args.context) {
930
+ return undefined;
931
+ }
932
+ ensureDir(path.dirname(args.savePath));
933
+ await args.context.storageState({ path: args.savePath });
934
+ const summarizedPath = summarizeLocalPath(args.savePath);
935
+ pushEvent(args.events, "storage_state_saved", `Saved Playwright storage state to '${summarizedPath}'.`, {
936
+ path: summarizedPath
937
+ });
938
+ return summarizedPath;
939
+ }
940
+ async function executeAuthFlowInContext(args) {
941
+ let verificationMethod = "none";
942
+ let savedStorageStatePath;
943
+ if (Date.now() >= args.deadline) {
944
+ throw new Error("Auth flow ran out of time before it could begin.");
945
+ }
946
+ const signupResult = await driveSignup({
947
+ page: args.page,
948
+ baseUrl: args.baseUrl,
949
+ signupUrl: args.signupUrl,
950
+ events: args.events
951
+ });
952
+ const identity = signupResult.identity;
953
+ if (Date.now() >= args.deadline) {
954
+ throw new Error("Auth flow ran out of time before verification.");
955
+ }
956
+ verificationMethod = await handleVerification({
957
+ page: args.page,
958
+ events: args.events,
959
+ baseUrl: args.baseUrl,
960
+ identity,
961
+ checkpoint: signupResult.checkpoint
962
+ });
963
+ if (Date.now() >= args.deadline) {
964
+ throw new Error("Auth flow ran out of time before login.");
965
+ }
966
+ await driveLogin({
967
+ page: args.page,
968
+ identity,
969
+ baseUrl: args.baseUrl,
970
+ loginUrl: args.loginUrl,
971
+ events: args.events
972
+ });
973
+ const accessCheck = await confirmAccess({
974
+ page: args.page,
975
+ accessUrl: args.accessUrl,
976
+ events: args.events
977
+ });
978
+ if (accessCheck.authenticated) {
979
+ saveStoredIdentity(args.baseUrl, identity);
980
+ pushEvent(args.events, "stored_identity_saved", "Saved the successful credentials for this target origin for future reuse.", {
981
+ credentialScope: new URL(args.baseUrl).origin,
982
+ accountEmail: identity.email,
983
+ accountUsername: identity.username ?? null
984
+ });
985
+ }
986
+ if (args.saveStorageStatePath) {
987
+ savedStorageStatePath = await saveStorageState({
988
+ context: args.context,
989
+ savePath: args.saveStorageStatePath,
990
+ events: args.events
991
+ });
992
+ }
993
+ const snapshot = accessCheck.snapshot;
994
+ return {
995
+ result: {
996
+ status: accessCheck.accessConfirmed ? "authenticated" : "partial_success",
997
+ finalUrl: snapshot.url,
998
+ finalTitle: snapshot.title,
999
+ accessConfirmed: accessCheck.accessConfirmed,
1000
+ verificationMethod,
1001
+ runDir: args.runDir,
1002
+ ...(savedStorageStatePath ? { savedStorageStatePath } : {})
1003
+ },
1004
+ accountEmail: identity.email,
1005
+ verificationMethod,
1006
+ ...(savedStorageStatePath ? { savedStorageStatePath } : {})
1007
+ };
1008
+ }
1009
+ export async function runAuthFlowInContext(options) {
1010
+ const authFlowPath = path.join(options.runDir, "auth-flow.json");
1011
+ const events = [];
1012
+ const timeoutMs = clampRunDurationMs(options.timeoutMs ?? config.maxSessionDurationMs);
1013
+ const deadline = Date.now() + timeoutMs;
1014
+ let verificationMethod = "none";
1015
+ let savedStorageStatePath;
1016
+ let accountEmail = "";
1017
+ let result = {
1018
+ status: "failed",
1019
+ finalUrl: options.page.url() || options.baseUrl,
1020
+ finalTitle: "",
1021
+ accessConfirmed: false,
1022
+ verificationMethod,
1023
+ runDir: options.runDir
1024
+ };
1025
+ pushEvent(events, "auth_flow_start", `Starting auth bootstrap for ${options.baseUrl}.`, {
1026
+ headed: Boolean(options.headed),
1027
+ mobile: Boolean(options.mobile),
1028
+ timeoutSeconds: Math.round(timeoutMs / 1000)
1029
+ });
1030
+ try {
1031
+ options.page.setDefaultNavigationTimeout(config.navigationTimeoutMs);
1032
+ options.page.setDefaultTimeout(config.navigationTimeoutMs);
1033
+ const execution = await executeAuthFlowInContext({
1034
+ page: options.page,
1035
+ context: options.context,
1036
+ baseUrl: options.baseUrl,
1037
+ runDir: options.runDir,
1038
+ signupUrl: options.signupUrl,
1039
+ loginUrl: options.loginUrl,
1040
+ accessUrl: options.accessUrl,
1041
+ saveStorageStatePath: options.saveStorageStatePath,
1042
+ events,
1043
+ deadline
1044
+ });
1045
+ verificationMethod = execution.verificationMethod;
1046
+ savedStorageStatePath = execution.savedStorageStatePath;
1047
+ accountEmail = execution.accountEmail;
1048
+ result = execution.result;
1049
+ }
1050
+ catch (error) {
1051
+ const snapshot = await readVisibleSnapshot(options.page).catch(() => ({
1052
+ url: options.baseUrl,
1053
+ title: "",
1054
+ textSnippet: ""
1055
+ }));
1056
+ pushEvent(events, "auth_flow_error", `Auth flow failed: ${cleanErrorMessage(error)}.`);
1057
+ result = {
1058
+ status: "failed",
1059
+ finalUrl: snapshot.url,
1060
+ finalTitle: snapshot.title,
1061
+ accessConfirmed: false,
1062
+ verificationMethod,
1063
+ runDir: options.runDir,
1064
+ error: cleanErrorMessage(error)
1065
+ };
1066
+ }
1067
+ finally {
1068
+ writeJson(authFlowPath, {
1069
+ ...result,
1070
+ accountEmail: accountEmail || null,
1071
+ savedStorageStatePath: savedStorageStatePath ?? null,
1072
+ verificationMethod,
1073
+ generatedAt: new Date().toISOString(),
1074
+ events
1075
+ });
1076
+ }
1077
+ return {
1078
+ ...result,
1079
+ accountEmail,
1080
+ events
1081
+ };
1082
+ }
1083
+ export async function runAuthFlow(options) {
1084
+ const saveStorageStatePath = options.saveStorageStatePath ?? resolveAuthSessionStatePath();
1085
+ const contextOptions = options.mobile
1086
+ ? {
1087
+ ...devices["iPhone 13"],
1088
+ viewport: config.mobileViewport,
1089
+ ignoreHTTPSErrors: Boolean(options.ignoreHttpsErrors),
1090
+ timezoneId: config.deviceTimezone
1091
+ }
1092
+ : {
1093
+ viewport: config.desktopViewport,
1094
+ ignoreHTTPSErrors: Boolean(options.ignoreHttpsErrors),
1095
+ timezoneId: config.deviceTimezone,
1096
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
1097
+ };
1098
+ let browser = null;
1099
+ let context = null;
1100
+ try {
1101
+ browser = await chromium.launch(await resolveLaunchOptions({ headed: options.headed }));
1102
+ context = await browser.newContext(contextOptions);
1103
+ await installPlaywrightPageCompat(context);
1104
+ const page = await context.newPage();
1105
+ const execution = await runAuthFlowInContext({
1106
+ page,
1107
+ context,
1108
+ baseUrl: options.baseUrl,
1109
+ runDir: options.runDir,
1110
+ signupUrl: options.signupUrl,
1111
+ loginUrl: options.loginUrl,
1112
+ accessUrl: options.accessUrl,
1113
+ saveStorageStatePath,
1114
+ timeoutMs: config.maxSessionDurationMs,
1115
+ headed: Boolean(options.headed),
1116
+ mobile: Boolean(options.mobile)
1117
+ });
1118
+ return execution;
1119
+ }
1120
+ finally {
1121
+ await context?.close().catch(() => undefined);
1122
+ await browser?.close().catch(() => undefined);
1123
+ }
1124
+ }