@grest-ts/http 0.0.48 → 0.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +243 -175
  2. package/dist/src/_dedupCheck.js +1 -1
  3. package/dist/src/client/GGContextKeySynchronizer.d.ts +11 -0
  4. package/dist/src/client/GGContextKeySynchronizer.d.ts.map +1 -0
  5. package/dist/src/client/GGContextKeySynchronizer.js +26 -0
  6. package/dist/src/client/GGContextKeySynchronizer.js.map +1 -0
  7. package/dist/src/index-browser.d.ts +4 -2
  8. package/dist/src/index-browser.d.ts.map +1 -1
  9. package/dist/src/index-browser.js +4 -2
  10. package/dist/src/index-browser.js.map +1 -1
  11. package/dist/src/index-node.d.ts +8 -3
  12. package/dist/src/index-node.d.ts.map +1 -1
  13. package/dist/src/index-node.js +16 -7
  14. package/dist/src/index-node.js.map +1 -1
  15. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts +4 -3
  16. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts.map +1 -1
  17. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js +9 -2
  18. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js.map +1 -1
  19. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts +3 -3
  20. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts.map +1 -1
  21. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js +13 -16
  22. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js.map +1 -1
  23. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts +1 -1
  24. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts.map +1 -1
  25. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js +3 -3
  26. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -1
  27. package/dist/src/schema/GGCookie.d.ts +50 -0
  28. package/dist/src/schema/GGCookie.d.ts.map +1 -0
  29. package/dist/src/schema/GGCookie.js +130 -0
  30. package/dist/src/schema/GGCookie.js.map +1 -0
  31. package/dist/src/schema/GGHeader.d.ts +23 -0
  32. package/dist/src/schema/GGHeader.d.ts.map +1 -0
  33. package/dist/src/schema/GGHeader.js +41 -0
  34. package/dist/src/schema/GGHeader.js.map +1 -0
  35. package/dist/src/schema/GGHttpPermissionsChecker.d.ts +9 -0
  36. package/dist/src/schema/GGHttpPermissionsChecker.d.ts.map +1 -0
  37. package/dist/src/schema/GGHttpPermissionsChecker.js +29 -0
  38. package/dist/src/schema/GGHttpPermissionsChecker.js.map +1 -0
  39. package/dist/src/schema/GGHttpSchema.d.ts +8 -59
  40. package/dist/src/schema/GGHttpSchema.d.ts.map +1 -1
  41. package/dist/src/schema/GGHttpSchema.js.map +1 -1
  42. package/dist/src/schema/GGWireContextKey.d.ts +30 -0
  43. package/dist/src/schema/GGWireContextKey.d.ts.map +1 -0
  44. package/dist/src/schema/GGWireContextKey.js +36 -0
  45. package/dist/src/schema/GGWireContextKey.js.map +1 -0
  46. package/dist/src/schema/GGWireContextKey.node.d.ts +36 -0
  47. package/dist/src/schema/GGWireContextKey.node.d.ts.map +1 -0
  48. package/dist/src/schema/GGWireContextKey.node.js +83 -0
  49. package/dist/src/schema/GGWireContextKey.node.js.map +1 -0
  50. package/dist/src/schema/httpSchema.d.ts +2 -2
  51. package/dist/src/schema/httpSchema.d.ts.map +1 -1
  52. package/dist/src/schema/httpSchema.js.map +1 -1
  53. package/dist/src/server/GGHttp.d.ts +2 -26
  54. package/dist/src/server/GGHttp.d.ts.map +1 -1
  55. package/dist/src/server/GGHttp.js +0 -15
  56. package/dist/src/server/GGHttp.js.map +1 -1
  57. package/dist/src/server/GGHttpSchema.startServer.d.ts +2 -11
  58. package/dist/src/server/GGHttpSchema.startServer.d.ts.map +1 -1
  59. package/dist/src/server/GGHttpSchema.startServer.js +11 -28
  60. package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
  61. package/dist/src/server/GGHttpServer.d.ts +16 -14
  62. package/dist/src/server/GGHttpServer.d.ts.map +1 -1
  63. package/dist/src/server/GGHttpServer.js +26 -79
  64. package/dist/src/server/GGHttpServer.js.map +1 -1
  65. package/dist/src/server/applyRequestMiddleware.d.ts +11 -0
  66. package/dist/src/server/applyRequestMiddleware.d.ts.map +1 -0
  67. package/dist/src/server/applyRequestMiddleware.js +38 -0
  68. package/dist/src/server/applyRequestMiddleware.js.map +1 -0
  69. package/dist/src/server/applyResponseMiddleware.d.ts +2 -2
  70. package/dist/src/server/applyResponseMiddleware.d.ts.map +1 -1
  71. package/dist/src/server/applyResponseMiddleware.js +2 -2
  72. package/dist/src/server/applyResponseMiddleware.js.map +1 -1
  73. package/dist/src/server/checkers/checkPermissionsAtStart.d.ts +39 -0
  74. package/dist/src/server/checkers/checkPermissionsAtStart.d.ts.map +1 -0
  75. package/dist/src/server/checkers/checkPermissionsAtStart.js +82 -0
  76. package/dist/src/server/checkers/checkPermissionsAtStart.js.map +1 -0
  77. package/dist/src/server/checkers/checkWireConflicts.d.ts +18 -0
  78. package/dist/src/server/checkers/checkWireConflicts.d.ts.map +1 -0
  79. package/dist/src/server/checkers/checkWireConflicts.js +43 -0
  80. package/dist/src/server/checkers/checkWireConflicts.js.map +1 -0
  81. package/dist/src/server/checkers/checkWiresImplemented.d.ts +11 -0
  82. package/dist/src/server/checkers/checkWiresImplemented.d.ts.map +1 -0
  83. package/dist/src/server/checkers/checkWiresImplemented.js +27 -0
  84. package/dist/src/server/checkers/checkWiresImplemented.js.map +1 -0
  85. package/dist/src/tsconfig.json +2 -1
  86. package/dist/tsconfig.publish.tsbuildinfo +1 -1
  87. package/package.json +14 -11
  88. package/src/_dedupCheck.ts +1 -1
  89. package/src/client/GGContextKeySynchronizer.test.ts +106 -0
  90. package/src/client/GGContextKeySynchronizer.ts +42 -0
  91. package/src/index-browser.ts +4 -2
  92. package/src/index-node.ts +17 -7
  93. package/src/rpc/RpcRequest/GGRpcRequestBuilder.ts +11 -4
  94. package/src/rpc/RpcRequest/GGRpcRequestParser.ts +16 -17
  95. package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +5 -4
  96. package/src/schema/GGCookie.ts +151 -0
  97. package/src/schema/GGHeader.ts +48 -0
  98. package/src/schema/GGHttpPermissionsChecker.test.ts +125 -0
  99. package/src/schema/GGHttpPermissionsChecker.ts +35 -0
  100. package/src/schema/GGHttpSchema.ts +8 -71
  101. package/src/schema/GGWireContextKey.node.ts +119 -0
  102. package/src/schema/GGWireContextKey.ts +46 -0
  103. package/src/schema/httpSchema.ts +3 -3
  104. package/src/server/GGHttp.ts +3 -34
  105. package/src/server/GGHttpSchema.startServer.ts +13 -38
  106. package/src/server/GGHttpServer.ts +33 -89
  107. package/src/server/applyRequestMiddleware.ts +40 -0
  108. package/src/server/applyResponseMiddleware.ts +5 -5
  109. package/src/server/checkers/checkPermissionsAtStart.ts +112 -0
  110. package/src/server/checkers/checkWireConflicts.ts +65 -0
  111. package/src/server/checkers/checkWiresImplemented.ts +42 -0
  112. package/src/tsconfig.json +2 -1
  113. package/dist/src/schema/cookieMiddleware.d.ts +0 -71
  114. package/dist/src/schema/cookieMiddleware.d.ts.map +0 -1
  115. package/dist/src/schema/cookieMiddleware.js +0 -126
  116. package/dist/src/schema/cookieMiddleware.js.map +0 -1
  117. package/dist/src/schema/headerBinding.d.ts +0 -35
  118. package/dist/src/schema/headerBinding.d.ts.map +0 -1
  119. package/dist/src/schema/headerBinding.js +0 -49
  120. package/dist/src/schema/headerBinding.js.map +0 -1
  121. package/dist/src/server/GG_PERMISSIONS.d.ts +0 -15
  122. package/dist/src/server/GG_PERMISSIONS.d.ts.map +0 -1
  123. package/dist/src/server/GG_PERMISSIONS.js +0 -15
  124. package/dist/src/server/GG_PERMISSIONS.js.map +0 -1
  125. package/src/schema/cookieMiddleware.ts +0 -164
  126. package/src/schema/headerBinding.ts +0 -65
  127. package/src/server/GG_PERMISSIONS.ts +0 -15
