@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.
- package/README.md +179 -47
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-api/SKILL.md +45 -22
- 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.map +1 -1
- package/build/helper.js +10 -1
- package/build/helper.js.map +1 -1
- package/build/hold.d.ts +52 -0
- package/build/hold.d.ts.map +1 -0
- package/build/hold.js +56 -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 +45 -0
- package/build/protocol.d.ts.map +1 -0
- package/build/protocol.js +86 -0
- package/build/protocol.js.map +1 -0
- package/build/server.d.ts.map +1 -1
- package/build/server.js +7 -12
- package/build/server.js.map +1 -1
- package/build/types.d.ts +1 -2
- package/build/types.d.ts.map +1 -1
- package/build/utils/error.d.ts +13 -0
- package/build/utils/error.d.ts.map +1 -1
- package/build/utils/error.js +21 -4
- package/build/utils/error.js.map +1 -1
- package/build/utils/guards.d.ts.map +1 -1
- package/build/utils/guards.js +11 -15
- package/build/utils/guards.js.map +1 -1
- package/build/utils/payload.d.ts.map +1 -1
- package/build/utils/payload.js +3 -0
- package/build/utils/payload.js.map +1 -1
- package/build/utils/server.d.ts.map +1 -1
- package/build/utils/server.js +4 -1
- package/build/utils/server.js.map +1 -1
- package/package.json +13 -11
- package/src/errors.ts +18 -0
- package/src/helper.ts +10 -1
- package/src/hold.ts +84 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +185 -0
- package/src/server.ts +34 -43
- package/src/types.ts +1 -3
- package/src/utils/error.ts +20 -3
- package/src/utils/guards.ts +10 -17
- package/src/utils/payload.ts +3 -0
- package/src/utils/server.ts +4 -1
- package/tests/handlers.spec.ts +83 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +1 -0
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -1,80 +1,212 @@
|
|
|
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-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
|
-
|
|
34
|
+
### Body, params and request handlers
|
|
21
35
|
|
|
22
|
-
```
|
|
23
|
-
import {
|
|
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
|
-
|
|
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
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
handler: (payload: T, ctx: Context, req: AbstractRequest) => Promise<any>
|
|
81
|
+
return context.project().update({ ...project, name })
|
|
82
|
+
})
|
|
49
83
|
```
|
|
50
84
|
|
|
51
|
-
###
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
###
|
|
110
|
+
### Holding the port during boot
|
|
59
111
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
###
|
|
121
|
+
### A context without `@owlmeans/server-app`
|
|
66
122
|
|
|
67
|
-
|
|
123
|
+
```ts
|
|
124
|
+
import { makeServerContext } from '@owlmeans/server-context'
|
|
125
|
+
import { appendApiServer } from '@owlmeans/server-api'
|
|
68
126
|
|
|
69
|
-
|
|
127
|
+
const context = appendApiServer(makeServerContext(cfg))
|
|
128
|
+
context.registerEntrypoints(serverBindings)
|
|
129
|
+
await context.configure().init()
|
|
70
130
|
|
|
71
|
-
|
|
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
|
-
|
|
136
|
+
await context.getApiServer().listen()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## API
|
|
74
140
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-api",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
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:
|
|
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.0"` in `dependencies`
|
|
10
|
+
**Install:** `bun add @owlmeans/server-api@^0.1.18-rc.27`
|
|
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
|
+
## Wrap exactly once
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
- `
|
|
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
|
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.map
CHANGED
|
@@ -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;
|
|
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
|
|
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":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAOxD,MAAM,wBAAwB,GAAG,CAAqE,GAAoB,EAAE,GAAM,EAAK,EAAE;IACvI,OAAO,GAAG,CAAC,QAAQ,
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"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"}
|
package/build/hold.d.ts
ADDED
|
@@ -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
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -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"}
|