@mearl/preset 3.0.0-beta.0 → 3.0.0-beta.10
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.md +66 -18
- package/README.zh-CN.md +106 -0
- package/dist/browserControls.d.ts +29 -0
- package/dist/browserControls.js +54 -0
- package/dist/browserPreset.js +8 -3
- package/dist/browserViews.d.ts +1 -1
- package/dist/browserViews.js +5 -5
- package/dist/bundle.d.ts +4 -1
- package/dist/bundle.js +24 -5
- package/dist/bundleStore.js +80 -75
- package/dist/bundles.d.ts +7 -1
- package/dist/bundles.js +4 -1
- package/dist/{nodeSnapshot.d.ts → capabilitySnapshot.d.ts} +11 -11
- package/dist/{nodeSnapshot.js → capabilitySnapshot.js} +36 -35
- package/dist/extensionContributions.d.ts +29 -0
- package/dist/extensionContributions.js +57 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/installation.d.ts +5 -1
- package/dist/installation.js +19 -6
- package/dist/management.d.ts +10 -0
- package/dist/management.js +36 -0
- package/dist/manifest.js +26 -8
- package/dist/networkRoutes.d.ts +12 -0
- package/dist/networkRoutes.js +32 -0
- package/dist/prepare.d.ts +6 -0
- package/dist/prepare.js +81 -16
- package/dist/preset.js +3 -1
- package/dist/requestUi.d.ts +3 -1
- package/dist/requestUi.js +2 -2
- package/dist/resolve.js +7 -0
- package/dist/toolboxCatalogs.d.ts +8 -0
- package/dist/toolboxCatalogs.js +34 -0
- package/dist/types.d.ts +4 -1
- package/installation-site.json +4 -0
- package/package.json +13 -2
package/dist/bundleStore.js
CHANGED
|
@@ -11,8 +11,8 @@ function installationMetadata(value) {
|
|
|
11
11
|
}
|
|
12
12
|
const STORE = 'bundles';
|
|
13
13
|
const PRESET = 'preset';
|
|
14
|
-
const
|
|
15
|
-
const PINS = '
|
|
14
|
+
const OBJECTS = 'objects';
|
|
15
|
+
const PINS = 'resourcePins';
|
|
16
16
|
function captureRevisions(input) {
|
|
17
17
|
if (input === undefined)
|
|
18
18
|
return;
|
|
@@ -25,39 +25,54 @@ function checkRevisions(records, expected) {
|
|
|
25
25
|
if (expected && (records.length !== Object.keys(expected).length || records.some(record => !Object.hasOwn(expected, record.id) || expected[record.id] !== record.revision)))
|
|
26
26
|
throw new BundleError('BUNDLE_CONFLICT', 'Browser bundles changed while contributions were being validated');
|
|
27
27
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
].filter((name) => typeof name === 'string'))];
|
|
28
|
+
function storedBundle(bundle) {
|
|
29
|
+
const { resources, ...metadata } = bundle;
|
|
30
|
+
return { id: bundle.manifest.id, revision: bundle.revision,
|
|
31
|
+
byteLength: Object.values(resources).reduce((sum, bytes) => sum + bytes.byteLength, 0), bundle: metadata };
|
|
33
32
|
}
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
for (const record of records)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
function bundleObjectHashes(records) {
|
|
34
|
+
const hashes = new Set();
|
|
35
|
+
for (const record of records) {
|
|
36
|
+
const resources = record?.bundle?.manifest?.resources;
|
|
37
|
+
if (!resources || typeof resources !== 'object' || Array.isArray(resources))
|
|
38
|
+
continue;
|
|
39
|
+
for (const resource of Object.values(resources))
|
|
40
|
+
if (resource && typeof resource.sha256 === 'string' && /^[a-f0-9]{64}$/.test(resource.sha256))
|
|
41
|
+
hashes.add(resource.sha256);
|
|
42
|
+
}
|
|
43
|
+
return hashes;
|
|
44
|
+
}
|
|
45
|
+
function referencedObjects(records, resources, pins) {
|
|
46
|
+
const referenced = bundleObjectHashes(records);
|
|
47
|
+
for (const pin of pins)
|
|
48
|
+
referenced.add(pin.sha256);
|
|
49
|
+
return resources.filter(resource => referenced.has(resource.sha256));
|
|
43
50
|
}
|
|
44
|
-
function
|
|
51
|
+
function bundleObjects(bundles) {
|
|
45
52
|
const result = new Map();
|
|
46
53
|
for (const bundle of bundles)
|
|
47
|
-
for (const name of
|
|
48
|
-
|
|
49
|
-
if (resource && typeof resource.sha256 === 'string' && /^[a-f0-9]{64}$/.test(resource.sha256) && bytes instanceof Uint8Array && bytes.byteLength === resource.byteLength)
|
|
50
|
-
result.set(resource.sha256, { sha256: resource.sha256, bytes });
|
|
51
|
-
}
|
|
54
|
+
for (const [name, resource] of Object.entries(bundle.manifest.resources))
|
|
55
|
+
result.set(resource.sha256, { sha256: resource.sha256, bytes: bundle.resources[name] });
|
|
52
56
|
return [...result.values()];
|
|
53
57
|
}
|
|
58
|
+
/** Schedule object reads inside the metadata transaction, preserving one consistent snapshot. */
|
|
59
|
+
function readObjects(records, store) {
|
|
60
|
+
const result = new Map();
|
|
61
|
+
const hashes = bundleObjectHashes(records);
|
|
62
|
+
for (const hash of hashes) {
|
|
63
|
+
const request = store.get(hash);
|
|
64
|
+
request.onsuccess = () => { if (request.result)
|
|
65
|
+
result.set(hash, request.result); };
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
54
69
|
function checkStorageBudget(records, runtimes) {
|
|
55
70
|
if (records.some(record => !Number.isSafeInteger(record.byteLength) || record.byteLength < 0 || record.byteLength > MAX_BUNDLE_BYTES) ||
|
|
56
71
|
runtimes.some(runtime => !(runtime.bytes instanceof Uint8Array) || runtime.bytes.byteLength > MAX_BUNDLE_BYTES)) {
|
|
57
72
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource size is invalid');
|
|
58
73
|
}
|
|
59
74
|
const retained = runtimes.reduce((sum, runtime) => sum + runtime.bytes.byteLength, 0);
|
|
60
|
-
const total =
|
|
75
|
+
const total = retained;
|
|
61
76
|
if (records.length > 64 || !Number.isSafeInteger(total) || total > MAX_BROWSER_STORAGE_BYTES)
|
|
62
77
|
throw new BundleError('BUNDLE_QUOTA', 'Installed and retained resources exceed the browser storage budget');
|
|
63
78
|
}
|
|
@@ -71,14 +86,14 @@ export class BrowserBundleStore {
|
|
|
71
86
|
if (this.database)
|
|
72
87
|
return this.database;
|
|
73
88
|
this.database = new Promise((resolve, reject) => {
|
|
74
|
-
const request = (this.options.indexedDB ?? indexedDB).open(this.options.databaseName ?? 'mearl-
|
|
89
|
+
const request = (this.options.indexedDB ?? indexedDB).open(this.options.databaseName ?? 'mearl-capabilities-v3', 1);
|
|
75
90
|
request.onupgradeneeded = () => {
|
|
76
91
|
if (!request.result.objectStoreNames.contains(STORE))
|
|
77
92
|
request.result.createObjectStore(STORE, { keyPath: 'id' });
|
|
78
93
|
if (!request.result.objectStoreNames.contains(PRESET))
|
|
79
94
|
request.result.createObjectStore(PRESET);
|
|
80
|
-
if (!request.result.objectStoreNames.contains(
|
|
81
|
-
request.result.createObjectStore(
|
|
95
|
+
if (!request.result.objectStoreNames.contains(OBJECTS))
|
|
96
|
+
request.result.createObjectStore(OBJECTS, { keyPath: 'sha256' });
|
|
82
97
|
if (!request.result.objectStoreNames.contains(PINS))
|
|
83
98
|
request.result.createObjectStore(PINS, { keyPath: 'owner' });
|
|
84
99
|
};
|
|
@@ -100,30 +115,41 @@ export class BrowserBundleStore {
|
|
|
100
115
|
async records() {
|
|
101
116
|
const database = await this.open();
|
|
102
117
|
return new Promise((resolve, reject) => {
|
|
103
|
-
const transaction = database.transaction([STORE, PRESET], 'readonly');
|
|
118
|
+
const transaction = database.transaction([STORE, PRESET, OBJECTS], 'readonly');
|
|
104
119
|
const request = transaction.objectStore(STORE).getAll();
|
|
120
|
+
let objects = new Map();
|
|
121
|
+
request.onsuccess = () => { objects = readObjects(request.result, transaction.objectStore(OBJECTS)); };
|
|
105
122
|
const active = transaction.objectStore(PRESET).get('active');
|
|
106
123
|
const installation = transaction.objectStore(PRESET).get('installation');
|
|
107
124
|
const token = transaction.objectStore(PRESET).get('installationToken');
|
|
108
|
-
transaction.oncomplete = () => resolve({ records: request.result, active: active.result, installation: installation.result, installationToken: token.result ?? null });
|
|
125
|
+
transaction.oncomplete = () => resolve({ objects, records: request.result, active: active.result, installation: installation.result, installationToken: token.result ?? null });
|
|
109
126
|
transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Bundle read failed'));
|
|
110
127
|
});
|
|
111
128
|
}
|
|
112
129
|
async get(id) {
|
|
113
130
|
const database = await this.open();
|
|
131
|
+
let objects = new Map();
|
|
114
132
|
const record = await new Promise((resolve, reject) => {
|
|
115
|
-
const transaction = database.transaction(STORE, 'readonly');
|
|
133
|
+
const transaction = database.transaction([STORE, OBJECTS], 'readonly');
|
|
116
134
|
const request = transaction.objectStore(STORE).get(id);
|
|
135
|
+
request.onsuccess = () => { objects = readObjects(request.result ? [request.result] : [], transaction.objectStore(OBJECTS)); };
|
|
117
136
|
transaction.oncomplete = () => resolve(request.result);
|
|
118
137
|
transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Bundle read failed'));
|
|
119
138
|
});
|
|
120
|
-
return record ? this.verified(record) : undefined;
|
|
139
|
+
return record ? this.verified(record, objects) : undefined;
|
|
121
140
|
}
|
|
122
|
-
async verified(record) {
|
|
141
|
+
async verified(record, objects) {
|
|
123
142
|
if (record.id !== record.bundle?.manifest?.id || record.revision !== record.bundle?.revision) {
|
|
124
143
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored bundle identity does not match');
|
|
125
144
|
}
|
|
126
|
-
const
|
|
145
|
+
const resources = {};
|
|
146
|
+
for (const [name, metadata] of Object.entries(record.bundle.manifest.resources)) {
|
|
147
|
+
const object = objects.get(metadata.sha256);
|
|
148
|
+
if (!object || object.sha256 !== metadata.sha256)
|
|
149
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource integrity check failed: object is missing');
|
|
150
|
+
resources[name] = object.bytes;
|
|
151
|
+
}
|
|
152
|
+
const bundle = await verifyBundle({ ...record.bundle, resources }, this.options.mearlApiVersion);
|
|
127
153
|
if (Object.values(bundle.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0) !== record.byteLength) {
|
|
128
154
|
throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource size does not match');
|
|
129
155
|
}
|
|
@@ -132,10 +158,10 @@ export class BrowserBundleStore {
|
|
|
132
158
|
async list() {
|
|
133
159
|
const bundles = [];
|
|
134
160
|
const errors = [];
|
|
135
|
-
const { records, active, installation: storedInstallation, installationToken } = await this.records();
|
|
161
|
+
const { objects, records, active, installation: storedInstallation, installationToken } = await this.records();
|
|
136
162
|
for (const record of records) {
|
|
137
163
|
try {
|
|
138
|
-
bundles.push(await this.verified(record));
|
|
164
|
+
bundles.push(await this.verified(record, objects));
|
|
139
165
|
}
|
|
140
166
|
catch (error) {
|
|
141
167
|
errors.push({ id: record.id, error: error instanceof Error ? error.message : 'Invalid stored bundle' });
|
|
@@ -191,8 +217,8 @@ export class BrowserBundleStore {
|
|
|
191
217
|
throw new BundleError('BUNDLE_INVALID', 'Invalid browser runtime digest');
|
|
192
218
|
const database = await this.open();
|
|
193
219
|
const record = await new Promise((resolve, reject) => {
|
|
194
|
-
const transaction = database.transaction(
|
|
195
|
-
const request = transaction.objectStore(
|
|
220
|
+
const transaction = database.transaction(OBJECTS, 'readonly');
|
|
221
|
+
const request = transaction.objectStore(OBJECTS).get(sha256);
|
|
196
222
|
transaction.oncomplete = () => resolve(request.result);
|
|
197
223
|
transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Browser runtime read failed'));
|
|
198
224
|
});
|
|
@@ -228,8 +254,8 @@ export class BrowserBundleStore {
|
|
|
228
254
|
});
|
|
229
255
|
const database = await this.open();
|
|
230
256
|
await new Promise((resolve, reject) => {
|
|
231
|
-
const transaction = database.transaction([STORE,
|
|
232
|
-
const runtimeStore = transaction.objectStore(
|
|
257
|
+
const transaction = database.transaction([STORE, OBJECTS, PINS], 'readwrite');
|
|
258
|
+
const runtimeStore = transaction.objectStore(OBJECTS);
|
|
233
259
|
const pinStore = transaction.objectStore(PINS);
|
|
234
260
|
const records = transaction.objectStore(STORE).getAll();
|
|
235
261
|
const resources = runtimeStore.getAll();
|
|
@@ -237,26 +263,7 @@ export class BrowserBundleStore {
|
|
|
237
263
|
let failure;
|
|
238
264
|
request.onsuccess = () => {
|
|
239
265
|
const cached = new Map(resources.result.map(resource => [resource.sha256, resource]));
|
|
240
|
-
const missing = new Set(patch.flatMap(({ sha256 }) => sha256 && !cached.has(sha256) ? [sha256] : []));
|
|
241
|
-
let added = false;
|
|
242
|
-
for (const runtime of runtimeResources(records.result.map(record => record.bundle))) {
|
|
243
|
-
if (missing.has(runtime.sha256)) {
|
|
244
|
-
cached.set(runtime.sha256, runtime);
|
|
245
|
-
runtimeStore.put(runtime);
|
|
246
|
-
added = true;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
266
|
const available = [...cached.values()];
|
|
250
|
-
if (added) {
|
|
251
|
-
try {
|
|
252
|
-
checkStorageBudget(records.result, available);
|
|
253
|
-
}
|
|
254
|
-
catch (error) {
|
|
255
|
-
failure = error;
|
|
256
|
-
transaction.abort();
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
267
|
const pins = new Map(replace ? [] : request.result.map((pin) => [pin.owner, pin]));
|
|
261
268
|
for (const { owner, sha256 } of patch) {
|
|
262
269
|
if (sha256 === null) {
|
|
@@ -273,7 +280,7 @@ export class BrowserBundleStore {
|
|
|
273
280
|
pinStore.clear();
|
|
274
281
|
for (const pin of pins.values())
|
|
275
282
|
pinStore.put(pin);
|
|
276
|
-
const retained = new Set(
|
|
283
|
+
const retained = new Set(referencedObjects(records.result, available, [...pins.values()]).map(runtime => runtime.sha256));
|
|
277
284
|
for (const runtime of available)
|
|
278
285
|
if (!retained.has(runtime.sha256))
|
|
279
286
|
runtimeStore.delete(runtime.sha256);
|
|
@@ -290,10 +297,12 @@ export class BrowserBundleStore {
|
|
|
290
297
|
return undefined;
|
|
291
298
|
const id = target.split('/')[0];
|
|
292
299
|
const database = await this.open();
|
|
300
|
+
let objects = new Map();
|
|
293
301
|
const record = await new Promise((resolve, reject) => {
|
|
294
|
-
const transaction = database.transaction([PRESET, STORE], 'readonly');
|
|
302
|
+
const transaction = database.transaction([PRESET, STORE, OBJECTS], 'readonly');
|
|
295
303
|
const current = transaction.objectStore(PRESET).get('active');
|
|
296
304
|
const bundle = transaction.objectStore(STORE).get(id);
|
|
305
|
+
bundle.onsuccess = () => { objects = readObjects(bundle.result ? [bundle.result] : [], transaction.objectStore(OBJECTS)); };
|
|
297
306
|
transaction.oncomplete = () => {
|
|
298
307
|
if (current.result?.revision !== activation.snapshot.revision) {
|
|
299
308
|
reject(new BundleError('BUNDLE_CONFLICT', 'The selected preset changed during profile lookup'));
|
|
@@ -306,7 +315,7 @@ export class BrowserBundleStore {
|
|
|
306
315
|
});
|
|
307
316
|
if (!record || record.revision !== activation.snapshot.bundleRevisions[id])
|
|
308
317
|
throw new BundleError('BUNDLE_INTEGRITY', 'The selected app provider is missing');
|
|
309
|
-
return { target, bundle: await this.verified(record) };
|
|
318
|
+
return { target, bundle: await this.verified(record, objects) };
|
|
310
319
|
}
|
|
311
320
|
/** Packages and the exact preset graph become visible together, after host contribution checks. */
|
|
312
321
|
async activatePreset(input, replacements, expectedRevision, association, expectedBundleRevisions) {
|
|
@@ -323,11 +332,11 @@ export class BrowserBundleStore {
|
|
|
323
332
|
const token = accepted ? await bundleDigest(crypto.getRandomValues(new Uint8Array(32))) : null;
|
|
324
333
|
const database = await this.open();
|
|
325
334
|
await new Promise((resolve, reject) => {
|
|
326
|
-
const transaction = database.transaction([STORE, PRESET,
|
|
335
|
+
const transaction = database.transaction([STORE, PRESET, OBJECTS, PINS], 'readwrite');
|
|
327
336
|
const store = transaction.objectStore(STORE);
|
|
328
337
|
const presets = transaction.objectStore(PRESET);
|
|
329
338
|
const records = store.getAll();
|
|
330
|
-
const runtimeStore = transaction.objectStore(
|
|
339
|
+
const runtimeStore = transaction.objectStore(OBJECTS);
|
|
331
340
|
const retained = runtimeStore.getAll();
|
|
332
341
|
const pins = transaction.objectStore(PINS).getAll();
|
|
333
342
|
const installation = presets.get('installationToken');
|
|
@@ -356,12 +365,12 @@ export class BrowserBundleStore {
|
|
|
356
365
|
}
|
|
357
366
|
const result = records.result.filter(record => !replacing.has(record.id) && !previousManaged.has(record.id));
|
|
358
367
|
for (const { bundle } of next)
|
|
359
|
-
result.push(
|
|
360
|
-
const resources =
|
|
368
|
+
result.push(storedBundle(bundle));
|
|
369
|
+
const resources = bundleObjects(next.map(item => item.bundle));
|
|
361
370
|
const cache = new Map(retained.result.map(runtime => [runtime.sha256, runtime]));
|
|
362
371
|
for (const runtime of resources)
|
|
363
372
|
cache.set(runtime.sha256, runtime);
|
|
364
|
-
const keeping =
|
|
373
|
+
const keeping = referencedObjects(result, [...cache.values()], pins.result);
|
|
365
374
|
try {
|
|
366
375
|
checkStorageBudget(result, keeping);
|
|
367
376
|
}
|
|
@@ -400,21 +409,18 @@ export class BrowserBundleStore {
|
|
|
400
409
|
async install(bundle, expectedRevision, expectedBundleRevisions) {
|
|
401
410
|
const selection = captureRevisions(expectedBundleRevisions);
|
|
402
411
|
const verified = await verifyBundle(bundle, this.options.mearlApiVersion);
|
|
403
|
-
|
|
404
|
-
await this.change(verified.manifest.id, expectedRevision, {
|
|
405
|
-
id: verified.manifest.id, revision: verified.revision, byteLength, bundle: verified,
|
|
406
|
-
}, selection);
|
|
412
|
+
await this.change(verified.manifest.id, expectedRevision, storedBundle(verified), selection, bundleObjects([verified]));
|
|
407
413
|
}
|
|
408
414
|
async remove(id, expectedRevision) {
|
|
409
415
|
await this.change(id, expectedRevision);
|
|
410
416
|
}
|
|
411
|
-
async change(id, expectedRevision, next, selection) {
|
|
417
|
+
async change(id, expectedRevision, next, selection, resources = []) {
|
|
412
418
|
const database = await this.open();
|
|
413
419
|
await new Promise((resolve, reject) => {
|
|
414
|
-
const transaction = database.transaction([STORE, PRESET,
|
|
420
|
+
const transaction = database.transaction([STORE, PRESET, OBJECTS, PINS], 'readwrite');
|
|
415
421
|
const store = transaction.objectStore(STORE);
|
|
416
422
|
const active = transaction.objectStore(PRESET).get('active');
|
|
417
|
-
const runtimeStore = transaction.objectStore(
|
|
423
|
+
const runtimeStore = transaction.objectStore(OBJECTS);
|
|
418
424
|
const retained = runtimeStore.getAll();
|
|
419
425
|
const pins = transaction.objectStore(PINS).getAll();
|
|
420
426
|
let failure;
|
|
@@ -442,11 +448,10 @@ export class BrowserBundleStore {
|
|
|
442
448
|
}
|
|
443
449
|
if (next) {
|
|
444
450
|
const others = records.filter(record => record.id !== id);
|
|
445
|
-
const resources = runtimeResources([next.bundle]);
|
|
446
451
|
const cache = new Map(retained.result.map(runtime => [runtime.sha256, runtime]));
|
|
447
452
|
for (const runtime of resources)
|
|
448
453
|
cache.set(runtime.sha256, runtime);
|
|
449
|
-
const keeping =
|
|
454
|
+
const keeping = referencedObjects([...others, next], [...cache.values()], pins.result);
|
|
450
455
|
try {
|
|
451
456
|
checkStorageBudget([...others, next], keeping);
|
|
452
457
|
}
|
|
@@ -465,7 +470,7 @@ export class BrowserBundleStore {
|
|
|
465
470
|
}
|
|
466
471
|
else {
|
|
467
472
|
store.delete(id);
|
|
468
|
-
const keep = new Set(
|
|
473
|
+
const keep = new Set(referencedObjects(records.filter(record => record.id !== id), retained.result, pins.result).map(runtime => runtime.sha256));
|
|
469
474
|
for (const runtime of retained.result)
|
|
470
475
|
if (!keep.has(runtime.sha256))
|
|
471
476
|
runtimeStore.delete(runtime.sha256);
|
package/dist/bundles.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { BundleError, MAX_BUNDLE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, MAX_BROWSER_STORAGE_BYTES, MAX_PAGE_RUNTIME_BYTES, MAX_REQUEST_UI_BYTES, bundleDigest, bundleSourceUrl, prepareBundle, readBundleJson, readBundleText, validateBundleManifest, verifyBundle, } from './bundle.js';
|
|
2
2
|
export type { BundleManifest, PreparedBundle, BundleLoader, BundleLoadOptions, PrepareBundleOptions, CapabilityRuntimeDeclaration } from './bundle.js';
|
|
3
|
+
export { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
|
|
4
|
+
export type { ExtensionContributions } from './extensionContributions.js';
|
|
3
5
|
export { createBundleFetchLoader } from './bundleFetch.js';
|
|
4
6
|
export { BrowserBundleStore } from './bundleStore.js';
|
|
5
7
|
export type { BrowserPresetInstallation } from './bundleStore.js';
|
|
@@ -12,7 +14,7 @@ export { installPageRuntimeHost, buildPageRuntimeRegistrationSource, buildPageRu
|
|
|
12
14
|
export type { PageRuntimeBinding, PageRuntimeConfig } from './pageRuntime.js';
|
|
13
15
|
export { receiveBrowserPreset, readPresetTransferResource, PRESET_TRANSFER_CHUNK_BYTES, MAX_PRESET_TRANSFER_HEADER_BYTES } from './browserTransfer.js';
|
|
14
16
|
export type { BrowserPresetTransferHeader, PresetTransferRequest, PresetTransferTransport } from './browserTransfer.js';
|
|
15
|
-
export { prepareBrowserBundleSnapshot } from './
|
|
17
|
+
export { prepareBrowserBundleSnapshot } from './capabilitySnapshot.js';
|
|
16
18
|
export { buildBrowserCapabilityPage, buildBrowserPureCapabilityPage } from './browserSandbox.js';
|
|
17
19
|
export type { BrowserCapabilityHost, BrowserCapabilityInvocation, BrowserCapabilityRequest, BrowserCapabilityResponse } from './browserSandbox.js';
|
|
18
20
|
export { validateBrowserRequestUi, selectBrowserRequestUi, browserUiText, browserRequestUiLink, browserRequestLoginError, validateBrowserRequestDetail } from './requestUi.js';
|
|
@@ -20,3 +22,7 @@ export type { BrowserUiText, BrowserRequestUi, BrowserRequestDetail, SelectedBro
|
|
|
20
22
|
export { validateBrowserView, selectBrowserView, buildBrowserViewPage } from './browserViews.js';
|
|
21
23
|
export type { BrowserViewSlot, BrowserViewDefinition, BrowserViewAction, BrowserViewTransport, SelectedBrowserView, BrowserViewBootstrap } from './browserViews.js';
|
|
22
24
|
export { browserTransportUrl, browserTransportPolicies } from './browserTransport.js';
|
|
25
|
+
export { validateBrowserControl, selectBrowserControl } from './browserControls.js';
|
|
26
|
+
export type { BrowserSegmentedControl, SelectedBrowserControl } from './browserControls.js';
|
|
27
|
+
export { parseNetworkRoutePlan, encodeNetworkRoutePlan, decodeNetworkRoutePlan } from './networkRoutes.js';
|
|
28
|
+
export type { NetworkHostRoute, NetworkRoutePlan } from './networkRoutes.js';
|
package/dist/bundles.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export { BundleError, MAX_BUNDLE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, MAX_BROWSER_STORAGE_BYTES, MAX_PAGE_RUNTIME_BYTES, MAX_REQUEST_UI_BYTES, bundleDigest, bundleSourceUrl, prepareBundle, readBundleJson, readBundleText, validateBundleManifest, verifyBundle, } from './bundle.js';
|
|
2
|
+
export { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
|
|
2
3
|
export { createBundleFetchLoader } from './bundleFetch.js';
|
|
3
4
|
export { BrowserBundleStore } from './bundleStore.js';
|
|
4
5
|
export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
|
|
5
6
|
export { preparePreset, verifyPreset, verifyPreparedPreset } from './preset.js';
|
|
6
7
|
export { installPageRuntimeHost, buildPageRuntimeRegistrationSource, buildPageRuntimeCallSource } from './pageRuntime.js';
|
|
7
8
|
export { receiveBrowserPreset, readPresetTransferResource, PRESET_TRANSFER_CHUNK_BYTES, MAX_PRESET_TRANSFER_HEADER_BYTES } from './browserTransfer.js';
|
|
8
|
-
export { prepareBrowserBundleSnapshot } from './
|
|
9
|
+
export { prepareBrowserBundleSnapshot } from './capabilitySnapshot.js';
|
|
9
10
|
export { buildBrowserCapabilityPage, buildBrowserPureCapabilityPage } from './browserSandbox.js';
|
|
10
11
|
export { validateBrowserRequestUi, selectBrowserRequestUi, browserUiText, browserRequestUiLink, browserRequestLoginError, validateBrowserRequestDetail } from './requestUi.js';
|
|
11
12
|
export { validateBrowserView, selectBrowserView, buildBrowserViewPage } from './browserViews.js';
|
|
12
13
|
export { browserTransportUrl, browserTransportPolicies } from './browserTransport.js';
|
|
14
|
+
export { validateBrowserControl, selectBrowserControl } from './browserControls.js';
|
|
15
|
+
export { parseNetworkRoutePlan, encodeNetworkRoutePlan, decodeNetworkRoutePlan } from './networkRoutes.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonObject } from './types.js';
|
|
2
|
-
export interface
|
|
2
|
+
export interface CapabilitySelection {
|
|
3
3
|
schemaVersion: 1;
|
|
4
4
|
modules: Record<string, {
|
|
5
5
|
sha256: string;
|
|
@@ -11,28 +11,28 @@ export interface NodeCapabilitySelection {
|
|
|
11
11
|
handler: string;
|
|
12
12
|
}>;
|
|
13
13
|
}
|
|
14
|
-
export interface
|
|
14
|
+
export interface CapabilitySnapshot extends CapabilitySelection {
|
|
15
15
|
revision: string;
|
|
16
16
|
}
|
|
17
|
-
export interface
|
|
17
|
+
export interface CapabilityActivation extends CapabilitySnapshot {
|
|
18
18
|
installationRevision?: string;
|
|
19
19
|
}
|
|
20
|
-
export declare const
|
|
20
|
+
export declare const MAX_CAPABILITY_SELECTION_BYTES = 262144;
|
|
21
21
|
/** Explicit module configuration survives periods where a preset disables its module. */
|
|
22
|
-
export declare function
|
|
22
|
+
export declare function validateCapabilityConfig(value: Record<string, JsonObject>): Record<string, JsonObject>;
|
|
23
23
|
/** Preset defaults are inherited; explicit host configuration stays a separate override. */
|
|
24
|
-
export declare function
|
|
24
|
+
export declare function resolveCapabilityConfig(defaults: JsonObject, overrides?: Record<string, JsonObject>): Record<string, JsonObject>;
|
|
25
25
|
/** Exact content and bindings are selected together; filesystem and code loading belong to hosts. */
|
|
26
|
-
export declare function
|
|
27
|
-
export declare function
|
|
26
|
+
export declare function prepareCapabilitySnapshot(input: CapabilitySelection): Promise<CapabilitySnapshot>;
|
|
27
|
+
export declare function verifyCapabilitySnapshot(input: CapabilitySnapshot): Promise<CapabilitySnapshot>;
|
|
28
28
|
/** The installer receipt and Node selection share one atomic filesystem pointer. */
|
|
29
|
-
export declare function
|
|
29
|
+
export declare function verifyCapabilityActivation(input: CapabilityActivation): Promise<CapabilityActivation>;
|
|
30
30
|
export declare function prepareNodeBundleSnapshot(bundles: import('./bundle.js').PreparedBundle[], host: string, mearlApiVersion: string, config?: Record<string, JsonObject>): Promise<{
|
|
31
|
-
snapshot:
|
|
31
|
+
snapshot: CapabilitySnapshot;
|
|
32
32
|
resources: Record<string, Uint8Array>;
|
|
33
33
|
}>;
|
|
34
34
|
/** Browser factories use the same exact module/configuration selection without a Node loader. */
|
|
35
35
|
export declare function prepareBrowserBundleSnapshot(bundles: import('./bundle.js').PreparedBundle[], host: string, mearlApiVersion: string, config?: Record<string, JsonObject>): Promise<{
|
|
36
|
-
snapshot:
|
|
36
|
+
snapshot: CapabilitySnapshot;
|
|
37
37
|
resources: Record<string, Uint8Array>;
|
|
38
38
|
}>;
|
|
@@ -1,95 +1,95 @@
|
|
|
1
1
|
import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_BYTES, bundleDigest, validateBundleManifest } from './bundle.js';
|
|
2
2
|
import { clonePresetDefaults, isJsonObject } from './manifest.js';
|
|
3
3
|
import { mergePresetDefaults } from './resolve.js';
|
|
4
|
-
export const
|
|
4
|
+
export const MAX_CAPABILITY_SELECTION_BYTES = 262_144;
|
|
5
5
|
const identifier = /^[a-z][a-z0-9-]{0,79}$/;
|
|
6
6
|
const hookName = /^[a-z][a-zA-Z0-9_.-]{0,127}$/;
|
|
7
7
|
const digest = /^[a-f0-9]{64}$/;
|
|
8
8
|
function invalid(message) { throw new BundleError('BUNDLE_INVALID', message); }
|
|
9
9
|
function fields(value, allowed) {
|
|
10
10
|
if (!isJsonObject(value) || Object.keys(value).some(key => !allowed.includes(key)))
|
|
11
|
-
invalid('Invalid
|
|
11
|
+
invalid('Invalid Capability selection structure');
|
|
12
12
|
}
|
|
13
13
|
/** Explicit module configuration survives periods where a preset disables its module. */
|
|
14
|
-
export function
|
|
14
|
+
export function validateCapabilityConfig(value) {
|
|
15
15
|
const configs = clonePresetDefaults(value);
|
|
16
|
-
if (Object.keys(configs).length > 64 || new TextEncoder().encode(JSON.stringify(configs)).byteLength >
|
|
17
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
16
|
+
if (Object.keys(configs).length > 64 || new TextEncoder().encode(JSON.stringify(configs)).byteLength > MAX_CAPABILITY_SELECTION_BYTES)
|
|
17
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability module configuration exceeds its limit');
|
|
18
18
|
for (const [id, config] of Object.entries(configs))
|
|
19
19
|
if (!identifier.test(id) || !isJsonObject(config))
|
|
20
|
-
invalid('
|
|
20
|
+
invalid('Capability module configuration must map IDs to JSON objects');
|
|
21
21
|
return configs;
|
|
22
22
|
}
|
|
23
23
|
/** Preset defaults are inherited; explicit host configuration stays a separate override. */
|
|
24
|
-
export function
|
|
24
|
+
export function resolveCapabilityConfig(defaults, overrides = {}) {
|
|
25
25
|
const value = defaults.capabilities === undefined ? {} : defaults.capabilities;
|
|
26
26
|
if (!isJsonObject(value))
|
|
27
27
|
invalid('Capability defaults must map module IDs to configuration objects');
|
|
28
|
-
return
|
|
28
|
+
return validateCapabilityConfig(mergePresetDefaults(validateCapabilityConfig(value), validateCapabilityConfig(overrides)));
|
|
29
29
|
}
|
|
30
30
|
/** Exact content and bindings are selected together; filesystem and code loading belong to hosts. */
|
|
31
|
-
export async function
|
|
31
|
+
export async function prepareCapabilitySnapshot(input) {
|
|
32
32
|
const value = clonePresetDefaults(input);
|
|
33
33
|
fields(value, ['schemaVersion', 'modules', 'bindings']);
|
|
34
34
|
if (value.schemaVersion !== 1 || !isJsonObject(value.modules) || !isJsonObject(value.bindings))
|
|
35
|
-
invalid('Invalid
|
|
35
|
+
invalid('Invalid Capability selection');
|
|
36
36
|
if (Object.keys(value.modules).length > 64 || Object.keys(value.bindings).length > 256)
|
|
37
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
37
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability selection exceeds the entry limit');
|
|
38
38
|
const modules = {};
|
|
39
39
|
let total = 0;
|
|
40
40
|
for (const id of Object.keys(value.modules).sort()) {
|
|
41
41
|
if (!identifier.test(id))
|
|
42
|
-
invalid('Invalid
|
|
42
|
+
invalid('Invalid Capability module ID');
|
|
43
43
|
const entry = value.modules[id];
|
|
44
44
|
fields(entry, ['sha256', 'byteLength', 'config']);
|
|
45
45
|
if (typeof entry.sha256 !== 'string' || !digest.test(entry.sha256) || typeof entry.byteLength !== 'number' || !Number.isSafeInteger(entry.byteLength) || entry.byteLength <= 0 || entry.byteLength > MAX_BUNDLE_BYTES)
|
|
46
|
-
invalid('Invalid
|
|
46
|
+
invalid('Invalid Capability module resource');
|
|
47
47
|
if (entry.config !== undefined && !isJsonObject(entry.config))
|
|
48
|
-
invalid('
|
|
48
|
+
invalid('Capability module configuration must be a JSON object');
|
|
49
49
|
total += entry.byteLength;
|
|
50
50
|
if (total > MAX_BROWSER_STORAGE_BYTES)
|
|
51
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
51
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability resources exceed 64 MiB');
|
|
52
52
|
modules[id] = { sha256: entry.sha256, byteLength: entry.byteLength, ...(entry.config === undefined ? {} : { config: clonePresetDefaults(entry.config) }) };
|
|
53
53
|
}
|
|
54
54
|
const bindings = {};
|
|
55
55
|
for (const name of Object.keys(value.bindings).sort()) {
|
|
56
56
|
if (!hookName.test(name))
|
|
57
|
-
invalid('Invalid
|
|
57
|
+
invalid('Invalid Capability hook name');
|
|
58
58
|
const binding = value.bindings[name];
|
|
59
59
|
fields(binding, ['module', 'handler']);
|
|
60
60
|
if (typeof binding.module !== 'string' || !Object.hasOwn(modules, binding.module) || typeof binding.handler !== 'string' || !hookName.test(binding.handler))
|
|
61
|
-
invalid('
|
|
61
|
+
invalid('Capability hook must bind a declared module handler');
|
|
62
62
|
bindings[name] = { module: binding.module, handler: binding.handler };
|
|
63
63
|
}
|
|
64
64
|
const selection = { schemaVersion: 1, modules, bindings };
|
|
65
|
-
if (new TextEncoder().encode(JSON.stringify(selection)).byteLength + 80 >
|
|
66
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
65
|
+
if (new TextEncoder().encode(JSON.stringify(selection)).byteLength + 80 > MAX_CAPABILITY_SELECTION_BYTES)
|
|
66
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability selection metadata exceeds 256 KiB');
|
|
67
67
|
return { ...selection, revision: await bundleDigest(new TextEncoder().encode(JSON.stringify(selection))) };
|
|
68
68
|
}
|
|
69
|
-
export async function
|
|
69
|
+
export async function verifyCapabilitySnapshot(input) {
|
|
70
70
|
const value = clonePresetDefaults(input);
|
|
71
71
|
fields(value, ['schemaVersion', 'modules', 'bindings', 'revision']);
|
|
72
72
|
if (typeof value.revision !== 'string' || !digest.test(value.revision))
|
|
73
|
-
invalid('Invalid
|
|
73
|
+
invalid('Invalid Capability revision');
|
|
74
74
|
const { revision, ...selection } = value;
|
|
75
|
-
const prepared = await
|
|
75
|
+
const prepared = await prepareCapabilitySnapshot(selection);
|
|
76
76
|
if (prepared.revision !== revision)
|
|
77
|
-
throw new BundleError('BUNDLE_INTEGRITY', '
|
|
77
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Capability selection revision does not match');
|
|
78
78
|
return prepared;
|
|
79
79
|
}
|
|
80
80
|
/** The installer receipt and Node selection share one atomic filesystem pointer. */
|
|
81
|
-
export async function
|
|
81
|
+
export async function verifyCapabilityActivation(input) {
|
|
82
82
|
const value = clonePresetDefaults(input);
|
|
83
83
|
const { installationRevision, ...selection } = value;
|
|
84
84
|
if (installationRevision !== undefined && (typeof installationRevision !== 'string' || !digest.test(installationRevision)))
|
|
85
85
|
invalid('Invalid capability installation revision');
|
|
86
|
-
const snapshot = await
|
|
86
|
+
const snapshot = await verifyCapabilitySnapshot(selection);
|
|
87
87
|
return { ...snapshot, ...(installationRevision === undefined ? {} : { installationRevision: installationRevision }) };
|
|
88
88
|
}
|
|
89
89
|
async function prepareRuntimeBundleSnapshot(bundles, kind, host, mearlApiVersion, config) {
|
|
90
90
|
if (!identifier.test(host))
|
|
91
|
-
invalid('Invalid
|
|
92
|
-
const configs =
|
|
91
|
+
invalid('Invalid capability runtime host');
|
|
92
|
+
const configs = validateCapabilityConfig(config);
|
|
93
93
|
const modules = {};
|
|
94
94
|
const bindings = {};
|
|
95
95
|
const resources = {};
|
|
@@ -101,20 +101,21 @@ async function prepareRuntimeBundleSnapshot(bundles, kind, host, mearlApiVersion
|
|
|
101
101
|
if (!runtime)
|
|
102
102
|
continue;
|
|
103
103
|
if (Object.hasOwn(modules, manifest.id))
|
|
104
|
-
invalid('Duplicate
|
|
104
|
+
invalid('Duplicate Capability module ID');
|
|
105
105
|
const metadata = manifest.resources[runtime.entry];
|
|
106
106
|
if (!Object.hasOwn(bundle.resources, runtime.entry))
|
|
107
|
-
invalid('
|
|
107
|
+
invalid('Capability module resource is missing');
|
|
108
108
|
total += metadata.byteLength;
|
|
109
109
|
if (pending.length >= 64 || total > MAX_BROWSER_STORAGE_BYTES)
|
|
110
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
110
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability resources exceed the selection limit');
|
|
111
111
|
pending.push({ id: manifest.id, bytes: bundle.resources[runtime.entry], sha256: metadata.sha256 });
|
|
112
112
|
if (pending[pending.length - 1].bytes.byteLength !== metadata.byteLength)
|
|
113
|
-
invalid('
|
|
114
|
-
|
|
113
|
+
invalid('Capability module length does not match');
|
|
114
|
+
const effectiveConfig = mergePresetDefaults(manifest.defaults ?? {}, configs[manifest.id] ?? {});
|
|
115
|
+
modules[manifest.id] = { ...metadata, ...(Object.keys(effectiveConfig).length ? { config: effectiveConfig } : {}) };
|
|
115
116
|
for (const [name, handler] of Object.entries(runtime.bindings)) {
|
|
116
117
|
if (Object.hasOwn(bindings, name))
|
|
117
|
-
invalid('
|
|
118
|
+
invalid('Capability hook has multiple providers');
|
|
118
119
|
bindings[name] = { module: manifest.id, handler };
|
|
119
120
|
}
|
|
120
121
|
}
|
|
@@ -122,10 +123,10 @@ async function prepareRuntimeBundleSnapshot(bundles, kind, host, mearlApiVersion
|
|
|
122
123
|
const captured = pending.map(resource => ({ ...resource, bytes: Uint8Array.from(resource.bytes) }));
|
|
123
124
|
for (const resource of captured) {
|
|
124
125
|
if (await bundleDigest(resource.bytes) !== resource.sha256)
|
|
125
|
-
throw new BundleError('BUNDLE_INTEGRITY', `
|
|
126
|
+
throw new BundleError('BUNDLE_INTEGRITY', `Capability module digest does not match: ${resource.id}`);
|
|
126
127
|
resources[resource.sha256] = resource.bytes;
|
|
127
128
|
}
|
|
128
|
-
return { snapshot: await
|
|
129
|
+
return { snapshot: await prepareCapabilitySnapshot({ schemaVersion: 1, modules, bindings }), resources };
|
|
129
130
|
}
|
|
130
131
|
export function prepareNodeBundleSnapshot(bundles, host, mearlApiVersion, config = {}) {
|
|
131
132
|
return prepareRuntimeBundleSnapshot(bundles, 'nodeRuntimes', host, mearlApiVersion, config);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type PreparedBundle } from './bundle.js';
|
|
2
|
+
import { type BrowserUiText } from './requestUi.js';
|
|
3
|
+
export interface ExtensionContributions {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
operations?: Record<string, {
|
|
6
|
+
hook: string;
|
|
7
|
+
setting?: string;
|
|
8
|
+
}>;
|
|
9
|
+
logins?: Record<string, {
|
|
10
|
+
url: string;
|
|
11
|
+
title: BrowserUiText;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export declare function validateExtensionContributions(input: unknown): ExtensionContributions;
|
|
15
|
+
/** Definitions belong to packages; preset defaults cannot redirect their handlers. */
|
|
16
|
+
export declare function selectExtensionContributions(bundles: PreparedBundle[]): {
|
|
17
|
+
operations: Record<string, {
|
|
18
|
+
hook: string;
|
|
19
|
+
setting?: string;
|
|
20
|
+
}>;
|
|
21
|
+
logins: Record<string, {
|
|
22
|
+
url: string;
|
|
23
|
+
title: BrowserUiText;
|
|
24
|
+
}>;
|
|
25
|
+
owners: Record<string, {
|
|
26
|
+
bundleId: string;
|
|
27
|
+
revision: string;
|
|
28
|
+
}>;
|
|
29
|
+
};
|