sh3-core 0.5.2 → 0.5.5

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.

Potentially problematic release.


This version of sh3-core might be problematic. Click here for more details.

Files changed (54) hide show
  1. package/dist/Shell.svelte +6 -3
  2. package/dist/admin/AuthSettingsView.svelte +105 -0
  3. package/dist/admin/AuthSettingsView.svelte.d.ts +3 -0
  4. package/dist/admin/SystemView.svelte +73 -0
  5. package/dist/admin/SystemView.svelte.d.ts +3 -0
  6. package/dist/admin/UsersView.svelte +189 -0
  7. package/dist/admin/UsersView.svelte.d.ts +3 -0
  8. package/dist/admin/adminApp.d.ts +7 -0
  9. package/dist/admin/adminApp.js +24 -0
  10. package/dist/admin/adminShard.svelte.d.ts +4 -0
  11. package/dist/admin/adminShard.svelte.js +52 -0
  12. package/dist/api.d.ts +2 -1
  13. package/dist/api.js +1 -1
  14. package/dist/apps/lifecycle.d.ts +6 -1
  15. package/dist/apps/lifecycle.js +28 -4
  16. package/dist/apps/registry.svelte.d.ts +5 -2
  17. package/dist/apps/registry.svelte.js +6 -7
  18. package/dist/apps/types.d.ts +13 -0
  19. package/dist/auth/GuestBanner.svelte +144 -0
  20. package/dist/auth/GuestBanner.svelte.d.ts +3 -0
  21. package/dist/auth/SignInWall.svelte +213 -0
  22. package/dist/auth/SignInWall.svelte.d.ts +8 -0
  23. package/dist/auth/auth.svelte.d.ts +42 -31
  24. package/dist/auth/auth.svelte.js +106 -89
  25. package/dist/auth/index.d.ts +2 -1
  26. package/dist/auth/index.js +1 -1
  27. package/dist/auth/types.d.ts +41 -0
  28. package/dist/auth/types.js +6 -0
  29. package/dist/build.js +70 -16
  30. package/dist/createShell.d.ts +2 -2
  31. package/dist/createShell.js +78 -33
  32. package/dist/diagnostic/DiagnosticPromptModal.svelte +1 -1
  33. package/dist/host-entry.d.ts +2 -1
  34. package/dist/host-entry.js +2 -2
  35. package/dist/host.d.ts +0 -2
  36. package/dist/host.js +11 -25
  37. package/dist/layout/DragPreview.svelte +1 -1
  38. package/dist/overlays/ModalFrame.svelte +1 -1
  39. package/dist/overlays/PopupFrame.svelte +1 -1
  40. package/dist/overlays/ToastItem.svelte +1 -1
  41. package/dist/primitives/TabbedPanel.svelte +1 -1
  42. package/dist/registry/installer.js +0 -2
  43. package/dist/shards/activate.svelte.d.ts +13 -6
  44. package/dist/shards/activate.svelte.js +19 -8
  45. package/dist/shards/types.d.ts +11 -0
  46. package/dist/shell-shard/ShellHome.svelte +32 -118
  47. package/dist/store/InstalledView.svelte +7 -7
  48. package/dist/store/StoreView.svelte +16 -16
  49. package/dist/store/storeApp.js +1 -1
  50. package/dist/store/storeShard.svelte.js +5 -4
  51. package/dist/tokens.css +14 -0
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/package.json +1 -1
@@ -1,127 +1,144 @@
1
1
  /**
2
- * Client-side admin mode API key elevation for SH3.
2
+ * Client-side authsession-based identity for SH3.
3
3
  *
4
- * Stores the key in the user state zone so it persists across sessions.
5
- * Provides reactive `isAdmin` for gating admin apps. The key is verified
6
- * against the server on elevation and on boot (via initAuth).
4
+ * The boot flow (createShell) calls initFromBoot() with the server's
5
+ * boot config. After that, login/logout call the server and update
6
+ * the reactive state. isAdmin/isGuest/isAuthenticated are reactive
7
+ * getters consumed by shell components.
7
8
  *
8
- * Boot-time verification uses a short timeout and fails open — the shell
9
- * remains usable without admin access when the server is slow or offline.
10
- *
11
- * OS analogy: sudo / elevated permissions, not web login.
12
- *
13
- * .svelte.ts because it uses $state for reactive admin status.
9
+ * .svelte.ts because it uses $state for reactive auth status.
14
10
  */
