@opencraw/core 0.1.1 → 0.1.3

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 (65) hide show
  1. package/README.md +4 -2
  2. package/dist/index.esm.js +3010 -638
  3. package/dist/src/access/access-profile.contract.d.ts +4 -0
  4. package/dist/src/access/index.d.ts +1 -1
  5. package/dist/src/api-steps/extract-from-document.use-case.d.ts +16 -3
  6. package/dist/src/api-steps/index.d.ts +1 -1
  7. package/dist/src/api-steps/send-request.use-case.d.ts +5 -4
  8. package/dist/src/browser-session/browser-profile.store.d.ts +52 -0
  9. package/dist/src/browser-session/browser.client.d.ts +8 -0
  10. package/dist/src/browser-session/index.d.ts +1 -0
  11. package/dist/src/captcha/captcha-budget.model.d.ts +21 -0
  12. package/dist/src/captcha/captcha-detection.client.d.ts +28 -0
  13. package/dist/src/captcha/captcha-guard.use-case.d.ts +64 -0
  14. package/dist/src/captcha/captcha-solver-registry.store.d.ts +19 -0
  15. package/dist/src/captcha/captcha-solver.contract.d.ts +47 -0
  16. package/dist/src/captcha/captcha.error.d.ts +13 -0
  17. package/dist/src/captcha/index.d.ts +10 -0
  18. package/dist/src/captcha/resolve-captcha.use-case.d.ts +40 -0
  19. package/dist/src/crawl-events/crawl-event.contract.d.ts +48 -0
  20. package/dist/src/crawl-execution/bootstrap-session.use-case.d.ts +27 -3
  21. package/dist/src/crawl-execution/crawl-options.config.d.ts +30 -0
  22. package/dist/src/crawl-execution/crawl-report.model.d.ts +6 -0
  23. package/dist/src/crawl-execution/create-crawler.use-case.d.ts +2 -1
  24. package/dist/src/crawl-execution/rotating-runner.use-case.d.ts +9 -0
  25. package/dist/src/crawl-execution/run-crawl.use-case.d.ts +6 -2
  26. package/dist/src/crawl-execution/run-input-recipe.use-case.d.ts +14 -3
  27. package/dist/src/deck-document/deck-document.model.d.ts +58 -0
  28. package/dist/src/deck-document/deck-table.algorithm.d.ts +35 -0
  29. package/dist/src/deck-document/index.d.ts +6 -0
  30. package/dist/src/deck-document/read-pptx.client.d.ts +16 -0
  31. package/dist/src/extraction-scope/extraction-scope.model.d.ts +3 -1
  32. package/dist/src/http-session/http-response.contract.d.ts +13 -1
  33. package/dist/src/http-session/text-decoding.algorithm.d.ts +35 -0
  34. package/dist/src/index.d.ts +13 -4
  35. package/dist/src/markdown-document/index.d.ts +3 -0
  36. package/dist/src/markdown-document/read-markdown.client.d.ts +29 -0
  37. package/dist/src/pdf-document/index.d.ts +1 -1
  38. package/dist/src/pdf-document/row-assembly.algorithm.d.ts +10 -1
  39. package/dist/src/recipe-loading/recipe-binding.validator.d.ts +3 -1
  40. package/dist/src/recipe-schema/index.d.ts +3 -3
  41. package/dist/src/recipe-schema/input-recipe.contract.d.ts +57 -4
  42. package/dist/src/recipe-schema/recipe-kind.enum.d.ts +3 -2
  43. package/dist/src/recipe-schema/step.contract.d.ts +46 -2
  44. package/dist/src/record-sink/dedupe.policy.d.ts +19 -7
  45. package/dist/src/record-sink/index.d.ts +1 -0
  46. package/dist/src/selection/index.d.ts +1 -1
  47. package/dist/src/selection/json-text.algorithm.d.ts +30 -3
  48. package/dist/src/step-flow/for-each.use-case.d.ts +4 -2
  49. package/dist/src/step-flow/host-throttle.policy.d.ts +49 -0
  50. package/dist/src/step-flow/index.d.ts +5 -0
  51. package/dist/src/step-flow/run-gate.policy.d.ts +12 -1
  52. package/dist/src/step-flow/step-runner.contract.d.ts +13 -0
  53. package/dist/src/step-flow/transport-retry.policy.d.ts +76 -0
  54. package/dist/src/web-steps/navigate.use-case.d.ts +3 -2
  55. package/dist/src/web-steps/run-web-step.use-case.d.ts +18 -3
  56. package/dist/src/workbook-document/csv-parser.algorithm.d.ts +26 -0
  57. package/dist/src/workbook-document/csv-workbook.mapper.d.ts +24 -0
  58. package/dist/src/workbook-document/grid-table.algorithm.d.ts +53 -0
  59. package/dist/src/workbook-document/html-tables.mapper.d.ts +14 -0
  60. package/dist/src/workbook-document/index.d.ts +9 -0
  61. package/dist/src/workbook-document/read-xlsx.client.d.ts +18 -0
  62. package/dist/src/workbook-document/workbook-document.model.d.ts +51 -0
  63. package/dist/src/yaml-document/index.d.ts +3 -0
  64. package/dist/src/yaml-document/read-yaml.client.d.ts +25 -0
  65. package/package.json +16 -2
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import type { ThrottleConfig } from '../step-flow/index.js';
2
3
  /**
3
4
  * Where a crawl's traffic goes. Profiles live in the runner's access config,
4
5
  * never in a recipe: they hold the user's accounts. Every string may use
@@ -84,8 +85,11 @@ export interface AccessConfig {
84
85
  $schema?: string;
85
86
  profiles: Record<string, AccessProfile>;
86
87
  default?: string;
88
+ /** How gently each site is crawled, across every recipe: `{ delayMs?, concurrency?, domains? }`. */
89
+ throttle?: ThrottleConfig;
87
90
  }
