scrapme-flow 0.0.0-stage → 0.2.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -2
  3. package/dist/src/commands/commands.models.d.ts +41 -0
  4. package/dist/src/commands/commands.models.js +2 -0
  5. package/dist/src/commands/commands.models.js.map +1 -0
  6. package/dist/src/commands/save-document.command.d.ts +9 -0
  7. package/dist/src/commands/save-document.command.js +33 -0
  8. package/dist/src/commands/save-document.command.js.map +1 -0
  9. package/dist/src/commands/scrape-page.command.d.ts +20 -0
  10. package/dist/src/commands/scrape-page.command.js +93 -0
  11. package/dist/src/commands/scrape-page.command.js.map +1 -0
  12. package/dist/src/commands/send-mail.command.d.ts +11 -0
  13. package/dist/src/commands/send-mail.command.js +39 -0
  14. package/dist/src/commands/send-mail.command.js.map +1 -0
  15. package/dist/src/config/config-builder.d.ts +24 -0
  16. package/dist/src/config/config-builder.js +129 -0
  17. package/dist/src/config/config-builder.js.map +1 -0
  18. package/dist/src/config/config-loader.d.ts +10 -0
  19. package/dist/src/config/config-loader.js +79 -0
  20. package/dist/src/config/config-loader.js.map +1 -0
  21. package/dist/src/config/config.models.d.ts +144 -0
  22. package/dist/src/config/config.models.js +2 -0
  23. package/dist/src/config/config.models.js.map +1 -0
  24. package/dist/src/config/config.schema.d.ts +75 -0
  25. package/dist/src/config/config.schema.js +87 -0
  26. package/dist/src/config/config.schema.js.map +1 -0
  27. package/dist/src/config/dsn-check.d.ts +5 -0
  28. package/dist/src/config/dsn-check.js +12 -0
  29. package/dist/src/config/dsn-check.js.map +1 -0
  30. package/dist/src/di/container.d.ts +13 -0
  31. package/dist/src/di/container.js +67 -0
  32. package/dist/src/di/container.js.map +1 -0
  33. package/dist/src/di/types.d.ts +27 -0
  34. package/dist/src/di/types.js +28 -0
  35. package/dist/src/di/types.js.map +1 -0
  36. package/dist/src/flow/command-executor.d.ts +18 -0
  37. package/dist/src/flow/command-executor.js +87 -0
  38. package/dist/src/flow/command-executor.js.map +1 -0
  39. package/dist/src/flow/command-resolver.d.ts +11 -0
  40. package/dist/src/flow/command-resolver.js +35 -0
  41. package/dist/src/flow/command-resolver.js.map +1 -0
  42. package/dist/src/flow/flow-catalog.d.ts +12 -0
  43. package/dist/src/flow/flow-catalog.js +31 -0
  44. package/dist/src/flow/flow-catalog.js.map +1 -0
  45. package/dist/src/flow/flow-errors.d.ts +66 -0
  46. package/dist/src/flow/flow-errors.js +119 -0
  47. package/dist/src/flow/flow-errors.js.map +1 -0
  48. package/dist/src/flow/flow-job-processor.d.ts +33 -0
  49. package/dist/src/flow/flow-job-processor.js +208 -0
  50. package/dist/src/flow/flow-job-processor.js.map +1 -0
  51. package/dist/src/flow/flow-params.d.ts +9 -0
  52. package/dist/src/flow/flow-params.js +52 -0
  53. package/dist/src/flow/flow-params.js.map +1 -0
  54. package/dist/src/flow/flow-runner.d.ts +36 -0
  55. package/dist/src/flow/flow-runner.js +209 -0
  56. package/dist/src/flow/flow-runner.js.map +1 -0
  57. package/dist/src/flow/flow-scheduler.d.ts +24 -0
  58. package/dist/src/flow/flow-scheduler.js +96 -0
  59. package/dist/src/flow/flow-scheduler.js.map +1 -0
  60. package/dist/src/flow/flow-validator.d.ts +29 -0
  61. package/dist/src/flow/flow-validator.js +141 -0
  62. package/dist/src/flow/flow-validator.js.map +1 -0
  63. package/dist/src/flow/flow.d.ts +32 -0
  64. package/dist/src/flow/flow.js +50 -0
  65. package/dist/src/flow/flow.js.map +1 -0
  66. package/dist/src/flow/flow.models.d.ts +288 -0
  67. package/dist/src/flow/flow.models.js +38 -0
  68. package/dist/src/flow/flow.models.js.map +1 -0
  69. package/dist/src/flow/retry-policy.d.ts +9 -0
  70. package/dist/src/flow/retry-policy.js +15 -0
  71. package/dist/src/flow/retry-policy.js.map +1 -0
  72. package/dist/src/flow/state-path.d.ts +21 -0
  73. package/dist/src/flow/state-path.js +37 -0
  74. package/dist/src/flow/state-path.js.map +1 -0
  75. package/dist/src/flow/step-input-builder.d.ts +15 -0
  76. package/dist/src/flow/step-input-builder.js +63 -0
  77. package/dist/src/flow/step-input-builder.js.map +1 -0
  78. package/dist/src/flows/scrape.flow.d.ts +6 -0
  79. package/dist/src/flows/scrape.flow.js +31 -0
  80. package/dist/src/flows/scrape.flow.js.map +1 -0
  81. package/dist/src/index.d.ts +57 -0
  82. package/dist/src/index.js +50 -0
  83. package/dist/src/index.js.map +1 -0
  84. package/dist/src/logging/logger.d.ts +17 -0
  85. package/dist/src/logging/logger.js +55 -0
  86. package/dist/src/logging/logger.js.map +1 -0
  87. package/dist/src/logging/logger.models.d.ts +25 -0
  88. package/dist/src/logging/logger.models.js +7 -0
  89. package/dist/src/logging/logger.models.js.map +1 -0
  90. package/dist/src/mail/mail.models.d.ts +15 -0
  91. package/dist/src/mail/mail.models.js +2 -0
  92. package/dist/src/mail/mail.models.js.map +1 -0
  93. package/dist/src/mail/smtp-mailer.d.ts +13 -0
  94. package/dist/src/mail/smtp-mailer.js +70 -0
  95. package/dist/src/mail/smtp-mailer.js.map +1 -0
  96. package/dist/src/mail/unconfigured-mailer.d.ts +6 -0
  97. package/dist/src/mail/unconfigured-mailer.js +20 -0
  98. package/dist/src/mail/unconfigured-mailer.js.map +1 -0
  99. package/dist/src/models.d.ts +11 -0
  100. package/dist/src/models.js +10 -0
  101. package/dist/src/models.js.map +1 -0
  102. package/dist/src/monitor/monitor-api.d.ts +18 -0
  103. package/dist/src/monitor/monitor-api.js +62 -0
  104. package/dist/src/monitor/monitor-api.js.map +1 -0
  105. package/dist/src/monitor/monitor-server.d.ts +33 -0
  106. package/dist/src/monitor/monitor-server.js +142 -0
  107. package/dist/src/monitor/monitor-server.js.map +1 -0
  108. package/dist/src/monitor/monitor.models.d.ts +27 -0
  109. package/dist/src/monitor/monitor.models.js +2 -0
  110. package/dist/src/monitor/monitor.models.js.map +1 -0
  111. package/dist/src/queue/pg-boss-flow-queue.d.ts +22 -0
  112. package/dist/src/queue/pg-boss-flow-queue.js +102 -0
  113. package/dist/src/queue/pg-boss-flow-queue.js.map +1 -0
  114. package/dist/src/queue/queue.models.d.ts +38 -0
  115. package/dist/src/queue/queue.models.js +2 -0
  116. package/dist/src/queue/queue.models.js.map +1 -0
  117. package/dist/src/reporting/lighty-error-reporter.d.ts +24 -0
  118. package/dist/src/reporting/lighty-error-reporter.js +96 -0
  119. package/dist/src/reporting/lighty-error-reporter.js.map +1 -0
  120. package/dist/src/reporting/log-error-reporter.d.ts +12 -0
  121. package/dist/src/reporting/log-error-reporter.js +35 -0
  122. package/dist/src/reporting/log-error-reporter.js.map +1 -0
  123. package/dist/src/reporting/reporting.models.d.ts +27 -0
  124. package/dist/src/reporting/reporting.models.js +2 -0
  125. package/dist/src/reporting/reporting.models.js.map +1 -0
  126. package/dist/src/scrapme.d.ts +61 -0
  127. package/dist/src/scrapme.js +133 -0
  128. package/dist/src/scrapme.js.map +1 -0
  129. package/dist/src/scrapme.models.d.ts +18 -0
  130. package/dist/src/scrapme.models.js +2 -0
  131. package/dist/src/scrapme.models.js.map +1 -0
  132. package/dist/src/storage/couch-connection.d.ts +19 -0
  133. package/dist/src/storage/couch-connection.js +67 -0
  134. package/dist/src/storage/couch-connection.js.map +1 -0
  135. package/dist/src/storage/couch-database.d.ts +44 -0
  136. package/dist/src/storage/couch-database.js +118 -0
  137. package/dist/src/storage/couch-database.js.map +1 -0
  138. package/dist/src/storage/couch-document-store.d.ts +14 -0
  139. package/dist/src/storage/couch-document-store.js +36 -0
  140. package/dist/src/storage/couch-document-store.js.map +1 -0
  141. package/dist/src/storage/couch-error.d.ts +12 -0
  142. package/dist/src/storage/couch-error.js +19 -0
  143. package/dist/src/storage/couch-error.js.map +1 -0
  144. package/dist/src/storage/couch-flow-run-store.d.ts +24 -0
  145. package/dist/src/storage/couch-flow-run-store.js +66 -0
  146. package/dist/src/storage/couch-flow-run-store.js.map +1 -0
  147. package/dist/src/storage/couch-run-store.d.ts +15 -0
  148. package/dist/src/storage/couch-run-store.js +52 -0
  149. package/dist/src/storage/couch-run-store.js.map +1 -0
  150. package/dist/src/storage/run-doc-ids.d.ts +9 -0
  151. package/dist/src/storage/run-doc-ids.js +15 -0
  152. package/dist/src/storage/run-doc-ids.js.map +1 -0
  153. package/dist/src/storage/storage.models.d.ts +95 -0
  154. package/dist/src/storage/storage.models.js +5 -0
  155. package/dist/src/storage/storage.models.js.map +1 -0
  156. package/dist/src/storage/unconfigured-document-store.d.ts +8 -0
  157. package/dist/src/storage/unconfigured-document-store.js +26 -0
  158. package/dist/src/storage/unconfigured-document-store.js.map +1 -0
  159. package/dist/src/util/preview.d.ts +10 -0
  160. package/dist/src/util/preview.js +44 -0
  161. package/dist/src/util/preview.js.map +1 -0
  162. package/dist/src/util/text.d.ts +8 -0
  163. package/dist/src/util/text.js +16 -0
  164. package/dist/src/util/text.js.map +1 -0
  165. package/dist/src/util/timing.d.ts +10 -0
  166. package/dist/src/util/timing.js +59 -0
  167. package/dist/src/util/timing.js.map +1 -0
  168. package/dist/src/util/url.d.ts +5 -0
  169. package/dist/src/util/url.js +11 -0
  170. package/dist/src/util/url.js.map +1 -0
  171. package/package.json +75 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lastseeds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # scrapme-flow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Scraping flows for Node apps. This repository has two separate things: the **`scrapme-flow` npm library** (root,
