@owlmeans/server-api 0.1.18-rc.5 → 0.1.18-rc.50

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 (103) hide show
  1. package/README.md +202 -46
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-api/SKILL.md +128 -22
  4. package/build/consts.local.d.ts +2 -0
  5. package/build/consts.local.d.ts.map +1 -0
  6. package/build/consts.local.js +2 -0
  7. package/build/consts.local.js.map +1 -0
  8. package/build/errors.d.ts +12 -0
  9. package/build/errors.d.ts.map +1 -1
  10. package/build/errors.js +16 -0
  11. package/build/errors.js.map +1 -1
  12. package/build/helper.d.ts +3 -6
  13. package/build/helper.d.ts.map +1 -1
  14. package/build/helper.js +10 -1
  15. package/build/helper.js.map +1 -1
  16. package/build/hold.d.ts +35 -0
  17. package/build/hold.d.ts.map +1 -0
  18. package/build/hold.js +57 -0
  19. package/build/hold.js.map +1 -0
  20. package/build/index.d.ts +2 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +2 -0
  23. package/build/index.js.map +1 -1
  24. package/build/protocol.d.ts +18 -0
  25. package/build/protocol.d.ts.map +1 -0
  26. package/build/protocol.js +87 -0
  27. package/build/protocol.js.map +1 -0
  28. package/build/server.d.ts +2 -3
  29. package/build/server.d.ts.map +1 -1
  30. package/build/server.js +37 -32
  31. package/build/server.js.map +1 -1
  32. package/build/types.d.ts +31 -2
  33. package/build/types.d.ts.map +1 -1
  34. package/build/types.local.d.ts +38 -0
  35. package/build/types.local.d.ts.map +1 -0
  36. package/build/types.local.js +2 -0
  37. package/build/types.local.js.map +1 -0
  38. package/build/utils/consts.d.ts +2 -0
  39. package/build/utils/consts.d.ts.map +1 -0
  40. package/build/utils/consts.js +2 -0
  41. package/build/utils/consts.js.map +1 -0
  42. package/build/utils/consts.local.d.ts +5 -0
  43. package/build/utils/consts.local.d.ts.map +1 -0
  44. package/build/utils/consts.local.js +5 -0
  45. package/build/utils/consts.local.js.map +1 -0
  46. package/build/utils/error.d.ts +47 -1
  47. package/build/utils/error.d.ts.map +1 -1
  48. package/build/utils/error.js +192 -9
  49. package/build/utils/error.js.map +1 -1
  50. package/build/utils/guards.d.ts +1 -5
  51. package/build/utils/guards.d.ts.map +1 -1
  52. package/build/utils/guards.js +11 -15
  53. package/build/utils/guards.js.map +1 -1
  54. package/build/utils/index.d.ts +2 -0
  55. package/build/utils/index.d.ts.map +1 -1
  56. package/build/utils/index.js +1 -0
  57. package/build/utils/index.js.map +1 -1
  58. package/build/utils/log.d.ts +11 -0
  59. package/build/utils/log.d.ts.map +1 -0
  60. package/build/utils/log.js +57 -0
  61. package/build/utils/log.js.map +1 -0
  62. package/build/utils/payload.d.ts.map +1 -1
  63. package/build/utils/payload.js +6 -2
  64. package/build/utils/payload.js.map +1 -1
  65. package/build/utils/server.d.ts +3 -6
  66. package/build/utils/server.d.ts.map +1 -1
  67. package/build/utils/server.js +8 -9
  68. package/build/utils/server.js.map +1 -1
  69. package/build/utils/types.d.ts +18 -0
  70. package/build/utils/types.d.ts.map +1 -0
  71. package/build/utils/types.js +2 -0
  72. package/build/utils/types.js.map +1 -0
  73. package/build/utils/types.local.d.ts +9 -0
  74. package/build/utils/types.local.d.ts.map +1 -0
  75. package/build/utils/types.local.js +2 -0
  76. package/build/utils/types.local.js.map +1 -0
  77. package/package.json +22 -18
  78. package/src/consts.local.ts +1 -0
  79. package/src/errors.ts +18 -0
  80. package/src/helper.ts +13 -8
  81. package/src/hold.ts +66 -0
  82. package/src/index.ts +2 -0
  83. package/src/protocol.ts +149 -0
  84. package/src/server.ts +67 -67
  85. package/src/types.local.ts +44 -0
  86. package/src/types.ts +38 -4
  87. package/src/utils/consts.local.ts +7 -0
  88. package/src/utils/consts.ts +1 -0
  89. package/src/utils/error.ts +213 -11
  90. package/src/utils/guards.ts +12 -22
  91. package/src/utils/index.ts +2 -0
  92. package/src/utils/log.ts +62 -0
  93. package/src/utils/payload.ts +6 -2
  94. package/src/utils/server.ts +12 -17
  95. package/src/utils/types.local.ts +12 -0
  96. package/src/utils/types.ts +18 -0
  97. package/tests/error-status.spec.ts +349 -0
  98. package/tests/handlers.spec.ts +83 -0
  99. package/tests/json-body.spec.ts +72 -0
  100. package/tests/log-adapter.spec.ts +52 -0
  101. package/tests/tsconfig.json +12 -0
  102. package/tsconfig.json +1 -0
  103. package/build/.gitkeep +0 -0
