@claxedo/workspace-relay-protocol 0.6.0 → 0.8.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 CHANGED
@@ -60,7 +60,7 @@ const claims = await verifier.verify("test-token")
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
62
  | `RelayHostVerifierClaims` (incl. `role`) | Stable | Claims shape for host-side verification: org, `role` (`viewer` \| `editor` \| `admin` \| `owner`), and cloud/user-hosted backing discriminant. |
63
- | `createClerkTokenVerifier` | Public beta | Verifies Clerk session tokens with issuer, audience, JWKS, and algorithm constraints. |
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. |
66
66
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export declare const TUNNEL_PROTOCOL_VERSION = 1;
2
- export { type TokenClaims, type TokenVerifierBaseClaims, type RuntimeAccessVerifierClaims, type RelayHostVerifierClaims, type TokenVerifier, type ClerkTokenVerifierClaims, TokenVerifierError, type ClerkTokenVerifierOptions, createClerkTokenVerifier, type HttpTokenVerifierOptions, createHttpTokenVerifier, type StaticTokenVerifierOptions, createStaticTokenVerifier, } from "./token-verifier";
2
+ export declare const SESSION_STREAM_LEASE_TTL_MS = 15000;
3
+ export declare const SESSION_TURN_LEASE_TTL_MS = 60000;
4
+ 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";
3
5
  export type TunnelHeaderMap = Record<string, string>;
