@mearl/preset 3.0.0 → 3.0.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/README.en.md +2 -0
- package/README.md +2 -0
- package/dist/browserPreset.js +30 -11
- package/dist/bundle.d.ts +2 -0
- package/dist/bundle.js +105 -28
- package/dist/bundleStore.d.ts +1 -0
- package/dist/bundleStore.js +138 -30
- package/dist/management.js +24 -8
- package/dist/manifest.js +35 -9
- package/dist/prepare.js +59 -20
- package/dist/preset.js +19 -8
- package/dist/resolve.js +26 -9
- package/package.json +5 -1
package/README.en.md
CHANGED
|
@@ -12,6 +12,8 @@ Browser and Node share one inheritance resolution and resource download; see [co
|
|
|
12
12
|
|
|
13
13
|
The main entry shares preset validation and inheritance resolution across browsers and Node, without filesystem, network, credential, or browser permission side effects. Callers supply manifest loading. Optional `@mearl/preset/bundles` provides resource validation, bounded downloads, and explicitly invoked browser-local storage; see [browser bundles](../../docs/browser-bundles.en.md).
|
|
14
14
|
|
|
15
|
+
`@mearl/preset/pageRuntime` provides the page runtime host and source generation functions for standalone page injection scripts.
|
|
16
|
+
|
|
15
17
|
```ts
|
|
16
18
|
import { resolvePreset, mergePresetDefaults } from '@mearl/preset';
|
|
17
19
|
|
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
主入口在浏览器和 Node 中共享 preset 清单校验与继承解析,没有文件系统、网络、凭据或浏览器权限副作用;清单加载由调用方提供。可选子入口 `@mearl/preset/bundles` 提供资源校验、受限下载和显式调用的浏览器本地存储,见 [能力包说明](../../docs/browser-bundles.md)。
|
|
14
14
|
|
|
15
|
+
`@mearl/preset/pageRuntime` 为独立的页面注入脚本提供页面运行时宿主及注入源码生成函数。
|
|
16
|
+
|
|
15
17
|
```ts
|
|
16
18
|
import { resolvePreset, mergePresetDefaults } from '@mearl/preset';
|
|
17
19
|
|
package/dist/browserPreset.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { selectBrowserControl } from './browserControls.js';
|
|
2
2
|
import { BundleError, MAX_BROWSER_STORAGE_BYTES, verifyBundle } from './bundle.js';
|
|
3
|
-
import
|
|
4
|
-
import { preparePresetPackages, presetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets } from './prepare.js';
|
|
3
|
+
import satisfies from 'semver/functions/satisfies.js';
|
|
4
|
+
import { preparePresetPackages, presetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets, } from './prepare.js';
|
|
5
5
|
/** Core browser defaults also need synchronization when no browser package is declared. */
|
|
6
6
|
export function presetRequiresBrowser(preset) {
|
|
7
|
-
return Object.values(preset.packages).some(entry => entry.enabled !== false && entry.targets?.includes('browser')) ||
|
|
7
|
+
return (Object.values(preset.packages).some(entry => entry.enabled !== false && entry.targets?.includes('browser')) ||
|
|
8
|
+
Object.keys(preset.appProfiles).length > 0 ||
|
|
9
|
+
Object.keys(preset.defaults).some(key => key === 'simulation' || key === 'extension' || key === 'services'));
|
|
8
10
|
}
|
|
9
11
|
/** Browser selection can be derived from the same accepted batch without another download. */
|
|
10
12
|
export async function browserPresetSnapshot(source, manifests, preset, bundles) {
|
|
11
13
|
const ids = new Set(presetPackages(preset, ['browser']).map(([id]) => id));
|
|
12
14
|
const selected = bundles.filter(bundle => ids.has(bundle.manifest.id));
|
|
13
15
|
selectBrowserControl(selected);
|
|
14
|
-
const packageTargets = Object.fromEntries(Object.entries(preset.packages)
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const packageTargets = Object.fromEntries(Object.entries(preset.packages)
|
|
17
|
+
.filter(([, entry]) => entry.enabled !== false)
|
|
18
|
+
.map(([id, entry]) => [id, entry.targets]));
|
|
19
|
+
const data = {
|
|
20
|
+
source,
|
|
21
|
+
manifests,
|
|
22
|
+
bundleRevisions: Object.fromEntries(selected.map(bundle => [bundle.manifest.id, bundle.revision])),
|
|
23
|
+
packageTargets,
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
snapshot: { ...data, revision: await presetSnapshotRevision(data) },
|
|
27
|
+
preset,
|
|
28
|
+
bundles: selected,
|
|
29
|
+
};
|
|
17
30
|
}
|
|
18
31
|
export async function prepareBrowserPreset(source, options) {
|
|
19
32
|
const prepared = await preparePresetPackages(source, options, ['browser']);
|
|
@@ -29,7 +42,9 @@ export async function verifyPreparedBrowserPreset(value, mearlApiVersion) {
|
|
|
29
42
|
throw new BundleError('BUNDLE_QUOTA', 'Browser preset package count exceeds 64');
|
|
30
43
|
let total = 0;
|
|
31
44
|
for (const bundle of value.bundles) {
|
|
32
|
-
if (!bundle?.resources ||
|
|
45
|
+
if (!bundle?.resources ||
|
|
46
|
+
typeof bundle.resources !== 'object' ||
|
|
47
|
+
Array.isArray(bundle.resources))
|
|
33
48
|
throw new BundleError('BUNDLE_INVALID', 'Expected package resource buffers');
|
|
34
49
|
const resources = Object.values(bundle.resources);
|
|
35
50
|
if (resources.length > 64)
|
|
@@ -46,10 +61,14 @@ export async function verifyPreparedBrowserPreset(value, mearlApiVersion) {
|
|
|
46
61
|
verifyBrowserPreset(value.snapshot, mearlApiVersion),
|
|
47
62
|
Promise.all(value.bundles.map(bundle => verifyBundle(bundle, mearlApiVersion))),
|
|
48
63
|
]);
|
|
49
|
-
if (bundles.length !== Object.keys(graph.snapshot.bundleRevisions).length ||
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
if (bundles.length !== Object.keys(graph.snapshot.bundleRevisions).length ||
|
|
65
|
+
new Set(bundles.map(bundle => bundle.manifest.id)).size !== bundles.length ||
|
|
66
|
+
bundles.some(bundle => {
|
|
67
|
+
const entry = graph.preset.packages[bundle.manifest.id];
|
|
68
|
+
return (graph.snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision ||
|
|
69
|
+
entry?.source !== bundle.source ||
|
|
70
|
+
(entry.version && !satisfies(bundle.manifest.version, entry.version)));
|
|
71
|
+
}))
|
|
53
72
|
throw new BundleError('BUNDLE_INTEGRITY', 'Browser activation requires every exact package');
|
|
54
73
|
selectBrowserControl(bundles);
|
|
55
74
|
verifyPackageTargets(graph.preset, bundles);
|
package/dist/bundle.d.ts
CHANGED
|
@@ -58,5 +58,7 @@ export declare function bundleDigest(bytes: Uint8Array): Promise<string>;
|
|
|
58
58
|
export declare function readBundleJson(bundle: PreparedBundle, name: string): unknown;
|
|
59
59
|
export declare function readBundleText(bundle: PreparedBundle, name: string): string;
|
|
60
60
|
export declare function prepareBundle(source: string, options: PrepareBundleOptions): Promise<PreparedBundle>;
|
|
61
|
+
/** Loads the resources declared by a validated manifest at its canonical source. */
|
|
62
|
+
export declare function loadBundleResources(source: string, manifest: BundleManifest, options: Pick<PrepareBundleOptions, 'load' | 'signal'>): Promise<PreparedBundle>;
|
|
61
63
|
/** Revalidates persisted snapshots without network access and returns independent resource buffers. */
|
|
62
64
|
export declare function verifyBundle(bundle: PreparedBundle, mearlApiVersion: string): Promise<PreparedBundle>;
|
package/dist/bundle.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import satisfies from 'semver/functions/satisfies.js';
|
|
2
|
+
import valid from 'semver/functions/valid.js';
|
|
3
|
+
import validRange from 'semver/ranges/valid.js';
|
|
2
4
|
import { clonePresetDefaults } from './manifest.js';
|
|
3
5
|
import { validateBrowserControl } from './browserControls.js';
|
|
4
6
|
export const MAX_BUNDLE_BYTES = 16 * 1024 * 1024;
|
|
@@ -18,7 +20,9 @@ function invalid(message) {
|
|
|
18
20
|
throw new BundleError('BUNDLE_INVALID', message);
|
|
19
21
|
}
|
|
20
22
|
function fields(value, names) {
|
|
21
|
-
if (!value ||
|
|
23
|
+
if (!value ||
|
|
24
|
+
typeof value !== 'object' ||
|
|
25
|
+
Array.isArray(value) ||
|
|
22
26
|
Object.keys(value).some(key => !names.includes(key)))
|
|
23
27
|
invalid('Unexpected manifest structure');
|
|
24
28
|
}
|
|
@@ -48,10 +52,20 @@ export function validateBundleManifest(value, mearlApiVersion) {
|
|
|
48
52
|
catch {
|
|
49
53
|
invalid('Expected a JSON manifest');
|
|
50
54
|
}
|
|
51
|
-
fields(json, [
|
|
55
|
+
fields(json, [
|
|
56
|
+
'schemaVersion',
|
|
57
|
+
'id',
|
|
58
|
+
'version',
|
|
59
|
+
'requires',
|
|
60
|
+
'defaults',
|
|
61
|
+
'resources',
|
|
62
|
+
'contributions',
|
|
63
|
+
]);
|
|
52
64
|
if (json.defaults !== undefined)
|
|
53
65
|
json.defaults = clonePresetDefaults(json.defaults);
|
|
54
|
-
if (json.schemaVersion !== 1 ||
|
|
66
|
+
if (json.schemaVersion !== 1 ||
|
|
67
|
+
typeof json.id !== 'string' ||
|
|
68
|
+
!/^[a-z][a-z0-9-]{0,79}$/.test(json.id))
|
|
55
69
|
invalid('Invalid bundle identity');
|
|
56
70
|
if (typeof json.version !== 'string' || valid(json.version) !== json.version)
|
|
57
71
|
invalid('Expected an exact bundle version');
|
|
@@ -74,45 +88,77 @@ export function validateBundleManifest(value, mearlApiVersion) {
|
|
|
74
88
|
for (const [name, resource] of resources) {
|
|
75
89
|
path(name);
|
|
76
90
|
fields(resource, ['sha256', 'byteLength']);
|
|
77
|
-
if (typeof resource.sha256 !== 'string' ||
|
|
78
|
-
|
|
91
|
+
if (typeof resource.sha256 !== 'string' ||
|
|
92
|
+
!/^[a-f0-9]{64}$/.test(resource.sha256) ||
|
|
93
|
+
typeof resource.byteLength !== 'number' ||
|
|
94
|
+
!Number.isSafeInteger(resource.byteLength) ||
|
|
95
|
+
resource.byteLength < 0)
|
|
79
96
|
invalid('Invalid resource metadata');
|
|
80
97
|
total += resource.byteLength;
|
|
81
98
|
if (total > MAX_BUNDLE_BYTES)
|
|
82
99
|
throw new BundleError('BUNDLE_QUOTA', 'Bundle resources exceed 16 MiB');
|
|
83
100
|
}
|
|
84
101
|
if (json.contributions !== undefined) {
|
|
85
|
-
fields(json.contributions, [
|
|
102
|
+
fields(json.contributions, [
|
|
103
|
+
'appProfiles',
|
|
104
|
+
'pageRuntimes',
|
|
105
|
+
'nodeRuntimes',
|
|
106
|
+
'browserRuntimes',
|
|
107
|
+
'extension',
|
|
108
|
+
'requestUi',
|
|
109
|
+
'browserViews',
|
|
110
|
+
'browserControls',
|
|
111
|
+
]);
|
|
86
112
|
const extension = json.contributions.extension;
|
|
87
|
-
if (extension !== undefined &&
|
|
113
|
+
if (extension !== undefined &&
|
|
114
|
+
(typeof extension !== 'string' || !Object.hasOwn(json.resources, extension)))
|
|
88
115
|
invalid('Extension contributions must reference a declared resource');
|
|
89
|
-
if (typeof extension === 'string' &&
|
|
116
|
+
if (typeof extension === 'string' &&
|
|
117
|
+
json.resources[extension].byteLength > MAX_REQUEST_UI_BYTES)
|
|
90
118
|
throw new BundleError('BUNDLE_QUOTA', 'Extension contributions exceed 64 KiB');
|
|
91
119
|
const controls = json.contributions.browserControls;
|
|
92
|
-
if (controls !== undefined &&
|
|
120
|
+
if (controls !== undefined &&
|
|
121
|
+
(typeof controls !== 'string' || !Object.hasOwn(json.resources, controls)))
|
|
93
122
|
invalid('Browser controls must reference a declared resource');
|
|
94
|
-
if (typeof controls === 'string' &&
|
|
123
|
+
if (typeof controls === 'string' &&
|
|
124
|
+
json.resources[controls].byteLength > MAX_REQUEST_UI_BYTES)
|
|
95
125
|
throw new BundleError('BUNDLE_QUOTA', 'Browser controls exceed 64 KiB');
|
|
96
|
-
if (json.contributions.requestUi !== undefined &&
|
|
126
|
+
if (json.contributions.requestUi !== undefined &&
|
|
127
|
+
(typeof json.contributions.requestUi !== 'string' ||
|
|
128
|
+
!Object.hasOwn(json.resources, json.contributions.requestUi)))
|
|
97
129
|
invalid('Request UI must reference a declared resource');
|
|
98
|
-
if (typeof json.contributions.requestUi === 'string' &&
|
|
130
|
+
if (typeof json.contributions.requestUi === 'string' &&
|
|
131
|
+
json.resources[json.contributions.requestUi].byteLength >
|
|
132
|
+
MAX_REQUEST_UI_BYTES)
|
|
99
133
|
throw new BundleError('BUNDLE_QUOTA', 'Request UI exceeds 64 KiB');
|
|
100
134
|
if (json.contributions.browserViews !== undefined) {
|
|
101
|
-
fields(json.contributions.browserViews, [
|
|
135
|
+
fields(json.contributions.browserViews, [
|
|
136
|
+
'requestImport',
|
|
137
|
+
'requestCases',
|
|
138
|
+
'captureLauncher',
|
|
139
|
+
'captureController',
|
|
140
|
+
]);
|
|
102
141
|
for (const resource of Object.values(json.contributions.browserViews)) {
|
|
103
142
|
if (typeof resource !== 'string' || !Object.hasOwn(json.resources, resource))
|
|
104
143
|
invalid('Browser views must reference declared resources');
|
|
105
|
-
if (json.resources[resource].byteLength >
|
|
144
|
+
if (json.resources[resource].byteLength >
|
|
145
|
+
MAX_REQUEST_UI_BYTES)
|
|
106
146
|
throw new BundleError('BUNDLE_QUOTA', 'Browser view definition exceeds 64 KiB');
|
|
107
147
|
}
|
|
108
148
|
}
|
|
109
149
|
const profiles = json.contributions.appProfiles === undefined ? [] : json.contributions.appProfiles;
|
|
110
|
-
if (!Array.isArray(profiles) ||
|
|
150
|
+
if (!Array.isArray(profiles) ||
|
|
151
|
+
profiles.some(name => typeof name !== 'string' || !Object.hasOwn(json.resources, name)) ||
|
|
111
152
|
new Set(profiles).size !== profiles.length)
|
|
112
153
|
invalid('App profiles must reference distinct declared resources');
|
|
113
154
|
const runtimes = json.contributions.pageRuntimes;
|
|
114
155
|
if (runtimes !== undefined) {
|
|
115
|
-
if (!runtimes ||
|
|
156
|
+
if (!runtimes ||
|
|
157
|
+
typeof runtimes !== 'object' ||
|
|
158
|
+
Array.isArray(runtimes) ||
|
|
159
|
+
Object.entries(runtimes).some(([id, name]) => !/^[a-z][a-z0-9-]{0,79}$/.test(id) ||
|
|
160
|
+
typeof name !== 'string' ||
|
|
161
|
+
!Object.hasOwn(json.resources, name)))
|
|
116
162
|
invalid('Page runtimes must reference declared resources');
|
|
117
163
|
for (const name of Object.values(runtimes)) {
|
|
118
164
|
const resource = json.resources[name];
|
|
@@ -125,15 +171,26 @@ export function validateBundleManifest(value, mearlApiVersion) {
|
|
|
125
171
|
const label = kind === 'nodeRuntimes' ? 'Node' : 'Browser';
|
|
126
172
|
if (runtimes === undefined)
|
|
127
173
|
continue;
|
|
128
|
-
if (!runtimes ||
|
|
174
|
+
if (!runtimes ||
|
|
175
|
+
typeof runtimes !== 'object' ||
|
|
176
|
+
Array.isArray(runtimes) ||
|
|
177
|
+
Object.keys(runtimes).length > 16)
|
|
129
178
|
invalid(`Invalid ${label} runtime declarations`);
|
|
130
179
|
for (const [host, declaration] of Object.entries(runtimes)) {
|
|
131
180
|
if (!/^[a-z][a-z0-9-]{0,79}$/.test(host))
|
|
132
181
|
invalid(`Invalid ${label} runtime host`);
|
|
133
182
|
fields(declaration, ['entry', 'bindings']);
|
|
134
|
-
if (typeof declaration.entry !== 'string' ||
|
|
183
|
+
if (typeof declaration.entry !== 'string' ||
|
|
184
|
+
!Object.hasOwn(json.resources, declaration.entry) ||
|
|
185
|
+
!declaration.entry.endsWith('.cjs'))
|
|
135
186
|
invalid(`${label} runtime entry must reference a declared CommonJS resource`);
|
|
136
|
-
if (!declaration.bindings ||
|
|
187
|
+
if (!declaration.bindings ||
|
|
188
|
+
typeof declaration.bindings !== 'object' ||
|
|
189
|
+
Array.isArray(declaration.bindings) ||
|
|
190
|
+
Object.keys(declaration.bindings).length > 256 ||
|
|
191
|
+
Object.entries(declaration.bindings).some(([hook, handler]) => !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(hook) ||
|
|
192
|
+
typeof handler !== 'string' ||
|
|
193
|
+
!/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(handler)))
|
|
137
194
|
invalid(`Invalid ${label} runtime bindings`);
|
|
138
195
|
}
|
|
139
196
|
}
|
|
@@ -143,7 +200,12 @@ export function validateBundleManifest(value, mearlApiVersion) {
|
|
|
143
200
|
return json;
|
|
144
201
|
}
|
|
145
202
|
export async function bundleDigest(bytes) {
|
|
146
|
-
const
|
|
203
|
+
const buffer = bytes.buffer;
|
|
204
|
+
// WebCrypto snapshots fixed, non-shared input views before hashing asynchronously.
|
|
205
|
+
const data = buffer instanceof ArrayBuffer && !('resizable' in buffer && buffer.resizable)
|
|
206
|
+
? new Uint8Array(buffer, bytes.byteOffset, bytes.byteLength)
|
|
207
|
+
: Uint8Array.from(bytes);
|
|
208
|
+
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
147
209
|
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
148
210
|
}
|
|
149
211
|
async function manifestRevision(source, manifest) {
|
|
@@ -172,7 +234,8 @@ export function readBundleText(bundle, name) {
|
|
|
172
234
|
export async function prepareBundle(source, options) {
|
|
173
235
|
options.signal?.throwIfAborted();
|
|
174
236
|
source = bundleSourceUrl(source);
|
|
175
|
-
if (options.version !== undefined &&
|
|
237
|
+
if (options.version !== undefined &&
|
|
238
|
+
(!options.version.trim() || validRange(options.version) === null))
|
|
176
239
|
invalid('Invalid requested bundle version');
|
|
177
240
|
const bytes = Uint8Array.from(await options.load(source, { maxBytes: MAX_BUNDLE_MANIFEST_BYTES, signal: options.signal }));
|
|
178
241
|
options.signal?.throwIfAborted();
|
|
@@ -190,18 +253,31 @@ export async function prepareBundle(source, options) {
|
|
|
190
253
|
invalid('Bundle identity does not match the requested package');
|
|
191
254
|
if (options.version && !satisfies(manifest.version, options.version))
|
|
192
255
|
throw new BundleError('BUNDLE_INCOMPATIBLE', 'Bundle version does not satisfy the requested range');
|
|
256
|
+
return loadBundleResources(source, manifest, options);
|
|
257
|
+
}
|
|
258
|
+
/** Loads the resources declared by a validated manifest at its canonical source. */
|
|
259
|
+
export async function loadBundleResources(source, manifest, options) {
|
|
193
260
|
const resources = {};
|
|
194
261
|
for (const [name, expected] of Object.entries(manifest.resources)) {
|
|
195
262
|
options.signal?.throwIfAborted();
|
|
196
|
-
const resource = Uint8Array.from(await options.load(new URL(name, source).href, {
|
|
263
|
+
const resource = Uint8Array.from(await options.load(new URL(name, source).href, {
|
|
264
|
+
maxBytes: expected.byteLength,
|
|
265
|
+
signal: options.signal,
|
|
266
|
+
}));
|
|
197
267
|
options.signal?.throwIfAborted();
|
|
198
|
-
if (resource.byteLength !== expected.byteLength ||
|
|
268
|
+
if (resource.byteLength !== expected.byteLength ||
|
|
269
|
+
(await bundleDigest(resource)) !== expected.sha256) {
|
|
199
270
|
throw new BundleError('BUNDLE_INTEGRITY', `Resource integrity check failed: ${name}`);
|
|
200
271
|
}
|
|
201
272
|
resources[name] = resource;
|
|
202
273
|
}
|
|
203
274
|
options.signal?.throwIfAborted();
|
|
204
|
-
const prepared = {
|
|
275
|
+
const prepared = {
|
|
276
|
+
source,
|
|
277
|
+
manifest,
|
|
278
|
+
revision: await manifestRevision(source, manifest),
|
|
279
|
+
resources,
|
|
280
|
+
};
|
|
205
281
|
if (manifest.contributions?.browserControls)
|
|
206
282
|
validateBrowserControl(readBundleJson(prepared, manifest.contributions.browserControls));
|
|
207
283
|
return prepared;
|
|
@@ -211,7 +287,8 @@ export async function verifyBundle(bundle, mearlApiVersion) {
|
|
|
211
287
|
const source = bundleSourceUrl(bundle.source);
|
|
212
288
|
const manifest = validateBundleManifest(bundle.manifest, mearlApiVersion);
|
|
213
289
|
const revision = bundle.revision;
|
|
214
|
-
if (!bundle.resources ||
|
|
290
|
+
if (!bundle.resources ||
|
|
291
|
+
typeof bundle.resources !== 'object' ||
|
|
215
292
|
Object.keys(bundle.resources).length !== Object.keys(manifest.resources).length)
|
|
216
293
|
invalid('Stored resource set does not match the manifest');
|
|
217
294
|
const resources = {};
|
|
@@ -225,10 +302,10 @@ export async function verifyBundle(bundle, mearlApiVersion) {
|
|
|
225
302
|
}
|
|
226
303
|
resources[name] = bytes;
|
|
227
304
|
}
|
|
228
|
-
if (await manifestRevision(source, manifest) !== revision)
|
|
305
|
+
if ((await manifestRevision(source, manifest)) !== revision)
|
|
229
306
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored manifest revision does not match');
|
|
230
307
|
for (const [name, expected] of Object.entries(manifest.resources)) {
|
|
231
|
-
if (await bundleDigest(resources[name]) !== expected.sha256)
|
|
308
|
+
if ((await bundleDigest(resources[name])) !== expected.sha256)
|
|
232
309
|
throw new BundleError('BUNDLE_INTEGRITY', `Stored resource integrity check failed: ${name}`);
|
|
233
310
|
}
|
|
234
311
|
const prepared = { source, manifest, revision, resources };
|
package/dist/bundleStore.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export declare class BrowserBundleStore {
|
|
|
63
63
|
installation?: BrowserPresetInstallation;
|
|
64
64
|
expectedInstallationToken: string | null;
|
|
65
65
|
}, expectedBundleRevisions?: Record<string, string>): Promise<void>;
|
|
66
|
+
uninstallPreset(expectedRevision: string, expectedInstallationToken: string | null): Promise<void>;
|
|
66
67
|
/** Verifies all resources before a compare-and-swap transaction replaces the active snapshot. */
|
|
67
68
|
install(bundle: PreparedBundle, expectedRevision: string | null, expectedBundleRevisions?: Record<string, string>): Promise<void>;
|
|
68
69
|
remove(id: string, expectedRevision: string): Promise<void>;
|
package/dist/bundleStore.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { BundleError, MAX_BUNDLE_BYTES, MAX_BROWSER_STORAGE_BYTES, MAX_PAGE_RUNTIME_BYTES, bundleDigest, verifyBundle } from './bundle.js';
|
|
2
|
-
import { verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
|
|
1
|
+
import { BundleError, MAX_BUNDLE_BYTES, MAX_BROWSER_STORAGE_BYTES, MAX_PAGE_RUNTIME_BYTES, bundleDigest, verifyBundle, } from './bundle.js';
|
|
2
|
+
import { verifyBrowserPreset, verifyPreparedBrowserPreset, } from './browserPreset.js';
|
|
3
3
|
import { mergePresetDefaults } from './resolve.js';
|
|
4
4
|
function installationMetadata(value) {
|
|
5
|
-
if (!value ||
|
|
5
|
+
if (!value ||
|
|
6
|
+
Object.keys(value).some(key => !['revision', 'browserRevision', 'overrides'].includes(key)) ||
|
|
7
|
+
!/^[a-f0-9]{64}$/.test(value.revision) ||
|
|
8
|
+
!/^[a-f0-9]{64}$/.test(value.browserRevision))
|
|
6
9
|
throw new BundleError('BUNDLE_INVALID', 'Invalid browser installation identity');
|
|
7
10
|
const overrides = mergePresetDefaults({}, value.overrides);
|
|
8
11
|
if (new TextEncoder().encode(JSON.stringify(overrides)).byteLength > 262144)
|
|
@@ -17,18 +20,25 @@ function captureRevisions(input) {
|
|
|
17
20
|
if (input === undefined)
|
|
18
21
|
return;
|
|
19
22
|
const entries = Object.entries(input);
|
|
20
|
-
if (entries.length > 64 ||
|
|
23
|
+
if (entries.length > 64 ||
|
|
24
|
+
entries.some(([id, revision]) => !/^[a-z][a-z0-9-]{0,79}$/.test(id) || typeof revision !== 'string'))
|
|
21
25
|
throw new BundleError('BUNDLE_INVALID', 'Invalid expected browser bundle selection');
|
|
22
26
|
return Object.fromEntries(entries);
|
|
23
27
|
}
|
|
24
28
|
function checkRevisions(records, expected) {
|
|
25
|
-
if (expected &&
|
|
29
|
+
if (expected &&
|
|
30
|
+
(records.length !== Object.keys(expected).length ||
|
|
31
|
+
records.some(record => !Object.hasOwn(expected, record.id) || expected[record.id] !== record.revision)))
|
|
26
32
|
throw new BundleError('BUNDLE_CONFLICT', 'Browser bundles changed while contributions were being validated');
|
|
27
33
|
}
|
|
28
34
|
function storedBundle(bundle) {
|
|
29
35
|
const { resources, ...metadata } = bundle;
|
|
30
|
-
return {
|
|
31
|
-
|
|
36
|
+
return {
|
|
37
|
+
id: bundle.manifest.id,
|
|
38
|
+
revision: bundle.revision,
|
|
39
|
+
byteLength: Object.values(resources).reduce((sum, bytes) => sum + bytes.byteLength, 0),
|
|
40
|
+
bundle: metadata,
|
|
41
|
+
};
|
|
32
42
|
}
|
|
33
43
|
function bundleObjectHashes(records) {
|
|
34
44
|
const hashes = new Set();
|
|
@@ -61,13 +71,17 @@ function readObjects(records, store) {
|
|
|
61
71
|
const hashes = bundleObjectHashes(records);
|
|
62
72
|
for (const hash of hashes) {
|
|
63
73
|
const request = store.get(hash);
|
|
64
|
-
request.onsuccess = () => {
|
|
65
|
-
|
|
74
|
+
request.onsuccess = () => {
|
|
75
|
+
if (request.result)
|
|
76
|
+
result.set(hash, request.result);
|
|
77
|
+
};
|
|
66
78
|
}
|
|
67
79
|
return result;
|
|
68
80
|
}
|
|
69
81
|
function checkStorageBudget(records, runtimes) {
|
|
70
|
-
if (records.some(record => !Number.isSafeInteger(record.byteLength) ||
|
|
82
|
+
if (records.some(record => !Number.isSafeInteger(record.byteLength) ||
|
|
83
|
+
record.byteLength < 0 ||
|
|
84
|
+
record.byteLength > MAX_BUNDLE_BYTES) ||
|
|
71
85
|
runtimes.some(runtime => !(runtime.bytes instanceof Uint8Array) || runtime.bytes.byteLength > MAX_BUNDLE_BYTES)) {
|
|
72
86
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource size is invalid');
|
|
73
87
|
}
|
|
@@ -100,10 +114,16 @@ export class BrowserBundleStore {
|
|
|
100
114
|
request.onerror = () => reject(request.error);
|
|
101
115
|
request.onsuccess = () => {
|
|
102
116
|
const database = request.result;
|
|
103
|
-
database.onversionchange = () => {
|
|
117
|
+
database.onversionchange = () => {
|
|
118
|
+
database.close();
|
|
119
|
+
this.database = undefined;
|
|
120
|
+
};
|
|
104
121
|
resolve(database);
|
|
105
122
|
};
|
|
106
|
-
}).catch(error => {
|
|
123
|
+
}).catch(error => {
|
|
124
|
+
this.database = undefined;
|
|
125
|
+
throw error;
|
|
126
|
+
});
|
|
107
127
|
return this.database;
|
|
108
128
|
}
|
|
109
129
|
async close() {
|
|
@@ -118,11 +138,19 @@ export class BrowserBundleStore {
|
|
|
118
138
|
const transaction = database.transaction([STORE, PRESET, OBJECTS], 'readonly');
|
|
119
139
|
const request = transaction.objectStore(STORE).getAll();
|
|
120
140
|
let objects = new Map();
|
|
121
|
-
request.onsuccess = () => {
|
|
141
|
+
request.onsuccess = () => {
|
|
142
|
+
objects = readObjects(request.result, transaction.objectStore(OBJECTS));
|
|
143
|
+
};
|
|
122
144
|
const active = transaction.objectStore(PRESET).get('active');
|
|
123
145
|
const installation = transaction.objectStore(PRESET).get('installation');
|
|
124
146
|
const token = transaction.objectStore(PRESET).get('installationToken');
|
|
125
|
-
transaction.oncomplete = () => resolve({
|
|
147
|
+
transaction.oncomplete = () => resolve({
|
|
148
|
+
objects,
|
|
149
|
+
records: request.result,
|
|
150
|
+
active: active.result,
|
|
151
|
+
installation: installation.result,
|
|
152
|
+
installationToken: token.result ?? null,
|
|
153
|
+
});
|
|
126
154
|
transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Bundle read failed'));
|
|
127
155
|
});
|
|
128
156
|
}
|
|
@@ -132,7 +160,9 @@ export class BrowserBundleStore {
|
|
|
132
160
|
const record = await new Promise((resolve, reject) => {
|
|
133
161
|
const transaction = database.transaction([STORE, OBJECTS], 'readonly');
|
|
134
162
|
const request = transaction.objectStore(STORE).get(id);
|
|
135
|
-
request.onsuccess = () => {
|
|
163
|
+
request.onsuccess = () => {
|
|
164
|
+
objects = readObjects(request.result ? [request.result] : [], transaction.objectStore(OBJECTS));
|
|
165
|
+
};
|
|
136
166
|
transaction.oncomplete = () => resolve(request.result);
|
|
137
167
|
transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Bundle read failed'));
|
|
138
168
|
});
|
|
@@ -150,7 +180,8 @@ export class BrowserBundleStore {
|
|
|
150
180
|
resources[name] = object.bytes;
|
|
151
181
|
}
|
|
152
182
|
const bundle = await verifyBundle({ ...record.bundle, resources }, this.options.mearlApiVersion);
|
|
153
|
-
if (Object.values(bundle.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0) !==
|
|
183
|
+
if (Object.values(bundle.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0) !==
|
|
184
|
+
record.byteLength) {
|
|
154
185
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource size does not match');
|
|
155
186
|
}
|
|
156
187
|
return bundle;
|
|
@@ -158,13 +189,16 @@ export class BrowserBundleStore {
|
|
|
158
189
|
async list() {
|
|
159
190
|
const bundles = [];
|
|
160
191
|
const errors = [];
|
|
161
|
-
const { objects, records, active, installation: storedInstallation, installationToken } = await this.records();
|
|
192
|
+
const { objects, records, active, installation: storedInstallation, installationToken, } = await this.records();
|
|
162
193
|
for (const record of records) {
|
|
163
194
|
try {
|
|
164
195
|
bundles.push(await this.verified(record, objects));
|
|
165
196
|
}
|
|
166
197
|
catch (error) {
|
|
167
|
-
errors.push({
|
|
198
|
+
errors.push({
|
|
199
|
+
id: record.id,
|
|
200
|
+
error: error instanceof Error ? error.message : 'Invalid stored bundle',
|
|
201
|
+
});
|
|
168
202
|
}
|
|
169
203
|
}
|
|
170
204
|
let activation;
|
|
@@ -181,8 +215,12 @@ export class BrowserBundleStore {
|
|
|
181
215
|
}
|
|
182
216
|
if (activation) {
|
|
183
217
|
for (const [id, revision] of Object.entries(activation.snapshot.bundleRevisions)) {
|
|
184
|
-
if (!bundles.some(bundle => bundle.manifest.id === id && bundle.revision === revision) &&
|
|
185
|
-
errors.
|
|
218
|
+
if (!bundles.some(bundle => bundle.manifest.id === id && bundle.revision === revision) &&
|
|
219
|
+
!errors.some(error => error.id === id)) {
|
|
220
|
+
errors.push({
|
|
221
|
+
id,
|
|
222
|
+
error: 'BUNDLE_INTEGRITY: An active preset package is missing or corrupt',
|
|
223
|
+
});
|
|
186
224
|
}
|
|
187
225
|
}
|
|
188
226
|
}
|
|
@@ -198,8 +236,17 @@ export class BrowserBundleStore {
|
|
|
198
236
|
installationError = error instanceof Error ? error.message : 'Invalid browser installation';
|
|
199
237
|
}
|
|
200
238
|
}
|
|
201
|
-
return {
|
|
202
|
-
|
|
239
|
+
return {
|
|
240
|
+
bundles,
|
|
241
|
+
errors,
|
|
242
|
+
activation,
|
|
243
|
+
activationError,
|
|
244
|
+
installation,
|
|
245
|
+
installationError,
|
|
246
|
+
installationToken,
|
|
247
|
+
activeRevision: active?.revision ?? null,
|
|
248
|
+
revisions: Object.fromEntries(records.map(record => [record.id, record.revision])),
|
|
249
|
+
};
|
|
203
250
|
}
|
|
204
251
|
async activePreset() {
|
|
205
252
|
const database = await this.open();
|
|
@@ -222,7 +269,10 @@ export class BrowserBundleStore {
|
|
|
222
269
|
transaction.oncomplete = () => resolve(request.result);
|
|
223
270
|
transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Browser runtime read failed'));
|
|
224
271
|
});
|
|
225
|
-
if (!(record?.bytes instanceof Uint8Array) ||
|
|
272
|
+
if (!(record?.bytes instanceof Uint8Array) ||
|
|
273
|
+
record.sha256 !== sha256 ||
|
|
274
|
+
record.bytes.byteLength > MAX_BUNDLE_BYTES ||
|
|
275
|
+
(await bundleDigest(record.bytes)) !== sha256) {
|
|
226
276
|
throw new BundleError('BUNDLE_INTEGRITY', 'Pinned browser runtime is missing or corrupt');
|
|
227
277
|
}
|
|
228
278
|
return Uint8Array.from(record.bytes);
|
|
@@ -248,7 +298,8 @@ export class BrowserBundleStore {
|
|
|
248
298
|
}
|
|
249
299
|
async updateRuntimePins(input, replace) {
|
|
250
300
|
const patch = Object.entries(input).map(([owner, sha256]) => {
|
|
251
|
-
if (!/^[a-zA-Z0-9:._-]{1,200}$/.test(owner) ||
|
|
301
|
+
if (!/^[a-zA-Z0-9:._-]{1,200}$/.test(owner) ||
|
|
302
|
+
(sha256 !== null && !/^[a-f0-9]{64}$/.test(sha256)))
|
|
252
303
|
throw new BundleError('BUNDLE_INVALID', 'Invalid page runtime reference');
|
|
253
304
|
return { owner, sha256 };
|
|
254
305
|
});
|
|
@@ -302,7 +353,9 @@ export class BrowserBundleStore {
|
|
|
302
353
|
const transaction = database.transaction([PRESET, STORE, OBJECTS], 'readonly');
|
|
303
354
|
const current = transaction.objectStore(PRESET).get('active');
|
|
304
355
|
const bundle = transaction.objectStore(STORE).get(id);
|
|
305
|
-
bundle.onsuccess = () => {
|
|
356
|
+
bundle.onsuccess = () => {
|
|
357
|
+
objects = readObjects(bundle.result ? [bundle.result] : [], transaction.objectStore(OBJECTS));
|
|
358
|
+
};
|
|
306
359
|
transaction.oncomplete = () => {
|
|
307
360
|
if (current.result?.revision !== activation.snapshot.revision) {
|
|
308
361
|
reject(new BundleError('BUNDLE_CONFLICT', 'The selected preset changed during profile lookup'));
|
|
@@ -320,10 +373,18 @@ export class BrowserBundleStore {
|
|
|
320
373
|
/** Packages and the exact preset graph become visible together, after host contribution checks. */
|
|
321
374
|
async activatePreset(input, replacements, expectedRevision, association, expectedBundleRevisions) {
|
|
322
375
|
const selection = captureRevisions(expectedBundleRevisions);
|
|
323
|
-
const accepted = association?.installation && {
|
|
324
|
-
|
|
376
|
+
const accepted = association?.installation && {
|
|
377
|
+
installation: installationMetadata(association.installation),
|
|
378
|
+
expected: association.expectedInstallationToken,
|
|
379
|
+
};
|
|
380
|
+
if ((association &&
|
|
381
|
+
association.expectedInstallationToken !== null &&
|
|
382
|
+
!/^[a-f0-9]{64}$/.test(association.expectedInstallationToken)) ||
|
|
383
|
+
(accepted && accepted.installation.browserRevision !== input.revision))
|
|
325
384
|
throw new BundleError('BUNDLE_INVALID', 'Invalid browser installation selection');
|
|
326
|
-
if (replacements.length > 64 ||
|
|
385
|
+
if (replacements.length > 64 ||
|
|
386
|
+
replacements.reduce((total, item) => total +
|
|
387
|
+
Object.values(item.bundle.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0), 0) > MAX_BROWSER_STORAGE_BYTES) {
|
|
327
388
|
throw new BundleError('BUNDLE_QUOTA', 'Preset packages exceed the browser storage budget');
|
|
328
389
|
}
|
|
329
390
|
const expected = replacements.map(item => item.expectedRevision);
|
|
@@ -351,14 +412,20 @@ export class BrowserBundleStore {
|
|
|
351
412
|
transaction.abort();
|
|
352
413
|
return;
|
|
353
414
|
}
|
|
354
|
-
if ((active.result?.revision ?? null) !== expectedRevision ||
|
|
415
|
+
if ((active.result?.revision ?? null) !== expectedRevision ||
|
|
416
|
+
(association &&
|
|
417
|
+
(installation.result ?? null) !== association.expectedInstallationToken) ||
|
|
418
|
+
next.some(item => (records.result.find(record => record.id === item.bundle.manifest.id)?.revision ??
|
|
419
|
+
null) !== item.expectedRevision)) {
|
|
355
420
|
failure = new BundleError('BUNDLE_CONFLICT', 'Preset or packages changed while activation was being prepared');
|
|
356
421
|
transaction.abort();
|
|
357
422
|
return;
|
|
358
423
|
}
|
|
359
424
|
const replacing = new Set(next.map(item => item.bundle.manifest.id));
|
|
360
425
|
const previousManaged = new Set(Object.keys(active.result?.bundleRevisions ?? {}));
|
|
361
|
-
if (accepted &&
|
|
426
|
+
if (accepted &&
|
|
427
|
+
next.some(item => !previousManaged.has(item.bundle.manifest.id) &&
|
|
428
|
+
records.result.some(record => record.id === item.bundle.manifest.id))) {
|
|
362
429
|
failure = new BundleError('BUNDLE_CONFLICT', 'A preset package conflicts with an independently installed browser package');
|
|
363
430
|
transaction.abort();
|
|
364
431
|
return;
|
|
@@ -405,6 +472,47 @@ export class BrowserBundleStore {
|
|
|
405
472
|
transaction.onabort = transaction.onerror = () => reject(failure ?? transaction.error ?? new Error('Preset activation failed'));
|
|
406
473
|
});
|
|
407
474
|
}
|
|
475
|
+
async uninstallPreset(expectedRevision, expectedInstallationToken) {
|
|
476
|
+
const selection = await this.activePreset();
|
|
477
|
+
if (selection?.snapshot.revision !== expectedRevision)
|
|
478
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The browser preset changed before removal');
|
|
479
|
+
const managed = selection.snapshot.bundleRevisions;
|
|
480
|
+
const database = await this.open();
|
|
481
|
+
await new Promise((resolve, reject) => {
|
|
482
|
+
const transaction = database.transaction([STORE, PRESET, OBJECTS, PINS], 'readwrite');
|
|
483
|
+
const store = transaction.objectStore(STORE);
|
|
484
|
+
const presets = transaction.objectStore(PRESET);
|
|
485
|
+
const objects = transaction.objectStore(OBJECTS);
|
|
486
|
+
const records = store.getAll();
|
|
487
|
+
const retained = objects.getAll();
|
|
488
|
+
const pins = transaction.objectStore(PINS).getAll();
|
|
489
|
+
const token = presets.get('installationToken');
|
|
490
|
+
const active = presets.get('active');
|
|
491
|
+
let failure;
|
|
492
|
+
active.onsuccess = () => {
|
|
493
|
+
if (active.result?.revision !== expectedRevision ||
|
|
494
|
+
(token.result ?? null) !== expectedInstallationToken) {
|
|
495
|
+
failure = new BundleError('BUNDLE_CONFLICT', 'The browser preset changed before removal');
|
|
496
|
+
transaction.abort();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const remaining = records.result.filter(record => {
|
|
500
|
+
if (!Object.hasOwn(managed, record.id) || managed[record.id] !== record.revision)
|
|
501
|
+
return true;
|
|
502
|
+
store.delete(record.id);
|
|
503
|
+
return false;
|
|
504
|
+
});
|
|
505
|
+
const keep = new Set(referencedObjects(remaining, retained.result, pins.result).map(object => object.sha256));
|
|
506
|
+
for (const object of retained.result)
|
|
507
|
+
if (!keep.has(object.sha256))
|
|
508
|
+
objects.delete(object.sha256);
|
|
509
|
+
for (const key of ['active', 'installation', 'installationToken'])
|
|
510
|
+
presets.delete(key);
|
|
511
|
+
};
|
|
512
|
+
transaction.oncomplete = () => resolve();
|
|
513
|
+
transaction.onabort = transaction.onerror = () => reject(failure ?? transaction.error ?? new Error('Browser preset removal failed'));
|
|
514
|
+
});
|
|
515
|
+
}
|
|
408
516
|
/** Verifies all resources before a compare-and-swap transaction replaces the active snapshot. */
|
|
409
517
|
async install(bundle, expectedRevision, expectedBundleRevisions) {
|
|
410
518
|
const selection = captureRevisions(expectedBundleRevisions);
|
package/dist/management.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
import
|
|
1
|
+
import satisfies from 'semver/functions/satisfies.js';
|
|
2
|
+
import valid from 'semver/functions/valid.js';
|
|
3
|
+
import subset from 'semver/ranges/subset.js';
|
|
4
|
+
import validRange from 'semver/ranges/valid.js';
|
|
2
5
|
import { isJsonObject, PresetError } from './manifest.js';
|
|
3
6
|
/** Declarative installation guidance; never accepts commands, arguments or executable hooks. */
|
|
4
7
|
export function parseSetupManagement(value) {
|
|
5
8
|
if (value === undefined)
|
|
6
9
|
return;
|
|
7
|
-
const invalid = () => {
|
|
8
|
-
|
|
10
|
+
const invalid = () => {
|
|
11
|
+
throw new PresetError('PRESET_INVALID', 'Invalid defaults.management.setup declaration');
|
|
12
|
+
};
|
|
13
|
+
if (!isJsonObject(value) ||
|
|
14
|
+
Object.keys(value).some(key => key !== 'setup') ||
|
|
15
|
+
!isJsonObject(value.setup))
|
|
9
16
|
return invalid();
|
|
10
17
|
const setup = value.setup;
|
|
11
18
|
if (Object.keys(setup).some(key => !['package', 'version', 'registry', 'runtime'].includes(key)) ||
|
|
12
|
-
typeof setup.package !== 'string' ||
|
|
13
|
-
|
|
19
|
+
typeof setup.package !== 'string' ||
|
|
20
|
+
setup.package.length > 214 ||
|
|
21
|
+
!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(setup.package) ||
|
|
22
|
+
typeof setup.version !== 'string' ||
|
|
23
|
+
setup.version.length > 64 ||
|
|
24
|
+
!(valid(setup.version) === setup.version || /^[a-z][a-z0-9.-]*$/.test(setup.version)))
|
|
14
25
|
return invalid();
|
|
15
26
|
if (setup.registry !== undefined) {
|
|
16
|
-
if (typeof setup.registry !== 'string' ||
|
|
27
|
+
if (typeof setup.registry !== 'string' ||
|
|
28
|
+
setup.registry.length > 2048 ||
|
|
29
|
+
!/^https:\/\/[a-zA-Z0-9./:_-]+$/.test(setup.registry))
|
|
17
30
|
return invalid();
|
|
18
31
|
try {
|
|
19
32
|
const url = new URL(setup.registry);
|
|
@@ -24,7 +37,8 @@ export function parseSetupManagement(value) {
|
|
|
24
37
|
return invalid();
|
|
25
38
|
}
|
|
26
39
|
}
|
|
27
|
-
if (setup.runtime !== undefined &&
|
|
40
|
+
if (setup.runtime !== undefined &&
|
|
41
|
+
(typeof setup.runtime !== 'string' || setup.runtime.length > 128 || !validRange(setup.runtime)))
|
|
28
42
|
return invalid();
|
|
29
43
|
return { ...setup };
|
|
30
44
|
}
|
|
@@ -32,5 +46,7 @@ export function setupManagementCommand(setup) {
|
|
|
32
46
|
return `npx${setup.registry ? ` --registry=${setup.registry}` : ''} ${setup.package}@${setup.version}`;
|
|
33
47
|
}
|
|
34
48
|
export function setupRuntimeCompatible(setup, version, targetLine) {
|
|
35
|
-
return !setup.runtime ||
|
|
49
|
+
return (!setup.runtime ||
|
|
50
|
+
(satisfies(version, setup.runtime, { includePrerelease: true }) &&
|
|
51
|
+
(!targetLine || subset(targetLine, setup.runtime, { includePrerelease: true }))));
|
|
36
52
|
}
|
package/dist/manifest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateInstallationOrigin } from './installation.js';
|
|
2
|
-
import
|
|
2
|
+
import valid from 'semver/functions/valid.js';
|
|
3
|
+
import validRange from 'semver/ranges/valid.js';
|
|
3
4
|
export class PresetError extends Error {
|
|
4
5
|
code;
|
|
5
6
|
constructor(code, message) {
|
|
@@ -14,9 +15,10 @@ function invalid(path, reason) {
|
|
|
14
15
|
throw new PresetError('PRESET_INVALID', `${path}: ${reason}`);
|
|
15
16
|
}
|
|
16
17
|
export function isJsonObject(value) {
|
|
17
|
-
return value !== null &&
|
|
18
|
+
return (value !== null &&
|
|
19
|
+
typeof value === 'object' &&
|
|
18
20
|
!Array.isArray(value) &&
|
|
19
|
-
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
21
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null));
|
|
20
22
|
}
|
|
21
23
|
function object(value, path) {
|
|
22
24
|
if (!isJsonObject(value))
|
|
@@ -88,7 +90,8 @@ function component(value, path, kind) {
|
|
|
88
90
|
invalid(`${path}.enabled`, 'expected a boolean');
|
|
89
91
|
if ('targets' in value) {
|
|
90
92
|
stringArray(value.targets, `${path}.targets`);
|
|
91
|
-
if (value.targets.length === 0 ||
|
|
93
|
+
if (value.targets.length === 0 ||
|
|
94
|
+
value.targets.some(item => item !== 'browser' && item !== 'node')) {
|
|
92
95
|
invalid(`${path}.targets`, 'expected browser and/or node');
|
|
93
96
|
}
|
|
94
97
|
}
|
|
@@ -100,7 +103,19 @@ function component(value, path, kind) {
|
|
|
100
103
|
return json(value, path);
|
|
101
104
|
}
|
|
102
105
|
export function validatePreset(value) {
|
|
103
|
-
fields(value, 'preset', [
|
|
106
|
+
fields(value, 'preset', [
|
|
107
|
+
'schemaVersion',
|
|
108
|
+
'id',
|
|
109
|
+
'version',
|
|
110
|
+
'extends',
|
|
111
|
+
'requires',
|
|
112
|
+
'packages',
|
|
113
|
+
'skills',
|
|
114
|
+
'providers',
|
|
115
|
+
'appProfiles',
|
|
116
|
+
'defaults',
|
|
117
|
+
'permissions',
|
|
118
|
+
]);
|
|
104
119
|
if (value.schemaVersion !== 1)
|
|
105
120
|
invalid('schemaVersion', 'only version 1 is supported');
|
|
106
121
|
id(value.id, 'id');
|
|
@@ -113,13 +128,20 @@ export function validatePreset(value) {
|
|
|
113
128
|
text(value.extends.source, 'extends.source');
|
|
114
129
|
if ('version' in value.extends)
|
|
115
130
|
range(value.extends.version, 'extends.version');
|
|
116
|
-
result.extends = {
|
|
131
|
+
result.extends = {
|
|
132
|
+
source: value.extends.source,
|
|
133
|
+
...(value.extends.version === undefined ? {} : { version: value.extends.version }),
|
|
134
|
+
};
|
|
117
135
|
}
|
|
118
136
|
if ('requires' in value) {
|
|
119
137
|
fields(value.requires, 'requires', ['mearlApi']);
|
|
120
138
|
if ('mearlApi' in value.requires)
|
|
121
139
|
range(value.requires.mearlApi, 'requires.mearlApi');
|
|
122
|
-
result.requires = {
|
|
140
|
+
result.requires = {
|
|
141
|
+
...(value.requires.mearlApi === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: { mearlApi: value.requires.mearlApi }),
|
|
144
|
+
};
|
|
123
145
|
}
|
|
124
146
|
for (const kind of ['packages', 'skills', 'providers']) {
|
|
125
147
|
if (!(kind in value))
|
|
@@ -148,14 +170,18 @@ export function validatePreset(value) {
|
|
|
148
170
|
const origins = value.permissions.origins ?? [];
|
|
149
171
|
if (origins.length > 64)
|
|
150
172
|
invalid('permissions.origins', 'exceeds 64 scopes');
|
|
151
|
-
result.permissions = {
|
|
173
|
+
result.permissions = {
|
|
174
|
+
origins: [
|
|
175
|
+
...new Set(origins.map((origin, index) => {
|
|
152
176
|
try {
|
|
153
177
|
return validateInstallationOrigin(origin);
|
|
154
178
|
}
|
|
155
179
|
catch {
|
|
156
180
|
return invalid(`permissions.origins[${index}]`, 'expected an explicit HTTP host scope');
|
|
157
181
|
}
|
|
158
|
-
}))
|
|
182
|
+
})),
|
|
183
|
+
],
|
|
184
|
+
};
|
|
159
185
|
}
|
|
160
186
|
if (JSON.stringify(result).length > 262_144)
|
|
161
187
|
invalid('preset', 'manifest exceeds 256 Ki UTF-16 code units');
|
package/dist/prepare.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, bundleDigest, bundleSourceUrl,
|
|
2
|
-
import
|
|
1
|
+
import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, bundleDigest, bundleSourceUrl, loadBundleResources, validateBundleManifest, } from './bundle.js';
|
|
2
|
+
import satisfies from 'semver/functions/satisfies.js';
|
|
3
3
|
import { clonePresetDefaults, validatePreset } from './manifest.js';
|
|
4
4
|
import { resolvePreset } from './resolve.js';
|
|
5
5
|
export async function presetSnapshotRevision(value) {
|
|
@@ -7,7 +7,8 @@ export async function presetSnapshotRevision(value) {
|
|
|
7
7
|
}
|
|
8
8
|
/** Select candidates without assuming that inferred locations have been discovered. */
|
|
9
9
|
function selectPresetPackages(preset, runtimes) {
|
|
10
|
-
const entries = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false &&
|
|
10
|
+
const entries = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false &&
|
|
11
|
+
(entry.targets ?? ['browser', 'node']).some(runtime => runtimes.includes(runtime)));
|
|
11
12
|
if (entries.length > 64)
|
|
12
13
|
throw new BundleError('BUNDLE_QUOTA', 'A preset can activate at most 64 packages');
|
|
13
14
|
return entries;
|
|
@@ -35,7 +36,14 @@ export function presetPackages(preset, runtimes) {
|
|
|
35
36
|
export function bundleTargets(bundle) {
|
|
36
37
|
const contributions = bundle.manifest.contributions;
|
|
37
38
|
const targets = [];
|
|
38
|
-
if (contributions &&
|
|
39
|
+
if (contributions &&
|
|
40
|
+
(Object.keys(contributions.browserRuntimes ?? {}).length ||
|
|
41
|
+
Object.keys(contributions.pageRuntimes ?? {}).length ||
|
|
42
|
+
contributions.appProfiles?.length ||
|
|
43
|
+
contributions.extension ||
|
|
44
|
+
contributions.requestUi ||
|
|
45
|
+
contributions.browserViews ||
|
|
46
|
+
contributions.browserControls))
|
|
39
47
|
targets.push('browser');
|
|
40
48
|
if (Object.keys(contributions?.nodeRuntimes ?? {}).length)
|
|
41
49
|
targets.push('node');
|
|
@@ -49,7 +57,12 @@ export function applyPackageTargets(preset, targets) {
|
|
|
49
57
|
const packages = { ...preset.packages };
|
|
50
58
|
for (const [id, entry] of enabled) {
|
|
51
59
|
const selected = targets[id];
|
|
52
|
-
if (!Array.isArray(selected) ||
|
|
60
|
+
if (!Array.isArray(selected) ||
|
|
61
|
+
!selected.length ||
|
|
62
|
+
selected.length > 2 ||
|
|
63
|
+
new Set(selected).size !== selected.length ||
|
|
64
|
+
selected.some(target => target !== 'browser' && target !== 'node') ||
|
|
65
|
+
(entry.targets && JSON.stringify(entry.targets) !== JSON.stringify(selected)))
|
|
53
66
|
throw new BundleError('BUNDLE_INTEGRITY', 'Invalid package target selection');
|
|
54
67
|
packages[id] = { ...entry, targets: [...selected] };
|
|
55
68
|
}
|
|
@@ -96,10 +109,14 @@ export async function preparePresetPackages(source, options, runtimes) {
|
|
|
96
109
|
},
|
|
97
110
|
});
|
|
98
111
|
const targetsKnown = Object.values(preset.packages).every(entry => entry.enabled === false || Boolean(entry.targets?.length));
|
|
99
|
-
const entries = targetsKnown
|
|
112
|
+
const entries = targetsKnown
|
|
113
|
+
? presetPackages(preset, runtimes)
|
|
114
|
+
: selectPresetPackages(preset, runtimes);
|
|
100
115
|
const bundles = [];
|
|
101
116
|
const pending = [];
|
|
102
|
-
const packageTargets = Object.fromEntries(Object.entries(preset.packages)
|
|
117
|
+
const packageTargets = Object.fromEntries(Object.entries(preset.packages)
|
|
118
|
+
.filter(([, entry]) => entry.enabled !== false)
|
|
119
|
+
.map(([id, entry]) => [id, entry.targets ?? []]));
|
|
103
120
|
for (const [id, entry] of entries) {
|
|
104
121
|
const manifestSource = bundleSourceUrl(entry.source);
|
|
105
122
|
const bytes = Uint8Array.from(await load(manifestSource, { maxBytes: MAX_BUNDLE_MANIFEST_BYTES, signal: options.signal }));
|
|
@@ -121,25 +138,38 @@ export async function preparePresetPackages(source, options, runtimes) {
|
|
|
121
138
|
if (targets.some(target => !available.includes(target)))
|
|
122
139
|
throw new BundleError('BUNDLE_INCOMPATIBLE', 'Package does not support the selected target');
|
|
123
140
|
packageTargets[id] = targets;
|
|
124
|
-
pending.push({
|
|
141
|
+
pending.push({ source: manifestSource, manifest, bytes });
|
|
125
142
|
}
|
|
126
143
|
preset = applyPackageTargets(preset, packageTargets);
|
|
127
144
|
const selected = new Set(presetPackages(preset, runtimes).map(([id]) => id));
|
|
128
|
-
for (const
|
|
129
|
-
if (!selected.has(
|
|
145
|
+
for (const { source: manifestSource, manifest, bytes } of pending) {
|
|
146
|
+
if (!selected.has(manifest.id))
|
|
130
147
|
continue;
|
|
131
|
-
bundles.push(await
|
|
148
|
+
bundles.push(await loadBundleResources(manifestSource, manifest, {
|
|
149
|
+
// A resource at the manifest URL belongs to the same captured response.
|
|
150
|
+
load: (url, request) => url === manifestSource ? Promise.resolve(bytes) : load(url, request),
|
|
151
|
+
signal: options.signal,
|
|
152
|
+
}));
|
|
132
153
|
}
|
|
133
|
-
verifyPackageTargets(preset, bundles);
|
|
134
154
|
options.signal?.throwIfAborted();
|
|
135
|
-
return { source, manifests, preset, bundles
|
|
155
|
+
return { source, manifests, preset, bundles, packageTargets };
|
|
136
156
|
}
|
|
137
157
|
/** The exact graph is checked offline; installed resources are verified by the target store. */
|
|
138
158
|
export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full) {
|
|
139
159
|
const json = clonePresetDefaults(value);
|
|
140
|
-
const allowed = [
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
const allowed = [
|
|
161
|
+
'source',
|
|
162
|
+
'manifests',
|
|
163
|
+
'bundleRevisions',
|
|
164
|
+
'packageTargets',
|
|
165
|
+
'revision',
|
|
166
|
+
...(full ? ['schemaVersion'] : []),
|
|
167
|
+
];
|
|
168
|
+
if (Object.keys(json).some(key => !allowed.includes(key)) ||
|
|
169
|
+
(full && json.schemaVersion !== 1) ||
|
|
170
|
+
typeof json.source !== 'string' ||
|
|
171
|
+
typeof json.revision !== 'string' ||
|
|
172
|
+
!/^[a-f0-9]{64}$/.test(json.revision)) {
|
|
143
173
|
throw new BundleError('BUNDLE_INTEGRITY', 'Invalid stored preset identity');
|
|
144
174
|
}
|
|
145
175
|
const source = bundleSourceUrl(json.source);
|
|
@@ -147,7 +177,8 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
|
|
|
147
177
|
if (Object.keys(manifests).length > 32)
|
|
148
178
|
throw new BundleError('BUNDLE_QUOTA', 'Stored preset inheritance exceeds 32 manifests');
|
|
149
179
|
const bundleRevisions = clonePresetDefaults(json.bundleRevisions);
|
|
150
|
-
if (Object.keys(bundleRevisions).length > 64 ||
|
|
180
|
+
if (Object.keys(bundleRevisions).length > 64 ||
|
|
181
|
+
Object.values(bundleRevisions).some(revision => typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))) {
|
|
151
182
|
throw new BundleError('BUNDLE_INTEGRITY', 'Invalid stored package revisions');
|
|
152
183
|
}
|
|
153
184
|
const validated = {};
|
|
@@ -157,8 +188,14 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
|
|
|
157
188
|
validated[url] = validatePreset(manifest);
|
|
158
189
|
}
|
|
159
190
|
const packageTargets = clonePresetDefaults(json.packageTargets);
|
|
160
|
-
const data = {
|
|
161
|
-
|
|
191
|
+
const data = {
|
|
192
|
+
...(full ? { schemaVersion: 1 } : {}),
|
|
193
|
+
source,
|
|
194
|
+
manifests: validated,
|
|
195
|
+
bundleRevisions,
|
|
196
|
+
packageTargets,
|
|
197
|
+
};
|
|
198
|
+
if ((await presetSnapshotRevision(data)) !== json.revision)
|
|
162
199
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored preset revision does not match');
|
|
163
200
|
let preset = await resolvePreset(source, {
|
|
164
201
|
mearlApiVersion,
|
|
@@ -170,7 +207,9 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
|
|
|
170
207
|
});
|
|
171
208
|
preset = applyPackageTargets(preset, packageTargets);
|
|
172
209
|
const entries = presetPackages(preset, runtimes);
|
|
173
|
-
if (preset.chain.length !== Object.keys(validated).length ||
|
|
210
|
+
if (preset.chain.length !== Object.keys(validated).length ||
|
|
211
|
+
entries.length !== Object.keys(bundleRevisions).length ||
|
|
212
|
+
entries.some(([id]) => !Object.hasOwn(bundleRevisions, id))) {
|
|
174
213
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored preset package graph does not match');
|
|
175
214
|
}
|
|
176
215
|
return { snapshot: { ...data, revision: json.revision }, preset };
|
package/dist/preset.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { browserPresetSnapshot } from './browserPreset.js';
|
|
2
|
-
import { preparePresetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets } from './prepare.js';
|
|
3
|
-
import
|
|
2
|
+
import { preparePresetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets, } from './prepare.js';
|
|
3
|
+
import satisfies from 'semver/functions/satisfies.js';
|
|
4
4
|
import { BundleError, MAX_BROWSER_STORAGE_BYTES, verifyBundle } from './bundle.js';
|
|
5
5
|
export async function preparePreset(source, options) {
|
|
6
6
|
const prepared = await preparePresetPackages(source, options, ['browser', 'node']);
|
|
@@ -27,7 +27,9 @@ export async function verifyPreparedPreset(value, mearlApiVersion) {
|
|
|
27
27
|
throw new BundleError('BUNDLE_QUOTA', 'Preset package count exceeds 64');
|
|
28
28
|
let total = 0;
|
|
29
29
|
for (const bundle of value.bundles) {
|
|
30
|
-
if (!bundle?.resources ||
|
|
30
|
+
if (!bundle?.resources ||
|
|
31
|
+
typeof bundle.resources !== 'object' ||
|
|
32
|
+
Array.isArray(bundle.resources))
|
|
31
33
|
throw new BundleError('BUNDLE_INVALID', 'Expected package resource buffers');
|
|
32
34
|
const resources = Object.values(bundle.resources);
|
|
33
35
|
if (resources.length > 64)
|
|
@@ -46,10 +48,19 @@ export async function verifyPreparedPreset(value, mearlApiVersion) {
|
|
|
46
48
|
]);
|
|
47
49
|
const { snapshot, preset } = graph;
|
|
48
50
|
verifyPackageTargets(preset, bundles);
|
|
49
|
-
if (bundles.length !== Object.keys(snapshot.bundleRevisions).length ||
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
if (bundles.length !== Object.keys(snapshot.bundleRevisions).length ||
|
|
52
|
+
new Set(bundles.map(bundle => bundle.manifest.id)).size !== bundles.length ||
|
|
53
|
+
bundles.some(bundle => {
|
|
54
|
+
const entry = preset.packages[bundle.manifest.id];
|
|
55
|
+
return (snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision ||
|
|
56
|
+
entry?.source !== bundle.source ||
|
|
57
|
+
(entry.version && !satisfies(bundle.manifest.version, entry.version)));
|
|
58
|
+
}))
|
|
53
59
|
throw new BundleError('BUNDLE_INTEGRITY', 'Preset installation requires every exact package');
|
|
54
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
snapshot,
|
|
62
|
+
preset,
|
|
63
|
+
bundles,
|
|
64
|
+
browser: await browserPresetSnapshot(snapshot.source, snapshot.manifests, preset, bundles),
|
|
65
|
+
};
|
|
55
66
|
}
|
package/dist/resolve.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { parseToolboxCatalogs } from './toolboxCatalogs.js';
|
|
2
2
|
import { parseSetupManagement } from './management.js';
|
|
3
|
-
import
|
|
3
|
+
import satisfies from 'semver/functions/satisfies.js';
|
|
4
|
+
import valid from 'semver/functions/valid.js';
|
|
4
5
|
import { clonePresetDefaults, isJsonObject, PresetError, validatePreset } from './manifest.js';
|
|
5
6
|
const kinds = ['packages', 'skills', 'providers'];
|
|
6
7
|
/** Objects merge recursively; arrays, scalars and explicit null replace the previous value. */
|
|
7
8
|
export function mergePresetDefaults(base, overrides) {
|
|
8
9
|
const result = clonePresetDefaults(base);
|
|
9
10
|
for (const [key, value] of Object.entries(clonePresetDefaults(overrides))) {
|
|
10
|
-
result[key] =
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
result[key] =
|
|
12
|
+
isJsonObject(value) && isJsonObject(result[key])
|
|
13
|
+
? mergePresetDefaults(result[key], value)
|
|
14
|
+
: value;
|
|
13
15
|
}
|
|
14
16
|
return result;
|
|
15
17
|
}
|
|
@@ -93,7 +95,8 @@ export async function resolvePreset(source, options) {
|
|
|
93
95
|
active.add(url);
|
|
94
96
|
const manifest = validatePreset(await options.load(url));
|
|
95
97
|
options.signal?.throwIfAborted();
|
|
96
|
-
if (manifest.requires?.mearlApi &&
|
|
98
|
+
if (manifest.requires?.mearlApi &&
|
|
99
|
+
!satisfies(options.mearlApiVersion, manifest.requires.mearlApi)) {
|
|
97
100
|
throw new PresetError('PRESET_INCOMPATIBLE', `${manifest.id} requires Mearl API ${manifest.requires.mearlApi}`);
|
|
98
101
|
}
|
|
99
102
|
let parent;
|
|
@@ -106,21 +109,35 @@ export async function resolvePreset(source, options) {
|
|
|
106
109
|
const result = {
|
|
107
110
|
id: manifest.id,
|
|
108
111
|
version: manifest.version,
|
|
109
|
-
chain: [
|
|
112
|
+
chain: [
|
|
113
|
+
...(parent?.chain ?? []),
|
|
114
|
+
{ source: url, id: manifest.id, version: manifest.version },
|
|
115
|
+
],
|
|
110
116
|
packages: { ...parent?.packages },
|
|
111
117
|
skills: { ...parent?.skills },
|
|
112
118
|
providers: { ...parent?.providers },
|
|
113
119
|
appProfiles: { ...parent?.appProfiles, ...manifest.appProfiles },
|
|
114
120
|
defaults: mergePresetDefaults(parent?.defaults ?? {}, manifest.defaults ?? {}),
|
|
115
|
-
permissions: {
|
|
121
|
+
permissions: {
|
|
122
|
+
origins: [
|
|
123
|
+
...new Set([
|
|
124
|
+
...(parent?.permissions.origins ?? []),
|
|
125
|
+
...(manifest.permissions?.origins ?? []),
|
|
126
|
+
]),
|
|
127
|
+
],
|
|
128
|
+
},
|
|
116
129
|
};
|
|
117
130
|
for (const kind of kinds) {
|
|
118
131
|
for (const [id, patch] of Object.entries(manifest[kind] ?? {})) {
|
|
119
132
|
result[kind][id] = {
|
|
120
133
|
...result[kind][id],
|
|
121
134
|
...patch,
|
|
122
|
-
...(patch.source === undefined
|
|
123
|
-
|
|
135
|
+
...(patch.source === undefined
|
|
136
|
+
? {}
|
|
137
|
+
: { source: componentSource(kind, patch.source, url) }),
|
|
138
|
+
...(patch.migrateFrom === undefined
|
|
139
|
+
? {}
|
|
140
|
+
: { migrateFrom: patch.migrateFrom.map(source => componentSource(kind, source, url)) }),
|
|
124
141
|
declaredBy: url,
|
|
125
142
|
};
|
|
126
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/preset",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Browser-compatible preset validation, inheritance and capability resolution for Mearl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/installation.d.ts",
|
|
20
20
|
"import": "./dist/installation.js"
|
|
21
21
|
},
|
|
22
|
+
"./pageRuntime": {
|
|
23
|
+
"types": "./dist/pageRuntime.d.ts",
|
|
24
|
+
"import": "./dist/pageRuntime.js"
|
|
25
|
+
},
|
|
22
26
|
"./installation-site.json": "./installation-site.json",
|
|
23
27
|
"./networkRoutes": {
|
|
24
28
|
"types": "./dist/networkRoutes.d.ts",
|