package/README.md CHANGED
@@ -1,80 +1,236 @@
1
1
  # @owlmeans/server-api
2
2
 
3
- Fastify-based HTTP/WebSocket server with handler wrappers for the OwlMeans module system.
4
-
5
- ## Overview
6
-
7
- - `handleBody`, `handleParams`, `handleRequest` wrap business logic with context injection and error handling
8
- - Built on [Fastify](https://fastify.dev/) — registered modules become Fastify routes automatically
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-rc.50
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
- Handler functions called via `elevate()` in modules:
39
+ ### Body, params and request handlers
21
40
 
22
- ```typescript
23
- import { handleBody, handleParams, handleRequest } from '@owlmeans/server-app'
41
+ ```ts
42
+ import { handlers } from '@owlmeans/server-api'
43
+ import { bind } from '@owlmeans/server-entrypoint'
44
+ import { requireEntityKey } from '@owlmeans/auth-common'
45
+ import { projectProtocols } from 'my-app-common'
46
+ import type { Context } from './context.js'
24
47
 
25
- // Body handler: receives parsed + validated body as first arg
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
- // Params handler: receives validated URL params as first arg
32
- export const get = handleParams<{ id: string }>(async (params, context, req) => {
33
- return await (context as Context).project().get(params.id)
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: requireEntityKey(request) }))
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: requireEntityKey(request) }))
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: requireEntityKey(request), ...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 = requireEntityKey(request)
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
- // Request handler: receives the full AbstractRequest
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
- ## API
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
- ### `handleBody<T>(handler): RefedModuleHandler`
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
- Wraps a handler that receives the validated request body as the first argument.
47
- ```typescript
48
- handler: (payload: T, ctx: Context, req: AbstractRequest) => Promise<any>
100
+ constructor(message: string = 'error') {
101
+ super(ProjectBusy.typeName, `project-busy:${message}`)
102
+ }
103
+ }
49
104
  ```
50
105
 
51
- ### `handleParams<T>(handler): RefedModuleHandler`
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
- Wraps a handler that receives the validated URL params as the first argument.
54
- ```typescript
55
- handler: (payload: T, ctx: Context, req: AbstractRequest) => Promise<any>
122
+ return context.attachment().create({
123
+ entityId: requireEntityKey(request),
124
+ name: file.filename,
125
+ mimetype: file.mimetype,
126
+ size: content.length,
127
+ })
128
+ })
56
129
  ```
57
130
 
58
- ### `handleRequest(handler): RefedModuleHandler`
131
+ ### Holding the port during boot
132
+
133
+ ```ts
134
+ import { holdApiPort } from '@owlmeans/server-api'
59
135
 
60
- Wraps a handler that receives the full request object.
61
- ```typescript
62
- handler: (req: AbstractRequest, ctx: Context, res?: AbstractResponse) => Promise<any>
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
- ### `handleIntermediate(handler): RefedModuleHandler`
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
- Wraps middleware-layer handlers that do not return a final response.
148
+ const context = appendApiServer(makeServerContext(cfg))
149
+ context.registerEntrypoints(serverBindings)
150
+ await context.configure().init()
68
151
 
