@claxedo/workspace-relay-protocol 0.8.0 → 0.10.1
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 +6 -3
- package/dist/index.d.ts +35 -0
- package/dist/index.mjs +135 -39
- package/dist/token-verifier.d.ts +13 -7
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ const claims = await verifier.verify("test-token")
|
|
|
59
59
|
| `TunnelMessageValidation` | Stable | Result type returned by `validateTunnelMessage` — `{ ok: true, message }` or a typed failure (`protocol_mismatch` / `invalid`). |
|
|
60
60
|
| `makeTunnelPing`, `makeTunnelPong` | Stable | Helpers for heartbeat requests and replies. |
|
|
61
61
|
| `TokenVerifier` and claim types | Stable | Narrow verifier interface shared by relay/runtime boundaries. |
|
|
62
|
-
| `RelayHostVerifierClaims` (incl. `role`) | Stable | Claims shape for host-side verification: org, `role` (`viewer` \| `editor` \| `admin` \| `owner`), and cloud/
|
|
62
|
+
| `RelayHostVerifierClaims` (incl. `role`) | Stable | Claims shape for host-side verification: org, `role` (`viewer` \| `editor` \| `admin` \| `owner`), and the `cloud-vm` / `local-worktree` backing that names the placement. |
|
|
63
63
|
| `createOidcTokenVerifier` | Public beta | Verifies session JWTs with issuer, audience, JWKS, and algorithm constraints. |
|
|
64
64
|
| `createHttpTokenVerifier` | Public beta | Calls an operator-controlled verifier endpoint over HTTP. |
|
|
65
65
|
| `createStaticTokenVerifier` | Test/single-tenant only | Fixed token table for tests and isolated self-hosted deployments. |
|
|
@@ -100,8 +100,11 @@ endpoint and expects `{ subject, scopes?, claims }` back. The endpoint is a
|
|
|
100
100
|
trusted operator configuration value. Do not derive it from a request, tenant
|
|
101
101
|
record, query string, workspace config, or other user-controlled input.
|
|
102
102
|
|
|
103
|
-
The endpoint
|
|
104
|
-
|
|
103
|
+
The endpoint requires HTTPS and redirects are refused. Explicit
|
|
104
|
+
`allowInsecureLoopback: true` permits HTTP only to `localhost`, `127.0.0.1`, or
|
|
105
|
+
`::1` for development. URL credentials and fragments are rejected. Returned
|
|
106
|
+
claims must match the subject and be within their issued/expiry/not-before
|
|
107
|
+
window. The endpoint must enforce issuer, audience, key selection, and replay/revocation
|
|
105
108
|
policy before returning claims. Treat a compromised verifier endpoint as
|
|
106
109
|
equivalent to a compromised token issuer.
|
|
107
110
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
export declare const TUNNEL_PROTOCOL_VERSION = 1;
|
|
2
2
|
export declare const SESSION_STREAM_LEASE_TTL_MS = 15000;
|
|
3
3
|
export declare const SESSION_TURN_LEASE_TTL_MS = 60000;
|
|
4
|
+
/**
|
|
5
|
+
* Which generation of the sender-identity contract authorizes. 0 is every
|
|
6
|
+
* binding row and every token written before the transports were proven to
|
|
7
|
+
* carry the platform's stable account id, whose key may be a handle that now
|
|
8
|
+
* belongs to somebody else. Authority stores write this number, the relay
|
|
9
|
+
* admits only this number, and a lower one is never promoted.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CURRENT_CHANNEL_IDENTITY_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* A close code a server may send: 1000–4999 except 1004, 1005 and 1006, which
|
|
14
|
+
* only ever describe a close and which workerd's `close()` refuses; anything
|
|
15
|
+
* else becomes `fallback`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function closeCode(code: number | undefined, fallback?: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* A WebSocket close reason cut to the 123 UTF-8 bytes a close frame carries,
|
|
20
|
+
* at a character boundary. A WHATWG or Node `ws` socket's `close()` throws past
|
|
21
|
+
* it, and workerd's does from compatibility date 2026-03-03
|
|
22
|
+
* (`websocket_close_reason_byte_limit`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function closeReason(reason: unknown): string;
|
|
25
|
+
/**
|
|
26
|
+
* What a Runtime Access Token's scope reaches, asked of the runtime path a
|
|
27
|
+
* request names. A token with no session is the workspace owner's and reaches
|
|
28
|
+
* every path; a token scoped to one session, a share holder's, reaches that
|
|
29
|
+
* session's routes, its own event stream and the answers to its agent's
|
|
30
|
+
* questions, and the session authority decides which writes the share
|
|
31
|
+
* carries. The relay asks it before forwarding and the runtime again on
|
|
32
|
+
* arrival.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sessionScopeReaches(sessionScope: string | undefined, path: string, search: string): boolean;
|
|
35
|
+
/** The relay host id of a session hosted in its own Durable Object, named by the session at the root of its tree. */
|
|
36
|
+
export declare const SESSION_HOST_PREFIX = "session-do:";
|
|
37
|
+
export declare function sessionHostId(rootSessionId: string): string;
|
|
38
|
+
export declare function sessionHostRootOf(hostId: string): string | undefined;
|
|
4
39
|
export { type TokenClaims, type TokenVerifierBaseClaims, type RuntimeAccessVerifierClaims, type RelayHostVerifierClaims, type TokenVerifier, type OidcTokenVerifierClaims, TokenVerifierError, type OidcTokenVerifierOptions, createOidcTokenVerifier, type HttpTokenVerifierOptions, createHttpTokenVerifier, type StaticTokenVerifierOptions, createStaticTokenVerifier, } from "./token-verifier";
|
|
5
40
|
export type TunnelHeaderMap = Record<string, string>;
|
|
6
41
|
export type TunnelPing = {
|
package/dist/index.mjs
CHANGED
|
@@ -9,11 +9,11 @@ function isFiniteNumber(value) {
|
|
|
9
9
|
return typeof value === "number" && Number.isFinite(value);
|
|
10
10
|
}
|
|
11
11
|
function asFiniteNumber(value) {
|
|
12
|
-
return isFiniteNumber(value) ? value :
|
|
12
|
+
return isFiniteNumber(value) ? value : undefined;
|
|
13
13
|
}
|
|
14
14
|
function stringClaim(payload, key) {
|
|
15
15
|
const value = payload[key];
|
|
16
|
-
return isString(value) && value.trim().length > 0 ? value :
|
|
16
|
+
return isString(value) && value.trim().length > 0 ? value : undefined;
|
|
17
17
|
}
|
|
18
18
|
function numberClaim(payload, key) {
|
|
19
19
|
return asFiniteNumber(payload[key]);
|
|
@@ -21,7 +21,22 @@ function numberClaim(payload, key) {
|
|
|
21
21
|
|
|
22
22
|
// src/token-verifier.ts
|
|
23
23
|
import { createRemoteJWKSet, errors as joseErrors, jwtVerify } from "jose";
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
// ../claxedo-helpers/dist/index.mjs
|
|
26
|
+
var requestPeerAddresses = new WeakMap;
|
|
27
|
+
function normalizeHostname(hostname) {
|
|
28
|
+
const value = (hostname ?? "").trim().toLowerCase();
|
|
29
|
+
if (value.startsWith("[") && value.endsWith("]") && value.includes(":"))
|
|
30
|
+
return value.slice(1, -1);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function isLoopbackHostname(hostname) {
|
|
34
|
+
const normalized = normalizeHostname(hostname);
|
|
35
|
+
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/token-verifier.ts
|
|
39
|
+
class TokenVerifierError extends Error {
|
|
25
40
|
code;
|
|
26
41
|
status;
|
|
27
42
|
constructor(input) {
|
|
@@ -29,10 +44,20 @@ var TokenVerifierError = class extends Error {
|
|
|
29
44
|
this.name = "TokenVerifierError";
|
|
30
45
|
this.code = input.code;
|
|
31
46
|
this.status = input.status ?? 401;
|
|
32
|
-
if (input.cause !==
|
|
47
|
+
if (input.cause !== undefined)
|
|
48
|
+
this.cause = input.cause;
|
|
33
49
|
}
|
|
34
|
-
}
|
|
50
|
+
}
|
|
35
51
|
function createHttpTokenVerifier(options) {
|
|
52
|
+
let endpoint;
|
|
53
|
+
try {
|
|
54
|
+
endpoint = new URL(options.endpoint);
|
|
55
|
+
if (endpoint.username || endpoint.password || endpoint.hash || endpoint.protocol !== "https:" && !(options.allowInsecureLoopback && endpoint.protocol === "http:" && isLoopbackHostname(endpoint.hostname))) {
|
|
56
|
+
throw new Error("untrusted verifier endpoint");
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
throw new TokenVerifierError({ code: "verifier_endpoint_invalid", status: 500, message: "Verifier endpoint requires HTTPS without URL credentials or a fragment; explicit loopback development is the only HTTP exception" });
|
|
60
|
+
}
|
|
36
61
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
37
62
|
if (!fetchImpl) {
|
|
38
63
|
throw new TokenVerifierError({
|
|
@@ -41,14 +66,15 @@ function createHttpTokenVerifier(options) {
|
|
|
41
66
|
status: 500
|
|
42
67
|
});
|
|
43
68
|
}
|
|
44
|
-
const timeoutMs = options.timeoutMs ??
|
|
69
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
45
70
|
return {
|
|
46
71
|
async verify(token) {
|
|
47
|
-
const controller = new AbortController
|
|
72
|
+
const controller = new AbortController;
|
|
48
73
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
49
74
|
try {
|
|
50
|
-
const res = await fetchImpl(
|
|
75
|
+
const res = await fetchImpl(endpoint.href, {
|
|
51
76
|
method: "POST",
|
|
77
|
+
redirect: "error",
|
|
52
78
|
headers: {
|
|
53
79
|
"content-type": "application/json",
|
|
54
80
|
...options.headers
|
|
@@ -84,13 +110,19 @@ function createHttpTokenVerifier(options) {
|
|
|
84
110
|
message: "Verifier response is missing required token claims"
|
|
85
111
|
});
|
|
86
112
|
}
|
|
113
|
+
const now = Date.now() / 1000;
|
|
114
|
+
const nbf = claims.nbf;
|
|
115
|
+
if (claims.exp <= now || claims.iat > now || claims.exp <= claims.iat || nbf !== undefined && (typeof nbf !== "number" || !Number.isFinite(nbf) || nbf > now)) {
|
|
116
|
+
throw new TokenVerifierError({ code: "verifier_claims_invalid", message: "Verifier claims are expired or not yet valid" });
|
|
117
|
+
}
|
|
87
118
|
return {
|
|
88
119
|
subject,
|
|
89
120
|
scopes: Array.isArray(body.scopes) ? body.scopes.filter((s) => typeof s === "string") : [],
|
|
90
121
|
claims
|
|
91
122
|
};
|
|
92
123
|
} catch (err) {
|
|
93
|
-
if (err instanceof TokenVerifierError)
|
|
124
|
+
if (err instanceof TokenVerifierError)
|
|
125
|
+
throw err;
|
|
94
126
|
throw new TokenVerifierError({
|
|
95
127
|
code: "verifier_unreachable",
|
|
96
128
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -103,7 +135,7 @@ function createHttpTokenVerifier(options) {
|
|
|
103
135
|
}
|
|
104
136
|
};
|
|
105
137
|
}
|
|
106
|
-
var oidcJwks =
|
|
138
|
+
var oidcJwks = new Map;
|
|
107
139
|
function createOidcTokenVerifier(options) {
|
|
108
140
|
const algorithms = options.algorithms ?? ["ES256", "EdDSA", "RS256"];
|
|
109
141
|
const remote = oidcJwks.get(options.jwksUrl) ?? createRemoteJWKSet(new URL(options.jwksUrl));
|
|
@@ -129,7 +161,8 @@ function createOidcTokenVerifier(options) {
|
|
|
129
161
|
claims
|
|
130
162
|
};
|
|
131
163
|
} catch (err) {
|
|
132
|
-
if (err instanceof TokenVerifierError)
|
|
164
|
+
if (err instanceof TokenVerifierError)
|
|
165
|
+
throw err;
|
|
133
166
|
const unavailable = err instanceof joseErrors.JWKSTimeout || !(err instanceof joseErrors.JOSEError);
|
|
134
167
|
throw new TokenVerifierError({
|
|
135
168
|
code: unavailable ? "oidc_verifier_unavailable" : "oidc_token_invalid",
|
|
@@ -157,7 +190,8 @@ function createStaticTokenVerifier(options) {
|
|
|
157
190
|
};
|
|
158
191
|
}
|
|
159
192
|
function tokenVerifierBaseClaims(input) {
|
|
160
|
-
if (!isRecord(input))
|
|
193
|
+
if (!isRecord(input))
|
|
194
|
+
return;
|
|
161
195
|
const iss = stringClaim(input, "iss");
|
|
162
196
|
const aud = stringClaim(input, "aud");
|
|
163
197
|
const sub = stringClaim(input, "sub");
|
|
@@ -166,7 +200,8 @@ function tokenVerifierBaseClaims(input) {
|
|
|
166
200
|
const exp = numberClaim(input, "exp");
|
|
167
201
|
const iat = numberClaim(input, "iat");
|
|
168
202
|
const jti = stringClaim(input, "jti");
|
|
169
|
-
if (!iss || !aud || !sub || !workspace_id || !host_id || !exp || !iat || !jti)
|
|
203
|
+
if (!iss || !aud || !sub || !workspace_id || !host_id || !exp || !iat || !jti)
|
|
204
|
+
return;
|
|
170
205
|
return {
|
|
171
206
|
...input,
|
|
172
207
|
iss,
|
|
@@ -180,7 +215,8 @@ function tokenVerifierBaseClaims(input) {
|
|
|
180
215
|
};
|
|
181
216
|
}
|
|
182
217
|
function oidcClaims(input) {
|
|
183
|
-
if (!input.iss || !input.sub || !input.exp)
|
|
218
|
+
if (!input.iss || !input.sub || !input.exp)
|
|
219
|
+
return;
|
|
184
220
|
const sid = stringClaim(input, "sid");
|
|
185
221
|
const org_id = stringClaim(input, "org_id");
|
|
186
222
|
const orgId = stringClaim(input, "orgId");
|
|
@@ -197,17 +233,65 @@ function oidcClaims(input) {
|
|
|
197
233
|
};
|
|
198
234
|
}
|
|
199
235
|
function scopeClaims(input) {
|
|
200
|
-
if (Array.isArray(input.scp))
|
|
201
|
-
|
|
236
|
+
if (Array.isArray(input.scp))
|
|
237
|
+
return input.scp.filter((item) => typeof item === "string");
|
|
238
|
+
if (typeof input.scope === "string")
|
|
239
|
+
return input.scope.split(" ").filter(Boolean);
|
|
202
240
|
return [];
|
|
203
241
|
}
|
|
204
242
|
|
|
205
243
|
// src/index.ts
|
|
206
244
|
var TUNNEL_PROTOCOL_VERSION = 1;
|
|
207
|
-
var SESSION_STREAM_LEASE_TTL_MS =
|
|
208
|
-
var SESSION_TURN_LEASE_TTL_MS =
|
|
245
|
+
var SESSION_STREAM_LEASE_TTL_MS = 15000;
|
|
246
|
+
var SESSION_TURN_LEASE_TTL_MS = 60000;
|
|
247
|
+
var CURRENT_CHANNEL_IDENTITY_VERSION = 1;
|
|
248
|
+
function closeCode(code, fallback = 1011) {
|
|
249
|
+
if (code === undefined)
|
|
250
|
+
return fallback;
|
|
251
|
+
if (code >= 1000 && code <= 4999 && ![1004, 1005, 1006].includes(code))
|
|
252
|
+
return code;
|
|
253
|
+
return fallback;
|
|
254
|
+
}
|
|
255
|
+
var CLOSE_REASON_MAX_BYTES = 123;
|
|
256
|
+
function closeReason(reason) {
|
|
257
|
+
if (typeof reason !== "string")
|
|
258
|
+
return "";
|
|
259
|
+
const encoder = new TextEncoder;
|
|
260
|
+
if (encoder.encode(reason).byteLength <= CLOSE_REASON_MAX_BYTES)
|
|
261
|
+
return reason;
|
|
262
|
+
let cut = "";
|
|
263
|
+
let bytes = 0;
|
|
264
|
+
for (const char of reason) {
|
|
265
|
+
bytes += encoder.encode(char).byteLength;
|
|
266
|
+
if (bytes > CLOSE_REASON_MAX_BYTES)
|
|
267
|
+
return cut;
|
|
268
|
+
cut += char;
|
|
269
|
+
}
|
|
270
|
+
return cut;
|
|
271
|
+
}
|
|
272
|
+
function sessionScopeReaches(sessionScope, path, search) {
|
|
273
|
+
if (sessionScope === undefined)
|
|
274
|
+
return true;
|
|
275
|
+
if (path === "/api/wr/events")
|
|
276
|
+
return new URLSearchParams(search).get("sessionID") === sessionScope;
|
|
277
|
+
if (path === `/session/${sessionScope}` || path.startsWith(`/session/${sessionScope}/`))
|
|
278
|
+
return true;
|
|
279
|
+
if (/^\/api\/wr\/execution-env\/(fs|exec|mcp\/[^/]+)$/.test(path))
|
|
280
|
+
return true;
|
|
281
|
+
return /^\/question\/[^/]+\/(reply|reject)$/.test(path);
|
|
282
|
+
}
|
|
283
|
+
var SESSION_HOST_PREFIX = "session-do:";
|
|
284
|
+
function sessionHostId(rootSessionId) {
|
|
285
|
+
return `${SESSION_HOST_PREFIX}${rootSessionId}`;
|
|
286
|
+
}
|
|
287
|
+
function sessionHostRootOf(hostId) {
|
|
288
|
+
if (!hostId.startsWith(SESSION_HOST_PREFIX))
|
|
289
|
+
return;
|
|
290
|
+
return hostId.slice(SESSION_HOST_PREFIX.length) || undefined;
|
|
291
|
+
}
|
|
209
292
|
function validateTunnelMessage(input) {
|
|
210
|
-
if (!isRecord(input))
|
|
293
|
+
if (!isRecord(input))
|
|
294
|
+
return { ok: false, reason: "invalid" };
|
|
211
295
|
if (input.protocol !== TUNNEL_PROTOCOL_VERSION) {
|
|
212
296
|
return {
|
|
213
297
|
ok: false,
|
|
@@ -217,7 +301,8 @@ function validateTunnelMessage(input) {
|
|
|
217
301
|
...typeof input.type === "string" ? { type: input.type } : {}
|
|
218
302
|
};
|
|
219
303
|
}
|
|
220
|
-
if (!isTunnelMessageRecord(input))
|
|
304
|
+
if (!isTunnelMessageRecord(input))
|
|
305
|
+
return { ok: false, reason: "invalid" };
|
|
221
306
|
return { ok: true, message: input };
|
|
222
307
|
}
|
|
223
308
|
function isTunnelMessage(input) {
|
|
@@ -241,7 +326,8 @@ function makeTunnelPing(sentAt = Date.now(), id = crypto.randomUUID()) {
|
|
|
241
326
|
};
|
|
242
327
|
}
|
|
243
328
|
function isTunnelMessageRecord(row) {
|
|
244
|
-
if (row.protocol !== TUNNEL_PROTOCOL_VERSION)
|
|
329
|
+
if (row.protocol !== TUNNEL_PROTOCOL_VERSION)
|
|
330
|
+
return false;
|
|
245
331
|
switch (row.type) {
|
|
246
332
|
case "ping":
|
|
247
333
|
return isNonEmptyString(row.id) && isFiniteNumber(row.sent_at);
|
|
@@ -258,15 +344,15 @@ function isTunnelMessageRecord(row) {
|
|
|
258
344
|
case "http.response.end":
|
|
259
345
|
return isNonEmptyString(row.request_id);
|
|
260
346
|
case "http.response.flow":
|
|
261
|
-
return isNonEmptyString(row.request_id) && typeof row.paused === "boolean" && (row.reason ===
|
|
347
|
+
return isNonEmptyString(row.request_id) && typeof row.paused === "boolean" && (row.reason === undefined || row.reason === "slow_consumer" || row.reason === "drained" || row.reason === "closed");
|
|
262
348
|
case "ws.open":
|
|
263
349
|
return isNonEmptyString(row.channel_id) && isNonEmptyString(row.workspace_id) && isNonEmptyString(row.path) && isTunnelHeaderMap(row.headers);
|
|
264
350
|
case "ws.frame":
|
|
265
351
|
return isNonEmptyString(row.channel_id) && isBase64(row.data_base64) && typeof row.binary === "boolean";
|
|
266
352
|
case "ws.close":
|
|
267
|
-
return isNonEmptyString(row.channel_id) && (row.code ===
|
|
353
|
+
return isNonEmptyString(row.channel_id) && (row.code === undefined || isCloseCode(row.code)) && (row.reason === undefined || isNonEmptyString(row.reason));
|
|
268
354
|
case "error":
|
|
269
|
-
return (row.request_id ===
|
|
355
|
+
return (row.request_id === undefined || isNonEmptyString(row.request_id)) && (row.channel_id === undefined || isNonEmptyString(row.channel_id)) && isNonEmptyString(row.code) && isNonEmptyString(row.message);
|
|
270
356
|
default:
|
|
271
357
|
return false;
|
|
272
358
|
}
|
|
@@ -281,31 +367,41 @@ function isHttpStatus(input) {
|
|
|
281
367
|
return typeof input === "number" && Number.isInteger(input) && input >= 100 && input <= 599;
|
|
282
368
|
}
|
|
283
369
|
function isCloseCode(input) {
|
|
284
|
-
return typeof input === "number" && Number.isInteger(input) && input >=
|
|
370
|
+
return typeof input === "number" && Number.isInteger(input) && input >= 1000 && input <= 4999 && input !== 1004 && input !== 1005 && input !== 1006 && input !== 1015;
|
|
285
371
|
}
|
|
372
|
+
var HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
286
373
|
function isTunnelHeaderMap(input) {
|
|
287
|
-
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
288
|
-
|
|
289
|
-
|
|
374
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
375
|
+
return false;
|
|
376
|
+
for (const [name, value] of Object.entries(input)) {
|
|
377
|
+
if (!HEADER_NAME.test(name) || typeof value !== "string")
|
|
378
|
+
return false;
|
|
290
379
|
}
|
|
291
380
|
return true;
|
|
292
381
|
}
|
|
293
382
|
function isOptionalBase64(input) {
|
|
294
|
-
return input ===
|
|
383
|
+
return input === undefined || isBase64(input);
|
|
295
384
|
}
|
|
296
385
|
function isBase64(input) {
|
|
297
386
|
return typeof input === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(input);
|
|
298
387
|
}
|
|
299
388
|
export {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
createHttpTokenVerifier,
|
|
305
|
-
createOidcTokenVerifier,
|
|
306
|
-
createStaticTokenVerifier,
|
|
307
|
-
isTunnelMessage,
|
|
308
|
-
makeTunnelPing,
|
|
389
|
+
validateTunnelMessage,
|
|
390
|
+
sessionScopeReaches,
|
|
391
|
+
sessionHostRootOf,
|
|
392
|
+
sessionHostId,
|
|
309
393
|
makeTunnelPong,
|
|
310
|
-
|
|
394
|
+
makeTunnelPing,
|
|
395
|
+
isTunnelMessage,
|
|
396
|
+
createStaticTokenVerifier,
|
|
397
|
+
createOidcTokenVerifier,
|
|
398
|
+
createHttpTokenVerifier,
|
|
399
|
+
closeReason,
|
|
400
|
+
closeCode,
|
|
401
|
+
TokenVerifierError,
|
|
402
|
+
TUNNEL_PROTOCOL_VERSION,
|
|
403
|
+
SESSION_TURN_LEASE_TTL_MS,
|
|
404
|
+
SESSION_STREAM_LEASE_TTL_MS,
|
|
405
|
+
SESSION_HOST_PREFIX,
|
|
406
|
+
CURRENT_CHANNEL_IDENTITY_VERSION
|
|
311
407
|
};
|
package/dist/token-verifier.d.ts
CHANGED
|
@@ -26,8 +26,13 @@ export type TokenVerifierBaseClaims = {
|
|
|
26
26
|
export type RuntimeAccessVerifierClaims = Omit<TokenVerifierBaseClaims, "sub"> & {
|
|
27
27
|
org_id: string;
|
|
28
28
|
role: "viewer" | "editor" | "admin" | "owner";
|
|
29
|
+
/** The whole workspace, or the one session `session_id` names. */
|
|
30
|
+
scope: "workspace" | "session";
|
|
31
|
+
session_id?: string;
|
|
32
|
+
purpose?: "turn-execution";
|
|
29
33
|
principal_kind: "user" | "service";
|
|
30
34
|
actor_id: string;
|
|
35
|
+
user_id?: string;
|
|
31
36
|
actor_kind: "human" | "agent";
|
|
32
37
|
actor_public_id?: string;
|
|
33
38
|
actor_name?: string;
|
|
@@ -36,21 +41,20 @@ export type RuntimeAccessVerifierClaims = Omit<TokenVerifierBaseClaims, "sub"> &
|
|
|
36
41
|
export type RelayHostVerifierClaims = Omit<TokenVerifierBaseClaims, "sub"> & {
|
|
37
42
|
org_id: string;
|
|
38
43
|
role: "viewer" | "editor" | "admin" | "owner";
|
|
44
|
+
scope: "workspace" | "session";
|
|
45
|
+
session_id?: string;
|
|
46
|
+
purpose?: "turn-execution";
|
|
39
47
|
principal_kind: "user" | "service";
|
|
40
48
|
actor_id: string;
|
|
49
|
+
user_id?: string;
|
|
41
50
|
actor_kind: "human" | "agent";
|
|
42
51
|
actor_public_id?: string;
|
|
43
52
|
actor_name?: string;
|
|
44
53
|
actor_avatar_url?: string;
|
|
45
54
|
/** Durable Runtime Access Token id; distinct from this short-lived RHT jti. */
|
|
46
55
|
parent_jti: string;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
backing: "cloud-vm";
|
|
50
|
-
} | {
|
|
51
|
-
access: "user-hosted";
|
|
52
|
-
backing: "local-worktree";
|
|
53
|
-
});
|
|
56
|
+
backing: "cloud-vm" | "local-worktree" | "durable-object";
|
|
57
|
+
};
|
|
54
58
|
export type TokenClaims<TClaims extends Record<string, unknown> = Record<string, unknown>> = {
|
|
55
59
|
/** Provider subject for provider-backed identity verifiers. Runtime and
|
|
56
60
|
* relay-host tokens instead carry canonical actor/principal claims. */
|
|
@@ -89,6 +93,8 @@ export type HttpTokenVerifierOptions = {
|
|
|
89
93
|
* and return `{ subject: string; scopes?: string[]; claims: TokenVerifierBaseClaims }`
|
|
90
94
|
* on success, or HTTP non-2xx on failure. */
|
|
91
95
|
endpoint: string;
|
|
96
|
+
/** Development only: permit HTTP to the exact loopback hosts. HTTPS is otherwise required. */
|
|
97
|
+
allowInsecureLoopback?: boolean;
|
|
92
98
|
/** Optional fixed headers (e.g., bearer auth on the verifier itself). */
|
|
93
99
|
headers?: Record<string, string>;
|
|
94
100
|
/** fetch override for tests. */
|
package/package.json
CHANGED
|
@@ -33,22 +33,20 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"name": "@claxedo/workspace-relay-protocol",
|
|
36
|
-
"version": "0.
|
|
36
|
+
"version": "0.10.1",
|
|
37
37
|
"scripts": {
|
|
38
|
-
"build": "
|
|
38
|
+
"build": "bun scripts/build.ts",
|
|
39
39
|
"prepublishOnly": "npm run build",
|
|
40
40
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
41
|
-
"test": "mkdir -p .artifacts/unit && node ../../
|
|
41
|
+
"test": "mkdir -p .artifacts/unit && node ../../script/test-deadline.mjs bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@claxedo/helpers": "0.1
|
|
44
|
+
"@claxedo/helpers": "0.2.1",
|
|
45
45
|
"jose": "6.0.11"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@tsconfig/node-lts": "22.0.4",
|
|
49
49
|
"@types/bun": "1.3.13",
|
|
50
|
-
"esbuild": "0.25.12",
|
|
51
|
-
"tsx": "4.22.3",
|
|
52
50
|
"typescript": "catalog:"
|
|
53
51
|
}
|
|
54
52
|
}
|