@spfn/core 0.2.0-beta.9 → 0.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +467 -305
  3. package/dist/authz/index.d.ts +34 -0
  4. package/dist/authz/index.js +810 -0
  5. package/dist/authz/index.js.map +1 -0
  6. package/dist/{boss-DI1r4kTS.d.ts → boss-D16fO2oG.d.ts} +41 -1
  7. package/dist/cache/index.js +42 -30
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/codegen/index.d.ts +121 -13
  10. package/dist/codegen/index.js +212 -15
  11. package/dist/codegen/index.js.map +1 -1
  12. package/dist/config/index.d.ts +615 -6
  13. package/dist/config/index.js +124 -5
  14. package/dist/config/index.js.map +1 -1
  15. package/dist/contract/index.d.ts +220 -0
  16. package/dist/contract/index.js +558 -0
  17. package/dist/contract/index.js.map +1 -0
  18. package/dist/db/index.d.ts +528 -85
  19. package/dist/db/index.js +831 -122
  20. package/dist/db/index.js.map +1 -1
  21. package/dist/define-middleware-DfDP39Nq.d.ts +167 -0
  22. package/dist/env/index.d.ts +26 -2
  23. package/dist/env/index.js +15 -5
  24. package/dist/env/index.js.map +1 -1
  25. package/dist/env/loader.d.ts +26 -19
  26. package/dist/env/loader.js +32 -25
  27. package/dist/env/loader.js.map +1 -1
  28. package/dist/errors/index.d.ts +10 -0
  29. package/dist/errors/index.js +418 -5
  30. package/dist/errors/index.js.map +1 -1
  31. package/dist/event/index.d.ts +33 -3
  32. package/dist/event/index.js +24 -3
  33. package/dist/event/index.js.map +1 -1
  34. package/dist/event/sse/client.d.ts +42 -3
  35. package/dist/event/sse/client.js +128 -45
  36. package/dist/event/sse/client.js.map +1 -1
  37. package/dist/event/sse/index.d.ts +12 -5
  38. package/dist/event/sse/index.js +280 -32
  39. package/dist/event/sse/index.js.map +1 -1
  40. package/dist/event/ws/client.d.ts +59 -0
  41. package/dist/event/ws/client.js +273 -0
  42. package/dist/event/ws/client.js.map +1 -0
  43. package/dist/event/ws/index.d.ts +94 -0
  44. package/dist/event/ws/index.js +272 -0
  45. package/dist/event/ws/index.js.map +1 -0
  46. package/dist/job/index.d.ts +2 -2
  47. package/dist/job/index.js +155 -42
  48. package/dist/job/index.js.map +1 -1
  49. package/dist/logger/index.d.ts +5 -0
  50. package/dist/logger/index.js +14 -0
  51. package/dist/logger/index.js.map +1 -1
  52. package/dist/middleware/index.d.ts +347 -9
  53. package/dist/middleware/index.js +1462 -15
  54. package/dist/middleware/index.js.map +1 -1
  55. package/dist/nextjs/index.d.ts +2 -2
  56. package/dist/nextjs/index.js +42 -28
  57. package/dist/nextjs/index.js.map +1 -1
  58. package/dist/nextjs/server.d.ts +35 -51
  59. package/dist/nextjs/server.js +126 -60
  60. package/dist/nextjs/server.js.map +1 -1
  61. package/dist/ops/index.d.ts +152 -0
  62. package/dist/ops/index.js +500 -0
  63. package/dist/ops/index.js.map +1 -0
  64. package/dist/route/index.d.ts +8 -694
  65. package/dist/route/index.js +111 -22
  66. package/dist/route/index.js.map +1 -1
  67. package/dist/router-Qbssr11H.d.ts +676 -0
  68. package/dist/security/index.d.ts +83 -0
  69. package/dist/security/index.js +173 -0
  70. package/dist/security/index.js.map +1 -0
  71. package/dist/server/index.d.ts +491 -22
  72. package/dist/server/index.js +1887 -308
  73. package/dist/server/index.js.map +1 -1
  74. package/dist/token-manager-BT5EnUAR.d.ts +278 -0
  75. package/dist/types-2AbaW4Ie.d.ts +205 -0
  76. package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
  77. package/dist/types-D1c57Ko-.d.ts +115 -0
  78. package/dist/types-ZQODsBft.d.ts +282 -0
  79. package/package.json +244 -208
  80. package/dist/router-Di7ENoah.d.ts +0 -151
  81. package/dist/types-B-e_f2dQ.d.ts +0 -121
  82. package/docs/cache.md +0 -133
  83. package/docs/codegen.md +0 -74
  84. package/docs/database.md +0 -346
  85. package/docs/entity.md +0 -539
  86. package/docs/env.md +0 -477
  87. package/docs/errors.md +0 -319
  88. package/docs/event.md +0 -116
  89. package/docs/job.md +0 -131
  90. package/docs/logger.md +0 -108
  91. package/docs/middleware.md +0 -337
  92. package/docs/nextjs.md +0 -241
  93. package/docs/repository.md +0 -496
  94. package/docs/route.md +0 -497
  95. package/docs/server.md +0 -307
