@epsilon-asi/actors 0.0.3 → 0.0.5

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 (112) hide show
  1. package/.ai/generators/_template.ts +37 -0
  2. package/.ai/generators/abstract.ts +24 -0
  3. package/.ai/generators/actor-task-form-filler.ts +140 -0
  4. package/.ai/generators/actor-task.ts +122 -0
  5. package/.ai/generators/auth-core.ts +126 -0
  6. package/.ai/generators/browser-runtime.ts +114 -0
  7. package/.ai/generators/cli-command.ts +96 -0
  8. package/.ai/generators/core-framework.ts +80 -0
  9. package/.ai/generators/docs.ts +92 -0
  10. package/.ai/generators/error-logging.ts +102 -0
  11. package/.ai/generators/extraction-helper.ts +96 -0
  12. package/.ai/generators/interaction-behavior.ts +129 -0
  13. package/.ai/generators/site-actor.ts +125 -0
  14. package/.ai/generators/site-login-flow.ts +117 -0
  15. package/.ai/generators/unit-test.ts +109 -0
  16. package/.ai/workflows/_template.ts +20 -0
  17. package/.ai/workflows/starter.ts +20 -0
  18. package/ai-gen.config.ts +67 -0
  19. package/package.json +4 -12
  20. package/src/auth/AuthStateDetector.ts +18 -0
  21. package/src/auth/CredentialsProvider.ts +48 -0
  22. package/src/auth/LoginFlow.ts +332 -0
  23. package/src/auth/LoginFlow.types.ts +141 -0
  24. package/src/auth/SessionStore.ts +21 -0
  25. package/src/auth/index.ts +5 -0
  26. package/src/browser/BrowserFactory.ts +253 -0
  27. package/src/browser/BrowserSession.ts +50 -0
  28. package/src/browser/PuppeteerLike.ts +65 -0
  29. package/src/browser/RuntimeConfig.ts +152 -0
  30. package/src/browser/index.ts +5 -0
  31. package/src/browser/profileValidation.ts +73 -0
  32. package/src/cli/run.ts +112 -0
  33. package/src/core/Actor.ts +167 -0
  34. package/src/core/ActorContext.ts +34 -0
  35. package/src/core/ActorRegistry.ts +26 -0
  36. package/src/core/ActorRunner.ts +240 -0
  37. package/src/core/defineActor.ts +5 -0
  38. package/src/core/index.ts +5 -0
  39. package/src/errors/AuthError.ts +7 -0
  40. package/src/errors/AutomationError.ts +26 -0
  41. package/src/errors/ConfigError.ts +7 -0
  42. package/src/errors/ExtractionError.ts +7 -0
  43. package/src/errors/NavigationError.ts +7 -0
  44. package/src/errors/SelectorError.ts +10 -0
  45. package/src/errors/index.ts +6 -0
  46. package/src/extraction/Extractor.ts +65 -0
  47. package/src/extraction/Pagination.ts +47 -0
  48. package/src/extraction/index.ts +2 -0
  49. package/src/index.ts +9 -0
  50. package/src/interaction/FieldClearer.ts +73 -0
  51. package/src/interaction/Forms.ts +27 -0
  52. package/src/interaction/GhostCursorAdapter.ts +79 -0
  53. package/src/interaction/HumanInteractor.ts +32 -0
  54. package/src/interaction/HumanTyping.ts +157 -0
  55. package/src/interaction/NativePuppeteerInteractor.ts +68 -0
  56. package/src/interaction/Navigation.ts +37 -0
  57. package/src/interaction/PageAdapter.ts +86 -0
  58. package/src/interaction/Waits.ts +5 -0
  59. package/src/interaction/index.ts +9 -0
  60. package/src/logging/ConsoleLogger.ts +44 -0
  61. package/src/logging/Logger.ts +15 -0
  62. package/src/logging/MemoryLogger.ts +34 -0
  63. package/src/logging/NullLogger.ts +8 -0
  64. package/src/logging/index.ts +4 -0
  65. package/src/sites/example/example.actor.ts +53 -0
  66. package/src/sites/example/example.selectors.ts +17 -0
  67. package/src/sites/example/example.types.ts +18 -0
  68. package/src/sites/example/index.ts +3 -0
  69. package/src/sites/index.ts +3 -0
  70. package/src/sites/myvistage-com/index.ts +3 -0
  71. package/src/sites/myvistage-com/login-action-list.json +349 -0
  72. package/src/sites/myvistage-com/myvistage-com.actor.ts +50 -0
  73. package/src/sites/myvistage-com/myvistage-com.selectors.ts +14 -0
  74. package/src/sites/myvistage-com/myvistage-com.types.ts +18 -0
  75. package/src/sites/myvistage-com/post-comment-action.json +81 -0
  76. package/src/sites/upwork-com/index.ts +6 -0
  77. package/src/sites/upwork-com/upwork-com.actor.ts +97 -0
  78. package/src/sites/upwork-com/upwork-com.runner.ts +17 -0
  79. package/src/sites/upwork-com/upwork-com.selectors.ts +10 -0
  80. package/src/sites/upwork-com/upwork-com.types.ts +102 -0
  81. package/src/sites/upwork-com/upwork-com.util.ts +41 -0
  82. package/src/utils/delay.ts +4 -0
  83. package/src/utils/index.ts +5 -0
  84. package/src/utils/invariant.ts +7 -0
  85. package/src/utils/redact.ts +53 -0
  86. package/src/utils/retry.ts +31 -0
  87. package/src/utils/url.ts +7 -0
  88. package/tests/fixtures/FakeCredentialsProvider.ts +12 -0
  89. package/tests/fixtures/FakeCursor.ts +48 -0
  90. package/tests/fixtures/FakePage.ts +266 -0
  91. package/tests/fixtures/makeContext.ts +76 -0
  92. package/tests/unit/auth/AuthStateDetector.test.ts +80 -0
  93. package/tests/unit/auth/LoginFlow.test.ts +296 -0
  94. package/tests/unit/browser/BrowserFactory.test.ts +370 -0
  95. package/tests/unit/core/ActorRunner.test.ts +370 -0
  96. package/tests/unit/core/defineActor.test.ts +112 -0
  97. package/tests/unit/extraction/Extractor.test.ts +48 -0
  98. package/tests/unit/extraction/Pagination.test.ts +54 -0
  99. package/tests/unit/interaction/FieldClearer.test.ts +29 -0
  100. package/tests/unit/interaction/Forms.test.ts +35 -0
  101. package/tests/unit/interaction/GhostCursorAdapter.test.ts +68 -0
  102. package/tests/unit/interaction/HumanTyping.test.ts +54 -0
  103. package/tests/unit/interaction/NativePuppeteerInteractor.test.ts +22 -0
  104. package/tests/unit/interaction/PageAdapter.test.ts +25 -0
  105. package/tests/unit/logging/redact.test.ts +36 -0
  106. package/tests/unit/sites/myvistage-com.actor.test.ts +19 -0
  107. package/tests/unit/sites/myvistage-com.login.test.ts +22 -0
  108. package/tests/unit/sites/myvistage-com.postComment.test.ts +70 -0
  109. package/tests/unit/sites/upwork-com.login.test.ts +52 -0
  110. package/tsconfig.build.json +9 -0
  111. package/tsconfig.json +22 -0
  112. package/vitest.config.ts +12 -0
