@outcrawl/sdk 0.1.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.
- package/README.md +198 -0
- package/dist/index.js +2493 -0
- package/dist/types/_deps/core/agent-alias.d.ts +219 -0
- package/dist/types/_deps/core/brand.d.ts +62 -0
- package/dist/types/_deps/core/certificate.d.ts +100 -0
- package/dist/types/_deps/core/cron.d.ts +116 -0
- package/dist/types/_deps/core/errors.d.ts +340 -0
- package/dist/types/_deps/core/index.d.ts +17 -0
- package/dist/types/_deps/core/money.d.ts +108 -0
- package/dist/types/_deps/core/registry.d.ts +466 -0
- package/dist/types/_deps/core/rules.d.ts +237 -0
- package/dist/types/_deps/core/secrets.d.ts +278 -0
- package/dist/types/_deps/core/types.d.ts +1677 -0
- package/dist/types/_deps/integrations/connector.d.ts +153 -0
- package/dist/types/_deps/replay/events.d.ts +1004 -0
- package/dist/types/agent.d.ts +313 -0
- package/dist/types/availability.d.ts +86 -0
- package/dist/types/browser.d.ts +107 -0
- package/dist/types/client.d.ts +128 -0
- package/dist/types/hands.d.ts +259 -0
- package/dist/types/index.d.ts +111 -0
- package/dist/types/integrations.d.ts +63 -0
- package/dist/types/monitors.d.ts +24 -0
- package/dist/types/page.d.ts +96 -0
- package/dist/types/profiles.d.ts +26 -0
- package/dist/types/result.d.ts +90 -0
- package/dist/types/rules.d.ts +41 -0
- package/dist/types/scrape.d.ts +68 -0
- package/dist/types/secrets.d.ts +36 -0
- package/dist/types/sessions.d.ts +124 -0
- package/dist/types/transport.d.ts +250 -0
- package/package.json +70 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors.
|
|
3
|
+
*
|
|
4
|
+
* `code` is the wire contract: stable, machine-readable, never renamed once
|
|
5
|
+
* shipped. `message` is for humans and may change freely. Every error carries
|
|
6
|
+
* enough structured detail to act on — a `ProfileInUseError` that does not say
|
|
7
|
+
* when the lease expires just moves the guesswork to the caller.
|
|
8
|
+
*/
|
|
9
|
+
import type { CapName, ExitTarget, Timestamp } from './types.js';
|
|
10
|
+
export declare const OUTCRAWL_ERROR_CODES: readonly ["unauthorized", "profile_in_use", "cap_exceeded", "exit_unavailable", "no_capacity", "tenant_scope", "bad_request", "not_found", "profile_not_found", "method_not_allowed", "capability_unavailable", "quota_exceeded", "concurrency_limit", "plan_required", "internal"];
|
|
11
|
+
export type OutcrawlErrorCode = (typeof OUTCRAWL_ERROR_CODES)[number];
|
|
12
|
+
/**
|
|
13
|
+
* What **we produce**. Every error this package throws serialises to exactly
|
|
14
|
+
* this, with a code drawn from the closed union above.
|
|
15
|
+
*/
|
|
16
|
+
export interface SerializedOutcrawlError {
|
|
17
|
+
code: OutcrawlErrorCode;
|
|
18
|
+
message: string;
|
|
19
|
+
details: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What **a client parses**. Deliberately wider: `code` is an open `string`.
|
|
23
|
+
*
|
|
24
|
+
* Strict in what we emit, lenient in what we accept. A client that rejects an
|
|
25
|
+
* error body because the code is newer than the client turns a 404 into a
|
|
26
|
+
* crash — and the failure lands during an incident, when a new code is most
|
|
27
|
+
* likely to be in flight and least likely to be welcome. Branch on the codes
|
|
28
|
+
* you know and fall through on the rest.
|
|
29
|
+
*/
|
|
30
|
+
export interface WireError {
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
details?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/** Narrows an open wire code to the closed union, for exhaustive branching. */
|
|
36
|
+
export declare function isKnownErrorCode(code: string): code is OutcrawlErrorCode;
|
|
37
|
+
export declare abstract class OutcrawlError extends Error {
|
|
38
|
+
/** Stable across releases. Callers branch on this, never on `message`. */
|
|
39
|
+
readonly code: OutcrawlErrorCode;
|
|
40
|
+
/** Default HTTP status for the API layer. */
|
|
41
|
+
readonly status: number;
|
|
42
|
+
/** Structured, JSON-safe payload. Subclasses also expose it as typed fields. */
|
|
43
|
+
readonly details: Readonly<Record<string, unknown>>;
|
|
44
|
+
protected constructor(code: OutcrawlErrorCode, message: string, status: number, details: Readonly<Record<string, unknown>>);
|
|
45
|
+
toJSON(): SerializedOutcrawlError;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* True for our errors, including ones raised by a second copy of this package
|
|
49
|
+
* in the same process — the SDK and the server can end up with their own — where
|
|
50
|
+
* `instanceof` is false despite an identical shape. Callers that only ever see
|
|
51
|
+
* errors from one copy can use `instanceof OutcrawlError` directly.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isOutcrawlError(value: unknown): value is OutcrawlError;
|
|
54
|
+
/**
|
|
55
|
+
* A profile is leased while in use. A second concurrent open throws rather than
|
|
56
|
+
* silently forking state, because the alternative is two sessions both writing
|
|
57
|
+
* cookies, last-writer-wins, and an account mysteriously logged out.
|
|
58
|
+
*/
|
|
59
|
+
export declare class ProfileInUseError extends OutcrawlError {
|
|
60
|
+
readonly profileId: string;
|
|
61
|
+
/** When the current lease expires on its own. Retry after this. */
|
|
62
|
+
readonly heldUntil: Date;
|
|
63
|
+
/** The session holding the lease, when the caller is allowed to see it. */
|
|
64
|
+
readonly heldBy: string | undefined;
|
|
65
|
+
constructor(profileId: string, heldUntil: Date, heldBy?: string);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A ceiling the caller set was reached. Caps, not refunds: the cost is bounded
|
|
69
|
+
* before it is incurred rather than argued about afterwards.
|
|
70
|
+
*
|
|
71
|
+
* An agent run that trips a cap mid-flight returns `status: 'partial' | 'failed'`
|
|
72
|
+
* with its data attached — this error is for a request that asks for more than
|
|
73
|
+
* the account or the call allows, refused before a browser is allocated.
|
|
74
|
+
*/
|
|
75
|
+
export declare class CapExceededError extends OutcrawlError {
|
|
76
|
+
/** Which ceiling: `steps`, `budget` or `duration`. */
|
|
77
|
+
readonly cap: CapName;
|
|
78
|
+
/** The ceiling that was set. Credits and durations are strings, steps a number. */
|
|
79
|
+
readonly limit: string | number;
|
|
80
|
+
/** What was reached or requested. Same units as `limit`. */
|
|
81
|
+
readonly observed: string | number;
|
|
82
|
+
constructor(cap: CapName, limit: string | number, observed: string | number);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* No exit could be resolved for the requested geography — the country, region
|
|
86
|
+
* or city is not in the pool, or every exit there is currently unhealthy.
|
|
87
|
+
* Never silently substitute a neighbouring city: a profile whose exit moved is
|
|
88
|
+
* an identity that cannot exist.
|
|
89
|
+
*/
|
|
90
|
+
export declare class ExitUnavailableError extends OutcrawlError {
|
|
91
|
+
readonly target: ExitTarget;
|
|
92
|
+
readonly reason: string | undefined;
|
|
93
|
+
/** Set when the exit exists but is temporarily unhealthy. */
|
|
94
|
+
readonly retryAfter: Timestamp | undefined;
|
|
95
|
+
constructor(target: ExitTarget, reason?: string, retryAfter?: Timestamp);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A capability this deployment cannot serve, because nothing is configured
|
|
99
|
+
* behind it.
|
|
100
|
+
*
|
|
101
|
+
* The request was valid: the route exists, the arguments were accepted, and
|
|
102
|
+
* another worker with a different configuration would have answered it. What is
|
|
103
|
+
* missing is a dependency on this side — a result source for `search`, an exit
|
|
104
|
+
* provisioner for a request that names a geography — so the caller is told
|
|
105
|
+
* plainly rather than handed an incident id.
|
|
106
|
+
*
|
|
107
|
+
* Not retryable. The identical call against the same deployment fails
|
|
108
|
+
* identically until an operator configures something; the caller's only other
|
|
109
|
+
* move is a different request.
|
|
110
|
+
*
|
|
111
|
+
* `detail` exists to separate the two states that look the same from outside:
|
|
112
|
+
* a deployment that deliberately has no such dependency, and a build where
|
|
113
|
+
* nobody bound one. The second is now a type error at every seam in this
|
|
114
|
+
* repository, which is what lets this message promise it is the first.
|
|
115
|
+
*/
|
|
116
|
+
export declare class CapabilityUnavailableError extends OutcrawlError {
|
|
117
|
+
/** What was asked for: `search`, `a request that names an exit`. */
|
|
118
|
+
readonly capability: string;
|
|
119
|
+
/** What is not configured: `result source`, `exit provisioner`. */
|
|
120
|
+
readonly missing: string;
|
|
121
|
+
constructor(capability: string, missing: string, detail?: string);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A CDP connection touched a target outside its own tenant. This is the
|
|
125
|
+
* perimeter now that clients connect directly to workers: one connection may
|
|
126
|
+
* only ever drive its own BrowserContext, and enforcement sits at the two
|
|
127
|
+
* lookups every target resolution funnels through, so a method added in a
|
|
128
|
+
* future Chromium is scoped automatically rather than needing an allowlist.
|
|
129
|
+
*/
|
|
130
|
+
export declare class TenantScopeError extends OutcrawlError {
|
|
131
|
+
/** The CDP method that was refused, e.g. `Target.attachToTarget`. */
|
|
132
|
+
readonly method: string;
|
|
133
|
+
/** The target that was out of scope, when one was named. */
|
|
134
|
+
readonly targetId: string | undefined;
|
|
135
|
+
constructor(method: string, targetId?: string);
|
|
136
|
+
}
|
|
137
|
+
/** Why the router refused the caller. Never says which value was presented. */
|
|
138
|
+
export type UnauthorizedReason = 'invalid_key' | 'ip_not_allowed' | 'missing_token';
|
|
139
|
+
/**
|
|
140
|
+
* The router refused the request at the front door — before a worker is picked
|
|
141
|
+
* and before anything is billed.
|
|
142
|
+
*/
|
|
143
|
+
export declare class UnauthorizedError extends OutcrawlError {
|
|
144
|
+
readonly reason: UnauthorizedReason;
|
|
145
|
+
constructor(reason: UnauthorizedReason);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* No worker could take the session: none is `ready` with free contexts and a
|
|
149
|
+
* cookie schema at least as new as the profile's. Cookie schema is the real
|
|
150
|
+
* compatibility key — an older worker cannot read a newer profile's stored
|
|
151
|
+
* state, and handing it one would silently lose the login rather than fail.
|
|
152
|
+
*
|
|
153
|
+
* Retryable: capacity is a moment-in-time property of the fleet, not of the
|
|
154
|
+
* request.
|
|
155
|
+
*/
|
|
156
|
+
export declare class NoCapacityError extends OutcrawlError {
|
|
157
|
+
/** The minimum cookie schema the caller's profile requires. */
|
|
158
|
+
readonly cookieSchema: number;
|
|
159
|
+
/** Set when the caller asked for a specific region. */
|
|
160
|
+
readonly region: string | undefined;
|
|
161
|
+
constructor(detail: {
|
|
162
|
+
cookieSchema: number;
|
|
163
|
+
region?: string;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** Which ceiling refused the request. Both are per-account, never per-key. */
|
|
167
|
+
export type QuotaCeiling = 'requests_per_minute' | 'monthly_credits';
|
|
168
|
+
/**
|
|
169
|
+
* An account is over a ceiling its plan sets, refused before a worker is
|
|
170
|
+
* picked and before anything is billed.
|
|
171
|
+
*
|
|
172
|
+
* ## Why this is not `no_capacity`
|
|
173
|
+
*
|
|
174
|
+
* `no_capacity` is documented retryable *because the fleet is busy*, so a
|
|
175
|
+
* client that reads one backs off and tries again — correctly. Answering a
|
|
176
|
+
* quota refusal with it tells a customer who has exhausted their month to
|
|
177
|
+
* retry until the month ends, and it puts their traffic in the router's
|
|
178
|
+
* capacity metrics, where an operator then goes looking for workers that were
|
|
179
|
+
* never missing. Two facts, two codes.
|
|
180
|
+
*
|
|
181
|
+
* ## Why 429 and not 402
|
|
182
|
+
*
|
|
183
|
+
* `cap_exceeded` is 402 and means a ceiling the *caller* set for one call.
|
|
184
|
+
* This is a ceiling somebody set *for* the caller, and it lapses on its own at
|
|
185
|
+
* `resetAt` — which is precisely what 429 plus `Retry-After` describes. A 402
|
|
186
|
+
* says "pay"; this says "wait, and here is exactly how long".
|
|
187
|
+
*
|
|
188
|
+
* `limit` and `observed` are `string | number` for the same reason
|
|
189
|
+
* `CapExceededError` makes them so: a request count is an integer and credits
|
|
190
|
+
* are an exact decimal string that must never have been a float.
|
|
191
|
+
*/
|
|
192
|
+
export declare class QuotaExceededError extends OutcrawlError {
|
|
193
|
+
readonly ceiling: QuotaCeiling;
|
|
194
|
+
/** The ceiling the plan sets. */
|
|
195
|
+
readonly limit: string | number;
|
|
196
|
+
/** What the account has reached. Same units as `limit`. */
|
|
197
|
+
readonly observed: string | number;
|
|
198
|
+
/** When the ceiling lapses on its own. */
|
|
199
|
+
readonly resetAt: Timestamp;
|
|
200
|
+
/**
|
|
201
|
+
* `Retry-After`, in seconds, and always at least 1.
|
|
202
|
+
*
|
|
203
|
+
* Carried on the error rather than computed by whoever renders it, because
|
|
204
|
+
* the header and the instant have to agree: a caller that trusts the header
|
|
205
|
+
* and a dashboard that trusts `resetAt` disagreeing by a minute is a support
|
|
206
|
+
* ticket nobody can reproduce.
|
|
207
|
+
*/
|
|
208
|
+
readonly retryAfterSeconds: number;
|
|
209
|
+
constructor(detail: {
|
|
210
|
+
ceiling: QuotaCeiling;
|
|
211
|
+
limit: string | number;
|
|
212
|
+
observed: string | number;
|
|
213
|
+
resetAt: Timestamp;
|
|
214
|
+
retryAfterSeconds: number;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The account is holding every browser its plan allows at once, refused before
|
|
219
|
+
* a worker is picked and before anything is billed.
|
|
220
|
+
*
|
|
221
|
+
* ## Why this is not `no_capacity`
|
|
222
|
+
*
|
|
223
|
+
* Because it is not about our hardware at all. `no_capacity` means no ready
|
|
224
|
+
* worker has a free context, which is a property of the FLEET at that instant
|
|
225
|
+
* and is fixed by machines; this means the customer is using everything they
|
|
226
|
+
* bought, which is a property of THEIR account and is fixed by ending a
|
|
227
|
+
* session or moving to a larger plan. Sharing a code between them tells a
|
|
228
|
+
* customer at their ceiling to blame us and wait, and it puts their traffic in
|
|
229
|
+
* the router's capacity metrics — where an operator then provisions Macs that
|
|
230
|
+
* will not help. Two facts, two codes, and this is the third one.
|
|
231
|
+
*
|
|
232
|
+
* ## Why this is not `quota_exceeded`
|
|
233
|
+
*
|
|
234
|
+
* Both of that error's ceilings lapse on their own: a rolling minute drains
|
|
235
|
+
* and a calendar month ends, so it carries a `resetAt` and a `Retry-After`
|
|
236
|
+
* that a client can obey. A concurrency slot lapses when the CUSTOMER closes a
|
|
237
|
+
* browser and at no other time, so there is no instant to name. Putting a
|
|
238
|
+
* number in `Retry-After` here would be an invention, and a client obeying an
|
|
239
|
+
* invented backoff is worse off than one told plainly that the next move is
|
|
240
|
+
* theirs — which is why `errorResponse` sets that header for `quota_exceeded`
|
|
241
|
+
* and deliberately not for this.
|
|
242
|
+
*
|
|
243
|
+
* ## Why 429 rather than 402 or 503
|
|
244
|
+
*
|
|
245
|
+
* 429 is literally "too many requests", and too many AT ONCE is the same
|
|
246
|
+
* family as too many per minute — a client that already handles one handles
|
|
247
|
+
* this. 402 (`cap_exceeded`) says "pay", which is wrong for a customer who has
|
|
248
|
+
* paid and is simply using it all. 503 says "we are broken", which is a lie
|
|
249
|
+
* about a healthy fleet.
|
|
250
|
+
*
|
|
251
|
+
* `plan` is named in the message because that is what makes the sentence
|
|
252
|
+
* actionable: `5` on its own is a number, and `plan builder allows 5` tells a
|
|
253
|
+
* customer which line of the pricing page to read and an operator which key to
|
|
254
|
+
* move. `observed` is the count at the instant of refusal, so support can tell
|
|
255
|
+
* "at the ceiling" from "nowhere near it, something else is wrong".
|
|
256
|
+
*/
|
|
257
|
+
export declare class ConcurrencyLimitError extends OutcrawlError {
|
|
258
|
+
/** The plan's name, as the pricing page spells it. */
|
|
259
|
+
readonly plan: string;
|
|
260
|
+
/** Browsers the plan allows at once. */
|
|
261
|
+
readonly limit: number;
|
|
262
|
+
/** Browsers the account was holding when it asked for one more. */
|
|
263
|
+
readonly observed: number;
|
|
264
|
+
constructor(detail: {
|
|
265
|
+
plan: string;
|
|
266
|
+
limit: number;
|
|
267
|
+
observed: number;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The key authenticated and names no plan this deployment can serve, so there
|
|
272
|
+
* is no ceiling to enforce and the request is refused.
|
|
273
|
+
*
|
|
274
|
+
* Three records reach this and the answer is the same for all three, because
|
|
275
|
+
* the operator's next action is: a key minted before plans existed, a key
|
|
276
|
+
* naming a tier that is not published, and a key carrying the pre-tier pair of
|
|
277
|
+
* loose ceiling numbers with no tier name at all.
|
|
278
|
+
*
|
|
279
|
+
* ## Why refusing is the only safe direction
|
|
280
|
+
*
|
|
281
|
+
* The alternative is treating an absent plan as unlimited, and the failure mode
|
|
282
|
+
* of that choice is a free fleet: one key opening sessions until the machines
|
|
283
|
+
* fall over, with the invoice as the first thing that notices. A key that
|
|
284
|
+
* cannot be metered against a ceiling is a key that must not be served, so an
|
|
285
|
+
* absent plan fails closed here rather than defaulting anywhere.
|
|
286
|
+
*
|
|
287
|
+
* ## Why it is not `unauthorized`
|
|
288
|
+
*
|
|
289
|
+
* Nothing is wrong with the credential. Reporting `invalid_key` would send a
|
|
290
|
+
* customer to rotate a perfectly good key, and rotation carries the plan over,
|
|
291
|
+
* so they would rotate forever. The party who can fix this is the operator who
|
|
292
|
+
* minted the key without one, and 403 with this code is what tells support
|
|
293
|
+
* which of the two to call.
|
|
294
|
+
*
|
|
295
|
+
* `keyId` is echoed deliberately. It is the half of the token that already
|
|
296
|
+
* travels in the clear so the record can be fetched by primary key, so it is
|
|
297
|
+
* not a secret, and it is the exact string an operator types into
|
|
298
|
+
* `outcrawl admin keys get`.
|
|
299
|
+
*/
|
|
300
|
+
export declare class PlanRequiredError extends OutcrawlError {
|
|
301
|
+
readonly keyId: string;
|
|
302
|
+
constructor(keyId: string);
|
|
303
|
+
}
|
|
304
|
+
/** Malformed or missing input. The caller can fix this and retry. */
|
|
305
|
+
export declare class BadRequestError extends OutcrawlError {
|
|
306
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
307
|
+
}
|
|
308
|
+
/** No such resource, or none this caller is allowed to know exists. */
|
|
309
|
+
export declare class NotFoundError extends OutcrawlError {
|
|
310
|
+
readonly resource: string;
|
|
311
|
+
constructor(resource: string, id?: string);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* A named profile does not exist, or belongs to another account.
|
|
315
|
+
*
|
|
316
|
+
* Deliberately indistinguishable from the not-belonging case: profiles are
|
|
317
|
+
* named by the customer, so a distinct "exists but is not yours" would let one
|
|
318
|
+
* account enumerate another's profile labels.
|
|
319
|
+
*/
|
|
320
|
+
export declare class ProfileNotFoundError extends OutcrawlError {
|
|
321
|
+
readonly profileId: string;
|
|
322
|
+
constructor(profileId: string);
|
|
323
|
+
}
|
|
324
|
+
/** Wrong HTTP method. `allow` populates the `Allow` header RFC 9110 requires. */
|
|
325
|
+
export declare class MethodNotAllowedError extends OutcrawlError {
|
|
326
|
+
readonly allow: readonly string[];
|
|
327
|
+
constructor(method: string, allow: readonly string[]);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Something broke on our side.
|
|
331
|
+
*
|
|
332
|
+
* `message` is fixed and carries no detail on purpose — an internal error is
|
|
333
|
+
* the one place where a helpful message leaks a stack, a query or a hostname to
|
|
334
|
+
* whoever provoked it. Diagnostics go to logs under `incidentId`, which the
|
|
335
|
+
* caller can quote to support.
|
|
336
|
+
*/
|
|
337
|
+
export declare class InternalError extends OutcrawlError {
|
|
338
|
+
readonly incidentId: string | undefined;
|
|
339
|
+
constructor(incidentId?: string);
|
|
340
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@outcrawl/core` — the shared contract.
|
|
3
|
+
*
|
|
4
|
+
* Wire types, the capability registry every surface is checked against, and the
|
|
5
|
+
* typed errors. No dependencies and no runtime beyond validation, because every
|
|
6
|
+
* other package imports this one.
|
|
7
|
+
*/
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './agent-alias.js';
|
|
10
|
+
export * from './brand.js';
|
|
11
|
+
export * from './certificate.js';
|
|
12
|
+
export * from './cron.js';
|
|
13
|
+
export * from './money.js';
|
|
14
|
+
export * from './rules.js';
|
|
15
|
+
export * from './secrets.js';
|
|
16
|
+
export * from './registry.js';
|
|
17
|
+
export * from './errors.js';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact decimal arithmetic on strings.
|
|
3
|
+
*
|
|
4
|
+
* Money and meters are never floats. `BilledResources.credits` and
|
|
5
|
+
* `UsageEventRow.quantity` are decimal strings precisely so a value cannot pass
|
|
6
|
+
* through binary64 and come back a fraction of a cent different, and every
|
|
7
|
+
* operation on them has to preserve that.
|
|
8
|
+
*
|
|
9
|
+
* This lives in core because three places need it — the agent loop's budget
|
|
10
|
+
* cap, the API's meter, and billing — and two independent implementations of
|
|
11
|
+
* money addition is how they disagree by a cent in a reconciliation nobody can
|
|
12
|
+
* attribute. It is the same argument as `ListResponse` and `WallClock`, with a
|
|
13
|
+
* worse failure mode: a wrong number that looks right.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is bigint underneath. No `Number` appears in an arithmetic
|
|
16
|
+
* path, only in the scale bookkeeping, where the values are small integers.
|
|
17
|
+
*/
|
|
18
|
+
export declare const ZERO_CREDITS = "0";
|
|
19
|
+
export declare function isDecimalString(value: unknown): value is string;
|
|
20
|
+
/** Exact addition. The result keeps the wider of the two scales. */
|
|
21
|
+
export declare function addDecimal(a: string, b: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Exact multiplication. The result's scale is the **sum** of the two scales,
|
|
24
|
+
* which is the only scale at which a product is exact: `0.001 * 0.002` is
|
|
25
|
+
* `0.000002`, and any narrower result has rounded.
|
|
26
|
+
*
|
|
27
|
+
* No rounding is applied here on purpose. A rate card times a quantity is a
|
|
28
|
+
* number three different consumers reconcile against — the meter, the token
|
|
29
|
+
* cost of a model call, and a captcha rate card amortised per solve — and each
|
|
30
|
+
* rounds at a different point for a different reason. Rounding here would make
|
|
31
|
+
* that decision once, silently, in the wrong place.
|
|
32
|
+
*/
|
|
33
|
+
export declare function multiplyDecimal(a: string, b: string): string;
|
|
34
|
+
/** -1, 0 or 1. `compareDecimal('0.30', '0.3') === 0`: scale is not value. */
|
|
35
|
+
export declare function compareDecimal(a: string, b: string): number;
|
|
36
|
+
/**
|
|
37
|
+
* The published price of one second of an agent run, and of one page it loads.
|
|
38
|
+
*
|
|
39
|
+
* ── Why this is in core and not only in the rate card ───────────────────────
|
|
40
|
+
*
|
|
41
|
+
* Because `AgentCaps.budget` is a promise about the number on the INVOICE, and
|
|
42
|
+
* until this existed the two were different quantities. `CapLedger` was charged
|
|
43
|
+
* only from `ModelChoice.credits` — model spend, priced in `@outcrawl/model`'s
|
|
44
|
+
* USD card — and `ModelClientOptions.creditsPerUsd` is deliberately unset in
|
|
45
|
+
* production, because the plan tiers price a credit at 204/251/313 to the
|
|
46
|
+
* dollar and the worker does not know the caller's tier. So `creditsFor`
|
|
47
|
+
* returned `undefined`, the loop's `if (choice.credits !== undefined)` guard
|
|
48
|
+
* never fired, and the ledger held `'0'` for the whole run. `tripped()` ran on
|
|
49
|
+
* every step and compared `'0' >= '0.50'` forever.
|
|
50
|
+
*
|
|
51
|
+
* The cap was not failing to hold the line. It was measuring a quantity that is
|
|
52
|
+
* always zero, while the customer was billed for three others. Measured on
|
|
53
|
+
* production 2026-09-08: 26 runs carried a budget, 12 exceeded it, worst 4.20x,
|
|
54
|
+
* and `max-budget` had never once been the stop reason.
|
|
55
|
+
*
|
|
56
|
+
* The invoice is `page_load + agent_seconds + context_seconds` — the exact rows
|
|
57
|
+
* `meterAgentRun` writes in `@outcrawl/api`. On `run_7a643435`, cap `0.50`:
|
|
58
|
+
*
|
|
59
|
+
* page_load 1 x 1 = 1.000000
|
|
60
|
+
* agent_seconds 7.606 @ 5/60 = 0.633833
|
|
61
|
+
* context_seconds 7.606 @ 1/60 = 0.126767
|
|
62
|
+
* tokens 9,954 = 0 <- the rate card has no token line
|
|
63
|
+
* ----------
|
|
64
|
+
* 1.760600 = 3.52x the cap
|
|
65
|
+
*
|
|
66
|
+
* So the run's price is a rate on TIME plus a rate on PAGES, and the loop can
|
|
67
|
+
* evaluate both while it is still running. It cannot reach the rate card:
|
|
68
|
+
* `@outcrawl/persistence` depends on `@outcrawl/runtime`, so the runtime
|
|
69
|
+
* importing it back is a cycle. Injecting the rate from the host was the option
|
|
70
|
+
* rejected — a host that forgets to pass it silently unbinds the cap, which is
|
|
71
|
+
* `creditsPerUsd`'s defect wearing a different name. So the number lives here,
|
|
72
|
+
* where both sides can reach it, and `test/rate-card.test.ts` in
|
|
73
|
+
* `@outcrawl/persistence` asserts these two constants against
|
|
74
|
+
* `OUTCRAWL_CREDIT_RATES` so a reprice cannot move one without the other.
|
|
75
|
+
*
|
|
76
|
+
* `'0.1'` and not the ratio `6/60`: it is the SUM of the two per-minute rates
|
|
77
|
+
* the invoice charges — Agent at +5 and the browser it holds at 1 — and six
|
|
78
|
+
* over sixty is exactly a tenth, so pricing a second needs no division and
|
|
79
|
+
* therefore no rounding scale. That matters because `multiplyDecimal` is exact
|
|
80
|
+
* and division is not: `divide` in the rate card has to choose six places, and a
|
|
81
|
+
* cap that rounded UP would stop a run fractionally before its ceiling while a
|
|
82
|
+
* cap that rounded DOWN would let it past. Neither is defensible in a ceiling,
|
|
83
|
+
* and at a tenth per second the question does not arise.
|
|
84
|
+
*
|
|
85
|
+
* The invoice still prices the two minute-rates as separate rows and rounds each
|
|
86
|
+
* to six places, so the ledger's figure and the bill's can differ by at most one
|
|
87
|
+
* unit in the last place of each row — 2e-6 credits. On `run_7a643435` the two
|
|
88
|
+
* roundings cancelled exactly: `0.633833 + 0.126767 = 0.760600`, and
|
|
89
|
+
* `7.606 x 0.1 = 0.7606`, the same number. That residual is an artefact of the
|
|
90
|
+
* invoice showing two lines, not a second opinion about the price.
|
|
91
|
+
*/
|
|
92
|
+
export declare const RUN_CREDITS_PER_SECOND = "0.1";
|
|
93
|
+
/** One rendered page, at the published Scrape rate. See {@link RUN_CREDITS_PER_SECOND}. */
|
|
94
|
+
export declare const RUN_CREDITS_PER_PAGE = "1";
|
|
95
|
+
/**
|
|
96
|
+
* What a run is charged for, as one value a ledger can be constructed with.
|
|
97
|
+
*
|
|
98
|
+
* Both fields are required. An optional rate reads as "this deployment does not
|
|
99
|
+
* charge for time", which is the shape that made the budget cap a no-op.
|
|
100
|
+
*/
|
|
101
|
+
export interface RunPricing {
|
|
102
|
+
/** Credits per second of run time. */
|
|
103
|
+
readonly creditsPerSecond: string;
|
|
104
|
+
/** Credits per page load. */
|
|
105
|
+
readonly creditsPerPage: string;
|
|
106
|
+
}
|
|
107
|
+
/** The published price list, as {@link RunPricing}. */
|
|
108
|
+
export declare const OUTCRAWL_RUN_PRICING: RunPricing;
|