4
6
  export type TunnelPing = {
5
7
  type: "ping";
package/dist/index.mjs CHANGED
@@ -1,5 +1,26 @@
1
+ // ../claxedo-helpers/dist/guards.mjs
2
+ function isRecord(value) {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function isString(value) {
6
+ return typeof value === "string";
7
+ }
8
+ function isFiniteNumber(value) {
9
+ return typeof value === "number" && Number.isFinite(value);
10
+ }
11
+ function asFiniteNumber(value) {
12
+ return isFiniteNumber(value) ? value : void 0;
13
+ }
14
+ function stringClaim(payload, key) {
15
+ const value = payload[key];
16
+ return isString(value) && value.trim().length > 0 ? value : void 0;
17
+ }
18
+ function numberClaim(payload, key) {
19
+ return asFiniteNumber(payload[key]);
20
+ }
21
+
1
22
  // src/token-verifier.ts
2
- import { createRemoteJWKSet, jwtVerify } from "jose";
23
+ import { createRemoteJWKSet, errors as joseErrors, jwtVerify } from "jose";
3
24
  var TokenVerifierError = class extends Error {
4
25
  code;
5
26
  status;
@@ -30,34 +51,41 @@ function createHttpTokenVerifier(options) {
30
51
  method: "POST",
31
52
  headers: {
32
53
  "content-type": "application/json",
33
- ...options.headers ?? {}
54
+ ...options.headers
34
55
  },
35
56
  body: JSON.stringify({ token }),
36
57
  signal: controller.signal
37
58
  });
38
59
  if (!res.ok) {
39
60
  throw new TokenVerifierError({
40
- code: "verifier_rejected",
61
+ code: res.status === 401 || res.status === 403 ? "verifier_rejected" : "verifier_unavailable",
41
62
  message: `Verifier endpoint returned ${res.status}`,
42
- status: res.status === 401 || res.status === 403 ? res.status : 401
63
+ status: res.status === 401 || res.status === 403 ? res.status : 503
43
64
  });
44
65
  }
45
66
  const body = await res.json().catch(() => null);
46
- if (!body || typeof body.subject !== "string" || !body.subject) {
67
+ if (!isRecord(body)) {
68
+ throw new TokenVerifierError({
69
+ code: "verifier_response_invalid",
70
+ message: "Verifier response is missing a subject"
71
+ });
72
+ }
73
+ const subject = stringClaim(body, "subject");
74
+ if (!subject) {
47
75
  throw new TokenVerifierError({
48
76
  code: "verifier_response_invalid",
49
77
  message: "Verifier response is missing a subject"
50
78
  });
51
79
  }
52
80
  const claims = tokenVerifierBaseClaims(body.claims);
53
- if (!claims || claims.sub !== body.subject) {
81
+ if (!claims || claims.sub !== subject) {
54
82
  throw new TokenVerifierError({
55
83
  code: "verifier_response_invalid",
56
84
  message: "Verifier response is missing required token claims"
57
85
  });
58
86
  }
59
87
  return {
60
- subject: body.subject,
88
+ subject,
61
89
  scopes: Array.isArray(body.scopes) ? body.scopes.filter((s) => typeof s === "string") : [],
62
90
  claims
63
91
  };
@@ -66,6 +94,7 @@ function createHttpTokenVerifier(options) {
66
94
  throw new TokenVerifierError({
67
95
  code: "verifier_unreachable",
68
96
  message: err instanceof Error ? err.message : String(err),
97
+ status: 503,
69
98
  cause: err
70
99
  });
71
100
  } finally {
@@ -74,11 +103,11 @@ function createHttpTokenVerifier(options) {
74
103
  }
75
104
  };
76
105
  }
77
- var clerkJwks = /* @__PURE__ */ new Map();
78
- function createClerkTokenVerifier(options) {
106
+ var oidcJwks = /* @__PURE__ */ new Map();
107
+ function createOidcTokenVerifier(options) {
79
108
  const algorithms = options.algorithms ?? ["ES256", "EdDSA", "RS256"];
80
- const remote = clerkJwks.get(options.jwksUrl) ?? createRemoteJWKSet(new URL(options.jwksUrl));
81
- clerkJwks.set(options.jwksUrl, remote);
109
+ const remote = oidcJwks.get(options.jwksUrl) ?? createRemoteJWKSet(new URL(options.jwksUrl));
110
+ oidcJwks.set(options.jwksUrl, remote);
82
111
  return {
83
112
  async verify(token) {
84
113
  try {
@@ -87,11 +116,11 @@ function createClerkTokenVerifier(options) {
87
116
  audience: options.audience,
88
117
  algorithms
89
118
  });
90
- const claims = clerkClaims(result.payload);
119
+ const claims = oidcClaims(result.payload);
91
120
  if (!claims) {
92
121
  throw new TokenVerifierError({
93
- code: "clerk_claims_invalid",
94
- message: "Clerk token is missing required session claims"
122
+ code: "oidc_claims_invalid",
123
+ message: "Session token is missing required claims"
95
124
  });
96
125
  }
97
126
  return {
@@ -101,9 +130,11 @@ function createClerkTokenVerifier(options) {
101
130
  };
102
131
  } catch (err) {
103
132
  if (err instanceof TokenVerifierError) throw err;
133
+ const unavailable = err instanceof joseErrors.JWKSTimeout || !(err instanceof joseErrors.JOSEError);
104
134
  throw new TokenVerifierError({
105
- code: "clerk_token_invalid",
106
- message: "Clerk session token is invalid",
135
+ code: unavailable ? "oidc_verifier_unavailable" : "oidc_token_invalid",
136
+ message: unavailable ? "Session verifier is unavailable" : "Session token is invalid",
137
+ status: unavailable ? 503 : 401,
107
138
  cause: err
108
139
  });
109
140
  }
@@ -111,9 +142,10 @@ function createClerkTokenVerifier(options) {
111
142
  };
112
143
  }
113
144
  function createStaticTokenVerifier(options) {
145
+ const tokens = new Map(Object.entries(options.tokens));
114
146
  return {
115
147
  async verify(token) {
116
- const claims = options.tokens[token];
148
+ const claims = tokens.get(token);
117
149
  if (!claims) {
118
150
  throw new TokenVerifierError({
119
151
  code: "token_not_in_static_table",
@@ -124,16 +156,8 @@ function createStaticTokenVerifier(options) {
124
156
  }
125
157
  };
126
158
  }
127
- function stringClaim(input, key) {
128
- const value = input[key];
129
- return typeof value === "string" && value.trim() ? value : void 0;
130
- }
131
- function numberClaim(input, key) {
132
- const value = input[key];
133
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
134
- }
135
159
  function tokenVerifierBaseClaims(input) {
136
- if (!input || typeof input !== "object") return;
160
+ if (!isRecord(input)) return void 0;
137
161
  const iss = stringClaim(input, "iss");
138
162
  const aud = stringClaim(input, "aud");
139
163
  const sub = stringClaim(input, "sub");
@@ -142,7 +166,7 @@ function tokenVerifierBaseClaims(input) {
142
166
  const exp = numberClaim(input, "exp");
143
167
  const iat = numberClaim(input, "iat");
144
168
  const jti = stringClaim(input, "jti");
145
- if (!iss || !aud || !sub || !workspace_id || !host_id || !exp || !iat || !jti) return;
169
+ if (!iss || !aud || !sub || !workspace_id || !host_id || !exp || !iat || !jti) return void 0;
146
170
  return {
147
171
  ...input,
148
172
  iss,
@@ -155,8 +179,11 @@ function tokenVerifierBaseClaims(input) {
155
179
  jti
156
180
  };
157
181
  }
158
- function clerkClaims(input) {
159
- if (!input.iss || !input.sub || !input.exp) return;
182
+ function oidcClaims(input) {
183
+ if (!input.iss || !input.sub || !input.exp) return void 0;
184
+ const sid = stringClaim(input, "sid");
185
+ const org_id = stringClaim(input, "org_id");
186
+ const orgId = stringClaim(input, "orgId");
160
187
  return {
161
188
  ...input,
162
189
  iss: input.iss,
@@ -164,9 +191,9 @@ function clerkClaims(input) {
164
191
  exp: input.exp,
165
192
  ...input.iat ? { iat: input.iat } : {},
166
193
  ...typeof input.jti === "string" ? { jti: input.jti } : {},
167
- ...stringClaim(input, "sid") ? { sid: stringClaim(input, "sid") } : {},
168
- ...stringClaim(input, "org_id") ? { org_id: stringClaim(input, "org_id") } : {},
169
- ...stringClaim(input, "orgId") ? { orgId: stringClaim(input, "orgId") } : {}
194
+ ...sid ? { sid } : {},
195
+ ...org_id ? { org_id } : {},
196
+ ...orgId ? { orgId } : {}
170
197
  };
171
198
  }
172
199
  function scopeClaims(input) {
@@ -177,39 +204,24 @@ function scopeClaims(input) {
177
204
 
178
205
  // src/index.ts
179
206
  var TUNNEL_PROTOCOL_VERSION = 1;
180
- var tunnelMessageTypes = /* @__PURE__ */ new Set([
181
- "ping",
182
- "pong",
183
- "host.registration.update",
184
- "http.request",
185
- "http.response.start",
186
- "http.response.chunk",
187
- "http.response.end",
188
- "http.response.flow",
189
- "ws.open",
190
- "ws.frame",
191
- "ws.close",
192
- "error"
193
- ]);
207
+ var SESSION_STREAM_LEASE_TTL_MS = 15e3;
208
+ var SESSION_TURN_LEASE_TTL_MS = 6e4;
194
209
  function validateTunnelMessage(input) {
195
- if (!input || typeof input !== "object") return { ok: false, reason: "invalid" };
196
- const row = input;
197
- if (row.protocol !== TUNNEL_PROTOCOL_VERSION) {
210
+ if (!isRecord(input)) return { ok: false, reason: "invalid" };
211
+ if (input.protocol !== TUNNEL_PROTOCOL_VERSION) {
198
212
  return {
199
213
  ok: false,
200
214
  reason: "protocol_mismatch",
201
215
  expected_protocol: TUNNEL_PROTOCOL_VERSION,
202
- actual_protocol: row.protocol,
203
- ...typeof row.type === "string" ? { type: row.type } : {}
216
+ actual_protocol: input.protocol,
217
+ ...typeof input.type === "string" ? { type: input.type } : {}
204
218
  };
205
219
  }
206
- if (typeof row.type !== "string") return { ok: false, reason: "invalid" };
207
- if (!tunnelMessageTypes.has(row.type)) return { ok: false, reason: "invalid" };
208
- if (!validateTunnelMessageShape(row)) return { ok: false, reason: "invalid" };
209
- return { ok: true, message: row };
220
+ if (!isTunnelMessageRecord(input)) return { ok: false, reason: "invalid" };
221
+ return { ok: true, message: input };
210
222
  }
211
223
  function isTunnelMessage(input) {
212
- return validateTunnelMessage(input).ok;
224
+ return isRecord(input) && isTunnelMessageRecord(input);
213
225
  }
214
226
  function makeTunnelPong(input, receivedAt = Date.now()) {
215
227
  return {
@@ -228,44 +240,42 @@ function makeTunnelPing(sentAt = Date.now(), id = crypto.randomUUID()) {
228
240
  sent_at: sentAt
229
241
  };
230
242
  }
231
- function validateTunnelMessageShape(row) {
243
+ function isTunnelMessageRecord(row) {
244
+ if (row.protocol !== TUNNEL_PROTOCOL_VERSION) return false;
232
245
  switch (row.type) {
233
246
  case "ping":
234
- return isString(row.id) && isFiniteNumber(row.sent_at);
247
+ return isNonEmptyString(row.id) && isFiniteNumber(row.sent_at);
235
248
  case "pong":
236
- return isString(row.id) && isFiniteNumber(row.sent_at) && isFiniteNumber(row.received_at);
249
+ return isNonEmptyString(row.id) && isFiniteNumber(row.sent_at) && isFiniteNumber(row.received_at);
237
250
  case "host.registration.update":
238
- return isStringArray(row.workspace_ids) && isString(row.token);
251
+ return isStringArray(row.workspace_ids) && isNonEmptyString(row.token);
239
252
  case "http.request":
240
- return isString(row.request_id) && isString(row.workspace_id) && isString(row.method) && isString(row.path) && isTunnelHeaderMap(row.headers) && isOptionalBase64(row.body_base64) && typeof row.end === "boolean";
253
+ return isNonEmptyString(row.request_id) && isNonEmptyString(row.workspace_id) && isNonEmptyString(row.method) && isNonEmptyString(row.path) && isTunnelHeaderMap(row.headers) && isOptionalBase64(row.body_base64) && typeof row.end === "boolean";
241
254
  case "http.response.start":
242
- return isString(row.request_id) && isHttpStatus(row.status) && isTunnelHeaderMap(row.headers);
255
+ return isNonEmptyString(row.request_id) && isHttpStatus(row.status) && isTunnelHeaderMap(row.headers);
243
256
  case "http.response.chunk":
244
- return isString(row.request_id) && isBase64(row.body_base64);
257
+ return isNonEmptyString(row.request_id) && isBase64(row.body_base64);
245
258
  case "http.response.end":
246
- return isString(row.request_id);
259
+ return isNonEmptyString(row.request_id);
247
260
  case "http.response.flow":
248
- return isString(row.request_id) && typeof row.paused === "boolean" && (row.reason === void 0 || row.reason === "slow_consumer" || row.reason === "drained" || row.reason === "closed");
261
+ return isNonEmptyString(row.request_id) && typeof row.paused === "boolean" && (row.reason === void 0 || row.reason === "slow_consumer" || row.reason === "drained" || row.reason === "closed");
249
262
  case "ws.open":
250
- return isString(row.channel_id) && isString(row.workspace_id) && isString(row.path) && isTunnelHeaderMap(row.headers);
263
+ return isNonEmptyString(row.channel_id) && isNonEmptyString(row.workspace_id) && isNonEmptyString(row.path) && isTunnelHeaderMap(row.headers);
251
264
  case "ws.frame":
252
- return isString(row.channel_id) && isBase64(row.data_base64) && typeof row.binary === "boolean";
265
+ return isNonEmptyString(row.channel_id) && isBase64(row.data_base64) && typeof row.binary === "boolean";
253
266
  case "ws.close":
254
- return isString(row.channel_id) && (row.code === void 0 || isCloseCode(row.code)) && (row.reason === void 0 || isString(row.reason));
267
+ return isNonEmptyString(row.channel_id) && (row.code === void 0 || isCloseCode(row.code)) && (row.reason === void 0 || isNonEmptyString(row.reason));
255
268
  case "error":
256
- return (row.request_id === void 0 || isString(row.request_id)) && (row.channel_id === void 0 || isString(row.channel_id)) && isString(row.code) && isString(row.message);
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);
257
270
  default:
258
271
  return false;
259
272
  }
260
273
  }
261
- function isString(input) {
274
+ function isNonEmptyString(input) {
262
275
  return typeof input === "string" && input.length > 0;
263
276
  }
264
277
  function isStringArray(input) {
265
- return Array.isArray(input) && input.length > 0 && input.every(isString);
266
- }
267
- function isFiniteNumber(input) {
268
- return typeof input === "number" && Number.isFinite(input);
278
+ return Array.isArray(input) && input.length > 0 && input.every(isNonEmptyString);
269
279
  }
270
280
  function isHttpStatus(input) {
271
281
  return typeof input === "number" && Number.isInteger(input) && input >= 100 && input <= 599;
@@ -287,10 +297,12 @@ function isBase64(input) {
287
297
  return typeof input === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(input);
288
298
  }
289
299
  export {
300
+ SESSION_STREAM_LEASE_TTL_MS,
301
+ SESSION_TURN_LEASE_TTL_MS,
290
302
  TUNNEL_PROTOCOL_VERSION,
291
303
  TokenVerifierError,
292
- createClerkTokenVerifier,
293
304
  createHttpTokenVerifier,
305
+ createOidcTokenVerifier,
294
306
  createStaticTokenVerifier,
295
307
  isTunnelMessage,
296
308
  makeTunnelPing,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Unified token-verifier interface (P-shared).
3
3
  *
4
- * Each of the four Claxedo boundaries (relay/tunnel, agent extension,
4
+ * Each of the four Claxedo boundaries (relay/tunnel, optional modules,
5
5
  * SDK runner, workspace host) historically defined its own ad-hoc
6
6
  * verifier shape. This module is the single seam every downstream user
7
7
  * can swap a custom implementation through.
@@ -23,13 +23,27 @@ export type TokenVerifierBaseClaims = {
23
23
  iat: number;
24
24
  jti: string;
25
25
  } & Record<string, unknown>;
26
- export type RuntimeAccessVerifierClaims = TokenVerifierBaseClaims & {
26
+ export type RuntimeAccessVerifierClaims = Omit<TokenVerifierBaseClaims, "sub"> & {
27
27
  org_id: string;
28
28
  role: "viewer" | "editor" | "admin" | "owner";
29
+ principal_kind: "user" | "service";
30
+ actor_id: string;
31
+ actor_kind: "human" | "agent";
32
+ actor_public_id?: string;
33
+ actor_name?: string;
34
+ actor_avatar_url?: string;
29
35
  };
30
- export type RelayHostVerifierClaims = TokenVerifierBaseClaims & {
36
+ export type RelayHostVerifierClaims = Omit<TokenVerifierBaseClaims, "sub"> & {
31
37
  org_id: string;
32
38
  role: "viewer" | "editor" | "admin" | "owner";
39
+ principal_kind: "user" | "service";
40
+ actor_id: string;
41
+ actor_kind: "human" | "agent";
42
+ actor_public_id?: string;
43
+ actor_name?: string;
44
+ actor_avatar_url?: string;
45
+ /** Durable Runtime Access Token id; distinct from this short-lived RHT jti. */
46
+ parent_jti: string;
33
47
  } & ({
34
48
  access: "cloud";
35
49
  backing: "cloud-vm";
@@ -38,9 +52,9 @@ export type RelayHostVerifierClaims = TokenVerifierBaseClaims & {
38
52
  backing: "local-worktree";
39
53
  });
40
54
  export type TokenClaims<TClaims extends Record<string, unknown> = Record<string, unknown>> = {
41
- /** Stable subject id. The boundary is responsible for mapping this
42
- * to a domain principal (user, workspace, runtime, etc.). */
43
- subject: string;
55
+ /** Provider subject for provider-backed identity verifiers. Runtime and
56
+ * relay-host tokens instead carry canonical actor/principal claims. */
57
+ subject?: string;
44
58
  /** Scopes / capabilities asserted by the issuer. May be empty. */
45
59
  scopes: string[];
46
60
  /** Full claim payload. Carries the common verifier token contract plus
@@ -50,7 +64,7 @@ export type TokenClaims<TClaims extends Record<string, unknown> = Record<string,
50
64
  export type TokenVerifier<TClaims extends Record<string, unknown> = Record<string, unknown>> = {
51
65
  verify(token: string): Promise<TokenClaims<TClaims>>;
52
66
  };
53
- export type ClerkTokenVerifierClaims = JWTPayload & {
67
+ export type OidcTokenVerifierClaims = JWTPayload & {
54
68
  iss: string;
55
69
  sub: string;
56
70
  exp: number;
@@ -83,14 +97,14 @@ export type HttpTokenVerifierOptions = {
83
97
  timeoutMs?: number;
84
98
  };
85
99
  export declare function createHttpTokenVerifier(options: HttpTokenVerifierOptions): TokenVerifier<TokenVerifierBaseClaims>;
86
- export type ClerkTokenVerifierOptions = {
100
+ export type OidcTokenVerifierOptions = {
87
101
  issuer: string;
88
102
  jwksUrl: string;
89
103
  audience?: string;
90
104
  scopes?: string[];
91
105
  algorithms?: Array<"ES256" | "EdDSA" | "RS256">;
92
106
  };
93
- export declare function createClerkTokenVerifier(options: ClerkTokenVerifierOptions): TokenVerifier<ClerkTokenVerifierClaims>;
107
+ export declare function createOidcTokenVerifier(options: OidcTokenVerifierOptions): TokenVerifier<OidcTokenVerifierClaims>;
94
108
  export type StaticTokenVerifierOptions<TClaims extends Record<string, unknown> = Record<string, unknown>> = {
95
109
  /** Map of token string → claims to return. */
96
110
  tokens: Record<string, TokenClaims<TClaims>>;
package/package.json CHANGED
@@ -1,6 +1,4 @@
1
1
  {
2
- "name": "@claxedo/workspace-relay-protocol",
3
- "version": "0.6.0",
4
2
  "type": "module",
5
3
  "description": "Wire types, message validation, and token verifier interfaces for the Claxedo workspace relay tunnel protocol",
6
4
  "license": "MIT",
@@ -19,9 +17,9 @@
19
17
  "types": "./dist/index.d.ts",
20
18
  "exports": {
21
19
  ".": {
20
+ "development": "./src/index.ts",
21
+ "bun": "./src/index.ts",
22
22
  "types": "./dist/index.d.ts",
23
- "bun": "./dist/index.mjs",
24
- "development": "./dist/index.mjs",
25
23
  "import": "./dist/index.mjs",
26
24
  "default": "./dist/index.mjs"
27
25
  }
@@ -34,14 +32,17 @@
34
32
  "publishConfig": {
35
33
  "access": "public"
36
34
  },
37
- "dependencies": {
38
- "jose": "6.0.11"
39
- },
35
+ "name": "@claxedo/workspace-relay-protocol",
36
+ "version": "0.8.0",
40
37
  "scripts": {
41
38
  "build": "tsx scripts/build.ts",
42
39
  "prepublishOnly": "npm run build",
43
40
  "typecheck": "tsc --noEmit -p tsconfig.json",
44
- "test": "bun test src"
41
+ "test": "mkdir -p .artifacts/unit && node ../../scripts/test-deadline.mjs bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml"
42
+ },
43
+ "dependencies": {
44
+ "@claxedo/helpers": "0.1.0",
45
+ "jose": "6.0.11"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@tsconfig/node-lts": "22.0.4",