4
+ `src/`) and the **monitor** app ([`monitor/`](monitor/README.md)) that shows the runs — deployable on CapRover.
5
+
6
+ A flow is a **`*.flow.ts` file** exporting `new Flow({...})`: a list of steps, each running a small command class (scrape a page, parse it, save it, mail it…). Every command has its own **retries and
7
+ timeout**. `scrapme.flow(flow, input)` puts a run on a **pg-boss queue** (Postgres) and returns at once; a worker runs
8
+ it, and a failed run is run again by pg-boss. The progress of every run is kept in **CouchDB** and shown by a
9
+ separate **monitor** app.
10
+
11
+ ```ts
12
+ import { Container } from 'inversify';
13
+ import { Scrapme } from 'scrapme-flow';
14
+ import { helloFlow } from './hello.flow.js';
15
+
16
+ const scrapme = Scrapme.create({
17
+ container: appContainer, // your inversify container: commands get your services from it
18
+ postgres: { connectionString: 'postgres://scrapme:scrapme@localhost:5434/scrapme' },
19
+ couch: { url: 'http://localhost:5984', user: 'admin', password: secret },
20
+ });
21
+ await scrapme.start(); // connects pg-boss
22
+
23
+ const runId = await scrapme.flow(helloFlow, { name: 'Ada' }); // queues a run; this process runs it
24
+ const run = await scrapme.waitFor(runId); // optional: wait for the end
25
+ console.log(run.status, run.steps[0]?.output);
26
+ await scrapme.stop();
27
+ ```
28
+
29
+ scrapme is a library: it reads **no environment variables** — the app passes the connection data of everything it
30
+ needs. `postgres` and `couch` are required; `scrapAnything`, `mail` and `lighty` only when a flow uses them
31
+ (without them `ScrapePageCommand` / `SendMailCommand` fail with `NOT_CONFIGURED`, errors are only logged).
32
+ Flows are not registered anywhere: you pass the flow to `flow()`.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm install scrapme-flow inversify reflect-metadata
38
+ ```
39
+
40
+ Node 26+, ESM. `inversify` and `reflect-metadata` are peer dependencies (one copy for your app and scrapme).
41
+
42
+ ## Hello world
43
+
44
+ ```ts
45
+ // print-message.command.ts
46
+ @injectable()
47
+ export class PrintMessageCommand implements Command<{ text: string }, string> {
48
+ public constructor(@inject(ConsolePrinter) private readonly printer: ConsolePrinter) {}
49
+ public async execute(input: { text: string }) {
50
+ this.printer.print(input.text);
51
+ return input.text;
52
+ }
53
+ }
54
+
55
+ // hello.flow.ts
56
+ export const helloFlow = new Flow({
57
+ name: 'hello',
58
+ params: { name: { default: 'World' } },
59
+ steps: [{ command: PrintMessageCommand, input: { text: (s) => 'Hello, ' + s.name + '!' } }],
60
+ });
61
+ ```
62
+
63
+ - **State:** each step's output is stored under its `key` (default: the command's class name). `input` picks from it
64
+ (`{ from: 'page.html' }`), adds literals (`{ value }`) or computes in plain JS (`(s) => 'doc:' + s.url`);
65
+ `inputFrom` passes one value; neither = the whole state. Format: [docs/FLOWS.md](docs/FLOWS.md).
66
+ - **Commands:** `@injectable()` classes, nothing to bind — scrapme's container is a child of yours (`autobind`), so
67
+ `@inject(YourService)` just works. Built in: `ScrapePageCommand` (scrap-anything), `SaveDocumentCommand` (CouchDB),
68
+ `SendMailCommand` (SMTP).
69
+
70
+ ## Retries and timeouts
71
+
72
+ | level | what | settings | default |
73
+ |---|---|---|---|
74
+ | command | attempts of one step inside a flow attempt, each with a timeout (`ctx.signal` aborts) | step `retries`, `timeoutMs`, `retryDelayMs` > command `options` > `defaults` | 3 retries, 60 s |
75
+ | flow | the pg-boss job: run the whole flow again after a delay (backoff), expire a too long attempt | flow `retries`, `retryDelaySeconds`, `retryBackoff`, `timeoutSeconds` > `queue` | 3 retries, 30 s, backoff, 1 h |
76
+
77
+ When a step runs out of attempts, the flow attempt fails and pg-boss runs the flow again from its first step.
78
+ `PermanentError` (or a missing input value) is not retried. A run that fails for good is reported once to
79
+ LightyErrors (`lighty.dsn`).
80
+
81
+ ## Monitor
82
+
83
+ The library has no UI. The runs are documents in CouchDB (`couch.runsDb`, `run:<id>`), and the **scrapme monitor**
84
+ — a separate app in [`monitor/`](monitor/README.md) — shows them: runs with status counts and a flow filter, each
85
+ run's steps (which one is running, which failed and why), every flow attempt and command attempt. Run it locally
86
+ (`./scripts/monitor.sh`) or deploy it to CapRover. Apps that only read runs can use `scrapme-flow/models` (the
87
+ document types and status constants, no Node code).
88
+
89
+ ## API
90
+
91
+ | | |
92
+ |---|---|
93
+ | `Scrapme.create(options)` | `container?`, `postgres`, `couch`, `queue?`, `scrapAnything?`, `mail?`, `lighty?`, `defaults?`, `logLevel?` |
94
+ | `start({ worker? })` | connect pg-boss. `worker: false` = this process only queues runs |
95
+ | `flow(flow, input, { runId?, startAfter?, priority? })` | queue a run → run id (UUID); starts the flow's worker here on first use |
96
+ | `work(flow)` | run this flow's queued runs in this process without queueing one (a worker-only process) |
97
+ | `getRun(id)`, `listRuns(limit)` | run documents: status, steps with outputs, attempts, errors |
98
+ | `waitFor(id, { timeoutMs? })` | until succeeded / failed (`RunWaitTimeoutError`) |
99
+ | `stop()` | stop the workers and the queue connection |
100
+
101
+ ## This repository
102
+
103
+ ```bash
104
+ nvm use && npm install
105
+ ./scripts/up.sh # everything at once: restarts scrap-anything, sentry-clone, the containers, the
106
+ # monitor (background) and runs hello; ./scripts/up.sh down stops it all
107
+ ./scripts/deps.sh # own containers: Postgres :5434, CouchDB :5985, Mailpit :1026/:8026; checks scrap-anything
108
+ ./scripts/monitor.sh # terminal 1, leave it open: the monitor on http://localhost:3100
109
+ ./scripts/example.sh hello Ada # terminal 2: the hello flow, then look at it in the monitor
110
+ ./scripts/example.sh biznesradar DOM-DEVELOPMENT # scrape → cheerio → CouchDB → e-mail (needs scrap-anything)
111
+ ```
112
+
113
+ The examples read `.env` (see `.env.example`) in `examples/example-services.ts` and pass the values to
114
+ `Scrapme.create`. Examples have no tests. [`example-external/`](example-external/README.md) is the hello example as a
115
+ standalone app that installs `scrapme-flow` from npm.
116
+
117
+ ```bash
118
+ npm test # every src/**/*.test.ts — no docker / internet / .env (fake queue, CouchDB, SMTP, Lighty)
119
+ npm run build # dist/ (tsc)
120
+ cd monitor && npm test # the monitor's tests
121
+ ```
122
+
123
+ Code layout and rules: `AGENTS.md`. Decisions: `docs/adr/`.
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Input of ScrapePageCommand. scrap-anything decides how long to wait (it always waits until the page —
3
+ * SPA or not — has rendered) and retries on its own, so there is nothing to tune per page.
4
+ */
5
+ export interface ScrapePageInput {
6
+ url: string;
7
+ /** Extra request headers for the target (e.g. Accept-Language). */
8
+ headers?: Record<string, string>;
9
+ /** 0–100, higher runs first on the server. */
10
+ priority?: number;
11
+ /** Accept HTTP 4xx answers of the target as a result (default: PermanentError HTTP_STATUS). */
12
+ allowHttpErrors?: boolean;
13
+ }
14
+ /** Output of ScrapePageCommand. */
15
+ export interface ScrapedPage {
16
+ jobId: string;
17
+ url: string;
18
+ finalUrl: string;
19
+ httpStatus: number;
20
+ title: string | null;
21
+ contentType: string;
22
+ html: string;
23
+ durationMs: number;
24
+ fetchedAt: string;
25
+ }
26
+ /** Input of SaveDocumentCommand. */
27
+ export interface SaveDocumentInput {
28
+ /** Document id (same id = new revision of the same document). */
29
+ id: string;
30
+ document: Record<string, unknown>;
31
+ /** Default couch.db. */
32
+ db?: string;
33
+ }
34
+ /** Input of SendMailCommand. */
35
+ export interface SendMailInput {
36
+ /** Default MAIL_TO. */
37
+ to?: string | string[];
38
+ subject: string;
39
+ text: string;
40
+ html?: string;
41
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=commands.models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.models.js","sourceRoot":"","sources":["../../../src/commands/commands.models.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import { type Command, type CommandContext } from '../flow/flow.models.js';
2
+ import type { DocumentStore, SavedDocument } from '../storage/storage.models.js';
3
+ import type { SaveDocumentInput } from './commands.models.js';
4
+ /** Upserts a document into CouchDB (couch.db or input.db). Idempotent: a retry writes the same id again. */
5
+ export declare class SaveDocumentCommand implements Command<SaveDocumentInput, SavedDocument> {
6
+ private readonly store;
7
+ constructor(store: DocumentStore);
8
+ execute(input: SaveDocumentInput, ctx: CommandContext): Promise<SavedDocument>;
9
+ }
@@ -0,0 +1,33 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { inject, injectable } from 'inversify';
11
+ import { TYPES } from '../di/types.js';
12
+ import { PermanentError } from '../flow/flow-errors.js';
13
+ import { ERROR_CODE } from '../flow/flow.models.js';
14
+ /** Upserts a document into CouchDB (couch.db or input.db). Idempotent: a retry writes the same id again. */
15
+ let SaveDocumentCommand = class SaveDocumentCommand {
16
+ store;
17
+ constructor(store) {
18
+ this.store = store;
19
+ }
20
+ async execute(input, ctx) {
21
+ if (!input?.id || !input.document)
22
+ throw new PermanentError('SaveDocumentCommand needs { id, document }', { code: ERROR_CODE.BAD_INPUT });
23
+ const saved = await this.store.save(input.id, input.document, input.db ? { db: input.db } : {});
24
+ ctx.logger.info(saved.created ? 'document created' : 'document updated', { db: saved.db, id: saved.id, rev: saved.rev, url: saved.url });
25
+ return saved;
26
+ }
27
+ };
28
+ SaveDocumentCommand = __decorate([
29
+ injectable(),
30
+ __param(0, inject(TYPES.DocumentStore))
31
+ ], SaveDocumentCommand);
32
+ export { SaveDocumentCommand };
33
+ //# sourceMappingURL=save-document.command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-document.command.js","sourceRoot":"","sources":["../../../src/commands/save-document.command.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAqC,MAAM,wBAAwB,CAAC;AAIvF,4GAA4G;AAErG,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IACmC;IAAjE,YAAiE,KAAoB;QAApB,UAAK,GAAL,KAAK,CAAe;IAAG,CAAC;IAElF,KAAK,CAAC,OAAO,CAAC,KAAwB,EAAE,GAAmB;QAChE,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ;YAAE,MAAM,IAAI,cAAc,CAAC,4CAA4C,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC;QAC1I,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QACzI,OAAO,KAAK,CAAC;IACf,CAAC;CACF,CAAA;AATY,mBAAmB;IAD/B,UAAU,EAAE;IAES,WAAA,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;GADpC,mBAAmB,CAS/B"}
@@ -0,0 +1,20 @@
1
+ import { type ScrapAnythingClient } from 'scrap-anything-client';
2
+ import type { ScrapmeConfig } from '../config/config.models.js';
3
+ import { type Command, type CommandContext, type CommandOptions } from '../flow/flow.models.js';
4
+ import type { ScrapedPage, ScrapePageInput } from './commands.models.js';
5
+ /**
6
+ * Renders a page with scrap-anything: schedules a job (createJob), waits for it (collect — the API is
7
+ * asynchronous, a queue with its own retries behind it) and returns the HTML. One attempt = one server job.
8
+ * When the attempt is aborted (step timeout, cancelled run) the server job is cancelled too. Retries follow the
9
+ * client's `retryable` (TLS / invalid target / 4xx from the API = permanent).
10
+ */
11
+ export declare class ScrapePageCommand implements Command<ScrapePageInput, ScrapedPage> {
12
+ private readonly client;
13
+ private static readonly FIRST_CLIENT_ERROR;
14
+ readonly options: CommandOptions;
15
+ constructor(client: ScrapAnythingClient | undefined, config: ScrapmeConfig);
16
+ execute(input: ScrapePageInput, ctx: CommandContext): Promise<ScrapedPage>;
17
+ isRetryable(error: unknown): boolean;
18
+ /** Wait for the job; an abort of the attempt cancels it on the server (the next poll then ends the wait). */
19
+ private collect;
20
+ }
@@ -0,0 +1,93 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ var ScrapePageCommand_1;
11
+ import { inject, injectable, optional } from 'inversify';
12
+ import { RESULT_KIND, ScrapAnythingError } from 'scrap-anything-client';
13
+ import { TYPES } from '../di/types.js';
14
+ import { FlowErrors, NotConfiguredError, PermanentError } from '../flow/flow-errors.js';
15
+ import { ERROR_CODE } from '../flow/flow.models.js';
16
+ /**
17
+ * Renders a page with scrap-anything: schedules a job (createJob), waits for it (collect — the API is
18
+ * asynchronous, a queue with its own retries behind it) and returns the HTML. One attempt = one server job.
19
+ * When the attempt is aborted (step timeout, cancelled run) the server job is cancelled too. Retries follow the
20
+ * client's `retryable` (TLS / invalid target / 4xx from the API = permanent).
21
+ */
22
+ let ScrapePageCommand = class ScrapePageCommand {
23
+ static { ScrapePageCommand_1 = this; }
24
+ client;
25
+ static FIRST_CLIENT_ERROR = 400;
26
+ options;
27
+ constructor(client, config) {
28
+ this.client = client;
29
+ // waiting for the server job is the slow part: the attempt must outlive scrapAnything.waitTimeoutMs
30
+ const s = config.scrapAnything;
31
+ this.options = s ? { timeoutMs: s.stepTimeoutMs, retries: s.stepRetries } : { retries: 0 };
32
+ }
33
+ async execute(input, ctx) {
34
+ if (!this.client)
35
+ throw new NotConfiguredError('scrapAnything', 'ScrapePageCommand');
36
+ if (!input?.url)
37
+ throw new PermanentError('ScrapePageCommand needs { url }', { code: ERROR_CODE.BAD_INPUT });
38
+ const client = this.client;
39
+ const created = await client.createJob({
40
+ url: input.url,
41
+ ...(input.headers ? { headers: input.headers } : {}),
42
+ ...(input.priority !== undefined ? { priority: input.priority } : {}),
43
+ });
44
+ ctx.logger.info('scrape job scheduled', { jobId: created.jobId, url: input.url, status: created.status });
45
+ const res = await this.collect(client, created.jobId, ctx);
46
+ ctx.logger.info('scrape job done', { jobId: res.jobId, httpStatus: res.result.httpStatus, attempts: res.job.attempts, ms: res.result.durationMs });
47
+ if (res.result.kind !== RESULT_KIND.HTML || res.html === undefined) {
48
+ throw new PermanentError(`${input.url} is not an HTML page (${res.result.contentType})`, { code: ERROR_CODE.NOT_HTML });
49
+ }
50
+ if (res.result.httpStatus >= ScrapePageCommand_1.FIRST_CLIENT_ERROR && !input.allowHttpErrors) {
51
+ throw new PermanentError(`${input.url} answered HTTP ${res.result.httpStatus}`, { code: ERROR_CODE.HTTP_STATUS });
52
+ }
53
+ return {
54
+ jobId: res.jobId,
55
+ url: input.url,
56
+ finalUrl: res.result.finalUrl,
57
+ httpStatus: res.result.httpStatus,
58
+ title: res.result.title,
59
+ contentType: res.result.contentType,
60
+ html: res.html,
61
+ durationMs: res.result.durationMs,
62
+ fetchedAt: new Date().toISOString(),
63
+ };
64
+ }
65
+ isRetryable(error) {
66
+ return error instanceof ScrapAnythingError ? error.retryable : FlowErrors.isRetryable(error);
67
+ }
68
+ /** Wait for the job; an abort of the attempt cancels it on the server (the next poll then ends the wait). */
69
+ async collect(client, jobId, ctx) {
70
+ const cancel = () => {
71
+ ctx.logger.warn('attempt aborted, cancelling the scrape job', { jobId });
72
+ client.cancelJob(jobId).catch((error) => ctx.logger.warn('could not cancel the scrape job', { jobId, error }));
73
+ };
74
+ if (ctx.signal.aborted)
75
+ cancel();
76
+ else
77
+ ctx.signal.addEventListener('abort', cancel, { once: true });
78
+ try {
79
+ return await client.collect(jobId);
80
+ }
81
+ finally {
82
+ ctx.signal.removeEventListener('abort', cancel);
83
+ }
84
+ }
85
+ };
86
+ ScrapePageCommand = ScrapePageCommand_1 = __decorate([
87
+ injectable(),
88
+ __param(0, inject(TYPES.ScrapAnythingClient)),
89
+ __param(0, optional()),
90
+ __param(1, inject(TYPES.Config))
91
+ ], ScrapePageCommand);
92
+ export { ScrapePageCommand };
93
+ //# sourceMappingURL=scrape-page.command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrape-page.command.js","sourceRoot":"","sources":["../../../src/commands/scrape-page.command.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ScrapmeConfig } from '../config/config.models.js';
2
+ import { type Command, type CommandContext } from '../flow/flow.models.js';
3
+ import type { Mailer, SentMail } from '../mail/mail.models.js';
4
+ import type { SendMailInput } from './commands.models.js';
5
+ /** Sends an e-mail (SMTP from Scrapme.create({ mail })). Recipients default to mail.to. Rejected addresses are not retried. */
6
+ export declare class SendMailCommand implements Command<SendMailInput, SentMail> {
7
+ private readonly mailer;
8
+ private readonly config;
9
+ constructor(mailer: Mailer, config: ScrapmeConfig);
10
+ execute(input: SendMailInput, ctx: CommandContext): Promise<SentMail>;
11
+ }
@@ -0,0 +1,39 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { inject, injectable } from 'inversify';
11
+ import { TYPES } from '../di/types.js';
12
+ import { PermanentError } from '../flow/flow-errors.js';
13
+ import { ERROR_CODE } from '../flow/flow.models.js';
14
+ /** Sends an e-mail (SMTP from Scrapme.create({ mail })). Recipients default to mail.to. Rejected addresses are not retried. */
15
+ let SendMailCommand = class SendMailCommand {
16
+ mailer;
17
+ config;
18
+ constructor(mailer, config) {
19
+ this.mailer = mailer;
20
+ this.config = config;
21
+ }
22
+ async execute(input, ctx) {
23
+ if (!input?.subject || !input.text)
24
+ throw new PermanentError('SendMailCommand needs { subject, text }', { code: ERROR_CODE.BAD_INPUT });
25
+ const to = input.to === undefined ? [...(this.config.mail?.to ?? [])] : Array.isArray(input.to) ? input.to : [input.to];
26
+ if (to.length === 0)
27
+ throw new PermanentError('no recipients (input.to or mail.to in Scrapme.create)', { code: ERROR_CODE.BAD_INPUT });
28
+ const sent = await this.mailer.send({ to, subject: input.subject, text: input.text, ...(input.html ? { html: input.html } : {}) });
29
+ ctx.logger.info('mail sent', { to, subject: input.subject, messageId: sent.messageId });
30
+ return sent;
31
+ }
32
+ };
33
+ SendMailCommand = __decorate([
34
+ injectable(),
35
+ __param(0, inject(TYPES.Mailer)),
36
+ __param(1, inject(TYPES.Config))
37
+ ], SendMailCommand);
38
+ export { SendMailCommand };
39
+ //# sourceMappingURL=send-mail.command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send-mail.command.js","sourceRoot":"","sources":["../../../src/commands/send-mail.command.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAqC,MAAM,wBAAwB,CAAC;AAIvF,+HAA+H;AAExH,IAAM,eAAe,GAArB,MAAM,eAAe;IAEe;IACA;IAFzC,YACyC,MAAc,EACd,MAAqB;QADrB,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAe;IAC3D,CAAC;IAEG,KAAK,CAAC,OAAO,CAAC,KAAoB,EAAE,GAAmB;QAC5D,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI;YAAE,MAAM,IAAI,cAAc,CAAC,yCAAyC,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC;QACxI,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxH,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,cAAc,CAAC,uDAAuD,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC;QACvI,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACnI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACxF,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAdY,eAAe;IAD3B,UAAU,EAAE;IAGR,WAAA,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IACpB,WAAA,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;GAHZ,eAAe,CAc3B"}
@@ -0,0 +1,24 @@
1
+ import type { ScrapmeConfig, ServiceOptions } from './config.models.js';
2
+ /** The services given to Scrapme.create are invalid (thrown by Scrapme.create). */
3
+ export declare class ConfigError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /** Checks the services an app gives to Scrapme.create (zod), fills in defaults and freezes the result. */
7
+ export declare class ConfigBuilder {
8
+ /**
9
+ * CouchDB's own rule for database names (docs.couchdb.org, "PUT /{db}"). It is a rule about characters, so a
10
+ * regex is the clearest way to state it.
11
+ */
12
+ private static readonly COUCH_DB_NAME;
13
+ private static readonly STEP_TIMEOUT_MARGIN_MS;
14
+ private static readonly SMTP_PORT;
15
+ private static readonly SMTPS_PORT;
16
+ /** pg-boss schema names go into SQL as identifiers. */
17
+ private static readonly SQL_IDENTIFIER;
18
+ private static readonly QUEUE_PREFIX;
19
+ private static readonly ms;
20
+ private static readonly dbName;
21
+ private static readonly schema;
22
+ static build(options: ServiceOptions): ScrapmeConfig;
23
+ private static deepFreeze;
24
+ }
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { LOG_LEVEL, LOG_LEVELS } from '../logging/logger.models.js';
3
+ import { Urls } from '../util/url.js';
4
+ import { DsnCheck } from './dsn-check.js';
5
+ /** The services given to Scrapme.create are invalid (thrown by Scrapme.create). */
6
+ export class ConfigError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'ConfigError';
10
+ }
11
+ }
12
+ /** Checks the services an app gives to Scrapme.create (zod), fills in defaults and freezes the result. */
13
+ export class ConfigBuilder {
14
+ /**
15
+ * CouchDB's own rule for database names (docs.couchdb.org, "PUT /{db}"). It is a rule about characters, so a
16
+ * regex is the clearest way to state it.
17
+ */
18
+ static COUCH_DB_NAME = /^[a-z][a-z0-9_$()+/-]*$/;
19
+ static STEP_TIMEOUT_MARGIN_MS = 60_000;
20
+ static SMTP_PORT = 587;
21
+ static SMTPS_PORT = 465;
22
+ /** pg-boss schema names go into SQL as identifiers. */
23
+ static SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
24
+ static QUEUE_PREFIX = 'scrapme_';
25
+ static ms = (min = 0) => z.number().int().min(min);
26
+ static dbName = z.string().regex(ConfigBuilder.COUCH_DB_NAME, 'CouchDB names: lowercase letters, digits and _$()+/-, starting with a letter');
27
+ static schema = z.strictObject({
28
+ logLevel: z.enum(LOG_LEVELS).default(LOG_LEVEL.INFO),
29
+ defaults: z
30
+ .strictObject({
31
+ retries: ConfigBuilder.ms().default(3),
32
+ timeoutMs: ConfigBuilder.ms(1).default(60_000),
33
+ retryDelayMs: ConfigBuilder.ms().default(1_000),
34
+ maxRetryDelayMs: ConfigBuilder.ms().default(30_000),
35
+ maxStoredChars: ConfigBuilder.ms(100).default(2_000),
36
+ })
37
+ .default({ retries: 3, timeoutMs: 60_000, retryDelayMs: 1_000, maxRetryDelayMs: 30_000, maxStoredChars: 2_000 })
38
+ .refine((d) => d.maxRetryDelayMs >= d.retryDelayMs, { message: 'maxRetryDelayMs must be >= retryDelayMs' }),
39
+ postgres: z.strictObject({
40
+ connectionString: z.string().refine((v) => URL.canParse(v), 'must be a postgres:// URL'),
41
+ schema: z.string().regex(ConfigBuilder.SQL_IDENTIFIER, 'letters, digits and _ only').default('scrapme'),
42
+ }),
43
+ queue: z
44
+ .strictObject({
45
+ prefix: z.string().regex(ConfigBuilder.SQL_IDENTIFIER, 'letters, digits and _ only').default(ConfigBuilder.QUEUE_PREFIX),
46
+ retries: ConfigBuilder.ms().default(3),
47
+ retryDelaySeconds: ConfigBuilder.ms().default(30),
48
+ retryBackoff: z.boolean().default(true),
49
+ timeoutSeconds: ConfigBuilder.ms(1).default(3_600),
50
+ concurrency: ConfigBuilder.ms(1).default(1),
51
+ // pg-boss: at least 0.5 s
52
+ pollingIntervalSeconds: z.number().min(0.5).default(2),
53
+ retentionDays: ConfigBuilder.ms(1).default(7),
54
+ shutdownTimeoutMs: ConfigBuilder.ms(0).default(30_000),
55
+ })
56
+ .default({ prefix: ConfigBuilder.QUEUE_PREFIX, retries: 3, retryDelaySeconds: 30, retryBackoff: true, timeoutSeconds: 3_600, concurrency: 1, pollingIntervalSeconds: 2, retentionDays: 7, shutdownTimeoutMs: 30_000 }),
57
+ scrapAnything: z
58
+ .strictObject({
59
+ url: z.url(),
60
+ apiSecret: z.string().min(1, 'apiSecret is required (API_SECRET of scrap-anything)'),
61
+ requestTimeoutMs: ConfigBuilder.ms(1).default(30_000),
62
+ pollIntervalMs: ConfigBuilder.ms(1).default(2_000),
63
+ waitTimeoutMs: ConfigBuilder.ms(1).default(600_000),
64
+ stepTimeoutMs: ConfigBuilder.ms(1).optional(),
65
+ stepRetries: ConfigBuilder.ms().default(1),
66
+ })
67
+ .transform((s) => ({ ...s, url: Urls.withoutTrailingSlash(s.url), stepTimeoutMs: s.stepTimeoutMs ?? s.waitTimeoutMs + ConfigBuilder.STEP_TIMEOUT_MARGIN_MS }))
68
+ .refine((s) => s.stepTimeoutMs > s.waitTimeoutMs, { message: 'stepTimeoutMs must be greater than waitTimeoutMs (the step waits for the server job)' })
69
+ .optional(),
70
+ couch: z
71
+ .strictObject({
72
+ url: z.url(),
73
+ user: z.string(),
74
+ password: z.string(),
75
+ db: ConfigBuilder.dbName.default('scrapme'),
76
+ runsDb: ConfigBuilder.dbName.default('scrapme_runs'),
77
+ publicUrl: z.url().optional(),
78
+ timeoutMs: ConfigBuilder.ms(1).default(15_000),
79
+ retryLimit: ConfigBuilder.ms().default(2),
80
+ })
81
+ .transform((c) => ({ ...c, url: Urls.withoutTrailingSlash(c.url), publicUrl: Urls.withoutTrailingSlash(c.publicUrl ?? c.url) })),
82
+ mail: z
83
+ .strictObject({
84
+ host: z.string().min(1),
85
+ port: ConfigBuilder.ms(1).optional(),
86
+ secure: z.boolean().default(false),
87
+ user: z.string().optional(),
88
+ password: z.string().optional(),
89
+ timeoutMs: ConfigBuilder.ms(1).default(10_000),
90
+ from: z.string().min(1),
91
+ to: z.array(z.string().min(1)).default([]),
92
+ })
93
+ .transform((m) => ({ ...m, port: m.port ?? (m.secure ? ConfigBuilder.SMTPS_PORT : ConfigBuilder.SMTP_PORT), user: m.user ?? null, password: m.password ?? null }))
94
+ .optional(),
95
+ lighty: z
96
+ .strictObject({
97
+ dsn: z.string().refine((dsn) => DsnCheck.isValid(dsn), 'must look like https://<project-key>@<host>/<project>'),
98
+ environment: z.string().default('production'),
99
+ release: z.string().default('scrapme'),
100
+ flushTimeoutMs: ConfigBuilder.ms(1).default(5_000),
101
+ })
102
+ .optional(),
103
+ });
104
+ static build(options) {
105
+ const parsed = ConfigBuilder.schema.safeParse(options);
106
+ if (!parsed.success)
107
+ throw new ConfigError(`Invalid scrapme options:\n${z.prettifyError(parsed.error)}`);
108
+ const o = parsed.data;
109
+ return ConfigBuilder.deepFreeze({
110
+ logLevel: o.logLevel,
111
+ flow: o.defaults,
112
+ postgres: o.postgres,
113
+ queue: o.queue,
114
+ scrapAnything: o.scrapAnything ?? null,
115
+ couch: o.couch,
116
+ mail: o.mail ?? null,
117
+ lighty: o.lighty ?? null,
118
+ });
119
+ }
120
+ static deepFreeze(value) {
121
+ if (value && typeof value === 'object') {
122
+ for (const v of Object.values(value))
123
+ ConfigBuilder.deepFreeze(v);
124
+ Object.freeze(value);
125
+ }
126
+ return value;
127
+ }
128
+ }
129
+ //# sourceMappingURL=config-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-builder.js","sourceRoot":"","sources":["../../../src/config/config-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ScrapmeConfig } from './config.models.js';
2
+ export declare class ConfigError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** The only place that reads environment variables. Empty strings count as "not set" (default applies). */
6
+ export declare class ConfigLoader {
7
+ static load(env?: Record<string, string | undefined>): ScrapmeConfig;
8
+ private static toConfig;
9
+ private static deepFreeze;
10
+ }