remote-codex 0.11.2 → 0.11.4

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.
Files changed (37) hide show
  1. package/README.md +4 -0
  2. package/apps/relay-server/dist/index.d.ts +2 -0
  3. package/apps/relay-server/dist/index.js +1254 -0
  4. package/apps/supervisor-api/dist/chunk-ZWZQVPDT.js +27893 -0
  5. package/apps/supervisor-api/dist/index.js +4 -25183
  6. package/apps/supervisor-api/dist/worker-index.d.ts +2 -0
  7. package/apps/supervisor-api/dist/worker-index.js +197 -0
  8. package/apps/supervisor-web/dist/assets/index-CbdWtyx0.js +5 -0
  9. package/apps/supervisor-web/dist/assets/index-Di1JBevU.css +1 -0
  10. package/apps/supervisor-web/dist/assets/thread-ui-ICfwCbte.js +3604 -0
  11. package/apps/supervisor-web/dist/assets/ui-vendor-D1uxdi-d.js +430 -0
  12. package/apps/supervisor-web/dist/index.html +6 -7
  13. package/bin/remote-codex.mjs +593 -21
  14. package/package.json +42 -2
  15. package/packages/agent-runtime/src/types.ts +2 -1
  16. package/packages/codex/src/appServerManager.ts +1 -0
  17. package/packages/codex/src/historyItems.test.ts +45 -0
  18. package/packages/codex/src/historyItems.ts +22 -0
  19. package/packages/codex/src/runtimeAdapter.ts +6 -0
  20. package/packages/codex/src/types.ts +2 -1
  21. package/packages/db/migrations/0018_control_plane.sql +129 -0
  22. package/packages/db/migrations/0019_control_plane_projects.sql +19 -0
  23. package/packages/db/migrations/0020_control_workspace_status.sql +1 -0
  24. package/packages/db/migrations/0021_control_sandbox_lifecycle_fields.sql +3 -0
  25. package/packages/db/migrations/0022_control_sandbox_resource_profile.sql +1 -0
  26. package/packages/db/migrations/0023_control_usage_import_state.sql +18 -0
  27. package/packages/db/migrations/0024_control_auth.sql +23 -0
  28. package/packages/db/migrations/0025_control_harness_credentials.sql +29 -0
  29. package/packages/db/migrations/0026_control_harness_usage_events.sql +27 -0
  30. package/packages/db/src/schema.ts +305 -1
  31. package/packages/shared/src/index.ts +186 -0
  32. package/packages/shared/src/tokens.ts +137 -0
  33. package/apps/supervisor-web/dist/assets/index-CbDzXN9T.css +0 -1
  34. package/apps/supervisor-web/dist/assets/index-DQpHiQXN.js +0 -4
  35. package/apps/supervisor-web/dist/assets/thread-ui-BEieA99i.css +0 -1
  36. package/apps/supervisor-web/dist/assets/thread-ui-CDk3ExRH.js +0 -3516
  37. package/apps/supervisor-web/dist/assets/ui-vendor-CgOZX1B8.js +0 -425
