@owlmeans/server-api 0.1.18-rc.3 → 0.1.18-rc.31

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 (56) hide show
  1. package/README.md +179 -47
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-api/SKILL.md +45 -22
  4. package/build/errors.d.ts +12 -0
  5. package/build/errors.d.ts.map +1 -1
  6. package/build/errors.js +16 -0
  7. package/build/errors.js.map +1 -1
  8. package/build/helper.d.ts.map +1 -1
  9. package/build/helper.js +10 -1
  10. package/build/helper.js.map +1 -1
  11. package/build/hold.d.ts +52 -0
  12. package/build/hold.d.ts.map +1 -0
  13. package/build/hold.js +56 -0
  14. package/build/hold.js.map +1 -0
  15. package/build/index.d.ts +2 -0
  16. package/build/index.d.ts.map +1 -1
  17. package/build/index.js +2 -0
  18. package/build/index.js.map +1 -1
  19. package/build/protocol.d.ts +45 -0
  20. package/build/protocol.d.ts.map +1 -0
  21. package/build/protocol.js +86 -0
  22. package/build/protocol.js.map +1 -0
  23. package/build/server.d.ts.map +1 -1
  24. package/build/server.js +7 -12
  25. package/build/server.js.map +1 -1
  26. package/build/types.d.ts +1 -2
  27. package/build/types.d.ts.map +1 -1
  28. package/build/utils/error.d.ts +13 -0
  29. package/build/utils/error.d.ts.map +1 -1
  30. package/build/utils/error.js +21 -4
  31. package/build/utils/error.js.map +1 -1
  32. package/build/utils/guards.d.ts.map +1 -1
  33. package/build/utils/guards.js +11 -15
  34. package/build/utils/guards.js.map +1 -1
  35. package/build/utils/payload.d.ts.map +1 -1
  36. package/build/utils/payload.js +3 -0
  37. package/build/utils/payload.js.map +1 -1
  38. package/build/utils/server.d.ts.map +1 -1
  39. package/build/utils/server.js +4 -1
  40. package/build/utils/server.js.map +1 -1
  41. package/package.json +13 -11
  42. package/src/errors.ts +18 -0
  43. package/src/helper.ts +10 -1
  44. package/src/hold.ts +84 -0
  45. package/src/index.ts +2 -0
  46. package/src/protocol.ts +185 -0
  47. package/src/server.ts +34 -43
  48. package/src/types.ts +1 -3
  49. package/src/utils/error.ts +20 -3
  50. package/src/utils/guards.ts +10 -17
  51. package/src/utils/payload.ts +3 -0
  52. package/src/utils/server.ts +4 -1
  53. package/tests/handlers.spec.ts +83 -0
  54. package/tests/tsconfig.json +12 -0
  55. package/tsconfig.json +1 -0
  56. package/build/.gitkeep +0 -0
package/README.md CHANGED
@@ -1,80 +1,212 @@
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.27
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, anything else 500, with the marshalled `ResilientError` as the body.
31
+
18
32
  ## Usage
19
33
 
20
- Handler functions called via `elevate()` in modules:
34
+ ### Body, params and request handlers
21
35
 
22
- ```typescript
23
- import { handleBody, handleParams, handleRequest } from '@owlmeans/server-app'
36
+ ```ts
37
+ import { handlers } from '@owlmeans/server-api'
38
+ import { bind } from '@owlmeans/server-entrypoint'
39
+ import { requireEntityKey } from '@owlmeans/auth-common'
40
+ import { projectProtocols } from 'my-app-common'
41
+ import type { Context } from './context.js'
24
42
 
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
- })
43
+ const api = handlers<Context>()
30
44
 
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
- })
45
+ // body: the validated body first, then context and the full request
46
+ const create = api.body(projectProtocols.create, async (payload, context, request) =>
47
+ context.project().create({ ...payload, entityId: requireEntityKey(request) }))
35
48
 
