@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.
- package/LICENSE +1 -1
- package/README.md +467 -305
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +810 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/{boss-DI1r4kTS.d.ts → boss-D16fO2oG.d.ts} +41 -1
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +121 -13
- package/dist/codegen/index.js +212 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +615 -6
- package/dist/config/index.js +124 -5
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +220 -0
- package/dist/contract/index.js +558 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.d.ts +528 -85
- package/dist/db/index.js +831 -122
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DfDP39Nq.d.ts +167 -0
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +15 -5
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +32 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +418 -5
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +24 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +42 -3
- package/dist/event/sse/client.js +128 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +280 -32
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +272 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +155 -42
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +347 -9
- package/dist/middleware/index.js +1462 -15
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +42 -28
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +35 -51
- package/dist/nextjs/server.js +126 -60
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +152 -0
- package/dist/ops/index.js +500 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/route/index.d.ts +8 -694
- package/dist/route/index.js +111 -22
- package/dist/route/index.js.map +1 -1
- package/dist/router-Qbssr11H.d.ts +676 -0
- package/dist/security/index.d.ts +83 -0
- package/dist/security/index.js +173 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server/index.d.ts +491 -22
- package/dist/server/index.js +1887 -308
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-BT5EnUAR.d.ts +278 -0
- package/dist/types-2AbaW4Ie.d.ts +205 -0
- package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
- package/dist/types-D1c57Ko-.d.ts +115 -0
- package/dist/types-ZQODsBft.d.ts +282 -0
- package/package.json +244 -208
- package/dist/router-Di7ENoah.d.ts +0 -151
- package/dist/types-B-e_f2dQ.d.ts +0 -121
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -346
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -477
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -116
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -241
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- 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
|