@treeseed/sdk 0.9.0 → 0.10.6

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 (76) hide show
  1. package/dist/api/app.d.ts +8 -0
  2. package/dist/api/app.js +404 -0
  3. package/dist/api/auth/d1-database.d.ts +3 -0
  4. package/dist/api/auth/d1-database.js +20 -0
  5. package/dist/api/auth/d1-provider.d.ts +79 -0
  6. package/dist/api/auth/d1-provider.js +92 -0
  7. package/dist/api/auth/d1-store.d.ts +114 -0
  8. package/dist/api/auth/d1-store.js +902 -0
  9. package/dist/api/auth/memory-provider.d.ts +77 -0
  10. package/dist/api/auth/memory-provider.js +256 -0
  11. package/dist/api/auth/rbac.d.ts +22 -0
  12. package/dist/api/auth/rbac.js +162 -0
  13. package/dist/api/auth/tokens.d.ts +18 -0
  14. package/dist/api/auth/tokens.js +56 -0
  15. package/dist/api/config.d.ts +2 -0
  16. package/dist/api/config.js +118 -0
  17. package/dist/api/http.d.ts +28 -0
  18. package/dist/api/http.js +51 -0
  19. package/dist/api/index.d.ts +10 -0
  20. package/dist/api/index.js +27 -0
  21. package/dist/api/operations-routes.d.ts +11 -0
  22. package/dist/api/operations-routes.js +39 -0
  23. package/dist/api/operations.d.ts +3 -0
  24. package/dist/api/operations.js +26 -0
  25. package/dist/api/providers.d.ts +2 -0
  26. package/dist/api/providers.js +68 -0
  27. package/dist/api/railway.d.ts +52 -0
  28. package/dist/api/railway.js +71 -0
  29. package/dist/api/sdk-dispatch.d.ts +6 -0
  30. package/dist/api/sdk-dispatch.js +14 -0
  31. package/dist/api/sdk-routes.d.ts +11 -0
  32. package/dist/api/sdk-routes.js +29 -0
  33. package/dist/api/templates.d.ts +3 -0
  34. package/dist/api/templates.js +31 -0
  35. package/dist/api/types.d.ts +232 -0
  36. package/dist/api/types.js +0 -0
  37. package/dist/capacity-provider.d.ts +383 -0
  38. package/dist/capacity-provider.js +535 -0
  39. package/dist/capacity.d.ts +2 -35
  40. package/dist/control-plane-client.d.ts +8 -3
  41. package/dist/control-plane-client.js +12 -1
  42. package/dist/dispatch.js +0 -1
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +40 -0
  45. package/dist/market-client.d.ts +1 -5
  46. package/dist/market-client.js +2 -8
  47. package/dist/operations/providers/default.js +0 -9
  48. package/dist/operations/services/config-runtime.d.ts +2 -2
  49. package/dist/operations/services/config-runtime.js +55 -3
  50. package/dist/operations/services/github-automation.d.ts +10 -15
  51. package/dist/operations/services/github-automation.js +3 -35
  52. package/dist/operations/services/hosting-audit.d.ts +1 -1
  53. package/dist/operations/services/hosting-audit.js +3 -27
  54. package/dist/operations/services/hub-launch.d.ts +0 -1
  55. package/dist/operations/services/hub-launch.js +1 -2
  56. package/dist/operations/services/hub-provider-launch.d.ts +0 -15
  57. package/dist/operations/services/hub-provider-launch.js +5 -41
  58. package/dist/operations/services/package-reference-policy.d.ts +1 -0
  59. package/dist/operations/services/package-reference-policy.js +10 -2
  60. package/dist/operations/services/project-platform.d.ts +9 -9
  61. package/dist/operations/services/project-platform.js +6 -17
  62. package/dist/operations/services/release-candidate.js +19 -3
  63. package/dist/operations-registry.js +1 -3
  64. package/dist/platform/contracts.d.ts +2 -2
  65. package/dist/project-workflow.d.ts +0 -3
  66. package/dist/scripts/publish-package.js +5 -1
  67. package/dist/scripts/tenant-workflow-action.js +3 -3
  68. package/dist/scripts/workflow-commands.test.js +3 -6
  69. package/dist/sdk-types.d.ts +33 -1
  70. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +1 -4
  71. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +1 -1
  72. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +1 -17
  73. package/dist/workflow/operations.js +26 -8
  74. package/package.json +14 -1
  75. package/templates/github/hosted-project.workflow.yml +0 -1
  76. package/templates/github/deploy-processing.workflow.yml +0 -123
