@owlmeans/server-api 0.1.18-rc.2 → 0.1.18-rc.21
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 +32 -34
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-api/SKILL.md +27 -24
- 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 +26 -0
- package/build/protocol.d.ts.map +1 -0
- package/build/protocol.js +41 -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 +10 -10
- package/src/helper.ts +10 -1
- package/src/hold.ts +84 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +104 -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/build/.gitkeep +0 -0
package/build/utils/error.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { FORBIDDEN_ERROR, SERVER_ERROR, UNAUTHORIZED_ERROR } from '@owlmeans/api';
|
|
2
2
|
import { AccessError, AuthFailedError } from '../errors.js';
|
|
3
|
+
import { AuthForbidden, AuthorizationError } from '@owlmeans/auth';
|
|
3
4
|
import { ResilientError } from '@owlmeans/error';
|
|
5
|
+
/**
|
|
6
|
+
* Map a thrown error onto an HTTP status.
|
|
7
|
+
*
|
|
8
|
+
* Two families reach here and both must be recognised. `AuthFailedError` / `AccessError` are this
|
|
9
|
+
* package's own, raised by the request pipeline itself. `AuthorizationError` / `AuthForbidden`
|
|
10
|
+
* come from `@owlmeans/auth` and are what the guards and gates actually throw — an unrecognised
|
|
11
|
+
* one answers 500, which reports a refused request as a crashed server: the client cannot tell
|
|
12
|
+
* "sign in again" from "the service is broken", and a consumer watching statuses concludes the
|
|
13
|
+
* application fell over.
|
|
14
|
+
*
|
|
15
|
+
* **Order matters.** `AuthForbidden extends AuthorizationError`, so the 403 branch has to be
|
|
16
|
+
* tested first or every refusal of a permission is reported as a failure to authenticate.
|
|
17
|
+
*/
|
|
4
18
|
export const handleError = (error, reply) => {
|
|
5
19
|
let code = SERVER_ERROR;
|
|
6
|
-
if (error instanceof
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
else if (error instanceof AccessError) {
|
|
20
|
+
if (error instanceof AuthForbidden || error instanceof AccessError) {
|
|
21
|
+
// An established identity that lacks the permission. Re-authenticating changes nothing.
|
|
10
22
|
code = FORBIDDEN_ERROR;
|
|
11
23
|
}
|
|
24
|
+
else if (error instanceof AuthorizationError || error instanceof AuthFailedError) {
|
|
25
|
+
// No usable credential — absent, expired, revoked, or naming a session this server no longer
|
|
26
|
+
// holds. The caller is told to sign in, not that signing in would not help.
|
|
27
|
+
code = UNAUTHORIZED_ERROR;
|
|
28
|
+
}
|
|
12
29
|
if (!reply.sent) {
|
|
13
30
|
reply.code(code).send(ResilientError.marshal(ResilientError.ensure(error)).message);
|
|
14
31
|
}
|
package/build/utils/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAEjF,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,KAAmB,EAAE,EAAE;IAC/D,IAAI,IAAI,GAAW,YAAY,CAAA;IAC/B,IAAI,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/utils/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAEjF,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,KAAmB,EAAE,EAAE;IAC/D,IAAI,IAAI,GAAW,YAAY,CAAA;IAC/B,IAAI,KAAK,YAAY,aAAa,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;QACnE,wFAAwF;QACxF,IAAI,GAAG,eAAe,CAAA;IACxB,CAAC;SAAM,IAAI,KAAK,YAAY,kBAAkB,IAAI,KAAK,YAAY,eAAe,EAAE,CAAC;QACnF,6FAA6F;QAC7F,4EAA4E;QAC5E,IAAI,GAAG,kBAAkB,CAAA;IAC3B,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CACnB,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,KAAc,CAAC,CAAC,CAAC,OAAO,CACtE,CAAA;IACH,CAAC;AACH,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/utils/guards.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/utils/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAGnE,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAM3E,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI;AAEzE,eAAO,MAAM,SAAS,GAAU,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WAC3D,CAAC,UAAU,gBAAgB,CAAC,cAAc,CAAC,OAC/C,cAAc,SAAS,YAAY,KACvC,OAAO,CAAC,CAAC,CAAC,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAC,CAmD/C,CAAA"}
|
package/build/utils/guards.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { isContextWithoutIds, Layer } from '@owlmeans/context';
|
|
2
1
|
import { provideResponse } from '@owlmeans/entrypoint';
|
|
3
2
|
import { executeResponse, provideRequest } from './payload.js';
|
|
4
3
|
import { AuthFailedError } from '../errors.js';
|
|
4
|
+
import { attachEntity } from '@owlmeans/auth-common';
|
|
5
5
|
export const authorize = async (context, module, req, reply) => {
|
|
6
6
|
const guards = module.getGuards();
|
|
7
7
|
if (guards.length > 0) {
|
|
@@ -34,20 +34,16 @@ export const authorize = async (context, module, req, reply) => {
|
|
|
34
34
|
if (request.auth != null) {
|
|
35
35
|
req._auth = request.auth;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await context.waitForInitialized();
|
|
48
|
-
// We elevate module to the context level if it was changed
|
|
49
|
-
module = context.entrypoint(module.alias);
|
|
50
|
-
await module.resolve();
|
|
37
|
+
// The token names the organization by slug; everything downstream — grants, records, minted
|
|
38
|
+
// infrastructure names — keys on the entity's stable id. Resolving here, once, is what keeps a
|
|
39
|
+
// rename from being a sweep of every handler: the id arrives on the request, and a slug that
|
|
40
|
+
// has since been retired still resolves to the entity that retired it.
|
|
41
|
+
const entity = await attachEntity(context, request);
|
|
42
|
+
if (entity != null) {
|
|
43
|
+
// Carried on the raw request the same way `_auth` is, so the request the handler is given
|
|
44
|
+
// later — a different object, built after this runs — sees the same resolution.
|
|
45
|
+
;
|
|
46
|
+
req._entity = entity;
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
49
|
// Update context in request object
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/utils/guards.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/utils/guards.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAKpD,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAU,EAAE,MAAwC,EACpD,GAAmB,EAAE,KAAmB,EACQ,EAAE;IAClD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAA;IACjC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;QAEjD,IAAI,KAAK,GAA6B,SAAS,CAAA;QAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAiB,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YACnD,IAAI,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;gBAC1C,KAAK,GAAG,MAAM,CAAA;YAChB,CAAC;YACD,eAAe,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;YACtC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,MAAK;YACP,CAAC;QACH,CAAC;QAED,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;YAClB,MAAM,IAAI,eAAe,EAAE,CAAA;QAC7B,CAAC;QAED,MAAM,YAAY,GAAG,eAAe,CAAO,KAAK,CAAC,CAAA;QACjD,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,CAAU,OAAO,EAAE,YAAY,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACxC,CAAC;QACD,eAAe,CAAC,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;QAC1C,6EAA6E;QAC7E,oCAAoC;QACpC,6GAA6G;QAC7G,IAAI;QACJ,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC;QAClC,IAAI,OAAO,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,GAAW,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAA;QACnC,CAAC;QAED,4FAA4F;QAC5F,+FAA+F;QAC/F,6FAA6F;QAC7F,uEAAuE;QACvE,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACnD,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;YACnB,0FAA0F;YAC1F,gFAAgF;YAChF,CAAC;YAAC,GAAW,CAAC,OAAO,GAAG,MAAM,CAAA;QAChC,CAAC;IACH,CAAC;IACD,mCAAmC;IAClC,GAAW,CAAC,IAAI,GAAG,OAAO,CAAA;IAE3B,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;AAC1B,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAA;AAE1C,eAAO,MAAM,cAAc,UAAW,MAAM,OAAO,OAAO,cAAc,OAAO,KAAG,
|
|
1
|
+
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAA;AAE1C,eAAO,MAAM,cAAc,UAAW,MAAM,OAAO,OAAO,cAAc,OAAO,KAAG,eAejF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,YAAY,gBAAgB,CAAC,CAAC,CAAC,SAAS,QAAQ,iBAAiB,OAAO,KAAG,OAuB3G,CAAA;AAGD,eAAO,MAAM,cAAc,WAAY,eAAe,oBAgBrD,CAAA"}
|
package/build/utils/payload.js
CHANGED
|
@@ -5,6 +5,9 @@ export const provideRequest = (alias, req, provision) => {
|
|
|
5
5
|
return {
|
|
6
6
|
alias,
|
|
7
7
|
auth: req._auth ?? undefined,
|
|
8
|
+
// Resolved once by `authorize` and carried by reference, exactly like `_auth`, so a handler
|
|
9
|
+
// reached through any path sees the same entity the guard admitted.
|
|
10
|
+
entity: req._entity ?? undefined,
|
|
8
11
|
params: req.params,
|
|
9
12
|
body: req.body,
|
|
10
13
|
headers: req.headers,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload.js","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAGxD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAGnE,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,GAAY,EAAE,SAAmB,EAAmB,EAAE;IAClG,SAAS,GAAG,SAAS,IAAI,KAAK,CAAA;IAC9B,OAAO;QACL,KAAK;QACL,IAAI,EAAG,GAAW,CAAC,KAAK,IAAI,SAAS;QACrC,MAAM,EAAE,GAAG,CAAC,MAA4D;QACxE,IAAI,EAAE,GAAG,CAAC,IAA2B;QACrC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,KAAK,EAAE,GAAG,CAAC,KAA2D;QACtE,IAAI,EAAE,GAAG,CAAC,GAAG;QACb,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;KACtC,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAI,QAA6B,EAAE,KAAe,EAAE,YAAsB,EAAW,EAAE;IACpH,IAAI,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;QAC3B,IAAI,YAAY,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,QAAQ,CAAC,KAAK,CAAA;QACtB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACrD,OAAO,IAAI,CAAA;IACb,CAAC;SAAM,IAAI,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;QACpC,QAAQ,QAAQ,CAAC,OAAO,EAAE,CAAC;YACzB,KAAK,iBAAiB,CAAC,QAAQ;gBAC7B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;gBACzC,MAAK;YACP,KAAK,iBAAiB,CAAC,OAAO;gBAC5B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;gBACxC,MAAK;YACP,KAAK,iBAAiB,CAAC,EAAE,CAAC;YAC1B;gBACE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAGD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAuB,EAAE,EAAE;IACxD,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAA;YACtB,MAAM,CAAC,MAAM,GAAG,WAAW,CAAA;YAC3B,IAAI,MAAM,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;gBAC5B,OAAO,MAAM,CAAC,QAAQ,CAAA;YACxB,CAAC;QACH,CAAC;aAAM,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;YACrC,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,WAAW,CACpC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,cAAc,CAAC,KAAwB,CAAC,CAAC,CAAC,CACzG,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"payload.js","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAGxD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAGnE,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,GAAY,EAAE,SAAmB,EAAmB,EAAE;IAClG,SAAS,GAAG,SAAS,IAAI,KAAK,CAAA;IAC9B,OAAO;QACL,KAAK;QACL,IAAI,EAAG,GAAW,CAAC,KAAK,IAAI,SAAS;QACrC,4FAA4F;QAC5F,oEAAoE;QACpE,MAAM,EAAG,GAAW,CAAC,OAAO,IAAI,SAAS;QACzC,MAAM,EAAE,GAAG,CAAC,MAA4D;QACxE,IAAI,EAAE,GAAG,CAAC,IAA2B;QACrC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,KAAK,EAAE,GAAG,CAAC,KAA2D;QACtE,IAAI,EAAE,GAAG,CAAC,GAAG;QACb,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;KACtC,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAI,QAA6B,EAAE,KAAe,EAAE,YAAsB,EAAW,EAAE;IACpH,IAAI,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;QAC3B,IAAI,YAAY,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,QAAQ,CAAC,KAAK,CAAA;QACtB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACrD,OAAO,IAAI,CAAA;IACb,CAAC;SAAM,IAAI,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;QACpC,QAAQ,QAAQ,CAAC,OAAO,EAAE,CAAC;YACzB,KAAK,iBAAiB,CAAC,QAAQ;gBAC7B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;gBACzC,MAAK;YACP,KAAK,iBAAiB,CAAC,OAAO;gBAC5B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;gBACxC,MAAK;YACP,KAAK,iBAAiB,CAAC,EAAE,CAAC;YAC1B;gBACE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAGD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAuB,EAAE,EAAE;IACxD,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAA;YACtB,MAAM,CAAC,MAAM,GAAG,WAAW,CAAA;YAC3B,IAAI,MAAM,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;gBAC5B,OAAO,MAAM,CAAC,QAAQ,CAAA;YACxB,CAAC;QACH,CAAC;aAAM,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;YACrC,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,WAAW,CACpC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,cAAc,CAAC,KAAwB,CAAC,CAAC,CAAC,CACzG,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAQ3E,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,KAAK,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;AAEpC,eAAO,MAAM,cAAc,YAAa,OAAO,UAAU,gBAAgB,KAAG,MAAM,IAAI,gBAAgB,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAQ3E,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,KAAK,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;AAEpC,eAAO,MAAM,cAAc,YAAa,OAAO,UAAU,gBAAgB,KAAG,MAAM,IAAI,gBAAgB,CAAC,OAAO,CAe7G,CAAA;AAED,eAAO,MAAM,mBAAmB,WAAY,gBAAgB,CAAC,cAAc,CAAC,YAAY,MAAM,WAChF,cAAc,SAAS,YAAY,kBA0C9C,CAAA"}
|
package/build/utils/server.js
CHANGED
|
@@ -14,7 +14,10 @@ export const canServeModule = (context, module) => {
|
|
|
14
14
|
if (module.route.route.service != null && module.route.route.service !== context.cfg.service) {
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// Only the protocols HTTP actually carries. A socket is upgraded elsewhere and a queued job is
|
|
18
|
+
// taken off the broker by the worker — mounting either on the HTTP server would answer it twice.
|
|
19
|
+
if (module.route.route.protocol === RouteProtocols.SOCKET
|
|
20
|
+
|| module.route.route.protocol === RouteProtocols.QUEUE) {
|
|
18
21
|
return false;
|
|
19
22
|
}
|
|
20
23
|
return 'isIntermediate' in module.route;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-api",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"@fastify/helmet": "^13.1.0",
|
|
33
33
|
"@fastify/middie": "^9.0.2",
|
|
34
34
|
"@fastify/multipart": "^9.0.1",
|
|
35
|
-
"@owlmeans/api": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
39
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
40
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
41
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
43
|
-
"@owlmeans/server-entrypoint": "^0.1.18-rc.
|
|
35
|
+
"@owlmeans/api": "^0.1.18-rc.17",
|
|
36
|
+
"@owlmeans/auth": "^0.1.18-rc.13",
|
|
37
|
+
"@owlmeans/auth-common": "^0.1.18-rc.17",
|
|
38
|
+
"@owlmeans/context": "^0.1.18-rc.12",
|
|
39
|
+
"@owlmeans/error": "^0.1.18-rc.12",
|
|
40
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.15",
|
|
41
|
+
"@owlmeans/route": "^0.1.18-rc.13",
|
|
42
|
+
"@owlmeans/server-context": "^0.1.18-rc.16",
|
|
43
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.15",
|
|
44
44
|
"ajv-errors": "^3.0.0",
|
|
45
45
|
"fastify": "^5.0.0",
|
|
46
46
|
"fastify-raw-body": "^5.0.0",
|
package/src/helper.ts
CHANGED
|
@@ -8,8 +8,17 @@ import type { Config, Context } from './types.js'
|
|
|
8
8
|
import type { FastifyRequest } from 'fastify'
|
|
9
9
|
import type { MultipartFile } from '@fastify/multipart'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The context a handler runs against.
|
|
13
|
+
*
|
|
14
|
+
* The HTTP boundary hangs the request-scoped context on the raw Fastify request, so a handler
|
|
15
|
+
* reached that way gets it from there. A request that did not come from Fastify has no `original`
|
|
16
|
+
* at all — a queued call is rebuilt from an envelope, and a socket frame carries its own — so this
|
|
17
|
+
* has to fall back rather than dereference: these handlers are the same functions on every
|
|
18
|
+
* transport, and only the way they were reached differs.
|
|
19
|
+
*/
|
|
11
20
|
const _castContextFromOriginal = <C extends BasicConfig, T extends BasicContext<C> = BasicContext<C>>(req: AbstractRequest, def: T): T => {
|
|
12
|
-
return req.original
|
|
21
|
+
return req.original?._ctx ?? def
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
export const handleBody: <T>(
|
package/src/hold.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import Fastify from 'fastify'
|
|
2
|
+
import type { ServerConfig } from '@owlmeans/server-context'
|
|
3
|
+
import { CLOSED_HOST, OPENED_HOST, PORT } from './consts.js'
|
|
4
|
+
|
|
5
|
+
export interface ApiPortHoldOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The one path answered 200 while the hold is in place — the app's health endpoint, so
|
|
8
|
+
* whatever supervises the process keeps a conventional liveness answer through the boot.
|
|
9
|
+
* Everything else answers 503 with the same body. Absent, everything is 503.
|
|
10
|
+
*/
|
|
11
|
+
okPath?: string
|
|
12
|
+
/** Body of every answer, evaluated per request so a changing boot phase is reported live. */
|
|
13
|
+
payload?: () => unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ApiPortHold {
|
|
17
|
+
/**
|
|
18
|
+
* Free the port for the real server. Awaits the actual close — `listen()` follows immediately,
|
|
19
|
+
* and a predecessor still bound makes it throw — and force-closes keep-alive sockets, so a
|
|
20
|
+
* supervisor's own poller cannot hold the release open.
|
|
21
|
+
*/
|
|
22
|
+
release: () => Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Own the app's port while the context initializes, and answer for the app until `listen()`
|
|
27
|
+
* takes over.
|
|
28
|
+
*
|
|
29
|
+
* The API server itself cannot do this: it builds its route table from the entrypoints the
|
|
30
|
+
* context knows about, so it can only bind AFTER `init()` resolves — and Fastify refuses new
|
|
31
|
+
* routes once it is listening. Without a hold, the whole boot window, and every way a boot can
|
|
32
|
+
* fail, leaves nothing on the port; the edge then answers a bare upstream connect error that
|
|
33
|
+
* names neither the app nor the reason, which is indistinguishable from a crashed pod or an app
|
|
34
|
+
* that was never installed.
|
|
35
|
+
*
|
|
36
|
+
* So the boot sequence an app wants is:
|
|
37
|
+
*
|
|
38
|
+
* const hold = await holdApiPort(cfg, { okPath, payload }) // bind FIRST
|
|
39
|
+
* try { ...initialize... } catch { record the failure; return } // hold keeps answering
|
|
40
|
+
* await hold.release()
|
|
41
|
+
* await context.getApiServer().listen()
|
|
42
|
+
*
|
|
43
|
+
* Everything that is not the health path answers **503, not 404**: the routes the app really
|
|
44
|
+
* serves do not exist yet, and "no such route" would be a lie that outlives the boot.
|
|
45
|
+
*
|
|
46
|
+
* The port and host are resolved exactly as `listen()` resolves them — from the service's own
|
|
47
|
+
* declaration in `cfg.services[cfg.service]` — so the hold and the server can never disagree
|
|
48
|
+
* about which socket they are trading.
|
|
49
|
+
*
|
|
50
|
+
* A bind failure here (EADDRINUSE above all) throws to the caller and must be treated as fatal:
|
|
51
|
+
* a process that continues past it ends the boot with nothing listening and nothing holding the
|
|
52
|
+
* event loop, i.e. a clean exit 0 while a predecessor keeps serving stale code. Name the error
|
|
53
|
+
* and exit non-zero — that is what makes a supervisor reclaim the port instead of respawning
|
|
54
|
+
* into it.
|
|
55
|
+
*/
|
|
56
|
+
export const holdApiPort = async (
|
|
57
|
+
cfg: ServerConfig, opts: ApiPortHoldOptions = {}
|
|
58
|
+
): Promise<ApiPortHold> => {
|
|
59
|
+
const config = cfg.services[cfg.service]
|
|
60
|
+
const port = config?.internalPort ?? config?.port ?? PORT
|
|
61
|
+
const host = config?.opened === true ? OPENED_HOST : CLOSED_HOST
|
|
62
|
+
|
|
63
|
+
const payload = opts.payload ?? (() => ({ status: 'OK' }))
|
|
64
|
+
|
|
65
|
+
// Quiet and disposable: no logger, no plugins, and connections are force-closed on release so
|
|
66
|
+
// the handover to the real server is bounded rather than waiting out a keep-alive.
|
|
67
|
+
const server = Fastify({ logger: false, forceCloseConnections: true })
|
|
68
|
+
|
|
69
|
+
if (opts.okPath != null) {
|
|
70
|
+
server.all(opts.okPath, async (_request, reply) => {
|
|
71
|
+
return await reply.code(200).send(payload())
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
server.setNotFoundHandler(async (_request, reply) => {
|
|
75
|
+
return await reply.code(503).send(payload())
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await server.listen({ port, host })
|
|
79
|
+
console.log(`api-server: holding ${host}:${port} for the boot${opts.okPath != null ? ` (${opts.okPath})` : ''}`)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
release: async () => { await server.close() },
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/index.ts
CHANGED
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assertContext } from '@owlmeans/context'
|
|
2
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
|
+
import { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
4
|
+
import type {
|
|
5
|
+
BodyOf, EntrypointProtocolDeclaration, HandlerRequest, OpenRequest, OpenValue, ParamsOf, RequestOf, ResponseOf,
|
|
6
|
+
} from '@owlmeans/entrypoint'
|
|
7
|
+
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
8
|
+
import type { BoundEntrypointHandler } from '@owlmeans/server-entrypoint'
|
|
9
|
+
|
|
10
|
+
type HandlerResponse<Response> = [Response] extends [undefined] ? void | undefined : Response
|
|
11
|
+
type MaybePromise<Value> = Value | Promise<Value>
|
|
12
|
+
type BodyProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends { body: OpenValue }
|
|
13
|
+
? Protocol : never
|
|
14
|
+
type ParamsProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends { params: object }
|
|
15
|
+
? Protocol : never
|
|
16
|
+
|
|
17
|
+
interface ContextCarrier<Context> {
|
|
18
|
+
_ctx?: Context
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const contextFor = <Context extends BasicContext<BasicConfig>>(
|
|
22
|
+
request: AbstractRequest,
|
|
23
|
+
fallback: Context,
|
|
24
|
+
): Context => {
|
|
25
|
+
const original = request.original
|
|
26
|
+
if (original != null && typeof original === 'object' && '_ctx' in original) {
|
|
27
|
+
return (original as ContextCarrier<Context>)._ctx ?? fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return fallback
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const bind = <Protocol extends EntrypointProtocolDeclaration, Context extends BasicContext<BasicConfig>>(
|
|
34
|
+
protocol: Protocol,
|
|
35
|
+
execute: (request: HandlerRequest<RequestOf<Protocol>>, context: Context) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>,
|
|
36
|
+
): BoundEntrypointHandler<Protocol> => ({
|
|
37
|
+
protocol,
|
|
38
|
+
bind: ref => async (request, response) => {
|
|
39
|
+
const fallback = assertContext<BasicConfig, Context>(ref.ref?.ctx, protocol.alias)
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const typedRequest = (request as HandlerRequest<OpenRequest>) as HandlerRequest<RequestOf<Protocol>>
|
|
43
|
+
const value = await execute(
|
|
44
|
+
typedRequest,
|
|
45
|
+
contextFor(request, fallback),
|
|
46
|
+
)
|
|
47
|
+
response.resolve(value as ResponseOf<Protocol>, EntrypointOutcome.Ok)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
response.reject(error as Error)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return response.value
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Make server handlers that infer request sections and response values from their protocol.
|
|
58
|
+
* The protocol is passed once; application callbacks never name request or reply generics.
|
|
59
|
+
*/
|
|
60
|
+
export const handlers = <Context extends BasicContext<BasicConfig>>() => ({
|
|
61
|
+
body: <Protocol extends EntrypointProtocolDeclaration>(
|
|
62
|
+
protocol: Protocol,
|
|
63
|
+
handler: Protocol extends BodyProtocol<Protocol> ? (
|
|
64
|
+
payload: BodyOf<Protocol>,
|
|
65
|
+
context: Context,
|
|
66
|
+
request: HandlerRequest<RequestOf<Protocol>>,
|
|
67
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>
|
|
68
|
+
: never,
|
|
69
|
+
): BoundEntrypointHandler<Protocol> => bind<Protocol, Context>(protocol, (request, context) =>
|
|
70
|
+
(handler as (
|
|
71
|
+
payload: BodyOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>,
|
|
72
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>)(request.body as BodyOf<Protocol>, context, request)),
|
|
73
|
+
|
|
74
|
+
params: <Protocol extends EntrypointProtocolDeclaration>(
|
|
75
|
+
protocol: Protocol,
|
|
76
|
+
handler: Protocol extends ParamsProtocol<Protocol> ? (
|
|
77
|
+
payload: ParamsOf<Protocol>,
|
|
78
|
+
context: Context,
|
|
79
|
+
request: HandlerRequest<RequestOf<Protocol>>,
|
|
80
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>
|
|
81
|
+
: never,
|
|
82
|
+
): BoundEntrypointHandler<Protocol> => bind<Protocol, Context>(protocol, (request, context) =>
|
|
83
|
+
(handler as (
|
|
84
|
+
payload: ParamsOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>,
|
|
85
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>)(request.params as ParamsOf<Protocol>, context, request)),
|
|
86
|
+
|
|
87
|
+
request: <Protocol extends EntrypointProtocolDeclaration>(
|
|
88
|
+
protocol: Protocol,
|
|
89
|
+
handler: (
|
|
90
|
+
request: HandlerRequest<RequestOf<Protocol>>,
|
|
91
|
+
context: Context,
|
|
92
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>,
|
|
93
|
+
): BoundEntrypointHandler<Protocol> => bind<Protocol, Context>(protocol, handler),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/** Access a Fastify-specific upload through an explicit boundary helper, never `request.original`. */
|
|
97
|
+
export const uploadedFile = async (
|
|
98
|
+
request: HandlerRequest<{ body: object }>,
|
|
99
|
+
): Promise<import('@fastify/multipart').MultipartFile | undefined> => {
|
|
100
|
+
const original = (request as AbstractRequest).original
|
|
101
|
+
if (original == null || typeof original !== 'object' || !('file' in original)) return undefined
|
|
102
|
+
|
|
103
|
+
return (original as import('fastify').FastifyRequest).file()
|
|
104
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
2
2
|
import type { ApiServer, ApiServerAppend } from './types.js'
|
|
3
|
-
import {
|
|
3
|
+
import { assertContext, createService } from '@owlmeans/context'
|
|
4
4
|
import { canServeModule, executeResponse, provideRequest } from './utils/index.js'
|
|
5
5
|
import { DEFAULT_ALIAS, CLOSED_HOST, PORT, OPENED_HOST } from './consts.js'
|
|
6
6
|
import type { ServerEntrypoint } from '@owlmeans/server-entrypoint'
|
|
@@ -57,8 +57,6 @@ export const createApiServer = (alias: string): ApiServer => {
|
|
|
57
57
|
}*/
|
|
58
58
|
}),
|
|
59
59
|
|
|
60
|
-
layers: [Layer.System],
|
|
61
|
-
|
|
62
60
|
listen: async () => {
|
|
63
61
|
const context = _assertContext(service.ctx as Context)
|
|
64
62
|
|
|
@@ -133,10 +131,8 @@ export const createApiServer = (alias: string): ApiServer => {
|
|
|
133
131
|
return context
|
|
134
132
|
}
|
|
135
133
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
// Actually intermediate module can be created without handler by elevate function
|
|
139
|
-
if (module.route.match(request) && module.handle != null) {
|
|
134
|
+
// An intermediate route can be created without a handler by the binding layer.
|
|
135
|
+
if (module.route.match(request, module.mount()) && module.handle != null) {
|
|
140
136
|
const response = provideResponse(reply)
|
|
141
137
|
const currentRequest = provideRequest(module.alias, request, true)
|
|
142
138
|
currentRequest.original._ctx = context
|
|
@@ -150,42 +146,38 @@ export const createApiServer = (alias: string): ApiServer => {
|
|
|
150
146
|
}, Promise.resolve(context))
|
|
151
147
|
})
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
context.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
headers: fixFormatDates(module.filter?.headers ?? {})
|
|
184
|
-
},
|
|
185
|
-
handler: createServerHandler(module, location)
|
|
186
|
-
})
|
|
149
|
+
context.entrypoints<ServerEntrypoint<FastifyRequest>>()
|
|
150
|
+
.filter(module => canServeModule(context, module) && !module.route.isIntermediate())
|
|
151
|
+
.forEach(module => {
|
|
152
|
+
const method = module.route.route.method ?? RouteMethod.GET
|
|
153
|
+
if (module.handle == null) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
server.route({
|
|
157
|
+
url: module.mount(), method,
|
|
158
|
+
schema: {
|
|
159
|
+
consumes: [
|
|
160
|
+
'application/json',
|
|
161
|
+
'application/x-www-form-urlencoded',
|
|
162
|
+
'multipart/form-data',
|
|
163
|
+
],
|
|
164
|
+
querystring: fixFormatDates(module.filter?.query ?? {}),
|
|
165
|
+
...(
|
|
166
|
+
[
|
|
167
|
+
RouteMethod.POST,
|
|
168
|
+
RouteMethod.PATCH,
|
|
169
|
+
RouteMethod.PUT
|
|
170
|
+
].includes(method)
|
|
171
|
+
? { body: fixFormatDates(module.filter?.body ?? {}) }
|
|
172
|
+
: {}
|
|
173
|
+
),
|
|
174
|
+
params: fixFormatDates(module.filter?.params ?? {}),
|
|
175
|
+
response: module.filter?.response,
|
|
176
|
+
headers: fixFormatDates(module.filter?.headers ?? {})
|
|
177
|
+
},
|
|
178
|
+
handler: createServerHandler(module, location)
|
|
187
179
|
})
|
|
188
|
-
|
|
180
|
+
})
|
|
189
181
|
|
|
190
182
|
service.initialized = true
|
|
191
183
|
})
|
|
@@ -208,4 +200,3 @@ export const appendApiServer = <C extends Config, T extends ServerContext<C>>(
|
|
|
208
200
|
return context
|
|
209
201
|
}
|
|
210
202
|
|
|
211
|
-
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import type { InitializedService
|
|
2
|
+
import type { InitializedService } from '@owlmeans/context'
|
|
3
3
|
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
4
4
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
|
5
5
|
import '@fastify/middie/types/index.d.ts'
|
|
@@ -7,8 +7,6 @@ import '@fastify/middie/types/index.d.ts'
|
|
|
7
7
|
export interface ApiServer extends InitializedService {
|
|
8
8
|
server: FastifyInstance
|
|
9
9
|
|
|
10
|
-
layers: [Layer.System]
|
|
11
|
-
|
|
12
10
|
listen: () => Promise<void>
|
|
13
11
|
}
|
|
14
12
|
|
package/src/utils/error.ts
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { FORBIDDEN_ERROR, SERVER_ERROR, UNAUTHORIZED_ERROR } from '@owlmeans/api'
|
|
2
2
|
import type { FastifyReply } from 'fastify'
|
|
3
3
|
import { AccessError, AuthFailedError } from '../errors.js'
|
|
4
|
+
import { AuthForbidden, AuthorizationError } from '@owlmeans/auth'
|
|
4
5
|
import { ResilientError } from '@owlmeans/error'
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Map a thrown error onto an HTTP status.
|
|
9
|
+
*
|
|
10
|
+
* Two families reach here and both must be recognised. `AuthFailedError` / `AccessError` are this
|
|
11
|
+
* package's own, raised by the request pipeline itself. `AuthorizationError` / `AuthForbidden`
|
|
12
|
+
* come from `@owlmeans/auth` and are what the guards and gates actually throw — an unrecognised
|
|
13
|
+
* one answers 500, which reports a refused request as a crashed server: the client cannot tell
|
|
14
|
+
* "sign in again" from "the service is broken", and a consumer watching statuses concludes the
|
|
15
|
+
* application fell over.
|
|
16
|
+
*
|
|
17
|
+
* **Order matters.** `AuthForbidden extends AuthorizationError`, so the 403 branch has to be
|
|
18
|
+
* tested first or every refusal of a permission is reported as a failure to authenticate.
|
|
19
|
+
*/
|
|
6
20
|
export const handleError = (error: Error, reply: FastifyReply) => {
|
|
7
21
|
let code: number = SERVER_ERROR
|
|
8
|
-
if (error instanceof
|
|
9
|
-
|
|
10
|
-
} else if (error instanceof AccessError) {
|
|
22
|
+
if (error instanceof AuthForbidden || error instanceof AccessError) {
|
|
23
|
+
// An established identity that lacks the permission. Re-authenticating changes nothing.
|
|
11
24
|
code = FORBIDDEN_ERROR
|
|
25
|
+
} else if (error instanceof AuthorizationError || error instanceof AuthFailedError) {
|
|
26
|
+
// No usable credential — absent, expired, revoked, or naming a session this server no longer
|
|
27
|
+
// holds. The caller is told to sign in, not that signing in would not help.
|
|
28
|
+
code = UNAUTHORIZED_ERROR
|
|
12
29
|
}
|
|
13
30
|
if (!reply.sent) {
|
|
14
31
|
reply.code(code).send(
|
package/src/utils/guards.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isContextWithoutIds, Layer } from '@owlmeans/context'
|
|
2
1
|
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
3
2
|
import type { ServerEntrypoint } from '@owlmeans/server-entrypoint'
|
|
4
3
|
import type { GuardService } from '@owlmeans/entrypoint'
|
|
@@ -7,6 +6,7 @@ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
|
7
6
|
import { executeResponse, provideRequest } from './payload.js'
|
|
8
7
|
import { AuthFailedError } from '../errors.js'
|
|
9
8
|
import type { Auth } from '@owlmeans/auth'
|
|
9
|
+
import { attachEntity } from '@owlmeans/auth-common'
|
|
10
10
|
|
|
11
11
|
type Config = ServerConfig
|
|
12
12
|
interface Context<C extends Config = Config> extends ServerContext<C> { }
|
|
@@ -50,22 +50,15 @@ export const authorize = async <C extends Config, T extends Context<C>>(
|
|
|
50
50
|
(req as any)._auth = request.auth
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
context = await context.updateContext(request.auth.entityId, Layer.Entity)
|
|
64
|
-
await context.waitForInitialized()
|
|
65
|
-
|
|
66
|
-
// We elevate module to the context level if it was changed
|
|
67
|
-
module = context.entrypoint(module.alias)
|
|
68
|
-
await module.resolve()
|
|
53
|
+
// The token names the organization by slug; everything downstream — grants, records, minted
|
|
54
|
+
// infrastructure names — keys on the entity's stable id. Resolving here, once, is what keeps a
|
|
55
|
+
// rename from being a sweep of every handler: the id arrives on the request, and a slug that
|
|
56
|
+
// has since been retired still resolves to the entity that retired it.
|
|
57
|
+
const entity = await attachEntity(context, request)
|
|
58
|
+
if (entity != null) {
|
|
59
|
+
// Carried on the raw request the same way `_auth` is, so the request the handler is given
|
|
60
|
+
// later — a different object, built after this runs — sees the same resolution.
|
|
61
|
+
;(req as any)._entity = entity
|
|
69
62
|
}
|
|
70
63
|
}
|
|
71
64
|
// Update context in request object
|