sh3-core 0.5.4 → 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.

@@ -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;
@@ -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 {};