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.
- package/dist/api.d.ts +2 -2
- package/dist/api.js +1 -1
- package/dist/app/store/StoreView.svelte +28 -35
- package/dist/app/store/storeShard.svelte.js +35 -49
- package/dist/app/store/verbs.js +24 -55
- package/dist/build.d.ts +5 -0
- package/dist/build.js +64 -8
- package/dist/build.test.js +30 -1
- package/dist/env/client.d.ts +6 -10
- package/dist/env/client.js +11 -21
- package/dist/env/index.d.ts +2 -1
- package/dist/env/index.js +1 -1
- package/dist/registry/archive.d.ts +12 -0
- package/dist/registry/archive.js +80 -0
- package/dist/registry/archive.test.d.ts +1 -0
- package/dist/registry/archive.test.js +84 -0
- package/dist/registry/client.d.ts +9 -29
- package/dist/registry/client.js +14 -60
- package/dist/registry/client.test.js +31 -21
- package/dist/registry/index.d.ts +2 -2
- package/dist/registry/index.js +1 -1
- package/dist/registry/installer.d.ts +4 -4
- package/dist/registry/installer.js +74 -45
- package/dist/registry/schema.js +4 -27
- package/dist/registry/schema.test.d.ts +1 -0
- package/dist/registry/schema.test.js +41 -0
- package/dist/registry/types.d.ts +16 -41
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -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
|
|
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
|
|
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
|
|
47
|
+
* Download a .sh3pkg archive and verify its SRI integrity hash.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/registry/client.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Registry client -- fetches and merges registry indices, resolves
|
|
3
|
-
* packages, and downloads
|
|
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
|
|
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
|
|
68
|
+
* Download a .sh3pkg archive and verify its SRI integrity hash.
|
|
69
69
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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
|
|
113
|
-
|
|
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(`
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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.
|
|
27
|
-
expect(meta.
|
|
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('
|
|
31
|
+
it('propagates requires when present', () => {
|
|
30
32
|
const v = {
|
|
31
33
|
version: '1.0.0',
|
|
32
34
|
contractVersion: '0.1.0',
|
|
33
|
-
|
|
35
|
+
archiveUrl: 'https://example.com/pkg-1.0.0.sh3pkg',
|
|
34
36
|
integrity: 'sha384-xxx',
|
|
35
|
-
|
|
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.
|
|
40
|
-
expect(meta.serverIntegrity).toBe('sha384-yyy');
|
|
40
|
+
expect(meta.requires).toEqual([{ id: 'dep', versionRange: '^1.0.0' }]);
|
|
41
41
|
});
|
|
42
|
-
|
|
43
|
-
|
|
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: '
|
|
46
|
-
|
|
47
|
-
integrity
|
|
48
|
-
serverBundleUrl: '/s.js',
|
|
51
|
+
contractVersion: '1',
|
|
52
|
+
archiveUrl: 'https://example.com/pkg-1.0.0.sh3pkg',
|
|
53
|
+
integrity,
|
|
49
54
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
});
|
package/dist/registry/index.d.ts
CHANGED
|
@@ -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,
|
|
12
|
+
export { fetchRegistries, fetchArchive, buildPackageMeta } from './client';
|
|
13
13
|
export { installPackage, uninstallPackage, listInstalledPackages, loadInstalledPackages, } from './installer';
|
package/dist/registry/index.js
CHANGED
|
@@ -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,
|
|
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
|
-
*
|
|
53
|
+
* Sync installed packages against the server list and load all into the framework.
|
|
54
54
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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.
|
|
39
|
-
|
|
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
|
-
*
|
|
146
|
+
* Sync installed packages against the server list and load all into the framework.
|
|
164
147
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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
|
|
153
|
+
let serverPackages = [];
|
|
171
154
|
try {
|
|
172
|
-
|
|
155
|
+
serverPackages = await fetchServerPackages();
|
|
173
156
|
}
|
|
174
157
|
catch (err) {
|
|
175
|
-
console.warn('[sh3]
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
}
|