36
- // Request handler: receives the full AbstractRequest
37
- export const health = handleRequest(async (req, context) => {
38
- return { status: 'ok' }
39
- })
49
+ // params: the validated URL params first
50
+ const get = api.params(projectProtocols.get, async ({ id }, context, request) =>
51
+ context.project().load({ id, entityId: requireEntityKey(request) }))
52
+
53
+ // request: every typed section plus request metadata (auth, entity, headers)
54
+ const search = api.request(projectProtocols.search, async (request, context) =>
55
+ context.project().list({ entityId: requireEntityKey(request), ...request.query }))
56
+
57
+ export const serverBindings = [
58
+ bind(projectProtocols.base),
59
+ bind(projectProtocols.create, create),
60
+ bind(projectProtocols.get, get),
61
+ bind(projectProtocols.search, search),
62
+ ]
40
63
  ```
41
64
 
42
- ## API
65
+ ### Refusing a request
66
+
67
+ Guards and gates run before the handler; a handler still checks ownership and throws a typed error,
68
+ which the server maps onto a status.
69
+
70
+ ```ts
71
+ import { AuthForbidden } from '@owlmeans/auth'
43
72
 
44
- ### `handleBody<T>(handler): RefedModuleHandler`
73
+ // the protocol declares both `params` and `body`
74
+ export const rename = api.body(projectProtocols.rename, async ({ name }, context, request) => {
75
+ const entityId = requireEntityKey(request)
76
+ const project = await context.project().load({ id: request.params.id, entityId })
77
+ if (project == null) {
78
+ throw new AuthForbidden('project') // 403
79
+ }
45
80
 
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>
81
+ return context.project().update({ ...project, name })
82
+ })
49
83
  ```
50
84
 
51
- ### `handleParams<T>(handler): RefedModuleHandler`
85
+ ### File upload
86
+
87
+ `uploadedFile(request)` is the only place a handler touches the Fastify multipart API. The protocol
88
+ must declare a `body` section.
89
+
90
+ ```ts
91
+ import { handlers, NoFileError, uploadedFile } from '@owlmeans/server-api'
52
92
 
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>
93
+ export const upload = api.request(invoiceProtocols.attach, async (request, context) => {
94
+ const file = await uploadedFile(request)
95
+ if (file == null) {
96
+ throw new NoFileError()
97
+ }
98
+
99
+ const content = await file.toBuffer()
100
+
101
+ return context.attachment().create({
102
+ entityId: requireEntityKey(request),
103
+ name: file.filename,
104
+ mimetype: file.mimetype,
105
+ size: content.length,
106
+ })
107
+ })
56
108
  ```
57
109
 
58
- ### `handleRequest(handler): RefedModuleHandler`
110
+ ### Holding the port during boot
59
111
 
60
- Wraps a handler that receives the full request object.
61
- ```typescript
62
- handler: (req: AbstractRequest, ctx: Context, res?: AbstractResponse) => Promise<any>
112
+ ```ts
113
+ import { holdApiPort } from '@owlmeans/server-api'
114
+
115
+ const hold = await holdApiPort(cfg, { okPath: '/healthz' }) // 200 on /healthz, 503 elsewhere
116
+ await context.configure().init()
117
+ await hold.release()
118
+ await context.getApiServer().listen()
63
119
  ```
64
120
 
65
- ### `handleIntermediate(handler): RefedModuleHandler`
121
+ ### A context without `@owlmeans/server-app`
66
122
 
