@grest-ts/http 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -128
- package/dist/src/_dedupCheck.js +1 -1
- package/dist/src/client/GGContextKeySynchronizer.d.ts +11 -0
- package/dist/src/client/GGContextKeySynchronizer.d.ts.map +1 -0
- package/dist/src/client/GGContextKeySynchronizer.js +26 -0
- package/dist/src/client/GGContextKeySynchronizer.js.map +1 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts +8 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts.map +1 -1
- package/dist/src/client/GGHttpSchema.createClient.js +2 -1
- package/dist/src/client/GGHttpSchema.createClient.js.map +1 -1
- package/dist/src/index-browser.d.ts +4 -0
- package/dist/src/index-browser.d.ts.map +1 -1
- package/dist/src/index-browser.js +4 -0
- package/dist/src/index-browser.js.map +1 -1
- package/dist/src/index-node.d.ts +9 -1
- package/dist/src/index-node.d.ts.map +1 -1
- package/dist/src/index-node.js +17 -5
- package/dist/src/index-node.js.map +1 -1
- package/dist/src/rpc/GGHttpRouteRPC.d.ts +4 -0
- package/dist/src/rpc/GGHttpRouteRPC.d.ts.map +1 -1
- package/dist/src/rpc/GGHttpRouteRPC.js +6 -0
- package/dist/src/rpc/GGHttpRouteRPC.js.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts +4 -3
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js +9 -2
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts +3 -3
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts.map +1 -1
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js +13 -16
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js.map +1 -1
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts.map +1 -1
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js +4 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -1
- package/dist/src/schema/GGCookie.d.ts +50 -0
- package/dist/src/schema/GGCookie.d.ts.map +1 -0
- package/dist/src/schema/GGCookie.js +130 -0
- package/dist/src/schema/GGCookie.js.map +1 -0
- package/dist/src/schema/GGHeader.d.ts +23 -0
- package/dist/src/schema/GGHeader.d.ts.map +1 -0
- package/dist/src/schema/GGHeader.js +41 -0
- package/dist/src/schema/GGHeader.js.map +1 -0
- package/dist/src/schema/GGHttpPermissionsChecker.d.ts +9 -0
- package/dist/src/schema/GGHttpPermissionsChecker.d.ts.map +1 -0
- package/dist/src/schema/GGHttpPermissionsChecker.js +29 -0
- package/dist/src/schema/GGHttpPermissionsChecker.js.map +1 -0
- package/dist/src/schema/GGHttpSchema.d.ts +14 -52
- package/dist/src/schema/GGHttpSchema.d.ts.map +1 -1
- package/dist/src/schema/GGHttpSchema.js.map +1 -1
- package/dist/src/schema/GGWireContextKey.d.ts +30 -0
- package/dist/src/schema/GGWireContextKey.d.ts.map +1 -0
- package/dist/src/schema/GGWireContextKey.js +36 -0
- package/dist/src/schema/GGWireContextKey.js.map +1 -0
- package/dist/src/schema/GGWireContextKey.node.d.ts +36 -0
- package/dist/src/schema/GGWireContextKey.node.d.ts.map +1 -0
- package/dist/src/schema/GGWireContextKey.node.js +83 -0
- package/dist/src/schema/GGWireContextKey.node.js.map +1 -0
- package/dist/src/schema/httpSchema.d.ts +2 -4
- package/dist/src/schema/httpSchema.d.ts.map +1 -1
- package/dist/src/schema/httpSchema.js +0 -48
- package/dist/src/schema/httpSchema.js.map +1 -1
- package/dist/src/server/GGHttp.d.ts +2 -26
- package/dist/src/server/GGHttp.d.ts.map +1 -1
- package/dist/src/server/GGHttp.js +0 -15
- package/dist/src/server/GGHttp.js.map +1 -1
- package/dist/src/server/GGHttpSchema.startServer.d.ts +2 -11
- package/dist/src/server/GGHttpSchema.startServer.d.ts.map +1 -1
- package/dist/src/server/GGHttpSchema.startServer.js +14 -28
- package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
- package/dist/src/server/GGHttpServer.d.ts +45 -14
- package/dist/src/server/GGHttpServer.d.ts.map +1 -1
- package/dist/src/server/GGHttpServer.js +61 -87
- package/dist/src/server/GGHttpServer.js.map +1 -1
- package/dist/src/server/applyRequestMiddleware.d.ts +11 -0
- package/dist/src/server/applyRequestMiddleware.d.ts.map +1 -0
- package/dist/src/server/applyRequestMiddleware.js +38 -0
- package/dist/src/server/applyRequestMiddleware.js.map +1 -0
- package/dist/src/server/applyResponseMiddleware.d.ts +12 -0
- package/dist/src/server/applyResponseMiddleware.d.ts.map +1 -0
- package/dist/src/server/applyResponseMiddleware.js +25 -0
- package/dist/src/server/applyResponseMiddleware.js.map +1 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.d.ts +39 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.d.ts.map +1 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.js +82 -0
- package/dist/src/server/checkers/checkPermissionsAtStart.js.map +1 -0
- package/dist/src/server/checkers/checkWireConflicts.d.ts +18 -0
- package/dist/src/server/checkers/checkWireConflicts.d.ts.map +1 -0
- package/dist/src/server/checkers/checkWireConflicts.js +43 -0
- package/dist/src/server/checkers/checkWireConflicts.js.map +1 -0
- package/dist/src/server/checkers/checkWiresImplemented.d.ts +11 -0
- package/dist/src/server/checkers/checkWiresImplemented.d.ts.map +1 -0
- package/dist/src/server/checkers/checkWiresImplemented.js +27 -0
- package/dist/src/server/checkers/checkWiresImplemented.js.map +1 -0
- package/dist/src/tsconfig.json +2 -1
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +14 -11
- package/src/_dedupCheck.ts +1 -1
- package/src/client/GGContextKeySynchronizer.test.ts +106 -0
- package/src/client/GGContextKeySynchronizer.ts +42 -0
- package/src/client/GGHttpSchema.createClient.ts +10 -1
- package/src/index-browser.ts +4 -0
- package/src/index-node.ts +18 -5
- package/src/rpc/GGHttpRouteRPC.ts +8 -0
- package/src/rpc/RpcRequest/GGRpcRequestBuilder.ts +11 -4
- package/src/rpc/RpcRequest/GGRpcRequestParser.ts +16 -17
- package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +5 -0
- package/src/schema/GGCookie.ts +151 -0
- package/src/schema/GGHeader.ts +48 -0
- package/src/schema/GGHttpPermissionsChecker.test.ts +125 -0
- package/src/schema/GGHttpPermissionsChecker.ts +35 -0
- package/src/schema/GGHttpSchema.ts +15 -63
- package/src/schema/GGWireContextKey.node.ts +119 -0
- package/src/schema/GGWireContextKey.ts +46 -0
- package/src/schema/httpSchema.ts +4 -57
- package/src/server/GGHttp.ts +3 -34
- package/src/server/GGHttpSchema.startServer.ts +16 -38
- package/src/server/GGHttpServer.ts +90 -97
- package/src/server/applyRequestMiddleware.ts +40 -0
- package/src/server/applyResponseMiddleware.ts +27 -0
- package/src/server/checkers/checkPermissionsAtStart.ts +112 -0
- package/src/server/checkers/checkWireConflicts.ts +65 -0
- package/src/server/checkers/checkWiresImplemented.ts +42 -0
- package/src/tsconfig.json +2 -1
- package/dist/src/server/GG_PERMISSIONS.d.ts +0 -15
- package/dist/src/server/GG_PERMISSIONS.d.ts.map +0 -1
- package/dist/src/server/GG_PERMISSIONS.js +0 -15
- package/dist/src/server/GG_PERMISSIONS.js.map +0 -1
- package/src/server/GG_PERMISSIONS.ts +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grest-ts/http",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "HTTP server and client library for Node.js and browser",
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"typecheck": "tsc --noEmit -p src"
|
|
28
|
+
"typecheck": "tsc --noEmit -p src",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
31
|
+
"test:coverage:html": "vitest run --coverage --reporter=html"
|
|
29
32
|
},
|
|
30
33
|
"publishConfig": {
|
|
31
34
|
"access": "public"
|
|
@@ -58,16 +61,16 @@
|
|
|
58
61
|
"find-my-way": "^9.4.0"
|
|
59
62
|
},
|
|
60
63
|
"peerDependencies": {
|
|
61
|
-
"@grest-ts/common": "0.0.
|
|
62
|
-
"@grest-ts/context": "0.0.
|
|
63
|
-
"@grest-ts/discovery": "0.0.
|
|
64
|
-
"@grest-ts/locator": "0.0.
|
|
65
|
-
"@grest-ts/logger": "0.0.
|
|
66
|
-
"@grest-ts/metrics": "0.0.
|
|
67
|
-
"@grest-ts/schema": "0.0.
|
|
68
|
-
"@grest-ts/trace": "0.0.
|
|
64
|
+
"@grest-ts/common": "0.0.49",
|
|
65
|
+
"@grest-ts/context": "0.0.49",
|
|
66
|
+
"@grest-ts/discovery": "0.0.49",
|
|
67
|
+
"@grest-ts/locator": "0.0.49",
|
|
68
|
+
"@grest-ts/logger": "0.0.49",
|
|
69
|
+
"@grest-ts/metrics": "0.0.49",
|
|
70
|
+
"@grest-ts/schema": "0.0.49",
|
|
71
|
+
"@grest-ts/trace": "0.0.49"
|
|
69
72
|
},
|
|
70
73
|
"devDependencies": {
|
|
71
|
-
"@grest-ts/testkit": "0.0.
|
|
74
|
+
"@grest-ts/testkit": "0.0.49"
|
|
72
75
|
}
|
|
73
76
|
}
|
package/src/_dedupCheck.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const __KEY = Symbol.for("grest-ts:dedup:@grest-ts/http");
|
|
13
13
|
const __slot = globalThis as Record<symbol, unknown>;
|
|
14
14
|
const __existing = __slot[__KEY] as { path: string; version: string } | undefined;
|
|
15
|
-
const __here = { path: import.meta.url, version: "0.0.
|
|
15
|
+
const __here = { path: import.meta.url, version: "0.0.49" };
|
|
16
16
|
// Same path + same version means the same physical package is being
|
|
17
17
|
// re-evaluated by a different ESM runner (e.g. vitest projects share one
|
|
18
18
|
// process but each project has its own module cache). That isn't a true
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {describe, it, expect, vi} from 'vitest'
|
|
2
|
+
import {GGContext} from '@grest-ts/context'
|
|
3
|
+
import {GGContextKey} from '@grest-ts/context'
|
|
4
|
+
import {GGContextKeySynchronizer} from './GGContextKeySynchronizer'
|
|
5
|
+
import {IsString} from '@grest-ts/schema'
|
|
6
|
+
|
|
7
|
+
const KEY_A = new GGContextKey<string | undefined>('sync-test:a', IsString.orUndefined)
|
|
8
|
+
const KEY_B = new GGContextKey<string | undefined>('sync-test:b', IsString.orUndefined)
|
|
9
|
+
|
|
10
|
+
function inContext<T>(fn: () => Promise<T>): Promise<T> {
|
|
11
|
+
return new GGContext('test').run(fn)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('GGContextKeySynchronizer', () => {
|
|
15
|
+
|
|
16
|
+
describe('waitFor — no controller', () => {
|
|
17
|
+
it('resolves immediately when no controller is registered', async () => {
|
|
18
|
+
await inContext(async () => {
|
|
19
|
+
await expect(GGContextKeySynchronizer.waitFor(KEY_A)).resolves.toBeUndefined()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('waitFor — not stale', () => {
|
|
25
|
+
it('does not call recover when isStale returns false', async () => {
|
|
26
|
+
await inContext(async () => {
|
|
27
|
+
const recover = vi.fn().mockResolvedValue(undefined)
|
|
28
|
+
GGContextKeySynchronizer.provide(KEY_A, {isStale: () => false, recover})
|
|
29
|
+
await GGContextKeySynchronizer.waitFor(KEY_A)
|
|
30
|
+
expect(recover).not.toHaveBeenCalled()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('waitFor — stale once', () => {
|
|
36
|
+
it('calls recover once and after it resolves a second waitFor does not call recover again', async () => {
|
|
37
|
+
await inContext(async () => {
|
|
38
|
+
let stale = true
|
|
39
|
+
const recover = vi.fn(async () => { stale = false })
|
|
40
|
+
GGContextKeySynchronizer.provide(KEY_A, {isStale: () => stale, recover})
|
|
41
|
+
|
|
42
|
+
await GGContextKeySynchronizer.waitFor(KEY_A)
|
|
43
|
+
expect(recover).toHaveBeenCalledTimes(1)
|
|
44
|
+
|
|
45
|
+
await GGContextKeySynchronizer.waitFor(KEY_A)
|
|
46
|
+
expect(recover).toHaveBeenCalledTimes(1)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('waitFor — coalescing', () => {
|
|
52
|
+
it('fires recover exactly once for 5 concurrent waitFor calls', async () => {
|
|
53
|
+
await inContext(async () => {
|
|
54
|
+
let resolveRecover!: () => void
|
|
55
|
+
const recoverDone = new Promise<void>(res => { resolveRecover = res })
|
|
56
|
+
const recover = vi.fn(() => recoverDone)
|
|
57
|
+
|
|
58
|
+
GGContextKeySynchronizer.provide(KEY_B, {isStale: () => true, recover})
|
|
59
|
+
|
|
60
|
+
const waits = Promise.all([
|
|
61
|
+
GGContextKeySynchronizer.waitFor(KEY_B),
|
|
62
|
+
GGContextKeySynchronizer.waitFor(KEY_B),
|
|
63
|
+
GGContextKeySynchronizer.waitFor(KEY_B),
|
|
64
|
+
GGContextKeySynchronizer.waitFor(KEY_B),
|
|
65
|
+
GGContextKeySynchronizer.waitFor(KEY_B),
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
resolveRecover()
|
|
69
|
+
await waits
|
|
70
|
+
expect(recover).toHaveBeenCalledTimes(1)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('waitFor — recover rejects', () => {
|
|
76
|
+
it('rejects waitFor when recover rejects, then clears inflight so next call can retry', async () => {
|
|
77
|
+
await inContext(async () => {
|
|
78
|
+
let callCount = 0
|
|
79
|
+
const recover = vi.fn(async () => {
|
|
80
|
+
callCount++
|
|
81
|
+
if (callCount === 1) throw new Error('refresh failed')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
GGContextKeySynchronizer.provide(KEY_A, {isStale: () => true, recover})
|
|
85
|
+
|
|
86
|
+
await expect(GGContextKeySynchronizer.waitFor(KEY_A)).rejects.toThrow('refresh failed')
|
|
87
|
+
expect(recover).toHaveBeenCalledTimes(1)
|
|
88
|
+
|
|
89
|
+
await expect(GGContextKeySynchronizer.waitFor(KEY_A)).resolves.toBeUndefined()
|
|
90
|
+
expect(recover).toHaveBeenCalledTimes(2)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('clear', () => {
|
|
96
|
+
it('removes the controller so subsequent waitFor is a no-op', async () => {
|
|
97
|
+
await inContext(async () => {
|
|
98
|
+
const recover = vi.fn().mockResolvedValue(undefined)
|
|
99
|
+
GGContextKeySynchronizer.provide(KEY_A, {isStale: () => true, recover})
|
|
100
|
+
GGContextKeySynchronizer.clear(KEY_A)
|
|
101
|
+
await GGContextKeySynchronizer.waitFor(KEY_A)
|
|
102
|
+
expect(recover).not.toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {GGContextKey} from "@grest-ts/context"
|
|
2
|
+
import {IsAny, type GGSchema} from "@grest-ts/schema"
|
|
3
|
+
|
|
4
|
+
export interface GGKeyController {
|
|
5
|
+
isStale: () => boolean
|
|
6
|
+
recover: () => Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ControllerEntry {
|
|
10
|
+
controller: GGKeyController
|
|
11
|
+
inflight: Promise<void> | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const GG_SYNC_CONTROLLERS = new GGContextKey<Map<string, ControllerEntry>>(
|
|
15
|
+
"sync:controllers",
|
|
16
|
+
IsAny as unknown as GGSchema<Map<string, ControllerEntry>>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export class GGContextKeySynchronizer {
|
|
20
|
+
|
|
21
|
+
static provide(key: GGContextKey<any>, controller: GGKeyController): void {
|
|
22
|
+
let registry = GG_SYNC_CONTROLLERS.get()
|
|
23
|
+
if (registry === undefined) {
|
|
24
|
+
registry = new Map()
|
|
25
|
+
GG_SYNC_CONTROLLERS.set(registry)
|
|
26
|
+
}
|
|
27
|
+
registry.set(key.name, {controller, inflight: undefined})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async waitFor(key: GGContextKey<any>): Promise<void> {
|
|
31
|
+
const entry = GG_SYNC_CONTROLLERS.get()?.get(key.name)
|
|
32
|
+
if (entry === undefined || !entry.controller.isStale()) return
|
|
33
|
+
entry.inflight ??= entry.controller.recover().finally(() => {
|
|
34
|
+
entry.inflight = undefined
|
|
35
|
+
})
|
|
36
|
+
await entry.inflight
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static clear(key: GGContextKey<any>): void {
|
|
40
|
+
GG_SYNC_CONTROLLERS.get()?.delete(key.name)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -29,6 +29,7 @@ export type GGHttpTransport = (
|
|
|
29
29
|
headers: Record<string, string>
|
|
30
30
|
body: string | FormData | undefined
|
|
31
31
|
signal: AbortSignal
|
|
32
|
+
credentials?: "omit" | "same-origin" | "include"
|
|
32
33
|
}
|
|
33
34
|
) => Promise<Response>
|
|
34
35
|
|
|
@@ -36,6 +37,13 @@ export interface GGHttpClientConfig {
|
|
|
36
37
|
url?: string;
|
|
37
38
|
timeout?: number;
|
|
38
39
|
noValidation?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Cross-origin credentials mode for the default fetch transport. Default
|
|
42
|
+
* (unset → fetch's "same-origin") is unchanged; pass "include" so the browser
|
|
43
|
+
* attaches/stores cross-origin cookies. Ignored by custom transports unless
|
|
44
|
+
* they read it.
|
|
45
|
+
*/
|
|
46
|
+
credentials?: "omit" | "same-origin" | "include"
|
|
39
47
|
/**
|
|
40
48
|
* Override the wire-layer call. Defaults to `fetch`. When provided,
|
|
41
49
|
* service discovery is skipped (the transport is presumed to know how
|
|
@@ -111,7 +119,8 @@ export function createClient<TContract extends GGContractApiDefinition, TContext
|
|
|
111
119
|
method: fetchRequest.method,
|
|
112
120
|
signal: controller.signal,
|
|
113
121
|
headers: fetchRequest.headers,
|
|
114
|
-
body: fetchRequest.body
|
|
122
|
+
body: fetchRequest.body,
|
|
123
|
+
credentials: config.credentials
|
|
115
124
|
}).finally(() => clearTimeout(timeoutId));
|
|
116
125
|
const resData = await wireFormat.parseResponse(wireResponse);
|
|
117
126
|
|
package/src/index-browser.ts
CHANGED
|
@@ -2,11 +2,15 @@ import "./_dedupCheck";
|
|
|
2
2
|
// API Schema
|
|
3
3
|
export * from "./schema/GGHttpSchema";
|
|
4
4
|
export * from "./schema/httpSchema";
|
|
5
|
+
export * from "./schema/GGCookie";
|
|
6
|
+
export * from "./schema/GGWireContextKey";
|
|
7
|
+
export * from "./schema/GGHeader";
|
|
5
8
|
export * from "./rpc/GGHttpRouteRPC";
|
|
6
9
|
export * from "./rpc/RpcRequest/GGRpcRequestBuilder";
|
|
7
10
|
export * from "./rpc/RpcResponse/GGRpcResponseParser";
|
|
8
11
|
|
|
9
12
|
// Client
|
|
13
|
+
export * from "./client/GGContextKeySynchronizer";
|
|
10
14
|
export * from "./client/GGHttpSchema.createClient";
|
|
11
15
|
|
|
12
16
|
// Extensions
|
package/src/index-node.ts
CHANGED
|
@@ -3,21 +3,29 @@ import "./_dedupCheck";
|
|
|
3
3
|
import {_registerRpcServerCodecFactory} from "./rpc/GGHttpRouteRPC";
|
|
4
4
|
import {GGRpcResponseBuilder} from "./rpc/RpcResponse/GGRpcResponseBuilder";
|
|
5
5
|
import {GGRpcRequestParser} from "./rpc/RpcRequest/GGRpcRequestParser";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
|
|
7
|
+
_registerRpcServerCodecFactory((method, path, config) => {
|
|
8
|
+
const parser = new GGRpcRequestParser(method, path, config);
|
|
9
|
+
return {
|
|
10
|
+
readRequest: parser.readRequest,
|
|
11
|
+
validateInput: parser.validateInput,
|
|
12
|
+
sendResponse: new GGRpcResponseBuilder(config).sendResponse
|
|
13
|
+
};
|
|
14
|
+
});
|
|
10
15
|
|
|
11
16
|
// Metrics
|
|
12
17
|
export * from "./server/GGHttpMetrics";
|
|
13
18
|
|
|
14
19
|
// Context
|
|
15
20
|
export * from "./server/GG_HTTP_REQUEST";
|
|
16
|
-
export * from "./server/GG_PERMISSIONS";
|
|
17
21
|
|
|
18
22
|
// API Schema
|
|
19
23
|
export * from "./schema/GGHttpSchema";
|
|
20
24
|
export * from "./schema/httpSchema";
|
|
25
|
+
export * from "./schema/GGCookie";
|
|
26
|
+
export * from "./schema/GGWireContextKey";
|
|
27
|
+
export * from "./schema/GGWireContextKey.node";
|
|
28
|
+
export * from "./schema/GGHeader";
|
|
21
29
|
export * from "./rpc/GGHttpRouteRPC";
|
|
22
30
|
export * from "./rpc/openApiSuccessResponse";
|
|
23
31
|
export * from "./rpc/openApiHelpers";
|
|
@@ -31,8 +39,12 @@ export * from "./server/GGHttp";
|
|
|
31
39
|
export * from "./server/GGHttp";
|
|
32
40
|
export * from "./server/GGHttpServer";
|
|
33
41
|
export * from "./server/GG_HTTP_SERVER";
|
|
42
|
+
export * from "./server/applyResponseMiddleware";
|
|
43
|
+
export * from "./server/applyRequestMiddleware";
|
|
44
|
+
export * from "./schema/GGHttpPermissionsChecker";
|
|
34
45
|
|
|
35
46
|
// Client
|
|
47
|
+
export * from "./client/GGContextKeySynchronizer";
|
|
36
48
|
export * from "./client/GGHttpSchema.createClient";
|
|
37
49
|
export * from "./server/GGHttpSchema.startServer";
|
|
38
50
|
|
|
@@ -40,3 +52,4 @@ export * from "./server/GGHttpSchema.startServer";
|
|
|
40
52
|
// Extensions
|
|
41
53
|
import "./client/GGHttpSchema.createClient";
|
|
42
54
|
import "./server/GGHttpSchema.startServer";
|
|
55
|
+
import "./schema/GGWireContextKey.node";
|
|
@@ -2,6 +2,7 @@ import {HttpMethod} from "@grest-ts/common"
|
|
|
2
2
|
import {ClientHttpRouteToRpcTransformClientCodec, ClientHttpRouteToRpcTransformClientConfig, ClientHttpRouteToRpcTransformServerCodec, ClientHttpRouteToRpcTransformServerConfig, GGHttpCodec, GGHttpCodecOpenApiConfig} from "../schema/GGHttpSchema"
|
|
3
3
|
import {GGRpcRequestBuilder} from "./RpcRequest/GGRpcRequestBuilder";
|
|
4
4
|
import {GGRpcResponseParser} from "./RpcResponse/GGRpcResponseParser";
|
|
5
|
+
import type {GGContextKey} from "@grest-ts/context";
|
|
5
6
|
import type {OpenAPIV3_1} from "openapi-types";
|
|
6
7
|
import {GGSchema} from "@grest-ts/schema";
|
|
7
8
|
import {buildRpcSuccessResponses} from "./openApiSuccessResponse";
|
|
@@ -28,6 +29,7 @@ class GGHttpRpcCodec implements GGHttpCodec {
|
|
|
28
29
|
public readonly path: string
|
|
29
30
|
public readonly deprecated: boolean | undefined
|
|
30
31
|
public readonly responseHeaders: Record<string, GGSchema<string | undefined>> = {}
|
|
32
|
+
public readonly updatesCookies: GGContextKey<string | undefined>[] = []
|
|
31
33
|
|
|
32
34
|
constructor(method: HttpMethod, path: string, deprecated?: boolean) {
|
|
33
35
|
this.method = method
|
|
@@ -35,6 +37,12 @@ class GGHttpRpcCodec implements GGHttpCodec {
|
|
|
35
37
|
this.deprecated = deprecated
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** Permit this route to set/clear the given cookie context key(s). Reading needs no declaration. */
|
|
41
|
+
public updatesCookie(...keys: GGContextKey<string | undefined>[]): this {
|
|
42
|
+
this.updatesCookies.push(...keys)
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
public createForClient(config: ClientHttpRouteToRpcTransformClientConfig): ClientHttpRouteToRpcTransformClientCodec {
|
|
39
47
|
return {
|
|
40
48
|
createRequest: new GGRpcRequestBuilder(this.method, this.path, config).createRequest,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type {HttpMethod} from "@grest-ts/common";
|
|
2
2
|
import {GGContractMethod} from "@grest-ts/schema";
|
|
3
|
-
import {ClientHttpRouteToRpcTransformClientConfig, GGHttpFetchRequest
|
|
3
|
+
import {ClientHttpRouteToRpcTransformClientConfig, GGHttpFetchRequest} from "../../schema/GGHttpSchema";
|
|
4
|
+
import {GGContextKey, type GGTransportMiddleware} from "@grest-ts/context";
|
|
5
|
+
import {GGContextKeySynchronizer} from "../../client/GGContextKeySynchronizer";
|
|
4
6
|
|
|
5
7
|
export class GGRpcRequestBuilder {
|
|
6
8
|
|
|
7
9
|
public readonly contract: GGContractMethod
|
|
8
|
-
public readonly middlewares: readonly
|
|
10
|
+
public readonly middlewares: readonly GGTransportMiddleware[]
|
|
9
11
|
public readonly method: HttpMethod;
|
|
10
12
|
public readonly pathTemplate: string;
|
|
11
13
|
public readonly pathParams: string[];
|
|
@@ -26,7 +28,7 @@ export class GGRpcRequestBuilder {
|
|
|
26
28
|
this.hasBody = method === "POST" || method === "PUT" || method === "PATCH"
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
public createRequest = (data: unknown): GGHttpFetchRequest => {
|
|
31
|
+
public createRequest = async (data: unknown): Promise<GGHttpFetchRequest> => {
|
|
30
32
|
let result: GGHttpFetchRequest;
|
|
31
33
|
if (this.hasBody) {
|
|
32
34
|
result = {
|
|
@@ -43,7 +45,12 @@ export class GGRpcRequestBuilder {
|
|
|
43
45
|
body: undefined
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
this.middlewares
|
|
48
|
+
for (const mw of this.middlewares ?? []) {
|
|
49
|
+
if (mw instanceof GGContextKey) {
|
|
50
|
+
await GGContextKeySynchronizer.waitFor(mw)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.middlewares?.forEach(mw => mw.update?.(result))
|
|
47
54
|
return result
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type http from "http";
|
|
2
2
|
import type {HttpMethod} from "@grest-ts/common";
|
|
3
3
|
import {GGContractExecutor, GGContractMethod, PAYLOAD_TOO_LARGE} from "@grest-ts/schema";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
4
|
+
import type {GGTransportMiddleware} from "@grest-ts/context";
|
|
5
|
+
import {ClientHttpRouteToRpcTransformServerConfig} from "../../schema/GGHttpSchema";
|
|
6
|
+
import {applyRequestMiddleware} from "../../server/applyRequestMiddleware";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
8
9
|
|
|
@@ -12,8 +13,7 @@ export class GGRpcRequestParser {
|
|
|
12
13
|
private readonly pathParams: string[];
|
|
13
14
|
private readonly hasBody: boolean;
|
|
14
15
|
protected readonly contract: GGContractMethod
|
|
15
|
-
private readonly
|
|
16
|
-
private readonly serverMiddlewares: readonly GGHttpServerMiddleware[]
|
|
16
|
+
private readonly middlewares: readonly GGTransportMiddleware[]
|
|
17
17
|
|
|
18
18
|
constructor(
|
|
19
19
|
method: HttpMethod,
|
|
@@ -24,32 +24,31 @@ export class GGRpcRequestParser {
|
|
|
24
24
|
this.pathParams = (pathTemplate.match(/:(\w+)/g) || []).map(m => m.slice(1))
|
|
25
25
|
this.hasBody = method === "POST" || method === "PUT" || method === "PATCH"
|
|
26
26
|
this.contract = config.contract
|
|
27
|
-
this.
|
|
28
|
-
this.serverMiddlewares = config.serverMiddlewares
|
|
27
|
+
this.middlewares = config.middlewares
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
// Resolve identity (run the wire pipeline) and extract the raw, unvalidated input.
|
|
31
|
+
// Validation is deferred to validateInput so the permission gate can run in between.
|
|
32
|
+
public readRequest = async (req: http.IncomingMessage): Promise<unknown> => {
|
|
32
33
|
const url = req.url || '/'
|
|
33
34
|
const qIndex = url.indexOf('?')
|
|
34
35
|
const queryArgs = this.parseQueryString(qIndex === -1 ? '' : url.substring(qIndex + 1))
|
|
35
|
-
|
|
36
|
-
const mwQuery: GGHttpRequest = {headers: req.headers, queryArgs: queryArgs}
|
|
37
|
-
this.apiMiddlewares?.forEach(mw => mw.parseRequest?.(mwQuery))
|
|
38
|
-
}
|
|
39
|
-
for (const mw of this.serverMiddlewares ?? []) await mw.process?.();
|
|
36
|
+
await applyRequestMiddleware(req, queryArgs, this.middlewares)
|
|
40
37
|
|
|
41
|
-
let input: unknown;
|
|
42
38
|
if (this.hasBody) {
|
|
43
|
-
|
|
39
|
+
return await this.parseBody(req);
|
|
44
40
|
} else if (this.pathParams.length > 0) {
|
|
45
|
-
|
|
41
|
+
return {
|
|
46
42
|
...this.extractPathParams(qIndex === -1 ? url : url.substring(0, qIndex)),
|
|
47
43
|
...queryArgs
|
|
48
44
|
};
|
|
49
45
|
} else {
|
|
50
|
-
|
|
46
|
+
return queryArgs;
|
|
51
47
|
}
|
|
52
|
-
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public validateInput = (rawInput: unknown): unknown => {
|
|
51
|
+
return GGContractExecutor.parseInput(this.contract.input, rawInput);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
private parseQueryString(rawQuery: string): Record<string, string | string[]> {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type http from "http";
|
|
2
2
|
import {ANY_ERROR, ERROR, GGContractExecutor, GGContractMethod, GGDebugData, GGErrorData, GGSchema, OK} from "@grest-ts/schema";
|
|
3
|
+
import type {GGTransportMiddleware} from "@grest-ts/context";
|
|
3
4
|
import {ClientHttpRouteToRpcTransformServerConfig} from "../../schema/GGHttpSchema";
|
|
5
|
+
import {applyResponseMiddleware} from "../../server/applyResponseMiddleware";
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
export class GGRpcResponseBuilder {
|
|
7
9
|
|
|
8
10
|
protected readonly contract: GGContractMethod
|
|
11
|
+
private readonly middlewares: readonly GGTransportMiddleware[]
|
|
9
12
|
|
|
10
13
|
constructor(config: ClientHttpRouteToRpcTransformServerConfig) {
|
|
11
14
|
this.contract = config.contract;
|
|
15
|
+
this.middlewares = config.middlewares;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
public sendResponse = async (res: http.ServerResponse, rpcResult: ERROR<string, unknown> | OK<unknown>): Promise<void> => {
|
|
@@ -25,6 +29,7 @@ export class GGRpcResponseBuilder {
|
|
|
25
29
|
json = this.makeError(GGContractExecutor.getResponseSchema(this.contract, rpcResult), rpcResult);
|
|
26
30
|
statusCode = rpcResult.statusCode;
|
|
27
31
|
}
|
|
32
|
+
applyResponseMiddleware(res, this.middlewares);
|
|
28
33
|
res.writeHead(statusCode, {
|
|
29
34
|
'Content-Type': 'application/json',
|
|
30
35
|
'Content-Length': Buffer.byteLength(json)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {GGContextKey, type GGInbound, type GGResponse} from "@grest-ts/context"
|
|
2
|
+
import {type GGSchema, IsAny, IsString, SERVER_ERROR} from "@grest-ts/schema"
|
|
3
|
+
import {GGWireContextKey} from "./GGWireContextKey"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-request set of context-key names the current route is permitted to write a cookie
|
|
7
|
+
* for (declared via GGRpc.*(...).updatesCookie(key)). Populated by the server route
|
|
8
|
+
* handler; read by setCookie() to reject undeclared cookie writes.
|
|
9
|
+
*/
|
|
10
|
+
export const GG_COOKIE_WRITES = new GGContextKey<Set<string>>("cookie:writes", IsAny as unknown as GGSchema<Set<string>>)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Per-request Set-Cookie writes scheduled via setCookie()/clearCookie(), keyed by the
|
|
14
|
+
* context key's name. Flushed onto the response by the matching cookie() binding.
|
|
15
|
+
*/
|
|
16
|
+
const GG_COOKIE_PENDING = new GGContextKey<Map<string, PendingCookie>>("cookie:pending", IsAny as unknown as GGSchema<Map<string, PendingCookie>>)
|
|
17
|
+
|
|
18
|
+
export interface CookieOptions {
|
|
19
|
+
httpOnly?: boolean
|
|
20
|
+
secure?: boolean
|
|
21
|
+
sameSite?: "lax" | "strict" | "none"
|
|
22
|
+
path?: string
|
|
23
|
+
domain?: string
|
|
24
|
+
maxAgeSec?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SAFE_DEFAULTS: CookieOptions = {
|
|
28
|
+
httpOnly: true,
|
|
29
|
+
secure: true,
|
|
30
|
+
sameSite: "lax",
|
|
31
|
+
path: "/"
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
interface PendingCookie {
|
|
35
|
+
value: string | undefined
|
|
36
|
+
options?: CookieOptions
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A cookie that IS its own context key.
|
|
41
|
+
*
|
|
42
|
+
* // ambient — the named cookie's value lands in the wire and persists, no implementation needed.
|
|
43
|
+
* const SESSION_WIRE = new GGCookie("session_id")
|
|
44
|
+
*
|
|
45
|
+
* Reads ONLY the named cookie — never a header — from the inbound Cookie header into the wire.
|
|
46
|
+
* For HTTP that is the request Cookie; for WebSocket the real upgrade Cookie (the runtime fills
|
|
47
|
+
* inbound.cookie from there, never from the spoofable in-band message). The cookie name is the
|
|
48
|
+
* first constructor argument (case-sensitive — unlike a header it is not lowercased). To write
|
|
49
|
+
* the cookie, call GGCookie.setCookie(key, …) from a route that declared .updatesCookie(key);
|
|
50
|
+
* this wire flushes it as Set-Cookie (HTTP only — a WebSocket has no response to set a cookie on).
|
|
51
|
+
*/
|
|
52
|
+
export class GGCookie extends GGWireContextKey {
|
|
53
|
+
|
|
54
|
+
public readonly cookieParams: Record<string, GGSchema<string | undefined>>
|
|
55
|
+
|
|
56
|
+
constructor(name: string) {
|
|
57
|
+
const WIRE_SCHEMA = IsString.orUndefined
|
|
58
|
+
super(name, WIRE_SCHEMA)
|
|
59
|
+
_assertCookieSafe("name", this.name)
|
|
60
|
+
this.cookieParams = {[this.name]: WIRE_SCHEMA}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public parse(inbound: GGInbound): void {
|
|
64
|
+
const value = _readCookie(inbound.cookie, this.name)
|
|
65
|
+
if (value !== undefined) this.set(value)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public respond(res: GGResponse): void {
|
|
69
|
+
const pending = GG_COOKIE_PENDING.get()?.get(this.name)
|
|
70
|
+
if (pending === undefined) return
|
|
71
|
+
const line = _serializeSetCookie(this.name, pending.value, pending.options)
|
|
72
|
+
const existing = res.headers["set-cookie"]
|
|
73
|
+
const arr = existing === undefined ? [] : Array.isArray(existing) ? existing : [existing]
|
|
74
|
+
arr.push(line)
|
|
75
|
+
res.headers["set-cookie"] = arr
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Schedule a Set-Cookie on the response (HTTP only). The value never touches key.get() —
|
|
80
|
+
* inbound reads stay set-once. The route must declare .updatesCookie(key); an undeclared
|
|
81
|
+
* call is invalid usage and throws SERVER_ERROR. The matching cookie() binding serializes
|
|
82
|
+
* the pending write with these options (HttpOnly/Secure/SameSite/Path/Domain/Max-Age).
|
|
83
|
+
*/
|
|
84
|
+
public static setCookie(key: GGContextKey<string | undefined>, value: string | undefined, options?: CookieOptions): void {
|
|
85
|
+
if (options) {
|
|
86
|
+
if (options.maxAgeSec !== undefined && !Number.isFinite(options.maxAgeSec)) {
|
|
87
|
+
throw new Error(`setCookie("${key.name}"): maxAgeSec must be a finite number, got ${options.maxAgeSec}.`)
|
|
88
|
+
}
|
|
89
|
+
if (options.path !== undefined) _assertCookieSafe("path", options.path)
|
|
90
|
+
if (options.domain !== undefined) _assertCookieSafe("domain", options.domain)
|
|
91
|
+
}
|
|
92
|
+
if (!GG_COOKIE_WRITES.get()?.has(key.name)) {
|
|
93
|
+
throw new SERVER_ERROR({debugMessage: `setCookie("${key.name}") was called, but this route did not declare .updatesCookie(<key>). Only routes that declare it may set or clear the cookie.`})
|
|
94
|
+
}
|
|
95
|
+
let pending = GG_COOKIE_PENDING.get()
|
|
96
|
+
if (pending === undefined) {
|
|
97
|
+
pending = new Map()
|
|
98
|
+
GG_COOKIE_PENDING.set(pending)
|
|
99
|
+
}
|
|
100
|
+
pending.set(key.name, {value, options})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Clear the cookie — schedules a Set-Cookie with Max-Age=0. Pass path/domain to match a
|
|
105
|
+
* scoped cookie (deletion only works when path+domain match what was set). Equivalent to
|
|
106
|
+
* setCookie(key, undefined, options), but reads clearer at logout-style call sites.
|
|
107
|
+
*/
|
|
108
|
+
public static clearCookie(key: GGContextKey<string | undefined>, options?: CookieOptions): void {
|
|
109
|
+
this.setCookie(key, undefined, options)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// Reject anything that could break out of the Set-Cookie line / inject attributes.
|
|
116
|
+
function _assertCookieSafe(label: string, value: string): void {
|
|
117
|
+
if (/[\x00-\x1f;]/.test(value)) {
|
|
118
|
+
throw new Error(`Cookie ${label} contains illegal characters (control chars or ';'): ${JSON.stringify(value)}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read a single named cookie out of a raw `Cookie` header value. A malformed
|
|
124
|
+
* percent-encoding (e.g. "sid=%") must not crash — fall back to the raw value.
|
|
125
|
+
*/
|
|
126
|
+
function _readCookie(rawCookieHeader: string | undefined, name: string): string | undefined {
|
|
127
|
+
if (typeof rawCookieHeader !== "string") return undefined
|
|
128
|
+
for (const part of rawCookieHeader.split(";")) {
|
|
129
|
+
const eq = part.indexOf("=")
|
|
130
|
+
if (eq === -1) continue
|
|
131
|
+
if (part.slice(0, eq).trim() === name) {
|
|
132
|
+
const rawValue = part.slice(eq + 1).trim()
|
|
133
|
+
try {
|
|
134
|
+
return decodeURIComponent(rawValue)
|
|
135
|
+
} catch {
|
|
136
|
+
return rawValue
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _serializeSetCookie(name: string, value: string | undefined, options?: CookieOptions): string {
|
|
144
|
+
const o = {...SAFE_DEFAULTS, ...options}
|
|
145
|
+
const secure = o.sameSite === "none" ? true : o.secure
|
|
146
|
+
const sameSite = `${o.sameSite.charAt(0).toUpperCase()}${o.sameSite.slice(1)}`
|
|
147
|
+
const attrs = `; Path=${o.path}${o.domain ? `; Domain=${o.domain}` : ""}; SameSite=${sameSite}${secure ? "; Secure" : ""}${o.httpOnly ? "; HttpOnly" : ""}`
|
|
148
|
+
return value
|
|
149
|
+
? `${name}=${encodeURIComponent(value)}${o.maxAgeSec !== undefined ? `; Max-Age=${Math.trunc(o.maxAgeSec)}` : ""}${attrs}`
|
|
150
|
+
: `${name}=; Max-Age=0${attrs}`
|
|
151
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {type GGInbound, type GGOutbound} from "@grest-ts/context"
|
|
2
|
+
import {type GGSchema, IsString} from "@grest-ts/schema"
|
|
3
|
+
import {GGWireContextKey} from "./GGWireContextKey"
|
|
4
|
+
|
|
5
|
+
const BEARER = "Bearer "
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A request header that IS its own context key.
|
|
10
|
+
*
|
|
11
|
+
* // verified: requires .define() server-side; raw credential is ephemeral.
|
|
12
|
+
* const USER_TOKEN_WIRE = new GGHeader("authorization", {scheme: "bearer"})
|
|
13
|
+
*
|
|
14
|
+
* // ambient: parsed value lands in the wire and persists, no implementation needed.
|
|
15
|
+
* const CLIENT_VERSION_WIRE = new GGHeader("x-client-version")
|
|
16
|
+
*/
|
|
17
|
+
export class GGHeader extends GGWireContextKey {
|
|
18
|
+
|
|
19
|
+
public readonly headers: Record<string, GGSchema<string | undefined>>
|
|
20
|
+
private readonly scheme?: "bearer"
|
|
21
|
+
private readonly wireName: string
|
|
22
|
+
|
|
23
|
+
constructor(name: string, options?: { scheme?: "bearer" }) {
|
|
24
|
+
const WIRE_SCHEMA = IsString.orUndefined
|
|
25
|
+
super(name, WIRE_SCHEMA)
|
|
26
|
+
this.scheme = options?.scheme
|
|
27
|
+
this.wireName = name.toLowerCase()
|
|
28
|
+
this.headers = {[this.wireName]: WIRE_SCHEMA}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public parse(inbound: GGInbound): void {
|
|
32
|
+
const raw = inbound.headers[this.wireName];
|
|
33
|
+
if (raw === undefined) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
const value = this.scheme === "bearer" && raw.startsWith(BEARER) ? raw.slice(BEARER.length) : raw
|
|
37
|
+
if (value !== undefined) {
|
|
38
|
+
this.set(value)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public update(outbound: GGOutbound): void {
|
|
43
|
+
const value = this.get()
|
|
44
|
+
if (value !== undefined) {
|
|
45
|
+
outbound.headers[this.wireName] = (this.scheme === "bearer" ? `${BEARER}${value}` : value)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|