sh3-core 0.20.2 → 0.21.0

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 (69) hide show
  1. package/dist/BrandSlot.svelte +2 -2
  2. package/dist/actions/ctx-actions.svelte.test.js +2 -2
  3. package/dist/api.d.ts +2 -2
  4. package/dist/api.js +1 -1
  5. package/dist/app/store/StoreView.svelte +26 -35
  6. package/dist/app/store/storeShard.svelte.js +35 -49
  7. package/dist/app/store/verbs.js +24 -55
  8. package/dist/artifact.d.ts +2 -0
  9. package/dist/boot/satellitePayload.d.ts +2 -0
  10. package/dist/boot/satellitePayload.test.js +19 -0
  11. package/dist/build.d.ts +7 -1
  12. package/dist/build.js +34 -9
  13. package/dist/build.test.js +27 -1
  14. package/dist/createShell.js +34 -9
  15. package/dist/documents/browse.d.ts +20 -0
  16. package/dist/documents/browse.js +35 -0
  17. package/dist/documents/browse.test.js +125 -0
  18. package/dist/documents/config.d.ts +0 -4
  19. package/dist/documents/config.js +0 -8
  20. package/dist/documents/http-backend.d.ts +5 -0
  21. package/dist/documents/http-backend.js +25 -0
  22. package/dist/documents/http-backend.test.js +66 -0
  23. package/dist/documents/index.d.ts +1 -1
  24. package/dist/documents/index.js +1 -1
  25. package/dist/documents/types.d.ts +11 -0
  26. package/dist/env/client.d.ts +6 -10
  27. package/dist/env/client.js +11 -21
  28. package/dist/env/index.d.ts +2 -1
  29. package/dist/env/index.js +1 -1
  30. package/dist/host-entry.d.ts +1 -1
  31. package/dist/host-entry.js +1 -1
  32. package/dist/host.d.ts +1 -1
  33. package/dist/host.js +1 -1
  34. package/dist/layout/slotHostPool.svelte.js +2 -2
  35. package/dist/overlays/FloatFrame.svelte +1 -0
  36. package/dist/projects/session-state.svelte.d.ts +3 -0
  37. package/dist/projects/session-state.svelte.js +25 -0
  38. package/dist/projects/session-state.test.js +43 -2
  39. package/dist/projects-shard/ProjectsSection.svelte +14 -18
  40. package/dist/registry/archive.d.ts +12 -0
  41. package/dist/registry/archive.js +80 -0
  42. package/dist/registry/archive.test.d.ts +1 -0
  43. package/dist/registry/archive.test.js +84 -0
  44. package/dist/registry/client.d.ts +9 -29
  45. package/dist/registry/client.js +14 -60
  46. package/dist/registry/client.test.js +31 -21
  47. package/dist/registry/index.d.ts +2 -2
  48. package/dist/registry/index.js +1 -1
  49. package/dist/registry/installer.d.ts +4 -4
  50. package/dist/registry/installer.js +74 -45
  51. package/dist/registry/schema.js +4 -27
  52. package/dist/registry/schema.test.d.ts +1 -0
  53. package/dist/registry/schema.test.js +41 -0
  54. package/dist/registry/types.d.ts +16 -41
  55. package/dist/runtime/runVerb-shell.test.js +2 -2
  56. package/dist/runtime/runVerb.test.js +2 -2
  57. package/dist/sh3core-shard/appActions.js +5 -2
  58. package/dist/shards/activate-browse.test.js +2 -2
  59. package/dist/shards/activate-contributions.test.js +2 -2
  60. package/dist/shards/activate-error-isolation.test.js +3 -3
  61. package/dist/shards/activate-on-key-revoked.test.js +2 -2
  62. package/dist/shards/activate-runtime.test.js +2 -2
  63. package/dist/shards/activate.svelte.js +4 -4
  64. package/dist/shards/ctx-fetch.test.js +4 -4
  65. package/dist/shell-shard/verbs/xfer.js +13 -27
  66. package/dist/shell-shard/verbs/xfer.test.js +36 -25
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +3 -2
@@ -12,6 +12,31 @@
12
12
  */
13
13
  import { activeApp, breadcrumbApp } from '../apps/registry.svelte';
14
14
  import { unloadApp } from '../apps/lifecycle';
