sh3-server 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +9 -0
  2. package/app/assets/SH3-BG5NVpSD.png +0 -0
  3. package/app/assets/icons-CnAqUqbR.svg +1126 -0
  4. package/app/assets/index-B1mKDfA-.css +1 -0
  5. package/app/assets/index-C7fUtJvb.js +9 -0
  6. package/app/assets/tauri-backend-B3LR3-lo.js +1 -0
  7. package/app/index.html +13 -0
  8. package/dist/auth.d.ts +34 -0
  9. package/dist/auth.js +107 -0
  10. package/dist/cli.d.ts +9 -0
  11. package/dist/cli.js +30 -0
  12. package/dist/index.d.ts +35 -0
  13. package/dist/index.js +228 -0
  14. package/dist/keys.d.ts +33 -0
  15. package/dist/keys.js +68 -0
  16. package/dist/packages.d.ts +38 -0
  17. package/dist/packages.js +256 -0
  18. package/dist/routes/admin.d.ts +10 -0
  19. package/dist/routes/admin.js +126 -0
  20. package/dist/routes/auth.d.ts +13 -0
  21. package/dist/routes/auth.js +97 -0
  22. package/dist/routes/boot.d.ts +11 -0
  23. package/dist/routes/boot.js +56 -0
  24. package/dist/routes/docs.d.ts +14 -0
  25. package/dist/routes/docs.js +133 -0
  26. package/dist/routes/env-state.d.ts +8 -0
  27. package/dist/routes/env-state.js +62 -0
  28. package/dist/sessions.d.ts +26 -0
  29. package/dist/sessions.js +59 -0
  30. package/dist/settings.d.ts +22 -0
  31. package/dist/settings.js +63 -0
  32. package/dist/shard-router.d.ts +68 -0
  33. package/dist/shard-router.js +145 -0
  34. package/dist/shell-shard/history-store.d.ts +11 -0
  35. package/dist/shell-shard/history-store.js +65 -0
  36. package/dist/shell-shard/index.d.ts +14 -0
  37. package/dist/shell-shard/index.js +51 -0
  38. package/dist/shell-shard/runner.d.ts +22 -0
  39. package/dist/shell-shard/runner.js +84 -0
  40. package/dist/shell-shard/session-manager.d.ts +56 -0
  41. package/dist/shell-shard/session-manager.js +161 -0
  42. package/dist/shell-shard/tokenize.d.ts +1 -0
  43. package/dist/shell-shard/tokenize.js +68 -0
  44. package/dist/shell-shard/ws.d.ts +2 -0
  45. package/dist/shell-shard/ws.js +78 -0
  46. package/dist/users.d.ts +44 -0
  47. package/dist/users.js +113 -0
  48. package/package.json +49 -0
  49. package/static/404.html +53 -0