88
91
  export declare const accessProfileSchema: z.ZodType<AccessProfile>;
92
+ export declare const throttleConfigSchema: z.ZodType<ThrottleConfig>;
89
93
  export declare const accessConfigSchema: z.ZodType<AccessConfig>;
90
94
  export {};
91
95
  //# sourceMappingURL=access-profile.contract.d.ts.map
@@ -3,7 +3,7 @@ export { loadAccessConfig } from './access-config.repository.js';
3
3
  export { AccessConfigError } from './access-config.error.js';
4
4
  export { ACCESS_PRESETS } from './access-preset.store.js';
5
5
  export type { AccessPreset } from './access-preset.store.js';
6
- export { accessConfigSchema, accessProfileSchema, BLOCKABLE_RESOURCES } from './access-profile.contract.js';
6
+ export { accessConfigSchema, accessProfileSchema, throttleConfigSchema, BLOCKABLE_RESOURCES } from './access-profile.contract.js';
7
7
  export type { AccessConfig, AccessProfile, ProxyProfile, PoolProfile, CdpProfile, PluginProfile, DirectProfile, ProxySettings, BlockableResource } from './access-profile.contract.js';
8
8
  export type { AccessPlugin, AccessLease, LeaseRequest, PluginLeaseRequest } from './access-plugin.contract.js';
9
9
  export { newSessionId } from './session-id.algorithm.js';
@@ -1,10 +1,12 @@
1
- import type { ExtractionScope } from '../extraction-scope/index.js';
1
+ import type { ExtractionScope, ScopeDocument } from '../extraction-scope/index.js';
2
2
  import type { ExtractStep } from '../recipe-schema/index.js';
3
3
  /**
4
4
  * Runs an `extract` step against a static document: the value bound under
5
5
  * `from`, else the scope's current document. `css` reads HTML, `jsonpath`
6
- * reads JSON (or a read PDF's rows), `table` reads a PDF's tables, `regex`
7
- * reads any document as text; `xpath` needs a live page and is refused here.
6
+ * reads JSON (or a read PDF, workbook or deck as data), `table` reads the
7
+ * tables of a PDF, a workbook (a spreadsheet, a CSV), a deck (a presentation)
8
+ * or HTML (its `<table>`s), `regex` reads any document as text; `xpath` needs
9
+ * a live page and is refused here.
8
10
  *
9
11
  * A `jsonpath` extract whose `from` is text parses that text as JSON, and a
10
12
  * list of texts (every `<script type="application/ld+json">` of a page) becomes
@@ -25,4 +27,15 @@ export declare function extractFromDocument(step: ExtractStep, scope: Extraction
25
27
  * @returns The selector to run.
26
28
  */
27
29
  export declare function renderSelector(selector: string, scope: ExtractionScope): string;
30
+ /**
31
+ * The tables a `table` extract finds in a document: a PDF's, a workbook's, a
32
+ * deck's, or an HTML document's `<table>`s (a fetched page, rendered Markdown,
33
+ * a live page's content).
34
+ *
35
+ * @param document - The document.
36
+ * @param step - The extract step.
37
+ * @param scope - Where its selector renders.
38
+ * @returns The tables.
39
+ */
40
+ export declare function tablesIn(document: ScopeDocument, step: ExtractStep, scope: ExtractionScope): unknown[];
28
41
  //# sourceMappingURL=extract-from-document.use-case.d.ts.map
@@ -1,4 +1,4 @@
1
1
  export { ApiStepRunner } from './run-api-step.use-case.js';
2
- export { extractFromDocument, renderSelector } from './extract-from-document.use-case.js';
2
+ export { extractFromDocument, renderSelector, tablesIn } from './extract-from-document.use-case.js';
3
3
  export { sendRequest, documentValue } from './send-request.use-case.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -5,8 +5,9 @@ import type { InputRecipe, RequestStep } from '../recipe-schema/index.js';
5
5
  import type { RunGate } from '../step-flow/index.js';
