@hachej/boring-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/CoreFront-CDeLdfb0.d.ts +19 -0
  4. package/dist/app/front/index.d.ts +18 -0
  5. package/dist/app/front/index.js +162 -0
  6. package/dist/app/front/styles.css +6 -0
  7. package/dist/app/server/index.d.ts +96 -0
  8. package/dist/app/server/index.js +507 -0
  9. package/dist/app/vite/index.d.ts +10 -0
  10. package/dist/app/vite/index.js +33 -0
  11. package/dist/authHook-vsRhOvnh.d.ts +38 -0
  12. package/dist/chunk-CZ4HIXII.js +2869 -0
  13. package/dist/chunk-H5KU6R6Y.js +68 -0
  14. package/dist/chunk-HSRBZLKT.js +1684 -0
  15. package/dist/chunk-HYNKZSTF.js +18 -0
  16. package/dist/chunk-MLKGABMK.js +9 -0
  17. package/dist/chunk-VTOS4C7B.js +3443 -0
  18. package/dist/connection-CE7z-wBp.d.ts +145 -0
  19. package/dist/front/index.d.ts +458 -0
  20. package/dist/front/index.js +126 -0
  21. package/dist/front/theme.css +168 -0
  22. package/dist/front/top-bar-slot.d.ts +10 -0
  23. package/dist/front/top-bar-slot.js +9 -0
  24. package/dist/index-COZa03RP.d.ts +266 -0
  25. package/dist/migrate-D49JsATX.d.ts +8 -0
  26. package/dist/server/db/index.d.ts +209 -0
  27. package/dist/server/db/index.js +18 -0
  28. package/dist/server/index.d.ts +395 -0
  29. package/dist/server/index.js +136 -0
  30. package/dist/shared/index.d.ts +1 -0
  31. package/dist/shared/index.js +13 -0
  32. package/drizzle/.gitkeep +0 -0
  33. package/drizzle/0000_easy_meggan.sql +53 -0
  34. package/drizzle/0001_groovy_smiling_tiger.sql +14 -0
  35. package/drizzle/0002_busy_iron_man.sql +16 -0
  36. package/drizzle/0003_aspiring_richard_fisk.sql +12 -0
  37. package/drizzle/0004_heavy_lenny_balinger.sql +9 -0
  38. package/drizzle/0005_flimsy_mastermind.sql +17 -0
  39. package/drizzle/0006_happy_callisto.sql +13 -0
  40. package/drizzle/0007_v7_substrate.sql +54 -0
  41. package/drizzle/0008_workspace_sandbox_handles.sql +32 -0
  42. package/drizzle/0009_workspace_runtime_resources.sql +39 -0
  43. package/drizzle/meta/0000_snapshot.json +380 -0
  44. package/drizzle/meta/0001_snapshot.json +471 -0
  45. package/drizzle/meta/0002_snapshot.json +599 -0
  46. package/drizzle/meta/0003_snapshot.json +693 -0
  47. package/drizzle/meta/0004_snapshot.json +753 -0
  48. package/drizzle/meta/0005_snapshot.json +886 -0
  49. package/drizzle/meta/0006_snapshot.json +968 -0
  50. package/drizzle/meta/_journal.json +76 -0
  51. package/drizzle/schema.ts +110 -0
  52. package/package.json +127 -0