69
- ### `extractUploadedFile(req, fieldName): UploadedFile | null`
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
- Extract a multipart-uploaded file from the request.
157
+ await context.getApiServer().listen()
158
+ ```
72
159
 
73
- ## Related Packages
160
+ ## API
74
161
 
75
- - [`@owlmeans/server-app`](../server-app) — re-exports all handlers; preferred import point
76
- - [`@owlmeans/server-module`](../server-module) — `elevate()` attaches handlers to modules
77
- - [`@owlmeans/server-socket`](../server-socket) — WebSocket `handleConnection` counterpart
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 `attachEntity` |
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
+ | `provideRequest(alias, req, provision?)` | function | Build an `AbstractRequest` from a Fastify request |
194
+ | `executeResponse(response, reply, throwOnError?)` | function | Send an `AbstractResponse` onto a reply |
195
+ | `handleError(error, reply)` | function | Answer an error with `errorStatus` and the marshalled body |
196
+ | `errorStatus(error)` | function | 403 / 401 for auth errors (by class or type name), else the class's declared 4xx, else 500 |
197
+ | `declaredErrorStatus(error)` | function | The integer 4xx a class declares through `static httpStatus`, or `null` |
198
+ | `HttpStatusDeclaration` | type | `{ httpStatus?: unknown }` — the structural shape a declaring class has |
199
+ | `fixFormatDates(schema)` | function | Rewrite `date-time` object schemas as strings |
200
+ | `populateContext(req, context)`, `extractContext(req, ctx?, location?)` | function | Carry the request-scoped context on the raw request |
201
+
202
+ ## Common pitfalls
203
+
204
+ - `.body` and `.params` exist only for a protocol that declares that section; for anything else use
205
+ `.request`.
206
+ - Do not reach into `request.original` from application code — `uploadedFile(request)` is the
207
+ multipart boundary.
208
+ - Prefer `handlers<Context>()` over the unbound `handle*` wrappers, and never hand-write a
209
+ `RefedEntrypointHandler` for an HTTP route.
210
+ - A handler is wrapped by `handlers<Context>()` **exactly once**: wrap a plain function where it is
211
+ bound (`bind(p, api.body(p, create))`), or bind the already-bound export directly
212
+ (`const create = api.body(p, ...)` then `bind(p, create)`) — never both. `tsc` rejects a double
213
+ wrap (`TS2345 "BoundEntrypointHandler<…> is not assignable"`); at runtime a handler already bound
214
+ to the SAME protocol is returned unchanged with a one-time console warning, and anything else that
215
+ is not a plain function fails that one route with `HandlerMisconfiguredError` instead of the
216
+ opaque `TypeError: handler is not a function`.
217
+ - A guard only authenticates. Gates authorize, and the handler keeps its organization check keyed on
218
+ `requireEntityKey(request)` — never an id read from the token.
219
+ - `request.entity` is absent when no entity resolver is registered; `requireEntityKey` falls back to
220
+ the slug there, `requireEntity` throws.
221
+ - A handler always answers 200 on success. The port comes from `cfg.services[cfg.service]`
222
+ (`internalPort ?? port ?? 80`).
223
+ - A `holdApiPort` bind failure must end the process with a non-zero exit.
224
+ - WebSocket routes use `connection(protocol, callback)` from `@owlmeans/server-socket`.
225
+
226
+ ## Related packages
227
+
228
+ - [`@owlmeans/server-entrypoint`](../server-entrypoint) — `bind()` attaches handlers to protocols
229
+ - [`@owlmeans/server-app`](../server-app) — application bootstrap and re-exports
230
+ - [`@owlmeans/server-socket`](../server-socket) — the WebSocket counterpart of `handlers`
231
+ - [`@owlmeans/entrypoint`](../entrypoint) — `contract`, `typed`, request/response types
232
+ - [`@owlmeans/auth-common`](../auth-common) — `requireEntityKey`, `attachEntity`, guard aliases
233
+ - [`@owlmeans/api`](../api) — the client that calls these endpoints from other services
78
234
 
79
235
  <!-- owlmeans:agent-guidance:start -->
80
236
  ## Agent guidance
@@ -84,7 +240,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
84
240
  your project's skill store (`.agents/skills/`):
85
241
 
86
242
  ```sh
