@treeseed/core 0.4.2 → 0.4.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 (64) hide show
  1. package/README.md +7 -1
  2. package/dist/api/agent-routes.d.ts +13 -0
  3. package/dist/api/agent-routes.js +402 -0
  4. package/dist/api/app.d.ts +5 -0
  5. package/dist/api/app.js +270 -0
  6. package/dist/api/auth/d1-database.d.ts +3 -0
  7. package/dist/api/auth/d1-database.js +24 -0
  8. package/dist/api/auth/d1-provider.d.ts +67 -0
  9. package/dist/api/auth/d1-provider.js +84 -0
  10. package/dist/api/auth/d1-store.d.ts +97 -0
  11. package/dist/api/auth/d1-store.js +631 -0
  12. package/dist/api/auth/memory-provider.d.ts +73 -0
  13. package/dist/api/auth/memory-provider.js +239 -0
  14. package/dist/api/auth/rbac.d.ts +22 -0
  15. package/dist/api/auth/rbac.js +158 -0
  16. package/dist/api/auth/tokens.d.ts +18 -0
  17. package/dist/api/auth/tokens.js +56 -0
  18. package/dist/api/config.d.ts +2 -0
  19. package/dist/api/config.js +65 -0
  20. package/dist/api/gateway.d.ts +5 -0
  21. package/dist/api/gateway.js +35 -0
  22. package/dist/api/http.d.ts +24 -0
  23. package/dist/api/http.js +44 -0
  24. package/dist/api/index.d.ts +9 -0
  25. package/dist/api/index.js +18 -0
  26. package/dist/api/operations-routes.d.ts +6 -0
  27. package/dist/api/operations-routes.js +34 -0
  28. package/dist/api/operations.d.ts +3 -0
  29. package/dist/api/operations.js +26 -0
  30. package/dist/api/providers.d.ts +2 -0
  31. package/dist/api/providers.js +61 -0
  32. package/dist/api/railway.d.ts +45 -0
  33. package/dist/api/railway.js +69 -0
  34. package/dist/api/sdk-dispatch.d.ts +14 -0
  35. package/dist/api/sdk-dispatch.js +145 -0
  36. package/dist/api/sdk-routes.d.ts +10 -0
  37. package/dist/api/sdk-routes.js +25 -0
  38. package/dist/api/server.d.ts +2 -0
  39. package/dist/api/server.js +10 -0
  40. package/dist/api/templates.d.ts +3 -0
  41. package/dist/api/templates.js +31 -0
  42. package/dist/api/types.d.ts +193 -0
  43. package/dist/api/types.js +0 -0
  44. package/dist/api.d.ts +1 -0
  45. package/dist/api.js +1 -0
  46. package/dist/dev.d.ts +41 -0
  47. package/dist/dev.js +189 -0
  48. package/dist/index.d.ts +9 -0
  49. package/dist/index.js +23 -1
  50. package/dist/platform-resources.d.ts +37 -0
  51. package/dist/platform-resources.js +133 -0
  52. package/dist/platform.d.ts +2 -0
  53. package/dist/platform.js +16 -0
  54. package/dist/plugin-default.d.ts +1 -0
  55. package/dist/plugin-default.js +4 -0
  56. package/dist/railway.d.ts +1 -0
  57. package/dist/railway.js +4 -0
  58. package/dist/scripts/build-dist.js +7 -0
  59. package/dist/scripts/dev-platform.js +24 -0
  60. package/dist/scripts/workspace-bootstrap.js +0 -1
  61. package/dist/site-resources.d.ts +1 -29
  62. package/dist/site-resources.js +7 -120
  63. package/dist/site.js +3 -1
  64. package/package.json +37 -3
