@onetype/stack-api-kit 1.0.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,327 @@
1
+ import { C as Context, a as Command, L as Listener, P as Participant, D as Definition, b as Plugin, R as Route, N as Narrowing, O as Outbox, S as Schedule, K as Kernel, c as Caller, d as Outbound, e as Store, f as Opening, g as Dialer, B as Budget, h as Logger } from './api-BZrn0c9T.js';
2
+ export { A as Announcement, i as Described, E as Endpoint, j as Event, F as Failure, H as Handle, k as Heard, l as Hook, I as Incoming, J as Joined, m as Log, M as Method, n as MigrationFault, o as Options, p as Outgoing, q as Permission, r as Registered, s as Run, t as Scheduled, u as Schematic, v as Source, w as Step, x as Storage, T as Tables, y as createKernel, z as database } from './api-BZrn0c9T.js';
3
+ import { z } from 'zod';
4
+ import { Context as Context$1, Hono } from 'hono';
5
+ import Database from 'better-sqlite3';
6
+ import 'drizzle-orm/better-sqlite3';
7
+
8
+ /** What a client is told: a status, a stable code, and one sentence. */
9
+ type Answer = {
10
+ status: number;
11
+ code: string;
12
+ message: string;
13
+ /** Field-level detail, only ever from an input schema. */
14
+ fields?: Readonly<Record<string, string>>;
15
+ };
16
+ /**
17
+ * What a handler returns when the body alone is not the answer.
18
+ *
19
+ * A route returning a plain value gets 200, or 201 for a POST, which is what
20
+ * almost every route wants. This is for the rest: a redirect, a created
21
+ * resource naming where it went, a 304, a download with a filename.
22
+ *
23
+ * The body still passes the output schema. Status and headers are the only
24
+ * things this adds, because they are the only things a schema cannot carry.
25
+ */
26
+ declare class Answered {
27
+ readonly status: number;
28
+ readonly body: unknown;
29
+ readonly headers: Readonly<Record<string, string>>;
30
+ constructor(status: number, body: unknown, headers?: Readonly<Record<string, string>>);
31
+ /** Sends the caller somewhere else. */
32
+ static redirect(to: string, permanent?: boolean): Answered;
33
+ }
34
+ /** A refusal a plugin raises on purpose, meant to reach the caller. */
35
+ declare class Refusal extends Error {
36
+ readonly status: number;
37
+ readonly code: string;
38
+ readonly fields: Readonly<Record<string, string>> | undefined;
39
+ constructor(status: number, code: string, message: string, fields?: Readonly<Record<string, string>>);
40
+ }
41
+ /**
42
+ * What the caller is told about a failure.
43
+ *
44
+ * Nothing that was not written for a caller ever reaches one. A thrown thing
45
+ * is either a Refusal, which a plugin wrote deliberately, or one of the few
46
+ * kernel codes that mean something to a client. Everything else answers 500
47
+ * with one fixed sentence, whatever it actually was.
48
+ *
49
+ * This is the whole point: a stack, a file path, a SQL fragment, a column
50
+ * name, a driver message and a config value are all things an attacker learns
51
+ * from, and none of them are things a caller needs.
52
+ */
53
+ declare function answer(cause: unknown): Answer;
54
+
55
+ /**
56
+ * Declares a plugin.
57
+ *
58
+ * The name is checked here rather than at start, so a typo names itself where
59
+ * it was written instead of in a stack from somewhere else.
60
+ *
61
+ * `Db` is what `ctx.db` will be: the Drizzle handle over this plugin's own
62
+ * tables. TypeScript cannot infer it, because the kernel never imports a
63
+ * driver, so a plugin that queries names it:
64
+ *
65
+ * export default definePlugin.over<ItemRows>()("items", { … });
66
+ *
67
+ * A plugin that never touches the database writes `definePlugin(name, …)` and
68
+ * ignores all of this.
69
+ */
70
+ declare function definePlugin<Schema extends z.ZodType, Services = unknown, Db = unknown>(name: string, definition: Definition<Schema, Services, Db>): Plugin;
71
+ declare namespace definePlugin {
72
+ var over: <Db, Services = unknown>() => <Schema extends z.ZodType>(name: string, definition: Definition<Schema, Services, Db>) => Plugin;
73
+ }
74
+ /**
75
+ * Declares one route, with its input typed by its own schema.
76
+ *
77
+ * TypeScript settles a whole object literal's type before it looks inside an
78
+ * array, so a route written inline hands its handler `unknown`. Declaring the
79
+ * route through a function infers the schema first, and the handler reads its
80
+ * fields.
81
+ *
82
+ * defineRoute<ItemCtx>()({
83
+ * method: "GET",
84
+ * input: z.object({ id: z.uuid() }),
85
+ * handle: (given) => given.id, // a string, no cast
86
+ * })
87
+ */
88
+ declare function defineRoute<Ctx = Context>(): <Input extends z.ZodType>(route: Route<Ctx, Input>) => Route<Ctx, Input>;
89
+ /**
90
+ * Declares one listener, with its payload typed by the event's own schema.
91
+ *
92
+ * Pass the schema the emitting plugin declared: the kernel parses against it
93
+ * before delivering, so what arrives has already passed.
94
+ *
95
+ * defineListener<ItemCtx>()(ItemMade.schema, {
96
+ * describe: "…",
97
+ * handle: (made) => made.id, // a string, no cast
98
+ * })
99
+ */
100
+ declare function defineListener<Ctx = Context>(): <Payload extends z.ZodType>(_schema: Payload, listener: Listener<Ctx, z.infer<Payload>>) => Listener<Ctx, z.infer<Payload>>;
101
+ /** Declares one participant, with its payload typed by the hook's schema. */
102
+ declare function defineParticipant<Ctx = Context>(): <Payload extends z.ZodType>(_schema: Payload, participant: Participant<Ctx, z.infer<Payload>>) => Participant<Ctx, z.infer<Payload>>;
103
+ /** Declares one command, with its input typed by its own schema. */
104
+ declare function defineCommand<Ctx = Context>(): <Input extends z.ZodType>(command: Command<Ctx, Input>) => Command<Ctx, Input>;
105
+
106
+ /**
107
+ * What the kernel refuses.
108
+ *
109
+ * A closed union rather than a string: a caller branches on it, and a new
110
+ * member is a compile error everywhere it is handled exhaustively.
111
+ */
112
+ type FaultCode = "DUPLICATE_PLUGIN" | "UNKNOWN_DEPENDENCY" | "DEPENDENCY_CYCLE" | "INVALID_NAME" | "INVALID_CONFIG" | "INVALID_ROUTE" | "INVALID_PAYLOAD" | "WRONG_PAYLOAD" | "INVALID_OUTPUT" | "UNDECLARED_EVENT" | "UNDECLARED_HOOK" | "UNDECLARED_COMMAND" | "UNDECLARED_SCOPE" | "OUT_OF_SCOPE" | "UNDECLARED_PERMISSION" | "UNDECLARED_DEPENDENCY" | "UNDECLARED_HOST" | "DUPLICATE_ROUTE" | "DUPLICATE_EVENT" | "DUPLICATE_HOOK" | "DUPLICATE_COMMAND" | "DUPLICATE_PERMISSION" | "DUPLICATE_TABLE" | "UNAUTHENTICATED" | "PERMISSION_DENIED" | "RATE_LIMITED" | "NOT_STARTED";
113
+ type Made = {
114
+ plugin?: string;
115
+ detail?: Readonly<Record<string, unknown>>;
116
+ cause?: unknown;
117
+ };
118
+ /**
119
+ * A refusal, naming the plugin it came from.
120
+ *
121
+ * What makes a message worth the line is the plugin, the key, the owner, and
122
+ * what to do about it. A code alone costs an hour.
123
+ *
124
+ * This is what the kernel says to whoever wrote the plugin. It is never what
125
+ * a client is told: `internal/answer.ts` decides that, and it says less.
126
+ */
127
+ declare class KernelFault extends Error {
128
+ readonly code: FaultCode;
129
+ readonly plugin: string | undefined;
130
+ readonly detail: Readonly<Record<string, unknown>>;
131
+ constructor(code: FaultCode, message: string, made?: Made);
132
+ toString(): string;
133
+ }
134
+
135
+ /** One thing wrong, and everything needed to fix it. */
136
+ type Wrong = {
137
+ code: KernelFault["code"];
138
+ plugin: string;
139
+ message: string;
140
+ };
141
+
142
+ /**
143
+ * Turns a declared scope into a condition.
144
+ *
145
+ * The kernel knows which table and which column, and nothing about how a
146
+ * query is built. This knows Drizzle and nothing about tenants, so neither
147
+ * half has to learn the other's business.
148
+ */
149
+ declare function narrowing(owned: Readonly<Record<string, Readonly<Record<string, unknown>>>>): Narrowing;
150
+
151
+ /**
152
+ * Where events wait, in the same database as the work they announce.
153
+ *
154
+ * That is the whole point: the row is written by the transaction that emitted
155
+ * the event, so the two cannot disagree. An event about work that rolled back
156
+ * rolls back with it, and work that committed leaves an event behind even if
157
+ * the process stops before anyone hears it.
158
+ *
159
+ * Its table is the kit's, not a plugin's, so no contract declares it and no
160
+ * `ctx.db` reaches it.
161
+ */
162
+ declare function outbox(connection: Database.Database): Outbox;
163
+
164
+ /**
165
+ * Where later work waits, in the same database as the work that asked for it.
166
+ *
167
+ * A job claimed by one process is not claimed by another: `take` marks and
168
+ * reads in one statement, so two processes beating at the same moment split
169
+ * the work rather than doubling it.
170
+ *
171
+ * Its table is the kit's, not a plugin's, so no contract declares it.
172
+ */
173
+ declare function schedule(connection: Database.Database): Schedule;
174
+
175
+ /**
176
+ * What every response carries, whatever it answers.
177
+ *
178
+ * An API serves data, not documents, so these are the ones that still mean
179
+ * something: the browser protections that matter for a page are set by
180
+ * whatever serves the page.
181
+ *
182
+ * - nosniff stops a browser guessing a content type it was already told.
183
+ * - DENY in frame-options and frame-ancestors 'none' keeps an error page out
184
+ * of someone else's iframe.
185
+ * - no-store keeps an authenticated answer out of a shared cache. An API
186
+ * response is per-caller, and a cache that kept one would hand it to the
187
+ * next.
188
+ * - no-referrer keeps a path with an id in it from reaching another origin.
189
+ */
190
+ declare const always: Readonly<Record<string, string>>;
191
+
192
+ /** What serving needs to know. */
193
+ type Serving = {
194
+ kernel: Kernel;
195
+ /**
196
+ * Who is calling. The project owns this entirely: a cookie, a bearer
197
+ * token, a header, whatever it decided a session is.
198
+ *
199
+ * Throwing answers 401. Returning undefined is an anonymous caller, which
200
+ * only a public route accepts.
201
+ */
202
+ identify?: ((c: Context$1) => Caller | undefined | Promise<Caller | undefined>) | undefined;
203
+ /**
204
+ * What to count an anonymous caller by, for a rate limit: an address, an
205
+ * api key, whatever the deployment can trust. Reading a forwarded header
206
+ * blindly lets anyone spend anyone's budget, so the project decides.
207
+ */
208
+ from?: ((c: Context$1) => string) | undefined;
209
+ origins?: readonly string[];
210
+ methods?: readonly string[];
211
+ headers?: readonly string[];
212
+ maxAge?: number;
213
+ /** The largest body accepted, before it is parsed. */
214
+ bodyBytes?: number;
215
+ /** Where a line goes. */
216
+ log?: ((level: "info" | "warn" | "error", line: string, about?: Readonly<Record<string, unknown>>) => void) | undefined;
217
+ };
218
+ declare function identifier(sent: string | undefined): string;
219
+ declare function serve(serving: Serving): Hono;
220
+
221
+ type Server = ReturnType<typeof serve>;
222
+
223
+ type Dialing = {
224
+ timeoutMs?: number;
225
+ maxBytes?: number;
226
+ headers?: (() => Readonly<Record<string, string>>) | undefined;
227
+ };
228
+ declare class OutboundFault extends Error {
229
+ readonly code: "TIMEOUT" | "ABORTED" | "NETWORK" | "TOO_LARGE" | "MALFORMED" | "STATUS";
230
+ readonly status: number | undefined;
231
+ constructor(code: OutboundFault["code"], message: string, status?: number, cause?: unknown);
232
+ }
233
+ declare function dial(dialing?: Dialing): (call: Outbound) => Promise<unknown>;
234
+
235
+ declare function same(left: string, right: string): boolean;
236
+
237
+ type Window = {
238
+ requests: number;
239
+ seconds: number;
240
+ };
241
+ type Verdict = {
242
+ allowed: boolean;
243
+ remaining: number;
244
+ resetsIn: number;
245
+ };
246
+ declare function limiter(now?: () => number): {
247
+ take: (key: string, window: Window) => Verdict;
248
+ sweep: () => number;
249
+ size: () => number;
250
+ };
251
+
252
+ type Limiter = ReturnType<typeof limiter>;
253
+
254
+ type Found = Readonly<Record<string, {
255
+ default?: Plugin;
256
+ }>>;
257
+ declare function discover(found: Found): Plugin[];
258
+
259
+ declare function start(starting: Starting): Promise<Started>;
260
+
261
+ type Starting = {
262
+ plugins: readonly Plugin[];
263
+ /**
264
+ * Where the database is, or a store the project built itself.
265
+ *
266
+ * Given a path, the kit opens SQLite and migrates. Given a store, it uses
267
+ * that one and migrates only if it can: a project running Postgres, or
268
+ * anything else answering `Store`, replaces the database without the kit
269
+ * knowing which one it got.
270
+ */
271
+ database: Opening | Store;
272
+ config?: Readonly<Record<string, unknown>> | undefined;
273
+ /**
274
+ * Who is calling.
275
+ *
276
+ * Given the kernel, because anything reading a session reads it from a
277
+ * plugin, and the kernel is what reaches one. Passing the function
278
+ * directly would mean holding a kernel that does not exist yet.
279
+ */
280
+ identify?: ((kernel: Kernel) => Serving["identify"]) | undefined;
281
+ http?: Omit<Serving, "kernel" | "identify" | "log"> | undefined;
282
+ /**
283
+ * How outbound calls are carried, or how the built-in one is configured.
284
+ *
285
+ * A dialler of its own is how a project reaches what the kit's cannot: a
286
+ * proxy, a signed request, a protocol that is not https. The kernel still
287
+ * refuses a host the plugin did not declare, whichever dialler carries it.
288
+ */
289
+ outbound?: Dialing | Dialer | undefined;
290
+ /**
291
+ * What counts requests against a route's declared limit.
292
+ *
293
+ * Omitted, the kit keeps one in memory, which counts for this process
294
+ * only. A deployment behind more than one process passes its own, so a
295
+ * limit means the same thing whichever process answered.
296
+ */
297
+ budget?: Budget | undefined;
298
+ /**
299
+ * Whether events are kept until a listener has heard them.
300
+ *
301
+ * Off by default, and that is a real choice rather than a default nobody
302
+ * revisits: without it an event lives only in memory, so a process that
303
+ * stops between a commit and its delivery leaves work done that nothing
304
+ * was ever told about. On, delivery is at least once and a listener that
305
+ * writes must survive hearing the same event twice.
306
+ */
307
+ outbox?: boolean | undefined;
308
+ /**
309
+ * Whether a plugin may ask for work later.
310
+ *
311
+ * Off by default. On, the kit keeps a schedule in the same database and
312
+ * asks it every second what is due. It knows seconds from now and nothing
313
+ * about calendars: work that repeats schedules itself again when it ends.
314
+ */
315
+ schedule?: boolean | undefined;
316
+ log?: Logger | undefined;
317
+ };
318
+ type Started = {
319
+ kernel: Kernel;
320
+ store: Store;
321
+ app: ReturnType<typeof serve>;
322
+ /** What a runtime serves: `export default { fetch }`. */
323
+ fetch: (request: Request) => Response | Promise<Response>;
324
+ stop: () => Promise<void>;
325
+ };
326
+
327
+ export { type Answer, Answered, Budget, Caller, Command, Context, Definition, Dialer, type Dialing, type FaultCode, Kernel, KernelFault, type Limiter, Listener, Logger, Narrowing, Opening, Outbound, OutboundFault, Outbox, Participant, Plugin, Refusal, Route, Schedule, type Server, type Serving, type Started, type Starting, Store, type Verdict, type Window, type Wrong, always, answer, defineCommand, defineListener, defineParticipant, definePlugin, defineRoute, dial, discover, identifier, limiter, narrowing, outbox, same, schedule, serve, start };