@owlmeans/server-api 0.1.18-rc.4 → 0.1.18-rc.40
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 +202 -46
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-api/SKILL.md +95 -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 +9 -13
- package/build/server.js.map +1 -1
- package/build/types.d.ts +10 -2
- package/build/types.d.ts.map +1 -1
- package/build/utils/error.d.ts +64 -1
- package/build/utils/error.d.ts.map +1 -1
- package/build/utils/error.js +160 -8
- 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 +6 -2
- package/build/utils/payload.js.map +1 -1
- package/build/utils/server.d.ts +3 -2
- package/build/utils/server.d.ts.map +1 -1
- package/build/utils/server.js +5 -6
- 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 +36 -45
- package/src/types.ts +15 -4
- package/src/utils/error.ts +198 -10
- package/src/utils/guards.ts +10 -17
- package/src/utils/payload.ts +6 -2
- package/src/utils/server.ts +8 -8
- package/tests/error-status.spec.ts +290 -0
- package/tests/handlers.spec.ts +83 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +1 -0
- package/build/.gitkeep +0 -0
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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context';
|
|
2
|
+
import type { BodyOf, EntrypointProtocolDeclaration, HandlerRequest, OpenValue, ParamsOf, RequestOf, ResponseOf } from '@owlmeans/entrypoint';
|
|
3
|
+
import type { BoundEntrypointHandler } from '@owlmeans/server-entrypoint';
|
|
4
|
+
/**
|
|
5
|
+
* True for a response the declaration left with no compile-time contract: `typed<any>()` (an
|
|
6
|
+
* `[any] extends [X]` conditional does not produce `any`'s usual both-branches union once the
|
|
7
|
+
* checked type is wrapped in a tuple, so it resolves to a single branch like any other type) and
|
|
8
|
+
* a response inferred as bare `typed()` with nothing to infer from, which resolves to `undefined`
|
|
9
|
+
* with no shape to validate a handler's return against — the same signal a zero-argument
|
|
10
|
+
* `contract()` produces for a genuinely response-less route. Neither means "this handler must
|
|
11
|
+
* return void": it means the declaration did not constrain the response, so the handler is free to
|
|
12
|
+
* return whatever its route actually sends.
|
|
13
|
+
*/
|
|
14
|
+
type IsUntypedResponse<T> = 0 extends (1 & T) ? true : [T] extends [undefined] ? true : false;
|
|
15
|
+
/**
|
|
16
|
+
* The type a bound handler is allowed to return for a given protocol response.
|
|
17
|
+
*
|
|
18
|
+
* A concretely typed or schema-backed `Response` is returned unchanged — the handler must produce
|
|
19
|
+
* exactly that shape. An untyped one (see `IsUntypedResponse` above) relaxes to `any`: the
|
|
20
|
+
* declaration made no promise about the response, so nothing here should force a handler bound to
|
|
21
|
+
* it into returning `void`.
|
|
22
|
+
*/
|
|
23
|
+
type HandlerResponse<Response> = IsUntypedResponse<Response> extends true ? any : Response;
|
|
24
|
+
type MaybePromise<Value> = Value | Promise<Value>;
|
|
25
|
+
type BodyProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends {
|
|
26
|
+
body: OpenValue;
|
|
27
|
+
} ? Protocol : never;
|
|
28
|
+
type ParamsProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends {
|
|
29
|
+
params: object;
|
|
30
|
+
} ? Protocol : never;
|
|
31
|
+
/**
|
|
32
|
+
* Make server handlers that infer request sections and response values from their protocol.
|
|
33
|
+
* The protocol is passed once; application callbacks never name request or reply generics.
|
|
34
|
+
*/
|
|
35
|
+
export declare const handlers: <Context extends BasicContext<BasicConfig>>() => {
|
|
36
|
+
body: <Protocol extends EntrypointProtocolDeclaration>(protocol: Protocol, handler: Protocol extends BodyProtocol<Protocol> ? (payload: BodyOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>> : never) => BoundEntrypointHandler<Protocol>;
|
|
37
|
+
params: <Protocol extends EntrypointProtocolDeclaration>(protocol: Protocol, handler: Protocol extends ParamsProtocol<Protocol> ? (payload: ParamsOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>> : never) => BoundEntrypointHandler<Protocol>;
|
|
38
|
+
request: <Protocol extends EntrypointProtocolDeclaration>(protocol: Protocol, handler: (request: HandlerRequest<RequestOf<Protocol>>, context: Context) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>) => BoundEntrypointHandler<Protocol>;
|
|
39
|
+
};
|
|
40
|
+
/** Access a Fastify-specific upload through an explicit boundary helper, never `request.original`. */
|
|
41
|
+
export declare const uploadedFile: (request: HandlerRequest<{
|
|
42
|
+
body: object;
|
|
43
|
+
}>) => Promise<import('@fastify/multipart').MultipartFile | undefined>;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,KAAK,EACV,MAAM,EAAE,6BAA6B,EAAE,cAAc,EAAe,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAC/G,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAGzE;;;;;;;;;GASG;AACH,KAAK,iBAAiB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,KAAK,CAAA;AAE7F;;;;;;;GAOG;AACH,KAAK,eAAe,CAAC,QAAQ,IAAI,iBAAiB,CAAC,QAAQ,CAAC,SAAS,IAAI,GAAG,GAAG,GAAG,QAAQ,CAAA;AAC1F,KAAK,YAAY,CAAC,KAAK,IAAI,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;AACjD,KAAK,YAAY,CAAC,QAAQ,SAAS,6BAA6B,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAC/G,QAAQ,GAAG,KAAK,CAAA;AACpB,KAAK,cAAc,CAAC,QAAQ,SAAS,6BAA6B,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAChH,QAAQ,GAAG,KAAK,CAAA;AAqGpB;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,SAAS,YAAY,CAAC,WAAW,CAAC;IAChE,IAAI,GAAG,QAAQ,SAAS,6BAA6B,YACzC,QAAQ,WACT,QAAQ,SAAS,YAAY,CAAC,QAAQ,CAAC,GAAG,CAC/C,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,EACzB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,KACzC,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,GACtD,KAAK,KACR,sBAAsB,CAAC,QAAQ,CAAC;IAKnC,MAAM,GAAG,QAAQ,SAAS,6BAA6B,YAC3C,QAAQ,WACT,QAAQ,SAAS,cAAc,CAAC,QAAQ,CAAC,GAAG,CACjD,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAC3B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,KACzC,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,GACtD,KAAK,KACR,sBAAsB,CAAC,QAAQ,CAAC;IAKnC,OAAO,GAAG,QAAQ,SAAS,6BAA6B,YAC5C,QAAQ,WACT,CACP,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EAC5C,OAAO,EAAE,OAAO,KACb,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,KACvD,sBAAsB,CAAC,QAAQ,CAAC;CACnC,CAAA;AAEF,sGAAsG;AACtG,eAAO,MAAM,YAAY,YACd,cAAc,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,KACxC,OAAO,CAAC,OAAO,oBAAoB,EAAE,aAAa,GAAG,SAAS,CAKhE,CAAA"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { assertContext } from '@owlmeans/context';
|
|
2
|
+
import { EntrypointOutcome } from '@owlmeans/entrypoint';
|
|
3
|
+
import { HandlerMisconfiguredError } from './errors.js';
|
|
4
|
+
const contextFor = (request, fallback) => {
|
|
5
|
+
const original = request.original;
|
|
6
|
+
if (original != null && typeof original === 'object' && '_ctx' in original) {
|
|
7
|
+
return original._ctx ?? fallback;
|
|
8
|
+
}
|
|
9
|
+
return fallback;
|
|
10
|
+
};
|
|
11
|
+
const bind = (protocol, execute) => ({
|
|
12
|
+
protocol,
|
|
13
|
+
bind: ref => async (request, response) => {
|
|
14
|
+
const fallback = assertContext(ref.ref?.ctx, protocol.alias);
|
|
15
|
+
try {
|
|
16
|
+
const typedRequest = request;
|
|
17
|
+
const value = await execute(typedRequest, contextFor(request, fallback));
|
|
18
|
+
response.resolve(value, EntrypointOutcome.Ok);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
response.reject(error);
|
|
22
|
+
}
|
|
23
|
+
return response.value;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* `value` is shaped like a `BoundEntrypointHandler` — never a plain function, which also carries
|
|
28
|
+
* its own `.bind` and would otherwise pass a `typeof value.bind === 'function'` check alone.
|
|
29
|
+
*/
|
|
30
|
+
const isBoundHandler = (value) => typeof value === 'object' && value != null
|
|
31
|
+
&& typeof value.bind === 'function'
|
|
32
|
+
&& value.protocol != null;
|
|
33
|
+
const warnedAliases = new Set();
|
|
34
|
+
/**
|
|
35
|
+
* What `body`/`params`/`request` do with something that is not the plain callback they declare —
|
|
36
|
+
* the shape a handler module wraps its own export in when it is bound TWICE. `null` means the
|
|
37
|
+
* second argument really is a callback and the caller should wrap it as usual.
|
|
38
|
+
*
|
|
39
|
+
* A handler bound to THIS protocol is returned unchanged, with a one-time warning: the module
|
|
40
|
+
* already produced a working `BoundEntrypointHandler`, and re-wrapping it here would only wrap it
|
|
41
|
+
* again — returning it as-is is what makes `bind(protocol, api.body(protocol, alreadyBound))`
|
|
42
|
+
* behave exactly like `bind(protocol, alreadyBound)`, which is what a wrap-once file looks like at
|
|
43
|
+
* this call site. Anything else — a handler bound to a DIFFERENT protocol, or a value that is
|
|
44
|
+
* neither a function nor a bound handler at all — answers every request on this route with
|
|
45
|
+
* {@link HandlerMisconfiguredError} instead of throwing `TypeError: handler is not a function`
|
|
46
|
+
* out of the request pipeline's own `try`, where the type name and the alias it happened on are
|
|
47
|
+
* both lost.
|
|
48
|
+
*/
|
|
49
|
+
const toleratedHandler = (protocol, handler) => {
|
|
50
|
+
if (typeof handler === 'function')
|
|
51
|
+
return null;
|
|
52
|
+
if (isBoundHandler(handler)) {
|
|
53
|
+
if (handler.protocol.alias === protocol.alias) {
|
|
54
|
+
if (!warnedAliases.has(protocol.alias)) {
|
|
55
|
+
warnedAliases.add(protocol.alias);
|
|
56
|
+
console.warn(`[server-api] ${protocol.alias}: handler is already bound — pass it to bind() directly; `
|
|
57
|
+
+ 'wrapping it again here is a type error (TS2345 "BoundEntrypointHandler<…> is not '
|
|
58
|
+
+ 'assignable"), tolerated at runtime for a project generated before the wrap-once rule.');
|
|
59
|
+
}
|
|
60
|
+
return handler;
|
|
61
|
+
}
|
|
62
|
+
return bind(protocol, () => {
|
|
63
|
+
throw new HandlerMisconfiguredError(`${protocol.alias}: handler is bound to a different protocol ('${handler.protocol.alias}')`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return bind(protocol, () => {
|
|
67
|
+
throw new HandlerMisconfiguredError(`${protocol.alias}: handler is not a function`);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Make server handlers that infer request sections and response values from their protocol.
|
|
72
|
+
* The protocol is passed once; application callbacks never name request or reply generics.
|
|
73
|
+
*/
|
|
74
|
+
export const handlers = () => ({
|
|
75
|
+
body: (protocol, handler) => toleratedHandler(protocol, handler) ?? bind(protocol, (request, context) => handler(request.body, context, request)),
|
|
76
|
+
params: (protocol, handler) => toleratedHandler(protocol, handler) ?? bind(protocol, (request, context) => handler(request.params, context, request)),
|
|
77
|
+
request: (protocol, handler) => toleratedHandler(protocol, handler) ?? bind(protocol, handler),
|
|
78
|
+
});
|
|
79
|
+
/** Access a Fastify-specific upload through an explicit boundary helper, never `request.original`. */
|
|
80
|
+
export const uploadedFile = async (request) => {
|
|
81
|
+
const original = request.original;
|
|
82
|
+
if (original == null || typeof original !== 'object' || !('file' in original))
|
|
83
|
+
return undefined;
|
|
84
|
+
return original.file();
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/build/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAkC5D,KAAK,MAAM,GAAG,YAAY,CAAA;AAK1B,eAAO,MAAM,eAAe,UAAW,MAAM,KAAG,SAkJ/C,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,OACrE,CAAC,UAAS,MAAM,KACpB,CAAC,GAAG,eAWN,CAAA"}
|
package/build/server.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertContext, createService } from '@owlmeans/context';
|
|
2
2
|
import { canServeModule, executeResponse, provideRequest } from './utils/index.js';
|
|
3
3
|
import { DEFAULT_ALIAS, CLOSED_HOST, PORT, OPENED_HOST } from './consts.js';
|
|
4
4
|
import { RouteMethod } from '@owlmeans/route';
|
|
5
5
|
import { createServerHandler, fixFormatDates } from './utils/index.js';
|
|
6
6
|
import { provideResponse } from '@owlmeans/entrypoint';
|
|
7
7
|
import { TOKEN_UPDATE } from '@owlmeans/auth-common';
|
|
8
|
+
import { INCIDENT_ID_HEADER } from './utils/error.js';
|
|
8
9
|
import Fastify from 'fastify';
|
|
9
10
|
import cors from '@fastify/cors';
|
|
10
11
|
import rawBody from 'fastify-raw-body';
|
|
@@ -42,7 +43,6 @@ export const createApiServer = (alias) => {
|
|
|
42
43
|
},
|
|
43
44
|
}*/
|
|
44
45
|
}),
|
|
45
|
-
layers: [Layer.System],
|
|
46
46
|
listen: async () => {
|
|
47
47
|
const context = _assertContext(service.ctx);
|
|
48
48
|
const config = context.cfg.services[context.cfg.service];
|
|
@@ -80,7 +80,7 @@ export const createApiServer = (alias) => {
|
|
|
80
80
|
...Object.values(RouteMethod).map(method => method.toUpperCase()),
|
|
81
81
|
'HEAD', 'OPTIONS'
|
|
82
82
|
],
|
|
83
|
-
exposedHeaders: [TOKEN_UPDATE]
|
|
83
|
+
exposedHeaders: [TOKEN_UPDATE, INCIDENT_ID_HEADER]
|
|
84
84
|
});
|
|
85
85
|
await server.register(Multipart, {
|
|
86
86
|
throwFileSizeLimit: true,
|
|
@@ -107,10 +107,8 @@ export const createApiServer = (alias) => {
|
|
|
107
107
|
if (responded || reply.sent) {
|
|
108
108
|
return context;
|
|
109
109
|
}
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
// Actually intermediate module can be created without handler by elevate function
|
|
113
|
-
if (module.route.match(request) && module.handle != null) {
|
|
110
|
+
// An intermediate route can be created without a handler by the binding layer.
|
|
111
|
+
if (module.route.match(request, module.mount()) && module.handle != null) {
|
|
114
112
|
const response = provideResponse(reply);
|
|
115
113
|
const currentRequest = provideRequest(module.alias, request, true);
|
|
116
114
|
currentRequest.original._ctx = context;
|
|
@@ -123,17 +121,15 @@ export const createApiServer = (alias) => {
|
|
|
123
121
|
return context;
|
|
124
122
|
}, Promise.resolve(context));
|
|
125
123
|
});
|
|
126
|
-
|
|
124
|
+
context.entrypoints()
|
|
127
125
|
.filter(module => canServeModule(context, module) && !module.route.isIntermediate())
|
|
128
|
-
.
|
|
129
|
-
// await module.route.resolve(context as any)
|
|
130
|
-
await module.resolve();
|
|
126
|
+
.forEach(module => {
|
|
131
127
|
const method = module.route.route.method ?? RouteMethod.GET;
|
|
132
128
|
if (module.handle == null) {
|
|
133
129
|
return;
|
|
134
130
|
}
|
|
135
131
|
server.route({
|
|
136
|
-
url: module.
|
|
132
|
+
url: module.mount(), method,
|
|
137
133
|
schema: {
|
|
138
134
|
consumes: [
|
|
139
135
|
'application/json',
|
|
@@ -154,7 +150,7 @@ export const createApiServer = (alias) => {
|
|
|
154
150
|
},
|
|
155
151
|
handler: createServerHandler(module, location)
|
|
156
152
|
});
|
|
157
|
-
})
|
|
153
|
+
});
|
|
158
154
|
service.initialized = true;
|
|
159
155
|
});
|
|
160
156
|
return service;
|
package/build/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { InitializedService
|
|
1
|
+
import type { InitializedService } from '@owlmeans/context';
|
|
2
2
|
import type { ServerConfig, ServerContext } from '@owlmeans/server-context';
|
|
3
3
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
4
4
|
import '@fastify/middie/types/index.d.ts';
|
|
5
5
|
export interface ApiServer extends InitializedService {
|
|
6
6
|
server: FastifyInstance;
|
|
7
|
-
layers: [Layer.System];
|
|
8
7
|
listen: () => Promise<void>;
|
|
9
8
|
}
|
|
10
9
|
export interface Request extends FastifyRequest {
|
|
@@ -14,7 +13,16 @@ export interface Response extends FastifyReply {
|
|
|
14
13
|
export interface ApiServerAppend {
|
|
15
14
|
getApiServer: () => ApiServer;
|
|
16
15
|
}
|
|
16
|
+
export type HttpErrorExposure = 'production' | 'development';
|
|
17
|
+
export interface HttpErrorsConfig {
|
|
18
|
+
/** Production is the safe default; development is the only mode that exposes stacks. */
|
|
19
|
+
exposure?: HttpErrorExposure;
|
|
20
|
+
}
|
|
21
|
+
export interface HttpConfig {
|
|
22
|
+
errors?: HttpErrorsConfig;
|
|
23
|
+
}
|
|
17
24
|
export interface Config extends ServerConfig {
|
|
25
|
+
http?: HttpConfig;
|
|
18
26
|
}
|
|
19
27
|
export interface Context<C extends Config = Config> extends ServerContext<C>, ApiServerAppend {
|
|
20
28
|
}
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,kCAAkC,CAAA;AAEzC,MAAM,WAAW,SAAU,SAAQ,kBAAkB;IACnD,MAAM,EAAE,eAAe,CAAA;IAEvB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED,MAAM,WAAW,OAAQ,SAAQ,cAAc;CAAI;AAEnD,MAAM,WAAW,QAAS,SAAQ,YAAY;CAAI;AAElD,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,SAAS,CAAA;CAC9B;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,aAAa,CAAA;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,gBAAgB,CAAA;CAC1B;AAED,MAAM,WAAW,MAAO,SAAQ,YAAY;IAC1C,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB;AAED,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC,EAC1E,eAAe;CAAI"}
|
package/build/utils/error.d.ts
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
1
|
import type { FastifyReply } from 'fastify';
|
|
2
|
-
|
|
2
|
+
import type { Config, HttpErrorExposure } from '../types.js';
|
|
3
|
+
export declare const INCIDENT_ID_HEADER = "X-Incident-ID";
|
|
4
|
+
/**
|
|
5
|
+
* What an error class may declare about the HTTP status it is answered with.
|
|
6
|
+
*
|
|
7
|
+
* Structural on purpose: the classes that declare it live in packages that must not depend on an
|
|
8
|
+
* HTTP server (`@owlmeans/payment`, a product's contract package), so they write a plain
|
|
9
|
+
* `public static httpStatus = 409` and nothing imports this type.
|
|
10
|
+
*/
|
|
11
|
+
export interface HttpStatusDeclaration {
|
|
12
|
+
httpStatus?: unknown;
|
|
13
|
+
/** Explicit opt-in for a declared 5xx response. Client-error declarations need no opt-in. */
|
|
14
|
+
allowServerErrorStatus?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The HTTP status an error's class declares, or `null` when it declares none that may be honoured.
|
|
18
|
+
*
|
|
19
|
+
* Read through the constructor chain — a static property is inherited, so a subclass answers what
|
|
20
|
+
* its nearest declaring ancestor says, and redeclares to change it. An integer 4xx is honoured
|
|
21
|
+
* directly. A 5xx is honoured only with `allowServerErrorStatus = true`, so a domain class must
|
|
22
|
+
* explicitly opt in to exposing a precise availability response. The read is structural, so a
|
|
23
|
+
* class from a duplicate module copy declares exactly what the imported one does.
|
|
24
|
+
*/
|
|
25
|
+
export declare const declaredErrorStatus: (error: unknown) => number | null;
|
|
26
|
+
/**
|
|
27
|
+
* The HTTP status an error is answered with, read off the error exactly as given.
|
|
28
|
+
*
|
|
29
|
+
* Two families reach here and both must be recognised. `AuthFailedError` / `AccessError` are this
|
|
30
|
+
* package's own, raised by the request pipeline itself. `AuthorizationError` / `AuthForbidden`
|
|
31
|
+
* come from `@owlmeans/auth` and are what the guards and gates actually throw — an unrecognised
|
|
32
|
+
* one answers 500, which reports a refused request as a crashed server: the client cannot tell
|
|
33
|
+
* "sign in again" from "the service is broken", and a consumer watching statuses concludes the
|
|
34
|
+
* application fell over. A family member is recognised by class OR by registered type name
|
|
35
|
+
* ({@link typeNamesOf}), so a class from a duplicate module copy answers the same status.
|
|
36
|
+
*
|
|
37
|
+
* **Order matters.** `AuthForbidden extends AuthorizationError`, so the 403 branch has to be
|
|
38
|
+
* tested first or every refusal of a permission is reported as a failure to authenticate. Both
|
|
39
|
+
* auth branches come before a class's own declaration, so an auth refusal can never be restated
|
|
40
|
+
* as something else. Everything that is neither an auth error nor a class declaring an allowed
|
|
41
|
+
* {@link declaredErrorStatus} answers 500.
|
|
42
|
+
*/
|
|
43
|
+
export declare const errorStatus: (error: unknown) => number;
|
|
44
|
+
export interface SerializedHttpError {
|
|
45
|
+
status: number;
|
|
46
|
+
body: string;
|
|
47
|
+
incidentId: string;
|
|
48
|
+
}
|
|
49
|
+
/** Invalid or absent configuration always resolves to the production-safe policy. */
|
|
50
|
+
export declare const errorExposure: (config?: Pick<Config, 'http'>) => HttpErrorExposure;
|
|
51
|
+
/** One central wire shape for every server error. */
|
|
52
|
+
export declare const serializeError: (error: Error, exposure?: HttpErrorExposure) => SerializedHttpError;
|
|
53
|
+
/** Apply safe response metadata carried by an error without coupling its package to Fastify. */
|
|
54
|
+
export declare const applyErrorHeaders: (error: unknown, reply: FastifyReply) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Answer a thrown error: a status, and the marshalled `ResilientError` as the body.
|
|
57
|
+
*
|
|
58
|
+
* The status is resolved in two steps. The error AS THROWN comes first: it is the one object whose
|
|
59
|
+
* class is certainly the class that was raised, while `ResilientError.ensure` rebuilds through the
|
|
60
|
+
* registry of whichever `@owlmeans/error` copy this module loaded — under duplicate module copies
|
|
61
|
+
* that turns a perfectly recognisable auth refusal into a plain `ResilientError` answering 500.
|
|
62
|
+
* Only when the thrown error answers 500 is the ENSURED error asked, because the rebuild is what
|
|
63
|
+
* gives a status to a marshalled error that crossed a hop as a plain `Error`.
|
|
64
|
+
*/
|
|
65
|
+
export declare const handleError: (error: Error, reply: FastifyReply, exposure?: HttpErrorExposure) => void;
|
|
3
66
|
//# sourceMappingURL=error.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAK3C,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAE5D,eAAO,MAAM,kBAAkB,kBAAkB,CAAA;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6FAA6F;IAC7F,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC;AAOD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,UAAW,OAAO,KAAG,MAAM,GAAG,IAc7D,CAAA;AAkCD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,UAAW,OAAO,KAAG,MAkB5C,CAAA;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qFAAqF;AACrF,eAAO,MAAM,aAAa,YAAa,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,iBACmB,CAAA;AAsBjF,qDAAqD;AACrD,eAAO,MAAM,cAAc,UAClB,KAAK,aAAY,iBAAiB,KACxC,mBAcF,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,iBAAiB,UAAW,OAAO,SAAS,YAAY,KAAG,IAQvE,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,UACf,KAAK,SAAS,YAAY,aAAY,iBAAiB,SAY/D,CAAA"}
|
package/build/utils/error.js
CHANGED
|
@@ -1,16 +1,168 @@
|
|
|
1
1
|
import { FORBIDDEN_ERROR, SERVER_ERROR, UNAUTHORIZED_ERROR } from '@owlmeans/api';
|
|
2
2
|
import { AccessError, AuthFailedError } from '../errors.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { AuthForbidden, AuthorizationError } from '@owlmeans/auth';
|
|
4
|
+
import { isResilientError, ResilientError, SEPARATOR } from '@owlmeans/error';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
export const INCIDENT_ID_HEADER = 'X-Incident-ID';
|
|
7
|
+
const CLIENT_ERROR_FIRST = 400;
|
|
8
|
+
const CLIENT_ERROR_LAST = 499;
|
|
9
|
+
const SERVER_ERROR_FIRST = 500;
|
|
10
|
+
const SERVER_ERROR_LAST = 599;
|
|
11
|
+
/**
|
|
12
|
+
* The HTTP status an error's class declares, or `null` when it declares none that may be honoured.
|
|
13
|
+
*
|
|
14
|
+
* Read through the constructor chain — a static property is inherited, so a subclass answers what
|
|
15
|
+
* its nearest declaring ancestor says, and redeclares to change it. An integer 4xx is honoured
|
|
16
|
+
* directly. A 5xx is honoured only with `allowServerErrorStatus = true`, so a domain class must
|
|
17
|
+
* explicitly opt in to exposing a precise availability response. The read is structural, so a
|
|
18
|
+
* class from a duplicate module copy declares exactly what the imported one does.
|
|
19
|
+
*/
|
|
20
|
+
export const declaredErrorStatus = (error) => {
|
|
21
|
+
if (error == null || typeof error !== 'object') {
|
|
22
|
+
return null;
|
|
8
23
|
}
|
|
9
|
-
|
|
10
|
-
|
|
24
|
+
const declaration = error.constructor;
|
|
25
|
+
const status = declaration?.httpStatus;
|
|
26
|
+
if (typeof status !== 'number' || !Number.isInteger(status)) {
|
|
27
|
+
return null;
|
|
11
28
|
}
|
|
29
|
+
if (status >= CLIENT_ERROR_FIRST && status <= CLIENT_ERROR_LAST)
|
|
30
|
+
return status;
|
|
31
|
+
if (status >= SERVER_ERROR_FIRST && status <= SERVER_ERROR_LAST
|
|
32
|
+
&& declaration?.allowServerErrorStatus === true)
|
|
33
|
+
return status;
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The registered type names an error carries: its own `type`, and the static `typeName` of every
|
|
38
|
+
* class on its constructor chain.
|
|
39
|
+
*
|
|
40
|
+
* This is how a class is recognised when `instanceof` cannot see it. A process that loads a package
|
|
41
|
+
* twice — `bun --preserve-symlinks` over a linked workspace resolves one package through several
|
|
42
|
+
* `node_modules` paths — holds several copies of every error class, and an error built by one copy
|
|
43
|
+
* is not an instance of another. The registered names are the same in every copy. A subclass's
|
|
44
|
+
* `typeName` does not reliably embed its parent's (`EntitlementRefusal` extends `AuthForbidden`), so
|
|
45
|
+
* a family is recognised by walking the chain and matching names exactly, never by a substring.
|
|
46
|
+
*/
|
|
47
|
+
const typeNamesOf = (error) => {
|
|
48
|
+
const names = new Set();
|
|
49
|
+
const type = error.type;
|
|
50
|
+
if (typeof type === 'string') {
|
|
51
|
+
names.add(type);
|
|
52
|
+
}
|
|
53
|
+
let ctor = error.constructor;
|
|
54
|
+
while (typeof ctor === 'function') {
|
|
55
|
+
const name = ctor.typeName;
|
|
56
|
+
if (typeof name === 'string') {
|
|
57
|
+
names.add(name);
|
|
58
|
+
}
|
|
59
|
+
ctor = Object.getPrototypeOf(ctor);
|
|
60
|
+
}
|
|
61
|
+
return names;
|
|
62
|
+
};
|
|
63
|
+
const isOfFamily = (names, family) => family.some(typeName => names.has(typeName));
|
|
64
|
+
/**
|
|
65
|
+
* The HTTP status an error is answered with, read off the error exactly as given.
|
|
66
|
+
*
|
|
67
|
+
* Two families reach here and both must be recognised. `AuthFailedError` / `AccessError` are this
|
|
68
|
+
* package's own, raised by the request pipeline itself. `AuthorizationError` / `AuthForbidden`
|
|
69
|
+
* come from `@owlmeans/auth` and are what the guards and gates actually throw — an unrecognised
|
|
70
|
+
* one answers 500, which reports a refused request as a crashed server: the client cannot tell
|
|
71
|
+
* "sign in again" from "the service is broken", and a consumer watching statuses concludes the
|
|
72
|
+
* application fell over. A family member is recognised by class OR by registered type name
|
|
73
|
+
* ({@link typeNamesOf}), so a class from a duplicate module copy answers the same status.
|
|
74
|
+
*
|
|
75
|
+
* **Order matters.** `AuthForbidden extends AuthorizationError`, so the 403 branch has to be
|
|
76
|
+
* tested first or every refusal of a permission is reported as a failure to authenticate. Both
|
|
77
|
+
* auth branches come before a class's own declaration, so an auth refusal can never be restated
|
|
78
|
+
* as something else. Everything that is neither an auth error nor a class declaring an allowed
|
|
79
|
+
* {@link declaredErrorStatus} answers 500.
|
|
80
|
+
*/
|
|
81
|
+
export const errorStatus = (error) => {
|
|
82
|
+
if (error == null || typeof error !== 'object') {
|
|
83
|
+
return SERVER_ERROR;
|
|
84
|
+
}
|
|
85
|
+
const names = typeNamesOf(error);
|
|
86
|
+
if (error instanceof AuthForbidden || error instanceof AccessError
|
|
87
|
+
|| isOfFamily(names, [AuthForbidden.typeName, AccessError.typeName])) {
|
|
88
|
+
// An established identity that lacks the permission. Re-authenticating changes nothing.
|
|
89
|
+
return FORBIDDEN_ERROR;
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof AuthorizationError || error instanceof AuthFailedError
|
|
92
|
+
|| isOfFamily(names, [AuthorizationError.typeName, AuthFailedError.typeName])) {
|
|
93
|
+
// No usable credential — absent, expired, revoked, or naming a session this server no longer
|
|
94
|
+
// holds. The caller is told to sign in, not that signing in would not help.
|
|
95
|
+
return UNAUTHORIZED_ERROR;
|
|
96
|
+
}
|
|
97
|
+
return declaredErrorStatus(error) ?? SERVER_ERROR;
|
|
98
|
+
};
|
|
99
|
+
/** Invalid or absent configuration always resolves to the production-safe policy. */
|
|
100
|
+
export const errorExposure = (config) => config?.http?.errors?.exposure === 'development' ? 'development' : 'production';
|
|
101
|
+
/**
|
|
102
|
+
* Preserve a typed/marshalled error when possible without asking `ensure` to convert a plain
|
|
103
|
+
* error. In particular, `ensure` deliberately rethrows SyntaxError; an HTTP boundary still has
|
|
104
|
+
* to serialize it safely and let the incident log retain the original object.
|
|
105
|
+
*/
|
|
106
|
+
const normalizeForWire = (error) => {
|
|
107
|
+
if (error instanceof SyntaxError || isResilientError(error)) {
|
|
108
|
+
return error;
|
|
109
|
+
}
|
|
110
|
+
if (error.message.includes(SEPARATOR)) {
|
|
111
|
+
const ensured = ResilientError.ensure(error);
|
|
112
|
+
// The catch-all converter's legacy constructor shape shifts an ordinary error's fields.
|
|
113
|
+
// Only keep `ensure`'s result when a registered wire prefix actually rebuilt something.
|
|
114
|
+
if (ensured.type !== error.message || ensured.message !== error.stack) {
|
|
115
|
+
return ensured;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return error;
|
|
119
|
+
};
|
|
120
|
+
/** One central wire shape for every server error. */
|
|
121
|
+
export const serializeError = (error, exposure = 'production') => {
|
|
122
|
+
const normalized = normalizeForWire(error);
|
|
123
|
+
const thrown = errorStatus(error);
|
|
124
|
+
const status = thrown !== SERVER_ERROR ? thrown : errorStatus(normalized);
|
|
125
|
+
const incidentId = randomUUID();
|
|
126
|
+
// The server log owns every diagnostic. Production callers receive only its correlation key;
|
|
127
|
+
// development keeps the typed wire form so local clients can retain their usual error details.
|
|
128
|
+
Object.assign(error, { incidentId });
|
|
129
|
+
if (isResilientError(normalized))
|
|
130
|
+
normalized.incidentId = incidentId;
|
|
131
|
+
const body = exposure === 'production'
|
|
132
|
+
? incidentId
|
|
133
|
+
: ResilientError.marshal(normalized, { includeStack: true, incidentId }).message;
|
|
134
|
+
return { status, body, incidentId };
|
|
135
|
+
};
|
|
136
|
+
/** Apply safe response metadata carried by an error without coupling its package to Fastify. */
|
|
137
|
+
export const applyErrorHeaders = (error, reply) => {
|
|
138
|
+
if (error == null || typeof error !== 'object') {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const retryAfter = error.retryAfter;
|
|
142
|
+
if (typeof retryAfter === 'number' && Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
143
|
+
reply.header('Retry-After', String(Math.ceil(retryAfter)));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Answer a thrown error: a status, and the marshalled `ResilientError` as the body.
|
|
148
|
+
*
|
|
149
|
+
* The status is resolved in two steps. The error AS THROWN comes first: it is the one object whose
|
|
150
|
+
* class is certainly the class that was raised, while `ResilientError.ensure` rebuilds through the
|
|
151
|
+
* registry of whichever `@owlmeans/error` copy this module loaded — under duplicate module copies
|
|
152
|
+
* that turns a perfectly recognisable auth refusal into a plain `ResilientError` answering 500.
|
|
153
|
+
* Only when the thrown error answers 500 is the ENSURED error asked, because the rebuild is what
|
|
154
|
+
* gives a status to a marshalled error that crossed a hop as a plain `Error`.
|
|
155
|
+
*/
|
|
156
|
+
export const handleError = (error, reply, exposure = 'production') => {
|
|
12
157
|
if (!reply.sent) {
|
|
13
|
-
|
|
158
|
+
const serialized = serializeError(error, exposure);
|
|
159
|
+
applyErrorHeaders(error, reply);
|
|
160
|
+
// Fastify/Pino's `err` serializer retains the original type, message and full stack. This is
|
|
161
|
+
// the one log entry that owns those details in production, keyed by the response incident ID.
|
|
162
|
+
reply.log.error({ err: error, incidentId: serialized.incidentId }, 'Request failed');
|
|
163
|
+
reply.header(INCIDENT_ID_HEADER, serialized.incidentId)
|
|
164
|
+
.code(serialized.status)
|
|
165
|
+
.send(serialized.body);
|
|
14
166
|
}
|
|
15
167
|
};
|
|
16
168
|
//# sourceMappingURL=error.js.map
|