sh3-core 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/Sh3.svelte +3 -1
  2. package/dist/actions/menuBarModel.js +8 -0
  3. package/dist/actions/menuBarModel.test.js +61 -0
  4. package/dist/app/admin/ApiKeysView.svelte +6 -5
  5. package/dist/app/store/PermissionConfirmModal.svelte +23 -0
  6. package/dist/app/store/PermissionConfirmModal.svelte.d.ts +1 -0
  7. package/dist/app/store/StoreView.svelte +6 -1
  8. package/dist/chrome/CompactChrome.svelte.test.js +7 -4
  9. package/dist/env/client.d.ts +5 -4
  10. package/dist/env/client.js +11 -17
  11. package/dist/env/serverUrl.d.ts +2 -0
  12. package/dist/env/serverUrl.js +8 -0
  13. package/dist/gestures/index.d.ts +17 -0
  14. package/dist/gestures/index.js +27 -0
  15. package/dist/keys/client.js +6 -7
  16. package/dist/keys/revocation-bus.svelte.js +11 -1
  17. package/dist/layout/compact/CarouselTabs.svelte +150 -14
  18. package/dist/layout/compact/CarouselTabs.svelte.test.js +222 -2
  19. package/dist/layout/compact/CompactRenderer.svelte +1 -1
  20. package/dist/layout/compact/CompactRenderer.svelte.test.js +5 -3
  21. package/dist/layout/compact/derive.js +7 -16
  22. package/dist/layout/compact/derive.test.js +30 -9
  23. package/dist/layout/drag.svelte.js +16 -3
  24. package/dist/layout/inspection.d.ts +20 -9
  25. package/dist/layout/inspection.js +66 -11
  26. package/dist/layout/inspection.svelte.test.d.ts +1 -0
  27. package/dist/layout/inspection.svelte.test.js +114 -0
  28. package/dist/layout/store.schemaVersion.test.js +2 -2
  29. package/dist/layout/types.d.ts +11 -8
  30. package/dist/layout/types.js +1 -1
  31. package/dist/layout/types.test.js +2 -2
  32. package/dist/overlays/FloatFrame.svelte +93 -22
  33. package/dist/primitives/ResizableSplitter.svelte +42 -8
  34. package/dist/registry/checkFetch.d.ts +6 -0
  35. package/dist/registry/checkFetch.js +23 -0
  36. package/dist/sh3/views/KeysAndPeers.svelte +4 -3
  37. package/dist/shards/activate-runtime.test.js +99 -1
  38. package/dist/shards/activate.svelte.js +12 -3
  39. package/dist/shards/registry.d.ts +8 -1
  40. package/dist/shards/registry.js +13 -2
  41. package/dist/shards/registry.test.js +25 -4
  42. package/dist/shards/types.d.ts +14 -1
  43. package/dist/shell-shard/ScrollbackView.svelte +145 -67
  44. package/dist/shell-shard/ScrollbackView.svelte.test.d.ts +1 -0
  45. package/dist/shell-shard/ScrollbackView.svelte.test.js +182 -0
  46. package/dist/shell-shard/dispatch-gating.test.js +38 -2
  47. package/dist/shell-shard/dispatch.js +9 -1
  48. package/dist/shell-shard/registry-resolve.test.js +50 -0
  49. package/dist/shell-shard/registry.d.ts +2 -1
  50. package/dist/shell-shard/registry.js +12 -2
  51. package/dist/shell-shard/verbs/help.js +5 -4
  52. package/dist/shell-shard/verbs/help.svelte.test.js +5 -2
  53. package/dist/verbs/types.d.ts +10 -5
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +1 -1
package/dist/Sh3.svelte CHANGED
@@ -30,6 +30,7 @@
30
30
  import CompactRenderer from './layout/compact/CompactRenderer.svelte';
31
31
  import { sh3 } from './sh3Runtime.svelte';
32
32
  import { claim, revoke } from './gestures/pointerClaim';
33
+ import { EDGE_PX } from './gestures';
33
34
 
34
35
  let contentEl: HTMLElement | undefined = $state();
35
36
 
@@ -70,7 +71,8 @@
70
71
  // Register left/right edge zones as priority:'edge' pointer claims so that
71
72
  // any shard with a priority:'normal' claim automatically beats the shell's
72
73
  // edge-swipe gesture (carousel navigation, future side-panel reveals).
