@grest-ts/http 0.0.47 → 0.0.49
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 +321 -128
- package/dist/src/_dedupCheck.js +1 -1
- package/dist/src/client/GGContextKeySynchronizer.d.ts +11 -0
- package/dist/src/client/GGContextKeySynchronizer.d.ts.map +1 -0
- package/dist/src/client/GGContextKeySynchronizer.js +26 -0
- package/dist/src/client/GGContextKeySynchronizer.js.map +1 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts +8 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts.map +1 -1
- package/dist/src/client/GGHttpSchema.createClient.js +2 -1
- package/dist/src/client/GGHttpSchema.createClient.js.map +1 -1
- package/dist/src/index-browser.d.ts +4 -0
- package/dist/src/index-browser.d.ts.map +1 -1
- package/dist/src/index-browser.js +4 -0
- package/dist/src/index-browser.js.map +1 -1
- package/dist/src/index-node.d.ts +9 -1
- package/dist/src/index-node.d.ts.map +1 -1
- package/dist/src/index-node.js +17 -5
- package/dist/src/index-node.js.map +1 -1
- package/dist/src/rpc/GGHttpRouteRPC.d.ts +4 -0
- package/dist/src/rpc/GGHttpRouteRPC.d.ts.map +1 -1
- package/dist/src/rpc/GGHttpRouteRPC.js +6 -0
- package/dist/src/rpc/GGHttpRouteRPC.js.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts +4 -3
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js +9 -2
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts +3 -3
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js +13 -16
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js.map +1 -1
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts.map +1 -1
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js +4 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -1
- package/dist/src/schema/GGCookie.d.ts +50 -0
- package/dist/src/schema/GGCookie.d.ts.map +1 -0
- package/dist/src/schema/GGCookie.js +130 -0
- package/dist/src/schema/GGCookie.js.map +1 -0
- package/dist/src/schema/GGHeader.d.ts +23 -0
- package/dist/src/schema/GGHeader.d.ts.map +1 -0
- package/dist/src/schema/GGHeader.js +41 -0
- package/dist/src/schema/GGHeader.js.map +1 -0
- package/dist/src/schema/GGHttpPermissionsChecker.d.ts +9 -0
- package/dist/src/schema/GGHttpPermissionsChecker.d.ts.map +1 -0
- package/dist/src/schema/GGHttpPermissionsChecker.js +29 -0
- package/dist/src/schema/GGHttpPermissionsChecker.js.map +1 -0
- package/dist/src/schema/GGHttpSchema.d.ts +14 -52
- package/dist/src/schema/GGHttpSchema.d.ts.map +1 -1
- package/dist/src/schema/GGHttpSchema.js.map +1 -1
- package/dist/src/schema/GGWireContextKey.d.ts +30 -0
- package/dist/src/schema/GGWireContextKey.d.ts.map +1 -0
- package/dist/src/schema/GGWireContextKey.js +36 -0
- package/dist/src/schema/GGWireContextKey.js.map +1 -0
- package/dist/src/schema/GGWireContextKey.node.d.ts +36 -0
- package/dist/src/schema/GGWireContextKey.node.d.ts.map +1 -0
- package/dist/src/schema/GGWireContextKey.node.js +83 -0
- package/dist/src/schema/GGWireContextKey.node.js.map +1 -0
- package/dist/src/schema/httpSchema.d.ts +2 -4
- package/dist/src/schema/httpSchema.d.ts.map +1 -1
- package/dist/src/schema/httpSchema.js +0 -48
- package/dist/src/schema/httpSchema.js.map +1 -1
- package/dist/src/server/GGHttp.d.ts +2 -26
- package/dist/src/server/GGHttp.d.ts.map +1 -1
- package/dist/src/server/GGHttp.js +0 -15
- package/dist/src/server/GGHttp.js.map +1 -1
- package/dist/src/server/GGHttpSchema.startServer.d.ts +2 -11
- package/dist/src/server/GGHttpSchema.startServer.d.ts.map +1 -1
- package/dist/src/server/GGHttpSchema.startServer.js +14 -28
- package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
- package/dist/src/server/GGHttpServer.d.ts +45 -14
- package/dist/src/server/GGHttpServer.d.ts.map +1 -1
- package/dist/src/server/GGHttpServer.js +61 -87
- package/dist/src/server/GGHttpServer.js.map +1 -1
- package/dist/src/server/applyRequestMiddleware.d.ts +11 -0
- package/dist/src/server/applyRequestMiddleware.d.ts.map +1 -0
- package/dist/src/server/applyRequestMiddleware.js +38 -0
- package/dist/src/server/applyRequestMiddleware.js.map +1 -0
- package/dist/src/server/applyResponseMiddleware.d.ts +12 -0
- package/dist/src/server/applyResponseMiddleware.d.ts.map +1 -0
- package/dist/src/server/applyResponseMiddleware.js +25 -0
- package/dist/src/server/applyResponseMiddleware.js.map +1 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.d.ts +39 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.d.ts.map +1 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.js +82 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.js.map +1 -0
- package/dist/src/server/checkers/checkWireConflicts.d.ts +18 -0
- package/dist/src/server/checkers/checkWireConflicts.d.ts.map +1 -0
- package/dist/src/server/checkers/checkWireConflicts.js +43 -0
- package/dist/src/server/checkers/checkWireConflicts.js.map +1 -0
- package/dist/src/server/checkers/checkWiresImplemented.d.ts +11 -0
- package/dist/src/server/checkers/checkWiresImplemented.d.ts.map +1 -0
- package/dist/src/server/checkers/checkWiresImplemented.js +27 -0
- package/dist/src/server/checkers/checkWiresImplemented.js.map +1 -0
- package/dist/src/tsconfig.json +2 -1
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +14 -11
- package/src/_dedupCheck.ts +1 -1
- package/src/client/GGContextKeySynchronizer.test.ts +106 -0
- package/src/client/GGContextKeySynchronizer.ts +42 -0
- package/src/client/GGHttpSchema.createClient.ts +10 -1
- package/src/index-browser.ts +4 -0
- package/src/index-node.ts +18 -5
- package/src/rpc/GGHttpRouteRPC.ts +8 -0
- package/src/rpc/RpcRequest/GGRpcRequestBuilder.ts +11 -4
- package/src/rpc/RpcRequest/GGRpcRequestParser.ts +16 -17
- package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +5 -0
- package/src/schema/GGCookie.ts +151 -0
- package/src/schema/GGHeader.ts +48 -0
- package/src/schema/GGHttpPermissionsChecker.test.ts +125 -0
- package/src/schema/GGHttpPermissionsChecker.ts +35 -0
- package/src/schema/GGHttpSchema.ts +15 -63
- package/src/schema/GGWireContextKey.node.ts +119 -0
- package/src/schema/GGWireContextKey.ts +46 -0
- package/src/schema/httpSchema.ts +4 -57
- package/src/server/GGHttp.ts +3 -34
- package/src/server/GGHttpSchema.startServer.ts +16 -38
- package/src/server/GGHttpServer.ts +90 -97
- package/src/server/applyRequestMiddleware.ts +40 -0
- package/src/server/applyResponseMiddleware.ts +27 -0
- package/src/server/checkers/checkPermissionsAtStart.ts +112 -0
- package/src/server/checkers/checkWireConflicts.ts +65 -0
- package/src/server/checkers/checkWiresImplemented.ts +42 -0
- package/src/tsconfig.json +2 -1
- package/dist/src/server/GG_PERMISSIONS.d.ts +0 -15
- package/dist/src/server/GG_PERMISSIONS.d.ts.map +0 -1
- package/dist/src/server/GG_PERMISSIONS.js +0 -15
- package/dist/src/server/GG_PERMISSIONS.js.map +0 -1
- package/src/server/GG_PERMISSIONS.ts +0 -15
|
@@ -6,19 +6,19 @@
|
|
|
6
6
|
import http from "http";
|
|
7
7
|
import {GGLocator} from "@grest-ts/locator";
|
|
8
8
|
import {ClientHttpRouteToRpcTransformServerCodec, GGHttpCodec, GGHttpSchema} from "../schema/GGHttpSchema";
|
|
9
|
-
import {
|
|
9
|
+
import {ERROR, GGContractApiDefinition, GGContractImplementation, GGContractMethod, OK, SERVER_ERROR} from "@grest-ts/schema";
|
|
10
10
|
import {HttpMethod} from "@grest-ts/common";
|
|
11
11
|
import {GG_DISCOVERY} from "@grest-ts/discovery";
|
|
12
|
-
import {GGContext, GGContextStore} from "@grest-ts/context";
|
|
12
|
+
import {GGContext, GGContextStore, type GGTransportMiddleware} from "@grest-ts/context";
|
|
13
13
|
import {GG_TRACE} from "@grest-ts/trace";
|
|
14
14
|
import {GG_HTTP_REQUEST} from "./GG_HTTP_REQUEST";
|
|
15
|
+
import {GG_COOKIE_WRITES} from "../schema/GGCookie";
|
|
15
16
|
import {GG_METRICS} from "@grest-ts/metrics";
|
|
16
17
|
import {GGHttpMetrics} from "./GGHttpMetrics";
|
|
17
18
|
import {GG_HTTP_SERVER} from "./GG_HTTP_SERVER";
|
|
18
19
|
import {GGHttpServer} from "./GGHttpServer";
|
|
19
20
|
import {GGLog} from "@grest-ts/logger";
|
|
20
|
-
import {
|
|
21
|
-
import type {GGScopeResolver} from "./GGHttp";
|
|
21
|
+
import {GGHttpPermissionsChecker} from "../schema/GGHttpPermissionsChecker";
|
|
22
22
|
|
|
23
23
|
export interface GGHttpSchemaConfig {
|
|
24
24
|
/**
|
|
@@ -28,17 +28,7 @@ export interface GGHttpSchemaConfig {
|
|
|
28
28
|
/**
|
|
29
29
|
* Additional middlewares to apply to all routes.
|
|
30
30
|
*/
|
|
31
|
-
middlewares?:
|
|
32
|
-
/**
|
|
33
|
-
* Optional scope resolver. When set, the gate calls it once per request,
|
|
34
|
-
* populates GG_PERMISSIONS, and rejects requests whose contract permission
|
|
35
|
-
* is not satisfied by the resolved scopes.
|
|
36
|
-
*/
|
|
37
|
-
permissionResolver?: GGScopeResolver;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface GGHttpServerMiddleware {
|
|
41
|
-
process?(): Promise<void>
|
|
31
|
+
middlewares?: GGTransportMiddleware[];
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
declare module "../schema/GGHttpSchema" {
|
|
@@ -80,9 +70,10 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
80
70
|
const scope = GGLocator.getScope();
|
|
81
71
|
const parentContext = GGContextStore.tryGetContext();
|
|
82
72
|
|
|
73
|
+
const permissionsChecker = new GGHttpPermissionsChecker(apiMiddlewares);
|
|
83
74
|
for (const mw of apiMiddlewares) {
|
|
84
|
-
const hKeys = Object.keys(mw.headers);
|
|
85
|
-
const rhKeys = Object.keys(mw.responseHeaders);
|
|
75
|
+
const hKeys = Object.keys(mw.headers ?? {});
|
|
76
|
+
const rhKeys = Object.keys(mw.responseHeaders ?? {});
|
|
86
77
|
if (hKeys.length) server.registerCorsHeaders(hKeys);
|
|
87
78
|
if (rhKeys.length) server.registerCorsExposeHeaders(rhKeys);
|
|
88
79
|
}
|
|
@@ -106,8 +97,6 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
106
97
|
// @TODO This is not really used and only for testkit so it would register implementation of the contract... Not cool
|
|
107
98
|
httpSchema.contract.implement(implementation);
|
|
108
99
|
|
|
109
|
-
if (config.permissionResolver) server._markResolverWired(httpSchema);
|
|
110
|
-
|
|
111
100
|
for (const methodName in httpSchema.codec) {
|
|
112
101
|
// Wire format.
|
|
113
102
|
const codec: GGHttpCodec = httpSchema.codec[methodName]
|
|
@@ -124,33 +113,22 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
124
113
|
// build request mapping
|
|
125
114
|
const requestParser: ClientHttpRouteToRpcTransformServerCodec = codec.createForServer({
|
|
126
115
|
contract: contractFunctionSchema,
|
|
127
|
-
|
|
128
|
-
serverMiddlewares: config.middlewares
|
|
116
|
+
middlewares: [...apiMiddlewares, ...(config.middlewares ?? [])]
|
|
129
117
|
})
|
|
118
|
+
const cookieWriteNames = new Set((codec.updatesCookies ?? []).map(k => k.name))
|
|
130
119
|
server.registerRoute(codec.method, pathPrefix + codec.path, async (req: http.IncomingMessage, res: http.ServerResponse): Promise<void> => {
|
|
131
120
|
scope.ensureEntered();
|
|
132
|
-
return new GGContext("REQ", parentContext).run(async () => {
|
|
121
|
+
return new GGContext("REQ", parentContext, true).run(async () => {
|
|
133
122
|
GG_TRACE.init();
|
|
134
123
|
GG_HTTP_REQUEST.set({port: server.port, method: req.method, path: req.url});
|
|
124
|
+
GG_COOKIE_WRITES.set(cookieWriteNames);
|
|
135
125
|
const startTime = performance.now()
|
|
136
126
|
let rpcResult: ERROR<string, unknown> | OK<unknown>
|
|
137
127
|
try {
|
|
138
|
-
const
|
|
128
|
+
const rawInput = await requestParser.readRequest(req)
|
|
129
|
+
await permissionsChecker.assert(httpSchema.name, methodName, contractFunctionSchema.permission)
|
|
139
130
|
try {
|
|
140
|
-
|
|
141
|
-
const scopes = await config.permissionResolver()
|
|
142
|
-
if (scopes != null) GG_PERMISSIONS.set(new GGPermissionChecker(scopes))
|
|
143
|
-
const required = contractFunctionSchema.permission
|
|
144
|
-
if (required !== undefined && required !== GG_NO_PERMISSIONS) {
|
|
145
|
-
if (scopes == null) throw new NOT_AUTHORIZED({
|
|
146
|
-
debugMessage: `${httpSchema.name}.${methodName} requires ${describePermission(required)} but no caller identity was resolved`
|
|
147
|
-
})
|
|
148
|
-
if (!satisfies(required, scopes)) throw new FORBIDDEN({
|
|
149
|
-
debugMessage: `${httpSchema.name}.${methodName} requires ${describePermission(required)} — caller scopes did not satisfy`
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
rpcResult = {success: true, type: "OK", data: await implFn(rpcInput)}
|
|
131
|
+
rpcResult = {success: true, type: "OK", data: await implFn(requestParser.validateInput(rawInput))}
|
|
154
132
|
// GGLog.debug(httpSchema, "Response", rpcResult) // This is very slow to log this (like 3x performance loss)
|
|
155
133
|
} catch (error: unknown) {
|
|
156
134
|
rpcResult = ERROR.fromUnknown(error);
|
|
@@ -167,7 +145,7 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
167
145
|
metrics(rpcResult?.type, httpSchema.name, codec.method, pathPrefix, codec.path, startTime)
|
|
168
146
|
}
|
|
169
147
|
});
|
|
170
|
-
})
|
|
148
|
+
}, `${httpSchema.name}.${methodName}`)
|
|
171
149
|
}
|
|
172
150
|
}
|
|
173
151
|
|
|
@@ -6,20 +6,50 @@ import {GG_HTTP_SERVER} from "./GG_HTTP_SERVER";
|
|
|
6
6
|
import {GGLog} from "@grest-ts/logger";
|
|
7
7
|
import findMyWay, {HTTPMethod} from "find-my-way";
|
|
8
8
|
import type {GGHttpSchema} from "../schema/GGHttpSchema";
|
|
9
|
-
import {
|
|
9
|
+
import {checkWiresImplemented} from "./checkers/checkWiresImplemented";
|
|
10
|
+
import {checkPermissionsAtStart} from "./checkers/checkPermissionsAtStart";
|
|
11
|
+
import {checkWireConflicts} from "./checkers/checkWireConflicts";
|
|
12
|
+
import "../schema/GGWireContextKey.node";
|
|
13
|
+
import {GGContractMethod, GGPermission} from "@grest-ts/schema";
|
|
14
|
+
import type {GGTransportMiddleware} from "@grest-ts/context";
|
|
10
15
|
// Forward declaration — actual type lives in @grest-ts/websocket to avoid circular dep.
|
|
11
16
|
// GGHttpServer only stores the array; callers cast as needed.
|
|
12
17
|
type AnyWebSocketSchema = {
|
|
13
18
|
name: string;
|
|
14
19
|
path: string;
|
|
15
|
-
contract: {
|
|
16
|
-
|
|
20
|
+
contract: {
|
|
21
|
+
clientToServer: {methods: Record<string, GGContractMethod>};
|
|
22
|
+
serverToClient: {methods: Record<string, GGContractMethod>};
|
|
23
|
+
};
|
|
24
|
+
middlewares: readonly GGTransportMiddleware[];
|
|
17
25
|
connectPermission?: GGPermission;
|
|
18
26
|
};
|
|
19
27
|
|
|
28
|
+
export interface GGCorsConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Allowed cross-origin origins. Either an exact-match list or a predicate.
|
|
31
|
+
* Only a matching Origin is echoed back — an arbitrary origin is never
|
|
32
|
+
* reflected (that would be a credential-leak / CSRF hole with credentials on).
|
|
33
|
+
*/
|
|
34
|
+
origins: string[] | ((origin: string) => boolean);
|
|
35
|
+
/**
|
|
36
|
+
* When true, adds Access-Control-Allow-Credentials: true so the browser will
|
|
37
|
+
* send/store cross-origin cookies. Requires an exact-origin echo (never `*`),
|
|
38
|
+
* which this enforces by construction.
|
|
39
|
+
*/
|
|
40
|
+
credentials?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
export interface GGHttpServerAdapterConfig {
|
|
21
44
|
key?: GGLocatorKey<GGHttpServer>;
|
|
22
45
|
port?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Cross-origin policy. Omitted (default) keeps the permissive
|
|
48
|
+
* `Access-Control-Allow-Origin: *` with no credentials — backward compatible.
|
|
49
|
+
* When set, switches to allowlisted mode: only an Origin in `origins` is echoed
|
|
50
|
+
* (never `*`), and `credentials: true` enables cross-origin cookies.
|
|
51
|
+
*/
|
|
52
|
+
cors?: GGCorsConfig;
|
|
23
53
|
/**
|
|
24
54
|
* If provided, the server listens over HTTPS using this cert+key. Both
|
|
25
55
|
* must be PEM-encoded strings or Buffers. Leaving undefined keeps the
|
|
@@ -41,6 +71,7 @@ export class GGHttpServer {
|
|
|
41
71
|
protected readonly scope: GGLocatorScope;
|
|
42
72
|
protected readonly runtimeName: string;
|
|
43
73
|
private readonly configuredPort: number;
|
|
74
|
+
private readonly cors?: GGCorsConfig;
|
|
44
75
|
|
|
45
76
|
private readonly _onStart: Array<() => void> = [];
|
|
46
77
|
private readonly _onTeardown: Array<() => void> = [];
|
|
@@ -50,16 +81,10 @@ export class GGHttpServer {
|
|
|
50
81
|
* Framework-internal — only setupRoutes() (in GGHttpSchema.startServer.ts) should push here.
|
|
51
82
|
*/
|
|
52
83
|
private readonly _registeredSchemas: GGHttpSchema<any, any>[] = [];
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* All GGHttpSchema instances registered on this server, in registration order.
|
|
56
|
-
* Available from the moment compose() begins; frozen (no further push allowed) once start() is called.
|
|
57
|
-
*/
|
|
58
|
-
get registeredSchemas(): ReadonlyArray<GGHttpSchema<any, any>> {
|
|
59
|
-
return this._registeredSchemas;
|
|
60
|
-
}
|
|
84
|
+
private readonly _registeredWebSocketSchemas: AnyWebSocketSchema[] = [];
|
|
61
85
|
|
|
62
86
|
public readonly httpServer: http.Server;
|
|
87
|
+
private readonly _routeOwners = new Map<string, string>();
|
|
63
88
|
private activeRequests = 0;
|
|
64
89
|
private router = findMyWay<findMyWay.HTTPVersion.V1>();
|
|
65
90
|
|
|
@@ -75,6 +100,7 @@ export class GGHttpServer {
|
|
|
75
100
|
this.runtimeName = GGLocator.getScope().serviceName;
|
|
76
101
|
this.scope = GGLocator.getScope();
|
|
77
102
|
this.configuredPort = config?.port ?? (process.env.PORT ? Number(process.env.PORT) : 0);
|
|
103
|
+
this.cors = config?.cors;
|
|
78
104
|
|
|
79
105
|
GGLocator.getScope().setWithLifecycle(config?.key ?? GG_HTTP_SERVER, this, {
|
|
80
106
|
type: GGLocatorServiceType.HTTP,
|
|
@@ -90,14 +116,8 @@ export class GGHttpServer {
|
|
|
90
116
|
}
|
|
91
117
|
this.activeRequests++;
|
|
92
118
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
96
|
-
res.setHeader('Access-Control-Allow-Headers', this._corsHeadersCache);
|
|
97
|
-
if (this._corsExposeHeadersCache) {
|
|
98
|
-
res.setHeader('Access-Control-Expose-Headers', this._corsExposeHeadersCache);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
119
|
+
const cors = corsResponseHeaders(req.headers.origin, this.cors, this._corsHeadersCache, this._corsExposeHeadersCache);
|
|
120
|
+
for (const name in cors) res.setHeader(name, cors[name]);
|
|
101
121
|
if (req.method === 'OPTIONS') {
|
|
102
122
|
res.writeHead(204);
|
|
103
123
|
res.end();
|
|
@@ -175,14 +195,12 @@ export class GGHttpServer {
|
|
|
175
195
|
this._registeredSchemas.push(schema);
|
|
176
196
|
}
|
|
177
197
|
|
|
178
|
-
private readonly _registeredWebSocketSchemas: AnyWebSocketSchema[] = [];
|
|
179
|
-
|
|
180
198
|
/**
|
|
181
|
-
* All
|
|
182
|
-
*
|
|
199
|
+
* All GGHttpSchema instances registered on this server, in registration order.
|
|
200
|
+
* Available from the moment compose() begins; frozen (no further push allowed) once start() is called.
|
|
183
201
|
*/
|
|
184
|
-
get
|
|
185
|
-
return this.
|
|
202
|
+
get registeredSchemas(): ReadonlyArray<GGHttpSchema<any, any>> {
|
|
203
|
+
return this._registeredSchemas;
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
/** @internal Called by GGWebSocketSchema.startServer(). Do not call directly. */
|
|
@@ -190,88 +208,32 @@ export class GGHttpServer {
|
|
|
190
208
|
this._registeredWebSocketSchemas.push(schema);
|
|
191
209
|
}
|
|
192
210
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
/**
|
|
212
|
+
* All GGWebSocketSchema instances registered on this server, in registration order.
|
|
213
|
+
* Populated automatically by GGWebSocketSchema.startServer() / .register().
|
|
214
|
+
*/
|
|
215
|
+
get registeredWebSocketSchemas(): ReadonlyArray<AnyWebSocketSchema> {
|
|
216
|
+
return this._registeredWebSocketSchemas;
|
|
198
217
|
}
|
|
199
218
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
for (const schema of this._registeredSchemas) {
|
|
205
|
-
const resolverWired = this._schemasWithResolver.has(schema);
|
|
206
|
-
const methods = schema.contract?.methods ?? {};
|
|
207
|
-
for (const name of Object.keys(methods)) {
|
|
208
|
-
surfaces.push({
|
|
209
|
-
label: `${schema.name}.${name}`,
|
|
210
|
-
permission: (methods[name] as GGContractMethod).permission,
|
|
211
|
-
resolverWired,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
for (const ws of this._registeredWebSocketSchemas) {
|
|
216
|
-
const resolverWired = this._schemasWithResolver.has(ws);
|
|
217
|
-
const methods = ws.contract.clientToServer.methods;
|
|
218
|
-
for (const name of Object.keys(methods)) {
|
|
219
|
-
surfaces.push({
|
|
220
|
-
label: `${ws.name}.${name}`,
|
|
221
|
-
permission: methods[name].permission,
|
|
222
|
-
resolverWired,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
if (ws.connectPermission !== undefined) {
|
|
226
|
-
surfaces.push({
|
|
227
|
-
label: `${ws.name} (connectPermission)`,
|
|
228
|
-
permission: ws.connectPermission,
|
|
229
|
-
resolverWired,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
let strict = false;
|
|
235
|
-
const undeclared: Surface[] = [];
|
|
236
|
-
const orphaned: Surface[] = [];
|
|
237
|
-
for (const s of surfaces) {
|
|
238
|
-
if (s.permission !== undefined || s.resolverWired) strict = true;
|
|
239
|
-
if (s.permission === undefined) undeclared.push(s);
|
|
240
|
-
else if (s.permission !== GG_NO_PERMISSIONS && !s.resolverWired) orphaned.push(s);
|
|
241
|
-
}
|
|
242
|
-
if (!strict) return;
|
|
243
|
-
|
|
244
|
-
if (undeclared.length > 0) {
|
|
245
|
-
const lines = undeclared.map(s => ` ${s.label}`).join("\n");
|
|
219
|
+
public registerRoute(method: HttpMethod, path: string, handler: GGHttpRequestCallback, owner?: string): void {
|
|
220
|
+
const route = `${method} ${path}`;
|
|
221
|
+
const existing = this._routeOwners.get(route);
|
|
222
|
+
if (existing !== undefined) {
|
|
246
223
|
throw new Error(
|
|
247
|
-
`GGHttpServer:
|
|
248
|
-
`
|
|
249
|
-
`but the following routes have no \`permission\` declared:\n\n` +
|
|
250
|
-
lines +
|
|
251
|
-
`\n\nFix: declare \`permission\` on every route — use \`GG_NO_PERMISSIONS\` for intentionally public ones.`
|
|
224
|
+
`GGHttpServer: route ${route} is declared by both ${existing} and ${owner ?? "another route"}. ` +
|
|
225
|
+
`Give each route a distinct path (change a pathPrefix or route path).`
|
|
252
226
|
);
|
|
253
227
|
}
|
|
254
|
-
|
|
255
|
-
const lines = orphaned.map(s =>
|
|
256
|
-
` ${s.label} requires ${describePermission(s.permission)}`
|
|
257
|
-
).join("\n");
|
|
258
|
-
throw new Error(
|
|
259
|
-
`GGHttpServer: these routes declare non-public permissions but their schema was ` +
|
|
260
|
-
`registered without a scope resolver:\n\n` +
|
|
261
|
-
lines +
|
|
262
|
-
`\n\nFix: call \`.usePermissions(yourResolver)\` on the GGHttp chain (or pass ` +
|
|
263
|
-
`\`permissionResolver\` to the WS schema config) before registering these routes.`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
public registerRoute(method: HttpMethod, path: string, handler: GGHttpRequestCallback): void {
|
|
228
|
+
this._routeOwners.set(route, owner ?? path);
|
|
269
229
|
this.router.on(method as HTTPMethod, path, handler as unknown as findMyWay.Handler<findMyWay.HTTPVersion.V1>);
|
|
270
230
|
}
|
|
271
231
|
|
|
272
232
|
public async start(): Promise<void> {
|
|
273
233
|
Object.freeze(this._registeredSchemas);
|
|
274
|
-
this.
|
|
234
|
+
checkWireConflicts(this.registeredSchemas, this.registeredWebSocketSchemas);
|
|
235
|
+
checkWiresImplemented(this.registeredSchemas, this.registeredWebSocketSchemas);
|
|
236
|
+
checkPermissionsAtStart(this.registeredSchemas, this.registeredWebSocketSchemas);
|
|
275
237
|
this._port = await new Promise((resolve) => {
|
|
276
238
|
this.httpServer.listen(this.configuredPort, '0.0.0.0', () => {
|
|
277
239
|
const port = (this.httpServer.address() as any).port;
|
|
@@ -313,3 +275,34 @@ export class GGHttpServer {
|
|
|
313
275
|
}
|
|
314
276
|
|
|
315
277
|
export type GGHttpRequestCallback = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Computes CORS response headers for a request Origin. Pure (no req/res) so it is
|
|
281
|
+
* unit-testable. Default (no config) = permissive `*`, no credentials. With config =
|
|
282
|
+
* allowlisted: only a matching Origin is echoed (never `*`), `credentials` adds
|
|
283
|
+
* Allow-Credentials, and Vary: Origin is always set so caches key on the origin.
|
|
284
|
+
*/
|
|
285
|
+
export function corsResponseHeaders(
|
|
286
|
+
origin: string | undefined,
|
|
287
|
+
cors: GGCorsConfig | undefined,
|
|
288
|
+
allowHeaders: string,
|
|
289
|
+
exposeHeaders: string
|
|
290
|
+
): Record<string, string> {
|
|
291
|
+
if (!origin) return {};
|
|
292
|
+
const headers: Record<string, string> = {};
|
|
293
|
+
if (cors) {
|
|
294
|
+
// Never reflect the literal "null" origin (sandboxed iframe / file:// / opaque
|
|
295
|
+
// origins) with credentials — the classic null-origin credential-leak footgun.
|
|
296
|
+
const allowed = origin !== "null" && (Array.isArray(cors.origins) ? cors.origins.includes(origin) : cors.origins(origin));
|
|
297
|
+
headers['Vary'] = 'Origin';
|
|
298
|
+
if (!allowed) return headers;
|
|
299
|
+
headers['Access-Control-Allow-Origin'] = origin;
|
|
300
|
+
if (cors.credentials) headers['Access-Control-Allow-Credentials'] = 'true';
|
|
301
|
+
} else {
|
|
302
|
+
headers['Access-Control-Allow-Origin'] = '*';
|
|
303
|
+
}
|
|
304
|
+
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
|
|
305
|
+
headers['Access-Control-Allow-Headers'] = allowHeaders;
|
|
306
|
+
if (exposeHeaders) headers['Access-Control-Expose-Headers'] = exposeHeaders;
|
|
307
|
+
return headers;
|
|
308
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type http from "http";
|
|
2
|
+
import type {GGInbound, GGTransportMiddleware} from "@grest-ts/context";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes a Node request + parsed query into a GGInbound and runs the server-side
|
|
6
|
+
* pipeline: every parse() (populating context keys) before any process() (async
|
|
7
|
+
* validation), so a middleware can validate against a key an earlier one parsed.
|
|
8
|
+
* The cookie is taken only from the real request header — never from anywhere a
|
|
9
|
+
* client could forge — preserving the anti-spoof guarantee on every transport.
|
|
10
|
+
*/
|
|
11
|
+
export async function applyRequestMiddleware(
|
|
12
|
+
req: http.IncomingMessage,
|
|
13
|
+
queryArgs: Record<string, string | string[]>,
|
|
14
|
+
middlewares: readonly GGTransportMiddleware[]
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
if (middlewares.length === 0) return;
|
|
17
|
+
const inbound: GGInbound = {
|
|
18
|
+
headers: flatten(req.headers),
|
|
19
|
+
cookie: typeof req.headers.cookie === "string" ? req.headers.cookie : undefined,
|
|
20
|
+
query: flatten(queryArgs)
|
|
21
|
+
};
|
|
22
|
+
for (const mw of middlewares) mw.parse?.(inbound);
|
|
23
|
+
// finally: a rejecting process() (e.g. anonymous → NOT_AUTHORIZED) must still drop every
|
|
24
|
+
// ephemeral credential — including those an earlier wire already parsed — so no raw token
|
|
25
|
+
// outlives the failed request in context.
|
|
26
|
+
try {
|
|
27
|
+
for (const mw of middlewares) await mw.process?.();
|
|
28
|
+
} finally {
|
|
29
|
+
for (const mw of middlewares) mw.clear?.();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function flatten(src: Record<string, string | string[] | undefined>): Record<string, string | undefined> {
|
|
34
|
+
const out: Record<string, string | undefined> = {};
|
|
35
|
+
for (const key in src) {
|
|
36
|
+
const value = src[key];
|
|
37
|
+
out[key] = Array.isArray(value) ? value[0] : value;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type http from "http";
|
|
2
|
+
import type {GGResponse, GGTransportMiddleware} from "@grest-ts/context";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs the transport chain's server-side response hook and writes the collected
|
|
6
|
+
* headers onto the Node response. Applied via setHeader (not writeHead) so a
|
|
7
|
+
* string[] value emits one header line per element — the only way to send
|
|
8
|
+
* multiple Set-Cookie headers — and so the codec's later writeHead(Content-*)
|
|
9
|
+
* merges rather than clobbers. Must run inside the request GGContext so a
|
|
10
|
+
* middleware can read per-request staged state.
|
|
11
|
+
*/
|
|
12
|
+
export function applyResponseMiddleware(
|
|
13
|
+
res: http.ServerResponse,
|
|
14
|
+
middlewares: readonly GGTransportMiddleware[] | undefined
|
|
15
|
+
): void {
|
|
16
|
+
if (!middlewares?.length || res.headersSent) return;
|
|
17
|
+
let response: GGResponse | undefined;
|
|
18
|
+
for (const mw of middlewares) {
|
|
19
|
+
if (!mw.respond) continue;
|
|
20
|
+
response ??= {headers: {}};
|
|
21
|
+
mw.respond(response);
|
|
22
|
+
}
|
|
23
|
+
if (!response) return;
|
|
24
|
+
for (const name in response.headers) {
|
|
25
|
+
res.setHeader(name, response.headers[name]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run at GGHttpServer.start() over the frozen schema graph. Three permission-coherence checks:
|
|
3
|
+
*
|
|
4
|
+
* - strict-mode coverage: once any route declares a permission, every route on the server must
|
|
5
|
+
* declare one (use GG_NO_PERMISSIONS for intentionally public routes).
|
|
6
|
+
* - unsatisfiable route: a route requiring a non-public permission on a schema with no
|
|
7
|
+
* permission-resolving wire (a .define()d wire whose handler exposes permissions()) can never
|
|
8
|
+
* pass the gate — its pooled grants are always empty, so it is permanently FORBIDDEN.
|
|
9
|
+
* - dead serverToClient permission: the gate has no caller identity for server-pushed messages,
|
|
10
|
+
* so a non-public permission on a WS serverToClient method is silently ignored.
|
|
11
|
+
*
|
|
12
|
+
* Scopes come only from the schema's wires; there is no resolver wiring.
|
|
13
|
+
*/
|
|
14
|
+
import {GG_NO_PERMISSIONS, GGContractMethod, GGPermission} from "@grest-ts/schema"
|
|
15
|
+
import type {GGTransportMiddleware} from "@grest-ts/context"
|
|
16
|
+
import {GGWireContextKey} from "../../schema/GGWireContextKey"
|
|
17
|
+
import "../../schema/GGWireContextKey.node"
|
|
18
|
+
|
|
19
|
+
interface HttpSchemaLike {
|
|
20
|
+
name: string
|
|
21
|
+
apiMiddlewares: readonly GGTransportMiddleware[]
|
|
22
|
+
contract?: {methods: Record<string, GGContractMethod>}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WebSocketSchemaLike {
|
|
26
|
+
name: string
|
|
27
|
+
middlewares: readonly GGTransportMiddleware[]
|
|
28
|
+
contract: {
|
|
29
|
+
clientToServer: {methods: Record<string, GGContractMethod>}
|
|
30
|
+
serverToClient: {methods: Record<string, GGContractMethod>}
|
|
31
|
+
}
|
|
32
|
+
connectPermission?: GGPermission
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNonPublic(permission: GGPermission | undefined): boolean {
|
|
36
|
+
return permission !== undefined && permission !== GG_NO_PERMISSIONS
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasPermissionWire(wires: readonly GGTransportMiddleware[]): boolean {
|
|
40
|
+
return wires.some(mw => mw instanceof GGWireContextKey && mw.hasPermissions())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function checkPermissionsAtStart(
|
|
44
|
+
httpSchemas: readonly HttpSchemaLike[],
|
|
45
|
+
webSocketSchemas: readonly WebSocketSchemaLike[],
|
|
46
|
+
): void {
|
|
47
|
+
type Surface = {label: string; permission: GGPermission | undefined; hasWire: boolean}
|
|
48
|
+
const surfaces: Surface[] = []
|
|
49
|
+
const deadServerToClient: string[] = []
|
|
50
|
+
|
|
51
|
+
for (const schema of httpSchemas) {
|
|
52
|
+
const hasWire = hasPermissionWire(schema.apiMiddlewares)
|
|
53
|
+
const methods = schema.contract?.methods ?? {}
|
|
54
|
+
for (const name of Object.keys(methods)) {
|
|
55
|
+
surfaces.push({label: `${schema.name}.${name}`, permission: methods[name].permission, hasWire})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const ws of webSocketSchemas) {
|
|
59
|
+
const hasWire = hasPermissionWire(ws.middlewares)
|
|
60
|
+
const methods = ws.contract.clientToServer.methods
|
|
61
|
+
for (const name of Object.keys(methods)) {
|
|
62
|
+
surfaces.push({label: `${ws.name}.${name}`, permission: methods[name].permission, hasWire})
|
|
63
|
+
}
|
|
64
|
+
if (ws.connectPermission !== undefined) {
|
|
65
|
+
surfaces.push({label: `${ws.name} (connectPermission)`, permission: ws.connectPermission, hasWire})
|
|
66
|
+
}
|
|
67
|
+
const pushed = ws.contract.serverToClient.methods
|
|
68
|
+
for (const name of Object.keys(pushed)) {
|
|
69
|
+
if (isNonPublic(pushed[name].permission)) deadServerToClient.push(` ${ws.name}.${name}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (deadServerToClient.length > 0) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`GGHttpServer: these WebSocket serverToClient methods declare a non-public permission, ` +
|
|
76
|
+
`but server-pushed messages have no caller to gate — the permission is silently ignored:\n\n` +
|
|
77
|
+
deadServerToClient.join("\n") +
|
|
78
|
+
`\n\nFix: set \`permission: GG_NO_PERMISSIONS\` on serverToClient methods.`
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const unsatisfiable = surfaces.filter(s => isNonPublic(s.permission) && !s.hasWire)
|
|
83
|
+
if (unsatisfiable.length > 0) {
|
|
84
|
+
const lines = unsatisfiable.map(s => ` ${s.label}`).join("\n")
|
|
85
|
+
throw new Error(
|
|
86
|
+
`GGHttpServer: these routes require a permission but their schema .use()s no wire that ` +
|
|
87
|
+
`resolves permissions — they can never pass the gate (permanently FORBIDDEN):\n\n` +
|
|
88
|
+
lines +
|
|
89
|
+
`\n\nFix: .use() an auth wire whose handler exposes permissions(), or set the route to ` +
|
|
90
|
+
`\`GG_NO_PERMISSIONS\` if it is meant to be public.`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let strict = false
|
|
95
|
+
const undeclared: Surface[] = []
|
|
96
|
+
for (const s of surfaces) {
|
|
97
|
+
if (s.permission !== undefined) strict = true
|
|
98
|
+
if (s.permission === undefined) undeclared.push(s)
|
|
99
|
+
}
|
|
100
|
+
if (!strict) return
|
|
101
|
+
|
|
102
|
+
if (undeclared.length > 0) {
|
|
103
|
+
const lines = undeclared.map(s => ` ${s.label}`).join("\n")
|
|
104
|
+
throw new Error(
|
|
105
|
+
`GGHttpServer: permission strict mode is active on this server ` +
|
|
106
|
+
`(at least one route declares a permission), ` +
|
|
107
|
+
`but the following routes have no \`permission\` declared:\n\n` +
|
|
108
|
+
lines +
|
|
109
|
+
`\n\nFix: declare \`permission\` on every route — use \`GG_NO_PERMISSIONS\` for intentionally public ones.`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run at GGHttpServer.start() over the frozen schema graph: the wires a single schema .use()s
|
|
3
|
+
* must not conflict with each other. Two conflicts are caught:
|
|
4
|
+
* - same context-key name → both wires resolve to one storage slot; parse() of one clobbers
|
|
5
|
+
* the value the other read.
|
|
6
|
+
* - same inbound header / cookie → both wires read (and a client writes) the same transport
|
|
7
|
+
* field; one silently overwrites the other in parse()/update().
|
|
8
|
+
* Both are per-schema: the same wire reused across different schemas is fine.
|
|
9
|
+
*/
|
|
10
|
+
import type {GGTransportMiddleware} from "@grest-ts/context"
|
|
11
|
+
import {GGWireContextKey} from "../../schema/GGWireContextKey"
|
|
12
|
+
|
|
13
|
+
interface SchemaLike {
|
|
14
|
+
name: string
|
|
15
|
+
wires: readonly GGTransportMiddleware[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function transportKeys(mw: GGTransportMiddleware): string[] {
|
|
19
|
+
return [
|
|
20
|
+
...Object.keys(mw.headers ?? {}).map(h => `header "${h.toLowerCase()}"`),
|
|
21
|
+
...Object.keys(mw.cookieParams ?? {}).map(c => `cookie "${c}"`),
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function checkWireConflicts(
|
|
26
|
+
httpSchemas: readonly {name: string; apiMiddlewares: readonly GGTransportMiddleware[]}[],
|
|
27
|
+
webSocketSchemas: readonly {name: string; middlewares: readonly GGTransportMiddleware[]}[],
|
|
28
|
+
): void {
|
|
29
|
+
const schemas: SchemaLike[] = [
|
|
30
|
+
...httpSchemas.map(s => ({name: s.name, wires: s.apiMiddlewares})),
|
|
31
|
+
...webSocketSchemas.map(s => ({name: s.name, wires: s.middlewares})),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const conflicts: string[] = []
|
|
35
|
+
for (const schema of schemas) {
|
|
36
|
+
const names = new Map<string, GGWireContextKey>()
|
|
37
|
+
const fields = new Map<string, GGTransportMiddleware>()
|
|
38
|
+
|
|
39
|
+
for (const mw of schema.wires) {
|
|
40
|
+
if (mw instanceof GGWireContextKey) {
|
|
41
|
+
if (names.has(mw.name)) {
|
|
42
|
+
conflicts.push(` ${schema.name}: two wires share context-key name "${mw.name}"`)
|
|
43
|
+
} else {
|
|
44
|
+
names.set(mw.name, mw)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const field of transportKeys(mw)) {
|
|
48
|
+
if (fields.has(field)) {
|
|
49
|
+
conflicts.push(` ${schema.name}: two wires bind the same ${field}`)
|
|
50
|
+
} else {
|
|
51
|
+
fields.set(field, mw)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (conflicts.length > 0) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`GGHttpServer: conflicting wires .use()d on one schema:\n\n` +
|
|
60
|
+
conflicts.join("\n") +
|
|
61
|
+
`\n\nFix: a schema must not .use() two wires that share a context-key name or read the ` +
|
|
62
|
+
`same header/cookie — one silently clobbers the other.`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run at GGHttpServer.start() over the frozen schema graph: every wire a schema
|
|
3
|
+
* .use()s and that is .define()d must have a .create()d handler on this runtime.
|
|
4
|
+
*/
|
|
5
|
+
import {GGWireContextKey} from "../../schema/GGWireContextKey"
|
|
6
|
+
import {wireIsDefined} from "../../schema/GGWireContextKey.node"
|
|
7
|
+
|
|
8
|
+
interface HttpSchemaLike {
|
|
9
|
+
name: string
|
|
10
|
+
apiMiddlewares: readonly unknown[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface WebSocketSchemaLike {
|
|
14
|
+
name: string
|
|
15
|
+
middlewares: readonly unknown[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function checkWiresImplemented(
|
|
19
|
+
httpSchemas: readonly HttpSchemaLike[],
|
|
20
|
+
webSocketSchemas: readonly WebSocketSchemaLike[],
|
|
21
|
+
): void {
|
|
22
|
+
const surfaces: {name: string; wires: readonly unknown[]}[] = [
|
|
23
|
+
...httpSchemas.map(s => ({name: s.name, wires: s.apiMiddlewares})),
|
|
24
|
+
...webSocketSchemas.map(s => ({name: s.name, wires: s.middlewares})),
|
|
25
|
+
]
|
|
26
|
+
const missing: string[] = []
|
|
27
|
+
for (const surface of surfaces) {
|
|
28
|
+
for (const mw of surface.wires) {
|
|
29
|
+
if (mw instanceof GGWireContextKey && wireIsDefined(mw) && !mw.hasHandler()) {
|
|
30
|
+
missing.push(` ${surface.name} uses wire "${mw.name}"`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`GGHttpServer: these schemas .use() a smart wire that was never implemented on this ` +
|
|
37
|
+
`runtime:\n\n` +
|
|
38
|
+
missing.join("\n") +
|
|
39
|
+
`\n\nFix: call \`${"<WIRE>"}.define(...).create(deps)\` in compose() before the server starts.`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|