@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.
Files changed (128) hide show
  1. package/README.md +321 -128
  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/client/GGHttpSchema.createClient.d.ts +8 -0
  8. package/dist/src/client/GGHttpSchema.createClient.d.ts.map +1 -1
  9. package/dist/src/client/GGHttpSchema.createClient.js +2 -1
  10. package/dist/src/client/GGHttpSchema.createClient.js.map +1 -1
  11. package/dist/src/index-browser.d.ts +4 -0
  12. package/dist/src/index-browser.d.ts.map +1 -1
  13. package/dist/src/index-browser.js +4 -0
  14. package/dist/src/index-browser.js.map +1 -1
  15. package/dist/src/index-node.d.ts +9 -1
  16. package/dist/src/index-node.d.ts.map +1 -1
  17. package/dist/src/index-node.js +17 -5
  18. package/dist/src/index-node.js.map +1 -1
  19. package/dist/src/rpc/GGHttpRouteRPC.d.ts +4 -0
  20. package/dist/src/rpc/GGHttpRouteRPC.d.ts.map +1 -1
  21. package/dist/src/rpc/GGHttpRouteRPC.js +6 -0
  22. package/dist/src/rpc/GGHttpRouteRPC.js.map +1 -1
  23. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts +4 -3
  24. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts.map +1 -1
  25. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js +9 -2
  26. package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js.map +1 -1
  27. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts +3 -3
  28. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts.map +1 -1
  29. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js +13 -16
  30. package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js.map +1 -1
  31. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts +1 -0
  32. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts.map +1 -1
  33. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js +4 -0
  34. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -1
  35. package/dist/src/schema/GGCookie.d.ts +50 -0
  36. package/dist/src/schema/GGCookie.d.ts.map +1 -0
  37. package/dist/src/schema/GGCookie.js +130 -0
  38. package/dist/src/schema/GGCookie.js.map +1 -0
  39. package/dist/src/schema/GGHeader.d.ts +23 -0
  40. package/dist/src/schema/GGHeader.d.ts.map +1 -0
  41. package/dist/src/schema/GGHeader.js +41 -0
  42. package/dist/src/schema/GGHeader.js.map +1 -0
  43. package/dist/src/schema/GGHttpPermissionsChecker.d.ts +9 -0
  44. package/dist/src/schema/GGHttpPermissionsChecker.d.ts.map +1 -0
  45. package/dist/src/schema/GGHttpPermissionsChecker.js +29 -0
  46. package/dist/src/schema/GGHttpPermissionsChecker.js.map +1 -0
  47. package/dist/src/schema/GGHttpSchema.d.ts +14 -52
  48. package/dist/src/schema/GGHttpSchema.d.ts.map +1 -1
  49. package/dist/src/schema/GGHttpSchema.js.map +1 -1
  50. package/dist/src/schema/GGWireContextKey.d.ts +30 -0
  51. package/dist/src/schema/GGWireContextKey.d.ts.map +1 -0
  52. package/dist/src/schema/GGWireContextKey.js +36 -0
  53. package/dist/src/schema/GGWireContextKey.js.map +1 -0
  54. package/dist/src/schema/GGWireContextKey.node.d.ts +36 -0
  55. package/dist/src/schema/GGWireContextKey.node.d.ts.map +1 -0
  56. package/dist/src/schema/GGWireContextKey.node.js +83 -0
  57. package/dist/src/schema/GGWireContextKey.node.js.map +1 -0
  58. package/dist/src/schema/httpSchema.d.ts +2 -4
  59. package/dist/src/schema/httpSchema.d.ts.map +1 -1
  60. package/dist/src/schema/httpSchema.js +0 -48
  61. package/dist/src/schema/httpSchema.js.map +1 -1
  62. package/dist/src/server/GGHttp.d.ts +2 -26
  63. package/dist/src/server/GGHttp.d.ts.map +1 -1
  64. package/dist/src/server/GGHttp.js +0 -15
  65. package/dist/src/server/GGHttp.js.map +1 -1
  66. package/dist/src/server/GGHttpSchema.startServer.d.ts +2 -11
  67. package/dist/src/server/GGHttpSchema.startServer.d.ts.map +1 -1
  68. package/dist/src/server/GGHttpSchema.startServer.js +14 -28
  69. package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
  70. package/dist/src/server/GGHttpServer.d.ts +45 -14
  71. package/dist/src/server/GGHttpServer.d.ts.map +1 -1
  72. package/dist/src/server/GGHttpServer.js +61 -87
  73. package/dist/src/server/GGHttpServer.js.map +1 -1
  74. package/dist/src/server/applyRequestMiddleware.d.ts +11 -0
  75. package/dist/src/server/applyRequestMiddleware.d.ts.map +1 -0
  76. package/dist/src/server/applyRequestMiddleware.js +38 -0
  77. package/dist/src/server/applyRequestMiddleware.js.map +1 -0
  78. package/dist/src/server/applyResponseMiddleware.d.ts +12 -0
  79. package/dist/src/server/applyResponseMiddleware.d.ts.map +1 -0
  80. package/dist/src/server/applyResponseMiddleware.js +25 -0
  81. package/dist/src/server/applyResponseMiddleware.js.map +1 -0
  82. package/dist/src/server/checkers/checkPermissionsAtStart.d.ts +39 -0
  83. package/dist/src/server/checkers/checkPermissionsAtStart.d.ts.map +1 -0
  84. package/dist/src/server/checkers/checkPermissionsAtStart.js +82 -0
  85. package/dist/src/server/checkers/checkPermissionsAtStart.js.map +1 -0
  86. package/dist/src/server/checkers/checkWireConflicts.d.ts +18 -0
  87. package/dist/src/server/checkers/checkWireConflicts.d.ts.map +1 -0
  88. package/dist/src/server/checkers/checkWireConflicts.js +43 -0
  89. package/dist/src/server/checkers/checkWireConflicts.js.map +1 -0
  90. package/dist/src/server/checkers/checkWiresImplemented.d.ts +11 -0
  91. package/dist/src/server/checkers/checkWiresImplemented.d.ts.map +1 -0
  92. package/dist/src/server/checkers/checkWiresImplemented.js +27 -0
  93. package/dist/src/server/checkers/checkWiresImplemented.js.map +1 -0
  94. package/dist/src/tsconfig.json +2 -1
  95. package/dist/tsconfig.publish.tsbuildinfo +1 -1
  96. package/package.json +14 -11
  97. package/src/_dedupCheck.ts +1 -1
  98. package/src/client/GGContextKeySynchronizer.test.ts +106 -0
  99. package/src/client/GGContextKeySynchronizer.ts +42 -0
  100. package/src/client/GGHttpSchema.createClient.ts +10 -1
  101. package/src/index-browser.ts +4 -0
  102. package/src/index-node.ts +18 -5
  103. package/src/rpc/GGHttpRouteRPC.ts +8 -0
  104. package/src/rpc/RpcRequest/GGRpcRequestBuilder.ts +11 -4
  105. package/src/rpc/RpcRequest/GGRpcRequestParser.ts +16 -17
  106. package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +5 -0
  107. package/src/schema/GGCookie.ts +151 -0
  108. package/src/schema/GGHeader.ts +48 -0
  109. package/src/schema/GGHttpPermissionsChecker.test.ts +125 -0
  110. package/src/schema/GGHttpPermissionsChecker.ts +35 -0
  111. package/src/schema/GGHttpSchema.ts +15 -63
  112. package/src/schema/GGWireContextKey.node.ts +119 -0
  113. package/src/schema/GGWireContextKey.ts +46 -0
  114. package/src/schema/httpSchema.ts +4 -57
  115. package/src/server/GGHttp.ts +3 -34
  116. package/src/server/GGHttpSchema.startServer.ts +16 -38
  117. package/src/server/GGHttpServer.ts +90 -97
  118. package/src/server/applyRequestMiddleware.ts +40 -0
  119. package/src/server/applyResponseMiddleware.ts +27 -0
  120. package/src/server/checkers/checkPermissionsAtStart.ts +112 -0
  121. package/src/server/checkers/checkWireConflicts.ts +65 -0
  122. package/src/server/checkers/checkWiresImplemented.ts +42 -0
  123. package/src/tsconfig.json +2 -1
  124. package/dist/src/server/GG_PERMISSIONS.d.ts +0 -15
  125. package/dist/src/server/GG_PERMISSIONS.d.ts.map +0 -1
  126. package/dist/src/server/GG_PERMISSIONS.js +0 -15
  127. package/dist/src/server/GG_PERMISSIONS.js.map +0 -1
  128. package/src/server/GG_PERMISSIONS.ts +0 -15