@@ -0,0 +1,332 @@
1
+ import { AuthError } from '../errors/AuthError.js';
2
+ import { delay } from '../utils/delay.js';
3
+ import { redact } from '../utils/redact.js';
4
+ import type { ActorContext } from '../core/ActorContext.js';
5
+ import type { FillTextOptions } from '../interaction/Forms.js';
6
+ import { mergeHumanTypingOptions, type HumanTypingOptions } from '../interaction/HumanTyping.js';
7
+ import type { Credentials, CredentialsProvider } from './CredentialsProvider.js';
8
+ import { AuthStateDetector } from './AuthStateDetector.js';
9
+ import {
10
+ hasStandardLoginSelectors,
11
+ type ClickLoginStep,
12
+ type FillCredentialLoginStep,
13
+ type LoginFillStep,
14
+ type LoginFlowDefinition,
15
+ type LoginStep,
16
+ type WaitForSelectorLoginStep
17
+ } from './LoginFlow.types.js';
18
+
19
+ export class LoginFlow {
20
+ constructor(
21
+ private readonly credentialsProvider: CredentialsProvider,
22
+ private readonly detector = new AuthStateDetector()
23
+ ) {}
24
+
25
+ async ensureAuthenticated(context: ActorContext, definition: LoginFlowDefinition): Promise<void> {
26
+ if (await this.detector.isLoggedIn(context, definition)) {
27
+ context.logger.info('Existing Chrome profile is already authenticated.', {
28
+ actorId: context.actor.id
29
+ });
30
+ return;
31
+ }
32
+
33
+ await this.login(context, definition);
34
+ }
35
+
36
+ async login(context: ActorContext, definition: LoginFlowDefinition): Promise<void> {
37
+ if (definition.credentials === undefined) {
38
+ throw new AuthError('Login is required, but no credentials ref was configured for this actor.', {
39
+ actorId: context.actor.id
40
+ });
41
+ }
42
+
43
+ const credentials = await this.credentialsProvider.getCredentials(definition.credentials);
44
+ const maxAttempts = definition.behavior?.maxAttempts ?? 1;
45
+ const steps = this.resolveSteps(definition);
46
+
47
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
48
+ context.logger.info('Starting login attempt.', {
49
+ actorId: context.actor.id,
50
+ attempt,
51
+ steps: steps.length,
52
+ credentials: redact({ username: credentials.username, password: credentials.password })
53
+ });
54
+
55
+ await context.nav.goto(definition.loginUrl);
56
+ await definition.hooks?.beforeLogin?.(context);
57
+
58
+ await this.executeSteps(context, definition, credentials, steps);
59
+
60
+ if ((definition.behavior?.waitAfterSubmitMs ?? 0) > 0) {
61
+ await delay(definition.behavior?.waitAfterSubmitMs ?? 0);
62
+ }
63
+
64
+ await this.throwIfConfiguredErrorIsVisible(context, definition);
65
+
66
+ if (await this.detector.isLoggedIn(context, definition)) {
67
+ context.logger.info('Login succeeded.', { actorId: context.actor.id, attempt });
68
+ return;
69
+ }
70
+ }
71
+
72
+ throw new AuthError('Login did not result in an authenticated session.', {
73
+ actorId: context.actor.id,
74
+ details: { maxAttempts }
75
+ });
76
+ }
77
+
78
+ private resolveSteps(definition: LoginFlowDefinition): LoginStep[] {
79
+ if (definition.steps !== undefined && definition.steps.length > 0) {
80
+ return definition.steps;
81
+ }
82
+
83
+ if (!hasStandardLoginSelectors(definition.selectors)) {
84
+ throw new AuthError(
85
+ 'Login flow requires either selectors.username/password/submit for a simple login or an ordered steps array for a multi-step login.'
86
+ );
87
+ }
88
+
89
+ const submitStep: ClickLoginStep = {
90
+ type: 'click',
91
+ name: 'submit',
92
+ selector: definition.selectors.submit,
93
+ submit: true,
94
+ navigationOptions: { waitUntil: 'domcontentloaded' },
95
+ checkForError: false
96
+ };
97
+
98
+ if (definition.behavior?.submitCausesNavigation !== undefined) {
99
+ submitStep.waitForNavigation = definition.behavior.submitCausesNavigation;
100
+ }
101
+
102
+ return [
103
+ {
104
+ type: 'fill',
105
+ name: 'username',
106
+ selector: definition.selectors.username,
107
+ credential: 'username'
108
+ },
109
+ {
110
+ type: 'fill',
111
+ name: 'password',
112
+ selector: definition.selectors.password,
113
+ credential: 'password'
114
+ },
115
+ submitStep
116
+ ];
117
+ }
118
+
119
+ private async executeSteps(
120
+ context: ActorContext,
121
+ definition: LoginFlowDefinition,
122
+ credentials: Credentials,
123
+ steps: LoginStep[]
124
+ ): Promise<void> {
125
+ for (const [index, step] of steps.entries()) {
126
+ const label = this.stepLabel(step, index);
127
+ context.logger.debug('Executing login step.', {
128
+ actorId: context.actor.id,
129
+ step: label,
130
+ type: step.type
131
+ });
132
+
133
+ if (step.type === 'hook') {
134
+ await step.run(context);
135
+ continue;
136
+ }
137
+
138
+ await step.before?.(context);
139
+
140
+ if (step.type === 'fill') {
141
+ await this.executeFillStep(context, definition, credentials, step);
142
+ } else if (step.type === 'click') {
143
+ await this.executeClickStep(context, definition, step, label);
144
+ } else {
145
+ await this.executeWaitForSelectorStep(context, definition, step, label);
146
+ }
147
+
148
+ await step.after?.(context);
149
+ }
150
+ }
151
+
152
+ private async executeFillStep(
153
+ context: ActorContext,
154
+ definition: LoginFlowDefinition,
155
+ credentials: Credentials,
156
+ step: LoginFillStep
157
+ ): Promise<void> {
158
+ const value = await this.resolveFillValue(step, credentials, context);
159
+ await context.forms.fillText(step.selector, value, this.buildFillOptions(definition, step));
160
+
161
+ if (this.isCredentialFillStep(step)) {
162
+ if (step.credential === 'username') {
163
+ await definition.hooks?.afterUsername?.(context);
164
+ } else {
165
+ await definition.hooks?.afterPassword?.(context);
166
+ }
167
+ }
168
+ }
169
+
170
+ private async executeClickStep(
171
+ context: ActorContext,
172
+ definition: LoginFlowDefinition,
173
+ step: ClickLoginStep,
174
+ label: string
175
+ ): Promise<void> {
176
+ if (step.submit) {
177
+ await definition.hooks?.beforeSubmit?.(context);
178
+ }
179
+
180
+ if (step.waitForNavigation) {
181
+ await Promise.all([
182
+ context.page.waitForNavigation(step.navigationOptions ?? { waitUntil: 'domcontentloaded' }),
183
+ context.cursor.click(step.selector, this.clickOptions(step))
184
+ ]);
185
+ } else {
186
+ await context.cursor.click(step.selector, this.clickOptions(step));
187
+ }
188
+
189
+ if (step.submit) {
190
+ await definition.hooks?.afterSubmit?.(context);
191
+ }
192
+
193
+ if ((step.waitAfterMs ?? 0) > 0) {
194
+ await delay(step.waitAfterMs ?? 0);
195
+ }
196
+
197
+ if (this.shouldCheckForStepError(definition, step.checkForError)) {
198
+ await this.throwIfConfiguredErrorIsVisible(context, definition, label);
199
+ }
200
+
201
+ if (step.waitForSelector !== undefined) {
202
+ await this.waitForNextStageSelector(context, definition, step, label);
203
+ }
204
+ }
205
+
206
+ private async executeWaitForSelectorStep(
207
+ context: ActorContext,
208
+ definition: LoginFlowDefinition,
209
+ step: WaitForSelectorLoginStep,
210
+ label: string
211
+ ): Promise<void> {
212
+ await this.waitForRequiredSelector(context, definition, step.selector, label, step.timeoutMs);
213
+
214
+ if (this.shouldCheckForStepError(definition, step.checkForError)) {
215
+ await this.throwIfConfiguredErrorIsVisible(context, definition, label);
216
+ }
217
+ }
218
+
219
+ private async waitForNextStageSelector(
220
+ context: ActorContext,
221
+ definition: LoginFlowDefinition,
222
+ step: ClickLoginStep,
223
+ label: string
224
+ ): Promise<void> {
225
+ await this.waitForRequiredSelector(
226
+ context,
227
+ definition,
228
+ step.waitForSelector!,
229
+ label,
230
+ step.waitForSelectorTimeoutMs
231
+ );
232
+ }
233
+
234
+ private async waitForRequiredSelector(
235
+ context: ActorContext,
236
+ definition: LoginFlowDefinition,
237
+ selector: string,
238
+ label: string,
239
+ timeoutMs: number | undefined
240
+ ): Promise<void> {
241
+ try {
242
+ await context.page.waitFor(selector, timeoutMs === undefined ? undefined : { timeout: timeoutMs });
243
+ } catch (error) {
244
+ await this.throwIfConfiguredErrorIsVisible(context, definition, label);
245
+ throw new AuthError(`Login step ${label} did not reach expected selector: ${selector}`, {
246
+ actorId: context.actor.id,
247
+ details: { selector, step: label, cause: error }
248
+ });
249
+ }
250
+ }
251
+
252
+ private buildFillOptions(definition: LoginFlowDefinition, step: LoginFillStep): FillTextOptions {
253
+ const options: FillTextOptions = {
254
+ required: step.required ?? true,
255
+ clear: step.clearFieldBeforeTyping ?? definition.behavior?.clearFieldBeforeTyping ?? true,
256
+ clearMethod: step.clearMethod ?? 'select-delete'
257
+ };
258
+
259
+ if (step.timeoutMs !== undefined) {
260
+ options.timeoutMs = step.timeoutMs;
261
+ }
262
+ if (step.clickBeforeTyping !== undefined) {
263
+ options.clickBeforeTyping = step.clickBeforeTyping;
264
+ }
265
+
266
+ const typing = this.mergeStepTyping(definition.behavior?.typing, step.typing);
267
+ if (typing !== undefined) {
268
+ options.typing = typing;
269
+ }
270
+
271
+ return options;
272
+ }
273
+
274
+ private mergeStepTyping(
275
+ behaviorTyping: HumanTypingOptions | undefined,
276
+ stepTyping: HumanTypingOptions | undefined
277
+ ): HumanTypingOptions | undefined {
278
+ if (behaviorTyping === undefined && stepTyping === undefined) return undefined;
279
+ return mergeHumanTypingOptions(behaviorTyping, stepTyping);
280
+ }
281
+
282
+ private async resolveFillValue(
283
+ step: LoginFillStep,
284
+ credentials: Credentials,
285
+ context: ActorContext
286
+ ): Promise<string> {
287
+ if (this.isCredentialFillStep(step)) {
288
+ return credentials[step.credential];
289
+ }
290
+
291
+ if (typeof step.value === 'function') {
292
+ return step.value(credentials, context);
293
+ }
294
+
295
+ return step.value;
296
+ }
297
+
298
+ private isCredentialFillStep(step: LoginFillStep): step is FillCredentialLoginStep {
299
+ return 'credential' in step;
300
+ }
301
+
302
+ private clickOptions(step: ClickLoginStep): { timeoutMs?: number } | undefined {
303
+ if (step.timeoutMs === undefined) return undefined;
304
+ return { timeoutMs: step.timeoutMs };
305
+ }
306
+
307
+ private shouldCheckForStepError(definition: LoginFlowDefinition, checkForError: boolean | undefined): boolean {
308
+ return definition.selectors.errorMessage !== undefined && (checkForError ?? true);
309
+ }
310
+
311
+ private async throwIfConfiguredErrorIsVisible(
312
+ context: ActorContext,
313
+ definition: LoginFlowDefinition,
314
+ stepLabel?: string
315
+ ): Promise<void> {
316
+ if (definition.selectors.errorMessage === undefined) return;
317
+
318
+ const hasError = await context.page.exists(definition.selectors.errorMessage, {
319
+ timeout: definition.behavior?.errorTimeoutMs ?? 500
320
+ });
321
+
322
+ if (!hasError) return;
323
+
324
+ const message = await context.page.text(definition.selectors.errorMessage);
325
+ const prefix = stepLabel === undefined ? 'Login failed' : `Login failed during step ${stepLabel}`;
326
+ throw new AuthError(`${prefix}: ${message}`, { actorId: context.actor.id });
327
+ }
328
+
329
+ private stepLabel(step: LoginStep, index: number): string {
330
+ return step.name === undefined ? `#${index + 1}` : `"${step.name}"`;
331
+ }
332
+ }
@@ -0,0 +1,141 @@
1
+ import type { WaitForOptions } from 'puppeteer-core';
2
+ import type { ActorContext } from '../core/ActorContext.js';
3
+ import type { ClearFieldStrategy } from '../interaction/FieldClearer.js';
4
+ import type { HumanTypingOptions } from '../interaction/HumanTyping.js';
5
+ import type { CredentialRef, Credentials } from './CredentialsProvider.js';
6
+
7
+ export interface LoginResultSelectors {
8
+ loggedInSignal: string;
9
+ errorMessage?: string;
10
+ }
11
+
12
+ export interface StandardLoginSelectors extends LoginResultSelectors {
13
+ username: string;
14
+ password: string;
15
+ submit: string;
16
+ }
17
+
18
+ export type LoginSelectors = LoginResultSelectors | StandardLoginSelectors;
19
+
20
+ export type AuthHook = (context: ActorContext) => Promise<void> | void;
21
+ export type AuthVerifier = (context: ActorContext) => Promise<boolean> | boolean;
22
+ export type CredentialField = keyof Pick<Credentials, 'username' | 'password'>;
23
+ export type LoginStepValueResolver = (credentials: Credentials, context: ActorContext) => Promise<string> | string;
24
+
25
+ interface LoginStepBase {
26
+ /** Optional diagnostic name used in logs and errors. */
27
+ name?: string;
28
+ before?: AuthHook;
29
+ after?: AuthHook;
30
+ }
31
+
32
+ export interface FillCredentialLoginStep extends LoginStepBase {
33
+ type: 'fill';
34
+ selector: string;
35
+ /** Choose which credential value to type into this field. */
36
+ credential: CredentialField;
37
+ required?: boolean;
38
+ timeoutMs?: number;
39
+ clickBeforeTyping?: boolean;
40
+ /** Overrides LoginBehavior.clearFieldBeforeTyping for this individual field. */
41
+ clearFieldBeforeTyping?: boolean;
42
+ clearMethod?: ClearFieldStrategy;
43
+ /** Overrides LoginBehavior.typing for this individual field. */
44
+ typing?: HumanTypingOptions;
45
+ }
46
+
47
+ export interface FillValueLoginStep extends LoginStepBase {
48
+ type: 'fill';
49
+ selector: string;
50
+ /** Literal value or value resolver for non-credential login fields, such as tenant or workspace. */
51
+ value: string | LoginStepValueResolver;
52
+ required?: boolean;
53
+ timeoutMs?: number;
54
+ clickBeforeTyping?: boolean;
55
+ /** Overrides LoginBehavior.clearFieldBeforeTyping for this individual field. */
56
+ clearFieldBeforeTyping?: boolean;
57
+ clearMethod?: ClearFieldStrategy;
58
+ /** Overrides LoginBehavior.typing for this individual field. */
59
+ typing?: HumanTypingOptions;
60
+ }
61
+
62
+ export type LoginFillStep = FillCredentialLoginStep | FillValueLoginStep;
63
+
64
+ export interface ClickLoginStep extends LoginStepBase {
65
+ type: 'click';
66
+ selector: string;
67
+ timeoutMs?: number;
68
+ waitForNavigation?: boolean;
69
+ navigationOptions?: WaitForOptions;
70
+ /** Wait for the next login-stage selector after the click, e.g. a password field. */
71
+ waitForSelector?: string;
72
+ waitForSelectorTimeoutMs?: number;
73
+ waitAfterMs?: number;
74
+ /** Marks this click as the final submit action, so legacy beforeSubmit/afterSubmit hooks are applied. */
75
+ submit?: boolean;
76
+ /** Defaults to true for click steps when selectors.errorMessage is configured. */
77
+ checkForError?: boolean;
78
+ }
79
+
80
+ export interface WaitForSelectorLoginStep extends LoginStepBase {
81
+ type: 'waitForSelector';
82
+ selector: string;
83
+ timeoutMs?: number;
84
+ /** Defaults to true when selectors.errorMessage is configured. */
85
+ checkForError?: boolean;
86
+ }
87
+
88
+ export interface HookLoginStep {
89
+ type: 'hook';
90
+ name?: string;
91
+ run: AuthHook;
92
+ }
93
+
94
+ export type LoginStep = LoginFillStep | ClickLoginStep | WaitForSelectorLoginStep | HookLoginStep;
95
+
96
+ export interface LoginBehavior {
97
+ authCheckUrl?: string;
98
+ /** Used by the legacy single-submit login flow when steps are omitted. */
99
+ submitCausesNavigation?: boolean;
100
+ waitAfterSubmitMs?: number;
101
+ maxAttempts?: number;
102
+ loggedInTimeoutMs?: number;
103
+ errorTimeoutMs?: number;
104
+ /**
105
+ * Selects and deletes existing login field contents before typing.
106
+ * Defaults to true for the standardized login flow and every fill step.
107
+ */
108
+ clearFieldBeforeTyping?: boolean;
109
+ /** Optional per-login typing profile. Defaults to approximately 65 WPM with tiny jitter. */
110
+ typing?: HumanTypingOptions;
111
+ }
112
+
113
+ export interface LoginFlowDefinition {
114
+ loginUrl: string;
115
+ /**
116
+ * For a simple one-page login, include username/password/submit/loggedInSignal.
117
+ * For a multi-step login, loggedInSignal is required and username/password/submit are optional
118
+ * because the ordered `steps` array supplies the fields and buttons.
119
+ */
120
+ selectors: LoginSelectors;
121
+ credentials?: CredentialRef;
122
+ /** Optional ordered flow for username-first/password-second or other multi-stage forms. */
123
+ steps?: LoginStep[];
124
+ behavior?: LoginBehavior;
125
+ hooks?: {
126
+ beforeLogin?: AuthHook;
127
+ afterUsername?: AuthHook;
128
+ afterPassword?: AuthHook;
129
+ beforeSubmit?: AuthHook;
130
+ afterSubmit?: AuthHook;
131
+ verifyLoggedIn?: AuthVerifier;
132
+ };
133
+ }
134
+
135
+ export function defineLoginFlow(definition: LoginFlowDefinition): LoginFlowDefinition {
136
+ return definition;
137
+ }
138
+
139
+ export function hasStandardLoginSelectors(selectors: LoginSelectors): selectors is StandardLoginSelectors {
140
+ return 'username' in selectors && 'password' in selectors && 'submit' in selectors;
141
+ }
@@ -0,0 +1,21 @@
1
+ export interface StoredSession {
2
+ cookies?: unknown[];
3
+ localStorage?: Record<string, string>;
4
+ savedAt: string;
5
+ }
6
+
7
+ export interface SessionStore {
8
+ load(actorId: string, accountId?: string): Promise<StoredSession | null>;
9
+ save(actorId: string, session: StoredSession, accountId?: string): Promise<void>;
10
+ clear(actorId: string, accountId?: string): Promise<void>;
11
+ }
12
+
13
+ export class NoopSessionStore implements SessionStore {
14
+ async load(): Promise<StoredSession | null> {
15
+ return null;
16
+ }
17
+
18
+ async save(): Promise<void> {}
19
+
20
+ async clear(): Promise<void> {}
21
+ }
@@ -0,0 +1,5 @@
1
+ export * from './AuthStateDetector.js';
2
+ export * from './CredentialsProvider.js';
3
+ export * from './LoginFlow.js';
4
+ export * from './LoginFlow.types.js';
5
+ export * from './SessionStore.js';