sh3-core 0.20.3 → 0.21.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.
@@ -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>;
@@ -16,11 +16,11 @@
16
16
  */
17
17
  import { loadBundleModule } from './loader';
18
18
  import { savePackage, loadBundle, listInstalled, removePackage } from './storage';
19
- import { verifyIntegrity } from './integrity';
20
19
  import { deactivateShard } from '../shards/activate.svelte';
21
20
  import { unregisterApp } from '../apps/lifecycle';
22
21
  import { registerLoadedBundle } from './register';
23
22
  import { extractBundlePermissions } from './permission-descriptions';
23
+ import { fetchServerPackages } from '../env/client';
24
24
  /**
25
25
  * Install a package from raw bundle bytes and metadata.
26
26
  *
@@ -35,25 +35,8 @@ import { extractBundlePermissions } from './permission-descriptions';
35
35
  * @returns Result object indicating success/failure and hot-load status.
36
36
  */
37
37
  export async function installPackage(bundle, meta, options) {
38
- // 1. Verify bundle integrity before executing any code.
39
- if (!meta.integrity) {
40
- return {
41
- success: false,
42
- hotLoaded: false,
43
- error: 'Missing integrity hash — refusing to install unverified bundle',
44
- };
45
- }
46
- try {
47
- await verifyIntegrity(bundle, meta.integrity);
48
- }
49
- catch (err) {
50
- return {
51
- success: false,
52
- hotLoaded: false,
53
- error: `Integrity check failed: ${err instanceof Error ? err.message : String(err)}`,
54
- };
55
- }
56
- // 2. Load the module from verified bytes (or reuse the caller's copy).
38
+ // 1. Load the module from bytes (or reuse the caller's copy).
39
+ // Archive integrity is verified upstream in fetchArchive() before extraction.
57
40
  let loaded;
58
41
  if (options === null || options === void 0 ? void 0 : options.loaded) {
59
42
  loaded = options.loaded;
@@ -160,40 +143,86 @@ export async function listInstalledPackages() {
160
143
  return listInstalled();
161
144
  }
162
145
  /**
163
- * Load all installed packages from IndexedDB and register them.
146
+ * Sync installed packages against the server list and load all into the framework.
164
147
  *
165
- * Called once at boot by `bootstrap()`, before any glob-discovered shards
166
- * or the sh3 shard are registered. Individual package failures are logged
167
- * as warnings but do not prevent the sh3 from booting.
148
+ * Server list is authoritative. Packages on server but missing from IndexedDB
149
+ * are fetched from the server's /packages/:id/client.js endpoint and cached.
150
+ * Packages in IndexedDB but absent from the server are evicted.
168
151
  */
169
152
  export async function loadInstalledPackages() {
170
- let packages;
153
+ let serverPackages = [];
171
154
  try {
172
- packages = await listInstalled();
155
+ serverPackages = await fetchServerPackages();
173
156
  }
174
157
  catch (err) {
175
- console.warn('[sh3] Failed to read installed packages from storage:', err instanceof Error ? err.message : err);
158
+ console.warn('[sh3] Could not reach server for package sync, loading from local cache:', err instanceof Error ? err.message : err);
159
+ const fallback = await listInstalled().catch(() => []);
160
+ for (const pkg of fallback) {
161
+ await _loadFromIndexedDB(pkg);
162
+ }
176
163
  return;
177
164
  }
178
- for (const pkg of packages) {
179
- try {
180
- const bytes = await loadBundle(pkg.id);
181
- if (!bytes) {
182
- console.warn(`[sh3] No bundle found for installed package "${pkg.id}", skipping`);
183
- continue;
184
- }
185
- const loaded = await loadBundleModule(bytes);
186
- registerLoadedBundle(loaded, {
187
- version: pkg.version,
188
- sourceRegistry: pkg.sourceRegistry,
189
- contractVersion: pkg.contractVersion,
190
- });
191
- if (loaded.shards.length === 0 && loaded.apps.length === 0) {
192
- console.warn(`[sh3] Package "${pkg.id}" contains no valid shards or apps, skipping`);
193
- }
165
+ const serverIds = new Set(serverPackages.map(p => p.id));
166
+ let localPackages = [];
167
+ try {
168
+ localPackages = await listInstalled();
169
+ }
170
+ catch ( /* treat as empty */_a) { /* treat as empty */ }
171
+ const localIds = new Set(localPackages.map(p => p.id));
172
+ // Evict packages no longer on the server
173
+ for (const pkg of localPackages) {
174
+ if (!serverIds.has(pkg.id)) {
175
+ await removePackage(pkg.id).catch(() => { });
194
176
  }
195
- catch (err) {
196
- console.warn(`[sh3] Failed to load installed package "${pkg.id}":`, err instanceof Error ? err.message : err);
177
+ }
178
+ // Load packages from server use IndexedDB cache if available, else fetch from server
179
+ for (const serverPkg of serverPackages) {
180
+ if (localIds.has(serverPkg.id)) {
181
+ const localPkg = localPackages.find(p => p.id === serverPkg.id);
182
+ await _loadFromIndexedDB(localPkg);
183
+ }
184
+ else {
185
+ await _fetchAndCacheFromServer(serverPkg);
186
+ }
187
+ }
188
+ }
189
+ async function _loadFromIndexedDB(pkg) {
190
+ try {
191
+ const bytes = await loadBundle(pkg.id);
192
+ if (!bytes) {
193
+ console.warn(`[sh3] No bundle in IndexedDB for "${pkg.id}", skipping`);
194
+ return;
195
+ }
196
+ const loaded = await loadBundleModule(bytes);
197
+ registerLoadedBundle(loaded, { version: pkg.version, sourceRegistry: pkg.sourceRegistry, contractVersion: pkg.contractVersion });
198
+ }
199
+ catch (err) {
200
+ console.warn(`[sh3] Failed to load "${pkg.id}" from cache:`, err instanceof Error ? err.message : err);
201
+ }
202
+ }
203
+ async function _fetchAndCacheFromServer(serverPkg) {
204
+ var _a, _b;
205
+ try {
206
+ const res = await fetch(serverPkg.bundleUrl);
207
+ if (!res.ok) {
208
+ console.warn(`[sh3] Failed to fetch bundle for "${serverPkg.id}": HTTP ${res.status}`);
209
+ return;
197
210
  }
211
+ const bundle = await res.arrayBuffer();
212
+ const record = {
213
+ id: serverPkg.id,
214
+ type: serverPkg.type,
215
+ version: serverPkg.version,
216
+ sourceRegistry: (_a = serverPkg.sourceRegistry) !== null && _a !== void 0 ? _a : '',
217
+ contractVersion: (_b = serverPkg.contractVersion) !== null && _b !== void 0 ? _b : '',
218
+ installedAt: new Date().toISOString(),
219
+ permissions: [],
220
+ };
221
+ await savePackage(serverPkg.id, bundle, record);
222
+ const loaded = await loadBundleModule(bundle);
223
+ registerLoadedBundle(loaded, { version: record.version, sourceRegistry: record.sourceRegistry, contractVersion: record.contractVersion });
224
+ }
225
+ catch (err) {
226
+ console.warn(`[sh3] Failed to fetch/cache "${serverPkg.id}" from server:`, err instanceof Error ? err.message : err);
198
227
  }
199
228
  }