sh3-server 0.8.1 → 0.9.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 (48) hide show
  1. package/app/assets/index-DKuJNK2S.js +17 -0
  2. package/app/assets/index-DKuJNK2S.js.map +1 -0
  3. package/app/assets/index-DkC3EpjJ.css +1 -0
  4. package/app/index.html +2 -2
  5. package/dist/auth.d.ts +10 -3
  6. package/dist/auth.js +14 -22
  7. package/dist/caller.d.ts +17 -0
  8. package/dist/caller.js +55 -0
  9. package/dist/doc-store/conflicts.d.ts +19 -0
  10. package/dist/doc-store/conflicts.js +79 -0
  11. package/dist/doc-store/index.d.ts +11 -0
  12. package/dist/doc-store/index.js +22 -0
  13. package/dist/doc-store/meta.d.ts +11 -0
  14. package/dist/doc-store/meta.js +37 -0
  15. package/dist/doc-store/policy.d.ts +15 -0
  16. package/dist/doc-store/policy.js +85 -0
  17. package/dist/doc-store/reserved.d.ts +7 -0
  18. package/dist/doc-store/reserved.js +26 -0
  19. package/dist/doc-store/roles.d.ts +12 -0
  20. package/dist/doc-store/roles.js +15 -0
  21. package/dist/doc-store/store.d.ts +71 -0
  22. package/dist/doc-store/store.js +336 -0
  23. package/dist/doc-store/tick.d.ts +13 -0
  24. package/dist/doc-store/tick.js +52 -0
  25. package/dist/fs-backend.d.ts +10 -0
  26. package/dist/fs-backend.js +105 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +32 -5
  29. package/dist/keys.d.ts +35 -19
  30. package/dist/keys.js +187 -49
  31. package/dist/migrations/sync-grants.d.ts +7 -0
  32. package/dist/migrations/sync-grants.js +27 -0
  33. package/dist/packages.d.ts +3 -2
  34. package/dist/packages.js +5 -5
  35. package/dist/routes/admin.js +7 -3
  36. package/dist/routes/docs.d.ts +11 -7
  37. package/dist/routes/docs.js +88 -122
  38. package/dist/routes/keys.d.ts +21 -0
  39. package/dist/routes/keys.js +166 -0
  40. package/dist/scope.d.ts +11 -0
  41. package/dist/scope.js +45 -0
  42. package/dist/shard-router.d.ts +10 -4
  43. package/dist/shard-router.js +130 -49
  44. package/dist/shell-shard/index.d.ts +4 -1
  45. package/package.json +1 -1
  46. package/app/assets/index-C3rCTpjL.js +0 -17
  47. package/app/assets/index-C3rCTpjL.js.map +0 -1
  48. package/app/assets/index-GfhVhkjD.css +0 -1
package/dist/index.js CHANGED
@@ -19,13 +19,18 @@ import { UserStore } from './users.js';
19
19
  import { SessionStore } from './sessions.js';
20
20
  import { SettingsStore } from './settings.js';
21
21
  import { sessionAuth, adminAuth } from './auth.js';
22
+ import { resolveCaller } from './caller.js';
22
23
  import { createAuthRouter } from './routes/auth.js';
23
24
  import { createBootRouter } from './routes/boot.js';
24
25
  import { createAdminRouter } from './routes/admin.js';
25
26
  import { createDocsRouter } from './routes/docs.js';
27
+ import { createTenantDocStore } from './doc-store/index.js';
26
28
  import { createEnvStateRouter } from './routes/env-state.js';
29
+ import { createKeysRouter } from './routes/keys.js';
27
30
  import { loadPackages, scanPackages, servePackageBundles, createPackageManagementRoutes } from './packages.js';
31
+ import { removeLegacyGrants } from './migrations/sync-grants.js';
28
32
  import { ShardRouter, adminOnly } from './shard-router.js';
33
+ import { scopeRequired, tenantRequired } from './scope.js';
29
34
  import { registerTenantFsRoutes } from './tenant-fs/index.js';
