sh3-server 0.19.6 → 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--m0u3gjJ.js +0 -21
  42. package/app/assets/index--m0u3gjJ.js.map +0 -1
  43. package/app/assets/index-DIpoXNrk.css +0 -1
package/dist/scope.d.ts CHANGED
@@ -10,7 +10,6 @@
10
10
  * unless their accessibleScopes list includes the requested scope.
11
11
  */
12
12
  import type { MiddlewareHandler } from 'hono';
13
- import type { SettingsStore } from './settings.js';
14
13
  export declare function scopeRequired(scope: string): MiddlewareHandler;
15
14
  export declare const requireCallerScope: MiddlewareHandler;
16
- export declare function scopeAccessMatch(paramName: string, settings?: SettingsStore): MiddlewareHandler;
15
+ export declare function scopeAccessMatch(paramName: string): MiddlewareHandler;
package/dist/scope.js CHANGED
@@ -28,12 +28,8 @@ export const requireCallerScope = async (c, next) => {
28
28
  return c.json({ error: 'Scope-bound credentials required' }, 401);
29
29
  return next();
30
30
  };
31
- export function scopeAccessMatch(paramName, settings) {
31
+ export function scopeAccessMatch(paramName) {
32
32
  return async (c, next) => {
33
- // Open / no-auth mode: admin has explicitly disabled scope isolation.
34
- // Mirrors sessionAuth's open-mode bypass; required for Tauri sidecar mode.
35
- if (settings && !settings.get().auth.required)
36
- return next();
37
33
  const caller = c.get('caller');
38
34
  if (!caller)
39
35
  return c.json({ error: 'Caller not resolved' }, 500);
@@ -4,7 +4,6 @@
4
4
  */
5
5
  export interface GlobalSettings {
6
6
  auth: {
7
- required: boolean;
8
7
  guestAllowed: boolean;
9
8
  sessionTTL: number;
10
9
  selfRegistration: boolean;
package/dist/settings.js CHANGED
@@ -7,7 +7,6 @@ import { dirname } from 'node:path';
7
7
  const MAX_PACKAGE_CACHE_AGE = 31536000; // 1 year
8
8
  const DEFAULTS = {
9
9
  auth: {
10
- required: true,
11
10
  guestAllowed: false,
12
11
  sessionTTL: 24,
13
12
  selfRegistration: false,
@@ -44,7 +43,6 @@ export class SettingsStore {
44
43
  const raw = JSON.parse(readFileSync(this.#path, 'utf-8'));
45
44
  return {
46
45
  auth: {
47
- required: raw.auth?.required ?? DEFAULTS.auth.required,
48
46
  guestAllowed: raw.auth?.guestAllowed ?? DEFAULTS.auth.guestAllowed,
49
47
  sessionTTL: raw.auth?.sessionTTL ?? DEFAULTS.auth.sessionTTL,
50
48
  selfRegistration: raw.auth?.selfRegistration ?? DEFAULTS.auth.selfRegistration,
@@ -72,8 +70,6 @@ export class SettingsStore {
72
70
  /** Patch settings. Only provided fields are updated. */
73
71
  update(patch) {
74
72
  if (patch.auth) {
75
- if (patch.auth.required !== undefined)
76
- this.#settings.auth.required = patch.auth.required;
77
73
  if (patch.auth.guestAllowed !== undefined)
78
74
  this.#settings.auth.guestAllowed = patch.auth.guestAllowed;
79
75
  if (patch.auth.sessionTTL !== undefined)
@@ -48,7 +48,7 @@ export interface MountContext {
48
48
  wsRegister(onConnect: (ws: any, c: any) => void): any;
49
49
  }
50
50
  /** Middleware requiring the caller's scope set to include admin:*. */
51
- export declare function adminOnly(_keys: KeyStore, settings: SettingsStore): MiddlewareHandler;
51
+ export declare function adminOnly(): MiddlewareHandler;
52
52
  /**
53
53
  * Dynamic shard route manager. Holds a Map of shard Hono sub-apps
54
54
  * and delegates requests from a single wildcard route.
@@ -5,10 +5,8 @@ import { join } from 'node:path';
5
5
  import { scopeRequired, requireCallerScope } from './scope.js';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  /** Middleware requiring the caller's scope set to include admin:*. */
8
- export function adminOnly(_keys, settings) {
8
+ export function adminOnly() {
9
9
  return async (c, next) => {
10
- if (!settings.get().auth.required)
11
- return next();
12
10
  const caller = c.get('caller');
13
11
  if (caller?.scopes.includes('admin:*'))
14
12
  return next();
@@ -154,6 +152,26 @@ export class ShardRouter {
154
152
  // Framework built-ins (mountStatic) may have no sibling manifest; default to empty.
155
153
  }
156
154
  const docStore = ctx.docStore;
155
+ // Symmetric path translator: same (shardId, path) pair that documents()
156
+ // accepts. `mounts/*` walks the MountedPathResolver wired on the docStore;
157
+ // any other shardId resolves to the canonical native-doc location under
158
+ // the host's docs dir. Throws on unresolvable mounts so callers can just
159
+ // hand the result to spawn/streams without branching.
160
+ const resolveFsPath = (tenant, targetShardId, path) => {
161
+ if (targetShardId === 'mounts') {
162
+ const resolver = docStore.mountResolver;
163
+ if (!resolver)
164
+ throw new Error('Mount resolver not configured on docStore');
165
+ const docPath = path ? `mounts/${path}` : 'mounts';
166
+ const resolved = resolver.resolve(tenant, docPath);
167
+ if (resolved.kind === 'mount')
168
+ return resolved.realPath;
169
+ if (resolved.kind === 'mount-unresolved')
170
+ throw new Error(resolved.error);
171
+ throw new Error(`Invalid mount path: ${docPath}`);
172
+ }
173
+ return join(docStore.dataDir, 'docs', tenant, targetShardId, path);
174
+ };
157
175
  // Hono's MiddlewareHandler uses a concrete Context generic that isn't
158
176
  // assignable to sh3-core's framework-agnostic stand-in (c: unknown).
159
177
  // Cast once at assembly — shards only call the handlers, never introspect.
@@ -161,7 +179,7 @@ export class ShardRouter {
161
179
  shardId,
162
180
  dataDir: shardDataDir,
163
181
  permissions,
164
- adminOnly: adminOnly(ctx.keys, ctx.settings),
182
+ adminOnly: adminOnly(),
165
183
  scopeRequired,
166
184
  tenantRequired: requireCallerScope,
167
185
  wsRegister: ctx.wsRegister,
@@ -172,6 +190,7 @@ export class ShardRouter {
172
190
  return; // silent no-op
173
191
  docStore.roles.set(tenant, role);
174
192
  },
193
+ resolveFsPath,
175
194
  };
176
195
  return ctxOut;
177
196
  }
@@ -5,11 +5,9 @@
5
5
  * Read-only. Writes are out of scope for this iteration.
6
6
  */
7
7
  import type { Hono } from 'hono';
8
- import type { SettingsStore } from '../settings.js';
9
8
  export interface TenantFsRouteContext {
10
9
  dataDir: string;
11
10
  rootBase: string;
12
- settings: SettingsStore;
13
11
  maxReadBytes: number;
14
12
  }
15
13
  export declare function registerTenantFsRoutes(app: Hono, ctx: TenantFsRouteContext): void;
@@ -16,7 +16,7 @@ function userIdFromContext(c) {
16
16
  return session.userId;
17
17
  }
18
18
  export function registerTenantFsRoutes(app, ctx) {
19
- const sessionRequired = makeSessionRequired(ctx.settings);
19
+ const sessionRequired = makeSessionRequired();
20
20
  app.get('/api/fs/list', sessionRequired, async (c) => {
21
21
  const rel = c.req.query('path') ?? '';
22
22
  const root = documentsRoot(ctx.dataDir, userIdFromContext(c), ctx.rootBase);
@@ -1,11 +1,9 @@
1
1
  import type { MiddlewareHandler } from 'hono';
2
- import type { SettingsStore } from '../settings.js';
3
2
  /**
4
3
  * Requires an authenticated session on the request. Any role passes.
5
- * When `auth.required` is false (dev/--no-auth), passes through.
6
4
  *
7
5
  * Contrast with adminOnly: this gate is for tenant-scoped APIs where any
8
6
  * logged-in user can operate — scope is enforced by the handler jailing to
9
7
  * the caller's own tenant root, not by role.
10
8
  */
11
- export declare function makeSessionRequired(settings: SettingsStore): MiddlewareHandler;
9
+ export declare function makeSessionRequired(): MiddlewareHandler;
@@ -1,15 +1,12 @@
1
1
  /**
2
2
  * Requires an authenticated session on the request. Any role passes.
3
- * When `auth.required` is false (dev/--no-auth), passes through.
4
3
  *
5
4
  * Contrast with adminOnly: this gate is for tenant-scoped APIs where any
6
5
  * logged-in user can operate — scope is enforced by the handler jailing to
7
6
  * the caller's own tenant root, not by role.
8
7
  */
9
- export function makeSessionRequired(settings) {
8
+ export function makeSessionRequired() {
10
9
  return async (c, next) => {
11
- if (!settings.get().auth.required)
12
- return next();
13
10
  const session = c.get('session') ?? c.env?.session;
14
11
  if (!session?.userId) {
15
12
  return c.json({ error: 'authentication required' }, 401);
package/dist/users.d.ts CHANGED
@@ -6,7 +6,7 @@ export interface StoredUser {
6
6
  id: string;
7
7
  username: string;
8
8
  displayName: string;
9
- passwordHash: string;
9
+ passwordHash: string | null;
10
10
  role: 'admin' | 'user';
11
11
  createdAt: string;
12
12
  updatedAt: string;
@@ -39,6 +39,19 @@ export declare class UserStore {
39
39
  }): Promise<PublicUser | null>;
40
40
  /** Delete a user by ID. Returns true if found and removed. */
41
41
  delete(id: string): boolean;
42
+ /**
43
+ * Upsert a synthetic user (no password). Used by `--local` desktop mode to
44
+ * persist the `local` owner without invoking bcrypt (which the SEA sidecar
45
+ * cannot load). Users created via this path have `passwordHash=null` and
46
+ * can never authenticate through the password flow — `authenticate()` filters
47
+ * them out.
48
+ */
49
+ upsertSynthetic(opts: {
50
+ id: string;
51
+ username: string;
52
+ displayName: string;
53
+ role: 'admin' | 'user';
54
+ }): PublicUser;
42
55
  /** Generate a random temporary password. */
43
56
  static generatePassword(): string;
44
57
  }
package/dist/users.js CHANGED
@@ -64,6 +64,8 @@ export class UserStore {
64
64
  const user = this.#users.find(u => u.username === username.toLowerCase());
65
65
  if (!user)
66
66
  return null;
67
+ if (!user.passwordHash)
68
+ return null;
67
69
  if (!(await (await getBcrypt()).compare(password, user.passwordHash)))
68
70
  return null;
69
71
  return this.#toPublic(user);
@@ -102,6 +104,38 @@ export class UserStore {
102
104
  }
103
105
  return false;
104
106
  }
107
+ /**
108
+ * Upsert a synthetic user (no password). Used by `--local` desktop mode to
109
+ * persist the `local` owner without invoking bcrypt (which the SEA sidecar
110
+ * cannot load). Users created via this path have `passwordHash=null` and
111
+ * can never authenticate through the password flow — `authenticate()` filters
112
+ * them out.
113
+ */
114
+ upsertSynthetic(opts) {
115
+ const lower = opts.username.toLowerCase();
116
+ const existing = this.#users.find((u) => u.id === opts.id);
117
+ const now = new Date().toISOString();
118
+ if (existing) {
119
+ existing.username = lower;
120
+ existing.displayName = opts.displayName;
121
+ existing.role = opts.role;
122
+ existing.updatedAt = now;
123
+ this.#save();
124
+ return this.#toPublic(existing);
125
+ }
126
+ const user = {
127
+ id: opts.id,
128
+ username: lower,
129
+ displayName: opts.displayName,
130
+ passwordHash: null,
131
+ role: opts.role,
132
+ createdAt: now,
133
+ updatedAt: now,
134
+ };
135
+ this.#users.push(user);
136
+ this.#save();
137
+ return this.#toPublic(user);
138
+ }
105
139
  /** Generate a random temporary password. */
106
140
  static generatePassword() {
107
141
  return randomBytes(6).toString('base64url');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-server",
3
- "version": "0.19.6",
3
+ "version": "0.20.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sh3-server": "dist/cli.js"