@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 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/user-hosted backing discriminant. |
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 should normally be an HTTPS URL on infrastructure you control. It
104
- must enforce issuer, audience, expiry, key selection, and replay/revocation
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 : void 0;
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 : void 0;
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
- var TokenVerifierError = class extends Error {
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 !== void 0) this.cause = 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 ?? 5e3;
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(options.endpoint, {
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) throw err;
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 = /* @__PURE__ */ new Map();
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) throw err;
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)) return void 0;
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) return void 0;
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) return void 0;
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)) return input.scp.filter((item) => typeof item === "string");
201
- if (typeof input.scope === "string") return input.scope.split(" ").filter(Boolean);
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 = 15e3;
208
- var SESSION_TURN_LEASE_TTL_MS = 6e4;
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)) return { ok: false, reason: "invalid" };
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)) return { ok: false, reason: "invalid" };
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) return false;
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 === void 0 || row.reason === "slow_consumer" || row.reason === "drained" || row.reason === "closed");
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 === void 0 || isCloseCode(row.code)) && (row.reason === void 0 || isNonEmptyString(row.reason));
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 === void 0 || isNonEmptyString(row.request_id)) && (row.channel_id === void 0 || isNonEmptyString(row.channel_id)) && isNonEmptyString(row.code) && isNonEmptyString(row.message);
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 >= 0 && input <= 65535;
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)) return false;
288
- for (const value of Object.values(input)) {
289
- if (typeof value !== "string") return false;
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 === void 0 || isBase64(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
- SESSION_STREAM_LEASE_TTL_MS,
301
- SESSION_TURN_LEASE_TTL_MS,
302
- TUNNEL_PROTOCOL_VERSION,
303
- TokenVerifierError,
304
- createHttpTokenVerifier,
305
- createOidcTokenVerifier,
306
- createStaticTokenVerifier,
307
- isTunnelMessage,
308
- makeTunnelPing,
389
+ validateTunnelMessage,
390
+ sessionScopeReaches,
391
+ sessionHostRootOf,
392
+ sessionHostId,
309
393
  makeTunnelPong,
310
- validateTunnelMessage
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
  };
@@ -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
- access: "cloud";
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.8.0",
36
+ "version": "0.10.1",
37
37
  "scripts": {
38
- "build": "tsx scripts/build.ts",
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 ../../scripts/test-deadline.mjs bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml"
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.0",
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
  }