sh3-core 0.8.1 → 0.8.2

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 (43) hide show
  1. package/dist/Shell.svelte +19 -0
  2. package/dist/api.d.ts +3 -0
  3. package/dist/api.js +5 -0
  4. package/dist/app/admin/ApiKeysView.svelte +16 -27
  5. package/dist/keys/ConsentDialog.svelte +176 -0
  6. package/dist/keys/ConsentDialog.svelte.d.ts +3 -0
  7. package/dist/keys/client.d.ts +13 -0
  8. package/dist/keys/client.js +65 -0
  9. package/dist/keys/client.test.d.ts +1 -0
  10. package/dist/keys/client.test.js +44 -0
  11. package/dist/keys/consent.svelte.d.ts +16 -0
  12. package/dist/keys/consent.svelte.js +29 -0
  13. package/dist/keys/consent.test.d.ts +1 -0
  14. package/dist/keys/consent.test.js +53 -0
  15. package/dist/keys/revocation-bus.svelte.d.ts +35 -0
  16. package/dist/keys/revocation-bus.svelte.js +92 -0
  17. package/dist/keys/revocation-bus.test.d.ts +1 -0
  18. package/dist/keys/revocation-bus.test.js +95 -0
  19. package/dist/keys/types.d.ts +32 -0
  20. package/dist/keys/types.js +13 -0
  21. package/dist/server-shard/types.d.ts +21 -2
  22. package/dist/server-sync.d.ts +6 -0
  23. package/dist/server-sync.js +634 -0
  24. package/dist/server-sync.js.map +7 -0
  25. package/dist/sh3core-shard/ShellHome.svelte +140 -63
  26. package/dist/sh3core-shard/sh3coreShard.svelte.js +12 -1
  27. package/dist/shards/activate-on-key-revoked.test.d.ts +1 -0
  28. package/dist/shards/activate-on-key-revoked.test.js +60 -0
  29. package/dist/shards/activate.svelte.js +24 -2
  30. package/dist/shards/types.d.ts +9 -0
  31. package/dist/shards/types.js +1 -1
  32. package/dist/shell/views/KeysAndPeers.svelte +110 -0
  33. package/dist/shell/views/KeysAndPeers.svelte.d.ts +3 -0
  34. package/dist/shell-shard/Terminal.svelte +0 -11
  35. package/dist/shell-shard/toolbar/Toolbar.svelte +11 -32
  36. package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +0 -2
  37. package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +29 -62
  38. package/dist/testing.d.ts +3 -0
  39. package/dist/testing.js +77 -0
  40. package/dist/testing.js.map +7 -0
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +10 -2
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Keys & Peers view — lists every API key minted for the current user's
4
+ * tenant (admin keys excluded) and allows individual revocation.
5
+ *
6
+ * Reachable from settings. No create-key button; keys are only minted
7
+ * in-shard via the consent flow.
8
+ */
9
+
10
+ import type { ApiKeyPublic } from '../../keys/types';
11
+ import { subscribe as subscribeBus } from '../../keys/revocation-bus.svelte';
12
+
13
+ let rows = $state<ApiKeyPublic[]>([]);
14
+ let loadError = $state<string | null>(null);
15
+ let loading = $state(true);
16
+ let confirmingId = $state<string | null>(null);
17
+
18
+ async function refresh(): Promise<void> {
19
+ loadError = null;
20
+ loading = true;
21
+ try {
22
+ const res = await fetch('/api/keys', { credentials: 'include' });
23
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
24
+ rows = await res.json();
25
+ } catch (err) {
26
+ loadError = err instanceof Error ? err.message : String(err);
27
+ } finally {
28
+ loading = false;
29
+ }
30
+ }
31
+
32
+ async function revoke(id: string): Promise<void> {
33
+ confirmingId = null;
34
+ const res = await fetch(`/api/keys/${encodeURIComponent(id)}`, {
35
+ method: 'DELETE',
36
+ credentials: 'include',
37
+ });
38
+ if (res.ok || res.status === 404) {
39
+ rows = rows.filter((r) => r.id !== id);
40
+ }
41
+ }
42
+
43
+ function formatDate(iso: string): string {
44
+ return new Date(iso).toLocaleDateString(undefined, {
45
+ year: 'numeric', month: 'short', day: 'numeric',
46
+ });
47
+ }
48
+
49
+ $effect(() => { refresh(); });
50
+
51
+ // Refresh whenever any key is revoked (by this or another tab/shard).
52
+ $effect(() => subscribeBus('*', () => { void refresh(); }));
53
+ </script>
54
+
55
+ <div class="keys-peers">
56
+ <div class="keys-peers-header">
57
+ <h2>Keys &amp; Peers</h2>
58
+ </div>
59
+
60
+ {#if loading}
61
+ <p class="keys-peers-muted">Loading...</p>
62
+ {:else if loadError}
63
+ <p class="keys-peers-error">Failed to load: {loadError}</p>
64
+ {:else if rows.length === 0}
65
+ <p class="keys-peers-muted">No keys yet. Shards will ask your permission before creating one.</p>
66
+ {:else}
67
+ <ul class="keys-peers-list">
68
+ {#each rows as row (row.id)}
69
+ <li class="keys-peers-item">
70
+ <div class="keys-peers-info">
71
+ <span class="keys-peers-label">{row.label}</span>
72
+ <span class="keys-peers-meta">
73
+ Minted by: {row.mintedByShardId ?? 'system'}{row.connectorId ? ` \u00B7 Connector: ${row.connectorId}` : ''}
74
+ </span>
75
+ <span class="keys-peers-meta">
76
+ Scopes: {row.scopes.join(', ')}
77
+ </span>
78
+ <span class="keys-peers-meta">
79
+ Created: {formatDate(row.createdAt)} &middot; Expires: {row.expiresAt ? formatDate(row.expiresAt) : 'never'}
80
+ </span>
81
+ </div>
82
+ <div class="keys-peers-actions">
83
+ {#if confirmingId === row.id}
84
+ <button type="button" class="keys-peers-btn-danger" onclick={() => revoke(row.id)}>Confirm</button>
85
+ <button type="button" class="keys-peers-btn-secondary" onclick={() => { confirmingId = null; }}>Cancel</button>
86
+ {:else}
87
+ <button type="button" class="keys-peers-btn-danger" onclick={() => { confirmingId = row.id; }}>Revoke</button>
88
+ {/if}
89
+ </div>
90
+ </li>
91
+ {/each}
92
+ </ul>
93
+ {/if}
94
+ </div>
95
+
96
+ <style>
97
+ .keys-peers { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
98
+ .keys-peers-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
99
+ .keys-peers-header h2 { margin: 0; font-size: 18px; }
100
+ .keys-peers-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
101
+ .keys-peers-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px 16px; background: var(--shell-bg-elevated, #252540); border: 1px solid var(--shell-border, #3a3a5c); border-radius: var(--shell-radius, 6px); gap: 12px; }
102
+ .keys-peers-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
103
+ .keys-peers-label { font-weight: 600; }
104
+ .keys-peers-meta { font-size: 11px; color: var(--shell-fg-subtle); }
105
+ .keys-peers-actions { display: flex; gap: 6px; flex-shrink: 0; align-items: flex-start; padding-top: 2px; }
106
+ .keys-peers-btn-danger { background: transparent; color: var(--shell-error, #d32f2f); border: 1px solid var(--shell-error, #d32f2f); font-size: 12px; padding: 4px 10px; border-radius: var(--shell-radius, 6px); cursor: pointer; }
107
+ .keys-peers-btn-secondary { background: transparent; color: var(--shell-fg-subtle); border: 1px solid var(--shell-border, #3a3a5c); font-size: 12px; padding: 4px 10px; border-radius: var(--shell-radius, 6px); cursor: pointer; }
108
+ .keys-peers-muted { color: var(--shell-fg-muted); font-style: italic; }
109
+ .keys-peers-error { color: var(--shell-error, #d32f2f); font-size: 13px; }
110
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const KeysAndPeers: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type KeysAndPeers = ReturnType<typeof KeysAndPeers>;
3
+ export default KeysAndPeers;
@@ -88,15 +88,6 @@
88
88
  toolbarRegistry.register({ id: 'focus-lock', order: 20, visible: (ctx) => ctx.mode.id === 'user', component: FocusLockSlot });
89
89
  toolbarRegistry.register({ id: 'target-shard', order: 30, visible: (ctx) => ctx.mode.id === 'user', component: TargetShardSlot });
90
90
 
91
- let toolbarExpanded = $state((() => {
92
- try { return localStorage.getItem('sh3.shell.toolbarExpanded') !== '0'; } catch { return true; }
93
- })());
94
-
95
- function toggleToolbar() {
96
- toolbarExpanded = !toolbarExpanded;
97
- try { localStorage.setItem('sh3.shell.toolbarExpanded', toolbarExpanded ? '1' : '0'); } catch {}
98
- }
99
-
100
91
  /** Walk the layout tree and return the viewId of the active tab in the first
101
92
  * TabsNode found (breadth-first). Returns null if the layout contains no
102
93
  * tabs node with a populated active tab. */
@@ -205,8 +196,6 @@
205
196
  <Toolbar
206
197
  registry={toolbarRegistry}
207
198
  ctx={{ mode, role }}
208
- expanded={toolbarExpanded}
209
- onToggle={toggleToolbar}
210
199
  slotProps={{
211
200
  mode: { mode, role, registry: modeRegistry, onSelect: setMode },
212
201
  'focus-lock': { locked: focusLocked, onToggle: () => (focusLocked = !focusLocked) },
@@ -4,28 +4,21 @@
4
4
  interface Props {
5
5
  registry: ToolbarSlotRegistry;
6
6
  ctx: ShellSlotCtx;
7
- expanded: boolean;
8
- onToggle: () => void;
9
7
  slotProps?: Record<string, Record<string, unknown>>;
10
8
  }
11
9
 
12
- let { registry, ctx, expanded, onToggle, slotProps = {} }: Props = $props();
10
+ let { registry, ctx, slotProps = {} }: Props = $props();
13
11
 
14
12
  let slots = $derived(registry.list(ctx));
15
13
  </script>
16
14
 
17
- <div class="toolbar" class:collapsed={!expanded}>
18
- <button class="toolbar-toggle" onclick={onToggle} title={expanded ? 'Collapse toolbar' : 'Expand toolbar'}>
19
- {expanded ? '▲' : '▼'}
20
- </button>
21
- {#if expanded}
22
- <div class="toolbar-slots">
23
- {#each slots as s (s.id)}
24
- {@const Slot = s.component}
25
- <Slot {...(slotProps[s.id] ?? {})} />
26
- {/each}
27
- </div>
28
- {/if}
15
+ <div class="toolbar">
16
+ <div class="toolbar-slots">
17
+ {#each slots as s (s.id)}
18
+ {@const Slot = s.component}
19
+ <Slot {...(slotProps[s.id] ?? {})} />
20
+ {/each}
21
+ </div>
29
22
  </div>
30
23
 
31
24
  <style>
@@ -33,30 +26,16 @@
33
26
  display: flex;
34
27
  align-items: center;
35
28
  gap: 6px;
36
- padding: 2px 6px;
37
- background: var(--shell-toolbar-bg, #1a1a1a);
29
+ padding: 4px 6px;
30
+ background: var(--shell-bg-elevated, var(--shell-bg, #1a1a1a));
38
31
  border-bottom: 1px solid var(--shell-border, #333);
39
32
  flex-shrink: 0;
40
- min-height: 24px;
41
- }
42
-
43
- .toolbar-toggle {
44
- background: none;
45
- border: none;
46
- color: var(--shell-fg-dim, #888);
47
- cursor: pointer;
48
- font-size: 0.7em;
49
- padding: 0 2px;
50
- line-height: 1;
51
- }
52
-
53
- .toolbar-toggle:hover {
54
- color: var(--shell-fg, #ddd);
55
33
  }
56
34
 
57
35
  .toolbar-slots {
58
36
  display: flex;
59
37
  align-items: center;
60
38
  gap: 8px;
39
+ flex-wrap: wrap;
61
40
  }
62
41
  </style>
@@ -2,8 +2,6 @@ import type { ToolbarSlotRegistry, ShellSlotCtx } from './slots';
2
2
  interface Props {
3
3
  registry: ToolbarSlotRegistry;
4
4
  ctx: ShellSlotCtx;
5
- expanded: boolean;
6
- onToggle: () => void;
7
5
  slotProps?: Record<string, Record<string, unknown>>;
8
6
  }
9
7
  declare const Toolbar: import("svelte").Component<Props, {}, "">;
@@ -11,92 +11,59 @@
11
11
 
12
12
  let { mode, role, registry, onSelect }: Props = $props();
13
13
 
14
- let open = $state(false);
15
-
16
- function select(id: string) {
17
- open = false;
18
- onSelect(id);
19
- }
14
+ let modes = $derived(registry.list(role));
20
15
  </script>
21
16
 
22
17
  {#if role === 'admin'}
23
- <div class="mode-slot">
24
- <button class="mode-btn" onclick={() => (open = !open)}>
25
- {mode.label} ▾
26
- </button>
27
- {#if open}
28
- <ul class="mode-menu" role="menu">
29
- {#each registry.list(role) as m (m.id)}
30
- <li role="menuitem">
31
- <button
32
- class="mode-option"
33
- class:active={m.id === mode.id}
34
- onclick={() => select(m.id)}
35
- >
36
- {m.label}
37
- </button>
38
- </li>
39
- {/each}
40
- </ul>
41
- {/if}
18
+ <div class="mode-bar" role="toolbar" aria-label="Shell mode">
19
+ {#each modes as m (m.id)}
20
+ <button
21
+ type="button"
22
+ class="mode-btn"
23
+ class:active={m.id === mode.id}
24
+ aria-pressed={m.id === mode.id}
25
+ onclick={() => onSelect(m.id)}
26
+ >
27
+ {m.label}
28
+ </button>
29
+ {/each}
42
30
  </div>
43
31
  {:else}
44
32
  <span class="mode-label">{mode.label}</span>
45
33
  {/if}
46
34
 
47
35
  <style>
48
- .mode-slot {
49
- position: relative;
50
- display: inline-block;
36
+ .mode-bar {
37
+ display: inline-flex;
38
+ gap: 2px;
39
+ padding: 1px;
40
+ border: 1px solid var(--shell-border, #444);
41
+ border-radius: 3px;
51
42
  }
52
43
 
53
44
  .mode-btn {
54
45
  background: none;
55
- border: 1px solid var(--shell-border, #444);
56
- color: var(--shell-fg, #ddd);
57
- padding: 2px 6px;
58
- border-radius: 3px;
46
+ border: none;
47
+ color: var(--shell-fg-dim, var(--shell-fg-muted, #888));
48
+ padding: 2px 8px;
49
+ border-radius: 2px;
59
50
  cursor: pointer;
60
51
  font-size: 0.85em;
52
+ line-height: 1.4;
61
53
  }
62
54
 
63
55
  .mode-btn:hover {
64
- background: var(--shell-hover, #222);
65
- }
66
-
67
- .mode-menu {
68
- position: absolute;
69
- top: 100%;
70
- left: 0;
71
- margin: 2px 0 0;
72
- padding: 0;
73
- list-style: none;
74
- background: var(--shell-bg, #111);
75
- border: 1px solid var(--shell-border, #444);
76
- border-radius: 3px;
77
- z-index: 100;
78
- min-width: 100%;
79
- }
80
-
81
- .mode-option {
82
- display: block;
83
- width: 100%;
84
- background: none;
85
- border: none;
56
+ background: var(--shell-hover, color-mix(in srgb, var(--shell-fg, #ddd) 10%, transparent));
86
57
  color: var(--shell-fg, #ddd);
87
- padding: 4px 10px;
88
- text-align: left;
89
- cursor: pointer;
90
- font-size: 0.85em;
91
58
  }
92
59
 
93
- .mode-option:hover,
94
- .mode-option.active {
95
- background: var(--shell-hover, #222);
60
+ .mode-btn.active {
61
+ background: var(--shell-accent, #7c7cf0);
62
+ color: var(--shell-bg, #1a1a2e);
96
63
  }
97
64
 
98
65
  .mode-label {
99
66
  font-size: 0.85em;
100
- color: var(--shell-fg-dim, #888);
67
+ color: var(--shell-fg-dim, var(--shell-fg-muted, #888));
101
68
  }
102
69
  </style>
@@ -0,0 +1,3 @@
1
+ import type { DocumentBackend } from './documents/types';
2
+ /** Factory wrapper so test code can call createMemoryDocumentBackend() without new. */
3
+ export declare function createMemoryDocumentBackend(): DocumentBackend;
@@ -0,0 +1,77 @@
1
+ // src/documents/backends.ts
2
+ function compositeKey(tenantId, shardId, path) {
3
+ return `${tenantId}/${shardId}/${path}`;
4
+ }
5
+ function keyPrefix(tenantId, shardId) {
6
+ return `${tenantId}/${shardId}/`;
7
+ }
8
+ var MemoryDocumentBackend = class {
9
+ #store = /* @__PURE__ */ new Map();
10
+ async read(tenantId, shardId, path) {
11
+ const entry = this.#store.get(compositeKey(tenantId, shardId, path));
12
+ return entry ? entry.content : null;
13
+ }
14
+ async write(tenantId, shardId, path, content) {
15
+ const size = typeof content === "string" ? new Blob([content]).size : content.byteLength;
16
+ this.#store.set(compositeKey(tenantId, shardId, path), {
17
+ content,
18
+ size,
19
+ lastModified: Date.now()
20
+ });
21
+ }
22
+ async delete(tenantId, shardId, path) {
23
+ this.#store.delete(compositeKey(tenantId, shardId, path));
24
+ }
25
+ async list(tenantId, shardId) {
26
+ const prefix = keyPrefix(tenantId, shardId);
27
+ const out = [];
28
+ for (const [key, entry] of this.#store) {
29
+ if (key.startsWith(prefix)) {
30
+ out.push({
31
+ path: key.slice(prefix.length),
32
+ size: entry.size,
33
+ lastModified: entry.lastModified
34
+ });
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ async exists(tenantId, shardId, path) {
40
+ return this.#store.has(compositeKey(tenantId, shardId, path));
41
+ }
42
+ async listAllShards(tenantId) {
43
+ const prefix = `${tenantId}/`;
44
+ const shards = /* @__PURE__ */ new Set();
45
+ for (const key of this.#store.keys()) {
46
+ if (!key.startsWith(prefix)) continue;
47
+ const rest = key.slice(prefix.length);
48
+ const slash = rest.indexOf("/");
49
+ if (slash < 0) continue;
50
+ shards.add(rest.slice(0, slash));
51
+ }
52
+ return [...shards];
53
+ }
54
+ async listAllDocuments(tenantId) {
55
+ const prefix = `${tenantId}/`;
56
+ const out = [];
57
+ for (const [key, entry] of this.#store) {
58
+ if (!key.startsWith(prefix)) continue;
59
+ const rest = key.slice(prefix.length);
60
+ const slash = rest.indexOf("/");
61
+ if (slash < 0) continue;
62
+ const shardId = rest.slice(0, slash);
63
+ const path = rest.slice(slash + 1);
64
+ out.push({ shardId, path, size: entry.size, lastModified: entry.lastModified });
65
+ }
66
+ return out;
67
+ }
68
+ };
69
+
70
+ // src/testing.ts
71
+ function createMemoryDocumentBackend() {
72
+ return new MemoryDocumentBackend();
73
+ }
74
+ export {
75
+ createMemoryDocumentBackend
76
+ };
77
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/documents/backends.ts", "../src/testing.ts"],
4
+ "sourcesContent": ["/*\n * Document zone backends \u2014 concrete storage implementations.\n *\n * MemoryDocumentBackend: Map-based, for tests and ephemeral use.\n * IndexedDBDocumentBackend: The web default. Lazy-inits on first\n * operation to avoid blocking bootstrap.\n */\n\nimport type { DocumentBackend, DocumentMeta } from './types';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction compositeKey(tenantId: string, shardId: string, path: string): string {\n return `${tenantId}/${shardId}/${path}`;\n}\n\nfunction keyPrefix(tenantId: string, shardId: string): string {\n return `${tenantId}/${shardId}/`;\n}\n\n// ---------------------------------------------------------------------------\n// MemoryDocumentBackend\n// ---------------------------------------------------------------------------\n\ninterface MemoryEntry {\n content: string | ArrayBuffer;\n size: number;\n lastModified: number;\n}\n\nexport class MemoryDocumentBackend implements DocumentBackend {\n #store = new Map<string, MemoryEntry>();\n\n async read(\n tenantId: string,\n shardId: string,\n path: string,\n ): Promise<string | ArrayBuffer | null> {\n const entry = this.#store.get(compositeKey(tenantId, shardId, path));\n return entry ? entry.content : null;\n }\n\n async write(\n tenantId: string,\n shardId: string,\n path: string,\n content: string | ArrayBuffer,\n ): Promise<void> {\n const size =\n typeof content === 'string' ? new Blob([content]).size : content.byteLength;\n this.#store.set(compositeKey(tenantId, shardId, path), {\n content,\n size,\n lastModified: Date.now(),\n });\n }\n\n async delete(tenantId: string, shardId: string, path: string): Promise<void> {\n this.#store.delete(compositeKey(tenantId, shardId, path));\n }\n\n async list(tenantId: string, shardId: string): Promise<DocumentMeta[]> {\n const prefix = keyPrefix(tenantId, shardId);\n const out: DocumentMeta[] = [];\n for (const [key, entry] of this.#store) {\n if (key.startsWith(prefix)) {\n out.push({\n path: key.slice(prefix.length),\n size: entry.size,\n lastModified: entry.lastModified,\n });\n }\n }\n return out;\n }\n\n async exists(tenantId: string, shardId: string, path: string): Promise<boolean> {\n return this.#store.has(compositeKey(tenantId, shardId, path));\n }\n\n async listAllShards(tenantId: string): Promise<string[]> {\n const prefix = `${tenantId}/`;\n const shards = new Set<string>();\n for (const key of this.#store.keys()) {\n if (!key.startsWith(prefix)) continue;\n const rest = key.slice(prefix.length);\n const slash = rest.indexOf('/');\n if (slash < 0) continue;\n shards.add(rest.slice(0, slash));\n }\n return [...shards];\n }\n\n async listAllDocuments(\n tenantId: string,\n ): Promise<Array<DocumentMeta & { shardId: string }>> {\n const prefix = `${tenantId}/`;\n const out: Array<DocumentMeta & { shardId: string }> = [];\n for (const [key, entry] of this.#store) {\n if (!key.startsWith(prefix)) continue;\n const rest = key.slice(prefix.length);\n const slash = rest.indexOf('/');\n if (slash < 0) continue;\n const shardId = rest.slice(0, slash);\n const path = rest.slice(slash + 1);\n out.push({ shardId, path, size: entry.size, lastModified: entry.lastModified });\n }\n return out;\n }\n}\n\n// ---------------------------------------------------------------------------\n// IndexedDBDocumentBackend\n// ---------------------------------------------------------------------------\n\nconst IDB_NAME = 'sh3-documents';\nconst IDB_STORE = 'docs';\nconst IDB_VERSION = 2;\n\ninterface IDBEntry {\n content: string | ArrayBuffer;\n size: number;\n lastModified: number;\n}\n\nexport class IndexedDBDocumentBackend implements DocumentBackend {\n #dbPromise: Promise<IDBDatabase> | null = null;\n\n /**\n * Lazy-open the database on first use. The promise is cached so\n * subsequent calls await the same open.\n */\n #db(): Promise<IDBDatabase> {\n if (!this.#dbPromise) {\n this.#dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const req = indexedDB.open(IDB_NAME, IDB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(IDB_STORE)) {\n db.createObjectStore(IDB_STORE);\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n }\n return this.#dbPromise;\n }\n\n /** Run a single-store transaction and return the request result. */\n async #tx<T>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>,\n ): Promise<T> {\n const db = await this.#db();\n return new Promise<T>((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, mode);\n const store = tx.objectStore(IDB_STORE);\n const req = fn(store);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n }\n\n async read(\n tenantId: string,\n shardId: string,\n path: string,\n ): Promise<string | ArrayBuffer | null> {\n const key = compositeKey(tenantId, shardId, path);\n const entry = await this.#tx<IDBEntry | undefined>('readonly', (s) => s.get(key));\n return entry ? entry.content : null;\n }\n\n async write(\n tenantId: string,\n shardId: string,\n path: string,\n content: string | ArrayBuffer,\n ): Promise<void> {\n const key = compositeKey(tenantId, shardId, path);\n const size =\n typeof content === 'string' ? new Blob([content]).size : content.byteLength;\n const entry: IDBEntry = { content, size, lastModified: Date.now() };\n await this.#tx('readwrite', (s) => s.put(entry, key));\n }\n\n async delete(tenantId: string, shardId: string, path: string): Promise<void> {\n const key = compositeKey(tenantId, shardId, path);\n await this.#tx('readwrite', (s) => s.delete(key));\n }\n\n async list(tenantId: string, shardId: string): Promise<DocumentMeta[]> {\n const prefix = keyPrefix(tenantId, shardId);\n const db = await this.#db();\n return new Promise<DocumentMeta[]>((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, 'readonly');\n const store = tx.objectStore(IDB_STORE);\n // IDBKeyRange.bound selects all keys that start with the prefix.\n // The upper bound appends a character beyond '/' to capture all\n // sub-paths without over-selecting.\n const range = IDBKeyRange.bound(prefix, prefix + '\\uffff', false, false);\n const req = store.openCursor(range);\n const out: DocumentMeta[] = [];\n req.onsuccess = () => {\n const cursor = req.result;\n if (cursor) {\n const entry = cursor.value as IDBEntry;\n out.push({\n path: (cursor.key as string).slice(prefix.length),\n size: entry.size,\n lastModified: entry.lastModified,\n });\n cursor.continue();\n } else {\n resolve(out);\n }\n };\n req.onerror = () => reject(req.error);\n });\n }\n\n async exists(tenantId: string, shardId: string, path: string): Promise<boolean> {\n const key = compositeKey(tenantId, shardId, path);\n // getKey is cheaper than get \u2014 avoids deserializing the value.\n const result = await this.#tx<IDBValidKey | undefined>(\n 'readonly',\n (s) => s.getKey(key),\n );\n return result !== undefined;\n }\n\n async listAllShards(tenantId: string): Promise<string[]> {\n const prefix = `${tenantId}/`;\n const db = await this.#db();\n return new Promise<string[]>((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, 'readonly');\n const store = tx.objectStore(IDB_STORE);\n const range = IDBKeyRange.bound(prefix, prefix + '\\uffff', false, false);\n const req = store.openKeyCursor(range);\n const shards = new Set<string>();\n req.onsuccess = () => {\n const cursor = req.result;\n if (cursor) {\n const rest = (cursor.key as string).slice(prefix.length);\n const slash = rest.indexOf('/');\n if (slash >= 0) shards.add(rest.slice(0, slash));\n cursor.continue();\n } else {\n resolve([...shards]);\n }\n };\n req.onerror = () => reject(req.error);\n });\n }\n\n async listAllDocuments(\n tenantId: string,\n ): Promise<Array<DocumentMeta & { shardId: string }>> {\n const prefix = `${tenantId}/`;\n const db = await this.#db();\n return new Promise<Array<DocumentMeta & { shardId: string }>>((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, 'readonly');\n const store = tx.objectStore(IDB_STORE);\n const range = IDBKeyRange.bound(prefix, prefix + '\\uffff', false, false);\n const req = store.openCursor(range);\n const out: Array<DocumentMeta & { shardId: string }> = [];\n req.onsuccess = () => {\n const cursor = req.result;\n if (cursor) {\n const rest = (cursor.key as string).slice(prefix.length);\n const slash = rest.indexOf('/');\n if (slash >= 0) {\n const entry = cursor.value as IDBEntry;\n out.push({\n shardId: rest.slice(0, slash),\n path: rest.slice(slash + 1),\n size: entry.size,\n lastModified: entry.lastModified,\n });\n }\n cursor.continue();\n } else {\n resolve(out);\n }\n };\n req.onerror = () => reject(req.error);\n });\n }\n}\n", "/*\n * Testing utilities subpath entry \u2014 exports in-memory helpers for use\n * in sh3-server and shard tests without pulling in browser-only code.\n */\n\nimport { MemoryDocumentBackend } from './documents/backends';\nimport type { DocumentBackend } from './documents/types';\n\n/** Factory wrapper so test code can call createMemoryDocumentBackend() without new. */\nexport function createMemoryDocumentBackend(): DocumentBackend {\n return new MemoryDocumentBackend();\n}\n"],
5
+ "mappings": ";AAcA,SAAS,aAAa,UAAkB,SAAiB,MAAsB;AAC7E,SAAO,GAAG,QAAQ,IAAI,OAAO,IAAI,IAAI;AACvC;AAEA,SAAS,UAAU,UAAkB,SAAyB;AAC5D,SAAO,GAAG,QAAQ,IAAI,OAAO;AAC/B;AAYO,IAAM,wBAAN,MAAuD;AAAA,EAC5D,SAAS,oBAAI,IAAyB;AAAA,EAEtC,MAAM,KACJ,UACA,SACA,MACsC;AACtC,UAAM,QAAQ,KAAK,OAAO,IAAI,aAAa,UAAU,SAAS,IAAI,CAAC;AACnE,WAAO,QAAQ,MAAM,UAAU;AAAA,EACjC;AAAA,EAEA,MAAM,MACJ,UACA,SACA,MACA,SACe;AACf,UAAM,OACJ,OAAO,YAAY,WAAW,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,QAAQ;AACnE,SAAK,OAAO,IAAI,aAAa,UAAU,SAAS,IAAI,GAAG;AAAA,MACrD;AAAA,MACA;AAAA,MACA,cAAc,KAAK,IAAI;AAAA,IACzB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,UAAkB,SAAiB,MAA6B;AAC3E,SAAK,OAAO,OAAO,aAAa,UAAU,SAAS,IAAI,CAAC;AAAA,EAC1D;AAAA,EAEA,MAAM,KAAK,UAAkB,SAA0C;AACrE,UAAM,SAAS,UAAU,UAAU,OAAO;AAC1C,UAAM,MAAsB,CAAC;AAC7B,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,QAAQ;AACtC,UAAI,IAAI,WAAW,MAAM,GAAG;AAC1B,YAAI,KAAK;AAAA,UACP,MAAM,IAAI,MAAM,OAAO,MAAM;AAAA,UAC7B,MAAM,MAAM;AAAA,UACZ,cAAc,MAAM;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAO,UAAkB,SAAiB,MAAgC;AAC9E,WAAO,KAAK,OAAO,IAAI,aAAa,UAAU,SAAS,IAAI,CAAC;AAAA,EAC9D;AAAA,EAEA,MAAM,cAAc,UAAqC;AACvD,UAAM,SAAS,GAAG,QAAQ;AAC1B,UAAM,SAAS,oBAAI,IAAY;AAC/B,eAAW,OAAO,KAAK,OAAO,KAAK,GAAG;AACpC,UAAI,CAAC,IAAI,WAAW,MAAM,EAAG;AAC7B,YAAM,OAAO,IAAI,MAAM,OAAO,MAAM;AACpC,YAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,UAAI,QAAQ,EAAG;AACf,aAAO,IAAI,KAAK,MAAM,GAAG,KAAK,CAAC;AAAA,IACjC;AACA,WAAO,CAAC,GAAG,MAAM;AAAA,EACnB;AAAA,EAEA,MAAM,iBACJ,UACoD;AACpD,UAAM,SAAS,GAAG,QAAQ;AAC1B,UAAM,MAAiD,CAAC;AACxD,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,QAAQ;AACtC,UAAI,CAAC,IAAI,WAAW,MAAM,EAAG;AAC7B,YAAM,OAAO,IAAI,MAAM,OAAO,MAAM;AACpC,YAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,UAAI,QAAQ,EAAG;AACf,YAAM,UAAU,KAAK,MAAM,GAAG,KAAK;AACnC,YAAM,OAAO,KAAK,MAAM,QAAQ,CAAC;AACjC,UAAI,KAAK,EAAE,SAAS,MAAM,MAAM,MAAM,MAAM,cAAc,MAAM,aAAa,CAAC;AAAA,IAChF;AACA,WAAO;AAAA,EACT;AACF;;;ACtGO,SAAS,8BAA+C;AAC7D,SAAO,IAAI,sBAAsB;AACnC;",
6
+ "names": []
7
+ }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.8.1";
2
+ export declare const VERSION = "0.8.2";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.8.1';
2
+ export const VERSION = '0.8.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -26,10 +26,18 @@
26
26
  "./build": {
27
27
  "types": "./dist/build.d.ts",
28
28
  "default": "./dist/build.js"
29
+ },
30
+ "./server-sync": {
31
+ "types": "./dist/server-sync.d.ts",
32
+ "default": "./dist/server-sync.js"
33
+ },
34
+ "./testing": {
35
+ "types": "./dist/testing.d.ts",
36
+ "default": "./dist/testing.js"
29
37
  }
30
38
  },
31
39
  "scripts": {
32
- "build": "node --import tsx scripts/sync-version.ts && svelte-package -i src -o dist && node --import tsx scripts/generate-api-docs.ts",
40
+ "build": "node --import tsx scripts/sync-version.ts && svelte-package -i src -o dist && node --import tsx scripts/bundle-node-entries.ts && node --import tsx scripts/generate-api-docs.ts",
33
41
  "check": "svelte-check --tsconfig ./tsconfig.json",
34
42
  "pack": "npm run build && npm pack",
35
43
  "test": "vitest run --passWithNoTests",