@startup-api/cloudflare 0.0.1

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 (39) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +114 -0
  3. package/package.json +53 -0
  4. package/public/index.html +405 -0
  5. package/public/users/accounts.html +504 -0
  6. package/public/users/admin/index.html +765 -0
  7. package/public/users/power-strip.js +658 -0
  8. package/public/users/profile.html +443 -0
  9. package/public/users/style.css +493 -0
  10. package/src/CookieManager.ts +56 -0
  11. package/src/PowerStrip.ts +23 -0
  12. package/src/StartupAPIEnv.ts +12 -0
  13. package/src/auth/GoogleProvider.ts +67 -0
  14. package/src/auth/OAuthProvider.ts +52 -0
  15. package/src/auth/TwitchProvider.ts +64 -0
  16. package/src/auth/index.ts +231 -0
  17. package/src/billing/PaymentEngine.ts +20 -0
  18. package/src/billing/Plan.ts +80 -0
  19. package/src/billing/plansConfig.ts +48 -0
  20. package/src/handlers/account.ts +246 -0
  21. package/src/handlers/admin.ts +144 -0
  22. package/src/handlers/auth.ts +54 -0
  23. package/src/handlers/ssr.ts +274 -0
  24. package/src/handlers/user.ts +168 -0
  25. package/src/handlers/utils.ts +120 -0
  26. package/src/index.ts +190 -0
  27. package/src/schemas/account.ts +37 -0
  28. package/src/schemas/admin.ts +10 -0
  29. package/src/schemas/billing.ts +11 -0
  30. package/src/schemas/credential.ts +38 -0
  31. package/src/schemas/membership.ts +9 -0
  32. package/src/schemas/session.ts +10 -0
  33. package/src/schemas/user.ts +22 -0
  34. package/src/storage/AccountDO.ts +370 -0
  35. package/src/storage/CredentialDO.ts +82 -0
  36. package/src/storage/SystemDO.ts +264 -0
  37. package/src/storage/UserDO.ts +385 -0
  38. package/worker-configuration.d.ts +11696 -0
  39. package/wrangler.template.jsonc +55 -0