67
- Wraps middleware-layer handlers that do not return a final response.
123
+ ```ts
124
+ import { makeServerContext } from '@owlmeans/server-context'
125
+ import { appendApiServer } from '@owlmeans/server-api'
68
126
 
69
- ### `extractUploadedFile(req, fieldName): UploadedFile | null`
127
+ const context = appendApiServer(makeServerContext(cfg))
128
+ context.registerEntrypoints(serverBindings)
129
+ await context.configure().init()
70
130
 
71
- Extract a multipart-uploaded file from the request.
131
+ // the Fastify instance, for plugins the framework does not register
132
+ context.getApiServer().server.addHook('onResponse', async (request, reply) => {
133
+ console.log(request.url, reply.statusCode)
134
+ })
72
135
 
73
- ## Related Packages
136
+ await context.getApiServer().listen()
137
+ ```
138
+
139
+ ## API
74
140
 
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
141
+ ### Root export
142
+
143
+ | Symbol | Kind | Purpose |
144
+ |---|---|---|
145
+ | `handlers<Context>()` | function | Factories `body(protocol, fn)`, `params(protocol, fn)`, `request(protocol, fn)` returning `BoundEntrypointHandler<Protocol>` |
146
+ | `uploadedFile(request)` | function | The multipart file of a request, or `undefined` |
147
+ | `extractUploadedFile(req)` | function | Same boundary for an untyped `AbstractRequest`; returns `Promise<UploadedFile \| undefined>` |
148
+ | `holdApiPort(cfg, opts?)` | function | Bind the service port before init; returns `ApiPortHold` with `release()` |
149
+ | `createApiServer(alias)` | function | Create the Fastify API server service |
150
+ | `appendApiServer(ctx, alias = DEFAULT_ALIAS)` | function | Register the API server and `context.getApiServer()` |
151
+ | `handleBody`, `handleParams`, `handleRequest`, `handleIntermediate` | function | Unbound compatibility wrappers returning `RefedEntrypointHandler` |
152
+ | `AuthFailedError`, `AccessError`, `NoFileError` | class | Registered `ResilientError` subclasses (401, 403, 500) |
153
+ | `DEFAULT_ALIAS` | const | `'api-server'` |
154
+ | `PORT` | const | `80`, used when the service route declares no port |
155
+ | `CLOSED_HOST`, `OPENED_HOST` | const | `'127.0.0.1'` and `'0.0.0.0'`; a service with `opened: true` listens on the second |
156
+ | `ApiServer`, `ApiServerAppend` | type | The server service and its context mixin |
157
+ | `ApiPortHold`, `ApiPortHoldOptions` | type | `holdApiPort` result and options (`okPath?`, `payload?`) |
158
+ | `Config`, `Context` | type | `ServerConfig` and `ServerContext & ApiServerAppend` |
159
+ | `Request`, `Response` | type | Fastify request and reply |
160
+ | `UploadedFile` | type | Fastify `MultipartFile` |
161
+
162
+ ### `./utils` subpath
163
+
164
+ For transport packages (such as `@owlmeans/server-socket`) that reuse the HTTP pipeline; application
165
+ code does not need it.
166
+
167
+ | Symbol | Kind | Purpose |
168
+ |---|---|---|
169
+ | `authorize(context, module, req, reply)` | function | Run guards, set `auth`, call `attachEntity` |
170
+ | `createServerHandler(module, location)` | function | The Fastify handler: authorize, gates, handle, respond |
171
+ | `canServeModule(context, module)` | function | Whether an entrypoint belongs on this HTTP server |
172
+ | `provideRequest(alias, req, provision?)` | function | Build an `AbstractRequest` from a Fastify request |
173
+ | `executeResponse(response, reply, throwOnError?)` | function | Send an `AbstractResponse` onto a reply |
174
+ | `handleError(error, reply)` | function | Map an error onto 401 / 403 / 500 |
175
+ | `fixFormatDates(schema)` | function | Rewrite `date-time` object schemas as strings |
176
+ | `populateContext(req, context)`, `extractContext(req, ctx?, location?)` | function | Carry the request-scoped context on the raw request |
177
+
178
+ ## Common pitfalls
179
+
180
+ - `.body` and `.params` exist only for a protocol that declares that section; for anything else use
181
+ `.request`.
182
+ - Do not reach into `request.original` from application code — `uploadedFile(request)` is the
183
+ multipart boundary.
184
+ - Prefer `handlers<Context>()` over the unbound `handle*` wrappers, and never hand-write a
185
+ `RefedEntrypointHandler` for an HTTP route.
186
+ - A handler is wrapped by `handlers<Context>()` **exactly once**: wrap a plain function where it is
187
+ bound (`bind(p, api.body(p, create))`), or bind the already-bound export directly
188
+ (`const create = api.body(p, ...)` then `bind(p, create)`) — never both. `tsc` rejects a double
189
+ wrap (`TS2345 "BoundEntrypointHandler<…> is not assignable"`); at runtime a handler already bound
190
+ to the SAME protocol is returned unchanged with a one-time console warning, and anything else that
191
+ is not a plain function fails that one route with `HandlerMisconfiguredError` instead of the
192
+ opaque `TypeError: handler is not a function`.
193
+ - A guard only authenticates. Gates authorize, and the handler keeps its organization check keyed on
194
+ `requireEntityKey(request)` — never an id read from the token.
195
+ - `request.entity` is absent when no entity resolver is registered; `requireEntityKey` falls back to
196
+ the slug there, `requireEntity` throws.
197
+ - A handler always answers 200 on success. The port comes from `cfg.services[cfg.service]`
198
+ (`internalPort ?? port ?? 80`).
199
+ - A `holdApiPort` bind failure must end the process with a non-zero exit.
200
+ - WebSocket routes use `connection(protocol, callback)` from `@owlmeans/server-socket`.
201
+
202
+ ## Related packages
203
+
204
+ - [`@owlmeans/server-entrypoint`](../server-entrypoint) — `bind()` attaches handlers to protocols
205
+ - [`@owlmeans/server-app`](../server-app) — application bootstrap and re-exports
206
+ - [`@owlmeans/server-socket`](../server-socket) — the WebSocket counterpart of `handlers`
207
+ - [`@owlmeans/entrypoint`](../entrypoint) — `contract`, `typed`, request/response types
208
+ - [`@owlmeans/auth-common`](../auth-common) — `requireEntityKey`, `attachEntity`, guard aliases
209
+ - [`@owlmeans/api`](../api) — the client that calls these endpoints from other services
78
210
 