73
- const EDGE_PX = 24;
74
+ // EDGE_PX is shared with carousel/swipe sites so the gutter width stays
75
+ // consistent across the framework.
74
76
  $effect(() => {
75
77
  const el = contentEl;
76
78
  if (!el) return;
@@ -62,6 +62,12 @@ export function resolveMenuItems(entries, state, containerId) {
62
62
  const winning = innermostActiveScope(entry.action.scope, state, entry.ownerShardId);
63
63
  if (!winning)
64
64
  continue;
65
+ // Menu surface only: when the action is active via the 'app' tier,
66
+ // require the owner shard to be in the active app's requiredShards.
67
+ // Dispatcher / palette / context menu / hotkey paths keep autostart
68
+ // activation. See issue #32 and the design spec dated 2026-05-12.
69
+ if (winning === 'app' && !state.activeAppRequiredShards.has(entry.ownerShardId))
70
+ continue;
65
71
  seen.add(entry.action.id);
66
72
  out.push({
67
73
  id: entry.action.id,
@@ -95,6 +101,8 @@ export function resolveSubmenuItems(entries, state, parentId) {
95
101
  const winning = innermostActiveScope(entry.action.scope, state, entry.ownerShardId);
96
102
  if (!winning)
97
103
  continue;
104
+ if (winning === 'app' && !state.activeAppRequiredShards.has(entry.ownerShardId))
105
+ continue;
98
106
  seen.add(entry.action.id);
99
107
  out.push({
100
108
  id: entry.action.id,
@@ -156,3 +156,64 @@ describe('resolveSubmenuItems', () => {
156
156
  expect(resolveSubmenuItems([], stateWithApp, 'nope')).toEqual([]);
157
157
  });
158
158
  });
159
+ describe("resolveMenuItems — required-shard menu filter (issue #32)", () => {
160
+ it("omits scope:'app' actions when owner shard is autostart-only (not required by active app)", () => {
161
+ const state = mkState({
162
+ activeAppId: 'other-app',
163
+ activeAppRequiredShards: new Set(['other-shard']),
164
+ autostartShards: new Set(['guml.core']),
165
+ });
166
+ const entries = [
167
+ mkEntry({ id: 'guml.project.new', scope: 'app', menuItem: 'file', label: 'New Project…' }, 'guml.core'),
168
+ ];
169
+ expect(resolveMenuItems(entries, state, 'file')).toEqual([]);
170
+ });
171
+ it("includes scope:'app' actions when owner shard IS in active app's requiredShards", () => {
172
+ const state = mkState({
173
+ activeAppId: 'guml-ide',
174
+ activeAppRequiredShards: new Set(['guml.core']),
175
+ autostartShards: new Set(['guml.core']),
176
+ });
177
+ const entries = [
178
+ mkEntry({ id: 'guml.project.new', scope: 'app', menuItem: 'file', label: 'New Project…' }, 'guml.core'),
179
+ ];
180
+ expect(resolveMenuItems(entries, state, 'file').map((i) => i.id))
181
+ .toEqual(['guml.project.new']);
182
+ });
183
+ it('still includes the action when a more-specific tier wins (view:editor over app)', () => {
184
+ const state = mkState({
185
+ activeAppId: 'other-app',
186
+ activeAppRequiredShards: new Set(['other-shard']),
187
+ autostartShards: new Set(['guml.core']),
188
+ mountedViewIds: new Set(['editor']),
189
+ });
190
+ const entries = [
191
+ mkEntry({ id: 'fmt', scope: ['view:editor', 'app'], menuItem: 'file', label: 'Format' }, 'guml.core'),
192
+ ];
193
+ expect(resolveMenuItems(entries, state, 'file').map((i) => i.id)).toEqual(['fmt']);
194
+ });
195
+ it("omits scope:['home','app'] action in an app that does not require the autostart owner", () => {
196
+ const state = mkState({
197
+ activeAppId: 'other-app',
198
+ activeAppRequiredShards: new Set(['other-shard']),
199
+ autostartShards: new Set(['guml.core']),
200
+ });
201
+ const entries = [
202
+ mkEntry({ id: 'g.global', scope: ['home', 'app'], menuItem: 'file', label: 'Global' }, 'guml.core'),
203
+ ];
204
+ expect(resolveMenuItems(entries, state, 'file')).toEqual([]);
205
+ });
206
+ it('drops submenu children whose owner shard is autostart-only', () => {
207
+ const state = mkState({
208
+ activeAppId: 'other-app',
209
+ activeAppRequiredShards: new Set(['other-shard']),
210
+ autostartShards: new Set(['guml.core']),
211
+ });
212
+ const entries = [
213
+ mkEntry({ id: 'g.parent', scope: 'app', menuItem: 'file', label: 'GUML', submenu: true }, 'guml.core'),
214
+ mkEntry({ id: 'g.parent.a', scope: 'app', label: 'A', submenuOf: 'g.parent' }, 'guml.core'),
215
+ ];
216
+ expect(resolveMenuItems(entries, state, 'file')).toEqual([]);
217
+ expect(resolveSubmenuItems(entries, state, 'g.parent')).toEqual([]);
218
+ });
219
+ });
@@ -3,6 +3,9 @@
3
3
  * Admin API Keys view — list, create, reveal, revoke API keys.
4
4
  */
5
5
 
6
+ import { apiFetch } from '../../transport/apiFetch';
7
+ import { getEnvServerUrl } from '../../env/serverUrl';
8
+
6
9
  interface ApiKeyPublic {
7
10
  id: string;
8
11
  label: string;
@@ -29,7 +32,7 @@
29
32
  loading = true;
30
33
  error = null;
31
34
  try {
32
- const res = await fetch('/api/admin/keys', { credentials: 'include' });
35
+ const res = await apiFetch(`${getEnvServerUrl()}/api/admin/keys`);
33
36
  if (!res.ok) throw new Error('Failed to fetch keys');
34
37
  keys = await res.json();
35
38
  } catch (err) {
@@ -42,10 +45,9 @@
42
45
  async function createKey() {
43
46
  createError = null;
44
47
  try {
45
- const res = await fetch('/api/admin/keys', {
48
+ const res = await apiFetch(`${getEnvServerUrl()}/api/admin/keys`, {
46
49
  method: 'POST',
47
50
  headers: { 'Content-Type': 'application/json' },
48
- credentials: 'include',
49
51
  body: JSON.stringify({ label: newLabel }),
50
52
  });
51
53
  if (!res.ok) {
@@ -66,9 +68,8 @@
66
68
  async function revokeKey(id: string) {
67
69
  confirmingId = null;
68
70
  try {
69
- await fetch(`/api/admin/keys/${id}`, {
71
+ await apiFetch(`${getEnvServerUrl()}/api/admin/keys/${id}`, {
70
72
  method: 'DELETE',
71
- credentials: 'include',
72
73
  });
73
74
  if (justCreated?.id === id) justCreated = null;
74
75
  await fetchKeys();
@@ -21,6 +21,7 @@
21
21
  permissions?: string[];
22
22
  added?: string[];
23
23
  removed?: string[];
24
+ warnings?: string[];
24
25
  onConfirm: () => void;
25
26
  onCancel: () => void;
26
27
  }
@@ -32,6 +33,7 @@
32
33
  permissions = [],
33
34
  added = [],
34
35
  removed = [],
36
+ warnings = [],
35
37
  onConfirm,
36
38
  onCancel,
37
39
  }: Props = $props();
@@ -111,6 +113,17 @@
111
113
  </ul>
112
114
  {/if}
113
115
  {/if}
116
+ {#if warnings.length > 0}
117
+ <p class="perm-modal-intro perm-modal-warn-heading">Potential compatibility issues:</p>
118
+ <ul class="perm-modal-list perm-modal-warnings">
119
+ {#each warnings as msg (msg)}
120
+ <li class="perm-modal-item perm-modal-warn-item">
121
+ <div class="perm-modal-item-title">⚠ Compatibility warning</div>
122
+ <div class="perm-modal-item-desc">{msg}</div>
123
+ </li>
124
+ {/each}
125
+ </ul>
126
+ {/if}
114
127
  </div>
115
128
 
116
129
  <footer class="perm-modal-footer">
@@ -203,6 +216,16 @@
203
216
  .perm-modal-removed .perm-modal-item {
204
217
  opacity: 0.75;
205
218
  }
219
+ .perm-modal-warn-heading {
220
+ margin-top: 12px;
221
+ }
222
+ .perm-modal-warn-item {
223
+ border-color: color-mix(in srgb, var(--sh3-warning, #ff9800) 60%, var(--sh3-border, #444));
224
+ background: color-mix(in srgb, var(--sh3-warning, #ff9800) 8%, var(--sh3-input-bg, #2a2a2a));
225
+ }
226
+ .perm-modal-warn-item .perm-modal-item-title {
227
+ color: var(--sh3-warning, #ff9800);
228
+ }
206
229
  .perm-modal-footer {
207
230
  padding: 12px 20px;
208
231
  border-top: 1px solid var(--sh3-border, #444);
@@ -9,6 +9,7 @@ interface Props {
9
9
  permissions?: string[];
10
10
  added?: string[];
11
11
  removed?: string[];
12
+ warnings?: string[];
12
13
  onConfirm: () => void;
13
14
  onCancel: () => void;
14
15
  }
@@ -12,6 +12,7 @@
12
12
  import { loadBundleModule, type LoadedBundle } from '../../registry/loader';
13
13
  import { extractBundlePermissions } from '../../registry/permission-descriptions';
14
14
  import { serverInstallPackage } from '../../env/client';
15
+ import { checkBundleFetch } from '../../registry/checkFetch';
15
16
  import { contract } from '../../contract';
16
17
  import type { ResolvedPackage } from '../../registry/client';
17
18
  import type { InstalledPackage } from '../../registry/types';
@@ -38,6 +39,7 @@
38
39
  bundle: ArrayBuffer;
39
40
  meta: ReturnType<typeof buildPackageMeta>;
40
41
  serverBundle: ArrayBuffer | undefined;
42
+ warnings: string[];
41
43
  }>(null);
42
44
 
43
45
  let updateModal = $state<null | {
@@ -186,7 +188,9 @@
186
188
 
187
189
  // 4. Show the confirmation modal. The actual install happens in
188
190
  // confirmInstall() once the user clicks Install.
189
- installModal = { pkg, permissions, loaded, bundle, meta, serverBundle };
191
+ const bundleText = new TextDecoder().decode(new Uint8Array(bundle));
192
+ const warnings = checkBundleFetch(bundleText);
193
+ installModal = { pkg, permissions, loaded, bundle, meta, serverBundle, warnings };
190
194
  } catch (err) {
191
195
  installError = err instanceof Error ? err.message : String(err);
192
196
  const next = new Set(installingIds);
@@ -400,6 +404,7 @@
400
404
  author: installModal.pkg.entry.author.name,
401
405
  }}
402
406
  permissions={installModal.permissions}
407
+ warnings={installModal.warnings}
403
408
  onConfirm={confirmInstall}
404
409
  onCancel={cancelInstall}
405
410
  />
@@ -125,9 +125,10 @@ describe('CompactChrome — breadcrumb', () => {
125
125
  initialLayout: {
126
126
  type: 'tabs',
127
127
  activeTab: 1,
128
+ role: 'body',
128
129
  tabs: [
129
- { slotId: 's0', viewId: null, label: 'First', role: 'body' },
130
- { slotId: 's1', viewId: null, label: 'Second', role: 'body' },
130
+ { slotId: 's0', viewId: null, label: 'First' },
131
+ { slotId: 's1', viewId: null, label: 'Second' },
131
132
  ],
132
133
  },
133
134
  };
@@ -152,12 +153,14 @@ describe('CompactChrome — breadcrumb', () => {
152
153
  {
153
154
  type: 'tabs',
154
155
  activeTab: 0,
155
- tabs: [{ slotId: 'top0', viewId: null, label: 'TopActive', role: 'body' }],
156
+ role: 'body',
157
+ tabs: [{ slotId: 'top0', viewId: null, label: 'TopActive' }],
156
158
  },
157
159
  {
158
160
  type: 'tabs',
159
161
  activeTab: 0,
160
- tabs: [{ slotId: 'bot0', viewId: null, label: 'BottomActive', role: 'body' }],
162
+ role: 'body',
163
+ tabs: [{ slotId: 'bot0', viewId: null, label: 'BottomActive' }],
161
164
  },
162
165
  ],
163
166
  },
@@ -2,10 +2,7 @@
2
2
  * Env state client — fetches and updates per-shard environment state
3
3
  * from the server.
4
4
  */
5
- /** Configure the server URL for env state operations. */
6
- export declare function __setEnvServerUrl(url: string): void;
7
- /** Return the configured server URL. */
8
- export declare function getEnvServerUrl(): string;
5
+ export { getEnvServerUrl, __setEnvServerUrl } from './serverUrl';
9
6
  /**
10
7
  * Fetch env state for a shard from the server.
11
8
  * Returns an empty object if the server has no stored state.
@@ -24,6 +21,10 @@ export interface ServerInstallResult {
24
21
  missing?: Array<{
25
22
  id: string;
26
23
  }>;
24
+ warnings?: Array<{
25
+ level: 'warn';
26
+ message: string;
27
+ }>;
27
28
  }
28
29
  /**
29
30
  * Install a package on the server via multipart upload.
@@ -4,22 +4,14 @@
4
4
  */
5
5
  import { getAuthHeader, isAdmin } from '../auth/index';
6
6
  import { apiFetch } from '../transport/apiFetch';
7
- /** Server base URL, set once during configuration. */
8
- let serverUrl = '';
9
- /** Configure the server URL for env state operations. */
10
- export function __setEnvServerUrl(url) {
11
- serverUrl = url;
12
- }
13
- /** Return the configured server URL. */
14
- export function getEnvServerUrl() {
15
- return serverUrl;
16
- }
7
+ import { getEnvServerUrl } from './serverUrl';
8
+ export { getEnvServerUrl, __setEnvServerUrl } from './serverUrl';
17
9
  /**
18
10
  * Fetch env state for a shard from the server.
19
11
  * Returns an empty object if the server has no stored state.
20
12
  */
21
13
  export async function fetchEnvState(shardId) {
22
- const res = await apiFetch(`${serverUrl}/api/env-state/${encodeURIComponent(shardId)}`, {
14
+ const res = await apiFetch(`${getEnvServerUrl()}/api/env-state/${encodeURIComponent(shardId)}`, {
23
15
  credentials: 'omit',
24
16
  });
25
17
  if (!res.ok) {
@@ -41,7 +33,7 @@ export async function putEnvState(shardId, state) {
41
33
  const headers = { 'Content-Type': 'application/json' };
42
34
  if (auth)
43
35
  headers['Authorization'] = auth;
44
- const res = await apiFetch(`${serverUrl}/api/env-state/${encodeURIComponent(shardId)}`, {
36
+ const res = await apiFetch(`${getEnvServerUrl()}/api/env-state/${encodeURIComponent(shardId)}`, {
45
37
  method: 'PUT',
46
38
  headers,
47
39
  body: JSON.stringify(state),
@@ -65,6 +57,7 @@ export async function putEnvState(shardId, state) {
65
57
  * back server-side.
66
58
  */
67
59
  export async function serverInstallPackage(manifest, clientBundle, serverBundle) {
60
+ var _a;
68
61
  if (!isAdmin())
69
62
  throw new Error('Cannot install: not elevated to admin');
70
63
  const auth = getAuthHeader();
@@ -77,7 +70,7 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
77
70
  const headers = {};
78
71
  if (auth)
79
72
  headers['Authorization'] = auth;
80
- const res = await apiFetch(`${serverUrl}/api/packages/install`, {
73
+ const res = await apiFetch(`${getEnvServerUrl()}/api/packages/install`, {
81
74
  method: 'POST',
82
75
  headers,
83
76
  body: form,
@@ -88,7 +81,7 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
88
81
  try {
89
82
  body = await res.json();
90
83
  }
91
- catch ( /* non-JSON */_a) { /* non-JSON */ }
84
+ catch ( /* non-JSON */_b) { /* non-JSON */ }
92
85
  return {
93
86
  ok: false,
94
87
  error: typeof body.error === 'string' ? body.error : `HTTP ${res.status}`,
@@ -96,7 +89,8 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
96
89
  missing: Array.isArray(body.missing) ? body.missing : undefined,
97
90
  };
98
91
  }
99
- return { ok: true };
92
+ const body = await res.json();
93
+ return { ok: true, warnings: (_a = body.warnings) !== null && _a !== void 0 ? _a : [] };
100
94
  }
101
95
  /**
102
96
  * Uninstall a package from the server.
@@ -109,7 +103,7 @@ export async function serverUninstallPackage(id) {
109
103
  const headers = { 'Content-Type': 'application/json' };
110
104
  if (auth)
111
105
  headers['Authorization'] = auth;
112
- const res = await apiFetch(`${serverUrl}/api/packages/uninstall`, {
106
+ const res = await apiFetch(`${getEnvServerUrl()}/api/packages/uninstall`, {
113
107
  method: 'POST',
114
108
  headers,
115
109
  body: JSON.stringify({ id }),
@@ -124,7 +118,7 @@ export async function serverUninstallPackage(id) {
124
118
  * Fetch the list of packages installed on the server.
125
119
  */
126
120
  export async function fetchServerPackages() {
127
- const res = await apiFetch(`${serverUrl}/api/packages`, { credentials: 'omit' });
121
+ const res = await apiFetch(`${getEnvServerUrl()}/api/packages`, { credentials: 'omit' });
128
122
  if (!res.ok)
129
123
  return [];
130
124
  return await res.json();
@@ -0,0 +1,2 @@
1
+ export declare function __setEnvServerUrl(url: string): void;
2
+ export declare function getEnvServerUrl(): string;
@@ -0,0 +1,8 @@
1
+ /** Server base URL, set once during shell initialization. */
2
+ let serverUrl = '';
3
+ export function __setEnvServerUrl(url) {
4
+ serverUrl = url;
5
+ }
6
+ export function getEnvServerUrl() {
7
+ return serverUrl;
8
+ }
@@ -4,3 +4,20 @@ export type { GestureRegistry } from './gestureRegistry';
4
4
  export type { GestureHandle, GestureOptions, GestureType, Axis, ClaimPriority, ClaimEntry, PanEvent, ScrollEvent, ButtonEvent, } from './types';
5
5
  /** Internal utility — used by framework gesture sites. Not re-exported via api.ts. */
6
6
  export declare function ancestorCount(el: Element): number;
7
+ /**
8
+ * Width of the reserved gutter at each side edge, in CSS pixels.
9
+ * Pointer-downs that land within this strip of either side of a swipe-aware
10
+ * surface (carousel, future side drawers) initiate the gesture unconditionally
11
+ * — content-specific bailouts (editable target, native horizontal scroll) are
12
+ * suppressed. Outside the gutter, those bailouts still apply so taps on
13
+ * inputs and horizontally-scrollable regions behave normally.
14
+ */
15
+ export declare const EDGE_PX = 24;
16
+ /**
17
+ * Always-on diagnostic for premature gesture-end paths (claim stolen, foreign
18
+ * pointercancel, foreign pointerup, etc.). The log only fires on anomalies, so
19
+ * a healthy drag is silent in production. Include `pointerType` so the user
20
+ * can tell touch from mouse from pen at a glance — the touch-only auto-release
21
+ * bug class doesn't reproduce with a mouse.
22
+ */
23
+ export declare function logGesture(label: string, ev: PointerEvent | null, extra?: Record<string, unknown>): void;
@@ -10,3 +10,30 @@ export function ancestorCount(el) {
10
10
  }
11
11
  return n;
12
12
  }
13
+ /**
14
+ * Width of the reserved gutter at each side edge, in CSS pixels.
15
+ * Pointer-downs that land within this strip of either side of a swipe-aware
16
+ * surface (carousel, future side drawers) initiate the gesture unconditionally
17
+ * — content-specific bailouts (editable target, native horizontal scroll) are
18
+ * suppressed. Outside the gutter, those bailouts still apply so taps on
19
+ * inputs and horizontally-scrollable regions behave normally.
20
+ */
21
+ export const EDGE_PX = 24;
22
+ /**
23
+ * Always-on diagnostic for premature gesture-end paths (claim stolen, foreign
24
+ * pointercancel, foreign pointerup, etc.). The log only fires on anomalies, so
25
+ * a healthy drag is silent in production. Include `pointerType` so the user
26
+ * can tell touch from mouse from pen at a glance — the touch-only auto-release
27
+ * bug class doesn't reproduce with a mouse.
28
+ */
29
+ export function logGesture(label, ev, extra) {
30
+ var _a, _b;
31
+ if (typeof console === 'undefined')
32
+ return;
33
+ const tgt = ev === null || ev === void 0 ? void 0 : ev.target;
34
+ const tag = (_a = tgt === null || tgt === void 0 ? void 0 : tgt.tagName) !== null && _a !== void 0 ? _a : '-';
35
+ const raw = (_b = tgt === null || tgt === void 0 ? void 0 : tgt.className) !== null && _b !== void 0 ? _b : '';
36
+ const cls = typeof raw === 'string' ? raw : '';
37
+ // eslint-disable-next-line no-console
38
+ console.log('[sh3:gesture]', label, Object.assign({ pointerId: ev === null || ev === void 0 ? void 0 : ev.pointerId, pointerType: ev === null || ev === void 0 ? void 0 : ev.pointerType, type: ev === null || ev === void 0 ? void 0 : ev.type, target: cls ? `${tag}.${cls}` : tag }, extra));
39
+ }
@@ -9,6 +9,8 @@
9
9
  import { ConsentDeniedError, ScopeEscalationError } from './types';
10
10
  import { requestConsent } from './consent.svelte';
11
11
  import { emit } from './revocation-bus.svelte';
12
+ import { apiFetch } from '../transport/apiFetch';
13
+ import { getEnvServerUrl } from '../env/serverUrl';
12
14
  export function createShardKeysApi(params) {
13
15
  const { shardId, shardPermissions } = params;
14
16
  const assertScopesSubset = (scopes) => {
@@ -24,18 +26,16 @@ export function createShardKeysApi(params) {
24
26
  const approved = await requestConsent(shardId, opts);
25
27
  if (!approved)
26
28
  throw new ConsentDeniedError();
27
- const ticketRes = await fetch('/api/keys/consent', {
29
+ const ticketRes = await apiFetch(`${getEnvServerUrl()}/api/keys/consent`, {
28
30
  method: 'POST',
29
- credentials: 'include',
30
31
  headers: { 'content-type': 'application/json' },
31
32
  body: JSON.stringify(Object.assign({ shardId }, opts)),
32
33
  });
33
34
  if (!ticketRes.ok)
34
35
  throw new Error(`Consent ticket failed: ${ticketRes.status}`);
35
36
  const { ticket } = await ticketRes.json();
36
- const mintRes = await fetch('/api/keys', {
37
+ const mintRes = await apiFetch(`${getEnvServerUrl()}/api/keys`, {
37
38
  method: 'POST',
38
- credentials: 'include',
39
39
  headers: { 'content-type': 'application/json' },
40
40
  body: JSON.stringify({ ticket }),
41
41
  });
@@ -44,16 +44,15 @@ export function createShardKeysApi(params) {
44
44
  return mintRes.json();
45
45
  },
46
46
  async list() {
47
- const res = await fetch('/api/keys', { credentials: 'include' });
47
+ const res = await apiFetch(`${getEnvServerUrl()}/api/keys`);
48
48
  if (!res.ok)
49
49
  throw new Error(`List failed: ${res.status}`);
50
50
  const all = (await res.json());
51
51
  return all.filter((k) => k.mintedByShardId === shardId);
52
52
  },
53
53
  async revoke(id) {
54
- const res = await fetch(`/api/keys/${encodeURIComponent(id)}`, {
54
+ const res = await apiFetch(`${getEnvServerUrl()}/api/keys/${encodeURIComponent(id)}`, {
55
55
  method: 'DELETE',
56
- credentials: 'include',
57
56
  });
58
57
  if (!res.ok && res.status !== 404)
59
58
  throw new Error(`Revoke failed: ${res.status}`);
@@ -6,6 +6,8 @@
6
6
  * The bus is populated by a server-sent events stream on /api/keys/events
7
7
  * (wired by the sh3 runtime at boot) and/or by local revoke() calls.
8
8
  */
9
+ import { getEnvServerUrl } from '../env/serverUrl';
10
+ import { getAuthToken } from '../transport/authToken';
9
11
  const handlersByShard = new Map();
10
12
  /**
11
13
  * Recently-emitted (shardId → Set<keyId>) with per-entry TTL timers.
@@ -78,7 +80,15 @@ export function emit(shardId, keyId) {
78
80
  export function startServerSideStream() {
79
81
  if (typeof EventSource === 'undefined')
80
82
  return () => { };
81
- const es = new EventSource('/api/keys/events', { withCredentials: true });
83
+ // EventSource cannot send custom headers, so cross-origin auth (Tauri remote)
84
+ // is handled by passing the session token as a query param. Same-origin
85
+ // builds fall back to cookies via withCredentials.
86
+ const base = getEnvServerUrl();
87
+ const token = getAuthToken();
88
+ const url = token
89
+ ? `${base}/api/keys/events?token=${encodeURIComponent(token)}`
90
+ : `${base}/api/keys/events`;
91
+ const es = new EventSource(url, token ? {} : { withCredentials: true });
82
92
  es.onmessage = (msg) => {
83
93
  try {
84
94
  const ev = JSON.parse(msg.data);