@@ -0,0 +1,370 @@
1
+ import { DurableObject } from 'cloudflare:workers';
2
+ import { initPlans } from '../billing/plansConfig';
3
+ import { Plan } from '../billing/Plan';
4
+ import { MockPaymentEngine } from '../billing/PaymentEngine';
5
+ import { StartupAPIEnv } from '../StartupAPIEnv';
6
+ import { AccountInfoSchema, MemberSchema } from '../schemas/account';
7
+ import { BillingStateSchema } from '../schemas/billing';
8
+ import type { AccountInfo, Member } from '../schemas/account';
9
+ import type { BillingState } from '../schemas/billing';
10
+
11
+ /**
12
+ * A Durable Object representing an Account (Tenant).
13
+ * This class handles account-specific data, settings, and memberships.
14
+ */
15
+ export class AccountDO extends DurableObject {
16
+ static ROLE_USER = 0;
17
+ static ROLE_ADMIN = 1;
18
+
19
+ sql: SqlStorage;
20
+ paymentEngine: MockPaymentEngine;
21
+
22
+ constructor(state: DurableObjectState, env: StartupAPIEnv) {
23
+ super(state, env);
24
+ this.sql = state.storage.sql;
25
+ this.paymentEngine = new MockPaymentEngine();
26
+
27
+ // Initialize plans if not already done
28
+ if (!Plan.isInitialized()) {
29
+ initPlans();
30
+ }
31
+
32
+ // Initialize database schema
33
+ this.sql.exec(`
34
+ CREATE TABLE IF NOT EXISTS account_info (
35
+ id INTEGER PRIMARY KEY CHECK (id = 1),
36
+ name TEXT,
37
+ plan TEXT,
38
+ billing TEXT,
39
+ personal INTEGER
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS members (
43
+ user_id TEXT PRIMARY KEY,
44
+ role INTEGER,
45
+ joined_at INTEGER
46
+ );
47
+ `);
48
+
49
+ // Ensure the single row exists
50
+ this.sql.exec('INSERT OR IGNORE INTO account_info (id, plan) VALUES (1, "free")');
51
+ }
52
+
53
+ async getImage(key: string): Promise<{ value: ArrayBuffer; mime_type: string } | null> {
54
+ const r2Key = `account/${this.ctx.id.toString()}/${key}`;
55
+ const object = await this.env.IMAGE_STORAGE.get(r2Key);
56
+ if (!object) return null;
57
+ return {
58
+ value: await object.arrayBuffer(),
59
+ mime_type: object.httpMetadata?.contentType || 'image/jpeg',
60
+ };
61
+ }
62
+
63
+ async storeImage(key: string, value: ArrayBuffer, mime_type: string): Promise<{ success: boolean }> {
64
+ const r2Key = `account/${this.ctx.id.toString()}/${key}`;
65
+ await this.env.IMAGE_STORAGE.put(r2Key, value, {
66
+ httpMetadata: { contentType: mime_type },
67
+ });
68
+ return { success: true };
69
+ }
70
+
71
+ async deleteImage(key: string): Promise<{ success: boolean }> {
72
+ const r2Key = `account/${this.ctx.id.toString()}/${key}`;
73
+ await this.env.IMAGE_STORAGE.delete(r2Key);
74
+ return { success: true };
75
+ }
76
+
77
+ async getInfo(): Promise<AccountInfo> {
78
+ try {
79
+ const result = this.sql.exec('SELECT * FROM account_info WHERE id = 1');
80
+ const row = result.next().value as any;
81
+ if (!row) return {};
82
+
83
+ return AccountInfoSchema.parse({
84
+ name: row.name,
85
+ plan: row.plan,
86
+ personal: row.personal === 1,
87
+ billing: row.billing ? JSON.parse(row.billing) : undefined,
88
+ });
89
+ } catch (_e) {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ async updateInfo(data: Record<string, any>): Promise<{ success: boolean }> {
95
+ try {
96
+ const validatedData = AccountInfoSchema.partial().parse(data);
97
+ const updates: string[] = [];
98
+ const values: any[] = [];
99
+
100
+ if ('name' in validatedData) {
101
+ updates.push('name = ?');
102
+ values.push(typeof validatedData.name === 'string' ? validatedData.name.substring(0, 50) : validatedData.name);
103
+ }
104
+ if ('plan' in validatedData) {
105
+ updates.push('plan = ?');
106
+ values.push(validatedData.plan);
107
+ }
108
+ if ('personal' in validatedData) {
109
+ updates.push('personal = ?');
110
+ values.push(validatedData.personal ? 1 : 0);
111
+ }
112
+
113
+ if (updates.length > 0) {
114
+ this.ctx.storage.transactionSync(() => {
115
+ this.sql.exec(`UPDATE account_info SET ${updates.join(', ')} WHERE id = 1`, ...values);
116
+
117
+ // If plan is updated manually (e.g. via Admin API), update billing state too
118
+ if ('plan' in data) {
119
+ const currentState = this.getBillingState();
120
+ if (currentState.plan_slug !== data.plan) {
121
+ const newState: BillingState = {
122
+ ...currentState,
123
+ plan_slug: data.plan,
124
+ };
125
+ this.sql.exec('UPDATE account_info SET billing = ? WHERE id = 1', JSON.stringify(newState));
126
+ }
127
+ }
128
+ });
129
+ }
130
+ return { success: true };
131
+ } catch (e) {
132
+ throw new Error(e instanceof Error ? e.message : String(e));
133
+ }
134
+ }
135
+
136
+ async getMembers(): Promise<Member[]> {
137
+ const result = Array.from(this.sql.exec('SELECT user_id, role, joined_at FROM members'));
138
+ const membersWithNames = await Promise.all(
139
+ result.map(async (m: any) => {
140
+ try {
141
+ const userStub = this.env.USER.get(this.env.USER.idFromString(m.user_id));
142
+ const profile = await userStub.getProfile();
143
+ const image = await userStub.getImage('avatar');
144
+
145
+ let picture = profile.picture || null;
146
+ if (image) {
147
+ picture = `/users/api/users/${m.user_id}/avatar`;
148
+ } else {
149
+ picture = null;
150
+ }
151
+
152
+ return MemberSchema.parse({
153
+ ...m,
154
+ name: profile.name || 'Unknown User',
155
+ picture: picture,
156
+ });
157
+ } catch (_e) {
158
+ return MemberSchema.parse({ ...m, name: 'Unknown User', picture: null });
159
+ }
160
+ }),
161
+ );
162
+ return membersWithNames;
163
+ }
164
+
165
+ async addMember(user_id: string, role: number): Promise<{ success: boolean }> {
166
+ const now = Date.now();
167
+
168
+ // Update Account DO
169
+ this.sql.exec('INSERT OR REPLACE INTO members (user_id, role, joined_at) VALUES (?, ?, ?)', user_id, role, now);
170
+
171
+ // Update SystemDO index
172
+ await this.syncMemberCount();
173
+
174
+ // Sync with User DO
175
+ try {
176
+ const userStub = this.env.USER.get(this.env.USER.idFromString(user_id));
177
+ await userStub.addMembership(this.ctx.id.toString(), role, false);
178
+ } catch (_e) {
179
+ console.error('Failed to sync membership to UserDO', _e);
180
+ }
181
+
182
+ return { success: true };
183
+ }
184
+
185
+ async updateMemberRole(userId: string, role: number): Promise<{ success: boolean }> {
186
+ this.sql.exec('UPDATE members SET role = ? WHERE user_id = ?', role, userId);
187
+
188
+ // Sync with User DO
189
+ try {
190
+ const userStub = this.env.USER.get(this.env.USER.idFromString(userId));
191
+ await userStub.addMembership(this.ctx.id.toString(), role, false);
192
+ } catch (_e) {
193
+ console.error('Failed to sync membership role to UserDO', _e);
194
+ }
195
+
196
+ return { success: true };
197
+ }
198
+
199
+ async removeMember(userId: string): Promise<{ success: boolean }> {
200
+ this.sql.exec('DELETE FROM members WHERE user_id = ?', userId);
201
+
202
+ // Update SystemDO index
203
+ await this.syncMemberCount();
204
+
205
+ // Sync with User DO
206
+ try {
207
+ const userStub = this.env.USER.get(this.env.USER.idFromString(userId));
208
+ await userStub.deleteMembership(this.ctx.id.toString());
209
+ } catch (_e) {
210
+ console.error('Failed to sync membership removal to UserDO', _e);
211
+ }
212
+
213
+ return { success: true };
214
+ }
215
+
216
+ private async syncMemberCount(): Promise<void> {
217
+ try {
218
+ const result = this.sql.exec('SELECT COUNT(*) as count FROM members');
219
+ const row = result.next().value as any;
220
+ const count = row ? row.count : 0;
221
+
222
+ const systemStub = this.env.SYSTEM.get(this.env.SYSTEM.idFromName('global'));
223
+ await systemStub.updateMemberCount(this.ctx.id.toString(), count);
224
+ } catch (_e) {
225
+ console.error('Failed to update member count in SystemDO', _e);
226
+ }
227
+ }
228
+
229
+ async delete(): Promise<{ success: boolean }> {
230
+ // Get all members to notify their UserDOs
231
+ const members = Array.from(this.sql.exec('SELECT user_id FROM members'));
232
+ for (const member of members as any[]) {
233
+ try {
234
+ const userStub = this.env.USER.get(this.env.USER.idFromString(member.user_id));
235
+ await userStub.deleteMembership(this.ctx.id.toString());
236
+ } catch (_e) {
237
+ console.error(`Failed to notify UserDO ${member.user_id} of account deletion`, _e);
238
+ }
239
+ }
240
+
241
+ // Delete all account images from R2
242
+ const prefix = `account/${this.ctx.id.toString()}/`;
243
+ const listed = await this.env.IMAGE_STORAGE.list({ prefix });
244
+ const keys = listed.objects.map((o) => o.key);
245
+ if (keys.length > 0) {
246
+ await this.env.IMAGE_STORAGE.delete(keys);
247
+ }
248
+
249
+ // Wipe all Durable Object storage
250
+ await this.ctx.storage.deleteAll();
251
+
252
+ return { success: true };
253
+ }
254
+
255
+ // Billing Implementation
256
+
257
+ private getBillingState(): BillingState {
258
+ try {
259
+ const result = this.sql.exec('SELECT billing FROM account_info WHERE id = 1');
260
+ const row = result.next().value as any;
261
+ if (row && row.billing) {
262
+ return BillingStateSchema.parse(JSON.parse(row.billing));
263
+ }
264
+ } catch (_e) {
265
+ // ignore
266
+ }
267
+ return {
268
+ plan_slug: 'free',
269
+ status: 'active',
270
+ };
271
+ }
272
+
273
+ private setBillingState(state: BillingState): void {
274
+ this.ctx.storage.transactionSync(() => {
275
+ this.sql.exec('UPDATE account_info SET billing = ?, plan = ? WHERE id = 1', JSON.stringify(state), state.plan_slug);
276
+ });
277
+ }
278
+
279
+ async getBillingInfo(): Promise<{ state: BillingState; plan_details: any }> {
280
+ const state = this.getBillingState();
281
+ const plan = Plan.get(state.plan_slug);
282
+
283
+ // Create a serializable version of the plan
284
+ const planDetails = plan
285
+ ? {
286
+ slug: plan.slug,
287
+ name: plan.name,
288
+ capabilities: plan.capabilities,
289
+ downgrade_to_slug: plan.downgrade_to_slug,
290
+ grace_period: plan.grace_period,
291
+ schedules: plan.schedules.map((s) => ({
292
+ charge_amount: s.charge_amount,
293
+ charge_period: s.charge_period,
294
+ is_default: s.is_default,
295
+ })),
296
+ }
297
+ : null;
298
+
299
+ return {
300
+ state,
301
+ plan_details: planDetails,
302
+ };
303
+ }
304
+
305
+ async subscribe(plan_slug: string, schedule_idx: number = 0): Promise<{ success: boolean; state: BillingState }> {
306
+ const plan = Plan.get(plan_slug);
307
+
308
+ if (!plan) {
309
+ throw new Error('Plan not found');
310
+ }
311
+
312
+ const currentState = this.getBillingState();
313
+
314
+ // Call hook if changing plans (simplification)
315
+ if (currentState.plan_slug !== plan_slug) {
316
+ if (currentState.plan_slug) {
317
+ const oldPlan = Plan.get(currentState.plan_slug);
318
+ if (oldPlan?.account_deactivate_hook) {
319
+ await oldPlan.account_deactivate_hook(this.ctx.id.toString());
320
+ }
321
+ }
322
+ if (plan.account_activate_hook) {
323
+ await plan.account_activate_hook(this.ctx.id.toString());
324
+ }
325
+ }
326
+
327
+ // Setup recurring payment
328
+ try {
329
+ await this.paymentEngine.setupRecurring(this.ctx.id.toString(), plan_slug, schedule_idx);
330
+ } catch (e) {
331
+ throw new Error(`Payment setup failed: ${e instanceof Error ? e.message : String(e)}`);
332
+ }
333
+
334
+ const newState: BillingState = {
335
+ ...currentState,
336
+ plan_slug,
337
+ status: 'active',
338
+ schedule_idx,
339
+ next_billing_date: Date.now() + (plan.schedules[schedule_idx]?.charge_period || 30) * 24 * 60 * 60 * 1000,
340
+ };
341
+
342
+ this.setBillingState(newState);
343
+
344
+ return { success: true, state: newState };
345
+ }
346
+
347
+ async cancelSubscription(): Promise<{ success: boolean; state: BillingState }> {
348
+ const currentState = this.getBillingState();
349
+ const currentPlan = Plan.get(currentState.plan_slug);
350
+
351
+ if (!currentPlan) {
352
+ throw new Error('No active plan');
353
+ }
354
+
355
+ await this.paymentEngine.cancelRecurring(this.ctx.id.toString());
356
+
357
+ // Downgrade logic (immediate or scheduled - simplification: scheduled if downgrade_to_slug exists)
358
+ // For this prototype, we'll mark it as canceled and set the next plan if applicable.
359
+
360
+ const newState: BillingState = {
361
+ ...currentState,
362
+ status: 'canceled',
363
+ next_plan_slug: currentPlan.downgrade_to_slug,
364
+ };
365
+
366
+ this.setBillingState(newState);
367
+
368
+ return { success: true, state: newState };
369
+ }
370
+ }
@@ -0,0 +1,82 @@
1
+ import { DurableObject } from 'cloudflare:workers';
2
+ import { StartupAPIEnv } from '../StartupAPIEnv';
3
+ import { OAuthCredentialSchema } from '../schemas/credential';
4
+ import type { OAuthCredential, OAuthCredentialOutput } from '../schemas/credential';
5
+
6
+ /**
7
+ * A Durable Object representing all OAuth credentials for a specific provider.
8
+ * Each instance is identified by the provider name (e.g., "google", "twitch").
9
+ */
10
+ export class CredentialDO extends DurableObject {
11
+ sql: SqlStorage;
12
+
13
+ constructor(state: DurableObjectState, env: StartupAPIEnv) {
14
+ super(state, env);
15
+ this.sql = state.storage.sql;
16
+
17
+ this.sql.exec(`
18
+ CREATE TABLE IF NOT EXISTS credentials (
19
+ subject_id TEXT PRIMARY KEY,
20
+ user_id TEXT NOT NULL,
21
+ access_token TEXT,
22
+ refresh_token TEXT,
23
+ expires_at INTEGER,
24
+ scope TEXT,
25
+ profile_data TEXT,
26
+ created_at INTEGER,
27
+ updated_at INTEGER
28
+ );
29
+ CREATE INDEX IF NOT EXISTS idx_user_id ON credentials(user_id);
30
+ `);
31
+ }
32
+
33
+ async get(subjectId: string): Promise<any | null> {
34
+ const result = this.sql.exec('SELECT * FROM credentials WHERE subject_id = ?', subjectId);
35
+ const row = result.next().value as any;
36
+ if (!row) return null;
37
+
38
+ row.profile_data = JSON.parse(row.profile_data);
39
+ return row;
40
+ }
41
+
42
+ async list(userId: string): Promise<any[]> {
43
+ const result = this.sql.exec('SELECT * FROM credentials WHERE user_id = ?', userId);
44
+ const credentials = [];
45
+ for (const row of result) {
46
+ (row as any).profile_data = JSON.parse((row as any).profile_data);
47
+ credentials.push(row);
48
+ }
49
+ return credentials;
50
+ }
51
+
52
+ async put(data: OAuthCredential): Promise<{ success: boolean }> {
53
+ console.log('[auth] Parsing Cred', data);
54
+
55
+ const validatedData: OAuthCredentialOutput = OAuthCredentialSchema.parse(data);
56
+
57
+ console.log('[auth] Validated Cred', validatedData);
58
+
59
+ const now = Date.now();
60
+
61
+ this.sql.exec(
62
+ `INSERT OR REPLACE INTO credentials
63
+ (subject_id, user_id, access_token, refresh_token, expires_at, scope, profile_data, created_at, updated_at)
64
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
65
+ validatedData.subject_id,
66
+ validatedData.user_id,
67
+ validatedData.access_token,
68
+ validatedData.refresh_token,
69
+ validatedData.expires_at,
70
+ validatedData.scope,
71
+ JSON.stringify(validatedData.profile_data),
72
+ validatedData.created_at || now,
73
+ now,
74
+ );
75
+ return { success: true };
76
+ }
77
+
78
+ async delete(subjectId: string): Promise<{ success: boolean }> {
79
+ this.sql.exec('DELETE FROM credentials WHERE subject_id = ?', subjectId);
80
+ return { success: true };
81
+ }
82
+ }
@@ -0,0 +1,264 @@
1
+ import { DurableObject } from 'cloudflare:workers';
2
+ import { StartupAPIEnv } from '../StartupAPIEnv';
3
+ import { SystemUserSchema } from '../schemas/user';
4
+ import { SystemAccountSchema } from '../schemas/account';
5
+ import type { SystemUser } from '../schemas/user';
6
+ import type { SystemAccount } from '../schemas/account';
7
+
8
+ export class SystemDO extends DurableObject {
9
+ sql: SqlStorage;
10
+
11
+ constructor(state: DurableObjectState, env: StartupAPIEnv) {
12
+ super(state, env);
13
+ this.sql = state.storage.sql;
14
+
15
+ this.sql.exec(`
16
+ CREATE TABLE IF NOT EXISTS users (
17
+ id TEXT PRIMARY KEY,
18
+ name TEXT,
19
+ email TEXT,
20
+ provider TEXT,
21
+ created_at INTEGER
22
+ );
23
+
24
+ CREATE TABLE IF NOT EXISTS accounts (
25
+ id TEXT PRIMARY KEY,
26
+ name TEXT,
27
+ status TEXT,
28
+ plan TEXT,
29
+ member_count INTEGER DEFAULT 0,
30
+ created_at INTEGER
31
+ );
32
+ `);
33
+ }
34
+
35
+ async listUsers(query?: string): Promise<any[]> {
36
+ let sql = 'SELECT * FROM users';
37
+ const args: any[] = [];
38
+
39
+ if (query) {
40
+ sql += ' WHERE name LIKE ? OR email LIKE ?';
41
+ args.push(`%${query}%`, `%${query}%`);
42
+ }
43
+
44
+ sql += ' ORDER BY created_at DESC LIMIT 50';
45
+
46
+ const result = this.sql.exec(sql, ...args);
47
+ const users = Array.from(result).map((u: any) => {
48
+ const adminIds = (this.env.ADMIN_IDS || '').split(',').map((id) => id.trim());
49
+ const isAdmin =
50
+ adminIds.includes(u.id) ||
51
+ (this.env.ENVIRONMENT === 'test' &&
52
+ adminIds.some((id) => {
53
+ try {
54
+ return u.id === this.env.USER.idFromName(id).toString();
55
+ } catch (_e) {
56
+ return false;
57
+ }
58
+ }));
59
+
60
+ return {
61
+ ...u,
62
+ is_admin: isAdmin,
63
+ };
64
+ });
65
+ return users;
66
+ }
67
+
68
+ async getUserMemberships(userId: string): Promise<any[]> {
69
+ const userStub = this.env.USER.get(this.env.USER.idFromString(userId));
70
+ return await userStub.getMemberships();
71
+ }
72
+
73
+ async getUser(userId: string): Promise<any> {
74
+ try {
75
+ const userStub = this.env.USER.get(this.env.USER.idFromString(userId));
76
+ const profile = await userStub.getProfile();
77
+ return profile;
78
+ } catch (e) {
79
+ throw new Error(e instanceof Error ? e.message : String(e));
80
+ }
81
+ }
82
+
83
+ async registerUser(data: SystemUser): Promise<{ success: boolean }> {
84
+ const validatedData = SystemUserSchema.parse(data);
85
+ const now = Date.now();
86
+
87
+ this.sql.exec(
88
+ 'INSERT OR REPLACE INTO users (id, name, email, provider, created_at) VALUES (?, ?, ?, ?, ?)',
89
+ validatedData.id,
90
+ validatedData.name,
91
+ validatedData.email || null,
92
+ validatedData.provider || null,
93
+ now,
94
+ );
95
+
96
+ return { success: true };
97
+ }
98
+
99
+ async deleteUser(userId: string): Promise<{ success: boolean }> {
100
+ // Delete from index
101
+ this.sql.exec('DELETE FROM users WHERE id = ?', userId);
102
+
103
+ // Call UserDO to delete its data
104
+ try {
105
+ const stub = this.env.USER.get(this.env.USER.idFromString(userId));
106
+ await stub.delete();
107
+ } catch (e) {
108
+ console.error('Failed to clear UserDO data', e);
109
+ }
110
+
111
+ return { success: true };
112
+ }
113
+
114
+ async updateUser(userId: string, data: Partial<SystemUser>): Promise<{ success: boolean }> {
115
+ const validatedData = SystemUserSchema.partial().parse(data);
116
+ // Update UserDO
117
+ try {
118
+ const userStub = this.env.USER.get(this.env.USER.idFromString(userId));
119
+ await userStub.updateProfile(validatedData);
120
+ } catch (e) {
121
+ console.error('Failed to update UserDO', e);
122
+ }
123
+
124
+ // Update Index
125
+ if (validatedData.name || validatedData.email) {
126
+ const updates: string[] = [];
127
+ const args: any[] = [];
128
+ if (validatedData.name !== undefined) {
129
+ updates.push('name = ?');
130
+ args.push(validatedData.name);
131
+ }
132
+ if (validatedData.email !== undefined) {
133
+ updates.push('email = ?');
134
+ args.push(validatedData.email);
135
+ }
136
+
137
+ if (updates.length > 0) {
138
+ args.push(userId);
139
+ this.sql.exec(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, ...args);
140
+ }
141
+ }
142
+
143
+ return { success: true };
144
+ }
145
+
146
+ async listAccounts(query?: string): Promise<any[]> {
147
+ let sql = 'SELECT * FROM accounts';
148
+ const args: any[] = [];
149
+
150
+ if (query) {
151
+ sql += ' WHERE name LIKE ?';
152
+ args.push(`%${query}%`);
153
+ }
154
+
155
+ sql += ' ORDER BY created_at DESC LIMIT 50';
156
+
157
+ const result = this.sql.exec(sql, ...args);
158
+ return Array.from(result);
159
+ }
160
+
161
+ async getAccount(accountId: string): Promise<any> {
162
+ try {
163
+ const stub = this.env.ACCOUNT.get(this.env.ACCOUNT.idFromString(accountId));
164
+ const info = await stub.getInfo();
165
+ const billing = await stub.getBillingInfo();
166
+
167
+ return { ...info, billing };
168
+ } catch (e) {
169
+ throw new Error(e instanceof Error ? e.message : String(e));
170
+ }
171
+ }
172
+
173
+ async registerAccount(data: SystemAccount): Promise<{ success: boolean; id: string }> {
174
+ const validatedData = SystemAccountSchema.parse(data);
175
+ let accountIdStr = validatedData.id;
176
+ const accountName = validatedData.name;
177
+
178
+ if (!accountIdStr) {
179
+ const id = this.env.ACCOUNT.newUniqueId();
180
+ accountIdStr = id.toString();
181
+
182
+ // Initialize AccountDO
183
+ const stub = this.env.ACCOUNT.get(id);
184
+ await stub.updateInfo({
185
+ name: accountName,
186
+ });
187
+
188
+ // If owner provided, add them as ADMIN
189
+ if (validatedData.ownerId) {
190
+ await stub.addMember(validatedData.ownerId, 1);
191
+ }
192
+ }
193
+
194
+ const now = Date.now();
195
+
196
+ this.sql.exec(
197
+ 'INSERT OR REPLACE INTO accounts (id, name, status, plan, member_count, created_at) VALUES (?, ?, ?, ?, ?, ?)',
198
+ accountIdStr,
199
+ accountName,
200
+ validatedData.status || 'active',
201
+ validatedData.plan || 'free',
202
+ validatedData.ownerId ? 1 : 0,
203
+ now,
204
+ );
205
+
206
+ return { success: true, id: accountIdStr };
207
+ }
208
+
209
+ async deleteAccount(accountId: string): Promise<{ success: boolean }> {
210
+ // Delete from index
211
+ this.sql.exec('DELETE FROM accounts WHERE id = ?', accountId);
212
+
213
+ // Call AccountDO to delete its data
214
+ try {
215
+ const stub = this.env.ACCOUNT.get(this.env.ACCOUNT.idFromString(accountId));
216
+ await stub.delete();
217
+ } catch (e) {
218
+ console.error('Failed to clear AccountDO data', e);
219
+ }
220
+
221
+ return { success: true };
222
+ }
223
+
224
+ async updateMemberCount(accountId: string, count: number): Promise<void> {
225
+ this.sql.exec('UPDATE accounts SET member_count = ? WHERE id = ?', count, accountId);
226
+ }
227
+
228
+ async updateAccount(accountId: string, data: Partial<SystemAccount>): Promise<{ success: boolean }> {
229
+ const validatedData = SystemAccountSchema.partial().parse(data);
230
+
231
+ // Update AccountDO
232
+ try {
233
+ const stub = this.env.ACCOUNT.get(this.env.ACCOUNT.idFromString(accountId));
234
+ await stub.updateInfo(validatedData);
235
+ } catch (e) {
236
+ console.error('Failed to update AccountDO', e);
237
+ }
238
+
239
+ // Update Index
240
+ const updates: string[] = [];
241
+ const args: any[] = [];
242
+
243
+ if (validatedData.name !== undefined) {
244
+ updates.push('name = ?');
245
+ args.push(validatedData.name);
246
+ }
247
+ if (validatedData.status !== undefined) {
248
+ updates.push('status = ?');
249
+ args.push(validatedData.status);
250
+ }
251
+ // Plan update usually via billing, but if forced:
252
+ if (validatedData.plan !== undefined) {
253
+ updates.push('plan = ?');
254
+ args.push(validatedData.plan);
255
+ }
256
+
257
+ if (updates.length > 0) {
258
+ args.push(accountId);
259
+ this.sql.exec(`UPDATE accounts SET ${updates.join(', ')} WHERE id = ?`, ...args);
260
+ }
261
+
262
+ return { success: true };
263
+ }
264
+ }