@@ -0,0 +1,256 @@
1
+ // Unified package discovery - scans data/packages/ for manifest.json files,
2
+ // mounts server routes, and provides install/uninstall management.
3
+ import { Hono } from 'hono';
4
+ import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ /** Validate package ID — alphanumeric, hyphens, underscores, @ for scoped. */
10
+ function isValidId(id) {
11
+ return /^[a-zA-Z0-9_@-]+$/.test(id);
12
+ }
13
+ // ---------------------------------------------------------------------------
14
+ // Boot-time scanner
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Scan `<dataDir>/packages/` for directories containing `manifest.json`.
18
+ * For each valid package, mount server routes (if server.js exists) and
19
+ * return the full list of discovered packages.
20
+ */
21
+ export async function loadPackages(shardRouter, dataDir, keys, settings, wsRegister) {
22
+ const packagesDir = join(dataDir, 'packages');
23
+ if (!existsSync(packagesDir)) {
24
+ mkdirSync(packagesDir, { recursive: true });
25
+ console.log('[sh3] No packages directory — created empty packages/');
26
+ return [];
27
+ }
28
+ const entries = readdirSync(packagesDir, { withFileTypes: true });
29
+ const dirs = entries.filter((e) => e.isDirectory());
30
+ if (dirs.length === 0) {
31
+ console.log('[sh3] No packages found.');
32
+ return [];
33
+ }
34
+ const discovered = [];
35
+ for (const dir of dirs) {
36
+ const pkgDir = join(packagesDir, dir.name);
37
+ const manifestPath = join(pkgDir, 'manifest.json');
38
+ if (!existsSync(manifestPath)) {
39
+ continue;
40
+ }
41
+ try {
42
+ const raw = readFileSync(manifestPath, 'utf-8');
43
+ const manifest = JSON.parse(raw);
44
+ if (typeof manifest.id !== 'string' || !manifest.id) {
45
+ console.warn(`[sh3] ${dir.name}/manifest.json missing "id" — skipping`);
46
+ continue;
47
+ }
48
+ if (typeof manifest.type !== 'string' || !manifest.type) {
49
+ console.warn(`[sh3] ${dir.name}/manifest.json missing "type" — skipping`);
50
+ continue;
51
+ }
52
+ if (typeof manifest.version !== 'string' || !manifest.version) {
53
+ console.warn(`[sh3] ${dir.name}/manifest.json missing "version" — skipping`);
54
+ continue;
55
+ }
56
+ const clientJs = join(pkgDir, 'client.js');
57
+ const serverJs = join(pkgDir, 'server.js');
58
+ const hasClient = existsSync(clientJs);
59
+ const hasServer = existsSync(serverJs);
60
+ const pkg = {
61
+ id: manifest.id,
62
+ type: manifest.type,
63
+ label: manifest.label ?? manifest.id,
64
+ version: manifest.version,
65
+ contractVersion: manifest.contractVersion ?? 0,
66
+ ...(hasClient ? { clientPath: resolve(clientJs) } : {}),
67
+ ...(hasServer ? { serverPath: resolve(serverJs) } : {}),
68
+ };
69
+ if (hasServer) {
70
+ try {
71
+ await shardRouter.mount(manifest.id, serverJs, { pkgDir, keys, settings, wsRegister });
72
+ }
73
+ catch (err) {
74
+ console.warn(`[sh3] ${manifest.id}/server.js failed to load:`, err);
75
+ }
76
+ }
77
+ discovered.push(pkg);
78
+ console.log(`[sh3] ${manifest.id} (${manifest.type}) v${manifest.version}` +
79
+ (hasClient ? ' [client]' : '') +
80
+ (hasServer ? ' [server]' : ''));
81
+ }
82
+ catch (err) {
83
+ console.warn(`[sh3] Failed to load ${dir.name}/manifest.json — skipping:`, err);
84
+ }
85
+ }
86
+ console.log(`[sh3] Discovered ${discovered.length} package(s).`);
87
+ return discovered;
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Lightweight re-scan (no route mounting)
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Re-scan `<dataDir>/packages/` and return metadata for all valid packages.
94
+ * Unlike `loadPackages`, this does NOT mount server routes — it only reads
95
+ * manifests so the listing endpoint always reflects what is on disk.
96
+ */
97
+ export function scanPackages(dataDir) {
98
+ const packagesDir = join(dataDir, 'packages');
99
+ if (!existsSync(packagesDir))
100
+ return [];
101
+ const entries = readdirSync(packagesDir, { withFileTypes: true });
102
+ const result = [];
103
+ for (const entry of entries) {
104
+ if (!entry.isDirectory())
105
+ continue;
106
+ const manifestPath = join(packagesDir, entry.name, 'manifest.json');
107
+ if (!existsSync(manifestPath))
108
+ continue;
109
+ try {
110
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
111
+ if (typeof manifest.id !== 'string' || !manifest.id)
112
+ continue;
113
+ if (typeof manifest.type !== 'string' || !manifest.type)
114
+ continue;
115
+ if (typeof manifest.version !== 'string' || !manifest.version)
116
+ continue;
117
+ const pkgDir = join(packagesDir, entry.name);
118
+ const hasClient = existsSync(join(pkgDir, 'client.js'));
119
+ const hasServer = existsSync(join(pkgDir, 'server.js'));
120
+ result.push({
121
+ id: manifest.id,
122
+ type: manifest.type,
123
+ label: manifest.label ?? manifest.id,
124
+ version: manifest.version,
125
+ contractVersion: manifest.contractVersion ?? 0,
126
+ ...(hasClient ? { clientPath: resolve(join(pkgDir, 'client.js')) } : {}),
127
+ ...(hasServer ? { serverPath: resolve(join(pkgDir, 'server.js')) } : {}),
128
+ });
129
+ }
130
+ catch {
131
+ // Skip malformed manifests
132
+ }
133
+ }
134
+ return result;
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Client bundle serving
138
+ // ---------------------------------------------------------------------------
139
+ /**
140
+ * Register `GET /packages/:id/client.js` to serve client bundles from disk.
141
+ */
142
+ export function servePackageBundles(app, dataDir) {
143
+ app.get('/packages/:id/client.js', (c) => {
144
+ const id = c.req.param('id');
145
+ if (!isValidId(id)) {
146
+ return c.json({ error: 'Invalid package id' }, 400);
147
+ }
148
+ const filePath = join(dataDir, 'packages', id, 'client.js');
149
+ if (!existsSync(filePath)) {
150
+ return c.json({ error: 'Client bundle not found' }, 404);
151
+ }
152
+ const content = readFileSync(filePath, 'utf-8');
153
+ return c.body(content, 200, {
154
+ 'Content-Type': 'application/javascript',
155
+ 'Cache-Control': 'public, max-age=31536000, immutable',
156
+ });
157
+ });
158
+ }
159
+ // ---------------------------------------------------------------------------
160
+ // Package management routes (install / uninstall)
161
+ // ---------------------------------------------------------------------------
162
+ /**
163
+ * Returns a Hono router with POST /install and POST /uninstall.
164
+ * Protected by the blanket `/api/*` auth middleware already applied upstream.
165
+ */
166
+ export function createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister) {
167
+ const router = new Hono();
168
+ router.post('/install', async (c) => {
169
+ const form = await c.req.formData();
170
+ const manifestFile = form.get('manifest');
171
+ if (!(manifestFile instanceof File)) {
172
+ return c.json({ error: 'Missing manifest file' }, 400);
173
+ }
174
+ let manifest;
175
+ try {
176
+ const text = await manifestFile.text();
177
+ manifest = JSON.parse(text);
178
+ }
179
+ catch {
180
+ return c.json({ error: 'Invalid manifest JSON' }, 400);
181
+ }
182
+ const id = manifest.id;
183
+ if (typeof id !== 'string' || !id || !isValidId(id)) {
184
+ return c.json({ error: 'Invalid or missing "id" in manifest' }, 400);
185
+ }
186
+ if (typeof manifest.type !== 'string' || !manifest.type) {
187
+ return c.json({ error: 'Missing "type" in manifest' }, 400);
188
+ }
189
+ if (typeof manifest.version !== 'string' || !manifest.version) {
190
+ return c.json({ error: 'Missing "version" in manifest' }, 400);
191
+ }
192
+ const pkgDir = join(dataDir, 'packages', id);
193
+ mkdirSync(pkgDir, { recursive: true });
194
+ // Write manifest
195
+ writeFileSync(join(pkgDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
196
+ // Write optional client bundle
197
+ const clientFile = form.get('client');
198
+ if (clientFile instanceof File) {
199
+ const buf = Buffer.from(await clientFile.arrayBuffer());
200
+ writeFileSync(join(pkgDir, 'client.js'), buf);
201
+ }
202
+ // Write optional server bundle
203
+ const serverFile = form.get('server');
204
+ if (serverFile instanceof File) {
205
+ const buf = Buffer.from(await serverFile.arrayBuffer());
206
+ writeFileSync(join(pkgDir, 'server.js'), buf);
207
+ // Hot-mount server routes
208
+ try {
209
+ await shardRouter.mount(id, join(pkgDir, 'server.js'), { pkgDir, keys, settings, wsRegister });
210
+ }
211
+ catch (err) {
212
+ // Roll back entire install — broken server bundle must not be half-installed
213
+ rmSync(join(pkgDir, 'manifest.json'), { force: true });
214
+ rmSync(join(pkgDir, 'client.js'), { force: true });
215
+ rmSync(join(pkgDir, 'server.js'), { force: true });
216
+ // Remove directory if empty (no pre-existing data/)
217
+ const dataSubDir = join(pkgDir, 'data');
218
+ if (!existsSync(dataSubDir) || !statSync(dataSubDir).isDirectory()) {
219
+ rmSync(pkgDir, { recursive: true, force: true });
220
+ }
221
+ const message = err instanceof Error ? err.message : String(err);
222
+ return c.json({ error: `Server bundle failed to load: ${message}` }, 422);
223
+ }
224
+ }
225
+ return c.json({ ok: true, id });
226
+ });
227
+ router.post('/uninstall', async (c) => {
228
+ const body = await c.req.json();
229
+ const id = body.id;
230
+ if (typeof id !== 'string' || !id || !isValidId(id)) {
231
+ return c.json({ error: 'Invalid or missing "id"' }, 400);
232
+ }
233
+ const pkgDir = join(dataDir, 'packages', id);
234
+ const manifestPath = join(pkgDir, 'manifest.json');
235
+ if (!existsSync(manifestPath)) {
236
+ return c.json({ error: `Package "${id}" not found` }, 404);
237
+ }
238
+ // Unmount server routes immediately
239
+ shardRouter.unmount(id);
240
+ // Remove code files
241
+ rmSync(manifestPath, { force: true });
242
+ const clientPath = join(pkgDir, 'client.js');
243
+ if (existsSync(clientPath))
244
+ rmSync(clientPath, { force: true });
245
+ const serverPath = join(pkgDir, 'server.js');
246
+ if (existsSync(serverPath))
247
+ rmSync(serverPath, { force: true });
248
+ // If no data/ subdirectory, remove the whole package directory
249
+ const dataSubDir = join(pkgDir, 'data');
250
+ if (!existsSync(dataSubDir) || !statSync(dataSubDir).isDirectory()) {
251
+ rmSync(pkgDir, { recursive: true, force: true });
252
+ }
253
+ return c.json({ ok: true, id });
254
+ });
255
+ return router;
256
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Admin routes — user management, settings, system controls.
3
+ * All routes require admin session or API key (adminAuth middleware).
4
+ */
5
+ import { Hono } from 'hono';
6
+ import type { UserStore } from '../users.js';
7
+ import type { SettingsStore } from '../settings.js';
8
+ import type { SessionStore } from '../sessions.js';
9
+ import type { KeyStore } from '../keys.js';
10
+ export declare function createAdminRouter(users: UserStore, settings: SettingsStore, sessions: SessionStore, keys: KeyStore, dataDir: string): Hono;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Admin routes — user management, settings, system controls.
3
+ * All routes require admin session or API key (adminAuth middleware).
4
+ */
5
+ import { Hono } from 'hono';
6
+ import { execFile } from 'node:child_process';
7
+ import { existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ export function createAdminRouter(users, settings, sessions, keys, dataDir) {
10
+ const router = new Hono();
11
+ // --- Users CRUD ---
12
+ router.get('/users', (c) => {
13
+ return c.json(users.list());
14
+ });
15
+ router.post('/users', async (c) => {
16
+ let body;
17
+ try {
18
+ body = await c.req.json();
19
+ }
20
+ catch {
21
+ return c.json({ error: 'Invalid JSON body' }, 400);
22
+ }
23
+ if (!body.username || !body.password) {
24
+ return c.json({ error: 'Username and password required' }, 400);
25
+ }
26
+ if (body.role && body.role !== 'admin' && body.role !== 'user') {
27
+ return c.json({ error: 'Role must be "admin" or "user"' }, 400);
28
+ }
29
+ try {
30
+ const user = await users.create({
31
+ username: body.username,
32
+ displayName: body.displayName || body.username,
33
+ password: body.password,
34
+ role: body.role || 'user',
35
+ });
36
+ return c.json(user, 201);
37
+ }
38
+ catch (err) {
39
+ return c.json({ error: err instanceof Error ? err.message : 'Failed to create user' }, 409);
40
+ }
41
+ });
42
+ router.put('/users/:id', async (c) => {
43
+ const id = c.req.param('id');
44
+ let body;
45
+ try {
46
+ body = await c.req.json();
47
+ }
48
+ catch {
49
+ return c.json({ error: 'Invalid JSON body' }, 400);
50
+ }
51
+ if (body.role && body.role !== 'admin' && body.role !== 'user') {
52
+ return c.json({ error: 'Role must be "admin" or "user"' }, 400);
53
+ }
54
+ const updated = await users.update(id, body);
55
+ if (!updated)
56
+ return c.json({ error: 'User not found' }, 404);
57
+ return c.json(updated);
58
+ });
59
+ router.delete('/users/:id', (c) => {
60
+ const id = c.req.param('id');
61
+ if (!users.delete(id))
62
+ return c.json({ error: 'User not found' }, 404);
63
+ return c.body(null, 204);
64
+ });
65
+ // --- Settings ---
66
+ router.get('/settings', (c) => {
67
+ return c.json(settings.get());
68
+ });
69
+ router.put('/settings', async (c) => {
70
+ let body;
71
+ try {
72
+ body = await c.req.json();
73
+ }
74
+ catch {
75
+ return c.json({ error: 'Invalid JSON body' }, 400);
76
+ }
77
+ const updated = settings.update(body);
78
+ // Sync session TTL if changed
79
+ if (body.auth?.sessionTTL !== undefined) {
80
+ sessions.setDefaultTTL(body.auth.sessionTTL);
81
+ }
82
+ return c.json(updated);
83
+ });
84
+ // --- System ---
85
+ router.post('/restart', (c) => {
86
+ const scriptPath = join(dataDir, 'restart.sh');
87
+ if (!existsSync(scriptPath)) {
88
+ return c.json({
89
+ error: 'No restart script found',
90
+ hint: `Create a script at ${scriptPath} to enable remote restart`,
91
+ }, 404);
92
+ }
93
+ // Fire and forget — the server will be killed by the script
94
+ execFile('bash', [scriptPath], (err) => {
95
+ if (err)
96
+ console.error('[sh3] Restart script failed:', err.message);
97
+ });
98
+ return c.json({ status: 'restarting' }, 202);
99
+ });
100
+ // --- API Keys ---
101
+ router.get('/keys', (c) => {
102
+ return c.json(keys.listFull());
103
+ });
104
+ router.post('/keys', async (c) => {
105
+ let body;
106
+ try {
107
+ body = await c.req.json();
108
+ }
109
+ catch {
110
+ return c.json({ error: 'Invalid JSON body' }, 400);
111
+ }
112
+ const label = body.label?.trim();
113
+ if (!label) {
114
+ return c.json({ error: 'Label required' }, 400);
115
+ }
116
+ const key = keys.generate(label);
117
+ return c.json(key, 201);
118
+ });
119
+ router.delete('/keys/:id', (c) => {
120
+ const id = c.req.param('id');
121
+ if (!keys.revoke(id))
122
+ return c.json({ error: 'Key not found' }, 404);
123
+ return c.body(null, 204);
124
+ });
125
+ return router;
126
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Auth routes.
3
+ *
4
+ * POST /api/auth/login — authenticate with username + password, set session cookie
5
+ * POST /api/auth/logout — clear session, delete cookie
6
+ * POST /api/auth/verify — validate an API key (for external tools)
7
+ */
8
+ import { Hono } from 'hono';
9
+ import type { KeyStore } from '../keys.js';
10
+ import type { UserStore } from '../users.js';
11
+ import type { SessionStore } from '../sessions.js';
12
+ import type { SettingsStore } from '../settings.js';
13
+ export declare function createAuthRouter(keys: KeyStore, users: UserStore, sessions: SessionStore, settings: SettingsStore): Hono;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Auth routes.
3
+ *
4
+ * POST /api/auth/login — authenticate with username + password, set session cookie
5
+ * POST /api/auth/logout — clear session, delete cookie
6
+ * POST /api/auth/verify — validate an API key (for external tools)
7
+ */
8
+ import { Hono } from 'hono';
9
+ import { setSessionCookie, clearSessionCookie } from '../auth.js';
10
+ export function createAuthRouter(keys, users, sessions, settings) {
11
+ const router = new Hono();
12
+ router.post('/login', async (c) => {
13
+ let body;
14
+ try {
15
+ body = await c.req.json();
16
+ }
17
+ catch {
18
+ return c.json({ error: 'Invalid JSON body' }, 400);
19
+ }
20
+ if (!body.username || !body.password) {
21
+ return c.json({ error: 'Username and password required' }, 400);
22
+ }
23
+ const user = await users.authenticate(body.username, body.password);
24
+ if (!user) {
25
+ return c.json({ error: 'Invalid credentials' }, 401);
26
+ }
27
+ const config = settings.get();
28
+ const session = sessions.create(user.id, user.role, config.auth.sessionTTL);
29
+ const maxAge = config.auth.sessionTTL * 3600;
30
+ setSessionCookie(c, session.token, maxAge);
31
+ return c.json({
32
+ session: {
33
+ token: session.token,
34
+ userId: session.userId,
35
+ role: session.role,
36
+ expiresAt: session.expiresAt,
37
+ },
38
+ user,
39
+ });
40
+ });
41
+ router.post('/logout', async (c) => {
42
+ // Extract session token from cookie
43
+ const cookie = c.req.raw.headers.get('cookie');
44
+ if (cookie) {
45
+ const match = cookie.match(/(?:^|;\s*)sh3_session=([^\s;]+)/);
46
+ if (match) {
47
+ sessions.revoke(match[1]);
48
+ }
49
+ }
50
+ clearSessionCookie(c);
51
+ return c.body(null, 204);
52
+ });
53
+ router.post('/register', async (c) => {
54
+ const config = settings.get();
55
+ if (!config.auth.selfRegistration) {
56
+ return c.json({ error: 'Registration is disabled' }, 403);
57
+ }
58
+ let body;
59
+ try {
60
+ body = await c.req.json();
61
+ }
62
+ catch {
63
+ return c.json({ error: 'Invalid JSON body' }, 400);
64
+ }
65
+ if (!body.username || !body.password) {
66
+ return c.json({ error: 'Username and password required' }, 400);
67
+ }
68
+ try {
69
+ const user = await users.create({
70
+ username: body.username,
71
+ displayName: body.displayName || body.username,
72
+ password: body.password,
73
+ role: 'user',
74
+ });
75
+ const session = sessions.create(user.id, user.role, config.auth.sessionTTL);
76
+ const maxAge = config.auth.sessionTTL * 3600;
77
+ setSessionCookie(c, session.token, maxAge);
78
+ return c.json({ session: { token: session.token, userId: session.userId, role: session.role, expiresAt: session.expiresAt }, user }, 201);
79
+ }
80
+ catch (err) {
81
+ return c.json({ error: err instanceof Error ? err.message : 'Registration failed' }, 409);
82
+ }
83
+ });
84
+ // API key verification — for external tools / CLI
85
+ router.post('/verify', async (c) => {
86
+ const authHeader = c.req.header('Authorization');
87
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
88
+ return c.json({ valid: false }, 401);
89
+ }
90
+ const token = authHeader.slice(7);
91
+ if (keys.validate(token)) {
92
+ return c.json({ valid: true });
93
+ }
94
+ return c.json({ valid: false }, 401);
95
+ });
96
+ return router;
97
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Boot config endpoint — GET /api/boot
3
+ *
4
+ * Returns everything the client needs to decide how to render:
5
+ * auth requirements, current session, user, and tenant ID.
6
+ */
7
+ import { Hono } from 'hono';
8
+ import type { SessionStore } from '../sessions.js';
9
+ import type { UserStore } from '../users.js';
10
+ import type { SettingsStore } from '../settings.js';
11
+ export declare function createBootRouter(sessions: SessionStore, users: UserStore, settings: SettingsStore): Hono;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Boot config endpoint — GET /api/boot
3
+ *
4
+ * Returns everything the client needs to decide how to render:
5
+ * auth requirements, current session, user, and tenant ID.
6
+ */
7
+ import { Hono } from 'hono';
8
+ import { randomUUID } from 'node:crypto';
9
+ export function createBootRouter(sessions, users, settings) {
10
+ const router = new Hono();
11
+ router.get('/', (c) => {
12
+ const config = settings.get();
13
+ // Try to extract session
14
+ let session = null;
15
+ const cookie = c.req.raw.headers.get('cookie');
16
+ if (cookie) {
17
+ const match = cookie.match(/(?:^|;\s*)sh3_session=([^\s;]+)/);
18
+ if (match)
19
+ session = sessions.validate(match[1]);
20
+ }
21
+ if (!session) {
22
+ const auth = c.req.header('Authorization');
23
+ if (auth?.startsWith('Bearer sh3s_')) {
24
+ session = sessions.validate(auth.slice(7));
25
+ }
26
+ }
27
+ const user = session ? users.get(session.userId) : null;
28
+ // Determine tenant ID
29
+ let tenantId;
30
+ if (user) {
31
+ tenantId = user.id;
32
+ }
33
+ else if (!config.auth.required || config.auth.guestAllowed) {
34
+ tenantId = `guest_${randomUUID()}`;
35
+ }
36
+ else {
37
+ tenantId = 'none';
38
+ }
39
+ return c.json({
40
+ auth: {
41
+ required: config.auth.required,
42
+ guestAllowed: config.auth.guestAllowed,
43
+ selfRegistration: config.auth.selfRegistration,
44
+ },
45
+ user,
46
+ session: session ? {
47
+ token: session.token,
48
+ userId: session.userId,
49
+ role: session.role,
50
+ expiresAt: session.expiresAt,
51
+ } : null,
52
+ tenantId,
53
+ });
54
+ });
55
+ return router;
56
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Document backend API routes.
3
+ *
4
+ * Maps the DocumentBackend interface to HTTP endpoints backed by the
5
+ * local filesystem. Files are stored at {dataDir}/docs/{tenant}/{shard}/{path}.
6
+ *
7
+ * GET /api/docs/:tenant/:shard → list
8
+ * GET /api/docs/:tenant/:shard/*path → read
9
+ * HEAD /api/docs/:tenant/:shard/*path → exists
10
+ * PUT /api/docs/:tenant/:shard/*path → write (auth required)
11
+ * DELETE /api/docs/:tenant/:shard/*path → delete (auth required)
12
+ */
13
+ import { Hono } from 'hono';
14
+ export declare function createDocsRouter(dataDir: string): Hono;