15
+ export const PENDING_SCOPE_KEY = 'sh3:pending-scope';
16
+ export function readPendingScope() {
17
+ if (typeof sessionStorage === 'undefined')
18
+ return null;
19
+ const raw = sessionStorage.getItem(PENDING_SCOPE_KEY);
20
+ if (!raw)
21
+ return null;
22
+ sessionStorage.removeItem(PENDING_SCOPE_KEY);
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ return typeof parsed.projectId === 'string' ? parsed.projectId : null;
26
+ }
27
+ catch (_a) {
28
+ return null;
29
+ }
30
+ }
31
+ export function switchProjectScope(projectId) {
32
+ if (projectId !== null) {
33
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ projectId }));
34
+ }
35
+ else {
36
+ sessionStorage.removeItem(PENDING_SCOPE_KEY);
37
+ }
38
+ window.location.reload();
39
+ }
15
40
  export const sessionState = $state({
16
41
  activeProjectId: null,
17
42
  });
@@ -1,5 +1,5 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { sessionState, setActiveProjectId } from './session-state.svelte';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { sessionState, setActiveProjectId, switchProjectScope, readPendingScope, PENDING_SCOPE_KEY, } from './session-state.svelte';
3
3
  import { breadcrumbApp, activeApp } from '../apps/registry.svelte';
4
4
  vi.mock('../apps/lifecycle', () => ({
5
5
  unloadApp: vi.fn(),
@@ -53,3 +53,44 @@ describe('sessionState.activeProjectId', () => {
53
53
  expect(lifecycle.unloadApp).not.toHaveBeenCalled();
54
54
  });
55
55
  });
56
+ describe('readPendingScope', () => {
57
+ beforeEach(() => sessionStorage.clear());
58
+ it('returns null when nothing is stored', () => {
59
+ expect(readPendingScope()).toBeNull();
60
+ });
61
+ it('returns the stored projectId and clears the key', () => {
62
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ projectId: 'proj-abc' }));
63
+ expect(readPendingScope()).toBe('proj-abc');
64
+ expect(sessionStorage.getItem(PENDING_SCOPE_KEY)).toBeNull();
65
+ });
66
+ it('returns null and clears a malformed entry', () => {
67
+ sessionStorage.setItem(PENDING_SCOPE_KEY, 'not-json');
68
+ expect(readPendingScope()).toBeNull();
69
+ expect(sessionStorage.getItem(PENDING_SCOPE_KEY)).toBeNull();
70
+ });
71
+ it('returns null if projectId field is not a string', () => {
72
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ other: 42 }));
73
+ expect(readPendingScope()).toBeNull();
74
+ });
75
+ });
76
+ describe('switchProjectScope', () => {
77
+ let reloadSpy;
78
+ beforeEach(() => {
79
+ sessionStorage.clear();
80
+ reloadSpy = vi.fn();
81
+ vi.stubGlobal('location', { reload: reloadSpy });
82
+ });
83
+ afterEach(() => vi.unstubAllGlobals());
84
+ it('writes projectId to sessionStorage and reloads', () => {
85
+ switchProjectScope('proj-abc');
86
+ const raw = sessionStorage.getItem(PENDING_SCOPE_KEY);
87
+ expect(JSON.parse(raw)).toEqual({ projectId: 'proj-abc' });
88
+ expect(reloadSpy).toHaveBeenCalledOnce();
89
+ });
90
+ it('removes the key for null (personal scope) and reloads', () => {
91
+ sessionStorage.setItem(PENDING_SCOPE_KEY, JSON.stringify({ projectId: 'old' }));
92
+ switchProjectScope(null);
93
+ expect(sessionStorage.getItem(PENDING_SCOPE_KEY)).toBeNull();
94
+ expect(reloadSpy).toHaveBeenCalledOnce();
95
+ });
96
+ });
@@ -1,26 +1,13 @@
1
1
  <script lang="ts">
2
- /*
3
- * Projects section for Sh3Home.
4
- *
5
- * Renders the list of projects the current user is a member of as
6
- * selectable cards. Selecting a project sets sessionState.activeProjectId
7
- * (which then filters the apps grid via the appAllowlist) and binds any
8
- * subsequently launched app's documents to the project scope.
9
- */
10
-
11
2
  import { projectsState, openProjectManage } from './projectsShard.svelte';
