remote-codex 0.11.3 → 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.
- package/README.md +4 -0
- package/apps/relay-server/dist/index.js +35 -2
- package/apps/supervisor-api/dist/chunk-ZWZQVPDT.js +27893 -0
- package/apps/supervisor-api/dist/index.js +4 -25727
- package/apps/supervisor-api/dist/worker-index.d.ts +2 -0
- package/apps/supervisor-api/dist/worker-index.js +197 -0
- package/apps/supervisor-web/dist/assets/index-CbdWtyx0.js +5 -0
- package/apps/supervisor-web/dist/assets/index-Di1JBevU.css +1 -0
- package/apps/supervisor-web/dist/assets/thread-ui-ICfwCbte.js +3604 -0
- package/apps/supervisor-web/dist/assets/ui-vendor-D1uxdi-d.js +430 -0
- package/apps/supervisor-web/dist/index.html +6 -7
- package/bin/remote-codex.mjs +534 -19
- package/package.json +41 -2
- package/packages/agent-runtime/src/types.ts +2 -1
- package/packages/codex/src/appServerManager.ts +1 -0
- package/packages/codex/src/historyItems.test.ts +45 -0
- package/packages/codex/src/historyItems.ts +22 -0
- package/packages/codex/src/runtimeAdapter.ts +6 -0
- package/packages/codex/src/types.ts +2 -1
- package/packages/db/migrations/0018_control_plane.sql +129 -0
- package/packages/db/migrations/0019_control_plane_projects.sql +19 -0
- package/packages/db/migrations/0020_control_workspace_status.sql +1 -0
- package/packages/db/migrations/0021_control_sandbox_lifecycle_fields.sql +3 -0
- package/packages/db/migrations/0022_control_sandbox_resource_profile.sql +1 -0
- package/packages/db/migrations/0023_control_usage_import_state.sql +18 -0
- package/packages/db/migrations/0024_control_auth.sql +23 -0
- package/packages/db/migrations/0025_control_harness_credentials.sql +29 -0
- package/packages/db/migrations/0026_control_harness_usage_events.sql +27 -0
- package/packages/db/src/schema.ts +305 -1
- package/packages/shared/src/index.ts +32 -0
- package/packages/shared/src/tokens.ts +137 -0
- package/apps/supervisor-web/dist/assets/index-CBIze1VS.css +0 -1
- package/apps/supervisor-web/dist/assets/index-YpGAPjED.js +0 -4
- package/apps/supervisor-web/dist/assets/thread-ui-BEieA99i.css +0 -1
- package/apps/supervisor-web/dist/assets/thread-ui-CF80LEEN.js +0 -3613
- package/apps/supervisor-web/dist/assets/ui-vendor-CW6egZBG.js +0 -430
|
@@ -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
|
+
});
|
|
@@ -21,6 +21,12 @@ export type ApiErrorCode =
|
|
|
21
21
|
| 'conflict'
|
|
22
22
|
| 'provider_goal_error'
|
|
23
23
|
| 'forbidden'
|
|
24
|
+
| 'unauthorized'
|
|
25
|
+
| 'invalid_route_token'
|
|
26
|
+
| 'gateway_unavailable'
|
|
27
|
+
| 'account_inactive'
|
|
28
|
+
| 'quota_exceeded'
|
|
29
|
+
| 'harness_unavailable'
|
|
24
30
|
| 'goal_feature_disabled'
|
|
25
31
|
| 'internal_error'
|
|
26
32
|
| 'service_unavailable';
|
|
@@ -444,6 +450,30 @@ export interface WorkspaceTreeDto {
|
|
|
444
450
|
nodes: WorkspaceTreeNodeDto[];
|
|
445
451
|
}
|
|
446
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
|
+
|
|
447
477
|
export interface ThreadWorkspaceTreeNodeDto {
|
|
448
478
|
name: string;
|
|
449
479
|
path: string;
|
|
@@ -662,6 +692,7 @@ export interface UpdatePluginInput {
|
|
|
662
692
|
export interface ImportPluginInput {
|
|
663
693
|
manifest?: unknown;
|
|
664
694
|
manifestJson?: string;
|
|
695
|
+
manifestUrl?: string;
|
|
665
696
|
enabled?: boolean;
|
|
666
697
|
}
|
|
667
698
|
|
|
@@ -1072,6 +1103,7 @@ export interface CreateThreadInput {
|
|
|
1072
1103
|
title?: string;
|
|
1073
1104
|
provider?: AgentBackendIdDto;
|
|
1074
1105
|
model: string;
|
|
1106
|
+
reasoningEffort?: ReasoningEffortDto | null;
|
|
1075
1107
|
approvalMode: ApprovalMode;
|
|
1076
1108
|
}
|
|
1077
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
|
+
}
|