sh3-server 0.19.5 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/app/assets/{icons-nOyIoORC.svg → icons-OMmH0JiM.svg} +5 -0
  2. package/app/assets/index-CgB99H18.js +21 -0
  3. package/app/assets/index-CgB99H18.js.map +1 -0
  4. package/app/assets/index-D0Q9JGHo.css +1 -0
  5. package/app/index.html +2 -2
  6. package/dist/auth.d.ts +7 -11
  7. package/dist/auth.js +7 -19
  8. package/dist/cli.js +2 -2
  9. package/dist/doc-store/store.d.ts +6 -0
  10. package/dist/doc-store/store.js +108 -1
  11. package/dist/index.d.ts +5 -2
  12. package/dist/index.js +21 -12
  13. package/dist/middleware/project-allowlist.d.ts +4 -0
  14. package/dist/middleware/project-allowlist.js +26 -12
  15. package/dist/mounts/resolver.d.ts +21 -0
  16. package/dist/mounts/resolver.js +41 -0
  17. package/dist/mounts/routes.d.ts +4 -0
  18. package/dist/mounts/routes.js +136 -0
  19. package/dist/mounts/store.d.ts +30 -0
  20. package/dist/mounts/store.js +115 -0
  21. package/dist/routes/admin.d.ts +3 -1
  22. package/dist/routes/admin.js +6 -1
  23. package/dist/routes/boot.d.ts +7 -1
  24. package/dist/routes/boot.js +13 -4
  25. package/dist/routes/docs.js +27 -2
  26. package/dist/routes/projects.d.ts +1 -3
  27. package/dist/routes/projects.js +1 -3
  28. package/dist/scope.d.ts +1 -2
  29. package/dist/scope.js +1 -5
  30. package/dist/settings.d.ts +0 -1
  31. package/dist/settings.js +0 -4
  32. package/dist/shard-router.d.ts +1 -1
  33. package/dist/shard-router.js +23 -4
  34. package/dist/tenant-fs/http.d.ts +0 -2
  35. package/dist/tenant-fs/http.js +1 -1
  36. package/dist/tenant-fs/session-required.d.ts +1 -3
  37. package/dist/tenant-fs/session-required.js +1 -4
  38. package/dist/users.d.ts +14 -1
  39. package/dist/users.js +34 -0
  40. package/package.json +1 -1
  41. package/app/assets/index-BUPUvuOA.css +0 -1
  42. package/app/assets/index-CygVqZor.js +0 -21
  43. package/app/assets/index-CygVqZor.js.map +0 -1
package/dist/index.js CHANGED
@@ -35,6 +35,8 @@ import { ShardRouter, adminOnly } from './shard-router.js';
35
35
  import { scopeRequired, requireCallerScope } from './scope.js';
36
36
  import { registerTenantFsRoutes } from './tenant-fs/index.js';
37
37
  import shellShardServer from './shell-shard/index.js';
