@claxedo/workspace-relay 0.7.0 → 0.10.0
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 +114 -191
- package/dist/auth.d.ts +130 -20
- package/dist/cloudflare.d.ts +26 -43
- package/dist/cors-origins.d.ts +2 -0
- package/dist/directory.d.ts +10 -32
- package/dist/gateway-placement.d.ts +7 -0
- package/dist/host-tunnel-failure.d.ts +7 -0
- package/dist/host-tunnel-forwarding.d.ts +29 -0
- package/dist/http.d.ts +6 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.mjs +1285 -2049
- package/dist/relay-audit.d.ts +4 -0
- package/dist/relay-cors.d.ts +12 -0
- package/dist/relay-location.d.ts +93 -0
- package/dist/relay-target.d.ts +25 -0
- package/dist/resolver-clients.d.ts +116 -0
- package/dist/server.d.ts +51 -188
- package/dist/session-host-forwarding.d.ts +22 -0
- package/dist/test-support/unfenced-host-token.d.ts +6 -0
- package/dist/worker.d.ts +32 -4
- package/dist/workerd-fixture/boot.d.ts +18 -0
- package/package.json +9 -20
- package/dist/bun.d.ts +0 -170
- package/dist/main.d.ts +0 -246
- package/dist/main.mjs +0 -23219
- package/dist/synthetic.d.ts +0 -60
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WorkspaceRelayAuditEvent, WorkspaceRelayOptions } from "./server";
|
|
2
|
+
/** Stops the flush timer for `options` and emits any suppressed count it still holds. */
|
|
3
|
+
export declare function disposeAuditSampler(options: WorkspaceRelayOptions): void;
|
|
4
|
+
export declare function audit(options: WorkspaceRelayOptions, event: WorkspaceRelayAuditEvent): Promise<void>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function setWorkspaceRelayAppOrigins(raw: string | undefined): void;
|
|
2
|
+
export declare function setWorkspaceRelayAllowedOrigins(raw: string | undefined): void;
|
|
3
|
+
/**
|
|
4
|
+
* The browser-origin gate on workspace WebSocket upgrades, applied after token
|
|
5
|
+
* authorization and before the upgrade is granted. A request carrying NO
|
|
6
|
+
* `Origin` header is a non-browser client — browsers always send Origin on
|
|
7
|
+
* WebSocket upgrades — so there is no origin verdict to enforce and it is
|
|
8
|
+
* admitted.
|
|
9
|
+
*/
|
|
10
|
+
export declare function workspaceUpgradeOriginDenied(request: Request): Response | null;
|
|
11
|
+
export declare function corsPreflight(request: Request): Response;
|
|
12
|
+
export declare function withRelayResponseHeaders(request: Request, response: Response): Response;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export declare const DEFAULT_RELAY_LOCATION_HINT = "apac";
|
|
2
|
+
/**
|
|
3
|
+
* Where a session's Durable Object lives, and so where the relay room of a
|
|
4
|
+
* workspace whose sessions run on their objects is created.
|
|
5
|
+
*
|
|
6
|
+
* A Durable Object is created near whoever first reaches it and never moves,
|
|
7
|
+
* so without a hint a session opened from India can land in HKG, where
|
|
8
|
+
* api.anthropic.com answers every request 403 `forbidden` "Request not
|
|
9
|
+
* allowed" whatever the credential. Every turn the object calls its model
|
|
10
|
+
* vendor and the control plane (D1 in ENAM, the API Worker pinned to
|
|
11
|
+
* aws:us-east-1), so it lives beside them rather than beside its first viewer.
|
|
12
|
+
*
|
|
13
|
+
* The room sits on the path of every request to the object and asks the
|
|
14
|
+
* control plane (IAD) for the token's revocation and the target on a cold
|
|
15
|
+
* start and every 10 s: from apac each lookup is ~220 ms, from enam ~10 ms,
|
|
16
|
+
* while the browser's path through the room is the same ~115 ms either way
|
|
17
|
+
* (docs/perf/pi-durable-latency-2026-10-18/README.md). A machine's room stays
|
|
18
|
+
* beside its machine, where those lookups would cost a browser in apac
|
|
19
|
+
* +220 ms a request.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SESSION_HOST_LOCATION_HINT = "enam";
|
|
22
|
+
/**
|
|
23
|
+
* The complete set of Cloudflare Durable Object location hints.
|
|
24
|
+
*
|
|
25
|
+
* Cloudflare rejects an unknown hint, and a rejected `get()` fails the request —
|
|
26
|
+
* so an unrecognised region must fall back rather than be passed through. This
|
|
27
|
+
* list is the validator for that.
|
|
28
|
+
*/
|
|
29
|
+
export declare const RELAY_LOCATION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
|
|
30
|
+
export type RelayLocationHint = typeof RELAY_LOCATION_HINTS[number];
|
|
31
|
+
/** A workspace region carried on the request, if the caller supplied one. */
|
|
32
|
+
export declare const RELAY_REGION_HEADER = "x-claxedo-workspace-region";
|
|
33
|
+
/**
|
|
34
|
+
* Chooses the location hint for a workspace's Durable Object.
|
|
35
|
+
*
|
|
36
|
+
* This decision is PERMANENT: a DO's location is fixed at first creation and
|
|
37
|
+
* never migrates, so a workspace created with the wrong hint pays the extra
|
|
38
|
+
* ocean crossing on every frame for its entire life. That is why an
|
|
39
|
+
* unrecognised value falls back to the configured default instead of being
|
|
40
|
+
* passed through or guessed at.
|
|
41
|
+
*
|
|
42
|
+
* Precedence, most authoritative first:
|
|
43
|
+
* 1. the workspace's own region (header or `?region=`), which is what the
|
|
44
|
+
* control plane knows and the only signal tied to the WORKSPACE rather than
|
|
45
|
+
* to whoever happens to be connecting first;
|
|
46
|
+
* 2. the requesting user's Cloudflare edge country, a decent proxy when the
|
|
47
|
+
* workspace has no region yet;
|
|
48
|
+
* 3. the deployment-wide configured hint (`CLAXEDO_RELAY_LOCATION_HINT`), then
|
|
49
|
+
* `DEFAULT_RELAY_LOCATION_HINT`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function relayLocationHint(input: {
|
|
52
|
+
configured?: string;
|
|
53
|
+
region?: string;
|
|
54
|
+
country?: string;
|
|
55
|
+
}): string;
|
|
56
|
+
/** The workspace region a request declares, if any. */
|
|
57
|
+
export declare function relayRequestRegion(request: Request): string | undefined;
|
|
58
|
+
export type RelayRoomPlacement = {
|
|
59
|
+
locationHint: string;
|
|
60
|
+
/** The request's URL and headers with the placement inputs removed. */
|
|
61
|
+
url: string;
|
|
62
|
+
headers: Headers;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* What the request's own credential says once its signature verifies for the
|
|
66
|
+
* room's workspace: the host it reaches. A `session-do:` host is the control
|
|
67
|
+
* plane's signed statement that the workspace's sessions run on their own
|
|
68
|
+
* Durable Objects.
|
|
69
|
+
*/
|
|
70
|
+
export type RelayPlacementCredential = {
|
|
71
|
+
hostId: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Where a workspace's room is created, decided from what the request declares
|
|
75
|
+
* and what its credential proves, and the request as the room should see it.
|
|
76
|
+
* The declaration is consumed here because the room forwards a request's
|
|
77
|
+
* remaining query and headers to the runtime, whose routes have no use for the
|
|
78
|
+
* relay's placement input.
|
|
79
|
+
*
|
|
80
|
+
* A credential naming a session's Durable Object places the room beside that
|
|
81
|
+
* object (`SESSION_HOST_LOCATION_HINT`), whatever the request declares: the
|
|
82
|
+
* host id is signed by the control plane, so no query parameter or header can
|
|
83
|
+
* drag the room elsewhere, and a forged token verifies for no workspace.
|
|
84
|
+
* Request-declared inputs (region, edge country) move the hint off the
|
|
85
|
+
* configured floor only when the credential verifies, so anonymous first-touch
|
|
86
|
+
* traffic cannot pin a workspace's room placement.
|
|
87
|
+
*/
|
|
88
|
+
export declare function relayRoomPlacement(request: Request, input: {
|
|
89
|
+
configured?: string;
|
|
90
|
+
credential: () => Promise<RelayPlacementCredential | undefined>;
|
|
91
|
+
}): Promise<RelayRoomPlacement>;
|
|
92
|
+
/** The Cloudflare edge country for the request, when running on Cloudflare. */
|
|
93
|
+
export declare function relayRequestCountry(request: Request): string | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RelayBacking } from "./auth";
|
|
2
|
+
export type WorkspaceRelayTarget = {
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
hostId: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
backing: RelayBacking;
|
|
7
|
+
};
|
|
8
|
+
export declare function parseWorkspaceRelayTarget(input: unknown): WorkspaceRelayTarget | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a resolved target's `baseUrl` names a destination the relay may
|
|
11
|
+
* forward to.
|
|
12
|
+
*
|
|
13
|
+
* A `cloud-vm` target is fetched over the network with the Relay Host Token
|
|
14
|
+
* attached, so it must be HTTPS — or HTTP to a loopback host, the only
|
|
15
|
+
* plaintext transport that cannot put the token on a network. A
|
|
16
|
+
* `local-worktree` target is reached over the host tunnel and its `baseUrl` is
|
|
17
|
+
* never fetched — the control plane sends the empty string — so only that
|
|
18
|
+
* placeholder or a well-formed HTTP(S) URL is admitted. A `durable-object`
|
|
19
|
+
* target is reached through the relay's own Durable Object binding, so it
|
|
20
|
+
* names no destination at all.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isAllowedRelayTargetBaseUrl(target: {
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
backing: RelayBacking;
|
|
25
|
+
}): boolean;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { WorkspaceRelayTarget } from "./relay-target";
|
|
2
|
+
export type RuntimeAccessTokenActiveResult = {
|
|
3
|
+
active: true;
|
|
4
|
+
} | {
|
|
5
|
+
active: false;
|
|
6
|
+
code: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function parseRuntimeAccessTokenActiveResult(input: unknown): RuntimeAccessTokenActiveResult | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* `GET /internal/relay/host-generation?enrollmentId=` as the control plane
|
|
12
|
+
* answers it: the enrollment's current serving generation, or 404 for an
|
|
13
|
+
* enrollment it does not know (`undefined` here).
|
|
14
|
+
*/
|
|
15
|
+
export type HostGenerationResult = {
|
|
16
|
+
enrollmentId: string;
|
|
17
|
+
generation: number;
|
|
18
|
+
revoked: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type HostGenerationResolverLookupOptions = {
|
|
21
|
+
headers: Record<string, string>;
|
|
22
|
+
fetch?: (url: URL, init: RequestInit) => Promise<Response>;
|
|
23
|
+
/** Deadline per lookup; past it the lookup rejects, which the relay grades as unavailable. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function createHostGenerationResolverLookup(url: string, options: HostGenerationResolverLookupOptions): HostGenerationLookup;
|
|
27
|
+
export type RevocationLookupArgs = {
|
|
28
|
+
jti: string;
|
|
29
|
+
workspaceId: string;
|
|
30
|
+
hostId: string;
|
|
31
|
+
};
|
|
32
|
+
export type RevocationLookup = (args: RevocationLookupArgs) => Promise<RuntimeAccessTokenActiveResult>;
|
|
33
|
+
export type CachedRevocationOptions = {
|
|
34
|
+
/** TTL in milliseconds for cached revocation responses. Defaults to 10_000. */
|
|
35
|
+
ttlMs?: number;
|
|
36
|
+
/** Clock injection for tests. Defaults to `Date.now`. */
|
|
37
|
+
now?: () => number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* `generation` is the value the caller holds (a token's, a socket's). The
|
|
41
|
+
* cached client uses it to decide whether a cached answer may stand in for a
|
|
42
|
+
* fresh one; an uncached lookup ignores it. Resolves `undefined` for an
|
|
43
|
+
* unknown enrollment and THROWS when the control plane cannot be reached —
|
|
44
|
+
* the throw is the "unavailable" signal every consumer grades separately.
|
|
45
|
+
*/
|
|
46
|
+
export type HostGenerationLookupArgs = {
|
|
47
|
+
enrollmentId: string;
|
|
48
|
+
generation: number;
|
|
49
|
+
};
|
|
50
|
+
export type HostGenerationLookup = (args: HostGenerationLookupArgs) => Promise<HostGenerationResult | undefined>;
|
|
51
|
+
export type CachedHostGenerationOptions = {
|
|
52
|
+
/** TTL in milliseconds for cached host-generation answers. Defaults to 10_000. */
|
|
53
|
+
ttlMs?: number;
|
|
54
|
+
/** Clock injection for tests. Defaults to `Date.now`. */
|
|
55
|
+
now?: () => number;
|
|
56
|
+
};
|
|
57
|
+
export type TargetLookupArgs = {
|
|
58
|
+
workspaceId: string;
|
|
59
|
+
hostId: string;
|
|
60
|
+
routingId?: string;
|
|
61
|
+
};
|
|
62
|
+
export type TargetLookup = (args: TargetLookupArgs) => Promise<WorkspaceRelayTarget | undefined>;
|
|
63
|
+
export declare function pruneExpiringCache<T extends {
|
|
64
|
+
expiresAt: number;
|
|
65
|
+
}>(cache: Map<string, T>, now: number, maxEntries: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Wraps a revocation lookup with a small in-memory cache keyed by `jti`.
|
|
68
|
+
* Both positive and negative results are cached for the full TTL; revocation
|
|
69
|
+
* lag is bounded by the same TTL by design. Concurrent misses share one lookup.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createCachedRevocationClient(inner: RevocationLookup, options?: CachedRevocationOptions): RevocationLookup;
|
|
72
|
+
/**
|
|
73
|
+
* The longest a revoked Runtime Access Token can keep working after the
|
|
74
|
+
* authority records the revocation, derived from the configured TTLs rather
|
|
75
|
+
* than measured timing.
|
|
76
|
+
*
|
|
77
|
+
* Every relayed HTTP request re-runs the active check, so a cached `active`
|
|
78
|
+
* answer delays denial by at most `revocationCacheTtlMs`. An established
|
|
79
|
+
* socket adds at most one re-check interval — its watcher can tick just
|
|
80
|
+
* before the stale entry expires — so pass the room's
|
|
81
|
+
* `runtimeAccessTokenActiveCheckIntervalMs` as `activeCheckIntervalMs`;
|
|
82
|
+
* omit it for the per-request path. A non-positive interval means the socket
|
|
83
|
+
* has no revocation watcher at all and the function reports no bound: the
|
|
84
|
+
* token's `exp`, enforced locally by the socket expiry timer, is then the
|
|
85
|
+
* only deadline, and it caps every path even while the revocation authority
|
|
86
|
+
* is unreachable.
|
|
87
|
+
*/
|
|
88
|
+
export declare function runtimeAccessTokenRevocationDelayMs(input?: {
|
|
89
|
+
revocationCacheTtlMs?: number;
|
|
90
|
+
activeCheckIntervalMs?: number;
|
|
91
|
+
}): number;
|
|
92
|
+
/**
|
|
93
|
+
* Caches host-generation answers by enrollment id with the fence's own rules:
|
|
94
|
+
* a cached answer stands only while it is at least the caller's generation
|
|
95
|
+
* (equal admits; higher is a refusal that needs no fresh read), and a caller
|
|
96
|
+
* holding a HIGHER generation than the cache — or than the answer an in-flight
|
|
97
|
+
* lookup settles on — forces a refresh so a fresh `acquire` is never refused
|
|
98
|
+
* on a stale answer. Unknown enrollments and failures are not cached;
|
|
99
|
+
* concurrent misses share one lookup.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createCachedHostGenerationClient(inner: HostGenerationLookup, options?: CachedHostGenerationOptions): HostGenerationLookup;
|
|
102
|
+
export type CachedTargetOptions = {
|
|
103
|
+
ttlMs?: number;
|
|
104
|
+
durableObjectTtlMs?: number;
|
|
105
|
+
now?: () => number;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Wraps the target lookup with a small in-memory cache keyed by workspace,
|
|
109
|
+
* host and routing id. Only a resolved target is cached: a workspace whose
|
|
110
|
+
* sandbox is still starting answers nothing, and must be seen the moment it
|
|
111
|
+
* is ready. Concurrent misses share one lookup. A cloud or host-tunnel target
|
|
112
|
+
* that stopped keeps answering for at most `ttlMs`, the same lag the
|
|
113
|
+
* revocation cache allows; a session's own Durable Object for
|
|
114
|
+
* `durableObjectTtlMs`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function createCachedTargetClient(inner: TargetLookup, options?: CachedTargetOptions): TargetLookup;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,59 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
import { type RelayClaimPair, type RelayJwtAlgorithm, type RelayKey, type RelayRole, type RuntimeAccessTokenClaims } from "./auth";
|
|
1
|
+
import { type RuntimeAccessVerifierClaims, type TokenVerifier } from "@claxedo/workspace-relay-protocol";
|
|
2
|
+
import { type HostTunnelTokenClaims, type RelayJwtAlgorithm, type RelayKey, type RelayRole, type RuntimeAccessTokenClaims } from "./auth";
|
|
4
3
|
import type { WorkspaceRelayDirectory } from "./directory";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
export type WorkspaceRelayTarget = {
|
|
10
|
-
workspaceId: string;
|
|
11
|
-
hostId: string;
|
|
12
|
-
baseUrl: string;
|
|
13
|
-
upstreamHeaders?: Record<string, string>;
|
|
14
|
-
} & RelayClaimPair;
|
|
15
|
-
export type RuntimeAccessTokenActiveResult = {
|
|
16
|
-
active: true;
|
|
17
|
-
} | {
|
|
18
|
-
active: false;
|
|
19
|
-
code: string;
|
|
20
|
-
reason: string;
|
|
21
|
-
};
|
|
22
|
-
export type RevocationLookupArgs = {
|
|
23
|
-
jti: string;
|
|
24
|
-
workspaceId: string;
|
|
25
|
-
hostId: string;
|
|
26
|
-
};
|
|
27
|
-
export type RevocationLookup = (args: RevocationLookupArgs) => Promise<RuntimeAccessTokenActiveResult>;
|
|
28
|
-
export type CachedRevocationOptions = {
|
|
29
|
-
/** TTL in milliseconds for cached revocation responses. Defaults to 10_000. */
|
|
30
|
-
ttlMs?: number;
|
|
31
|
-
/** Clock injection for tests. Defaults to `Date.now`. */
|
|
32
|
-
now?: () => number;
|
|
33
|
-
};
|
|
34
|
-
export type TargetLookupArgs = {
|
|
35
|
-
workspaceId: string;
|
|
36
|
-
hostId: string;
|
|
37
|
-
};
|
|
38
|
-
export type TargetLookup = (args: TargetLookupArgs) => Promise<WorkspaceRelayTarget | undefined>;
|
|
39
|
-
export type CachedTargetOptions = {
|
|
40
|
-
/** TTL in milliseconds for positive target responses. Defaults to 5_000. */
|
|
41
|
-
ttlMs?: number;
|
|
42
|
-
/** Clock injection for tests. Defaults to `Date.now`. */
|
|
43
|
-
now?: () => number;
|
|
44
|
-
};
|
|
4
|
+
import { type WorkspaceRelayTarget } from "./relay-target";
|
|
5
|
+
import type { HostGenerationLookup, RuntimeAccessTokenActiveResult } from "./resolver-clients";
|
|
6
|
+
export declare function hostTunnelIncumbentOutranks(incumbentGeneration: number, candidateGeneration: number): boolean;
|
|
45
7
|
export type WorkspaceRelayAuditEvent = {
|
|
46
|
-
action: "relay.request.accepted" | "relay.request.denied" | "relay.request.suppressed_summary"
|
|
8
|
+
action: "relay.request.accepted" | "relay.request.denied" | "relay.request.suppressed_summary";
|
|
47
9
|
result: "allow" | "deny";
|
|
48
10
|
reason?: string;
|
|
49
|
-
|
|
11
|
+
actorId?: string;
|
|
12
|
+
actorKind?: "human" | "agent";
|
|
13
|
+
principalKind?: "user" | "service";
|
|
50
14
|
role?: RelayRole;
|
|
51
15
|
workspaceId?: string;
|
|
52
16
|
hostId?: string;
|
|
53
17
|
method: string;
|
|
54
18
|
path: string;
|
|
55
19
|
/**
|
|
56
|
-
*
|
|
20
|
+
* Only set on `relay.request.suppressed_summary` events. Counts the
|
|
57
21
|
* number of `relay.request.accepted` audit events that were dropped by the
|
|
58
22
|
* sampler in the previous flush interval.
|
|
59
23
|
*/
|
|
@@ -61,6 +25,11 @@ export type WorkspaceRelayAuditEvent = {
|
|
|
61
25
|
};
|
|
62
26
|
export type WorkspaceRelayAuthOptions = {
|
|
63
27
|
runtimeAccessKey: RelayKey;
|
|
28
|
+
/**
|
|
29
|
+
* Serving-generation fence for host tunnels. A relay composed without it
|
|
30
|
+
* refuses every host tunnel `host_generation_unverifiable`.
|
|
31
|
+
*/
|
|
32
|
+
resolveHostGeneration?: HostGenerationLookup;
|
|
64
33
|
/**
|
|
65
34
|
* Cache signature/introspection-verified Runtime Access Token claims by the
|
|
66
35
|
* full token string. Revocation/active checks, role enforcement, and target
|
|
@@ -79,7 +48,7 @@ export type WorkspaceRelayAuthOptions = {
|
|
|
79
48
|
* raw JWT codepath.
|
|
80
49
|
*
|
|
81
50
|
* This is the seam downstream users plug their own auth backend
|
|
82
|
-
* through (
|
|
51
|
+
* through (issuer-signed tokens, OIDC introspection, custom keys, etc.).
|
|
83
52
|
* See `@claxedo/workspace-relay-protocol/token-verifier` for the
|
|
84
53
|
* `HttpTokenVerifier` and `StaticTokenVerifier` reference impls.
|
|
85
54
|
*/
|
|
@@ -89,19 +58,11 @@ export type WorkspaceRelayAuthOptions = {
|
|
|
89
58
|
export type WorkspaceRelayHostTokenOptions = {
|
|
90
59
|
relayHostSigningKey: CryptoKey | Uint8Array;
|
|
91
60
|
relayHostAlgorithm: RelayJwtAlgorithm;
|
|
92
|
-
/**
|
|
93
|
-
* Public keys to publish at `/.well-known/jwks.json`. The first entry is the
|
|
94
|
-
* "current" mint key; any further entries are "next" keys (for rolling
|
|
95
|
-
* rotation). Verifiers (workspace-runtime → workspace-host-service) consume
|
|
96
|
-
* this set via `jose.createRemoteJWKSet`. Omit to disable the endpoint
|
|
97
|
-
* (during rollout / for tests that do not need JWKS).
|
|
98
|
-
*/
|
|
99
|
-
relayHostPublicKeys?: RelayHostPublicKey[];
|
|
100
61
|
/**
|
|
101
62
|
* `kid` to embed in the protected header of freshly minted RHTs. When
|
|
102
63
|
* provided, verifiers can dispatch on `kid` to pick the matching key from
|
|
103
|
-
* the published JWKS
|
|
104
|
-
*
|
|
64
|
+
* the published JWKS, so it must be the `kid` of the current key the Worker
|
|
65
|
+
* publishes at `/.well-known/jwks.json`.
|
|
105
66
|
*/
|
|
106
67
|
relayHostMintKid?: string;
|
|
107
68
|
/**
|
|
@@ -117,103 +78,31 @@ export type WorkspaceRelayRoutingOptions = {
|
|
|
117
78
|
fetch?: typeof fetch;
|
|
118
79
|
forwardTimeoutMs?: number;
|
|
119
80
|
directory?: WorkspaceRelayDirectory;
|
|
120
|
-
allowedOrigins?: string[];
|
|
121
|
-
};
|
|
122
|
-
export type WorkspaceRelayDrainOptions = {
|
|
123
|
-
/**
|
|
124
|
-
* T9: when this returns true the relay reports unhealthy via `/health` and
|
|
125
|
-
* fast-paths every workspace request to a 503 (`relay_draining`) before
|
|
126
|
-
* doing any auth or upstream work. Wired by `createWorkspaceRelayBun` to
|
|
127
|
-
* the drain controller it exposes.
|
|
128
|
-
*/
|
|
129
|
-
isDraining?: () => boolean;
|
|
130
81
|
};
|
|
131
82
|
export type WorkspaceRelayTelemetryOptions = {
|
|
132
83
|
audit?: (event: WorkspaceRelayAuditEvent) => void | Promise<void>;
|
|
133
84
|
/**
|
|
134
|
-
*
|
|
135
|
-
* events are emitted to the user `audit` callback. Denies
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
85
|
+
* Probability (0.0 – 1.0) at which `relay.request.accepted` audit
|
|
86
|
+
* events are emitted to the user `audit` callback. Denies are never
|
|
87
|
+
* sampled. When unset, defaults to 1.0 (emit every accept). Suppressed
|
|
88
|
+
* accepts are aggregated into a periodic `relay.request.suppressed_summary`
|
|
89
|
+
* event (see `auditFlushIntervalMs`).
|
|
139
90
|
*/
|
|
140
91
|
auditAcceptSampleRate?: number;
|
|
141
92
|
/**
|
|
142
|
-
*
|
|
93
|
+
* How often (ms) the sampler emits a `relay.request.suppressed_summary`
|
|
143
94
|
* audit event with the count of suppressed accepts in the past interval.
|
|
144
95
|
* Defaults to 60_000 (one minute). When sample rate is 1.0 no interval is
|
|
145
96
|
* armed (nothing to flush). Exposed for tests.
|
|
146
97
|
*/
|
|
147
98
|
auditFlushIntervalMs?: number;
|
|
148
99
|
/**
|
|
149
|
-
*
|
|
150
|
-
* for deterministic tests.
|
|
100
|
+
* Random source for audit and trace sampling. Defaults to `Math.random`.
|
|
101
|
+
* Override for deterministic tests.
|
|
151
102
|
*/
|
|
152
103
|
random?: () => number;
|
|
153
104
|
};
|
|
154
|
-
export type
|
|
155
|
-
/**
|
|
156
|
-
* T31: bearer token required to access `GET /metrics` from non-loopback
|
|
157
|
-
* remotes. When unset, the endpoint allows loopback callers only (using
|
|
158
|
-
* `metricsRemoteAddress` to identify the caller). When set, every request
|
|
159
|
-
* must present `Authorization: Bearer <metricsToken>` regardless of origin.
|
|
160
|
-
* Wired from `CLAXEDO_RELAY_METRICS_TOKEN` in main.ts.
|
|
161
|
-
*/
|
|
162
|
-
metricsToken?: string;
|
|
163
|
-
/**
|
|
164
|
-
* T31: returns the remote IP of the inbound `/metrics` request (used to
|
|
165
|
-
* gate access to the loopback when `metricsToken` is unset). When this is
|
|
166
|
-
* also unset, `/metrics` fails closed. The Bun adapter wires this resolver
|
|
167
|
-
* through `Bun.Server.requestIP`; other callers should set either this or
|
|
168
|
-
* `metricsToken`.
|
|
169
|
-
*/
|
|
170
|
-
metricsRemoteAddress?: (request: Request) => string | undefined;
|
|
171
|
-
/**
|
|
172
|
-
* T31: optional providers used to assemble the `/metrics` response body. The
|
|
173
|
-
* fragmentation/slow-consumer counters live in the bun adapter's per-handler
|
|
174
|
-
* telemetry, and the in-flight pending count lives on the drain controller.
|
|
175
|
-
* `createWorkspaceRelayBun` wires these to make ops counters visible on
|
|
176
|
-
* `/metrics` without giving `server.ts` a hard dependency on bun-only state.
|
|
177
|
-
*/
|
|
178
|
-
metricsSources?: WorkspaceRelayMetricsSources;
|
|
179
|
-
};
|
|
180
|
-
export type WorkspaceRelayOptions = WorkspaceRelayAuthOptions & WorkspaceRelayHostTokenOptions & WorkspaceRelayRoutingOptions & WorkspaceRelayDrainOptions & WorkspaceRelayTelemetryOptions & WorkspaceRelayMetricsOptions;
|
|
181
|
-
export type WorkspaceRelayMetricsSources = {
|
|
182
|
-
fragmentation?: () => {
|
|
183
|
-
fragmentsBuffered: number;
|
|
184
|
-
oversizedClosed: number;
|
|
185
|
-
};
|
|
186
|
-
slowConsumer?: () => {
|
|
187
|
-
overflowEvents: number;
|
|
188
|
-
timerFired: number;
|
|
189
|
-
droppedRequests: number;
|
|
190
|
-
};
|
|
191
|
-
drainPending?: () => number;
|
|
192
|
-
};
|
|
193
|
-
/**
|
|
194
|
-
* T31: shape of the JSON body returned by `GET /metrics`. Exported so the
|
|
195
|
-
* bun adapter and ops dashboards can type-narrow against it.
|
|
196
|
-
*/
|
|
197
|
-
export type WorkspaceRelayMetrics = {
|
|
198
|
-
fragmentation: {
|
|
199
|
-
fragmentsBuffered: number;
|
|
200
|
-
oversizedClosed: number;
|
|
201
|
-
};
|
|
202
|
-
slowConsumer: {
|
|
203
|
-
overflowEvents: number;
|
|
204
|
-
timerFired: number;
|
|
205
|
-
droppedRequests: number;
|
|
206
|
-
};
|
|
207
|
-
directory: {
|
|
208
|
-
activeHostCount: number;
|
|
209
|
-
};
|
|
210
|
-
audit: {
|
|
211
|
-
suppressedAccepts: number;
|
|
212
|
-
};
|
|
213
|
-
drain?: {
|
|
214
|
-
pendingCount: number;
|
|
215
|
-
};
|
|
216
|
-
};
|
|
105
|
+
export type WorkspaceRelayOptions = WorkspaceRelayAuthOptions & WorkspaceRelayHostTokenOptions & WorkspaceRelayRoutingOptions & WorkspaceRelayTelemetryOptions;
|
|
217
106
|
export type AuthorizedWorkspaceRelayRequest = {
|
|
218
107
|
claims: RuntimeAccessTokenClaims;
|
|
219
108
|
target: WorkspaceRelayTarget;
|
|
@@ -223,37 +112,35 @@ export type AuthorizedWorkspaceRelayRequest = {
|
|
|
223
112
|
export type WorkspaceRelayAuthorizeTrace = {
|
|
224
113
|
span<T>(name: string, run: () => Promise<T>): Promise<T>;
|
|
225
114
|
};
|
|
226
|
-
export
|
|
227
|
-
name: string;
|
|
228
|
-
ms: number;
|
|
229
|
-
};
|
|
230
|
-
export type WorkspaceRelayTrace = WorkspaceRelayAuthorizeTrace & {
|
|
231
|
-
phases: WorkspaceRelayTracePhase[];
|
|
232
|
-
};
|
|
115
|
+
export declare const RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 30000;
|
|
233
116
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
117
|
+
* `retryable: false` means the control plane refused this generation, so the
|
|
118
|
+
* host must not simply reconnect with it; `retryable: true` means the control
|
|
119
|
+
* plane could not be asked, and a reconnect is the recovery.
|
|
237
120
|
*/
|
|
238
|
-
export
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
121
|
+
export type HostTunnelGenerationDecision = {
|
|
122
|
+
ok: true;
|
|
123
|
+
} | {
|
|
124
|
+
ok: false;
|
|
125
|
+
retryable: false;
|
|
126
|
+
code: "host_generation_superseded" | "host_generation_unknown" | "host_generation_unverifiable" | "host_enrollment_revoked" | "host_enrollment_unknown";
|
|
127
|
+
reason: string;
|
|
128
|
+
} | {
|
|
129
|
+
ok: false;
|
|
130
|
+
retryable: true;
|
|
131
|
+
code: "host_generation_lookup_unavailable";
|
|
132
|
+
reason: string;
|
|
133
|
+
};
|
|
134
|
+
export declare function checkHostTunnelGeneration(lookup: HostGenerationLookup | undefined, claims: Pick<HostTunnelTokenClaims, "enrollment_id" | "generation">): Promise<HostTunnelGenerationDecision>;
|
|
245
135
|
export type WorkspaceRelayForwardHeadersOptions = {
|
|
246
136
|
/**
|
|
247
|
-
* When true, also strip the `Cookie` request header.
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* may legitimately be needed (e.g. workspace dashboards), so the default
|
|
253
|
-
* is to NOT strip cookies. T26.
|
|
137
|
+
* When true, also strip the `Cookie` request header. A host tunnel ends on a
|
|
138
|
+
* machine somebody uses, whose cookie jar the browser may share (localhost
|
|
139
|
+
* dev tooling); passing cookies verbatim risks leaking session data to it. A
|
|
140
|
+
* cloud-vm sits behind a dedicated network boundary where a session cookie
|
|
141
|
+
* may legitimately be needed, so the default is to not strip cookies.
|
|
254
142
|
*/
|
|
255
|
-
|
|
256
|
-
upstreamHeaders?: Record<string, string>;
|
|
143
|
+
hostTunnel?: boolean;
|
|
257
144
|
};
|
|
258
145
|
export type WorkspaceRelayForwardRequestInitOptions = WorkspaceRelayForwardHeadersOptions & {
|
|
259
146
|
signal?: AbortSignal;
|
|
@@ -261,14 +148,6 @@ export type WorkspaceRelayForwardRequestInitOptions = WorkspaceRelayForwardHeade
|
|
|
261
148
|
export declare function workspaceRelayTargetUrl(target: WorkspaceRelayTarget, path: string, search: string): URL;
|
|
262
149
|
export declare function workspaceRelayForwardHeaders(input: Headers, relayHostToken: string, workspaceId: string, options?: WorkspaceRelayForwardHeadersOptions): Headers;
|
|
263
150
|
export declare function workspaceRelayForwardRequestInit(request: Request, relayHostToken: string, workspaceId: string, options?: WorkspaceRelayForwardRequestInitOptions): RequestInit;
|
|
264
|
-
/**
|
|
265
|
-
* T16: dispose the sampler's periodic flush timer for a given options object.
|
|
266
|
-
* Tests call this in cleanup so the bun process can exit.
|
|
267
|
-
*/
|
|
268
|
-
export declare function disposeAuditSampler(options: WorkspaceRelayOptions): void;
|
|
269
|
-
export declare function createWorkspaceRelayTrace(): WorkspaceRelayTrace;
|
|
270
|
-
export declare function workspaceRelayServerTiming(trace: WorkspaceRelayTrace): string;
|
|
271
|
-
export declare function workspaceRelayTimingResponse(response: Response, trace: WorkspaceRelayTrace): Response;
|
|
272
151
|
export declare function authorizeWorkspaceRelayRequest(options: WorkspaceRelayOptions, request: Request, workspaceId: string, trace?: WorkspaceRelayAuthorizeTrace): Promise<{
|
|
273
152
|
ok: true;
|
|
274
153
|
request: AuthorizedWorkspaceRelayRequest;
|
|
@@ -277,19 +156,3 @@ export declare function authorizeWorkspaceRelayRequest(options: WorkspaceRelayOp
|
|
|
277
156
|
response: Response;
|
|
278
157
|
code: string;
|
|
279
158
|
}>;
|
|
280
|
-
export declare function forwardWorkspaceRelayRequest(request: Request, target: WorkspaceRelayTarget, relayHostToken: string, path: string, requestFetch: typeof fetch, timeoutMs: number, trace?: WorkspaceRelayAuthorizeTrace, originAllowed?: (origin: string) => boolean): Promise<Response>;
|
|
281
|
-
export type WorkspaceRelayApp = Hono & {
|
|
282
|
-
/**
|
|
283
|
-
* T16: clear the sampler's periodic flush timer. Tests must call this so
|
|
284
|
-
* `setInterval` doesn't keep the process alive after the test exits.
|
|
285
|
-
*/
|
|
286
|
-
disposeAuditSampler(): void;
|
|
287
|
-
};
|
|
288
|
-
/**
|
|
289
|
-
* T31: read the suppressed-accept counter for a given options object. Surfaced
|
|
290
|
-
* for the `/metrics` route handler. Returns 0 if no sampler state has been
|
|
291
|
-
* created yet (no requests have been audit-sampled), which is the same value
|
|
292
|
-
* an external observer would see.
|
|
293
|
-
*/
|
|
294
|
-
export declare function getAuditSuppressedCount(options: WorkspaceRelayOptions): number;
|
|
295
|
-
export declare function createWorkspaceRelay(options: WorkspaceRelayOptions): WorkspaceRelayApp;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RelayBacking } from "./auth";
|
|
2
|
+
import { SESSION_HOST_LOCATION_HINT } from "./relay-location";
|
|
3
|
+
import { type AuthorizedWorkspaceRelayRequest } from "./server";
|
|
4
|
+
/** The `SESSION_HOST` binding: one Durable Object per top-level session, named by that session's id. */
|
|
5
|
+
export type SessionHostNamespace = {
|
|
6
|
+
getByName: (name: string, options?: {
|
|
7
|
+
locationHint?: typeof SESSION_HOST_LOCATION_HINT;
|
|
8
|
+
}) => {
|
|
9
|
+
fetch: (input: string, init?: RequestInit) => Promise<Response>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export declare function isSessionHostTarget(target: {
|
|
13
|
+
backing: RelayBacking;
|
|
14
|
+
}): boolean;
|
|
15
|
+
export declare function isSessionHostNamespace(value: unknown): value is SessionHostNamespace;
|
|
16
|
+
/**
|
|
17
|
+
* Forwards one HTTP request to the session's Durable Object with the Relay
|
|
18
|
+
* Host Token the authorization minted. The browser's cookies never reach it,
|
|
19
|
+
* as they never reach a host tunnel, and the response body streams through
|
|
20
|
+
* untouched so a session's event stream stays open.
|
|
21
|
+
*/
|
|
22
|
+
export declare function forwardSessionHostHttp(namespace: SessionHostNamespace | undefined, request: Request, authorized: AuthorizedWorkspaceRelayRequest): Promise<Response>;
|