15
- import { createStateZones } from '../state/zones.svelte';
16
- const state = createStateZones('__shell__:auth', {
17
- user: { apiKey: null },
18
- });
19
- /** Reactive admin status. */
20
- let admin = $state(false);
21
- /** Server base URL, set once during initAuth. */
11
+ /** Reactive auth state. */
12
+ let currentUser = $state(null);
13
+ let currentSession = $state(null);
14
+ let guest = $state(false);
15
+ /** Server base URL, set during boot. */
22
16
  let serverUrl = '';
23
17
  /**
24
- * Verify a key against the server. Returns true if valid.
25
- * Accepts an optional AbortSignal for timeout control.
18
+ * Initialize auth from boot config. Called once by createShell()
19
+ * after fetching /api/boot.
26
20
  */
27
- async function verifyKey(key, signal) {
21
+ export function initFromBoot(url, config) {
22
+ serverUrl = url;
23
+ currentUser = config.user;
24
+ currentSession = config.session;
25
+ guest = !config.session && !config.user;
26
+ }
27
+ /**
28
+ * Log in with username + password. On success, updates reactive state.
29
+ * Returns { ok: true } or { ok: false, error: string }.
30
+ */
31
+ export async function login(username, password) {
28
32
  try {
29
- const response = await fetch(`${serverUrl}/api/auth/verify`, {
33
+ const res = await fetch(`${serverUrl}/api/auth/login`, {
30
34
  method: 'POST',
31
- headers: { Authorization: `Bearer ${key}` },
32
- signal,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ credentials: 'include',
37
+ body: JSON.stringify({ username, password }),
33
38
  });
34
- if (!response.ok)
35
- return false;
36
- const body = await response.json();
37
- return body.valid === true;
39
+ if (!res.ok) {
40
+ const body = await res.json().catch(() => ({}));
41
+ return { ok: false, error: body.error || 'Login failed' };
42
+ }
43
+ const body = await res.json();
44
+ currentUser = body.user;
45
+ currentSession = body.session;
46
+ guest = false;
47
+ return { ok: true };
38
48
  }
39
49
  catch (_a) {
40
- return false;
50
+ return { ok: false, error: 'Network error' };
41
51
  }
42
52
  }
43
53
  /**
44
- * Initialize auth at boot. Call once from bootstrap().
45
- *
46
- * If a key is stored from a previous session, verifies it against the
47
- * server with a 3-second timeout. If verification fails (key revoked,
48
- * server down, timeout), the shell boots without admin access — the
49
- * stored key is cleared only on explicit rejection (401), not on
50
- * network failure (so a temporary outage doesn't force re-entry).
51
- *
52
- * @param url - Server base URL ('' for same-origin).
54
+ * Register a new account (when self-registration is enabled).
55
+ * On success, auto-logs in and updates reactive state.
53
56
  */
54
- export async function initAuth(url = '') {
55
- serverUrl = url;
56
- const stored = state.user.apiKey;
57
- if (!stored)
58
- return;
59
- const controller = new AbortController();
60
- const timeout = setTimeout(() => controller.abort(), 3000);
57
+ export async function register(username, password, displayName) {
61
58
  try {
62
- const response = await fetch(`${serverUrl}/api/auth/verify`, {
59
+ const res = await fetch(`${serverUrl}/api/auth/register`, {
63
60
  method: 'POST',
64
- headers: { Authorization: `Bearer ${stored}` },
65
- signal: controller.signal,
61
+ headers: { 'Content-Type': 'application/json' },
62
+ credentials: 'include',
63
+ body: JSON.stringify({ username, password, displayName }),
66
64
  });
67
- clearTimeout(timeout);
68
- if (response.ok) {
69
- const body = await response.json();
70
- if (body.valid === true) {
71
- admin = true;
72
- return;
73
- }
74
- }
75
- // Server explicitly rejected the key — clear it.
76
- if (response.status === 401 || response.status === 403) {
77
- state.user.apiKey = null;
65
+ if (!res.ok) {
66
+ const body = await res.json().catch(() => ({}));
67
+ return { ok: false, error: body.error || 'Registration failed' };
78
68
  }
79
- // Other errors (5xx, etc.): keep the key, boot unelevated.
69
+ const body = await res.json();
70
+ currentUser = body.user;
71
+ currentSession = body.session;
72
+ guest = false;
73
+ return { ok: true };
80
74
  }
81
75
  catch (_a) {
82
- clearTimeout(timeout);
83
- // Network error or timeout: keep the key, boot unelevated.
84
- // User can re-elevate manually once connectivity returns.
76
+ return { ok: false, error: 'Network error' };
85
77
  }
86
78
  }
87
79
  /**
88
- * Elevate to admin mode with an API key. Verifies against the server
89
- * before storing. Returns true on success, false if the key is invalid.
80
+ * Log out clear session on server and client.
90
81
  */
91
- export async function elevate(key) {
92
- const valid = await verifyKey(key);
93
- if (valid) {
94
- state.user.apiKey = key;
95
- admin = true;
82
+ export async function logout() {
83
+ try {
84
+ await fetch(`${serverUrl}/api/auth/logout`, {
85
+ method: 'POST',
86
+ credentials: 'include',
87
+ });
96
88
  }
97
- return valid;
89
+ catch (_a) {
90
+ // Best effort
91
+ }
92
+ currentUser = null;
93
+ currentSession = null;
94
+ guest = true;
98
95
  }
99
96
  /**
100
- * De-escalate from admin modeclear the stored key and drop elevation.
97
+ * Mark this session as local-owner auto-elevate to admin without
98
+ * server verification. Called by the host in Tauri / dev environments.
101
99
  */
102
- export function deescalate() {
103
- state.user.apiKey = null;
104
- admin = false;
100
+ export function setLocalOwner() {
101
+ currentUser = {
102
+ id: 'local',
103
+ username: 'local',
104
+ displayName: 'Local Owner',
105
+ role: 'admin',
106
+ createdAt: '',
107
+ updatedAt: '',
108
+ };
109
+ currentSession = {
110
+ token: 'local',
111
+ userId: 'local',
112
+ role: 'admin',
113
+ expiresAt: Infinity,
114
+ };
115
+ guest = false;
105
116
  }
106
- /**
107
- * Reactive getter — true when the user has elevated to admin mode.
108
- */
117
+ /** Reactive — true when the user has admin role. */
109
118
  export function isAdmin() {
110
- return admin;
119
+ return (currentSession === null || currentSession === void 0 ? void 0 : currentSession.role) === 'admin';
111
120
  }
112
- /**
113
- * Mark this session as local-owner — auto-elevate to admin without
114
- * key verification. Called by the host in Tauri / dev environments
115
- * where the user owns the machine. Idempotent.
116
- */
117
- export function setLocalOwner() {
118
- admin = true;
121
+ /** Reactive — true when the user has a valid session. */
122
+ export function isAuthenticated() {
123
+ return currentSession !== null;
124
+ }
125
+ /** Reactive — true when browsing without a session. */
126
+ export function isGuest() {
127
+ return guest;
128
+ }
129
+ /** Get the current user (reactive). */
130
+ export function getUser() {
131
+ return currentUser;
132
+ }
133
+ /** Get the current session (reactive). */
134
+ export function getSession() {
135
+ return currentSession;
119
136
  }
120
137
  /**
121
- * Build an Authorization header value for authenticated fetch calls.
122
- * Returns null if not elevated.
138
+ * Build an Authorization header value for authenticated fetch calls
139
+ * that need explicit headers (e.g. non-cookie contexts).
140
+ * Returns null if not authenticated.
123
141
  */
124
142
  export function getAuthHeader() {
125
- const key = state.user.apiKey;
126
- return key ? `Bearer ${key}` : null;
143
+ return currentSession ? `Bearer ${currentSession.token}` : null;
127
144
  }
@@ -1 +1,2 @@
1
- export { initAuth, elevate, deescalate, isAdmin, getAuthHeader, setLocalOwner } from './auth.svelte';
1
+ export { initFromBoot, login, logout, register, isAdmin, isAuthenticated, isGuest, getUser, getSession, getAuthHeader, setLocalOwner, } from './auth.svelte';
2
+ export type { AuthUser, AuthSession, BootConfig, GlobalSettings } from './types';
@@ -1 +1 @@
1
- export { initAuth, elevate, deescalate, isAdmin, getAuthHeader, setLocalOwner } from './auth.svelte';
1
+ export { initFromBoot, login, logout, register, isAdmin, isAuthenticated, isGuest, getUser, getSession, getAuthHeader, setLocalOwner, } from './auth.svelte';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared auth types — used by both client and server.
3
+ * Kept in sh3-core so the server can import them at build time
4
+ * and the client uses them directly.
5
+ */
6
+ /** Public user shape (never includes passwordHash). */
7
+ export interface AuthUser {
8
+ id: string;
9
+ username: string;
10
+ displayName: string;
11
+ role: 'admin' | 'user';
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ }
15
+ /** Session shape returned to the client. */
16
+ export interface AuthSession {
17
+ token: string;
18
+ userId: string;
19
+ role: 'admin' | 'user';
20
+ expiresAt: number;
21
+ }
22
+ /** Response from GET /api/boot. */
23
+ export interface BootConfig {
24
+ auth: {
25
+ required: boolean;
26
+ guestAllowed: boolean;
27
+ selfRegistration: boolean;
28
+ };
29
+ user: AuthUser | null;
30
+ session: AuthSession | null;
31
+ tenantId: string;
32
+ }
33
+ /** Global settings shape. */
34
+ export interface GlobalSettings {
35
+ auth: {
36
+ required: boolean;
37
+ guestAllowed: boolean;
38
+ sessionTTL: number;
39
+ selfRegistration: boolean;
40
+ };
41
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared auth types — used by both client and server.
3
+ * Kept in sh3-core so the server can import them at build time
4
+ * and the client uses them directly.
5
+ */
6
+ export {};
package/dist/build.js CHANGED
@@ -112,7 +112,7 @@ export function sh3Artifact(options = {}) {
112
112
  }
113
113
  },
114
114
  async closeBundle() {
115
- var _a, _b, _c, _d;
115
+ var _a, _b, _c, _d, _e;
116
116
  if (!entryFileName)
117
117
  return;
118
118
  const clientSrc = join(outDir, entryFileName);
@@ -122,7 +122,7 @@ export function sh3Artifact(options = {}) {
122
122
  try {
123
123
  source = readFileSync(clientSrc, 'utf-8');
124
124
  }
125
- catch (_e) {
125
+ catch (_f) {
126
126
  console.warn('[sh3-artifact] Could not read entry chunk:', clientSrc);
127
127
  return;
128
128
  }
@@ -131,17 +131,71 @@ export function sh3Artifact(options = {}) {
131
131
  writeFileSync(clientDest, source);
132
132
  unlinkSync(clientSrc);
133
133
  }
134
- // --- Extract manifest fields via regex ---
135
- const extract = (pattern) => {
136
- const m = source.match(pattern);
137
- return m ? m[1] : '';
138
- };
139
- const id = extract(/\bid\s*:\s*["']([^"']+)["']/);
140
- const label = extract(/\blabel\s*:\s*["']([^"']+)["']/);
141
- const version = extract(/\bversion\s*:\s*["']([^"']+)["']/);
142
- const hasShard = /\bviews\s*:\s*\[/.test(source);
134
+ // --- Extract manifest fields from App or Shard block ---
135
+ //
136
+ // The bundle may contain both an App manifest (has `requiredShards`)
137
+ // and a Shard manifest (has `views: [`). We extract id/label/version
138
+ // from the App block first, then fall back to the Shard block.
139
+ // If neither block exists, the build fails with a clear error.
143
140
  const hasApp = /\brequiredShards\s*:\s*\[/.test(source);
141
+ const hasShard = /\bviews\s*:\s*\[/.test(source);
142
+ if (!hasApp && !hasShard) {
143
+ throw new Error('[sh3-artifact] Could not find an App manifest (requiredShards) or Shard manifest (views) in the entry chunk. '
144
+ + 'Ensure the entry exports an App or Shard object.');
145
+ }
144
146
  const type = hasShard && hasApp ? 'combo' : hasApp ? 'app' : 'shard';
147
+ /**
148
+ * Extract id, label, and version from the manifest object block that
149
+ * contains `anchor`. Walks backwards from the anchor to find the
150
+ * opening `{`, then forward to find the matching `}`, and extracts
151
+ * fields from within that slice.
152
+ */
153
+ function extractFromBlock(anchor) {
154
+ const anchorMatch = anchor.exec(source);
155
+ if (!anchorMatch)
156
+ return null;
157
+ // Walk backwards from the anchor to find the enclosing `{`.
158
+ let depth = 0;
159
+ let blockStart = anchorMatch.index;
160
+ for (let i = anchorMatch.index - 1; i >= 0; i--) {
161
+ if (source[i] === '}')
162
+ depth++;
163
+ if (source[i] === '{') {
164
+ if (depth === 0) {
165
+ blockStart = i;
166
+ break;
167
+ }
168
+ depth--;
169
+ }
170
+ }
171
+ // Walk forwards from the anchor to find the matching `}`.
172
+ depth = 0;
173
+ let blockEnd = source.length;
174
+ for (let i = blockStart; i < source.length; i++) {
175
+ if (source[i] === '{')
176
+ depth++;
177
+ if (source[i] === '}') {
178
+ depth--;
179
+ if (depth === 0) {
180
+ blockEnd = i + 1;
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ const block = source.slice(blockStart, blockEnd);
186
+ const get = (pattern) => {
187
+ const m = block.match(pattern);
188
+ return m ? m[1] : '';
189
+ };
190
+ return {
191
+ id: get(/\bid\s*:\s*["']([^"']+)["']/),
192
+ label: get(/\blabel\s*:\s*["']([^"']+)["']/),
193
+ version: get(/\bversion\s*:\s*["']([^"']+)["']/),
194
+ };
195
+ }
196
+ // App first, then Shard.
197
+ const extracted = (_a = extractFromBlock(/\brequiredShards\s*:\s*\[/)) !== null && _a !== void 0 ? _a : extractFromBlock(/\bviews\s*:\s*\[/);
198
+ const { id, label, version } = extracted;
145
199
  // --- Optional server bundle ---
146
200
  let hasServer = false;
147
201
  if (options.serverEntry && existsSync(options.serverEntry)) {
@@ -156,13 +210,13 @@ export function sh3Artifact(options = {}) {
156
210
  pkgDescription = typeof pkg.description === 'string' ? pkg.description : undefined;
157
211
  pkgAuthor = typeof pkg.author === 'string'
158
212
  ? pkg.author
159
- : typeof ((_a = pkg.author) === null || _a === void 0 ? void 0 : _a.name) === 'string' ? pkg.author.name : undefined;
213
+ : typeof ((_b = pkg.author) === null || _b === void 0 ? void 0 : _b.name) === 'string' ? pkg.author.name : undefined;
160
214
  }
161
- catch ( /* no package.json or unreadable */_f) { /* no package.json or unreadable */ }
215
+ catch ( /* no package.json or unreadable */_g) { /* no package.json or unreadable */ }
162
216
  // --- Write manifest.json ---
163
- const overrides = (_b = options.manifest) !== null && _b !== void 0 ? _b : {};
164
- const finalDescription = (_c = overrides.description) !== null && _c !== void 0 ? _c : pkgDescription;
165
- const finalAuthor = (_d = overrides.author) !== null && _d !== void 0 ? _d : pkgAuthor;
217
+ const overrides = (_c = options.manifest) !== null && _c !== void 0 ? _c : {};
218
+ const finalDescription = (_d = overrides.description) !== null && _d !== void 0 ? _d : pkgDescription;
219
+ const finalAuthor = (_e = overrides.author) !== null && _e !== void 0 ? _e : pkgAuthor;
166
220
  if (!finalDescription) {
167
221
  throw new Error('[sh3-artifact] Missing "description". Add it to package.json or pass it via sh3Artifact({ manifest: { description } }).');
168
222
  }
@@ -2,8 +2,6 @@ import type { Shard, App } from './index';
2
2
  export interface ShellConfig {
3
3
  /** Framework shard IDs to exclude (all included by default) */
4
4
  excludeShards?: string[];
5
- /** Framework app IDs to exclude (all included by default) */
6
- excludeApps?: string[];
7
5
  /** Additional shards to register */
8
6
  shards?: Shard[];
9
7
  /** Additional apps to register */
@@ -20,5 +18,7 @@ export interface ShellConfig {
20
18
  }>;
21
19
  /** Mount target — CSS selector or element (defaults to '#app') */
22
20
  target?: string | HTMLElement;
21
+ /** Server base URL ('' for same-origin) */
22
+ serverUrl?: string;
23
23
  }
24
24
  export declare function createShell(config?: ShellConfig): Promise<void>;
@@ -3,20 +3,22 @@
3
3
  *
4
4
  * Consumers call this from their own main.ts instead of manually
5
5
  * importing registerShard / registerApp / bootstrap / Shell. The
6
- * factory handles platform detection, registration, bootstrap, and
7
- * mounting in the correct order.
6
+ * factory handles platform detection, boot config, auth gating,
7
+ * registration, bootstrap, and mounting in the correct order.
8
8
  */
9
- import { mount } from 'svelte';
9
+ import { mount, unmount } from 'svelte';
10
10
  import { Shell } from './index';
11
11
  import { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner, } from './host';
12
12
  import { resolvePlatform } from './platform/index';
13
13
  import { hydrateTokenOverrides } from './theme';
14
14
  import { __setEnvServerUrl } from './env/index';
15
+ import { __setTenantId } from './documents/config';
16
+ import { initFromBoot } from './auth/index';
17
+ import SignInWall from './auth/SignInWall.svelte';
15
18
  export async function createShell(config) {
16
- var _a, _b;
17
- // 1. Platform detection must run before bootstrap so state zones
18
- // hydrate from the right backend, and local-owner environments
19
- // auto-elevate to admin.
19
+ var _a, _b, _c;
20
+ const sUrl = (_a = config === null || config === void 0 ? void 0 : config.serverUrl) !== null && _a !== void 0 ? _a : '';
21
+ // 1. Platform detection
20
22
  const platform = await resolvePlatform();
21
23
  if (platform.backends) {
22
24
  __setBackend('workspace', platform.backends.workspace);
@@ -25,15 +27,52 @@ export async function createShell(config) {
25
27
  if (platform.localOwner) {
26
28
  setLocalOwner();
27
29
  }
28
- // 1d. Default env state to same-origin. The server frontend overrides
29
- // this if it knows a specific URL, but this ensures env() works for
30
- // same-origin deployments without explicit configuration.
31
- __setEnvServerUrl('');
32
- // 1c. Apply persisted theme token overrides before any component mounts,
33
- // so the first frame renders with the user's chosen theme.
30
+ __setEnvServerUrl(sUrl);
34
31
  hydrateTokenOverrides();
35
- // 1b. Load server-discovered packages (fetched by frontend from /api/packages).
36
- if ((_a = config === null || config === void 0 ? void 0 : config.discoveredPackages) === null || _a === void 0 ? void 0 : _a.length) {
32
+ // 2. Resolve mount target early (needed for both sign-in wall and shell)
33
+ const target = typeof (config === null || config === void 0 ? void 0 : config.target) === 'string'
34
+ ? document.querySelector(config.target)
35
+ : (_b = config === null || config === void 0 ? void 0 : config.target) !== null && _b !== void 0 ? _b : document.getElementById('app');
36
+ if (!target) {
37
+ throw new Error('SH3: mount target not found');
38
+ }
39
+ // 3. Fetch boot config (skip for local-owner platforms like Tauri/dev)
40
+ let bootConfig = null;
41
+ if (!platform.localOwner) {
42
+ try {
43
+ const res = await fetch(`${sUrl}/api/boot`, { credentials: 'include' });
44
+ if (res.ok) {
45
+ bootConfig = await res.json();
46
+ }
47
+ }
48
+ catch (_d) {
49
+ // Server unreachable — boot without auth (offline mode)
50
+ }
51
+ }
52
+ // 4. Auth decision point
53
+ if (platform.localOwner) {
54
+ // Local-owner (Tauri/dev): no auth, no sign-in, tenant is 'local'.
55
+ // setLocalOwner() already called above — admin is assumed.
56
+ __setTenantId('local');
57
+ }
58
+ else if (bootConfig) {
59
+ initFromBoot(sUrl, bootConfig);
60
+ __setTenantId(bootConfig.tenantId);
61
+ const { auth, session } = bootConfig;
62
+ // Hard gate: no session, auth required, no guest allowed → sign-in wall
63
+ if (!session && auth.required && !auth.guestAllowed) {
64
+ await showSignInWall(target, sUrl, bootConfig);
65
+ // After successful sign-in, re-fetch boot config
66
+ const res = await fetch(`${sUrl}/api/boot`, { credentials: 'include' });
67
+ if (res.ok) {
68
+ bootConfig = await res.json();
69
+ initFromBoot(sUrl, bootConfig);
70
+ __setTenantId(bootConfig.tenantId);
71
+ }
72
+ }
73
+ }
74
+ // 5. Load server-discovered packages
75
+ if ((_c = config === null || config === void 0 ? void 0 : config.discoveredPackages) === null || _c === void 0 ? void 0 : _c.length) {
37
76
  const { loadBundleModule } = await import('./registry/loader');
38
77
  for (const pkg of config.discoveredPackages) {
39
78
  try {
@@ -55,33 +94,39 @@ export async function createShell(config) {
55
94
  }
56
95
  }
57
96
  }
58
- // 2. Register consumer-provided shards and apps. These go in before
59
- // bootstrap() so they appear in registeredShards, but framework
60
- // shards activate first (insertion-order guarantee in bootstrap).
97
+ // 6. Register consumer-provided shards and apps
61
98
  if (config === null || config === void 0 ? void 0 : config.shards) {
62
- for (const shard of config.shards) {
99
+ for (const shard of config.shards)
63
100
  registerShard(shard);
64
- }
65
101
  }
66
102
  if (config === null || config === void 0 ? void 0 : config.apps) {
67
- for (const app of config.apps) {
103
+ for (const app of config.apps)
68
104
  registerApp(app);
69
- }
70
105
  }
71
- // 3. Bootstrap — framework shards/apps registered internally,
72
- // filtered by the exclude lists.
106
+ // 7. Bootstrap
73
107
  const bootstrapConfig = {};
74
108
  if (config === null || config === void 0 ? void 0 : config.excludeShards)
75
109
  bootstrapConfig.excludeShards = config.excludeShards;
76
- if (config === null || config === void 0 ? void 0 : config.excludeApps)
77
- bootstrapConfig.excludeApps = config.excludeApps;
78
110
  await bootstrap(bootstrapConfig);
79
- // 4. Mount the shell.
80
- const target = typeof (config === null || config === void 0 ? void 0 : config.target) === 'string'
81
- ? document.querySelector(config.target)
82
- : (_b = config === null || config === void 0 ? void 0 : config.target) !== null && _b !== void 0 ? _b : document.getElementById('app');
83
- if (!target) {
84
- throw new Error('SH3: mount target not found');
85
- }
111
+ // 8. Mount the shell
86
112
  mount(Shell, { target });
87
113
  }
114
+ /**
115
+ * Show the sign-in wall and wait until the user authenticates.
116
+ * Returns a promise that resolves after successful login.
117
+ */
118
+ function showSignInWall(target, serverUrl, bootConfig) {
119
+ return new Promise((resolve) => {
120
+ const instance = mount(SignInWall, {
121
+ target,
122
+ props: {
123
+ serverUrl,
124
+ selfRegistration: bootConfig.auth.selfRegistration,
125
+ onSuccess: () => {
126
+ unmount(instance);
127
+ resolve();
128
+ },
129
+ },
130
+ });
131
+ });
132
+ }
@@ -73,7 +73,7 @@
73
73
  background: var(--shell-accent-muted);
74
74
  color: var(--shell-fg);
75
75
  border: 1px solid var(--shell-border-strong);
76
- border-radius: 3px;
76
+ border-radius: var(--shell-radius-sm);
77
77
  cursor: pointer;
78
78
  }
79
79
  button:hover { background: var(--shell-accent); }
@@ -7,6 +7,7 @@ export { HttpDocumentBackend } from './documents/http-backend';
7
7
  export { __setEnvServerUrl } from './env/index';
8
8
  export { installPackage, uninstallPackage, listInstalledPackages, loadInstalledPackages, } from './registry/index';
9
9
  export type { InstalledPackage, InstallResult, PackageMeta } from './registry/types';
10
- export { initAuth, elevate, deescalate } from './auth/index';
10
+ export { initFromBoot, login, logout, register, setLocalOwner as setLocalOwnerAuth } from './auth/index';
11
+ export type { AuthUser, AuthSession, BootConfig, GlobalSettings } from './auth/types';
11
12
  export { createShell } from './createShell';
12
13
  export type { ShellConfig } from './createShell';
@@ -11,7 +11,7 @@ export { HttpDocumentBackend } from './documents/http-backend';
11
11
  export { __setEnvServerUrl } from './env/index';
12
12
  // Install API (host-only).
13
13
  export { installPackage, uninstallPackage, listInstalledPackages, loadInstalledPackages, } from './registry/index';
14
- // Admin mode (host-only — elevate/deescalate drive the shell UI, initAuth runs at boot).
15
- export { initAuth, elevate, deescalate } from './auth/index';
14
+ // Auth (host-only — session lifecycle, boot initialization).
15
+ export { initFromBoot, login, logout, register, setLocalOwner as setLocalOwnerAuth } from './auth/index';
16
16
  // Shell boot factory.
17
17
  export { createShell } from './createShell';
package/dist/host.d.ts CHANGED
@@ -10,8 +10,6 @@ export { registerApp };
10
10
  export interface BootstrapConfig {
11
11
  /** Framework shard IDs to skip registration for */
12
12
  excludeShards?: string[];
13
- /** Framework app IDs to skip registration for */
14
- excludeApps?: string[];
15
13
  }
16
14
  export declare function bootstrap(config?: BootstrapConfig): Promise<void>;
17
15
  export { installPackage, listInstalledPackages } from './registry/installer';