12
- import { sessionState, setActiveProjectId } from '../projects/session-state.svelte';
3
+ import { sessionState, switchProjectScope } from '../projects/session-state.svelte';
13
4
  import { isAdmin } from '../auth/auth.svelte';
14
5
  import HomeSection from '../sh3core-shard/HomeSection.svelte';
6
+ import Button from '../primitives/Button.svelte';
15
7
 
16
8
  const visible = $derived(projectsState.projects.length > 0);
17
9
  const activeId = $derived(sessionState.activeProjectId);
18
10
  const elevated = $derived(isAdmin());
19
-
20
- function selectProject(id: string) {
21
- setActiveProjectId(activeId === id ? null : id);
22
- }
23
-
24
11
  function editProject(id: string, ev: MouseEvent) {
25
12
  ev.stopPropagation();
26
13
  const project = projectsState.projects.find((p) => p.id === id) ?? null;
@@ -28,7 +15,13 @@
28
15
  }
29
16
  </script>
30
17
 
31
- {#if visible}
18
+ {#if activeId !== null}
19
+ <HomeSection title="Project" persistKey="project-active">
20
+ <div class="leave-project">
21
+ <Button onclick={() => switchProjectScope(null)}>Leave project</Button>
22
+ </div>
23
+ </HomeSection>
24
+ {:else if visible}
32
25
  <HomeSection title="Projects" persistKey="projects">
33
26
  <div class="projects-grid">
34
27
  {#each projectsState.projects as project (project.id)}
@@ -36,8 +29,7 @@
36
29
  <button
37
30
  type="button"
38
31
  class="project-card"
39
- class:active={activeId === project.id}
40
- onclick={() => selectProject(project.id)}
32
+ onclick={() => switchProjectScope(project.id)}
41
33
  title={project.description ?? `${project.members.length} member${project.members.length === 1 ? '' : 's'}`}
42
34
  >
43
35
  <span class="project-name">{project.name}</span>
@@ -104,4 +96,8 @@
104
96
  }
105
97
  .project-name { font-weight: 600; font-size: 13px; }
106
98
  .project-meta { font-size: 11px; color: var(--sh3-fg-muted); }
99
+ .leave-project {
100
+ display: flex;
101
+ justify-content: flex-start;
102
+ }
107
103
  </style>
@@ -0,0 +1,12 @@
1
+ import type { ArtifactManifest } from '../artifact.js';
2
+ export declare class ArchiveValidationError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function validateArchive(bytes: Uint8Array): void;
6
+ export declare function readManifestFromArchive(bytes: Uint8Array): ArtifactManifest;
7
+ export declare function readFileFromArchive(bytes: Uint8Array, filename: string): ArrayBuffer | null;
8
+ export declare function createArchive(entries: {
9
+ manifest: ArtifactManifest;
10
+ client?: Uint8Array;
11
+ server?: Uint8Array;
12
+ }): Uint8Array;
@@ -0,0 +1,80 @@
1
+ import { unzipSync, zipSync, strToU8 } from 'fflate';
2
+ export class ArchiveValidationError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'ArchiveValidationError';
6
+ }
7
+ }
8
+ export function validateArchive(bytes) {
9
+ let files;
10
+ try {
11
+ files = unzipSync(bytes);
12
+ }
13
+ catch (_a) {
14
+ throw new ArchiveValidationError('Invalid ZIP archive');
15
+ }
16
+ if (!files['manifest.json']) {
17
+ throw new ArchiveValidationError('Archive missing manifest.json');
18
+ }
19
+ if (!('client.js' in files) && !('server.js' in files)) {
20
+ throw new ArchiveValidationError('Archive must contain at least one of client.js or server.js');
21
+ }
22
+ }
23
+ export function readManifestFromArchive(bytes) {
24
+ let files;
25
+ try {
26
+ files = unzipSync(bytes);
27
+ }
28
+ catch (_a) {
29
+ throw new ArchiveValidationError('Invalid ZIP archive');
30
+ }
31
+ const raw = files['manifest.json'];
32
+ if (!raw)
33
+ throw new ArchiveValidationError('Archive missing manifest.json');
34
+ let manifest;
35
+ try {
36
+ manifest = JSON.parse(new TextDecoder().decode(raw));
37
+ }
38
+ catch (_b) {
39
+ throw new ArchiveValidationError('manifest.json is not valid JSON');
40
+ }
41
+ assertManifestShape(manifest);
42
+ return manifest;
43
+ }
44
+ function assertManifestShape(m) {
45
+ if (!m || typeof m !== 'object' || Array.isArray(m)) {
46
+ throw new ArchiveValidationError('manifest.json must be a JSON object');
47
+ }
48
+ const obj = m;
49
+ for (const field of ['id', 'type', 'version', 'contractVersion']) {
50
+ if (obj[field] == null) {
51
+ throw new ArchiveValidationError(`manifest.json missing required field: ${field}`);
52
+ }
53
+ }
54
+ }
55
+ export function readFileFromArchive(bytes, filename) {
56
+ let files;
57
+ try {
58
+ files = unzipSync(bytes);
59
+ }
60
+ catch (_a) {
61
+ return null;
62
+ }
63
+ const data = files[filename];
64
+ if (!data)
65
+ return null;
66
+ return data.slice().buffer;
67
+ }
68
+ export function createArchive(entries) {
69
+ if (!entries.client && !entries.server) {
70
+ throw new ArchiveValidationError('Archive must contain at least one of client or server');
71
+ }
72
+ const files = {
73
+ 'manifest.json': strToU8(JSON.stringify(entries.manifest, null, 2)),
74
+ };
75
+ if (entries.client)
76
+ files['client.js'] = entries.client;
77
+ if (entries.server)
78
+ files['server.js'] = entries.server;
79
+ return zipSync(files);
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { zipSync, strToU8 } from 'fflate';
3
+ import { ArchiveValidationError, validateArchive, readManifestFromArchive, readFileFromArchive, createArchive, } from './archive.js';
4
+ const MANIFEST = {
5
+ id: 'test-shard',
6
+ type: 'shard',
7
+ label: 'Test Shard',
8
+ version: '1.0.0',
9
+ contractVersion: 1,
10
+ };
11
+ function makeZip(files) {
12
+ const entries = {};
13
+ for (const [k, v] of Object.entries(files))
14
+ entries[k] = strToU8(v);
15
+ return zipSync(entries);
16
+ }
17
+ describe('validateArchive', () => {
18
+ it('accepts archive with client.js', () => {
19
+ const zip = makeZip({ 'manifest.json': JSON.stringify(MANIFEST), 'client.js': 'export default {}' });
20
+ expect(() => validateArchive(zip)).not.toThrow();
21
+ });
22
+ it('accepts archive with server.js only', () => {
23
+ const zip = makeZip({ 'manifest.json': JSON.stringify(MANIFEST), 'server.js': 'export default {}' });
24
+ expect(() => validateArchive(zip)).not.toThrow();
25
+ });
26
+ it('accepts archive with both client.js and server.js', () => {
27
+ const zip = makeZip({ 'manifest.json': JSON.stringify(MANIFEST), 'client.js': 'x', 'server.js': 'y' });
28
+ expect(() => validateArchive(zip)).not.toThrow();
29
+ });
30
+ it('rejects non-ZIP bytes', () => {
31
+ expect(() => validateArchive(new Uint8Array([1, 2, 3]))).toThrow(ArchiveValidationError);
32
+ });
33
+ it('rejects archive missing manifest.json', () => {
34
+ const zip = makeZip({ 'client.js': 'x' });
35
+ expect(() => validateArchive(zip)).toThrow(ArchiveValidationError);
36
+ });
37
+ it('rejects archive with no JS files', () => {
38
+ const zip = makeZip({ 'manifest.json': JSON.stringify(MANIFEST) });
39
+ expect(() => validateArchive(zip)).toThrow(/at least one/);
40
+ });
41
+ });
42
+ describe('readManifestFromArchive', () => {
43
+ it('extracts and parses manifest.json', () => {
44
+ const zip = makeZip({ 'manifest.json': JSON.stringify(MANIFEST), 'client.js': '' });
45
+ const result = readManifestFromArchive(zip);
46
+ expect(result.id).toBe('test-shard');
47
+ expect(result.contractVersion).toBe(1);
48
+ });
49
+ it('throws if required field is missing', () => {
50
+ const bad = { id: 'x', type: 'shard', version: '1.0.0' }; // missing contractVersion
51
+ const zip = makeZip({ 'manifest.json': JSON.stringify(bad), 'client.js': '' });
52
+ expect(() => readManifestFromArchive(zip)).toThrow(ArchiveValidationError);
53
+ });
54
+ it('throws if manifest.json is not valid JSON', () => {
55
+ const zip = makeZip({ 'manifest.json': 'not json {{{', 'client.js': '' });
56
+ expect(() => readManifestFromArchive(zip)).toThrow(ArchiveValidationError);
57
+ });
58
+ });
59
+ describe('readFileFromArchive', () => {
60
+ it('returns bytes for a present file', () => {
61
+ const zip = makeZip({ 'manifest.json': JSON.stringify(MANIFEST), 'client.js': 'hello' });
62
+ const result = readFileFromArchive(zip, 'client.js');
63
+ expect(result).not.toBeNull();
64
+ expect(new TextDecoder().decode(new Uint8Array(result))).toBe('hello');
65
+ });
66
+ it('returns null for an absent file', () => {
67
+ const zip = makeZip({ 'manifest.json': JSON.stringify(MANIFEST), 'client.js': '' });
68
+ expect(readFileFromArchive(zip, 'server.js')).toBeNull();
69
+ });
70
+ });
71
+ describe('createArchive', () => {
72
+ it('round-trips the manifest', () => {
73
+ const archive = createArchive({ manifest: MANIFEST, client: strToU8('export default {}') });
74
+ expect(readManifestFromArchive(archive).id).toBe('test-shard');
75
+ });
76
+ it('includes client.js when provided', () => {
77
+ const archive = createArchive({ manifest: MANIFEST, client: strToU8('console.log(1)') });
78
+ const bytes = readFileFromArchive(archive, 'client.js');
79
+ expect(new TextDecoder().decode(new Uint8Array(bytes))).toBe('console.log(1)');
80
+ });
81
+ it('throws when neither client nor server is provided', () => {
82
+ expect(() => createArchive({ manifest: MANIFEST })).toThrow(ArchiveValidationError);
83
+ });
84
+ });
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Registry client -- fetches and merges registry indices, resolves
3
- * packages, and downloads bundles with integrity verification.
3
+ * packages, and downloads archives with integrity verification.
4
4
  *
5
5
  * Typical usage:
6
6
  * ```ts
7
7
  * const packages = await fetchRegistries(['https://example.com/registry.json']);
8
8
  * const pkg = packages.find(p => p.entry.id === 'my-shard');
9
9
  * if (pkg) {
10
- * const data = await fetchBundle(pkg.latest);
10
+ * const archive = await fetchArchive(pkg.latest, pkg.sourceRegistry);
11
11
  * const meta = buildPackageMeta(pkg, pkg.latest);
12
12
  * }
13
13
  * ```
@@ -44,36 +44,16 @@ export interface ResolvedPackage {
44
44
  */
45
45
  export declare function fetchRegistries(urls: string[]): Promise<ResolvedPackage[]>;
46
46
  /**
47
- * Download a bundle and verify its SRI integrity hash.
47
+ * Download a .sh3pkg archive and verify its SRI integrity hash.
48
48
  *
49
- * Fetches `version.bundleUrl`, reads the response as an `ArrayBuffer`, then
50
- * calls `verifyIntegrity` before returning. Throws on any network error,
51
- * non-OK HTTP status, or integrity mismatch — the caller must not execute
52
- * a bundle for which this function threw.
49
+ * Resolves relative `archiveUrl` values against `sourceRegistry`.
50
+ * Throws on network error, non-OK HTTP status, or integrity mismatch.
53
51
  *
54
- * If `bundleUrl` is relative (starts with `/`), it is resolved against
55
- * `sourceRegistry` so cross-origin installs hit the correct server.
56
- *
57
- * @param version - The `PackageVersion` describing the bundle to download.
58
- * @param sourceRegistry - The registry URL this package came from (used to resolve relative bundle paths).
59
- * @returns Raw bundle bytes, verified against `version.integrity`.
60
- * @throws If the fetch fails, the server returns a non-OK status, or the integrity check fails.
61
- */
62
- export declare function fetchBundle(version: PackageVersion, sourceRegistry?: string): Promise<ArrayBuffer>;
63
- /**
64
- * Download a server-side bundle and optionally verify its SRI integrity.
65
- *
66
- * Mirrors `fetchBundle` except `serverIntegrity` is optional in contract v1
67
- * (see ADR-015 proposal). When absent, the download is returned without
68
- * verification and a warning is logged so operators notice provisional
69
- * registries. When present, an integrity mismatch throws.
70
- *
71
- * @param version - The `PackageVersion` describing the server bundle.
72
- * @param sourceRegistry - The registry URL (used to resolve relative paths).
73
- * @returns Raw server bundle bytes.
74
- * @throws If `serverBundleUrl` is absent, the fetch fails, or an integrity mismatch occurs.
52
+ * @param version - PackageVersion describing the archive to fetch.
53
+ * @param sourceRegistry - Registry URL used to resolve relative archiveUrl.
54
+ * @returns Verified archive bytes.
75
55
  */
76
- export declare function fetchServerBundle(version: PackageVersion, sourceRegistry?: string): Promise<ArrayBuffer>;
56
+ export declare function fetchArchive(version: PackageVersion, sourceRegistry?: string): Promise<Uint8Array>;
77
57
  /**
78
58
  * Build a `PackageMeta` record from a resolved package and a chosen version.
79
59
  *
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Registry client -- fetches and merges registry indices, resolves
3
- * packages, and downloads bundles with integrity verification.
3
+ * packages, and downloads archives with integrity verification.
4
4
  *
5
5
  * Typical usage:
6
6
  * ```ts
7
7
  * const packages = await fetchRegistries(['https://example.com/registry.json']);
8
8
  * const pkg = packages.find(p => p.entry.id === 'my-shard');
9
9
  * if (pkg) {
10
- * const data = await fetchBundle(pkg.latest);
10
+ * const archive = await fetchArchive(pkg.latest, pkg.sourceRegistry);
11
11
  * const meta = buildPackageMeta(pkg, pkg.latest);
12
12
  * }
13
13
  * ```
@@ -65,71 +65,27 @@ export async function fetchRegistries(urls) {
65
65
  return results;
66
66
  }
67
67
  /**
68
- * Download a bundle and verify its SRI integrity hash.
68
+ * Download a .sh3pkg archive and verify its SRI integrity hash.
69
69
  *
70
- * Fetches `version.bundleUrl`, reads the response as an `ArrayBuffer`, then
71
- * calls `verifyIntegrity` before returning. Throws on any network error,
72
- * non-OK HTTP status, or integrity mismatch — the caller must not execute
73
- * a bundle for which this function threw.
70
+ * Resolves relative `archiveUrl` values against `sourceRegistry`.
71
+ * Throws on network error, non-OK HTTP status, or integrity mismatch.
74
72
  *
75
- * If `bundleUrl` is relative (starts with `/`), it is resolved against
76
- * `sourceRegistry` so cross-origin installs hit the correct server.
77
- *
78
- * @param version - The `PackageVersion` describing the bundle to download.
79
- * @param sourceRegistry - The registry URL this package came from (used to resolve relative bundle paths).
80
- * @returns Raw bundle bytes, verified against `version.integrity`.
81
- * @throws If the fetch fails, the server returns a non-OK status, or the integrity check fails.
82
- */
83
- export async function fetchBundle(version, sourceRegistry) {
84
- if (!version.bundleUrl || !version.integrity) {
85
- throw new Error('fetchBundle called on a version with no client bundle');
86
- }
87
- let url = version.bundleUrl;
88
- if (sourceRegistry && !/^https?:\/\//i.test(url)) {
89
- url = new URL(url, sourceRegistry).href;
90
- }
91
- const response = await fetch(url);
92
- if (!response.ok) {
93
- throw new Error(`Bundle fetch failed: HTTP ${response.status} ${response.statusText} from ${url}`);
94
- }
95
- const data = await response.arrayBuffer();
96
- await verifyIntegrity(data, version.integrity);
97
- return data;
98
- }
99
- /**
100
- * Download a server-side bundle and optionally verify its SRI integrity.
101
- *
102
- * Mirrors `fetchBundle` except `serverIntegrity` is optional in contract v1
103
- * (see ADR-015 proposal). When absent, the download is returned without
104
- * verification and a warning is logged so operators notice provisional
105
- * registries. When present, an integrity mismatch throws.
106
- *
107
- * @param version - The `PackageVersion` describing the server bundle.
108
- * @param sourceRegistry - The registry URL (used to resolve relative paths).
109
- * @returns Raw server bundle bytes.
110
- * @throws If `serverBundleUrl` is absent, the fetch fails, or an integrity mismatch occurs.
73
+ * @param version - PackageVersion describing the archive to fetch.
74
+ * @param sourceRegistry - Registry URL used to resolve relative archiveUrl.
75
+ * @returns Verified archive bytes.
111
76
  */
112
- export async function fetchServerBundle(version, sourceRegistry) {
113
- if (!version.serverBundleUrl) {
114
- throw new Error('fetchServerBundle called on a version with no serverBundleUrl');
115
- }
116
- let url = version.serverBundleUrl;
77
+ export async function fetchArchive(version, sourceRegistry) {
78
+ let url = version.archiveUrl;
117
79
  if (sourceRegistry && !/^https?:\/\//i.test(url)) {
118
80
  url = new URL(url, sourceRegistry).href;
119
81
  }
120
82
  const response = await fetch(url);
121
83
  if (!response.ok) {
122
- throw new Error(`Server bundle fetch failed: HTTP ${response.status} ${response.statusText} from ${url}`);
123
- }
124
- const data = await response.arrayBuffer();
125
- if (version.serverIntegrity) {
126
- await verifyIntegrity(data, version.serverIntegrity);
127
- }
128
- else {
129
- console.warn(`[sh3] Server bundle at ${url} has no serverIntegrity declared — skipping SRI check. `
130
- + 'This will become an error once the formal registry spec (ADR-015) lands.');
84
+ throw new Error(`Archive fetch failed: HTTP ${response.status} from ${url}`);
131
85
  }
132
- return data;
86
+ const buffer = await response.arrayBuffer();
87
+ await verifyIntegrity(buffer, version.integrity);
88
+ return new Uint8Array(buffer);
133
89
  }
134
90
  /**
135
91
  * Build a `PackageMeta` record from a resolved package and a chosen version.
@@ -151,7 +107,5 @@ export function buildPackageMeta(resolved, version) {
151
107
  sourceRegistry: resolved.sourceRegistry,
152
108
  integrity: version.integrity,
153
109
  requires: version.requires,
154
- hasServerBundle: Boolean(version.serverBundleUrl),
155
- serverIntegrity: version.serverIntegrity,
156
110
  };
157
111
  }
@@ -1,5 +1,5 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { buildPackageMeta } from './client.js';
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { buildPackageMeta, fetchArchive } from './client.js';
3
3
  function makeResolved(version) {
4
4
  return {
5
5
  entry: {
@@ -15,40 +15,50 @@ function makeResolved(version) {
15
15
  };
16
16
  }
17
17
  describe('buildPackageMeta', () => {
18
- it('sets hasServerBundle false when no serverBundleUrl', () => {
18
+ it('builds meta from a resolved package and version', () => {
19
19
  const v = {
20
20
  version: '1.0.0',
21
21
  contractVersion: '0.1.0',
22
- bundleUrl: '/b.js',
22
+ archiveUrl: 'https://example.com/pkg-1.0.0.sh3pkg',
23
23
  integrity: 'sha384-xxx',
24
24
  };
25
25
  const meta = buildPackageMeta(makeResolved(v), v);
26
- expect(meta.hasServerBundle).toBe(false);
27
- expect(meta.serverIntegrity).toBeUndefined();
26
+ expect(meta.id).toBe('test-pkg');
27
+ expect(meta.version).toBe('1.0.0');
28
+ expect(meta.integrity).toBe('sha384-xxx');
29
+ expect(meta.sourceRegistry).toBe('https://example.com/registry.json');
28
30
  });
29
- it('sets hasServerBundle true and propagates serverIntegrity when present', () => {
31
+ it('propagates requires when present', () => {
30
32
  const v = {
31
33
  version: '1.0.0',
32
34
  contractVersion: '0.1.0',
33
- bundleUrl: '/b.js',
35
+ archiveUrl: 'https://example.com/pkg-1.0.0.sh3pkg',
34
36
  integrity: 'sha384-xxx',
35
- serverBundleUrl: '/s.js',
36
- serverIntegrity: 'sha384-yyy',
37
+ requires: [{ id: 'dep', versionRange: '^1.0.0' }],
37
38
  };
38
39
  const meta = buildPackageMeta(makeResolved(v), v);
39
- expect(meta.hasServerBundle).toBe(true);
40
- expect(meta.serverIntegrity).toBe('sha384-yyy');
40
+ expect(meta.requires).toEqual([{ id: 'dep', versionRange: '^1.0.0' }]);
41
41
  });
42
- it('sets hasServerBundle true even when serverIntegrity is missing', () => {
43
- const v = {
42
+ });
43
+ describe('fetchArchive', () => {
44
+ it('fetches and returns archive bytes', async () => {
45
+ const mockBytes = new Uint8Array([1, 2, 3]);
46
+ const hashBuffer = await crypto.subtle.digest('SHA-384', mockBytes.buffer);
47
+ const b64 = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
48
+ const integrity = `sha384-${b64}`;
49
+ const version = {
44
50
  version: '1.0.0',
45
- contractVersion: '0.1.0',
46
- bundleUrl: '/b.js',
47
- integrity: 'sha384-xxx',
48
- serverBundleUrl: '/s.js',
51
+ contractVersion: '1',
52
+ archiveUrl: 'https://example.com/pkg-1.0.0.sh3pkg',
53
+ integrity,
49
54
  };
50
- const meta = buildPackageMeta(makeResolved(v), v);
51
- expect(meta.hasServerBundle).toBe(true);
52
- expect(meta.serverIntegrity).toBeUndefined();
55
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
56
+ ok: true,
57
+ arrayBuffer: () => Promise.resolve(mockBytes.buffer),
58
+ }));
59
+ const result = await fetchArchive(version);
60
+ expect(result).toBeInstanceOf(Uint8Array);
61
+ expect(result.length).toBe(3);
62
+ vi.unstubAllGlobals();
53
63
  });
54
64
  });
@@ -5,9 +5,9 @@
5
5
  * schema validation, integrity verification, client functions, and the
6
6
  * install API.
7
7
  */
8
- export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, } from './types';
8
+ export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, RemoteInstallRequest, } from './types';
9
9
  export { RegistryValidationError, validateRegistryIndex } from './schema';
10
10
  export { verifyIntegrity, computeIntegrity } from './integrity';
11
11
  export type { ResolvedPackage } from './client';
12
- export { fetchRegistries, fetchBundle, buildPackageMeta } from './client';
12
+ export { fetchRegistries, fetchArchive, buildPackageMeta } from './client';
13
13
  export { installPackage, uninstallPackage, listInstalledPackages, loadInstalledPackages, } from './installer';
@@ -9,6 +9,6 @@
9
9
  export { RegistryValidationError, validateRegistryIndex } from './schema';
10
10
  // Integrity verification.
11
11
  export { verifyIntegrity, computeIntegrity } from './integrity';
12
- export { fetchRegistries, fetchBundle, buildPackageMeta } from './client';
12
+ export { fetchRegistries, fetchArchive, buildPackageMeta } from './client';
13
13
  // Install API.
14
14
  export { installPackage, uninstallPackage, listInstalledPackages, loadInstalledPackages, } from './installer';
@@ -50,10 +50,10 @@ export declare function uninstallPackage(id: string): Promise<void>;
50
50
  */
51
51
  export declare function listInstalledPackages(): Promise<InstalledPackage[]>;
52
52
  /**
53
- * Load all installed packages from IndexedDB and register them.
53
+ * Sync installed packages against the server list and load all into the framework.
54
54
  *
55
- * Called once at boot by `bootstrap()`, before any glob-discovered shards
56
- * or the sh3 shard are registered. Individual package failures are logged
57
- * as warnings but do not prevent the sh3 from booting.
55
+ * Server list is authoritative. Packages on server but missing from IndexedDB
56
+ * are fetched from the server's /packages/:id/client.js endpoint and cached.
57
+ * Packages in IndexedDB but absent from the server are evicted.
58
58
  */
59
59
  export declare function loadInstalledPackages(): Promise<void>;