@epsilon-asi/actors 0.0.3 → 0.0.4

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 -13
  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,50 @@
1
+ import { defineLoginFlow } from '../../auth/LoginFlow.types.js';
2
+ import { defineFormFillerTask } from '../../core/Actor.js';
3
+ import { defineActor } from '../../core/defineActor.js';
4
+ import { myvistageComSelectors } from './myvistage-com.selectors.js';
5
+ import type {
6
+ PingInput,
7
+ PingOutput,
8
+ PostCommentInput,
9
+ PostCommentOutput
10
+ } from './myvistage-com.types.js';
11
+
12
+ export const myvistageComActor = defineActor({
13
+ id: 'myvistage-com',
14
+ baseUrl: 'https://myvistage.com',
15
+ auth: defineLoginFlow({
16
+ loginUrl: 'https://myvistage.com',
17
+ selectors: myvistageComSelectors.login,
18
+ credentials: { id: 'myvistage-com' },
19
+ behavior: {
20
+ authCheckUrl: '/',
21
+ submitCausesNavigation: true,
22
+ loggedInTimeoutMs: 5_000,
23
+ errorTimeoutMs: 1_500,
24
+ typing: {
25
+ targetWordsPerMinute: 65,
26
+ intervalJitterMs: 18
27
+ }
28
+ }
29
+ }),
30
+ tasks: {
31
+ ping: async (_context, input: PingInput = {}): Promise<PingOutput> => ({
32
+ ok: true,
33
+ echo: input.message ?? null
34
+ }),
35
+ postComment: defineFormFillerTask<PostCommentInput, PostCommentOutput>({
36
+ url: '/?status/151341-151341-1779377493/',
37
+ fields: [
38
+ { selector: myvistageComSelectors.postComment.comment, inputKey: 'comment' },
39
+ { selector: myvistageComSelectors.postComment.subject, inputKey: 'subject', required: false }
40
+ ],
41
+ submit: {
42
+ selector: myvistageComSelectors.postComment.submit
43
+ },
44
+ onComplete: (_context, input) => ({
45
+ submittedComment: input.comment,
46
+ submittedSubject: input.subject ?? null
47
+ })
48
+ })
49
+ }
50
+ });
@@ -0,0 +1,14 @@
1
+ export const myvistageComSelectors = {
2
+ login: {
3
+ username: '[name="username"]',
4
+ password: '[name="password"]',
5
+ submit: '#form-login button[type="submit"]',
6
+ loggedInSignal: 'a[href*="logout"], a[href*="signout"]',
7
+ errorMessage: '#mv3login [role="alert"], #mv3login .error, #mv3login .woocommerce-error'
8
+ },
9
+ postComment: {
10
+ comment: 'textarea[name="comment"]',
11
+ subject: 'input[name="subject"]',
12
+ submit: 'div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > button:nth-of-type(2)'
13
+ }
14
+ } as const;
@@ -0,0 +1,18 @@
1
+ export interface PingInput {
2
+ message?: string;
3
+ }
4
+
5
+ export interface PingOutput {
6
+ ok: true;
7
+ echo: string | null;
8
+ }
9
+
10
+ export interface PostCommentInput extends Record<string, string | undefined> {
11
+ comment: string;
12
+ subject?: string;
13
+ }
14
+
15
+ export interface PostCommentOutput {
16
+ submittedComment: string;
17
+ submittedSubject: string | null;
18
+ }
@@ -0,0 +1,81 @@
1
+ [
2
+ {
3
+ "actionType": "click",
4
+ "frameUrl": "https://myvistage.com/?status/151341-151341-1779377493/",
5
+ "id": "evt_1779456308524_qrd1e362",
6
+ "input": null,
7
+ "metadata": {
8
+ "ariaLabel": null,
9
+ "button": 0,
10
+ "buttons": 0,
11
+ "clientX": 529,
12
+ "clientY": 1042,
13
+ "id": null,
14
+ "inputType": null,
15
+ "label": "Write a comment...",
16
+ "name": "comment",
17
+ "pageX": 529,
18
+ "pageY": 1042,
19
+ "role": null,
20
+ "sensitive": false,
21
+ "tagName": "textarea"
22
+ },
23
+ "selector": "[placeholder=\"Write a comment...\"]",
24
+ "sessionId": "session_1779456305296_opr4xo87",
25
+ "tagName": "textarea",
26
+ "text": null,
27
+ "timestamp": "2026-05-22T13:25:08.522Z",
28
+ "url": "https://myvistage.com/?status/151341-151341-1779377493/"
29
+ },
30
+ {
31
+ "actionType": "input",
32
+ "frameUrl": "https://myvistage.com/?status/151341-151341-1779377493/",
33
+ "id": "evt_1779456314495_qrzdnyxn",
34
+ "input": null,
35
+ "metadata": {
36
+ "ariaLabel": null,
37
+ "id": null,
38
+ "inputType": null,
39
+ "label": "Write a comment...",
40
+ "name": "comment",
41
+ "role": null,
42
+ "sensitive": false,
43
+ "tagName": "textarea",
44
+ "valueLength": null
45
+ },
46
+ "selector": "[placeholder=\"Write a comment...\"]",
47
+ "sessionId": "session_1779456305296_opr4xo87",
48
+ "tagName": "textarea",
49
+ "text": null,
50
+ "timestamp": "2026-05-22T13:25:14.139Z",
51
+ "url": "https://myvistage.com/?status/151341-151341-1779377493/"
52
+ },
53
+ {
54
+ "actionType": "click",
55
+ "frameUrl": "https://myvistage.com/?status/151341-151341-1779377493/",
56
+ "id": "evt_1779456319321_fujuvmqc",
57
+ "input": null,
58
+ "metadata": {
59
+ "ariaLabel": null,
60
+ "button": 0,
61
+ "buttons": 0,
62
+ "clientX": 1167,
63
+ "clientY": 1042,
64
+ "id": null,
65
+ "inputType": null,
66
+ "label": "Post",
67
+ "name": null,
68
+ "pageX": 1167,
69
+ "pageY": 1042,
70
+ "role": null,
71
+ "sensitive": false,
72
+ "tagName": "button"
73
+ },
74
+ "selector": "div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > button:nth-of-type(2)",
75
+ "sessionId": "session_1779456305296_opr4xo87",
76
+ "tagName": "button",
77
+ "text": "Post",
78
+ "timestamp": "2026-05-22T13:25:19.320Z",
79
+ "url": "https://myvistage.com/?status/151341-151341-1779377493/"
80
+ }
81
+ ]
@@ -0,0 +1,6 @@
1
+ export * from './upwork-com.actor.js';
2
+ export * from './upwork-com.selectors.js';
3
+ export * from './upwork-com.types.js';
4
+ export {parseRate} from "./upwork-com.util.js";
5
+
6
+ export {buildSearchParams} from "./upwork-com.util.js";
@@ -0,0 +1,97 @@
1
+ import {defineLoginFlow} from '../../auth/LoginFlow.types.js';
2
+ import {defineActor} from '../../core/defineActor.js';
3
+ import {upworkComSelectors} from './upwork-com.selectors.js';
4
+ import type {
5
+ UpworkApplyToJobInput,
6
+ UpworkApplyToJobResult,
7
+ UpworkJobSearchFields,
8
+ UpworkJobSearchResult
9
+ } from './upwork-com.types.js';
10
+ import {buildSearchParams, parseRate} from "./upwork-com.util.js";
11
+
12
+ export const upworkComActor = defineActor({
13
+ id: 'upwork-com',
14
+ baseUrl: 'https://upwork.com',
15
+ auth: defineLoginFlow({
16
+ loginUrl: 'https://www.upwork.com/ab/account-security/login',
17
+ selectors: {
18
+ loggedInSignal: upworkComSelectors.login.loggedInSignal,
19
+ errorMessage: upworkComSelectors.login.errorMessage
20
+ },
21
+ credentials: { id: 'upwork' },
22
+ behavior: {
23
+ authCheckUrl: '/',
24
+ loggedInTimeoutMs: 5_000,
25
+ errorTimeoutMs: 1_500,
26
+ typing: {
27
+ targetWordsPerMinute: 65,
28
+ intervalJitterMs: 18
29
+ }
30
+ },
31
+ steps: [
32
+ {
33
+ type: 'fill',
34
+ name: 'username',
35
+ selector: upworkComSelectors.login.username,
36
+ credential: 'username'
37
+ },
38
+ {
39
+ type: 'click',
40
+ name: 'continue to password',
41
+ selector: upworkComSelectors.login.continueToPassword,
42
+ waitForSelector: upworkComSelectors.login.password,
43
+ waitForSelectorTimeoutMs: 5_000
44
+ },
45
+ {
46
+ type: 'fill',
47
+ name: 'password',
48
+ selector: upworkComSelectors.login.password,
49
+ credential: 'password'
50
+ },
51
+ {
52
+ type: 'click',
53
+ name: 'submit password',
54
+ selector: upworkComSelectors.login.submit,
55
+ submit: true,
56
+ waitForNavigation: true,
57
+ checkForError: false
58
+ }
59
+ ]
60
+ }),
61
+ tasks: {
62
+ searchJobs: async (context, input: UpworkJobSearchFields = {}): Promise<any> => {
63
+ const path = '/nx/s/universal-search/jobs?client_hires=1-9,10-&payment_verified=1&q=%27rancher%27%20or%20%27terraform%27%20or%20%27gitops%27%20or%20%27azure%27%20or%20%27microsoft%20azure%27%20or%20%27cloud%20architect%27%20or%20%27ai%20architect%27%20or%20%27forward%20deployed%20engineer%27%20or%20%27aws%27%20or%20%27aks%27%20or%20%27eks%27%20or%20%27gke%27%20or%20%27cloud%20engineer%27%20or%20devops%20or%20kuberentes%20or%20%27platform%20engineer%27%20or%20%27infrastructure%20engineer%27%20or%20"google%20cloud%20platform"%20or%20"GCP"%20or%20"langsmith"%20or%20"langgraph"%20or%20"gemini%20enterprise"&sort=recency&user_location_match=1';
64
+
65
+ await context.nav.goto(path, {});
66
+
67
+ console.log(context.page.raw());
68
+
69
+ // Get by classname class="jobs-main-panel - get all children of article type
70
+ // loop through each. Check
71
+ //
72
+ //
73
+ },
74
+ applyToJob: async (_context, input: UpworkApplyToJobInput): Promise<UpworkApplyToJobResult> => {
75
+ const coverLetter = input.coverLetter.trim();
76
+ if (coverLetter.length === 0) {
77
+ throw new Error('coverLetter must be a non-empty string.');
78
+ }
79
+
80
+ const rate = parseRate(input.rate);
81
+ if (!Number.isFinite(rate) || rate <= 0) {
82
+ throw new Error('rate must be a positive number.');
83
+ }
84
+
85
+ return {
86
+ coverLetter,
87
+ rate
88
+ };
89
+ }
90
+ }
91
+ });
92
+
93
+
94
+ const likeScript = `reactions.action_reactions(this, 1632181); `
95
+ const commentScript = `activity.comment_save(1630927, this);`
96
+
97
+
@@ -0,0 +1,17 @@
1
+ import {ActorRunner} from "../../core/index.js";
2
+ import {upworkComActor} from "./upwork-com.actor.js";
3
+
4
+ const runner = new ActorRunner({
5
+ config: {
6
+ browser: {
7
+ mode: 'remote-debugging',
8
+ }
9
+ }
10
+ });
11
+
12
+
13
+ const run = async () => {
14
+ await runner.run(upworkComActor, 'searchJobs', {} as any)
15
+ }
16
+
17
+ run().catch(e=>console.error(e)).then(r=>console.log(r))
@@ -0,0 +1,10 @@
1
+ export const upworkComSelectors = {
2
+ login: {
3
+ username: '#login_username',
4
+ password: '#login_password',
5
+ continueToPassword: '#login_password_continue',
6
+ submit: '#login_control_continue',
7
+ loggedInSignal: '[data-test="nav-user-dropdown"]',
8
+ errorMessage: '#login_error'
9
+ }
10
+ } as const;
@@ -0,0 +1,102 @@
1
+ export enum JobSearchSort {
2
+ BestMatch = 'best_match',
3
+ }
4
+
5
+ export enum DomesticFilter {
6
+ USOnly = 'us_only',
7
+ }
8
+
9
+ export enum ContractorTier {
10
+ EntryLevel = '1',
11
+ Intermediate = '2',
12
+ Expert = '3',
13
+ }
14
+
15
+ export enum JobType {
16
+ Hourly = 'hourly',
17
+ FixedPrice = 'fixed_price',
18
+ }
19
+
20
+ export enum FixedPriceRange {
21
+ LessThan100 = 'lt_100',
22
+ From100To500 = '100_to_500',
23
+ From500To1000 = '500_to_1000',
24
+ From1000To5000 = '1000_to_5000',
25
+ GreaterThan5000 = 'gte_5000',
26
+ }
27
+
28
+ export enum ProposalRange {
29
+ FewerThan5 = '0-4',
30
+ From5To10 = '5-9',
31
+ From10To15 = '10-14',
32
+ From15To20 = '15-19',
33
+ From20To50 = '20-49',
34
+ }
35
+
36
+ export enum ClientInfoFilter {
37
+ PreviousClients = 'previous_clients',
38
+ PaymentVerified = 'payment_verified',
39
+ }
40
+
41
+ export enum ClientHiresRange {
42
+ NoHires = '0',
43
+ From1To9 = '1-9',
44
+ TenPlus = '10-',
45
+ }
46
+
47
+ export enum ProjectLength {
48
+ LessThanOneMonth = 'week',
49
+ OneToThreeMonths = 'month',
50
+ ThreeToSixMonths = 'semester',
51
+ MoreThanSixMonths = 'ongoing',
52
+ }
53
+
54
+ export enum HoursPerWeek {
55
+ LessThan30 = 'as_needed',
56
+ MoreThan30 = 'full_time',
57
+ }
58
+
59
+ export enum JobDuration {
60
+ ContractToHire = 'contract_to_hire',
61
+ }
62
+
63
+ export type MoneyRange = {
64
+ min?: number;
65
+ max?: number;
66
+ currency?: 'USD';
67
+ };
68
+
69
+ export type UpworkJobSearchFields = {
70
+ sort?: JobSearchSort;
71
+ domestic?: DomesticFilter;
72
+ categories?: string[];
73
+ contractorTier?: ContractorTier[];
74
+ jobType?: JobType[];
75
+ hourlyRate?: MoneyRange;
76
+ fixedPriceRanges?: FixedPriceRange[];
77
+ fixedPriceCustom?: MoneyRange;
78
+ proposals?: ProposalRange[];
79
+ clientInfo?: ClientInfoFilter[];
80
+ clientHires?: ClientHiresRange[];
81
+ clientLocations?: string[];
82
+ clientTimezones?: string[];
83
+ projectLength?: ProjectLength[];
84
+ hoursPerWeek?: HoursPerWeek[];
85
+ jobDuration?: JobDuration[];
86
+ };
87
+
88
+ export interface UpworkJobSearchResult {
89
+ searchUrl: string;
90
+ query: string;
91
+ filters: UpworkJobSearchFields;
92
+ }
93
+
94
+ export interface UpworkApplyToJobInput {
95
+ coverLetter: string;
96
+ rate: number | string;
97
+ }
98
+
99
+ export interface UpworkApplyToJobResult {
100
+ coverLetter: string;
101
+ rate: number;
102
+ }
@@ -0,0 +1,41 @@
1
+ import type {UpworkApplyToJobInput, UpworkJobSearchFields} from "./upwork-com.types.js";
2
+
3
+ export function buildSearchParams(input: UpworkJobSearchFields): URLSearchParams {
4
+ const params = new URLSearchParams();
5
+
6
+ if (input.sort !== undefined) params.set('sort', input.sort);
7
+ if (input.domestic !== undefined) params.set('domestic', input.domestic);
8
+ appendList(params, 'categories', input.categories);
9
+ appendList(params, 'contractor_tier', input.contractorTier);
10
+ appendList(params, 'job_type', input.jobType);
11
+
12
+ if (input.hourlyRate?.min !== undefined) params.set('hourly_rate_min', String(input.hourlyRate.min));
13
+ if (input.hourlyRate?.max !== undefined) params.set('hourly_rate_max', String(input.hourlyRate.max));
14
+ if (input.hourlyRate?.currency !== undefined) params.set('hourly_rate_currency', input.hourlyRate.currency);
15
+
16
+ appendList(params, 'fixed_price_ranges', input.fixedPriceRanges);
17
+ if (input.fixedPriceCustom?.min !== undefined) params.set('fixed_price_min', String(input.fixedPriceCustom.min));
18
+ if (input.fixedPriceCustom?.max !== undefined) params.set('fixed_price_max', String(input.fixedPriceCustom.max));
19
+ if (input.fixedPriceCustom?.currency !== undefined) params.set('fixed_price_currency', input.fixedPriceCustom.currency);
20
+
21
+ appendList(params, 'proposals', input.proposals);
22
+ appendList(params, 'client_info', input.clientInfo);
23
+ appendList(params, 'client_hires', input.clientHires);
24
+ appendList(params, 'client_locations', input.clientLocations);
25
+ appendList(params, 'client_timezones', input.clientTimezones);
26
+ appendList(params, 'project_length', input.projectLength);
27
+ appendList(params, 'hours_per_week', input.hoursPerWeek);
28
+ appendList(params, 'job_duration', input.jobDuration);
29
+
30
+ return params;
31
+ }
32
+
33
+ function appendList(params: URLSearchParams, key: string, values: readonly string[] | undefined): void {
34
+ if (values === undefined || values.length === 0) return;
35
+ params.set(key, values.join(','));
36
+ }
37
+
38
+ export function parseRate(rate: UpworkApplyToJobInput['rate']): number {
39
+ if (typeof rate === 'number') return rate;
40
+ return Number(rate.trim());
41
+ }
@@ -0,0 +1,4 @@
1
+ export function delay(ms: number): Promise<void> {
2
+ if (ms <= 0) return Promise.resolve();
3
+ return new Promise(resolve => setTimeout(resolve, ms));
4
+ }
@@ -0,0 +1,5 @@
1
+ export * from './delay.js';
2
+ export * from './invariant.js';
3
+ export * from './redact.js';
4
+ export * from './retry.js';
5
+ export * from './url.js';
@@ -0,0 +1,7 @@
1
+ import { ConfigError } from '../errors/ConfigError.js';
2
+
3
+ export function invariant(condition: unknown, message: string): asserts condition {
4
+ if (!condition) {
5
+ throw new ConfigError(message);
6
+ }
7
+ }
@@ -0,0 +1,53 @@
1
+ const DEFAULT_REDACTION_KEYS = [
2
+ 'password',
3
+ 'passwd',
4
+ 'secret',
5
+ 'token',
6
+ 'apiKey',
7
+ 'api_key',
8
+ 'authorization',
9
+ 'auth',
10
+ 'cookie',
11
+ 'session',
12
+ 'credential',
13
+ 'refreshToken',
14
+ 'accessToken'
15
+ ];
16
+
17
+ export interface RedactOptions {
18
+ replacement?: string;
19
+ sensitiveKeys?: string[];
20
+ }
21
+
22
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ }
25
+
26
+ function keyIsSensitive(key: string, sensitiveKeys: string[]): boolean {
27
+ const normalized = key.toLowerCase();
28
+ return sensitiveKeys.some(sensitiveKey => normalized.includes(sensitiveKey.toLowerCase()));
29
+ }
30
+
31
+ export function redact<T>(value: T, options: RedactOptions = {}): T {
32
+ const replacement = options.replacement ?? '[REDACTED]';
33
+ const sensitiveKeys = options.sensitiveKeys ?? DEFAULT_REDACTION_KEYS;
34
+
35
+ const visit = (input: unknown): unknown => {
36
+ if (Array.isArray(input)) {
37
+ return input.map(item => visit(item));
38
+ }
39
+
40
+ if (!isPlainObject(input)) {
41
+ return input;
42
+ }
43
+
44
+ return Object.fromEntries(
45
+ Object.entries(input).map(([key, nestedValue]) => [
46
+ key,
47
+ keyIsSensitive(key, sensitiveKeys) ? replacement : visit(nestedValue)
48
+ ])
49
+ );
50
+ };
51
+
52
+ return visit(value) as T;
53
+ }
@@ -0,0 +1,31 @@
1
+ import { delay } from './delay.js';
2
+
3
+ export interface RetryPolicy {
4
+ attempts: number;
5
+ baseDelayMs: number;
6
+ maxDelayMs?: number;
7
+ jitter?: boolean;
8
+ }
9
+
10
+ export async function retry<T>(fn: () => Promise<T>, policy: RetryPolicy): Promise<T> {
11
+ if (!Number.isInteger(policy.attempts) || policy.attempts < 1) {
12
+ throw new Error('Retry attempts must be a positive integer.');
13
+ }
14
+
15
+ let lastError: unknown;
16
+ for (let attempt = 1; attempt <= policy.attempts; attempt += 1) {
17
+ try {
18
+ return await fn();
19
+ } catch (error) {
20
+ lastError = error;
21
+ if (attempt === policy.attempts) break;
22
+
23
+ const exponentialDelay = policy.baseDelayMs * 2 ** (attempt - 1);
24
+ const cappedDelay = Math.min(exponentialDelay, policy.maxDelayMs ?? exponentialDelay);
25
+ const jitter = policy.jitter ? Math.floor(Math.random() * Math.max(1, cappedDelay / 3)) : 0;
26
+ await delay(cappedDelay + jitter);
27
+ }
28
+ }
29
+
30
+ throw lastError;
31
+ }
@@ -0,0 +1,7 @@
1
+ export function resolveUrl(baseUrl: string, urlOrPath: string): string {
2
+ try {
3
+ return new URL(urlOrPath).toString();
4
+ } catch {
5
+ return new URL(urlOrPath, baseUrl).toString();
6
+ }
7
+ }
@@ -0,0 +1,12 @@
1
+ import type { CredentialRef, Credentials, CredentialsProvider } from '../../src/auth/CredentialsProvider.js';
2
+
3
+ export class FakeCredentialsProvider implements CredentialsProvider {
4
+ readonly requested: CredentialRef[] = [];
5
+
6
+ constructor(private readonly credentials: Credentials = { username: 'user@example.com', password: 'secret' }) {}
7
+
8
+ async getCredentials(ref: CredentialRef): Promise<Credentials> {
9
+ this.requested.push(ref);
10
+ return this.credentials;
11
+ }
12
+ }
@@ -0,0 +1,48 @@
1
+ import type { HumanClickOptions, HumanInteractor, HumanMoveOptions, HumanTypeOptions } from '../../src/interaction/HumanInteractor.js';
2
+
3
+ export class FakeHumanInteractor implements HumanInteractor {
4
+ clicks: Array<{ selector: string; options?: HumanClickOptions }> = [];
5
+ moves: Array<{ selector: string; options?: HumanMoveOptions }> = [];
6
+ typed: Array<{ selector: string; value: string; options?: HumanTypeOptions }> = [];
7
+ scrolled: string[] = [];
8
+
9
+ async click(selector: string, options?: HumanClickOptions): Promise<void> {
10
+ const record: { selector: string; options?: HumanClickOptions } = { selector };
11
+ if (options !== undefined) record.options = options;
12
+ this.clicks.push(record);
13
+ }
14
+
15
+ async move(selector: string, options?: HumanMoveOptions): Promise<void> {
16
+ const record: { selector: string; options?: HumanMoveOptions } = { selector };
17
+ if (options !== undefined) record.options = options;
18
+ this.moves.push(record);
19
+ }
20
+
21
+ async type(selector: string, value: string, options?: HumanTypeOptions): Promise<void> {
22
+ const record: { selector: string; value: string; options?: HumanTypeOptions } = { selector, value };
23
+ if (options !== undefined) record.options = options;
24
+ this.typed.push(record);
25
+ }
26
+
27
+ async scrollIntoView(selector: string): Promise<void> {
28
+ this.scrolled.push(selector);
29
+ }
30
+ }
31
+
32
+ export class FakeGhostCursor {
33
+ clicks: Array<{ selector?: string; options?: Record<string, unknown> }> = [];
34
+ moves: Array<{ selector: string; options?: Record<string, unknown> }> = [];
35
+
36
+ async click(selector?: string, options?: Record<string, unknown>): Promise<void> {
37
+ const record: { selector?: string; options?: Record<string, unknown> } = {};
38
+ if (selector !== undefined) record.selector = selector;
39
+ if (options !== undefined) record.options = options;
40
+ this.clicks.push(record);
41
+ }
42
+
43
+ async move(selector: string, options?: Record<string, unknown>): Promise<void> {
44
+ const record: { selector: string; options?: Record<string, unknown> } = { selector };
45
+ if (options !== undefined) record.options = options;
46
+ this.moves.push(record);
47
+ }
48
+ }