79
211
  <!-- owlmeans:agent-guidance:start -->
80
212
  ## Agent guidance
@@ -84,7 +216,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
84
216
  your project's skill store (`.agents/skills/`):
85
217
 
86
218
  ```sh
87
- npx @owlmeans/agent-skills
219
+ npx @owlmeans/agent-skills@^0.1.18-rc.26
88
220
  ```
89
221
 
90
222
  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.31",
5
+ "generatedAt": "2026-09-15T13:12:48.098Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,40 +1,63 @@
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.27`
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
+ ## Wrap exactly once
30
42
 
31
- ```typescript
32
- import { makeServer } from '@owlmeans/server-api'
33
- const server = makeServer({ port: 8080 })
34
- context.registerService(server)
43
+ A handler is wrapped by `handlers<Context>()` exactly once — either shape above (create the bound
44
+ handler and bind it directly) is correct on its own. Never combine them: a handler module that
45
+ already exports a bound handler must be bound directly, not wrapped again where it is bound.
46
+
47
+ ```ts
48
+ // WRONG — bound once in the handler module, wrapped a second time here
49
+ export const create = api.body(projectProtocols.create, async (body, context) => ...)
50
+ bind(projectProtocols.create, api.body(projectProtocols.create, create))
35
51
  ```
36
52
 
37
- ## Depends On
53
+ `tsc` rejects the double wrap (`TS2345 "Argument of type 'BoundEntrypointHandler<…>' is not
54
+ assignable"`). At runtime, `body`/`params`/`request` return an already-bound handler for the SAME
55
+ protocol unchanged, with a one-time warning; anything else that is not a plain function fails only
56
+ that one route with `HandlerMisconfiguredError`, instead of the opaque
57
+ `TypeError: handler is not a function`.
58
+
59
+ `uploadedFile(request)` is the Fastify multipart boundary. Keep raw Fastify access there rather
60
+ than reaching through `request.original` in application code.
38
61
 