6
6
  /**
7
7
  * Sends a `request` step: renders its templates, waits for the gate's throttle,
8
- * sends, checks the response against the recipe's block rule, then binds it as
9
- * the scope's current document (and under the step id).
8
+ * sends (again, after a pause, while it fails in passing: `limits.retry`),
9
+ * checks the response against the recipe's block rule, then binds it as the
10
+ * scope's current document (and under the step id).
10
11
  *
11
12
  * @param step - The request step.
12
13
  * @param scope - The scope to render in and bind into.
@@ -14,9 +15,9 @@ import type { RunGate } from '../step-flow/index.js';
14
15
  * @param recipe - The recipe: its limits, block rule and id.
15
16
  * @param gate - Spaces request starts by `delayMs`.
16
17
  * @param events - Where to report the visit.
17
- * @throws BlockedError when the response is a block; HttpError for any other 4xx/5xx.
18
+ * @throws BlockedError when the response is a block, or a captcha page under `session.captcha`; HttpError for any other 4xx/5xx.
18
19
  */
19
20
  export declare function sendRequest(step: RequestStep, scope: ExtractionScope, client: HttpSender, recipe: InputRecipe, gate: RunGate, events: EventBus): Promise<void>;
20
- /** What a step id holds for a document: parsed JSON, the read PDF, or the markup / text. */
21
+ /** What a step id holds for a document: parsed JSON, the read PDF, workbook or deck, or the markup / text. */
21
22
  export declare function documentValue(body: HttpBody): unknown;
22
23
  //# sourceMappingURL=send-request.use-case.d.ts.map
@@ -0,0 +1,52 @@
1
+ import type { BrowserSessionConfig } from './browser-session.config.js';
2
+ import { BrowserSession } from './browser.client.js';
3
+ import type { SessionOptions } from './browser.client.js';
4
+ /** A browser profile name: it becomes a directory, so no separators or dots. */
5
+ export declare const BROWSER_PROFILE_NAME: RegExp;
6
+ /**
7
+ * Browser profiles that persist between runs: each is a directory of a real
8
+ * browser's user data (cookies, local storage, IndexedDB, cache, service
9
+ * workers), so a login, a consent choice or a site's trust in a returning
10
+ * visitor carries over to the next run. The browser equivalent of a user who
11
+ * never clears their history.
12
+ *
13
+ * A profile directory can be open in one browser at a time. Within this
14
+ * crawler, a second use waits for the first to close; the same owner (one
15
+ * recipe run reopening after a rotation) takes it over instead. Another
16
+ * crawler holding it, in this process or another, is reported, not waited
17
+ * for: a lock file in the profile names the process, and one left by a
18
+ * process that died is taken over. (Chromium's own profile lock is not
19
+ * enough: headless builds do not take it.)
20
+ */
21
+ export declare class BrowserProfiles {
22
+ readonly directory: string;
23
+ private readonly config;
24
+ private readonly held;
25
+ private readonly waiting;
26
+ /**
27
+ * @param directory - Where the profiles live, one subdirectory each.
28
+ * @param config - The crawler's browser settings (type, binary, headless, timeouts).
29
+ */
30
+ constructor(directory: string, config?: BrowserSessionConfig);
31
+ private take;
32
+ private free;
33
+ private launch;
34
+ /**
35
+ * The profile's directory.
36
+ *
37
+ * @param name - A profile name.
38
+ * @returns The absolute path.
39
+ */
40
+ pathOf(name: string): string;
41
+ /**
42
+ * Opens a profile in its own browser, waiting while another run of this
43
+ * crawler uses it.
44
+ *
45
+ * @param name - The profile.
46
+ * @param options - Proxy, headers, viewport, cookies to add. `storageState` is ignored: the profile has its own.
47
+ * @param owner - Who opens it; the same owner reopening closes its previous session first.
48
+ * @returns The session; closing it frees the profile.
49
+ */
50
+ open(name: string, options: SessionOptions, owner: object): Promise<BrowserSession>;
51
+ }
52
+ //# sourceMappingURL=browser-profile.store.d.ts.map
@@ -39,6 +39,14 @@ export declare class BrowserSession {
39
39
  storageState(): Promise<StorageState>;
40
40
  close(): Promise<void>;
41
41
  }
42
+ /**
43
+ * What a context gets after it opened: the cookies to add and the resource
44
+ * types to skip.
45
+ *
46
+ * @param context - The context.
47
+ * @param options - The session options.
48
+ */
49
+ export declare function applySessionExtras(context: BrowserContext, options: SessionOptions): Promise<void>;
42
50
  /** A launched browser; sessions are opened from it and closed independently. */