@@ -0,0 +1,902 @@
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 approvalUrl(baseUrl, userCode) {
5
+ const url = new URL("/auth/device/approve", `${baseUrl.replace(/\/+$/u, "")}/`);
6
+ if (userCode) {
7
+ url.searchParams.set("user_code", userCode);
8
+ }
9
+ return url.toString();
10
+ }
11
+ const AUTH_SCHEMA_SQL = [
12
+ `CREATE TABLE IF NOT EXISTS users (
13
+ id TEXT PRIMARY KEY,
14
+ email TEXT,
15
+ username TEXT UNIQUE,
16
+ display_name TEXT,
17
+ status TEXT NOT NULL DEFAULT 'active',
18
+ metadata_json TEXT,
19
+ created_at TEXT NOT NULL,
20
+ updated_at TEXT NOT NULL
21
+ )`,
22
+ `CREATE TABLE IF NOT EXISTS user_identities (
23
+ id TEXT PRIMARY KEY,
24
+ user_id TEXT NOT NULL,
25
+ provider TEXT NOT NULL,
26
+ provider_subject TEXT NOT NULL,
27
+ email TEXT,
28
+ email_verified INTEGER NOT NULL DEFAULT 0,
29
+ profile_json TEXT,
30
+ created_at TEXT NOT NULL,
31
+ updated_at TEXT NOT NULL,
32
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
33
+ )`,
34
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_user_identities_provider_subject
35
+ ON user_identities(provider, provider_subject)`,
36
+ `CREATE TABLE IF NOT EXISTS roles (
37
+ id TEXT PRIMARY KEY,
38
+ key TEXT NOT NULL UNIQUE,
39
+ description TEXT,
40
+ created_at TEXT NOT NULL
41
+ )`,
42
+ `CREATE TABLE IF NOT EXISTS permissions (
43
+ id TEXT PRIMARY KEY,
44
+ key TEXT NOT NULL UNIQUE,
45
+ resource TEXT NOT NULL,
46
+ action TEXT NOT NULL,
47
+ scope TEXT NOT NULL,
48
+ description TEXT,
49
+ created_at TEXT NOT NULL
50
+ )`,
51
+ `CREATE TABLE IF NOT EXISTS role_permissions (
52
+ role_id TEXT NOT NULL,
53
+ permission_id TEXT NOT NULL,
54
+ created_at TEXT NOT NULL,
55
+ PRIMARY KEY (role_id, permission_id),
56
+ FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
57
+ FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
58
+ )`,
59
+ `CREATE TABLE IF NOT EXISTS user_role_bindings (
60
+ id TEXT PRIMARY KEY,
61
+ user_id TEXT NOT NULL,
62
+ role_id TEXT NOT NULL,
63
+ created_at TEXT NOT NULL,
64
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
65
+ FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
66
+ )`,
67
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_user_role_bindings_user_role
68
+ ON user_role_bindings(user_id, role_id)`,
69
+ `CREATE TABLE IF NOT EXISTS api_tokens (
70
+ id TEXT PRIMARY KEY,
71
+ user_id TEXT NOT NULL,
72
+ kind TEXT NOT NULL,
73
+ name TEXT NOT NULL,
74
+ token_prefix TEXT NOT NULL,
75
+ token_hash TEXT NOT NULL,
76
+ scopes_json TEXT NOT NULL,
77
+ expires_at TEXT,
78
+ last_used_at TEXT,
79
+ revoked_at TEXT,
80
+ metadata_json TEXT,
81
+ created_at TEXT NOT NULL,
82
+ updated_at TEXT NOT NULL,
83
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
84
+ )`,
85
+ `CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id
86
+ ON api_tokens(user_id)`,
87
+ `CREATE INDEX IF NOT EXISTS idx_api_tokens_prefix
88
+ ON api_tokens(token_prefix)`,
89
+ `CREATE TABLE IF NOT EXISTS service_credentials (
90
+ id TEXT PRIMARY KEY,
91
+ service_id TEXT NOT NULL UNIQUE,
92
+ name TEXT NOT NULL,
93
+ secret_hash TEXT NOT NULL,
94
+ roles_json TEXT NOT NULL,
95
+ permissions_json TEXT NOT NULL,
96
+ revoked_at TEXT,
97
+ created_at TEXT NOT NULL,
98
+ updated_at TEXT NOT NULL,
99
+ last_used_at TEXT
100
+ )`,
101
+ `CREATE TABLE IF NOT EXISTS auth_sessions (
102
+ id TEXT PRIMARY KEY,
103
+ user_id TEXT NOT NULL,
104
+ session_type TEXT NOT NULL,
105
+ refresh_token_hash TEXT NOT NULL,
106
+ scopes_json TEXT NOT NULL,
107
+ expires_at TEXT NOT NULL,
108
+ revoked_at TEXT,
109
+ data_json TEXT,
110
+ created_at TEXT NOT NULL,
111
+ updated_at TEXT NOT NULL,
112
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
113
+ )`,
114
+ `CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id
115
+ ON auth_sessions(user_id)`,
116
+ `CREATE TABLE IF NOT EXISTS audit_events (
117
+ id TEXT PRIMARY KEY,
118
+ actor_type TEXT NOT NULL,
119
+ actor_id TEXT,
120
+ event_type TEXT NOT NULL,
121
+ target_type TEXT,
122
+ target_id TEXT,
123
+ data_json TEXT,
124
+ created_at TEXT NOT NULL
125
+ )`,
126
+ `CREATE INDEX IF NOT EXISTS idx_audit_events_target
127
+ ON audit_events(target_type, target_id)`,
128
+ `CREATE TABLE IF NOT EXISTS device_codes (
129
+ id TEXT PRIMARY KEY,
130
+ device_code TEXT NOT NULL UNIQUE,
131
+ user_code TEXT NOT NULL UNIQUE,
132
+ requested_scopes_json TEXT NOT NULL,
133
+ expires_at TEXT NOT NULL,
134
+ interval_seconds INTEGER NOT NULL,
135
+ status TEXT NOT NULL,
136
+ user_id TEXT,
137
+ created_at TEXT NOT NULL,
138
+ updated_at TEXT NOT NULL
139
+ )`
140
+ ];
141
+ function now() {
142
+ return /* @__PURE__ */ new Date();
143
+ }
144
+ function isoNow() {
145
+ return now().toISOString();
146
+ }
147
+ function addSeconds(date, seconds) {
148
+ return new Date(date.getTime() + seconds * 1e3);
149
+ }
150
+ function parseJson(value, fallback) {
151
+ if (!value) return fallback;
152
+ try {
153
+ return JSON.parse(value);
154
+ } catch {
155
+ return fallback;
156
+ }
157
+ }
158
+ function stableHash(value, secret) {
159
+ return createHash("sha256").update(`${secret}:${value}`).digest("hex");
160
+ }
161
+ function equalHash(left, right) {
162
+ const leftBuffer = Buffer.from(left);
163
+ const rightBuffer = Buffer.from(right);
164
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
165
+ }
166
+ class D1AuthStore {
167
+ constructor(config, db) {
168
+ this.config = config;
169
+ this.db = db;
170
+ }
171
+ config;
172
+ db;
173
+ initializationPromise = null;
174
+ async run(query, params = []) {
175
+ await this.db.prepare(query).bind(...params).run();
176
+ }
177
+ async first(query, params = []) {
178
+ return this.db.prepare(query).bind(...params).first();
179
+ }
180
+ async all(query, params = []) {
181
+ const result = await this.db.prepare(query).bind(...params).all();
182
+ return result.results ?? [];
183
+ }
184
+ ensureInitialized() {
185
+ if (!this.initializationPromise) {
186
+ this.initializationPromise = this.ensureAuthSchema().then(() => this.seedCatalog()).then(() => this.seedConfiguredServices());
187
+ }
188
+ return this.initializationPromise;
189
+ }
190
+ async ensureAuthSchema() {
191
+ for (const statement of AUTH_SCHEMA_SQL) await this.run(statement);
192
+ const result = await this.db.prepare("PRAGMA table_info(users)").all();
193
+ const columns = new Set((result.results ?? []).map((row) => row.name));
194
+ if (!columns.has("username")) {
195
+ await this.run("ALTER TABLE users ADD COLUMN username TEXT");
196
+ }
197
+ await this.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username)");
198
+ }
199
+ async seedCatalog() {
200
+ const createdAt = isoNow();
201
+ const seeded = await this.first(
202
+ `SELECT key FROM permissions WHERE key = '*:*:*' LIMIT 1`
203
+ );
204
+ const adminRole = await this.first(
205
+ `SELECT key FROM roles WHERE key = 'platform_admin' LIMIT 1`
206
+ );
207
+ if (seeded?.key && adminRole?.key) {
208
+ return;
209
+ }
210
+ for (const permission of DEFAULT_PERMISSIONS) {
211
+ await this.run(
212
+ `INSERT OR IGNORE INTO permissions (id, key, resource, action, scope, description, created_at)
213
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
214
+ [randomUUID(), permission.key, permission.resource, permission.action, permission.scope, permission.description, createdAt]
215
+ );
216
+ }
217
+ for (const role of DEFAULT_ROLES) {
218
+ await this.run(
219
+ `INSERT OR IGNORE INTO roles (id, key, description, created_at)
220
+ VALUES (?, ?, ?, ?)`,
221
+ [randomUUID(), role.key, role.description, createdAt]
222
+ );
223
+ const roleRow = await this.first(`SELECT id FROM roles WHERE key = ?`, [role.key]);
224
+ if (!roleRow) continue;
225
+ for (const permissionKey of role.permissions) {
226
+ const permissionRow = await this.first(`SELECT id FROM permissions WHERE key = ?`, [permissionKey]);
227
+ if (permissionRow) {
228
+ await this.run(
229
+ `INSERT OR IGNORE INTO role_permissions (role_id, permission_id, created_at)
230
+ VALUES (?, ?, ?)`,
231
+ [roleRow.id, permissionRow.id, createdAt]
232
+ );
233
+ }
234
+ }
235
+ }
236
+ }
237
+ async seedConfiguredServices() {
238
+ if (!this.config.webServiceSecret) return;
239
+ await this.upsertServiceCredential({
240
+ serviceId: this.config.webServiceId,
241
+ name: "Trusted web tier",
242
+ secret: this.config.webServiceSecret,
243
+ roles: ["market_admin"],
244
+ permissions: ["services:impersonate:global"]
245
+ });
246
+ }
247
+ async loadUser(userId) {
248
+ return this.first(`SELECT * FROM users WHERE id = ?`, [userId]);
249
+ }
250
+ async loadIdentityByProvider(provider, providerSubject) {
251
+ return this.first(
252
+ `SELECT * FROM user_identities WHERE provider = ? AND provider_subject = ?`,
253
+ [provider, providerSubject]
254
+ );
255
+ }
256
+ async loadUserByVerifiedEmail(email) {
257
+ return this.first(
258
+ `SELECT * FROM users WHERE LOWER(email) = LOWER(?) AND status = 'active' LIMIT 1`,
259
+ [email]
260
+ );
261
+ }
262
+ async rolesForUser(userId) {
263
+ const rows = await this.all(
264
+ `SELECT roles.key AS key
265
+ FROM user_role_bindings
266
+ INNER JOIN roles ON roles.id = user_role_bindings.role_id
267
+ WHERE user_role_bindings.user_id = ?`,
268
+ [userId]
269
+ );
270
+ return rows.map((row) => row.key);
271
+ }
272
+ async permissionsForUser(userId) {
273
+ const rows = await this.all(
274
+ `SELECT DISTINCT permissions.key AS key
275
+ FROM user_role_bindings
276
+ INNER JOIN role_permissions ON role_permissions.role_id = user_role_bindings.role_id
277
+ INNER JOIN permissions ON permissions.id = role_permissions.permission_id
278
+ WHERE user_role_bindings.user_id = ?`,
279
+ [userId]
280
+ );
281
+ return rows.map((row) => row.key);
282
+ }
283
+ async permissionsForRoles(roleKeys) {
284
+ if (roleKeys.length === 0) {
285
+ return [];
286
+ }
287
+ const placeholders = roleKeys.map(() => "?").join(", ");
288
+ const rows = await this.all(
289
+ `SELECT DISTINCT permissions.key AS key
290
+ FROM roles
291
+ INNER JOIN role_permissions ON role_permissions.role_id = roles.id
292
+ INNER JOIN permissions ON permissions.id = role_permissions.permission_id
293
+ WHERE roles.key IN (${placeholders})`,
294
+ roleKeys
295
+ );
296
+ return rows.map((row) => row.key);
297
+ }
298
+ scopesForPrincipal(permissions) {
299
+ const scopes = /* @__PURE__ */ new Set(["auth:me"]);
300
+ if (permissions.includes("*:*:*") || permissions.includes("sdk:execute:global")) scopes.add("sdk");
301
+ if (permissions.includes("*:*:*") || permissions.includes("agent:execute:global")) scopes.add("agent");
302
+ if (permissions.includes("*:*:*") || permissions.includes("operations:execute:global")) scopes.add("operations");
303
+ return [...scopes];
304
+ }
305
+ async principalForUser(userId) {
306
+ const user = await this.loadUser(userId);
307
+ if (!user) {
308
+ throw new Error(`Unknown user "${userId}".`);
309
+ }
310
+ const roles = await this.rolesForUser(userId);
311
+ const permissions = await this.permissionsForUser(userId);
312
+ return {
313
+ userId,
314
+ principal: {
315
+ id: user.id,
316
+ displayName: user.display_name ?? void 0,
317
+ roles,
318
+ permissions,
319
+ scopes: this.scopesForPrincipal(permissions),
320
+ metadata: {
321
+ ...parseJson(user.metadata_json, {}),
322
+ username: user.username ?? void 0
323
+ }
324
+ }
325
+ };
326
+ }
327
+ async assignRole(userId, roleKey) {
328
+ const role = await this.first(`SELECT id FROM roles WHERE key = ?`, [roleKey]);
329
+ if (!role) return;
330
+ await this.run(
331
+ `INSERT OR IGNORE INTO user_role_bindings (id, user_id, role_id, created_at)
332
+ VALUES (?, ?, ?, ?)`,
333
+ [randomUUID(), userId, role.id, isoNow()]
334
+ );
335
+ }
336
+ async replaceRoles(userId, roleKeys) {
337
+ await this.run(`DELETE FROM user_role_bindings WHERE user_id = ?`, [userId]);
338
+ for (const roleKey of roleKeys) {
339
+ await this.assignRole(userId, roleKey);
340
+ }
341
+ }
342
+ async bootstrapRolesForUser(userId, identity) {
343
+ await this.assignRole(userId, "member");
344
+ if ((await this.rolesForUser(userId)).includes("platform_admin")) return;
345
+ const allowlist = this.config.bootstrapAdminAllowlist;
346
+ const email = identity.email?.trim().toLowerCase() ?? "";
347
+ const providerSubject = `${identity.provider}:${identity.providerSubject}`;
348
+ if (allowlist.includes(email) || allowlist.includes(providerSubject)) {
349
+ await this.assignRole(userId, "platform_admin");
350
+ await this.writeAuditEvent({
351
+ actorType: "system",
352
+ actorId: null,
353
+ eventType: "auth.bootstrap_admin",
354
+ targetType: "user",
355
+ targetId: userId,
356
+ data: { matched: allowlist.includes(providerSubject) ? providerSubject : email }
357
+ });
358
+ }
359
+ }
360
+ async writeAuditEvent(input) {
361
+ await this.run(
362
+ `INSERT INTO audit_events (id, actor_type, actor_id, event_type, target_type, target_id, data_json, created_at)
363
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
364
+ [
365
+ randomUUID(),
366
+ input.actorType,
367
+ input.actorId,
368
+ input.eventType,
369
+ input.targetType,
370
+ input.targetId,
371
+ JSON.stringify(input.data ?? {}),
372
+ isoNow()
373
+ ]
374
+ );
375
+ }
376
+ userMetadata(identity, existingUsername = null) {
377
+ const profile = identity.profile ?? {};
378
+ return {
379
+ emailVerified: identity.emailVerified ?? false,
380
+ authProvider: identity.provider,
381
+ username: identity.username ?? existingUsername,
382
+ firstName: typeof profile.firstName === "string" ? profile.firstName : null,
383
+ lastName: typeof profile.lastName === "string" ? profile.lastName : null
384
+ };
385
+ }
386
+ async syncUser(identity) {
387
+ await this.ensureInitialized();
388
+ const nowIso = isoNow();
389
+ const existingIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject);
390
+ let userId = existingIdentity?.user_id;
391
+ if (!userId) {
392
+ const linkedUser = identity.email && identity.emailVerified ? await this.loadUserByVerifiedEmail(identity.email) : null;
393
+ userId = linkedUser?.id ?? randomUUID();
394
+ if (linkedUser) {
395
+ await this.run(
396
+ `UPDATE users
397
+ SET email = COALESCE(?, email),
398
+ username = COALESCE(username, ?),
399
+ display_name = COALESCE(?, display_name),
400
+ metadata_json = ?,
401
+ updated_at = ?
402
+ WHERE id = ?`,
403
+ [
404
+ identity.email ?? null,
405
+ identity.username ?? null,
406
+ identity.displayName ?? null,
407
+ JSON.stringify(this.userMetadata(identity, linkedUser.username ?? null)),
408
+ nowIso,
409
+ userId
410
+ ]
411
+ );
412
+ } else {
413
+ await this.run(
414
+ `INSERT INTO users (id, email, username, display_name, status, metadata_json, created_at, updated_at)
415
+ VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`,
416
+ [
417
+ userId,
418
+ identity.email ?? null,
419
+ identity.username ?? null,
420
+ identity.displayName ?? null,
421
+ JSON.stringify(this.userMetadata(identity)),
422
+ nowIso,
423
+ nowIso
424
+ ]
425
+ );
426
+ }
427
+ await this.run(
428
+ `INSERT INTO user_identities (id, user_id, provider, provider_subject, email, email_verified, profile_json, created_at, updated_at)
429
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
430
+ [
431
+ randomUUID(),
432
+ userId,
433
+ identity.provider,
434
+ identity.providerSubject,
435
+ identity.email ?? null,
436
+ identity.emailVerified ? 1 : 0,
437
+ JSON.stringify(identity.profile ?? {}),
438
+ nowIso,
439
+ nowIso
440
+ ]
441
+ );
442
+ } else {
443
+ await this.run(
444
+ `UPDATE users
445
+ SET email = COALESCE(?, email),
446
+ username = COALESCE(username, ?),
447
+ display_name = COALESCE(?, display_name),
448
+ metadata_json = ?,
449
+ updated_at = ?
450
+ WHERE id = ?`,
451
+ [
452
+ identity.email ?? null,
453
+ identity.username ?? null,
454
+ identity.displayName ?? null,
455
+ JSON.stringify(this.userMetadata(identity)),
456
+ nowIso,
457
+ userId
458
+ ]
459
+ );
460
+ await this.run(
461
+ `UPDATE user_identities
462
+ SET email = ?, email_verified = ?, profile_json = ?, updated_at = ?
463
+ WHERE provider = ? AND provider_subject = ?`,
464
+ [
465
+ identity.email ?? null,
466
+ identity.emailVerified ? 1 : 0,
467
+ JSON.stringify(identity.profile ?? {}),
468
+ nowIso,
469
+ identity.provider,
470
+ identity.providerSubject
471
+ ]
472
+ );
473
+ }
474
+ await this.bootstrapRolesForUser(userId, identity);
475
+ await this.writeAuditEvent({
476
+ actorType: "service",
477
+ actorId: this.config.webServiceId,
478
+ eventType: "auth.user_synced",
479
+ targetType: "user",
480
+ targetId: userId,
481
+ data: { provider: identity.provider }
482
+ });
483
+ const principal = await this.principalForUser(userId);
484
+ const syncedIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject);
485
+ return {
486
+ ...principal,
487
+ identityId: syncedIdentity?.id ?? null
488
+ };
489
+ }
490
+ async createUser(input) {
491
+ await this.ensureInitialized();
492
+ const timestamp = isoNow();
493
+ const userId = randomUUID();
494
+ await this.run(
495
+ `INSERT INTO users (id, email, username, display_name, status, metadata_json, created_at, updated_at)
496
+ VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`,
497
+ [
498
+ userId,
499
+ input.email?.trim() || null,
500
+ input.username?.trim().toLowerCase() || null,
501
+ input.displayName?.trim() || null,
502
+ JSON.stringify(input.metadata ?? {}),
503
+ timestamp,
504
+ timestamp
505
+ ]
506
+ );
507
+ await this.assignRole(userId, "member");
508
+ await this.writeAuditEvent({
509
+ actorType: "service",
510
+ actorId: this.config.webServiceId,
511
+ eventType: "auth.user_created",
512
+ targetType: "user",
513
+ targetId: userId,
514
+ data: { source: "admin" }
515
+ });
516
+ return this.principalForUser(userId);
517
+ }
518
+ async setUserRoles(userId, roles) {
519
+ await this.ensureInitialized();
520
+ const requestedRoles = [...new Set(roles.map((role) => role.trim()).filter(Boolean))];
521
+ await this.replaceRoles(userId, requestedRoles.length > 0 ? requestedRoles : ["member"]);
522
+ await this.writeAuditEvent({
523
+ actorType: "service",
524
+ actorId: this.config.webServiceId,
525
+ eventType: "auth.user_roles_set",
526
+ targetType: "user",
527
+ targetId: userId,
528
+ data: { roles: requestedRoles }
529
+ });
530
+ return this.principalForUser(userId);
531
+ }
532
+ async startDeviceFlow(request) {
533
+ await this.ensureInitialized();
534
+ const current = now();
535
+ const expiresAt = addSeconds(current, this.config.deviceCodeTtlSeconds);
536
+ const deviceCode = nextOpaqueToken("device");
537
+ const userCode = `${Math.random().toString(36).slice(2, 6).toUpperCase()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;
538
+ await this.run(
539
+ `INSERT INTO device_codes (id, device_code, user_code, requested_scopes_json, expires_at, interval_seconds, status, user_id, created_at, updated_at)
540
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?, ?)`,
541
+ [
542
+ randomUUID(),
543
+ deviceCode,
544
+ userCode,
545
+ JSON.stringify(request.scopes?.length ? request.scopes : ["auth:me"]),
546
+ expiresAt.toISOString(),
547
+ this.config.deviceCodePollIntervalSeconds,
548
+ current.toISOString(),
549
+ current.toISOString()
550
+ ]
551
+ );
552
+ return {
553
+ ok: true,
554
+ deviceCode,
555
+ userCode,
556
+ verificationUri: approvalUrl(this.config.baseUrl),
557
+ verificationUriComplete: approvalUrl(this.config.baseUrl, userCode),
558
+ intervalSeconds: this.config.deviceCodePollIntervalSeconds,
559
+ expiresAt: expiresAt.toISOString(),
560
+ expiresInSeconds: this.config.deviceCodeTtlSeconds
561
+ };
562
+ }
563
+ async approveDeviceFlow(request) {
564
+ await this.ensureInitialized();
565
+ const row = await this.first(`SELECT * FROM device_codes WHERE user_code = ?`, [request.userCode]);
566
+ if (!row || new Date(row.expires_at).getTime() <= Date.now()) {
567
+ throw new Error("Device code approval failed because the user code is unknown or expired.");
568
+ }
569
+ let userId = request.principalId;
570
+ if (!await this.loadUser(userId)) {
571
+ const createdAt = isoNow();
572
+ await this.run(
573
+ `INSERT INTO users (id, email, display_name, status, metadata_json, created_at, updated_at)
574
+ VALUES (?, NULL, ?, 'active', ?, ?, ?)`,
575
+ [userId, request.displayName ?? null, JSON.stringify(request.metadata ?? {}), createdAt, createdAt]
576
+ );
577
+ await this.assignRole(userId, "member");
578
+ }
579
+ await this.run(`UPDATE device_codes SET status = 'approved', user_id = ?, updated_at = ? WHERE id = ?`, [userId, isoNow(), row.id]);
580
+ await this.writeAuditEvent({
581
+ actorType: "user",
582
+ actorId: userId,
583
+ eventType: "auth.device_approved",
584
+ targetType: "device_code",
585
+ targetId: row.id
586
+ });
587
+ return { ok: true };
588
+ }
589
+ async pollDeviceFlow(request) {
590
+ await this.ensureInitialized();
591
+ const row = await this.first(`SELECT * FROM device_codes WHERE device_code = ?`, [request.deviceCode]);
592
+ if (!row) {
593
+ return { ok: false, status: "invalid", error: "Unknown device code." };
594
+ }
595
+ if (new Date(row.expires_at).getTime() <= Date.now()) {
596
+ return { ok: false, status: "expired", error: "Device code expired." };
597
+ }
598
+ if (row.status === "pending" || !row.user_id) {
599
+ return { ok: true, status: "pending", intervalSeconds: row.interval_seconds };
600
+ }
601
+ if (row.status === "used") {
602
+ return { ok: false, status: "already_used", error: "Device code already used." };
603
+ }
604
+ await this.run(`UPDATE device_codes SET status = 'used', updated_at = ? WHERE id = ?`, [isoNow(), row.id]);
605
+ const principalRecord = await this.principalForUser(row.user_id);
606
+ const refreshToken = nextOpaqueToken("refresh");
607
+ const sessionId = randomUUID();
608
+ const refreshTokenHash = stableHash(refreshToken, this.config.authSecret);
609
+ const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds);
610
+ const refreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds);
611
+ await this.run(
612
+ `INSERT INTO auth_sessions (id, user_id, session_type, refresh_token_hash, scopes_json, expires_at, revoked_at, data_json, created_at, updated_at)
613
+ VALUES (?, ?, 'device', ?, ?, ?, NULL, ?, ?, ?)`,
614
+ [
615
+ sessionId,
616
+ row.user_id,
617
+ refreshTokenHash,
618
+ row.requested_scopes_json,
619
+ refreshExpiresAt.toISOString(),
620
+ JSON.stringify({ deviceCodeId: row.id }),
621
+ isoNow(),
622
+ isoNow()
623
+ ]
624
+ );
625
+ const requestedScopes = parseJson(row.requested_scopes_json, principalRecord.principal.scopes);
626
+ const accessToken = createAccessToken({
627
+ sub: principalRecord.principal.id,
628
+ displayName: principalRecord.principal.displayName,
629
+ scopes: requestedScopes,
630
+ roles: principalRecord.principal.roles,
631
+ permissions: principalRecord.principal.permissions,
632
+ metadata: principalRecord.principal.metadata,
633
+ iat: Math.floor(Date.now() / 1e3),
634
+ exp: Math.floor(expiresAt.getTime() / 1e3),
635
+ iss: this.config.issuer,
636
+ jti: randomUUID(),
637
+ tokenType: "access"
638
+ }, this.config.authSecret);
639
+ return {
640
+ ok: true,
641
+ status: "approved",
642
+ accessToken,
643
+ refreshToken,
644
+ tokenType: "Bearer",
645
+ expiresAt: expiresAt.toISOString(),
646
+ expiresInSeconds: this.config.accessTokenTtlSeconds,
647
+ principal: {
648
+ ...principalRecord.principal,
649
+ scopes: requestedScopes
650
+ }
651
+ };
652
+ }
653
+ async refreshAccessToken(request) {
654
+ await this.ensureInitialized();
655
+ const refreshHash = stableHash(request.refreshToken, this.config.authSecret);
656
+ const row = await this.first(
657
+ `SELECT * FROM auth_sessions WHERE refresh_token_hash = ? AND revoked_at IS NULL`,
658
+ [refreshHash]
659
+ );
660
+ if (!row || new Date(row.expires_at).getTime() <= Date.now()) {
661
+ throw new Error("Refresh token is invalid or expired.");
662
+ }
663
+ const principalRecord = await this.principalForUser(row.user_id);
664
+ const nextRefreshToken = nextOpaqueToken("refresh");
665
+ const nextRefreshHash = stableHash(nextRefreshToken, this.config.authSecret);
666
+ const nextRefreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds);
667
+ await this.run(
668
+ `UPDATE auth_sessions SET refresh_token_hash = ?, expires_at = ?, updated_at = ? WHERE id = ?`,
669
+ [nextRefreshHash, nextRefreshExpiresAt.toISOString(), isoNow(), row.id]
670
+ );
671
+ const requestedScopes = parseJson(row.scopes_json, principalRecord.principal.scopes);
672
+ const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds);
673
+ const accessToken = createAccessToken({
674
+ sub: principalRecord.principal.id,
675
+ displayName: principalRecord.principal.displayName,
676
+ scopes: requestedScopes,
677
+ roles: principalRecord.principal.roles,
678
+ permissions: principalRecord.principal.permissions,
679
+ metadata: principalRecord.principal.metadata,
680
+ iat: Math.floor(Date.now() / 1e3),
681
+ exp: Math.floor(expiresAt.getTime() / 1e3),
682
+ iss: this.config.issuer,
683
+ jti: randomUUID(),
684
+ tokenType: "access"
685
+ }, this.config.authSecret);
686
+ return {
687
+ ok: true,
688
+ accessToken,
689
+ refreshToken: nextRefreshToken,
690
+ tokenType: "Bearer",
691
+ expiresAt: expiresAt.toISOString(),
692
+ expiresInSeconds: this.config.accessTokenTtlSeconds,
693
+ principal: {
694
+ ...principalRecord.principal,
695
+ scopes: requestedScopes
696
+ }
697
+ };
698
+ }
699
+ async createPersonalAccessToken(userId, input) {
700
+ await this.ensureInitialized();
701
+ const nowIso = isoNow();
702
+ const token = nextOpaqueToken("pat");
703
+ const id = randomUUID();
704
+ const tokenHash = stableHash(token, this.config.authSecret);
705
+ const prefix = token.slice(0, 12);
706
+ await this.run(
707
+ `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)
708
+ VALUES (?, ?, 'personal_access_token', ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?)`,
709
+ [
710
+ id,
711
+ userId,
712
+ input.name,
713
+ prefix,
714
+ tokenHash,
715
+ JSON.stringify(input.scopes?.length ? input.scopes : ["auth:me"]),
716
+ input.expiresAt ?? null,
717
+ JSON.stringify({}),
718
+ nowIso,
719
+ nowIso
720
+ ]
721
+ );
722
+ await this.writeAuditEvent({
723
+ actorType: "user",
724
+ actorId: userId,
725
+ eventType: "auth.pat_created",
726
+ targetType: "api_token",
727
+ targetId: id,
728
+ data: { name: input.name }
729
+ });
730
+ return { id, token, prefix, name: input.name, expiresAt: input.expiresAt ?? null };
731
+ }
732
+ async listPersonalAccessTokens(userId) {
733
+ await this.ensureInitialized();
734
+ return this.all(
735
+ `SELECT id, name, token_prefix, expires_at, last_used_at, revoked_at, created_at
736
+ FROM api_tokens
737
+ WHERE user_id = ?
738
+ ORDER BY created_at DESC`,
739
+ [userId]
740
+ );
741
+ }
742
+ async revokePersonalAccessToken(userId, tokenId) {
743
+ await this.ensureInitialized();
744
+ await this.run(`UPDATE api_tokens SET revoked_at = ? WHERE id = ? AND user_id = ?`, [isoNow(), tokenId, userId]);
745
+ await this.writeAuditEvent({
746
+ actorType: "user",
747
+ actorId: userId,
748
+ eventType: "auth.pat_revoked",
749
+ targetType: "api_token",
750
+ targetId: tokenId
751
+ });
752
+ }
753
+ async upsertServiceCredential(input) {
754
+ const nowIso = isoNow();
755
+ const existing = await this.first(`SELECT id FROM service_credentials WHERE service_id = ?`, [input.serviceId]);
756
+ const secretHash = stableHash(input.secret, this.config.authSecret);
757
+ if (existing) {
758
+ await this.run(
759
+ `UPDATE service_credentials
760
+ SET name = ?, secret_hash = ?, roles_json = ?, permissions_json = ?, revoked_at = NULL, updated_at = ?
761
+ WHERE id = ?`,
762
+ [input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, existing.id]
763
+ );
764
+ return existing.id;
765
+ }
766
+ const id = randomUUID();
767
+ await this.run(
768
+ `INSERT INTO service_credentials (id, service_id, name, secret_hash, roles_json, permissions_json, revoked_at, last_used_at, created_at, updated_at)
769
+ VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`,
770
+ [id, input.serviceId, input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, nowIso]
771
+ );
772
+ return id;
773
+ }
774
+ async createServiceCredential(input) {
775
+ await this.ensureInitialized();
776
+ const secret = nextOpaqueToken("svc");
777
+ const id = await this.upsertServiceCredential({ ...input, secret });
778
+ return { id, serviceId: input.serviceId, secret };
779
+ }
780
+ async rotateServiceCredential(serviceId) {
781
+ await this.ensureInitialized();
782
+ const row = await this.first(
783
+ `SELECT name, roles_json, permissions_json FROM service_credentials WHERE service_id = ? AND revoked_at IS NULL`,
784
+ [serviceId]
785
+ );
786
+ if (!row) {
787
+ throw new Error(`Unknown active service credential "${serviceId}".`);
788
+ }
789
+ return this.createServiceCredential({
790
+ serviceId,
791
+ name: row.name,
792
+ roles: parseJson(row.roles_json, []),
793
+ permissions: parseJson(row.permissions_json, [])
794
+ });
795
+ }
796
+ async authenticateBearerToken(token) {
797
+ await this.ensureInitialized();
798
+ const patHash = stableHash(token, this.config.authSecret);
799
+ const pat = await this.first(
800
+ `SELECT id, user_id, name, scopes_json, expires_at, revoked_at
801
+ FROM api_tokens
802
+ WHERE token_hash = ?`,
803
+ [patHash]
804
+ );
805
+ if (pat && !pat.revoked_at && (!pat.expires_at || new Date(pat.expires_at).getTime() > Date.now())) {
806
+ await this.run(`UPDATE api_tokens SET last_used_at = ? WHERE id = ?`, [isoNow(), pat.id]);
807
+ const principal = (await this.principalForUser(pat.user_id)).principal;
808
+ return {
809
+ principal: { ...principal, scopes: parseJson(pat.scopes_json, principal.scopes) },
810
+ credential: { type: "personal_access_token", id: pat.id, label: pat.name }
811
+ };
812
+ }
813
+ const payload = verifyAccessToken(token, this.config.authSecret);
814
+ if (!payload) return null;
815
+ return {
816
+ principal: principalFromAccessTokenPayload(payload),
817
+ credential: {
818
+ type: payload.tokenType === "service" ? "service_token" : "access_token",
819
+ id: payload.jti,
820
+ label: payload.tokenType
821
+ }
822
+ };
823
+ }
824
+ async authenticateService(serviceId, secret) {
825
+ await this.ensureInitialized();
826
+ const row = await this.first(
827
+ `SELECT id, name, secret_hash, roles_json, permissions_json, revoked_at
828
+ FROM service_credentials
829
+ WHERE service_id = ?`,
830
+ [serviceId]
831
+ );
832
+ if (!row || row.revoked_at) return null;
833
+ const incomingHash = stableHash(secret, this.config.authSecret);
834
+ if (!equalHash(row.secret_hash, incomingHash)) return null;
835
+ await this.run(`UPDATE service_credentials SET last_used_at = ?, updated_at = ? WHERE id = ?`, [isoNow(), isoNow(), row.id]);
836
+ const roles = parseJson(row.roles_json, []);
837
+ const permissions = [
838
+ .../* @__PURE__ */ new Set([
839
+ ...await this.permissionsForRoles(roles),
840
+ ...parseJson(row.permissions_json, [])
841
+ ])
842
+ ];
843
+ return {
844
+ principal: {
845
+ id: serviceId,
846
+ displayName: row.name,
847
+ roles,
848
+ permissions,
849
+ scopes: this.scopesForPrincipal(permissions),
850
+ metadata: { serviceId }
851
+ },
852
+ credential: { type: "service_secret", id: row.id, label: row.name }
853
+ };
854
+ }
855
+ async exchangeTrustedUserAssertion(claims) {
856
+ await this.ensureInitialized();
857
+ const principalRecord = await this.principalForUser(claims.userId);
858
+ const expiresAt = addSeconds(now(), this.config.webExchangeTtlSeconds);
859
+ const accessToken = createAccessToken({
860
+ sub: principalRecord.principal.id,
861
+ displayName: principalRecord.principal.displayName,
862
+ scopes: principalRecord.principal.scopes,
863
+ roles: principalRecord.principal.roles,
864
+ permissions: principalRecord.principal.permissions,
865
+ metadata: {
866
+ ...principalRecord.principal.metadata,
867
+ actingSessionId: claims.sessionId,
868
+ identityId: claims.identityId,
869
+ teamId: claims.teamId ?? null,
870
+ projectId: claims.projectId ?? null,
871
+ membershipId: claims.membershipId ?? null,
872
+ teamRoles: [...new Set((claims.teamRoles ?? []).filter((entry) => typeof entry === "string" && entry.trim()))],
873
+ teamCapabilities: [...new Set((claims.teamCapabilities ?? []).filter((entry) => typeof entry === "string" && entry.trim()))],
874
+ authTime: claims.authTime
875
+ },
876
+ iat: Math.floor(Date.now() / 1e3),
877
+ exp: Math.floor(expiresAt.getTime() / 1e3),
878
+ iss: this.config.issuer,
879
+ jti: randomUUID(),
880
+ tokenType: "access"
881
+ }, this.config.authSecret);
882
+ await this.writeAuditEvent({
883
+ actorType: "service",
884
+ actorId: this.config.webServiceId,
885
+ eventType: "auth.web_exchange",
886
+ targetType: "user",
887
+ targetId: claims.userId,
888
+ data: { sessionId: claims.sessionId }
889
+ });
890
+ return {
891
+ ok: true,
892
+ accessToken,
893
+ tokenType: "Bearer",
894
+ expiresAt: expiresAt.toISOString(),
895
+ expiresInSeconds: this.config.webExchangeTtlSeconds,
896
+ principal: principalRecord.principal
897
+ };
898
+ }
899
+ }
900
+ export {
901
+ D1AuthStore
902
+ };