39
- - `@owlmeans/context`, `@owlmeans/entrypoint`, `@owlmeans/route`
40
- - `fastify` (runtime)
62
+ Do not use unbound compatibility handler wrappers. For a WebSocket route use
63
+ `@owlmeans/server-socket`'s `connection(protocol, callback)`.
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"}
@@ -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,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;AAevD,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"}
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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"}
@@ -0,0 +1,52 @@
1
+ import type { ServerConfig } from '@owlmeans/server-context';
2
+ export interface ApiPortHoldOptions {
3
+ /**
4
+ * The one path answered 200 while the hold is in place — the app's health endpoint, so
5
+ * whatever supervises the process keeps a conventional liveness answer through the boot.
6
+ * Everything else answers 503 with the same body. Absent, everything is 503.
7
+ */
8
+ okPath?: string;
9
+ /** Body of every answer, evaluated per request so a changing boot phase is reported live. */
10
+ payload?: () => unknown;
11
+ }
12
+ export interface ApiPortHold {
13
+ /**
14
+ * Free the port for the real server. Awaits the actual close — `listen()` follows immediately,
15
+ * and a predecessor still bound makes it throw — and force-closes keep-alive sockets, so a
16
+ * supervisor's own poller cannot hold the release open.
17
+ */
18
+ release: () => Promise<void>;
19
+ }
20
+ /**
21
+ * Own the app's port while the context initializes, and answer for the app until `listen()`
22
+ * takes over.
23
+ *
24
+ * The API server itself cannot do this: it builds its route table from the entrypoints the
25
+ * context knows about, so it can only bind AFTER `init()` resolves — and Fastify refuses new
26
+ * routes once it is listening. Without a hold, the whole boot window, and every way a boot can
27
+ * fail, leaves nothing on the port; the edge then answers a bare upstream connect error that
28
+ * names neither the app nor the reason, which is indistinguishable from a crashed pod or an app
29
+ * that was never installed.
30
+ *
31
+ * So the boot sequence an app wants is:
32
+ *
33
+ * const hold = await holdApiPort(cfg, { okPath, payload }) // bind FIRST
34
+ * try { ...initialize... } catch { record the failure; return } // hold keeps answering
35
+ * await hold.release()
36
+ * await context.getApiServer().listen()
37
+ *
38
+ * Everything that is not the health path answers **503, not 404**: the routes the app really
39
+ * serves do not exist yet, and "no such route" would be a lie that outlives the boot.
40
+ *
41
+ * The port and host are resolved exactly as `listen()` resolves them — from the service's own
42
+ * declaration in `cfg.services[cfg.service]` — so the hold and the server can never disagree
43
+ * about which socket they are trading.
44
+ *
45
+ * A bind failure here (EADDRINUSE above all) throws to the caller and must be treated as fatal:
46
+ * a process that continues past it ends the boot with nothing listening and nothing holding the
47
+ * event loop, i.e. a clean exit 0 while a predecessor keeps serving stale code. Name the error
48
+ * and exit non-zero — that is what makes a supervisor reclaim the port instead of respawning
49
+ * into it.
50
+ */
51
+ export declare const holdApiPort: (cfg: ServerConfig, opts?: ApiPortHoldOptions) => Promise<ApiPortHold>;
52
+ //# sourceMappingURL=hold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hold.d.ts","sourceRoot":"","sources":["../src/hold.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,WAAW,QACjB,YAAY,SAAQ,kBAAkB,KAC1C,OAAO,CAAC,WAAW,CA0BrB,CAAA"}
package/build/hold.js ADDED
@@ -0,0 +1,56 @@
1
+ import Fastify from 'fastify';
2
+ import { CLOSED_HOST, OPENED_HOST, PORT } from './consts.js';
3
+ /**
4
+ * Own the app's port while the context initializes, and answer for the app until `listen()`
5
+ * takes over.
6
+ *
7
+ * The API server itself cannot do this: it builds its route table from the entrypoints the
8
+ * context knows about, so it can only bind AFTER `init()` resolves — and Fastify refuses new
9
+ * routes once it is listening. Without a hold, the whole boot window, and every way a boot can
10
+ * fail, leaves nothing on the port; the edge then answers a bare upstream connect error that
11
+ * names neither the app nor the reason, which is indistinguishable from a crashed pod or an app
12
+ * that was never installed.
13
+ *
14
+ * So the boot sequence an app wants is:
15
+ *
16
+ * const hold = await holdApiPort(cfg, { okPath, payload }) // bind FIRST
17
+ * try { ...initialize... } catch { record the failure; return } // hold keeps answering
18
+ * await hold.release()
19
+ * await context.getApiServer().listen()
20
+ *
21
+ * Everything that is not the health path answers **503, not 404**: the routes the app really
22
+ * serves do not exist yet, and "no such route" would be a lie that outlives the boot.
23
+ *
24
+ * The port and host are resolved exactly as `listen()` resolves them — from the service's own
25
+ * declaration in `cfg.services[cfg.service]` — so the hold and the server can never disagree
26
+ * about which socket they are trading.
27
+ *
28
+ * A bind failure here (EADDRINUSE above all) throws to the caller and must be treated as fatal:
29
+ * a process that continues past it ends the boot with nothing listening and nothing holding the
30
+ * event loop, i.e. a clean exit 0 while a predecessor keeps serving stale code. Name the error
31
+ * and exit non-zero — that is what makes a supervisor reclaim the port instead of respawning
32
+ * into it.
33
+ */
34
+ export const holdApiPort = async (cfg, opts = {}) => {
35
+ const config = cfg.services[cfg.service];
36
+ const port = config?.internalPort ?? config?.port ?? PORT;
37
+ const host = config?.opened === true ? OPENED_HOST : CLOSED_HOST;
38
+ const payload = opts.payload ?? (() => ({ status: 'OK' }));
39
+ // Quiet and disposable: no logger, no plugins, and connections are force-closed on release so
40
+ // the handover to the real server is bounded rather than waiting out a keep-alive.
41
+ const server = Fastify({ logger: false, forceCloseConnections: true });
42
+ if (opts.okPath != null) {
43
+ server.all(opts.okPath, async (_request, reply) => {
44
+ return await reply.code(200).send(payload());
45
+ });
46
+ }
47
+ server.setNotFoundHandler(async (_request, reply) => {
48
+ return await reply.code(503).send(payload());
49
+ });
50
+ await server.listen({ port, host });
51
+ console.log(`api-server: holding ${host}:${port} for the boot${opts.okPath != null ? ` (${opts.okPath})` : ''}`);
52
+ return {
53
+ release: async () => { await server.close(); },
54
+ };
55
+ };
56
+ //# sourceMappingURL=hold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hold.js","sourceRoot":"","sources":["../src/hold.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAE7B,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAsB5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,GAAiB,EAAE,IAAI,GAAuB,EAAE,EAC1B,EAAE;IACxB,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,MAAM,EAAE,YAAY,IAAI,MAAM,EAAE,IAAI,IAAI,IAAI,CAAA;IACzD,MAAM,IAAI,GAAG,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAA;IAEhE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAE1D,8FAA8F;IAC9F,mFAAmF;IACnF,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAA;IAEtE,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;YAChD,OAAO,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9C,CAAC,CAAC,CAAA;IACJ,CAAC;IACD,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QAClD,OAAO,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;IAC9C,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IACnC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,IAAI,IAAI,gBAAgB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEhH,OAAO;QACL,OAAO,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA,CAAC,CAAC;KAC9C,CAAA;AACH,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -3,4 +3,6 @@ export * from './server.js';
3
3
  export * from './consts.js';
4
4
  export * from './errors.js';
5
5
  export * from './helper.js';
6
+ export * from './hold.js';
7
+ export * from './protocol.js';
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
package/build/index.js CHANGED
@@ -2,4 +2,6 @@ export * from './server.js';
2
2
  export * from './consts.js';
3
3
  export * from './errors.js';
4
4
  export * from './helper.js';
5
+ export * from './hold.js';
6
+ export * from './protocol.js';
5
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}