43
51
  export declare class BrowserClient {
44
52
  private readonly browser;
@@ -1,4 +1,5 @@
1
1
  export { BrowserClient, BrowserSession } from './browser.client.js';
2
+ export { BrowserProfiles, BROWSER_PROFILE_NAME } from './browser-profile.store.js';
2
3
  export type { StorageState, SessionOptions } from './browser.client.js';
3
4
  export { DEFAULT_BROWSER_CONFIG } from './browser-session.config.js';
4
5
  export type { BrowserSessionConfig } from './browser-session.config.js';
@@ -0,0 +1,21 @@
1
+ /** Default solves a recipe run may spend. */
2
+ export declare const DEFAULT_MAX_SOLVES = 10;
3
+ /**
4
+ * The solves a recipe run may still spend. Every solve costs money: a detector
5
+ * that matches the wrong element would drain a balance without it. Shared by
6
+ * every runner of the run, rotations included.
7
+ */
8
+ export declare class CaptchaBudget {
9
+ readonly max: number;
10
+ private used;
11
+ constructor(max?: number);
12
+ /** Solves spent so far. */
13
+ get spent(): number;
14
+ /**
15
+ * Spends one solve.
16
+ *
17
+ * @returns Whether one was left.
18
+ */
19
+ take(): boolean;
20
+ }
21
+ //# sourceMappingURL=captcha-budget.model.d.ts.map
@@ -0,0 +1,28 @@
1
+ import type { Page } from 'playwright';
2
+ import type { CaptchaChallenge } from './captcha-solver.contract.js';
3
+ /** The widgets detection looks for when a recipe names none: reCAPTCHA v2, hCaptcha and Turnstile, as a container or as their iframe. */
4
+ export declare const DEFAULT_CAPTCHA_SELECTOR: string;
5
+ /**
6
+ * The first visible challenge on the page, if any. An element counts only
7
+ * when visible: sites keep hidden widgets around after a solve, and an
8
+ * invisible reCAPTCHA shows nothing until it challenges.
9
+ *
10
+ * @param page - The live page.
11
+ * @param selector - Where challenges are; `DEFAULT_CAPTCHA_SELECTOR` when omitted.
12
+ * @param options - `v3`: also report a reCAPTCHA v3 script (a `captcha` step asks for it; the automatic checks do not, since v3 never blocks a page by itself).
13
+ * @returns The challenge, or `undefined`.
14
+ */
15
+ export declare function detectChallenge(page: Page, selector?: string, options?: {
16
+ v3?: boolean;
17
+ }): Promise<CaptchaChallenge | undefined>;
18
+ /**
19
+ * Whether the page is clear of challenges, tolerating a page that is
20
+ * navigating (a solve often submits a form): an evaluation cut short by the
21
+ * navigation counts as not clear yet.
22
+ *
23
+ * @param page - The live page.
24
+ * @param selector - Where challenges are.
25
+ * @returns Whether no challenge is visible.
26
+ */
27
+ export declare function isClear(page: Page, selector: string): Promise<boolean>;
28
+ //# sourceMappingURL=captcha-detection.client.d.ts.map
@@ -0,0 +1,64 @@
1
+ import type { Page } from 'playwright';
2
+ import type { AccessLease } from '../access/index.js';
3
+ import type { EventBus } from '../crawl-events/index.js';
4
+ import type { CaptchaStep, InputRecipe } from '../recipe-schema/index.js';
5
+ import type { BlockedError } from '../step-flow/index.js';
6
+ import type { CaptchaBudget } from './captcha-budget.model.js';
7
+ import type { CaptchaSolverRegistry } from './captcha-solver-registry.store.js';
8
+ export interface CaptchaGuardOptions {
9
+ recipe: InputRecipe;
10
+ events: EventBus;
11
+ solvers: CaptchaSolverRegistry;
12
+ /** Shared by every runner of the recipe run, rotations and the bootstrap included. */
13
+ budget: CaptchaBudget;
14
+ /** The access the page goes through, handed to solvers. */
15
+ lease?: AccessLease;
16
+ }
17
+ /**
18
+ * Where a web runner meets captchas: after each navigation, click and key
19
+ * press (`session.captcha`), on a block page (`onBlock.solve`), and at a
20
+ * `captcha` step.
21
+ */
22
+ export declare class CaptchaGuard {
23
+ private readonly options;
24
+ constructor(options: CaptchaGuardOptions);
25
+ private settings;
26
+ private solve;
27
+ /** Whether a block page is searched for a challenge before the block counts. */
28
+ get solvesBlocks(): boolean;
29
+ /**
30
+ * The automatic check: with `session.captcha`, solves the challenge the page
31
+ * shows, if any. Without it, nothing is looked for.
32
+ *
33
+ * @param page - The live page.
34
+ * @throws CaptchaError when the challenge could not be solved.
35
+ */
36
+ check(page: Page): Promise<void>;
37
+ /**
38
+ * A block page under `onBlock.solve`: solves the challenge it shows. A block
39
+ * without a challenge stays a block.
40
+ *
41
+ * @param page - The page showing the block.
42
+ * @param blocked - The block.
43
+ * @throws BlockedError (`blocked`) when the page shows no challenge; CaptchaError when it could not be solved.
44
+ */
45
+ solveBlock(page: Page, blocked: BlockedError): Promise<void>;
46
+ /**
47
+ * A `captcha` step: solves the challenge the page shows, reCAPTCHA v3
48
+ * included; a page without one is fine.
49
+ *
50
+ * @param page - The live page.
51
+ * @param step - The step.
52
+ * @throws CaptchaError when the challenge could not be solved.
53
+ */
54
+ step(page: Page, step: CaptchaStep): Promise<void>;
55
+ }
56
+ /**
57
+ * Every solver name a recipe uses (`session.captcha` and its `captcha`
58
+ * steps, the bootstrap's included), to check them before the run starts.
59
+ *
60
+ * @param recipe - The input recipe.
61
+ * @returns The names, without repeats.
62
+ */
63
+ export declare function captchaSolverNames(recipe: InputRecipe): string[];
64
+ //# sourceMappingURL=captcha-guard.use-case.d.ts.map
@@ -0,0 +1,19 @@
1
+ import type { CaptchaSolver } from './captcha-solver.contract.js';
2
+ /** The captcha solvers a crawler was given, by name. */
3
+ export declare class CaptchaSolverRegistry {
4
+ private readonly solvers;
5
+ private readonly order;
6
+ constructor(solvers?: readonly CaptchaSolver[]);
7
+ /** The registered names. */
8
+ get names(): string[];
9
+ has(name: string): boolean;
10
+ /**
11
+ * The solver of that name.
12
+ *
13
+ * @param name - As a recipe names it.
14
+ * @returns The solver.
15
+ * @throws Error naming what is registered when it is not.
16
+ */
17
+ resolve(name: string): CaptchaSolver;
18
+ }
19
+ //# sourceMappingURL=captcha-solver-registry.store.d.ts.map
@@ -0,0 +1,47 @@
1
+ import type { Page } from 'playwright';
2
+ import type { AccessLease } from '../access/index.js';
3
+ /** The kinds of challenge detection tells apart. */
4
+ export type CaptchaKind = 'recaptcha-v2' | 'recaptcha-v3' | 'hcaptcha' | 'turnstile' | 'image' | 'unknown';
5
+ /** What the engine found on the page: enough for a token service, or for a solver that works on the page itself. */
6
+ export interface CaptchaChallenge {
7
+ kind: CaptchaKind;
8
+ /** The page the challenge is on. */
9
+ url: string;
10
+ /** The widget's site key (`data-sitekey`, or the `k` / `sitekey` of its iframe), when it has one. */
11
+ siteKey?: string;
12
+ /** The widget's action (`data-action`), when it declares one. */
13
+ action?: string;
14
+ /** A Playwright selector for the widget, or for the image of an image captcha. */
15
+ selector?: string;
16
+ }
17
+ export type CaptchaLog = (level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: Record<string, unknown>) => void;
18
+ /** What a solver gets besides the challenge. */
19
+ export interface CaptchaContext {
20
+ /** The live page: the solver may inject a token, fill a field, click. */
21
+ page: Page;
22
+ /** The access in use: token services often need the same IP (proxy) as the browser. */
23
+ lease?: AccessLease;
24
+ /** 1, 2, … within one solve loop. */
25
+ attempt: number;
26
+ /** Aborted when the engine's timeout for this attempt runs out. */
27
+ signal: AbortSignal;
28
+ log: CaptchaLog;
29
+ }
30
+ /** A solver's report. The engine checks it: `solved` is a claim until the page confirms it. */
31
+ export type CaptchaOutcome = {
32
+ status: 'solved';
33
+ } | {
34
+ status: 'failed';
35
+ reason: string;
36
+ };
37
+ /**
38
+ * A captcha solver: solves a challenge and applies the answer on the page
39
+ * (injects a token and calls the widget's callback, types an image's text,
40
+ * submits). Detection, verification, retries, rotation and the budget are the
41
+ * engine's.
42
+ */
43
+ export interface CaptchaSolver {
44
+ name: string;
45
+ solve: (challenge: CaptchaChallenge, context: CaptchaContext) => Promise<CaptchaOutcome> | CaptchaOutcome;
46
+ }
47
+ //# sourceMappingURL=captcha-solver.contract.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { BlockedError } from '../step-flow/index.js';
2
+ /**
3
+ * A challenge the engine could not get past: the solver failed, the page did
4
+ * not confirm it, or the budget ran out. It is a block, so a recipe with
5
+ * `onBlock.rotate` retries the step on a new access lease (a new IP often
6
+ * means an easier challenge, or none), then the step's error policy applies.
7
+ */
8
+ export declare class CaptchaError extends BlockedError {
9
+ readonly kind: string;
10
+ readonly attempts: number;
11
+ constructor(url: string, kind: string, attempts: number, reason: string);
12
+ }
13
+ //# sourceMappingURL=captcha.error.d.ts.map
@@ -0,0 +1,10 @@
1
+ export type { CaptchaSolver, CaptchaChallenge, CaptchaContext, CaptchaOutcome, CaptchaKind, CaptchaLog } from './captcha-solver.contract.js';
2
+ export { CaptchaError } from './captcha.error.js';
3
+ export { CaptchaSolverRegistry } from './captcha-solver-registry.store.js';
4
+ export { CaptchaBudget, DEFAULT_MAX_SOLVES } from './captcha-budget.model.js';
5
+ export { DEFAULT_CAPTCHA_SELECTOR, detectChallenge } from './captcha-detection.client.js';
6
+ export { resolveCaptcha, DEFAULT_CAPTCHA_ATTEMPTS, DEFAULT_CAPTCHA_TIMEOUT_MS } from './resolve-captcha.use-case.js';
7
+ export type { CaptchaAttemptPlan } from './resolve-captcha.use-case.js';
8
+ export { CaptchaGuard, captchaSolverNames } from './captcha-guard.use-case.js';
9
+ export type { CaptchaGuardOptions } from './captcha-guard.use-case.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,40 @@
1
+ import type { Page } from 'playwright';
2
+ import type { AccessLease } from '../access/index.js';
3
+ import type { EventBus } from '../crawl-events/index.js';
4
+ import type { CaptchaCheck } from '../recipe-schema/index.js';
5
+ import type { CaptchaBudget } from './captcha-budget.model.js';
6
+ import type { CaptchaChallenge, CaptchaSolver } from './captcha-solver.contract.js';
7
+ /** Default solves tried per challenge. */
8
+ export declare const DEFAULT_CAPTCHA_ATTEMPTS = 3;
9
+ /** Default time one solve may take: token services take 10 to 60 seconds. */
10
+ export declare const DEFAULT_CAPTCHA_TIMEOUT_MS = 120000;
11
+ /** One challenge to get past, and everything the loop needs for it. */
12
+ export interface CaptchaAttemptPlan {
13
+ recipeId: string;
14
+ page: Page;
15
+ challenge: CaptchaChallenge;
16
+ solver: CaptchaSolver;
17
+ /** Where challenges are, to re-detect and to confirm one is gone. */
18
+ selector: string;
19
+ verify?: CaptchaCheck;
20
+ attempts: number;
21
+ timeoutMs: number;
22
+ budget: CaptchaBudget;
23
+ events: EventBus;
24
+ lease?: AccessLease;
25
+ }
26
+ /**
27
+ * Gets past one challenge: asks the solver, then checks the page (a solver's
28
+ * `solved` is a claim; the challenge must be gone and/or the `verify` element
29
+ * must appear), and tries again with what the page shows next, up to
30
+ * `attempts`. Each try spends one solve of the run's budget.
31
+ *
32
+ * A failed try re-detects the challenge (a widget re-renders after a wrong
33
+ * answer). When it is gone without the page confirming, the page is reloaded
34
+ * for a fresh one; when a reload shows none, there is nothing left to solve.
35
+ *
36
+ * @param plan - The challenge, the solver and the limits.
37
+ * @throws CaptchaError when every try failed, or the budget is spent.
38
+ */
39
+ export declare function resolveCaptcha(plan: CaptchaAttemptPlan): Promise<void>;
40
+ //# sourceMappingURL=resolve-captcha.use-case.d.ts.map
@@ -48,6 +48,54 @@ export type CrawlEvent = (Base & {
48
48
  type: 'access:rotate';
49
49
  attempt: number;
50
50
  reason: string;
51
+ }) |
52
+ /** A request failed in passing (a dropped connection, a 503, a 429) and is sent again after `delayMs`. */
53
+ (Base & {
54
+ type: 'request:retry';
55
+ url: string;
56
+ attempt: number;
57
+ reason: string;
58
+ delayMs: number;
59
+ }) |
60
+ /** A captcha challenge is on the page. */
61
+ (Base & {
62
+ type: 'captcha:detected';
63
+ url: string;
64
+ kind: string;
65
+ siteKey?: string;
66
+ }) |
67
+ /** A solver is trying (`attempt` counts from 1 per challenge). */
68
+ (Base & {
69
+ type: 'captcha:solve';
70
+ url: string;
71
+ kind: string;
72
+ solver: string;
73
+ attempt: number;
74
+ }) |
75
+ /** The page confirmed the solve. */
76
+ (Base & {
77
+ type: 'captcha:solved';
78
+ url: string;
79
+ kind: string;
80
+ solver: string;
81
+ attempt: number;
82
+ durationMs: number;
83
+ }) |
84
+ /** The solver failed, timed out, or the page still shows the challenge. */
85
+ (Base & {
86
+ type: 'captcha:failed';
87
+ url: string;
88
+ kind: string;
89
+ solver: string;
90
+ attempt: number;
91
+ reason: string;
92
+ }) |
93
+ /** A challenge was left unsolved: the run spent its `maxSolves`. */
94
+ (Base & {
95
+ type: 'captcha:budget';
96
+ url: string;
97
+ kind: string;
98
+ max: number;
51
99
  }) | (Base & {
52
100
  type: 'step:start';
53
101
  stepType: string;
@@ -1,14 +1,20 @@
1
1
  import type { AccessLease } from '../access/index.js';
2
- import type { BrowserClient, BrowserSession, SessionOptions, StorageState } from '../browser-session/index.js';
2
+ import type { BrowserClient, BrowserProfiles, BrowserSession, SessionOptions, StorageState } from '../browser-session/index.js';
3
+ import type { CaptchaGuard } from '../captcha/index.js';
3
4
  import type { EventBus } from '../crawl-events/index.js';
4
5
  import type { HookRegistry } from '../hooks/index.js';
5
6
  import type { InputRecipe } from '../recipe-schema/index.js';
7
+ import type { HostThrottle } from '../step-flow/index.js';
6
8
  export interface BootstrapDependencies {
7
9
  /** Launches (or returns) the shared browser; only called when a browser is needed. */
8
10
  browser: () => Promise<BrowserClient>;
9
11
  hooks: HookRegistry;
10
12
  events: EventBus;
11
13
  storageStateDir?: string;
14
+ /** The crawler's per-site throttle: the bootstrap's pages count too. */
15
+ hosts?: HostThrottle;
16
+ /** The runner's persistent browser profiles, for `session.browserProfile`. */
17
+ profiles?: BrowserProfiles;
12
18
  }
13
19
  /**
14
20
  * The session options an access lease contributes: its proxy, TLS leniency,
@@ -27,12 +33,29 @@ export declare function accessOptions(lease: AccessLease | undefined, headers: R
27
33
  * The bootstrap runs through the same access lease as the crawl that follows,
28
34
  * so a login and the requests that use its cookies come from one IP.
29
35
  *
36
+ * With `session.browserProfile`, the bootstrap runs in that profile, and
37
+ * without a bootstrap the profile's own cookies and storage are the state: an
38
+ * api recipe picks up a login a browser left in the profile.
39
+ *
30
40
  * @param recipe - The input recipe.
31
41
  * @param deps - Browser, hooks, events.
32
42
  * @param lease - The recipe run's access; direct when omitted.
43
+ * @param captcha - Solves the bootstrap's captchas (a login form's).
44
+ * @param owner - The recipe run, which a browser profile is held by.
33
45
  * @returns The state, or `undefined` when the recipe declares none.
34
46
  */
35
- export declare function resolveStorageState(recipe: InputRecipe, deps: BootstrapDependencies, lease?: AccessLease): Promise<StorageState | undefined>;
47
+ export declare function resolveStorageState(recipe: InputRecipe, deps: BootstrapDependencies, lease?: AccessLease, captcha?: CaptchaGuard, owner?: object): Promise<StorageState | undefined>;
48
+ /**
49
+ * Opens the recipe's `session.browserProfile` with its session options and
50
+ * the lease's proxy.
51
+ *
52
+ * @param recipe - A recipe with `session.browserProfile`.
53
+ * @param deps - For `profiles`.
54
+ * @param lease - The access lease.
55
+ * @param owner - The recipe run.
56
+ * @returns The session in the profile.
57
+ */
58
+ export declare function openBrowserProfile(recipe: InputRecipe, deps: Pick<BootstrapDependencies, 'profiles'>, lease: AccessLease | undefined, owner: object): Promise<BrowserSession>;
36
59
  /**
37
60
  * The storage state saved by an earlier bootstrap (`session.storageStatePath`), if the recipe names one.
38
61
  *
@@ -49,7 +72,8 @@ export declare function readSavedState(recipe: InputRecipe, deps: Pick<Bootstrap
49
72
  * @param recipe - An input recipe with `session.bootstrap`.
50
73
  * @param browserSession - Where the steps run.
51
74
  * @param deps - Hooks, events, `storageStateDir`.
75
+ * @param captcha - Solves the bootstrap's captchas.
52
76
  * @returns The kept state.
53
77
  */
54
- export declare function runBootstrap(recipe: InputRecipe, browserSession: BrowserSession, deps: Omit<BootstrapDependencies, 'browser'>): Promise<StorageState>;
78
+ export declare function runBootstrap(recipe: InputRecipe, browserSession: BrowserSession, deps: Omit<BootstrapDependencies, 'browser'>, captcha?: CaptchaGuard): Promise<StorageState>;
55
79
  //# sourceMappingURL=bootstrap-session.use-case.d.ts.map
@@ -1,7 +1,10 @@
1
1
  import type { AccessConfig, AccessPlugin } from '../access/index.js';
2
2
  import type { BrowserSessionConfig } from '../browser-session/index.js';
3
+ import type { CaptchaSolver } from '../captcha/index.js';
3
4
  import type { CrawlListener } from '../crawl-events/index.js';
4
5
  import type { HookMap } from '../hooks/index.js';
6
+ import type { ThrottleConfig } from '../step-flow/index.js';
7
+ import type { RetryRule } from '../recipe-schema/index.js';
5
8
  import type { DedupeScope, RecordSink } from '../record-sink/index.js';
6
9
  /** How a crawler is created. Everything is optional. */
7
10
  export interface CrawlOptions {
@@ -14,10 +17,21 @@ export interface CrawlOptions {
14
17
  onEvent?: CrawlListener;
15
18
  /** Default `run`: a key seen once is dropped for the rest of the run. */
16
19
  dedupe?: DedupeScope;
20
+ /**
21
+ * How many input recipes of a set run at once; default 1, one after
22
+ * another. Each has its own browser context or HTTP session; the browser,
23
+ * the sink and the per-site `throttle` are shared.
24
+ */
25
+ parallel?: number;
17
26
  /** Whether a failed input recipe stops the run; default `continue`. */
18
27
  onRecipeError?: 'continue' | 'stop';
19
28
  /** Base directory for relative `storageStatePath` and `saveTo` values. */
20
29
  storageStateDir?: string;
30
+ /**
31
+ * Where `session.browserProfile` profiles live, one directory each. Default:
32
+ * `.opencraw/profiles` under `storageStateDir` (or the working directory).
33
+ */
34
+ profilesDir?: string;
21
35
  /**
22
36
  * Skip records whose key the sink already has (`sink.has`), reporting them as
23
37
  * `skipped`. Needs a sink that can answer, such as `jsonLinesSink(path, { append: true })`.
@@ -31,7 +45,23 @@ export interface CrawlOptions {
31
45
  * with `session.access.profile`. Without it every recipe goes direct.
32
46
  */
33
47
  access?: AccessConfig;
48
+ /**
49
+ * How gently each site is crawled, across every recipe this crawler runs:
50
+ * `delayMs` between request starts and `concurrency` requests in flight,
51
+ * per site, with `domains` for site-specific rules. Defaults to
52
+ * `access.throttle`; without either, only each recipe's `limits` apply.
53
+ */
54
+ throttle?: ThrottleConfig;
55
+ /**
56
+ * How a request that fails in passing (a dropped connection, a timeout, a
57
+ * 503, a 429) is sent again, for recipes whose `limits.retry` says
58
+ * nothing: `{ attempts?, backoffMs?, maxDelayMs?, statuses? }`. Default:
59
+ * three tries, one then two seconds apart, `Retry-After` honoured.
60
+ */
61
+ retry?: RetryRule;
34
62
  /** Plugins `{ kind: 'plugin', name }` profiles refer to. */
35
63
  accessPlugins?: AccessPlugin[];
64
+ /** Solvers recipes name in `session.captcha.solver` and `captcha` steps. */
65
+ captchaSolvers?: CaptchaSolver[];
36
66
  }
37
67
  //# sourceMappingURL=crawl-options.config.d.ts.map
@@ -12,6 +12,12 @@ export interface RecipeReport {
12
12
  stepsSkipped: number;
13
13
  pages: number;
14
14
  durationMs: number;
15
+ /** Captcha challenges met, solved, and solve attempts that failed; present when any was met. */
16
+ captchas?: {
17
+ detected: number;
18
+ solved: number;
19
+ failed: number;
20
+ };
15
21
  /** Set when the recipe stopped on a failure. */
16
22
  error?: string;
17
23
  }
@@ -11,9 +11,10 @@ export interface Crawler {
11
11
  * Creates a crawler. The browser is launched lazily, on the first recipe or
12
12
  * bootstrap that needs it, and shared by every run until `close`.
13
13
  *
14
- * @param options - Hooks, sink, events, browser settings, access, policies.
14
+ * @param options - Hooks, sink, events, browser settings, access, captcha solvers, policies.
15
15
  * @returns The crawler.
16
16
  * @throws AccessConfigError when the access config cannot work.
17
+ * @throws Error when two captcha solvers share a name.
17
18
  */
18
19
  export declare function createCrawler(options?: CrawlOptions): Crawler;
19
20
  //# sourceMappingURL=create-crawler.use-case.d.ts.map
@@ -50,6 +50,15 @@ export declare class RotatingRunner implements StepRunner {
50
50
  private note;
51
51
  runLeaf(step: Step, scope: ExtractionScope): Promise<void>;
52
52
  nextPage(next: PaginateNext, scope: ExtractionScope): Promise<NextPageResult>;
53
+ /**
54
+ * A runner for one parallel iteration, forked from whichever runner is
55
+ * current when it runs a step: after a rotation it forks again from the new
56
+ * one, since the old context is gone (or going). Blocks are noted and
57
+ * rotated like the main runner's.
58
+ *
59
+ * @returns The iteration's runner.
60
+ */
61
+ fork(): Promise<StepRunner>;
53
62
  elements(selector: string, scope: ExtractionScope): Promise<LiveElement[]>;
54
63
  rotate(error: BlockedError): Promise<boolean>;
55
64
  dispose(): Promise<void>;