87
- npx @owlmeans/agent-skills
243
+ npx @owlmeans/agent-skills@^0.1.18-rc.48
88
244
  ```
89
245
 
90
246
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-api",
4
- "version": "0.1.18-rc.0",
5
- "generatedAt": "2026-08-16T22:20:50.496Z",
4
+ "version": "0.1.18-rc.50",
5
+ "generatedAt": "2026-10-05T08:32:19.085Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,40 +1,146 @@
1
1
  ---
2
2
  name: server-api
3
- description: How to use @owlmeans/server-api — Fastify-based API server factory, request/response shapes, server middleware integration. Auto-invoked when importing server-api types or extending the server middleware stack.
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
- **Layer:** Server
11
- **Install:** `"@owlmeans/server-api": "^0.1.18-rc.0"` in `dependencies`
10
+ **Install:** `bun add @owlmeans/server-api@^0.1.18-rc.50`
12
11
 
13
- ## Key Exports
12
+ Make handlers from the protocol declaration so input and output types stay coupled to the shared
13
+ contract:
14
14
 
15
- | Export | Description |
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
- ## Subpath Exports
18
+ const create = api.body(projectProtocols.create, async (body, context, request) =>
19
+ context.projects.create(body, request.auth)
20
+ )
24
21
 
25
- - `./utils` — server utility functions
22
+ const get = api.params(projectProtocols.get, async ({ id }, context) =>
23
+ context.projects.get(id)
24
+ )
26
25
 
27
- ## Usage
26
+ const search = api.request(projectProtocols.search, async (request, context) =>
27
+ context.projects.search(request.query)
28
+ )
28
29
 
29
- The server is normally constructed transparently by `@owlmeans/server-app#main()`. Use `server-api` directly only when you need to register raw Fastify plugins or customize transport:
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
30
48
 
31
- ```typescript
32
- import { makeServer } from '@owlmeans/server-api'
33
- const server = makeServer({ port: 8080 })
34
- context.registerService(server)
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.
52
+
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
- ## Depends On
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 `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
+ - `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`). `executeResponse` ensures
99
+ nothing and answers the rejected error's status. `@owlmeans/server-socket` answers an upgrade
100
+ through the same `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: Fastify gets a pino-shaped adapter as its
118
+ `loggerInstance` (scope `http`) with its own request/response lines disabled. A request is one `debug`
119
+ record from `onResponse` (method, path without the query, status, ms). A failed request is logged once,
120
+ in `handleError`, by what it means: **5xx → `error`** (the error with its stack and the incident id),
121
+ **403 → `warn`, `event: 'access.forbidden'`**, 401 → `debug`, `event: 'auth.refused'`, any other 4xx →
122
+ `debug`. The level and format come from `cfg.log` (`/log`); access lines at info are
123
+ `cfg.log.debug: 'http'`.
124
+
125
+ ## Error exposure
126
+
127
+ An exact IAM `AuthForbidden` or `AccessError` refusal answers with
128
+ `X-OwlMeans-Denial: access-denied`, exposed through CORS. Subclasses such as entitlement refusals
129
+ do not get this marker. The marker survives production exposure so browser clients can present a
130
+ permission message while keeping diagnostic bodies private.
131
+
132
+ `handleError` always assigns an incident UUID, attaches it to the logged error and returns it in the
133
+ `X-Incident-ID` response header (`INCIDENT_ID_HEADER` in `./utils`, the same name and value
134
+ `@owlmeans/api` exports; exposed through CORS).
135
+
136
+ - **Production (the default):** a non-2xx body carries ONLY that incident id, under the resolved
137
+ HTTP status. A typed refusal is never exposed — its class, message and packed fields stay in the
138
+ server log under the id.
139
+ - **Development** (an explicit `cfg.http.errors.exposure = 'development'`): the typed marshalled
140
+ form with message and stack, which the client rebuilds into its class.
141
+ - A client reads a production failure with `@owlmeans/api`'s `./status` subpath: `httpStatusOf(e)`
142
+ (the status a 428 or 409 is acted on by) and `incidentIdOf(e)` (the id a person reports). Keep
143
+ the default production-safe, and tell a client to report the incident id.
38
144
 
39
- - `@owlmeans/context`, `@owlmeans/entrypoint`, `@owlmeans/route`
40
- - `fastify` (runtime)
145
+ Do not use unbound compatibility handler wrappers. For a WebSocket route use
146
+ `@owlmeans/server-socket`'s `connection(protocol, callback)`.
@@ -0,0 +1,2 @@
1
+ export declare const bodyLimit: number;
2
+ //# sourceMappingURL=consts.local.d.ts.map
@@ -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,2 @@
1
+ export const bodyLimit = 1024 * 1024 * 20;
2
+ //# sourceMappingURL=consts.local.js.map
@@ -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
@@ -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
@@ -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 { BasicConfig, BasicContext } from '@owlmeans/context';
2
- import type { AbstractRequest, AbstractResponse } from '@owlmeans/entrypoint';
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
@@ -1 +1 @@
1
- {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGlE,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACzE,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,YAAY,CAAA;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAMvD,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;AAED,MAAM,WAAW,YAAa,SAAQ,aAAa;CAAI"}
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._ctx ?? def;
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);
@@ -1 +1 @@
1
- {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"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"}