@teammy/server 0.0.4 → 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/README.md +6 -4
- package/dist/main.js +931 -433
- package/dist/web/assets/Dashboard-B_Sm86Eu.js +1 -0
- package/dist/web/assets/Files-DO76AdRz.js +1 -0
- package/dist/web/assets/Markdown-BtKARCJq.js +29 -0
- package/dist/web/assets/Sessions-D0AuOQYE.js +1 -0
- package/dist/web/assets/index-BdkEmA80.css +1 -0
- package/dist/web/assets/index-ae0dgkXe.js +64 -0
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/dist/web/assets/Dashboard-CfMDmukc.js +0 -1
- package/dist/web/assets/Files-DvK_N3lO.js +0 -1
- package/dist/web/assets/Markdown-CcCzS4DP.js +0 -29
- package/dist/web/assets/Sessions-DTLr1cTK.js +0 -1
- package/dist/web/assets/file-links-BAMah1K6.js +0 -1
- package/dist/web/assets/index-CeGPJ_an.css +0 -1
- package/dist/web/assets/index-l1r3Ekhs.js +0 -64
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
395
|
-
workspaceId: NameSchema,
|
|
460
|
+
var WebIdentitySchema = z3.object({
|
|
396
461
|
memberId: NameSchema,
|
|
397
462
|
displayName: z3.string(),
|
|
398
|
-
|
|
463
|
+
siteRole: z3.enum(["admin", "member"])
|
|
464
|
+
});
|
|
465
|
+
var WebSessionSchema = WebIdentitySchema.extend({
|
|
466
|
+
workspaceId: NameSchema
|
|
399
467
|
});
|
|
400
|
-
var MemberCreateSchema = z3.object({
|
|
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
|
|
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,
|
|
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,
|
|
551
|
-
const iv =
|
|
552
|
-
cipher.setAAD(Buffer.from(JSON.stringify([
|
|
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(
|
|
557
|
-
return this.hub.db.prepare("SELECT * FROM ai_profiles
|
|
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(
|
|
564
|
-
const profile = this.list(
|
|
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(
|
|
570
|
-
const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE
|
|
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([
|
|
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
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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(
|
|
594
|
-
).run(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
1134
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
608
1135
|
|
|
609
1136
|
// apps/server/files.ts
|
|
610
|
-
import { createHash as
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
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 ||
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
2469
|
+
const jobs = new AiJobs(hub);
|
|
2108
2470
|
const id = (params) => z7.object({ id: z7.uuid() }).parse(params).id;
|
|
2109
|
-
server.get("/
|
|
2110
|
-
const session =
|
|
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("/
|
|
2477
|
+
server.post("/ai/jobs", async (req) => {
|
|
2116
2478
|
webMutation(req);
|
|
2117
|
-
return jobs.create(
|
|
2479
|
+
return jobs.create(authorizeWeb(hub, req), AiJobCreateSchema.parse(req.body));
|
|
2118
2480
|
});
|
|
2119
2481
|
server.get(
|
|
2120
|
-
"/
|
|
2121
|
-
async (req) => jobs.get(
|
|
2482
|
+
"/ai/jobs/:id",
|
|
2483
|
+
async (req) => jobs.get(authorizeWeb(hub, req).workspaceId, id(req.params))
|
|
2122
2484
|
);
|
|
2123
|
-
server.get("/
|
|
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(
|
|
2487
|
+
return jobs.events(authorizeWeb(hub, req).workspaceId, id(req.params), query.after);
|
|
2126
2488
|
});
|
|
2127
|
-
server.post("/
|
|
2489
|
+
server.post("/ai/jobs/:id/cancel", async (req) => {
|
|
2128
2490
|
webMutation(req);
|
|
2129
|
-
return jobs.cancel(
|
|
2491
|
+
return jobs.cancel(authorizeWeb(hub, req), id(req.params));
|
|
2130
2492
|
});
|
|
2131
|
-
server.post("/
|
|
2493
|
+
server.post("/ai/jobs/:id/retry", async (req) => {
|
|
2132
2494
|
webMutation(req);
|
|
2133
2495
|
return jobs.retry(
|
|
2134
|
-
|
|
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("/
|
|
2140
|
-
const session =
|
|
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
|
|
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
|
-
|
|
2159
|
-
return { profiles: config.list(
|
|
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.
|
|
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.
|
|
2172
|
-
|
|
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(
|
|
2567
|
+
config.secret(profile.id),
|
|
2189
2568
|
mode,
|
|
2190
2569
|
AbortSignal.any([controller.signal, AbortSignal.timeout(3e4)])
|
|
2191
2570
|
);
|
|
@@ -2216,7 +2595,22 @@ function registerWeb(server, root) {
|
|
|
2216
2595
|
dotfiles: "deny"
|
|
2217
2596
|
});
|
|
2218
2597
|
server.get("/ai", (_request, reply) => reply.redirect("/"));
|
|
2219
|
-
for (const path of [
|
|
2598
|
+
for (const path of [
|
|
2599
|
+
"/",
|
|
2600
|
+
"/files",
|
|
2601
|
+
"/members",
|
|
2602
|
+
"/settings",
|
|
2603
|
+
"/dashboard",
|
|
2604
|
+
"/sessions",
|
|
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"
|
|
2613
|
+
])
|
|
2220
2614
|
server.get(
|
|
2221
2615
|
path,
|
|
2222
2616
|
(_request, reply) => reply.header("cache-control", "no-cache").header(
|
|
@@ -2256,6 +2650,7 @@ import { z as z9 } from "zod";
|
|
|
2256
2650
|
var CredentialSchema = z9.string().regex(/^tmy_[a-f0-9]{64}$/);
|
|
2257
2651
|
var EnrollmentSchema = z9.object({
|
|
2258
2652
|
operationId: z9.uuid(),
|
|
2653
|
+
projectId: NameSchema,
|
|
2259
2654
|
memberId: NameSchema,
|
|
2260
2655
|
deviceId: DeviceIdSchema,
|
|
2261
2656
|
credential: CredentialSchema
|
|
@@ -2295,12 +2690,8 @@ var WebDevicesSchema = z9.object({
|
|
|
2295
2690
|
|
|
2296
2691
|
// apps/server/control-routes.ts
|
|
2297
2692
|
function registerControlRoutes(server, store) {
|
|
2298
|
-
server.put("/
|
|
2299
|
-
const
|
|
2300
|
-
const identity = store.authorize(
|
|
2301
|
-
request.headers.authorization,
|
|
2302
|
-
typeof device === "string" ? device : void 0
|
|
2303
|
-
);
|
|
2693
|
+
server.put("/devices/self/status", async (request) => {
|
|
2694
|
+
const identity = authorizeDevice(store, request);
|
|
2304
2695
|
new ProjectRepository(store).config(identity);
|
|
2305
2696
|
const status = DeviceStatusSchema.parse(request.body);
|
|
2306
2697
|
store.db.prepare(
|
|
@@ -2314,8 +2705,8 @@ function registerControlRoutes(server, store) {
|
|
|
2314
2705
|
);
|
|
2315
2706
|
return { ok: true };
|
|
2316
2707
|
});
|
|
2317
|
-
server.get("/
|
|
2318
|
-
const identity =
|
|
2708
|
+
server.get("/web/devices", async (request) => {
|
|
2709
|
+
const identity = authorizeWeb(store, request);
|
|
2319
2710
|
const devices = store.db.prepare(
|
|
2320
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"
|
|
2321
2712
|
).all(identity.workspaceId).map((row) => ({
|
|
@@ -2328,6 +2719,71 @@ function registerControlRoutes(server, store) {
|
|
|
2328
2719
|
});
|
|
2329
2720
|
}
|
|
2330
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
|
+
|
|
2331
2787
|
// apps/server/devices.ts
|
|
2332
2788
|
var DeviceRepository = class {
|
|
2333
2789
|
constructor(hub) {
|
|
@@ -2337,14 +2793,17 @@ var DeviceRepository = class {
|
|
|
2337
2793
|
enroll(input) {
|
|
2338
2794
|
const value = EnrollmentSchema.parse(input);
|
|
2339
2795
|
return transaction(this.hub.db, () => {
|
|
2340
|
-
const { projectId } =
|
|
2341
|
-
|
|
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);
|
|
2342
2801
|
if (!member)
|
|
2343
2802
|
throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
|
|
2344
2803
|
const fingerprint = digest2(JSON.stringify(value));
|
|
2345
2804
|
const prior = this.hub.db.prepare(
|
|
2346
2805
|
"SELECT fingerprint FROM device_enrollments WHERE workspace_id=? AND member_id=? AND operation_id=?"
|
|
2347
|
-
).get(
|
|
2806
|
+
).get(projectId2, value.memberId, value.operationId);
|
|
2348
2807
|
if (prior && prior.fingerprint !== fingerprint)
|
|
2349
2808
|
throw new HubError(
|
|
2350
2809
|
409,
|
|
@@ -2368,23 +2827,28 @@ var DeviceRepository = class {
|
|
|
2368
2827
|
);
|
|
2369
2828
|
this.hub.db.prepare(
|
|
2370
2829
|
"UPDATE members SET token_hash=?,device_id=?,member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
|
|
2371
|
-
).run(tokenHash, value.deviceId,
|
|
2372
|
-
this.hub.db.prepare("INSERT INTO device_enrollments VALUES (?,?,?,?)").run(
|
|
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);
|
|
2373
2832
|
}
|
|
2374
|
-
return { workspaceId:
|
|
2833
|
+
return { workspaceId: projectId2, memberId: value.memberId, deviceId: value.deviceId };
|
|
2375
2834
|
});
|
|
2376
2835
|
}
|
|
2377
2836
|
};
|
|
2378
2837
|
|
|
2379
|
-
// apps/server/
|
|
2380
|
-
|
|
2381
|
-
function registerProjectRoutes(server, store) {
|
|
2838
|
+
// apps/server/site-routes.ts
|
|
2839
|
+
function registerSiteRoutes(server, store) {
|
|
2382
2840
|
const project = new ProjectRepository(store);
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
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());
|
|
2388
2852
|
server.post(
|
|
2389
2853
|
"/api/v1/setup",
|
|
2390
2854
|
{ onRequest: async (request) => webMutation(request) },
|
|
@@ -2394,8 +2858,8 @@ function registerProjectRoutes(server, store) {
|
|
|
2394
2858
|
"/api/v1/web/session",
|
|
2395
2859
|
{ onRequest: async (request) => webMutation(request) },
|
|
2396
2860
|
async (request, reply) => {
|
|
2397
|
-
const { id } =
|
|
2398
|
-
const { token, session } =
|
|
2861
|
+
const { id } = z11.object({ id: NameSchema }).strict().parse(request.body);
|
|
2862
|
+
const { token, session } = site.login(id);
|
|
2399
2863
|
const secure = request.protocol === "https" || request.headers.origin?.startsWith("https://");
|
|
2400
2864
|
reply.header(
|
|
2401
2865
|
"set-cookie",
|
|
@@ -2404,108 +2868,116 @@ function registerProjectRoutes(server, store) {
|
|
|
2404
2868
|
return session;
|
|
2405
2869
|
}
|
|
2406
2870
|
);
|
|
2407
|
-
server.get("/api/v1/web/session", async (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
|
+
});
|
|
2408
2878
|
server.delete("/api/v1/web/session", async (request, reply) => {
|
|
2409
2879
|
webMutation(request);
|
|
2410
|
-
|
|
2880
|
+
site.logout(cookieToken(request));
|
|
2411
2881
|
reply.header("set-cookie", "teammy_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0");
|
|
2412
2882
|
return { ok: true };
|
|
2413
2883
|
});
|
|
2414
|
-
server.get("/api/v1/
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
reply.header("cache-control", "private, no-cache").header("etag", etag);
|
|
2418
|
-
if (request.headers["if-none-match"] === etag) return reply.code(304).send();
|
|
2419
|
-
return config;
|
|
2884
|
+
server.get("/api/v1/site/config", async (request) => {
|
|
2885
|
+
requireSiteAdmin(site.identity(cookieToken(request)));
|
|
2886
|
+
return site.config();
|
|
2420
2887
|
});
|
|
2421
|
-
server.patch("/api/v1/
|
|
2888
|
+
server.patch("/api/v1/site/config", async (request) => {
|
|
2422
2889
|
webMutation(request);
|
|
2423
|
-
return
|
|
2424
|
-
|
|
2425
|
-
|
|
2890
|
+
return site.updateConfig(
|
|
2891
|
+
site.identity(cookieToken(request)),
|
|
2892
|
+
SiteConfigUpdateSchema.parse(request.body)
|
|
2426
2893
|
);
|
|
2427
2894
|
});
|
|
2428
|
-
server.get("/api/v1/
|
|
2429
|
-
|
|
2895
|
+
server.get("/api/v1/users", async (request) => ({
|
|
2896
|
+
users: site.users(site.identity(cookieToken(request)))
|
|
2430
2897
|
}));
|
|
2431
|
-
server.post("/api/v1/
|
|
2898
|
+
server.post("/api/v1/users", async (request) => {
|
|
2432
2899
|
webMutation(request);
|
|
2433
|
-
return
|
|
2434
|
-
|
|
2435
|
-
|
|
2900
|
+
return site.createUser(
|
|
2901
|
+
site.identity(cookieToken(request)),
|
|
2902
|
+
UserCreateSchema.parse(request.body)
|
|
2436
2903
|
);
|
|
2437
2904
|
});
|
|
2438
|
-
server.patch("/api/v1/
|
|
2905
|
+
server.patch("/api/v1/users/:id", async (request) => {
|
|
2439
2906
|
webMutation(request);
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
MemberUpdateSchema.parse(request.body)
|
|
2907
|
+
return site.updateUser(
|
|
2908
|
+
site.identity(cookieToken(request)),
|
|
2909
|
+
NameSchema.parse(request.params.id),
|
|
2910
|
+
UserUpdateSchema.parse(request.body)
|
|
2445
2911
|
);
|
|
2446
2912
|
});
|
|
2447
|
-
server.post("/api/v1/web/members/:id/device-reset", async (request) => {
|
|
2448
|
-
webMutation(request);
|
|
2449
|
-
const { id } = z10.object({ id: NameSchema }).parse(request.params);
|
|
2450
|
-
return project.resetDevice(project.session(cookieToken(request)), id);
|
|
2451
|
-
});
|
|
2452
2913
|
}
|
|
2453
2914
|
|
|
2454
2915
|
// apps/server/session-routes.ts
|
|
2455
|
-
import { z as
|
|
2916
|
+
import { z as z13 } from "zod";
|
|
2456
2917
|
|
|
2457
2918
|
// packages/protocol/sessions.ts
|
|
2458
|
-
import { z as
|
|
2459
|
-
var AgentSchema =
|
|
2460
|
-
var SessionSnapshotSchema =
|
|
2461
|
-
formatVersion:
|
|
2462
|
-
adapterVersion:
|
|
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),
|
|
2463
2924
|
agent: AgentSchema,
|
|
2464
|
-
nativeId:
|
|
2465
|
-
cwd:
|
|
2466
|
-
messages:
|
|
2467
|
-
|
|
2468
|
-
id:
|
|
2469
|
-
role:
|
|
2470
|
-
text:
|
|
2471
|
-
source:
|
|
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)
|
|
2472
2933
|
}).strict()
|
|
2473
2934
|
).max(5e4)
|
|
2474
2935
|
}).strict().refine(
|
|
2475
2936
|
(s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
|
|
2476
2937
|
"Duplicate message IDs."
|
|
2477
2938
|
);
|
|
2478
|
-
var SessionCommitSchema =
|
|
2479
|
-
operationId:
|
|
2939
|
+
var SessionCommitSchema = z12.object({
|
|
2940
|
+
operationId: z12.uuid(),
|
|
2480
2941
|
sessionKey: HashSchema,
|
|
2481
2942
|
parentRevision: RevisionSchema,
|
|
2482
2943
|
blob: BlobRefSchema
|
|
2483
2944
|
}).strict();
|
|
2484
|
-
var SessionInfoSchema =
|
|
2945
|
+
var SessionInfoSchema = z12.object({
|
|
2485
2946
|
owner: NameSchema,
|
|
2486
2947
|
sessionKey: HashSchema,
|
|
2487
2948
|
agent: AgentSchema,
|
|
2488
|
-
nativeId:
|
|
2489
|
-
cwd:
|
|
2490
|
-
title:
|
|
2949
|
+
nativeId: z12.string(),
|
|
2950
|
+
cwd: z12.string(),
|
|
2951
|
+
title: z12.string(),
|
|
2491
2952
|
revision: RevisionSchema,
|
|
2492
|
-
updatedAt:
|
|
2493
|
-
messageCount:
|
|
2953
|
+
updatedAt: z12.string(),
|
|
2954
|
+
messageCount: z12.number().int().nonnegative(),
|
|
2494
2955
|
blob: BlobRefSchema
|
|
2495
2956
|
});
|
|
2496
|
-
var SessionListSchema =
|
|
2497
|
-
sessions:
|
|
2957
|
+
var SessionListSchema = z12.object({
|
|
2958
|
+
sessions: z12.array(SessionInfoSchema),
|
|
2498
2959
|
nextCursor: RevisionSchema.nullable()
|
|
2499
2960
|
});
|
|
2500
|
-
var
|
|
2501
|
-
|
|
2502
|
-
|
|
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()
|
|
2966
|
+
});
|
|
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({
|
|
2503
2974
|
owner: NameSchema,
|
|
2504
2975
|
sessionKey: HashSchema,
|
|
2505
2976
|
revision: RevisionSchema,
|
|
2506
|
-
messageId:
|
|
2507
|
-
role:
|
|
2508
|
-
snippet:
|
|
2977
|
+
messageId: z12.string(),
|
|
2978
|
+
role: z12.enum(["user", "assistant"]),
|
|
2979
|
+
snippet: z12.string(),
|
|
2980
|
+
title: z12.string()
|
|
2509
2981
|
})
|
|
2510
2982
|
),
|
|
2511
2983
|
nextCursor: RevisionSchema.nullable()
|
|
@@ -2657,26 +3129,40 @@ var SessionRepository = class {
|
|
|
2657
3129
|
);
|
|
2658
3130
|
return SessionInfoSchema.parse(JSON.parse(String(row.info_json)));
|
|
2659
3131
|
}
|
|
2660
|
-
|
|
3132
|
+
stats(identity) {
|
|
3133
|
+
return SessionStatsSchema.parse(
|
|
3134
|
+
this.hub.db.prepare(
|
|
3135
|
+
`SELECT count(*) AS totalSessions, coalesce(sum(message_count), 0) AS totalMessages,
|
|
3136
|
+
count(DISTINCT owner) AS memberCount, max(updated_at) AS updatedAt
|
|
3137
|
+
FROM sessions WHERE workspace_id=?`
|
|
3138
|
+
).get(identity.workspaceId)
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
list(identity, after, limit, owner) {
|
|
2661
3142
|
const rows = this.hub.db.prepare(
|
|
2662
3143
|
`SELECT s.id, r.info_json FROM sessions s JOIN session_revisions r USING(workspace_id, owner, session_key, revision)
|
|
2663
|
-
WHERE s.workspace_id=? AND s.id>? ORDER BY s.id LIMIT ?`
|
|
2664
|
-
).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);
|
|
2665
3146
|
return {
|
|
2666
3147
|
sessions: rows.slice(0, limit).map((row) => SessionInfoSchema.parse(JSON.parse(String(row.info_json)))),
|
|
2667
3148
|
nextCursor: rows.length > limit ? Number(rows[limit - 1].id) : null
|
|
2668
3149
|
};
|
|
2669
3150
|
}
|
|
2670
|
-
search(identity, query, after, limit) {
|
|
3151
|
+
search(identity, query, after, limit, scope = {}) {
|
|
3152
|
+
SessionScopeSchema.parse(scope);
|
|
2671
3153
|
const lower = query.toLowerCase();
|
|
2672
3154
|
const indexed = [...lower].length >= 3;
|
|
2673
3155
|
const rows = this.hub.db.prepare(
|
|
2674
|
-
`SELECT
|
|
2675
|
-
|
|
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 ?`
|
|
2676
3160
|
).all(
|
|
2677
3161
|
identity.workspaceId,
|
|
2678
3162
|
after,
|
|
2679
3163
|
lower,
|
|
3164
|
+
...scope.owner ? [scope.owner] : [],
|
|
3165
|
+
...scope.sessionKey ? [scope.sessionKey] : [],
|
|
2680
3166
|
...indexed ? [`"${lower.replaceAll('"', '""')}"`] : [],
|
|
2681
3167
|
limit + 1
|
|
2682
3168
|
);
|
|
@@ -2689,7 +3175,8 @@ var SessionRepository = class {
|
|
|
2689
3175
|
revision: Number(row.revision),
|
|
2690
3176
|
messageId: String(row.message_id),
|
|
2691
3177
|
role: String(row.role),
|
|
2692
|
-
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)
|
|
2693
3180
|
};
|
|
2694
3181
|
}),
|
|
2695
3182
|
nextCursor: rows.length > limit ? Number(rows[limit - 1].id) : null
|
|
@@ -2701,18 +3188,18 @@ var SessionRepository = class {
|
|
|
2701
3188
|
function registerSessionRoutes(server, store) {
|
|
2702
3189
|
const sessions = new SessionRepository(store);
|
|
2703
3190
|
const device = (request) => typeof request.headers["x-teammy-device"] === "string" ? request.headers["x-teammy-device"] : void 0;
|
|
2704
|
-
const authorize = (request) => store
|
|
3191
|
+
const authorize = (request) => authorizeDevice(store, request);
|
|
2705
3192
|
const guard = {
|
|
2706
3193
|
onRequest: async (request) => {
|
|
2707
3194
|
authorize(request);
|
|
2708
3195
|
}
|
|
2709
3196
|
};
|
|
2710
|
-
const pagination =
|
|
2711
|
-
after:
|
|
2712
|
-
limit:
|
|
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)
|
|
2713
3200
|
});
|
|
2714
3201
|
server.post(
|
|
2715
|
-
"/
|
|
3202
|
+
"/sessions/commits",
|
|
2716
3203
|
guard,
|
|
2717
3204
|
(request) => sessions.commit(
|
|
2718
3205
|
request.headers.authorization,
|
|
@@ -2720,32 +3207,41 @@ function registerSessionRoutes(server, store) {
|
|
|
2720
3207
|
SessionCommitSchema.parse(request.body)
|
|
2721
3208
|
)
|
|
2722
3209
|
);
|
|
2723
|
-
server.get("/
|
|
2724
|
-
const q = pagination.parse(request.query);
|
|
2725
|
-
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);
|
|
2726
3213
|
});
|
|
2727
|
-
server.get("/
|
|
3214
|
+
server.get("/sessions/stats", async (request) => sessions.stats(authorizeRead(store, request)));
|
|
3215
|
+
server.get("/sessions/search", async (request) => {
|
|
2728
3216
|
const q = pagination.extend({
|
|
2729
|
-
|
|
3217
|
+
owner: NameSchema.optional(),
|
|
3218
|
+
sessionKey: HashSchema.optional(),
|
|
3219
|
+
q: z13.string().min(1).max(500).refine((s) => !s.includes("\0"))
|
|
2730
3220
|
}).parse(request.query);
|
|
2731
|
-
return sessions.search(
|
|
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
|
+
);
|
|
2732
3228
|
});
|
|
2733
|
-
server.get("/
|
|
2734
|
-
const p =
|
|
2735
|
-
const q =
|
|
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);
|
|
2736
3232
|
return sessions.read(authorizeRead(store, request), p.owner, p.key, q.revision);
|
|
2737
3233
|
});
|
|
2738
3234
|
}
|
|
2739
3235
|
|
|
2740
3236
|
// apps/server/file-routes.ts
|
|
2741
|
-
import { z as
|
|
3237
|
+
import { z as z14 } from "zod";
|
|
2742
3238
|
function registerFileRoutes(server, store) {
|
|
2743
3239
|
const files = new FileRepository(store);
|
|
2744
3240
|
const device = (request) => {
|
|
2745
3241
|
const value = request.headers["x-teammy-device"];
|
|
2746
3242
|
return typeof value === "string" ? value : void 0;
|
|
2747
3243
|
};
|
|
2748
|
-
const authorize = (request) => store
|
|
3244
|
+
const authorize = (request) => authorizeDevice(store, request);
|
|
2749
3245
|
const guard = {
|
|
2750
3246
|
onRequest: async (request) => {
|
|
2751
3247
|
authorize(request);
|
|
@@ -2756,21 +3252,21 @@ function registerFileRoutes(server, store) {
|
|
|
2756
3252
|
{ parseAs: "buffer", bodyLimit: MAX_BLOB_BYTES },
|
|
2757
3253
|
(_request, body, done) => done(null, body)
|
|
2758
3254
|
);
|
|
2759
|
-
server.post("/
|
|
3255
|
+
server.post("/blobs/check", guard, async (request) => ({
|
|
2760
3256
|
missing: files.missing(authorize(request), BlobCheckSchema.parse(request.body).hashes)
|
|
2761
3257
|
}));
|
|
2762
|
-
server.put("/
|
|
2763
|
-
const { hash: hash2 } =
|
|
3258
|
+
server.put("/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
|
|
3259
|
+
const { hash: hash2 } = z14.object({ hash: HashSchema }).parse(request.params);
|
|
2764
3260
|
if (!Buffer.isBuffer(request.body))
|
|
2765
3261
|
throw Object.assign(new Error("Expected binary content"), { statusCode: 400 });
|
|
2766
3262
|
return files.upload(request.headers.authorization, device(request), hash2, request.body);
|
|
2767
3263
|
});
|
|
2768
|
-
server.get("/
|
|
2769
|
-
const { hash: hash2 } =
|
|
3264
|
+
server.get("/blobs/:hash", async (request, reply) => {
|
|
3265
|
+
const { hash: hash2 } = z14.object({ hash: HashSchema }).parse(request.params);
|
|
2770
3266
|
return reply.type("application/octet-stream").send(Buffer.from(await files.download(authorizeRead(store, request), hash2)));
|
|
2771
3267
|
});
|
|
2772
3268
|
server.post(
|
|
2773
|
-
"/
|
|
3269
|
+
"/files/commits",
|
|
2774
3270
|
guard,
|
|
2775
3271
|
async (request) => files.commit(
|
|
2776
3272
|
request.headers.authorization,
|
|
@@ -2778,12 +3274,12 @@ function registerFileRoutes(server, store) {
|
|
|
2778
3274
|
FileCommitSchema.parse(request.body)
|
|
2779
3275
|
)
|
|
2780
3276
|
);
|
|
2781
|
-
server.get("/
|
|
2782
|
-
const { owner } =
|
|
2783
|
-
const query =
|
|
2784
|
-
revision:
|
|
2785
|
-
after:
|
|
2786
|
-
limit:
|
|
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)
|
|
2787
3283
|
}).parse(request.query);
|
|
2788
3284
|
return files.manifest(
|
|
2789
3285
|
authorizeRead(store, request),
|
|
@@ -2793,29 +3289,29 @@ function registerFileRoutes(server, store) {
|
|
|
2793
3289
|
query.limit
|
|
2794
3290
|
);
|
|
2795
3291
|
});
|
|
2796
|
-
server.get("/
|
|
2797
|
-
const { owner } =
|
|
2798
|
-
const q =
|
|
3292
|
+
server.get("/files/:owner/entry", async (request) => {
|
|
3293
|
+
const { owner } = z14.object({ owner: NameSchema }).parse(request.params);
|
|
3294
|
+
const q = z14.object({
|
|
2799
3295
|
path: FilePathSchema,
|
|
2800
|
-
revision:
|
|
3296
|
+
revision: z14.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
|
|
2801
3297
|
}).parse(request.query);
|
|
2802
3298
|
return files.entry(authorizeRead(store, request), owner, q.path, q.revision);
|
|
2803
3299
|
});
|
|
2804
|
-
server.get("/
|
|
2805
|
-
const { owner } =
|
|
2806
|
-
const q =
|
|
2807
|
-
path:
|
|
2808
|
-
revision:
|
|
2809
|
-
after:
|
|
2810
|
-
limit:
|
|
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)
|
|
2811
3307
|
}).parse(request.query);
|
|
2812
3308
|
return files.tree(authorizeRead(store, request), owner, q.path, q.revision, q.after, q.limit);
|
|
2813
3309
|
});
|
|
2814
|
-
server.get("/
|
|
2815
|
-
const { owner } =
|
|
2816
|
-
const query =
|
|
2817
|
-
after:
|
|
2818
|
-
limit:
|
|
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)
|
|
2819
3315
|
}).parse(request.query);
|
|
2820
3316
|
return files.changes(authorizeRead(store, request), owner, query.after, query.limit);
|
|
2821
3317
|
});
|
|
@@ -2870,6 +3366,7 @@ function buildServer(options) {
|
|
|
2870
3366
|
"session-revisions",
|
|
2871
3367
|
"session-search",
|
|
2872
3368
|
"project-config",
|
|
3369
|
+
"multi-project",
|
|
2873
3370
|
"web-identity",
|
|
2874
3371
|
"device-status",
|
|
2875
3372
|
"ai-profiles",
|
|
@@ -2878,23 +3375,24 @@ function buildServer(options) {
|
|
|
2878
3375
|
]
|
|
2879
3376
|
}));
|
|
2880
3377
|
if (store) {
|
|
3378
|
+
registerSiteRoutes(server, store);
|
|
2881
3379
|
registerProjectRoutes(server, store);
|
|
2882
3380
|
registerAiRoutes(server, store);
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
async (request) =>
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
);
|
|
2897
|
-
return
|
|
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);
|
|
2898
3396
|
});
|
|
2899
3397
|
}
|
|
2900
3398
|
if (options.webDir) registerWeb(server, options.webDir);
|