@claxedo/workspace-relay-protocol 0.8.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 +6 -3
- package/dist/index.d.ts +22 -0
- package/dist/index.mjs +109 -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,28 @@
|
|
|
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
|
+
* What a Runtime Access Token's scope reaches, asked of the runtime path a
|
|
14
|
+
* request names. A token with no session is the workspace owner's and reaches
|
|
15
|
+
* every path; a token scoped to one session, a share holder's, reaches that
|
|
16
|
+
* session's routes, its own event stream and the answers to its agent's
|
|
17
|
+
* questions, and the session authority decides which writes the share
|
|
18
|
+
* carries. The relay asks it before forwarding and the runtime again on
|
|
19
|
+
* arrival.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sessionScopeReaches(sessionScope: string | undefined, path: string, search: string): boolean;
|
|
22
|
+
/** The relay host id of a session hosted in its own Durable Object, named by the session at the root of its tree. */
|
|
23
|
+
export declare const SESSION_HOST_PREFIX = "session-do:";
|
|
24
|
+
export declare function sessionHostId(rootSessionId: string): string;
|
|
25
|
+
export declare function sessionHostRootOf(hostId: string): string | undefined;
|
|
4
26
|
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
27
|
export type TunnelHeaderMap = Record<string, string>;
|
|
6
28
|
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,41 @@ 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 sessionScopeReaches(sessionScope, path, search) {
|
|
249
|
+
if (sessionScope === undefined)
|
|
250
|
+
return true;
|
|
251
|
+
if (path === "/api/wr/events")
|
|
252
|
+
return new URLSearchParams(search).get("sessionID") === sessionScope;
|
|
253
|
+
if (path === `/session/${sessionScope}` || path.startsWith(`/session/${sessionScope}/`))
|
|
254
|
+
return true;
|
|
255
|
+
if (/^\/api\/wr\/execution-env\/(fs|exec|mcp\/[^/]+)$/.test(path))
|
|
256
|
+
return true;
|
|
257
|
+
return /^\/question\/[^/]+\/(reply|reject)$/.test(path);
|
|
258
|
+
}
|
|
259
|
+
var SESSION_HOST_PREFIX = "session-do:";
|
|
260
|
+
function sessionHostId(rootSessionId) {
|
|
261
|
+
return `${SESSION_HOST_PREFIX}${rootSessionId}`;
|
|
262
|
+
}
|
|
263
|
+
function sessionHostRootOf(hostId) {
|
|
264
|
+
if (!hostId.startsWith(SESSION_HOST_PREFIX))
|
|
265
|
+
return;
|
|
266
|
+
return hostId.slice(SESSION_HOST_PREFIX.length) || undefined;
|
|
267
|
+
}
|
|
209
268
|
function validateTunnelMessage(input) {
|
|
210
|
-
if (!isRecord(input))
|
|
269
|
+
if (!isRecord(input))
|
|
270
|
+
return { ok: false, reason: "invalid" };
|
|
211
271
|
if (input.protocol !== TUNNEL_PROTOCOL_VERSION) {
|
|
212
272
|
return {
|
|
213
273
|
ok: false,
|
|
@@ -217,7 +277,8 @@ function validateTunnelMessage(input) {
|
|
|
217
277
|
...typeof input.type === "string" ? { type: input.type } : {}
|
|
218
278
|
};
|
|
219
279
|
}
|
|
220
|
-
if (!isTunnelMessageRecord(input))
|
|
280
|
+
if (!isTunnelMessageRecord(input))
|
|
281
|
+
return { ok: false, reason: "invalid" };
|
|
221
282
|
return { ok: true, message: input };
|
|
222
283
|
}
|
|
223
284
|
function isTunnelMessage(input) {
|
|
@@ -241,7 +302,8 @@ function makeTunnelPing(sentAt = Date.now(), id = crypto.randomUUID()) {
|
|
|
241
302
|
};
|
|
242
303
|
}
|
|
243
304
|
function isTunnelMessageRecord(row) {
|
|
244
|
-
if (row.protocol !== TUNNEL_PROTOCOL_VERSION)
|
|
305
|
+
if (row.protocol !== TUNNEL_PROTOCOL_VERSION)
|
|
306
|
+
return false;
|
|
245
307
|
switch (row.type) {
|
|
246
308
|
case "ping":
|
|
247
309
|
return isNonEmptyString(row.id) && isFiniteNumber(row.sent_at);
|
|
@@ -258,15 +320,15 @@ function isTunnelMessageRecord(row) {
|
|
|
258
320
|
case "http.response.end":
|
|
259
321
|
return isNonEmptyString(row.request_id);
|
|
260
322
|
case "http.response.flow":
|
|
261
|
-
return isNonEmptyString(row.request_id) && typeof row.paused === "boolean" && (row.reason ===
|
|
323
|
+
return isNonEmptyString(row.request_id) && typeof row.paused === "boolean" && (row.reason === undefined || row.reason === "slow_consumer" || row.reason === "drained" || row.reason === "closed");
|
|
262
324
|
case "ws.open":
|
|
263
325
|
return isNonEmptyString(row.channel_id) && isNonEmptyString(row.workspace_id) && isNonEmptyString(row.path) && isTunnelHeaderMap(row.headers);
|
|
264
326
|
case "ws.frame":
|
|
265
327
|
return isNonEmptyString(row.channel_id) && isBase64(row.data_base64) && typeof row.binary === "boolean";
|
|
266
328
|
case "ws.close":
|
|
267
|
-
return isNonEmptyString(row.channel_id) && (row.code ===
|
|
329
|
+
return isNonEmptyString(row.channel_id) && (row.code === undefined || isCloseCode(row.code)) && (row.reason === undefined || isNonEmptyString(row.reason));
|
|
268
330
|
case "error":
|
|
269
|
-
return (row.request_id ===
|
|
331
|
+
return (row.request_id === undefined || isNonEmptyString(row.request_id)) && (row.channel_id === undefined || isNonEmptyString(row.channel_id)) && isNonEmptyString(row.code) && isNonEmptyString(row.message);
|
|
270
332
|
default:
|
|
271
333
|
return false;
|
|
272
334
|
}
|
|
@@ -281,31 +343,39 @@ function isHttpStatus(input) {
|
|
|
281
343
|
return typeof input === "number" && Number.isInteger(input) && input >= 100 && input <= 599;
|
|
282
344
|
}
|
|
283
345
|
function isCloseCode(input) {
|
|
284
|
-
return typeof input === "number" && Number.isInteger(input) && input >=
|
|
346
|
+
return typeof input === "number" && Number.isInteger(input) && input >= 1000 && input <= 4999 && input !== 1004 && input !== 1005 && input !== 1006 && input !== 1015;
|
|
285
347
|
}
|
|
348
|
+
var HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
286
349
|
function isTunnelHeaderMap(input) {
|
|
287
|
-
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
288
|
-
|
|
289
|
-
|
|
350
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
351
|
+
return false;
|
|
352
|
+
for (const [name, value] of Object.entries(input)) {
|
|
353
|
+
if (!HEADER_NAME.test(name) || typeof value !== "string")
|
|
354
|
+
return false;
|
|
290
355
|
}
|
|
291
356
|
return true;
|
|
292
357
|
}
|
|
293
358
|
function isOptionalBase64(input) {
|
|
294
|
-
return input ===
|
|
359
|
+
return input === undefined || isBase64(input);
|
|
295
360
|
}
|
|
296
361
|
function isBase64(input) {
|
|
297
362
|
return typeof input === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(input);
|
|
298
363
|
}
|
|
299
364
|
export {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
createHttpTokenVerifier,
|
|
305
|
-
createOidcTokenVerifier,
|
|
306
|
-
createStaticTokenVerifier,
|
|
307
|
-
isTunnelMessage,
|
|
308
|
-
makeTunnelPing,
|
|
365
|
+
validateTunnelMessage,
|
|
366
|
+
sessionScopeReaches,
|
|
367
|
+
sessionHostRootOf,
|
|
368
|
+
sessionHostId,
|
|
309
369
|
makeTunnelPong,
|
|
310
|
-
|
|
370
|
+
makeTunnelPing,
|
|
371
|
+
isTunnelMessage,
|
|
372
|
+
createStaticTokenVerifier,
|
|
373
|
+
createOidcTokenVerifier,
|
|
374
|
+
createHttpTokenVerifier,
|
|
375
|
+
TokenVerifierError,
|
|
376
|
+
TUNNEL_PROTOCOL_VERSION,
|
|
377
|
+
SESSION_TURN_LEASE_TTL_MS,
|
|
378
|
+
SESSION_STREAM_LEASE_TTL_MS,
|
|
379
|
+
SESSION_HOST_PREFIX,
|
|
380
|
+
CURRENT_CHANNEL_IDENTITY_VERSION
|
|
311
381
|
};
|
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.0",
|
|
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.
|
|
44
|
+
"@claxedo/helpers": "0.2.0",
|
|
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
|
}
|