@@ -0,0 +1,631 @@
1
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { DEFAULT_PERMISSIONS, DEFAULT_ROLES } from "./rbac.js";
3
+ import { createAccessToken, nextOpaqueToken, principalFromAccessTokenPayload, verifyAccessToken } from "./tokens.js";
4
+ function now() {
5
+ return /* @__PURE__ */ new Date();
6
+ }
7
+ function isoNow() {
8
+ return now().toISOString();
9
+ }
10
+ function addSeconds(date, seconds) {
11
+ return new Date(date.getTime() + seconds * 1e3);
12
+ }
13
+ function parseJson(value, fallback) {
14
+ if (!value) return fallback;
15
+ try {
16
+ return JSON.parse(value);
17
+ } catch {
18
+ return fallback;
19
+ }
20
+ }
21
+ function stableHash(value, secret) {
22
+ return createHash("sha256").update(`${secret}:${value}`).digest("hex");
23
+ }
24
+ function equalHash(left, right) {
25
+ const leftBuffer = Buffer.from(left);
26
+ const rightBuffer = Buffer.from(right);
27
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
28
+ }
29
+ class D1AuthStore {
30
+ constructor(config, db) {
31
+ this.config = config;
32
+ this.db = db;
33
+ }
34
+ config;
35
+ db;
36
+ initializationPromise = null;
37
+ async run(query, params = []) {
38
+ await this.db.prepare(query).bind(...params).run();
39
+ }
40
+ async first(query, params = []) {
41
+ return this.db.prepare(query).bind(...params).first();
42
+ }
43
+ async all(query, params = []) {
44
+ const result = await this.db.prepare(query).bind(...params).all();
45
+ return result.results ?? [];
46
+ }
47
+ ensureInitialized() {
48
+ if (!this.initializationPromise) {
49
+ this.initializationPromise = this.seedCatalog().then(() => this.seedConfiguredServices());
50
+ }
51
+ return this.initializationPromise;
52
+ }
53
+ async seedCatalog() {
54
+ const createdAt = isoNow();
55
+ for (const permission of DEFAULT_PERMISSIONS) {
56
+ await this.run(
57
+ `INSERT OR IGNORE INTO permissions (id, key, resource, action, scope, description, created_at)
58
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
59
+ [randomUUID(), permission.key, permission.resource, permission.action, permission.scope, permission.description, createdAt]
60
+ );
61
+ }
62
+ for (const role of DEFAULT_ROLES) {
63
+ await this.run(
64
+ `INSERT OR IGNORE INTO roles (id, key, description, created_at)
65
+ VALUES (?, ?, ?, ?)`,
66
+ [randomUUID(), role.key, role.description, createdAt]
67
+ );
68
+ const roleRow = await this.first(`SELECT id FROM roles WHERE key = ?`, [role.key]);
69
+ if (!roleRow) continue;
70
+ for (const permissionKey of role.permissions) {
71
+ const permissionRow = await this.first(`SELECT id FROM permissions WHERE key = ?`, [permissionKey]);
72
+ if (permissionRow) {
73
+ await this.run(
74
+ `INSERT OR IGNORE INTO role_permissions (role_id, permission_id, created_at)
75
+ VALUES (?, ?, ?)`,
76
+ [roleRow.id, permissionRow.id, createdAt]
77
+ );
78
+ }
79
+ }
80
+ }
81
+ }
82
+ async seedConfiguredServices() {
83
+ if (!this.config.webServiceSecret) return;
84
+ await this.upsertServiceCredential({
85
+ serviceId: this.config.webServiceId,
86
+ name: "Trusted web tier",
87
+ secret: this.config.webServiceSecret,
88
+ roles: ["market_admin"],
89
+ permissions: ["services:impersonate:global"]
90
+ });
91
+ }
92
+ async loadUser(userId) {
93
+ return this.first(`SELECT * FROM users WHERE id = ?`, [userId]);
94
+ }
95
+ async loadIdentityByProvider(provider, providerSubject) {
96
+ return this.first(
97
+ `SELECT * FROM user_identities WHERE provider = ? AND provider_subject = ?`,
98
+ [provider, providerSubject]
99
+ );
100
+ }
101
+ async rolesForUser(userId) {
102
+ const rows = await this.all(
103
+ `SELECT roles.key AS key
104
+ FROM user_role_bindings
105
+ INNER JOIN roles ON roles.id = user_role_bindings.role_id
106
+ WHERE user_role_bindings.user_id = ?`,
107
+ [userId]
108
+ );
109
+ return rows.map((row) => row.key);
110
+ }
111
+ async permissionsForUser(userId) {
112
+ const rows = await this.all(
113
+ `SELECT DISTINCT permissions.key AS key
114
+ FROM user_role_bindings
115
+ INNER JOIN role_permissions ON role_permissions.role_id = user_role_bindings.role_id
116
+ INNER JOIN permissions ON permissions.id = role_permissions.permission_id
117
+ WHERE user_role_bindings.user_id = ?`,
118
+ [userId]
119
+ );
120
+ return rows.map((row) => row.key);
121
+ }
122
+ scopesForPrincipal(permissions) {
123
+ const scopes = /* @__PURE__ */ new Set(["auth:me"]);
124
+ if (permissions.includes("*:*:*") || permissions.includes("sdk:execute:global")) scopes.add("sdk");
125
+ if (permissions.includes("*:*:*") || permissions.includes("agent:execute:global")) scopes.add("agent");
126
+ if (permissions.includes("*:*:*") || permissions.includes("operations:execute:global")) scopes.add("operations");
127
+ return [...scopes];
128
+ }
129
+ async principalForUser(userId) {
130
+ const user = await this.loadUser(userId);
131
+ if (!user) {
132
+ throw new Error(`Unknown user "${userId}".`);
133
+ }
134
+ const roles = await this.rolesForUser(userId);
135
+ const permissions = await this.permissionsForUser(userId);
136
+ return {
137
+ userId,
138
+ principal: {
139
+ id: user.id,
140
+ displayName: user.display_name ?? void 0,
141
+ roles,
142
+ permissions,
143
+ scopes: this.scopesForPrincipal(permissions),
144
+ metadata: parseJson(user.metadata_json, {})
145
+ }
146
+ };
147
+ }
148
+ async assignRole(userId, roleKey) {
149
+ const role = await this.first(`SELECT id FROM roles WHERE key = ?`, [roleKey]);
150
+ if (!role) return;
151
+ await this.run(
152
+ `INSERT OR IGNORE INTO user_role_bindings (id, user_id, role_id, created_at)
153
+ VALUES (?, ?, ?, ?)`,
154
+ [randomUUID(), userId, role.id, isoNow()]
155
+ );
156
+ }
157
+ async bootstrapRolesForUser(userId, identity) {
158
+ await this.assignRole(userId, "member");
159
+ if ((await this.rolesForUser(userId)).includes("platform_admin")) return;
160
+ const allowlist = this.config.bootstrapAdminAllowlist;
161
+ const email = identity.email?.trim().toLowerCase() ?? "";
162
+ const providerSubject = `${identity.provider}:${identity.providerSubject}`;
163
+ if (allowlist.includes(email) || allowlist.includes(providerSubject)) {
164
+ await this.assignRole(userId, "platform_admin");
165
+ await this.writeAuditEvent({
166
+ actorType: "system",
167
+ actorId: null,
168
+ eventType: "auth.bootstrap_admin",
169
+ targetType: "user",
170
+ targetId: userId,
171
+ data: { matched: allowlist.includes(providerSubject) ? providerSubject : email }
172
+ });
173
+ }
174
+ }
175
+ async writeAuditEvent(input) {
176
+ await this.run(
177
+ `INSERT INTO audit_events (id, actor_type, actor_id, event_type, target_type, target_id, data_json, created_at)
178
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
179
+ [
180
+ randomUUID(),
181
+ input.actorType,
182
+ input.actorId,
183
+ input.eventType,
184
+ input.targetType,
185
+ input.targetId,
186
+ JSON.stringify(input.data ?? {}),
187
+ isoNow()
188
+ ]
189
+ );
190
+ }
191
+ async syncUser(identity) {
192
+ await this.ensureInitialized();
193
+ const nowIso = isoNow();
194
+ const existingIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject);
195
+ let userId = existingIdentity?.user_id;
196
+ if (!userId) {
197
+ userId = randomUUID();
198
+ await this.run(
199
+ `INSERT INTO users (id, email, display_name, status, metadata_json, created_at, updated_at)
200
+ VALUES (?, ?, ?, 'active', ?, ?, ?)`,
201
+ [
202
+ userId,
203
+ identity.email ?? null,
204
+ identity.displayName ?? null,
205
+ JSON.stringify({ emailVerified: identity.emailVerified ?? false, authProvider: identity.provider }),
206
+ nowIso,
207
+ nowIso
208
+ ]
209
+ );
210
+ await this.run(
211
+ `INSERT INTO user_identities (id, user_id, provider, provider_subject, email, email_verified, profile_json, created_at, updated_at)
212
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
213
+ [
214
+ randomUUID(),
215
+ userId,
216
+ identity.provider,
217
+ identity.providerSubject,
218
+ identity.email ?? null,
219
+ identity.emailVerified ? 1 : 0,
220
+ JSON.stringify(identity.profile ?? {}),
221
+ nowIso,
222
+ nowIso
223
+ ]
224
+ );
225
+ } else {
226
+ await this.run(
227
+ `UPDATE users
228
+ SET email = COALESCE(?, email),
229
+ display_name = COALESCE(?, display_name),
230
+ metadata_json = ?,
231
+ updated_at = ?
232
+ WHERE id = ?`,
233
+ [
234
+ identity.email ?? null,
235
+ identity.displayName ?? null,
236
+ JSON.stringify({ emailVerified: identity.emailVerified ?? false, authProvider: identity.provider }),
237
+ nowIso,
238
+ userId
239
+ ]
240
+ );
241
+ await this.run(
242
+ `UPDATE user_identities
243
+ SET email = ?, email_verified = ?, profile_json = ?, updated_at = ?
244
+ WHERE provider = ? AND provider_subject = ?`,
245
+ [
246
+ identity.email ?? null,
247
+ identity.emailVerified ? 1 : 0,
248
+ JSON.stringify(identity.profile ?? {}),
249
+ nowIso,
250
+ identity.provider,
251
+ identity.providerSubject
252
+ ]
253
+ );
254
+ }
255
+ await this.bootstrapRolesForUser(userId, identity);
256
+ await this.writeAuditEvent({
257
+ actorType: "service",
258
+ actorId: this.config.webServiceId,
259
+ eventType: "auth.user_synced",
260
+ targetType: "user",
261
+ targetId: userId,
262
+ data: { provider: identity.provider }
263
+ });
264
+ const principal = await this.principalForUser(userId);
265
+ const syncedIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject);
266
+ return {
267
+ ...principal,
268
+ identityId: syncedIdentity?.id ?? null
269
+ };
270
+ }
271
+ async startDeviceFlow(request) {
272
+ await this.ensureInitialized();
273
+ const current = now();
274
+ const expiresAt = addSeconds(current, this.config.deviceCodeTtlSeconds);
275
+ const deviceCode = nextOpaqueToken("device");
276
+ const userCode = `${Math.random().toString(36).slice(2, 6).toUpperCase()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;
277
+ await this.run(
278
+ `INSERT INTO device_codes (id, device_code, user_code, requested_scopes_json, expires_at, interval_seconds, status, user_id, created_at, updated_at)
279
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?, ?)`,
280
+ [
281
+ randomUUID(),
282
+ deviceCode,
283
+ userCode,
284
+ JSON.stringify(request.scopes?.length ? request.scopes : ["auth:me"]),
285
+ expiresAt.toISOString(),
286
+ this.config.deviceCodePollIntervalSeconds,
287
+ current.toISOString(),
288
+ current.toISOString()
289
+ ]
290
+ );
291
+ return {
292
+ ok: true,
293
+ deviceCode,
294
+ userCode,
295
+ verificationUri: `${this.config.baseUrl}/auth/device/approve`,
296
+ verificationUriComplete: `${this.config.baseUrl}/auth/device/approve?user_code=${encodeURIComponent(userCode)}`,
297
+ intervalSeconds: this.config.deviceCodePollIntervalSeconds,
298
+ expiresAt: expiresAt.toISOString(),
299
+ expiresInSeconds: this.config.deviceCodeTtlSeconds
300
+ };
301
+ }
302
+ async approveDeviceFlow(request) {
303
+ await this.ensureInitialized();
304
+ const row = await this.first(`SELECT * FROM device_codes WHERE user_code = ?`, [request.userCode]);
305
+ if (!row || new Date(row.expires_at).getTime() <= Date.now()) {
306
+ throw new Error("Device code approval failed because the user code is unknown or expired.");
307
+ }
308
+ let userId = request.principalId;
309
+ if (!await this.loadUser(userId)) {
310
+ const createdAt = isoNow();
311
+ await this.run(
312
+ `INSERT INTO users (id, email, display_name, status, metadata_json, created_at, updated_at)
313
+ VALUES (?, NULL, ?, 'active', ?, ?, ?)`,
314
+ [userId, request.displayName ?? null, JSON.stringify(request.metadata ?? {}), createdAt, createdAt]
315
+ );
316
+ await this.assignRole(userId, "member");
317
+ }
318
+ await this.run(`UPDATE device_codes SET status = 'approved', user_id = ?, updated_at = ? WHERE id = ?`, [userId, isoNow(), row.id]);
319
+ await this.writeAuditEvent({
320
+ actorType: "user",
321
+ actorId: userId,
322
+ eventType: "auth.device_approved",
323
+ targetType: "device_code",
324
+ targetId: row.id
325
+ });
326
+ return { ok: true };
327
+ }
328
+ async pollDeviceFlow(request) {
329
+ await this.ensureInitialized();
330
+ const row = await this.first(`SELECT * FROM device_codes WHERE device_code = ?`, [request.deviceCode]);
331
+ if (!row) {
332
+ return { ok: false, status: "invalid", error: "Unknown device code." };
333
+ }
334
+ if (new Date(row.expires_at).getTime() <= Date.now()) {
335
+ return { ok: false, status: "expired", error: "Device code expired." };
336
+ }
337
+ if (row.status === "pending" || !row.user_id) {
338
+ return { ok: true, status: "pending", intervalSeconds: row.interval_seconds };
339
+ }
340
+ if (row.status === "used") {
341
+ return { ok: false, status: "already_used", error: "Device code already used." };
342
+ }
343
+ await this.run(`UPDATE device_codes SET status = 'used', updated_at = ? WHERE id = ?`, [isoNow(), row.id]);
344
+ const principalRecord = await this.principalForUser(row.user_id);
345
+ const refreshToken = nextOpaqueToken("refresh");
346
+ const sessionId = randomUUID();
347
+ const refreshTokenHash = stableHash(refreshToken, this.config.authSecret);
348
+ const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds);
349
+ const refreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds);
350
+ await this.run(
351
+ `INSERT INTO auth_sessions (id, user_id, session_type, refresh_token_hash, scopes_json, expires_at, revoked_at, data_json, created_at, updated_at)
352
+ VALUES (?, ?, 'device', ?, ?, ?, NULL, ?, ?, ?)`,
353
+ [
354
+ sessionId,
355
+ row.user_id,
356
+ refreshTokenHash,
357
+ row.requested_scopes_json,
358
+ refreshExpiresAt.toISOString(),
359
+ JSON.stringify({ deviceCodeId: row.id }),
360
+ isoNow(),
361
+ isoNow()
362
+ ]
363
+ );
364
+ const requestedScopes = parseJson(row.requested_scopes_json, principalRecord.principal.scopes);
365
+ const accessToken = createAccessToken({
366
+ sub: principalRecord.principal.id,
367
+ displayName: principalRecord.principal.displayName,
368
+ scopes: requestedScopes,
369
+ roles: principalRecord.principal.roles,
370
+ permissions: principalRecord.principal.permissions,
371
+ metadata: principalRecord.principal.metadata,
372
+ iat: Math.floor(Date.now() / 1e3),
373
+ exp: Math.floor(expiresAt.getTime() / 1e3),
374
+ iss: this.config.issuer,
375
+ jti: randomUUID(),
376
+ tokenType: "access"
377
+ }, this.config.authSecret);
378
+ return {
379
+ ok: true,
380
+ status: "approved",
381
+ accessToken,
382
+ refreshToken,
383
+ tokenType: "Bearer",
384
+ expiresAt: expiresAt.toISOString(),
385
+ expiresInSeconds: this.config.accessTokenTtlSeconds,
386
+ principal: {
387
+ ...principalRecord.principal,
388
+ scopes: requestedScopes
389
+ }
390
+ };
391
+ }
392
+ async refreshAccessToken(request) {
393
+ await this.ensureInitialized();
394
+ const refreshHash = stableHash(request.refreshToken, this.config.authSecret);
395
+ const row = await this.first(
396
+ `SELECT * FROM auth_sessions WHERE refresh_token_hash = ? AND revoked_at IS NULL`,
397
+ [refreshHash]
398
+ );
399
+ if (!row || new Date(row.expires_at).getTime() <= Date.now()) {
400
+ throw new Error("Refresh token is invalid or expired.");
401
+ }
402
+ const principalRecord = await this.principalForUser(row.user_id);
403
+ const nextRefreshToken = nextOpaqueToken("refresh");
404
+ const nextRefreshHash = stableHash(nextRefreshToken, this.config.authSecret);
405
+ const nextRefreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds);
406
+ await this.run(
407
+ `UPDATE auth_sessions SET refresh_token_hash = ?, expires_at = ?, updated_at = ? WHERE id = ?`,
408
+ [nextRefreshHash, nextRefreshExpiresAt.toISOString(), isoNow(), row.id]
409
+ );
410
+ const requestedScopes = parseJson(row.scopes_json, principalRecord.principal.scopes);
411
+ const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds);
412
+ const accessToken = createAccessToken({
413
+ sub: principalRecord.principal.id,
414
+ displayName: principalRecord.principal.displayName,
415
+ scopes: requestedScopes,
416
+ roles: principalRecord.principal.roles,
417
+ permissions: principalRecord.principal.permissions,
418
+ metadata: principalRecord.principal.metadata,
419
+ iat: Math.floor(Date.now() / 1e3),
420
+ exp: Math.floor(expiresAt.getTime() / 1e3),
421
+ iss: this.config.issuer,
422
+ jti: randomUUID(),
423
+ tokenType: "access"
424
+ }, this.config.authSecret);
425
+ return {
426
+ ok: true,
427
+ accessToken,
428
+ refreshToken: nextRefreshToken,
429
+ tokenType: "Bearer",
430
+ expiresAt: expiresAt.toISOString(),
431
+ expiresInSeconds: this.config.accessTokenTtlSeconds,
432
+ principal: {
433
+ ...principalRecord.principal,
434
+ scopes: requestedScopes
435
+ }
436
+ };
437
+ }
438
+ async createPersonalAccessToken(userId, input) {
439
+ await this.ensureInitialized();
440
+ const nowIso = isoNow();
441
+ const token = nextOpaqueToken("pat");
442
+ const id = randomUUID();
443
+ const tokenHash = stableHash(token, this.config.authSecret);
444
+ const prefix = token.slice(0, 12);
445
+ await this.run(
446
+ `INSERT INTO api_tokens (id, user_id, kind, name, token_prefix, token_hash, scopes_json, expires_at, last_used_at, revoked_at, metadata_json, created_at, updated_at)
447
+ VALUES (?, ?, 'personal_access_token', ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?)`,
448
+ [
449
+ id,
450
+ userId,
451
+ input.name,
452
+ prefix,
453
+ tokenHash,
454
+ JSON.stringify(input.scopes?.length ? input.scopes : ["auth:me"]),
455
+ input.expiresAt ?? null,
456
+ JSON.stringify({}),
457
+ nowIso,
458
+ nowIso
459
+ ]
460
+ );
461
+ await this.writeAuditEvent({
462
+ actorType: "user",
463
+ actorId: userId,
464
+ eventType: "auth.pat_created",
465
+ targetType: "api_token",
466
+ targetId: id,
467
+ data: { name: input.name }
468
+ });
469
+ return { id, token, prefix, name: input.name, expiresAt: input.expiresAt ?? null };
470
+ }
471
+ async listPersonalAccessTokens(userId) {
472
+ await this.ensureInitialized();
473
+ return this.all(
474
+ `SELECT id, name, token_prefix, expires_at, last_used_at, revoked_at, created_at
475
+ FROM api_tokens
476
+ WHERE user_id = ?
477
+ ORDER BY created_at DESC`,
478
+ [userId]
479
+ );
480
+ }
481
+ async revokePersonalAccessToken(userId, tokenId) {
482
+ await this.ensureInitialized();
483
+ await this.run(`UPDATE api_tokens SET revoked_at = ? WHERE id = ? AND user_id = ?`, [isoNow(), tokenId, userId]);
484
+ await this.writeAuditEvent({
485
+ actorType: "user",
486
+ actorId: userId,
487
+ eventType: "auth.pat_revoked",
488
+ targetType: "api_token",
489
+ targetId: tokenId
490
+ });
491
+ }
492
+ async upsertServiceCredential(input) {
493
+ const nowIso = isoNow();
494
+ const existing = await this.first(`SELECT id FROM service_credentials WHERE service_id = ?`, [input.serviceId]);
495
+ const secretHash = stableHash(input.secret, this.config.authSecret);
496
+ if (existing) {
497
+ await this.run(
498
+ `UPDATE service_credentials
499
+ SET name = ?, secret_hash = ?, roles_json = ?, permissions_json = ?, revoked_at = NULL, updated_at = ?
500
+ WHERE id = ?`,
501
+ [input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, existing.id]
502
+ );
503
+ return existing.id;
504
+ }
505
+ const id = randomUUID();
506
+ await this.run(
507
+ `INSERT INTO service_credentials (id, service_id, name, secret_hash, roles_json, permissions_json, revoked_at, last_used_at, created_at, updated_at)
508
+ VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`,
509
+ [id, input.serviceId, input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, nowIso]
510
+ );
511
+ return id;
512
+ }
513
+ async createServiceCredential(input) {
514
+ await this.ensureInitialized();
515
+ const secret = nextOpaqueToken("svc");
516
+ const id = await this.upsertServiceCredential({ ...input, secret });
517
+ return { id, serviceId: input.serviceId, secret };
518
+ }
519
+ async rotateServiceCredential(serviceId) {
520
+ await this.ensureInitialized();
521
+ const row = await this.first(
522
+ `SELECT name, roles_json, permissions_json FROM service_credentials WHERE service_id = ? AND revoked_at IS NULL`,
523
+ [serviceId]
524
+ );
525
+ if (!row) {
526
+ throw new Error(`Unknown active service credential "${serviceId}".`);
527
+ }
528
+ return this.createServiceCredential({
529
+ serviceId,
530
+ name: row.name,
531
+ roles: parseJson(row.roles_json, []),
532
+ permissions: parseJson(row.permissions_json, [])
533
+ });
534
+ }
535
+ async authenticateBearerToken(token) {
536
+ await this.ensureInitialized();
537
+ const patHash = stableHash(token, this.config.authSecret);
538
+ const pat = await this.first(
539
+ `SELECT id, user_id, name, scopes_json, expires_at, revoked_at
540
+ FROM api_tokens
541
+ WHERE token_hash = ?`,
542
+ [patHash]
543
+ );
544
+ if (pat && !pat.revoked_at && (!pat.expires_at || new Date(pat.expires_at).getTime() > Date.now())) {
545
+ await this.run(`UPDATE api_tokens SET last_used_at = ? WHERE id = ?`, [isoNow(), pat.id]);
546
+ const principal = (await this.principalForUser(pat.user_id)).principal;
547
+ return {
548
+ principal: { ...principal, scopes: parseJson(pat.scopes_json, principal.scopes) },
549
+ credential: { type: "personal_access_token", id: pat.id, label: pat.name }
550
+ };
551
+ }
552
+ const payload = verifyAccessToken(token, this.config.authSecret);
553
+ if (!payload) return null;
554
+ return {
555
+ principal: principalFromAccessTokenPayload(payload),
556
+ credential: {
557
+ type: payload.tokenType === "service" ? "service_token" : "access_token",
558
+ id: payload.jti,
559
+ label: payload.tokenType
560
+ }
561
+ };
562
+ }
563
+ async authenticateService(serviceId, secret) {
564
+ await this.ensureInitialized();
565
+ const row = await this.first(
566
+ `SELECT id, name, secret_hash, roles_json, permissions_json, revoked_at
567
+ FROM service_credentials
568
+ WHERE service_id = ?`,
569
+ [serviceId]
570
+ );
571
+ if (!row || row.revoked_at) return null;
572
+ const incomingHash = stableHash(secret, this.config.authSecret);
573
+ if (!equalHash(row.secret_hash, incomingHash)) return null;
574
+ await this.run(`UPDATE service_credentials SET last_used_at = ?, updated_at = ? WHERE id = ?`, [isoNow(), isoNow(), row.id]);
575
+ const roles = parseJson(row.roles_json, []);
576
+ const permissions = parseJson(row.permissions_json, []);
577
+ return {
578
+ principal: {
579
+ id: serviceId,
580
+ displayName: row.name,
581
+ roles,
582
+ permissions,
583
+ scopes: this.scopesForPrincipal(permissions),
584
+ metadata: { serviceId }
585
+ },
586
+ credential: { type: "service_secret", id: row.id, label: row.name }
587
+ };
588
+ }
589
+ async exchangeTrustedUserAssertion(claims) {
590
+ await this.ensureInitialized();
591
+ const principalRecord = await this.principalForUser(claims.userId);
592
+ const expiresAt = addSeconds(now(), this.config.webExchangeTtlSeconds);
593
+ const accessToken = createAccessToken({
594
+ sub: principalRecord.principal.id,
595
+ displayName: principalRecord.principal.displayName,
596
+ scopes: principalRecord.principal.scopes,
597
+ roles: principalRecord.principal.roles,
598
+ permissions: principalRecord.principal.permissions,
599
+ metadata: {
600
+ ...principalRecord.principal.metadata,
601
+ actingSessionId: claims.sessionId,
602
+ identityId: claims.identityId,
603
+ authTime: claims.authTime
604
+ },
605
+ iat: Math.floor(Date.now() / 1e3),
606
+ exp: Math.floor(expiresAt.getTime() / 1e3),
607
+ iss: this.config.issuer,
608
+ jti: randomUUID(),
609
+ tokenType: "access"
610
+ }, this.config.authSecret);
611
+ await this.writeAuditEvent({
612
+ actorType: "service",
613
+ actorId: this.config.webServiceId,
614
+ eventType: "auth.web_exchange",
615
+ targetType: "user",
616
+ targetId: claims.userId,
617
+ data: { sessionId: claims.sessionId }
618
+ });
619
+ return {
620
+ ok: true,
621
+ accessToken,
622
+ tokenType: "Bearer",
623
+ expiresAt: expiresAt.toISOString(),
624
+ expiresInSeconds: this.config.webExchangeTtlSeconds,
625
+ principal: principalRecord.principal
626
+ };
627
+ }
628
+ }
629
+ export {
630
+ D1AuthStore
631
+ };
@@ -0,0 +1,73 @@
1
+ import type { ApiAuthProvider, ApiConfig, ApiPrincipal, DeviceCodeApproveRequest, DeviceCodePollRequest, DeviceCodePollResponse, DeviceCodeStartRequest, DeviceCodeStartResponse, TokenRefreshRequest, TokenRefreshResponse, TrustedUserAssertionClaims, UserIdentityProfileInput } from '../types.ts';
2
+ export declare class MemoryDeviceCodeAuthProvider implements ApiAuthProvider {
3
+ private readonly config;
4
+ readonly id = "memory";
5
+ private readonly devices;
6
+ private readonly refreshSessions;
7
+ constructor(config: ApiConfig);
8
+ startDeviceFlow(request: DeviceCodeStartRequest): Promise<DeviceCodeStartResponse>;
9
+ approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{
10
+ ok: true;
11
+ }>;
12
+ pollDeviceFlow(request: DeviceCodePollRequest): Promise<DeviceCodePollResponse>;
13
+ refreshAccessToken(request: TokenRefreshRequest): Promise<TokenRefreshResponse>;
14
+ authenticateBearerToken(token: string): Promise<{
15
+ principal: ApiPrincipal;
16
+ credential: {
17
+ type: "access_token";
18
+ id: string;
19
+ label: "service" | "access";
20
+ };
21
+ }>;
22
+ authenticateServiceCredential(_serviceId: string, _secret: string): Promise<any>;
23
+ createPersonalAccessToken(_userId: string, _input: {
24
+ name: string;
25
+ scopes?: string[];
26
+ expiresAt?: string | null;
27
+ }): Promise<{
28
+ id: string;
29
+ token: string;
30
+ prefix: string;
31
+ name: string;
32
+ expiresAt: string | null;
33
+ }>;
34
+ listPersonalAccessTokens(): Promise<any[]>;
35
+ revokePersonalAccessToken(): Promise<void>;
36
+ syncUserIdentity(identity: UserIdentityProfileInput): Promise<{
37
+ userId: string;
38
+ identityId: string;
39
+ principal: {
40
+ id: string;
41
+ displayName: string;
42
+ scopes: string[];
43
+ roles: string[];
44
+ permissions: string[];
45
+ metadata: Record<string, unknown>;
46
+ };
47
+ }>;
48
+ createServiceToken(_input: {
49
+ serviceId: string;
50
+ name: string;
51
+ roles?: string[];
52
+ permissions?: string[];
53
+ }): Promise<{
54
+ id: string;
55
+ serviceId: string;
56
+ secret: string;
57
+ }>;
58
+ rotateServiceToken(_serviceId: string): Promise<{
59
+ id: string;
60
+ serviceId: string;
61
+ secret: string;
62
+ }>;
63
+ createTrustedUserAssertion(claims: TrustedUserAssertionClaims): string;
64
+ verifyTrustedUserAssertion(assertion: string): TrustedUserAssertionClaims;
65
+ exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims): Promise<{
66
+ ok: true;
67
+ accessToken: string;
68
+ tokenType: "Bearer";
69
+ expiresAt: string;
70
+ expiresInSeconds: number;
71
+ principal: ApiPrincipal;
72
+ }>;
73
+ }