@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.
- package/README.md +243 -175
- 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/index-browser.d.ts +4 -2
- package/dist/src/index-browser.d.ts.map +1 -1
- package/dist/src/index-browser.js +4 -2
- package/dist/src/index-browser.js.map +1 -1
- package/dist/src/index-node.d.ts +8 -3
- package/dist/src/index-node.d.ts.map +1 -1
- package/dist/src/index-node.js +16 -7
- package/dist/src/index-node.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 -1
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts.map +1 -1
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js +3 -3
- 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 +8 -59
- 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 -2
- package/dist/src/schema/httpSchema.d.ts.map +1 -1
- 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 +11 -28
- package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
- package/dist/src/server/GGHttpServer.d.ts +16 -14
- package/dist/src/server/GGHttpServer.d.ts.map +1 -1
- package/dist/src/server/GGHttpServer.js +26 -79
- 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 +2 -2
- package/dist/src/server/applyResponseMiddleware.d.ts.map +1 -1
- package/dist/src/server/applyResponseMiddleware.js +2 -2
- package/dist/src/server/applyResponseMiddleware.js.map +1 -1
- 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/index-browser.ts +4 -2
- package/src/index-node.ts +17 -7
- package/src/rpc/RpcRequest/GGRpcRequestBuilder.ts +11 -4
- package/src/rpc/RpcRequest/GGRpcRequestParser.ts +16 -17
- package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +5 -4
- 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 +8 -71
- package/src/schema/GGWireContextKey.node.ts +119 -0
- package/src/schema/GGWireContextKey.ts +46 -0
- package/src/schema/httpSchema.ts +3 -3
- package/src/server/GGHttp.ts +3 -34
- package/src/server/GGHttpSchema.startServer.ts +13 -38
- package/src/server/GGHttpServer.ts +33 -89
- package/src/server/applyRequestMiddleware.ts +40 -0
- package/src/server/applyResponseMiddleware.ts +5 -5
- 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/schema/cookieMiddleware.d.ts +0 -71
- package/dist/src/schema/cookieMiddleware.d.ts.map +0 -1
- package/dist/src/schema/cookieMiddleware.js +0 -126
- package/dist/src/schema/cookieMiddleware.js.map +0 -1
- package/dist/src/schema/headerBinding.d.ts +0 -35
- package/dist/src/schema/headerBinding.d.ts.map +0 -1
- package/dist/src/schema/headerBinding.js +0 -49
- package/dist/src/schema/headerBinding.js.map +0 -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/schema/cookieMiddleware.ts +0 -164
- package/src/schema/headerBinding.ts +0 -65
- 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,15 +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 {
|
|
5
|
-
import type {GGContextKey} from "@grest-ts/context";
|
|
4
|
+
import type {GGContextKey, GGTransportMiddleware} from "@grest-ts/context";
|
|
6
5
|
import type {OpenAPIV3_1} from "openapi-types";
|
|
7
6
|
|
|
8
7
|
export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
|
|
9
8
|
|
|
10
9
|
public readonly name: string
|
|
11
10
|
public readonly pathPrefix: string
|
|
12
|
-
public readonly apiMiddlewares: readonly
|
|
11
|
+
public readonly apiMiddlewares: readonly GGTransportMiddleware[]
|
|
13
12
|
public readonly codec: Record<keyof TContract, GGHttpCodec>
|
|
14
13
|
public readonly contract: GGContractClass<TContract> | null = null
|
|
15
14
|
|
|
@@ -17,7 +16,7 @@ export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
|
|
|
17
16
|
pathPrefix: string,
|
|
18
17
|
contract: GGContractClass<TContract>,
|
|
19
18
|
wireCodec: Record<keyof TContract, GGHttpCodec>,
|
|
20
|
-
middlewares: readonly
|
|
19
|
+
middlewares: readonly GGTransportMiddleware[] = []
|
|
21
20
|
) {
|
|
22
21
|
this.name = contract.name
|
|
23
22
|
this.pathPrefix = pathPrefix
|
|
@@ -37,7 +36,7 @@ export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
|
|
|
37
36
|
export interface ClientHttpRouteToRpcTransformClientConfig {
|
|
38
37
|
pathPrefix: string,
|
|
39
38
|
contract: GGContractMethod,
|
|
40
|
-
middlewares: readonly
|
|
39
|
+
middlewares: readonly GGTransportMiddleware[]
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export interface ClientHttpRouteToRpcTransformClientCodec {
|
|
@@ -58,12 +57,13 @@ export interface GGHttpFetchRequest {
|
|
|
58
57
|
|
|
59
58
|
export interface ClientHttpRouteToRpcTransformServerConfig {
|
|
60
59
|
contract: GGContractMethod,
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/** Schema bindings + server middlewares, already merged into one ordered server pipeline. */
|
|
61
|
+
middlewares: readonly GGTransportMiddleware[]
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
export interface ClientHttpRouteToRpcTransformServerCodec {
|
|
66
|
-
|
|
65
|
+
readRequest: (req: http.IncomingMessage) => Promise<unknown>,
|
|
66
|
+
validateInput: (rawInput: unknown) => unknown,
|
|
67
67
|
sendResponse: (res: http.ServerResponse, rpcResult: ERROR<string, unknown> | OK<unknown>) => Promise<void>
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -137,66 +137,3 @@ export interface GGHttpCodec {
|
|
|
137
137
|
toOpenApiOperation?(config: GGHttpCodecOpenApiConfig): Partial<OpenAPIV3_1.OperationObject>
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// --------------------------------------------------------------------------------------------------------
|
|
141
|
-
// Middleware
|
|
142
|
-
// --------------------------------------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
export interface GGHttpTransportMiddleware {
|
|
145
|
-
/**
|
|
146
|
-
* Request headers this middleware reads or writes, mapped to their value schemas.
|
|
147
|
-
* Keys are header names; values describe the header value format for validation and docs.
|
|
148
|
-
* Used for CORS Access-Control-Allow-Headers and OpenAPI parameter docs.
|
|
149
|
-
* Use {} if the middleware touches no custom request headers.
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* headers: {
|
|
153
|
-
* "authorization": IsString.nonEmpty.docs({title: "Bearer token", example: "Bearer ..."}),
|
|
154
|
-
* "accept-language": IsLocale.orUndefined
|
|
155
|
-
* }
|
|
156
|
-
*/
|
|
157
|
-
readonly headers: Record<string, GGSchema<string | undefined>>;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Response headers this middleware sets, mapped to their value schemas.
|
|
161
|
-
* Keys are header names; values describe the header value format for validation and docs.
|
|
162
|
-
* Used for CORS Access-Control-Expose-Headers and OpenAPI response header docs.
|
|
163
|
-
* Use {} if the middleware sets no custom response headers.
|
|
164
|
-
*/
|
|
165
|
-
readonly responseHeaders: Record<string, GGSchema<string | undefined>>;
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Cookies this middleware reads, mapped to their value schemas. Emitted as `in: cookie`
|
|
169
|
-
* parameters in OpenAPI so cookie-bound inputs are documented. Absent for header/plain
|
|
170
|
-
* middlewares.
|
|
171
|
-
*/
|
|
172
|
-
readonly cookieParams?: Record<string, GGSchema<string | undefined>>;
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Client-side: modify outgoing request (add headers, etc.)
|
|
176
|
-
*/
|
|
177
|
-
updateRequest?(req: GGHttpRequest): void;
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Server-side: parse incoming request (extract context from headers, etc.)
|
|
181
|
-
*/
|
|
182
|
-
parseRequest?(req: GGHttpRequest): void;
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Server-side: modify outgoing response
|
|
186
|
-
*/
|
|
187
|
-
updateResponse?(res: GGHttpResponse): void;
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Client-side: parse incoming response
|
|
191
|
-
*/
|
|
192
|
-
parseResponse?(res: GGHttpResponse): void;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export interface GGHttpRequest {
|
|
196
|
-
headers?: Record<string, string | string[]>;
|
|
197
|
-
queryArgs?: Record<string, string | string[]>;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export interface GGHttpResponse {
|
|
201
|
-
headers: Record<string, string | string[]>;
|
|
202
|
-
}
|
|
@@ -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
|
+
}
|
package/src/schema/httpSchema.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {GGHttpCodec, GGHttpSchema} from "./GGHttpSchema";
|
|
2
|
-
import {GGHttpTransportMiddleware} from "./GGHttpSchema";
|
|
3
2
|
import {GGContractApiDefinition, GGContractClass} from "@grest-ts/schema";
|
|
3
|
+
import type {GGTransportMiddleware} from "@grest-ts/context";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Create an HTTP API schema builder from a contract.
|
|
@@ -36,7 +36,7 @@ class GGHttpSchemaBuilder<TContract extends GGContractApiDefinition, TContext =
|
|
|
36
36
|
|
|
37
37
|
private readonly _contract: GGContractClass<TContract>
|
|
38
38
|
private _pathPrefix: string = ""
|
|
39
|
-
private _middlewares:
|
|
39
|
+
private _middlewares: GGTransportMiddleware[] = []
|
|
40
40
|
|
|
41
41
|
constructor(contract: GGContractClass<TContract>) {
|
|
42
42
|
this._contract = contract;
|
|
@@ -47,7 +47,7 @@ class GGHttpSchemaBuilder<TContract extends GGContractApiDefinition, TContext =
|
|
|
47
47
|
return this
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
use<M extends
|
|
50
|
+
use<M extends GGTransportMiddleware>(middleware: M): GGHttpSchemaBuilder<TContract, TContext | M> {
|
|
51
51
|
this._middlewares.push(middleware)
|
|
52
52
|
return this as unknown as GGHttpSchemaBuilder<TContract, TContext | M>
|
|
53
53
|
}
|
package/src/server/GGHttp.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
import {GGHttpSchema} from "../schema/GGHttpSchema";
|
|
2
2
|
import {GGContractApiDefinition, GGContractImplementation} from "@grest-ts/schema";
|
|
3
|
-
import {
|
|
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:
|
|
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
|
|
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
|
}
|
|
@@ -6,20 +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/
|
|
15
|
+
import {GG_COOKIE_WRITES} from "../schema/GGCookie";
|
|
16
16
|
import {GG_METRICS} from "@grest-ts/metrics";
|
|
17
17
|
import {GGHttpMetrics} from "./GGHttpMetrics";
|
|
18
18
|
import {GG_HTTP_SERVER} from "./GG_HTTP_SERVER";
|
|
19
19
|
import {GGHttpServer} from "./GGHttpServer";
|
|
20
20
|
import {GGLog} from "@grest-ts/logger";
|
|
21
|
-
import {
|
|
22
|
-
import type {GGScopeResolver} from "./GGHttp";
|
|
21
|
+
import {GGHttpPermissionsChecker} from "../schema/GGHttpPermissionsChecker";
|
|
23
22
|
|
|
24
23
|
export interface GGHttpSchemaConfig {
|
|
25
24
|
/**
|
|
@@ -29,17 +28,7 @@ export interface GGHttpSchemaConfig {
|
|
|
29
28
|
/**
|
|
30
29
|
* Additional middlewares to apply to all routes.
|
|
31
30
|
*/
|
|
32
|
-
middlewares?:
|
|
33
|
-
/**
|
|
34
|
-
* Optional scope resolver. When set, the gate calls it once per request,
|
|
35
|
-
* populates GG_PERMISSIONS, and rejects requests whose contract permission
|
|
36
|
-
* is not satisfied by the resolved scopes.
|
|
37
|
-
*/
|
|
38
|
-
permissionResolver?: GGScopeResolver;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface GGHttpServerMiddleware {
|
|
42
|
-
process?(): Promise<void>
|
|
31
|
+
middlewares?: GGTransportMiddleware[];
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
declare module "../schema/GGHttpSchema" {
|
|
@@ -81,9 +70,10 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
81
70
|
const scope = GGLocator.getScope();
|
|
82
71
|
const parentContext = GGContextStore.tryGetContext();
|
|
83
72
|
|
|
73
|
+
const permissionsChecker = new GGHttpPermissionsChecker(apiMiddlewares);
|
|
84
74
|
for (const mw of apiMiddlewares) {
|
|
85
|
-
const hKeys = Object.keys(mw.headers);
|
|
86
|
-
const rhKeys = Object.keys(mw.responseHeaders);
|
|
75
|
+
const hKeys = Object.keys(mw.headers ?? {});
|
|
76
|
+
const rhKeys = Object.keys(mw.responseHeaders ?? {});
|
|
87
77
|
if (hKeys.length) server.registerCorsHeaders(hKeys);
|
|
88
78
|
if (rhKeys.length) server.registerCorsExposeHeaders(rhKeys);
|
|
89
79
|
}
|
|
@@ -107,8 +97,6 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
107
97
|
// @TODO This is not really used and only for testkit so it would register implementation of the contract... Not cool
|
|
108
98
|
httpSchema.contract.implement(implementation);
|
|
109
99
|
|
|
110
|
-
if (config.permissionResolver) server._markResolverWired(httpSchema);
|
|
111
|
-
|
|
112
100
|
for (const methodName in httpSchema.codec) {
|
|
113
101
|
// Wire format.
|
|
114
102
|
const codec: GGHttpCodec = httpSchema.codec[methodName]
|
|
@@ -125,8 +113,7 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
125
113
|
// build request mapping
|
|
126
114
|
const requestParser: ClientHttpRouteToRpcTransformServerCodec = codec.createForServer({
|
|
127
115
|
contract: contractFunctionSchema,
|
|
128
|
-
|
|
129
|
-
serverMiddlewares: config.middlewares
|
|
116
|
+
middlewares: [...apiMiddlewares, ...(config.middlewares ?? [])]
|
|
130
117
|
})
|
|
131
118
|
const cookieWriteNames = new Set((codec.updatesCookies ?? []).map(k => k.name))
|
|
132
119
|
server.registerRoute(codec.method, pathPrefix + codec.path, async (req: http.IncomingMessage, res: http.ServerResponse): Promise<void> => {
|
|
@@ -138,22 +125,10 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
138
125
|
const startTime = performance.now()
|
|
139
126
|
let rpcResult: ERROR<string, unknown> | OK<unknown>
|
|
140
127
|
try {
|
|
141
|
-
const
|
|
128
|
+
const rawInput = await requestParser.readRequest(req)
|
|
129
|
+
await permissionsChecker.assert(httpSchema.name, methodName, contractFunctionSchema.permission)
|
|
142
130
|
try {
|
|
143
|
-
|
|
144
|
-
const scopes = await config.permissionResolver()
|
|
145
|
-
if (scopes != null) GG_PERMISSIONS.set(new GGPermissionChecker(scopes))
|
|
146
|
-
const required = contractFunctionSchema.permission
|
|
147
|
-
if (required !== undefined && required !== GG_NO_PERMISSIONS) {
|
|
148
|
-
if (scopes == null) throw new NOT_AUTHORIZED({
|
|
149
|
-
debugMessage: `${httpSchema.name}.${methodName} requires ${describePermission(required)} but no caller identity was resolved`
|
|
150
|
-
})
|
|
151
|
-
if (!satisfies(required, scopes)) throw new FORBIDDEN({
|
|
152
|
-
debugMessage: `${httpSchema.name}.${methodName} requires ${describePermission(required)} — caller scopes did not satisfy`
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
rpcResult = {success: true, type: "OK", data: await implFn(rpcInput)}
|
|
131
|
+
rpcResult = {success: true, type: "OK", data: await implFn(requestParser.validateInput(rawInput))}
|
|
157
132
|
// GGLog.debug(httpSchema, "Response", rpcResult) // This is very slow to log this (like 3x performance loss)
|
|
158
133
|
} catch (error: unknown) {
|
|
159
134
|
rpcResult = ERROR.fromUnknown(error);
|
|
@@ -170,7 +145,7 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
170
145
|
metrics(rpcResult?.type, httpSchema.name, codec.method, pathPrefix, codec.path, startTime)
|
|
171
146
|
}
|
|
172
147
|
});
|
|
173
|
-
})
|
|
148
|
+
}, `${httpSchema.name}.${methodName}`)
|
|
174
149
|
}
|
|
175
150
|
}
|
|
176
151
|
|