@@ -0,0 +1,152 @@
1
+ import { a as NamedMiddleware } from '../define-middleware-DfDP39Nq.js';
2
+ import { a as RouteDef, R as Router, b as RouteBuilder } from '../router-Qbssr11H.js';
3
+ import { J as JsonSchema } from '../types-D1c57Ko-.js';
4
+ import { HttpMethod } from '../route/types.js';
5
+ import 'hono';
6
+ import '@sinclair/typebox';
7
+ import 'hono/utils/http-status';
8
+
9
+ /**
10
+ * Ops Router
11
+ *
12
+ * The structure half of SPFN's CLI-first operations surface. An app develops
13
+ * its own ops as ordinary routes — domain operations only that app can name —
14
+ * and this factory turns them into a mountable package router that:
15
+ *
16
+ * - requires every route to come from `opsRoute`, which applies the `/_ops`
17
+ * namespace, so the surface is recognizable and an ops route can never
18
+ * shadow an app route;
19
+ * - injects the given auth middleware into every route, the manifest
20
+ * included, so an unauthenticated ops surface cannot be created by
21
+ * accident — there is no opt-out;
22
+ * - serves `GET /_ops/_manifest`, the self-description the `spfn ops` CLI
23
+ * discovers commands from, registered first so no app route takes its URL.
24
+ *
25
+ * What the path looks like after the namespace is the app's business, decided
26
+ * when the ops route is written — this factory does not audit its shape.
27
+ *
28
+ * The auth middleware itself lives with the app's auth stack (`@spfn/auth`
29
+ * ships `opsTokenAuth`); core owns only the structure, so the ops surface has
30
+ * no opinion about how a token is stored or verified.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { createOpsRouter, opsRoute } from '@spfn/core/ops';
35
+ * import { opsTokenAuth, requireOpsScope } from '@spfn/auth/server';
36
+ *
37
+ * export const opsRouter = createOpsRouter({
38
+ * listSignups: opsRoute.get('/signups') // GET /_ops/signups
39
+ * .use([requireOpsScope('waitlist:read')])
40
+ * .handler(async () => signupsRepository.list()),
41
+ * }, { auth: opsTokenAuth });
42
+ *
43
+ * // mounted like any package router:
44
+ * export const appRouter = defineRouter({ ... }).packages([opsRouter]);
45
+ * ```
46
+ */
47
+
48
+ /** Every ops route lives under this prefix. */
49
+ declare const OPS_PATH_PREFIX = "/_ops/";
50
+ /** Where the manifest is served. Reserved — an app route cannot claim it. */
51
+ declare const OPS_MANIFEST_PATH = "/_ops/_manifest";
52
+ interface OpsRouterOptions {
53
+ /**
54
+ * The middleware that authenticates every ops request. Required — an ops
55
+ * surface without authentication is refused at definition time, not
56
+ * discovered in production.
57
+ */
58
+ auth: NamedMiddleware<string>;
59
+ }
60
+ /**
61
+ * Build the app's ops surface from its ops routes.
62
+ *
63
+ * Returns an ordinary `Router` meant to be mounted with `.packages()`, so ops
64
+ * routes stay out of the app's client types exactly like other package
65
+ * routes.
66
+ */
67
+ declare function createOpsRouter<TRoutes extends Record<string, RouteDef<any, any, any> | Router<any>>>(routes: TRoutes, options: OpsRouterOptions): Router<any>;
68
+
69
+ /**
70
+ * Ops Route Builder
71
+ *
72
+ * `route` with the ops namespace already applied. An ops route lives under
73
+ * `/_ops/` without exception, so the prefix is the helper's business rather
74
+ * than something every definition retypes and the factory then checks:
75
+ *
76
+ * ```ts
77
+ * const countExamples = opsRoute.get('/examples/count') // GET /_ops/examples/count
78
+ * .handler(async () => ({ count: await repo.countAll() }));
79
+ * ```
80
+ *
81
+ * Everything after the prefix belongs to the app. What the paths look like,
82
+ * how they nest, which segments are parameters — those are the app author's
83
+ * decisions, made when the ops route is written.
84
+ *
85
+ * The builder returned is an ordinary `RouteBuilder`, so `.use()`, `.input()`
86
+ * and `.handler()` work exactly as they do elsewhere.
87
+ */
88
+
89
+ /** The ops namespace, without the trailing slash. */
90
+ declare const OPS_PATH_ROOT = "/_ops";
91
+ /**
92
+ * Ops route builder entry point — `route`, namespaced under `/_ops`.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const listRecent = opsRoute.get('/examples')
97
+ * .use([requireOpsScope('example:read')])
98
+ * .input({ query: Type.Object({ limit: Type.Optional(Type.Number()) }) })
99
+ * .handler(async (c) => ({ items: await repo.findAll((await c.data()).query.limit ?? 10, 0) }));
100
+ * ```
101
+ */
102
+ declare const opsRoute: {
103
+ get: (path: string) => RouteBuilder;
104
+ post: (path: string) => RouteBuilder;
105
+ put: (path: string) => RouteBuilder;
106
+ patch: (path: string) => RouteBuilder;
107
+ delete: (path: string) => RouteBuilder;
108
+ };
109
+
110
+ /**
111
+ * Ops Manifest
112
+ *
113
+ * The server's self-description of its ops surface: every command an app
114
+ * exposes under `/_ops`, with each command's input schemas as plain JSON
115
+ * Schema. The ops CLI fetches this from the running server, so command
116
+ * discovery needs neither the app's source nor a generated artifact on the
117
+ * operator's machine.
118
+ *
119
+ * The router is loaded and walked (the contract collector's approach) rather
120
+ * than parsed from source: route schemas built from imported values resolve,
121
+ * and TypeBox schemas serialize to JSON Schema by construction.
122
+ */
123
+
124
+ /** One invokable ops command, as the CLI sees it. */
125
+ interface OpsCommand {
126
+ name: string;
127
+ method: HttpMethod;
128
+ path: string;
129
+ /** Input sections the route declares, each as JSON Schema. */
130
+ input: {
131
+ params?: JsonSchema;
132
+ query?: JsonSchema;
133
+ body?: JsonSchema;
134
+ };
135
+ }
136
+ /** What `GET /_ops/_manifest` answers. */
137
+ interface OpsManifest {
138
+ manifestVersion: 1;
139
+ commands: OpsCommand[];
140
+ }
141
+ /** Thrown when a route cannot be part of an ops surface. */
142
+ declare class OpsRouterError extends Error {
143
+ constructor(message: string);
144
+ }
145
+ /**
146
+ * Walk a routes record (nested routers included) and collect every RouteDef
147
+ * as an ops command. Validation of paths and names happens in
148
+ * `createOpsRouter` before this runs.
149
+ */
150
+ declare function collectOpsCommands(routes: Record<string, RouteDef<any> | Router<any>>): OpsCommand[];
151
+
152
+ export { OPS_MANIFEST_PATH, OPS_PATH_PREFIX, OPS_PATH_ROOT, type OpsCommand, type OpsManifest, OpsRouterError, type OpsRouterOptions, collectOpsCommands, createOpsRouter, opsRoute };
@@ -0,0 +1,500 @@
1
+ // src/route/route-builder.ts
2
+ var RouteBuilder = class _RouteBuilder {
3
+ _method;
4
+ _path;
5
+ _input;
6
+ _interceptor;
7
+ _middlewares;
8
+ _skipMiddlewares;
9
+ _contract;
10
+ /**
11
+ * Create a new RouteBuilder with copied properties and optional overrides
12
+ */
13
+ clone(overrides) {
14
+ const builder = new _RouteBuilder();
15
+ builder._method = this._method;
16
+ builder._path = this._path;
17
+ builder._input = overrides?.input ?? this._input;
18
+ builder._interceptor = overrides?.interceptor ?? this._interceptor;
19
+ builder._middlewares = overrides?.middlewares ?? this._middlewares;
20
+ builder._skipMiddlewares = overrides?.skipMiddlewares ?? this._skipMiddlewares;
21
+ builder._contract = overrides?.contract ?? this._contract;
22
+ return builder;
23
+ }
24
+ /**
25
+ * Define input schemas
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * route.get('/users/:id')
30
+ * .input({
31
+ * params: Type.Object({ id: Type.String() }),
32
+ * query: Type.Object({ page: Type.Number() }),
33
+ * headers: Type.Object({ authorization: Type.String() })
34
+ * })
35
+ * .handler(async (c) => {
36
+ * const { params, query, headers } = await c.data();
37
+ * // params = { id: string }
38
+ * // query = { page: number }
39
+ * // headers = { authorization: string }
40
+ * })
41
+ * ```
42
+ */
43
+ input(input) {
44
+ return this.clone({ input });
45
+ }
46
+ /**
47
+ * Define fields injected by interceptors
48
+ *
49
+ * These fields are:
50
+ * - Available in the handler (merged with input)
51
+ * - Excluded from client types (codegen uses only input)
52
+ * - Not validated by route input schema (injected by middleware)
53
+ *
54
+ * Use this when middleware/interceptors add fields to the request
55
+ * before it reaches the handler.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * // Auth interceptor injects crypto key fields
60
+ * route.post('/_auth/login')
61
+ * .input({
62
+ * body: Type.Object({
63
+ * email: Type.String(),
64
+ * password: Type.String()
65
+ * })
66
+ * })
67
+ * .interceptor({
68
+ * body: Type.Object({
69
+ * publicKey: Type.String(),
70
+ * keyId: Type.String(),
71
+ * fingerprint: Type.String()
72
+ * })
73
+ * })
74
+ * .handler(async (c) => {
75
+ * const { body } = await c.data();
76
+ * // body type: { email, password, publicKey, keyId, fingerprint }
77
+ * // Client only sees: { email, password }
78
+ * return loginService(body);
79
+ * });
80
+ * ```
81
+ */
82
+ interceptor(interceptor) {
83
+ return this.clone({ interceptor });
84
+ }
85
+ /**
86
+ * Add middlewares to the route
87
+ *
88
+ * Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
89
+ * Named middlewares that are already registered globally will be automatically
90
+ * deduplicated to prevent double execution.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * import { authenticate } from '@spfn/auth/server/middleware';
95
+ *
96
+ * // With NamedMiddleware (auto-deduped if registered globally)
97
+ * route.get('/users')
98
+ * .use([authenticate, RateLimitMiddleware()])
99
+ *
100
+ * // With regular middleware handlers
101
+ * route.get('/users')
102
+ * .use([AuthMiddleware(), RateLimitMiddleware()])
103
+ * ```
104
+ */
105
+ middleware(middlewares) {
106
+ return this.clone({ middlewares });
107
+ }
108
+ /**
109
+ * Add middlewares to the route (alias for `.middleware()`)
110
+ *
111
+ * Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
112
+ * Named middlewares that are already registered globally will be automatically
113
+ * deduplicated to prevent double execution.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * import { authenticate } from '@spfn/auth/server/middleware';
118
+ *
119
+ * // With NamedMiddleware (auto-deduped if registered globally)
120
+ * route.get('/users')
121
+ * .use([authenticate, RateLimitMiddleware()])
122
+ *
123
+ * // With regular middleware handlers
124
+ * route.get('/users')
125
+ * .use([AuthMiddleware(), RateLimitMiddleware()])
126
+ * ```
127
+ */
128
+ use(middlewares) {
129
+ return this.middleware(middlewares);
130
+ }
131
+ /**
132
+ * Skip server-level named middlewares
133
+ *
134
+ * Useful for public endpoints that should bypass auth or rate limiting
135
+ *
136
+ * @param middlewareNames - Array of middleware names to skip, or '*' to skip all
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * // Skip specific middlewares
141
+ * route.get('/health')
142
+ * .skip(['auth', 'rateLimit'])
143
+ * .handler(async (c) => c.json({ status: 'ok' }));
144
+ *
145
+ * // Skip only auth (still apply rate limiting)
146
+ * route.get('/public-data')
147
+ * .skip(['auth'])
148
+ * .handler(async (c) => { ... });
149
+ *
150
+ * // Skip all middlewares
151
+ * route.get('/public-health')
152
+ * .skip('*')
153
+ * .handler(async (c) => c.json({ status: 'ok' }));
154
+ * ```
155
+ */
156
+ skip(middlewareNames) {
157
+ return this.clone({ skipMiddlewares: middlewareNames });
158
+ }
159
+ /**
160
+ * Publish this route as a versioned contract operation
161
+ *
162
+ * Marks the route as a promise to clients that are compiled and deployed
163
+ * separately from the server — a mobile app, an external API consumer.
164
+ * The `@spfn/core:contract` generator writes every contracted route into
165
+ * `contracts/current.json`, and the build refuses a change that would break
166
+ * an already-released client.
167
+ *
168
+ * Routes without `.contract()` are unaffected: they simply do not appear in
169
+ * the contract. A web client needs nothing here — it derives its types from
170
+ * the router in the same build.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * export const getUser = route.get('/users/:id')
175
+ * .input({ params: Type.Object({ id: Type.String() }) })
176
+ * .contract({
177
+ * since: '1.2.0',
178
+ * auth: 'clientProofV1',
179
+ * requiresSession: true,
180
+ * response: Type.Object({
181
+ * id: Type.String(),
182
+ * name: Type.String(),
183
+ * email: Type.Optional(Type.String()),
184
+ * }),
185
+ * })
186
+ * .handler(async (c) => { ... });
187
+ * ```
188
+ */
189
+ contract(contract) {
190
+ return this.clone({ contract });
191
+ }
192
+ /**
193
+ * Define handler function
194
+ *
195
+ * Response type is automatically inferred from the return value.
196
+ * Use helper methods like `c.created()`, `c.paginated()` for proper type inference.
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * // Direct return - type inferred from data
201
+ * route.get('/users/:id')
202
+ * .input({ params: Type.Object({ id: Type.String() }) })
203
+ * .handler(async (c) => {
204
+ * const { params } = await c.data();
205
+ * return await getUser(params.id); // Type: User
206
+ * })
207
+ *
208
+ * // Using c.created() - returns data with 201 status, type preserved
209
+ * route.post('/users')
210
+ * .input({ body: Type.Object({ name: Type.String() }) })
211
+ * .handler(async (c) => {
212
+ * const { body } = await c.data();
213
+ * return c.created(await createUser(body)); // Type: User
214
+ * })
215
+ *
216
+ * // Using c.paginated() - returns PaginatedResult<T>
217
+ * route.get('/users')
218
+ * .handler(async (c) => {
219
+ * const users = await getUsers();
220
+ * return c.paginated(users, 1, 20, 100); // Type: PaginatedResult<User>
221
+ * })
222
+ *
223
+ * // Using c.noContent() - returns void
224
+ * route.delete('/users/:id')
225
+ * .handler(async (c) => {
226
+ * await deleteUser(params.id);
227
+ * return c.noContent(); // Type: void
228
+ * })
229
+ *
230
+ * // Using c.json() - returns Response (type inference lost)
231
+ * // Use only when you need custom status codes not covered by helpers
232
+ * route.get('/custom')
233
+ * .handler(async (c) => {
234
+ * return c.json({ data }, 418); // Type: Response
235
+ * })
236
+ * ```
237
+ */
238
+ handler(fn) {
239
+ return {
240
+ method: this._method,
241
+ path: this._path,
242
+ input: this._input,
243
+ interceptor: this._interceptor,
244
+ middlewares: this._middlewares,
245
+ skipMiddlewares: this._skipMiddlewares,
246
+ contract: this._contract,
247
+ handler: fn,
248
+ _input: {},
249
+ _interceptor: {},
250
+ _response: {}
251
+ };
252
+ }
253
+ };
254
+ function createMethodRoute(method) {
255
+ return (path) => {
256
+ const builder = new RouteBuilder();
257
+ builder._method = method;
258
+ builder._path = path;
259
+ return builder;
260
+ };
261
+ }
262
+ var route = {
263
+ get: createMethodRoute("GET"),
264
+ post: createMethodRoute("POST"),
265
+ put: createMethodRoute("PUT"),
266
+ patch: createMethodRoute("PATCH"),
267
+ delete: createMethodRoute("DELETE")
268
+ };
269
+
270
+ // src/route/router.ts
271
+ function createRouterInstance(routes, packageRouters = [], globalMiddlewares = [], contractVersion = null) {
272
+ return {
273
+ routes,
274
+ _routes: routes,
275
+ _packageRouters: packageRouters,
276
+ _globalMiddlewares: globalMiddlewares,
277
+ _contractVersion: contractVersion,
278
+ packages(routers) {
279
+ const newPackageRouters = [...this._packageRouters, ...routers];
280
+ for (const pkgRouter of routers) {
281
+ if (pkgRouter._packageRouters?.length > 0) {
282
+ newPackageRouters.push(...pkgRouter._packageRouters);
283
+ }
284
+ }
285
+ return createRouterInstance(
286
+ this.routes,
287
+ newPackageRouters,
288
+ this._globalMiddlewares,
289
+ this._contractVersion
290
+ );
291
+ },
292
+ use(middlewares) {
293
+ return createRouterInstance(
294
+ this.routes,
295
+ this._packageRouters,
296
+ [...this._globalMiddlewares, ...middlewares],
297
+ this._contractVersion
298
+ );
299
+ },
300
+ contractVersion(version) {
301
+ assertContractVersion(version);
302
+ return createRouterInstance(
303
+ this.routes,
304
+ this._packageRouters,
305
+ this._globalMiddlewares,
306
+ version
307
+ );
308
+ }
309
+ };
310
+ }
311
+ function assertContractVersion(version) {
312
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/.test(version)) {
313
+ throw new Error(
314
+ `contractVersion("${version}") is not a version of the form major.minor.patch. The released snapshot is named from this value and releases are compared by it.`
315
+ );
316
+ }
317
+ }
318
+ function defineRouter(routes) {
319
+ return createRouterInstance(routes);
320
+ }
321
+
322
+ // src/ops/manifest.ts
323
+ var OpsRouterError = class extends Error {
324
+ constructor(message) {
325
+ super(message);
326
+ this.name = "OpsRouterError";
327
+ }
328
+ };
329
+ function isRouter(value) {
330
+ return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
331
+ }
332
+ function isRouteDef(value) {
333
+ return value !== null && typeof value === "object" && "handler" in value;
334
+ }
335
+ function toJsonSchema(schema) {
336
+ return JSON.parse(JSON.stringify(schema));
337
+ }
338
+ var INPUT_SECTIONS = ["params", "query", "body"];
339
+ function toCommandInput(input) {
340
+ const sections = {};
341
+ if (!input) {
342
+ return sections;
343
+ }
344
+ for (const section of INPUT_SECTIONS) {
345
+ const schema = input[section];
346
+ if (schema) {
347
+ sections[section] = toJsonSchema(schema);
348
+ }
349
+ }
350
+ return sections;
351
+ }
352
+ function collectOpsCommands(routes) {
353
+ const commands = [];
354
+ visit(routes, commands, /* @__PURE__ */ new Map());
355
+ commands.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
356
+ return commands;
357
+ }
358
+ function visit(routes, commands, claimed) {
359
+ for (const [name, entry] of Object.entries(routes)) {
360
+ if (isRouter(entry)) {
361
+ visit(entry.routes, commands, claimed);
362
+ continue;
363
+ }
364
+ if (!isRouteDef(entry) || !entry.method || !entry.path) {
365
+ continue;
366
+ }
367
+ assertUnclaimedName(name, entry.path, claimed);
368
+ commands.push({
369
+ name,
370
+ method: entry.method,
371
+ path: entry.path,
372
+ input: toCommandInput(entry.input)
373
+ });
374
+ }
375
+ }
376
+ function assertUnclaimedName(name, path, claimed) {
377
+ const existing = claimed.get(name);
378
+ if (existing !== void 0) {
379
+ throw new OpsRouterError(
380
+ `Two ops routes are named "${name}" ("${existing}" and "${path}"). Command names are flattened across nested routers, so each must be unique for the CLI to resolve the one an operator asked for.`
381
+ );
382
+ }
383
+ claimed.set(name, path);
384
+ }
385
+
386
+ // src/ops/create-ops-router.ts
387
+ var OPS_PATH_PREFIX = "/_ops/";
388
+ var OPS_MANIFEST_PATH = "/_ops/_manifest";
389
+ var OPS_MANIFEST_NAME = "getOpsManifest";
390
+ function isRouter2(value) {
391
+ return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
392
+ }
393
+ function isRouteDef2(value) {
394
+ return value !== null && typeof value === "object" && "handler" in value;
395
+ }
396
+ function assertOpsRoute(name, def) {
397
+ if (!def.method || !def.path) {
398
+ throw new OpsRouterError(
399
+ `Ops route "${name}" has no method or path. An ops command is invoked on the wire, so both are required.`
400
+ );
401
+ }
402
+ if (!def.path.startsWith(OPS_PATH_PREFIX)) {
403
+ throw new OpsRouterError(
404
+ `Ops route "${name}" is at "${def.path}", outside "${OPS_PATH_PREFIX}". Build ops routes with \`opsRoute\` rather than \`route\` \u2014 it applies the namespace, so the path a definition carries is only the part the app owns.`
405
+ );
406
+ }
407
+ if (def.path === OPS_MANIFEST_PATH) {
408
+ throw new OpsRouterError(
409
+ `Ops route "${name}" claims "${OPS_MANIFEST_PATH}", which is reserved for the manifest. The manifest is registered first, so this route would never answer.`
410
+ );
411
+ }
412
+ }
413
+ function assertOpsName(name) {
414
+ if (name === OPS_MANIFEST_NAME) {
415
+ throw new OpsRouterError(
416
+ `Ops route name "${OPS_MANIFEST_NAME}" is reserved for the manifest route.`
417
+ );
418
+ }
419
+ }
420
+ function rebuildNestedRouter(name, router, auth, inherited) {
421
+ if (router._packageRouters?.length > 0) {
422
+ throw new OpsRouterError(
423
+ `Ops router "${name}" mounts package routers with .packages(). Their routes bypass the prefix check and the auth injection, so an ops surface cannot carry them.`
424
+ );
425
+ }
426
+ const handedDown = [...inherited, ...router._globalMiddlewares ?? []];
427
+ let rebuilt = defineRouter(
428
+ secureRoutes(router.routes, auth, handedDown)
429
+ );
430
+ if (router._contractVersion) {
431
+ rebuilt = rebuilt.contractVersion(router._contractVersion);
432
+ }
433
+ return rebuilt;
434
+ }
435
+ function secureRoutes(routes, auth, inherited = []) {
436
+ const secured = {};
437
+ for (const [name, entry] of Object.entries(routes)) {
438
+ assertOpsName(name);
439
+ if (isRouter2(entry)) {
440
+ secured[name] = rebuildNestedRouter(name, entry, auth, inherited);
441
+ continue;
442
+ }
443
+ if (!isRouteDef2(entry)) {
444
+ throw new OpsRouterError(`Ops router entry "${name}" is neither a route nor a router.`);
445
+ }
446
+ assertOpsRoute(name, entry);
447
+ secured[name] = {
448
+ ...entry,
449
+ middlewares: [auth, ...inherited, ...entry.middlewares ?? []]
450
+ };
451
+ }
452
+ return secured;
453
+ }
454
+ function createOpsRouter(routes, options) {
455
+ if (!options?.auth) {
456
+ throw new OpsRouterError(
457
+ "createOpsRouter requires an auth middleware ({ auth: ... }). An ops surface reachable without authentication cannot be created."
458
+ );
459
+ }
460
+ const manifest = {
461
+ manifestVersion: 1,
462
+ commands: collectOpsCommands(routes)
463
+ };
464
+ const secured = secureRoutes(routes, options.auth);
465
+ const manifestRoute = route.get(OPS_MANIFEST_PATH).use([options.auth]).handler(async () => manifest);
466
+ return defineRouter({
467
+ [OPS_MANIFEST_NAME]: manifestRoute,
468
+ ...secured
469
+ });
470
+ }
471
+
472
+ // src/ops/ops-route.ts
473
+ var OPS_PATH_ROOT = "/_ops";
474
+ function toOpsPath(path) {
475
+ if (!path.startsWith("/")) {
476
+ throw new OpsRouterError(
477
+ `Ops route path "${path}" must start with "/". It is appended to "${OPS_PATH_ROOT}", so "${path}" would read as "${OPS_PATH_ROOT}${path}".`
478
+ );
479
+ }
480
+ if (path === "/") {
481
+ throw new OpsRouterError(
482
+ `Ops route path "/" names no command \u2014 "${OPS_PATH_ROOT}" itself is not a command.`
483
+ );
484
+ }
485
+ return OPS_PATH_ROOT + path;
486
+ }
487
+ function opsMethod(method) {
488
+ return (path) => route[method](toOpsPath(path));
489
+ }
490
+ var opsRoute = {
491
+ get: opsMethod("get"),
492
+ post: opsMethod("post"),
493
+ put: opsMethod("put"),
494
+ patch: opsMethod("patch"),
495
+ delete: opsMethod("delete")
496
+ };
497
+
498
+ export { OPS_MANIFEST_PATH, OPS_PATH_PREFIX, OPS_PATH_ROOT, OpsRouterError, collectOpsCommands, createOpsRouter, opsRoute };
499
+ //# sourceMappingURL=index.js.map
500
+ //# sourceMappingURL=index.js.map