@teammy/server 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -4,6 +4,8 @@ import { setTimeout as delay } from "node:timers/promises";
4
4
  // apps/server/store.ts
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { join } from "node:path";
7
+ import { existsSync } from "node:fs";
8
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
7
9
 
8
10
  // packages/state/sqlite.ts
9
11
  import { chmodSync, mkdirSync } from "node:fs";
@@ -20,13 +22,15 @@ function transaction(db, action) {
20
22
  throw error;
21
23
  }
22
24
  }
23
- function openDatabase(path, migrations2) {
25
+ function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
26
+ if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0)
27
+ throw new Error("Invalid SQLite busy timeout.");
24
28
  mkdirSync(dirname(path), { recursive: true, mode: 448 });
25
29
  const db = new DatabaseSync(path);
26
30
  try {
27
31
  chmodSync(path, 384);
28
32
  db.exec(
29
- "PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;"
33
+ `PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;`
30
34
  );
31
35
  transaction(db, () => {
32
36
  const version = Number(db.prepare("PRAGMA user_version").get().user_version);
@@ -48,7 +52,7 @@ function openDatabase(path, migrations2) {
48
52
  import { z } from "zod";
49
53
  var NameSchema = z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/);
50
54
  var DeviceIdSchema = z.uuid();
51
- var BindRequestSchema = z.object({ deviceId: DeviceIdSchema }).strict();
55
+ var BindRequestSchema = z.object({ deviceId: DeviceIdSchema, projectId: NameSchema.optional() }).strict();
52
56
  var BindingSchema = z.object({
53
57
  workspaceId: NameSchema,
54
58
  memberId: NameSchema,
@@ -97,6 +101,7 @@ var sessionMigration = String.raw`
97
101
  CREATE INDEX session_message_lookup ON session_messages(workspace_id, owner, session_key);
98
102
  CREATE VIRTUAL TABLE session_fts USING fts5(body, tokenize='trigram');
99
103
  `;
104
+ var sessionOwnerIndexMigration = `CREATE INDEX session_owner_listing ON sessions(workspace_id,owner,id);`;
100
105
 
101
106
  // apps/server/control-schema.ts
102
107
  var controlMigration = String.raw`
@@ -118,6 +123,45 @@ var projectMigration = String.raw`
118
123
  CREATE TABLE project_settings(id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL, setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL);
119
124
  CREATE TABLE web_sessions(token_hash TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, member_id TEXT NOT NULL, expires_at INTEGER NOT NULL);
120
125
  `;
126
+ var multiProjectMigration = String.raw`
127
+ DROP TABLE project_settings;
128
+ DROP TABLE web_sessions;
129
+ ALTER TABLE members DROP COLUMN display_name;
130
+ CREATE TABLE users (
131
+ id TEXT PRIMARY KEY, display_name TEXT NOT NULL DEFAULT '',
132
+ site_role TEXT NOT NULL DEFAULT 'member', enabled INTEGER NOT NULL DEFAULT 1,
133
+ revision INTEGER NOT NULL DEFAULT 1
134
+ );
135
+ CREATE TABLE projects (
136
+ id TEXT PRIMARY KEY, config_json TEXT NOT NULL,
137
+ operation_id TEXT NOT NULL UNIQUE, fingerprint TEXT NOT NULL
138
+ );
139
+ CREATE TABLE site_settings (
140
+ id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL,
141
+ setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL
142
+ );
143
+ CREATE TABLE web_sessions (
144
+ token_hash TEXT PRIMARY KEY, member_id TEXT NOT NULL REFERENCES users(id), expires_at INTEGER NOT NULL
145
+ );
146
+ DROP TABLE ai_profiles;
147
+ CREATE TABLE ai_profiles (
148
+ id TEXT PRIMARY KEY, revision INTEGER NOT NULL, config_json TEXT NOT NULL, secret TEXT
149
+ );
150
+ `;
151
+ var sitePolicyMigration = `
152
+ UPDATE site_settings SET config_json=json_set(config_json,'$.sync',json(COALESCE(
153
+ (SELECT json_extract(config_json,'$.sync') FROM projects ORDER BY rowid LIMIT 1),
154
+ '{"checkIntervalSeconds":60,"fileSettleSeconds":60}'
155
+ )));
156
+ UPDATE projects SET config_json=json_set(config_json,
157
+ '$.sync',json((SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1)),
158
+ '$.configRevision',json_extract(config_json,'$.configRevision')+1)
159
+ WHERE json_extract(config_json,'$.sync') != (SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1);
160
+ `;
161
+ var equalMembershipMigration = `
162
+ ALTER TABLE members DROP COLUMN role;
163
+ ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
164
+ `;
121
165
 
122
166
  // apps/server/file-schema.ts
123
167
  var fileMigration = String.raw`
@@ -168,6 +212,11 @@ CREATE TABLE ai_attempts(job_id TEXT NOT NULL REFERENCES ai_jobs(id),number INTE
168
212
  CREATE TABLE ai_events(seq INTEGER PRIMARY KEY AUTOINCREMENT,job_id TEXT NOT NULL REFERENCES ai_jobs(id),attempt INTEGER NOT NULL,kind TEXT NOT NULL,message TEXT NOT NULL,created_at TEXT NOT NULL);
169
213
  CREATE INDEX ai_events_job ON ai_events(job_id,seq);
170
214
  `;
215
+ var aiDefaultsMigration = `
216
+ CREATE TABLE ai_defaults (
217
+ scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
218
+ );
219
+ `;
171
220
 
172
221
  // apps/server/store.ts
173
222
  function digest(token) {
@@ -189,12 +238,30 @@ var migrations = [
189
238
  deviceMigration,
190
239
  controlMigration,
191
240
  aiConfigMigration,
192
- aiJobsMigration
241
+ aiJobsMigration,
242
+ multiProjectMigration,
243
+ aiDefaultsMigration,
244
+ sitePolicyMigration,
245
+ sessionOwnerIndexMigration,
246
+ equalMembershipMigration
193
247
  ];
194
248
  var HubStore = class {
195
249
  constructor(directory) {
196
250
  this.directory = directory;
197
- this.db = openDatabase(join(directory, "hub.sqlite3"), migrations);
251
+ const path = join(directory, "hub.sqlite3");
252
+ if (existsSync(path)) {
253
+ const existing = new DatabaseSync2(path, { readOnly: true });
254
+ try {
255
+ const version = Number(existing.prepare("PRAGMA user_version").get().user_version);
256
+ if (version > 0 && version < 9)
257
+ throw new Error(
258
+ "Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
259
+ );
260
+ } finally {
261
+ existing.close();
262
+ }
263
+ }
264
+ this.db = openDatabase(path, migrations);
198
265
  }
199
266
  directory;
200
267
  db;
@@ -206,6 +273,7 @@ var HubStore = class {
206
273
  NameSchema.parse(memberId);
207
274
  const token = `tmy_${randomBytes(32).toString("hex")}`;
208
275
  transaction(this.db, () => {
276
+ this.db.prepare("INSERT OR IGNORE INTO users(id) VALUES (?)").run(memberId);
209
277
  const exists = this.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(workspaceId, memberId);
210
278
  if (exists && !reset)
211
279
  throw new HubError(
@@ -226,12 +294,12 @@ var HubStore = class {
226
294
  "UPDATE members SET token_hash=NULL, device_id=NULL, enabled=0, member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
227
295
  ).run(NameSchema.parse(workspaceId), NameSchema.parse(memberId));
228
296
  if (!result.changes) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
229
- this.db.prepare("DELETE FROM web_sessions WHERE workspace_id=? AND member_id=?").run(workspaceId, memberId);
297
+ this.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(workspaceId, memberId);
230
298
  }
231
299
  identity(authorization) {
232
300
  const match = authorization?.match(/^Bearer (tmy_[a-f0-9]{64})$/);
233
301
  const row = match && this.db.prepare(
234
- "SELECT workspace_id, member_id, device_id FROM members WHERE token_hash=? AND enabled=1"
302
+ "SELECT m.workspace_id,m.member_id,m.device_id FROM members m JOIN users u ON u.id=m.member_id WHERE m.token_hash=? AND m.enabled=1 AND u.enabled=1"
235
303
  ).get(digest(match[1]));
236
304
  if (!row) throw new HubError(401, "UNAUTHORIZED", "A valid member credential is required.");
237
305
  return {
@@ -240,9 +308,11 @@ var HubStore = class {
240
308
  deviceId: row.device_id
241
309
  };
242
310
  }
243
- bind(authorization, deviceId) {
311
+ bind(authorization, deviceId, projectId2) {
244
312
  return transaction(this.db, () => {
245
313
  const identity = this.identity(authorization);
314
+ if (projectId2 && projectId2 !== identity.workspaceId)
315
+ throw new HubError(403, "PROJECT_MISMATCH", "Token belongs to a different project.");
246
316
  if (identity.deviceId && identity.deviceId !== deviceId)
247
317
  throw new HubError(
248
318
  409,
@@ -269,7 +339,7 @@ var HubStore = class {
269
339
  };
270
340
 
271
341
  // apps/server/ai/jobs.ts
272
- import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
342
+ import { createHash as createHash6, randomUUID as randomUUID2 } from "node:crypto";
273
343
 
274
344
  // packages/protocol/ai.ts
275
345
  import { z as z4 } from "zod";
@@ -368,41 +438,83 @@ var SetupSchema = z3.object({
368
438
  publicServerUrl: ServerUrlSchema,
369
439
  adminId: NameSchema,
370
440
  members: z3.array(NameSchema).max(100).default([]),
371
- existingWorkspaceId: NameSchema.optional(),
372
441
  sync: SyncPolicySchema.default(DEFAULT_POLICY)
373
442
  }).strict();
374
443
  var SetupStatusSchema = z3.object({
375
- initialized: z3.boolean(),
376
- existingWorkspaces: z3.array(NameSchema)
444
+ initialized: z3.boolean()
377
445
  });
378
446
  var ConfigUpdateSchema = ProjectConfigSchema.pick({
379
447
  projectName: true,
380
- description: true,
381
- publicServerUrl: true,
382
- sync: true
383
- }).extend({ expectedRevision: RevisionSchema }).strict();
448
+ description: true
449
+ }).extend({ expectedRevision: RevisionSchema.optional() }).strict();
384
450
  var MemberSchema = z3.object({
385
451
  id: NameSchema,
386
452
  displayName: z3.string().max(120),
387
- role: z3.enum(["admin", "member"]),
388
453
  enabled: z3.boolean(),
454
+ accountEnabled: z3.boolean(),
389
455
  revision: RevisionSchema,
390
456
  bound: z3.boolean(),
391
457
  fileRevision: RevisionSchema,
392
458
  lastPublishedAt: z3.string().nullable()
393
459
  });
394
- var WebSessionSchema = z3.object({
395
- workspaceId: NameSchema,
460
+ var WebIdentitySchema = z3.object({
396
461
  memberId: NameSchema,
397
462
  displayName: z3.string(),
398
- role: z3.enum(["admin", "member"])
463
+ siteRole: z3.enum(["admin", "member"])
464
+ });
465
+ var WebSessionSchema = WebIdentitySchema.extend({
466
+ workspaceId: NameSchema
399
467
  });
400
- var MemberCreateSchema = z3.object({ id: NameSchema, displayName: z3.string().trim().max(120).default("") }).strict();
468
+ var MemberCreateSchema = z3.object({
469
+ id: NameSchema
470
+ }).strict();
401
471
  var MemberUpdateSchema = z3.object({
402
472
  expectedRevision: RevisionSchema,
403
473
  displayName: z3.string().trim().max(120).optional(),
404
474
  enabled: z3.boolean().optional()
405
475
  }).strict();
476
+ var ProjectCreateSchema = z3.object({
477
+ operationId: z3.uuid(),
478
+ projectName: z3.string().trim().min(1).max(120),
479
+ description: z3.string().max(2e3).default("")
480
+ }).strict();
481
+ var ProjectSummarySchema = z3.object({
482
+ projectId: NameSchema,
483
+ projectName: z3.string(),
484
+ description: z3.string()
485
+ });
486
+ var ProjectsSchema = z3.object({ projects: z3.array(ProjectSummarySchema) });
487
+ var SiteConfigSchema = z3.object({
488
+ sync: SyncPolicySchema,
489
+ publicServerUrl: ServerUrlSchema,
490
+ revision: RevisionSchema
491
+ });
492
+ var SiteConfigUpdateSchema = z3.object({
493
+ publicServerUrl: ServerUrlSchema.optional(),
494
+ sync: SyncPolicySchema.optional(),
495
+ expectedRevision: RevisionSchema
496
+ }).strict().refine(
497
+ (v) => v.publicServerUrl !== void 0 || v.sync !== void 0,
498
+ "Provide a setting to update."
499
+ );
500
+ var UserSchema = z3.object({
501
+ id: NameSchema,
502
+ displayName: z3.string(),
503
+ siteRole: z3.enum(["admin", "member"]),
504
+ enabled: z3.boolean(),
505
+ revision: RevisionSchema
506
+ });
507
+ var UserCreateSchema = z3.object({
508
+ id: NameSchema,
509
+ displayName: z3.string().trim().max(120).default(""),
510
+ siteRole: z3.enum(["admin", "member"]).default("member")
511
+ }).strict();
512
+ var UserUpdateSchema = z3.object({
513
+ expectedRevision: RevisionSchema,
514
+ displayName: z3.string().trim().max(120).optional(),
515
+ enabled: z3.boolean().optional(),
516
+ siteRole: z3.enum(["admin", "member"]).optional()
517
+ }).strict();
406
518
 
407
519
  // packages/protocol/ai.ts
408
520
  var AiProfileSchema = z4.object({
@@ -428,6 +540,17 @@ var AiProfileUpdateSchema = z4.object({
428
540
  apiKey: z4.string().min(1).max(4096).nullable().optional()
429
541
  }).strict();
430
542
  var AiProfilesSchema = z4.object({ profiles: z4.array(AiPublicProfileSchema) });
543
+ var AiDefaultUpdateSchema = z4.object({
544
+ expectedRevision: z4.number().int().nonnegative(),
545
+ profileId: NameSchema.nullable()
546
+ }).strict();
547
+ var AiDefaultSchema = z4.object({
548
+ revision: z4.number().int().nonnegative(),
549
+ profileId: NameSchema.nullable(),
550
+ effectiveProfileId: NameSchema.nullable(),
551
+ source: z4.enum(["site", "project", "none"]),
552
+ available: z4.boolean()
553
+ });
431
554
  var AiProbeSchema = z4.object({ mode: z4.enum(["text", "tools", "json"]) }).strict();
432
555
  var AiProbeResultSchema = z4.object({
433
556
  ok: z4.literal(true),
@@ -444,7 +567,7 @@ var AiSelectionSchema = z4.object({
444
567
  }).strict().refine((s) => s.kind === "directory" || s.path !== "", "A file path is required.");
445
568
  var AiJobCreateSchema = z4.object({
446
569
  operationId: z4.uuid(),
447
- profileId: NameSchema,
570
+ profileId: NameSchema.optional(),
448
571
  workflow: z4.enum(["file-summary", "directory-analysis"]),
449
572
  selection: AiSelectionSchema,
450
573
  question: z4.string().trim().max(2e3).default("")
@@ -521,14 +644,357 @@ var AiAttemptSchema = z4.object({
521
644
  });
522
645
  var AiJobDetailSchema = z4.object({ job: AiJobSchema, attempts: z4.array(AiAttemptSchema) });
523
646
 
647
+ // apps/server/project.ts
648
+ import { randomBytes as randomBytes3 } from "node:crypto";
649
+
650
+ // apps/server/site.ts
651
+ import { randomBytes as randomBytes2, createHash as createHash2 } from "node:crypto";
652
+ var digest2 = (value) => createHash2("sha256").update(value).digest("hex");
653
+ function requireSiteAdmin(identity) {
654
+ if (identity.siteRole !== "admin")
655
+ throw new HubError(403, "SITE_ADMIN_REQUIRED", "Site administrator access required.");
656
+ }
657
+ var SiteRepository = class {
658
+ constructor(hub) {
659
+ this.hub = hub;
660
+ }
661
+ hub;
662
+ status() {
663
+ return {
664
+ initialized: Boolean(this.hub.db.prepare("SELECT 1 FROM site_settings").get())
665
+ };
666
+ }
667
+ config() {
668
+ const row = this.hub.db.prepare("SELECT config_json FROM site_settings WHERE id=1").get();
669
+ if (!row) throw new HubError(409, "SETUP_REQUIRED", "Initialize the server first.");
670
+ return SiteConfigSchema.parse(JSON.parse(String(row.config_json)));
671
+ }
672
+ updateConfig(identity, input) {
673
+ requireSiteAdmin(identity);
674
+ const value = SiteConfigUpdateSchema.parse(input);
675
+ return transaction(this.hub.db, () => {
676
+ const current = this.config();
677
+ if (current.revision !== value.expectedRevision)
678
+ throw new HubError(409, "STALE_CONFIG", "Settings changed; reload before saving.");
679
+ const next = {
680
+ ...current,
681
+ publicServerUrl: value.publicServerUrl ?? current.publicServerUrl,
682
+ sync: value.sync ?? current.sync
683
+ };
684
+ if (JSON.stringify(next) === JSON.stringify(current)) return current;
685
+ next.revision++;
686
+ this.hub.db.prepare("UPDATE site_settings SET config_json=? WHERE id=1").run(JSON.stringify(next));
687
+ for (const row of this.hub.db.prepare("SELECT id,config_json FROM projects").all()) {
688
+ const config = ProjectConfigSchema.parse(JSON.parse(String(row.config_json)));
689
+ config.publicServerUrl = next.publicServerUrl;
690
+ config.sync = next.sync;
691
+ config.configRevision++;
692
+ this.hub.db.prepare("UPDATE projects SET config_json=? WHERE id=?").run(JSON.stringify(config), row.id);
693
+ }
694
+ return next;
695
+ });
696
+ }
697
+ login(id) {
698
+ const row = this.hub.db.prepare("SELECT 1 FROM users WHERE id=? AND enabled=1").get(id);
699
+ if (!row)
700
+ throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
701
+ const token = randomBytes2(32).toString("hex");
702
+ this.hub.db.prepare("DELETE FROM web_sessions WHERE expires_at<=?").run(Date.now());
703
+ this.hub.db.prepare("INSERT INTO web_sessions VALUES (?,?,?)").run(digest2(token), id, Date.now() + 7 * 864e5);
704
+ return { token, session: this.identity(token) };
705
+ }
706
+ identity(token) {
707
+ const row = token && /^[a-f0-9]{64}$/.test(token) ? this.hub.db.prepare(
708
+ `SELECT u.* FROM web_sessions s JOIN users u ON u.id=s.member_id WHERE s.token_hash=? AND s.expires_at>? AND u.enabled=1`
709
+ ).get(digest2(token), Date.now()) : void 0;
710
+ if (!row)
711
+ throw new HubError(401, "LOGIN_REQUIRED", "Please sign in with an existing member ID.");
712
+ return {
713
+ memberId: String(row.id),
714
+ displayName: String(row.display_name || row.id),
715
+ siteRole: row.site_role
716
+ };
717
+ }
718
+ logout(token) {
719
+ if (token)
720
+ this.hub.db.prepare("DELETE FROM web_sessions WHERE token_hash=?").run(digest2(token));
721
+ }
722
+ users(identity) {
723
+ requireSiteAdmin(identity);
724
+ return this.hub.db.prepare("SELECT * FROM users ORDER BY id").all().map(
725
+ (row) => UserSchema.parse({
726
+ id: row.id,
727
+ displayName: row.display_name || row.id,
728
+ siteRole: row.site_role,
729
+ enabled: Boolean(row.enabled),
730
+ revision: row.revision
731
+ })
732
+ );
733
+ }
734
+ createUser(identity, input) {
735
+ requireSiteAdmin(identity);
736
+ const value = UserCreateSchema.parse(input);
737
+ if (!this.hub.db.prepare("INSERT OR IGNORE INTO users(id,display_name,site_role) VALUES (?,?,?)").run(value.id, value.displayName, value.siteRole).changes)
738
+ throw new HubError(409, "USER_EXISTS", "User already exists.");
739
+ return this.users(identity).find((u) => u.id === value.id);
740
+ }
741
+ updateUser(identity, id, input) {
742
+ requireSiteAdmin(identity);
743
+ const value = UserUpdateSchema.parse(input);
744
+ return transaction(this.hub.db, () => {
745
+ const row = this.hub.db.prepare("SELECT * FROM users WHERE id=?").get(id);
746
+ if (!row) throw new HubError(404, "USER_NOT_FOUND", "User not found.");
747
+ if (row.revision !== value.expectedRevision)
748
+ throw new HubError(409, "STALE_MEMBER", "User changed; reload before saving.");
749
+ if ((value.enabled === false || value.siteRole === "member") && row.site_role === "admin" && row.enabled && Number(
750
+ this.hub.db.prepare("SELECT COUNT(*) n FROM users WHERE site_role='admin' AND enabled=1").get().n
751
+ ) <= 1)
752
+ throw new HubError(409, "LAST_ADMIN", "Keep at least one enabled site administrator.");
753
+ this.hub.db.prepare(
754
+ "UPDATE users SET display_name=?,enabled=?,site_role=?,revision=revision+1 WHERE id=?"
755
+ ).run(
756
+ value.displayName ?? row.display_name,
757
+ value.enabled === void 0 ? row.enabled : Number(value.enabled),
758
+ value.siteRole ?? row.site_role,
759
+ id
760
+ );
761
+ if (value.enabled === false) {
762
+ this.hub.db.prepare("DELETE FROM web_sessions WHERE member_id=?").run(id);
763
+ this.hub.db.prepare(
764
+ "UPDATE members SET token_hash=NULL,device_id=NULL,member_revision=member_revision+1 WHERE member_id=?"
765
+ ).run(id);
766
+ this.hub.db.prepare("DELETE FROM device_status WHERE member_id=?").run(id);
767
+ }
768
+ return this.users(identity).find((u) => u.id === id);
769
+ });
770
+ }
771
+ renameSelf(identity, displayName) {
772
+ this.hub.db.prepare("UPDATE users SET display_name=?,revision=revision+1 WHERE id=?").run(displayName, identity.memberId);
773
+ return { ...identity, displayName: displayName || identity.memberId };
774
+ }
775
+ };
776
+
777
+ // apps/server/project.ts
778
+ var ProjectRepository = class {
779
+ constructor(hub) {
780
+ this.hub = hub;
781
+ this.site = new SiteRepository(hub);
782
+ }
783
+ hub;
784
+ site;
785
+ config(identity) {
786
+ const row = this.hub.db.prepare("SELECT config_json FROM projects WHERE id=?").get(identity.workspaceId);
787
+ if (!row) throw new HubError(404, "PROJECT_NOT_FOUND", "Project not found.");
788
+ return ProjectConfigSchema.parse(JSON.parse(String(row.config_json)));
789
+ }
790
+ insertProject(value, creator, url) {
791
+ const fingerprint = digest2(JSON.stringify([creator, value]));
792
+ const prior = this.hub.db.prepare("SELECT id,fingerprint FROM projects WHERE operation_id=?").get(value.operationId);
793
+ if (prior) {
794
+ if (prior.fingerprint !== fingerprint)
795
+ throw new HubError(
796
+ 409,
797
+ "OPERATION_MISMATCH",
798
+ "Operation was reused with different content."
799
+ );
800
+ return this.config({ workspaceId: String(prior.id), memberId: creator });
801
+ }
802
+ const config = ProjectConfigSchema.parse({
803
+ schemaVersion: 1,
804
+ projectId: `proj-${randomBytes3(8).toString("hex")}`,
805
+ projectName: value.projectName,
806
+ description: value.description,
807
+ publicServerUrl: url,
808
+ configRevision: 1,
809
+ scopeProfile: "workspace-and-sessions-v1",
810
+ sync: this.site.config().sync
811
+ });
812
+ this.hub.db.prepare("INSERT INTO projects VALUES (?,?,?,?)").run(config.projectId, JSON.stringify(config), value.operationId, fingerprint);
813
+ this.hub.db.prepare("INSERT INTO members(workspace_id,member_id) VALUES (?,?)").run(config.projectId, creator);
814
+ return config;
815
+ }
816
+ setup(input) {
817
+ const value = SetupSchema.parse(input);
818
+ const fingerprint = digest2(JSON.stringify(value));
819
+ return transaction(this.hub.db, () => {
820
+ const prior = this.hub.db.prepare("SELECT setup_operation,setup_fingerprint FROM site_settings").get();
821
+ if (prior) {
822
+ if (prior.setup_operation === value.operationId && prior.setup_fingerprint === fingerprint) {
823
+ const row = this.hub.db.prepare("SELECT id FROM projects WHERE operation_id=?").get(value.operationId);
824
+ return this.config({ workspaceId: String(row.id), memberId: value.adminId });
825
+ }
826
+ throw new HubError(
827
+ 409,
828
+ "ALREADY_INITIALIZED",
829
+ "Server initialization has already completed."
830
+ );
831
+ }
832
+ this.hub.db.prepare("INSERT INTO site_settings VALUES (1,?,?,?)").run(
833
+ JSON.stringify({ publicServerUrl: value.publicServerUrl, revision: 1, sync: value.sync }),
834
+ value.operationId,
835
+ fingerprint
836
+ );
837
+ for (const id of /* @__PURE__ */ new Set([value.adminId, ...value.members]))
838
+ this.hub.db.prepare("INSERT OR IGNORE INTO users(id) VALUES (?)").run(id);
839
+ this.hub.db.prepare("UPDATE users SET site_role='admin',enabled=1 WHERE id=?").run(value.adminId);
840
+ const config = this.insertProject(
841
+ ProjectCreateSchema.parse({
842
+ operationId: value.operationId,
843
+ projectName: value.projectName,
844
+ description: value.description
845
+ }),
846
+ value.adminId,
847
+ value.publicServerUrl
848
+ );
849
+ for (const id of new Set(value.members))
850
+ this.hub.db.prepare("INSERT OR IGNORE INTO members(workspace_id,member_id) VALUES (?,?)").run(config.projectId, id);
851
+ return config;
852
+ });
853
+ }
854
+ createProject(identity, input) {
855
+ requireSiteAdmin(identity);
856
+ const value = ProjectCreateSchema.parse(input);
857
+ return transaction(
858
+ this.hub.db,
859
+ () => this.insertProject(value, identity.memberId, this.site.config().publicServerUrl)
860
+ );
861
+ }
862
+ projects(identity) {
863
+ return this.hub.db.prepare(
864
+ `SELECT p.config_json FROM projects p JOIN members m ON m.workspace_id=p.id AND m.member_id=? WHERE m.enabled=1 ORDER BY p.rowid`
865
+ ).all(identity.memberId).map((row) => {
866
+ const config = ProjectConfigSchema.parse(JSON.parse(String(row.config_json)));
867
+ return {
868
+ projectId: config.projectId,
869
+ projectName: config.projectName,
870
+ description: config.description
871
+ };
872
+ });
873
+ }
874
+ session(token, workspaceId) {
875
+ const identity = this.site.identity(token);
876
+ NameSchema.parse(workspaceId);
877
+ const row = this.hub.db.prepare(
878
+ "SELECT m.enabled FROM projects p LEFT JOIN members m ON m.workspace_id=p.id AND m.member_id=? WHERE p.id=?"
879
+ ).get(identity.memberId, workspaceId);
880
+ if (!row?.enabled)
881
+ throw new HubError(404, "PROJECT_NOT_FOUND", "Project not found or access was removed.");
882
+ return {
883
+ ...identity,
884
+ workspaceId
885
+ };
886
+ }
887
+ requireMember(identity) {
888
+ if (!this.hub.db.prepare(
889
+ "SELECT 1 FROM members m JOIN users u ON u.id=m.member_id WHERE workspace_id=? AND member_id=? AND m.enabled=1 AND u.enabled=1"
890
+ ).get(identity.workspaceId, identity.memberId))
891
+ throw new HubError(404, "PROJECT_NOT_FOUND", "Project not found or access was removed.");
892
+ }
893
+ updateConfig(identity, input) {
894
+ this.requireMember(identity);
895
+ const value = ConfigUpdateSchema.parse(input);
896
+ return transaction(this.hub.db, () => {
897
+ const current = this.config(identity);
898
+ if (value.expectedRevision !== void 0 && current.configRevision !== value.expectedRevision)
899
+ throw new HubError(409, "STALE_CONFIG", "Project settings changed; reload before saving.");
900
+ const { expectedRevision: _, ...fields } = value;
901
+ const next = { ...current, ...fields };
902
+ if (JSON.stringify(next) !== JSON.stringify(current)) next.configRevision++;
903
+ this.hub.db.prepare("UPDATE projects SET config_json=? WHERE id=?").run(JSON.stringify(next), identity.workspaceId);
904
+ return next;
905
+ });
906
+ }
907
+ members(identity) {
908
+ return this.hub.db.prepare(
909
+ `SELECT m.*,COALESCE(NULLIF(m.display_name,''),NULLIF(u.display_name,''),m.member_id) member_name,u.enabled user_enabled,(SELECT MAX(created_at) FROM file_revisions f WHERE f.workspace_id=m.workspace_id AND f.owner=m.member_id) last_published FROM members m JOIN users u ON u.id=m.member_id WHERE m.workspace_id=? ORDER BY m.member_id`
910
+ ).all(identity.workspaceId).map(
911
+ (row) => MemberSchema.parse({
912
+ id: row.member_id,
913
+ displayName: row.member_name,
914
+ enabled: Boolean(row.enabled),
915
+ accountEnabled: Boolean(row.user_enabled),
916
+ revision: row.member_revision,
917
+ bound: Boolean(row.device_id),
918
+ fileRevision: row.file_revision,
919
+ lastPublishedAt: row.last_published ?? null
920
+ })
921
+ );
922
+ }
923
+ createMember(identity, input) {
924
+ this.requireMember(identity);
925
+ const value = MemberCreateSchema.parse(input);
926
+ return transaction(this.hub.db, () => {
927
+ if (!this.hub.db.prepare("SELECT 1 FROM users WHERE id=? AND enabled=1").get(value.id))
928
+ throw new HubError(
929
+ 404,
930
+ "USER_NOT_FOUND",
931
+ "Ask a site administrator to create or enable this user first."
932
+ );
933
+ const result = this.hub.db.prepare(
934
+ "INSERT INTO members(workspace_id,member_id) VALUES (?,?) ON CONFLICT(workspace_id,member_id) DO UPDATE SET enabled=1,member_revision=member_revision+1 WHERE enabled=0"
935
+ ).run(identity.workspaceId, value.id);
936
+ if (!result.changes)
937
+ throw new HubError(409, "MEMBER_EXISTS", "Member already belongs to this project.");
938
+ return this.members(identity).find((m) => m.id === value.id);
939
+ });
940
+ }
941
+ updateMember(identity, id, input) {
942
+ const value = MemberUpdateSchema.parse(input);
943
+ this.requireMember(identity);
944
+ if (value.enabled !== void 0) requireSiteAdmin(identity);
945
+ return transaction(this.hub.db, () => {
946
+ const row = this.hub.db.prepare("SELECT * FROM members WHERE workspace_id=? AND member_id=?").get(identity.workspaceId, id);
947
+ if (!row) throw new HubError(404, "MEMBER_NOT_FOUND", "Member not found.");
948
+ if (row.member_revision !== value.expectedRevision)
949
+ throw new HubError(409, "STALE_MEMBER", "Member changed; reload before saving.");
950
+ this.hub.db.prepare(
951
+ "UPDATE members SET enabled=?,display_name=?,member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
952
+ ).run(
953
+ value.enabled === void 0 ? row.enabled : Number(value.enabled),
954
+ value.displayName ?? row.display_name,
955
+ identity.workspaceId,
956
+ id
957
+ );
958
+ if (value.enabled === false) this.clearAccess(identity.workspaceId, id);
959
+ return this.members(identity).find((m) => m.id === id);
960
+ });
961
+ }
962
+ leave(identity, workspaceId) {
963
+ return transaction(this.hub.db, () => {
964
+ const row = this.hub.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(workspaceId, identity.memberId);
965
+ if (!row) throw new HubError(404, "PROJECT_NOT_FOUND", "Project not found.");
966
+ this.hub.db.prepare(
967
+ "UPDATE members SET enabled=0,member_revision=member_revision+1 WHERE workspace_id=? AND member_id=? AND enabled=1"
968
+ ).run(workspaceId, identity.memberId);
969
+ this.clearAccess(workspaceId, identity.memberId);
970
+ return { ok: true };
971
+ });
972
+ }
973
+ clearAccess(workspace, id) {
974
+ this.hub.db.prepare(
975
+ "UPDATE members SET token_hash=NULL,device_id=NULL WHERE workspace_id=? AND member_id=?"
976
+ ).run(workspace, id);
977
+ this.hub.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(workspace, id);
978
+ }
979
+ resetDevice(identity, id) {
980
+ this.requireMember(identity);
981
+ requireSiteAdmin(identity);
982
+ if (!this.hub.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(identity.workspaceId, id))
983
+ throw new HubError(404, "MEMBER_NOT_FOUND", "Member not found.");
984
+ transaction(this.hub.db, () => {
985
+ this.clearAccess(identity.workspaceId, id);
986
+ this.hub.db.prepare(
987
+ "UPDATE members SET member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
988
+ ).run(identity.workspaceId, id);
989
+ });
990
+ return { ok: true };
991
+ }
992
+ };
993
+
524
994
  // apps/server/ai/config.ts
525
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "node:crypto";
995
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes4 } from "node:crypto";
526
996
  import { readFileSync, writeFileSync } from "node:fs";
527
997
  import { join as join2 } from "node:path";
528
- function requireAiAdmin(session) {
529
- if (session.role !== "admin")
530
- throw new HubError(403, "ADMIN_REQUIRED", "Administrator access required.");
531
- }
532
998
  var AiConfig = class {
533
999
  constructor(hub) {
534
1000
  this.hub = hub;
@@ -538,7 +1004,7 @@ var AiConfig = class {
538
1004
  const path = join2(this.hub.directory, "ai-secret.key");
539
1005
  if (create) {
540
1006
  try {
541
- writeFileSync(path, randomBytes2(32), { flag: "wx", mode: 384 });
1007
+ writeFileSync(path, randomBytes4(32), { flag: "wx", mode: 384 });
542
1008
  } catch (e) {
543
1009
  if (e.code !== "EEXIST") throw e;
544
1010
  }
@@ -547,75 +1013,136 @@ var AiConfig = class {
547
1013
  if (key.length !== 32) throw new Error("Invalid AI secret encryption key.");
548
1014
  return key;
549
1015
  }
550
- encrypt(value, workspace, id) {
551
- const iv = randomBytes2(12), cipher = createCipheriv("aes-256-gcm", this.key(true), iv);
552
- cipher.setAAD(Buffer.from(JSON.stringify([workspace, id])));
1016
+ encrypt(value, id) {
1017
+ const iv = randomBytes4(12), cipher = createCipheriv("aes-256-gcm", this.key(true), iv);
1018
+ cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
553
1019
  const body = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
554
1020
  return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
555
1021
  }
556
- list(workspace) {
557
- return this.hub.db.prepare("SELECT * FROM ai_profiles WHERE workspace_id=? ORDER BY id").all(workspace).map((row) => ({
1022
+ list() {
1023
+ return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => ({
558
1024
  ...AiProfileSchema.parse(JSON.parse(String(row.config_json))),
559
1025
  revision: Number(row.revision),
560
1026
  keyConfigured: !!row.secret
561
1027
  }));
562
1028
  }
563
- get(workspace, id) {
564
- const profile = this.list(workspace).find((p) => p.id === id);
1029
+ get(id) {
1030
+ const profile = this.list().find((p) => p.id === id);
565
1031
  if (!profile) throw new HubError(404, "AI_PROFILE_NOT_FOUND", "Model profile not found.");
566
1032
  const { keyConfigured: _, ...result } = profile;
567
1033
  return result;
568
1034
  }
569
- secret(workspace, id) {
570
- const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE workspace_id=? AND id=?").get(workspace, id);
1035
+ secret(id) {
1036
+ const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE id=?").get(id);
571
1037
  if (!row?.secret) return "";
572
1038
  const [iv, tag, body] = String(row.secret).split(".").map((s) => Buffer.from(s, "base64"));
573
1039
  const cipher = createDecipheriv("aes-256-gcm", this.key(), iv);
574
- cipher.setAAD(Buffer.from(JSON.stringify([workspace, id])));
1040
+ cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
575
1041
  cipher.setAuthTag(tag);
576
1042
  return Buffer.concat([cipher.update(body), cipher.final()]).toString("utf8");
577
1043
  }
578
1044
  save(session, value) {
579
- requireAiAdmin(session);
1045
+ requireSiteAdmin(session);
580
1046
  const input = AiProfileUpdateSchema.parse(value), { profile } = input;
581
1047
  return transaction(this.hub.db, () => {
582
- const old = this.hub.db.prepare("SELECT * FROM ai_profiles WHERE workspace_id=? AND id=?").get(session.workspaceId, profile.id);
1048
+ const old = this.hub.db.prepare("SELECT * FROM ai_profiles WHERE id=?").get(profile.id);
583
1049
  if ((old ? Number(old.revision) : 0) !== input.expectedRevision)
584
1050
  throw new HubError(
585
1051
  409,
586
1052
  "AI_CONFIG_CONFLICT",
587
1053
  "Model configuration changed; reload before saving."
588
1054
  );
589
- if (!old && this.list(session.workspaceId).length >= 20)
1055
+ if (!old && this.list().length >= 20)
590
1056
  throw new HubError(400, "AI_PROFILE_LIMIT", "At most 20 model profiles are supported.");
591
- const secret = input.apiKey === void 0 ? old?.secret ?? null : input.apiKey === null ? null : this.encrypt(input.apiKey, session.workspaceId, profile.id);
1057
+ const secret = input.apiKey === void 0 ? old?.secret ?? null : input.apiKey === null ? null : this.encrypt(input.apiKey, profile.id);
592
1058
  this.hub.db.prepare(
593
- "INSERT INTO ai_profiles VALUES(?,?,?,?,?) ON CONFLICT(workspace_id,id) DO UPDATE SET revision=excluded.revision,config_json=excluded.config_json,secret=excluded.secret"
594
- ).run(
595
- session.workspaceId,
596
- profile.id,
597
- input.expectedRevision + 1,
598
- JSON.stringify(profile),
599
- secret
600
- );
601
- return this.list(session.workspaceId).find((p) => p.id === profile.id);
1059
+ "INSERT INTO ai_profiles VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision,config_json=excluded.config_json,secret=excluded.secret"
1060
+ ).run(profile.id, input.expectedRevision + 1, JSON.stringify(profile), secret);
1061
+ return this.list().find((p) => p.id === profile.id);
1062
+ });
1063
+ }
1064
+ };
1065
+
1066
+ // apps/server/ai/defaults.ts
1067
+ var AiDefaults = class {
1068
+ constructor(hub) {
1069
+ this.hub = hub;
1070
+ }
1071
+ hub;
1072
+ read(scope) {
1073
+ const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope);
1074
+ return {
1075
+ profileId: row?.profile_id ? String(row.profile_id) : null,
1076
+ revision: Number(row?.revision ?? 0)
1077
+ };
1078
+ }
1079
+ view(projectId2) {
1080
+ const site = this.read("site");
1081
+ const own = projectId2 ? this.read(`project:${projectId2}`) : site;
1082
+ const effectiveProfileId = own.profileId ?? site.profileId;
1083
+ return {
1084
+ ...own,
1085
+ effectiveProfileId,
1086
+ source: projectId2 && own.profileId ? "project" : site.profileId ? "site" : "none",
1087
+ available: !!effectiveProfileId && new AiConfig(this.hub).get(effectiveProfileId).enabled
1088
+ };
1089
+ }
1090
+ site() {
1091
+ return this.view();
1092
+ }
1093
+ project(projectId2) {
1094
+ return this.view(projectId2);
1095
+ }
1096
+ save(scope, value) {
1097
+ const input = AiDefaultUpdateSchema.parse(value);
1098
+ transaction(this.hub.db, () => {
1099
+ if (this.read(scope).revision !== input.expectedRevision)
1100
+ throw new HubError(
1101
+ 409,
1102
+ "AI_CONFIG_CONFLICT",
1103
+ "Model default changed; reload before saving."
1104
+ );
1105
+ if (input.profileId && !new AiConfig(this.hub).get(input.profileId).enabled)
1106
+ throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1107
+ this.hub.db.prepare(
1108
+ `INSERT INTO ai_defaults VALUES(?,?,?) ON CONFLICT(scope) DO UPDATE SET profile_id=excluded.profile_id,revision=excluded.revision`
1109
+ ).run(scope, input.profileId, input.expectedRevision + 1);
602
1110
  });
603
1111
  }
1112
+ saveSite(identity, value) {
1113
+ requireSiteAdmin(identity);
1114
+ this.save("site", value);
1115
+ return this.site();
1116
+ }
1117
+ saveProject(identity, value) {
1118
+ new ProjectRepository(this.hub).requireMember(identity);
1119
+ this.save(`project:${identity.workspaceId}`, value);
1120
+ return this.project(identity.workspaceId);
1121
+ }
1122
+ resolve(projectId2, explicit) {
1123
+ const id = explicit ?? this.project(projectId2).effectiveProfileId;
1124
+ if (!id)
1125
+ throw new HubError(400, "AI_DEFAULT_REQUIRED", "Select a model or configure a default.");
1126
+ const profile = new AiConfig(this.hub).get(id);
1127
+ if (!profile.enabled)
1128
+ throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1129
+ return profile;
1130
+ }
604
1131
  };
605
1132
 
606
1133
  // apps/server/ai/snapshots.ts
607
- import { createHash as createHash4 } from "node:crypto";
1134
+ import { createHash as createHash5 } from "node:crypto";
608
1135
 
609
1136
  // apps/server/files.ts
610
- import { createHash as createHash3 } from "node:crypto";
1137
+ import { createHash as createHash4 } from "node:crypto";
611
1138
  import { join as join4 } from "node:path";
612
1139
 
613
1140
  // packages/storage/local-blob-store.ts
614
- import { createHash as createHash2, randomUUID } from "node:crypto";
1141
+ import { createHash as createHash3, randomUUID } from "node:crypto";
615
1142
  import { link, mkdir, open, readFile, unlink } from "node:fs/promises";
616
1143
  import { dirname as dirname2, join as join3, resolve } from "node:path";
617
1144
  function hash(content) {
618
- return createHash2("sha256").update(content).digest("hex");
1145
+ return createHash3("sha256").update(content).digest("hex");
619
1146
  }
620
1147
  function hasCode(error, code) {
621
1148
  return error instanceof Error && "code" in error && error.code === code;
@@ -705,7 +1232,7 @@ var FileRepository = class {
705
1232
  }
706
1233
  async upload(authorization, device, hash2, bytes) {
707
1234
  this.hub.authorize(authorization, device);
708
- if (createHash3("sha256").update(bytes).digest("hex") !== HashSchema.parse(hash2))
1235
+ if (createHash4("sha256").update(bytes).digest("hex") !== HashSchema.parse(hash2))
709
1236
  throw new HubError(400, "HASH_MISMATCH", "Content does not match its SHA-256.");
710
1237
  const ref = await this.blobs.put(bytes);
711
1238
  transaction(this.hub.db, () => {
@@ -731,7 +1258,7 @@ var FileRepository = class {
731
1258
  const changes = [...batch.changes].sort(
732
1259
  (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
733
1260
  );
734
- const fingerprint = createHash3("sha256").update(JSON.stringify({ parentRevision: batch.parentRevision, changes })).digest("hex");
1261
+ const fingerprint = createHash4("sha256").update(JSON.stringify({ parentRevision: batch.parentRevision, changes })).digest("hex");
735
1262
  return transaction(this.hub.db, () => {
736
1263
  const identity = this.hub.authorize(authorization, device);
737
1264
  const { workspaceId: workspace, memberId: owner } = identity;
@@ -948,7 +1475,7 @@ var FileRepository = class {
948
1475
 
949
1476
  // apps/server/ai/snapshots.ts
950
1477
  function snapshotDigest(files) {
951
- return createHash4("sha256").update(JSON.stringify(files.map((f) => [f.path, f.sha256]))).digest("hex");
1478
+ return createHash5("sha256").update(JSON.stringify(files.map((f) => [f.path, f.sha256]))).digest("hex");
952
1479
  }
953
1480
  function captureSnapshot(hub, identity, selection) {
954
1481
  const repository = new FileRepository(hub);
@@ -1026,7 +1553,7 @@ var SnapshotReader = class {
1026
1553
  if (entry.size > 256 * 1024)
1027
1554
  throw new HubError(413, "AI_FILE_LIMIT", "File exceeds the 256 KiB analysis limit.");
1028
1555
  const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
1029
- if (bytes.byteLength !== entry.size || createHash4("sha256").update(bytes).digest("hex") !== entry.sha256)
1556
+ if (bytes.byteLength !== entry.size || createHash5("sha256").update(bytes).digest("hex") !== entry.sha256)
1030
1557
  throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
1031
1558
  let text;
1032
1559
  try {
@@ -1212,7 +1739,7 @@ var AiJobs = class {
1212
1739
  this.hub.db.prepare("INSERT INTO ai_events(job_id,attempt,kind,message,created_at) VALUES(?,?,?,?,?)").run(id, attempt, kind, message.slice(0, 2e3), now());
1213
1740
  }
1214
1741
  operation(session, operationId, input, action) {
1215
- const fingerprint = createHash5("sha256").update(JSON.stringify(input)).digest("hex");
1742
+ const fingerprint = createHash6("sha256").update(JSON.stringify(input)).digest("hex");
1216
1743
  return transaction(this.hub.db, () => {
1217
1744
  const old = this.hub.db.prepare(
1218
1745
  "SELECT * FROM ai_operations WHERE workspace_id=? AND requester=? AND operation_id=?"
@@ -1242,7 +1769,7 @@ var AiJobs = class {
1242
1769
  ).get(session.workspaceId).n
1243
1770
  );
1244
1771
  if (pending >= 50) throw new HubError(429, "AI_QUEUE_FULL", "Too many pending AI tasks.");
1245
- const profile = new AiConfig(this.hub).get(session.workspaceId, input.profileId);
1772
+ const profile = new AiDefaults(this.hub).resolve(session.workspaceId, input.profileId);
1246
1773
  if (!profile.enabled)
1247
1774
  throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1248
1775
  const snapshot = captureSnapshot(this.hub, session, input.selection), id2 = randomUUID2(), date = now();
@@ -1266,7 +1793,7 @@ var AiJobs = class {
1266
1793
  return this.get(session.workspaceId, id);
1267
1794
  }
1268
1795
  own(session, row) {
1269
- if (session.role !== "admin" && session.memberId !== row.requester)
1796
+ if (session.siteRole !== "admin" && session.memberId !== row.requester)
1270
1797
  throw new HubError(
1271
1798
  403,
1272
1799
  "AI_TASK_FORBIDDEN",
@@ -1822,18 +2349,22 @@ async function runAiCycle(hub, stop) {
1822
2349
  const jobs = new AiJobs(hub), job = jobs.claim();
1823
2350
  if (!job) return false;
1824
2351
  const lost = new AbortController(), signal = AbortSignal.any([stop, lost.signal, AbortSignal.timeout(12e4)]);
2352
+ const requesterActive = () => Boolean(
2353
+ hub.db.prepare(
2354
+ "SELECT (u.enabled AND m.enabled=1) enabled FROM users u LEFT JOIN members m ON m.member_id=u.id AND m.workspace_id=? WHERE u.id=?"
2355
+ ).get(job.workspace, job.requester)?.enabled
2356
+ );
1825
2357
  const timer = setInterval(() => {
1826
2358
  try {
1827
- if (!jobs.heartbeat(job)) lost.abort();
2359
+ if (!requesterActive() || !jobs.heartbeat(job)) lost.abort();
1828
2360
  } catch {
1829
2361
  lost.abort();
1830
2362
  }
1831
2363
  }, 1e3);
1832
2364
  try {
1833
- const active = hub.db.prepare("SELECT enabled FROM members WHERE workspace_id=? AND member_id=?").get(job.workspace, job.requester);
1834
- if (!active?.enabled)
2365
+ if (!requesterActive())
1835
2366
  throw new HubError(403, "AI_REQUESTER_DISABLED", "Requester is disabled.");
1836
- const config = new AiConfig(hub), profile = config.get(job.workspace, job.profile.id);
2367
+ const config = new AiConfig(hub), profile = config.get(job.profile.id);
1837
2368
  if (!profile.enabled)
1838
2369
  throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1839
2370
  if (profile.baseUrl !== job.profile.baseUrl || profile.protocol !== job.profile.protocol)
@@ -1846,7 +2377,7 @@ async function runAiCycle(hub, stop) {
1846
2377
  job.snapshot,
1847
2378
  Math.max(2048, job.profile.contextWindow - job.profile.maxOutputTokens - 4e3)
1848
2379
  );
1849
- const key = config.secret(job.workspace, profile.id);
2380
+ const key = config.secret(profile.id);
1850
2381
  const output = job.request.workflow === "file-summary" ? await summarizeFile(job, reader, key, signal) : await analyzeDirectory(
1851
2382
  job,
1852
2383
  reader,
@@ -1854,6 +2385,8 @@ async function runAiCycle(hub, stop) {
1854
2385
  signal,
1855
2386
  (kind, message) => jobs.progress(job, kind, message)
1856
2387
  );
2388
+ if (!requesterActive())
2389
+ throw new HubError(403, "AI_REQUESTER_DISABLED", "Requester is disabled.");
1857
2390
  signal.throwIfAborted();
1858
2391
  jobs.finish(job, output.result, null, output.usage);
1859
2392
  } catch (error) {
@@ -1890,190 +2423,6 @@ import { parseArgs } from "node:util";
1890
2423
  // apps/server/ai/job-routes.ts
1891
2424
  import { z as z7 } from "zod";
1892
2425
 
1893
- // apps/server/project.ts
1894
- import { createHash as createHash6, randomBytes as randomBytes3 } from "node:crypto";
1895
- var digest2 = (value) => createHash6("sha256").update(value).digest("hex");
1896
- var ProjectRepository = class {
1897
- constructor(hub) {
1898
- this.hub = hub;
1899
- }
1900
- hub;
1901
- status() {
1902
- return {
1903
- initialized: Boolean(this.hub.db.prepare("SELECT 1 FROM project_settings").get()),
1904
- existingWorkspaces: this.hub.db.prepare("SELECT DISTINCT workspace_id FROM members ORDER BY workspace_id").all().map((r) => String(r.workspace_id))
1905
- };
1906
- }
1907
- config(identity) {
1908
- const row = this.hub.db.prepare("SELECT config_json FROM project_settings WHERE id=1").get();
1909
- if (!row)
1910
- throw new HubError(409, "SETUP_REQUIRED", "Initialize the project in the browser first.");
1911
- const config = ProjectConfigSchema.parse(JSON.parse(String(row.config_json)));
1912
- if (identity && identity.workspaceId !== config.projectId)
1913
- throw new HubError(
1914
- 404,
1915
- "PROJECT_NOT_FOUND",
1916
- "This workspace has no Web project configuration."
1917
- );
1918
- return config;
1919
- }
1920
- setup(input) {
1921
- const value = SetupSchema.parse(input);
1922
- const fingerprint = digest2(JSON.stringify(value));
1923
- return transaction(this.hub.db, () => {
1924
- const prior = this.hub.db.prepare("SELECT setup_operation,setup_fingerprint FROM project_settings").get();
1925
- if (prior) {
1926
- if (prior.setup_operation === value.operationId && prior.setup_fingerprint === fingerprint)
1927
- return this.config();
1928
- throw new HubError(
1929
- 409,
1930
- "ALREADY_INITIALIZED",
1931
- "Project initialization has already completed."
1932
- );
1933
- }
1934
- const existing = this.status().existingWorkspaces;
1935
- if (existing.length && !value.existingWorkspaceId)
1936
- throw new HubError(
1937
- 400,
1938
- "WORKSPACE_REQUIRED",
1939
- "Select an existing workspace to preserve its data."
1940
- );
1941
- if (value.existingWorkspaceId && !existing.includes(value.existingWorkspaceId))
1942
- throw new HubError(400, "WORKSPACE_NOT_FOUND", "Existing workspace was not found.");
1943
- const projectId = value.existingWorkspaceId ?? `proj-${randomBytes3(8).toString("hex")}`;
1944
- for (const id of /* @__PURE__ */ new Set([value.adminId, ...value.members])) {
1945
- this.hub.db.prepare("INSERT OR IGNORE INTO members(workspace_id,member_id) VALUES (?,?)").run(projectId, id);
1946
- }
1947
- this.hub.db.prepare(
1948
- "UPDATE members SET role='admin',enabled=1,member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
1949
- ).run(projectId, value.adminId);
1950
- const config = ProjectConfigSchema.parse({
1951
- schemaVersion: 1,
1952
- projectId,
1953
- projectName: value.projectName,
1954
- description: value.description,
1955
- publicServerUrl: value.publicServerUrl,
1956
- configRevision: 1,
1957
- scopeProfile: "workspace-and-sessions-v1",
1958
- sync: value.sync
1959
- });
1960
- this.hub.db.prepare("INSERT INTO project_settings VALUES (1,?,?,?)").run(JSON.stringify(config), value.operationId, fingerprint);
1961
- return config;
1962
- });
1963
- }
1964
- login(id) {
1965
- const { projectId } = this.config();
1966
- const row = this.hub.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=? AND enabled=1").get(projectId, id);
1967
- if (!row)
1968
- throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
1969
- const token = randomBytes3(32).toString("hex");
1970
- this.hub.db.prepare("DELETE FROM web_sessions WHERE expires_at<=?").run(Date.now());
1971
- this.hub.db.prepare("INSERT INTO web_sessions VALUES (?,?,?,?)").run(digest2(token), projectId, id, Date.now() + 7 * 864e5);
1972
- return { token, session: this.session(token) };
1973
- }
1974
- session(token) {
1975
- const row = token && /^[a-f0-9]{64}$/.test(token) ? this.hub.db.prepare(
1976
- `SELECT s.workspace_id,s.member_id,m.display_name,m.role FROM web_sessions s JOIN members m USING(workspace_id,member_id) WHERE s.token_hash=? AND s.expires_at>? AND m.enabled=1`
1977
- ).get(digest2(token), Date.now()) : void 0;
1978
- if (!row)
1979
- throw new HubError(401, "LOGIN_REQUIRED", "Please sign in with an existing member ID.");
1980
- return {
1981
- workspaceId: String(row.workspace_id),
1982
- memberId: String(row.member_id),
1983
- displayName: String(row.display_name || row.member_id),
1984
- role: row.role
1985
- };
1986
- }
1987
- logout(token) {
1988
- if (token)
1989
- this.hub.db.prepare("DELETE FROM web_sessions WHERE token_hash=?").run(digest2(token));
1990
- }
1991
- admin(identity) {
1992
- if (identity.role !== "admin")
1993
- throw new HubError(403, "ADMIN_REQUIRED", "This action requires an administrator.");
1994
- }
1995
- updateConfig(identity, input) {
1996
- this.admin(identity);
1997
- const value = ConfigUpdateSchema.parse(input);
1998
- return transaction(this.hub.db, () => {
1999
- const current = this.config(identity);
2000
- if (current.configRevision !== value.expectedRevision)
2001
- throw new HubError(409, "STALE_CONFIG", "Project settings changed; reload before saving.");
2002
- const { expectedRevision: _, ...fields } = value;
2003
- const next = { ...current, ...fields };
2004
- if (JSON.stringify(next) !== JSON.stringify(current)) next.configRevision++;
2005
- this.hub.db.prepare("UPDATE project_settings SET config_json=? WHERE id=1").run(JSON.stringify(next));
2006
- return next;
2007
- });
2008
- }
2009
- members(identity) {
2010
- return this.hub.db.prepare(
2011
- `SELECT m.*, (SELECT MAX(created_at) FROM file_revisions f WHERE f.workspace_id=m.workspace_id AND f.owner=m.member_id) last_published FROM members m WHERE workspace_id=? ORDER BY member_id`
2012
- ).all(identity.workspaceId).map(
2013
- (row) => MemberSchema.parse({
2014
- id: row.member_id,
2015
- displayName: row.display_name || row.member_id,
2016
- role: row.role,
2017
- enabled: Boolean(row.enabled),
2018
- revision: row.member_revision,
2019
- bound: Boolean(row.device_id),
2020
- fileRevision: row.file_revision,
2021
- lastPublishedAt: row.last_published ?? null
2022
- })
2023
- );
2024
- }
2025
- createMember(identity, input) {
2026
- this.admin(identity);
2027
- const value = MemberCreateSchema.parse(input);
2028
- const result = this.hub.db.prepare("INSERT OR IGNORE INTO members(workspace_id,member_id,display_name) VALUES (?,?,?)").run(identity.workspaceId, value.id, value.displayName);
2029
- if (!result.changes) throw new HubError(409, "MEMBER_EXISTS", "Member ID already exists.");
2030
- return this.members(identity).find((m) => m.id === value.id);
2031
- }
2032
- updateMember(identity, id, input) {
2033
- const value = MemberUpdateSchema.parse(input);
2034
- if (identity.role !== "admin" && (id !== identity.memberId || value.enabled !== void 0))
2035
- throw new HubError(403, "FORBIDDEN", "You can only edit your own display name.");
2036
- return transaction(this.hub.db, () => {
2037
- const row = this.hub.db.prepare("SELECT * FROM members WHERE workspace_id=? AND member_id=?").get(identity.workspaceId, id);
2038
- if (!row) throw new HubError(404, "MEMBER_NOT_FOUND", "Member not found.");
2039
- if (row.member_revision !== value.expectedRevision)
2040
- throw new HubError(409, "STALE_MEMBER", "Member changed; reload before saving.");
2041
- if (value.enabled === false && row.role === "admin" && row.enabled) {
2042
- const count = this.hub.db.prepare(
2043
- "SELECT COUNT(*) n FROM members WHERE workspace_id=? AND role='admin' AND enabled=1"
2044
- ).get(identity.workspaceId);
2045
- if (Number(count.n) <= 1)
2046
- throw new HubError(409, "LAST_ADMIN", "Keep at least one enabled administrator.");
2047
- }
2048
- this.hub.db.prepare(
2049
- "UPDATE members SET display_name=?,enabled=?,member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
2050
- ).run(
2051
- value.displayName ?? row.display_name,
2052
- value.enabled === void 0 ? row.enabled : Number(value.enabled),
2053
- identity.workspaceId,
2054
- id
2055
- );
2056
- if (value.enabled === false) this.clearAccess(identity.workspaceId, id);
2057
- return this.members(identity).find((m) => m.id === id);
2058
- });
2059
- }
2060
- clearAccess(workspace, id) {
2061
- this.hub.db.prepare(
2062
- "UPDATE members SET token_hash=NULL,device_id=NULL WHERE workspace_id=? AND member_id=?"
2063
- ).run(workspace, id);
2064
- this.hub.db.prepare("DELETE FROM web_sessions WHERE workspace_id=? AND member_id=?").run(workspace, id);
2065
- }
2066
- resetDevice(identity, id) {
2067
- this.admin(identity);
2068
- const result = this.hub.db.prepare(
2069
- "UPDATE members SET token_hash=NULL,device_id=NULL,member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
2070
- ).run(identity.workspaceId, id);
2071
- if (!result.changes) throw new HubError(404, "MEMBER_NOT_FOUND", "Member not found.");
2072
- this.hub.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(identity.workspaceId, id);
2073
- return { ok: true };
2074
- }
2075
- };
2076
-
2077
2426
  // apps/server/web-auth.ts
2078
2427
  function cookieToken(request) {
2079
2428
  return request.headers.cookie?.split(";").map((p) => p.trim()).find((p) => p.startsWith("teammy_session="))?.slice("teammy_session=".length);
@@ -2093,51 +2442,64 @@ function webMutation(request) {
2093
2442
  throw new HubError(403, "ORIGIN_MISMATCH", "Request origin does not match this server.");
2094
2443
  }
2095
2444
  }
2445
+ function projectId(request) {
2446
+ const id = request.params.projectId;
2447
+ if (!id) throw new HubError(400, "PROJECT_REQUIRED", "Use an explicit project URL.");
2448
+ return id;
2449
+ }
2450
+ function authorizeWeb(store, request) {
2451
+ return new ProjectRepository(store).session(cookieToken(request), projectId(request));
2452
+ }
2453
+ function authorizeDevice(store, request) {
2454
+ const identity = store.authorize(
2455
+ request.headers.authorization,
2456
+ typeof request.headers["x-teammy-device"] === "string" ? request.headers["x-teammy-device"] : void 0
2457
+ );
2458
+ const target = request.params.projectId;
2459
+ if (target && target !== identity.workspaceId)
2460
+ throw new HubError(403, "PROJECT_MISMATCH", "Device credential belongs to another project.");
2461
+ return identity;
2462
+ }
2096
2463
  function authorizeRead(store, request) {
2097
- if (request.headers.authorization)
2098
- return store.authorize(
2099
- request.headers.authorization,
2100
- typeof request.headers["x-teammy-device"] === "string" ? request.headers["x-teammy-device"] : void 0
2101
- );
2102
- return new ProjectRepository(store).session(cookieToken(request));
2464
+ return request.headers.authorization ? authorizeDevice(store, request) : authorizeWeb(store, request);
2103
2465
  }
2104
2466
 
2105
2467
  // apps/server/ai/job-routes.ts
2106
2468
  function registerAiJobRoutes(server, hub) {
2107
- const project = new ProjectRepository(hub), jobs = new AiJobs(hub);
2469
+ const jobs = new AiJobs(hub);
2108
2470
  const id = (params) => z7.object({ id: z7.uuid() }).parse(params).id;
2109
- server.get("/api/v1/ai/jobs", async (req) => {
2110
- const session = project.session(cookieToken(req)), query = z7.object({
2471
+ server.get("/ai/jobs", async (req) => {
2472
+ const session = authorizeWeb(hub, req), query = z7.object({
2111
2473
  after: z7.string().regex(/^\d{0,15}$/).default("")
2112
2474
  }).parse(req.query);
2113
2475
  return jobs.list(session.workspaceId, query.after);
2114
2476
  });
2115
- server.post("/api/v1/ai/jobs", async (req) => {
2477
+ server.post("/ai/jobs", async (req) => {
2116
2478
  webMutation(req);
2117
- return jobs.create(project.session(cookieToken(req)), AiJobCreateSchema.parse(req.body));
2479
+ return jobs.create(authorizeWeb(hub, req), AiJobCreateSchema.parse(req.body));
2118
2480
  });
2119
2481
  server.get(
2120
- "/api/v1/ai/jobs/:id",
2121
- async (req) => jobs.get(project.session(cookieToken(req)).workspaceId, id(req.params))
2482
+ "/ai/jobs/:id",
2483
+ async (req) => jobs.get(authorizeWeb(hub, req).workspaceId, id(req.params))
2122
2484
  );
2123
- server.get("/api/v1/ai/jobs/:id/events", async (req) => {
2485
+ server.get("/ai/jobs/:id/events", async (req) => {
2124
2486
  const query = z7.object({ after: z7.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
2125
- return jobs.events(project.session(cookieToken(req)).workspaceId, id(req.params), query.after);
2487
+ return jobs.events(authorizeWeb(hub, req).workspaceId, id(req.params), query.after);
2126
2488
  });
2127
- server.post("/api/v1/ai/jobs/:id/cancel", async (req) => {
2489
+ server.post("/ai/jobs/:id/cancel", async (req) => {
2128
2490
  webMutation(req);
2129
- return jobs.cancel(project.session(cookieToken(req)), id(req.params));
2491
+ return jobs.cancel(authorizeWeb(hub, req), id(req.params));
2130
2492
  });
2131
- server.post("/api/v1/ai/jobs/:id/retry", async (req) => {
2493
+ server.post("/ai/jobs/:id/retry", async (req) => {
2132
2494
  webMutation(req);
2133
2495
  return jobs.retry(
2134
- project.session(cookieToken(req)),
2496
+ authorizeWeb(hub, req),
2135
2497
  id(req.params),
2136
2498
  z7.object({ operationId: z7.uuid() }).strict().parse(req.body).operationId
2137
2499
  );
2138
2500
  });
2139
- server.get("/api/v1/ai/jobs/:id/source", async (req) => {
2140
- const session = project.session(cookieToken(req)), jobId = id(req.params);
2501
+ server.get("/ai/jobs/:id/source", async (req) => {
2502
+ const session = authorizeWeb(hub, req), jobId = id(req.params);
2141
2503
  jobs.get(session.workspaceId, jobId);
2142
2504
  const query = z7.object({ path: z7.string().max(1024), startLine: z7.coerce.number().int().min(1).default(1) }).parse(req.query);
2143
2505
  const row = hub.db.prepare("SELECT snapshot_json FROM ai_jobs WHERE id=? AND workspace_id=?").get(jobId, session.workspaceId);
@@ -2152,15 +2514,35 @@ function registerAiJobRoutes(server, hub) {
2152
2514
 
2153
2515
  // apps/server/ai/routes.ts
2154
2516
  function registerAiRoutes(server, store) {
2155
- const project = new ProjectRepository(store), config = new AiConfig(store);
2517
+ const project = new SiteRepository(store), config = new AiConfig(store);
2518
+ const defaults = new AiDefaults(store);
2519
+ server.get("/api/v1/ai/default", async (request) => {
2520
+ project.identity(cookieToken(request));
2521
+ return defaults.site();
2522
+ });
2523
+ server.put("/api/v1/ai/default", async (request) => {
2524
+ webMutation(request);
2525
+ return defaults.saveSite(
2526
+ project.identity(cookieToken(request)),
2527
+ AiDefaultUpdateSchema.parse(request.body)
2528
+ );
2529
+ });
2530
+ server.get(
2531
+ "/api/v1/projects/:projectId/ai/default",
2532
+ async (req) => defaults.project(authorizeWeb(store, req).workspaceId)
2533
+ );
2534
+ server.put("/api/v1/projects/:projectId/ai/default", async (req) => {
2535
+ webMutation(req);
2536
+ return defaults.saveProject(authorizeWeb(store, req), AiDefaultUpdateSchema.parse(req.body));
2537
+ });
2156
2538
  let probing = false;
2157
2539
  server.get("/api/v1/ai/profiles", async (request) => {
2158
- const session = project.session(cookieToken(request));
2159
- return { profiles: config.list(session.workspaceId) };
2540
+ project.identity(cookieToken(request));
2541
+ return { profiles: config.list() };
2160
2542
  });
2161
2543
  server.put("/api/v1/ai/profiles/:id", async (request) => {
2162
2544
  webMutation(request);
2163
- const session = project.session(cookieToken(request));
2545
+ const session = project.identity(cookieToken(request));
2164
2546
  const id = NameSchema.parse(request.params.id);
2165
2547
  const input = AiProfileUpdateSchema.parse(request.body);
2166
2548
  if (id !== input.profile.id) throw new HubError(400, "INVALID_REQUEST", "Profile ID mismatch.");
@@ -2168,12 +2550,9 @@ function registerAiRoutes(server, store) {
2168
2550
  });
2169
2551
  server.post("/api/v1/ai/profiles/:id/test", async (request, reply) => {
2170
2552
  webMutation(request);
2171
- const session = project.session(cookieToken(request));
2172
- requireAiAdmin(session);
2173
- const profile = config.get(
2174
- session.workspaceId,
2175
- NameSchema.parse(request.params.id)
2176
- );
2553
+ const session = project.identity(cookieToken(request));
2554
+ requireSiteAdmin(session);
2555
+ const profile = config.get(NameSchema.parse(request.params.id));
2177
2556
  const { mode } = AiProbeSchema.parse(request.body);
2178
2557
  if (probing) throw new HubError(429, "AI_TEST_BUSY", "Another connection test is running.");
2179
2558
  probing = true;
@@ -2185,7 +2564,7 @@ function registerAiRoutes(server, store) {
2185
2564
  try {
2186
2565
  return await probeModel(
2187
2566
  profile,
2188
- config.secret(session.workspaceId, profile.id),
2567
+ config.secret(profile.id),
2189
2568
  mode,
2190
2569
  AbortSignal.any([controller.signal, AbortSignal.timeout(3e4)])
2191
2570
  );
@@ -2223,7 +2602,14 @@ function registerWeb(server, root) {
2223
2602
  "/settings",
2224
2603
  "/dashboard",
2225
2604
  "/sessions",
2226
- "/account"
2605
+ "/account",
2606
+ "/projects",
2607
+ "/site-settings",
2608
+ "/projects/:projectId/dashboard",
2609
+ "/projects/:projectId/files",
2610
+ "/projects/:projectId/sessions",
2611
+ "/projects/:projectId/members",
2612
+ "/projects/:projectId/settings"
2227
2613
  ])
2228
2614
  server.get(
2229
2615
  path,
@@ -2264,6 +2650,7 @@ import { z as z9 } from "zod";
2264
2650
  var CredentialSchema = z9.string().regex(/^tmy_[a-f0-9]{64}$/);
2265
2651
  var EnrollmentSchema = z9.object({
2266
2652
  operationId: z9.uuid(),
2653
+ projectId: NameSchema,
2267
2654
  memberId: NameSchema,
2268
2655
  deviceId: DeviceIdSchema,
2269
2656
  credential: CredentialSchema
@@ -2303,12 +2690,8 @@ var WebDevicesSchema = z9.object({
2303
2690
 
2304
2691
  // apps/server/control-routes.ts
2305
2692
  function registerControlRoutes(server, store) {
2306
- server.put("/api/v1/devices/self/status", async (request) => {
2307
- const device = request.headers["x-teammy-device"];
2308
- const identity = store.authorize(
2309
- request.headers.authorization,
2310
- typeof device === "string" ? device : void 0
2311
- );
2693
+ server.put("/devices/self/status", async (request) => {
2694
+ const identity = authorizeDevice(store, request);
2312
2695
  new ProjectRepository(store).config(identity);
2313
2696
  const status = DeviceStatusSchema.parse(request.body);
2314
2697
  store.db.prepare(
@@ -2322,8 +2705,8 @@ function registerControlRoutes(server, store) {
2322
2705
  );
2323
2706
  return { ok: true };
2324
2707
  });
2325
- server.get("/api/v1/web/devices", async (request) => {
2326
- const identity = new ProjectRepository(store).session(cookieToken(request));
2708
+ server.get("/web/devices", async (request) => {
2709
+ const identity = authorizeWeb(store, request);
2327
2710
  const devices = store.db.prepare(
2328
2711
  "SELECT m.member_id,m.device_id,d.seen_at,d.status_json FROM members m LEFT JOIN device_status d ON d.workspace_id=m.workspace_id AND d.member_id=m.member_id AND d.device_id=m.device_id WHERE m.workspace_id=? ORDER BY m.member_id"
2329
2712
  ).all(identity.workspaceId).map((row) => ({
@@ -2336,6 +2719,71 @@ function registerControlRoutes(server, store) {
2336
2719
  });
2337
2720
  }
2338
2721
 
2722
+ // apps/server/project-routes.ts
2723
+ import { z as z10 } from "zod";
2724
+ function registerProjectRoutes(server, store) {
2725
+ const project = new ProjectRepository(store), site = new SiteRepository(store);
2726
+ server.get("/api/v1/projects", async (request) => ({
2727
+ projects: project.projects(site.identity(cookieToken(request)))
2728
+ }));
2729
+ server.post("/api/v1/projects", async (request) => {
2730
+ webMutation(request);
2731
+ return project.createProject(
2732
+ site.identity(cookieToken(request)),
2733
+ ProjectCreateSchema.parse(request.body)
2734
+ );
2735
+ });
2736
+ }
2737
+ function registerProjectScopeRoutes(server, store) {
2738
+ const project = new ProjectRepository(store);
2739
+ server.delete("/membership", async (request) => {
2740
+ webMutation(request);
2741
+ return project.leave(project.site.identity(cookieToken(request)), projectId(request));
2742
+ });
2743
+ server.get("/identity", async (request) => authorizeWeb(store, request));
2744
+ server.get("/project/config", async (request, reply) => {
2745
+ const config = project.config(authorizeRead(store, request));
2746
+ const etag = `"project-${config.projectId}-${config.configRevision}"`;
2747
+ reply.header("cache-control", "private, no-cache").header("etag", etag);
2748
+ if (request.headers["if-none-match"] === etag) return reply.code(304).send();
2749
+ return config;
2750
+ });
2751
+ server.patch("/project/config", async (request) => {
2752
+ webMutation(request);
2753
+ return project.updateConfig(
2754
+ authorizeWeb(store, request),
2755
+ ConfigUpdateSchema.parse(request.body)
2756
+ );
2757
+ });
2758
+ server.get("/web/members", async (request) => ({
2759
+ members: project.members(authorizeWeb(store, request))
2760
+ }));
2761
+ server.post("/web/members", async (request) => {
2762
+ webMutation(request);
2763
+ return project.createMember(
2764
+ authorizeWeb(store, request),
2765
+ MemberCreateSchema.parse(request.body)
2766
+ );
2767
+ });
2768
+ server.patch("/web/members/:id", async (request) => {
2769
+ webMutation(request);
2770
+ const { id } = z10.object({ id: NameSchema }).parse(request.params);
2771
+ return project.updateMember(
2772
+ authorizeWeb(store, request),
2773
+ id,
2774
+ MemberUpdateSchema.parse(request.body)
2775
+ );
2776
+ });
2777
+ server.post("/web/members/:id/device-reset", async (request) => {
2778
+ webMutation(request);
2779
+ const { id } = z10.object({ id: NameSchema }).parse(request.params);
2780
+ return project.resetDevice(authorizeWeb(store, request), id);
2781
+ });
2782
+ }
2783
+
2784
+ // apps/server/site-routes.ts
2785
+ import { z as z11 } from "zod";
2786
+
2339
2787
  // apps/server/devices.ts
2340
2788
  var DeviceRepository = class {
2341
2789
  constructor(hub) {
@@ -2345,14 +2793,17 @@ var DeviceRepository = class {
2345
2793
  enroll(input) {
2346
2794
  const value = EnrollmentSchema.parse(input);
2347
2795
  return transaction(this.hub.db, () => {
2348
- const { projectId } = new ProjectRepository(this.hub).config();
2349
- const member = this.hub.db.prepare("SELECT * FROM members WHERE workspace_id=? AND member_id=? AND enabled=1").get(projectId, value.memberId);
2796
+ const { projectId: projectId2 } = value;
2797
+ new ProjectRepository(this.hub).config({ workspaceId: projectId2, memberId: value.memberId });
2798
+ const member = this.hub.db.prepare(
2799
+ "SELECT m.* FROM members m JOIN users u ON u.id=m.member_id WHERE m.workspace_id=? AND m.member_id=? AND m.enabled=1 AND u.enabled=1"
2800
+ ).get(projectId2, value.memberId);
2350
2801
  if (!member)
2351
2802
  throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
2352
2803
  const fingerprint = digest2(JSON.stringify(value));
2353
2804
  const prior = this.hub.db.prepare(
2354
2805
  "SELECT fingerprint FROM device_enrollments WHERE workspace_id=? AND member_id=? AND operation_id=?"
2355
- ).get(projectId, value.memberId, value.operationId);
2806
+ ).get(projectId2, value.memberId, value.operationId);
2356
2807
  if (prior && prior.fingerprint !== fingerprint)
2357
2808
  throw new HubError(
2358
2809
  409,
@@ -2376,23 +2827,28 @@ var DeviceRepository = class {
2376
2827
  );
2377
2828
  this.hub.db.prepare(
2378
2829
  "UPDATE members SET token_hash=?,device_id=?,member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
2379
- ).run(tokenHash, value.deviceId, projectId, value.memberId);
2380
- this.hub.db.prepare("INSERT INTO device_enrollments VALUES (?,?,?,?)").run(projectId, value.memberId, value.operationId, fingerprint);
2830
+ ).run(tokenHash, value.deviceId, projectId2, value.memberId);
2831
+ this.hub.db.prepare("INSERT INTO device_enrollments VALUES (?,?,?,?)").run(projectId2, value.memberId, value.operationId, fingerprint);
2381
2832
  }
2382
- return { workspaceId: projectId, memberId: value.memberId, deviceId: value.deviceId };
2833
+ return { workspaceId: projectId2, memberId: value.memberId, deviceId: value.deviceId };
2383
2834
  });
2384
2835
  }
2385
2836
  };
2386
2837
 
2387
- // apps/server/project-routes.ts
2388
- import { z as z10 } from "zod";
2389
- function registerProjectRoutes(server, store) {
2838
+ // apps/server/site-routes.ts
2839
+ function registerSiteRoutes(server, store) {
2390
2840
  const project = new ProjectRepository(store);
2391
- server.post(
2392
- "/api/v1/devices/enroll",
2393
- async (request) => new DeviceRepository(store).enroll(EnrollmentSchema.parse(request.body))
2394
- );
2395
- server.get("/api/v1/setup", async () => project.status());
2841
+ const site = new SiteRepository(store);
2842
+ server.post("/api/v1/devices/enroll", async (request) => {
2843
+ if (request.body && typeof request.body === "object" && !("projectId" in request.body))
2844
+ throw new HubError(
2845
+ 400,
2846
+ "PROJECT_REQUIRED",
2847
+ "Upgrade the client and connect with --project ID from the project settings."
2848
+ );
2849
+ return new DeviceRepository(store).enroll(EnrollmentSchema.parse(request.body));
2850
+ });
2851
+ server.get("/api/v1/setup", async () => site.status());
2396
2852
  server.post(
2397
2853
  "/api/v1/setup",
2398
2854
  { onRequest: async (request) => webMutation(request) },
@@ -2402,8 +2858,8 @@ function registerProjectRoutes(server, store) {
2402
2858
  "/api/v1/web/session",
2403
2859
  { onRequest: async (request) => webMutation(request) },
2404
2860
  async (request, reply) => {
2405
- const { id } = z10.object({ id: NameSchema }).strict().parse(request.body);
2406
- const { token, session } = project.login(id);
2861
+ const { id } = z11.object({ id: NameSchema }).strict().parse(request.body);
2862
+ const { token, session } = site.login(id);
2407
2863
  const secure = request.protocol === "https" || request.headers.origin?.startsWith("https://");
2408
2864
  reply.header(
2409
2865
  "set-cookie",
@@ -2412,114 +2868,116 @@ function registerProjectRoutes(server, store) {
2412
2868
  return session;
2413
2869
  }
2414
2870
  );
2415
- server.get("/api/v1/web/session", async (request) => project.session(cookieToken(request)));
2871
+ server.get("/api/v1/web/session", async (request) => site.identity(cookieToken(request)));
2872
+ server.patch("/api/v1/web/session", async (request) => {
2873
+ webMutation(request);
2874
+ const identity = site.identity(cookieToken(request));
2875
+ const { displayName } = z11.object({ displayName: z11.string().trim().max(120) }).strict().parse(request.body);
2876
+ return site.renameSelf(identity, displayName);
2877
+ });
2416
2878
  server.delete("/api/v1/web/session", async (request, reply) => {
2417
2879
  webMutation(request);
2418
- project.logout(cookieToken(request));
2880
+ site.logout(cookieToken(request));
2419
2881
  reply.header("set-cookie", "teammy_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0");
2420
2882
  return { ok: true };
2421
2883
  });
2422
- server.get("/api/v1/project/config", async (request, reply) => {
2423
- const config = project.config(authorizeRead(store, request));
2424
- const etag = `"project-${config.configRevision}"`;
2425
- reply.header("cache-control", "private, no-cache").header("etag", etag);
2426
- if (request.headers["if-none-match"] === etag) return reply.code(304).send();
2427
- return config;
2884
+ server.get("/api/v1/site/config", async (request) => {
2885
+ requireSiteAdmin(site.identity(cookieToken(request)));
2886
+ return site.config();
2428
2887
  });
2429
- server.patch("/api/v1/project/config", async (request) => {
2888
+ server.patch("/api/v1/site/config", async (request) => {
2430
2889
  webMutation(request);
2431
- return project.updateConfig(
2432
- project.session(cookieToken(request)),
2433
- ConfigUpdateSchema.parse(request.body)
2890
+ return site.updateConfig(
2891
+ site.identity(cookieToken(request)),
2892
+ SiteConfigUpdateSchema.parse(request.body)
2434
2893
  );
2435
2894
  });
2436
- server.get("/api/v1/web/members", async (request) => ({
2437
- members: project.members(project.session(cookieToken(request)))
2895
+ server.get("/api/v1/users", async (request) => ({
2896
+ users: site.users(site.identity(cookieToken(request)))
2438
2897
  }));
2439
- server.post("/api/v1/web/members", async (request) => {
2898
+ server.post("/api/v1/users", async (request) => {
2440
2899
  webMutation(request);
2441
- return project.createMember(
2442
- project.session(cookieToken(request)),
2443
- MemberCreateSchema.parse(request.body)
2900
+ return site.createUser(
2901
+ site.identity(cookieToken(request)),
2902
+ UserCreateSchema.parse(request.body)
2444
2903
  );
2445
2904
  });
2446
- server.patch("/api/v1/web/members/:id", async (request) => {
2905
+ server.patch("/api/v1/users/:id", async (request) => {
2447
2906
  webMutation(request);
2448
- const { id } = z10.object({ id: NameSchema }).parse(request.params);
2449
- return project.updateMember(
2450
- project.session(cookieToken(request)),
2451
- id,
2452
- MemberUpdateSchema.parse(request.body)
2907
+ return site.updateUser(
2908
+ site.identity(cookieToken(request)),
2909
+ NameSchema.parse(request.params.id),
2910
+ UserUpdateSchema.parse(request.body)
2453
2911
  );
2454
2912
  });
2455
- server.post("/api/v1/web/members/:id/device-reset", async (request) => {
2456
- webMutation(request);
2457
- const { id } = z10.object({ id: NameSchema }).parse(request.params);
2458
- return project.resetDevice(project.session(cookieToken(request)), id);
2459
- });
2460
2913
  }
2461
2914
 
2462
2915
  // apps/server/session-routes.ts
2463
- import { z as z12 } from "zod";
2916
+ import { z as z13 } from "zod";
2464
2917
 
2465
2918
  // packages/protocol/sessions.ts
2466
- import { z as z11 } from "zod";
2467
- var AgentSchema = z11.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
2468
- var SessionSnapshotSchema = z11.object({
2469
- formatVersion: z11.literal(1),
2470
- adapterVersion: z11.literal(1),
2919
+ import { z as z12 } from "zod";
2920
+ var AgentSchema = z12.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
2921
+ var SessionSnapshotSchema = z12.object({
2922
+ formatVersion: z12.literal(1),
2923
+ adapterVersion: z12.literal(1),
2471
2924
  agent: AgentSchema,
2472
- nativeId: z11.string().min(1).max(512),
2473
- cwd: z11.string().min(1).max(4096),
2474
- messages: z11.array(
2475
- z11.object({
2476
- id: z11.string().min(1).max(1024),
2477
- role: z11.enum(["user", "assistant"]),
2478
- text: z11.string().min(1).max(2 * 1024 * 1024),
2479
- source: z11.string().min(1).max(1024)
2925
+ nativeId: z12.string().min(1).max(512),
2926
+ cwd: z12.string().min(1).max(4096),
2927
+ messages: z12.array(
2928
+ z12.object({
2929
+ id: z12.string().min(1).max(1024),
2930
+ role: z12.enum(["user", "assistant"]),
2931
+ text: z12.string().min(1).max(2 * 1024 * 1024),
2932
+ source: z12.string().min(1).max(1024)
2480
2933
  }).strict()
2481
2934
  ).max(5e4)
2482
2935
  }).strict().refine(
2483
2936
  (s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
2484
2937
  "Duplicate message IDs."
2485
2938
  );
2486
- var SessionCommitSchema = z11.object({
2487
- operationId: z11.uuid(),
2939
+ var SessionCommitSchema = z12.object({
2940
+ operationId: z12.uuid(),
2488
2941
  sessionKey: HashSchema,
2489
2942
  parentRevision: RevisionSchema,
2490
2943
  blob: BlobRefSchema
2491
2944
  }).strict();
2492
- var SessionInfoSchema = z11.object({
2945
+ var SessionInfoSchema = z12.object({
2493
2946
  owner: NameSchema,
2494
2947
  sessionKey: HashSchema,
2495
2948
  agent: AgentSchema,
2496
- nativeId: z11.string(),
2497
- cwd: z11.string(),
2498
- title: z11.string(),
2949
+ nativeId: z12.string(),
2950
+ cwd: z12.string(),
2951
+ title: z12.string(),
2499
2952
  revision: RevisionSchema,
2500
- updatedAt: z11.string(),
2501
- messageCount: z11.number().int().nonnegative(),
2953
+ updatedAt: z12.string(),
2954
+ messageCount: z12.number().int().nonnegative(),
2502
2955
  blob: BlobRefSchema
2503
2956
  });
2504
- var SessionListSchema = z11.object({
2505
- sessions: z11.array(SessionInfoSchema),
2957
+ var SessionListSchema = z12.object({
2958
+ sessions: z12.array(SessionInfoSchema),
2506
2959
  nextCursor: RevisionSchema.nullable()
2507
2960
  });
2508
- var SessionStatsSchema = z11.object({
2509
- totalSessions: z11.number().int().nonnegative(),
2510
- totalMessages: z11.number().int().nonnegative(),
2511
- memberCount: z11.number().int().nonnegative(),
2512
- updatedAt: z11.string().nullable()
2961
+ var SessionStatsSchema = z12.object({
2962
+ totalSessions: z12.number().int().nonnegative(),
2963
+ totalMessages: z12.number().int().nonnegative(),
2964
+ memberCount: z12.number().int().nonnegative(),
2965
+ updatedAt: z12.string().nullable()
2513
2966
  });
2514
- var SessionSearchSchema = z11.object({
2515
- matches: z11.array(
2516
- z11.object({
2967
+ var SessionScopeSchema = z12.object({
2968
+ owner: NameSchema.optional(),
2969
+ sessionKey: HashSchema.optional()
2970
+ }).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
2971
+ var SessionSearchSchema = z12.object({
2972
+ matches: z12.array(
2973
+ z12.object({
2517
2974
  owner: NameSchema,
2518
2975
  sessionKey: HashSchema,
2519
2976
  revision: RevisionSchema,
2520
- messageId: z11.string(),
2521
- role: z11.enum(["user", "assistant"]),
2522
- snippet: z11.string()
2977
+ messageId: z12.string(),
2978
+ role: z12.enum(["user", "assistant"]),
2979
+ snippet: z12.string(),
2980
+ title: z12.string()
2523
2981
  })
2524
2982
  ),
2525
2983
  nextCursor: RevisionSchema.nullable()
@@ -2680,26 +3138,31 @@ var SessionRepository = class {
2680
3138
  ).get(identity.workspaceId)
2681
3139
  );
2682
3140
  }
2683
- list(identity, after, limit) {
3141
+ list(identity, after, limit, owner) {
2684
3142
  const rows = this.hub.db.prepare(
2685
3143
  `SELECT s.id, r.info_json FROM sessions s JOIN session_revisions r USING(workspace_id, owner, session_key, revision)
2686
- WHERE s.workspace_id=? AND s.id>? ORDER BY s.id LIMIT ?`
2687
- ).all(identity.workspaceId, after, limit + 1);
3144
+ WHERE s.workspace_id=? AND s.id>? ${owner ? "AND s.owner=?" : ""} ORDER BY s.id LIMIT ?`
3145
+ ).all(identity.workspaceId, after, ...owner ? [owner] : [], limit + 1);
2688
3146
  return {
2689
3147
  sessions: rows.slice(0, limit).map((row) => SessionInfoSchema.parse(JSON.parse(String(row.info_json)))),
2690
3148
  nextCursor: rows.length > limit ? Number(rows[limit - 1].id) : null
2691
3149
  };
2692
3150
  }
2693
- search(identity, query, after, limit) {
3151
+ search(identity, query, after, limit, scope = {}) {
3152
+ SessionScopeSchema.parse(scope);
2694
3153
  const lower = query.toLowerCase();
2695
3154
  const indexed = [...lower].length >= 3;
2696
3155
  const rows = this.hub.db.prepare(
2697
- `SELECT * FROM session_messages WHERE workspace_id=? AND id>? AND instr(search_text, ?)>0
2698
- ${indexed ? "AND id IN (SELECT rowid FROM session_fts WHERE session_fts MATCH ?)" : ""} ORDER BY id LIMIT ?`
3156
+ `SELECT m.*,s.title,s.native_id FROM session_messages m JOIN sessions s USING(workspace_id,owner,session_key)
3157
+ WHERE m.workspace_id=? AND m.id>? AND instr(m.search_text, ?)>0
3158
+ ${scope.owner ? "AND m.owner=?" : ""} ${scope.sessionKey ? "AND m.session_key=?" : ""}
3159
+ ${indexed ? "AND m.id IN (SELECT rowid FROM session_fts WHERE session_fts MATCH ?)" : ""} ORDER BY m.id LIMIT ?`
2699
3160
  ).all(
2700
3161
  identity.workspaceId,
2701
3162
  after,
2702
3163
  lower,
3164
+ ...scope.owner ? [scope.owner] : [],
3165
+ ...scope.sessionKey ? [scope.sessionKey] : [],
2703
3166
  ...indexed ? [`"${lower.replaceAll('"', '""')}"`] : [],
2704
3167
  limit + 1
2705
3168
  );
@@ -2712,7 +3175,8 @@ var SessionRepository = class {
2712
3175
  revision: Number(row.revision),
2713
3176
  messageId: String(row.message_id),
2714
3177
  role: String(row.role),
2715
- snippet: String(row.body).slice(start, start + 500)
3178
+ snippet: String(row.body).slice(start, start + 500),
3179
+ title: String(row.title || row.native_id)
2716
3180
  };
2717
3181
  }),
2718
3182
  nextCursor: rows.length > limit ? Number(rows[limit - 1].id) : null
@@ -2724,18 +3188,18 @@ var SessionRepository = class {
2724
3188
  function registerSessionRoutes(server, store) {
2725
3189
  const sessions = new SessionRepository(store);
2726
3190
  const device = (request) => typeof request.headers["x-teammy-device"] === "string" ? request.headers["x-teammy-device"] : void 0;
2727
- const authorize = (request) => store.authorize(request.headers.authorization, device(request));
3191
+ const authorize = (request) => authorizeDevice(store, request);
2728
3192
  const guard = {
2729
3193
  onRequest: async (request) => {
2730
3194
  authorize(request);
2731
3195
  }
2732
3196
  };
2733
- const pagination = z12.object({
2734
- after: z12.coerce.number().pipe(RevisionSchema).default(0),
2735
- limit: z12.coerce.number().int().min(1).max(100).default(50)
3197
+ const pagination = z13.object({
3198
+ after: z13.coerce.number().pipe(RevisionSchema).default(0),
3199
+ limit: z13.coerce.number().int().min(1).max(100).default(50)
2736
3200
  });
2737
3201
  server.post(
2738
- "/api/v1/sessions/commits",
3202
+ "/sessions/commits",
2739
3203
  guard,
2740
3204
  (request) => sessions.commit(
2741
3205
  request.headers.authorization,
@@ -2743,36 +3207,41 @@ function registerSessionRoutes(server, store) {
2743
3207
  SessionCommitSchema.parse(request.body)
2744
3208
  )
2745
3209
  );
2746
- server.get("/api/v1/sessions", async (request) => {
2747
- const q = pagination.parse(request.query);
2748
- return sessions.list(authorizeRead(store, request), q.after, q.limit);
3210
+ server.get("/sessions", async (request) => {
3211
+ const q = pagination.extend({ owner: NameSchema.optional() }).parse(request.query);
3212
+ return sessions.list(authorizeRead(store, request), q.after, q.limit, q.owner);
2749
3213
  });
2750
- server.get(
2751
- "/api/v1/sessions/stats",
2752
- async (request) => sessions.stats(authorizeRead(store, request))
2753
- );
2754
- server.get("/api/v1/sessions/search", async (request) => {
3214
+ server.get("/sessions/stats", async (request) => sessions.stats(authorizeRead(store, request)));
3215
+ server.get("/sessions/search", async (request) => {
2755
3216
  const q = pagination.extend({
2756
- q: z12.string().min(1).max(500).refine((s) => !s.includes("\0"))
3217
+ owner: NameSchema.optional(),
3218
+ sessionKey: HashSchema.optional(),
3219
+ q: z13.string().min(1).max(500).refine((s) => !s.includes("\0"))
2757
3220
  }).parse(request.query);
2758
- return sessions.search(authorizeRead(store, request), q.q, q.after, q.limit);
3221
+ return sessions.search(
3222
+ authorizeRead(store, request),
3223
+ q.q,
3224
+ q.after,
3225
+ q.limit,
3226
+ SessionScopeSchema.parse({ owner: q.owner, sessionKey: q.sessionKey })
3227
+ );
2759
3228
  });
2760
- server.get("/api/v1/sessions/:owner/:key", async (request) => {
2761
- const p = z12.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
2762
- const q = z12.object({ revision: z12.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
3229
+ server.get("/sessions/:owner/:key", async (request) => {
3230
+ const p = z13.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
3231
+ const q = z13.object({ revision: z13.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
2763
3232
  return sessions.read(authorizeRead(store, request), p.owner, p.key, q.revision);
2764
3233
  });
2765
3234
  }
2766
3235
 
2767
3236
  // apps/server/file-routes.ts
2768
- import { z as z13 } from "zod";
3237
+ import { z as z14 } from "zod";
2769
3238
  function registerFileRoutes(server, store) {
2770
3239
  const files = new FileRepository(store);
2771
3240
  const device = (request) => {
2772
3241
  const value = request.headers["x-teammy-device"];
2773
3242
  return typeof value === "string" ? value : void 0;
2774
3243
  };
2775
- const authorize = (request) => store.authorize(request.headers.authorization, device(request));
3244
+ const authorize = (request) => authorizeDevice(store, request);
2776
3245
  const guard = {
2777
3246
  onRequest: async (request) => {
2778
3247
  authorize(request);
@@ -2783,21 +3252,21 @@ function registerFileRoutes(server, store) {
2783
3252
  { parseAs: "buffer", bodyLimit: MAX_BLOB_BYTES },
2784
3253
  (_request, body, done) => done(null, body)
2785
3254
  );
2786
- server.post("/api/v1/blobs/check", guard, async (request) => ({
3255
+ server.post("/blobs/check", guard, async (request) => ({
2787
3256
  missing: files.missing(authorize(request), BlobCheckSchema.parse(request.body).hashes)
2788
3257
  }));
2789
- server.put("/api/v1/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
2790
- const { hash: hash2 } = z13.object({ hash: HashSchema }).parse(request.params);
3258
+ server.put("/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
3259
+ const { hash: hash2 } = z14.object({ hash: HashSchema }).parse(request.params);
2791
3260
  if (!Buffer.isBuffer(request.body))
2792
3261
  throw Object.assign(new Error("Expected binary content"), { statusCode: 400 });
2793
3262
  return files.upload(request.headers.authorization, device(request), hash2, request.body);
2794
3263
  });
2795
- server.get("/api/v1/blobs/:hash", async (request, reply) => {
2796
- const { hash: hash2 } = z13.object({ hash: HashSchema }).parse(request.params);
3264
+ server.get("/blobs/:hash", async (request, reply) => {
3265
+ const { hash: hash2 } = z14.object({ hash: HashSchema }).parse(request.params);
2797
3266
  return reply.type("application/octet-stream").send(Buffer.from(await files.download(authorizeRead(store, request), hash2)));
2798
3267
  });
2799
3268
  server.post(
2800
- "/api/v1/files/commits",
3269
+ "/files/commits",
2801
3270
  guard,
2802
3271
  async (request) => files.commit(
2803
3272
  request.headers.authorization,
@@ -2805,12 +3274,12 @@ function registerFileRoutes(server, store) {
2805
3274
  FileCommitSchema.parse(request.body)
2806
3275
  )
2807
3276
  );
2808
- server.get("/api/v1/files/:owner/manifest", async (request) => {
2809
- const { owner } = z13.object({ owner: NameSchema }).parse(request.params);
2810
- const query = z13.object({
2811
- revision: z13.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
2812
- after: z13.string().max(1024).default(""),
2813
- limit: z13.coerce.number().int().min(1).max(500).default(200)
3277
+ server.get("/files/:owner/manifest", async (request) => {
3278
+ const { owner } = z14.object({ owner: NameSchema }).parse(request.params);
3279
+ const query = z14.object({
3280
+ revision: z14.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
3281
+ after: z14.string().max(1024).default(""),
3282
+ limit: z14.coerce.number().int().min(1).max(500).default(200)
2814
3283
  }).parse(request.query);
2815
3284
  return files.manifest(
2816
3285
  authorizeRead(store, request),
@@ -2820,29 +3289,29 @@ function registerFileRoutes(server, store) {
2820
3289
  query.limit
2821
3290
  );
2822
3291
  });
2823
- server.get("/api/v1/files/:owner/entry", async (request) => {
2824
- const { owner } = z13.object({ owner: NameSchema }).parse(request.params);
2825
- const q = z13.object({
3292
+ server.get("/files/:owner/entry", async (request) => {
3293
+ const { owner } = z14.object({ owner: NameSchema }).parse(request.params);
3294
+ const q = z14.object({
2826
3295
  path: FilePathSchema,
2827
- revision: z13.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
3296
+ revision: z14.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
2828
3297
  }).parse(request.query);
2829
3298
  return files.entry(authorizeRead(store, request), owner, q.path, q.revision);
2830
3299
  });
2831
- server.get("/api/v1/files/:owner/tree", async (request) => {
2832
- const { owner } = z13.object({ owner: NameSchema }).parse(request.params);
2833
- const q = z13.object({
2834
- path: z13.union([z13.literal(""), FilePathSchema]).default(""),
2835
- revision: z13.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
2836
- after: z13.string().max(255).default(""),
2837
- limit: z13.coerce.number().int().min(1).max(200).default(100)
3300
+ server.get("/files/:owner/tree", async (request) => {
3301
+ const { owner } = z14.object({ owner: NameSchema }).parse(request.params);
3302
+ const q = z14.object({
3303
+ path: z14.union([z14.literal(""), FilePathSchema]).default(""),
3304
+ revision: z14.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
3305
+ after: z14.string().max(255).default(""),
3306
+ limit: z14.coerce.number().int().min(1).max(200).default(100)
2838
3307
  }).parse(request.query);
2839
3308
  return files.tree(authorizeRead(store, request), owner, q.path, q.revision, q.after, q.limit);
2840
3309
  });
2841
- server.get("/api/v1/files/:owner/changes", async (request) => {
2842
- const { owner } = z13.object({ owner: NameSchema }).parse(request.params);
2843
- const query = z13.object({
2844
- after: z13.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
2845
- limit: z13.coerce.number().int().min(1).max(100).default(100)
3310
+ server.get("/files/:owner/changes", async (request) => {
3311
+ const { owner } = z14.object({ owner: NameSchema }).parse(request.params);
3312
+ const query = z14.object({
3313
+ after: z14.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
3314
+ limit: z14.coerce.number().int().min(1).max(100).default(100)
2846
3315
  }).parse(request.query);
2847
3316
  return files.changes(authorizeRead(store, request), owner, query.after, query.limit);
2848
3317
  });
@@ -2897,6 +3366,7 @@ function buildServer(options) {
2897
3366
  "session-revisions",
2898
3367
  "session-search",
2899
3368
  "project-config",
3369
+ "multi-project",
2900
3370
  "web-identity",
2901
3371
  "device-status",
2902
3372
  "ai-profiles",
@@ -2905,23 +3375,24 @@ function buildServer(options) {
2905
3375
  ]
2906
3376
  }));
2907
3377
  if (store) {
3378
+ registerSiteRoutes(server, store);
2908
3379
  registerProjectRoutes(server, store);
2909
3380
  registerAiRoutes(server, store);
2910
- registerAiJobRoutes(server, store);
2911
- registerControlRoutes(server, store);
2912
- registerFileRoutes(server, store);
2913
- registerSessionRoutes(server, store);
2914
- server.post(
2915
- "/api/v1/bind",
2916
- async (request) => store.bind(request.headers.authorization, BindRequestSchema.parse(request.body).deviceId)
2917
- );
2918
- server.get("/api/v1/members", async (request) => {
2919
- const device = request.headers["x-teammy-device"];
2920
- const identity = store.authorize(
2921
- request.headers.authorization,
2922
- typeof device === "string" ? device : void 0
2923
- );
2924
- return { members: store.members(identity) };
3381
+ const projectRoutes = async (scope) => {
3382
+ registerProjectScopeRoutes(scope, store);
3383
+ registerAiJobRoutes(scope, store);
3384
+ registerControlRoutes(scope, store);
3385
+ registerFileRoutes(scope, store);
3386
+ registerSessionRoutes(scope, store);
3387
+ scope.get("/members", async (request) => ({
3388
+ members: store.members(authorizeDevice(store, request))
3389
+ }));
3390
+ };
3391
+ server.register(projectRoutes, { prefix: "/api/v1/projects/:projectId" });
3392
+ server.register(projectRoutes, { prefix: "/api/v1" });
3393
+ server.post("/api/v1/bind", async (request) => {
3394
+ const input = BindRequestSchema.parse(request.body);
3395
+ return store.bind(request.headers.authorization, input.deviceId, input.projectId);
2925
3396
  });
2926
3397
  }
2927
3398
  if (options.webDir) registerWeb(server, options.webDir);