@grest-ts/http 0.0.34 → 0.0.36
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 +24 -0
- package/dist/src/_dedupCheck.js +1 -1
- package/dist/src/index-node.d.ts +1 -0
- package/dist/src/index-node.d.ts.map +1 -1
- package/dist/src/index-node.js +1 -0
- package/dist/src/index-node.js.map +1 -1
- package/dist/src/server/GGHttp.d.ts +24 -0
- package/dist/src/server/GGHttp.d.ts.map +1 -1
- package/dist/src/server/GGHttp.js +19 -1
- package/dist/src/server/GGHttp.js.map +1 -1
- package/dist/src/server/GGHttpSchema.startServer.d.ts +7 -0
- package/dist/src/server/GGHttpSchema.startServer.d.ts.map +1 -1
- package/dist/src/server/GGHttpSchema.startServer.js +20 -1
- package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
- package/dist/src/server/GGHttpServer.d.ts +11 -1
- package/dist/src/server/GGHttpServer.d.ts.map +1 -1
- package/dist/src/server/GGHttpServer.js +68 -0
- package/dist/src/server/GGHttpServer.js.map +1 -1
- package/dist/src/server/GG_PERMISSIONS.d.ts +15 -0
- package/dist/src/server/GG_PERMISSIONS.d.ts.map +1 -0
- package/dist/src/server/GG_PERMISSIONS.js +15 -0
- package/dist/src/server/GG_PERMISSIONS.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/_dedupCheck.ts +1 -1
- package/src/index-node.ts +1 -0
- package/src/server/GGHttp.ts +35 -1
- package/src/server/GGHttpSchema.startServer.ts +24 -1
- package/src/server/GGHttpServer.ts +84 -1
- package/src/server/GG_PERMISSIONS.ts +15 -0
package/src/server/GGHttp.ts
CHANGED
|
@@ -3,6 +3,21 @@ import {GGContractApiDefinition, GGContractImplementation} from "@grest-ts/schem
|
|
|
3
3
|
import {GGHttpServerMiddleware} from "./GGHttpSchema.startServer";
|
|
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
|
+
|
|
6
21
|
export class GGHttp<TContext = undefined> {
|
|
7
22
|
|
|
8
23
|
/**
|
|
@@ -12,6 +27,7 @@ export class GGHttp<TContext = undefined> {
|
|
|
12
27
|
*/
|
|
13
28
|
protected readonly httpServer: GGHttpServer
|
|
14
29
|
private readonly middlewares: GGHttpServerMiddleware[] = [];
|
|
30
|
+
private permissionResolver?: GGScopeResolver;
|
|
15
31
|
|
|
16
32
|
constructor(httpServer: GGHttpServer) {
|
|
17
33
|
this.httpServer = httpServer;
|
|
@@ -22,11 +38,29 @@ export class GGHttp<TContext = undefined> {
|
|
|
22
38
|
return this as any;
|
|
23
39
|
}
|
|
24
40
|
|
|
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
|
+
|
|
25
55
|
public http<TContract extends GGContractApiDefinition, TSchemaContext>(
|
|
26
56
|
schema: GGHttpSchema<TContract, TSchemaContext>,
|
|
27
57
|
implementation: GGContractImplementation<TContract>
|
|
28
58
|
): this {
|
|
29
|
-
schema.register(implementation, {
|
|
59
|
+
schema.register(implementation, {
|
|
60
|
+
http: this.httpServer,
|
|
61
|
+
middlewares: this.middlewares,
|
|
62
|
+
permissionResolver: this.permissionResolver,
|
|
63
|
+
});
|
|
30
64
|
return this as any;
|
|
31
65
|
}
|
|
32
66
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
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 {ERROR, GGContractApiDefinition, GGContractImplementation, GGContractMethod, OK, SERVER_ERROR} from "@grest-ts/schema";
|
|
9
|
+
import {describePermission, ERROR, FORBIDDEN, GG_NO_PERMISSIONS, GGContractApiDefinition, GGContractImplementation, GGContractMethod, GGPermissionChecker, NOT_AUTHORIZED, OK, satisfies, SERVER_ERROR} from "@grest-ts/schema";
|
|
10
10
|
import {HttpMethod} from "@grest-ts/common";
|
|
11
11
|
import {GG_DISCOVERY} from "@grest-ts/discovery";
|
|
12
12
|
import {GGContext, GGContextStore} from "@grest-ts/context";
|
|
@@ -17,6 +17,8 @@ import {GGHttpMetrics} from "./GGHttpMetrics";
|
|
|
17
17
|
import {GG_HTTP_SERVER} from "./GG_HTTP_SERVER";
|
|
18
18
|
import {GGHttpServer} from "./GGHttpServer";
|
|
19
19
|
import {GGLog} from "@grest-ts/logger";
|
|
20
|
+
import {GG_PERMISSIONS} from "./GG_PERMISSIONS";
|
|
21
|
+
import type {GGScopeResolver} from "./GGHttp";
|
|
20
22
|
|
|
21
23
|
export interface GGHttpSchemaConfig {
|
|
22
24
|
/**
|
|
@@ -27,6 +29,12 @@ export interface GGHttpSchemaConfig {
|
|
|
27
29
|
* Additional middlewares to apply to all routes.
|
|
28
30
|
*/
|
|
29
31
|
middlewares?: GGHttpServerMiddleware[];
|
|
32
|
+
/**
|
|
33
|
+
* Optional scope resolver. When set, the gate calls it once per request,
|
|
34
|
+
* populates GG_PERMISSIONS, and rejects requests whose contract permission
|
|
35
|
+
* is not satisfied by the resolved scopes.
|
|
36
|
+
*/
|
|
37
|
+
permissionResolver?: GGScopeResolver;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export interface GGHttpServerMiddleware {
|
|
@@ -98,6 +106,8 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
98
106
|
// @TODO This is not really used and only for testkit so it would register implementation of the contract... Not cool
|
|
99
107
|
httpSchema.contract.implement(implementation);
|
|
100
108
|
|
|
109
|
+
if (config.permissionResolver) server._markResolverWired(httpSchema);
|
|
110
|
+
|
|
101
111
|
for (const methodName in httpSchema.codec) {
|
|
102
112
|
// Wire format.
|
|
103
113
|
const codec: GGHttpCodec = httpSchema.codec[methodName]
|
|
@@ -127,6 +137,19 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
127
137
|
try {
|
|
128
138
|
const rpcInput = await requestParser.parseRequest(req)
|
|
129
139
|
try {
|
|
140
|
+
if (config.permissionResolver) {
|
|
141
|
+
const scopes = await config.permissionResolver()
|
|
142
|
+
if (scopes != null) GG_PERMISSIONS.set(new GGPermissionChecker(scopes))
|
|
143
|
+
const required = contractFunctionSchema.permission
|
|
144
|
+
if (required !== undefined && required !== GG_NO_PERMISSIONS) {
|
|
145
|
+
if (scopes == null) throw new NOT_AUTHORIZED({
|
|
146
|
+
debugMessage: `${httpSchema.name}.${methodName} requires ${describePermission(required)} but no caller identity was resolved`
|
|
147
|
+
})
|
|
148
|
+
if (!satisfies(required, scopes)) throw new FORBIDDEN({
|
|
149
|
+
debugMessage: `${httpSchema.name}.${methodName} requires ${describePermission(required)} — caller scopes did not satisfy`
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
130
153
|
rpcResult = {success: true, type: "OK", data: await implFn(rpcInput)}
|
|
131
154
|
// GGLog.debug(httpSchema, "Response", rpcResult) // This is very slow to log this (like 3x performance loss)
|
|
132
155
|
} catch (error: unknown) {
|
|
@@ -6,9 +6,16 @@ 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
10
|
// Forward declaration — actual type lives in @grest-ts/websocket to avoid circular dep.
|
|
10
11
|
// GGHttpServer only stores the array; callers cast as needed.
|
|
11
|
-
type AnyWebSocketSchema = {
|
|
12
|
+
type AnyWebSocketSchema = {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
contract: {clientToServer: {methods: Record<string, GGContractMethod>}};
|
|
16
|
+
middlewares: readonly unknown[];
|
|
17
|
+
connectPermission?: GGPermission;
|
|
18
|
+
};
|
|
12
19
|
|
|
13
20
|
export interface GGHttpServerAdapterConfig {
|
|
14
21
|
key?: GGLocatorKey<GGHttpServer>;
|
|
@@ -183,12 +190,88 @@ export class GGHttpServer {
|
|
|
183
190
|
this._registeredWebSocketSchemas.push(schema);
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
private readonly _schemasWithResolver = new Set<object>();
|
|
194
|
+
|
|
195
|
+
/** @internal Called by HTTP / WS register paths when a scope resolver is wired. */
|
|
196
|
+
public _markResolverWired(schema: object): void {
|
|
197
|
+
this._schemasWithResolver.add(schema);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _checkPermissionsAtStart(): void {
|
|
201
|
+
type Surface = {label: string; permission: GGPermission | undefined; resolverWired: boolean};
|
|
202
|
+
const surfaces: Surface[] = [];
|
|
203
|
+
|
|
204
|
+
for (const schema of this._registeredSchemas) {
|
|
205
|
+
const resolverWired = this._schemasWithResolver.has(schema);
|
|
206
|
+
const methods = schema.contract?.methods ?? {};
|
|
207
|
+
for (const name of Object.keys(methods)) {
|
|
208
|
+
surfaces.push({
|
|
209
|
+
label: `${schema.name}.${name}`,
|
|
210
|
+
permission: (methods[name] as GGContractMethod).permission,
|
|
211
|
+
resolverWired,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const ws of this._registeredWebSocketSchemas) {
|
|
216
|
+
const resolverWired = this._schemasWithResolver.has(ws);
|
|
217
|
+
const methods = ws.contract.clientToServer.methods;
|
|
218
|
+
for (const name of Object.keys(methods)) {
|
|
219
|
+
surfaces.push({
|
|
220
|
+
label: `${ws.name}.${name}`,
|
|
221
|
+
permission: methods[name].permission,
|
|
222
|
+
resolverWired,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (ws.connectPermission !== undefined) {
|
|
226
|
+
surfaces.push({
|
|
227
|
+
label: `${ws.name} (connectPermission)`,
|
|
228
|
+
permission: ws.connectPermission,
|
|
229
|
+
resolverWired,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let strict = false;
|
|
235
|
+
const undeclared: Surface[] = [];
|
|
236
|
+
const orphaned: Surface[] = [];
|
|
237
|
+
for (const s of surfaces) {
|
|
238
|
+
if (s.permission !== undefined || s.resolverWired) strict = true;
|
|
239
|
+
if (s.permission === undefined) undeclared.push(s);
|
|
240
|
+
else if (s.permission !== GG_NO_PERMISSIONS && !s.resolverWired) orphaned.push(s);
|
|
241
|
+
}
|
|
242
|
+
if (!strict) return;
|
|
243
|
+
|
|
244
|
+
if (undeclared.length > 0) {
|
|
245
|
+
const lines = undeclared.map(s => ` ${s.label}`).join("\n");
|
|
246
|
+
throw new Error(
|
|
247
|
+
`GGHttpServer: permission strict mode is active on this server ` +
|
|
248
|
+
`(at least one route declares a permission or has .usePermissions(...) wired), ` +
|
|
249
|
+
`but the following routes have no \`permission\` declared:\n\n` +
|
|
250
|
+
lines +
|
|
251
|
+
`\n\nFix: declare \`permission\` on every route — use \`GG_NO_PERMISSIONS\` for intentionally public ones.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (orphaned.length > 0) {
|
|
255
|
+
const lines = orphaned.map(s =>
|
|
256
|
+
` ${s.label} requires ${describePermission(s.permission)}`
|
|
257
|
+
).join("\n");
|
|
258
|
+
throw new Error(
|
|
259
|
+
`GGHttpServer: these routes declare non-public permissions but their schema was ` +
|
|
260
|
+
`registered without a scope resolver:\n\n` +
|
|
261
|
+
lines +
|
|
262
|
+
`\n\nFix: call \`.usePermissions(yourResolver)\` on the GGHttp chain (or pass ` +
|
|
263
|
+
`\`permissionResolver\` to the WS schema config) before registering these routes.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
186
268
|
public registerRoute(method: HttpMethod, path: string, handler: GGHttpRequestCallback): void {
|
|
187
269
|
this.router.on(method as HTTPMethod, path, handler as unknown as findMyWay.Handler<findMyWay.HTTPVersion.V1>);
|
|
188
270
|
}
|
|
189
271
|
|
|
190
272
|
public async start(): Promise<void> {
|
|
191
273
|
Object.freeze(this._registeredSchemas);
|
|
274
|
+
this._checkPermissionsAtStart();
|
|
192
275
|
this._port = await new Promise((resolve) => {
|
|
193
276
|
this.httpServer.listen(this.configuredPort, '0.0.0.0', () => {
|
|
194
277
|
const port = (this.httpServer.address() as any).port;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {GGContextKey} from "@grest-ts/context";
|
|
2
|
+
import {GGPermissionChecker, IsAny} from "@grest-ts/schema";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-request permission context populated by the gate when a scope resolver
|
|
6
|
+
* is wired via GGHttp.usePermissions(...). Handler code can read it to make
|
|
7
|
+
* sub-decisions inside an authorized method:
|
|
8
|
+
*
|
|
9
|
+
* const perm = GG_PERMISSIONS.get();
|
|
10
|
+
* if (perm.has(AppPermission.Admin)) { ... }
|
|
11
|
+
*
|
|
12
|
+
* The checker uses the same satisfies() the framework gate uses — no parallel
|
|
13
|
+
* implementations.
|
|
14
|
+
*/
|
|
15
|
+
export const GG_PERMISSIONS = new GGContextKey<GGPermissionChecker>("permissions", IsAny as any);
|