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
@@ -77,4 +77,17 @@ export interface App {
77
77
  activate?(ctx: AppContext): void | Promise<void>;
78
78
  /** Optional hook called before the app's shards are deactivated and the layout is detached. */
79
79
  deactivate?(): void | Promise<void>;
80
+ /**
81
+ * Called when the user navigates to Home while the app is active. The
82
+ * app's shards, state, and pooled views remain alive. Return `false`
83
+ * (sync or async) to cancel the navigation — useful for "unsaved
84
+ * changes" confirmation modals.
85
+ */
86
+ suspend?(): void | false | Promise<void | false>;
87
+ /**
88
+ * Called when the app is re-entered from Home (root swap back to app).
89
+ * Does NOT fire on first launch — that is `activate`. Receives the
90
+ * same `AppContext` that `activate` received.
91
+ */
92
+ resume?(ctx: AppContext): void | Promise<void>;
80
93
  }
@@ -0,0 +1,144 @@
1
+ <script lang="ts">
2
+ /**
3
+ * GuestBanner — persistent bar shown when browsing as guest.
4
+ * Clicking "Sign in" opens a modal-like overlay with the sign-in form.
5
+ */
6
+
7
+ import { isGuest, login, register } from './index';
8
+
9
+ let showSignIn = $state(false);
10
+ let username = $state('');
11
+ let password = $state('');
12
+ let error = $state<string | null>(null);
13
+ let loading = $state(false);
14
+
15
+ async function handleLogin() {
16
+ if (!username.trim() || !password.trim() || loading) return;
17
+ loading = true;
18
+ error = null;
19
+ const result = await login(username.trim(), password.trim());
20
+ loading = false;
21
+ if (result.ok) {
22
+ showSignIn = false;
23
+ } else {
24
+ error = result.error;
25
+ }
26
+ }
27
+ </script>
28
+
29
+ <div class="guest-banner-slot">
30
+ {#if isGuest()}
31
+ <div class="guest-banner">
32
+ <span class="guest-banner-text">Browsing as guest. Sign in to save your work.</span>
33
+ <button type="button" class="guest-banner-action" onclick={() => { showSignIn = true; }}>
34
+ Sign in
35
+ </button>
36
+ </div>
37
+
38
+ {#if showSignIn}
39
+ <div class="guest-signin-overlay" role="dialog">
40
+ <div class="guest-signin-card">
41
+ <form class="guest-signin-form" onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
42
+ <input class="guest-signin-input" type="text" placeholder="Username" bind:value={username} disabled={loading} autocomplete="username" />
43
+ <input class="guest-signin-input" type="password" placeholder="Password" bind:value={password} disabled={loading} autocomplete="current-password" />
44
+ <div class="guest-signin-actions">
45
+ <button type="submit" class="guest-signin-btn" disabled={loading || !username.trim() || !password.trim()}>
46
+ {loading ? 'Signing in...' : 'Sign in'}
47
+ </button>
48
+ <button type="button" class="guest-signin-cancel" onclick={() => { showSignIn = false; error = null; }}>
49
+ Cancel
50
+ </button>
51
+ </div>
52
+ </form>
53
+ {#if error}
54
+ <div class="guest-signin-error">{error}</div>
55
+ {/if}
56
+ </div>
57
+ </div>
58
+ {/if}
59
+ {/if}
60
+ </div>
61
+
62
+ <style>
63
+ .guest-banner {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ gap: 12px;
68
+ padding: 6px var(--shell-pad-md, 12px);
69
+ background: color-mix(in srgb, var(--shell-accent, #7c7cf0) 15%, transparent);
70
+ border-bottom: 1px solid var(--shell-border, #3a3a5c);
71
+ font-size: 12px;
72
+ color: var(--shell-fg, #e0e0e0);
73
+ }
74
+ .guest-banner-action {
75
+ padding: 3px 10px;
76
+ background: var(--shell-accent, #7c7cf0);
77
+ color: var(--shell-bg, #1a1a2e);
78
+ border: none;
79
+ border-radius: var(--shell-radius, 6px);
80
+ font-size: 11px;
81
+ font-weight: 600;
82
+ cursor: pointer;
83
+ }
84
+ .guest-signin-overlay {
85
+ position: fixed;
86
+ inset: 0;
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ background: rgba(0, 0, 0, 0.5);
91
+ z-index: 9999;
92
+ }
93
+ .guest-signin-card {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 12px;
97
+ padding: 32px;
98
+ background: var(--shell-bg-elevated, #252540);
99
+ border: 1px solid var(--shell-border, #3a3a5c);
100
+ border-radius: var(--shell-radius-lg, 12px);
101
+ min-width: 300px;
102
+ }
103
+ .guest-signin-form {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 10px;
107
+ }
108
+ .guest-signin-input {
109
+ padding: 8px 12px;
110
+ background: var(--shell-bg, #1a1a2e);
111
+ color: var(--shell-fg, #e0e0e0);
112
+ border: 1px solid var(--shell-border, #3a3a5c);
113
+ border-radius: var(--shell-radius, 6px);
114
+ font-size: 13px;
115
+ }
116
+ .guest-signin-input::placeholder { color: var(--shell-fg-muted, #888); }
117
+ .guest-signin-actions { display: flex; gap: 8px; }
118
+ .guest-signin-btn {
119
+ flex: 1;
120
+ padding: 8px;
121
+ background: var(--shell-accent, #7c7cf0);
122
+ color: var(--shell-bg, #1a1a2e);
123
+ border: none;
124
+ border-radius: var(--shell-radius, 6px);
125
+ font-weight: 600;
126
+ cursor: pointer;
127
+ }
128
+ .guest-signin-btn:disabled { opacity: 0.6; cursor: not-allowed; }
129
+ .guest-signin-cancel {
130
+ padding: 8px 12px;
131
+ background: transparent;
132
+ color: var(--shell-fg-subtle, #aaa);
133
+ border: 1px solid var(--shell-border, #3a3a5c);
134
+ border-radius: var(--shell-radius, 6px);
135
+ cursor: pointer;
136
+ }
137
+ .guest-signin-error {
138
+ padding: 6px 10px;
139
+ font-size: 12px;
140
+ color: var(--shell-error, #d32f2f);
141
+ background: color-mix(in srgb, var(--shell-error, #d32f2f) 10%, transparent);
142
+ border-radius: var(--shell-radius, 6px);
143
+ }
144
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const GuestBanner: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type GuestBanner = ReturnType<typeof GuestBanner>;
3
+ export default GuestBanner;
@@ -0,0 +1,213 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SignInWall — standalone sign-in screen shown before shell boots.
4
+ * Mounted directly to the target element by createShell().
5
+ */
6
+
7
+ import { login, register } from './index';
8
+
9
+ interface Props {
10
+ serverUrl: string;
11
+ selfRegistration: boolean;
12
+ onSuccess: () => void;
13
+ }
14
+
15
+ let { serverUrl, selfRegistration, onSuccess }: Props = $props();
16
+
17
+ let mode = $state<'login' | 'register'>('login');
18
+ let username = $state('');
19
+ let password = $state('');
20
+ let displayName = $state('');
21
+ let error = $state<string | null>(null);
22
+ let loading = $state(false);
23
+
24
+ async function handleLogin() {
25
+ if (!username.trim() || !password.trim() || loading) return;
26
+ loading = true;
27
+ error = null;
28
+ const result = await login(username.trim(), password.trim());
29
+ loading = false;
30
+ if (result.ok) {
31
+ onSuccess();
32
+ } else {
33
+ error = result.error;
34
+ }
35
+ }
36
+
37
+ async function handleRegister() {
38
+ if (!username.trim() || !password.trim() || loading) return;
39
+ loading = true;
40
+ error = null;
41
+ const result = await register(
42
+ username.trim(),
43
+ password.trim(),
44
+ displayName.trim() || undefined,
45
+ );
46
+ loading = false;
47
+ if (result.ok) {
48
+ onSuccess();
49
+ } else {
50
+ error = result.error;
51
+ }
52
+ }
53
+
54
+ function switchMode(m: 'login' | 'register') {
55
+ mode = m;
56
+ error = null;
57
+ }
58
+ </script>
59
+
60
+ <div class="signin-wall">
61
+ <div class="signin-card">
62
+ <h1 class="signin-brand">SH3</h1>
63
+
64
+ {#if mode === 'login'}
65
+ <form class="signin-form" onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
66
+ <input
67
+ class="signin-input"
68
+ type="text"
69
+ placeholder="Username"
70
+ bind:value={username}
71
+ disabled={loading}
72
+ autocomplete="username"
73
+ />
74
+ <input
75
+ class="signin-input"
76
+ type="password"
77
+ placeholder="Password"
78
+ bind:value={password}
79
+ disabled={loading}
80
+ autocomplete="current-password"
81
+ />
82
+ <button type="submit" class="signin-btn" disabled={loading || !username.trim() || !password.trim()}>
83
+ {loading ? 'Signing in...' : 'Sign in'}
84
+ </button>
85
+ </form>
86
+ {#if selfRegistration}
87
+ <button type="button" class="signin-link" onclick={() => switchMode('register')}>
88
+ Create an account
89
+ </button>
90
+ {/if}
91
+ {:else}
92
+ <form class="signin-form" onsubmit={(e) => { e.preventDefault(); handleRegister(); }}>
93
+ <input
94
+ class="signin-input"
95
+ type="text"
96
+ placeholder="Username"
97
+ bind:value={username}
98
+ disabled={loading}
99
+ autocomplete="username"
100
+ />
101
+ <input
102
+ class="signin-input"
103
+ type="text"
104
+ placeholder="Display name (optional)"
105
+ bind:value={displayName}
106
+ disabled={loading}
107
+ />
108
+ <input
109
+ class="signin-input"
110
+ type="password"
111
+ placeholder="Password"
112
+ bind:value={password}
113
+ disabled={loading}
114
+ autocomplete="new-password"
115
+ />
116
+ <button type="submit" class="signin-btn" disabled={loading || !username.trim() || !password.trim()}>
117
+ {loading ? 'Creating...' : 'Create account'}
118
+ </button>
119
+ </form>
120
+ <button type="button" class="signin-link" onclick={() => switchMode('login')}>
121
+ Back to sign in
122
+ </button>
123
+ {/if}
124
+
125
+ {#if error}
126
+ <div class="signin-error">{error}</div>
127
+ {/if}
128
+ </div>
129
+ </div>
130
+
131
+ <style>
132
+ .signin-wall {
133
+ position: absolute;
134
+ inset: 0;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ background: var(--shell-grad-bg, var(--shell-bg, #1a1a2e));
139
+ color: var(--shell-fg, #e0e0e0);
140
+ font-family: system-ui, sans-serif;
141
+ }
142
+ .signin-card {
143
+ display: flex;
144
+ flex-direction: column;
145
+ align-items: center;
146
+ gap: 16px;
147
+ padding: 48px 40px;
148
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated, #252540));
149
+ border: 1px solid var(--shell-border, #3a3a5c);
150
+ border-radius: var(--shell-radius-lg, 12px);
151
+ min-width: 320px;
152
+ }
153
+ .signin-brand {
154
+ margin: 0 0 8px;
155
+ font-size: 42px;
156
+ color: var(--shell-accent, #7c7cf0);
157
+ letter-spacing: 2px;
158
+ }
159
+ .signin-form {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 12px;
163
+ width: 100%;
164
+ }
165
+ .signin-input {
166
+ padding: 10px 14px;
167
+ background: var(--shell-bg, #1a1a2e);
168
+ color: var(--shell-fg, #e0e0e0);
169
+ border: 1px solid var(--shell-border, #3a3a5c);
170
+ border-radius: var(--shell-radius, 6px);
171
+ font-size: 14px;
172
+ }
173
+ .signin-input::placeholder {
174
+ color: var(--shell-fg-muted, #888);
175
+ }
176
+ .signin-btn {
177
+ padding: 10px 16px;
178
+ background: var(--shell-accent, #7c7cf0);
179
+ color: var(--shell-bg, #1a1a2e);
180
+ border: none;
181
+ border-radius: var(--shell-radius, 6px);
182
+ font-weight: 600;
183
+ font-size: 14px;
184
+ cursor: pointer;
185
+ }
186
+ .signin-btn:disabled {
187
+ opacity: 0.6;
188
+ cursor: not-allowed;
189
+ }
190
+ .signin-btn:hover:not(:disabled) {
191
+ filter: brightness(1.1);
192
+ }
193
+ .signin-link {
194
+ background: none;
195
+ border: none;
196
+ color: var(--shell-accent, #7c7cf0);
197
+ cursor: pointer;
198
+ font-size: 13px;
199
+ padding: 0;
200
+ }
201
+ .signin-link:hover {
202
+ text-decoration: underline;
203
+ }
204
+ .signin-error {
205
+ padding: 8px 12px;
206
+ font-size: 13px;
207
+ color: var(--shell-error, #d32f2f);
208
+ background: color-mix(in srgb, var(--shell-error, #d32f2f) 10%, transparent);
209
+ border-radius: var(--shell-radius, 6px);
210
+ width: 100%;
211
+ text-align: center;
212
+ }
213
+ </style>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ serverUrl: string;
3
+ selfRegistration: boolean;
4
+ onSuccess: () => void;
5
+ }
6
+ declare const SignInWall: import("svelte").Component<Props, {}, "">;
7
+ type SignInWall = ReturnType<typeof SignInWall>;
8
+ export default SignInWall;
@@ -1,50 +1,61 @@
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
  */
11
+ import type { AuthUser, AuthSession, BootConfig } from './types';
15
12
  /**
16
- * Initialize auth at boot. Call once from bootstrap().
17
- *
18
- * If a key is stored from a previous session, verifies it against the
19
- * server with a 3-second timeout. If verification fails (key revoked,
20
- * server down, timeout), the shell boots without admin access — the
21
- * stored key is cleared only on explicit rejection (401), not on
22
- * network failure (so a temporary outage doesn't force re-entry).
23
- *
24
- * @param url - Server base URL ('' for same-origin).
13
+ * Initialize auth from boot config. Called once by createShell()
14
+ * after fetching /api/boot.
25
15
  */
26
- export declare function initAuth(url?: string): Promise<void>;
16
+ export declare function initFromBoot(url: string, config: BootConfig): void;
27
17
  /**
28
- * Elevate to admin mode with an API key. Verifies against the server
29
- * before storing. Returns true on success, false if the key is invalid.
18
+ * Log in with username + password. On success, updates reactive state.
19
+ * Returns { ok: true } or { ok: false, error: string }.
30
20
  */
31
- export declare function elevate(key: string): Promise<boolean>;
21
+ export declare function login(username: string, password: string): Promise<{
22
+ ok: true;
23
+ } | {
24
+ ok: false;
25
+ error: string;
26
+ }>;
32
27
  /**
33
- * De-escalate from admin mode clear the stored key and drop elevation.
28
+ * Register a new account (when self-registration is enabled).
29
+ * On success, auto-logs in and updates reactive state.
34
30
  */
35
- export declare function deescalate(): void;
31
+ export declare function register(username: string, password: string, displayName?: string): Promise<{
32
+ ok: true;
33
+ } | {
34
+ ok: false;
35
+ error: string;
36
+ }>;
36
37
  /**
37
- * Reactive gettertrue when the user has elevated to admin mode.
38
+ * Log outclear session on server and client.
38
39
  */
39
- export declare function isAdmin(): boolean;
40
+ export declare function logout(): Promise<void>;
40
41
  /**
41
42
  * Mark this session as local-owner — auto-elevate to admin without
42
- * key verification. Called by the host in Tauri / dev environments
43
- * where the user owns the machine. Idempotent.
43
+ * server verification. Called by the host in Tauri / dev environments.
44
44
  */
45
45
  export declare function setLocalOwner(): void;
46
+ /** Reactive — true when the user has admin role. */
47
+ export declare function isAdmin(): boolean;
48
+ /** Reactive — true when the user has a valid session. */
49
+ export declare function isAuthenticated(): boolean;
50
+ /** Reactive — true when browsing without a session. */
51
+ export declare function isGuest(): boolean;
52
+ /** Get the current user (reactive). */
53
+ export declare function getUser(): AuthUser | null;
54
+ /** Get the current session (reactive). */
55
+ export declare function getSession(): AuthSession | null;
46
56
  /**
47
- * Build an Authorization header value for authenticated fetch calls.
48
- * Returns null if not elevated.
57
+ * Build an Authorization header value for authenticated fetch calls
58
+ * that need explicit headers (e.g. non-cookie contexts).
59
+ * Returns null if not authenticated.
49
60
  */
50
61
  export declare function getAuthHeader(): string | null;