@arki/http 0.0.1

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/bundle.d.ts +121 -0
  4. package/dist/bundle.d.ts.map +1 -0
  5. package/dist/bundle.js +98 -0
  6. package/dist/bundle.js.map +1 -0
  7. package/dist/context.d.ts +29 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +34 -0
  10. package/dist/context.js.map +1 -0
  11. package/dist/contract.d.ts +120 -0
  12. package/dist/contract.d.ts.map +1 -0
  13. package/dist/contract.js +49 -0
  14. package/dist/contract.js.map +1 -0
  15. package/dist/derive.d.ts +58 -0
  16. package/dist/derive.d.ts.map +1 -0
  17. package/dist/derive.js +28 -0
  18. package/dist/derive.js.map +1 -0
  19. package/dist/dot.d.ts +106 -0
  20. package/dist/dot.d.ts.map +1 -0
  21. package/dist/dot.js +210 -0
  22. package/dist/dot.js.map +1 -0
  23. package/dist/engine.d.ts +36 -0
  24. package/dist/engine.d.ts.map +1 -0
  25. package/dist/engine.js +173 -0
  26. package/dist/engine.js.map +1 -0
  27. package/dist/error.d.ts +107 -0
  28. package/dist/error.d.ts.map +1 -0
  29. package/dist/error.js +122 -0
  30. package/dist/error.js.map +1 -0
  31. package/dist/index.d.ts +28 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +21 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/listen.d.ts +23 -0
  36. package/dist/listen.d.ts.map +1 -0
  37. package/dist/listen.js +71 -0
  38. package/dist/listen.js.map +1 -0
  39. package/dist/openapi.d.ts +45 -0
  40. package/dist/openapi.d.ts.map +1 -0
  41. package/dist/openapi.js +140 -0
  42. package/dist/openapi.js.map +1 -0
  43. package/dist/sse.d.ts +12 -0
  44. package/dist/sse.d.ts.map +1 -0
  45. package/dist/sse.js +86 -0
  46. package/dist/sse.js.map +1 -0
  47. package/package.json +73 -0
  48. package/src/bundle.ts +221 -0
  49. package/src/context.ts +39 -0
  50. package/src/contract.ts +152 -0
  51. package/src/derive.ts +67 -0
  52. package/src/dot.ts +282 -0
  53. package/src/engine.ts +239 -0
  54. package/src/error.ts +158 -0
  55. package/src/index.ts +53 -0
  56. package/src/listen.ts +100 -0
  57. package/src/openapi.ts +168 -0
  58. package/src/sse.ts +92 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ARKI Contributors
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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # @arki/http
2
+
3
+ Typed HTTP for ARKI — route contracts as data, zod-validated handlers,
4
+ request-scope derivers, SSE streaming, and deterministic OpenAPI
5
+ generation. Ships a DOT adapter (`@arki/http/dot`) whose `http()` pip
6
+ turns a composed app into a running server with correct drain-on-shutdown
7
+ semantics.
8
+
9
+ No decorators, no controller scanning, no runtime DI container. Contracts
10
+ are plain values; wiring is checked at compile time.
11
+
12
+ ## The model
13
+
14
+ A route is split in two:
15
+
16
+ - **Contract** — pure data at module scope: method + path + zod schemas +
17
+ a stable id. Registerable in the DOT manifest (sync `configure`),
18
+ renderable as OpenAPI without booting, and the source of the handler's
19
+ inferred signature.
20
+ - **Binding** — the handler, attached inside a pip's `boot` where the
21
+ pip's typed needs are already injected. Handlers close over real
22
+ services; no service locator exists.
23
+
24
+ ```ts
25
+ import { route, routes } from '@arki/http';
26
+ import { z } from 'zod';
27
+
28
+ export const listOrders = route.get('/orders', {
29
+ id: 'orders.list',
30
+ query: z.object({ status: z.enum(['open', 'shipped']).optional() }),
31
+ output: z.array(Order),
32
+ });
33
+
34
+ export const createOrder = route.post('/orders', {
35
+ id: 'orders.create',
36
+ body: CreateOrder,
37
+ output: Order,
38
+ });
39
+ ```
40
+
41
+ Input validation (query/body) happens before your handler runs; output is
42
+ validated against the contract on the way out. Failures serialize as a
43
+ coded envelope: `{ "error": { "code", "message", "remediation?", "docsUrl?" } }`.
44
+
45
+ ## Inside a DOT app
46
+
47
+ ```ts
48
+ import { pip, provide, service, token } from '@arki/dot/pip';
49
+ import { routes, type RouteBundle } from '@arki/http';
50
+ import { registerRoutes } from '@arki/http/dot';
51
+
52
+ export const OrdersRoutes = token<RouteBundle>()('orders.routes');
53
+
54
+ export const ordersPip = pip({
55
+ name: 'orders',
56
+ needs: { db: service<Db>() },
57
+ configure(ctx) {
58
+ registerRoutes(ctx, [listOrders, createOrder]); // manifest + OpenAPI metadata
59
+ },
60
+ async boot({ db }) {
61
+ return provide(
62
+ OrdersRoutes,
63
+ routes()
64
+ .bind(listOrders, async ({ query }) => db.orders.list(query))
65
+ .bind(createOrder, async ({ body }, ctx) => db.orders.create(body, ctx.requestId)),
66
+ );
67
+ },
68
+ });
69
+ ```
70
+
71
+ Mount the `http()` pip **last**, naming the bundles it serves:
72
+
73
+ ```ts
74
+ import { defineApp, hookSignals } from '@arki/dot';
75
+ import { http } from '@arki/http/dot';
76
+
77
+ const app = await defineApp('shop-api')
78
+ .use(db({ url: DATABASE_URL }))
79
+ .use(ordersPip)
80
+ .use(http({ port: 3000, bundles: [OrdersRoutes] }))
81
+ .start();
82
+
83
+ hookSignals(app);
84
+ ```
85
+
86
+ `http()` derives its `needs` from the tokens you pass — forget to mount
87
+ `ordersPip` and the `.use(http(...))` line fails to typecheck. Because the
88
+ pip is declared last, reverse-order `stop` halts ingress FIRST: the server
89
+ stops accepting, drains in-flight requests (`drainTimeoutMs`, default
90
+ 10 s, `ARKI_HTTP_E005` on overrun), and only then do feature pips tear
91
+ down. `boot` builds the router without listening; `start` listens;
92
+ `dispose` severs remaining connections and releases the port.
93
+
94
+ The published `httpServer` service carries the composed
95
+ `fetch: (Request) => Promise<Response>` — also the unit-test seam: call
96
+ your whole app without a socket.
97
+
98
+ ## Request scope — derivers, not containers
99
+
100
+ Per-request values are typed context keys derived by plain functions.
101
+ Derivers run in declaration order; each adds a key later handlers (and
102
+ later derivers) see typed. A deriver that throws `HttpError` is the whole
103
+ "guard" story:
104
+
105
+ ```ts
106
+ import { derive, HttpError, HTTP_ERROR_CODES } from '@arki/http';
107
+
108
+ // inside boot — `auth` is an injected singleton
109
+ const withPrincipal = derive('principal', async req => {
110
+ const p = await auth.verify(req.headers.get('authorization'));
111
+ if (!p) throw new HttpError(401, HTTP_ERROR_CODES.unauthorized, 'missing or invalid credentials');
112
+ return p;
113
+ });
114
+
115
+ routes()
116
+ .derive(withPrincipal)
117
+ .derive('db', (_req, ctx) => dbForTenant(ctx.principal.tenantId))
118
+ .bind(listOrders, async (_input, ctx) => ctx.db.orders.list());
119
+ ```
120
+
121
+ Request scope is function application over the compile-time-wired
122
+ singleton graph — no per-request container resolution, no infectious
123
+ scopes. For code deep in a call stack you don't control, the fenced-off
124
+ `@arki/http/context` entry exposes `requestContext()` (AsyncLocalStorage).
125
+ It is a last resort; prefer the explicit `ctx`.
126
+
127
+ ## App-wide middleware
128
+
129
+ For cross-cutting concerns that must see or touch the raw
130
+ request/response — CORS, request logging — pass fetch-shaped middleware
131
+ to `http()` (first = outermost). A middleware that throws `HttpError`
132
+ short-circuits into the coded envelope:
133
+
134
+ ```ts
135
+ http({ port: 3000, bundles: [OrdersRoutes], middleware: [requestLog(logger), cors(origins)] })
136
+ ```
137
+
138
+ Middleware wraps the whole engine and is untyped by design; anything that
139
+ produces a per-request *value* belongs in a deriver instead. Standalone
140
+ users compose the same way with `composeMiddleware(engine.fetch, [...])`.
141
+
142
+ ## Streaming
143
+
144
+ **Typed SSE.** `route.sse` declares a `text/event-stream` contract; the
145
+ binding is an async generator. Every yield is validated against the event
146
+ schema and framed as a `data:` message; client disconnect aborts
147
+ `ctx.signal` and runs the generator's `finally`:
148
+
149
+ ```ts
150
+ const progress = route.sse('/orders/:id/progress', {
151
+ id: 'orders.progress',
152
+ event: z.discriminatedUnion('type', [Queued, Shipped]),
153
+ heartbeatMs: 15_000, // optional :keepalive comments
154
+ });
155
+
156
+ routes().bind(progress, async function* ({ params }, ctx) {
157
+ for await (const step of tracker.watch(params.id, ctx.signal)) {
158
+ yield step;
159
+ }
160
+ });
161
+ ```
162
+
163
+ **Raw streams.** Any handler may return a `Response` wrapping a
164
+ `ReadableStream` — file downloads, AI token streams. The engine is
165
+ fetch-native end to end.
166
+
167
+ ## Mounting foreign handlers
168
+
169
+ An oRPC router, a tRPC fetch adapter, or any fetch-shaped handler mounts
170
+ as one manifest route — the migration on-ramp for existing backends:
171
+
172
+ ```ts
173
+ routes().mount('/rpc', rpcHandler, { id: 'rpc', transport: 'orpc' });
174
+ ```
175
+
176
+ ## OpenAPI
177
+
178
+ ```ts
179
+ import { toOpenApi } from '@arki/http';
180
+
181
+ const document = toOpenApi([listOrders, createOrder, progress], {
182
+ title: 'shop-api',
183
+ version: '1.0.0',
184
+ });
185
+ ```
186
+
187
+ Deterministic — same contracts, byte-identical output. Inside a DOT app,
188
+ `registerRoutes` puts the JSON Schemas into the manifest, so
189
+ `dot explain --openapi` renders the document from the static manifest
190
+ without booting anything.
191
+
192
+ ## Without DOT
193
+
194
+ The core is framework-agnostic:
195
+
196
+ ```ts
197
+ import { buildEngine, listen, routes } from '@arki/http';
198
+
199
+ const engine = buildEngine([bundle]);
200
+ const handle = await listen(engine.fetch, { port: 3000 });
201
+ ```
202
+
203
+ `listen` uses `Bun.serve` under Bun and `@hono/node-server` under Node.
204
+ Routing is Hono internally; no Hono type appears in the public API.
205
+
206
+ ## Error codes
207
+
208
+ Stable API — match on codes, never parse messages.
209
+
210
+ | Code | Meaning |
211
+ | --- | --- |
212
+ | `ARKI_HTTP_E001` | `start` could not bind the port |
213
+ | `ARKI_HTTP_E002` | duplicate route: two bindings claim the same method+path (or a bundle token is listed twice) |
214
+ | `ARKI_HTTP_E005` | `stop` exceeded `drainTimeoutMs` with requests in flight |
215
+ | `ARKI_HTTP_E006` | malformed mount path |
216
+ | `ARKI_HTTP_E007` | deriver key duplicates an earlier deriver or a base context key |
217
+ | `ARKI_HTTP_E008` | a bundle token's service was missing at boot (erased composition only) |
218
+ | `ARKI_HTTP_E400` | request validation failed (query/body) or body is not valid JSON |
219
+ | `ARKI_HTTP_E401` / `E403` | credential / permission rejection (thrown by your derivers) |
220
+ | `ARKI_HTTP_E404` | no route matches |
221
+ | `ARKI_HTTP_E500` | handler crash or output-schema violation |
222
+
223
+ `E0NN` codes surface through the DOT lifecycle into `app.diagnostics`;
224
+ `E4xx`/`E5xx` codes surface on the wire in the error envelope. Unexpected
225
+ error messages are redacted when `NODE_ENV=production`.
226
+
227
+ ## License
228
+
229
+ MIT
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Route bundles — the dynamic half of an HTTP surface.
3
+ *
4
+ * A bundle binds contracts to handlers. It is assembled inside a pip's
5
+ * `boot` hook — where the pip's `needs` are already injected — so handlers
6
+ * and derivers close over typed singletons:
7
+ *
8
+ * ```ts
9
+ * export const OrdersRoutes = token<RouteBundle>()('orders.routes');
10
+ *
11
+ * export const ordersPip = pip({
12
+ * name: 'orders',
13
+ * needs: { db: service<Db>() },
14
+ * configure(ctx) {
15
+ * registerRoutes(ctx, [listOrders, createOrder]); // from '@arki/http/dot'
16
+ * },
17
+ * async boot({ db }) {
18
+ * return provide(
19
+ * OrdersRoutes,
20
+ * routes()
21
+ * .bind(listOrders, async ({ query }) => db.orders.list(query))
22
+ * .bind(createOrder, async ({ body }, ctx) => db.orders.create(body, ctx.requestId)),
23
+ * );
24
+ * },
25
+ * });
26
+ * ```
27
+ *
28
+ * The builder accumulates deriver-added context keys in its type — a
29
+ * handler bound after `.derive('principal', …)` sees `ctx.principal`
30
+ * typed. Same accumulation idiom as `defineApp().use()`.
31
+ */
32
+ import type { ContractLike, ParamsOf, RouteContract, SseContract } from './contract.js';
33
+ import type { BaseRequestCtx, Deriver, ErasedDeriver } from './derive.js';
34
+ type MaybePromise<T> = T | Promise<T>;
35
+ /** The validated inputs a bound handler receives. */
36
+ export type RouteInput<TPath extends string, TQuery, TBody> = {
37
+ readonly params: ParamsOf<TPath>;
38
+ readonly query: TQuery;
39
+ readonly body: TBody;
40
+ };
41
+ /**
42
+ * What a JSON handler may return: the contract's output type (serialized
43
+ * and validated), or a raw `Response` as the escape hatch — redirects,
44
+ * files, custom streams. Contracts without an output schema must return
45
+ * a `Response`.
46
+ */
47
+ export type HandlerResult<TOutput> = [TOutput] extends [undefined] ? Response : TOutput | Response;
48
+ /** Handler signature inferred from a {@link RouteContract}. */
49
+ export type RouteHandler<TPath extends string, TQuery, TBody, TOutput, TCtx extends BaseRequestCtx> = (input: RouteInput<TPath, TQuery, TBody>, ctx: TCtx) => MaybePromise<HandlerResult<TOutput>>;
50
+ /**
51
+ * Handler signature inferred from an {@link SseContract}: an async
52
+ * iterable (usually an async generator) of events. Each yielded value is
53
+ * validated against the contract's event schema and framed as an SSE
54
+ * `data:` message. Client disconnect aborts `ctx.signal` and terminates
55
+ * the iterator.
56
+ */
57
+ export type SseHandler<TPath extends string, TQuery, TEvent, TCtx extends BaseRequestCtx> = (input: RouteInput<TPath, TQuery, undefined>, ctx: TCtx) => AsyncIterable<TEvent>;
58
+ /** Non-HTTP transports a {@link RouteBundleBuilder.mount} can declare. */
59
+ export type MountTransport = 'orpc' | 'trpc' | 'rpc' | 'custom';
60
+ export type MountMeta = {
61
+ /** Stable identifier — lands in the manifest like a contract id. */
62
+ readonly id: string;
63
+ /** Manifest transport tag. Defaults to `'custom'`. */
64
+ readonly transport?: MountTransport;
65
+ };
66
+ /** A contract bound to its (generics-erased) handler. */
67
+ export type RouteBinding = {
68
+ readonly contract: ContractLike;
69
+ /**
70
+ * Stored erased (`never` parameters) — typed handlers are assignable
71
+ * via parameter contravariance from the bottom type; the engine crosses
72
+ * the erasure boundary at the call site. Mirrors the DOT kernel's
73
+ * hook-storage pattern.
74
+ */
75
+ readonly handler: (input: never, ctx: never) => unknown;
76
+ };
77
+ /** A mounted foreign fetch handler (oRPC/tRPC router, static files, …). */
78
+ export type MountBinding = {
79
+ readonly id: string;
80
+ readonly path: string;
81
+ readonly transport: MountTransport;
82
+ readonly handler: (req: Request) => Response | Promise<Response>;
83
+ };
84
+ /**
85
+ * A plain value: derivers + bound contracts + mounts. Published as an
86
+ * ordinary DOT service under a token and collected by the `http()` pip —
87
+ * no new kernel concept.
88
+ */
89
+ export type RouteBundle = {
90
+ readonly derivers: readonly ErasedDeriver[];
91
+ readonly bindings: readonly RouteBinding[];
92
+ readonly mounts: readonly MountBinding[];
93
+ };
94
+ /**
95
+ * Immutable bundle builder. Every method returns a new builder; deriver
96
+ * keys accumulate in `TCtx` so later `bind` handlers see them typed.
97
+ */
98
+ export type RouteBundleBuilder<TCtx extends BaseRequestCtx = BaseRequestCtx> = RouteBundle & {
99
+ /** Add a reusable deriver (see {@link Deriver}). */
100
+ derive<K extends string, V>(deriver: Deriver<K, V, TCtx>): RouteBundleBuilder<TCtx & Readonly<Record<K, V>>>;
101
+ /** Add an inline deriver. */
102
+ derive<K extends string, V>(key: K, fn: (req: Request, ctx: TCtx) => V | Promise<V>): RouteBundleBuilder<TCtx & Readonly<Record<K, V>>>;
103
+ /**
104
+ * Bind a JSON contract to its handler. `NoInfer` pins the input/output
105
+ * generics to the contract — without it, a handler returning the wrong
106
+ * shape would silently widen `TOutput` instead of failing to compile.
107
+ */
108
+ bind<TPath extends string, TQuery, TBody, TOutput>(contract: RouteContract<TPath, TQuery, TBody, TOutput>, handler: RouteHandler<TPath, NoInfer<TQuery>, NoInfer<TBody>, NoInfer<TOutput>, TCtx>): RouteBundleBuilder<TCtx>;
109
+ /** Bind an SSE contract to its event generator. */
110
+ bind<TPath extends string, TQuery, TEvent>(contract: SseContract<TPath, TQuery, TEvent>, handler: SseHandler<TPath, NoInfer<TQuery>, NoInfer<TEvent>, TCtx>): RouteBundleBuilder<TCtx>;
111
+ /**
112
+ * Mount a foreign fetch handler under a path prefix — an oRPC router, a
113
+ * tRPC fetch adapter, a static-file handler. One manifest route, no
114
+ * schemas, derivers do NOT run for mounted handlers.
115
+ */
116
+ mount(path: string, handler: (req: Request) => Response | Promise<Response>, meta: MountMeta): RouteBundleBuilder<TCtx>;
117
+ };
118
+ /** Start an empty bundle. See the module docs for the authoring pattern. */
119
+ export declare function routes(): RouteBundleBuilder;
120
+ export {};
121
+ //# sourceMappingURL=bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"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"}
package/dist/bundle.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Route bundles — the dynamic half of an HTTP surface.
3
+ *
4
+ * A bundle binds contracts to handlers. It is assembled inside a pip's
5
+ * `boot` hook — where the pip's `needs` are already injected — so handlers
6
+ * and derivers close over typed singletons:
7
+ *
8
+ * ```ts
9
+ * export const OrdersRoutes = token<RouteBundle>()('orders.routes');
10
+ *
11
+ * export const ordersPip = pip({
12
+ * name: 'orders',
13
+ * needs: { db: service<Db>() },
14
+ * configure(ctx) {
15
+ * registerRoutes(ctx, [listOrders, createOrder]); // from '@arki/http/dot'
16
+ * },
17
+ * async boot({ db }) {
18
+ * return provide(
19
+ * OrdersRoutes,
20
+ * routes()
21
+ * .bind(listOrders, async ({ query }) => db.orders.list(query))
22
+ * .bind(createOrder, async ({ body }, ctx) => db.orders.create(body, ctx.requestId)),
23
+ * );
24
+ * },
25
+ * });
26
+ * ```
27
+ *
28
+ * The builder accumulates deriver-added context keys in its type — a
29
+ * handler bound after `.derive('principal', …)` sees `ctx.principal`
30
+ * typed. Same accumulation idiom as `defineApp().use()`.
31
+ */
32
+ import { BASE_CTX_KEYS } from './derive.js';
33
+ import { HTTP_ERROR_CODES, HttpConfigError } from './error.js';
34
+ function assertFreshDeriverKey(key, existing) {
35
+ const reserved = BASE_CTX_KEYS.includes(key);
36
+ const duplicate = existing.some(d => d.key === key);
37
+ if (!reserved && !duplicate)
38
+ return;
39
+ throw new HttpConfigError({
40
+ code: HTTP_ERROR_CODES.duplicateDeriverKey,
41
+ message: reserved
42
+ ? `[http] deriver key "${key}" shadows a base request-context key.`
43
+ : `[http] deriver key "${key}" is already derived in this bundle.`,
44
+ remediation: `Pick a different key — base keys (${BASE_CTX_KEYS.join(', ')}) and earlier deriver keys are reserved.`,
45
+ });
46
+ }
47
+ function makeBuilder(state) {
48
+ const impl = {
49
+ derivers: state.derivers,
50
+ bindings: state.bindings,
51
+ mounts: state.mounts,
52
+ derive(keyOrDeriver, fn) {
53
+ let deriver;
54
+ if (typeof keyOrDeriver === 'string') {
55
+ if (fn === undefined) {
56
+ throw new HttpConfigError({
57
+ code: HTTP_ERROR_CODES.duplicateDeriverKey,
58
+ message: `[http] derive("${keyOrDeriver}") is missing its deriver function.`,
59
+ remediation: 'Pass derive(key, fn) or derive(deriverObject).',
60
+ });
61
+ }
62
+ deriver = { key: keyOrDeriver, derive: fn };
63
+ }
64
+ else {
65
+ deriver = keyOrDeriver;
66
+ }
67
+ assertFreshDeriverKey(deriver.key, state.derivers);
68
+ return makeBuilder({ ...state, derivers: [...state.derivers, deriver] });
69
+ },
70
+ bind(contract, handler) {
71
+ return makeBuilder({ ...state, bindings: [...state.bindings, { contract, handler }] });
72
+ },
73
+ mount(path, handler, meta) {
74
+ if (!path.startsWith('/') || path.includes(' ')) {
75
+ throw new HttpConfigError({
76
+ code: HTTP_ERROR_CODES.invalidMount,
77
+ message: `[http] mount path "${path}" is invalid — it must start with "/" and contain no spaces.`,
78
+ remediation: 'Mount under an absolute path prefix, e.g. mount("/rpc", handler, { id: "rpc" }).',
79
+ });
80
+ }
81
+ return makeBuilder({
82
+ ...state,
83
+ mounts: [...state.mounts, { id: meta.id, path, transport: meta.transport ?? 'custom', handler }],
84
+ });
85
+ },
86
+ };
87
+ // Erasure seam: the implementation works on erased contracts/handlers/
88
+ // derivers; the public builder type re-attaches the generics. The typed
89
+ // methods' arguments are structurally assignable to the erased
90
+ // parameters, so the only unsafe step is this widening of the return
91
+ // type — same seam as the DOT app builder's makeBuilder.
92
+ return impl;
93
+ }
94
+ /** Start an empty bundle. See the module docs for the authoring pattern. */
95
+ export function routes() {
96
+ return makeBuilder({ derivers: [], bindings: [], mounts: [] });
97
+ }
98
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Ambient request context — the last resort.
3
+ *
4
+ * Explicit `ctx` threading fails in one honest case: code deep in a call
5
+ * stack whose signatures you don't control (log enrichment inside a
6
+ * shared library). For that, this entry exposes the current request's
7
+ * {@link BaseRequestCtx} via `AsyncLocalStorage`.
8
+ *
9
+ * Ambient context is a smell — prefer the explicit `ctx` parameter and
10
+ * derivers everywhere a signature is yours to change. Note that OTel
11
+ * trace context already propagates this way, so span/log correlation
12
+ * needs none of this.
13
+ *
14
+ * This module is deliberately NOT re-exported from the package root.
15
+ * Import it as `@arki/http/context`.
16
+ */
17
+ import type { BaseRequestCtx } from './derive.js';
18
+ /**
19
+ * The context of the request currently being handled, or `undefined`
20
+ * outside a request (startup, background jobs, tests that call handlers
21
+ * directly).
22
+ */
23
+ export declare function requestContext(): BaseRequestCtx | undefined;
24
+ /**
25
+ * Engine seam: run `fn` with `ctx` as the ambient request context. Called
26
+ * by the engine around every request — not part of the public surface.
27
+ */
28
+ export declare function runWithRequestContext<T>(ctx: BaseRequestCtx, fn: () => T): T;
29
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,cAAc,GAAG,SAAS,CAE3D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE5E"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Ambient request context — the last resort.
3
+ *
4
+ * Explicit `ctx` threading fails in one honest case: code deep in a call
5
+ * stack whose signatures you don't control (log enrichment inside a
6
+ * shared library). For that, this entry exposes the current request's
7
+ * {@link BaseRequestCtx} via `AsyncLocalStorage`.
8
+ *
9
+ * Ambient context is a smell — prefer the explicit `ctx` parameter and
10
+ * derivers everywhere a signature is yours to change. Note that OTel
11
+ * trace context already propagates this way, so span/log correlation
12
+ * needs none of this.
13
+ *
14
+ * This module is deliberately NOT re-exported from the package root.
15
+ * Import it as `@arki/http/context`.
16
+ */
17
+ import { AsyncLocalStorage } from 'node:async_hooks';
18
+ const storage = new AsyncLocalStorage();
19
+ /**
20
+ * The context of the request currently being handled, or `undefined`
21
+ * outside a request (startup, background jobs, tests that call handlers
22
+ * directly).
23
+ */
24
+ export function requestContext() {
25
+ return storage.getStore();
26
+ }
27
+ /**
28
+ * Engine seam: run `fn` with `ctx` as the ambient request context. Called
29
+ * by the engine around every request — not part of the public surface.
30
+ */
31
+ export function runWithRequestContext(ctx, fn) {
32
+ return storage.run(ctx, fn);
33
+ }
34
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAExD;;;;GAIG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAI,GAAmB,EAAE,EAAW;IACvE,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC9B,CAAC"}