@@ -0,0 +1,125 @@
1
+ import {describe, it, expect} from "vitest";
2
+ import type {GGTransportMiddleware} from "@grest-ts/context";
3
+ import {FORBIDDEN, GG_ANY_PERMISSION, GG_NO_PERMISSIONS, GGPermission, IsString} from "@grest-ts/schema";
4
+ import {GGHttpPermissionsChecker} from "./GGHttpPermissionsChecker";
5
+ import {GGWireContextKey} from "./GGWireContextKey";
6
+
7
+ class TestWire extends GGWireContextKey {
8
+ constructor(name: string, private readonly grants: readonly string[] | null) {
9
+ super(name, IsString.orUndefined);
10
+ }
11
+
12
+ public hasPermissions(): boolean {
13
+ return this.grants !== null;
14
+ }
15
+
16
+ public async getGrantedPermissions(): Promise<readonly string[]> {
17
+ return this.grants ?? [];
18
+ }
19
+ }
20
+
21
+ const plainMiddleware: GGTransportMiddleware = {};
22
+
23
+ describe("GGHttpPermissionsChecker", () => {
24
+
25
+ describe("constructor / wire selection", () => {
26
+ it("ignores non-wire middlewares and wires without permissions", async () => {
27
+ const checker = new GGHttpPermissionsChecker([
28
+ plainMiddleware,
29
+ new TestWire("ambient", null),
30
+ ]);
31
+ expect(await checker.assert("Api", "method", GG_NO_PERMISSIONS)).toEqual([]);
32
+ await expect(checker.assert("Api", "method", "anything")).rejects.toBeInstanceOf(FORBIDDEN);
33
+ });
34
+
35
+ it("collects scopes only from permission-bearing wires", async () => {
36
+ const checker = new GGHttpPermissionsChecker([
37
+ plainMiddleware,
38
+ new TestWire("ambient", null),
39
+ new TestWire("header", ["a", "b"]),
40
+ new TestWire("cookie", ["c"]),
41
+ ]);
42
+ expect(await checker.assert("Api", "method", GG_NO_PERMISSIONS)).toEqual([["a", "b"], ["c"]]);
43
+ });
44
+ });
45
+
46
+ describe("assert — no permission wires (fail closed)", () => {
47
+ const checker = new GGHttpPermissionsChecker([]);
48
+
49
+ it("passes when nothing is required", async () => {
50
+ expect(await checker.assert("Api", "method", GG_NO_PERMISSIONS)).toEqual([]);
51
+ expect(await checker.assert("Api", "method", undefined as unknown as GGPermission)).toEqual([]);
52
+ });
53
+
54
+ it("denies a real or any-permission requirement instead of skipping the check", async () => {
55
+ await expect(checker.assert("Api", "method", "items:read")).rejects.toBeInstanceOf(FORBIDDEN);
56
+ await expect(checker.assert("Api", "method", GG_ANY_PERMISSION)).rejects.toBeInstanceOf(FORBIDDEN);
57
+ });
58
+ });
59
+
60
+ describe("assert — with permission wires", () => {
61
+ const checker = new GGHttpPermissionsChecker([new TestWire("header", ["items:read", "items:write"])]);
62
+
63
+ it("returns the resolved (frozen) array-of-arrays scopes when satisfied", async () => {
64
+ const scopes = await checker.assert("Api", "method", "items:read");
65
+ expect(scopes).toEqual([["items:read", "items:write"]]);
66
+ expect(Object.isFrozen(scopes)).toBe(true);
67
+ });
68
+
69
+ it("passes GG_NO_PERMISSIONS and undefined even when wires are present", async () => {
70
+ expect(await checker.assert("Api", "method", GG_NO_PERMISSIONS)).toEqual([["items:read", "items:write"]]);
71
+ expect(await checker.assert("Api", "method", undefined as unknown as GGPermission)).toEqual([["items:read", "items:write"]]);
72
+ });
73
+
74
+ it("throws FORBIDDEN when the required scope is missing", async () => {
75
+ await expect(checker.assert("Api", "method", "admin:all")).rejects.toBeInstanceOf(FORBIDDEN);
76
+ });
77
+
78
+ it("unions grants across wires when checking a requirement", async () => {
79
+ const twoWires = new GGHttpPermissionsChecker([
80
+ new TestWire("header", ["a"]),
81
+ new TestWire("cookie", ["b"]),
82
+ ]);
83
+ expect(await twoWires.assert("Api", "method", {allOf: ["a", "b"]})).toEqual([["a"], ["b"]]);
84
+ expect(await twoWires.assert("Api", "method", {anyOf: ["a", "z"]})).toEqual([["a"], ["b"]]);
85
+ await expect(twoWires.assert("Api", "method", "c")).rejects.toBeInstanceOf(FORBIDDEN);
86
+ });
87
+ });
88
+
89
+ describe("assertScopes", () => {
90
+ const checker = new GGHttpPermissionsChecker([]);
91
+
92
+ it("passes when scopes satisfy the requirement", () => {
93
+ expect(() => checker.assertGrants("Api", "method", [["x"]], "x")).not.toThrow();
94
+ });
95
+
96
+ it("throws FORBIDDEN when scopes do not satisfy", () => {
97
+ expect(() => checker.assertGrants("Api", "method", [["x"]], "y")).toThrow(FORBIDDEN);
98
+ });
99
+
100
+ it("throws FORBIDDEN when scopes are undefined and a real permission is required", () => {
101
+ expect(() => checker.assertGrants("Api", "method", undefined, "y")).toThrow(FORBIDDEN);
102
+ });
103
+
104
+ it("debug message includes schema.method and the described requirement", () => {
105
+ try {
106
+ checker.assertGrants("Api", "method", [["x"]], "y");
107
+ expect.unreachable();
108
+ } catch (e) {
109
+ expect(e).toBeInstanceOf(FORBIDDEN);
110
+ expect((e as InstanceType<typeof FORBIDDEN>).getDebugContext()?.debugMessage).toContain("Api.method requires");
111
+ expect((e as InstanceType<typeof FORBIDDEN>).getDebugContext()?.debugMessage).toContain('"y"');
112
+ }
113
+ });
114
+
115
+ it("omits the dot when method is empty (WS connect gate)", () => {
116
+ try {
117
+ checker.assertGrants("Api", "", undefined, "y");
118
+ expect.unreachable();
119
+ } catch (e) {
120
+ expect((e as InstanceType<typeof FORBIDDEN>).getDebugContext()?.debugMessage).toContain("Api requires");
121
+ expect((e as InstanceType<typeof FORBIDDEN>).getDebugContext()?.debugMessage).not.toContain("Api. requires");
122
+ }
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,35 @@
1
+ import {GGWireContextKey} from "./GGWireContextKey";
2
+ import type {GGTransportMiddleware} from "@grest-ts/context";
3
+ import {FORBIDDEN, GGPermission, GGPermissionChecker} from "@grest-ts/schema";
4
+
5
+ export class GGHttpPermissionsChecker {
6
+
7
+ private readonly middlewaresWithPermissions: GGWireContextKey[] = []
8
+
9
+ constructor(apiMiddlewares: readonly GGTransportMiddleware[]) {
10
+ for (const mw of apiMiddlewares) {
11
+ if (mw instanceof GGWireContextKey && mw.hasPermissions()) {
12
+ this.middlewaresWithPermissions.push(mw);
13
+ }
14
+ }
15
+ Object.freeze(this.middlewaresWithPermissions)
16
+ }
17
+
18
+ public async assert(schema: string, method: string, required: GGPermission): Promise<ReadonlyArray<ReadonlyArray<string>>> {
19
+ const scopes: Array<readonly string[]> = [];
20
+ for (let i = 0; i < this.middlewaresWithPermissions.length; i++) {
21
+ scopes.push(await this.middlewaresWithPermissions[i].getGrantedPermissions())
22
+ }
23
+ this.assertGrants(schema, method, scopes, required)
24
+ return Object.freeze(scopes) as readonly string[][]
25
+ }
26
+
27
+ public assertGrants(schema: string, method: string, granted: undefined | ReadonlyArray<ReadonlyArray<string>>, required: GGPermission): void {
28
+ if (!GGPermissionChecker.satisfies(required, granted)) {
29
+ throw new FORBIDDEN({
30
+ debugMessage: `${schema + (method ? "." + method : "")} requires ${GGPermissionChecker.describePermission(required)} — caller scopes did not satisfy`
31
+ })
32
+ }
33
+ }
34
+
35
+ }
@@ -1,14 +1,14 @@
1
1
  import {ERROR, ERROR_JSON, GGContractApiDefinition, GGContractClass, GGContractMethod, GGSchema, GGSchemaDescription, OK} from "@grest-ts/schema";
2
2
  import type {HttpMethod} from "@grest-ts/common";
3
3
  import type http from "http";
4
- import type {GGHttpServerMiddleware} from "../server/GGHttpSchema.startServer";
4
+ import type {GGContextKey, GGTransportMiddleware} from "@grest-ts/context";
5
5
  import type {OpenAPIV3_1} from "openapi-types";
6
6
 
7
7
  export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
8
8
 
9
9
  public readonly name: string
10
10
  public readonly pathPrefix: string
11
- public readonly apiMiddlewares: readonly GGHttpTransportMiddleware[]
11
+ public readonly apiMiddlewares: readonly GGTransportMiddleware[]
12
12
  public readonly codec: Record<keyof TContract, GGHttpCodec>
13
13
  public readonly contract: GGContractClass<TContract> | null = null
14
14
 
@@ -16,7 +16,7 @@ export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
16
16
  pathPrefix: string,
17
17
  contract: GGContractClass<TContract>,
18
18
  wireCodec: Record<keyof TContract, GGHttpCodec>,
19
- middlewares: readonly GGHttpTransportMiddleware[] = []
19
+ middlewares: readonly GGTransportMiddleware[] = []
20
20
  ) {
21
21
  this.name = contract.name
22
22
  this.pathPrefix = pathPrefix
@@ -36,7 +36,7 @@ export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
36
36
  export interface ClientHttpRouteToRpcTransformClientConfig {
37
37
  pathPrefix: string,
38
38
  contract: GGContractMethod,
39
- middlewares: readonly GGHttpTransportMiddleware[]
39
+ middlewares: readonly GGTransportMiddleware[]
40
40
  }
41
41
 
42
42
  export interface ClientHttpRouteToRpcTransformClientCodec {
@@ -57,12 +57,13 @@ export interface GGHttpFetchRequest {
57
57
 
58
58
  export interface ClientHttpRouteToRpcTransformServerConfig {
59
59
  contract: GGContractMethod,
60
- apiMiddlewares: readonly GGHttpTransportMiddleware[],
61
- serverMiddlewares: readonly GGHttpServerMiddleware[]
60
+ /** Schema bindings + server middlewares, already merged into one ordered server pipeline. */
61
+ middlewares: readonly GGTransportMiddleware[]
62
62
  }
63
63
 
64
64
  export interface ClientHttpRouteToRpcTransformServerCodec {
65
- parseRequest: (req: http.IncomingMessage) => Promise<unknown>,
65
+ readRequest: (req: http.IncomingMessage) => Promise<unknown>,
66
+ validateInput: (rawInput: unknown) => unknown,
66
67
  sendResponse: (res: http.ServerResponse, rpcResult: ERROR<string, unknown> | OK<unknown>) => Promise<void>
67
68
  }
68
69
 
@@ -114,6 +115,13 @@ export interface GGHttpCodec {
114
115
  */
115
116
  readonly responseHeaders: Record<string, GGSchema<string | undefined>>;
116
117
 
118
+ /**
119
+ * Cookie context keys this route is permitted to modify (set/clear), declared via
120
+ * GGRpc.*(...).updatesCookie(key). A handler that changes a cookie its route did not
121
+ * declare is a SERVER_ERROR. Reading a cookie needs no declaration.
122
+ */
123
+ readonly updatesCookies?: readonly GGContextKey<string | undefined>[];
124
+
117
125
  createForClient(config: ClientHttpRouteToRpcTransformClientConfig): ClientHttpRouteToRpcTransformClientCodec
118
126
 
119
127
  createForServer(config: ClientHttpRouteToRpcTransformServerConfig): ClientHttpRouteToRpcTransformServerCodec
@@ -129,59 +137,3 @@ export interface GGHttpCodec {
129
137
  toOpenApiOperation?(config: GGHttpCodecOpenApiConfig): Partial<OpenAPIV3_1.OperationObject>
130
138
  }
131
139
 
132
- // --------------------------------------------------------------------------------------------------------
133
- // Middleware
134
- // --------------------------------------------------------------------------------------------------------
135
-
136
- export interface GGHttpTransportMiddleware {
137
- /**
138
- * Request headers this middleware reads or writes, mapped to their value schemas.
139
- * Keys are header names; values describe the header value format for validation and docs.
140
- * Used for CORS Access-Control-Allow-Headers and OpenAPI parameter docs.
141
- * Use {} if the middleware touches no custom request headers.
142
- *
143
- * @example
144
- * headers: {
145
- * "authorization": IsString.nonEmpty.docs({title: "Bearer token", example: "Bearer ..."}),
146
- * "accept-language": IsLocale.orUndefined
147
- * }
148
- */
149
- readonly headers: Record<string, GGSchema<string | undefined>>;
150
-
151
- /**
152
- * Response headers this middleware sets, mapped to their value schemas.
153
- * Keys are header names; values describe the header value format for validation and docs.
154
- * Used for CORS Access-Control-Expose-Headers and OpenAPI response header docs.
155
- * Use {} if the middleware sets no custom response headers.
156
- */
157
- readonly responseHeaders: Record<string, GGSchema<string | undefined>>;
158
-
159
- /**
160
- * Client-side: modify outgoing request (add headers, etc.)
161
- */
162
- updateRequest?(req: GGHttpRequest): void;
163
-
164
- /**
165
- * Server-side: parse incoming request (extract context from headers, etc.)
166
- */
167
- parseRequest?(req: GGHttpRequest): void;
168
-
169
- /**
170
- * Server-side: modify outgoing response
171
- */
172
- updateResponse?(res: GGHttpResponse): void;
173
-
174
- /**
175
- * Client-side: parse incoming response
176
- */
177
- parseResponse?(res: GGHttpResponse): void;
178
- }
179
-
180
- export interface GGHttpRequest {
181
- headers?: Record<string, string | string[]>;
182
- queryArgs?: Record<string, string | string[]>;
183
- }
184
-
185
- export interface GGHttpResponse {
186
- headers: Record<string, string | string[]>;
187
- }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Server/inbound half of the wire model: define/create the verification handler, run it during
3
+ * the request pipeline (process), and expose the caller's grants to the permission gate
4
+ * (permissions). Node-only — keeps @grest-ts/locator out of the browser bundle.
5
+ */
6
+ import {GGLocator, GGLocatorKey} from "@grest-ts/locator"
7
+ import {GGWireContextKey} from "./GGWireContextKey"
8
+
9
+ export interface GGWireServerHandler {
10
+ /** Verify the raw credential and mint the durable principal. Throws to reject the request. */
11
+ process: () => Promise<void>
12
+ /** The caller's grants from this wire — read off the durable principal minted in process(). */
13
+ permissions?: () => Promise<readonly string[]>
14
+ }
15
+
16
+ const HANDLER_KEYS = new WeakMap<GGWireContextKey, GGLocatorKey<GGWireServerHandler>>()
17
+ const FACTORIES = new WeakMap<GGWireContextKey, unknown>()
18
+
19
+ // Locator keys resolve by their string name, so the handler key must be unique per wire
20
+ // instance — two wires over the same name (e.g. a public ambient cookie and a gated one
21
+ // over the same cookie name) must not share a handler.
22
+ let nextWireHandlerId = 0
23
+
24
+ /** True once .define() has run on this wire — distinguishes verified wires from ambient ones. */
25
+ export function wireIsDefined(wire: GGWireContextKey): boolean {
26
+ return FACTORIES.has(wire)
27
+ }
28
+
29
+ function handlerKeyFor(wire: GGWireContextKey): GGLocatorKey<GGWireServerHandler> {
30
+ let key = HANDLER_KEYS.get(wire)
31
+ if (!key) {
32
+ key = new GGLocatorKey<GGWireServerHandler>(`wire:${wire.name}:${nextWireHandlerId++}`)
33
+ HANDLER_KEYS.set(wire, key)
34
+ }
35
+ return key
36
+ }
37
+
38
+ function resolveHandler(wire: GGWireContextKey): GGWireServerHandler {
39
+ const handler = handlerKeyFor(wire).tryGet()
40
+ if (!handler) {
41
+ throw new Error(`Wire "${wire.name}" is used but not implemented — call ${wire.name}.define(...).create(deps) in compose().`)
42
+ }
43
+ return handler
44
+ }
45
+
46
+ /**
47
+ * Binds the verification handler's deps into one runtime's locator scope. Returned by
48
+ * `.define()`; `.create(deps)` runs once per scope (a fresh worker / restart gets its own).
49
+ */
50
+ export class GGWireHandlerRegistration<Deps> {
51
+ constructor(
52
+ private readonly wire: GGWireContextKey,
53
+ private readonly factory: (deps: Deps) => GGWireServerHandler,
54
+ ) {
55
+ }
56
+
57
+ public create(deps: Deps): void {
58
+ const key = handlerKeyFor(this.wire)
59
+ if (GGLocator.getScope().has(key)) {
60
+ throw new Error(`Wire "${this.wire.name}" was already created in this runtime scope — .create() runs once per runtime.`)
61
+ }
62
+ GGLocator.getScope().set(key, this.factory(deps))
63
+ }
64
+ }
65
+
66
+ declare module "./GGWireContextKey" {
67
+ interface GGWireContextKey {
68
+ /** Attach the server-side verification handler. Process-global, frozen once — a second call throws. */
69
+ define<Deps>(factory: (deps: Deps) => GGWireServerHandler): GGWireHandlerRegistration<Deps>
70
+
71
+ /** @internal server pipeline hook — verify the credential and mint the durable principal. */
72
+ process(): Promise<void>
73
+
74
+ /** @internal If wire is handling permissions **/
75
+ hasPermissions(): boolean;
76
+
77
+ /** @internal permission-gate hook — the caller's grants from this wire. */
78
+ getGrantedPermissions(): Promise<readonly string[]>
79
+
80
+ /** @internal startup validation — true once .create() has run on the current runtime scope. */
81
+ hasHandler(): boolean
82
+
83
+ /** Drop the ephemeral raw credential after process(); ambient wires keep their value. */
84
+ clear(): void
85
+ }
86
+ }
87
+
88
+ GGWireContextKey.prototype.define = function (factory) {
89
+ if (FACTORIES.has(this)) {
90
+ throw new Error(`Wire "${this.name}" already has .define() — it can only be defined once.`)
91
+ }
92
+ FACTORIES.set(this, factory)
93
+ return new GGWireHandlerRegistration(this, factory as any)
94
+ }
95
+
96
+ GGWireContextKey.prototype.process = async function () {
97
+ if (!this.hasHandler()) return
98
+ await resolveHandler(this).process()
99
+ }
100
+
101
+ GGWireContextKey.prototype.hasPermissions = function (): boolean {
102
+ if (!this.hasHandler()) return false
103
+ const handler = resolveHandler(this)
104
+ return !!handler.permissions
105
+ }
106
+
107
+ GGWireContextKey.prototype.getGrantedPermissions = async function () {
108
+ if (!this.hasHandler()) return []
109
+ const handler = resolveHandler(this)
110
+ return handler.permissions ? await handler.permissions() : []
111
+ }
112
+
113
+ GGWireContextKey.prototype.hasHandler = function () {
114
+ return handlerKeyFor(this).has()
115
+ }
116
+
117
+ GGWireContextKey.prototype.clear = function () {
118
+ if (this.hasHandler() && this.has()) this.delete()
119
+ }
@@ -0,0 +1,46 @@
1
+ import {GGContextKey, type GGTransportMiddleware} from "@grest-ts/context"
2
+ import {GGContextKeySynchronizer} from "../client/GGContextKeySynchronizer"
3
+
4
+ /** Outbound freshness gate for a smart wire. The outbound value is always the wire's own context value. */
5
+ export interface GGWireClientHandler {
6
+ /** Async — true when the wire value is stale and recover() should run before the next outbound read. */
7
+ isStale?: () => boolean
8
+ /** Async — refresh the credential into the wire (via key.set); run by the synchronizer's waitFor before the outbound read. */
9
+ recover?: () => Promise<void>
10
+ }
11
+
12
+ /**
13
+ * Base for credential wires (GGHeader, GGCookie). A wire IS a GGContextKey and a transport
14
+ * middleware at once. Verified vs ambient is decided by whether `.define()` was called:
15
+ * - verified (`.define()`d): the wire holds the raw inbound credential, which is verified
16
+ * during process(), then clear()d before the handler runs — handlers read the durable key
17
+ * minted by the handler, never the credential.
18
+ * - ambient (never `.define()`d): the parsed value lands in the wire and persists through the
19
+ * handler (read via WIRE.get()); no process, no clear, no implementation needed.
20
+ *
21
+ * The server/inbound half (define/create/process/permissions) is attached node-side via
22
+ * ./GGWireContextKey.node — the browser bundle never pulls @grest-ts/locator.
23
+ */
24
+ export abstract class GGWireContextKey extends GGContextKey<string | undefined> implements GGTransportMiddleware {
25
+
26
+ private _clientHandler?: GGWireClientHandler
27
+
28
+ /**
29
+ * Attach the outbound freshness gate. Freeze-once. The outbound value is always the wire's
30
+ * context value; this only registers an isStale/recover gate that refreshes it before the read.
31
+ * GGAuthSession calls it internally on the wires it holds.
32
+ */
33
+ public defineClient(handler: GGWireClientHandler): void {
34
+ if (this._clientHandler) {
35
+ throw new Error(`Wire "${this.name}" already has .defineClient() — it can only be defined once.`)
36
+ }
37
+ this._clientHandler = handler
38
+ if (handler.isStale || handler.recover) {
39
+ GGContextKeySynchronizer.provide(this, {
40
+ isStale: handler.isStale ?? (() => false),
41
+ recover: handler.recover ?? (async () => {
42
+ }),
43
+ })
44
+ }
45
+ }
46
+ }
@@ -1,8 +1,6 @@
1
- import type http from "http";
2
1
  import {GGHttpCodec, GGHttpSchema} from "./GGHttpSchema";
3
- import {GGHttpRequest, GGHttpTransportMiddleware} from "./GGHttpSchema";
4
- import {GGContractApiDefinition, GGContractClass, GGSchema} from "@grest-ts/schema";
5
- import {GGContextKey} from "@grest-ts/context";
2
+ import {GGContractApiDefinition, GGContractClass} from "@grest-ts/schema";
3
+ import type {GGTransportMiddleware} from "@grest-ts/context";
6
4
 
7
5
  /**
8
6
  * Create an HTTP API schema builder from a contract.
@@ -38,7 +36,7 @@ class GGHttpSchemaBuilder<TContract extends GGContractApiDefinition, TContext =
38
36
 
39
37
  private readonly _contract: GGContractClass<TContract>
40
38
  private _pathPrefix: string = ""
41
- private _middlewares: GGHttpTransportMiddleware[] = []
39
+ private _middlewares: GGTransportMiddleware[] = []
42
40
 
43
41
  constructor(contract: GGContractClass<TContract>) {
44
42
  this._contract = contract;
@@ -49,62 +47,11 @@ class GGHttpSchemaBuilder<TContract extends GGContractApiDefinition, TContext =
49
47
  return this
50
48
  }
51
49
 
52
- use<M extends GGHttpTransportMiddleware>(middleware: M): GGHttpSchemaBuilder<TContract, TContext | M> {
50
+ use<M extends GGTransportMiddleware>(middleware: M): GGHttpSchemaBuilder<TContract, TContext | M> {
53
51
  this._middlewares.push(middleware)
54
52
  return this as unknown as GGHttpSchemaBuilder<TContract, TContext | M>
55
53
  }
56
54
 
57
- useHeader<Input>(contextKey: GGContextKey<Input>): GGHttpSchemaBuilder<TContract, TContext | Input> {
58
- const codec = contextKey.getCodec("http");
59
- if (!codec) {
60
- throw new Error(`Context key '${contextKey.name}' does not have an 'http-header' codec registered.`);
61
- }
62
-
63
- // Build the typed header map from codec.inputSchema — the IsObject({headerName: schema})
64
- // that describes which headers this codec reads and what format each value has.
65
- const inputSchema = codec.inputSchema;
66
- const headers: Record<string, GGSchema<string | undefined>> = {};
67
- if (inputSchema) {
68
- const desc = inputSchema.toSchemaDescription();
69
- if (desc.node.kind === 'object') {
70
- for (const [k, fieldDesc] of Object.entries(desc.node.properties)) {
71
- headers[k] = fieldDesc.schema as GGSchema<string | undefined>;
72
- }
73
- }
74
- }
75
-
76
- const middleware: GGHttpTransportMiddleware = {
77
- headers,
78
- responseHeaders: {},
79
- updateRequest(req: GGHttpRequest) {
80
- const contextValue = contextKey.get();
81
- if (contextValue !== undefined) {
82
- const result = codec.decode(contextValue);
83
- if (result.success) {
84
- Object.assign(req.headers, result.value);
85
- }
86
- } else {
87
- const emptyResult = codec.decode({} as Input);
88
- if (emptyResult.success) {
89
- for (const key of Object.keys(emptyResult.value as object)) {
90
- delete req.headers[key];
91
- }
92
- }
93
- }
94
- },
95
- parseRequest(req: http.IncomingMessage) {
96
- const headers = req.headers as Record<string, string>;
97
- const result = codec.encode(headers);
98
- if (result.success) {
99
- contextKey.set(result.value);
100
- }
101
- }
102
- };
103
-
104
- this._middlewares.push(middleware);
105
- return this as unknown as GGHttpSchemaBuilder<TContract, TContext | Input>;
106
- }
107
-
108
55
  routes(mapping: { [K in keyof TContract]: GGHttpCodec }): GGHttpSchema<TContract, TContext> {
109
56
  return new GGHttpSchema(this._pathPrefix, this._contract, mapping, this._middlewares)
110
57
  }
@@ -1,23 +1,8 @@
1
1
  import {GGHttpSchema} from "../schema/GGHttpSchema";
2
2
  import {GGContractApiDefinition, GGContractImplementation} from "@grest-ts/schema";
3
- import {GGHttpServerMiddleware} from "./GGHttpSchema.startServer";
3
+ import type {GGTransportMiddleware} from "@grest-ts/context";
4
4
  import {GGHttpServer} from "./GGHttpServer";
5
5
 
6
- /**
7
- * Resolves the caller's permission scopes for the current request. The function
8
- * is called once between transport middleware and the handler. Sync or async —
9
- * both supported (the gate awaits).
10
- *
11
- * Return `null` when no caller identity is available (the gate will throw
12
- * NOT_AUTHORIZED for non-public methods). Return a (possibly empty) set
13
- * otherwise; the gate will call satisfies() against the method's permission.
14
- *
15
- * The resolver should read whatever app-defined context the upstream auth
16
- * middleware populated — it should NOT parse the token itself.
17
- */
18
- export type GGScopeResolver =
19
- () => ReadonlySet<string> | null | Promise<ReadonlySet<string> | null>
20
-
21
6
  export class GGHttp<TContext = undefined> {
22
7
 
23
8
  /**
@@ -26,32 +11,17 @@ export class GGHttp<TContext = undefined> {
26
11
  * Do not tighten back to private.
27
12
  */
28
13
  protected readonly httpServer: GGHttpServer
29
- private readonly middlewares: GGHttpServerMiddleware[] = [];
30
- private permissionResolver?: GGScopeResolver;
14
+ private readonly middlewares: GGTransportMiddleware[] = [];
31
15
 
32
16
  constructor(httpServer: GGHttpServer) {
33
17
  this.httpServer = httpServer;
34
18
  }
35
19
 
36
- public use<M extends GGHttpServerMiddleware>(middleware: M): GGHttp<TContext | M> {
20
+ public use<M extends GGTransportMiddleware>(middleware: M): GGHttp<TContext | M> {
37
21
  this.middlewares.push(middleware);
38
22
  return this as any;
39
23
  }
40
24
 
41
- /**
42
- * Register a scope resolver. Subsequent .http(...) calls capture this
43
- * resolver and use it to gate every request against the contract's
44
- * declared `permission`.
45
- *
46
- * Calling order matters: .use(auth) → .usePermissions(getScopes) → .http(api).
47
- * If .http() runs before .usePermissions(), the call sees no resolver — and
48
- * the startup check will hard-fail if its schema has any non-public method.
49
- */
50
- public usePermissions(resolver: GGScopeResolver): this {
51
- this.permissionResolver = resolver;
52
- return this;
53
- }
54
-
55
25
  public http<TContract extends GGContractApiDefinition, TSchemaContext>(
56
26
  schema: GGHttpSchema<TContract, TSchemaContext>,
57
27
  implementation: GGContractImplementation<TContract>
@@ -59,7 +29,6 @@ export class GGHttp<TContext = undefined> {
59
29
  schema.register(implementation, {
60
30
  http: this.httpServer,
61
31
  middlewares: this.middlewares,
62
- permissionResolver: this.permissionResolver,
63
32
  });
64
33
  return this as any;
65
34
  }