@owlmeans/server-api 0.1.18-rc.6 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -46
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/server-api/SKILL.md +142 -22
- package/build/consts.local.d.ts +2 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +2 -0
- package/build/consts.local.js.map +1 -0
- package/build/errors.d.ts +12 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +16 -0
- package/build/errors.js.map +1 -1
- package/build/helper.d.ts +3 -6
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +10 -1
- package/build/helper.js.map +1 -1
- package/build/hold.d.ts +35 -0
- package/build/hold.d.ts.map +1 -0
- package/build/hold.js +57 -0
- package/build/hold.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/protocol.d.ts +18 -0
- package/build/protocol.d.ts.map +1 -0
- package/build/protocol.js +87 -0
- package/build/protocol.js.map +1 -0
- package/build/server.d.ts +2 -3
- package/build/server.d.ts.map +1 -1
- package/build/server.js +42 -34
- package/build/server.js.map +1 -1
- package/build/types.d.ts +31 -2
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +38 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/consts.d.ts +2 -0
- package/build/utils/consts.d.ts.map +1 -0
- package/build/utils/consts.js +2 -0
- package/build/utils/consts.js.map +1 -0
- package/build/utils/consts.local.d.ts +5 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +5 -0
- package/build/utils/consts.local.js.map +1 -0
- package/build/utils/context/types.d.ts +13 -0
- package/build/utils/context/types.d.ts.map +1 -0
- package/build/utils/context/types.js +2 -0
- package/build/utils/context/types.js.map +1 -0
- package/build/utils/context.d.ts +3 -3
- package/build/utils/context.d.ts.map +1 -1
- package/build/utils/context.js +5 -2
- package/build/utils/context.js.map +1 -1
- package/build/utils/error/types.d.ts +52 -0
- package/build/utils/error/types.d.ts.map +1 -0
- package/build/utils/error/types.js +2 -0
- package/build/utils/error/types.js.map +1 -0
- package/build/utils/error.d.ts +5 -2
- package/build/utils/error.d.ts.map +1 -1
- package/build/utils/error.js +135 -13
- package/build/utils/error.js.map +1 -1
- package/build/utils/guards.d.ts +1 -5
- package/build/utils/guards.d.ts.map +1 -1
- package/build/utils/guards.js +15 -19
- package/build/utils/guards.js.map +1 -1
- package/build/utils/index.d.ts +6 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +2 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/log/consts.d.ts +14 -0
- package/build/utils/log/consts.d.ts.map +1 -0
- package/build/utils/log/consts.js +17 -0
- package/build/utils/log/consts.js.map +1 -0
- package/build/utils/log/types.d.ts +42 -0
- package/build/utils/log/types.d.ts.map +1 -0
- package/build/utils/log/types.js +2 -0
- package/build/utils/log/types.js.map +1 -0
- package/build/utils/log.d.ts +4 -0
- package/build/utils/log.d.ts.map +1 -0
- package/build/utils/log.js +170 -0
- package/build/utils/log.js.map +1 -0
- package/build/utils/payload/types.d.ts +18 -0
- package/build/utils/payload/types.d.ts.map +1 -0
- package/build/utils/payload/types.js +2 -0
- package/build/utils/payload/types.js.map +1 -0
- package/build/utils/payload.d.ts +3 -15
- package/build/utils/payload.d.ts.map +1 -1
- package/build/utils/payload.js +41 -57
- package/build/utils/payload.js.map +1 -1
- package/build/utils/schema.d.ts +7 -0
- package/build/utils/schema.d.ts.map +1 -0
- package/build/utils/schema.js +20 -0
- package/build/utils/schema.js.map +1 -0
- package/build/utils/server.d.ts +3 -6
- package/build/utils/server.d.ts.map +1 -1
- package/build/utils/server.js +12 -13
- package/build/utils/server.js.map +1 -1
- package/build/utils/types.d.ts +18 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/build/utils/types.local.d.ts +9 -0
- package/build/utils/types.local.d.ts.map +1 -0
- package/build/utils/types.local.js +2 -0
- package/build/utils/types.local.js.map +1 -0
- package/package.json +23 -19
- package/src/consts.local.ts +1 -0
- package/src/errors.ts +18 -0
- package/src/helper.ts +13 -8
- package/src/hold.ts +66 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +149 -0
- package/src/server.ts +72 -69
- package/src/types.local.ts +44 -0
- package/src/types.ts +38 -4
- package/src/utils/consts.local.ts +7 -0
- package/src/utils/consts.ts +1 -0
- package/src/utils/context/types.ts +13 -0
- package/src/utils/context.ts +9 -4
- package/src/utils/error/types.ts +52 -0
- package/src/utils/error.ts +157 -13
- package/src/utils/guards.ts +16 -26
- package/src/utils/index.ts +6 -1
- package/src/utils/log/consts.ts +18 -0
- package/src/utils/log/types.ts +43 -0
- package/src/utils/log.ts +193 -0
- package/src/utils/payload/types.ts +18 -0
- package/src/utils/payload.ts +42 -60
- package/src/utils/schema.ts +23 -0
- package/src/utils/server.ts +16 -21
- package/src/utils/types.local.ts +12 -0
- package/src/utils/types.ts +18 -0
- package/tests/error-status.spec.ts +349 -0
- package/tests/handlers.spec.ts +83 -0
- package/tests/json-body.spec.ts +72 -0
- package/tests/log-adapter.spec.ts +62 -0
- package/tests/log-redaction.spec.ts +181 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +1 -0
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -1,80 +1,237 @@
|
|
|
1
1
|
# @owlmeans/server-api
|
|
2
2
|
|
|
3
|
-
Fastify
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
- `createApiServer` / `appendApiServer` initialize the HTTP server in a context
|
|
10
|
-
- Not typically used directly — import handlers from `@owlmeans/server-app`
|
|
3
|
+
The Fastify HTTP server of an OwlMeans backend and the protocol-bound handler factories that serve
|
|
4
|
+
it. An application imports `handlers<Context>()` in every HTTP handler module (often through the
|
|
5
|
+
`@owlmeans/server-app` re-export) and `holdApiPort` for a guarded boot. The server itself —
|
|
6
|
+
`appendApiServer` — is already added by `@owlmeans/server-app`'s `makeContext`; call it directly
|
|
7
|
+
only when composing a context without that package. WebSocket routes belong to
|
|
8
|
+
`@owlmeans/server-socket`, and calls to other services go through the `@owlmeans/api` client.
|
|
11
9
|
|
|
12
10
|
## Installation
|
|
13
11
|
|
|
14
12
|
```bash
|
|
15
|
-
bun add @owlmeans/server-api
|
|
13
|
+
bun add @owlmeans/server-api@^0.1.18
|
|
16
14
|
```
|
|
17
15
|
|
|
16
|
+
## Concepts
|
|
17
|
+
|
|
18
|
+
- **API server** — the `api-server` service (`context.getApiServer()`), holding a `FastifyInstance`
|
|
19
|
+
and `listen()`. At context init it builds its route table from the registered entrypoints: backend
|
|
20
|
+
routes of this `cfg.service` only, excluding socket and queue protocols.
|
|
21
|
+
- **Request pipeline** — intermediates run first; then the protocol's guards (the first guard whose
|
|
22
|
+
`match` hits handles the request and sets `request.auth`, and the organization entity is attached);
|
|
23
|
+
then its gates (`gate.assert`); then the handler.
|
|
24
|
+
- **Protocol-bound handler** — `handlers<Context>().body / .params / .request` infer the request
|
|
25
|
+
sections and the response type from the declaration. A returned value resolves the entrypoint with
|
|
26
|
+
`EntrypointOutcome.Ok` (HTTP 200); a thrown error rejects it.
|
|
27
|
+
- **Validation** — the contract's schemas are compiled with AJV (`removeAdditional`, `useDefaults`,
|
|
28
|
+
`coerceTypes`), so a handler receives cleaned, coerced input.
|
|
29
|
+
- **Error mapping** — `AuthForbidden` / `AccessError` answer 403, `AuthorizationError` /
|
|
30
|
+
`AuthFailedError` answer 401, a class declaring `static httpStatus` as an integer 4xx answers that
|
|
31
|
+
status, and anything else 500 — always with the marshalled `ResilientError` as the body. A refusal
|
|
32
|
+
of the caller's condition declares its status; a fault declares nothing and stays 500. The status
|
|
33
|
+
is taken from the error as thrown first, and from the ensured (rebuilt) error only when that
|
|
34
|
+
answers 500. An auth family is matched by class or by exact registered type name (the instance's
|
|
35
|
+
`type` or a `typeName` on its constructor chain), so duplicate module copies answer the same.
|
|
36
|
+
|
|
18
37
|
## Usage
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
### Body, params and request handlers
|
|
21
40
|
|
|
22
|
-
```
|
|
23
|
-
import {
|
|
41
|
+
```ts
|
|
42
|
+
import { handlers } from '@owlmeans/server-api'
|
|
43
|
+
import { bind } from '@owlmeans/server-entrypoint'
|
|
44
|
+
import { makeEntityScope } from '@owlmeans/auth-common'
|
|
45
|
+
import { projectProtocols } from 'my-app-common'
|
|
46
|
+
import type { Context } from './context.js'
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
export const create = handleBody<CreateProject>(async (payload, context, req) => {
|
|
27
|
-
const ctx = context as Context
|
|
28
|
-
return await ctx.project().create({ ...payload, entityId: req.auth!.entityId })
|
|
29
|
-
})
|
|
48
|
+
const api = handlers<Context>()
|
|
30
49
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
// body: the validated body first, then context and the full request
|
|
51
|
+
const create = api.body(projectProtocols.create, async (payload, context, request) =>
|
|
52
|
+
context.project().create({ ...payload, entityId: makeEntityScope(request).requireEntityKey() }))
|
|
53
|
+
|
|
54
|
+
// params: the validated URL params first
|
|
55
|
+
const get = api.params(projectProtocols.get, async ({ id }, context, request) =>
|
|
56
|
+
context.project().load({ id, entityId: makeEntityScope(request).requireEntityKey() }))
|
|
57
|
+
|
|
58
|
+
// request: every typed section plus request metadata (auth, entity, headers)
|
|
59
|
+
const search = api.request(projectProtocols.search, async (request, context) =>
|
|
60
|
+
context.project().list({ entityId: makeEntityScope(request).requireEntityKey(), ...request.query }))
|
|
61
|
+
|
|
62
|
+
export const serverBindings = [
|
|
63
|
+
bind(projectProtocols.base),
|
|
64
|
+
bind(projectProtocols.create, create),
|
|
65
|
+
bind(projectProtocols.get, get),
|
|
66
|
+
bind(projectProtocols.search, search),
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Refusing a request
|
|
71
|
+
|
|
72
|
+
Guards and gates run before the handler; a handler still checks ownership and throws a typed error,
|
|
73
|
+
which the server maps onto a status.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { AuthForbidden } from '@owlmeans/auth'
|
|
77
|
+
|
|
78
|
+
// the protocol declares both `params` and `body`
|
|
79
|
+
export const rename = api.body(projectProtocols.rename, async ({ name }, context, request) => {
|
|
80
|
+
const entityId = makeEntityScope(request).requireEntityKey()
|
|
81
|
+
const project = await context.project().load({ id: request.params.id, entityId })
|
|
82
|
+
if (project == null) {
|
|
83
|
+
throw new AuthForbidden('project') // 403
|
|
84
|
+
}
|
|
35
85
|
|
|
36
|
-
|
|
37
|
-
export const health = handleRequest(async (req, context) => {
|
|
38
|
-
return { status: 'ok' }
|
|
86
|
+
return context.project().update({ ...project, name })
|
|
39
87
|
})
|
|
40
88
|
```
|
|
41
89
|
|
|
42
|
-
|
|
90
|
+
A refusal that is not about permission declares its own 4xx on the class. The declaring package
|
|
91
|
+
never imports this one — the static property is read structurally and inherited by subclasses:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { ResilientError } from '@owlmeans/error'
|
|
43
95
|
|
|
44
|
-
|
|
96
|
+
export class ProjectBusy extends ResilientError {
|
|
97
|
+
public static override typeName = 'ProjectBusy'
|
|
98
|
+
public static httpStatus = 409 // honoured only as an integer 400–499; anything else answers 500
|
|
45
99
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
100
|
+
constructor(message: string = 'error') {
|
|
101
|
+
super(ProjectBusy.typeName, `project-busy:${message}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
49
104
|
```
|
|
50
105
|
|
|
51
|
-
###
|
|
106
|
+
### File upload
|
|
107
|
+
|
|
108
|
+
`uploadedFile(request)` is the only place a handler touches the Fastify multipart API. The protocol
|
|
109
|
+
must declare a `body` section.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { handlers, NoFileError, uploadedFile } from '@owlmeans/server-api'
|
|
113
|
+
|
|
114
|
+
export const upload = api.request(invoiceProtocols.attach, async (request, context) => {
|
|
115
|
+
const file = await uploadedFile(request)
|
|
116
|
+
if (file == null) {
|
|
117
|
+
throw new NoFileError()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const content = await file.toBuffer()
|
|
52
121
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
122
|
+
return context.attachment().create({
|
|
123
|
+
entityId: makeEntityScope(request).requireEntityKey(),
|
|
124
|
+
name: file.filename,
|
|
125
|
+
mimetype: file.mimetype,
|
|
126
|
+
size: content.length,
|
|
127
|
+
})
|
|
128
|
+
})
|
|
56
129
|
```
|
|
57
130
|
|
|
58
|
-
###
|
|
131
|
+
### Holding the port during boot
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { holdApiPort } from '@owlmeans/server-api'
|
|
59
135
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
136
|
+
const hold = await holdApiPort(cfg, { okPath: '/healthz' }) // 200 on /healthz, 503 elsewhere
|
|
137
|
+
await context.configure().init()
|
|
138
|
+
await hold.release()
|
|
139
|
+
await context.getApiServer().listen()
|
|
63
140
|
```
|
|
64
141
|
|
|
65
|
-
###
|
|
142
|
+
### A context without `@owlmeans/server-app`
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { makeServerContext } from '@owlmeans/server-context'
|
|
146
|
+
import { appendApiServer } from '@owlmeans/server-api'
|
|
66
147
|
|
|
67
|
-
|
|
148
|
+
const context = appendApiServer(makeServerContext(cfg))
|
|
149
|
+
context.registerEntrypoints(serverBindings)
|
|
150
|
+
await context.configure().init()
|
|
68
151
|
|
|
69
|
-
|
|
152
|
+
// the Fastify instance, for plugins the framework does not register
|
|
153
|
+
context.getApiServer().server.addHook('onResponse', async (request, reply) => {
|
|
154
|
+
console.log(request.url, reply.statusCode)
|
|
155
|
+
})
|
|
70
156
|
|
|
71
|
-
|
|
157
|
+
await context.getApiServer().listen()
|
|
158
|
+
```
|
|
72
159
|
|
|
73
|
-
##
|
|
160
|
+
## API
|
|
74
161
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
162
|
+
### Root export
|
|
163
|
+
|
|
164
|
+
| Symbol | Kind | Purpose |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `handlers<Context>()` | function | Factories `body(protocol, fn)`, `params(protocol, fn)`, `request(protocol, fn)` returning `BoundEntrypointHandler<Protocol>` |
|
|
167
|
+
| `uploadedFile(request)` | function | The multipart file of a request, or `undefined` |
|
|
168
|
+
| `extractUploadedFile(req)` | function | Same boundary for an untyped `AbstractRequest`; returns `Promise<UploadedFile \| undefined>` |
|
|
169
|
+
| `holdApiPort(cfg, opts?)` | function | Bind the service port before init; returns `ApiPortHold` with `release()` |
|
|
170
|
+
| `createApiServer(alias)` | function | Create the Fastify API server service |
|
|
171
|
+
| `appendApiServer(ctx, alias = DEFAULT_ALIAS)` | function | Register the API server and `context.getApiServer()` |
|
|
172
|
+
| `handleBody`, `handleParams`, `handleRequest`, `handleIntermediate` | function | Unbound compatibility wrappers returning `RefedEntrypointHandler` |
|
|
173
|
+
| `AuthFailedError`, `AccessError`, `NoFileError` | class | Registered `ResilientError` subclasses (401, 403, 500) |
|
|
174
|
+
| `DEFAULT_ALIAS` | const | `'api-server'` |
|
|
175
|
+
| `PORT` | const | `80`, used when the service route declares no port |
|
|
176
|
+
| `CLOSED_HOST`, `OPENED_HOST` | const | `'127.0.0.1'` and `'0.0.0.0'`; a service with `opened: true` listens on the second |
|
|
177
|
+
| `ApiServer`, `ApiServerAppend` | type | The server service and its context mixin |
|
|
178
|
+
| `ApiPortHold`, `ApiPortHoldOptions` | type | `holdApiPort` result and options (`okPath?`, `payload?`) |
|
|
179
|
+
| `Config`, `Context` | type | `ServerConfig` and `ServerContext & ApiServerAppend` |
|
|
180
|
+
| `Request`, `Response` | type | Fastify request and reply |
|
|
181
|
+
| `UploadedFile` | type | Fastify `MultipartFile` |
|
|
182
|
+
|
|
183
|
+
### `./utils` subpath
|
|
184
|
+
|
|
185
|
+
For transport packages (such as `@owlmeans/server-socket`) that reuse the HTTP pipeline; application
|
|
186
|
+
code does not need it.
|
|
187
|
+
|
|
188
|
+
| Symbol | Kind | Purpose |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `authorize(context, module, req, reply)` | function | Run guards, set `auth`, call `makeEntityScope(req).attachEntity(context)` |
|
|
191
|
+
| `createServerHandler(module, location)` | function | The Fastify handler: authorize, gates, handle, respond |
|
|
192
|
+
| `canServeModule(context, module)` | function | Whether an entrypoint belongs on this HTTP server |
|
|
193
|
+
| `payloadHelper.provideRequest(alias, req, provision?)` | helper member | Build an `AbstractRequest` from a Fastify request |
|
|
194
|
+
| `payloadHelper.executeResponse(response, reply, throwOnError?)` | helper member | Send an `AbstractResponse` onto a reply |
|
|
195
|
+
| `httpErrorHelper.handleError(error, reply, exposure?)` | helper member | Answer an error with `httpErrorHelper.errorStatus` and the marshalled body |
|
|
196
|
+
| `httpErrorHelper.errorStatus(error)` | helper member | 403 / 401 for auth errors (by class or type name), else the class's declared 4xx, else 500 |
|
|
197
|
+
| `httpErrorHelper.declaredErrorStatus(error)` | helper member | The integer 4xx a class declares through `static httpStatus`, or `null` |
|
|
198
|
+
| `httpErrorHelper.errorExposure(config?)`, `.serializeError(error, exposure?)`, `.applyErrorHeaders(error, reply)` | helper member | The exposure policy, the wire shape and the response headers of an error |
|
|
199
|
+
| `HttpStatusDeclaration` | type | `{ httpStatus?: unknown }` — the structural shape a declaring class has |
|
|
200
|
+
| `fixFormatDates(schema)` | function | Rewrite `date-time` object schemas as strings |
|
|
201
|
+
| `makeRequestContextHelper(req).populateContext(context)`, `.extractContext(ctx?, location?)` | helper member | Carry the request-scoped context on the raw request |
|
|
202
|
+
|
|
203
|
+
## Common pitfalls
|
|
204
|
+
|
|
205
|
+
- `.body` and `.params` exist only for a protocol that declares that section; for anything else use
|
|
206
|
+
`.request`.
|
|
207
|
+
- Do not reach into `request.original` from application code — `uploadedFile(request)` is the
|
|
208
|
+
multipart boundary.
|
|
209
|
+
- Prefer `handlers<Context>()` over the unbound `handle*` wrappers, and never hand-write a
|
|
210
|
+
`RefedEntrypointHandler` for an HTTP route.
|
|
211
|
+
- A handler is wrapped by `handlers<Context>()` **exactly once**: wrap a plain function where it is
|
|
212
|
+
bound (`bind(p, api.body(p, create))`), or bind the already-bound export directly
|
|
213
|
+
(`const create = api.body(p, ...)` then `bind(p, create)`) — never both. `tsc` rejects a double
|
|
214
|
+
wrap (`TS2345 "BoundEntrypointHandler<…> is not assignable"`); at runtime a handler already bound
|
|
215
|
+
to the SAME protocol is returned unchanged with a one-time console warning, and anything else that
|
|
216
|
+
is not a plain function fails that one route with `HandlerMisconfiguredError` instead of the
|
|
217
|
+
opaque `TypeError: handler is not a function`.
|
|
218
|
+
- A guard only authenticates. Gates authorize, and the handler keeps its organization check keyed on
|
|
219
|
+
`makeEntityScope(request).requireEntityKey()` — never an id read from the token.
|
|
220
|
+
- `request.entity` is absent when no entity resolver is registered; `makeEntityScope(request).requireEntityKey()` falls back to
|
|
221
|
+
the slug there, `.requireEntity()` throws.
|
|
222
|
+
- A handler always answers 200 on success. The port comes from `cfg.services[cfg.service]`
|
|
223
|
+
(`internalPort ?? port ?? 80`).
|
|
224
|
+
- A `holdApiPort` bind failure must end the process with a non-zero exit.
|
|
225
|
+
- WebSocket routes use `connection(protocol, callback)` from `@owlmeans/server-socket`.
|
|
226
|
+
|
|
227
|
+
## Related packages
|
|
228
|
+
|
|
229
|
+
- [`@owlmeans/server-entrypoint`](../server-entrypoint) — `bind()` attaches handlers to protocols
|
|
230
|
+
- [`@owlmeans/server-app`](../server-app) — application bootstrap and re-exports
|
|
231
|
+
- [`@owlmeans/server-socket`](../server-socket) — the WebSocket counterpart of `handlers`
|
|
232
|
+
- [`@owlmeans/entrypoint`](../entrypoint) — `contract`, `typed`, request/response types
|
|
233
|
+
- [`@owlmeans/auth-common`](../auth-common) — `makeEntityScope` (`requireEntityKey`, `attachEntity`), guard aliases
|
|
234
|
+
- [`@owlmeans/api`](../api) — the client that calls these endpoints from other services
|
|
78
235
|
|
|
79
236
|
<!-- owlmeans:agent-guidance:start -->
|
|
80
237
|
## Agent guidance
|
|
@@ -84,7 +241,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
84
241
|
your project's skill store (`.agents/skills/`):
|
|
85
242
|
|
|
86
243
|
```sh
|
|
87
|
-
npx @owlmeans/agent-skills
|
|
244
|
+
npx @owlmeans/agent-skills@^0.1.18
|
|
88
245
|
```
|
|
89
246
|
|
|
90
247
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/server-api",
|
|
4
|
-
"version": "0.1.18
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18",
|
|
5
|
+
"generatedAt": "2026-10-28T00:31:34.700Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,40 +1,160 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: server-api
|
|
3
|
-
description:
|
|
3
|
+
description: Implement HTTP entrypoint protocols with @owlmeans/server-api handlers<Context>().body(), params(), request(), and uploadedFile(). Load before writing an API handler.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
7
|
|
|
8
8
|
# @owlmeans/server-api
|
|
9
9
|
|
|
10
|
-
**
|
|
11
|
-
**Install:** `"@owlmeans/server-api": "^0.1.18-rc.6"` in `dependencies`
|
|
10
|
+
**Install:** `bun add @owlmeans/server-api@^0.1.18`
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
Make handlers from the protocol declaration so input and output types stay coupled to the shared
|
|
13
|
+
contract:
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
| `makeServer()` | Factory for the Fastify-based API server |
|
|
18
|
-
| `Server`, `ServerRequest`, `ServerResponse` types | Runtime shapes |
|
|
19
|
-
| Errors | Typed transport errors |
|
|
20
|
-
| Constants | Default ports, paths |
|
|
21
|
-
| Helpers | Middleware composition |
|
|
15
|
+
```ts
|
|
16
|
+
const api = handlers<AppContext>()
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
const create = api.body(projectProtocols.create, async (body, context, request) =>
|
|
19
|
+
context.projects.create(body, request.auth)
|
|
20
|
+
)
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
const get = api.params(projectProtocols.get, async ({ id }, context) =>
|
|
23
|
+
context.projects.get(id)
|
|
24
|
+
)
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
const search = api.request(projectProtocols.search, async (request, context) =>
|
|
27
|
+
context.projects.search(request.query)
|
|
28
|
+
)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
export const serverBindings = [
|
|
31
|
+
bind(projectProtocols.create, create),
|
|
32
|
+
bind(projectProtocols.get, get),
|
|
33
|
+
bind(projectProtocols.search, search),
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`body` and `params` are available only for a protocol declaring that section. `request` works for
|
|
38
|
+
any protocol and receives all its typed sections plus request metadata. A successful callback
|
|
39
|
+
return resolves the entrypoint with `EntrypointOutcome.Ok`; a thrown error rejects it.
|
|
40
|
+
|
|
41
|
+
A body contract may be a scalar (`schema<string>({ type: 'string' })`, a number, a boolean): the
|
|
42
|
+
server's JSON parser takes any JSON value, so `"abc"`, `42` or `false` reach the handler as that
|
|
43
|
+
value. An unquoted string or an empty body under `application/json` is invalid JSON and answers
|
|
44
|
+
400 before the handler runs — `@owlmeans/api` never sends either (it quotes a scalar body).
|
|
45
|
+
`tests/json-body.spec.ts` runs it through `appendApiServer`.
|
|
46
|
+
|
|
47
|
+
## Wrap exactly once
|
|
48
|
+
|
|
49
|
+
A handler is wrapped by `handlers<Context>()` exactly once — either shape above (create the bound
|
|
50
|
+
handler and bind it directly) is correct on its own. Never combine them: a handler module that
|
|
51
|
+
already exports a bound handler must be bound directly, not wrapped again where it is bound.
|
|
30
52
|
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
53
|
+
```ts
|
|
54
|
+
// WRONG — bound once in the handler module, wrapped a second time here
|
|
55
|
+
export const create = api.body(projectProtocols.create, async (body, context) => ...)
|
|
56
|
+
bind(projectProtocols.create, api.body(projectProtocols.create, create))
|
|
35
57
|
```
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
`tsc` rejects the double wrap (`TS2345 "Argument of type 'BoundEntrypointHandler<…>' is not
|
|
60
|
+
assignable"`). At runtime, `body`/`params`/`request` return an already-bound handler for the SAME
|
|
61
|
+
protocol unchanged, with a one-time warning; anything else that is not a plain function fails only
|
|
62
|
+
that one route with `HandlerMisconfiguredError`, instead of the opaque
|
|
63
|
+
`TypeError: handler is not a function`.
|
|
64
|
+
|
|
65
|
+
## The status a thrown error answers
|
|
66
|
+
|
|
67
|
+
A thrown error (or a rejected response) is answered with a body chosen by the exposure (§ Error
|
|
68
|
+
exposure) and a status from `httpErrorHelper.errorStatus(error)` (`./utils`), resolved in this order:
|
|
69
|
+
|
|
70
|
+
| Error | Status |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `AuthForbidden`, `AccessError` or a subclass — by class or registered type name | 403 |
|
|
73
|
+
| `AuthorizationError`, `AuthFailedError` or a subclass — by class or registered type name | 401 |
|
|
74
|
+
| a class declaring `static httpStatus` as an integer 400–499, or 500–599 with `static allowServerErrorStatus = true` | that status |
|
|
75
|
+
| anything else, including a 5xx declared without the opt-in | 500 |
|
|
76
|
+
|
|
77
|
+
- **A refusal of the caller's condition declares its status; a fault declares nothing.** 400 a
|
|
78
|
+
malformed request, 402 an unpaid balance or plan, 404 an addressed target that does not exist
|
|
79
|
+
(or is another organization's), 409 a target whose current state conflicts, 422 content or a body
|
|
80
|
+
that is understood and refused — and a missing configuration, a broken peer, a timeout or a bug
|
|
81
|
+
stays 500, because monitoring, logs, proxies and retry logic read a 5xx as the server failing.
|
|
82
|
+
The table and the leaf-class rule are the `error` skill's.
|
|
83
|
+
- The storage refusals of `@owlmeans/resource` declare theirs: `UnknownRecordError` answers 404,
|
|
84
|
+
`RecordExists` 409, every other `ResourceError` 500 — so a handler that lets a `get()` miss
|
|
85
|
+
escape answers 404, and one reading a row the request did not address uses `load` instead.
|
|
86
|
+
- Declare it on the class: `public static httpStatus = 409` (`override` only when an ancestor
|
|
87
|
+
already declares one). The declaration is structural — the package declaring an error never
|
|
88
|
+
imports `@owlmeans/server-api` — and a static property is inherited, so a subclass answers its
|
|
89
|
+
nearest declaring ancestor's status and redeclares to change it.
|
|
90
|
+
- The auth branches win over a declaration, in that order (`AuthForbidden extends
|
|
91
|
+
AuthorizationError`, so 403 is tested first). An entitlement or permission refusal extends
|
|
92
|
+
`AuthForbidden` rather than declaring 403.
|
|
93
|
+
- `httpErrorHelper.handleError` resolves the status on the error AS THROWN first, and asks the ENSURED
|
|
94
|
+
(`ResilientError.ensure`) error only when that answers 500. The thrown object is the one whose
|
|
95
|
+
class is certainly what was raised; the rebuild is what gives a status to a marshalled error that
|
|
96
|
+
crossed a hop as a plain `Error`. `ensure` returns an error from any `@owlmeans/error` copy
|
|
97
|
+
untouched, so a development body keeps the thrown class's `type` (`AuthFailedError|||api:auth:…`) even in a
|
|
98
|
+
process holding duplicate module copies (`bun --preserve-symlinks`). `payloadHelper.executeResponse` ensures
|
|
99
|
+
nothing and answers the rejected error's status. `@owlmeans/server-socket` answers an upgrade
|
|
100
|
+
through the same `httpErrorHelper.handleError`.
|
|
101
|
+
- An auth family is recognised by `instanceof` OR by an exact registered type name — the instance's
|
|
102
|
+
`type` or any static `typeName` on its constructor chain — so a class from another module copy
|
|
103
|
+
answers the same status. Match whole names, never substrings: a subclass's `typeName` does not
|
|
104
|
+
reliably embed its parent's (`EntitlementRefusal` extends `AuthForbidden`). A declared
|
|
105
|
+
`httpStatus` is a structural static read and survives duplicate copies as it is.
|
|
106
|
+
- **The status is what a production client can act on.** Only a development body is rebuilt into
|
|
107
|
+
its class; a production body is the incident id alone, so a client knows a refusal by its status
|
|
108
|
+
(§ Error exposure) — give a refusal the client must act on its own distinct 4xx. Nothing in the
|
|
109
|
+
framework treats a 404 specially — a missing route is a Fastify JSON body the client turns into
|
|
110
|
+
a 404 `ApiStatusError`.
|
|
111
|
+
|
|
112
|
+
`uploadedFile(request)` is the Fastify multipart boundary. Keep raw Fastify access there rather
|
|
113
|
+
than reaching through `request.original` in application code.
|
|
114
|
+
|
|
115
|
+
## Logging
|
|
116
|
+
|
|
117
|
+
The server logs through `@owlmeans/log`, not a second pino, and **never with what a caller sent**: a
|
|
118
|
+
request body, its raw form (`rawBody`), multipart fields and file bytes, headers (authorization,
|
|
119
|
+
cookies) and query strings never reach a record. Two seams in `utils/log.ts` (`fastifyLogUtils`) hold
|
|
120
|
+
that for every record Fastify or a plugin writes:
|
|
121
|
+
|
|
122
|
+
| Seam | What it does |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `loggerInstance` — the pino-shaped adapter (scope `http`) | every record's data passes `safeData`: a request object becomes `{ method, path }`, a reply `{ statusCode }`, bytes `[Buffer N bytes]`, any other class instance `[Name]`; the keys `body`, `rawBody`, `payload`, `files`, `file`, `headers`, `rawHeaders`, `query`, `querystring`, `cookies`, `cookie`, `authorization` are dropped at every depth (a number or boolean under them stays); every string — the message too — loses the query of a URL it names. Fastify's own pino serializers are ignored |
|
|
125
|
+
| `logController` — a subclass of Fastify's `LogController` (5.12+; Fastify accepts only an instance of its class) | no per-request pair; a request the default error handler answers — a body the route schema refuses, an unparsable payload, a size or multipart limit, an error thrown by an intermediate's `preHandler` — goes to `failure`; a missing route is one debug `Route not found` with method and path |
|
|
126
|
+
|
|
127
|
+
A request is one `debug` record from `onResponse` (method, path without the query, status, ms). A
|
|
128
|
+
failed request is logged once — by `httpErrorHelper.handleError` for a handler's error, by the log
|
|
129
|
+
controller for one Fastify answers itself — through `fastifyLogUtils.failure`, by what its status
|
|
130
|
+
means: **5xx → `error`** (the error with its stack, the incident id when there is one), **403 → `warn`,
|
|
131
|
+
`event: 'access.forbidden'`**, 401 → `debug`, `event: 'auth.refused'`, any other 4xx → `debug`
|
|
132
|
+
`Request refused` with `code` (`FST_ERR_VALIDATION`, …) and message. Method, path and status only — a
|
|
133
|
+
schema refusal therefore writes nothing at info. The response a refusal answers is Fastify's own
|
|
134
|
+
(`{ statusCode, code, error, message }`), unchanged. The level and format come from `cfg.log`
|
|
135
|
+
(`/log`); access lines at info are `cfg.log.debug: 'http'`. `tests/log-redaction.spec.ts` posts a
|
|
136
|
+
schema-refused JSON and multipart body with a marker, an authorization header, a cookie and a query
|
|
137
|
+
token, and asserts no record and no written line holds any of them.
|
|
138
|
+
|
|
139
|
+
## Error exposure
|
|
140
|
+
|
|
141
|
+
An exact IAM `AuthForbidden` or `AccessError` refusal answers with
|
|
142
|
+
`X-OwlMeans-Denial: access-denied`, exposed through CORS. Subclasses such as entitlement refusals
|
|
143
|
+
do not get this marker. The marker survives production exposure so browser clients can present a
|
|
144
|
+
permission message while keeping diagnostic bodies private.
|
|
145
|
+
|
|
146
|
+
`httpErrorHelper.handleError` always assigns an incident UUID, attaches it to the logged error and returns it in the
|
|
147
|
+
`X-Incident-ID` response header (`INCIDENT_ID_HEADER` in `./utils`, the same name and value
|
|
148
|
+
`@owlmeans/api` exports; exposed through CORS).
|
|
149
|
+
|
|
150
|
+
- **Production (the default):** a non-2xx body carries ONLY that incident id, under the resolved
|
|
151
|
+
HTTP status. A typed refusal is never exposed — its class, message and packed fields stay in the
|
|
152
|
+
server log under the id.
|
|
153
|
+
- **Development** (an explicit `cfg.http.errors.exposure = 'development'`): the typed marshalled
|
|
154
|
+
form with message and stack, which the client rebuilds into its class.
|
|
155
|
+
- A client reads a production failure with `@owlmeans/api`'s `./status` subpath: `apiStatusHelper.httpStatusOf(e)`
|
|
156
|
+
(the status a 428 or 409 is acted on by) and `.incidentIdOf(e)` (the id a person reports). Keep
|
|
157
|
+
the default production-safe, and tell a client to report the incident id.
|
|
38
158
|
|
|
39
|
-
|
|
40
|
-
- `
|
|
159
|
+
Do not use unbound compatibility handler wrappers. For a WebSocket route use
|
|
160
|
+
`@owlmeans/server-socket`'s `connection(protocol, callback)`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.local.d.ts","sourceRoot":"","sources":["../src/consts.local.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,QAAmB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.local.js","sourceRoot":"","sources":["../src/consts.local.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,EAAE,CAAA"}
|
package/build/errors.d.ts
CHANGED
|
@@ -11,4 +11,16 @@ export declare class NoFileError extends ApiError {
|
|
|
11
11
|
static typeName: string;
|
|
12
12
|
constructor();
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* `handlers().body/params/request` was handed something it cannot serve — not a plain callback,
|
|
16
|
+
* and not a `BoundEntrypointHandler` bound to the SAME protocol it is being wrapped for. Thrown
|
|
17
|
+
* from inside the bound handler's own request path, so the module still loads and every OTHER
|
|
18
|
+
* route on it still answers; only the misconfigured one reports why, as a 500 rather than the
|
|
19
|
+
* opaque `TypeError: handler is not a function` a double wrap used to fail with. See
|
|
20
|
+
* `handlers()` in `protocol.ts` for what does and does not reach here.
|
|
21
|
+
*/
|
|
22
|
+
export declare class HandlerMisconfiguredError extends ApiError {
|
|
23
|
+
static typeName: string;
|
|
24
|
+
constructor(message?: string);
|
|
25
|
+
}
|
|
14
26
|
//# sourceMappingURL=errors.d.ts.map
|
package/build/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAGxC,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,OAAuB,QAAQ,SAAoB;IAEnD,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF;AAED,qBAAa,WAAY,SAAQ,QAAQ;IACvC,OAAuB,QAAQ,SAAsC;IAErE,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF;AAED,qBAAa,WAAY,SAAQ,QAAQ;IACvC,OAAuB,QAAQ,SAAgB;IAE/C,cAGC;CACF"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAGxC,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,OAAuB,QAAQ,SAAoB;IAEnD,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF;AAED,qBAAa,WAAY,SAAQ,QAAQ;IACvC,OAAuB,QAAQ,SAAsC;IAErE,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF;AAED,qBAAa,WAAY,SAAQ,QAAQ;IACvC,OAAuB,QAAQ,SAAgB;IAE/C,cAGC;CACF;AAED;;;;;;;GAOG;AACH,qBAAa,yBAA0B,SAAQ,QAAQ;IACrD,OAAuB,QAAQ,SAA8B;IAE7D,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF"}
|
package/build/errors.js
CHANGED
|
@@ -21,7 +21,23 @@ export class NoFileError extends ApiError {
|
|
|
21
21
|
this.type = NoFileError.typeName;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* `handlers().body/params/request` was handed something it cannot serve — not a plain callback,
|
|
26
|
+
* and not a `BoundEntrypointHandler` bound to the SAME protocol it is being wrapped for. Thrown
|
|
27
|
+
* from inside the bound handler's own request path, so the module still loads and every OTHER
|
|
28
|
+
* route on it still answers; only the misconfigured one reports why, as a 500 rather than the
|
|
29
|
+
* opaque `TypeError: handler is not a function` a double wrap used to fail with. See
|
|
30
|
+
* `handlers()` in `protocol.ts` for what does and does not reach here.
|
|
31
|
+
*/
|
|
32
|
+
export class HandlerMisconfiguredError extends ApiError {
|
|
33
|
+
static typeName = 'HandlerMisconfiguredError';
|
|
34
|
+
constructor(message = 'error') {
|
|
35
|
+
super(`handler:misconfigured:${message}`);
|
|
36
|
+
this.type = HandlerMisconfiguredError.typeName;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
24
39
|
ResilientError.registerErrorClass(AuthFailedError);
|
|
25
40
|
ResilientError.registerErrorClass(AccessError);
|
|
26
41
|
ResilientError.registerErrorClass(NoFileError);
|
|
42
|
+
ResilientError.registerErrorClass(HandlerMisconfiguredError);
|
|
27
43
|
//# sourceMappingURL=errors.js.map
|
package/build/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IACpC,MAAM,CAAU,QAAQ,GAAG,iBAAiB,CAAA;IAEnD,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAA;IACtC,CAAC;CACF;AAED,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAChC,MAAM,CAAU,QAAQ,GAAG,SAAS,eAAe,CAAC,QAAQ,EAAE,CAAA;IAErE,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAA;IAClC,CAAC;CACF;AAED,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAChC,MAAM,CAAU,QAAQ,GAAG,aAAa,CAAA;IAE/C;QACE,KAAK,CAAC,SAAS,CAAC,CAAA;QAChB,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAA;IAClC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAA;AAClD,cAAc,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAA;AAC9C,cAAc,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IACpC,MAAM,CAAU,QAAQ,GAAG,iBAAiB,CAAA;IAEnD,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAA;IACtC,CAAC;CACF;AAED,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAChC,MAAM,CAAU,QAAQ,GAAG,SAAS,eAAe,CAAC,QAAQ,EAAE,CAAA;IAErE,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAA;IAClC,CAAC;CACF;AAED,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAChC,MAAM,CAAU,QAAQ,GAAG,aAAa,CAAA;IAE/C;QACE,KAAK,CAAC,SAAS,CAAC,CAAA;QAChB,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAA;IAClC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,yBAA0B,SAAQ,QAAQ;IAC9C,MAAM,CAAU,QAAQ,GAAG,2BAA2B,CAAA;IAE7D,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,yBAAyB,OAAO,EAAE,CAAC,CAAA;QACzC,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC,QAAQ,CAAA;IAChD,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAA;AAClD,cAAc,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAA;AAC9C,cAAc,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAA;AAC9C,cAAc,CAAC,kBAAkB,CAAC,yBAAyB,CAAC,CAAA"}
|
package/build/helper.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
1
|
+
import { type BasicConfig, type BasicContext } from '@owlmeans/context';
|
|
2
|
+
import { type AbstractRequest, type AbstractResponse } from '@owlmeans/entrypoint';
|
|
3
3
|
import type { RefedEntrypointHandler } from '@owlmeans/server-entrypoint';
|
|
4
|
-
import type { Context } from './types.js';
|
|
5
|
-
import type { MultipartFile } from '@fastify/multipart';
|
|
4
|
+
import type { Context, UploadedFile } from './types.js';
|
|
6
5
|
export declare const handleBody: <T>(handler: (payload: T, ctx: BasicContext<BasicConfig>, req: AbstractRequest) => Promise<any>) => RefedEntrypointHandler<AbstractResponse<any>>;
|
|
7
6
|
export declare const handleParams: <T>(handler: (payload: T, ctx: BasicContext<BasicConfig>, req: AbstractRequest) => Promise<any>) => RefedEntrypointHandler<AbstractResponse<any>>;
|
|
8
7
|
export declare const handleRequest: (handler: (payload: AbstractRequest, ctx: BasicContext<BasicConfig>, res?: AbstractResponse<any>) => Promise<any>) => RefedEntrypointHandler<AbstractResponse<any>>;
|
|
9
8
|
export declare const handleIntermediate: (handler: (payload: AbstractRequest, ctx: BasicContext<BasicConfig>) => Promise<BasicContext<BasicConfig> | null>) => RefedEntrypointHandler<AbstractResponse<Context | null>>;
|
|
10
9
|
export declare const extractUploadedFile: <T extends {} = {}>(req: AbstractRequest<T>) => Promise<UploadedFile | undefined>;
|
|
11
|
-
export interface UploadedFile extends MultipartFile {
|
|
12
|
-
}
|
|
13
10
|
//# sourceMappingURL=helper.d.ts.map
|
package/build/helper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAiB,MAAM,mBAAmB,CAAA;AACtF,OAAO,EAAqB,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACrG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACzE,OAAO,KAAK,EAAU,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAgB/D,eAAO,MAAM,UAAU,EAAE,CAAC,CAAC,EACzB,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,GAAG,CAAC,KACxF,sBAAsB,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAahD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,GAAG,CAAC,KAExF,sBAAsB,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAahD,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,CAC1B,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,GAAG,CAAC,KAC7G,sBAAsB,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAWhD,CAAA;AAED,eAAO,MAAM,kBAAkB,EAAE,CAC/B,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,KAC7G,sBAAsB,CAAC,gBAAgB,CAAC,OAAO,GAAG,IAAI,CAAC,CAc3D,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAU,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,eAAe,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAItH,CAAA"}
|
package/build/helper.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { assertContext } from '@owlmeans/context';
|
|
2
2
|
import { EntrypointOutcome } from '@owlmeans/entrypoint';
|
|
3
|
+
/**
|
|
4
|
+
* The context a handler runs against.
|
|
5
|
+
*
|
|
6
|
+
* The HTTP boundary hangs the request-scoped context on the raw Fastify request, so a handler
|
|
7
|
+
* reached that way gets it from there. A request that did not come from Fastify has no `original`
|
|
8
|
+
* at all — a queued call is rebuilt from an envelope, and a socket frame carries its own — so this
|
|
9
|
+
* has to fall back rather than dereference: these handlers are the same functions on every
|
|
10
|
+
* transport, and only the way they were reached differs.
|
|
11
|
+
*/
|
|
3
12
|
const _castContextFromOriginal = (req, def) => {
|
|
4
|
-
return req.original
|
|
13
|
+
return req.original?._ctx ?? def;
|
|
5
14
|
};
|
|
6
15
|
export const handleBody = handler => ref => async (req, res) => {
|
|
7
16
|
const ctx = assertContext(ref.ref?.ctx);
|
package/build/helper.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"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"}
|