38
+ import { MountStore } from './mounts/store.js';
39
+ import { MountedPathResolver } from './mounts/resolver.js';
38
40
  export async function createServer(options = {}) {
39
41
  const port = options.port ?? 3000;
40
42
  const dataDir = options.dataDir ?? './data';
@@ -53,9 +55,17 @@ export async function createServer(options = {}) {
53
55
  const users = new UserStore(dataDir);
54
56
  const settings = new SettingsStore(dataDir);
55
57
  const projects = new ProjectStore(dataDir);
56
- // --no-auth: disable auth enforcement (Tauri sidecar / local-owner mode)
57
- if (options.noAuth) {
58
- settings.update({ auth: { required: false, guestAllowed: true } });
58
+ // Mount system
59
+ const mountStore = new MountStore(dataDir);
60
+ const mountResolver = new MountedPathResolver(mountStore);
61
+ // --local: bootstrap the synthetic owner so guards see a real admin session.
62
+ if (options.localMode) {
63
+ users.upsertSynthetic({
64
+ id: 'local',
65
+ username: 'local',
66
+ displayName: 'Local Owner',
67
+ role: 'admin',
68
+ });
59
69
  }
60
70
  const sessions = new SessionStore(settings.get().auth.sessionTTL);
61
71
  const app = new Hono();
@@ -113,16 +123,16 @@ export async function createServer(options = {}) {
113
123
  }
114
124
  app.get('/api/version', (c) => c.json({ version: serverVersion }));
115
125
  // Boot config
116
- app.route('/api/boot', createBootRouter(sessions, users, settings, serverVersion));
126
+ app.route('/api/boot', createBootRouter(sessions, users, settings, serverVersion, options.localMode ?? false));
117
127
  // Auth endpoints (login, logout, register, verify)
118
128
  app.route('/api/auth', createAuthRouter(keys, users, sessions, settings));
119
129
  // --- Session-gated routes ---
120
- app.use('/api/*', sessionAuth(sessions, settings, keys));
130
+ app.use('/api/*', sessionAuth(sessions, keys));
121
131
  app.use('/api/*', resolveCaller(keys, projects));
122
132
  // Document backend API — gated by sessionAuth + resolveCaller (mounted above).
123
133
  // The router itself enforces scopeAccessMatch and the __-prefix reservation.
124
- // Settings is threaded so scopeAccessMatch respects open / no-auth mode.
125
134
  const docStore = createScopedDocStore(dataDir);
135
+ docStore.setMountResolver(mountResolver);
126
136
  app.route('/api/docs', createDocsRouter(docStore, {
127
137
  settings,
128
138
  projectStore: projects,
@@ -183,12 +193,12 @@ export async function createServer(options = {}) {
183
193
  });
184
194
  // --- Admin-gated routes ---
185
195
  // Admin API
186
- const adminRouter = createAdminRouter(users, settings, sessions, keys, dataDir);
187
- app.use('/api/admin/*', adminAuth(sessions, keys, settings));
196
+ const adminRouter = createAdminRouter(users, settings, sessions, keys, dataDir, mountStore, mountResolver);
197
+ app.use('/api/admin/*', adminAuth());
188
198
  app.route('/api/admin', adminRouter);
189
199
  // Package management (install/uninstall) — admin-gated
190
- app.use('/api/packages/install', adminAuth(sessions, keys, settings));
191
- app.use('/api/packages/uninstall', adminAuth(sessions, keys, settings));
200
+ app.use('/api/packages/install', adminAuth());
201
+ app.use('/api/packages/uninstall', adminAuth());
192
202
  const frameworkShardIds = ['__sh3core__', 'shell', 'sh3-store', 'sh3-admin'];
193
203
  app.route('/api/packages', createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister, docStore, frameworkShardIds));
194
204
  // Serve client bundles from discovered packages
@@ -213,7 +223,7 @@ export async function createServer(options = {}) {
213
223
  shardId: 'shell',
214
224
  dataDir: shellDataDir,
215
225
  permissions: [],
216
- adminOnly: adminOnly(keys, settings),
226
+ adminOnly: adminOnly(),
217
227
  scopeRequired,
218
228
  tenantRequired: requireCallerScope,
219
229
  wsRegister,
@@ -225,7 +235,6 @@ export async function createServer(options = {}) {
225
235
  registerTenantFsRoutes(app, {
226
236
  dataDir,
227
237
  rootBase: settings.get().tenants?.rootBase ?? '',
228
- settings,
229
238
  maxReadBytes: 10 * 1024 * 1024,
230
239
  });
231
240
  // Dynamic shard routes (packages). The catch-all comes after static
@@ -5,6 +5,10 @@
5
5
  * only target shards owned by allowlisted apps, plus the framework shards
6
6
  * listed in FRAMEWORK_SHARDS. Personal scopes are not subject to allowlists.
7
7
  *
8
+ * An empty appAllowlist means no restrictions — all apps can write to the
9
+ * project. When at least one app is listed, only that app's required shards
10
+ * (plus framework shards) are permitted.
11
+ *
8
12
  * Reads (GET / HEAD) are unrestricted; the membership check in
9
13
  * scopeAccessMatch already gates access to project data. This middleware
10
14
  * is layered on top to scope-down what apps can persist into the
@@ -5,6 +5,10 @@
5
5
  * only target shards owned by allowlisted apps, plus the framework shards
6
6
  * listed in FRAMEWORK_SHARDS. Personal scopes are not subject to allowlists.
7
7
  *
8
+ * An empty appAllowlist means no restrictions — all apps can write to the
9
+ * project. When at least one app is listed, only that app's required shards
10
+ * (plus framework shards) are permitted.
11
+ *
8
12
  * Reads (GET / HEAD) are unrestricted; the membership check in
9
13
  * scopeAccessMatch already gates access to project data. This middleware
10
14
  * is layered on top to scope-down what apps can persist into the
@@ -32,18 +36,28 @@ export function projectAppAllowlist(opts) {
32
36
  if (FRAMEWORK_SHARDS.includes(shard))
33
37
  return next();
34
38
  const allowed = new Set(FRAMEWORK_SHARDS);
35
- for (const appId of project.appAllowlist) {
36
- const app = opts.appRegistry.get(appId);
37
- if (!app)
38
- continue;
39
- for (const s of app.manifest.requiredShards)
40
- allowed.add(s);
39
+ // Empty allowlist means no restrictions — all apps can write.
40
+ if (project.appAllowlist.length > 0) {
41
+ for (const appId of project.appAllowlist) {
42
+ const app = opts.appRegistry.get(appId);
43
+ if (app) {
44
+ for (const s of app.manifest.requiredShards)
45
+ allowed.add(s);
46
+ }
47
+ else {
48
+ // App not found in the server manifest registry — fall back to
49
+ // allowing the appId itself as a shard id (common convention).
50
+ allowed.add(appId);
51
+ }
52
+ }
53
+ if (!allowed.has(shard)) {
54
+ return c.json({
55
+ error: `Shard "${shard}" is not in the project's app allowlist`,
56
+ project: project.id,
57
+ }, 403);
58
+ }
41
59
  }
42
- if (allowed.has(shard))
43
- return next();
44
- return c.json({
45
- error: `Shard "${shard}" is not in the project's app allowlist`,
46
- project: project.id,
47
- }, 403);
60
+ // Empty allowlist or shard in allowed set → pass.
61
+ return next();
48
62
  };
49
63
  }
@@ -0,0 +1,21 @@
1
+ import type { MountStore, MountRecord } from './store.js';
2
+ export type MountStatus = 'resolved' | 'unresolved' | 'error';
3
+ export type ResolvedPath = {
4
+ kind: 'mount';
5
+ mountId: string;
6
+ realPath: string;
7
+ } | {
8
+ kind: 'native';
9
+ } | {
10
+ kind: 'mount-unresolved';
11
+ mountId: string;
12
+ error: string;
13
+ };
14
+ export declare class MountedPathResolver {
15
+ #private;
16
+ constructor(store: MountStore);
17
+ resolve(tenantId: string, docPath: string): ResolvedPath;
18
+ probe(mountId: string): MountStatus;
19
+ listTenantMountIds(tenantId: string): string[];
20
+ getMount(mountId: string): MountRecord | null;
21
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join, resolve as pathResolve } from 'node:path';
3
+ export class MountedPathResolver {
4
+ #store;
5
+ constructor(store) {
6
+ this.#store = store;
7
+ }
8
+ resolve(tenantId, docPath) {
9
+ const mountMatch = docPath.match(/^mounts\/([^/]+)(?:\/(.*))?$/);
10
+ if (!mountMatch)
11
+ return { kind: 'native' };
12
+ const mountId = mountMatch[1];
13
+ const subPath = mountMatch[2] ?? '';
14
+ const mount = this.#store.get(mountId);
15
+ if (!mount) {
16
+ return { kind: 'mount-unresolved', mountId, error: `Mount "${mountId}" not found` };
17
+ }
18
+ if (!this.#store.isAttached(mountId, tenantId)) {
19
+ return { kind: 'mount-unresolved', mountId, error: `Mount "${mountId}" not attached to tenant "${tenantId}"` };
20
+ }
21
+ const realPath = subPath ? join(mount.path, subPath) : mount.path;
22
+ return { kind: 'mount', mountId, realPath: pathResolve(realPath) };
23
+ }
24
+ probe(mountId) {
25
+ const mount = this.#store.get(mountId);
26
+ if (!mount)
27
+ return 'error';
28
+ try {
29
+ return existsSync(mount.path) ? 'resolved' : 'unresolved';
30
+ }
31
+ catch {
32
+ return 'error';
33
+ }
34
+ }
35
+ listTenantMountIds(tenantId) {
36
+ return this.#store.listTenantAttachments(tenantId).map(a => a.mountId);
37
+ }
38
+ getMount(mountId) {
39
+ return this.#store.get(mountId);
40
+ }
41
+ }
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import type { MountStore } from './store.js';
3
+ import type { MountedPathResolver } from './resolver.js';
4
+ export declare function createMountsRouter(store: MountStore, resolver: MountedPathResolver): Hono;
@@ -0,0 +1,136 @@
1
+ import { Hono } from 'hono';
2
+ import { readdirSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export function createMountsRouter(store, resolver) {
5
+ const router = new Hono();
6
+ // --- Mounts CRUD ---
7
+ router.get('/mounts', (c) => {
8
+ const mounts = store.list().map(m => ({
9
+ ...m,
10
+ status: resolver.probe(m.id),
11
+ attachmentCount: store.listAttachments(m.id).length,
12
+ }));
13
+ return c.json(mounts);
14
+ });
15
+ router.get('/mounts/:id', (c) => {
16
+ const id = c.req.param('id');
17
+ const mount = store.get(id);
18
+ if (!mount)
19
+ return c.json({ error: 'Mount not found' }, 404);
20
+ return c.json({
21
+ ...mount,
22
+ status: resolver.probe(id),
23
+ attachmentCount: store.listAttachments(id).length,
24
+ });
25
+ });
26
+ router.post('/mounts', async (c) => {
27
+ let body;
28
+ try {
29
+ body = await c.req.json();
30
+ }
31
+ catch {
32
+ return c.json({ error: 'Invalid JSON body' }, 400);
33
+ }
34
+ if (!body.id || !body.path) {
35
+ return c.json({ error: 'id and path required' }, 400);
36
+ }
37
+ try {
38
+ const mount = store.create({ id: body.id, label: body.label, path: body.path });
39
+ const status = resolver.probe(mount.id);
40
+ const response = { ...mount, status };
41
+ if (status !== 'resolved') {
42
+ response.warning = `Path "${mount.path}" does not exist on the server. Mount created but will be unavailable until the path exists.`;
43
+ }
44
+ return c.json(response, 201);
45
+ }
46
+ catch (err) {
47
+ return c.json({ error: err instanceof Error ? err.message : 'Failed to create mount' }, 409);
48
+ }
49
+ });
50
+ router.put('/mounts/:id', async (c) => {
51
+ const id = c.req.param('id');
52
+ let body;
53
+ try {
54
+ body = await c.req.json();
55
+ }
56
+ catch {
57
+ return c.json({ error: 'Invalid JSON body' }, 400);
58
+ }
59
+ const updated = store.update(id, body ?? {});
60
+ if (!updated)
61
+ return c.json({ error: 'Mount not found' }, 404);
62
+ return c.json({ ...updated, status: resolver.probe(id) });
63
+ });
64
+ router.delete('/mounts/:id', (c) => {
65
+ const id = c.req.param('id');
66
+ if (!store.delete(id))
67
+ return c.json({ error: 'Mount not found' }, 404);
68
+ return c.body(null, 204);
69
+ });
70
+ // --- Browse mount directory ---
71
+ router.get('/mounts/:id/browse', (c) => {
72
+ const id = c.req.param('id');
73
+ const mount = store.get(id);
74
+ if (!mount)
75
+ return c.json({ error: 'Mount not found' }, 404);
76
+ try {
77
+ const entries = readdirSync(mount.path, { withFileTypes: true });
78
+ const listing = entries.map(e => ({
79
+ name: e.name,
80
+ kind: e.isDirectory() ? 'directory' : 'file',
81
+ ...(e.isFile() ? { size: statSync(join(mount.path, e.name)).size } : {}),
82
+ }));
83
+ return c.json(listing);
84
+ }
85
+ catch {
86
+ return c.json({ error: `Cannot read mount path: ${mount.path}` }, 503);
87
+ }
88
+ });
89
+ // --- Attachments CRUD ---
90
+ router.get('/mount-attachments', (c) => {
91
+ const mountId = c.req.query('mountId');
92
+ const tenantId = c.req.query('tenantId');
93
+ if (mountId)
94
+ return c.json(store.listAttachments(mountId));
95
+ if (tenantId)
96
+ return c.json(store.listTenantAttachments(tenantId));
97
+ return c.json({ error: 'Provide mountId or tenantId query param' }, 400);
98
+ });
99
+ router.post('/mount-attachments', async (c) => {
100
+ let body;
101
+ try {
102
+ body = await c.req.json();
103
+ }
104
+ catch {
105
+ return c.json({ error: 'Invalid JSON body' }, 400);
106
+ }
107
+ if (!body.mountId || !body.tenantId) {
108
+ return c.json({ error: 'mountId and tenantId required' }, 400);
109
+ }
110
+ if (!store.get(body.mountId)) {
111
+ return c.json({ error: `Mount "${body.mountId}" not found` }, 404);
112
+ }
113
+ const att = store.attach(body.mountId, body.tenantId);
114
+ return c.json(att, 201);
115
+ });
116
+ router.delete('/mount-attachments', async (c) => {
117
+ let body;
118
+ try {
119
+ body = await c.req.json();
120
+ }
121
+ catch {
122
+ return c.json({ error: 'Invalid JSON body' }, 400);
123
+ }
124
+ if (!body.mountId || !body.tenantId) {
125
+ return c.json({ error: 'mountId and tenantId required' }, 400);
126
+ }
127
+ store.detach(body.mountId, body.tenantId);
128
+ return c.body(null, 204);
129
+ });
130
+ // --- Per-tenant attachment listing ---
131
+ router.get('/tenants/:id/attachments', (c) => {
132
+ const tenantId = c.req.param('id');
133
+ return c.json(store.listTenantAttachments(tenantId));
134
+ });
135
+ return router;
136
+ }
@@ -0,0 +1,30 @@
1
+ export interface MountRecord {
2
+ id: string;
3
+ label?: string;
4
+ path: string;
5
+ createdAt: string;
6
+ updatedAt: string;
7
+ }
8
+ export interface MountAttachment {
9
+ mountId: string;
10
+ tenantId: string;
11
+ attachedAt: string;
12
+ }
13
+ export declare class MountStore {
14
+ #private;
15
+ constructor(dataDir: string);
16
+ create(input: {
17
+ id: string;
18
+ label?: string;
19
+ path: string;
20
+ }): MountRecord;
21
+ get(id: string): MountRecord | null;
22
+ list(): MountRecord[];
23
+ update(id: string, patch: Partial<Pick<MountRecord, 'label' | 'path'>>): MountRecord | null;
24
+ delete(id: string): boolean;
25
+ attach(mountId: string, tenantId: string): MountAttachment;
26
+ detach(mountId: string, tenantId: string): void;
27
+ listAttachments(mountId: string): MountAttachment[];
28
+ listTenantAttachments(tenantId: string): MountAttachment[];
29
+ isAttached(mountId: string, tenantId: string): boolean;
30
+ }
@@ -0,0 +1,115 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ export class MountStore {
4
+ #mountsPath;
5
+ #attachmentsPath;
6
+ #mounts = [];
7
+ #attachments = [];
8
+ constructor(dataDir) {
9
+ this.#mountsPath = `${dataDir}/mounts.json`;
10
+ this.#attachmentsPath = `${dataDir}/mount-attachments.json`;
11
+ this.#load();
12
+ }
13
+ #load() {
14
+ if (existsSync(this.#mountsPath)) {
15
+ try {
16
+ const data = JSON.parse(readFileSync(this.#mountsPath, 'utf-8'));
17
+ this.#mounts = data.mounts ?? [];
18
+ }
19
+ catch {
20
+ this.#mounts = [];
21
+ }
22
+ }
23
+ if (existsSync(this.#attachmentsPath)) {
24
+ try {
25
+ const data = JSON.parse(readFileSync(this.#attachmentsPath, 'utf-8'));
26
+ this.#attachments = data.attachments ?? [];
27
+ }
28
+ catch {
29
+ this.#attachments = [];
30
+ }
31
+ }
32
+ }
33
+ #saveMounts() {
34
+ mkdirSync(dirname(this.#mountsPath), { recursive: true });
35
+ writeFileSync(this.#mountsPath, JSON.stringify({ mounts: this.#mounts }, null, 2));
36
+ }
37
+ #saveAttachments() {
38
+ mkdirSync(dirname(this.#attachmentsPath), { recursive: true });
39
+ writeFileSync(this.#attachmentsPath, JSON.stringify({ attachments: this.#attachments }, null, 2));
40
+ }
41
+ create(input) {
42
+ if (this.#mounts.some(m => m.id === input.id)) {
43
+ throw new Error(`Mount "${input.id}" already exists`);
44
+ }
45
+ const now = new Date().toISOString();
46
+ const record = {
47
+ id: input.id,
48
+ label: input.label,
49
+ path: input.path,
50
+ createdAt: now,
51
+ updatedAt: now,
52
+ };
53
+ this.#mounts.push(record);
54
+ this.#saveMounts();
55
+ return { ...record };
56
+ }
57
+ get(id) {
58
+ return this.#mounts.find(m => m.id === id) ?? null;
59
+ }
60
+ list() {
61
+ return [...this.#mounts];
62
+ }
63
+ update(id, patch) {
64
+ const mount = this.#mounts.find(m => m.id === id);
65
+ if (!mount)
66
+ return null;
67
+ if (patch.label !== undefined)
68
+ mount.label = patch.label;
69
+ if (patch.path !== undefined)
70
+ mount.path = patch.path;
71
+ mount.updatedAt = new Date().toISOString();
72
+ this.#saveMounts();
73
+ return { ...mount };
74
+ }
75
+ delete(id) {
76
+ const before = this.#mounts.length;
77
+ this.#mounts = this.#mounts.filter(m => m.id !== id);
78
+ if (this.#mounts.length < before) {
79
+ this.#attachments = this.#attachments.filter(a => a.mountId !== id);
80
+ this.#saveMounts();
81
+ this.#saveAttachments();
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ attach(mountId, tenantId) {
87
+ const existing = this.#attachments.find(a => a.mountId === mountId && a.tenantId === tenantId);
88
+ if (existing)
89
+ return { ...existing };
90
+ const att = {
91
+ mountId,
92
+ tenantId,
93
+ attachedAt: new Date().toISOString(),
94
+ };
95
+ this.#attachments.push(att);
96
+ this.#saveAttachments();
97
+ return { ...att };
98
+ }
99
+ detach(mountId, tenantId) {
100
+ const before = this.#attachments.length;
101
+ this.#attachments = this.#attachments.filter(a => !(a.mountId === mountId && a.tenantId === tenantId));
102
+ if (this.#attachments.length < before) {
103
+ this.#saveAttachments();
104
+ }
105
+ }
106
+ listAttachments(mountId) {
107
+ return this.#attachments.filter(a => a.mountId === mountId);
108
+ }
109
+ listTenantAttachments(tenantId) {
110
+ return this.#attachments.filter(a => a.tenantId === tenantId);
111
+ }
112
+ isAttached(mountId, tenantId) {
113
+ return this.#attachments.some(a => a.mountId === mountId && a.tenantId === tenantId);
114
+ }
115
+ }
@@ -7,4 +7,6 @@ import type { UserStore } from '../users.js';
7
7
  import type { SettingsStore } from '../settings.js';
8
8
  import type { SessionStore } from '../sessions.js';
9
9
  import type { KeyStore } from '../keys.js';
10
- export declare function createAdminRouter(users: UserStore, settings: SettingsStore, sessions: SessionStore, keys: KeyStore, dataDir: string): Hono;
10
+ import type { MountStore } from '../mounts/store.js';
11
+ import type { MountedPathResolver } from '../mounts/resolver.js';
12
+ export declare function createAdminRouter(users: UserStore, settings: SettingsStore, sessions: SessionStore, keys: KeyStore, dataDir: string, mountStore?: MountStore, mountResolver?: MountedPathResolver): Hono;
@@ -6,7 +6,8 @@ import { Hono } from 'hono';
6
6
  import { execFile } from 'node:child_process';
7
7
  import { existsSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
- export function createAdminRouter(users, settings, sessions, keys, dataDir) {
9
+ import { createMountsRouter } from '../mounts/routes.js';
10
+ export function createAdminRouter(users, settings, sessions, keys, dataDir, mountStore, mountResolver) {
10
11
  const router = new Hono();
11
12
  // --- Users CRUD ---
12
13
  router.get('/users', (c) => {
@@ -126,5 +127,9 @@ export function createAdminRouter(users, settings, sessions, keys, dataDir) {
126
127
  return c.json({ error: 'Key not found' }, 404);
127
128
  return c.body(null, 204);
128
129
  });
130
+ // Mount management routes (if stores provided)
131
+ if (mountStore && mountResolver) {
132
+ router.route('/', createMountsRouter(mountStore, mountResolver));
133
+ }
129
134
  return router;
130
135
  }
@@ -3,9 +3,15 @@
3
3
  *
4
4
  * Returns everything the client needs to decide how to render:
5
5
  * auth requirements, current session, user, and tenant ID.
6
+ *
7
+ * When localMode is true (Tauri sidecar), the route ensures a session
8
+ * for the local owner: if no valid session is on the request, a new
9
+ * one is minted against the SessionStore and returned in the response
10
+ * body. The client then carries it as Authorization: Bearer on
11
+ * subsequent calls.
6
12
  */
7
13
  import { Hono } from 'hono';
8
14
  import type { SessionStore } from '../sessions.js';
9
15
  import type { UserStore } from '../users.js';
10
16
  import type { SettingsStore } from '../settings.js';
11
- export declare function createBootRouter(sessions: SessionStore, users: UserStore, settings: SettingsStore, version: string): Hono;
17
+ export declare function createBootRouter(sessions: SessionStore, users: UserStore, settings: SettingsStore, version: string, localMode?: boolean): Hono;
@@ -3,14 +3,20 @@
3
3
  *
4
4
  * Returns everything the client needs to decide how to render:
5
5
  * auth requirements, current session, user, and tenant ID.
6
+ *
7
+ * When localMode is true (Tauri sidecar), the route ensures a session
8
+ * for the local owner: if no valid session is on the request, a new
9
+ * one is minted against the SessionStore and returned in the response
10
+ * body. The client then carries it as Authorization: Bearer on
11
+ * subsequent calls.
6
12
  */
7
13
  import { Hono } from 'hono';
8
14
  import { randomUUID } from 'node:crypto';
9
- export function createBootRouter(sessions, users, settings, version) {
15
+ export function createBootRouter(sessions, users, settings, version, localMode = false) {
10
16
  const router = new Hono();
11
17
  router.get('/', (c) => {
12
18
  const config = settings.get();
13
- // Try to extract session
19
+ // Try to extract session from cookie or Authorization header
14
20
  let session = null;
15
21
  const cookie = c.req.raw.headers.get('cookie');
16
22
  if (cookie) {
@@ -24,13 +30,17 @@ export function createBootRouter(sessions, users, settings, version) {
24
30
  session = sessions.validate(auth.slice(7));
25
31
  }
26
32
  }
33
+ // localMode: ensure a session for the local owner.
34
+ if (localMode && !session) {
35
+ session = sessions.create('local', 'admin');
36
+ }
27
37
  const user = session ? users.get(session.userId) : null;
28
38
  // Determine tenant ID
29
39
  let tenantId;
30
40
  if (user) {
31
41
  tenantId = user.id;
32
42
  }
33
- else if (!config.auth.required || config.auth.guestAllowed) {
43
+ else if (config.auth.guestAllowed) {
34
44
  tenantId = `guest_${randomUUID()}`;
35
45
  }
36
46
  else {
@@ -38,7 +48,6 @@ export function createBootRouter(sessions, users, settings, version) {
38
48
  }
39
49
  return c.json({
40
50
  auth: {
41
- required: config.auth.required,
42
51
  guestAllowed: config.auth.guestAllowed,
43
52
  selfRegistration: config.auth.selfRegistration,
44
53
  },
@@ -24,8 +24,8 @@ export function createDocsRouter(store, options = {}) {
24
24
  ? { settings: options }
25
25
  : options;
26
26
  const router = new Hono();
27
- router.use('/:scope/*', scopeAccessMatch('scope', opts.settings));
28
- router.use('/:scope', scopeAccessMatch('scope', opts.settings));
27
+ router.use('/:scope/*', scopeAccessMatch('scope'));
28
+ router.use('/:scope', scopeAccessMatch('scope'));
29
29
  if (opts.projectStore && opts.appRegistry) {
30
30
  router.use('/:scope/:shard/*', projectAppAllowlist({ projectStore: opts.projectStore, appRegistry: opts.appRegistry }));
31
31
  }
@@ -157,6 +157,31 @@ export function createDocsRouter(store, options = {}) {
157
157
  const meta = await store.readMeta(scope, shard, body.to);
158
158
  return c.json({ ok: true, version: meta?.version });
159
159
  }
160
+ if (rawPath.endsWith('/transfer')) {
161
+ const filePath = rawPath.replace(/\/transfer$/, '');
162
+ if (!filePath)
163
+ return c.json({ error: 'Missing file path' }, 400);
164
+ const body = await c.req.json().catch(() => null);
165
+ if (!body || typeof body.targetScope !== 'string' || body.targetScope.length === 0) {
166
+ return c.json({ error: 'Body must include { targetScope: string }' }, 400);
167
+ }
168
+ if (body.targetScope === scope) {
169
+ return c.json({ error: 'targetScope must differ from source scope' }, 400);
170
+ }
171
+ const caller = c.get('caller');
172
+ if (caller && !caller.accessibleScopes.includes(body.targetScope)) {
173
+ return c.json({ error: `Caller is not a member of scope "${body.targetScope}"` }, 403);
174
+ }
175
+ const targetShard = body.targetShardId ?? shard;
176
+ const content = await store.read(scope, shard, filePath);
177
+ if (content === null)
178
+ return c.json({ error: 'Source document not found' }, 404);
179
+ await store.write(body.targetScope, targetShard, filePath, content);
180
+ if (body.delete) {
181
+ await store.delete(scope, shard, filePath);
182
+ }
183
+ return c.json({ ok: true, [body.delete ? 'deleted' : 'copied']: true });
184
+ }
160
185
  return c.json({ error: 'Unknown docs POST endpoint' }, 404);
161
186
  });
162
187
  // Write
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Projects HTTP API.
2
+ * Projects routes — admin-only management + member-visible listing.
3
3
  *
4
4
  * - GET / — list projects the caller is a member of
5
- * - GET /all — admin: list every project
6
- * - GET /:id — fetch one (member or admin)
7
5
  * - POST / — admin: create a project
8
6
  * - PATCH /:id — admin: mutate a project
9
7
  * - DELETE /:id — admin: delete a project
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Projects HTTP API.
2
+ * Projects routes — admin-only management + member-visible listing.
3
3
  *
4
4
  * - GET / — list projects the caller is a member of
5
- * - GET /all — admin: list every project
6
- * - GET /:id — fetch one (member or admin)
7
5
  * - POST / — admin: create a project
8
6
  * - PATCH /:id — admin: mutate a project
9
7
  * - DELETE /:id — admin: delete a project