@@ -1,4 +1,4 @@
1
- import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
1
+ import { integer, real, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
2
2
 
3
3
  export const hosts = sqliteTable('hosts', {
4
4
  id: text('id').primaryKey(),
@@ -176,3 +176,307 @@ export const policies = sqliteTable('policies', {
176
176
  createdAt: text('created_at').notNull(),
177
177
  updatedAt: text('updated_at').notNull()
178
178
  });
179
+
180
+ export const controlUsers = sqliteTable(
181
+ 'control_users',
182
+ {
183
+ id: text('id').primaryKey(),
184
+ authProvider: text('auth_provider').notNull(),
185
+ authSubject: text('auth_subject').notNull(),
186
+ email: text('email').notNull(),
187
+ displayName: text('display_name'),
188
+ status: text('status').notNull().default('active'),
189
+ plan: text('plan').notNull().default('developer'),
190
+ billingCustomerId: text('billing_customer_id'),
191
+ quotaProfile: text('quota_profile').notNull().default('developer'),
192
+ createdAt: text('created_at').notNull(),
193
+ updatedAt: text('updated_at').notNull(),
194
+ lastSeenAt: text('last_seen_at'),
195
+ },
196
+ (table) => ({
197
+ authSubjectUnique: uniqueIndex('control_users_auth_subject_idx').on(
198
+ table.authProvider,
199
+ table.authSubject,
200
+ ),
201
+ }),
202
+ );
203
+
204
+ export const controlAuthIdentities = sqliteTable(
205
+ 'control_auth_identities',
206
+ {
207
+ id: text('id').primaryKey(),
208
+ userId: text('user_id').notNull(),
209
+ authProvider: text('auth_provider').notNull(),
210
+ authSubject: text('auth_subject').notNull(),
211
+ email: text('email'),
212
+ displayName: text('display_name'),
213
+ createdAt: text('created_at').notNull(),
214
+ updatedAt: text('updated_at').notNull(),
215
+ },
216
+ (table) => ({
217
+ authSubjectUnique: uniqueIndex('control_auth_identities_subject_idx').on(
218
+ table.authProvider,
219
+ table.authSubject,
220
+ ),
221
+ }),
222
+ );
223
+
224
+ export const controlPasswordCredentials = sqliteTable(
225
+ 'control_password_credentials',
226
+ {
227
+ id: text('id').primaryKey(),
228
+ userId: text('user_id').notNull(),
229
+ email: text('email').notNull(),
230
+ passwordHash: text('password_hash').notNull(),
231
+ createdAt: text('created_at').notNull(),
232
+ updatedAt: text('updated_at').notNull(),
233
+ lastUsedAt: text('last_used_at'),
234
+ },
235
+ (table) => ({
236
+ emailUnique: uniqueIndex('control_password_credentials_email_idx').on(table.email),
237
+ userUnique: uniqueIndex('control_password_credentials_user_idx').on(table.userId),
238
+ }),
239
+ );
240
+
241
+ export const controlProjects = sqliteTable('control_projects', {
242
+ id: text('id').primaryKey(),
243
+ userId: text('user_id').notNull(),
244
+ name: text('name').notNull(),
245
+ slug: text('slug').notNull(),
246
+ status: text('status').notNull().default('active'),
247
+ createdAt: text('created_at').notNull(),
248
+ updatedAt: text('updated_at').notNull(),
249
+ });
250
+
251
+ export const controlSandboxes = sqliteTable('control_sandboxes', {
252
+ id: text('id').primaryKey(),
253
+ userId: text('user_id').notNull().unique(),
254
+ state: text('state').notNull(),
255
+ image: text('image').notNull(),
256
+ region: text('region').notNull(),
257
+ resourceProfile: text('resource_profile').notNull().default('standard'),
258
+ k8sNamespace: text('k8s_namespace'),
259
+ k8sPodName: text('k8s_pod_name'),
260
+ routerBaseUrl: text('router_base_url'),
261
+ workerServiceName: text('worker_service_name'),
262
+ s3Prefix: text('s3_prefix').notNull(),
263
+ gatewayKeyId: text('gateway_key_id'),
264
+ lastStartedAt: text('last_started_at'),
265
+ lastSeenAt: text('last_seen_at'),
266
+ idleTimeoutAt: text('idle_timeout_at'),
267
+ statusReason: text('status_reason'),
268
+ startupProgress: integer('startup_progress').notNull().default(0),
269
+ lastFailureCode: text('last_failure_code'),
270
+ lastFailureMessage: text('last_failure_message'),
271
+ createdAt: text('created_at').notNull(),
272
+ updatedAt: text('updated_at').notNull(),
273
+ });
274
+
275
+ export const controlWorkspaces = sqliteTable(
276
+ 'control_workspaces',
277
+ {
278
+ id: text('id').primaryKey(),
279
+ userId: text('user_id').notNull(),
280
+ projectId: text('project_id'),
281
+ sandboxId: text('sandbox_id').notNull(),
282
+ name: text('name').notNull(),
283
+ slug: text('slug').notNull(),
284
+ status: text('status').notNull().default('active'),
285
+ path: text('path').notNull(),
286
+ sourceType: text('source_type').notNull(),
287
+ gitUrl: text('git_url'),
288
+ defaultBranch: text('default_branch'),
289
+ createdAt: text('created_at').notNull(),
290
+ updatedAt: text('updated_at').notNull(),
291
+ },
292
+ (table) => ({
293
+ sandboxSlugUnique: uniqueIndex('control_workspaces_sandbox_slug_idx').on(
294
+ table.sandboxId,
295
+ table.slug,
296
+ ),
297
+ }),
298
+ );
299
+
300
+ export const controlSessions = sqliteTable('control_sessions', {
301
+ id: text('id').primaryKey(),
302
+ userId: text('user_id').notNull(),
303
+ sandboxId: text('sandbox_id').notNull(),
304
+ workspaceId: text('workspace_id').notNull(),
305
+ provider: text('provider').notNull(),
306
+ workerSessionId: text('worker_session_id'),
307
+ title: text('title').notNull(),
308
+ status: text('status').notNull(),
309
+ lastActivityAt: text('last_activity_at'),
310
+ createdAt: text('created_at').notNull(),
311
+ updatedAt: text('updated_at').notNull(),
312
+ });
313
+
314
+ export const controlGatewayUsers = sqliteTable(
315
+ 'control_gateway_users',
316
+ {
317
+ id: text('id').primaryKey(),
318
+ userId: text('user_id').notNull(),
319
+ provider: text('provider').notNull(),
320
+ externalUserId: text('external_user_id').notNull(),
321
+ createdAt: text('created_at').notNull(),
322
+ },
323
+ (table) => ({
324
+ userProviderUnique: uniqueIndex('control_gateway_users_user_provider_idx').on(
325
+ table.userId,
326
+ table.provider,
327
+ ),
328
+ providerExternalUnique: uniqueIndex('control_gateway_users_provider_external_idx').on(
329
+ table.provider,
330
+ table.externalUserId,
331
+ ),
332
+ }),
333
+ );
334
+
335
+ export const controlGatewayKeys = sqliteTable(
336
+ 'control_gateway_keys',
337
+ {
338
+ id: text('id').primaryKey(),
339
+ userId: text('user_id').notNull(),
340
+ sandboxId: text('sandbox_id').notNull(),
341
+ provider: text('provider').notNull(),
342
+ externalKeyId: text('external_key_id').notNull(),
343
+ keyCiphertext: text('key_ciphertext'),
344
+ status: text('status').notNull(),
345
+ createdAt: text('created_at').notNull(),
346
+ rotatedAt: text('rotated_at'),
347
+ revokedAt: text('revoked_at'),
348
+ },
349
+ (table) => ({
350
+ sandboxProviderUnique: uniqueIndex('control_gateway_keys_sandbox_provider_idx').on(
351
+ table.sandboxId,
352
+ table.provider,
353
+ ),
354
+ providerExternalUnique: uniqueIndex('control_gateway_keys_provider_external_idx').on(
355
+ table.provider,
356
+ table.externalKeyId,
357
+ ),
358
+ }),
359
+ );
360
+
361
+ export const controlHarnessUsers = sqliteTable(
362
+ 'control_harness_users',
363
+ {
364
+ id: text('id').primaryKey(),
365
+ userId: text('user_id').notNull(),
366
+ provider: text('provider').notNull(),
367
+ externalUserId: text('external_user_id').notNull(),
368
+ createdAt: text('created_at').notNull(),
369
+ },
370
+ (table) => ({
371
+ userProviderUnique: uniqueIndex('control_harness_users_user_provider_idx').on(
372
+ table.userId,
373
+ table.provider,
374
+ ),
375
+ providerExternalUnique: uniqueIndex('control_harness_users_provider_external_idx').on(
376
+ table.provider,
377
+ table.externalUserId,
378
+ ),
379
+ }),
380
+ );
381
+
382
+ export const controlHarnessKeys = sqliteTable(
383
+ 'control_harness_keys',
384
+ {
385
+ id: text('id').primaryKey(),
386
+ userId: text('user_id').notNull(),
387
+ sandboxId: text('sandbox_id').notNull(),
388
+ provider: text('provider').notNull(),
389
+ externalKeyId: text('external_key_id').notNull(),
390
+ keyCiphertext: text('key_ciphertext'),
391
+ secretName: text('secret_name'),
392
+ secretKey: text('secret_key'),
393
+ status: text('status').notNull(),
394
+ createdAt: text('created_at').notNull(),
395
+ rotatedAt: text('rotated_at'),
396
+ revokedAt: text('revoked_at'),
397
+ },
398
+ (table) => ({
399
+ sandboxProviderUnique: uniqueIndex('control_harness_keys_sandbox_provider_idx').on(
400
+ table.sandboxId,
401
+ table.provider,
402
+ ),
403
+ providerExternalUnique: uniqueIndex('control_harness_keys_provider_external_idx').on(
404
+ table.provider,
405
+ table.externalKeyId,
406
+ ),
407
+ }),
408
+ );
409
+
410
+ export const controlUsageEvents = sqliteTable('control_usage_events', {
411
+ id: text('id').primaryKey(),
412
+ userId: text('user_id').notNull(),
413
+ sandboxId: text('sandbox_id').notNull(),
414
+ workspaceId: text('workspace_id'),
415
+ sessionId: text('session_id'),
416
+ gatewayKeyId: text('gateway_key_id'),
417
+ provider: text('provider').notNull(),
418
+ model: text('model').notNull(),
419
+ inputTokens: integer('input_tokens').notNull().default(0),
420
+ outputTokens: integer('output_tokens').notNull().default(0),
421
+ cachedTokens: integer('cached_tokens').notNull().default(0),
422
+ costUsd: real('cost_usd').notNull().default(0),
423
+ externalRequestId: text('external_request_id'),
424
+ occurredAt: text('occurred_at').notNull(),
425
+ importedAt: text('imported_at').notNull(),
426
+ });
427
+
428
+ export const controlHarnessUsageEvents = sqliteTable(
429
+ 'control_harness_usage_events',
430
+ {
431
+ id: text('id').primaryKey(),
432
+ userId: text('user_id').notNull(),
433
+ sandboxId: text('sandbox_id').notNull(),
434
+ workspaceId: text('workspace_id'),
435
+ sessionId: text('session_id'),
436
+ provider: text('provider').notNull(),
437
+ module: text('module').notNull(),
438
+ tool: text('tool'),
439
+ runId: text('run_id'),
440
+ jobId: text('job_id'),
441
+ externalEventId: text('external_event_id'),
442
+ computeUnits: real('compute_units').notNull().default(0),
443
+ costUsd: real('cost_usd').notNull().default(0),
444
+ status: text('status').notNull().default('unknown'),
445
+ metadataJson: text('metadata_json').notNull().default('{}'),
446
+ occurredAt: text('occurred_at').notNull(),
447
+ importedAt: text('imported_at').notNull(),
448
+ },
449
+ (table) => ({
450
+ providerExternalEventUnique: uniqueIndex('control_harness_usage_provider_event_idx')
451
+ .on(table.provider, table.externalEventId),
452
+ }),
453
+ );
454
+
455
+ export const controlUsageImportState = sqliteTable('control_usage_import_state', {
456
+ id: text('id').primaryKey(),
457
+ provider: text('provider').notNull(),
458
+ source: text('source').notNull(),
459
+ cursor: text('cursor'),
460
+ lastStartedAt: text('last_started_at'),
461
+ lastSucceededAt: text('last_succeeded_at'),
462
+ lastFailedAt: text('last_failed_at'),
463
+ lastFailureMessage: text('last_failure_message'),
464
+ lastSourceCount: integer('last_source_count').notNull().default(0),
465
+ lastImportedCount: integer('last_imported_count').notNull().default(0),
466
+ lastDuplicateCount: integer('last_duplicate_count').notNull().default(0),
467
+ lastFailureCount: integer('last_failure_count').notNull().default(0),
468
+ updatedAt: text('updated_at').notNull(),
469
+ }, (table) => ({
470
+ providerSourceIdx: uniqueIndex('control_usage_import_state_provider_source_idx')
471
+ .on(table.provider, table.source),
472
+ }));
473
+
474
+ export const controlAuditLogs = sqliteTable('control_audit_logs', {
475
+ id: text('id').primaryKey(),
476
+ userId: text('user_id'),
477
+ action: text('action').notNull(),
478
+ resourceType: text('resource_type').notNull(),
479
+ resourceId: text('resource_id'),
480
+ metadataJson: text('metadata_json').notNull(),
481
+ createdAt: text('created_at').notNull(),
482
+ });
@@ -16,10 +16,17 @@ export type {
16
16
 
17
17
  export type ApiErrorCode =
18
18
  | 'bad_request'
19
+ | 'unauthorized'
19
20
  | 'not_found'
20
21
  | 'conflict'
21
22
  | 'provider_goal_error'
22
23
  | 'forbidden'
24
+ | 'unauthorized'
25
+ | 'invalid_route_token'
26
+ | 'gateway_unavailable'
27
+ | 'account_inactive'
28
+ | 'quota_exceeded'
29
+ | 'harness_unavailable'
23
30
  | 'goal_feature_disabled'
24
31
  | 'internal_error'
25
32
  | 'service_unavailable';
@@ -53,12 +60,165 @@ export function truncateAutoThreadTitle(value: string) {
53
60
  export interface RuntimeConfigDto {
54
61
  appName: string;
55
62
  appVersion: string;
63
+ mode: 'local' | 'server' | 'relay';
56
64
  host: string;
57
65
  port: number;
58
66
  workspaceRoot: string;
59
67
  environment: string;
60
68
  }
61
69
 
70
+ export interface AuthSessionDto {
71
+ authenticated: boolean;
72
+ username: string | null;
73
+ expiresAt: string | null;
74
+ mode: 'local' | 'server' | 'relay';
75
+ authRequired: boolean;
76
+ }
77
+
78
+ export interface AuthLoginResultDto {
79
+ token: string | null;
80
+ session: AuthSessionDto;
81
+ }
82
+
83
+ export interface RelayHealthDto {
84
+ status: 'ok';
85
+ supervisorConnected: boolean;
86
+ supervisorConnectedAt: string | null;
87
+ lastSupervisorHeartbeatAt: string | null;
88
+ supervisorCount?: number;
89
+ }
90
+
91
+ export type RelayUserRoleDto = 'admin' | 'user';
92
+
93
+ export interface RelayUserDto {
94
+ id: string;
95
+ email: string;
96
+ username: string;
97
+ role: RelayUserRoleDto;
98
+ enabled: boolean;
99
+ createdAt: string;
100
+ }
101
+
102
+ export interface RelayDeviceDto {
103
+ id: string;
104
+ ownerUserId: string;
105
+ name: string;
106
+ tokenPreview: string;
107
+ connected: boolean;
108
+ connectedAt: string | null;
109
+ lastHeartbeatAt: string | null;
110
+ createdAt: string;
111
+ }
112
+
113
+ export interface RelaySessionShareDto {
114
+ id: string;
115
+ ownerUserId: string;
116
+ ownerUsername: string;
117
+ targetUsername: string;
118
+ targetUserId: string;
119
+ deviceId: string;
120
+ deviceName: string;
121
+ threadId: string;
122
+ label: string | null;
123
+ createdAt: string;
124
+ revokedAt: string | null;
125
+ }
126
+
127
+ export interface RelaySessionDto {
128
+ authenticated: boolean;
129
+ user: RelayUserDto | null;
130
+ registrationEnabled: boolean;
131
+ }
132
+
133
+ export interface RelayLoginResultDto {
134
+ token: string;
135
+ session: RelaySessionDto;
136
+ }
137
+
138
+ export interface RelayRegisterResultDto {
139
+ token: string;
140
+ session: RelaySessionDto;
141
+ }
142
+
143
+ export interface RelayCreateDeviceResultDto {
144
+ device: RelayDeviceDto;
145
+ token: string;
146
+ }
147
+
148
+ export interface RelayPortalSummaryDto {
149
+ user: RelayUserDto;
150
+ devices: RelayDeviceDto[];
151
+ sharedWithMe: RelaySessionShareDto[];
152
+ sharedByMe: RelaySessionShareDto[];
153
+ }
154
+
155
+ export interface RelayAdminSummaryDto {
156
+ users: RelayUserDto[];
157
+ devices: RelayDeviceDto[];
158
+ registrationEnabled: boolean;
159
+ }
160
+
161
+ export type RelaySupervisorEnvelope =
162
+ | {
163
+ type: 'relay.connected';
164
+ timestamp: string;
165
+ deviceId?: string;
166
+ }
167
+ | {
168
+ type: 'relay.heartbeat';
169
+ timestamp: string;
170
+ deviceId?: string;
171
+ }
172
+ | {
173
+ type: 'relay.request';
174
+ timestamp: string;
175
+ requestId: string;
176
+ deviceId?: string;
177
+ payload: RelayHttpRequestPayload;
178
+ }
179
+ | {
180
+ type: 'relay.response';
181
+ timestamp: string;
182
+ requestId: string;
183
+ deviceId?: string;
184
+ payload: RelayHttpResponsePayload;
185
+ }
186
+ | {
187
+ type: 'relay.client.connected';
188
+ timestamp: string;
189
+ clientId: string;
190
+ }
191
+ | {
192
+ type: 'relay.client.disconnected';
193
+ timestamp: string;
194
+ clientId: string;
195
+ }
196
+ | {
197
+ type: 'relay.client.message';
198
+ timestamp: string;
199
+ clientId: string;
200
+ payload: SupervisorSocketClientEnvelope;
201
+ }
202
+ | {
203
+ type: 'relay.server.message';
204
+ timestamp: string;
205
+ clientId: string;
206
+ payload: SupervisorSocketServerEnvelope;
207
+ };
208
+
209
+ export interface RelayHttpRequestPayload {
210
+ method: string;
211
+ path: string;
212
+ headers: Record<string, string>;
213
+ body: string | null;
214
+ }
215
+
216
+ export interface RelayHttpResponsePayload {
217
+ statusCode: number;
218
+ headers: Record<string, string>;
219
+ body: string;
220
+ }
221
+
62
222
  export interface AgentRuntimeStatusDto {
63
223
  state: 'starting' | 'ready' | 'degraded' | 'stopped' | 'failed';
64
224
  transport: 'stdio' | 'sdk' | 'none';
@@ -290,6 +450,30 @@ export interface WorkspaceTreeDto {
290
450
  nodes: WorkspaceTreeNodeDto[];
291
451
  }
292
452
 
453
+ export interface WorkspaceFileDto {
454
+ path: string;
455
+ absPath: string;
456
+ kind: 'file' | 'directory';
457
+ size: number;
458
+ updatedAt: string;
459
+ }
460
+
461
+ export interface WriteWorkspaceFileInput {
462
+ path: string;
463
+ content: string;
464
+ }
465
+
466
+ export interface MoveWorkspaceFileInput {
467
+ fromPath: string;
468
+ toPath: string;
469
+ overwrite?: boolean;
470
+ }
471
+
472
+ export interface DeleteWorkspaceFileInput {
473
+ path: string;
474
+ recursive?: boolean;
475
+ }
476
+
293
477
  export interface ThreadWorkspaceTreeNodeDto {
294
478
  name: string;
295
479
  path: string;
@@ -508,6 +692,7 @@ export interface UpdatePluginInput {
508
692
  export interface ImportPluginInput {
509
693
  manifest?: unknown;
510
694
  manifestJson?: string;
695
+ manifestUrl?: string;
511
696
  enabled?: boolean;
512
697
  }
513
698
 
@@ -918,6 +1103,7 @@ export interface CreateThreadInput {
918
1103
  title?: string;
919
1104
  provider?: AgentBackendIdDto;
920
1105
  model: string;
1106
+ reasoningEffort?: ReasoningEffortDto | null;
921
1107
  approvalMode: ApprovalMode;
922
1108
  }
923
1109
 
@@ -0,0 +1,137 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+
3
+ function base64UrlEncode(value: Buffer | string): string {
4
+ return Buffer.from(value)
5
+ .toString('base64')
6
+ .replaceAll('+', '-')
7
+ .replaceAll('/', '_')
8
+ .replaceAll('=', '');
9
+ }
10
+
11
+ function base64UrlDecode(value: string): Buffer {
12
+ const normalized = value.replaceAll('-', '+').replaceAll('_', '/');
13
+ const padding = '='.repeat((4 - (normalized.length % 4)) % 4);
14
+ return Buffer.from(`${normalized}${padding}`, 'base64');
15
+ }
16
+
17
+ function sign(input: string, secret: string): string {
18
+ return base64UrlEncode(createHmac('sha256', secret).update(input).digest());
19
+ }
20
+
21
+ export interface RouteTokenPayload extends SignedTokenPayload {
22
+ sandbox_id: string;
23
+ project_id?: string;
24
+ workspace_id?: string;
25
+ session_id?: string;
26
+ scopes: string[];
27
+ iat: number;
28
+ jti: string;
29
+ }
30
+
31
+ export interface SignedTokenPayload {
32
+ sub: string;
33
+ iat?: number;
34
+ exp: number;
35
+ jti?: string;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export interface SigningKey {
40
+ id: string;
41
+ secret: string;
42
+ }
43
+
44
+ export function createSignedToken(
45
+ payload: SignedTokenPayload,
46
+ secret: string,
47
+ options: { kid?: string } = {},
48
+ ): string {
49
+ const header = {
50
+ alg: 'HS256',
51
+ typ: 'JWT',
52
+ ...(options.kid ? { kid: options.kid } : {}),
53
+ };
54
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
55
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
56
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
57
+ return `${signingInput}.${sign(signingInput, secret)}`;
58
+ }
59
+
60
+ function decodeHeader(token: string) {
61
+ const [encodedHeader] = token.split('.');
62
+ if (!encodedHeader) {
63
+ throw new Error('Invalid token shape.');
64
+ }
65
+ return JSON.parse(base64UrlDecode(encodedHeader).toString('utf8')) as {
66
+ alg?: string;
67
+ typ?: string;
68
+ kid?: string;
69
+ };
70
+ }
71
+
72
+ export function verifySignedToken<
73
+ TPayload extends { sub: string; exp: number } = RouteTokenPayload,
74
+ >(
75
+ token: string,
76
+ secret: string,
77
+ nowSeconds = Math.floor(Date.now() / 1000),
78
+ ) {
79
+ const parts = token.split('.');
80
+ if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
81
+ throw new Error('Invalid token shape.');
82
+ }
83
+
84
+ const signingInput = `${parts[0]}.${parts[1]}`;
85
+ const expected = sign(signingInput, secret);
86
+ const actualBuffer = Buffer.from(parts[2]);
87
+ const expectedBuffer = Buffer.from(expected);
88
+ if (
89
+ actualBuffer.length !== expectedBuffer.length ||
90
+ !timingSafeEqual(actualBuffer, expectedBuffer)
91
+ ) {
92
+ throw new Error('Invalid token signature.');
93
+ }
94
+
95
+ const payload = JSON.parse(base64UrlDecode(parts[1]).toString('utf8')) as TPayload;
96
+ if (payload.exp <= nowSeconds) {
97
+ throw new Error('Token expired.');
98
+ }
99
+
100
+ return payload;
101
+ }
102
+
103
+ export function verifySignedTokenWithKeys<
104
+ TPayload extends { sub: string; exp: number } = RouteTokenPayload,
105
+ >(
106
+ token: string,
107
+ keys: SigningKey[],
108
+ nowSeconds = Math.floor(Date.now() / 1000),
109
+ ) {
110
+ if (keys.length === 0) {
111
+ throw new Error('No signing keys configured.');
112
+ }
113
+
114
+ const header = decodeHeader(token);
115
+ if (header.alg !== 'HS256') {
116
+ throw new Error('Unsupported token algorithm.');
117
+ }
118
+
119
+ if (header.kid) {
120
+ const key = keys.find((candidate) => candidate.id === header.kid);
121
+ if (!key) {
122
+ throw new Error('Unknown token key id.');
123
+ }
124
+ return verifySignedToken<TPayload>(token, key.secret, nowSeconds);
125
+ }
126
+
127
+ let lastError: unknown = null;
128
+ for (const key of keys) {
129
+ try {
130
+ return verifySignedToken<TPayload>(token, key.secret, nowSeconds);
131
+ } catch (error) {
132
+ lastError = error;
133
+ }
134
+ }
135
+
136
+ throw lastError instanceof Error ? lastError : new Error('Invalid token.');
137
+ }