@@ -6,14 +6,22 @@ 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 {describePermission, GG_NO_PERMISSIONS, GGContractMethod, GGPermission} from "@grest-ts/schema";
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: {clientToServer: {methods: Record<string, GGContractMethod>}};
16
- middlewares: readonly unknown[];
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
 
@@ -73,16 +81,10 @@ export class GGHttpServer {
73
81
  * Framework-internal — only setupRoutes() (in GGHttpSchema.startServer.ts) should push here.
74
82
  */
75
83
  private readonly _registeredSchemas: GGHttpSchema<any, any>[] = [];
76
-
77
- /**
78
- * All GGHttpSchema instances registered on this server, in registration order.
79
- * Available from the moment compose() begins; frozen (no further push allowed) once start() is called.
80
- */
81
- get registeredSchemas(): ReadonlyArray<GGHttpSchema<any, any>> {
82
- return this._registeredSchemas;
83
- }
84
+ private readonly _registeredWebSocketSchemas: AnyWebSocketSchema[] = [];
84
85
 
85
86
  public readonly httpServer: http.Server;
87
+ private readonly _routeOwners = new Map<string, string>();
86
88
  private activeRequests = 0;
87
89
  private router = findMyWay<findMyWay.HTTPVersion.V1>();
88
90
 
@@ -193,14 +195,12 @@ export class GGHttpServer {
193
195
  this._registeredSchemas.push(schema);
194
196
  }
195
197
 
196
- private readonly _registeredWebSocketSchemas: AnyWebSocketSchema[] = [];
197
-
198
198
  /**
199
- * All GGWebSocketSchema instances registered on this server, in registration order.
200
- * Populated automatically by GGWebSocketSchema.startServer() / .register().
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.
201
201
  */
202
- get registeredWebSocketSchemas(): ReadonlyArray<AnyWebSocketSchema> {
203
- return this._registeredWebSocketSchemas;
202
+ get registeredSchemas(): ReadonlyArray<GGHttpSchema<any, any>> {
203
+ return this._registeredSchemas;
204
204
  }
205
205
 
206
206
  /** @internal Called by GGWebSocketSchema.startServer(). Do not call directly. */
@@ -208,88 +208,32 @@ export class GGHttpServer {
208
208
  this._registeredWebSocketSchemas.push(schema);
209
209
  }
210
210
 
211
- private readonly _schemasWithResolver = new Set<object>();
212
-
213
- /** @internal Called by HTTP / WS register paths when a scope resolver is wired. */
214
- public _markResolverWired(schema: object): void {
215
- this._schemasWithResolver.add(schema);
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;
216
217
  }
217
218
 
218
- private _checkPermissionsAtStart(): void {
219
- type Surface = {label: string; permission: GGPermission | undefined; resolverWired: boolean};
220
- const surfaces: Surface[] = [];
221
-
222
- for (const schema of this._registeredSchemas) {
223
- const resolverWired = this._schemasWithResolver.has(schema);
224
- const methods = schema.contract?.methods ?? {};
225
- for (const name of Object.keys(methods)) {
226
- surfaces.push({
227
- label: `${schema.name}.${name}`,
228
- permission: (methods[name] as GGContractMethod).permission,
229
- resolverWired,
230
- });
231
- }
232
- }
233
- for (const ws of this._registeredWebSocketSchemas) {
234
- const resolverWired = this._schemasWithResolver.has(ws);
235
- const methods = ws.contract.clientToServer.methods;
236
- for (const name of Object.keys(methods)) {
237
- surfaces.push({
238
- label: `${ws.name}.${name}`,
239
- permission: methods[name].permission,
240
- resolverWired,
241
- });
242
- }
243
- if (ws.connectPermission !== undefined) {
244
- surfaces.push({
245
- label: `${ws.name} (connectPermission)`,
246
- permission: ws.connectPermission,
247
- resolverWired,
248
- });
249
- }
250
- }
251
-
252
- let strict = false;
253
- const undeclared: Surface[] = [];
254
- const orphaned: Surface[] = [];
255
- for (const s of surfaces) {
256
- if (s.permission !== undefined || s.resolverWired) strict = true;
257
- if (s.permission === undefined) undeclared.push(s);
258
- else if (s.permission !== GG_NO_PERMISSIONS && !s.resolverWired) orphaned.push(s);
259
- }
260
- if (!strict) return;
261
-
262
- if (undeclared.length > 0) {
263
- 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) {
264
223
  throw new Error(
265
- `GGHttpServer: permission strict mode is active on this server ` +
266
- `(at least one route declares a permission or has .usePermissions(...) wired), ` +
267
- `but the following routes have no \`permission\` declared:\n\n` +
268
- lines +
269
- `\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).`
270
226
  );
271
227
  }
272
- if (orphaned.length > 0) {
273
- const lines = orphaned.map(s =>
274
- ` ${s.label} requires ${describePermission(s.permission)}`
275
- ).join("\n");
276
- throw new Error(
277
- `GGHttpServer: these routes declare non-public permissions but their schema was ` +
278
- `registered without a scope resolver:\n\n` +
279
- lines +
280
- `\n\nFix: call \`.usePermissions(yourResolver)\` on the GGHttp chain (or pass ` +
281
- `\`permissionResolver\` to the WS schema config) before registering these routes.`
282
- );
283
- }
284
- }
285
-
286
- public registerRoute(method: HttpMethod, path: string, handler: GGHttpRequestCallback): void {
228
+ this._routeOwners.set(route, owner ?? path);
287
229
  this.router.on(method as HTTPMethod, path, handler as unknown as findMyWay.Handler<findMyWay.HTTPVersion.V1>);
288
230
  }
289
231
 
290
232
  public async start(): Promise<void> {
291
233
  Object.freeze(this._registeredSchemas);
292
- this._checkPermissionsAtStart();
234
+ checkWireConflicts(this.registeredSchemas, this.registeredWebSocketSchemas);
235
+ checkWiresImplemented(this.registeredSchemas, this.registeredWebSocketSchemas);
236
+ checkPermissionsAtStart(this.registeredSchemas, this.registeredWebSocketSchemas);
293
237
  this._port = await new Promise((resolve) => {
294
238
  this.httpServer.listen(this.configuredPort, '0.0.0.0', () => {
295
239
  const port = (this.httpServer.address() as any).port;
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import type http from "http";
2
- import type {GGHttpResponse, GGHttpTransportMiddleware} from "../schema/GGHttpSchema";
2
+ import type {GGResponse, GGTransportMiddleware} from "@grest-ts/context";
3
3
 
4
4
  /**
5
5
  * Runs the transport chain's server-side response hook and writes the collected
@@ -11,14 +11,14 @@ import type {GGHttpResponse, GGHttpTransportMiddleware} from "../schema/GGHttpSc
11
11
  */
12
12
  export function applyResponseMiddleware(
13
13
  res: http.ServerResponse,
14
- middlewares: readonly GGHttpTransportMiddleware[] | undefined
14
+ middlewares: readonly GGTransportMiddleware[] | undefined
15
15
  ): void {
16
16
  if (!middlewares?.length || res.headersSent) return;
17
- let response: GGHttpResponse | undefined;
17
+ let response: GGResponse | undefined;
18
18
  for (const mw of middlewares) {
19
- if (!mw.updateResponse) continue;
19
+ if (!mw.respond) continue;
20
20
  response ??= {headers: {}};
21
- mw.updateResponse(response);
21
+ mw.respond(response);
22
22
  }
23
23
  if (!response) return;
24
24
  for (const name in response.headers) {
@@ -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
+ }
package/src/tsconfig.json CHANGED
@@ -8,7 +8,8 @@
8
8
  "DOM"
9
9
  ],
10
10
  "types": [
11
- "node"
11
+ "node",
12
+ "vitest/globals"
12
13
  ]
13
14
  },
14
15
  "include": [
@@ -1,71 +0,0 @@
1
- import { GGContextKey } from "@grest-ts/context";
2
- import { type GGSchema } from "@grest-ts/schema";
3
- import type { GGHttpTransportMiddleware } from "./GGHttpSchema";
4
- /**
5
- * Per-request set of context-key names the current route is permitted to write a cookie
6
- * for (declared via GGRpc.*(...).updatesCookie(key)). Populated by the server route
7
- * handler; read by setCookie() to reject undeclared cookie writes.
8
- */
9
- export declare const GG_COOKIE_WRITES: GGContextKey<Set<string>>;
10
- /**
11
- * Cookie write rules — passed to setCookie(key, value, options). They live at the write
12
- * call site (the handler that mints the cookie), never in the shared API.
13
- */
14
- export interface CookieOptions {
15
- httpOnly?: boolean;
16
- secure?: boolean;
17
- sameSite?: "lax" | "strict" | "none";
18
- path?: string;
19
- domain?: string;
20
- maxAgeSec?: number;
21
- }
22
- /**
23
- * Read a single named cookie out of a raw `Cookie` header value. Shared by the HTTP
24
- * binding (parseRequest) and the WebSocket binding (parseHandshake). A malformed
25
- * percent-encoding (e.g. "sid=%") must not crash — fall back to the raw value.
26
- */
27
- export declare function readCookie(rawCookieHeader: string | undefined, name: string): string | undefined;
28
- /**
29
- * Local structural copy of GGWebSocketHandshakeContext (defined in @grest-ts/websocket).
30
- * http must not import websocket; cookie()'s WS half is typed against this shape and
31
- * matches GGWebSocketMiddleware structurally at the webSocketSchema.use() call site.
32
- * `upgradeHeaders` is the real browser upgrade request (carries httpOnly cookies); the
33
- * in-band `headers` map cannot spoof it.
34
- */
35
- interface WsHandshakeContextLike {
36
- headers: Record<string, string>;
37
- upgradeHeaders?: Record<string, string>;
38
- queryArgs: Record<string, string>;
39
- }
40
- export interface GGCookieBinding extends GGHttpTransportMiddleware {
41
- /** The cookie this binding reads, mapped to its value schema — emitted as an `in: cookie` doc param. */
42
- readonly cookieParams: Record<string, GGSchema<string | undefined>>;
43
- parseHandshake(ctx: WsHandshakeContextLike): void;
44
- }
45
- /**
46
- * Bind a context key to a cookie, read-only on the wire. Reads ONLY the named cookie —
47
- * never a header — from the incoming HTTP Cookie (parseRequest) or the real WS upgrade
48
- * Cookie (parseHandshake), into key.get(). The cookie's wire name defaults to the key's
49
- * name. To write the cookie, call setCookie(key, …) from a route that declared
50
- * .updatesCookie(key); the matching binding flushes it as Set-Cookie (HTTP only — a
51
- * WebSocket has no response to set a cookie on).
52
- */
53
- export declare function cookie(key: GGContextKey<string | undefined>, opts?: {
54
- name?: string;
55
- schema?: GGSchema<string | undefined>;
56
- }): GGCookieBinding;
57
- /**
58
- * Schedule a Set-Cookie on the response (HTTP only). The value never touches key.get() —
59
- * inbound reads stay set-once. The route must declare .updatesCookie(key); an undeclared
60
- * call is invalid usage and throws SERVER_ERROR. The matching cookie() binding serializes
61
- * the pending write with these options (HttpOnly/Secure/SameSite/Path/Domain/Max-Age).
62
- */
63
- export declare function setCookie(key: GGContextKey<string | undefined>, value: string | undefined, options?: CookieOptions): void;
64
- /**
65
- * Clear the cookie — schedules a Set-Cookie with Max-Age=0. Pass path/domain to match a
66
- * scoped cookie (deletion only works when path+domain match what was set). Equivalent to
67
- * setCookie(key, undefined, options), but reads clearer at logout-style call sites.
68
- */
69
- export declare function clearCookie(key: GGContextKey<string | undefined>, options?: CookieOptions): void;
70
- export {};
71
- //# sourceMappingURL=cookieMiddleware.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cookieMiddleware.d.ts","sourceRoot":"","sources":["../../../src/schema/cookieMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAgC,KAAK,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AAC7E,OAAO,KAAK,EAAgC,yBAAyB,EAAC,MAAM,gBAAgB,CAAA;AAE5F;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,2BAA4F,CAAA;AAEzH;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAsBD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAehG;AAYD;;;;;;GAMG;AACH,UAAU,sBAAsB;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,MAAM,WAAW,eAAgB,SAAQ,yBAAyB;IAC9D,wGAAwG;IACxG,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IACnE,cAAc,CAAC,GAAG,EAAE,sBAAsB,GAAG,IAAI,CAAA;CACpD;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,IAAI,CAAC,EAAE;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAAC,GAAG,eAAe,CA0B5I;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAiBzH;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAEhG"}