@forinda/kickjs 5.17.0 → 5.18.0-alpha.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.
- package/dist/adapter-D_uzU-QA.d.mts +2121 -0
- package/dist/adapter-D_uzU-QA.d.mts.map +1 -0
- package/dist/{context-decorator-DDZ30BIW.d.mts → app-module-CuAS6hJC.d.mts} +184 -4
- package/dist/app-module-CuAS6hJC.d.mts.map +1 -0
- package/dist/application-DwTrT9cq.mjs +12 -0
- package/dist/application-DwTrT9cq.mjs.map +1 -0
- package/dist/{application-CAnpDQ8d.d.mts → application-pbx53JnX.d.mts} +31 -5
- package/dist/application-pbx53JnX.d.mts.map +1 -0
- package/dist/{bootstrap-CHmCh_cE.mjs → bootstrap-BaqYQe4i.mjs} +3 -3
- package/dist/{bootstrap-CHmCh_cE.mjs.map → bootstrap-BaqYQe4i.mjs.map} +1 -1
- package/dist/{bootstrap-8DDzBjhx.d.mts → bootstrap-DzfBj6Ja.d.mts} +2 -2
- package/dist/{bootstrap-8DDzBjhx.d.mts.map → bootstrap-DzfBj6Ja.d.mts.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-BvyPboRZ.mjs → config-okfM1SlN.mjs} +3 -3
- package/dist/{config-BvyPboRZ.mjs.map → config-okfM1SlN.mjs.map} +1 -1
- package/dist/{container-CPz0l8-b.mjs → container-B0BU5TY4.mjs} +3 -3
- package/dist/{container-CPz0l8-b.mjs.map → container-B0BU5TY4.mjs.map} +1 -1
- package/dist/context-DGsCYQRP.mjs +12 -0
- package/dist/context-DGsCYQRP.mjs.map +1 -0
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.d.mts +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/errors.mjs +2 -2
- package/dist/core/errors.mjs.map +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/plugin.d.mts +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +1 -1
- package/dist/{core-C_gfh7JQ.mjs → core-BV0AxVuK.mjs} +3 -3
- package/dist/{core-C_gfh7JQ.mjs.map → core-BV0AxVuK.mjs.map} +1 -1
- package/dist/{env-LIrKUSAJ.mjs → env-wT1GSzGW.mjs} +3 -3
- package/dist/{env-LIrKUSAJ.mjs.map → env-wT1GSzGW.mjs.map} +1 -1
- package/dist/{errors-CRCTjEUK.d.mts → errors-n0H96eEz.d.mts} +3 -1
- package/dist/errors-n0H96eEz.d.mts.map +1 -0
- package/dist/http/application.d.mts +1 -1
- package/dist/http/application.mjs +2 -2
- package/dist/http/bootstrap.d.mts +1 -1
- package/dist/http/bootstrap.mjs +2 -2
- package/dist/http/context.d.mts +2 -2
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +1 -1
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +1 -1
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/session.mjs +1 -1
- package/dist/http/middleware/spa.d.mts +1 -1
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/upload.d.mts +2 -2
- package/dist/http/middleware/upload.mjs +3 -3
- package/dist/http/middleware/upload.mjs.map +1 -1
- package/dist/http/middleware/validate.mjs +2 -2
- package/dist/http/middleware/views.d.mts +1 -1
- package/dist/http/middleware/views.mjs +2 -2
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.d.mts +2 -2
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/router-builder.mjs.map +1 -1
- package/dist/http/runtimes/fastify.d.mts +66 -0
- package/dist/http/runtimes/fastify.d.mts.map +1 -0
- package/dist/http/runtimes/fastify.mjs +13 -0
- package/dist/http/runtimes/fastify.mjs.map +1 -0
- package/dist/http/runtimes/h3.d.mts +29 -0
- package/dist/http/runtimes/h3.d.mts.map +1 -0
- package/dist/http/runtimes/h3.mjs +12 -0
- package/dist/http/runtimes/h3.mjs.map +1 -0
- package/dist/index.d.mts +11 -16
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{metadata-D8hSFaux.mjs → metadata-B069pGq2.mjs} +2 -2
- package/dist/{metadata-D8hSFaux.mjs.map → metadata-B069pGq2.mjs.map} +1 -1
- package/dist/{query-NhwucLHS.mjs → query-CimrAth7.mjs} +2 -2
- package/dist/{query-NhwucLHS.mjs.map → query-CimrAth7.mjs.map} +1 -1
- package/dist/request-scope-DiBal5Ln.mjs +12 -0
- package/dist/request-scope-DiBal5Ln.mjs.map +1 -0
- package/dist/router-builder-Bh3qSiDz.d.mts +39 -0
- package/dist/router-builder-Bh3qSiDz.d.mts.map +1 -0
- package/dist/{spa-IRFC2eMN.d.mts → spa-BdvZFXJz.d.mts} +3 -2
- package/dist/spa-BdvZFXJz.d.mts.map +1 -0
- package/dist/{upload-D6edAELH.d.mts → upload-Bc_rBRtE.d.mts} +37 -2
- package/dist/upload-Bc_rBRtE.d.mts.map +1 -0
- package/dist/{views-DkllbVB3.d.mts → views-DcTVDiOh.d.mts} +31 -7
- package/dist/views-DcTVDiOh.d.mts.map +1 -0
- package/package.json +35 -1
- package/dist/adapter-CkpcZwzZ.d.mts +0 -407
- package/dist/adapter-CkpcZwzZ.d.mts.map +0 -1
- package/dist/app-module-BTzb-Vd6.d.mts +0 -92
- package/dist/app-module-BTzb-Vd6.d.mts.map +0 -1
- package/dist/application-CAnpDQ8d.d.mts.map +0 -1
- package/dist/application-CRmMytW1.mjs +0 -12
- package/dist/application-CRmMytW1.mjs.map +0 -1
- package/dist/context-C6e-S0uc.mjs +0 -12
- package/dist/context-C6e-S0uc.mjs.map +0 -1
- package/dist/context-DV7RjM9F.d.mts +0 -445
- package/dist/context-DV7RjM9F.d.mts.map +0 -1
- package/dist/context-decorator-DDZ30BIW.d.mts.map +0 -1
- package/dist/errors-CRCTjEUK.d.mts.map +0 -1
- package/dist/execution-context-B4cfhivz.d.mts +0 -98
- package/dist/execution-context-B4cfhivz.d.mts.map +0 -1
- package/dist/index-frSEHiL_.d.mts +0 -796
- package/dist/index-frSEHiL_.d.mts.map +0 -1
- package/dist/plugin-BQCn6zyj.d.mts +0 -260
- package/dist/plugin-BQCn6zyj.d.mts.map +0 -1
- package/dist/router-builder-oQpcQMWl.d.mts +0 -31
- package/dist/router-builder-oQpcQMWl.d.mts.map +0 -1
- package/dist/spa-IRFC2eMN.d.mts.map +0 -1
- package/dist/upload-D6edAELH.d.mts.map +0 -1
- package/dist/views-DkllbVB3.d.mts.map +0 -1
|
@@ -0,0 +1,2121 @@
|
|
|
1
|
+
|
|
2
|
+
import { i as Constructor, o as MaybePromise } from "./interfaces-DbTyKGdS.mjs";
|
|
3
|
+
import { s as InjectionToken, t as Container } from "./container-TWOUyR_0.mjs";
|
|
4
|
+
import { O as RouteDefinition, g as FileUploadConfig, x as MiddlewareHandler } from "./decorators-ejABLo9H.mjs";
|
|
5
|
+
import { _ as ExecutionContext, a as AnyContributorRegistration, d as ContributorRegistrations, r as AppModuleEntry, t as AppModule, v as MetaValue } from "./app-module-CuAS6hJC.mjs";
|
|
6
|
+
import { i as ProblemDetails } from "./errors-n0H96eEz.mjs";
|
|
7
|
+
import { S as QueryFieldConfig, T as TypedParsedQuery, t as ParseQueryOptions } from "./index-BtWJ9SLP.mjs";
|
|
8
|
+
import http, { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
+
import { ErrorRequestHandler, Express as Express$1, NextFunction, Request, RequestHandler, Response } from "express";
|
|
10
|
+
|
|
11
|
+
//#region src/core/introspect.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* Per-primitive introspection snapshot — the JSON shape an adapter or
|
|
14
|
+
* plugin's optional `introspect()` hook returns for DevTools.
|
|
15
|
+
*
|
|
16
|
+
* Lives in `@forinda/kickjs` (not `@forinda/kickjs-devtools-kit`) so
|
|
17
|
+
* adopters can type their `introspect()` return without taking on a
|
|
18
|
+
* dep on the kit just to satisfy the contract. The kit's own
|
|
19
|
+
* `IntrospectionSnapshot` type is structurally identical and continues
|
|
20
|
+
* to be the canonical doc location for DevTools-side consumers; the
|
|
21
|
+
* two stay in lockstep — if a field is added on one side it MUST be
|
|
22
|
+
* added on the other (covered by the type-equivalence test in the
|
|
23
|
+
* devtools-kit package).
|
|
24
|
+
*
|
|
25
|
+
* Cheap-by-default principle: `state` and `metrics` should be O(1) to
|
|
26
|
+
* compute (counters, flags, recent samples). Anything that requires a
|
|
27
|
+
* DB round trip or a full DI graph walk belongs behind a separate
|
|
28
|
+
* explicit RPC — DevTools polls topology aggressively.
|
|
29
|
+
*/
|
|
30
|
+
interface IntrospectionSnapshot {
|
|
31
|
+
/**
|
|
32
|
+
* Wire-protocol version this snapshot conforms to. The kit ships a
|
|
33
|
+
* `PROTOCOL_VERSION` constant typed as the literal `1`; we mirror
|
|
34
|
+
* that literal here so structural parity is enforced both ways —
|
|
35
|
+
* an adopter who writes `protocolVersion: 1` satisfies both this
|
|
36
|
+
* type and the kit's `IntrospectionSnapshot`. Bumping is a
|
|
37
|
+
* coordinated kit + framework release; widen this literal in
|
|
38
|
+
* lockstep.
|
|
39
|
+
*/
|
|
40
|
+
protocolVersion: 1;
|
|
41
|
+
/** Stable identity matching the `name` from `definePlugin` / `defineAdapter`. */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Discriminator for what kind of primitive this is. */
|
|
44
|
+
kind: IntrospectionKind;
|
|
45
|
+
/** Optional version string from the plugin / adapter metadata. */
|
|
46
|
+
version?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Reactive state surface — JSON-serialisable snapshot. Keep small
|
|
49
|
+
* (under a few KB) so polling stays cheap.
|
|
50
|
+
*/
|
|
51
|
+
state?: Record<string, unknown>;
|
|
52
|
+
/** DI tokens this primitive registers (`provides`) or consumes (`requires`). */
|
|
53
|
+
tokens?: {
|
|
54
|
+
provides: readonly string[];
|
|
55
|
+
requires: readonly string[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Per-instance counters — active connections, in-flight jobs, cached
|
|
59
|
+
* items, etc. Numbers only so the DevTools panel can chart trends.
|
|
60
|
+
*/
|
|
61
|
+
metrics?: Record<string, number>;
|
|
62
|
+
/** Self-reported memory footprint estimate, in bytes. Optional. */
|
|
63
|
+
memoryBytes?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* What kind of primitive an introspection snapshot describes. Matches
|
|
67
|
+
* the kit's `IntrospectionKind` and the routing the topology aggregator
|
|
68
|
+
* does when bucketing adapters vs plugins vs middleware vs contributor
|
|
69
|
+
* primitives.
|
|
70
|
+
*/
|
|
71
|
+
type IntrospectionKind = 'plugin' | 'adapter' | 'middleware' | 'contributor';
|
|
72
|
+
/**
|
|
73
|
+
* Convenience function signature for adopter-side helpers that return
|
|
74
|
+
* a complete snapshot. Used by the optional `introspect?()` hook on
|
|
75
|
+
* `AppAdapter` and `KickPlugin`.
|
|
76
|
+
*/
|
|
77
|
+
type IntrospectFn = () => IntrospectionSnapshot | Promise<IntrospectionSnapshot>;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/core/augmentation.d.ts
|
|
80
|
+
/**
|
|
81
|
+
* Augmentation registry — runtime no-op + the canonical
|
|
82
|
+
* `KickJsPluginRegistry` interface that `kick typegen` augments.
|
|
83
|
+
*
|
|
84
|
+
* See `architecture.md` §21.2.1 (typegen for `dependsOn`) and §21.3.3
|
|
85
|
+
* (standardized augmentation registry) for the design rationale.
|
|
86
|
+
*
|
|
87
|
+
* @module @forinda/kickjs/core/augmentation
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* Marker interface that lists every plugin/adapter in the project. The
|
|
91
|
+
* keys are the literal `name` field passed to `defineAdapter` /
|
|
92
|
+
* `definePlugin` (or declared on a `class implements AppAdapter`); the
|
|
93
|
+
* values are the kind tag (`'plugin' | 'adapter'`).
|
|
94
|
+
*
|
|
95
|
+
* The framework ships this empty. `kick typegen` augments it from
|
|
96
|
+
* source so `keyof KickJsPluginRegistry` resolves to a string-literal
|
|
97
|
+
* union of every name in scope — and therefore so do `dependsOn`
|
|
98
|
+
* declarations on plugins/adapters.
|
|
99
|
+
*/
|
|
100
|
+
interface KickJsPluginRegistry {}
|
|
101
|
+
/**
|
|
102
|
+
* Resolves to a string-literal union of every registered plugin/adapter
|
|
103
|
+
* name when the project's typegen has populated `KickJsPluginRegistry`,
|
|
104
|
+
* or to `string` when the registry is empty (no typegen pass yet).
|
|
105
|
+
*
|
|
106
|
+
* Falling back to `string` keeps fresh projects compiling — the typegen
|
|
107
|
+
* narrowing is a progressive enhancement, not a hard prerequisite.
|
|
108
|
+
*/
|
|
109
|
+
type KickJsPluginName = keyof KickJsPluginRegistry extends never ? string : keyof KickJsPluginRegistry & string;
|
|
110
|
+
/**
|
|
111
|
+
* Metadata attached to a `defineAugmentation` call. The fields are
|
|
112
|
+
* documentation-only — `kick typegen` surfaces them in the generated
|
|
113
|
+
* `.kickjs/types/augmentations.d.ts` so adopters can browse every
|
|
114
|
+
* augmentable interface from one place.
|
|
115
|
+
*
|
|
116
|
+
* `description` and `example` may both be multi-line — typegen preserves
|
|
117
|
+
* line breaks when rendering them as JSDoc. There is no upper limit on
|
|
118
|
+
* the example size; entire shape definitions or worked snippets are fine.
|
|
119
|
+
*/
|
|
120
|
+
interface AugmentationMeta {
|
|
121
|
+
/**
|
|
122
|
+
* Free-form description of what the augmentation customises. May be
|
|
123
|
+
* multi-line — newlines are preserved in the generated JSDoc.
|
|
124
|
+
*/
|
|
125
|
+
description?: string;
|
|
126
|
+
/**
|
|
127
|
+
* TS snippet showing a typical shape. May be multi-line; rendered
|
|
128
|
+
* inside a fenced code block in the generated catalogue. Use this
|
|
129
|
+
* for anything from `'{ foo: string }'` up to a full interface body
|
|
130
|
+
* — typegen no longer flattens newlines.
|
|
131
|
+
*/
|
|
132
|
+
example?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Advertise an augmentable interface so `kick typegen` can list it in
|
|
136
|
+
* `.kickjs/types/augmentations.d.ts` for project-wide discovery.
|
|
137
|
+
*
|
|
138
|
+
* **This function is a runtime AND type-level no-op.** It does NOT
|
|
139
|
+
* augment the interface for TypeScript. The actual augmentation is
|
|
140
|
+
* the `declare module` block — `defineAugmentation` only adds an
|
|
141
|
+
* entry to the typegen catalogue so other contributors / adopters
|
|
142
|
+
* can find the interface and know its expected shape.
|
|
143
|
+
*
|
|
144
|
+
* Both calls are needed:
|
|
145
|
+
*
|
|
146
|
+
* ```ts
|
|
147
|
+
* import { defineAugmentation } from '@forinda/kickjs'
|
|
148
|
+
*
|
|
149
|
+
* // (1) The actual TS augmentation — what `ctx.get('tenant')` reads.
|
|
150
|
+
* declare module '@forinda/kickjs' {
|
|
151
|
+
* interface ContextMeta {
|
|
152
|
+
* tenant: { id: string; name: string }
|
|
153
|
+
* }
|
|
154
|
+
* }
|
|
155
|
+
*
|
|
156
|
+
* // (2) Catalogue entry — what `kick typegen` lists in
|
|
157
|
+
* // `.kickjs/types/augmentations.d.ts` for browseability.
|
|
158
|
+
* defineAugmentation('ContextMeta', {
|
|
159
|
+
* description: 'Tenant resolved from x-tenant-id by TenantAdapter',
|
|
160
|
+
* example: `{
|
|
161
|
+
* tenant: {
|
|
162
|
+
* id: string
|
|
163
|
+
* name: string
|
|
164
|
+
* plan: 'free' | 'pro' | 'enterprise'
|
|
165
|
+
* featureFlags: Record<string, boolean>
|
|
166
|
+
* }
|
|
167
|
+
* }`,
|
|
168
|
+
* })
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* Skip `defineAugmentation` if you don't care about the catalogue — the
|
|
172
|
+
* `declare module` block alone is enough for runtime + type behaviour.
|
|
173
|
+
*
|
|
174
|
+
* @deprecated Low value in practice: it does nothing at runtime or at
|
|
175
|
+
* the type level — the `declare module` block alone gives you the
|
|
176
|
+
* augmentation, and the generated `.kickjs/types/kick__augmentations.d.ts`
|
|
177
|
+
* catalogue it feeds is documentation-only. Prefer a plain
|
|
178
|
+
* `declare module '@forinda/kickjs' { ... }` block and (optionally) a
|
|
179
|
+
* JSDoc comment on your own interface. `defineAugmentation` and the
|
|
180
|
+
* `kick/augmentations` typegen plugin will be removed in a future major.
|
|
181
|
+
*/
|
|
182
|
+
declare function defineAugmentation(name: string, meta?: AugmentationMeta): void;
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/core/module-registry.d.ts
|
|
185
|
+
/**
|
|
186
|
+
* Imperative receiver passed to {@link KickPlugin.setup} and the
|
|
187
|
+
* top-level `bootstrap({ setup })` callback so adopters can register
|
|
188
|
+
* modules conditionally — based on env flags, runtime config,
|
|
189
|
+
* tenant lists, etc. — instead of being limited to the static
|
|
190
|
+
* `modules: [...]` array.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* bootstrap({
|
|
195
|
+
* modules: [HelloModule()], // static — always mounted
|
|
196
|
+
* setup(registry) {
|
|
197
|
+
* if (env.ENABLE_ADMIN) registry.mount(AdminModule())
|
|
198
|
+
* for (const tenant of env.TENANTS) {
|
|
199
|
+
* registry.mount(TenantModule.scoped(tenant.id, { id: tenant.id }))
|
|
200
|
+
* }
|
|
201
|
+
* },
|
|
202
|
+
* })
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* **Currently exposes only `.mount(module)` — the HTTP-feature path.**
|
|
206
|
+
* A future `.use(module)` for non-HTTP modules (queues, cron, workers,
|
|
207
|
+
* DI-only seeds) is planned but not yet implemented; existing non-HTTP
|
|
208
|
+
* modules continue returning `null` from `routes()` and registering
|
|
209
|
+
* via `.mount()`.
|
|
210
|
+
*
|
|
211
|
+
* Same dispatch rules as the static `modules: [...]` array — class
|
|
212
|
+
* entries get `new`-ed at boot, `defineModule` factory output is used
|
|
213
|
+
* directly. The registry doesn't validate the entry shape itself; the
|
|
214
|
+
* Application loader does the typeof discrimination at mount time.
|
|
215
|
+
*/
|
|
216
|
+
interface ModuleRegistry {
|
|
217
|
+
/**
|
|
218
|
+
* Register a module for the full HTTP lifecycle: `register()` runs,
|
|
219
|
+
* `import.meta.glob` decorator side-effects fire, `routes()` is
|
|
220
|
+
* called and its result mounted, `contributors()` are merged at
|
|
221
|
+
* `'module'` precedence and applied to the mounted routes.
|
|
222
|
+
*
|
|
223
|
+
* Modules are mounted in registration order (first call → first
|
|
224
|
+
* mount). Order across sources is: plugin static modules → plugin
|
|
225
|
+
* `setup()` calls → user static modules → user `setup()` callback.
|
|
226
|
+
*/
|
|
227
|
+
mount(module: AppModuleEntry): void;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Internal collector implementing {@link ModuleRegistry}. Application
|
|
231
|
+
* passes one of these to every `setup()` callback during bootstrap;
|
|
232
|
+
* the collected `entries` array is then run through the same
|
|
233
|
+
* class-vs-instance dispatch as the static `modules: [...]` array.
|
|
234
|
+
*
|
|
235
|
+
* Not exported from the public surface — adopters interact only
|
|
236
|
+
* through the {@link ModuleRegistry} interface.
|
|
237
|
+
*/
|
|
238
|
+
declare class MutableModuleRegistry implements ModuleRegistry {
|
|
239
|
+
readonly entries: AppModuleEntry[];
|
|
240
|
+
mount(module: AppModuleEntry): void;
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/core/plugin.d.ts
|
|
244
|
+
/**
|
|
245
|
+
* Plugin interface for extending KickJS applications.
|
|
246
|
+
*
|
|
247
|
+
* Plugins bundle modules, adapters, middleware, and DI bindings into a single
|
|
248
|
+
* reusable unit. They run before the application bootstraps, so they can
|
|
249
|
+
* register services and configure the app before any module loads.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* import type { KickPlugin } from '@forinda/kickjs'
|
|
254
|
+
*
|
|
255
|
+
* export class CorsPlugin implements KickPlugin {
|
|
256
|
+
* name = 'CorsPlugin'
|
|
257
|
+
*
|
|
258
|
+
* middleware() {
|
|
259
|
+
* return [cors({ origin: '*' })]
|
|
260
|
+
* }
|
|
261
|
+
* }
|
|
262
|
+
*
|
|
263
|
+
* // Usage:
|
|
264
|
+
* bootstrap({
|
|
265
|
+
* modules,
|
|
266
|
+
* plugins: [new CorsPlugin()],
|
|
267
|
+
* })
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* // A more complex plugin that bundles modules and adapters
|
|
273
|
+
* export class AuthPlugin implements KickPlugin {
|
|
274
|
+
* name = 'AuthPlugin'
|
|
275
|
+
*
|
|
276
|
+
* constructor(private config: AuthConfig) {}
|
|
277
|
+
*
|
|
278
|
+
* register(container: Container) {
|
|
279
|
+
* container.registerFactory(AUTH_SERVICE, () => new JwtAuthService(this.config))
|
|
280
|
+
* }
|
|
281
|
+
*
|
|
282
|
+
* modules() {
|
|
283
|
+
* return [AuthModule]
|
|
284
|
+
* }
|
|
285
|
+
*
|
|
286
|
+
* adapters() {
|
|
287
|
+
* return [new AuthGuardAdapter(this.config)]
|
|
288
|
+
* }
|
|
289
|
+
*
|
|
290
|
+
* middleware() {
|
|
291
|
+
* return [passport.initialize()]
|
|
292
|
+
* }
|
|
293
|
+
* }
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
interface KickPlugin {
|
|
297
|
+
/** Human-readable name for logging, debugging, and `dependsOn` resolution. */
|
|
298
|
+
name: string;
|
|
299
|
+
/**
|
|
300
|
+
* Other plugin names that must mount before this one. The framework
|
|
301
|
+
* topologically sorts plugins at boot — cycles or unknown names throw
|
|
302
|
+
* via {@link MountCycleError} / {@link MissingMountDepError} so bad
|
|
303
|
+
* configurations fail boot rather than corrupt live traffic.
|
|
304
|
+
*
|
|
305
|
+
* Plugins with no `dependsOn` retain their declaration order, so
|
|
306
|
+
* existing apps that don't use this field see no behaviour change.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* class MonitoringPlugin implements KickPlugin {
|
|
311
|
+
* name = 'MonitoringPlugin'
|
|
312
|
+
* // OTel must initialize traces before request logger reads them
|
|
313
|
+
* dependsOn = ['OtelPlugin']
|
|
314
|
+
* }
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
dependsOn?: readonly KickJsPluginName[];
|
|
318
|
+
/**
|
|
319
|
+
* Register DI bindings before modules load.
|
|
320
|
+
* Use this to provide services that modules depend on.
|
|
321
|
+
*/
|
|
322
|
+
register?(container: Container): void;
|
|
323
|
+
/**
|
|
324
|
+
* Return modules to be loaded alongside user modules. Plugin
|
|
325
|
+
* modules are loaded before user modules.
|
|
326
|
+
*
|
|
327
|
+
* Accepts both class form (`SomeModule extends AppModule`) and
|
|
328
|
+
* instance form (output of {@link defineModule}'s factory call,
|
|
329
|
+
* e.g. `TasksModule({ scope: 'admin' })`) — the bootstrap loader
|
|
330
|
+
* discriminates and either `new`-s the class or uses the instance
|
|
331
|
+
* directly.
|
|
332
|
+
*
|
|
333
|
+
* Static path — for conditional / dynamic registration, use
|
|
334
|
+
* {@link KickPlugin.setup} instead.
|
|
335
|
+
*/
|
|
336
|
+
modules?(): AppModuleEntry[];
|
|
337
|
+
/**
|
|
338
|
+
* Imperative module registration hook. Receives a {@link ModuleRegistry}
|
|
339
|
+
* the plugin can call `.mount(module)` on to register modules
|
|
340
|
+
* conditionally — e.g. based on injected config, env flags, or a
|
|
341
|
+
* tenant list resolved at boot.
|
|
342
|
+
*
|
|
343
|
+
* Runs after the static {@link KickPlugin.modules} array is collected
|
|
344
|
+
* but before user modules and the user-level `setup()` callback. The
|
|
345
|
+
* plugin sees the same dispatch rules as everything else: class
|
|
346
|
+
* entries get `new`-ed, factory output is used directly.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```ts
|
|
350
|
+
* definePlugin<{ tenants: string[] }>({
|
|
351
|
+
* name: 'MultiTenantPlugin',
|
|
352
|
+
* build: (config) => ({
|
|
353
|
+
* setup(registry) {
|
|
354
|
+
* for (const tenant of config.tenants) {
|
|
355
|
+
* registry.mount(TenantModule.scoped(tenant, { id: tenant }))
|
|
356
|
+
* }
|
|
357
|
+
* },
|
|
358
|
+
* }),
|
|
359
|
+
* })
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
setup?(registry: ModuleRegistry): void;
|
|
363
|
+
/**
|
|
364
|
+
* Return adapter instances to be added to the application.
|
|
365
|
+
* Plugin adapters are added before user adapters.
|
|
366
|
+
*/
|
|
367
|
+
adapters?(): AppAdapter[];
|
|
368
|
+
/**
|
|
369
|
+
* Return Express middleware to be added to the global pipeline.
|
|
370
|
+
* Plugin middleware runs before user-defined middleware.
|
|
371
|
+
*/
|
|
372
|
+
middleware?(): any[];
|
|
373
|
+
/**
|
|
374
|
+
* Return Context Contributors (#107) the plugin ships. Contributors
|
|
375
|
+
* returned here merge into the per-route pipeline at the **`'adapter'`
|
|
376
|
+
* precedence level** — same as those returned by adapters the plugin
|
|
377
|
+
* ships via {@link KickPlugin.adapters}, and for the same reason: a
|
|
378
|
+
* plugin is a cross-cutting bundle, narrower than the global default
|
|
379
|
+
* but broader than a per-module hook.
|
|
380
|
+
*
|
|
381
|
+
* Use when a plugin wants to ship a typed contributor without standing
|
|
382
|
+
* up an accompanying adapter. For full coverage of the precedence
|
|
383
|
+
* matrix (method > class > module > adapter > global) see
|
|
384
|
+
* `docs/guide/context-decorators.md`.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```ts
|
|
388
|
+
* import { defineContextDecorator, type KickPlugin } from '@forinda/kickjs'
|
|
389
|
+
*
|
|
390
|
+
* const LoadFlags = defineContextDecorator({
|
|
391
|
+
* key: 'flags',
|
|
392
|
+
* resolve: (ctx) => fetchFlags(ctx.requestId!),
|
|
393
|
+
* })
|
|
394
|
+
*
|
|
395
|
+
* export class FlagsPlugin implements KickPlugin {
|
|
396
|
+
* name = 'FlagsPlugin'
|
|
397
|
+
* contributors() {
|
|
398
|
+
* return [LoadFlags.registration]
|
|
399
|
+
* }
|
|
400
|
+
* }
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
contributors?(): ContributorRegistrations;
|
|
404
|
+
/**
|
|
405
|
+
* Called after the application has fully bootstrapped.
|
|
406
|
+
* Use for post-startup logic like logging, health registration, etc.
|
|
407
|
+
*/
|
|
408
|
+
onReady?(container: Container): void | Promise<void>;
|
|
409
|
+
/**
|
|
410
|
+
* Called during application shutdown.
|
|
411
|
+
* Clean up plugin resources (connections, intervals, etc.).
|
|
412
|
+
*/
|
|
413
|
+
shutdown?(): void | Promise<void>;
|
|
414
|
+
/**
|
|
415
|
+
* Optional DevTools introspection hook (architecture.md §23). Returns
|
|
416
|
+
* a snapshot describing this plugin's current state, metrics, and DI
|
|
417
|
+
* tokens. DevTools awaits the result, so async work is fine — but keep
|
|
418
|
+
* the implementation cheap (counters + flags, no DB round trips) since
|
|
419
|
+
* the topology endpoint polls on a short interval.
|
|
420
|
+
*
|
|
421
|
+
* The return type lives in `@forinda/kickjs` itself
|
|
422
|
+
* ({@link IntrospectionSnapshot}) so adopters can author `introspect()`
|
|
423
|
+
* without importing the kit just to satisfy the contract. The kit's
|
|
424
|
+
* own `IntrospectionSnapshot` type stays structurally identical;
|
|
425
|
+
* either import works.
|
|
426
|
+
*/
|
|
427
|
+
introspect?(): IntrospectionSnapshot | Promise<IntrospectionSnapshot>;
|
|
428
|
+
/**
|
|
429
|
+
* Optional DevTools tabs this plugin contributes (architecture.md §23).
|
|
430
|
+
* Same shape as {@link AppAdapter.devtoolsTabs}; the kit's
|
|
431
|
+
* `DevtoolsTabDescriptor` types it for plugin authors.
|
|
432
|
+
*/
|
|
433
|
+
devtoolsTabs?(): readonly unknown[];
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/core/define-plugin.d.ts
|
|
437
|
+
/**
|
|
438
|
+
* Options passed to {@link definePlugin}. Mirrors the spec in
|
|
439
|
+
* `architecture.md` §21.3.1 + §21.2.2 (Phase A "lite").
|
|
440
|
+
*/
|
|
441
|
+
interface DefinePluginOptions<TConfig> {
|
|
442
|
+
/** Stable identity used for logging, `dependsOn` lookups, and `.scoped()` namespacing. */
|
|
443
|
+
name: string;
|
|
444
|
+
/** Optional plugin version — surfaced to DevTools and `kick add` compatibility checks (Phase B). */
|
|
445
|
+
version?: string;
|
|
446
|
+
/**
|
|
447
|
+
* Peer-version ranges this plugin needs from the framework or other
|
|
448
|
+
* plugins. Recorded as metadata; runtime enforcement lands in §21.4.4.
|
|
449
|
+
*/
|
|
450
|
+
requires?: {
|
|
451
|
+
kickjs?: string;
|
|
452
|
+
};
|
|
453
|
+
/** Default config merged under any caller overrides. */
|
|
454
|
+
defaults?: Partial<TConfig>;
|
|
455
|
+
/** Builds the underlying {@link KickPlugin} from the resolved config. */
|
|
456
|
+
build(config: TConfig, ctx: BuildContext): Omit<KickPlugin, 'name'>;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Hook context handed to a plugin/adapter `build` function. `name` is the
|
|
460
|
+
* resolved instance name (the bare definition name for the bare call,
|
|
461
|
+
* or `${defName}:${scope}` for `.scoped()`). `scoped` is the discriminator
|
|
462
|
+
* authors use to namespace DI tokens or resource keys.
|
|
463
|
+
*/
|
|
464
|
+
interface BuildContext {
|
|
465
|
+
name: string;
|
|
466
|
+
scoped: boolean;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Async-config form of a plugin or adapter — `inject` lists DI tokens to
|
|
470
|
+
* resolve from the container, then `useFactory` produces the config.
|
|
471
|
+
*
|
|
472
|
+
* Plugins resolve this lazily inside `onReady`, so any module/adapter/
|
|
473
|
+
* middleware/contributor a plugin contributes via `build` is **not
|
|
474
|
+
* available** when `.async()` is used. Use the bare/`.scoped()` form for
|
|
475
|
+
* plugins that need to register modules or middleware.
|
|
476
|
+
*/
|
|
477
|
+
interface PluginAsyncOptions<TConfig> {
|
|
478
|
+
inject?: ReadonlyArray<unknown>;
|
|
479
|
+
useFactory(...deps: any[]): TConfig | Promise<TConfig>;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Factory returned by {@link definePlugin}. Callable form is the singleton
|
|
483
|
+
* (`AuthPlugin(config)`); `.scoped()` and `.async()` are the multi-instance
|
|
484
|
+
* and deferred-config forms.
|
|
485
|
+
*/
|
|
486
|
+
interface PluginFactory<TConfig> {
|
|
487
|
+
(config?: Partial<TConfig>): KickPlugin;
|
|
488
|
+
scoped(scopeName: string, config?: Partial<TConfig>): KickPlugin;
|
|
489
|
+
async(opts: PluginAsyncOptions<TConfig>): KickPlugin;
|
|
490
|
+
/** Read-only access to the original definition — useful for DevTools introspection. */
|
|
491
|
+
readonly definition: Readonly<DefinePluginOptions<TConfig>>;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Build a {@link KickPlugin} factory from a typed definition. See
|
|
495
|
+
* `architecture.md` §21.2.2 for the design rationale.
|
|
496
|
+
*
|
|
497
|
+
* @example
|
|
498
|
+
* ```ts
|
|
499
|
+
* const FlagsPlugin = definePlugin<FlagsConfig>({
|
|
500
|
+
* name: 'FlagsPlugin',
|
|
501
|
+
* defaults: { defaultTtl: 60_000 },
|
|
502
|
+
* build(config) {
|
|
503
|
+
* return {
|
|
504
|
+
* register(container) {
|
|
505
|
+
* container.registerInstance(FLAGS, makeProvider(config))
|
|
506
|
+
* },
|
|
507
|
+
* }
|
|
508
|
+
* },
|
|
509
|
+
* })
|
|
510
|
+
*
|
|
511
|
+
* bootstrap({ plugins: [FlagsPlugin({ provider: launchDarkly })] })
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
514
|
+
declare function definePlugin<TConfig = Record<string, unknown>>(options: DefinePluginOptions<TConfig>): PluginFactory<TConfig>;
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/core/define-module.d.ts
|
|
517
|
+
/**
|
|
518
|
+
* Options passed to {@link defineModule}. Mirrors {@link DefineAdapterOptions}
|
|
519
|
+
* and {@link DefinePluginOptions} so adopters learn one mental model
|
|
520
|
+
* across all three primitives.
|
|
521
|
+
*
|
|
522
|
+
* The optional `TExtra` generic lets a module ship public methods
|
|
523
|
+
* beyond the standard {@link AppModule} contract — they're preserved
|
|
524
|
+
* on the returned module instance so external callers (tests, sibling
|
|
525
|
+
* modules) can invoke them directly.
|
|
526
|
+
*/
|
|
527
|
+
interface DefineModuleOptions<TConfig, TExtra = unknown> {
|
|
528
|
+
/**
|
|
529
|
+
* Stable identity surfaced in diagnostics, route logs, and
|
|
530
|
+
* `.scoped()` namespacing. Required because boot-time error
|
|
531
|
+
* messages reference modules by name — anonymous modules make
|
|
532
|
+
* cycles + missing-dep errors hard to localize.
|
|
533
|
+
*/
|
|
534
|
+
name: string;
|
|
535
|
+
/** Optional version string surfaced in diagnostics. */
|
|
536
|
+
version?: string;
|
|
537
|
+
/**
|
|
538
|
+
* Optional kickjs version constraint. Enforced by the framework
|
|
539
|
+
* when implemented; today it lives on the definition for
|
|
540
|
+
* documentation and forward compatibility.
|
|
541
|
+
*/
|
|
542
|
+
requires?: {
|
|
543
|
+
kickjs?: string;
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* Default config merged with overrides at instantiation time
|
|
547
|
+
* (`MyModule({ field: x })`). Call-site overrides win.
|
|
548
|
+
*/
|
|
549
|
+
defaults?: Partial<TConfig>;
|
|
550
|
+
/**
|
|
551
|
+
* Build the AppModule shape from the resolved config and context.
|
|
552
|
+
* `ctx.name` is the resolved instance name (the bare definition
|
|
553
|
+
* name for the default call, or `${name}:${scope}` for
|
|
554
|
+
* `.scoped()`); `ctx.scoped` discriminates so the build function
|
|
555
|
+
* can branch on namespacing if needed.
|
|
556
|
+
*
|
|
557
|
+
* Return any extension methods alongside the standard
|
|
558
|
+
* {@link AppModule} surface — they're typed via `TExtra` and stay
|
|
559
|
+
* accessible on the resulting instance.
|
|
560
|
+
*/
|
|
561
|
+
build(config: TConfig, ctx: BuildContext): AppModule & TExtra;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Factory returned by {@link defineModule}. Mirrors {@link AdapterFactory}
|
|
565
|
+
* — call (`MyModule({...})`), scope (`MyModule.scoped('public', {...})`),
|
|
566
|
+
* inspect (`MyModule.definition`).
|
|
567
|
+
*
|
|
568
|
+
* `.async()` is intentionally **not** part of the module factory
|
|
569
|
+
* surface. Module config has no async-resolution window: `register()`
|
|
570
|
+
* and `routes()` both run synchronously during bootstrap, before any
|
|
571
|
+
* adapter `beforeStart` hook fires. Adopters that need async-resolved
|
|
572
|
+
* config push it into an adapter (which has `beforeStart`), then
|
|
573
|
+
* inject the resolved value into the module via DI tokens.
|
|
574
|
+
*/
|
|
575
|
+
interface ModuleFactory<TConfig, TExtra = unknown> {
|
|
576
|
+
(config?: Partial<TConfig>): AppModule & TExtra;
|
|
577
|
+
scoped(scopeName: string, config?: Partial<TConfig>): AppModule & TExtra;
|
|
578
|
+
readonly definition: Readonly<DefineModuleOptions<TConfig, TExtra>>;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Build an {@link AppModule} factory from a typed definition. Pairs
|
|
582
|
+
* with {@link defineAdapter}, {@link definePlugin}, and
|
|
583
|
+
* {@link defineContextDecorator} as the fourth `define*` primitive
|
|
584
|
+
* — adopters write modules the same way they write adapters and
|
|
585
|
+
* plugins.
|
|
586
|
+
*
|
|
587
|
+
* The returned factory produces real {@link AppModule} instances
|
|
588
|
+
* that drop into `bootstrap({ modules })` directly. The legacy
|
|
589
|
+
* class form (`class TasksModule implements AppModule { ... }`) keeps
|
|
590
|
+
* working — `bootstrap` accepts either shape.
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```ts
|
|
594
|
+
* interface TasksConfig {
|
|
595
|
+
* scope: 'public' | 'admin'
|
|
596
|
+
* }
|
|
597
|
+
*
|
|
598
|
+
* const TasksModule = defineModule<TasksConfig>({
|
|
599
|
+
* name: 'TasksModule',
|
|
600
|
+
* defaults: { scope: 'public' },
|
|
601
|
+
* build: (config, { name }) => ({
|
|
602
|
+
* register(container) {
|
|
603
|
+
* container.registerInstance(`tasks:scope:${name}`, config.scope)
|
|
604
|
+
* },
|
|
605
|
+
* routes() {
|
|
606
|
+
* return { path: `/${config.scope}/tasks`, router: buildTasksRouter() }
|
|
607
|
+
* },
|
|
608
|
+
* contributors() {
|
|
609
|
+
* return [LoadTenant.registration]
|
|
610
|
+
* },
|
|
611
|
+
* }),
|
|
612
|
+
* })
|
|
613
|
+
*
|
|
614
|
+
* bootstrap({
|
|
615
|
+
* modules: [
|
|
616
|
+
* TasksModule(), // public scope (defaults)
|
|
617
|
+
* TasksModule.scoped('admin', { scope: 'admin' }),
|
|
618
|
+
* ],
|
|
619
|
+
* })
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
declare function defineModule<TConfig = Record<string, unknown>, TExtra = unknown>(options: DefineModuleOptions<TConfig, TExtra>): ModuleFactory<TConfig, TExtra>;
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/core/define-modules.d.ts
|
|
625
|
+
/**
|
|
626
|
+
* Chainable registry returned by {@link defineModules}. Extends
|
|
627
|
+
* `Array<AppModuleEntry>` so it drops into `bootstrap({ modules })`
|
|
628
|
+
* unchanged — the framework loader already iterates the array, so a
|
|
629
|
+
* `ModuleList` IS a valid `modules` value with no special handling.
|
|
630
|
+
*
|
|
631
|
+
* The added affordance is `.mount(module)` returning `this` so
|
|
632
|
+
* adopters can build the list fluently:
|
|
633
|
+
*
|
|
634
|
+
* ```ts
|
|
635
|
+
* const modules = defineModules()
|
|
636
|
+
* .mount(HelloModule())
|
|
637
|
+
* .mount(AdminModule())
|
|
638
|
+
*
|
|
639
|
+
* bootstrap({ modules })
|
|
640
|
+
* ```
|
|
641
|
+
*
|
|
642
|
+
* **Why not just use a plain array?** Two reasons. First, the
|
|
643
|
+
* fluent shape reads more naturally for module-list construction —
|
|
644
|
+
* adopters used to `definePlugin().scoped(...)` / `defineAdapter(...)`
|
|
645
|
+
* see the same call-then-call pattern. Second, the registry is the
|
|
646
|
+
* stable extension point: a future `.use(module)` for non-HTTP
|
|
647
|
+
* modules (queues, cron, workers) lands as a method on `ModuleList`
|
|
648
|
+
* without breaking the `Array<AppModuleEntry>` superclass contract.
|
|
649
|
+
*/
|
|
650
|
+
declare class ModuleList extends Array<AppModuleEntry> {
|
|
651
|
+
/**
|
|
652
|
+
* Append a module to the list. Returns `this` for chaining.
|
|
653
|
+
*
|
|
654
|
+
* Accepts both class form (`SomeModule extends AppModule`) and
|
|
655
|
+
* `defineModule` factory output — same as the underlying
|
|
656
|
+
* `bootstrap({ modules })` array. The framework loader
|
|
657
|
+
* discriminates `typeof entry === 'function'` at boot.
|
|
658
|
+
*/
|
|
659
|
+
mount(module: AppModuleEntry): this;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Build a {@link ModuleList} for the application. Optional vararg
|
|
663
|
+
* lets adopters seed the list inline; subsequent `.mount()` calls
|
|
664
|
+
* append.
|
|
665
|
+
*
|
|
666
|
+
* @example
|
|
667
|
+
* ```ts
|
|
668
|
+
* import { bootstrap, defineModules } from '@forinda/kickjs'
|
|
669
|
+
* import { HelloModule } from './modules/hello/hello.module'
|
|
670
|
+
* import { AdminModule } from './modules/admin/admin.module'
|
|
671
|
+
*
|
|
672
|
+
* // Fluent — most common
|
|
673
|
+
* const modules = defineModules()
|
|
674
|
+
* .mount(HelloModule())
|
|
675
|
+
* .mount(AdminModule())
|
|
676
|
+
*
|
|
677
|
+
* // Or seeded inline + chained
|
|
678
|
+
* const modules2 = defineModules(HelloModule()).mount(AdminModule())
|
|
679
|
+
*
|
|
680
|
+
* await bootstrap({ modules })
|
|
681
|
+
* ```
|
|
682
|
+
*
|
|
683
|
+
* The returned value is an `AppModuleEntry[]` subclass — `bootstrap`
|
|
684
|
+
* accepts it directly without any special-casing. For conditional /
|
|
685
|
+
* dynamic registration at bootstrap time (env flags, runtime config,
|
|
686
|
+
* tenant lists), use the `setup(registry)` callback on `bootstrap`
|
|
687
|
+
* instead — both surfaces compose.
|
|
688
|
+
*/
|
|
689
|
+
declare function defineModules(...initial: AppModuleEntry[]): ModuleList;
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/core/kick-error.d.ts
|
|
692
|
+
/**
|
|
693
|
+
* Structured framework error with code, cause, and actionable fix hint.
|
|
694
|
+
*
|
|
695
|
+
* The goal is "the developer who hits this knows what to do next." A
|
|
696
|
+
* cryptic `Error('No binding found for: UserService')` becomes a
|
|
697
|
+
* multi-line explanation pointing at the likely cause, the exact code
|
|
698
|
+
* change to apply, and a docs link.
|
|
699
|
+
*
|
|
700
|
+
* `.message` carries the full multi-line plain-text body so Node's
|
|
701
|
+
* default `Error.toString()` and unhandled-exception printing still
|
|
702
|
+
* surface the helpful version. Call {@link formatKickError} to render a
|
|
703
|
+
* colorized version for terminals.
|
|
704
|
+
*
|
|
705
|
+
* Per-error factories live in `kick-errors.ts` — those are the public
|
|
706
|
+
* entry points framework code uses. `KickError` is exported so adopters
|
|
707
|
+
* can build their own structured errors using the same shape.
|
|
708
|
+
*/
|
|
709
|
+
interface KickErrorInit {
|
|
710
|
+
/** Short stable identifier (e.g. `KICK001`, `NO_PROVIDER`). */
|
|
711
|
+
code: string;
|
|
712
|
+
/** One-line headline. */
|
|
713
|
+
summary: string;
|
|
714
|
+
/** Multi-line explanation of why this happened. */
|
|
715
|
+
cause?: string;
|
|
716
|
+
/** Multi-line actionable fix — show exact code if possible. */
|
|
717
|
+
fix?: string;
|
|
718
|
+
/** Absolute URL to the docs page that covers this error. */
|
|
719
|
+
docsUrl?: string;
|
|
720
|
+
/** Arbitrary structured fields for log consumers / debug tooling. */
|
|
721
|
+
context?: Record<string, unknown>;
|
|
722
|
+
}
|
|
723
|
+
declare class KickError extends Error {
|
|
724
|
+
readonly code: string;
|
|
725
|
+
readonly summary: string;
|
|
726
|
+
readonly cause?: string;
|
|
727
|
+
readonly fix?: string;
|
|
728
|
+
readonly docsUrl?: string;
|
|
729
|
+
readonly context?: Record<string, unknown>;
|
|
730
|
+
constructor(init: KickErrorInit);
|
|
731
|
+
}
|
|
732
|
+
interface FormatKickErrorOptions {
|
|
733
|
+
/**
|
|
734
|
+
* Render ANSI colour codes. Defaults to TTY detection (`NO_COLOR` /
|
|
735
|
+
* `FORCE_COLOR` env vars honoured). Pass `false` for log files,
|
|
736
|
+
* structured loggers, or HTTP response bodies.
|
|
737
|
+
*/
|
|
738
|
+
color?: boolean;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Render a {@link KickErrorInit} (or {@link KickError} instance) into a
|
|
742
|
+
* multi-line human-readable string. Identical layout regardless of
|
|
743
|
+
* colour — the colour flag only toggles ANSI codes.
|
|
744
|
+
*/
|
|
745
|
+
declare function formatKickError(err: KickErrorInit | KickError, options?: FormatKickErrorOptions): string;
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/core/metadata.d.ts
|
|
748
|
+
/**
|
|
749
|
+
* Typed utilities for reflect-metadata operations.
|
|
750
|
+
*
|
|
751
|
+
* Replaces scattered Reflect.defineMetadata/getMetadata calls with a single
|
|
752
|
+
* source of truth. If we later move to Stage 3 decorators or a WeakMap-based
|
|
753
|
+
* store, only this file changes — all 24+ consumer files stay untouched.
|
|
754
|
+
*
|
|
755
|
+
* Keys may be symbols, strings, or `InjectionToken<T>` objects produced by
|
|
756
|
+
* `createToken()`. Using `createToken` is the preferred pattern because the
|
|
757
|
+
* token's phantom type flows through the helpers, enabling type-safe reads.
|
|
758
|
+
*/
|
|
759
|
+
type MetaKey = symbol | string | InjectionToken<unknown>;
|
|
760
|
+
/** Set metadata on a class (or prototype for property decorators) */
|
|
761
|
+
declare function setClassMeta<T>(key: MetaKey, value: T, target: object): void;
|
|
762
|
+
/** Set metadata on a specific method */
|
|
763
|
+
declare function setMethodMeta<T>(key: MetaKey, value: T, target: object, method: string): void;
|
|
764
|
+
/** Get metadata from a class, returning `fallback` if not set */
|
|
765
|
+
declare function getClassMeta<T>(key: MetaKey, target: object, fallback: T): T;
|
|
766
|
+
/** Get metadata from a specific method, returning `fallback` if not set */
|
|
767
|
+
declare function getMethodMeta<T>(key: MetaKey, target: object, method: string, fallback: T): T;
|
|
768
|
+
/** Check if metadata exists on a class */
|
|
769
|
+
declare function hasClassMeta(key: MetaKey, target: object): boolean;
|
|
770
|
+
/** Get metadata from a class with no default (returns undefined if absent) */
|
|
771
|
+
declare function getClassMetaOrUndefined<T>(key: MetaKey, target: object): T | undefined;
|
|
772
|
+
/** Get metadata from a method with no default (returns undefined if absent) */
|
|
773
|
+
declare function getMethodMetaOrUndefined<T>(key: MetaKey, target: object, method: string): T | undefined;
|
|
774
|
+
/** Append items to an array stored in class metadata */
|
|
775
|
+
declare function pushClassMeta<T>(key: MetaKey, target: object, ...items: T[]): void;
|
|
776
|
+
/** Append items to an array stored in method metadata */
|
|
777
|
+
declare function pushMethodMeta<T>(key: MetaKey, target: object, method: string, ...items: T[]): void;
|
|
778
|
+
/** Set a key/value in a Map stored in class metadata */
|
|
779
|
+
declare function setInMetaMap<K, V>(key: MetaKey, target: object, mapKey: K, mapValue: V): void;
|
|
780
|
+
/** Get a Map from class metadata (returns empty Map if absent) */
|
|
781
|
+
declare function getMetaMap<K, V>(key: MetaKey, target: object): Map<K, V>;
|
|
782
|
+
/** Set a key/value in a Record stored in class metadata */
|
|
783
|
+
declare function setInMetaRecord<V>(key: MetaKey, target: object, recKey: string | number, recValue: V): void;
|
|
784
|
+
/** Get a Record from class metadata (returns empty object if absent) */
|
|
785
|
+
declare function getMetaRecord<V>(key: MetaKey, target: object): Record<string | number, V>;
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/core/circuit-breaker.d.ts
|
|
788
|
+
/**
|
|
789
|
+
* Circuit Breaker pattern for external service calls.
|
|
790
|
+
*
|
|
791
|
+
* Protects your application from cascading failures when downstream
|
|
792
|
+
* services are unhealthy by short-circuiting requests after a
|
|
793
|
+
* configurable failure threshold.
|
|
794
|
+
*
|
|
795
|
+
* @example
|
|
796
|
+
* ```ts
|
|
797
|
+
* const breaker = new CircuitBreaker('payment-api', {
|
|
798
|
+
* failureThreshold: 5,
|
|
799
|
+
* resetTimeout: 30_000,
|
|
800
|
+
* })
|
|
801
|
+
*
|
|
802
|
+
* const result = await breaker.execute(() => fetch('https://payment.example.com/charge'))
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
type CircuitBreakerState = 'closed' | 'open' | 'half_open';
|
|
806
|
+
interface CircuitBreakerOptions {
|
|
807
|
+
/** Number of consecutive failures before the circuit opens. */
|
|
808
|
+
failureThreshold: number;
|
|
809
|
+
/** Milliseconds to wait before transitioning from OPEN to HALF_OPEN. */
|
|
810
|
+
resetTimeout: number;
|
|
811
|
+
/** Max requests allowed in HALF_OPEN state before deciding (default 1). */
|
|
812
|
+
halfOpenMax?: number;
|
|
813
|
+
}
|
|
814
|
+
interface CircuitBreakerStats {
|
|
815
|
+
failures: number;
|
|
816
|
+
successes: number;
|
|
817
|
+
state: CircuitBreakerState;
|
|
818
|
+
lastFailure?: Date;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Error thrown when the circuit is open and calls are being rejected.
|
|
822
|
+
*/
|
|
823
|
+
declare class CircuitOpenError extends Error {
|
|
824
|
+
readonly breakerName: string;
|
|
825
|
+
constructor(breakerName: string);
|
|
826
|
+
}
|
|
827
|
+
declare class CircuitBreaker {
|
|
828
|
+
readonly name: string;
|
|
829
|
+
private state;
|
|
830
|
+
private failures;
|
|
831
|
+
private successes;
|
|
832
|
+
private halfOpenAttempts;
|
|
833
|
+
private lastFailure?;
|
|
834
|
+
private openedAt?;
|
|
835
|
+
private readonly failureThreshold;
|
|
836
|
+
private readonly resetTimeout;
|
|
837
|
+
private readonly halfOpenMax;
|
|
838
|
+
constructor(name: string, options: CircuitBreakerOptions);
|
|
839
|
+
/**
|
|
840
|
+
* Execute an async function through the circuit breaker.
|
|
841
|
+
* Throws `CircuitOpenError` if the circuit is open.
|
|
842
|
+
*/
|
|
843
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
844
|
+
/** Returns the current circuit state. */
|
|
845
|
+
getState(): CircuitBreakerState;
|
|
846
|
+
/** Returns current statistics. */
|
|
847
|
+
getStats(): CircuitBreakerStats;
|
|
848
|
+
/** Manually reset the circuit breaker to CLOSED state. */
|
|
849
|
+
reset(): void;
|
|
850
|
+
private onSuccess;
|
|
851
|
+
private onFailure;
|
|
852
|
+
private maybeTransitionToHalfOpen;
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/core/context-errors.d.ts
|
|
856
|
+
/**
|
|
857
|
+
* Errors thrown by the Context Contributor pipeline.
|
|
858
|
+
*
|
|
859
|
+
* All three are startup-time errors raised during pipeline build (topo-sort
|
|
860
|
+
* + validation), not per-request — a misconfigured app fails to boot rather
|
|
861
|
+
* than fails a request. Per the locked design in `architecture.md` §20.4,
|
|
862
|
+
* the pipeline is validated once when each route is mounted and the
|
|
863
|
+
* resolved order is cached on the route.
|
|
864
|
+
*/
|
|
865
|
+
/**
|
|
866
|
+
* A contributor declared `dependsOn: ['<key>']` but no other contributor
|
|
867
|
+
* (at any precedence level) produces that key for the same route.
|
|
868
|
+
*
|
|
869
|
+
* @example
|
|
870
|
+
* ```text
|
|
871
|
+
* MissingContributorError: Missing context contributor 'tenant' required by 'project' on route GET /projects/:id
|
|
872
|
+
* declared at src/contributors/load-project.ts:42:18
|
|
873
|
+
* ```
|
|
874
|
+
*/
|
|
875
|
+
declare class MissingContributorError extends Error {
|
|
876
|
+
readonly key: string;
|
|
877
|
+
readonly dependent: string;
|
|
878
|
+
readonly route?: string;
|
|
879
|
+
readonly dependentDefinedAt?: string;
|
|
880
|
+
constructor(key: string, dependent: string, route?: string, dependentDefinedAt?: string);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Topo-sort detected a cycle in `dependsOn` declarations. The `cycle` field
|
|
884
|
+
* lists the keys in the order they form the loop, with the first key
|
|
885
|
+
* repeated at the end so the path reads naturally.
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```text
|
|
889
|
+
* ContributorCycleError: Cycle in context contributors on route GET /projects/:id: tenant → project → tenant
|
|
890
|
+
* ```
|
|
891
|
+
*/
|
|
892
|
+
declare class ContributorCycleError extends Error {
|
|
893
|
+
readonly cycle: readonly string[];
|
|
894
|
+
readonly route?: string;
|
|
895
|
+
constructor(cycle: readonly string[], route?: string);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Two contributors at the same precedence level both produce the same key
|
|
899
|
+
* for the same route. Precedence resolution (method > class > module >
|
|
900
|
+
* adapter > global, per architecture.md §20.4) silently drops
|
|
901
|
+
* lower-precedence duplicates; this error fires only when ties occur
|
|
902
|
+
* within a single level — e.g., two method-level decorators declaring
|
|
903
|
+
* `key: 'tenant'`.
|
|
904
|
+
*
|
|
905
|
+
* `sources` is best-effort identifying labels for each conflicting
|
|
906
|
+
* registration (typically the decorator's call site or registering
|
|
907
|
+
* module/adapter name).
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* ```text
|
|
911
|
+
* DuplicateContributorError: Duplicate context contributor for key 'tenant' at the same precedence level. Sources: LoadTenantFromHeader, LoadTenantFromSubdomain
|
|
912
|
+
* ```
|
|
913
|
+
*/
|
|
914
|
+
declare class DuplicateContributorError extends Error {
|
|
915
|
+
readonly key: string;
|
|
916
|
+
readonly sources: readonly string[];
|
|
917
|
+
constructor(key: string, sources: readonly string[]);
|
|
918
|
+
}
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/core/contributor-pipeline.d.ts
|
|
921
|
+
/**
|
|
922
|
+
* Where a contributor was registered, ordered by precedence
|
|
923
|
+
* (lower index wins). Mirrors `architecture.md` §20.4.
|
|
924
|
+
*
|
|
925
|
+
* Levels (highest → lowest precedence):
|
|
926
|
+
* - `'method'` — `@LoadX` on a controller method.
|
|
927
|
+
* - `'class'` — `@LoadX` on a controller class.
|
|
928
|
+
* - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.
|
|
929
|
+
* - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies
|
|
930
|
+
* to every module's routes.
|
|
931
|
+
* - `'global'` — `ApplicationOptions.contributors`. App-wide default.
|
|
932
|
+
*/
|
|
933
|
+
type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global';
|
|
934
|
+
/**
|
|
935
|
+
* Input row to {@link buildPipeline}. Pairs a registration with its origin
|
|
936
|
+
* so dedup can apply the precedence rule and diagnostic errors can name
|
|
937
|
+
* the offending site.
|
|
938
|
+
*
|
|
939
|
+
* `registration` is the type-erased {@link AnyContributorRegistration}
|
|
940
|
+
* — the runner doesn't care about each registration's narrow `D` /
|
|
941
|
+
* `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic
|
|
942
|
+
* contributors in the same `externalSources` / `contributors()`
|
|
943
|
+
* array. The narrow generics from `defineContextDecorator` survive
|
|
944
|
+
* at the *definition* site (where `resolve` is typed); they're erased
|
|
945
|
+
* here so collections compose without `as any` casts.
|
|
946
|
+
*/
|
|
947
|
+
interface SourcedRegistration {
|
|
948
|
+
registration: AnyContributorRegistration;
|
|
949
|
+
source: ContributorSource;
|
|
950
|
+
/** Human label surfaced in DuplicateContributorError messages. */
|
|
951
|
+
label?: string;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Resolved, validated, topo-sorted pipeline ready for execution.
|
|
955
|
+
*
|
|
956
|
+
* `contributors` is in execution order — every entry's `dependsOn`
|
|
957
|
+
* keys appear earlier. `keys` is the set of all keys this pipeline
|
|
958
|
+
* produces; the runner uses it for fast existence checks and the
|
|
959
|
+
* builder uses it for missing-dep validation.
|
|
960
|
+
*/
|
|
961
|
+
interface ContributorPipeline {
|
|
962
|
+
readonly contributors: readonly AnyContributorRegistration[];
|
|
963
|
+
readonly keys: ReadonlySet<string>;
|
|
964
|
+
}
|
|
965
|
+
interface BuildPipelineOptions {
|
|
966
|
+
/** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */
|
|
967
|
+
route?: string;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Build, validate, and topo-sort a contributor pipeline.
|
|
971
|
+
*
|
|
972
|
+
* Pipeline:
|
|
973
|
+
*
|
|
974
|
+
* 1. Group sources by precedence (method > class > module > adapter > global).
|
|
975
|
+
* 2. Within each precedence level, two contributors with the same `key`
|
|
976
|
+
* raise {@link DuplicateContributorError}.
|
|
977
|
+
* 3. Across levels, the highest-precedence contributor wins each key
|
|
978
|
+
* (lower-precedence duplicates are silently dropped — that is the
|
|
979
|
+
* documented override mechanism, not an error).
|
|
980
|
+
* 4. Every key referenced by `dependsOn` must be produced by the
|
|
981
|
+
* surviving set, otherwise {@link MissingContributorError}.
|
|
982
|
+
* 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after
|
|
983
|
+
* the queue drains form a cycle → {@link ContributorCycleError}
|
|
984
|
+
* with a reconstructed path.
|
|
985
|
+
*
|
|
986
|
+
* Pure function — no DI, no I/O. Safe to call once at startup per
|
|
987
|
+
* route and cache the result.
|
|
988
|
+
*/
|
|
989
|
+
declare function buildPipeline(sources: readonly SourcedRegistration[], options?: BuildPipelineOptions): ContributorPipeline;
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/core/contributor-runner.d.ts
|
|
992
|
+
interface RunContributorsOptions {
|
|
993
|
+
/** Pre-built, validated, topo-sorted pipeline. */
|
|
994
|
+
pipeline: ContributorPipeline;
|
|
995
|
+
/** The execution context contributors read from and write into. */
|
|
996
|
+
ctx: ExecutionContext;
|
|
997
|
+
/** DI container used to resolve declared `deps`. */
|
|
998
|
+
container: Container;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Execute a built {@link ContributorPipeline} against an {@link ExecutionContext}.
|
|
1002
|
+
*
|
|
1003
|
+
* Sequential — one contributor at a time, in the topo order chosen at
|
|
1004
|
+
* pipeline-build time. Per `architecture.md` §20.10, V1 does not run
|
|
1005
|
+
* contributors in parallel even within the same topo level; that is a
|
|
1006
|
+
* V2 follow-up gated on profiling.
|
|
1007
|
+
*
|
|
1008
|
+
* ## Error matrix (architecture.md §20.9)
|
|
1009
|
+
*
|
|
1010
|
+
* | resolve outcome | optional | onError defined | runner behaviour |
|
|
1011
|
+
* |-----------------|----------|-----------------|--------------------------------------------------|
|
|
1012
|
+
* | throws | true | — | skip; `ctx.get(key)` remains undefined |
|
|
1013
|
+
* | throws | false | yes | call hook; returned value (if any) stored |
|
|
1014
|
+
* | throws | false | no | propagate the original error |
|
|
1015
|
+
* | hook throws | — | yes | propagate the new error |
|
|
1016
|
+
* | hook returns | — | yes | undefined/void → skip; value → `ctx.set(key, v)` |
|
|
1017
|
+
* | resolves | — | — | `ctx.set(key, value)` |
|
|
1018
|
+
*
|
|
1019
|
+
* Container.resolve() throwing while building `deps` is treated as a
|
|
1020
|
+
* resolve-side throw and flows through the same matrix — adopters who
|
|
1021
|
+
* need different behaviour for missing-DI vs. data-lookup failures can
|
|
1022
|
+
* wrap the container access inside their own `resolve()`.
|
|
1023
|
+
*/
|
|
1024
|
+
declare function runContributors(options: RunContributorsOptions): Promise<void>;
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/core/mount-sort.d.ts
|
|
1027
|
+
/**
|
|
1028
|
+
* Topological sort for plugins and adapters with `dependsOn` declarations.
|
|
1029
|
+
*
|
|
1030
|
+
* Mirrors the algorithm used by the Context Contributor pipeline
|
|
1031
|
+
* (`contributor-pipeline.ts:topoSort`) but operates on a name-keyed
|
|
1032
|
+
* `MountSortItem` shape rather than a `ContributorRegistration`. Kept
|
|
1033
|
+
* standalone instead of factored generic so that:
|
|
1034
|
+
*
|
|
1035
|
+
* 1. The contributor pipeline doesn't need refactoring (risk reduction).
|
|
1036
|
+
* 2. The error classes can carry plugin/adapter-specific diagnostic
|
|
1037
|
+
* fields (`kind`, `dependent`) without bending the contributor types.
|
|
1038
|
+
*/
|
|
1039
|
+
/**
|
|
1040
|
+
* A plugin or adapter that participates in dependency-ordered mounting.
|
|
1041
|
+
* Both `KickPlugin` and `AppAdapter` satisfy this shape via their `name`
|
|
1042
|
+
* and `dependsOn` fields.
|
|
1043
|
+
*/
|
|
1044
|
+
interface MountSortItem {
|
|
1045
|
+
/** Unique mount name within the plugin/adapter set being sorted. */
|
|
1046
|
+
name: string;
|
|
1047
|
+
/**
|
|
1048
|
+
* Other plugin/adapter names that must mount before this one. Cycles
|
|
1049
|
+
* and missing names fail at boot — same fail-fast contract as the
|
|
1050
|
+
* Context Contributor pipeline (architecture.md §20.4).
|
|
1051
|
+
*/
|
|
1052
|
+
dependsOn?: readonly string[];
|
|
1053
|
+
}
|
|
1054
|
+
/** What kind of mount set the sort is operating on — surfaces in error messages. */
|
|
1055
|
+
type MountKind = 'plugin' | 'adapter';
|
|
1056
|
+
/**
|
|
1057
|
+
* Two items in the same mount set declared the same `name`. Plugin/adapter
|
|
1058
|
+
* names must be unique so `dependsOn` references resolve unambiguously.
|
|
1059
|
+
*/
|
|
1060
|
+
declare class DuplicateMountNameError extends Error {
|
|
1061
|
+
readonly kind: MountKind;
|
|
1062
|
+
readonly mountName: string;
|
|
1063
|
+
constructor(kind: MountKind, mountName: string);
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* A `dependsOn` entry references a plugin/adapter that isn't in the
|
|
1067
|
+
* current set. The dependency might not be installed, might be misspelled,
|
|
1068
|
+
* or might live in a different mount set (plugin referencing an adapter,
|
|
1069
|
+
* or vice versa — they sort independently).
|
|
1070
|
+
*/
|
|
1071
|
+
declare class MissingMountDepError extends Error {
|
|
1072
|
+
readonly kind: MountKind;
|
|
1073
|
+
readonly missing: string;
|
|
1074
|
+
readonly dependent: string;
|
|
1075
|
+
constructor(kind: MountKind, missing: string, dependent: string);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* The `dependsOn` declarations form a cycle. Boot fails so the cycle
|
|
1079
|
+
* can't poison live traffic. The `cycle` field reports one path through
|
|
1080
|
+
* the offending nodes for diagnostics — formatted `A → B → C → A`.
|
|
1081
|
+
*/
|
|
1082
|
+
declare class MountCycleError extends Error {
|
|
1083
|
+
readonly kind: MountKind;
|
|
1084
|
+
readonly cycle: readonly string[];
|
|
1085
|
+
constructor(kind: MountKind, cycle: readonly string[]);
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Topologically sort `items` so every item with `dependsOn: ['X']` mounts
|
|
1089
|
+
* after `X`. Items without `dependsOn` keep their input order via Kahn's
|
|
1090
|
+
* algorithm with a FIFO queue seeded in input order — preserves the
|
|
1091
|
+
* historical "plugins/adapters mount in declaration order" behaviour for
|
|
1092
|
+
* any set that doesn't use `dependsOn`.
|
|
1093
|
+
*
|
|
1094
|
+
* @param items - mountable items, e.g. plugins or adapters
|
|
1095
|
+
* @param kind - 'plugin' | 'adapter', used in error messages
|
|
1096
|
+
* @returns a new array in valid mount order
|
|
1097
|
+
* @throws {DuplicateMountNameError} two items share a name
|
|
1098
|
+
* @throws {MissingMountDepError} a `dependsOn` references an unknown name
|
|
1099
|
+
* @throws {MountCycleError} the dep graph contains a cycle
|
|
1100
|
+
*/
|
|
1101
|
+
declare function mountSort<T extends MountSortItem>(items: readonly T[], kind: MountKind): T[];
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/core/define-adapter.d.ts
|
|
1104
|
+
/**
|
|
1105
|
+
* Options passed to {@link defineAdapter}. Mirrors {@link DefinePluginOptions}
|
|
1106
|
+
* exactly — see `architecture.md` §21.3.4 for the definePlugin/defineAdapter
|
|
1107
|
+
* symmetry rationale.
|
|
1108
|
+
*
|
|
1109
|
+
* The optional `TExtra` generic lets an adapter ship public methods
|
|
1110
|
+
* beyond the standard {@link AppAdapter} contract — they're preserved
|
|
1111
|
+
* on the returned adapter instance so external callers (tests, peer
|
|
1112
|
+
* adapters) can invoke them directly. See `OtelAdapter.applyRedaction`
|
|
1113
|
+
* for the canonical use case.
|
|
1114
|
+
*/
|
|
1115
|
+
interface DefineAdapterOptions<TConfig, TExtra = unknown> {
|
|
1116
|
+
/**
|
|
1117
|
+
* Stable identity used for logging, `dependsOn` lookups, and `.scoped()`
|
|
1118
|
+
* namespacing. Required for adapters that participate in `dependsOn`
|
|
1119
|
+
* sort — error messages reference adapters by name.
|
|
1120
|
+
*/
|
|
1121
|
+
name: string;
|
|
1122
|
+
version?: string;
|
|
1123
|
+
requires?: {
|
|
1124
|
+
kickjs?: string;
|
|
1125
|
+
};
|
|
1126
|
+
defaults?: Partial<TConfig>;
|
|
1127
|
+
build(config: TConfig, ctx: BuildContext): Omit<AppAdapter, 'name'> & TExtra;
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Async-config form of an adapter. Identical shape to `PluginAsyncOptions`
|
|
1131
|
+
* but resolved during `beforeStart` (the earliest async hook in
|
|
1132
|
+
* `AppAdapter`) — meaning `middleware()` and `contributors()` from the inner
|
|
1133
|
+
* adapter are **not** picked up. Use the bare/`.scoped()` form when you need
|
|
1134
|
+
* those.
|
|
1135
|
+
*/
|
|
1136
|
+
interface AdapterAsyncOptions<TConfig> {
|
|
1137
|
+
inject?: ReadonlyArray<unknown>;
|
|
1138
|
+
useFactory(...deps: any[]): TConfig | Promise<TConfig>;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Factory returned by {@link defineAdapter}. Same surface as
|
|
1142
|
+
* {@link PluginFactory} (call / `.scoped()` / `.async()`) so adopters learn
|
|
1143
|
+
* one mental model for both primitives. Returned instances carry both
|
|
1144
|
+
* the standard {@link AppAdapter} contract and any `TExtra` extension
|
|
1145
|
+
* methods the `build` function exposed.
|
|
1146
|
+
*/
|
|
1147
|
+
interface AdapterFactory<TConfig, TExtra = unknown> {
|
|
1148
|
+
(config?: Partial<TConfig>): AppAdapter & TExtra;
|
|
1149
|
+
scoped(scopeName: string, config?: Partial<TConfig>): AppAdapter & TExtra;
|
|
1150
|
+
async(opts: AdapterAsyncOptions<TConfig>): AppAdapter;
|
|
1151
|
+
readonly definition: Readonly<DefineAdapterOptions<TConfig, TExtra>>;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Build an {@link AppAdapter} factory from a typed definition. See
|
|
1155
|
+
* `architecture.md` §21.3.4 for migration guidance — most first-party
|
|
1156
|
+
* adapter packages collapse to a single `defineAdapter()` call after this
|
|
1157
|
+
* lands.
|
|
1158
|
+
*
|
|
1159
|
+
* @example
|
|
1160
|
+
* ```ts
|
|
1161
|
+
* const TenantAdapter = defineAdapter<TenantConfig>({
|
|
1162
|
+
* name: 'TenantAdapter',
|
|
1163
|
+
* defaults: { strategy: 'header', required: true },
|
|
1164
|
+
* build: (config) => ({
|
|
1165
|
+
* middleware: () => [tenantResolverMiddleware(config)],
|
|
1166
|
+
* beforeStart: ({ container }) => registerTenantFactory(container, config),
|
|
1167
|
+
* }),
|
|
1168
|
+
* })
|
|
1169
|
+
*
|
|
1170
|
+
* bootstrap({
|
|
1171
|
+
* adapters: [
|
|
1172
|
+
* TenantAdapter({ strategy: 'subdomain' }),
|
|
1173
|
+
* TenantAdapter.scoped('shard-eu', { headerName: 'x-eu-tenant' }),
|
|
1174
|
+
* ],
|
|
1175
|
+
* })
|
|
1176
|
+
* ```
|
|
1177
|
+
*/
|
|
1178
|
+
declare function defineAdapter<TConfig = Record<string, unknown>, TExtra = unknown>(options: DefineAdapterOptions<TConfig, TExtra>): AdapterFactory<TConfig, TExtra>;
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/core/asset-keys.d.ts
|
|
1181
|
+
type AssetKeyStrategy = 'auto' | 'strip' | 'with-extension';
|
|
1182
|
+
interface AssetKeyPair {
|
|
1183
|
+
/** Source-relative path the file was discovered at. */
|
|
1184
|
+
rel: string;
|
|
1185
|
+
/** Final logical key for the manifest, including the namespace prefix. */
|
|
1186
|
+
key: string;
|
|
1187
|
+
}
|
|
1188
|
+
interface GroupAssetKeysOptions {
|
|
1189
|
+
/**
|
|
1190
|
+
* Strategy applied to this namespace. Default `'auto'`.
|
|
1191
|
+
*
|
|
1192
|
+
* Adopters override per-namespace via `assetMap.<ns>.keys` in
|
|
1193
|
+
* kick.config.ts when they want uniform behaviour (e.g. always
|
|
1194
|
+
* include the extension regardless of collisions).
|
|
1195
|
+
*/
|
|
1196
|
+
strategy?: AssetKeyStrategy;
|
|
1197
|
+
}
|
|
1198
|
+
interface GroupAssetKeysResult {
|
|
1199
|
+
pairs: AssetKeyPair[];
|
|
1200
|
+
/**
|
|
1201
|
+
* Number of collision groups resolved by keeping extensions.
|
|
1202
|
+
* Surfaces as an informational log ("auto-resolved N collisions")
|
|
1203
|
+
* so adopters know whether they're relying on auto-mode or not.
|
|
1204
|
+
* Always 0 for `'strip'` (overwrites) and `'with-extension'`
|
|
1205
|
+
* (extensions always preserved, no collision possible).
|
|
1206
|
+
*/
|
|
1207
|
+
collisionGroupsResolved: number;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Compute final manifest keys for a namespace's discovered paths,
|
|
1211
|
+
* given the chosen strategy. Pure function — no fs / config / globals
|
|
1212
|
+
* involved.
|
|
1213
|
+
*
|
|
1214
|
+
* @param namespace assetMap key — e.g. `'mails'`, `'pages'`.
|
|
1215
|
+
* @param paths source-relative file paths (POSIX separators) discovered
|
|
1216
|
+
* under the namespace's `src` directory.
|
|
1217
|
+
* @param opts grouping options.
|
|
1218
|
+
*/
|
|
1219
|
+
declare function groupAssetKeys(namespace: string, paths: readonly string[], opts?: GroupAssetKeysOptions): GroupAssetKeysResult;
|
|
1220
|
+
/** Strip the final extension from a path. `'mails/welcome.ejs'` → `'mails/welcome'`. */
|
|
1221
|
+
declare function stripExt(path: string): string;
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/http/context.d.ts
|
|
1224
|
+
/**
|
|
1225
|
+
* Input shape for the typed problem-detail convenience methods on
|
|
1226
|
+
* {@link ProblemSender} (`ctx.problem.notFound`, `.badRequest`, etc.) —
|
|
1227
|
+
* RFC 9457 details minus the `status`, which each method pre-fills.
|
|
1228
|
+
*/
|
|
1229
|
+
type ProblemInput = Omit<ProblemDetails, 'status'>;
|
|
1230
|
+
/**
|
|
1231
|
+
* Callable response helper for RFC 9457 — Problem Details responses.
|
|
1232
|
+
* Returned by `ctx.problem`. Invoke directly for a fully-specified
|
|
1233
|
+
* payload, or use the named convenience methods to pre-fill `status` +
|
|
1234
|
+
* `title` for the common codes.
|
|
1235
|
+
*
|
|
1236
|
+
* Each call sets `Content-Type: application/problem+json` and serializes
|
|
1237
|
+
* the canonical shape (defaulting `type` to `about:blank` and `title` to
|
|
1238
|
+
* the IANA reason phrase per RFC 9457 §3.1.1 / §3.1.4).
|
|
1239
|
+
*
|
|
1240
|
+
* @example
|
|
1241
|
+
* ```ts
|
|
1242
|
+
* ctx.problem({
|
|
1243
|
+
* type: 'https://api.example.com/problems/out-of-credit',
|
|
1244
|
+
* status: 403,
|
|
1245
|
+
* detail: 'Your balance is 30, but that costs 50.',
|
|
1246
|
+
* balance: 30, // extension per §3.2
|
|
1247
|
+
* })
|
|
1248
|
+
*
|
|
1249
|
+
* ctx.problem.notFound({ detail: 'User abc not found' })
|
|
1250
|
+
* ctx.problem.validation(zodResult.error.issues)
|
|
1251
|
+
* ```
|
|
1252
|
+
*/
|
|
1253
|
+
interface ProblemSender {
|
|
1254
|
+
(input: ProblemDetails): RuntimeResponse;
|
|
1255
|
+
/** 400 Bad Request — RFC 9457. */
|
|
1256
|
+
badRequest(input?: ProblemInput): RuntimeResponse;
|
|
1257
|
+
/** 401 Unauthorized — RFC 9457. */
|
|
1258
|
+
unauthorized(input?: ProblemInput): RuntimeResponse;
|
|
1259
|
+
/** 403 Forbidden — RFC 9457. */
|
|
1260
|
+
forbidden(input?: ProblemInput): RuntimeResponse;
|
|
1261
|
+
/** 404 Not Found — RFC 9457. */
|
|
1262
|
+
notFound(input?: ProblemInput): RuntimeResponse;
|
|
1263
|
+
/** 409 Conflict — RFC 9457. */
|
|
1264
|
+
conflict(input?: ProblemInput): RuntimeResponse;
|
|
1265
|
+
/** 422 Unprocessable Entity — RFC 9457. */
|
|
1266
|
+
unprocessable(input?: ProblemInput): RuntimeResponse;
|
|
1267
|
+
/** 429 Too Many Requests — RFC 9457. */
|
|
1268
|
+
tooManyRequests(input?: ProblemInput): RuntimeResponse;
|
|
1269
|
+
/** 500 Internal Server Error — RFC 9457. */
|
|
1270
|
+
internal(input?: ProblemInput): RuntimeResponse;
|
|
1271
|
+
/**
|
|
1272
|
+
* 422 with Zod issues serialized into the `errors` extension per
|
|
1273
|
+
* RFC 9457 §3.2's array convention. Pass `error.issues` from a Zod
|
|
1274
|
+
* `safeParse` failure or `error.issues` from a thrown ZodError.
|
|
1275
|
+
*/
|
|
1276
|
+
validation(issues: any[], input?: ProblemInput): RuntimeResponse;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Recursively marks every property of `T` as `readonly`. Preserves
|
|
1280
|
+
* `Function`, `Date`, and `RegExp` unchanged, and converts `Map` / `Set`
|
|
1281
|
+
* to `ReadonlyMap` / `ReadonlySet` so their mutating methods (`set`,
|
|
1282
|
+
* `delete`, `add`, `clear`) are removed from the public surface.
|
|
1283
|
+
* Tuples keep their positional element types — only their per-element
|
|
1284
|
+
* mutability is locked.
|
|
1285
|
+
*
|
|
1286
|
+
* Exposed as a utility for adopters who want to opt into compile-time
|
|
1287
|
+
* sealing of their own shapes. **No longer applied to
|
|
1288
|
+
* `RequestContext.{body,params,query,headers,file,files}`** — that
|
|
1289
|
+
* broke TS narrowing / discriminated-union drilldowns on typed request
|
|
1290
|
+
* payloads. Runtime read-only enforcement now lives in
|
|
1291
|
+
* {@link makeReadOnlyProxy} (dev-only Proxy that warns on writes,
|
|
1292
|
+
* no-op in production).
|
|
1293
|
+
*
|
|
1294
|
+
* Type-only. No runtime cost. `DeepReadonly<any>` is `any` (TS
|
|
1295
|
+
* distributes conditional types over `any`).
|
|
1296
|
+
*/
|
|
1297
|
+
type DeepReadonly<T> = T extends ((...args: never[]) => unknown) ? T : T extends Date | RegExp ? T : T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer V> ? ReadonlySet<DeepReadonly<V>> : T extends readonly (infer U)[] ? number extends T['length'] ? ReadonlyArray<DeepReadonly<U>> : { readonly [K in keyof T]: DeepReadonly<T[K]> } : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
|
|
1298
|
+
/**
|
|
1299
|
+
* The shape of a single route entry in `KickRoutes`. Generated by
|
|
1300
|
+
* `kick typegen` and consumed via the `Ctx<T>` helper. Each field is
|
|
1301
|
+
* `unknown` by default and is filled in as the typegen learns more
|
|
1302
|
+
* about the route (URL params from the path, body/query from validation
|
|
1303
|
+
* schemas, etc.).
|
|
1304
|
+
*/
|
|
1305
|
+
interface RouteShape {
|
|
1306
|
+
params?: unknown;
|
|
1307
|
+
body?: unknown;
|
|
1308
|
+
query?: unknown;
|
|
1309
|
+
response?: unknown;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Unified request/response abstraction passed to every controller method.
|
|
1313
|
+
* Shields handlers from raw Express objects and provides convenience methods.
|
|
1314
|
+
*
|
|
1315
|
+
* Implements {@link ExecutionContext} so the transport-agnostic Context
|
|
1316
|
+
* Contributor pipeline (#107) can run against this concrete HTTP context
|
|
1317
|
+
* the same way it will run against future WS / queue / cron contexts.
|
|
1318
|
+
*/
|
|
1319
|
+
declare class RequestContext<TBody = any, TParams = any, TQuery = any> implements ExecutionContext {
|
|
1320
|
+
readonly req: ActiveRuntime['request'];
|
|
1321
|
+
readonly res: ActiveRuntime['response'];
|
|
1322
|
+
readonly next: NextFunction;
|
|
1323
|
+
/**
|
|
1324
|
+
* Engine-agnostic response driver the response helpers write through. Under
|
|
1325
|
+
* the Express runtime it IS `res` (Express's Response satisfies
|
|
1326
|
+
* {@link RuntimeResponse} structurally); other runtimes pass a thin wrapper
|
|
1327
|
+
* over their native reply as the optional fourth argument.
|
|
1328
|
+
*/
|
|
1329
|
+
private readonly _response;
|
|
1330
|
+
constructor(req: ActiveRuntime['request'], res: ActiveRuntime['response'], next: NextFunction, responseDriver?: RuntimeResponse);
|
|
1331
|
+
/**
|
|
1332
|
+
* Memo of the most recent {@link qs} call this request. The query
|
|
1333
|
+
* object is immutable for a request's lifetime, so a repeat call with
|
|
1334
|
+
* the same `(fieldConfig, options)` references returns the cached
|
|
1335
|
+
* parse instead of re-walking the filter/sort strings.
|
|
1336
|
+
*/
|
|
1337
|
+
private _qsMemo?;
|
|
1338
|
+
/**
|
|
1339
|
+
* Read-side accessor for the per-request metadata map.
|
|
1340
|
+
*
|
|
1341
|
+
* Returns the AsyncLocalStorage map (`requestStore.getStore().values`)
|
|
1342
|
+
* shared with the Context Contributor pipeline when an ALS frame is
|
|
1343
|
+
* active. Returns `null` outside any frame — read-side callers
|
|
1344
|
+
* (`ctx.get`, `ctx.user`, `ctx.tenantId`, `ctx.roles`) treat that as
|
|
1345
|
+
* "no metadata" and fall through to their `req.*` defaults. Reads
|
|
1346
|
+
* never throw because outside an ALS frame nobody could have written
|
|
1347
|
+
* via `ctx.set` anyway, so an empty answer is the truthful one.
|
|
1348
|
+
*/
|
|
1349
|
+
private metadataReadOnly;
|
|
1350
|
+
/**
|
|
1351
|
+
* Write-side accessor for the per-request metadata map.
|
|
1352
|
+
*
|
|
1353
|
+
* Returns the same map as {@link metadataReadOnly} when an ALS frame
|
|
1354
|
+
* is active. Throws otherwise — writing to a fallback map outside
|
|
1355
|
+
* the ALS frame would silently lose data because other call sites
|
|
1356
|
+
* (downstream contributors, request-logger, etc.) read from the
|
|
1357
|
+
* canonical store and would never see it. Mount
|
|
1358
|
+
* `requestScopeMiddleware()` (Application does this for you in
|
|
1359
|
+
* `'auto'` mode) or wrap your test setup in
|
|
1360
|
+
* `requestStore.run({...}, () => …)` to fix.
|
|
1361
|
+
*/
|
|
1362
|
+
private metadataForWrite;
|
|
1363
|
+
/**
|
|
1364
|
+
* Parsed request body. Wrapped in a dev-only Proxy that warns when
|
|
1365
|
+
* adopters try to mutate Zod-validated payloads in place — compute
|
|
1366
|
+
* new values and stash them via `ctx.set('key', value)` or a Context
|
|
1367
|
+
* Contributor instead. Reach for `ctx.req.body` if you genuinely
|
|
1368
|
+
* need the raw, mutable Express handle (rare).
|
|
1369
|
+
*
|
|
1370
|
+
* In production the Proxy is bypassed and `this.req.body` is
|
|
1371
|
+
* returned as-is — no runtime cost on the hot path.
|
|
1372
|
+
*/
|
|
1373
|
+
get body(): TBody;
|
|
1374
|
+
/** Path parameters parsed from the route — read-only, see {@link body}. */
|
|
1375
|
+
get params(): TParams;
|
|
1376
|
+
/** Parsed query string — read-only, see {@link body}. */
|
|
1377
|
+
get query(): TQuery;
|
|
1378
|
+
/** Inbound HTTP headers — read-only, see {@link body}. */
|
|
1379
|
+
get headers(): Request['headers'];
|
|
1380
|
+
get requestId(): string | undefined;
|
|
1381
|
+
/**
|
|
1382
|
+
* `AbortSignal` that fires when the underlying HTTP request is
|
|
1383
|
+
* closed — either because the client disconnected before the
|
|
1384
|
+
* response was sent, or because Node's socket-close event ran
|
|
1385
|
+
* after a normal response. Multiple `RequestContext` wrappers for
|
|
1386
|
+
* the same request (router-builder constructs one per
|
|
1387
|
+
* middleware/contributor pipeline / main handler) share a single
|
|
1388
|
+
* `AbortController` cached on the underlying `req` object, so the
|
|
1389
|
+
* signal is stable across the request lifecycle.
|
|
1390
|
+
*
|
|
1391
|
+
* Thread it through anything that takes an `AbortSignal` so the
|
|
1392
|
+
* work cancels as soon as the client gives up — e.g. a kickjs-db
|
|
1393
|
+
* relational query:
|
|
1394
|
+
*
|
|
1395
|
+
* ```ts
|
|
1396
|
+
* @Get('/:id/full')
|
|
1397
|
+
* async show(ctx: RequestContext) {
|
|
1398
|
+
* const row = await this.repo.findFullById(ctx.params.id, ctx.signal)
|
|
1399
|
+
* if (!row) return ctx.notFound()
|
|
1400
|
+
* ctx.json(row)
|
|
1401
|
+
* }
|
|
1402
|
+
* ```
|
|
1403
|
+
*
|
|
1404
|
+
* The repo passes `signal` to `db.query.<table>.findUnique({ signal })`;
|
|
1405
|
+
* if the client disconnects mid-flight, the in-flight query rejects
|
|
1406
|
+
* with `RelationalQueryCancelledError` instead of consuming the
|
|
1407
|
+
* connection until completion.
|
|
1408
|
+
*
|
|
1409
|
+
* Implementation note — both `req.on('close')` and `res.on('close')`
|
|
1410
|
+
* are wired so the signal aborts regardless of whether the close
|
|
1411
|
+
* event lands on the request stream or the response stream first.
|
|
1412
|
+
* Subsequent fires are no-ops (`AbortController.abort()` is
|
|
1413
|
+
* idempotent).
|
|
1414
|
+
*/
|
|
1415
|
+
get signal(): AbortSignal;
|
|
1416
|
+
/** Session data (requires session middleware) */
|
|
1417
|
+
get session(): any;
|
|
1418
|
+
/**
|
|
1419
|
+
* The authenticated user set by AuthAdapter.
|
|
1420
|
+
* Reads from the per-request metadata store first (`ctx.set('user', ...)`),
|
|
1421
|
+
* then falls back to `req.user` (set directly by AuthAdapter middleware).
|
|
1422
|
+
*
|
|
1423
|
+
* Extend the `AuthUser` interface via module augmentation in `@forinda/kickjs-auth`
|
|
1424
|
+
* to get full typing.
|
|
1425
|
+
*
|
|
1426
|
+
* @example
|
|
1427
|
+
* ```ts
|
|
1428
|
+
* @Get('/me')
|
|
1429
|
+
* @Authenticated()
|
|
1430
|
+
* getProfile(ctx: RequestContext) {
|
|
1431
|
+
* const user = ctx.user
|
|
1432
|
+
* if (!user) return ctx.notFound()
|
|
1433
|
+
* return ctx.json({ user })
|
|
1434
|
+
* }
|
|
1435
|
+
* ```
|
|
1436
|
+
*/
|
|
1437
|
+
get user(): MetaValue<'user', Record<string, any>> | undefined;
|
|
1438
|
+
/**
|
|
1439
|
+
* Tenant identifier for multi-tenant apps. Reads `user.tenantId` —
|
|
1440
|
+
* populated either by `AuthAdapter.testMode({ tenantId })`, a tenant
|
|
1441
|
+
* resolver middleware, or manually on the strategy's `mapPayload`.
|
|
1442
|
+
*
|
|
1443
|
+
* Augment `AuthUser` (or `ContextMeta['user']`) to widen the return
|
|
1444
|
+
* type if your user shape guarantees a non-`string | undefined` id.
|
|
1445
|
+
*/
|
|
1446
|
+
get tenantId(): string | undefined;
|
|
1447
|
+
/**
|
|
1448
|
+
* Effective role list for the current user. Prefers `user.tenantRoles`
|
|
1449
|
+
* (populated by `AuthAdapterOptions.roleResolver` under a tenant) and
|
|
1450
|
+
* falls back to `user.roles`. Returns an empty array when the user is
|
|
1451
|
+
* absent or the shape has neither field.
|
|
1452
|
+
*/
|
|
1453
|
+
get roles(): string[];
|
|
1454
|
+
/**
|
|
1455
|
+
* Parse the request query string into structured filters, sort, pagination, and search.
|
|
1456
|
+
* Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).
|
|
1457
|
+
*
|
|
1458
|
+
* Generic over the field config: when passed an inline literal (or
|
|
1459
|
+
* `as const`), `filters[].field` and `sort[].field` are narrowed to
|
|
1460
|
+
* the exact whitelisted field names. Pass nothing or a non-literal
|
|
1461
|
+
* config and you get the loose `string` fallback.
|
|
1462
|
+
*
|
|
1463
|
+
* @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields
|
|
1464
|
+
* @param options - Limits (`maxLimit`, `defaultLimit`, `maxSearchLength`) and an
|
|
1465
|
+
* `onReject` hook. By default a rejected/over-broad field is logged via
|
|
1466
|
+
* `console.warn` with the request id; pass an explicit `onReject` to override
|
|
1467
|
+
* (e.g. throw to return a 400), or `onReject: () => {}` to silence.
|
|
1468
|
+
*
|
|
1469
|
+
* @example
|
|
1470
|
+
* ```ts
|
|
1471
|
+
* @Get('/')
|
|
1472
|
+
* async list(ctx: RequestContext) {
|
|
1473
|
+
* const parsed = ctx.qs({
|
|
1474
|
+
* filterable: ['status', 'priority'],
|
|
1475
|
+
* sortable: ['createdAt', 'title'],
|
|
1476
|
+
* } as const)
|
|
1477
|
+
* parsed.filters[0].field // typed as 'status' | 'priority'
|
|
1478
|
+
* parsed.sort[0].field // typed as 'createdAt' | 'title'
|
|
1479
|
+
* }
|
|
1480
|
+
* ```
|
|
1481
|
+
*/
|
|
1482
|
+
qs<TConfig extends QueryFieldConfig | undefined = undefined>(fieldConfig?: TConfig, options?: ParseQueryOptions): TypedParsedQuery<TConfig>;
|
|
1483
|
+
/**
|
|
1484
|
+
* Single uploaded file (requires `@FileUpload({ mode: 'single' })`).
|
|
1485
|
+
* Wrapped in a dev-only Proxy that warns on mutation — see {@link body}.
|
|
1486
|
+
* File metadata is set by the upload middleware and shouldn't be
|
|
1487
|
+
* mutated downstream.
|
|
1488
|
+
*/
|
|
1489
|
+
get file(): Express.Multer.File | undefined;
|
|
1490
|
+
/**
|
|
1491
|
+
* Array of uploaded files (requires `@FileUpload({ mode: 'array' })`).
|
|
1492
|
+
* The array (and each entry in dev) is wrapped in a read-only Proxy
|
|
1493
|
+
* that warns on `push`/`pop` and per-file mutation. Use
|
|
1494
|
+
* `ctx.req.files` if you genuinely need the mutable Express handle.
|
|
1495
|
+
*/
|
|
1496
|
+
get files(): Express.Multer.File[] | undefined;
|
|
1497
|
+
/**
|
|
1498
|
+
* Read a value from the per-request metadata store.
|
|
1499
|
+
*
|
|
1500
|
+
* When `ContextMeta` has been augmented with a matching key, the return
|
|
1501
|
+
* type is inferred automatically. For ad-hoc keys, pass a generic:
|
|
1502
|
+
* `ctx.get<MyType>('custom')`.
|
|
1503
|
+
*/
|
|
1504
|
+
get<K extends string>(key: K): MetaValue<K> | undefined;
|
|
1505
|
+
/**
|
|
1506
|
+
* Write a value to the per-request metadata store. Throws when no
|
|
1507
|
+
* AsyncLocalStorage frame is active — see {@link metadataForWrite}
|
|
1508
|
+
* for why a silent fallback is the wrong default.
|
|
1509
|
+
*/
|
|
1510
|
+
set<K extends string>(key: K, value: MetaValue<K>): void;
|
|
1511
|
+
json(data: any, status?: number): RuntimeResponse;
|
|
1512
|
+
created(data: any): RuntimeResponse;
|
|
1513
|
+
noContent(): unknown;
|
|
1514
|
+
/**
|
|
1515
|
+
* @deprecated Prefer {@link ProblemSender.notFound | ctx.problem.notFound()},
|
|
1516
|
+
* which emits an RFC 9457 — Problem Details response. This helper stays
|
|
1517
|
+
* for backward compatibility and will not be removed.
|
|
1518
|
+
*/
|
|
1519
|
+
notFound(message?: string): RuntimeResponse;
|
|
1520
|
+
/**
|
|
1521
|
+
* @deprecated Prefer {@link ProblemSender.badRequest | ctx.problem.badRequest()},
|
|
1522
|
+
* which emits an RFC 9457 — Problem Details response. This helper stays
|
|
1523
|
+
* for backward compatibility and will not be removed.
|
|
1524
|
+
*/
|
|
1525
|
+
badRequest(message: string): RuntimeResponse;
|
|
1526
|
+
/**
|
|
1527
|
+
* RFC 9457 — Problem Details for HTTP APIs response helper.
|
|
1528
|
+
* https://datatracker.ietf.org/doc/html/rfc9457
|
|
1529
|
+
*
|
|
1530
|
+
* `ctx.problem(...)` sends a canonical problem-detail response with
|
|
1531
|
+
* `Content-Type: application/problem+json`. The convenience methods
|
|
1532
|
+
* (`ctx.problem.notFound`, `.badRequest`, etc.) pre-fill `status` +
|
|
1533
|
+
* `title` for the common codes.
|
|
1534
|
+
*
|
|
1535
|
+
* `type` defaults to `about:blank` (§3.1.1); `title` defaults to the
|
|
1536
|
+
* IANA reason phrase for `status` (§3.1.4). Extension members per §3.2
|
|
1537
|
+
* pass through unchanged.
|
|
1538
|
+
*
|
|
1539
|
+
* @example
|
|
1540
|
+
* ```ts
|
|
1541
|
+
* ctx.problem({
|
|
1542
|
+
* type: 'https://api.example.com/problems/out-of-credit',
|
|
1543
|
+
* status: 403,
|
|
1544
|
+
* detail: 'Your balance is 30, but that costs 50.',
|
|
1545
|
+
* instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,
|
|
1546
|
+
* balance: 30,
|
|
1547
|
+
* })
|
|
1548
|
+
*
|
|
1549
|
+
* ctx.problem.notFound({ detail: 'User abc not found' })
|
|
1550
|
+
* ctx.problem.validation(zodResult.error.issues)
|
|
1551
|
+
* ```
|
|
1552
|
+
*
|
|
1553
|
+
* @see {@link ProblemException} for the exception-throwing equivalent
|
|
1554
|
+
* (use it from services where `ctx` isn't in scope).
|
|
1555
|
+
*/
|
|
1556
|
+
get problem(): ProblemSender;
|
|
1557
|
+
private sendProblem;
|
|
1558
|
+
html(content: string, status?: number): RuntimeResponse;
|
|
1559
|
+
download(buffer: Buffer, filename: string, contentType?: string): RuntimeResponse;
|
|
1560
|
+
/**
|
|
1561
|
+
* Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).
|
|
1562
|
+
* Requires a ViewAdapter to be configured in bootstrap().
|
|
1563
|
+
*
|
|
1564
|
+
* @param template - Template name (without extension, relative to viewsDir)
|
|
1565
|
+
* @param data - Data to pass to the template
|
|
1566
|
+
*
|
|
1567
|
+
* @example
|
|
1568
|
+
* ```ts
|
|
1569
|
+
* ctx.render('dashboard', { user, title: 'Dashboard' })
|
|
1570
|
+
* ctx.render('emails/welcome', { name: 'Alice' })
|
|
1571
|
+
* ```
|
|
1572
|
+
*/
|
|
1573
|
+
render(template: string, data?: Record<string, any>): unknown;
|
|
1574
|
+
/**
|
|
1575
|
+
* Parse query params and return a standardized paginated response.
|
|
1576
|
+
* Calls `ctx.qs()` internally, then wraps your data with pagination meta.
|
|
1577
|
+
*
|
|
1578
|
+
* @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`
|
|
1579
|
+
* @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields
|
|
1580
|
+
*
|
|
1581
|
+
* @example
|
|
1582
|
+
* ```ts
|
|
1583
|
+
* @Get('/')
|
|
1584
|
+
* async list(ctx: RequestContext) {
|
|
1585
|
+
* return ctx.paginate(
|
|
1586
|
+
* async (parsed) => {
|
|
1587
|
+
* const data = await db.select().from(users)
|
|
1588
|
+
* .where(query.where).limit(parsed.pagination.limit)
|
|
1589
|
+
* .offset(parsed.pagination.offset).all()
|
|
1590
|
+
* const total = await db.select({ count: count() }).from(users).get()
|
|
1591
|
+
* return { data, total: total?.count ?? 0 }
|
|
1592
|
+
* },
|
|
1593
|
+
* { filterable: ['name', 'role'], sortable: ['createdAt'] },
|
|
1594
|
+
* )
|
|
1595
|
+
* }
|
|
1596
|
+
* ```
|
|
1597
|
+
*/
|
|
1598
|
+
paginate<T, TConfig extends QueryFieldConfig | undefined = undefined>(fetcher: (parsed: TypedParsedQuery<TConfig>) => Promise<{
|
|
1599
|
+
data: T[];
|
|
1600
|
+
total: number;
|
|
1601
|
+
}>, fieldConfig?: TConfig): Promise<RuntimeResponse>;
|
|
1602
|
+
/**
|
|
1603
|
+
* Start an SSE (Server-Sent Events) stream.
|
|
1604
|
+
* Sets the correct headers and returns helpers to send events.
|
|
1605
|
+
*
|
|
1606
|
+
* @example
|
|
1607
|
+
* ```ts
|
|
1608
|
+
* @Get('/events')
|
|
1609
|
+
* async stream(ctx: RequestContext) {
|
|
1610
|
+
* const sse = ctx.sse()
|
|
1611
|
+
*
|
|
1612
|
+
* const interval = setInterval(() => {
|
|
1613
|
+
* sse.send({ time: new Date().toISOString() }, 'tick')
|
|
1614
|
+
* }, 1000)
|
|
1615
|
+
*
|
|
1616
|
+
* sse.onClose(() => clearInterval(interval))
|
|
1617
|
+
* }
|
|
1618
|
+
* ```
|
|
1619
|
+
*/
|
|
1620
|
+
sse(): {
|
|
1621
|
+
/** Send an SSE event with optional event name and id */send: (data: any, event?: string, id?: string) => void; /** Send a comment (keeps connection alive) */
|
|
1622
|
+
comment: (text: string) => void; /** Register a callback when the client disconnects */
|
|
1623
|
+
onClose: (fn: () => void) => void; /** End the SSE stream */
|
|
1624
|
+
close: () => void;
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Short alias for `RequestContext` parameterised by a route shape.
|
|
1629
|
+
*
|
|
1630
|
+
* Use with `kick typegen`'s generated `KickRoutes` namespace for
|
|
1631
|
+
* fully-typed `ctx.params`, `ctx.body`, and `ctx.query`. Metadata
|
|
1632
|
+
* typing (`ctx.get()`, `ctx.set()`, `ctx.user`) comes from
|
|
1633
|
+
* `ContextMeta` augmentation and works on all `RequestContext` instances.
|
|
1634
|
+
*
|
|
1635
|
+
* @example
|
|
1636
|
+
* ```ts
|
|
1637
|
+
* @Get('/:id')
|
|
1638
|
+
* getUser(ctx: Ctx<KickRoutes.UserController['getUser']>) {
|
|
1639
|
+
* ctx.params.id // typed from RouteShape
|
|
1640
|
+
* ctx.body // typed from RouteShape
|
|
1641
|
+
* ctx.user // typed from ContextMeta
|
|
1642
|
+
* ctx.get('tenant') // typed from ContextMeta
|
|
1643
|
+
* }
|
|
1644
|
+
* ```
|
|
1645
|
+
*/
|
|
1646
|
+
type Ctx<TRoute extends RouteShape = RouteShape> = RequestContext<TRoute extends {
|
|
1647
|
+
body: infer B;
|
|
1648
|
+
} ? B : any, TRoute extends {
|
|
1649
|
+
params: infer P;
|
|
1650
|
+
} ? P : any, TRoute extends {
|
|
1651
|
+
query: infer Q;
|
|
1652
|
+
} ? Q : any>;
|
|
1653
|
+
/**
|
|
1654
|
+
* Global ambient registry of typed routes, populated by `kick typegen`.
|
|
1655
|
+
*
|
|
1656
|
+
* Each interface inside `KickRoutes` corresponds to a controller class;
|
|
1657
|
+
* each property is one route method on that controller. The values
|
|
1658
|
+
* conform to `RouteShape`.
|
|
1659
|
+
*
|
|
1660
|
+
* Empty by default — declarations come from `.kickjs/types/routes.d.ts`
|
|
1661
|
+
* generated alongside the rest of the typegen output.
|
|
1662
|
+
*/
|
|
1663
|
+
declare global {
|
|
1664
|
+
namespace KickRoutes {}
|
|
1665
|
+
} //# sourceMappingURL=context.d.ts.map
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region src/http/runtime.d.ts
|
|
1668
|
+
/** HTTP verbs the router emits. Mirrors the decorator surface (@Get/@Post/...). */
|
|
1669
|
+
type RouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
1670
|
+
/**
|
|
1671
|
+
* A context-based handler: the runtime-neutral unit of the request pipeline.
|
|
1672
|
+
* Contributors, route middleware (via {@link MiddlewareHandler}), and the
|
|
1673
|
+
* terminal controller call all reduce to this shape. The runtime owns how a
|
|
1674
|
+
* `CtxHandler` is wrapped onto its engine (Express wraps it in `(req, res,
|
|
1675
|
+
* next)`; Fastify in its own handler).
|
|
1676
|
+
*/
|
|
1677
|
+
type CtxHandler = (ctx: RequestContext) => unknown | Promise<unknown>;
|
|
1678
|
+
/**
|
|
1679
|
+
* Connect-style middleware — the portable middleware format. Built-in
|
|
1680
|
+
* middleware and adopter-supplied Express middleware are this shape. The
|
|
1681
|
+
* Express runtime hands them straight to `app.use`; later runtimes bridge
|
|
1682
|
+
* them (Fastify via `@fastify/middie`, h3 via its node adapter).
|
|
1683
|
+
*
|
|
1684
|
+
* Typed as the Express handler union for M1 (Express is the only runtime);
|
|
1685
|
+
* a future milestone narrows this to bare node `(req, res, next)` primitives
|
|
1686
|
+
* once the driver layer lands.
|
|
1687
|
+
*/
|
|
1688
|
+
type ConnectMiddleware = RequestHandler | ErrorRequestHandler;
|
|
1689
|
+
/**
|
|
1690
|
+
* One materialized route, as plain data. Produced by {@link buildRouteTable}
|
|
1691
|
+
* from decorator metadata; consumed by {@link HttpRuntime.mountRoutes}.
|
|
1692
|
+
*
|
|
1693
|
+
* The shape is deliberately richer than a flat `CtxHandler[]`: `middlewares`
|
|
1694
|
+
* keep the `(ctx, next)` signature (they may short-circuit or call `next(err)`),
|
|
1695
|
+
* `contributorRunner` is a closure over the pre-built pipeline + container (the
|
|
1696
|
+
* pipeline is built — and any cycle / missing-dep error thrown — at table-build
|
|
1697
|
+
* time, i.e. boot), and `validation` / `upload` stay as metadata the runtime
|
|
1698
|
+
* materializes its own way. Faithfully reproducing the legacy Express chain —
|
|
1699
|
+
* not collapsing it — is what keeps M1 behavior-neutral.
|
|
1700
|
+
*/
|
|
1701
|
+
interface RouteEntry {
|
|
1702
|
+
method: RouteMethod;
|
|
1703
|
+
/** Path with `:param` segments — the portable subset all engines accept. */
|
|
1704
|
+
path: string;
|
|
1705
|
+
/** Class + method middleware, in execution order; each receives `(ctx, next)`. */
|
|
1706
|
+
middlewares: MiddlewareHandler[];
|
|
1707
|
+
/**
|
|
1708
|
+
* Runs the contributor pipeline against `ctx` (populates `ctx.set(...)`),
|
|
1709
|
+
* or `null` when no contributors apply. Closes over the built pipeline and
|
|
1710
|
+
* the DI container so it stays engine-neutral; the runtime advances the
|
|
1711
|
+
* chain (Express: call `next()`) once it resolves.
|
|
1712
|
+
*/
|
|
1713
|
+
contributorRunner: CtxHandler | null;
|
|
1714
|
+
/** Terminal handler: resolves the controller per-request and invokes it. */
|
|
1715
|
+
handler: CtxHandler;
|
|
1716
|
+
/** Introspection + runtime-materialized concerns (swagger/typegen read this). */
|
|
1717
|
+
meta: RouteMeta;
|
|
1718
|
+
}
|
|
1719
|
+
/** Per-route metadata: introspection refs plus runtime-materialized concerns. */
|
|
1720
|
+
interface RouteMeta {
|
|
1721
|
+
/**
|
|
1722
|
+
* Owning controller class — for DI resolution and adapter introspection.
|
|
1723
|
+
* Absent for ad-hoc routes registered through {@link AdapterHttp.route},
|
|
1724
|
+
* which have no controller class behind them.
|
|
1725
|
+
*/
|
|
1726
|
+
controller?: Constructor;
|
|
1727
|
+
/** Handler method name on the controller, when there is one. */
|
|
1728
|
+
handlerName?: string;
|
|
1729
|
+
/** Zod/JSON-schema validation config from the route decorator, if any. */
|
|
1730
|
+
validation?: RouteDefinition['validation'];
|
|
1731
|
+
/** `@FileUpload` config, if present — the runtime supplies the backend. */
|
|
1732
|
+
upload?: FileUploadConfig;
|
|
1733
|
+
}
|
|
1734
|
+
/** A controller's routes grouped under the module mount prefix. */
|
|
1735
|
+
type RouteTable = {
|
|
1736
|
+
mountPath: string;
|
|
1737
|
+
routes: RouteEntry[];
|
|
1738
|
+
}[];
|
|
1739
|
+
/**
|
|
1740
|
+
* The response surface {@link RequestContext}'s helpers (`json` / `html` /
|
|
1741
|
+
* `sse` / `download` / `render` / `problem`) drive, instead of calling Express
|
|
1742
|
+
* `res` methods directly (spec §4.3). Sized so `express.Response` satisfies it
|
|
1743
|
+
* structurally — under the Express runtime the driver IS the Express response,
|
|
1744
|
+
* so there is no wrapping and no behavior change. Other runtimes (Fastify / h3)
|
|
1745
|
+
* provide a thin object implementing this over their native reply.
|
|
1746
|
+
*
|
|
1747
|
+
* The terminal methods return `RuntimeResponse` for fluent chaining (Express's
|
|
1748
|
+
* `Response` is assignable, since it is a superset).
|
|
1749
|
+
*/
|
|
1750
|
+
interface RuntimeResponse {
|
|
1751
|
+
status(code: number): RuntimeResponse;
|
|
1752
|
+
json(data: unknown): RuntimeResponse;
|
|
1753
|
+
send(data: unknown): RuntimeResponse;
|
|
1754
|
+
type(contentType: string): RuntimeResponse;
|
|
1755
|
+
setHeader(name: string, value: string | number | readonly string[]): unknown;
|
|
1756
|
+
render(view: string, data?: Record<string, unknown>): unknown;
|
|
1757
|
+
writeHead(statusCode: number, headers?: Record<string, string>): unknown;
|
|
1758
|
+
flushHeaders(): unknown;
|
|
1759
|
+
write(chunk: string | Buffer): boolean;
|
|
1760
|
+
end(data?: unknown): unknown;
|
|
1761
|
+
once(event: 'close', listener: () => void): unknown;
|
|
1762
|
+
readonly headersSent: boolean;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Per-runtime type map: the engine-native request / response / app types an
|
|
1766
|
+
* `HttpRuntime` exposes through `ctx.req.raw` / `ctx.res.raw` / `AdapterContext.app`.
|
|
1767
|
+
*/
|
|
1768
|
+
interface RuntimeTypeMap {
|
|
1769
|
+
request: unknown;
|
|
1770
|
+
response: unknown;
|
|
1771
|
+
app: unknown;
|
|
1772
|
+
}
|
|
1773
|
+
/** The Express runtime's native types — the default when nothing is augmented. */
|
|
1774
|
+
interface ExpressRuntimeTypes {
|
|
1775
|
+
request: Request;
|
|
1776
|
+
response: Response;
|
|
1777
|
+
app: Express$1;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Augmentable runtime registry. Empty by default; the `kick/runtime` typegen
|
|
1781
|
+
* plugin emits a `{ runtime: <RuntimeTypeMap> }` augmentation from the configured
|
|
1782
|
+
* runtime, which flips the engine-native escape-hatch types — `AdapterContext.app`,
|
|
1783
|
+
* `getRuntimeApp()`, and (once the driver layer lands) `ctx.req.raw` / `ctx.res.raw`
|
|
1784
|
+
* — to that runtime. Mirrors `KickDbRegister` / `KickEnv`, so the type story is
|
|
1785
|
+
* uniform with `kick/db` and env typing.
|
|
1786
|
+
*
|
|
1787
|
+
* @example
|
|
1788
|
+
* ```ts
|
|
1789
|
+
* // .kickjs/types/kick__runtime.d.ts — generated by typegen
|
|
1790
|
+
* declare module '@forinda/kickjs' {
|
|
1791
|
+
* interface KickRuntimeRegister {
|
|
1792
|
+
* runtime: import('@forinda/kickjs/fastify').FastifyRuntimeTypes
|
|
1793
|
+
* }
|
|
1794
|
+
* }
|
|
1795
|
+
* ```
|
|
1796
|
+
*/
|
|
1797
|
+
interface KickRuntimeRegister {}
|
|
1798
|
+
/** The active runtime's type map — the augmented one, or Express by default. */
|
|
1799
|
+
type ActiveRuntime = KickRuntimeRegister extends {
|
|
1800
|
+
runtime: infer R extends RuntimeTypeMap;
|
|
1801
|
+
} ? R : ExpressRuntimeTypes;
|
|
1802
|
+
/** Options passed to {@link HttpRuntime.createApp}. */
|
|
1803
|
+
interface RuntimeAppOptions {
|
|
1804
|
+
/**
|
|
1805
|
+
* Express `trust proxy` setting (and the equivalent on other engines). The
|
|
1806
|
+
* function form is Express-specific; other runtimes interpret the simpler
|
|
1807
|
+
* shapes (or normalize X-Forwarded-For themselves — see spec §10 Q1).
|
|
1808
|
+
*/
|
|
1809
|
+
trustProxy?: boolean | string | number | string[] | ((ip: string, hopIndex: number) => boolean);
|
|
1810
|
+
}
|
|
1811
|
+
/** Where a connect middleware sits and what it scopes to. */
|
|
1812
|
+
interface UseConnectOptions {
|
|
1813
|
+
path?: string | RegExp | ReadonlyArray<string | RegExp>;
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Optional engine capabilities. Absence means the corresponding `ctx` feature
|
|
1817
|
+
* raises a clear error on that runtime rather than failing obscurely.
|
|
1818
|
+
*/
|
|
1819
|
+
interface RuntimeCapabilities {
|
|
1820
|
+
/** View-engine rendering (`ctx.render`). Express: true; Fastify/h3: false initially. */
|
|
1821
|
+
render: boolean;
|
|
1822
|
+
/** File uploads (`ctx.file` / `@FileUpload`). All engines, different backends. */
|
|
1823
|
+
uploads: boolean;
|
|
1824
|
+
/** Connect-style middleware. Express/Fastify(middie): true; h3: best-effort. */
|
|
1825
|
+
connectMiddleware: boolean;
|
|
1826
|
+
/**
|
|
1827
|
+
* The engine parses request bodies itself (Fastify, h3). When true, the
|
|
1828
|
+
* Application skips its default `express.json()` so the body stream isn't
|
|
1829
|
+
* consumed twice (which would deadlock the engine's own parser). Express:
|
|
1830
|
+
* false — it relies on the `express.json()` connect middleware.
|
|
1831
|
+
*/
|
|
1832
|
+
nativeBodyParsing: boolean;
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* The engine driver. KickJS owns decorators → {@link RouteTable}, the
|
|
1836
|
+
* contributor pipeline, the `RequestContext` surface, and error mapping. The
|
|
1837
|
+
* runtime owns app/server creation, route materialization, connect-middleware
|
|
1838
|
+
* registration, static serving, and the terminal not-found / error handlers.
|
|
1839
|
+
*
|
|
1840
|
+
* `expressRuntime()` is the default and the M1 reference implementation.
|
|
1841
|
+
*/
|
|
1842
|
+
interface HttpRuntime<TApp = unknown> {
|
|
1843
|
+
readonly name: 'express' | 'fastify' | 'h3' | (string & {});
|
|
1844
|
+
/** Create the engine app. Called once per Application (and per HMR rebuild). */
|
|
1845
|
+
createApp(options?: RuntimeAppOptions): TApp;
|
|
1846
|
+
/**
|
|
1847
|
+
* Node-compatible request listener — the transport contract.
|
|
1848
|
+
* `http.createServer(handler)` in prod, Vite post-middleware in dev. MUST
|
|
1849
|
+
* invoke `next` (when given) instead of 404-ing if no route matched — the
|
|
1850
|
+
* Vite dev chain depends on fall-through.
|
|
1851
|
+
*/
|
|
1852
|
+
nodeHandler(app: TApp): (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;
|
|
1853
|
+
/** Materialize the framework-built route table onto the engine. */
|
|
1854
|
+
mountRoutes(app: TApp, table: RouteTable): void;
|
|
1855
|
+
/** Register a connect-style middleware (built-ins + adopter middleware). */
|
|
1856
|
+
useConnect(app: TApp, mw: ConnectMiddleware, opts?: UseConnectOptions): void;
|
|
1857
|
+
/** Serve a static directory (swagger-ui assets, devtools SPA). */
|
|
1858
|
+
serveStatic(app: TApp, prefix: string, dir: string): void;
|
|
1859
|
+
/** Terminal not-found handler. */
|
|
1860
|
+
setNotFound(app: TApp, mw: ConnectMiddleware): void;
|
|
1861
|
+
/** Terminal error handler. */
|
|
1862
|
+
setErrorHandler(app: TApp, mw: ConnectMiddleware): void;
|
|
1863
|
+
readonly capabilities: RuntimeCapabilities;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* The supported surface adapters use to add HTTP routes, mounts, static dirs,
|
|
1867
|
+
* and middleware — engine-agnostic, in place of reaching for the raw Express
|
|
1868
|
+
* `app` (spec §4.4). Exposed as {@link AdapterContext.http}. Built by the
|
|
1869
|
+
* Application over the active {@link HttpRuntime}, so an adapter written against
|
|
1870
|
+
* this works under any runtime; the raw `app` escape hatch stays available for
|
|
1871
|
+
* the rare engine-specific need.
|
|
1872
|
+
*/
|
|
1873
|
+
interface AdapterHttp {
|
|
1874
|
+
/** Register a single context-handler route (e.g. a docs or transport endpoint). */
|
|
1875
|
+
route(method: RouteMethod, path: string, handler: CtxHandler): void;
|
|
1876
|
+
/** Mount a pre-built route table under a path prefix. */
|
|
1877
|
+
mount(prefix: string, routes: RouteEntry[]): void;
|
|
1878
|
+
/** Serve a static directory under a path prefix. */
|
|
1879
|
+
serveStatic(prefix: string, dir: string): void;
|
|
1880
|
+
/** Register a connect-style middleware (optionally path-scoped). */
|
|
1881
|
+
use(mw: ConnectMiddleware, opts?: UseConnectOptions): void;
|
|
1882
|
+
}
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/core/adapter.d.ts
|
|
1885
|
+
/**
|
|
1886
|
+
* Where in the middleware pipeline an adapter's middleware should be inserted.
|
|
1887
|
+
*
|
|
1888
|
+
* - `beforeGlobal` — runs **before** any user-defined global middleware.
|
|
1889
|
+
* Suitable for transport-level concerns (request tagging, low-level
|
|
1890
|
+
* request rewriting). Fires on every request.
|
|
1891
|
+
* - `afterGlobal` — runs **after** global middleware, **before** module
|
|
1892
|
+
* routes are matched. Default for most adapter middleware (auth pre-checks,
|
|
1893
|
+
* tenant resolution, body shaping). Fires on every request.
|
|
1894
|
+
* - `beforeRoutes` — runs just before module routes are mounted. Functionally
|
|
1895
|
+
* equivalent to `afterGlobal` for most cases; the explicit phase exists so
|
|
1896
|
+
* adapters that *must* sit at the very end of the pre-route stack can
|
|
1897
|
+
* declare intent. Fires on every request.
|
|
1898
|
+
* - `afterRoutes` — sits **after** module routes but **before** error
|
|
1899
|
+
* handlers. **Fires only on fall-through** — when no route matched (404
|
|
1900
|
+
* path) or a route handler called `next()` without ending the response.
|
|
1901
|
+
* Controllers that respond with `ctx.json(...)` end the chain and skip
|
|
1902
|
+
* this phase, so it is *not* a "post-response" hook. For per-response
|
|
1903
|
+
* work (logging, metrics) use `res.on('finish', ...)` from inside an
|
|
1904
|
+
* earlier-phase middleware instead.
|
|
1905
|
+
*/
|
|
1906
|
+
type MiddlewarePhase = 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes';
|
|
1907
|
+
/**
|
|
1908
|
+
* Path scope for an {@link AdapterMiddleware} entry. Mirrors what
|
|
1909
|
+
* Express's `app.use(path, handler)` accepts so adapters get the full
|
|
1910
|
+
* range without learning a new mini-language:
|
|
1911
|
+
*
|
|
1912
|
+
* - A `string` prefix — `'/api'` matches `/api`, `/api/v1`, `/api/v1/x`.
|
|
1913
|
+
* - A `RegExp` — `/^\/api\/v\d+/` matches `/api/v1`, `/api/v42`.
|
|
1914
|
+
* - An array of either — `['/api', '/admin']` matches either prefix;
|
|
1915
|
+
* `['/api', /^\/internal\//]` mixes shapes.
|
|
1916
|
+
*
|
|
1917
|
+
* Omit to apply the middleware unconditionally (Express `app.use(handler)`).
|
|
1918
|
+
*/
|
|
1919
|
+
type MiddlewarePath = string | RegExp | ReadonlyArray<string | RegExp>;
|
|
1920
|
+
/** A middleware entry contributed by an adapter */
|
|
1921
|
+
interface AdapterMiddleware {
|
|
1922
|
+
/**
|
|
1923
|
+
* Express handler: either a `RequestHandler` (`(req, res, next) => …`)
|
|
1924
|
+
* or an `ErrorRequestHandler` (`(err, req, res, next) => …`).
|
|
1925
|
+
* Express dispatches based on arity, so the same field accepts both
|
|
1926
|
+
* without a discriminant.
|
|
1927
|
+
*/
|
|
1928
|
+
handler: RequestHandler | ErrorRequestHandler;
|
|
1929
|
+
/** Which phase to insert into (default: 'afterGlobal') */
|
|
1930
|
+
phase?: MiddlewarePhase;
|
|
1931
|
+
/**
|
|
1932
|
+
* Optional path scope. See {@link MiddlewarePath} for the accepted
|
|
1933
|
+
* shapes (string prefix, RegExp, or an array mixing both). Omit to
|
|
1934
|
+
* apply the middleware to every request that reaches this phase.
|
|
1935
|
+
*/
|
|
1936
|
+
path?: MiddlewarePath;
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Context passed to adapter lifecycle hooks.
|
|
1940
|
+
* Populated by the Application — provides access to the Express app,
|
|
1941
|
+
* DI container, http.Server, and environment info without requiring
|
|
1942
|
+
* external type imports. Cast `app` or `server` if you need specific types.
|
|
1943
|
+
*
|
|
1944
|
+
* @example
|
|
1945
|
+
* ```ts
|
|
1946
|
+
* beforeMount({ app, container }: AdapterContext) {
|
|
1947
|
+
* app.use(myMiddleware())
|
|
1948
|
+
* }
|
|
1949
|
+
* ```
|
|
1950
|
+
*/
|
|
1951
|
+
interface AdapterContext {
|
|
1952
|
+
/**
|
|
1953
|
+
* Engine-agnostic HTTP surface — the supported way to add routes, mounts,
|
|
1954
|
+
* static dirs, and middleware (spec §4.4). Prefer this over {@link app}:
|
|
1955
|
+
* an adapter written against `http` works under any runtime.
|
|
1956
|
+
*/
|
|
1957
|
+
http: AdapterHttp;
|
|
1958
|
+
/**
|
|
1959
|
+
* Engine-native application instance — Express under the default runtime,
|
|
1960
|
+
* typed from the active runtime via {@link ActiveRuntime} (spec §4.3b).
|
|
1961
|
+
* Escape hatch for genuinely engine-specific needs; for everything else use
|
|
1962
|
+
* {@link http}.
|
|
1963
|
+
*/
|
|
1964
|
+
app: ActiveRuntime['app'];
|
|
1965
|
+
/** DI container */
|
|
1966
|
+
container: Container;
|
|
1967
|
+
/** Node.js http.Server — only available in afterStart */
|
|
1968
|
+
server?: http.Server;
|
|
1969
|
+
/** Current NODE_ENV value (default: 'development') */
|
|
1970
|
+
env: string;
|
|
1971
|
+
/** true when NODE_ENV === 'production' */
|
|
1972
|
+
isProduction: boolean;
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Adapters plug into the Application lifecycle.
|
|
1976
|
+
* Implement this to add WebSocket, database, rate limiting, docs, etc.
|
|
1977
|
+
*
|
|
1978
|
+
* All hooks receive an `AdapterContext` populated by the framework.
|
|
1979
|
+
* No external type imports needed — the context is fully typed from KickJS internals.
|
|
1980
|
+
*
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```ts
|
|
1983
|
+
* import type { AppAdapter, AdapterContext } from '@forinda/kickjs/adapter'
|
|
1984
|
+
*
|
|
1985
|
+
* class SentryAdapter implements AppAdapter {
|
|
1986
|
+
* name = 'SentryAdapter'
|
|
1987
|
+
*
|
|
1988
|
+
* beforeMount({ app, container }: AdapterContext) {
|
|
1989
|
+
* app.use(Sentry.expressRequestHandler())
|
|
1990
|
+
* }
|
|
1991
|
+
*
|
|
1992
|
+
* afterStart({ server }: AdapterContext) {
|
|
1993
|
+
* // server is the http.Server — available after listen()
|
|
1994
|
+
* }
|
|
1995
|
+
*
|
|
1996
|
+
* async shutdown() {
|
|
1997
|
+
* await Sentry.close(2000)
|
|
1998
|
+
* }
|
|
1999
|
+
* }
|
|
2000
|
+
* ```
|
|
2001
|
+
*/
|
|
2002
|
+
interface AppAdapter {
|
|
2003
|
+
/**
|
|
2004
|
+
* Human-readable name for logging and `dependsOn` resolution.
|
|
2005
|
+
* Required when the adapter (or any other in the same set) declares
|
|
2006
|
+
* `dependsOn` — sort errors will reference adapters by name.
|
|
2007
|
+
*/
|
|
2008
|
+
name?: string;
|
|
2009
|
+
/**
|
|
2010
|
+
* Other adapter names that must mount before this one. The framework
|
|
2011
|
+
* topologically sorts adapters at boot — cycles or unknown names throw
|
|
2012
|
+
* via `MountCycleError` / `MissingMountDepError` so bad configurations
|
|
2013
|
+
* fail boot rather than corrupt live traffic.
|
|
2014
|
+
*
|
|
2015
|
+
* Common pattern: `TenantAdapter` populates `req.tenant` and
|
|
2016
|
+
* `AuthAdapter` reads it for tenant-scoped RBAC, so AuthAdapter
|
|
2017
|
+
* declares `dependsOn = ['TenantAdapter']`. Adapters without
|
|
2018
|
+
* `dependsOn` retain their declaration order.
|
|
2019
|
+
*
|
|
2020
|
+
* @example
|
|
2021
|
+
* ```ts
|
|
2022
|
+
* class AuthAdapter implements AppAdapter {
|
|
2023
|
+
* name = 'AuthAdapter'
|
|
2024
|
+
* dependsOn = ['TenantAdapter']
|
|
2025
|
+
* }
|
|
2026
|
+
* ```
|
|
2027
|
+
*/
|
|
2028
|
+
dependsOn?: readonly KickJsPluginName[];
|
|
2029
|
+
/**
|
|
2030
|
+
* Return middleware entries to be inserted into the pipeline.
|
|
2031
|
+
* The `phase` controls ordering relative to global middleware and routes.
|
|
2032
|
+
*/
|
|
2033
|
+
middleware?(): AdapterMiddleware[];
|
|
2034
|
+
/**
|
|
2035
|
+
* Return Context Contributors (#107) that apply to every route in the
|
|
2036
|
+
* application. Adapter-level contributors merge into the per-route
|
|
2037
|
+
* pipeline at the `'adapter'` precedence level — they win over global
|
|
2038
|
+
* contributors but lose to module, class, and method decorators.
|
|
2039
|
+
*
|
|
2040
|
+
* Useful for cross-cutting concerns: an auth adapter contributing
|
|
2041
|
+
* `user`, a multi-tenant adapter contributing `tenant`, etc. Optional.
|
|
2042
|
+
*/
|
|
2043
|
+
contributors?(): ContributorRegistrations;
|
|
2044
|
+
/**
|
|
2045
|
+
* Called before global middleware — register early routes (docs UI, health).
|
|
2046
|
+
* May return a Promise — async rejections are caught and logged.
|
|
2047
|
+
*/
|
|
2048
|
+
beforeMount?(ctx: AdapterContext): void | Promise<void>;
|
|
2049
|
+
/**
|
|
2050
|
+
* Called for each module route that gets mounted.
|
|
2051
|
+
* Use this to collect route metadata (e.g. for OpenAPI spec generation).
|
|
2052
|
+
*/
|
|
2053
|
+
onRouteMount?(controllerClass: Constructor, mountPath: string): void;
|
|
2054
|
+
/**
|
|
2055
|
+
* Called after modules are registered and the DI container is bootstrapped,
|
|
2056
|
+
* before the HTTP server starts listening. **This is the correct hook for
|
|
2057
|
+
* adapters that register DI bindings, resolve services, or otherwise prepare
|
|
2058
|
+
* container-side state** (e.g. `container.registerInstance(MAILER, ...)`).
|
|
2059
|
+
*
|
|
2060
|
+
* `beforeStart` runs inside `app.setup()`, which means it also fires under
|
|
2061
|
+
* `createTestApp` — adapters using this hook work in unit tests without
|
|
2062
|
+
* needing a live HTTP server. See {@link afterStart} for the contrast.
|
|
2063
|
+
*
|
|
2064
|
+
* May return a Promise — async rejections are caught and logged.
|
|
2065
|
+
*/
|
|
2066
|
+
beforeStart?(ctx: AdapterContext): void | Promise<void>;
|
|
2067
|
+
/**
|
|
2068
|
+
* Called after the HTTP server is listening — `ctx.server` is the live
|
|
2069
|
+
* `http.Server`. **Use this hook only for work that genuinely needs a
|
|
2070
|
+
* listening server**: attaching Socket.IO / WS upgrades, reading the bound
|
|
2071
|
+
* port via `server.address()`, dispatching to the local server over HTTP,
|
|
2072
|
+
* etc.
|
|
2073
|
+
*
|
|
2074
|
+
* `afterStart` does NOT fire under `createTestApp`, since tests call
|
|
2075
|
+
* `app.setup()` and never `app.start()`. If your adapter only registers DI
|
|
2076
|
+
* bindings or resolves services, use {@link beforeStart} — that hook runs
|
|
2077
|
+
* in both production (`start()`) and tests (`setup()`).
|
|
2078
|
+
*
|
|
2079
|
+
* May return a Promise — async rejections are caught and logged.
|
|
2080
|
+
*/
|
|
2081
|
+
afterStart?(ctx: AdapterContext): void | Promise<void>;
|
|
2082
|
+
/**
|
|
2083
|
+
* Called by the /health/ready endpoint. Return the health status of your adapter's
|
|
2084
|
+
* backing service (database, Redis, queue, etc.).
|
|
2085
|
+
*/
|
|
2086
|
+
onHealthCheck?(): Promise<{
|
|
2087
|
+
name: string;
|
|
2088
|
+
status: 'up' | 'down';
|
|
2089
|
+
}>;
|
|
2090
|
+
/** Called on shutdown — clean up connections */
|
|
2091
|
+
shutdown?(): MaybePromise;
|
|
2092
|
+
/**
|
|
2093
|
+
* Optional DevTools introspection hook (architecture.md §23). Returns
|
|
2094
|
+
* a snapshot describing this adapter's current state, metrics, and DI
|
|
2095
|
+
* tokens. DevTools awaits the result, so async work is fine — but keep
|
|
2096
|
+
* the implementation cheap (counters + flags, no DB round trips) since
|
|
2097
|
+
* the topology endpoint polls on a short interval.
|
|
2098
|
+
*
|
|
2099
|
+
* The return type lives in `@forinda/kickjs` itself ({@link IntrospectionSnapshot})
|
|
2100
|
+
* so adopters can author `introspect()` without importing the kit just
|
|
2101
|
+
* to satisfy the contract. The kit's own `IntrospectionSnapshot` type
|
|
2102
|
+
* stays structurally identical; either import works.
|
|
2103
|
+
*/
|
|
2104
|
+
introspect?(): IntrospectionSnapshot | Promise<IntrospectionSnapshot>;
|
|
2105
|
+
/**
|
|
2106
|
+
* Optional DevTools tabs this adapter contributes (architecture.md §23).
|
|
2107
|
+
* Returns an array of `DevtoolsTabDescriptor` shapes — typically one
|
|
2108
|
+
* entry per adapter-owned panel (iframe / launch / html view).
|
|
2109
|
+
*
|
|
2110
|
+
* Same untyped-at-this-layer convention as {@link introspect}: adapter
|
|
2111
|
+
* authors import `defineDevtoolsTab` (or the bare type) from
|
|
2112
|
+
* `@forinda/kickjs-devtools-kit` so `@forinda/kickjs` doesn't take on
|
|
2113
|
+
* a runtime dep on the kit.
|
|
2114
|
+
*/
|
|
2115
|
+
devtoolsTabs?(): readonly unknown[];
|
|
2116
|
+
}
|
|
2117
|
+
/** Constructor type for AppAdapter classes */
|
|
2118
|
+
type AppAdapterClass = new () => AppAdapter;
|
|
2119
|
+
//#endregion
|
|
2120
|
+
export { CircuitBreaker as $, GroupAssetKeysResult as A, definePlugin as At, MountKind as B, IntrospectionSnapshot as Bt, Ctx as C, DefineModuleOptions as Ct, AssetKeyPair as D, DefinePluginOptions as Dt, RouteShape as E, BuildContext as Et, DefineAdapterOptions as F, KickJsPluginName as Ft, BuildPipelineOptions as G, mountSort as H, defineAdapter as I, KickJsPluginRegistry as It, SourcedRegistration as J, ContributorPipeline as K, DuplicateMountNameError as L, defineAugmentation as Lt, stripExt as M, ModuleRegistry as Mt, AdapterAsyncOptions as N, MutableModuleRegistry as Nt, AssetKeyStrategy as O, PluginAsyncOptions as Ot, AdapterFactory as P, AugmentationMeta as Pt, MissingContributorError as Q, MissingMountDepError as R, IntrospectFn as Rt, UseConnectOptions as S, defineModules as St, RequestContext as T, defineModule as Tt, RunContributorsOptions as U, MountSortItem as V, runContributors as W, ContributorCycleError as X, buildPipeline as Y, DuplicateContributorError as Z, RouteTable as _, FormatKickErrorOptions as _t, MiddlewarePath as a, getClassMetaOrUndefined as at, RuntimeResponse as b, formatKickError as bt, AdapterHttp as c, getMethodMeta as ct, ExpressRuntimeTypes as d, pushClassMeta as dt, CircuitBreakerOptions as et, HttpRuntime as f, pushMethodMeta as ft, RouteMethod as g, setMethodMeta as gt, RouteMeta as h, setInMetaRecord as ht, AppAdapterClass as i, getClassMeta as it, groupAssetKeys as j, KickPlugin as jt, GroupAssetKeysOptions as k, PluginFactory as kt, ConnectMiddleware as l, getMethodMetaOrUndefined as lt, RouteEntry as m, setInMetaMap as mt, AdapterMiddleware as n, CircuitBreakerStats as nt, MiddlewarePhase as o, getMetaMap as ot, KickRuntimeRegister as p, setClassMeta as pt, ContributorSource as q, AppAdapter as r, CircuitOpenError as rt, ActiveRuntime as s, getMetaRecord as st, AdapterContext as t, CircuitBreakerState as tt, CtxHandler as u, hasClassMeta as ut, RuntimeAppOptions as v, KickError as vt, DeepReadonly as w, ModuleFactory as wt, RuntimeTypeMap as x, ModuleList as xt, RuntimeCapabilities as y, KickErrorInit as yt, MountCycleError as z, IntrospectionKind as zt };
|
|
2121
|
+
//# sourceMappingURL=adapter-D_uzU-QA.d.mts.map
|