@@ -0,0 +1,1684 @@
1
+ import {
2
+ ERROR_CODES,
3
+ HttpError
4
+ } from "./chunk-H5KU6R6Y.js";
5
+ import {
6
+ __export
7
+ } from "./chunk-MLKGABMK.js";
8
+
9
+ // src/server/db/connection.ts
10
+ import { drizzle } from "drizzle-orm/postgres-js";
11
+ import postgres from "postgres";
12
+ function createDatabase(config) {
13
+ if (!config.databaseUrl) {
14
+ throw new Error("databaseUrl is required to create a database connection");
15
+ }
16
+ const sql6 = postgres(config.databaseUrl, {
17
+ max: 10,
18
+ idle_timeout: 20,
19
+ connect_timeout: 10
20
+ });
21
+ const db = drizzle(sql6);
22
+ return { db, sql: sql6 };
23
+ }
24
+
25
+ // src/server/db/migrate.ts
26
+ import { migrate } from "drizzle-orm/postgres-js/migrator";
27
+ import postgres2 from "postgres";
28
+ import { drizzle as drizzle2 } from "drizzle-orm/postgres-js";
29
+ import { sql } from "drizzle-orm";
30
+ import { resolve, dirname } from "path";
31
+ import { fileURLToPath } from "url";
32
+ import { existsSync } from "fs";
33
+ var ADVISORY_LOCK_ID = 1651470949;
34
+ var JOURNAL = "meta/_journal.json";
35
+ function defaultMigrationsFolder() {
36
+ const thisDir = dirname(fileURLToPath(import.meta.url));
37
+ const candidates = [
38
+ resolve(thisDir, "../../../drizzle"),
39
+ resolve(thisDir, "../../drizzle"),
40
+ resolve(thisDir, "../drizzle")
41
+ ];
42
+ for (const candidate of candidates) {
43
+ if (existsSync(resolve(candidate, JOURNAL))) return candidate;
44
+ }
45
+ return candidates[0];
46
+ }
47
+ async function runMigrations(config, options) {
48
+ if (!config.databaseUrl) {
49
+ throw new Error("databaseUrl is required to run migrations");
50
+ }
51
+ const migrationClient = postgres2(config.databaseUrl, { max: 1 });
52
+ const db = drizzle2(migrationClient);
53
+ try {
54
+ await db.execute(sql.raw(`SELECT pg_advisory_lock(${ADVISORY_LOCK_ID})`));
55
+ try {
56
+ await db.execute(sql.raw("CREATE EXTENSION IF NOT EXISTS pgcrypto"));
57
+ const migrationsFolder = options?.migrationsFolder ?? defaultMigrationsFolder();
58
+ await migrate(db, { migrationsFolder });
59
+ } finally {
60
+ await db.execute(sql.raw(`SELECT pg_advisory_unlock(${ADVISORY_LOCK_ID})`));
61
+ }
62
+ } finally {
63
+ await migrationClient.end();
64
+ }
65
+ }
66
+
67
+ // src/server/db/stores/LocalUserStore.ts
68
+ var LocalUserStore = class {
69
+ users = /* @__PURE__ */ new Map();
70
+ usersByEmail = /* @__PURE__ */ new Map();
71
+ settings = /* @__PURE__ */ new Map();
72
+ // key: `${userId}:${appId}`
73
+ seed(user) {
74
+ const now = (/* @__PURE__ */ new Date()).toISOString();
75
+ const full = { ...user, createdAt: now, updatedAt: now };
76
+ this.users.set(full.id, full);
77
+ this.usersByEmail.set(full.email.toLowerCase(), full);
78
+ }
79
+ async getById(id) {
80
+ return this.users.get(id) ?? null;
81
+ }
82
+ async getByEmail(email) {
83
+ return this.usersByEmail.get(email.toLowerCase()) ?? null;
84
+ }
85
+ async upsert(userId, data) {
86
+ const now = (/* @__PURE__ */ new Date()).toISOString();
87
+ const existing = this.users.get(userId);
88
+ if (existing) {
89
+ const updated = {
90
+ ...existing,
91
+ email: data.email,
92
+ name: data.name ?? existing.name,
93
+ updatedAt: now
94
+ };
95
+ this.usersByEmail.delete(existing.email.toLowerCase());
96
+ this.users.set(userId, updated);
97
+ this.usersByEmail.set(updated.email.toLowerCase(), updated);
98
+ return updated;
99
+ }
100
+ const user = {
101
+ id: userId,
102
+ email: data.email,
103
+ name: data.name ?? null,
104
+ emailVerified: false,
105
+ image: null,
106
+ createdAt: now,
107
+ updatedAt: now
108
+ };
109
+ this.users.set(userId, user);
110
+ this.usersByEmail.set(user.email.toLowerCase(), user);
111
+ return user;
112
+ }
113
+ async getUserSettings(userId, appId) {
114
+ const key = `${userId}:${appId}`;
115
+ const existing = this.settings.get(key);
116
+ if (existing) return { ...existing };
117
+ const user = this.users.get(userId);
118
+ return {
119
+ displayName: user?.name ?? "",
120
+ email: user?.email ?? "",
121
+ settings: {}
122
+ };
123
+ }
124
+ async putUserSettings(userId, appId, updates) {
125
+ const key = `${userId}:${appId}`;
126
+ const current = await this.getUserSettings(userId, appId);
127
+ const updated = {
128
+ displayName: updates.displayName ?? current.displayName,
129
+ email: updates.email ?? current.email,
130
+ settings: updates.settings ?? current.settings
131
+ };
132
+ this.settings.set(key, updated);
133
+ return { ...updated };
134
+ }
135
+ };
136
+
137
+ // src/server/db/stores/LocalWorkspaceStore.ts
138
+ import { randomUUID, createHash } from "crypto";
139
+ var LocalWorkspaceStore = class {
140
+ // key: `${userId}:${workspaceId}`
141
+ constructor(userStore) {
142
+ this.userStore = userStore;
143
+ }
144
+ userStore;
145
+ workspaces = /* @__PURE__ */ new Map();
146
+ members = /* @__PURE__ */ new Map();
147
+ // key: `${workspaceId}:${userId}`
148
+ invites = /* @__PURE__ */ new Map();
149
+ runtimes = /* @__PURE__ */ new Map();
150
+ runtimeResources = /* @__PURE__ */ new Map();
151
+ wsSettings = /* @__PURE__ */ new Map();
152
+ uiStates = /* @__PURE__ */ new Map();
153
+ async create(userId, name, appId, opts) {
154
+ const now = (/* @__PURE__ */ new Date()).toISOString();
155
+ const ws = {
156
+ id: randomUUID(),
157
+ appId,
158
+ name,
159
+ createdBy: userId,
160
+ createdAt: now,
161
+ deletedAt: null,
162
+ isDefault: opts?.isDefault ?? false
163
+ };
164
+ this.workspaces.set(ws.id, ws);
165
+ const memberKey = `${ws.id}:${userId}`;
166
+ this.members.set(memberKey, {
167
+ workspaceId: ws.id,
168
+ userId,
169
+ role: "owner",
170
+ createdAt: now
171
+ });
172
+ this.runtimes.set(ws.id, {
173
+ workspaceId: ws.id,
174
+ spriteUrl: null,
175
+ spriteName: null,
176
+ state: "ready",
177
+ lastError: null,
178
+ volumePath: null,
179
+ lastErrorOp: null,
180
+ sandboxProvider: null,
181
+ sandboxId: null,
182
+ sandboxStatus: null,
183
+ sandboxSnapshotId: null,
184
+ sandboxCreatedAt: null,
185
+ sandboxLastUsedAt: null,
186
+ sandboxLastSeenAt: null,
187
+ sandboxExpiresAt: null,
188
+ provisioningStep: null,
189
+ stepStartedAt: null,
190
+ updatedAt: now
191
+ });
192
+ return ws;
193
+ }
194
+ async list(userId, appId) {
195
+ const result = [];
196
+ for (const ws of this.workspaces.values()) {
197
+ if (ws.deletedAt) continue;
198
+ if (ws.appId !== appId) continue;
199
+ const memberKey = `${ws.id}:${userId}`;
200
+ if (this.members.has(memberKey)) result.push(ws);
201
+ }
202
+ result.sort((a, b) => {
203
+ if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
204
+ return b.createdAt.localeCompare(a.createdAt);
205
+ });
206
+ return result;
207
+ }
208
+ async get(id) {
209
+ const ws = this.workspaces.get(id);
210
+ if (!ws || ws.deletedAt) return null;
211
+ return ws;
212
+ }
213
+ async update(id, updates) {
214
+ const ws = this.workspaces.get(id);
215
+ if (!ws || ws.deletedAt) return null;
216
+ const updated = { ...ws, ...updates };
217
+ this.workspaces.set(id, updated);
218
+ return updated;
219
+ }
220
+ async delete(id) {
221
+ const ws = this.workspaces.get(id);
222
+ if (!ws || ws.deletedAt) return { removed: false, code: ERROR_CODES.NOT_FOUND };
223
+ ws.deletedAt = (/* @__PURE__ */ new Date()).toISOString();
224
+ this.workspaces.set(id, ws);
225
+ return { removed: true };
226
+ }
227
+ async getWorkspacesWhereSoleOwner(userId) {
228
+ const result = [];
229
+ for (const ws of this.workspaces.values()) {
230
+ if (ws.deletedAt) continue;
231
+ const memberKey = `${ws.id}:${userId}`;
232
+ const membership = this.members.get(memberKey);
233
+ if (!membership || membership.role !== "owner") continue;
234
+ let otherOwnerExists = false;
235
+ for (const [key, m] of this.members) {
236
+ if (m.workspaceId === ws.id && m.userId !== userId && m.role === "owner") {
237
+ otherOwnerExists = true;
238
+ break;
239
+ }
240
+ }
241
+ if (!otherOwnerExists) result.push(ws);
242
+ }
243
+ return result;
244
+ }
245
+ async isMember(workspaceId, userId) {
246
+ return this.members.has(`${workspaceId}:${userId}`);
247
+ }
248
+ async getMemberRole(workspaceId, userId) {
249
+ const m = this.members.get(`${workspaceId}:${userId}`);
250
+ return m?.role ?? null;
251
+ }
252
+ async listMembers(workspaceId) {
253
+ const result = [];
254
+ for (const m of this.members.values()) {
255
+ if (m.workspaceId !== workspaceId) continue;
256
+ const user = await this.userStore.getById(m.userId);
257
+ result.push({
258
+ ...m,
259
+ user: {
260
+ id: m.userId,
261
+ email: user?.email ?? "",
262
+ name: user?.name ?? null,
263
+ image: user?.image ?? null
264
+ }
265
+ });
266
+ }
267
+ return result;
268
+ }
269
+ async upsertMember(workspaceId, userId, role) {
270
+ const key = `${workspaceId}:${userId}`;
271
+ const existing = this.members.get(key);
272
+ const now = (/* @__PURE__ */ new Date()).toISOString();
273
+ const member = {
274
+ workspaceId,
275
+ userId,
276
+ role,
277
+ createdAt: existing?.createdAt ?? now
278
+ };
279
+ this.members.set(key, member);
280
+ return member;
281
+ }
282
+ async updateMemberRole(workspaceId, userId, role) {
283
+ const key = `${workspaceId}:${userId}`;
284
+ const membership = this.members.get(key);
285
+ if (!membership) return { code: ERROR_CODES.NOT_MEMBER };
286
+ if (membership.role === "owner" && role !== "owner") {
287
+ let otherOwnerExists = false;
288
+ for (const m of this.members.values()) {
289
+ if (m.workspaceId === workspaceId && m.userId !== userId && m.role === "owner") {
290
+ otherOwnerExists = true;
291
+ break;
292
+ }
293
+ }
294
+ if (!otherOwnerExists) return { code: ERROR_CODES.LAST_OWNER };
295
+ }
296
+ const updated = { ...membership, role };
297
+ this.members.set(key, updated);
298
+ return { member: updated };
299
+ }
300
+ async removeMember(workspaceId, userId) {
301
+ const key = `${workspaceId}:${userId}`;
302
+ const membership = this.members.get(key);
303
+ if (!membership) return { removed: false, code: ERROR_CODES.NOT_MEMBER };
304
+ if (membership.role === "owner") {
305
+ let otherOwnerExists = false;
306
+ for (const m of this.members.values()) {
307
+ if (m.workspaceId === workspaceId && m.userId !== userId && m.role === "owner") {
308
+ otherOwnerExists = true;
309
+ break;
310
+ }
311
+ }
312
+ if (!otherOwnerExists) return { removed: false, code: ERROR_CODES.LAST_OWNER };
313
+ }
314
+ this.members.delete(key);
315
+ return { removed: true };
316
+ }
317
+ async listInvites(workspaceId) {
318
+ const result = [];
319
+ for (const inv of this.invites.values()) {
320
+ if (inv.workspaceId === workspaceId) result.push(inv);
321
+ }
322
+ return result;
323
+ }
324
+ async createInvite(workspaceId, email, role, invitedBy, opts) {
325
+ const rawToken = randomUUID();
326
+ const tokenHash = createHash("sha256").update(rawToken).digest("hex");
327
+ const now = (/* @__PURE__ */ new Date()).toISOString();
328
+ const ttlDays = opts?.ttlDays ?? 7;
329
+ const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1e3).toISOString();
330
+ const invite = {
331
+ id: randomUUID(),
332
+ workspaceId,
333
+ email,
334
+ tokenHash,
335
+ role,
336
+ expiresAt,
337
+ acceptedAt: null,
338
+ createdBy: invitedBy,
339
+ createdAt: now,
340
+ failedAttempts: 0,
341
+ lockedUntil: null
342
+ };
343
+ this.invites.set(invite.id, invite);
344
+ return { invite, rawToken };
345
+ }
346
+ async getInvite(workspaceId, inviteId) {
347
+ const inv = this.invites.get(inviteId);
348
+ if (!inv || inv.workspaceId !== workspaceId) return null;
349
+ return inv;
350
+ }
351
+ async getInviteByTokenHash(tokenHash) {
352
+ for (const inv of this.invites.values()) {
353
+ if (inv.tokenHash === tokenHash) return inv;
354
+ }
355
+ return null;
356
+ }
357
+ async revokeInvite(workspaceId, inviteId) {
358
+ const inv = this.invites.get(inviteId);
359
+ if (!inv || inv.workspaceId !== workspaceId) return false;
360
+ this.invites.delete(inviteId);
361
+ return true;
362
+ }
363
+ async acceptInvite(workspaceId, inviteId, userId) {
364
+ const inv = this.invites.get(inviteId);
365
+ if (!inv || inv.workspaceId !== workspaceId) {
366
+ throw new HttpError({ status: 404, code: ERROR_CODES.INVITE_NOT_FOUND, message: "Invite not found" });
367
+ }
368
+ if (inv.acceptedAt) {
369
+ throw new HttpError({ status: 410, code: ERROR_CODES.INVITE_ALREADY_ACCEPTED, message: "Invite already accepted" });
370
+ }
371
+ if (new Date(inv.expiresAt) <= /* @__PURE__ */ new Date()) {
372
+ throw new HttpError({ status: 410, code: ERROR_CODES.INVITE_EXPIRED, message: "Invite has expired" });
373
+ }
374
+ const user = await this.userStore.getById(userId);
375
+ if (!user || inv.email.toLowerCase() !== user.email.toLowerCase()) {
376
+ throw new HttpError({ status: 403, code: ERROR_CODES.INVITE_EMAIL_MISMATCH, message: "Invite email does not match your account" });
377
+ }
378
+ const now = (/* @__PURE__ */ new Date()).toISOString();
379
+ inv.acceptedAt = now;
380
+ this.invites.set(inviteId, inv);
381
+ const member = await this.upsertMember(workspaceId, userId, inv.role);
382
+ return { invite: inv, member };
383
+ }
384
+ async incrementInviteFailedAttempts(inviteId) {
385
+ const inv = this.invites.get(inviteId);
386
+ if (!inv) return { failedAttempts: 0, lockedUntil: null };
387
+ inv.failedAttempts = (inv.failedAttempts ?? 0) + 1;
388
+ if (inv.failedAttempts >= 50) {
389
+ inv.lockedUntil = new Date(Date.now() + 60 * 60 * 1e3).toISOString();
390
+ }
391
+ this.invites.set(inviteId, inv);
392
+ return { failedAttempts: inv.failedAttempts, lockedUntil: inv.lockedUntil };
393
+ }
394
+ async resetInviteFailedAttempts(inviteId) {
395
+ const inv = this.invites.get(inviteId);
396
+ if (!inv) return;
397
+ inv.failedAttempts = 0;
398
+ inv.lockedUntil = null;
399
+ this.invites.set(inviteId, inv);
400
+ }
401
+ async getWorkspaceSettings(workspaceId) {
402
+ const settings = this.wsSettings.get(workspaceId);
403
+ if (!settings) return [];
404
+ return Array.from(settings.entries()).map(([key, { updatedAt }]) => ({
405
+ key,
406
+ configured: true,
407
+ updated_at: updatedAt
408
+ }));
409
+ }
410
+ async putWorkspaceSettings(workspaceId, settings) {
411
+ const now = (/* @__PURE__ */ new Date()).toISOString();
412
+ let wsSettings = this.wsSettings.get(workspaceId);
413
+ if (!wsSettings) {
414
+ wsSettings = /* @__PURE__ */ new Map();
415
+ this.wsSettings.set(workspaceId, wsSettings);
416
+ }
417
+ for (const [key, value] of Object.entries(settings)) {
418
+ wsSettings.set(key, { value: JSON.stringify(value), updatedAt: now });
419
+ }
420
+ return this.getWorkspaceSettings(workspaceId);
421
+ }
422
+ async getWorkspaceRuntime(workspaceId) {
423
+ const ws = this.workspaces.get(workspaceId);
424
+ if (!ws || ws.deletedAt) return null;
425
+ const existing = this.runtimes.get(workspaceId);
426
+ if (existing) return existing;
427
+ const now = (/* @__PURE__ */ new Date()).toISOString();
428
+ const runtime = {
429
+ workspaceId,
430
+ spriteUrl: null,
431
+ spriteName: null,
432
+ state: "ready",
433
+ lastError: null,
434
+ volumePath: null,
435
+ lastErrorOp: null,
436
+ provisioningStep: null,
437
+ stepStartedAt: null,
438
+ updatedAt: now
439
+ };
440
+ this.runtimes.set(workspaceId, runtime);
441
+ return runtime;
442
+ }
443
+ async putWorkspaceRuntime(workspaceId, state) {
444
+ const existing = await this.getWorkspaceRuntime(workspaceId);
445
+ if (!existing) throw new Error(`Workspace ${workspaceId} not found`);
446
+ const updated = {
447
+ ...existing,
448
+ ...state,
449
+ workspaceId,
450
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
451
+ };
452
+ this.runtimes.set(workspaceId, updated);
453
+ return updated;
454
+ }
455
+ async listWorkspaceRuntimes() {
456
+ return Array.from(this.runtimes.values());
457
+ }
458
+ runtimeResourceKey(workspaceId, selector) {
459
+ return `${workspaceId}:${selector.kind}:${selector.purpose}:${selector.provider}`;
460
+ }
461
+ async getWorkspaceRuntimeResource(workspaceId, selector) {
462
+ const resource = this.runtimeResources.get(this.runtimeResourceKey(workspaceId, selector));
463
+ if (!resource || resource.state === "deleted") return null;
464
+ return resource;
465
+ }
466
+ async putWorkspaceRuntimeResource(workspaceId, resource) {
467
+ const ws = this.workspaces.get(workspaceId);
468
+ if (!ws || ws.deletedAt) throw new Error(`Workspace ${workspaceId} not found`);
469
+ const key = this.runtimeResourceKey(workspaceId, resource);
470
+ const existing = this.runtimeResources.get(key);
471
+ const now = (/* @__PURE__ */ new Date()).toISOString();
472
+ const next = {
473
+ id: resource.id ?? existing?.id ?? randomUUID(),
474
+ workspaceId,
475
+ kind: resource.kind,
476
+ purpose: resource.purpose,
477
+ provider: resource.provider,
478
+ handleKind: resource.handleKind,
479
+ stableKey: resource.stableKey ?? null,
480
+ providerResourceId: resource.providerResourceId ?? null,
481
+ parentResourceId: resource.parentResourceId ?? null,
482
+ state: resource.state,
483
+ persistenceMode: resource.persistenceMode,
484
+ config: resource.config ?? {},
485
+ providerMeta: resource.providerMeta ?? {},
486
+ lastError: resource.lastError ?? null,
487
+ lastErrorCode: resource.lastErrorCode ?? null,
488
+ createdAt: existing?.createdAt ?? now,
489
+ updatedAt: now,
490
+ lastSeenAt: resource.lastSeenAt ?? null,
491
+ lastUsedAt: resource.lastUsedAt ?? null,
492
+ expiresAt: resource.expiresAt ?? null,
493
+ generation: resource.generation ?? (existing?.generation ?? -1) + 1
494
+ };
495
+ this.runtimeResources.set(key, next);
496
+ return next;
497
+ }
498
+ async deleteWorkspaceRuntimeResource(workspaceId, selector) {
499
+ const key = this.runtimeResourceKey(workspaceId, selector);
500
+ const existing = this.runtimeResources.get(key);
501
+ if (!existing) return;
502
+ this.runtimeResources.set(key, {
503
+ ...existing,
504
+ state: "deleted",
505
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
506
+ });
507
+ }
508
+ async listWorkspaceRuntimeResources(workspaceId) {
509
+ return Array.from(this.runtimeResources.values()).filter(
510
+ (resource) => !workspaceId || resource.workspaceId === workspaceId
511
+ );
512
+ }
513
+ async retryWorkspaceRuntime(workspaceId) {
514
+ const existing = this.runtimes.get(workspaceId);
515
+ if (!existing || existing.state !== "error") return null;
516
+ const updated = {
517
+ ...existing,
518
+ state: "pending",
519
+ lastError: null,
520
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
521
+ };
522
+ this.runtimes.set(workspaceId, updated);
523
+ return updated;
524
+ }
525
+ async getUiState(userId, workspaceId) {
526
+ return this.uiStates.get(`${userId}:${workspaceId}`) ?? null;
527
+ }
528
+ async putUiState(userId, workspaceId, state) {
529
+ this.uiStates.set(`${userId}:${workspaceId}`, state);
530
+ }
531
+ };
532
+
533
+ // src/server/db/stores/PostgresWorkspaceStore.ts
534
+ import { createHash as createHash2, randomBytes } from "crypto";
535
+ import { and, eq, isNull, sql as sql4, desc } from "drizzle-orm";
536
+
537
+ // src/server/db/schema.ts
538
+ var schema_exports = {};
539
+ __export(schema_exports, {
540
+ accounts: () => accounts,
541
+ accountsRelations: () => accountsRelations,
542
+ idempotencyKeys: () => idempotencyKeys,
543
+ sessions: () => sessions,
544
+ sessionsRelations: () => sessionsRelations,
545
+ userSettings: () => userSettings,
546
+ userSettingsRelations: () => userSettingsRelations,
547
+ users: () => users,
548
+ usersRelations: () => usersRelations,
549
+ verification_tokens: () => verification_tokens,
550
+ workspaceInvites: () => workspaceInvites,
551
+ workspaceInvitesRelations: () => workspaceInvitesRelations,
552
+ workspaceMembers: () => workspaceMembers,
553
+ workspaceMembersRelations: () => workspaceMembersRelations,
554
+ workspaceRuntimeResources: () => workspaceRuntimeResources,
555
+ workspaceRuntimeResourcesRelations: () => workspaceRuntimeResourcesRelations,
556
+ workspaceRuntimes: () => workspaceRuntimes,
557
+ workspaceRuntimesRelations: () => workspaceRuntimesRelations,
558
+ workspaceSettings: () => workspaceSettings,
559
+ workspaceSettingsRelations: () => workspaceSettingsRelations,
560
+ workspaces: () => workspaces,
561
+ workspacesRelations: () => workspacesRelations
562
+ });
563
+
564
+ // drizzle/schema.ts
565
+ import { relations, sql as sql2 } from "drizzle-orm";
566
+ import {
567
+ pgTable,
568
+ text,
569
+ timestamp,
570
+ boolean,
571
+ uuid,
572
+ index
573
+ } from "drizzle-orm/pg-core";
574
+ var users = pgTable("users", {
575
+ id: uuid("id").default(sql2`pg_catalog.gen_random_uuid()`).primaryKey(),
576
+ name: text("name").notNull(),
577
+ email: text("email").notNull().unique(),
578
+ email_verified: boolean("email_verified").default(false).notNull(),
579
+ image: text("image"),
580
+ created_at: timestamp("created_at").defaultNow().notNull(),
581
+ updated_at: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
582
+ });
583
+ var sessions = pgTable(
584
+ "sessions",
585
+ {
586
+ id: uuid("id").default(sql2`pg_catalog.gen_random_uuid()`).primaryKey(),
587
+ expires_at: timestamp("expires_at").notNull(),
588
+ token: text("token").notNull().unique(),
589
+ created_at: timestamp("created_at").defaultNow().notNull(),
590
+ updated_at: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull(),
591
+ ip_address: text("ip_address"),
592
+ user_agent: text("user_agent"),
593
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" })
594
+ },
595
+ (table) => [index("sessions_userId_idx").on(table.userId)]
596
+ );
597
+ var accounts = pgTable(
598
+ "accounts",
599
+ {
600
+ id: uuid("id").default(sql2`pg_catalog.gen_random_uuid()`).primaryKey(),
601
+ account_id: text("account_id").notNull(),
602
+ provider_id: text("provider_id").notNull(),
603
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
604
+ access_token: text("access_token"),
605
+ refresh_token: text("refresh_token"),
606
+ id_token: text("id_token"),
607
+ access_token_expires_at: timestamp("access_token_expires_at"),
608
+ refresh_token_expires_at: timestamp("refresh_token_expires_at"),
609
+ scope: text("scope"),
610
+ password: text("password"),
611
+ created_at: timestamp("created_at").defaultNow().notNull(),
612
+ updated_at: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
613
+ },
614
+ (table) => [index("accounts_userId_idx").on(table.userId)]
615
+ );
616
+ var verification_tokens = pgTable(
617
+ "verification_tokens",
618
+ {
619
+ id: uuid("id").default(sql2`pg_catalog.gen_random_uuid()`).primaryKey(),
620
+ identifier: text("identifier").notNull(),
621
+ value: text("value").notNull(),
622
+ expires_at: timestamp("expires_at").notNull(),
623
+ created_at: timestamp("created_at").defaultNow().notNull(),
624
+ updated_at: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
625
+ },
626
+ (table) => [index("verification_tokens_identifier_idx").on(table.identifier)]
627
+ );
628
+ var usersRelations = relations(users, ({ many }) => ({
629
+ sessionss: many(sessions),
630
+ accountss: many(accounts)
631
+ }));
632
+ var sessionsRelations = relations(sessions, ({ one }) => ({
633
+ users: one(users, {
634
+ fields: [sessions.userId],
635
+ references: [users.id]
636
+ })
637
+ }));
638
+ var accountsRelations = relations(accounts, ({ one }) => ({
639
+ users: one(users, {
640
+ fields: [accounts.userId],
641
+ references: [users.id]
642
+ })
643
+ }));
644
+
645
+ // src/server/db/schema.ts
646
+ import { pgTable as pgTable2, text as text2, uuid as uuid2, jsonb, timestamp as timestamp2, primaryKey, index as index2, integer, boolean as boolean2, uniqueIndex, check, customType } from "drizzle-orm/pg-core";
647
+ import { relations as relations2, sql as sql3 } from "drizzle-orm";
648
+ var bytea = customType({
649
+ dataType() {
650
+ return "bytea";
651
+ }
652
+ });
653
+ var userSettings = pgTable2(
654
+ "user_settings",
655
+ {
656
+ userId: uuid2("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
657
+ appId: text2("app_id").notNull(),
658
+ displayName: text2("display_name").notNull().default(""),
659
+ email: text2("email").notNull().default(""),
660
+ settings: jsonb("settings").notNull().default({}),
661
+ updatedAt: timestamp2("updated_at").defaultNow().notNull()
662
+ },
663
+ (table) => [
664
+ primaryKey({ columns: [table.userId, table.appId] }),
665
+ index2("user_settings_user_id_idx").on(table.userId)
666
+ ]
667
+ );
668
+ var userSettingsRelations = relations2(userSettings, ({ one }) => ({
669
+ user: one(users, {
670
+ fields: [userSettings.userId],
671
+ references: [users.id]
672
+ })
673
+ }));
674
+ var workspaces = pgTable2(
675
+ "workspaces",
676
+ {
677
+ id: uuid2("id").default(sql3`gen_random_uuid()`).primaryKey(),
678
+ appId: text2("app_id").notNull(),
679
+ name: text2("name").notNull(),
680
+ createdBy: uuid2("created_by").notNull().references(() => users.id),
681
+ createdAt: timestamp2("created_at").defaultNow().notNull(),
682
+ deletedAt: timestamp2("deleted_at"),
683
+ isDefault: boolean2("is_default").notNull().default(false)
684
+ },
685
+ (table) => [
686
+ index2("workspaces_created_by_idx").on(table.createdBy),
687
+ uniqueIndex("idx_workspaces_default_per_user_app").on(table.createdBy, table.appId).where(sql3`${table.isDefault} = true`)
688
+ ]
689
+ );
690
+ var workspacesRelations = relations2(workspaces, ({ one }) => ({
691
+ creator: one(users, {
692
+ fields: [workspaces.createdBy],
693
+ references: [users.id]
694
+ })
695
+ }));
696
+ var workspaceMembers = pgTable2(
697
+ "workspace_members",
698
+ {
699
+ workspaceId: uuid2("workspace_id").notNull().references(() => workspaces.id, { onDelete: "no action" }),
700
+ userId: uuid2("user_id").notNull().references(() => users.id, { onDelete: "restrict" }),
701
+ role: text2("role").notNull(),
702
+ createdAt: timestamp2("created_at").defaultNow().notNull()
703
+ },
704
+ (table) => [
705
+ primaryKey({ columns: [table.workspaceId, table.userId] }),
706
+ index2("workspace_members_user_id_idx").on(table.userId),
707
+ check("workspace_members_role_check", sql3`${table.role} IN ('owner', 'editor', 'viewer')`)
708
+ ]
709
+ );
710
+ var workspaceMembersRelations = relations2(workspaceMembers, ({ one }) => ({
711
+ workspace: one(workspaces, {
712
+ fields: [workspaceMembers.workspaceId],
713
+ references: [workspaces.id]
714
+ }),
715
+ user: one(users, {
716
+ fields: [workspaceMembers.userId],
717
+ references: [users.id]
718
+ })
719
+ }));
720
+ var workspaceSettings = pgTable2(
721
+ "workspace_settings",
722
+ {
723
+ workspaceId: uuid2("workspace_id").notNull().references(() => workspaces.id, { onDelete: "no action" }),
724
+ key: text2("key").notNull(),
725
+ value: bytea("value").notNull(),
726
+ updatedAt: timestamp2("updated_at").defaultNow().notNull()
727
+ },
728
+ (table) => [
729
+ primaryKey({ columns: [table.workspaceId, table.key] })
730
+ ]
731
+ );
732
+ var workspaceSettingsRelations = relations2(workspaceSettings, ({ one }) => ({
733
+ workspace: one(workspaces, {
734
+ fields: [workspaceSettings.workspaceId],
735
+ references: [workspaces.id]
736
+ })
737
+ }));
738
+ var workspaceRuntimes = pgTable2(
739
+ "workspace_runtimes",
740
+ {
741
+ workspaceId: uuid2("workspace_id").primaryKey().references(() => workspaces.id),
742
+ spriteUrl: text2("sprite_url"),
743
+ spriteName: text2("sprite_name"),
744
+ state: text2("state").notNull().default("pending"),
745
+ lastError: text2("last_error"),
746
+ volumePath: text2("volume_path"),
747
+ lastErrorOp: text2("last_error_op"),
748
+ sandboxProvider: text2("sandbox_provider"),
749
+ sandboxId: text2("sandbox_id"),
750
+ sandboxStatus: text2("sandbox_status"),
751
+ sandboxSnapshotId: text2("sandbox_snapshot_id"),
752
+ sandboxCreatedAt: timestamp2("sandbox_created_at"),
753
+ sandboxLastUsedAt: timestamp2("sandbox_last_used_at"),
754
+ sandboxLastSeenAt: timestamp2("sandbox_last_seen_at"),
755
+ sandboxExpiresAt: timestamp2("sandbox_expires_at"),
756
+ updatedAt: timestamp2("updated_at").defaultNow().notNull(),
757
+ provisioningStep: text2("provisioning_step"),
758
+ stepStartedAt: timestamp2("step_started_at")
759
+ },
760
+ (table) => [
761
+ check(
762
+ "workspace_runtimes_state_check",
763
+ sql3`${table.state} IN ('pending', 'ready', 'error')`
764
+ )
765
+ ]
766
+ );
767
+ var workspaceRuntimesRelations = relations2(
768
+ workspaceRuntimes,
769
+ ({ one }) => ({
770
+ workspace: one(workspaces, {
771
+ fields: [workspaceRuntimes.workspaceId],
772
+ references: [workspaces.id]
773
+ })
774
+ })
775
+ );
776
+ var workspaceRuntimeResources = pgTable2(
777
+ "workspace_runtime_resources",
778
+ {
779
+ id: uuid2("id").default(sql3`gen_random_uuid()`).primaryKey(),
780
+ workspaceId: uuid2("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
781
+ kind: text2("kind").notNull(),
782
+ purpose: text2("purpose").notNull().default("main"),
783
+ provider: text2("provider").notNull(),
784
+ handleKind: text2("handle_kind").notNull(),
785
+ stableKey: text2("stable_key"),
786
+ providerResourceId: text2("provider_resource_id"),
787
+ parentResourceId: uuid2("parent_resource_id"),
788
+ state: text2("state").notNull(),
789
+ persistenceMode: text2("persistence_mode").notNull(),
790
+ config: jsonb("config").notNull().default({}),
791
+ providerMeta: jsonb("provider_meta").notNull().default({}),
792
+ lastError: text2("last_error"),
793
+ lastErrorCode: text2("last_error_code"),
794
+ createdAt: timestamp2("created_at").defaultNow().notNull(),
795
+ updatedAt: timestamp2("updated_at").defaultNow().notNull(),
796
+ lastSeenAt: timestamp2("last_seen_at"),
797
+ lastUsedAt: timestamp2("last_used_at"),
798
+ expiresAt: timestamp2("expires_at"),
799
+ generation: integer("generation").notNull().default(0)
800
+ },
801
+ (table) => [
802
+ uniqueIndex("workspace_runtime_resources_active_idx").on(table.workspaceId, table.kind, table.purpose, table.provider).where(sql3`${table.state} <> 'deleted'`),
803
+ index2("workspace_runtime_resources_workspace_kind_idx").on(table.workspaceId, table.kind),
804
+ index2("workspace_runtime_resources_provider_stable_key_idx").on(table.provider, table.stableKey),
805
+ index2("workspace_runtime_resources_provider_resource_id_idx").on(table.provider, table.providerResourceId)
806
+ ]
807
+ );
808
+ var workspaceRuntimeResourcesRelations = relations2(
809
+ workspaceRuntimeResources,
810
+ ({ one }) => ({
811
+ workspace: one(workspaces, {
812
+ fields: [workspaceRuntimeResources.workspaceId],
813
+ references: [workspaces.id]
814
+ })
815
+ })
816
+ );
817
+ var workspaceInvites = pgTable2(
818
+ "workspace_invites",
819
+ {
820
+ id: uuid2("id").default(sql3`gen_random_uuid()`).primaryKey(),
821
+ workspaceId: uuid2("workspace_id").notNull().references(() => workspaces.id),
822
+ email: text2("email").notNull(),
823
+ tokenHash: text2("token_hash").notNull(),
824
+ role: text2("role").notNull(),
825
+ expiresAt: timestamp2("expires_at").notNull(),
826
+ acceptedAt: timestamp2("accepted_at"),
827
+ failedAttempts: integer("failed_attempts").notNull().default(0),
828
+ lockedUntil: timestamp2("locked_until"),
829
+ createdBy: uuid2("created_by").references(() => users.id, {
830
+ onDelete: "restrict"
831
+ }),
832
+ createdAt: timestamp2("created_at").defaultNow().notNull()
833
+ },
834
+ (table) => [
835
+ uniqueIndex("workspace_invites_token_hash_idx").on(table.tokenHash),
836
+ index2("workspace_invites_workspace_id_idx").on(table.workspaceId),
837
+ check(
838
+ "workspace_invites_role_check",
839
+ sql3`${table.role} IN ('owner', 'editor', 'viewer')`
840
+ )
841
+ ]
842
+ );
843
+ var workspaceInvitesRelations = relations2(
844
+ workspaceInvites,
845
+ ({ one }) => ({
846
+ workspace: one(workspaces, {
847
+ fields: [workspaceInvites.workspaceId],
848
+ references: [workspaces.id]
849
+ }),
850
+ creator: one(users, {
851
+ fields: [workspaceInvites.createdBy],
852
+ references: [users.id]
853
+ })
854
+ })
855
+ );
856
+ var idempotencyKeys = pgTable2(
857
+ "idempotency_keys",
858
+ {
859
+ key: text2("key").primaryKey(),
860
+ scope: text2("scope").notNull(),
861
+ responseStatus: integer("response_status").notNull(),
862
+ responseBody: jsonb("response_body").notNull(),
863
+ createdAt: timestamp2("created_at").defaultNow().notNull()
864
+ },
865
+ (table) => [
866
+ index2("idempotency_keys_created_at_idx").on(table.createdAt)
867
+ ]
868
+ );
869
+
870
+ // src/server/db/stores/PostgresWorkspaceStore.ts
871
+ var UI_STATE_KEY_PREFIX = "workspace_ui_state:";
872
+ function normalizeEmail(email) {
873
+ return email.trim().toLowerCase();
874
+ }
875
+ function toIso(value) {
876
+ if (value === null) return null;
877
+ if (typeof value === "string") return value;
878
+ return value.toISOString();
879
+ }
880
+ function toDate(value) {
881
+ return value ? new Date(value) : null;
882
+ }
883
+ function asRecord(value) {
884
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
885
+ }
886
+ function toWorkspaceRuntime(row) {
887
+ return {
888
+ workspaceId: row.workspaceId,
889
+ spriteUrl: row.spriteUrl,
890
+ spriteName: row.spriteName,
891
+ state: row.state,
892
+ lastError: row.lastError,
893
+ volumePath: row.volumePath,
894
+ lastErrorOp: row.lastErrorOp,
895
+ sandboxProvider: row.sandboxProvider,
896
+ sandboxId: row.sandboxId,
897
+ sandboxStatus: row.sandboxStatus,
898
+ sandboxSnapshotId: row.sandboxSnapshotId,
899
+ sandboxCreatedAt: toIso(row.sandboxCreatedAt),
900
+ sandboxLastUsedAt: toIso(row.sandboxLastUsedAt),
901
+ sandboxLastSeenAt: toIso(row.sandboxLastSeenAt),
902
+ sandboxExpiresAt: toIso(row.sandboxExpiresAt),
903
+ provisioningStep: row.provisioningStep,
904
+ stepStartedAt: toIso(row.stepStartedAt),
905
+ updatedAt: toIso(row.updatedAt)
906
+ };
907
+ }
908
+ function toWorkspaceRuntimeResource(row) {
909
+ return {
910
+ id: row.id,
911
+ workspaceId: row.workspaceId,
912
+ kind: row.kind,
913
+ purpose: row.purpose,
914
+ provider: row.provider,
915
+ handleKind: row.handleKind,
916
+ stableKey: row.stableKey,
917
+ providerResourceId: row.providerResourceId,
918
+ parentResourceId: row.parentResourceId,
919
+ state: row.state,
920
+ persistenceMode: row.persistenceMode,
921
+ config: asRecord(row.config),
922
+ providerMeta: asRecord(row.providerMeta),
923
+ lastError: row.lastError,
924
+ lastErrorCode: row.lastErrorCode,
925
+ createdAt: toIso(row.createdAt),
926
+ updatedAt: toIso(row.updatedAt),
927
+ lastSeenAt: toIso(row.lastSeenAt),
928
+ lastUsedAt: toIso(row.lastUsedAt),
929
+ expiresAt: toIso(row.expiresAt),
930
+ generation: row.generation
931
+ };
932
+ }
933
+ function toWorkspaceInvite(row) {
934
+ return {
935
+ id: row.id,
936
+ workspaceId: row.workspaceId,
937
+ email: row.email,
938
+ tokenHash: row.tokenHash,
939
+ role: row.role,
940
+ expiresAt: toIso(row.expiresAt),
941
+ acceptedAt: toIso(row.acceptedAt),
942
+ createdBy: row.createdBy,
943
+ createdAt: toIso(row.createdAt),
944
+ failedAttempts: row.failedAttempts,
945
+ lockedUntil: toIso(row.lockedUntil)
946
+ };
947
+ }
948
+ function toWorkspaceMember(row) {
949
+ return {
950
+ workspaceId: row.workspaceId,
951
+ userId: row.userId,
952
+ role: row.role,
953
+ createdAt: toIso(row.createdAt)
954
+ };
955
+ }
956
+ function toWorkspace(row) {
957
+ return {
958
+ id: row.id,
959
+ appId: row.appId,
960
+ name: row.name,
961
+ createdBy: row.createdBy,
962
+ createdAt: row.createdAt.toISOString(),
963
+ deletedAt: row.deletedAt?.toISOString() ?? null,
964
+ isDefault: row.isDefault
965
+ };
966
+ }
967
+ var PostgresWorkspaceStore = class {
968
+ constructor(db, workspaceSettingsKey = process.env.WORKSPACE_SETTINGS_ENCRYPTION_KEY ?? "") {
969
+ this.db = db;
970
+ this.workspaceSettingsKey = workspaceSettingsKey;
971
+ }
972
+ db;
973
+ workspaceSettingsKey;
974
+ uiStateKey(workspaceId) {
975
+ return `${UI_STATE_KEY_PREFIX}${workspaceId}`;
976
+ }
977
+ async getWorkspaceAppId(workspaceId) {
978
+ const rows = await this.db.select({ appId: workspaces.appId }).from(workspaces).where(and(eq(workspaces.id, workspaceId), isNull(workspaces.deletedAt))).limit(1);
979
+ return rows[0]?.appId ?? null;
980
+ }
981
+ // ---------------------------------------------------------------------------
982
+ // Workspace CRUD (Sub-PR 1)
983
+ // ---------------------------------------------------------------------------
984
+ async create(userId, name, appId, opts) {
985
+ return this.db.transaction(async (tx) => {
986
+ const [row] = await tx.insert(workspaces).values({ appId, name, createdBy: userId, isDefault: opts?.isDefault ?? false }).returning();
987
+ await tx.insert(workspaceMembers).values({
988
+ workspaceId: row.id,
989
+ userId,
990
+ role: "owner"
991
+ });
992
+ return toWorkspace(row);
993
+ });
994
+ }
995
+ async list(userId, appId) {
996
+ const rows = await this.db.select({ ws: workspaces }).from(workspaces).innerJoin(
997
+ workspaceMembers,
998
+ and(
999
+ eq(workspaceMembers.workspaceId, workspaces.id),
1000
+ eq(workspaceMembers.userId, userId)
1001
+ )
1002
+ ).where(and(eq(workspaces.appId, appId), isNull(workspaces.deletedAt))).orderBy(desc(workspaces.isDefault), desc(workspaces.createdAt));
1003
+ return rows.map((r) => toWorkspace(r.ws));
1004
+ }
1005
+ async get(id) {
1006
+ const rows = await this.db.select().from(workspaces).where(and(eq(workspaces.id, id), isNull(workspaces.deletedAt))).limit(1);
1007
+ return rows.length > 0 ? toWorkspace(rows[0]) : null;
1008
+ }
1009
+ async update(id, updates) {
1010
+ const rows = await this.db.update(workspaces).set(updates).where(and(eq(workspaces.id, id), isNull(workspaces.deletedAt))).returning();
1011
+ return rows.length > 0 ? toWorkspace(rows[0]) : null;
1012
+ }
1013
+ async delete(id) {
1014
+ const ws = await this.get(id);
1015
+ if (!ws) return { removed: false, code: ERROR_CODES.NOT_FOUND };
1016
+ await this.db.update(workspaces).set({ deletedAt: /* @__PURE__ */ new Date() }).where(eq(workspaces.id, id));
1017
+ return { removed: true };
1018
+ }
1019
+ // ---------------------------------------------------------------------------
1020
+ // Sole-owner query (Sub-PR 1)
1021
+ // ---------------------------------------------------------------------------
1022
+ async getWorkspacesWhereSoleOwner(userId) {
1023
+ const rows = await this.db.select({ ws: workspaces }).from(workspaces).innerJoin(
1024
+ workspaceMembers,
1025
+ and(
1026
+ eq(workspaceMembers.workspaceId, workspaces.id),
1027
+ eq(workspaceMembers.userId, userId),
1028
+ eq(workspaceMembers.role, sql4`'owner'`)
1029
+ )
1030
+ ).where(
1031
+ and(
1032
+ isNull(workspaces.deletedAt),
1033
+ sql4`(SELECT count(*) FROM workspace_members WHERE workspace_id = ${workspaces.id} AND role = 'owner') = 1`
1034
+ )
1035
+ );
1036
+ return rows.map((r) => toWorkspace(r.ws));
1037
+ }
1038
+ // ---------------------------------------------------------------------------
1039
+ // Member methods (Sub-PR 1)
1040
+ // ---------------------------------------------------------------------------
1041
+ async isMember(workspaceId, userId) {
1042
+ const rows = await this.db.select({ n: sql4`1` }).from(workspaceMembers).where(
1043
+ and(
1044
+ eq(workspaceMembers.workspaceId, workspaceId),
1045
+ eq(workspaceMembers.userId, userId)
1046
+ )
1047
+ ).limit(1);
1048
+ return rows.length > 0;
1049
+ }
1050
+ async getMemberRole(workspaceId, userId) {
1051
+ const rows = await this.db.select({ role: workspaceMembers.role }).from(workspaceMembers).where(
1052
+ and(
1053
+ eq(workspaceMembers.workspaceId, workspaceId),
1054
+ eq(workspaceMembers.userId, userId)
1055
+ )
1056
+ ).limit(1);
1057
+ return rows.length > 0 ? rows[0].role : null;
1058
+ }
1059
+ async listMembers(workspaceId) {
1060
+ const rows = await this.db.select({
1061
+ member: workspaceMembers,
1062
+ user: {
1063
+ id: users.id,
1064
+ email: users.email,
1065
+ name: users.name,
1066
+ image: users.image
1067
+ }
1068
+ }).from(workspaceMembers).innerJoin(users, eq(users.id, workspaceMembers.userId)).where(eq(workspaceMembers.workspaceId, workspaceId));
1069
+ return rows.map((r) => ({
1070
+ ...toWorkspaceMember(r.member),
1071
+ user: {
1072
+ id: r.user.id,
1073
+ email: r.user.email,
1074
+ name: r.user.name,
1075
+ image: r.user.image
1076
+ }
1077
+ }));
1078
+ }
1079
+ async upsertMember(workspaceId, userId, role) {
1080
+ const [row] = await this.db.insert(workspaceMembers).values({ workspaceId, userId, role }).onConflictDoUpdate({
1081
+ target: [workspaceMembers.workspaceId, workspaceMembers.userId],
1082
+ set: { role }
1083
+ }).returning();
1084
+ return toWorkspaceMember(row);
1085
+ }
1086
+ async updateMemberRole(workspaceId, userId, role) {
1087
+ return this.db.transaction(async (tx) => {
1088
+ await tx.execute(sql4`
1089
+ SELECT user_id
1090
+ FROM workspace_members
1091
+ WHERE workspace_id = ${workspaceId}
1092
+ FOR UPDATE
1093
+ `);
1094
+ const memberRows = await tx.select({ role: workspaceMembers.role }).from(workspaceMembers).where(
1095
+ and(
1096
+ eq(workspaceMembers.workspaceId, workspaceId),
1097
+ eq(workspaceMembers.userId, userId)
1098
+ )
1099
+ ).limit(1);
1100
+ const currentRole = memberRows[0]?.role;
1101
+ if (!currentRole) return { code: ERROR_CODES.NOT_MEMBER };
1102
+ if (currentRole === "owner" && role !== "owner") {
1103
+ const [{ count }] = await tx.select({ count: sql4`count(*)::int` }).from(workspaceMembers).where(
1104
+ and(
1105
+ eq(workspaceMembers.workspaceId, workspaceId),
1106
+ eq(workspaceMembers.role, sql4`'owner'`)
1107
+ )
1108
+ );
1109
+ if (Number(count) <= 1) {
1110
+ return { code: ERROR_CODES.LAST_OWNER };
1111
+ }
1112
+ }
1113
+ const [updated] = await tx.update(workspaceMembers).set({ role }).where(
1114
+ and(
1115
+ eq(workspaceMembers.workspaceId, workspaceId),
1116
+ eq(workspaceMembers.userId, userId)
1117
+ )
1118
+ ).returning();
1119
+ return { member: toWorkspaceMember(updated) };
1120
+ });
1121
+ }
1122
+ async removeMember(workspaceId, userId) {
1123
+ return this.db.transaction(async (tx) => {
1124
+ await tx.execute(sql4`
1125
+ SELECT user_id
1126
+ FROM workspace_members
1127
+ WHERE workspace_id = ${workspaceId}
1128
+ AND role = 'owner'
1129
+ FOR UPDATE
1130
+ `);
1131
+ const memberRows = await tx.select({ role: workspaceMembers.role }).from(workspaceMembers).where(
1132
+ and(
1133
+ eq(workspaceMembers.workspaceId, workspaceId),
1134
+ eq(workspaceMembers.userId, userId)
1135
+ )
1136
+ ).limit(1);
1137
+ const role = memberRows[0]?.role;
1138
+ if (!role) return { removed: false, code: ERROR_CODES.NOT_MEMBER };
1139
+ if (role === "owner") {
1140
+ const [{ count }] = await tx.select({ count: sql4`count(*)::int` }).from(workspaceMembers).where(
1141
+ and(
1142
+ eq(workspaceMembers.workspaceId, workspaceId),
1143
+ eq(workspaceMembers.role, sql4`'owner'`)
1144
+ )
1145
+ );
1146
+ if (Number(count) <= 1) {
1147
+ return { removed: false, code: ERROR_CODES.LAST_OWNER };
1148
+ }
1149
+ }
1150
+ const deletedRows = await tx.delete(workspaceMembers).where(
1151
+ and(
1152
+ eq(workspaceMembers.workspaceId, workspaceId),
1153
+ eq(workspaceMembers.userId, userId)
1154
+ )
1155
+ ).returning({ userId: workspaceMembers.userId });
1156
+ if (deletedRows.length === 0) {
1157
+ return { removed: false, code: ERROR_CODES.NOT_MEMBER };
1158
+ }
1159
+ return { removed: true };
1160
+ });
1161
+ }
1162
+ // ---------------------------------------------------------------------------
1163
+ // Invite methods (Sub-PR 2)
1164
+ // ---------------------------------------------------------------------------
1165
+ async createInvite(workspaceId, email, role, invitedBy, opts) {
1166
+ const rawToken = randomBytes(32).toString("base64url");
1167
+ const tokenHash = createHash2("sha256").update(rawToken).digest("hex");
1168
+ const ttlDays = opts?.ttlDays ?? 7;
1169
+ const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1e3);
1170
+ const rows = await this.db.insert(workspaceInvites).values({
1171
+ workspaceId,
1172
+ email: normalizeEmail(email),
1173
+ tokenHash,
1174
+ role,
1175
+ expiresAt,
1176
+ createdBy: invitedBy
1177
+ }).returning();
1178
+ return {
1179
+ invite: toWorkspaceInvite(rows[0]),
1180
+ rawToken
1181
+ };
1182
+ }
1183
+ async listInvites(workspaceId) {
1184
+ const rows = await this.db.select().from(workspaceInvites).where(eq(workspaceInvites.workspaceId, workspaceId)).orderBy(workspaceInvites.createdAt);
1185
+ return rows.map(toWorkspaceInvite);
1186
+ }
1187
+ async getInvite(workspaceId, inviteId) {
1188
+ const rows = await this.db.select().from(workspaceInvites).where(
1189
+ and(
1190
+ eq(workspaceInvites.workspaceId, workspaceId),
1191
+ eq(workspaceInvites.id, inviteId)
1192
+ )
1193
+ ).limit(1);
1194
+ return rows[0] ? toWorkspaceInvite(rows[0]) : null;
1195
+ }
1196
+ async getInviteByTokenHash(tokenHash) {
1197
+ const rows = await this.db.select().from(workspaceInvites).where(eq(workspaceInvites.tokenHash, tokenHash)).limit(1);
1198
+ return rows[0] ? toWorkspaceInvite(rows[0]) : null;
1199
+ }
1200
+ async revokeInvite(workspaceId, inviteId) {
1201
+ const rows = await this.db.delete(workspaceInvites).where(
1202
+ and(
1203
+ eq(workspaceInvites.workspaceId, workspaceId),
1204
+ eq(workspaceInvites.id, inviteId)
1205
+ )
1206
+ ).returning({ id: workspaceInvites.id });
1207
+ return rows.length > 0;
1208
+ }
1209
+ async acceptInvite(workspaceId, inviteId, userId) {
1210
+ return this.db.transaction(async (tx) => {
1211
+ const inviteRows = await tx.select().from(workspaceInvites).where(
1212
+ and(
1213
+ eq(workspaceInvites.workspaceId, workspaceId),
1214
+ eq(workspaceInvites.id, inviteId)
1215
+ )
1216
+ ).limit(1);
1217
+ const invite = inviteRows[0];
1218
+ if (!invite) {
1219
+ throw new HttpError({
1220
+ status: 404,
1221
+ code: ERROR_CODES.INVITE_NOT_FOUND,
1222
+ message: "Invite not found"
1223
+ });
1224
+ }
1225
+ if (invite.acceptedAt) {
1226
+ throw new HttpError({
1227
+ status: 410,
1228
+ code: ERROR_CODES.INVITE_ALREADY_ACCEPTED,
1229
+ message: "Invite already accepted"
1230
+ });
1231
+ }
1232
+ if (invite.expiresAt.getTime() <= Date.now()) {
1233
+ throw new HttpError({
1234
+ status: 410,
1235
+ code: ERROR_CODES.INVITE_EXPIRED,
1236
+ message: "Invite expired"
1237
+ });
1238
+ }
1239
+ const userRows = await tx.select({ email: users.email }).from(users).where(eq(users.id, userId)).limit(1);
1240
+ const user = userRows[0];
1241
+ const userEmail = user ? normalizeEmail(user.email) : null;
1242
+ if (!userEmail || userEmail !== normalizeEmail(invite.email)) {
1243
+ throw new HttpError({
1244
+ status: 403,
1245
+ code: ERROR_CODES.INVITE_EMAIL_MISMATCH,
1246
+ message: "Invite email mismatch"
1247
+ });
1248
+ }
1249
+ const acceptedRows = await tx.update(workspaceInvites).set({ acceptedAt: /* @__PURE__ */ new Date() }).where(
1250
+ and(
1251
+ eq(workspaceInvites.workspaceId, workspaceId),
1252
+ eq(workspaceInvites.id, inviteId),
1253
+ isNull(workspaceInvites.acceptedAt)
1254
+ )
1255
+ ).returning();
1256
+ if (acceptedRows.length === 0) {
1257
+ const refreshedRows = await tx.select().from(workspaceInvites).where(
1258
+ and(
1259
+ eq(workspaceInvites.workspaceId, workspaceId),
1260
+ eq(workspaceInvites.id, inviteId)
1261
+ )
1262
+ ).limit(1);
1263
+ const refreshed = refreshedRows[0];
1264
+ if (!refreshed) {
1265
+ throw new HttpError({
1266
+ status: 404,
1267
+ code: ERROR_CODES.INVITE_NOT_FOUND,
1268
+ message: "Invite not found"
1269
+ });
1270
+ }
1271
+ throw new HttpError({
1272
+ status: 410,
1273
+ code: ERROR_CODES.INVITE_ALREADY_ACCEPTED,
1274
+ message: "Invite already accepted"
1275
+ });
1276
+ }
1277
+ const memberRows = await tx.insert(workspaceMembers).values({
1278
+ workspaceId,
1279
+ userId,
1280
+ role: invite.role
1281
+ }).onConflictDoUpdate({
1282
+ target: [workspaceMembers.workspaceId, workspaceMembers.userId],
1283
+ set: { role: invite.role }
1284
+ }).returning();
1285
+ const acceptedInvite = acceptedRows[0];
1286
+ const member = memberRows[0];
1287
+ if (!acceptedInvite || !member) {
1288
+ throw new HttpError({
1289
+ status: 500,
1290
+ code: ERROR_CODES.INTERNAL_ERROR,
1291
+ message: "Failed to accept invite"
1292
+ });
1293
+ }
1294
+ return {
1295
+ invite: toWorkspaceInvite(acceptedInvite),
1296
+ member: toWorkspaceMember(member)
1297
+ };
1298
+ });
1299
+ }
1300
+ async incrementInviteFailedAttempts(inviteId) {
1301
+ const rows = await this.db.update(workspaceInvites).set({
1302
+ failedAttempts: sql4`${workspaceInvites.failedAttempts} + 1`,
1303
+ lockedUntil: sql4`CASE WHEN ${workspaceInvites.failedAttempts} + 1 >= 50 THEN now() + interval '1 hour' ELSE ${workspaceInvites.lockedUntil} END`
1304
+ }).where(eq(workspaceInvites.id, inviteId)).returning({
1305
+ failedAttempts: workspaceInvites.failedAttempts,
1306
+ lockedUntil: workspaceInvites.lockedUntil
1307
+ });
1308
+ if (rows.length === 0) return { failedAttempts: 0, lockedUntil: null };
1309
+ return {
1310
+ failedAttempts: rows[0].failedAttempts,
1311
+ lockedUntil: toIso(rows[0].lockedUntil)
1312
+ };
1313
+ }
1314
+ async resetInviteFailedAttempts(inviteId) {
1315
+ await this.db.update(workspaceInvites).set({ failedAttempts: 0, lockedUntil: null }).where(eq(workspaceInvites.id, inviteId));
1316
+ }
1317
+ async decryptSetting(workspaceId, key, db = this.db) {
1318
+ try {
1319
+ const rows = await db.select({
1320
+ plaintext: sql4`pgp_sym_decrypt(${workspaceSettings.value}, ${this.workspaceSettingsKey})::text`
1321
+ }).from(workspaceSettings).where(
1322
+ and(
1323
+ eq(workspaceSettings.workspaceId, workspaceId),
1324
+ eq(workspaceSettings.key, key)
1325
+ )
1326
+ ).limit(1);
1327
+ return rows[0]?.plaintext ?? null;
1328
+ } catch (err) {
1329
+ const code = err instanceof Error ? err.constructor.name : "unknown";
1330
+ console.error(`[workspace-store] decryptSetting failed for key="${key}" workspace="${workspaceId}": ${code}`);
1331
+ return null;
1332
+ }
1333
+ }
1334
+ async encryptAndPut(workspaceId, key, value, db = this.db) {
1335
+ if (!this.workspaceSettingsKey) {
1336
+ throw new Error("WORKSPACE_SETTINGS_ENCRYPTION_KEY is not configured \u2014 cannot store encrypted settings");
1337
+ }
1338
+ await db.execute(sql4`
1339
+ INSERT INTO workspace_settings (workspace_id, key, value, updated_at)
1340
+ VALUES (${workspaceId}::uuid, ${key}, pgp_sym_encrypt(${value}, ${this.workspaceSettingsKey}), NOW())
1341
+ ON CONFLICT (workspace_id, key)
1342
+ DO UPDATE SET value = pgp_sym_encrypt(${value}, ${this.workspaceSettingsKey}), updated_at = NOW()
1343
+ `);
1344
+ }
1345
+ async getWorkspaceSettings(workspaceId) {
1346
+ const rows = await this.db.select({ key: workspaceSettings.key, updatedAt: workspaceSettings.updatedAt }).from(workspaceSettings).where(eq(workspaceSettings.workspaceId, workspaceId)).orderBy(workspaceSettings.key);
1347
+ const metadata = [];
1348
+ for (const row of rows) {
1349
+ const decrypted = await this.decryptSetting(workspaceId, row.key);
1350
+ metadata.push({
1351
+ key: row.key,
1352
+ configured: decrypted !== null,
1353
+ updated_at: row.updatedAt.toISOString()
1354
+ });
1355
+ }
1356
+ return metadata;
1357
+ }
1358
+ async putWorkspaceSettings(workspaceId, settings) {
1359
+ await this.db.transaction(async (tx) => {
1360
+ for (const [key, value] of Object.entries(settings)) {
1361
+ await this.encryptAndPut(workspaceId, key, value, tx);
1362
+ }
1363
+ });
1364
+ return this.getWorkspaceSettings(workspaceId);
1365
+ }
1366
+ async getWorkspaceRuntime(workspaceId) {
1367
+ const appId = await this.getWorkspaceAppId(workspaceId);
1368
+ if (!appId) return null;
1369
+ const existingRows = await this.db.select().from(workspaceRuntimes).where(eq(workspaceRuntimes.workspaceId, workspaceId)).limit(1);
1370
+ if (existingRows.length > 0) {
1371
+ return toWorkspaceRuntime(existingRows[0]);
1372
+ }
1373
+ await this.db.insert(workspaceRuntimes).values({
1374
+ workspaceId,
1375
+ spriteUrl: null,
1376
+ spriteName: null,
1377
+ state: "ready",
1378
+ lastError: null,
1379
+ sandboxProvider: null,
1380
+ sandboxId: null,
1381
+ sandboxStatus: null,
1382
+ sandboxSnapshotId: null,
1383
+ sandboxCreatedAt: null,
1384
+ sandboxLastUsedAt: null,
1385
+ sandboxLastSeenAt: null,
1386
+ sandboxExpiresAt: null,
1387
+ provisioningStep: null,
1388
+ stepStartedAt: null,
1389
+ updatedAt: /* @__PURE__ */ new Date()
1390
+ }).onConflictDoNothing();
1391
+ const createdRows = await this.db.select().from(workspaceRuntimes).where(eq(workspaceRuntimes.workspaceId, workspaceId)).limit(1);
1392
+ if (createdRows.length === 0) return null;
1393
+ return toWorkspaceRuntime(createdRows[0]);
1394
+ }
1395
+ async putWorkspaceRuntime(workspaceId, state) {
1396
+ const existing = await this.getWorkspaceRuntime(workspaceId);
1397
+ if (!existing) {
1398
+ throw new Error(`Workspace ${workspaceId} not found`);
1399
+ }
1400
+ const merged = {
1401
+ ...existing,
1402
+ ...state,
1403
+ workspaceId,
1404
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1405
+ };
1406
+ const rows = await this.db.insert(workspaceRuntimes).values({
1407
+ workspaceId,
1408
+ spriteUrl: merged.spriteUrl,
1409
+ spriteName: merged.spriteName,
1410
+ state: merged.state,
1411
+ lastError: merged.lastError,
1412
+ volumePath: merged.volumePath,
1413
+ lastErrorOp: merged.lastErrorOp,
1414
+ sandboxProvider: merged.sandboxProvider ?? null,
1415
+ sandboxId: merged.sandboxId ?? null,
1416
+ sandboxStatus: merged.sandboxStatus ?? null,
1417
+ sandboxSnapshotId: merged.sandboxSnapshotId ?? null,
1418
+ sandboxCreatedAt: merged.sandboxCreatedAt ? new Date(merged.sandboxCreatedAt) : null,
1419
+ sandboxLastUsedAt: merged.sandboxLastUsedAt ? new Date(merged.sandboxLastUsedAt) : null,
1420
+ sandboxLastSeenAt: merged.sandboxLastSeenAt ? new Date(merged.sandboxLastSeenAt) : null,
1421
+ sandboxExpiresAt: merged.sandboxExpiresAt ? new Date(merged.sandboxExpiresAt) : null,
1422
+ provisioningStep: merged.provisioningStep,
1423
+ stepStartedAt: merged.stepStartedAt ? new Date(merged.stepStartedAt) : null,
1424
+ updatedAt: new Date(merged.updatedAt)
1425
+ }).onConflictDoUpdate({
1426
+ target: workspaceRuntimes.workspaceId,
1427
+ set: {
1428
+ spriteUrl: merged.spriteUrl,
1429
+ spriteName: merged.spriteName,
1430
+ state: merged.state,
1431
+ lastError: merged.lastError,
1432
+ volumePath: merged.volumePath,
1433
+ lastErrorOp: merged.lastErrorOp,
1434
+ sandboxProvider: merged.sandboxProvider ?? null,
1435
+ sandboxId: merged.sandboxId ?? null,
1436
+ sandboxStatus: merged.sandboxStatus ?? null,
1437
+ sandboxSnapshotId: merged.sandboxSnapshotId ?? null,
1438
+ sandboxCreatedAt: merged.sandboxCreatedAt ? new Date(merged.sandboxCreatedAt) : null,
1439
+ sandboxLastUsedAt: merged.sandboxLastUsedAt ? new Date(merged.sandboxLastUsedAt) : null,
1440
+ sandboxLastSeenAt: merged.sandboxLastSeenAt ? new Date(merged.sandboxLastSeenAt) : null,
1441
+ sandboxExpiresAt: merged.sandboxExpiresAt ? new Date(merged.sandboxExpiresAt) : null,
1442
+ provisioningStep: merged.provisioningStep,
1443
+ stepStartedAt: merged.stepStartedAt ? new Date(merged.stepStartedAt) : null,
1444
+ updatedAt: /* @__PURE__ */ new Date()
1445
+ }
1446
+ }).returning();
1447
+ return toWorkspaceRuntime(rows[0]);
1448
+ }
1449
+ async retryWorkspaceRuntime(workspaceId) {
1450
+ const rows = await this.db.update(workspaceRuntimes).set({
1451
+ state: "pending",
1452
+ lastError: null,
1453
+ updatedAt: /* @__PURE__ */ new Date()
1454
+ }).where(
1455
+ and(
1456
+ eq(workspaceRuntimes.workspaceId, workspaceId),
1457
+ eq(workspaceRuntimes.state, "error")
1458
+ )
1459
+ ).returning();
1460
+ if (rows.length === 0) return null;
1461
+ return toWorkspaceRuntime(rows[0]);
1462
+ }
1463
+ async listWorkspaceRuntimes() {
1464
+ const rows = await this.db.select().from(workspaceRuntimes);
1465
+ return rows.map((row) => toWorkspaceRuntime(row));
1466
+ }
1467
+ async getWorkspaceRuntimeResource(workspaceId, selector) {
1468
+ const rows = await this.db.select().from(workspaceRuntimeResources).where(
1469
+ and(
1470
+ eq(workspaceRuntimeResources.workspaceId, workspaceId),
1471
+ eq(workspaceRuntimeResources.kind, selector.kind),
1472
+ eq(workspaceRuntimeResources.purpose, selector.purpose),
1473
+ eq(workspaceRuntimeResources.provider, selector.provider),
1474
+ sql4`${workspaceRuntimeResources.state} <> 'deleted'`
1475
+ )
1476
+ ).limit(1);
1477
+ return rows[0] ? toWorkspaceRuntimeResource(rows[0]) : null;
1478
+ }
1479
+ async putWorkspaceRuntimeResource(workspaceId, resource) {
1480
+ const workspace = await this.get(workspaceId);
1481
+ if (!workspace) throw new Error(`Workspace ${workspaceId} not found`);
1482
+ const now = /* @__PURE__ */ new Date();
1483
+ const existing = await this.getWorkspaceRuntimeResource(workspaceId, resource);
1484
+ const values = {
1485
+ workspaceId,
1486
+ kind: resource.kind,
1487
+ purpose: resource.purpose,
1488
+ provider: resource.provider,
1489
+ handleKind: resource.handleKind,
1490
+ stableKey: resource.stableKey ?? null,
1491
+ providerResourceId: resource.providerResourceId ?? null,
1492
+ parentResourceId: resource.parentResourceId ?? null,
1493
+ state: resource.state,
1494
+ persistenceMode: resource.persistenceMode,
1495
+ config: resource.config ?? {},
1496
+ providerMeta: resource.providerMeta ?? {},
1497
+ lastError: resource.lastError ?? null,
1498
+ lastErrorCode: resource.lastErrorCode ?? null,
1499
+ updatedAt: now,
1500
+ lastSeenAt: toDate(resource.lastSeenAt),
1501
+ lastUsedAt: toDate(resource.lastUsedAt),
1502
+ expiresAt: toDate(resource.expiresAt),
1503
+ generation: resource.generation ?? (existing?.generation ?? -1) + 1
1504
+ };
1505
+ if (existing) {
1506
+ const rows2 = await this.db.update(workspaceRuntimeResources).set(values).where(eq(workspaceRuntimeResources.id, existing.id)).returning();
1507
+ return toWorkspaceRuntimeResource(rows2[0]);
1508
+ }
1509
+ const rows = await this.db.insert(workspaceRuntimeResources).values({
1510
+ id: resource.id,
1511
+ ...values,
1512
+ createdAt: now
1513
+ }).returning();
1514
+ return toWorkspaceRuntimeResource(rows[0]);
1515
+ }
1516
+ async deleteWorkspaceRuntimeResource(workspaceId, selector) {
1517
+ const existing = await this.getWorkspaceRuntimeResource(workspaceId, selector);
1518
+ if (!existing) return;
1519
+ await this.db.update(workspaceRuntimeResources).set({
1520
+ state: "deleted",
1521
+ updatedAt: /* @__PURE__ */ new Date()
1522
+ }).where(eq(workspaceRuntimeResources.id, existing.id));
1523
+ }
1524
+ async listWorkspaceRuntimeResources(workspaceId) {
1525
+ const base = this.db.select().from(workspaceRuntimeResources);
1526
+ const rows = workspaceId ? await base.where(eq(workspaceRuntimeResources.workspaceId, workspaceId)) : await base;
1527
+ return rows.map((row) => toWorkspaceRuntimeResource(row));
1528
+ }
1529
+ async getUiState(userId, workspaceId) {
1530
+ const appId = await this.getWorkspaceAppId(workspaceId);
1531
+ if (!appId) return null;
1532
+ const rows = await this.db.select({ settings: userSettings.settings }).from(userSettings).where(
1533
+ and(eq(userSettings.userId, userId), eq(userSettings.appId, appId))
1534
+ ).limit(1);
1535
+ const key = this.uiStateKey(workspaceId);
1536
+ const payload = rows[0]?.settings?.[key];
1537
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1538
+ return null;
1539
+ }
1540
+ return payload;
1541
+ }
1542
+ async putUiState(userId, workspaceId, state) {
1543
+ const appId = await this.getWorkspaceAppId(workspaceId);
1544
+ if (!appId) return;
1545
+ const patch = JSON.stringify({ [this.uiStateKey(workspaceId)]: state });
1546
+ await this.db.execute(sql4`
1547
+ INSERT INTO user_settings (user_id, app_id, display_name, email, settings, updated_at)
1548
+ VALUES (
1549
+ ${userId}::uuid,
1550
+ ${appId},
1551
+ COALESCE(
1552
+ (SELECT us.display_name FROM user_settings us WHERE us.user_id = ${userId}::uuid AND us.app_id = ${appId}),
1553
+ (SELECT COALESCE(u.name, '') FROM users u WHERE u.id = ${userId}::uuid),
1554
+ ''
1555
+ ),
1556
+ COALESCE(
1557
+ (SELECT us.email FROM user_settings us WHERE us.user_id = ${userId}::uuid AND us.app_id = ${appId}),
1558
+ (SELECT u.email FROM users u WHERE u.id = ${userId}::uuid),
1559
+ ''
1560
+ ),
1561
+ COALESCE(
1562
+ (SELECT us.settings FROM user_settings us WHERE us.user_id = ${userId}::uuid AND us.app_id = ${appId}),
1563
+ '{}'::jsonb
1564
+ ) || ${patch}::jsonb,
1565
+ NOW()
1566
+ )
1567
+ ON CONFLICT (user_id, app_id)
1568
+ DO UPDATE SET
1569
+ settings = COALESCE(user_settings.settings, '{}'::jsonb) || ${patch}::jsonb,
1570
+ updated_at = NOW()
1571
+ `);
1572
+ }
1573
+ };
1574
+
1575
+ // src/server/db/stores/PostgresUserStore.ts
1576
+ import { eq as eq2, sql as sql5 } from "drizzle-orm";
1577
+ function normalizeEmail2(email) {
1578
+ return email.trim().toLowerCase();
1579
+ }
1580
+ function rowToUser(row) {
1581
+ return {
1582
+ id: row.id,
1583
+ email: row.email,
1584
+ name: row.name,
1585
+ emailVerified: row.email_verified,
1586
+ image: row.image,
1587
+ createdAt: row.created_at.toISOString(),
1588
+ updatedAt: row.updated_at.toISOString()
1589
+ };
1590
+ }
1591
+ var PostgresUserStore = class {
1592
+ constructor(db) {
1593
+ this.db = db;
1594
+ }
1595
+ db;
1596
+ async getById(id) {
1597
+ const rows = await this.db.select().from(users).where(eq2(users.id, id)).limit(1);
1598
+ return rows.length > 0 ? rowToUser(rows[0]) : null;
1599
+ }
1600
+ async getByEmail(email) {
1601
+ const normalized = normalizeEmail2(email);
1602
+ const rows = await this.db.select().from(users).where(eq2(users.email, normalized)).limit(1);
1603
+ return rows.length > 0 ? rowToUser(rows[0]) : null;
1604
+ }
1605
+ async upsert(userId, data) {
1606
+ const normalized = normalizeEmail2(data.email);
1607
+ const rows = await this.db.insert(users).values({
1608
+ id: userId,
1609
+ email: normalized,
1610
+ name: data.name ?? "",
1611
+ email_verified: false
1612
+ }).onConflictDoUpdate({
1613
+ target: users.id,
1614
+ set: {
1615
+ email: normalized,
1616
+ ...data.name !== void 0 ? { name: data.name } : {},
1617
+ updated_at: /* @__PURE__ */ new Date()
1618
+ }
1619
+ }).returning();
1620
+ return rowToUser(rows[0]);
1621
+ }
1622
+ async getUserSettings(userId, appId) {
1623
+ const rows = await this.db.select().from(userSettings).where(
1624
+ sql5`${userSettings.userId} = ${userId} AND ${userSettings.appId} = ${appId}`
1625
+ ).limit(1);
1626
+ if (rows.length === 0) {
1627
+ const user = await this.getById(userId);
1628
+ return {
1629
+ displayName: user?.name ?? "",
1630
+ email: user?.email ?? "",
1631
+ settings: {}
1632
+ };
1633
+ }
1634
+ return {
1635
+ displayName: rows[0].displayName,
1636
+ email: rows[0].email,
1637
+ settings: rows[0].settings ?? {}
1638
+ };
1639
+ }
1640
+ async putUserSettings(userId, appId, updates) {
1641
+ const current = await this.getUserSettings(userId, appId);
1642
+ const nextDisplayName = updates.displayName ?? current.displayName;
1643
+ const nextEmail = updates.email ?? current.email;
1644
+ const nextSettings = updates.settings ?? current.settings;
1645
+ const rows = await this.db.insert(userSettings).values({
1646
+ userId,
1647
+ appId,
1648
+ displayName: nextDisplayName,
1649
+ email: nextEmail,
1650
+ settings: nextSettings
1651
+ }).onConflictDoUpdate({
1652
+ target: [userSettings.userId, userSettings.appId],
1653
+ set: {
1654
+ displayName: nextDisplayName,
1655
+ email: nextEmail,
1656
+ settings: nextSettings,
1657
+ updatedAt: /* @__PURE__ */ new Date()
1658
+ }
1659
+ }).returning();
1660
+ return {
1661
+ displayName: rows[0].displayName,
1662
+ email: rows[0].email,
1663
+ settings: rows[0].settings ?? {}
1664
+ };
1665
+ }
1666
+ };
1667
+
1668
+ export {
1669
+ users,
1670
+ verification_tokens,
1671
+ workspaces,
1672
+ workspaceMembers,
1673
+ workspaceSettings,
1674
+ workspaceRuntimes,
1675
+ workspaceInvites,
1676
+ idempotencyKeys,
1677
+ schema_exports,
1678
+ createDatabase,
1679
+ runMigrations,
1680
+ LocalUserStore,
1681
+ LocalWorkspaceStore,
1682
+ PostgresWorkspaceStore,
1683
+ PostgresUserStore
1684
+ };