sproutboat 0.8.0 → 0.10.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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `sproutboat types` — the project's `env`, as TypeScript.
3
+ *
4
+ * In Workers `env` is a parameter, so an editor can infer it from a signature.
5
+ * Here it is a global, which means nothing is inferable at all: `env.SESSIONS`
6
+ * is an undeclared identifier until this file exists. That makes generated
7
+ * types worth more here than they are there, not less.
8
+ *
9
+ * The output declares `env` from `sproutboat.jsonc` and references the ambient
10
+ * types shipped in `types/sproutboat.d.ts`, so binding *shapes* version with
11
+ * the CLI and binding *names* version with the project.
12
+ *
13
+ * Names need no quoting: `parseConfig` already holds every binding and var to
14
+ * /^[A-Z][A-Z0-9_]*$/, so all of them are plain identifiers.
15
+ */
16
+ import { resourceRefs, type SproutboatConfig } from "./config";
17
+
18
+ /** The file the generator writes, relative to the project directory. */
19
+ export const TYPES_FILE = "sproutboat-env.d.ts";
20
+
21
+ const HEADER = `// Generated by \`sproutboat types\`. Do not edit.
22
+ //
23
+ // Regenerated whenever sproutboat.jsonc is newer than this file, so a binding
24
+ // added to the config shows up in the editor without asking. Commit it: it is
25
+ // part of the project's type surface, like a schema.
26
+ `;
27
+
28
+ type Entry = { name: string; type: string; note?: string };
29
+
30
+ function entries(config: SproutboatConfig): Entry[] {
31
+ const out: Entry[] = [];
32
+
33
+ for (const [name, value] of Object.entries(config.vars ?? {})) {
34
+ out.push({ name, type: "string", note: `vars: ${JSON.stringify(value)}` });
35
+ }
36
+ for (const name of config.secrets ?? []) {
37
+ out.push({ name, type: "string", note: "secret, set with `sproutboat secrets set`" });
38
+ }
39
+ for (const { binding } of resourceRefs(config.kv_namespaces)) out.push({ name: binding, type: "KVNamespace" });
40
+ for (const { binding } of resourceRefs(config.d1_databases)) out.push({ name: binding, type: "D1Database" });
41
+ for (const { binding } of resourceRefs(config.r2_buckets)) out.push({ name: binding, type: "R2Bucket" });
42
+ for (const { binding } of resourceRefs(config.queues)) out.push({ name: binding, type: "Queue" });
43
+ for (const name of config.analytics_engine_datasets ?? []) {
44
+ out.push({ name, type: "AnalyticsEngineDataset" });
45
+ }
46
+ for (const [binding, className] of Object.entries(config.durable_objects ?? {})) {
47
+ out.push({ name: binding, type: "DurableObjectNamespace", note: `class ${className}` });
48
+ }
49
+ for (const service of config.services ?? []) {
50
+ out.push({ name: service.binding, type: "Fetcher", note: `service binding: ${service.service}` });
51
+ }
52
+ for (const rl of config.ratelimiters ?? []) {
53
+ out.push({ name: rl.binding, type: "RateLimit", note: `rate limit: ${rl.limit} per ${rl.period}s` });
54
+ }
55
+ if (config.assets?.binding) {
56
+ out.push({ name: config.assets.binding, type: "Fetcher", note: `static assets: ${config.assets.directory}` });
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /**
62
+ * The contents of `sproutboat-env.d.ts` for `config`.
63
+ *
64
+ * Pure, so the test can assert on the text and `dev` can compare it against
65
+ * what is already on disk without writing.
66
+ */
67
+ export function generateTypes(config: SproutboatConfig): string {
68
+ const rows = entries(config);
69
+ const body = rows.length
70
+ ? rows
71
+ .map((entry) => ` ${entry.note ? `/** ${entry.note} */\n ` : ""}${entry.name}: ${entry.type};`)
72
+ .join("\n")
73
+ : " // No bindings declared in sproutboat.jsonc yet.";
74
+
75
+ return `${HEADER}
76
+ /// <reference types="sproutboat/types" />
77
+
78
+ declare global {
79
+ /**
80
+ * Bindings from sproutboat.jsonc, for project "${config.name}".
81
+ *
82
+ * A global, not a parameter: the handler signature is \`fetch(request)\`.
83
+ * Every call on these is synchronous, so \`await\` is allowed and does
84
+ * nothing.
85
+ */
86
+ const env: {
87
+ ${body}
88
+ };
89
+ }
90
+
91
+ export {};
92
+ `;
93
+ }
package/src/wrap.ts CHANGED
@@ -1,267 +1,5 @@
1
- /**
2
- * The build-independent half of sprout compilation: the binding/trigger wrapper
3
- * that turns a user's `export default { fetch }` into a native-fetch module, plus
4
- * the `Bindings` shape and the `SPROUTBOAT_*_JSON` env readers.
5
- *
6
- * This module has no imports on purpose — the monorepo consumes it via the
7
- * `sproutboat/runtime/wrap` export to drive its own (host-native, non-musl)
8
- * compile path without pulling in `toolchain.ts` / `patch-porffor.ts`.
9
- */
10
-
11
- /** The prelude file (Web API shims + broker binding shim + trigger dispatcher).
12
- * It is read as text and string-prepended before Porffor sees it, never
13
- * imported — callers do `readFile(preludePath, "utf8")`. */
14
- export const preludePath = new URL("./native-fetch-prelude.js", import.meta.url);
15
-
16
- /**
17
- * #15 — the two transports the prelude can be built with.
18
- *
19
- * Both define `__sbCall(reqJson) -> replyJson` and nothing else; every binding
20
- * shim above that line is identical, which is what lets one conformance suite
21
- * hold both honest. `broker` talks to the per-deployment broker over loopback
22
- * (deployed, dev, phase-0 standalone); `embedded` compiles SQLite into the
23
- * sprout and needs no second process at all.
24
- */
25
- export type Transport = "broker" | "embedded";
26
- export const transportPath = (transport: Transport): URL =>
27
- new URL(transport === "embedded" ? "./transport-embedded.js" : "./transport-broker.js", import.meta.url);
28
-
29
- /** Where the prelude expects its transport spliced in. */
30
- export const TRANSPORT_MARKER =
31
- "// TRANSPORT: wrap.ts splices one of transport-broker.js / transport-embedded.js here.";
32
-
33
- // The server honours $PORT at runtime (patches/porffor-render.patch); this baked
34
- // value is only a fallback for a directly-run binary.
35
- const DEFAULT_PORT = 8080;
36
-
37
- /**
38
- * What an artifact with no `compatibilityDate` means. Artifacts built before
39
- * the field existed keep the semantics of that day forever, because the binary
40
- * is immutable and `rollback` can reactivate it at any time.
41
- *
42
- * How to use it: when a runtime behaviour has to change in a way that would
43
- * break a deployed handler, don't change it unconditionally — gate it in the
44
- * prelude on `__sbCompat >= "YYYY-MM-DD"` (ISO dates compare correctly as
45
- * strings) and document the flip date. Old binaries carry their old date and
46
- * keep the old behaviour; a project opts in by moving `compatibility_date` in
47
- * its `sproutboat.jsonc` and rebuilding.
48
- */
49
- export const BASELINE_COMPATIBILITY_DATE = "2026-08-26";
50
-
51
- /**
52
- * Binding names a project declares. `do` maps a binding name to a Durable Object
53
- * class name; `crons` are schedule expressions with no name.
54
- */
55
- export type Bindings = {
56
- kv: string[];
57
- secrets: string[];
58
- outbound: string[];
59
- d1: string[];
60
- r2: string[];
61
- queues: string[];
62
- analytics: string[];
63
- do: Array<{ binding: string; className: string }>;
64
- /** #48 — worker-to-worker: binding name -> the project it calls. The hostname
65
- * it resolves to is a runtime input, not part of the artifact. */
66
- services: Array<{ binding: string; service: string }>;
67
- crons: string[];
68
- /** Static-asset binding name for `env.<NAME>.fetch(request)`; `""` when assets are edge-only. */
69
- assets: string;
70
- };
71
-
72
- export const EMPTY_BINDINGS: Bindings = {
73
- kv: [],
74
- secrets: [],
75
- outbound: [],
76
- d1: [],
77
- r2: [],
78
- queues: [],
79
- analytics: [],
80
- do: [],
81
- services: [],
82
- crons: [],
83
- assets: "",
84
- };
85
-
86
- function hasBindings(b: Bindings): boolean {
87
- return (
88
- b.kv.length > 0 ||
89
- b.secrets.length > 0 ||
90
- b.outbound.length > 0 ||
91
- b.d1.length > 0 ||
92
- b.r2.length > 0 ||
93
- b.queues.length > 0 ||
94
- b.analytics.length > 0 ||
95
- b.do.length > 0 ||
96
- b.services.length > 0 ||
97
- b.assets !== ""
98
- );
99
- }
100
-
101
- /**
102
- * Turn the module's exports into plain top-level declarations, so the handler
103
- * object is reachable as `__sbHandlers` and Durable Object classes stay
104
- * addressable by name.
105
- *
106
- * Two shapes reach us. A hand-written file exports inline
107
- * (`export default { fetch }`), while a bundled one declares everything first
108
- * and re-exports at the end (`export { src_default as default, Counter }`) —
109
- * #89 made the second shape the normal case. Returns null when neither matches.
110
- */
111
- export function neutraliseExports(source: string): string | null {
112
- if (/\bexport\s+default\s*\{/.test(source)) {
113
- return source
114
- .replace(/^(\s*)export\s+default\s*/m, "$1const __sbHandlers = ")
115
- .replace(/^export\s+(async\s+function|function|class|const|let|var)\b/gm, "$1");
116
- }
117
- // Not anchored to a line: a minified bundle puts the whole module on one
118
- // line. Bundlers emit exactly one such block, at the end.
119
- const blocks = [...source.matchAll(/export\s*\{([^}]*)\}\s*;?/g)];
120
- const block = blocks[blocks.length - 1];
121
- if (block === undefined) return null;
122
- let handler: string | null = null;
123
- const aliases: string[] = [];
124
- for (const entry of block[1]
125
- .split(",")
126
- .map((part) => part.trim())
127
- .filter(Boolean)) {
128
- const parts = entry.match(/^(\S+)(?:\s+as\s+(\S+))?$/);
129
- if (parts === null) continue;
130
- const local = parts[1];
131
- const exported = parts[2] ?? local;
132
- if (exported === "default") handler = local;
133
- // `export { Counter as Counter }` needs no alias; a renamed one does, so
134
- // `durable_objects` in the config can still name the class it expects.
135
- else if (exported !== local) aliases.push(`const ${exported} = ${local};`);
136
- }
137
- if (handler === null) return null;
138
- return source.replace(block[0], [`const __sbHandlers = ${handler};`, ...aliases].join("\n"));
139
- }
140
-
141
- /**
142
- * Build the final native-fetch module: the prelude (Web API shims + the broker
143
- * binding shim + the trigger dispatcher), then `const env = {…}` with the baked
144
- * `vars`, then — if any binding is declared — one `__sbInstallBindings(env, …)`
145
- * line, then the user's source with its `export` keywords neutralised (so its
146
- * `export default {…}` becomes a plain object we can hand to the dispatcher),
147
- * then our single `export default { fetch }` that routes every request through
148
- * `__sbEntry` (HTTP → `handlers.fetch`; `x-sb-trigger` → scheduled / queue / DO).
149
- *
150
- * With no bindings and no `scheduled`/`queue`/DO the output behaves exactly like
151
- * a plain `export default { fetch }` sprout.
152
- *
153
- * `port` is only the baked fallback in `export default { port }`; the runtime
154
- * reads `$PORT` first. The monorepo's bench path overrides it.
155
- *
156
- * ponytail: the sprout process is long-lived, so a handler that mutates `env`
157
- * leaks that change to later requests. Freeze upstream once Porffor supports
158
- * Object.freeze in native mode.
159
- */
160
- export function wrapNativeFetchHandler(
161
- source: string,
162
- prelude: string,
163
- vars: Record<string, string> = {},
164
- bindings: Bindings = EMPTY_BINDINGS,
165
- port: number = DEFAULT_PORT,
166
- compatibilityDate: string = BASELINE_COMPATIBILITY_DATE,
167
- appName: string = "app",
168
- /** #15 — assets baked into the module for a binary that has no files beside it. */
169
- assets?: { manifest: unknown; files: Record<string, string> },
170
- ): string {
171
- const neutralised = neutraliseExports(source);
172
- if (neutralised === null || !/\bfetch\s*\(/.test(source)) {
173
- throw new Error("handler must default-export an object with a fetch(request) method");
174
- }
175
-
176
- const env = `const env = ${JSON.stringify(vars)};\nglobalThis.env = env;\n`;
177
- // Baked, not a binding: the date belongs to the artifact, and a handler must
178
- // not be able to change the semantics it was compiled against at runtime.
179
- const compat =
180
- `globalThis.__sbCompat = ${JSON.stringify(compatibilityDate)};\n` +
181
- // #15 — the embedded transport derives its default data directory from this.
182
- `globalThis.__sbAppName = ${JSON.stringify(appName)};\n` +
183
- // #15 — and enforces the outbound allowlist itself, with no broker to do it.
184
- `globalThis.__sbOutbound = ${JSON.stringify(bindings.outbound)};\n` +
185
- (assets ? `globalThis.__sbAssets = ${JSON.stringify(assets)};\n` : "");
186
- const wire = hasBindings(bindings) ? `__sbInstallBindings(env, ${JSON.stringify(bindings)});\n` : "";
187
- const registerDO = bindings.do.length
188
- ? `__sbRegisterDO({ ${bindings.do.map((d) => `${d.className}: ${d.className}`).join(", ")} });\n`
189
- : "";
190
- // Cron / queue / alarm timers, for a transport that has no broker to deliver
191
- // them. The broker transport defines this as a no-op, so the emitted module
192
- // is the same either way.
193
- const triggers = hasBindings(bindings) ? `__sbStartLocalTriggers(__sbHandlers, ${JSON.stringify(bindings)});\n` : "";
194
-
195
- return (
196
- `${prelude}\n${compat}${env}${wire}` +
197
- `${neutralised}\n` +
198
- `${registerDO}${triggers}` +
199
- `export default {\n port: ${port},\n fetch(request) { return __sbEntry(__sbHandlers, request); }\n};\n`
200
- );
201
- }
202
-
203
- type VarsJson = string | number | boolean | null | { readonly [key: string]: VarsJson } | VarsJson[];
204
- function isVarsObject(value: VarsJson): value is { readonly [key: string]: VarsJson } {
205
- return value !== null && Object(value) === value && !Array.isArray(value);
206
- }
207
- function isVarsString(value: VarsJson): value is string {
208
- return Object(value) !== value && value === String(value);
209
- }
210
-
211
- /** `SPROUTBOAT_VARS_JSON` (set by the build) → a validated flat string map. */
212
- export function readVarsFromEnv(): Record<string, string> {
213
- const raw = process.env.SPROUTBOAT_VARS_JSON;
214
- if (!raw) return {};
215
- const parsed: VarsJson = JSON.parse(raw);
216
- if (!isVarsObject(parsed)) throw new Error("SPROUTBOAT_VARS_JSON must be a JSON object");
217
- return Object.fromEntries(
218
- Object.entries(parsed).map(([key, value]): [string, string] => {
219
- if (!/^[A-Z][A-Z0-9_]*$/.test(key) || !isVarsString(value))
220
- throw new Error(`SPROUTBOAT_VARS_JSON.${key} must map an UPPER_SNAKE name to a string`);
221
- return [key, value];
222
- }),
223
- );
224
- }
225
-
226
- /**
227
- * `SPROUTBOAT_BINDINGS_JSON` (the artifact's `bindings.json`, passed by the
228
- * build) → a `Bindings` shape. Every field is re-validated here; unknown keys
229
- * are dropped and a missing / empty payload is `EMPTY_BINDINGS`, so an old build
230
- * with no bindings still compiles.
231
- */
232
- export function readBindingsFromEnv(): Bindings {
233
- const raw = process.env.SPROUTBOAT_BINDINGS_JSON;
234
- if (!raw) return EMPTY_BINDINGS;
235
- const parsed: VarsJson = JSON.parse(raw);
236
- if (!isVarsObject(parsed)) throw new Error("SPROUTBOAT_BINDINGS_JSON must be a JSON object");
237
- const strings = (v: VarsJson): string[] => (Array.isArray(v) ? v.filter(isVarsString) : []);
238
- const services: Array<{ binding: string; service: string }> = [];
239
- if (Array.isArray(parsed.services)) {
240
- for (const entry of parsed.services) {
241
- if (isVarsObject(entry) && isVarsString(entry.binding) && isVarsString(entry.service)) {
242
- services.push({ binding: entry.binding, service: entry.service });
243
- }
244
- }
245
- }
246
- const dos: Array<{ binding: string; className: string }> = [];
247
- if (Array.isArray(parsed.do)) {
248
- for (const entry of parsed.do) {
249
- if (isVarsObject(entry) && isVarsString(entry.binding) && isVarsString(entry.className)) {
250
- dos.push({ binding: entry.binding, className: entry.className });
251
- }
252
- }
253
- }
254
- return {
255
- kv: strings(parsed.kv),
256
- secrets: strings(parsed.secrets),
257
- outbound: strings(parsed.outbound),
258
- d1: strings(parsed.d1),
259
- r2: strings(parsed.r2),
260
- queues: strings(parsed.queues),
261
- analytics: strings(parsed.analytics),
262
- do: dos,
263
- services,
264
- crons: strings(parsed.crons),
265
- assets: isVarsString(parsed.assets) ? parsed.assets : "",
266
- };
267
- }
1
+ // Re-exported from @sproutboat/runtime (moved verbatim with its file-URL
2
+ // siblings: transports and prelude travel together because wrap.ts locates
3
+ // them by file URL next to itself). This shim keeps every `./wrap` importer
4
+ // and the `sproutboat/runtime/wrap` export path working.
5
+ export * from "@sproutboat/runtime";
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Ambient types for a Sproutboat handler.
3
+ *
4
+ * Shipped with the CLI rather than as a separate package, so the types always
5
+ * describe the runtime that the installed CLI compiles. `sproutboat types`
6
+ * generates a project's `sproutboat-env.d.ts`, which references this file and
7
+ * declares `env` with that project's bindings.
8
+ *
9
+ * Two things differ from Cloudflare Workers and are the reason these exist:
10
+ * `env` is a global rather than a parameter, so nothing is inferable from a
11
+ * signature, and every binding call is synchronous.
12
+ */
13
+
14
+ declare global {
15
+ // ---------------------------------------------------------------- KV
16
+
17
+ interface KVNamespace {
18
+ /** The stored string, or `null`. Synchronous: no `await`. */
19
+ get(key: string): string | null;
20
+ put(key: string, value: string): void;
21
+ delete(key: string): void;
22
+ /** Keys under `prefix` (all keys when omitted). */
23
+ list(prefix?: string): string[];
24
+ }
25
+
26
+ // ---------------------------------------------------------------- D1
27
+
28
+ interface D1Meta {
29
+ /** `INSERT` only. The autoincrement id of the row just written. */
30
+ last_row_id: number;
31
+ changes: number;
32
+ }
33
+
34
+ interface D1Result<T = Record<string, unknown>> {
35
+ results: T[];
36
+ success: boolean;
37
+ meta: D1Meta;
38
+ }
39
+
40
+ interface D1PreparedStatement {
41
+ /** Positional `?` parameters, in order. */
42
+ bind(...values: Array<string | number | boolean | null>): D1PreparedStatement;
43
+ all<T = Record<string, unknown>>(): D1Result<T>;
44
+ run(): D1Result;
45
+ /** The first row, or one column of it when named. `null` if there are none. */
46
+ first<T = Record<string, unknown>>(): T | null;
47
+ first<T = unknown>(column: string): T | null;
48
+ }
49
+
50
+ interface D1Database {
51
+ prepare(sql: string): D1PreparedStatement;
52
+ batch(statements: D1PreparedStatement[]): D1Result[];
53
+ /** Runs a whole script. Use this for multi-statement DDL. */
54
+ exec(sql: string): void;
55
+ }
56
+
57
+ // ---------------------------------------------------------------- R2
58
+
59
+ interface R2Object {
60
+ key: string;
61
+ size: number;
62
+ etag: string;
63
+ uploaded: string;
64
+ customMetadata?: Record<string, string>;
65
+ /**
66
+ * The object as a string. An object is held whole in memory on the way in
67
+ * and on the way out, so keep them small.
68
+ */
69
+ body: string;
70
+ }
71
+
72
+ type R2ObjectHead = Omit<R2Object, "body">;
73
+
74
+ interface R2Objects {
75
+ objects: R2ObjectHead[];
76
+ truncated: boolean;
77
+ cursor?: string;
78
+ }
79
+
80
+ interface R2PutOptions {
81
+ customMetadata?: Record<string, string>;
82
+ }
83
+
84
+ interface R2ListOptions {
85
+ prefix?: string;
86
+ limit?: number;
87
+ cursor?: string;
88
+ }
89
+
90
+ interface R2Bucket {
91
+ put(key: string, value: string, options?: R2PutOptions): void;
92
+ get(key: string): R2Object | null;
93
+ /** Metadata without the body. */
94
+ head(key: string): R2ObjectHead | null;
95
+ delete(key: string): void;
96
+ list(options?: R2ListOptions): R2Objects;
97
+ }
98
+
99
+ // ------------------------------------------------------------ Queues
100
+
101
+ interface QueueSendOptions {
102
+ delaySeconds?: number;
103
+ }
104
+
105
+ interface Queue<Body = unknown> {
106
+ /** Returns immediately. The batch reaches `queue()` out of band. */
107
+ send(body: Body, options?: QueueSendOptions): void;
108
+ sendBatch(messages: Array<{ body: Body }>): void;
109
+ }
110
+
111
+ interface QueueMessage<Body = unknown> {
112
+ id: string;
113
+ timestamp: number;
114
+ body: Body;
115
+ /** Mark as handled. Without this the message is redelivered. */
116
+ ack(): void;
117
+ /** Hand it back for another attempt. */
118
+ retry(): void;
119
+ }
120
+
121
+ interface MessageBatch<Body = unknown> {
122
+ queue: string;
123
+ messages: Array<QueueMessage<Body>>;
124
+ }
125
+
126
+ // --------------------------------------------------- Durable Objects
127
+
128
+ interface DurableObjectId {
129
+ toString(): string;
130
+ name?: string;
131
+ }
132
+
133
+ interface DurableObjectStub {
134
+ fetch(request: Request): Response;
135
+ }
136
+
137
+ interface DurableObjectNamespace {
138
+ idFromName(name: string): DurableObjectId;
139
+ idFromString(hex: string): DurableObjectId;
140
+ newUniqueId(): DurableObjectId;
141
+ get(id: DurableObjectId): DurableObjectStub;
142
+ }
143
+
144
+ interface DurableObjectStorage {
145
+ get<T = unknown>(key: string): T | undefined;
146
+ put<T>(key: string, value: T): void;
147
+ delete(key: string): void;
148
+ deleteAll(): void;
149
+ /** Stored values under `prefix`, keyed by storage key. */
150
+ list<T = unknown>(options?: { prefix?: string; limit?: number }): Map<string, T>;
151
+ /**
152
+ * At most one alarm is pending per object; a later `setAlarm` replaces it.
153
+ * The handler is the class's `alarm()` method.
154
+ */
155
+ setAlarm(scheduledTime: number): void;
156
+ getAlarm(): number | null;
157
+ deleteAlarm(): void;
158
+ }
159
+
160
+ interface DurableObjectState {
161
+ id: DurableObjectId;
162
+ /** Synchronous, like every other binding. */
163
+ storage: DurableObjectStorage;
164
+ }
165
+
166
+ // -------------------------------------------------- Analytics Engine
167
+
168
+ interface AnalyticsEngineDataPoint {
169
+ blobs?: string[];
170
+ doubles?: number[];
171
+ indexes?: string[];
172
+ }
173
+
174
+ interface AnalyticsEngineDataset {
175
+ writeDataPoint(event: AnalyticsEngineDataPoint): void;
176
+ query<T = unknown>(options?: { limit?: number }): { count: number; rows: T[] };
177
+ }
178
+
179
+ // ------------------------------------ Rate limiting
180
+
181
+ /**
182
+ * A rate-limiter binding (#69). `limit` and `period` are fixed in
183
+ * sproutboat.jsonc; `limit({ key })` counts one call against a fixed window
184
+ * and reports whether the key is still under the cap.
185
+ */
186
+ interface RateLimit {
187
+ limit(options: { key: string }): { success: boolean };
188
+ }
189
+
190
+ // ------------------------------------ Crypto
191
+
192
+ /**
193
+ * `crypto.subtle` covers `digest` (SHA-256/384/512) and HMAC
194
+ * `importKey` / `sign` / `verify`; other algorithms throw.
195
+ *
196
+ * `crypto.scryptVerify` is a Sproutboat extension (#153), not WebCrypto: it
197
+ * re-derives a scrypt hash and compares it in constant time, for migrating
198
+ * password hashes made by Node/Bun `scrypt`. Verify-only on purpose; new
199
+ * credentials should use HMAC/PBKDF2 via `crypto.subtle`.
200
+ */
201
+ interface Crypto {
202
+ scryptVerify(
203
+ password: string | ArrayBuffer | ArrayBufferView,
204
+ salt: string | ArrayBuffer | ArrayBufferView,
205
+ expected: string | ArrayBuffer | ArrayBufferView,
206
+ params?: { N?: number; r?: number; p?: number },
207
+ ): boolean;
208
+ }
209
+
210
+ // ------------------------------------ Fetchers: services and assets
211
+
212
+ /** A service binding, and the shape of the static-asset binding. */
213
+ interface Fetcher {
214
+ fetch(request: Request): Response;
215
+ }
216
+
217
+ // --------------------------------------------------------- Handlers
218
+
219
+ interface ScheduledEvent {
220
+ /** The expression that fired, as written in `triggers.crons`. */
221
+ cron: string;
222
+ scheduledTime: number;
223
+ }
224
+
225
+ /**
226
+ * The default export.
227
+ *
228
+ * `fetch` takes only a request: `env` is a global, and there is no `ctx`, so
229
+ * there is no `ctx.waitUntil`. Use a queue for work that outlives a response.
230
+ */
231
+ interface SproutboatHandler<QueueBody = unknown> {
232
+ fetch(request: Request): Response | Promise<Response>;
233
+ scheduled?(event: ScheduledEvent): void | Promise<void>;
234
+ queue?(batch: MessageBatch<QueueBody>): void | Promise<void>;
235
+ }
236
+
237
+ /**
238
+ * What a Durable Object class implements. Declare it above the default
239
+ * export and name it in `durable_objects`.
240
+ */
241
+ interface DurableObject {
242
+ fetch(request: Request): Response | Promise<Response>;
243
+ /** Runs after `storage.setAlarm`, with no request in flight. */
244
+ alarm?(): void | Promise<void>;
245
+ }
246
+ }
247
+
248
+ export {};