@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.
- package/README.md +58 -0
- package/dist/api-BZrn0c9T.d.ts +654 -0
- package/dist/chunk-UCWOCNTI.js +1749 -0
- package/dist/chunk-UCWOCNTI.js.map +1 -0
- package/dist/index.d.ts +327 -0
- package/dist/index.js +381 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +165 -0
- package/dist/testing.js +450 -0
- package/dist/testing.js.map +1 -0
- package/package.json +75 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|