30
35
  import shellShardServer from './shell-shard/index.js';
31
36
  export async function createServer(options = {}) {
@@ -109,12 +114,21 @@ export async function createServer(options = {}) {
109
114
  app.route('/api/boot', createBootRouter(sessions, users, settings));
110
115
  // Auth endpoints (login, logout, register, verify)
111
116
  app.route('/api/auth', createAuthRouter(keys, users, sessions, settings));
112
- // Document backend API
113
- app.route('/api/docs', createDocsRouter(dataDir));
114
117
  // --- Session-gated routes ---
115
118
  app.use('/api/*', sessionAuth(sessions, settings));
119
+ app.use('/api/*', resolveCaller(keys));
120
+ // Document backend API — gated by sessionAuth + resolveCaller (mounted above).
121
+ // The router itself enforces tenantParamMatch and the __-prefix reservation.
122
+ // Settings is threaded so tenantParamMatch respects open / no-auth mode.
123
+ const docStore = createTenantDocStore(dataDir);
124
+ app.route('/api/docs', createDocsRouter(docStore, settings));
116
125
  // Environment state API (per-shard server-backed config)
117
126
  app.route('/api/env-state', createEnvStateRouter(dataDir));
127
+ // User-tenant key management (list/revoke). Mint lives at /api/shards-keys.
128
+ app.route('/api/keys', createKeysRouter(keys, async (event) => {
129
+ // Broadcast will be wired in Task 10 once the shell has a consumer.
130
+ console.log(`[sh3] key revoked: tenant=${event.tenantId} id=${event.id}`);
131
+ }));
118
132
  // Package listing (public read, writes need admin — handled by admin middleware on management routes)
119
133
  app.get('/api/packages', (c) => {
120
134
  const packages = scanPackages(dataDir);
@@ -168,7 +182,7 @@ export async function createServer(options = {}) {
168
182
  app.use('/api/packages/install', adminAuth(sessions, keys, settings));
169
183
  app.use('/api/packages/uninstall', adminAuth(sessions, keys, settings));
170
184
  const frameworkShardIds = ['__sh3core__', 'shell', 'sh3-store', 'sh3-admin'];
171
- app.route('/api/packages', createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister, frameworkShardIds));
185
+ app.route('/api/packages', createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister, docStore, frameworkShardIds));
172
186
  // Serve client bundles from discovered packages
173
187
  servePackageBundles(app, dataDir, settings);
174
188
  // Framework built-in: shell-shard server routes.
@@ -190,7 +204,10 @@ export async function createServer(options = {}) {
190
204
  await shellShardServer.routes(shellSubApp, {
191
205
  shardId: 'shell',
192
206
  dataDir: shellDataDir,
207
+ permissions: [],
193
208
  adminOnly: adminOnly(keys, settings),
209
+ scopeRequired,
210
+ tenantRequired,
194
211
  wsRegister,
195
212
  });
196
213
  app.route('/api/shell', shellSubApp);
@@ -215,10 +232,12 @@ export async function createServer(options = {}) {
215
232
  users,
216
233
  sessions,
217
234
  settings,
235
+ docStore,
218
236
  async start() {
237
+ removeLegacyGrants(dataDir);
219
238
  // First-boot: generate admin key + admin user
220
239
  if (keys.isEmpty()) {
221
- const initial = keys.generate('Initial admin key');
240
+ const initial = keys.generate({ label: 'Initial admin key', tenantId: null, ownerUserId: null, mintedByShardId: null, scopes: ['admin:*'] });
222
241
  const tempPassword = UserStore.generatePassword();
223
242
  await users.create({
224
243
  username: 'admin',
@@ -246,7 +265,7 @@ export async function createServer(options = {}) {
246
265
  console.log(`[sh3] Seeded official registry: ${registryUrl}`);
247
266
  }
248
267
  }
249
- await loadPackages(shardRouter, dataDir, keys, settings, wsRegister);
268
+ await loadPackages(shardRouter, dataDir, keys, settings, wsRegister, docStore);
250
269
  // Periodic session cleanup (every 15 minutes)
251
270
  setInterval(() => sessions.cleanup(), 15 * 60 * 1000);
252
271
  // API 404
@@ -269,6 +288,14 @@ export async function createServer(options = {}) {
269
288
  console.log(`sh3-server listening on http://localhost:${port}`);
270
289
  });
271
290
  injectWebSocket(server);
291
+ const shutdown = async (sig) => {
292
+ console.log(`[sh3] received ${sig}, tearing down shards...`);
293
+ await shardRouter.unmountAll();
294
+ server.close();
295
+ process.exit(0);
296
+ };
297
+ process.on('SIGINT', () => { void shutdown('SIGINT'); });
298
+ process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
272
299
  },
273
300
  };
274
301
  }
package/dist/keys.d.ts CHANGED
@@ -1,33 +1,49 @@
1
1
  /**
2
- * API key managementgenerate, validate, list, revoke.
2
+ * Unified API key storeadmin, user-tenant, and connector-bound keys.
3
3
  *
4
- * Keys are stored in {dataDir}/keys.json. Each key has an id
5
- * (short identifier for display/revocation), the full key value (used
6
- * for auth), a label, and creation timestamp.
4
+ * Layout on disk:
5
+ * <dataDir>/admin-keys.json — tenantId: null rows only
6
+ * <dataDir>/users/<tenantId>/__system__/keys.json user-tenant rows
7
+ *
8
+ * Legacy migration: a pre-existing <dataDir>/keys.json is treated as admin
9
+ * keys and moved to admin-keys.json on first load, with the original renamed
10
+ * to keys.json.legacy.
7
11
  */
8
12
  export interface ApiKey {
9
- /** Short id for display and revocation. */
10
13
  id: string;
11
- /** The full bearer token value. */
12
14
  key: string;
13
- /** Human-readable label. */
14
15
  label: string;
15
- /** ISO timestamp of creation. */
16
+ tenantId: string | null;
17
+ ownerUserId: string | null;
18
+ mintedByShardId: string | null;
19
+ scopes: string[];
20
+ peerRole?: 'primary' | 'replica';
21
+ peerId?: string;
16
22
  createdAt: string;
23
+ expiresAt?: string;
24
+ }
25
+ export type ApiKeyPublic = Omit<ApiKey, 'key'>;
26
+ export interface GenerateInput {
27
+ label: string;
28
+ tenantId: string | null;
29
+ ownerUserId: string | null;
30
+ mintedByShardId: string | null;
31
+ scopes: string[];
32
+ peerRole?: 'primary' | 'replica';
33
+ peerId?: string;
34
+ expiresAt?: string;
17
35
  }
18
36
  export declare class KeyStore {
19
37
  #private;
20
38
  constructor(dataDir: string);
21
- /** Validate a bearer token. Returns true if the key exists. */
22
- validate(token: string): boolean;
23
- /** List all keys (returns id, label, createdAt — never the full key). */
24
- list(): Omit<ApiKey, 'key'>[];
25
- /** List all keys including full key values (admin only). */
26
- listFull(): ApiKey[];
27
- /** Generate a new API key. Returns the full key (only shown once). */
28
- generate(label: string): ApiKey;
29
- /** Revoke a key by id. Returns true if found and removed. */
30
- revoke(id: string): boolean;
31
- /** True if no keys exist (first boot). */
39
+ generate(input: GenerateInput): ApiKey;
40
+ resolve(token: string): ApiKey | null;
41
+ listForTenant(tenantId: string): ApiKeyPublic[];
42
+ listForShard(tenantId: string, shardId: string): ApiKeyPublic[];
43
+ listAdmin(): ApiKeyPublic[];
44
+ listAll(): ApiKeyPublic[];
45
+ revoke(tenantId: string | null, id: string): ApiKeyPublic | null;
32
46
  isEmpty(): boolean;
47
+ /** @deprecated use resolve() — kept until all callers migrate. */
48
+ validate(token: string): boolean;
33
49
  }
package/dist/keys.js CHANGED
@@ -1,68 +1,206 @@
1
1
  /**
2
- * API key managementgenerate, validate, list, revoke.
2
+ * Unified API key storeadmin, user-tenant, and connector-bound keys.
3
3
  *
4
- * Keys are stored in {dataDir}/keys.json. Each key has an id
5
- * (short identifier for display/revocation), the full key value (used
6
- * for auth), a label, and creation timestamp.
4
+ * Layout on disk:
5
+ * <dataDir>/admin-keys.json — tenantId: null rows only
6
+ * <dataDir>/users/<tenantId>/__system__/keys.json user-tenant rows
7
+ *
8
+ * Legacy migration: a pre-existing <dataDir>/keys.json is treated as admin
9
+ * keys and moved to admin-keys.json on first load, with the original renamed
10
+ * to keys.json.legacy.
7
11
  */
8
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
9
- import { join, dirname } from 'node:path';
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, renameSync, } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
10
14
  import { randomBytes } from 'node:crypto';
15
+ function strip(k) {
16
+ const { key: _drop, ...rest } = k;
17
+ return rest;
18
+ }
11
19
  export class KeyStore {
12
- #path;
13
- #keys = [];
20
+ #dataDir;
21
+ #admin = [];
22
+ #byTenant = new Map();
14
23
  constructor(dataDir) {
15
- this.#path = join(dataDir, 'keys.json');
16
- this.#load();
24
+ this.#dataDir = dataDir;
25
+ this.#migrateLegacy();
26
+ this.#loadAdmin();
27
+ this.#loadAllTenants();
28
+ }
29
+ // ---------- public API ----------
30
+ generate(input) {
31
+ const id = randomBytes(4).toString('hex');
32
+ const key = `sh3_${randomBytes(32).toString('hex')}`;
33
+ const row = {
34
+ id,
35
+ key,
36
+ label: input.label,
37
+ tenantId: input.tenantId,
38
+ ownerUserId: input.ownerUserId,
39
+ mintedByShardId: input.mintedByShardId,
40
+ scopes: [...input.scopes],
41
+ peerRole: input.peerRole,
42
+ peerId: input.peerId,
43
+ createdAt: new Date().toISOString(),
44
+ expiresAt: input.expiresAt,
45
+ };
46
+ if (row.tenantId === null) {
47
+ this.#admin.push(row);
48
+ this.#saveAdmin();
49
+ }
50
+ else {
51
+ const bucket = this.#byTenant.get(row.tenantId) ?? [];
52
+ bucket.push(row);
53
+ this.#byTenant.set(row.tenantId, bucket);
54
+ this.#saveTenant(row.tenantId);
55
+ }
56
+ return { ...row, scopes: [...row.scopes] };
57
+ }
58
+ resolve(token) {
59
+ const now = Date.now();
60
+ const match = (row) => {
61
+ if (row.key !== token)
62
+ return false;
63
+ if (row.expiresAt && Date.parse(row.expiresAt) <= now)
64
+ return false;
65
+ return true;
66
+ };
67
+ const adminHit = this.#admin.find(match);
68
+ if (adminHit)
69
+ return adminHit;
70
+ for (const bucket of this.#byTenant.values()) {
71
+ const hit = bucket.find(match);
72
+ if (hit)
73
+ return hit;
74
+ }
75
+ return null;
76
+ }
77
+ listForTenant(tenantId) {
78
+ return (this.#byTenant.get(tenantId) ?? []).map(strip);
79
+ }
80
+ listForShard(tenantId, shardId) {
81
+ return this.listForTenant(tenantId).filter((k) => k.mintedByShardId === shardId);
82
+ }
83
+ listAdmin() {
84
+ return this.#admin.map(strip);
85
+ }
86
+ listAll() {
87
+ const out = this.#admin.map(strip);
88
+ for (const bucket of this.#byTenant.values())
89
+ out.push(...bucket.map(strip));
90
+ return out;
91
+ }
92
+ revoke(tenantId, id) {
93
+ if (tenantId === null) {
94
+ const idx = this.#admin.findIndex((k) => k.id === id);
95
+ if (idx < 0)
96
+ return null;
97
+ const [removed] = this.#admin.splice(idx, 1);
98
+ this.#saveAdmin();
99
+ return strip(removed);
100
+ }
101
+ const bucket = this.#byTenant.get(tenantId);
102
+ if (!bucket)
103
+ return null;
104
+ const idx = bucket.findIndex((k) => k.id === id);
105
+ if (idx < 0)
106
+ return null;
107
+ const [removed] = bucket.splice(idx, 1);
108
+ this.#saveTenant(tenantId);
109
+ return strip(removed);
17
110
  }
18
- #load() {
19
- if (existsSync(this.#path)) {
111
+ isEmpty() {
112
+ return this.#admin.length === 0;
113
+ }
114
+ // ---------- persistence ----------
115
+ #adminPath() {
116
+ return join(this.#dataDir, 'admin-keys.json');
117
+ }
118
+ #tenantPath(tenantId) {
119
+ return join(this.#dataDir, 'users', tenantId, '__system__', 'keys.json');
120
+ }
121
+ #loadAdmin() {
122
+ const p = this.#adminPath();
123
+ if (!existsSync(p))
124
+ return;
125
+ try {
126
+ const raw = JSON.parse(readFileSync(p, 'utf-8'));
127
+ const dirty = raw.some((row) => 'connectorId' in row);
128
+ const cleaned = raw.map((row) => {
129
+ const { connectorId: _drop, ...rest } = row;
130
+ return rest;
131
+ });
132
+ this.#admin.push(...cleaned);
133
+ if (dirty)
134
+ this.#saveAdmin();
135
+ }
136
+ catch {
137
+ // Corrupt admin file — leave empty; the next generate() overwrites.
138
+ }
139
+ }
140
+ #loadAllTenants() {
141
+ const usersDir = join(this.#dataDir, 'users');
142
+ if (!existsSync(usersDir))
143
+ return;
144
+ for (const entry of readdirSync(usersDir, { withFileTypes: true })) {
145
+ if (!entry.isDirectory())
146
+ continue;
147
+ const tenantId = entry.name;
148
+ const file = this.#tenantPath(tenantId);
149
+ if (!existsSync(file))
150
+ continue;
20
151
  try {
21
- this.#keys = JSON.parse(readFileSync(this.#path, 'utf-8'));
152
+ const raw = JSON.parse(readFileSync(file, 'utf-8'));
153
+ const dirty = raw.some((row) => 'connectorId' in row);
154
+ const cleaned = raw.map((row) => {
155
+ const { connectorId: _drop, ...rest } = row;
156
+ return rest;
157
+ });
158
+ this.#byTenant.set(tenantId, cleaned);
159
+ if (dirty)
160
+ this.#saveTenant(tenantId);
22
161
  }
23
162
  catch {
24
- this.#keys = [];
163
+ this.#byTenant.set(tenantId, []);
25
164
  }
26
165
  }
27
166
  }
28
- #save() {
29
- const dir = dirname(this.#path);
30
- mkdirSync(dir, { recursive: true });
31
- writeFileSync(this.#path, JSON.stringify(this.#keys, null, 2));
32
- }
33
- /** Validate a bearer token. Returns true if the key exists. */
34
- validate(token) {
35
- return this.#keys.some((k) => k.key === token);
36
- }
37
- /** List all keys (returns id, label, createdAt — never the full key). */
38
- list() {
39
- return this.#keys.map(({ id, label, createdAt }) => ({ id, label, createdAt }));
167
+ #saveAdmin() {
168
+ const p = this.#adminPath();
169
+ mkdirSync(dirname(p), { recursive: true });
170
+ writeFileSync(p, JSON.stringify(this.#admin, null, 2));
40
171
  }
41
- /** List all keys including full key values (admin only). */
42
- listFull() {
43
- return this.#keys.map((k) => ({ ...k }));
172
+ #saveTenant(tenantId) {
173
+ const bucket = this.#byTenant.get(tenantId) ?? [];
174
+ const p = this.#tenantPath(tenantId);
175
+ mkdirSync(dirname(p), { recursive: true });
176
+ writeFileSync(p, JSON.stringify(bucket, null, 2));
44
177
  }
45
- /** Generate a new API key. Returns the full key (only shown once). */
46
- generate(label) {
47
- const id = randomBytes(4).toString('hex');
48
- const key = `sh3_${randomBytes(32).toString('hex')}`;
49
- const entry = { id, key, label, createdAt: new Date().toISOString() };
50
- this.#keys.push(entry);
51
- this.#save();
52
- return entry;
53
- }
54
- /** Revoke a key by id. Returns true if found and removed. */
55
- revoke(id) {
56
- const before = this.#keys.length;
57
- this.#keys = this.#keys.filter((k) => k.id !== id);
58
- if (this.#keys.length < before) {
59
- this.#save();
60
- return true;
178
+ #migrateLegacy() {
179
+ const legacyPath = join(this.#dataDir, 'keys.json');
180
+ const adminPath = this.#adminPath();
181
+ if (!existsSync(legacyPath) || existsSync(adminPath))
182
+ return;
183
+ try {
184
+ const legacy = JSON.parse(readFileSync(legacyPath, 'utf-8'));
185
+ const migrated = legacy.map((row) => ({
186
+ ...row,
187
+ tenantId: null,
188
+ ownerUserId: null,
189
+ mintedByShardId: null,
190
+ scopes: ['admin:*'],
191
+ }));
192
+ mkdirSync(dirname(adminPath), { recursive: true });
193
+ writeFileSync(adminPath, JSON.stringify(migrated, null, 2));
194
+ renameSync(legacyPath, legacyPath + '.legacy');
195
+ }
196
+ catch {
197
+ // If migration fails, leave everything alone; admin can investigate.
61
198
  }
62
- return false;
63
199
  }
64
- /** True if no keys exist (first boot). */
65
- isEmpty() {
66
- return this.#keys.length === 0;
200
+ // ---------- legacy compat ----------
201
+ /** @deprecated use resolve() — kept until all callers migrate. */
202
+ validate(token) {
203
+ const hit = this.resolve(token);
204
+ return !!hit && hit.scopes.includes('admin:*');
67
205
  }
68
206
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * ADR-019 §11.2: retire the legacy `grants/` namespace under the reserved
3
+ * `__sync__` shard id for every tenant. Idempotent; safe to run on every
4
+ * boot. Other `__sync__/` subdirectories are out of scope here — they
5
+ * disappear when the sync runtime is rebuilt in sh3-server.
6
+ */
7
+ export declare function removeLegacyGrants(dataDir: string): void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ADR-019 §11.2: retire the legacy `grants/` namespace under the reserved
3
+ * `__sync__` shard id for every tenant. Idempotent; safe to run on every
4
+ * boot. Other `__sync__/` subdirectories are out of scope here — they
5
+ * disappear when the sync runtime is rebuilt in sh3-server.
6
+ */
7
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ export function removeLegacyGrants(dataDir) {
10
+ const docsDir = join(dataDir, 'docs');
11
+ if (!existsSync(docsDir))
12
+ return;
13
+ const tenants = readdirSync(docsDir, { withFileTypes: true })
14
+ .filter((e) => e.isDirectory())
15
+ .map((e) => e.name);
16
+ let removed = 0;
17
+ for (const tenant of tenants) {
18
+ const grants = join(docsDir, tenant, '__sync__', 'grants');
19
+ if (existsSync(grants)) {
20
+ rmSync(grants, { recursive: true, force: true });
21
+ removed += 1;
22
+ }
23
+ }
24
+ if (removed > 0) {
25
+ console.log(`[sh3] migration: removed legacy __sync__/grants/ for ${removed} tenant(s)`);
26
+ }
27
+ }
@@ -3,6 +3,7 @@ import type { KeyStore } from './keys.js';
3
3
  import type { SettingsStore } from './settings.js';
4
4
  import { ShardRouter } from './shard-router.js';
5
5
  import type { MountContext } from './shard-router.js';
6
+ import type { TenantDocStore } from './doc-store/index.js';
6
7
  /** Type of the `wsRegister` factory field from MountContext. */
7
8
  type WsRegister = MountContext['wsRegister'];
8
9
  export interface DiscoveredPackage {
@@ -19,7 +20,7 @@ export interface DiscoveredPackage {
19
20
  * For each valid package, mount server routes (if server.js exists) and
20
21
  * return the full list of discovered packages.
21
22
  */
22
- export declare function loadPackages(shardRouter: ShardRouter, dataDir: string, keys: KeyStore, settings: SettingsStore, wsRegister: WsRegister): Promise<DiscoveredPackage[]>;
23
+ export declare function loadPackages(shardRouter: ShardRouter, dataDir: string, keys: KeyStore, settings: SettingsStore, wsRegister: WsRegister, docStore: TenantDocStore): Promise<DiscoveredPackage[]>;
23
24
  /**
24
25
  * Re-scan `<dataDir>/packages/` and return metadata for all valid packages.
25
26
  * Unlike `loadPackages`, this does NOT mount server routes — it only reads
@@ -52,5 +53,5 @@ export declare function validateRequiredShards(manifest: Record<string, unknown>
52
53
  * Returns a Hono router with POST /install and POST /uninstall.
53
54
  * Protected by the blanket `/api/*` auth middleware already applied upstream.
54
55
  */
55
- export declare function createPackageManagementRoutes(shardRouter: ShardRouter, dataDir: string, keys: KeyStore, settings: SettingsStore, wsRegister: WsRegister, frameworkShardIds?: string[]): Hono;
56
+ export declare function createPackageManagementRoutes(shardRouter: ShardRouter, dataDir: string, keys: KeyStore, settings: SettingsStore, wsRegister: WsRegister, docStore: TenantDocStore, frameworkShardIds?: string[]): Hono;
56
57
  export {};
package/dist/packages.js CHANGED
@@ -18,7 +18,7 @@ function isValidId(id) {
18
18
  * For each valid package, mount server routes (if server.js exists) and
19
19
  * return the full list of discovered packages.
20
20
  */
21
- export async function loadPackages(shardRouter, dataDir, keys, settings, wsRegister) {
21
+ export async function loadPackages(shardRouter, dataDir, keys, settings, wsRegister, docStore) {
22
22
  const packagesDir = join(dataDir, 'packages');
23
23
  if (!existsSync(packagesDir)) {
24
24
  mkdirSync(packagesDir, { recursive: true });
@@ -68,7 +68,7 @@ export async function loadPackages(shardRouter, dataDir, keys, settings, wsRegis
68
68
  };
69
69
  if (hasServer) {
70
70
  try {
71
- await shardRouter.mount(manifest.id, serverJs, { pkgDir, keys, settings, wsRegister });
71
+ await shardRouter.mount(manifest.id, serverJs, { pkgDir, keys, settings, wsRegister, docStore });
72
72
  }
73
73
  catch (err) {
74
74
  console.warn(`[sh3] ${manifest.id}/server.js failed to load:`, err);
@@ -193,7 +193,7 @@ export function validateRequiredShards(manifest, knownShards) {
193
193
  * Returns a Hono router with POST /install and POST /uninstall.
194
194
  * Protected by the blanket `/api/*` auth middleware already applied upstream.
195
195
  */
196
- export function createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister, frameworkShardIds = []) {
196
+ export function createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister, docStore, frameworkShardIds = []) {
197
197
  const router = new Hono();
198
198
  router.post('/install', async (c) => {
199
199
  const form = await c.req.formData();
@@ -267,7 +267,7 @@ export function createPackageManagementRoutes(shardRouter, dataDir, keys, settin
267
267
  writeFileSync(join(pkgDir, 'server.js'), buf);
268
268
  // Hot-mount server routes
269
269
  try {
270
- await shardRouter.mount(id, join(pkgDir, 'server.js'), { pkgDir, keys, settings, wsRegister });
270
+ await shardRouter.mount(id, join(pkgDir, 'server.js'), { pkgDir, keys, settings, wsRegister, docStore });
271
271
  }
272
272
  catch (err) {
273
273
  // Roll back entire install — broken server bundle must not be half-installed
@@ -297,7 +297,7 @@ export function createPackageManagementRoutes(shardRouter, dataDir, keys, settin
297
297
  return c.json({ error: `Package "${id}" not found` }, 404);
298
298
  }
299
299
  // Unmount server routes immediately
300
- shardRouter.unmount(id);
300
+ await shardRouter.unmount(id);
301
301
  // Remove code files
302
302
  rmSync(manifestPath, { force: true });
303
303
  const clientPath = join(pkgDir, 'client.js');
@@ -99,7 +99,11 @@ export function createAdminRouter(users, settings, sessions, keys, dataDir) {
99
99
  });
100
100
  // --- API Keys ---
101
101
  router.get('/keys', (c) => {
102
- return c.json(keys.listFull());
102
+ return c.json(keys.listAdmin());
103
+ });
104
+ /** Read-only audit endpoint — every key across all tenants and admin. */
105
+ router.get('/keys/all', (c) => {
106
+ return c.json(keys.listAll());
103
107
  });
104
108
  router.post('/keys', async (c) => {
105
109
  let body;
@@ -113,12 +117,12 @@ export function createAdminRouter(users, settings, sessions, keys, dataDir) {
113
117
  if (!label) {
114
118
  return c.json({ error: 'Label required' }, 400);
115
119
  }
116
- const key = keys.generate(label);
120
+ const key = keys.generate({ label, tenantId: null, ownerUserId: null, mintedByShardId: null, scopes: ['admin:*'] });
117
121
  return c.json(key, 201);
118
122
  });
119
123
  router.delete('/keys/:id', (c) => {
120
124
  const id = c.req.param('id');
121
- if (!keys.revoke(id))
125
+ if (!keys.revoke(null, id))
122
126
  return c.json({ error: 'Key not found' }, 404);
123
127
  return c.body(null, 204);
124
128
  });
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Document backend API routes.
3
3
  *
4
- * Maps the DocumentBackend interface to HTTP endpoints backed by the
5
- * local filesystem. Files are stored at {dataDir}/docs/{tenant}/{shard}/{path}.
4
+ * Delegates every read/write/list/delete to the TenantDocStore, which owns
5
+ * the metadata-sidecar write pipeline (version bump, syncMode cache, tick
6
+ * advance, conflict bucket). This router is intentionally thin: it validates
7
+ * auth and path shape, then calls the store.
6
8
  *
7
- * GET /api/docs/:tenant/_shards → listAllShards
8
- * GET /api/docs/:tenant/_all → listAllDocuments
9
+ * GET /api/docs/:tenant/_shards → listAll (shard ids)
10
+ * GET /api/docs/:tenant/_all → listAll
9
11
  * GET /api/docs/:tenant/:shard → list
10
12
  * GET /api/docs/:tenant/:shard/*path → read
11
13
  * HEAD /api/docs/:tenant/:shard/*path → exists
12
- * PUT /api/docs/:tenant/:shard/*path → write (auth required)
13
- * DELETE /api/docs/:tenant/:shard/*path → delete (auth required)
14
+ * PUT /api/docs/:tenant/:shard/*path → write
15
+ * DELETE /api/docs/:tenant/:shard/*path → delete
14
16
  */
15
17
  import { Hono } from 'hono';
16
- export declare function createDocsRouter(dataDir: string): Hono;
18
+ import type { SettingsStore } from '../settings.js';
19
+ import type { TenantDocStore } from '../doc-store/index.js';
20
+ export declare function createDocsRouter(store: TenantDocStore, settings?: SettingsStore): Hono;