sh3-core 0.17.2 → 0.19.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.
- package/dist/Sh3.svelte +59 -4
- package/dist/actions/CommandPalette.svelte +1 -2
- package/dist/actions/listeners.js +12 -1
- package/dist/api.d.ts +4 -0
- package/dist/app/store/storeShard.svelte.js +1 -21
- package/dist/app/store/version.d.ts +11 -0
- package/dist/app/store/version.js +39 -0
- package/dist/app/store/version.test.d.ts +1 -0
- package/dist/app/store/version.test.js +44 -0
- package/dist/apps/lifecycle.d.ts +6 -0
- package/dist/apps/lifecycle.js +5 -2
- package/dist/apps/lifecycle.test.js +30 -0
- package/dist/apps/types.d.ts +12 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +5 -0
- package/dist/assets/icons.svg +31 -0
- package/dist/auth/auth.svelte.js +18 -8
- package/dist/auth/types.d.ts +6 -0
- package/dist/chrome/CompactChrome.svelte +54 -20
- package/dist/chrome/CompactChrome.svelte.test.js +112 -5
- package/dist/createShell.d.ts +9 -0
- package/dist/createShell.js +20 -7
- package/dist/createShell.remoteAuth.test.d.ts +1 -0
- package/dist/createShell.remoteAuth.test.js +71 -0
- package/dist/documents/http-backend.js +12 -11
- package/dist/env/client.js +11 -5
- package/dist/files/types.d.ts +106 -0
- package/dist/files/types.js +1 -0
- package/dist/gestures/gestureRegistry.d.ts +6 -0
- package/dist/gestures/gestureRegistry.js +190 -0
- package/dist/gestures/gestureRegistry.test.d.ts +1 -0
- package/dist/gestures/gestureRegistry.test.js +120 -0
- package/dist/gestures/index.d.ts +6 -0
- package/dist/gestures/index.js +12 -0
- package/dist/gestures/pointerClaim.d.ts +7 -0
- package/dist/gestures/pointerClaim.js +36 -0
- package/dist/gestures/pointerClaim.test.d.ts +1 -0
- package/dist/gestures/pointerClaim.test.js +64 -0
- package/dist/gestures/types.d.ts +83 -0
- package/dist/gestures/types.js +1 -0
- package/dist/host-entry.d.ts +1 -0
- package/dist/host-entry.js +1 -0
- package/dist/layout/LayoutRenderer.browser.test.js +15 -3
- package/dist/layout/LayoutRenderer.svelte +16 -3
- package/dist/layout/LayoutRenderer.svelte.d.ts +2 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-3-splitter-drag-updates-split-sizes-when-the-splitter-handle-is-dragged-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-hides-the-collapse-widget-on-a-fixed-pane-but-keeps-it-on-panes-with-a-non-fixed-neighbor-1.png +0 -0
- package/dist/layout/compact/CarouselTabs.svelte +362 -0
- package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
- package/dist/layout/compact/CompactRenderer.svelte +1 -1
- package/dist/layout/compact/CompactRenderer.svelte.test.js +49 -0
- package/dist/layout/compact/derive.js +2 -0
- package/dist/layout/compact/derive.test.js +37 -0
- package/dist/layout/compact/enrichCarousels.d.ts +8 -0
- package/dist/layout/compact/enrichCarousels.js +44 -0
- package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
- package/dist/layout/compact/enrichCarousels.test.js +88 -0
- package/dist/layout/compact/types.d.ts +3 -0
- package/dist/layout/drag.svelte.js +13 -0
- package/dist/layout/store.schemaVersion.test.js +2 -2
- package/dist/layout/types.d.ts +9 -1
- package/dist/layout/types.js +1 -1
- package/dist/layout/types.test.d.ts +1 -0
- package/dist/layout/types.test.js +26 -0
- package/dist/overlays/ModalFrame.svelte +3 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- package/dist/overlays/floatDismiss.js +5 -0
- package/dist/overlays/focusTrap.d.ts +11 -1
- package/dist/overlays/focusTrap.js +11 -9
- package/dist/overlays/modal.js +1 -0
- package/dist/overlays/popup.js +4 -0
- package/dist/overlays/types.d.ts +9 -0
- package/dist/primitives/Button.svelte +18 -0
- package/dist/primitives/Button.svelte.d.ts +6 -0
- package/dist/primitives/ResizableSplitter.svelte +71 -11
- package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
- package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
- package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
- package/dist/server-shard/types.d.ts +2 -1
- package/dist/shards/activate.svelte.js +16 -0
- package/dist/shards/ctx-fetch.test.d.ts +1 -0
- package/dist/shards/ctx-fetch.test.js +136 -0
- package/dist/shards/types.d.ts +29 -0
- package/dist/transport/apiFetch.d.ts +1 -0
- package/dist/transport/apiFetch.js +65 -0
- package/dist/transport/apiFetch.test.d.ts +1 -0
- package/dist/transport/apiFetch.test.js +37 -0
- package/dist/transport/authToken.d.ts +2 -0
- package/dist/transport/authToken.js +53 -0
- package/dist/transport/authToken.test.d.ts +1 -0
- package/dist/transport/authToken.test.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/*
|
|
3
3
|
* Top app bar for compact mode. Three-column grid:
|
|
4
|
-
* leading — one
|
|
4
|
+
* leading — one Button per non-null drawer anchor (read from
|
|
5
5
|
* the active CompactRendering)
|
|
6
6
|
* title — active app name
|
|
7
7
|
* trailing — palette button + overflow (menu sheet) button
|
|
@@ -10,21 +10,49 @@
|
|
|
10
10
|
* state and renders MenuSheet conditionally.
|
|
11
11
|
*/
|
|
12
12
|
import { sh3 } from '../sh3Runtime.svelte';
|
|
13
|
-
import { layoutStore } from '../layout/store.svelte';
|
|
13
|
+
import { layoutStore, getActiveRoot } from '../layout/store.svelte';
|
|
14
14
|
import { derive } from '../layout/compact/derive';
|
|
15
15
|
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
16
16
|
import { getRegisteredApp } from '../apps/registry.svelte';
|
|
17
|
+
import { returnToHome } from '../apps/lifecycle';
|
|
18
|
+
import Button from '../primitives/Button.svelte';
|
|
17
19
|
import MenuSheet from './MenuSheet.svelte';
|
|
18
20
|
import type { DrawerAnchor } from '../layout/compact/types';
|
|
19
21
|
|
|
20
22
|
const rendering = $derived(derive(layoutStore.root));
|
|
21
23
|
const dispatcher = $derived(getLiveDispatcherState());
|
|
22
|
-
const
|
|
24
|
+
const onHome = $derived(getActiveRoot() === 'home');
|
|
25
|
+
const appLabel = $derived.by(() => {
|
|
23
26
|
const id = dispatcher.activeAppId;
|
|
24
27
|
if (!id) return 'SH3';
|
|
25
28
|
return getRegisteredApp(id)?.manifest.label ?? id;
|
|
26
29
|
});
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Pick the topmost carousel — the one with the lexicographically smallest
|
|
33
|
+
* path key. `''` (root) sorts before `'0'` etc.; among siblings, smaller
|
|
34
|
+
* indices sort first. This deterministically selects the spatially-topmost
|
|
35
|
+
* full-width tab group when multiple carousels are stacked vertically.
|
|
36
|
+
*/
|
|
37
|
+
const topmostCarouselLabel = $derived.by(() => {
|
|
38
|
+
if (rendering.carousels.size === 0) return null;
|
|
39
|
+
let bestKey: string | null = null;
|
|
40
|
+
let bestLabel = '';
|
|
41
|
+
for (const [key, info] of rendering.carousels) {
|
|
42
|
+
if (bestKey === null || key < bestKey) {
|
|
43
|
+
bestKey = key;
|
|
44
|
+
bestLabel = info.activeLabel;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return bestLabel;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const title = $derived.by(() => {
|
|
51
|
+
const carouselLabel = topmostCarouselLabel;
|
|
52
|
+
if (carouselLabel) return `${appLabel} › ${carouselLabel}`;
|
|
53
|
+
return appLabel;
|
|
54
|
+
});
|
|
55
|
+
|
|
28
56
|
let menuOpen = $state(false);
|
|
29
57
|
|
|
30
58
|
function toggleDrawer(anchor: DrawerAnchor) {
|
|
@@ -37,20 +65,34 @@
|
|
|
37
65
|
|
|
38
66
|
<header class="sh3-compact-chrome" data-sh3-region="compact-chrome">
|
|
39
67
|
<div class="leading">
|
|
68
|
+
<Button
|
|
69
|
+
variant="icon"
|
|
70
|
+
icon="house"
|
|
71
|
+
ariaLabel="Home"
|
|
72
|
+
title="Home"
|
|
73
|
+
disabled={onHome}
|
|
74
|
+
onclick={() => returnToHome()}
|
|
75
|
+
/>
|
|
40
76
|
{#if rendering.drawers.left}
|
|
41
|
-
<
|
|
77
|
+
<span data-sh3-anchor="left">
|
|
78
|
+
<Button variant="icon" icon="menu" ariaLabel="Toggle left drawer" title="Toggle left drawer" onclick={() => toggleDrawer('left')} />
|
|
79
|
+
</span>
|
|
42
80
|
{/if}
|
|
43
81
|
{#if rendering.drawers.right}
|
|
44
|
-
<
|
|
82
|
+
<span data-sh3-anchor="right">
|
|
83
|
+
<Button variant="icon" icon="panel-right" ariaLabel="Toggle right drawer" title="Toggle right drawer" onclick={() => toggleDrawer('right')} />
|
|
84
|
+
</span>
|
|
45
85
|
{/if}
|
|
46
86
|
{#if rendering.drawers.top}
|
|
47
|
-
<
|
|
87
|
+
<span data-sh3-anchor="top">
|
|
88
|
+
<Button variant="icon" icon="panel-top" ariaLabel="Toggle top drawer" title="Toggle top drawer" onclick={() => toggleDrawer('top')} />
|
|
89
|
+
</span>
|
|
48
90
|
{/if}
|
|
49
91
|
</div>
|
|
50
92
|
<div class="title">{title}</div>
|
|
51
93
|
<div class="trailing">
|
|
52
|
-
<
|
|
53
|
-
<
|
|
94
|
+
<Button variant="icon" icon="command" ariaLabel="Open command palette" title="Open command palette" onclick={openPalette} />
|
|
95
|
+
<Button variant="icon" icon="ellipsis-vertical" ariaLabel="Open menu" title="Open menu" onclick={() => { menuOpen = true; }} />
|
|
54
96
|
</div>
|
|
55
97
|
</header>
|
|
56
98
|
|
|
@@ -71,20 +113,12 @@
|
|
|
71
113
|
.leading,
|
|
72
114
|
.trailing {
|
|
73
115
|
display: inline-flex;
|
|
116
|
+
align-items: center;
|
|
74
117
|
gap: var(--sh3-pad-xs);
|
|
75
118
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
font-size: var(--sh3-font-lg);
|
|
80
|
-
border: none;
|
|
81
|
-
background: none;
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
border-radius: var(--sh3-radius-sm);
|
|
84
|
-
color: var(--sh3-fg);
|
|
85
|
-
}
|
|
86
|
-
button:active {
|
|
87
|
-
background: var(--sh3-bg-sunken);
|
|
119
|
+
.leading > span {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
88
122
|
}
|
|
89
123
|
.title {
|
|
90
124
|
font-weight: 600;
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
7
|
import { mount, unmount, flushSync } from 'svelte';
|
|
8
8
|
import CompactChrome from './CompactChrome.svelte';
|
|
9
|
-
import { __resetLayoutStoreForTest, attachApp, detachApp, switchToApp, } from '../layout/store.svelte';
|
|
9
|
+
import { __resetLayoutStoreForTest, attachApp, detachApp, switchToApp, switchToHome, } from '../layout/store.svelte';
|
|
10
10
|
import { drawerStore } from '../layout/compact/drawerStore.svelte';
|
|
11
|
+
import { setActiveApp, __resetDispatcherStateForTest } from '../actions/state.svelte';
|
|
12
|
+
import { registerApp, __resetAppRegistryForTest } from '../apps/registry.svelte';
|
|
11
13
|
const CompactChromeAny = CompactChrome;
|
|
12
14
|
function fakeApp() {
|
|
13
15
|
return {
|
|
@@ -34,11 +36,22 @@ afterEach(() => {
|
|
|
34
36
|
host = null;
|
|
35
37
|
}
|
|
36
38
|
detachApp();
|
|
39
|
+
__resetAppRegistryForTest();
|
|
40
|
+
__resetDispatcherStateForTest();
|
|
37
41
|
});
|
|
38
42
|
beforeEach(() => {
|
|
39
43
|
__resetLayoutStoreForTest();
|
|
40
44
|
drawerStore.__reset();
|
|
45
|
+
__resetAppRegistryForTest();
|
|
46
|
+
__resetDispatcherStateForTest();
|
|
41
47
|
});
|
|
48
|
+
function attachAndActivate(app) {
|
|
49
|
+
var _a;
|
|
50
|
+
registerApp(app);
|
|
51
|
+
attachApp(app);
|
|
52
|
+
switchToApp();
|
|
53
|
+
setActiveApp(app.manifest.id, new Set((_a = app.manifest.requiredShards) !== null && _a !== void 0 ? _a : []));
|
|
54
|
+
}
|
|
42
55
|
describe('CompactChrome (dom)', () => {
|
|
43
56
|
it('renders a leading toggle for each present drawer anchor', () => {
|
|
44
57
|
attachApp(fakeApp());
|
|
@@ -48,10 +61,9 @@ describe('CompactChrome (dom)', () => {
|
|
|
48
61
|
document.body.appendChild(host);
|
|
49
62
|
mounted = mount(CompactChromeAny, { target: host });
|
|
50
63
|
flushSync();
|
|
51
|
-
|
|
52
|
-
expect(leading
|
|
53
|
-
expect(host.querySelector('.leading
|
|
54
|
-
expect(host.querySelector('.leading button[data-sh3-anchor="right"]')).not.toBeNull();
|
|
64
|
+
expect(host.querySelector('.leading [data-sh3-anchor="left"] button')).not.toBeNull();
|
|
65
|
+
expect(host.querySelector('.leading [data-sh3-anchor="right"] button')).not.toBeNull();
|
|
66
|
+
expect(host.querySelector('.leading [data-sh3-anchor="top"] button')).toBeNull();
|
|
55
67
|
});
|
|
56
68
|
it('renders palette + overflow buttons in the trailing section', () => {
|
|
57
69
|
attachApp(fakeApp());
|
|
@@ -65,3 +77,98 @@ describe('CompactChrome (dom)', () => {
|
|
|
65
77
|
expect(trailing.length).toBe(2);
|
|
66
78
|
});
|
|
67
79
|
});
|
|
80
|
+
describe('CompactChrome — home button', () => {
|
|
81
|
+
it('renders a home button as the first leading item', () => {
|
|
82
|
+
attachApp(fakeApp());
|
|
83
|
+
switchToApp();
|
|
84
|
+
flushSync();
|
|
85
|
+
host = document.createElement('div');
|
|
86
|
+
document.body.appendChild(host);
|
|
87
|
+
mounted = mount(CompactChromeAny, { target: host });
|
|
88
|
+
flushSync();
|
|
89
|
+
const homeBtn = host.querySelector('.leading button[aria-label="Home"]');
|
|
90
|
+
expect(homeBtn).not.toBeNull();
|
|
91
|
+
expect(homeBtn === null || homeBtn === void 0 ? void 0 : homeBtn.hasAttribute('disabled')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it('disables the home button when on home', () => {
|
|
94
|
+
switchToHome();
|
|
95
|
+
flushSync();
|
|
96
|
+
host = document.createElement('div');
|
|
97
|
+
document.body.appendChild(host);
|
|
98
|
+
mounted = mount(CompactChromeAny, { target: host });
|
|
99
|
+
flushSync();
|
|
100
|
+
const homeBtn = host.querySelector('.leading button[aria-label="Home"]');
|
|
101
|
+
expect(homeBtn).not.toBeNull();
|
|
102
|
+
expect(homeBtn === null || homeBtn === void 0 ? void 0 : homeBtn.hasAttribute('disabled')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('CompactChrome — breadcrumb', () => {
|
|
106
|
+
it('renders only AppName when no carousel is active', () => {
|
|
107
|
+
var _a;
|
|
108
|
+
const app = {
|
|
109
|
+
manifest: { id: 'plain-app', label: 'Plain App', layoutVersion: 6 },
|
|
110
|
+
initialLayout: { type: 'slot', slotId: 'b', viewId: null, role: 'body' },
|
|
111
|
+
};
|
|
112
|
+
attachAndActivate(app);
|
|
113
|
+
flushSync();
|
|
114
|
+
host = document.createElement('div');
|
|
115
|
+
document.body.appendChild(host);
|
|
116
|
+
mounted = mount(CompactChromeAny, { target: host });
|
|
117
|
+
flushSync();
|
|
118
|
+
const title = host.querySelector('.title');
|
|
119
|
+
expect((_a = title === null || title === void 0 ? void 0 : title.textContent) === null || _a === void 0 ? void 0 : _a.trim()).toBe('Plain App');
|
|
120
|
+
});
|
|
121
|
+
it('renders AppName › ActiveLabel when a carousel is active', () => {
|
|
122
|
+
var _a;
|
|
123
|
+
const app = {
|
|
124
|
+
manifest: { id: 'carousel-app', label: 'Carousel App', layoutVersion: 6 },
|
|
125
|
+
initialLayout: {
|
|
126
|
+
type: 'tabs',
|
|
127
|
+
activeTab: 1,
|
|
128
|
+
tabs: [
|
|
129
|
+
{ slotId: 's0', viewId: null, label: 'First', role: 'body' },
|
|
130
|
+
{ slotId: 's1', viewId: null, label: 'Second', role: 'body' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
attachAndActivate(app);
|
|
135
|
+
flushSync();
|
|
136
|
+
host = document.createElement('div');
|
|
137
|
+
document.body.appendChild(host);
|
|
138
|
+
mounted = mount(CompactChromeAny, { target: host });
|
|
139
|
+
flushSync();
|
|
140
|
+
const title = host.querySelector('.title');
|
|
141
|
+
expect((_a = title === null || title === void 0 ? void 0 : title.textContent) === null || _a === void 0 ? void 0 : _a.trim()).toBe('Carousel App › Second');
|
|
142
|
+
});
|
|
143
|
+
it('with multiple stacked carousels, breadcrumb uses the topmost (lowest path-key sort order)', () => {
|
|
144
|
+
var _a;
|
|
145
|
+
const app = {
|
|
146
|
+
manifest: { id: 'stacked-app', label: 'Stacked', layoutVersion: 6 },
|
|
147
|
+
initialLayout: {
|
|
148
|
+
type: 'split',
|
|
149
|
+
direction: 'vertical',
|
|
150
|
+
sizes: [0.5, 0.5],
|
|
151
|
+
children: [
|
|
152
|
+
{
|
|
153
|
+
type: 'tabs',
|
|
154
|
+
activeTab: 0,
|
|
155
|
+
tabs: [{ slotId: 'top0', viewId: null, label: 'TopActive', role: 'body' }],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'tabs',
|
|
159
|
+
activeTab: 0,
|
|
160
|
+
tabs: [{ slotId: 'bot0', viewId: null, label: 'BottomActive', role: 'body' }],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
attachAndActivate(app);
|
|
166
|
+
flushSync();
|
|
167
|
+
host = document.createElement('div');
|
|
168
|
+
document.body.appendChild(host);
|
|
169
|
+
mounted = mount(CompactChromeAny, { target: host });
|
|
170
|
+
flushSync();
|
|
171
|
+
const title = host.querySelector('.title');
|
|
172
|
+
expect((_a = title === null || title === void 0 ? void 0 : title.textContent) === null || _a === void 0 ? void 0 : _a.trim()).toBe('Stacked › TopActive');
|
|
173
|
+
});
|
|
174
|
+
});
|
package/dist/createShell.d.ts
CHANGED
|
@@ -22,5 +22,14 @@ export interface Sh3Config {
|
|
|
22
22
|
target?: string | HTMLElement;
|
|
23
23
|
/** Server base URL ('' for same-origin) */
|
|
24
24
|
serverUrl?: string;
|
|
25
|
+
/**
|
|
26
|
+
* When true, override the local-owner short-circuit and run the
|
|
27
|
+
* full server-driven auth flow (boot config fetch, SignInWall when
|
|
28
|
+
* required) against `serverUrl`. Used by Tauri clients connecting
|
|
29
|
+
* to a remote sh3-server (e.g. Android), where Tauri presence makes
|
|
30
|
+
* `platform.localOwner` true even though authentication is server-
|
|
31
|
+
* gated.
|
|
32
|
+
*/
|
|
33
|
+
remoteAuth?: boolean;
|
|
25
34
|
}
|
|
26
35
|
export declare function createShell(config?: Sh3Config): Promise<void>;
|
package/dist/createShell.js
CHANGED
|
@@ -10,8 +10,9 @@ import { mount, unmount } from 'svelte';
|
|
|
10
10
|
import { Sh3 } from './index';
|
|
11
11
|
import { registerShard, registerApp, bootstrap, bootstrapSatellite, __setBackend, setLocalOwner, } from './host';
|
|
12
12
|
import { resolvePlatform } from './platform/index';
|
|
13
|
+
import { apiFetch } from './transport/apiFetch';
|
|
13
14
|
import { hydrateTokenOverrides } from './theme';
|
|
14
|
-
import { __setEnvServerUrl } from './env/index';
|
|
15
|
+
import { __setEnvServerUrl, getEnvServerUrl } from './env/index';
|
|
15
16
|
import { __setActiveScope } from './documents/config';
|
|
16
17
|
import { initFromBoot } from './auth/index';
|
|
17
18
|
import SignInWall from './auth/SignInWall.svelte';
|
|
@@ -83,11 +84,13 @@ export async function createShell(config) {
|
|
|
83
84
|
mount(SatelliteShell, { target, props: { payload: satellite.payload } });
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
|
-
// 3. Fetch boot config (skip for local
|
|
87
|
+
// 3. Fetch boot config (skip for purely-local owners; remoteAuth
|
|
88
|
+
// forces it for cross-origin Tauri clients).
|
|
87
89
|
let bootConfig = null;
|
|
88
|
-
|
|
90
|
+
const useServerAuth = !platform.localOwner || (config === null || config === void 0 ? void 0 : config.remoteAuth) === true;
|
|
91
|
+
if (useServerAuth) {
|
|
89
92
|
try {
|
|
90
|
-
const res = await
|
|
93
|
+
const res = await apiFetch(`${sUrl}/api/boot`);
|
|
91
94
|
if (res.ok) {
|
|
92
95
|
bootConfig = await res.json();
|
|
93
96
|
}
|
|
@@ -97,7 +100,7 @@ export async function createShell(config) {
|
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
// 4. Auth decision point
|
|
100
|
-
if (platform.localOwner) {
|
|
103
|
+
if (platform.localOwner && !(config === null || config === void 0 ? void 0 : config.remoteAuth)) {
|
|
101
104
|
// Local-owner (Tauri/dev): no auth, no sign-in, scope is 'local'.
|
|
102
105
|
// setLocalOwner() already called above — admin is assumed.
|
|
103
106
|
__setActiveScope('local');
|
|
@@ -110,7 +113,7 @@ export async function createShell(config) {
|
|
|
110
113
|
if (!session && auth.required && !auth.guestAllowed) {
|
|
111
114
|
await showSignInWall(target, bootConfig);
|
|
112
115
|
// After successful sign-in, re-fetch boot config
|
|
113
|
-
const res = await
|
|
116
|
+
const res = await apiFetch(`${sUrl}/api/boot`);
|
|
114
117
|
if (res.ok) {
|
|
115
118
|
bootConfig = await res.json();
|
|
116
119
|
initFromBoot(sUrl, bootConfig);
|
|
@@ -150,7 +153,17 @@ async function loadDiscoveredPackages(packages) {
|
|
|
150
153
|
return;
|
|
151
154
|
for (const pkg of packages) {
|
|
152
155
|
try {
|
|
153
|
-
|
|
156
|
+
// Server returns server-relative paths like `/packages/<id>/client.js`.
|
|
157
|
+
// In a cross-origin Tauri client these resolve against the webview
|
|
158
|
+
// origin (`tauri://localhost`) instead of the configured server, so
|
|
159
|
+
// we get the SPA's index.html back and the loader chokes on `<`.
|
|
160
|
+
// Resolve against the active serverUrl and route through apiFetch
|
|
161
|
+
// for the cross-origin-safe transport + bearer header.
|
|
162
|
+
const isAbsolute = pkg.bundleUrl.startsWith('http://') || pkg.bundleUrl.startsWith('https://');
|
|
163
|
+
const base = getEnvServerUrl();
|
|
164
|
+
const sep = pkg.bundleUrl.startsWith('/') ? '' : '/';
|
|
165
|
+
const url = isAbsolute ? pkg.bundleUrl : `${base}${sep}${pkg.bundleUrl}`;
|
|
166
|
+
const res = await apiFetch(url);
|
|
154
167
|
if (!res.ok) {
|
|
155
168
|
console.warn(`[sh3] Failed to fetch discovered package "${pkg.id}": HTTP ${res.status}`);
|
|
156
169
|
continue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
describe('createShell remoteAuth flag', () => {
|
|
3
|
+
let originalFetch;
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
originalFetch = globalThis.fetch;
|
|
6
|
+
vi.resetModules();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
globalThis.fetch = originalFetch;
|
|
10
|
+
vi.doUnmock('./platform/index');
|
|
11
|
+
vi.doUnmock('./host');
|
|
12
|
+
});
|
|
13
|
+
function shortCircuitAfterBoot() {
|
|
14
|
+
// Mock bootstrap so createShell throws right after the boot-config
|
|
15
|
+
// fetch we want to observe — and never reaches mount(Sh3, ...).
|
|
16
|
+
vi.doMock('./host', async () => {
|
|
17
|
+
const actual = await vi.importActual('./host');
|
|
18
|
+
return Object.assign(Object.assign({}, actual), { bootstrap: async () => {
|
|
19
|
+
throw new Error('test-skip-bootstrap');
|
|
20
|
+
} });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
it('fetches /api/boot when remoteAuth is true even if localOwner is detected', async () => {
|
|
24
|
+
const calls = [];
|
|
25
|
+
globalThis.fetch = vi.fn(async (input) => {
|
|
26
|
+
calls.push(String(input));
|
|
27
|
+
return new Response(JSON.stringify({
|
|
28
|
+
version: '0.18.0',
|
|
29
|
+
tenantId: 't1',
|
|
30
|
+
auth: { required: false, guestAllowed: true, selfRegistration: false },
|
|
31
|
+
session: { token: 'tok', userId: 'u1', role: 'user', expiresAt: Number.MAX_SAFE_INTEGER },
|
|
32
|
+
user: { id: 'u1', username: 'u', displayName: 'U', role: 'user', createdAt: '', updatedAt: '' },
|
|
33
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
34
|
+
});
|
|
35
|
+
vi.doMock('./platform/index', () => ({
|
|
36
|
+
resolvePlatform: async () => ({ backends: null, localOwner: true }),
|
|
37
|
+
}));
|
|
38
|
+
shortCircuitAfterBoot();
|
|
39
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
40
|
+
const { createShell } = await import('./createShell');
|
|
41
|
+
// The full boot pipeline (mount, bootstrap, etc.) may throw in this
|
|
42
|
+
// test env — we only care that the boot-config fetch was attempted.
|
|
43
|
+
try {
|
|
44
|
+
await createShell({ serverUrl: 'https://remote.example.com', remoteAuth: true });
|
|
45
|
+
}
|
|
46
|
+
catch (_a) {
|
|
47
|
+
// ignore — assertion below is the contract.
|
|
48
|
+
}
|
|
49
|
+
expect(calls.some(u => u === 'https://remote.example.com/api/boot')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('keeps the legacy localOwner short-circuit when remoteAuth is absent', async () => {
|
|
52
|
+
const calls = [];
|
|
53
|
+
globalThis.fetch = vi.fn(async (input) => {
|
|
54
|
+
calls.push(String(input));
|
|
55
|
+
return new Response('ok');
|
|
56
|
+
});
|
|
57
|
+
vi.doMock('./platform/index', () => ({
|
|
58
|
+
resolvePlatform: async () => ({ backends: null, localOwner: true }),
|
|
59
|
+
}));
|
|
60
|
+
shortCircuitAfterBoot();
|
|
61
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
62
|
+
const { createShell } = await import('./createShell');
|
|
63
|
+
try {
|
|
64
|
+
await createShell({ serverUrl: '' });
|
|
65
|
+
}
|
|
66
|
+
catch (_a) {
|
|
67
|
+
// ignore — assertion below is the contract.
|
|
68
|
+
}
|
|
69
|
+
expect(calls.some(u => u.endsWith('/api/boot'))).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -20,6 +20,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
20
20
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
21
21
|
};
|
|
22
22
|
var _HttpDocumentBackend_instances, _HttpDocumentBackend_baseUrl, _HttpDocumentBackend_apiKey, _HttpDocumentBackend_authHeaders;
|
|
23
|
+
import { apiFetch } from '../transport/apiFetch';
|
|
23
24
|
export class HttpDocumentBackend {
|
|
24
25
|
/**
|
|
25
26
|
* @param baseUrl - The server origin (e.g. 'http://localhost:3000' or window.location.origin).
|
|
@@ -36,7 +37,7 @@ export class HttpDocumentBackend {
|
|
|
36
37
|
async read(tenantId, shardId, path) {
|
|
37
38
|
var _a;
|
|
38
39
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}`;
|
|
39
|
-
const res = await
|
|
40
|
+
const res = await apiFetch(url, { credentials: 'include' });
|
|
40
41
|
if (res.status === 404)
|
|
41
42
|
return null;
|
|
42
43
|
if (!res.ok)
|
|
@@ -50,45 +51,45 @@ export class HttpDocumentBackend {
|
|
|
50
51
|
async write(tenantId, shardId, path, content) {
|
|
51
52
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}`;
|
|
52
53
|
const headers = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': typeof content === 'string' ? 'text/plain' : 'application/octet-stream' });
|
|
53
|
-
const res = await
|
|
54
|
+
const res = await apiFetch(url, { method: 'PUT', headers, body: content, credentials: 'include' });
|
|
54
55
|
if (!res.ok)
|
|
55
56
|
throw new Error(`Document write failed: ${res.status}`);
|
|
56
57
|
}
|
|
57
58
|
async delete(tenantId, shardId, path) {
|
|
58
59
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}`;
|
|
59
|
-
const res = await
|
|
60
|
+
const res = await apiFetch(url, { method: 'DELETE', headers: __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this), credentials: 'include' });
|
|
60
61
|
if (!res.ok)
|
|
61
62
|
throw new Error(`Document delete failed: ${res.status}`);
|
|
62
63
|
}
|
|
63
64
|
async list(tenantId, shardId) {
|
|
64
65
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}`;
|
|
65
|
-
const res = await
|
|
66
|
+
const res = await apiFetch(url, { credentials: 'include' });
|
|
66
67
|
if (!res.ok)
|
|
67
68
|
throw new Error(`Document list failed: ${res.status}`);
|
|
68
69
|
return res.json();
|
|
69
70
|
}
|
|
70
71
|
async exists(tenantId, shardId, path) {
|
|
71
72
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}`;
|
|
72
|
-
const res = await
|
|
73
|
+
const res = await apiFetch(url, { method: 'HEAD', credentials: 'include' });
|
|
73
74
|
return res.ok;
|
|
74
75
|
}
|
|
75
76
|
async listAllShards(tenantId) {
|
|
76
77
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/_shards`;
|
|
77
|
-
const res = await
|
|
78
|
+
const res = await apiFetch(url, { credentials: 'include' });
|
|
78
79
|
if (!res.ok)
|
|
79
80
|
throw new Error(`listAllShards failed: ${res.status}`);
|
|
80
81
|
return res.json();
|
|
81
82
|
}
|
|
82
83
|
async listAllDocuments(tenantId) {
|
|
83
84
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/_all`;
|
|
84
|
-
const res = await
|
|
85
|
+
const res = await apiFetch(url, { credentials: 'include' });
|
|
85
86
|
if (!res.ok)
|
|
86
87
|
throw new Error(`listAllDocuments failed: ${res.status}`);
|
|
87
88
|
return res.json();
|
|
88
89
|
}
|
|
89
90
|
async readMeta(tenantId, shardId, path) {
|
|
90
91
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}?meta=1`;
|
|
91
|
-
const res = await
|
|
92
|
+
const res = await apiFetch(url, { credentials: 'include' });
|
|
92
93
|
if (!res.ok)
|
|
93
94
|
throw new Error(`readMeta failed: ${res.status}`);
|
|
94
95
|
const body = await res.json();
|
|
@@ -101,7 +102,7 @@ export class HttpDocumentBackend {
|
|
|
101
102
|
const body = typeof choice === 'string'
|
|
102
103
|
? { choice }
|
|
103
104
|
: { choice: choice.origin };
|
|
104
|
-
const res = await
|
|
105
|
+
const res = await apiFetch(url, {
|
|
105
106
|
method: 'POST',
|
|
106
107
|
credentials: 'include',
|
|
107
108
|
headers: Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' }),
|
|
@@ -112,7 +113,7 @@ export class HttpDocumentBackend {
|
|
|
112
113
|
}
|
|
113
114
|
async readBranch(tenantId, shardId, path, origin) {
|
|
114
115
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}/branch?origin=${encodeURIComponent(origin)}`;
|
|
115
|
-
const res = await
|
|
116
|
+
const res = await apiFetch(url, { credentials: 'include' });
|
|
116
117
|
if (res.status === 404)
|
|
117
118
|
return null;
|
|
118
119
|
if (!res.ok)
|
|
@@ -122,7 +123,7 @@ export class HttpDocumentBackend {
|
|
|
122
123
|
async rename(tenantId, shardId, oldPath, newPath) {
|
|
123
124
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${oldPath}/rename`;
|
|
124
125
|
const headers = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' });
|
|
125
|
-
const res = await
|
|
126
|
+
const res = await apiFetch(url, {
|
|
126
127
|
method: 'POST',
|
|
127
128
|
headers,
|
|
128
129
|
body: JSON.stringify({ to: newPath }),
|
package/dist/env/client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* from the server.
|
|
4
4
|
*/
|
|
5
5
|
import { getAuthHeader, isAdmin } from '../auth/index';
|
|
6
|
+
import { apiFetch } from '../transport/apiFetch';
|
|
6
7
|
/** Server base URL, set once during configuration. */
|
|
7
8
|
let serverUrl = '';
|
|
8
9
|
/** Configure the server URL for env state operations. */
|
|
@@ -18,7 +19,9 @@ export function getEnvServerUrl() {
|
|
|
18
19
|
* Returns an empty object if the server has no stored state.
|
|
19
20
|
*/
|
|
20
21
|
export async function fetchEnvState(shardId) {
|
|
21
|
-
const res = await
|
|
22
|
+
const res = await apiFetch(`${serverUrl}/api/env-state/${encodeURIComponent(shardId)}`, {
|
|
23
|
+
credentials: 'omit',
|
|
24
|
+
});
|
|
22
25
|
if (!res.ok) {
|
|
23
26
|
console.warn(`[sh3] Failed to fetch env state for "${shardId}": HTTP ${res.status}`);
|
|
24
27
|
return {};
|
|
@@ -38,10 +41,11 @@ export async function putEnvState(shardId, state) {
|
|
|
38
41
|
const headers = { 'Content-Type': 'application/json' };
|
|
39
42
|
if (auth)
|
|
40
43
|
headers['Authorization'] = auth;
|
|
41
|
-
const res = await
|
|
44
|
+
const res = await apiFetch(`${serverUrl}/api/env-state/${encodeURIComponent(shardId)}`, {
|
|
42
45
|
method: 'PUT',
|
|
43
46
|
headers,
|
|
44
47
|
body: JSON.stringify(state),
|
|
48
|
+
credentials: 'omit',
|
|
45
49
|
});
|
|
46
50
|
if (!res.ok) {
|
|
47
51
|
const body = await res.json().catch(() => ({}));
|
|
@@ -73,10 +77,11 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
|
|
|
73
77
|
const headers = {};
|
|
74
78
|
if (auth)
|
|
75
79
|
headers['Authorization'] = auth;
|
|
76
|
-
const res = await
|
|
80
|
+
const res = await apiFetch(`${serverUrl}/api/packages/install`, {
|
|
77
81
|
method: 'POST',
|
|
78
82
|
headers,
|
|
79
83
|
body: form,
|
|
84
|
+
credentials: 'omit',
|
|
80
85
|
});
|
|
81
86
|
if (!res.ok) {
|
|
82
87
|
let body = {};
|
|
@@ -104,10 +109,11 @@ export async function serverUninstallPackage(id) {
|
|
|
104
109
|
const headers = { 'Content-Type': 'application/json' };
|
|
105
110
|
if (auth)
|
|
106
111
|
headers['Authorization'] = auth;
|
|
107
|
-
const res = await
|
|
112
|
+
const res = await apiFetch(`${serverUrl}/api/packages/uninstall`, {
|
|
108
113
|
method: 'POST',
|
|
109
114
|
headers,
|
|
110
115
|
body: JSON.stringify({ id }),
|
|
116
|
+
credentials: 'omit',
|
|
111
117
|
});
|
|
112
118
|
if (!res.ok) {
|
|
113
119
|
const body = await res.json().catch(() => ({}));
|
|
@@ -118,7 +124,7 @@ export async function serverUninstallPackage(id) {
|
|
|
118
124
|
* Fetch the list of packages installed on the server.
|
|
119
125
|
*/
|
|
120
126
|
export async function fetchServerPackages() {
|
|
121
|
-
const res = await
|
|
127
|
+
const res = await apiFetch(`${serverUrl}/api/packages`, { credentials: 'omit' });
|
|
122
128
|
if (!res.ok)
|
|
123
129
|
return [];
|
|
124
130
|
return await res.json();
|