@mearl/preset 0.0.0-stage → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,478 @@
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
+ import { mergePresetDefaults } from './resolve.js';
4
+ function installationMetadata(value) {
5
+ if (!value || Object.keys(value).some(key => !['revision', 'browserRevision', 'overrides'].includes(key)) || !/^[a-f0-9]{64}$/.test(value.revision) || !/^[a-f0-9]{64}$/.test(value.browserRevision))
6
+ throw new BundleError('BUNDLE_INVALID', 'Invalid browser installation identity');
7
+ const overrides = mergePresetDefaults({}, value.overrides);
8
+ if (new TextEncoder().encode(JSON.stringify(overrides)).byteLength > 262144)
9
+ throw new BundleError('BUNDLE_QUOTA', 'Browser user overrides exceed 256 KiB');
10
+ return { revision: value.revision, browserRevision: value.browserRevision, overrides };
11
+ }
12
+ const STORE = 'bundles';
13
+ const PRESET = 'preset';
14
+ const RUNTIMES = 'pageRuntimes';
15
+ const PINS = 'pageRuntimePins';
16
+ function captureRevisions(input) {
17
+ if (input === undefined)
18
+ return;
19
+ const entries = Object.entries(input);
20
+ if (entries.length > 64 || entries.some(([id, revision]) => !/^[a-z][a-z0-9-]{0,79}$/.test(id) || typeof revision !== 'string'))
21
+ throw new BundleError('BUNDLE_INVALID', 'Invalid expected browser bundle selection');
22
+ return Object.fromEntries(entries);
23
+ }
24
+ function checkRevisions(records, expected) {
25
+ if (expected && (records.length !== Object.keys(expected).length || records.some(record => !Object.hasOwn(expected, record.id) || expected[record.id] !== record.revision)))
26
+ throw new BundleError('BUNDLE_CONFLICT', 'Browser bundles changed while contributions were being validated');
27
+ }
28
+ function runtimePaths(bundle) {
29
+ return [...new Set([
30
+ ...Object.values(bundle?.manifest?.contributions?.pageRuntimes ?? {}),
31
+ ...Object.values(bundle?.manifest?.contributions?.browserRuntimes ?? {}).map(runtime => runtime?.entry),
32
+ ].filter((name) => typeof name === 'string'))];
33
+ }
34
+ function referencedRuntimes(records, runtimes, pins) {
35
+ const referenced = new Set(pins.map(pin => pin.sha256));
36
+ for (const record of records)
37
+ for (const name of runtimePaths(record.bundle)) {
38
+ const digest = record.bundle?.manifest?.resources?.[name]?.sha256;
39
+ if (digest)
40
+ referenced.add(digest);
41
+ }
42
+ return runtimes.filter(runtime => referenced.has(runtime.sha256));
43
+ }
44
+ function runtimeResources(bundles) {
45
+ const result = new Map();
46
+ for (const bundle of bundles)
47
+ for (const name of runtimePaths(bundle)) {
48
+ const resource = bundle?.manifest?.resources?.[name], bytes = bundle?.resources?.[name];
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
+ }
52
+ return [...result.values()];
53
+ }
54
+ function checkStorageBudget(records, runtimes) {
55
+ if (records.some(record => !Number.isSafeInteger(record.byteLength) || record.byteLength < 0 || record.byteLength > MAX_BUNDLE_BYTES) ||
56
+ runtimes.some(runtime => !(runtime.bytes instanceof Uint8Array) || runtime.bytes.byteLength > MAX_BUNDLE_BYTES)) {
57
+ throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource size is invalid');
58
+ }
59
+ const retained = runtimes.reduce((sum, runtime) => sum + runtime.bytes.byteLength, 0);
60
+ const total = records.reduce((sum, record) => sum + record.byteLength, retained);
61
+ if (records.length > 64 || !Number.isSafeInteger(total) || total > MAX_BROWSER_STORAGE_BYTES)
62
+ throw new BundleError('BUNDLE_QUOTA', 'Installed and retained resources exceed the browser storage budget');
63
+ }
64
+ export class BrowserBundleStore {
65
+ options;
66
+ database;
67
+ constructor(options) {
68
+ this.options = options;
69
+ }
70
+ open() {
71
+ if (this.database)
72
+ return this.database;
73
+ this.database = new Promise((resolve, reject) => {
74
+ const request = (this.options.indexedDB ?? indexedDB).open(this.options.databaseName ?? 'mearl-capability-bundles', 4);
75
+ request.onupgradeneeded = () => {
76
+ if (!request.result.objectStoreNames.contains(STORE))
77
+ request.result.createObjectStore(STORE, { keyPath: 'id' });
78
+ if (!request.result.objectStoreNames.contains(PRESET))
79
+ request.result.createObjectStore(PRESET);
80
+ if (!request.result.objectStoreNames.contains(RUNTIMES))
81
+ request.result.createObjectStore(RUNTIMES, { keyPath: 'sha256' });
82
+ if (!request.result.objectStoreNames.contains(PINS))
83
+ request.result.createObjectStore(PINS, { keyPath: 'owner' });
84
+ };
85
+ request.onerror = () => reject(request.error);
86
+ request.onsuccess = () => {
87
+ const database = request.result;
88
+ database.onversionchange = () => { database.close(); this.database = undefined; };
89
+ resolve(database);
90
+ };
91
+ }).catch(error => { this.database = undefined; throw error; });
92
+ return this.database;
93
+ }
94
+ async close() {
95
+ const current = this.database;
96
+ this.database = undefined;
97
+ if (current)
98
+ (await current).close();
99
+ }
100
+ async records() {
101
+ const database = await this.open();
102
+ return new Promise((resolve, reject) => {
103
+ const transaction = database.transaction([STORE, PRESET], 'readonly');
104
+ const request = transaction.objectStore(STORE).getAll();
105
+ const active = transaction.objectStore(PRESET).get('active');
106
+ const installation = transaction.objectStore(PRESET).get('installation');
107
+ const token = transaction.objectStore(PRESET).get('installationToken');
108
+ transaction.oncomplete = () => resolve({ records: request.result, active: active.result, installation: installation.result, installationToken: token.result ?? null });
109
+ transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Bundle read failed'));
110
+ });
111
+ }
112
+ async get(id) {
113
+ const database = await this.open();
114
+ const record = await new Promise((resolve, reject) => {
115
+ const transaction = database.transaction(STORE, 'readonly');
116
+ const request = transaction.objectStore(STORE).get(id);
117
+ transaction.oncomplete = () => resolve(request.result);
118
+ transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Bundle read failed'));
119
+ });
120
+ return record ? this.verified(record) : undefined;
121
+ }
122
+ async verified(record) {
123
+ if (record.id !== record.bundle?.manifest?.id || record.revision !== record.bundle?.revision) {
124
+ throw new BundleError('BUNDLE_INTEGRITY', 'Stored bundle identity does not match');
125
+ }
126
+ const bundle = await verifyBundle(record.bundle, this.options.mearlApiVersion);
127
+ if (Object.values(bundle.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0) !== record.byteLength) {
128
+ throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource size does not match');
129
+ }
130
+ return bundle;
131
+ }
132
+ async list() {
133
+ const bundles = [];
134
+ const errors = [];
135
+ const { records, active, installation: storedInstallation, installationToken } = await this.records();
136
+ for (const record of records) {
137
+ try {
138
+ bundles.push(await this.verified(record));
139
+ }
140
+ catch (error) {
141
+ errors.push({ id: record.id, error: error instanceof Error ? error.message : 'Invalid stored bundle' });
142
+ }
143
+ }
144
+ let activation;
145
+ let activationError;
146
+ let installation;
147
+ let installationError;
148
+ if (active) {
149
+ try {
150
+ activation = await verifyBrowserPreset(active, this.options.mearlApiVersion);
151
+ }
152
+ catch (error) {
153
+ activation = undefined;
154
+ activationError = error instanceof Error ? error.message : 'Invalid active preset';
155
+ }
156
+ if (activation) {
157
+ for (const [id, revision] of Object.entries(activation.snapshot.bundleRevisions)) {
158
+ if (!bundles.some(bundle => bundle.manifest.id === id && bundle.revision === revision) && !errors.some(error => error.id === id)) {
159
+ errors.push({ id, error: 'BUNDLE_INTEGRITY: An active preset package is missing or corrupt' });
160
+ }
161
+ }
162
+ }
163
+ }
164
+ if (storedInstallation !== undefined) {
165
+ try {
166
+ installation = installationMetadata(storedInstallation);
167
+ if (!activation || installation.browserRevision !== activation.snapshot.revision)
168
+ throw new BundleError('BUNDLE_INTEGRITY', 'Browser installation does not match its active preset');
169
+ }
170
+ catch (error) {
171
+ installation = undefined;
172
+ installationError = error instanceof Error ? error.message : 'Invalid browser installation';
173
+ }
174
+ }
175
+ return { bundles, errors, activation, activationError, installation, installationError, installationToken, activeRevision: active?.revision ?? null,
176
+ revisions: Object.fromEntries(records.map(record => [record.id, record.revision])) };
177
+ }
178
+ async activePreset() {
179
+ const database = await this.open();
180
+ const active = await new Promise((resolve, reject) => {
181
+ const transaction = database.transaction(PRESET, 'readonly');
182
+ const request = transaction.objectStore(PRESET).get('active');
183
+ transaction.oncomplete = () => resolve(request.result);
184
+ transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Preset read failed'));
185
+ });
186
+ return active ? verifyBrowserPreset(active, this.options.mearlApiVersion) : undefined;
187
+ }
188
+ /** Verified browser modules remain addressable while a bound document retains them. */
189
+ async browserRuntimeBytes(sha256) {
190
+ if (!/^[a-f0-9]{64}$/.test(sha256))
191
+ throw new BundleError('BUNDLE_INVALID', 'Invalid browser runtime digest');
192
+ const database = await this.open();
193
+ const record = await new Promise((resolve, reject) => {
194
+ const transaction = database.transaction(RUNTIMES, 'readonly');
195
+ const request = transaction.objectStore(RUNTIMES).get(sha256);
196
+ transaction.oncomplete = () => resolve(request.result);
197
+ transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Browser runtime read failed'));
198
+ });
199
+ if (!(record?.bytes instanceof Uint8Array) || record.sha256 !== sha256 || record.bytes.byteLength > MAX_BUNDLE_BYTES || await bundleDigest(record.bytes) !== sha256) {
200
+ throw new BundleError('BUNDLE_INTEGRITY', 'Pinned browser runtime is missing or corrupt');
201
+ }
202
+ return Uint8Array.from(record.bytes);
203
+ }
204
+ /** Content-addressed code survives package updates so pinned documents can still navigate. */
205
+ async pageRuntimeSource(sha256) {
206
+ const bytes = await this.browserRuntimeBytes(sha256);
207
+ if (bytes.byteLength > MAX_PAGE_RUNTIME_BYTES)
208
+ throw new BundleError('BUNDLE_INTEGRITY', 'Page runtime exceeds its size limit');
209
+ try {
210
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
211
+ }
212
+ catch {
213
+ throw new BundleError('BUNDLE_INTEGRITY', 'Page runtime is not UTF-8');
214
+ }
215
+ }
216
+ async pinPageRuntimes(patch) {
217
+ await this.updateRuntimePins(patch, false);
218
+ }
219
+ /** Hosts rebuild these references from their authoritative session state after restart. */
220
+ async synchronizePageRuntimePins(pins) {
221
+ await this.updateRuntimePins(pins, true);
222
+ }
223
+ async updateRuntimePins(input, replace) {
224
+ const patch = Object.entries(input).map(([owner, sha256]) => {
225
+ if (!/^[a-zA-Z0-9:._-]{1,200}$/.test(owner) || sha256 !== null && !/^[a-f0-9]{64}$/.test(sha256))
226
+ throw new BundleError('BUNDLE_INVALID', 'Invalid page runtime reference');
227
+ return { owner, sha256 };
228
+ });
229
+ const database = await this.open();
230
+ await new Promise((resolve, reject) => {
231
+ const transaction = database.transaction([STORE, RUNTIMES, PINS], 'readwrite');
232
+ const runtimeStore = transaction.objectStore(RUNTIMES);
233
+ const pinStore = transaction.objectStore(PINS);
234
+ const records = transaction.objectStore(STORE).getAll();
235
+ const resources = runtimeStore.getAll();
236
+ const request = pinStore.getAll();
237
+ let failure;
238
+ request.onsuccess = () => {
239
+ 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
+ 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
+ const pins = new Map(replace ? [] : request.result.map((pin) => [pin.owner, pin]));
261
+ for (const { owner, sha256 } of patch) {
262
+ if (sha256 === null) {
263
+ pins.delete(owner);
264
+ continue;
265
+ }
266
+ if (!replace && !cached.has(sha256)) {
267
+ failure = new BundleError('BUNDLE_INTEGRITY', 'Cannot retain a missing page runtime');
268
+ transaction.abort();
269
+ return;
270
+ }
271
+ pins.set(owner, { owner, sha256 });
272
+ }
273
+ pinStore.clear();
274
+ for (const pin of pins.values())
275
+ pinStore.put(pin);
276
+ const retained = new Set(referencedRuntimes(records.result, available, [...pins.values()]).map(runtime => runtime.sha256));
277
+ for (const runtime of available)
278
+ if (!retained.has(runtime.sha256))
279
+ runtimeStore.delete(runtime.sha256);
280
+ };
281
+ transaction.oncomplete = () => resolve();
282
+ transaction.onabort = transaction.onerror = () => reject(failure ?? transaction.error ?? new Error('Page runtime reference update failed'));
283
+ });
284
+ }
285
+ /** Pins the verified selection while reading its package from the same IndexedDB snapshot. */
286
+ async profileBinding(name) {
287
+ const activation = await this.activePreset();
288
+ const target = activation?.preset.appProfiles[name];
289
+ if (!activation || !target)
290
+ return undefined;
291
+ const id = target.split('/')[0];
292
+ const database = await this.open();
293
+ const record = await new Promise((resolve, reject) => {
294
+ const transaction = database.transaction([PRESET, STORE], 'readonly');
295
+ const current = transaction.objectStore(PRESET).get('active');
296
+ const bundle = transaction.objectStore(STORE).get(id);
297
+ transaction.oncomplete = () => {
298
+ if (current.result?.revision !== activation.snapshot.revision) {
299
+ reject(new BundleError('BUNDLE_CONFLICT', 'The selected preset changed during profile lookup'));
300
+ }
301
+ else {
302
+ resolve(bundle.result);
303
+ }
304
+ };
305
+ transaction.onabort = transaction.onerror = () => reject(transaction.error ?? new Error('Preset binding read failed'));
306
+ });
307
+ if (!record || record.revision !== activation.snapshot.bundleRevisions[id])
308
+ throw new BundleError('BUNDLE_INTEGRITY', 'The selected app provider is missing');
309
+ return { target, bundle: await this.verified(record) };
310
+ }
311
+ /** Packages and the exact preset graph become visible together, after host contribution checks. */
312
+ async activatePreset(input, replacements, expectedRevision, association, expectedBundleRevisions) {
313
+ const selection = captureRevisions(expectedBundleRevisions);
314
+ const accepted = association?.installation && { installation: installationMetadata(association.installation), expected: association.expectedInstallationToken };
315
+ if (association && association.expectedInstallationToken !== null && !/^[a-f0-9]{64}$/.test(association.expectedInstallationToken) || accepted && accepted.installation.browserRevision !== input.revision)
316
+ throw new BundleError('BUNDLE_INVALID', 'Invalid browser installation selection');
317
+ if (replacements.length > 64 || replacements.reduce((total, item) => total + Object.values(item.bundle.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0), 0) > MAX_BROWSER_STORAGE_BYTES) {
318
+ throw new BundleError('BUNDLE_QUOTA', 'Preset packages exceed the browser storage budget');
319
+ }
320
+ const expected = replacements.map(item => item.expectedRevision);
321
+ const { snapshot, bundles } = await verifyPreparedBrowserPreset({ snapshot: input, bundles: replacements.map(item => item.bundle) }, this.options.mearlApiVersion);
322
+ const next = bundles.map((bundle, index) => ({ bundle, expectedRevision: expected[index] }));
323
+ const token = accepted ? await bundleDigest(crypto.getRandomValues(new Uint8Array(32))) : null;
324
+ const database = await this.open();
325
+ await new Promise((resolve, reject) => {
326
+ const transaction = database.transaction([STORE, PRESET, RUNTIMES, PINS], 'readwrite');
327
+ const store = transaction.objectStore(STORE);
328
+ const presets = transaction.objectStore(PRESET);
329
+ const records = store.getAll();
330
+ const runtimeStore = transaction.objectStore(RUNTIMES);
331
+ const retained = runtimeStore.getAll();
332
+ const pins = transaction.objectStore(PINS).getAll();
333
+ const installation = presets.get('installationToken');
334
+ const active = presets.get('active');
335
+ let failure;
336
+ active.onsuccess = () => {
337
+ try {
338
+ checkRevisions(records.result, selection);
339
+ }
340
+ catch (error) {
341
+ failure = error;
342
+ transaction.abort();
343
+ return;
344
+ }
345
+ if ((active.result?.revision ?? null) !== expectedRevision || association && (installation.result ?? null) !== association.expectedInstallationToken || next.some(item => (records.result.find(record => record.id === item.bundle.manifest.id)?.revision ?? null) !== item.expectedRevision)) {
346
+ failure = new BundleError('BUNDLE_CONFLICT', 'Preset or packages changed while activation was being prepared');
347
+ transaction.abort();
348
+ return;
349
+ }
350
+ const replacing = new Set(next.map(item => item.bundle.manifest.id));
351
+ const previousManaged = new Set(Object.keys(active.result?.bundleRevisions ?? {}));
352
+ if (accepted && next.some(item => !previousManaged.has(item.bundle.manifest.id) && records.result.some(record => record.id === item.bundle.manifest.id))) {
353
+ failure = new BundleError('BUNDLE_CONFLICT', 'A preset package conflicts with an independently installed browser package');
354
+ transaction.abort();
355
+ return;
356
+ }
357
+ const result = records.result.filter(record => !replacing.has(record.id) && !previousManaged.has(record.id));
358
+ for (const { bundle } of next)
359
+ result.push({ id: bundle.manifest.id, revision: bundle.revision, byteLength: Object.values(bundle.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0), bundle });
360
+ const resources = runtimeResources(next.map(item => item.bundle));
361
+ const cache = new Map(retained.result.map(runtime => [runtime.sha256, runtime]));
362
+ for (const runtime of resources)
363
+ cache.set(runtime.sha256, runtime);
364
+ const keeping = referencedRuntimes(result, [...cache.values()], pins.result);
365
+ try {
366
+ checkStorageBudget(result, keeping);
367
+ }
368
+ catch (error) {
369
+ failure = error;
370
+ transaction.abort();
371
+ return;
372
+ }
373
+ for (const record of result)
374
+ if (replacing.has(record.id))
375
+ store.put(record);
376
+ for (const id of previousManaged)
377
+ if (!replacing.has(id))
378
+ store.delete(id);
379
+ for (const runtime of resources)
380
+ runtimeStore.put(runtime);
381
+ const keep = new Set(keeping.map(runtime => runtime.sha256));
382
+ for (const runtime of retained.result)
383
+ if (!keep.has(runtime.sha256))
384
+ runtimeStore.delete(runtime.sha256);
385
+ presets.put(snapshot, 'active');
386
+ if (accepted) {
387
+ presets.put(accepted.installation, 'installation');
388
+ presets.put(token, 'installationToken');
389
+ }
390
+ else {
391
+ presets.delete('installation');
392
+ presets.delete('installationToken');
393
+ }
394
+ };
395
+ transaction.oncomplete = () => resolve();
396
+ transaction.onabort = transaction.onerror = () => reject(failure ?? transaction.error ?? new Error('Preset activation failed'));
397
+ });
398
+ }
399
+ /** Verifies all resources before a compare-and-swap transaction replaces the active snapshot. */
400
+ async install(bundle, expectedRevision, expectedBundleRevisions) {
401
+ const selection = captureRevisions(expectedBundleRevisions);
402
+ const verified = await verifyBundle(bundle, this.options.mearlApiVersion);
403
+ const byteLength = Object.values(verified.resources).reduce((sum, bytes) => sum + bytes.byteLength, 0);
404
+ await this.change(verified.manifest.id, expectedRevision, {
405
+ id: verified.manifest.id, revision: verified.revision, byteLength, bundle: verified,
406
+ }, selection);
407
+ }
408
+ async remove(id, expectedRevision) {
409
+ await this.change(id, expectedRevision);
410
+ }
411
+ async change(id, expectedRevision, next, selection) {
412
+ const database = await this.open();
413
+ await new Promise((resolve, reject) => {
414
+ const transaction = database.transaction([STORE, PRESET, RUNTIMES, PINS], 'readwrite');
415
+ const store = transaction.objectStore(STORE);
416
+ const active = transaction.objectStore(PRESET).get('active');
417
+ const runtimeStore = transaction.objectStore(RUNTIMES);
418
+ const retained = runtimeStore.getAll();
419
+ const pins = transaction.objectStore(PINS).getAll();
420
+ let failure;
421
+ const request = store.getAll();
422
+ request.onsuccess = () => {
423
+ const records = request.result;
424
+ try {
425
+ checkRevisions(records, selection);
426
+ }
427
+ catch (error) {
428
+ failure = error;
429
+ transaction.abort();
430
+ return;
431
+ }
432
+ if (Object.hasOwn(active.result?.bundleRevisions ?? {}, id)) {
433
+ failure = new BundleError('BUNDLE_CONFLICT', 'The package is managed by the active preset');
434
+ transaction.abort();
435
+ return;
436
+ }
437
+ const current = records.find(record => record.id === id);
438
+ if ((current?.revision ?? null) !== expectedRevision) {
439
+ failure = new BundleError('BUNDLE_CONFLICT', 'Bundle changed while the operation was being prepared');
440
+ transaction.abort();
441
+ return;
442
+ }
443
+ if (next) {
444
+ const others = records.filter(record => record.id !== id);
445
+ const resources = runtimeResources([next.bundle]);
446
+ const cache = new Map(retained.result.map(runtime => [runtime.sha256, runtime]));
447
+ for (const runtime of resources)
448
+ cache.set(runtime.sha256, runtime);
449
+ const keeping = referencedRuntimes([...others, next], [...cache.values()], pins.result);
450
+ try {
451
+ checkStorageBudget([...others, next], keeping);
452
+ }
453
+ catch (error) {
454
+ failure = error;
455
+ transaction.abort();
456
+ return;
457
+ }
458
+ store.put(next);
459
+ for (const runtime of resources)
460
+ runtimeStore.put(runtime);
461
+ const keep = new Set(keeping.map(runtime => runtime.sha256));
462
+ for (const runtime of retained.result)
463
+ if (!keep.has(runtime.sha256))
464
+ runtimeStore.delete(runtime.sha256);
465
+ }
466
+ else {
467
+ store.delete(id);
468
+ const keep = new Set(referencedRuntimes(records.filter(record => record.id !== id), retained.result, pins.result).map(runtime => runtime.sha256));
469
+ for (const runtime of retained.result)
470
+ if (!keep.has(runtime.sha256))
471
+ runtimeStore.delete(runtime.sha256);
472
+ }
473
+ };
474
+ transaction.oncomplete = () => resolve();
475
+ transaction.onabort = transaction.onerror = () => reject(failure ?? transaction.error ?? new Error('Bundle transaction failed'));
476
+ });
477
+ }
478
+ }
@@ -0,0 +1,22 @@
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 type { BundleManifest, PreparedBundle, BundleLoader, BundleLoadOptions, PrepareBundleOptions, CapabilityRuntimeDeclaration } from './bundle.js';
3
+ export { createBundleFetchLoader } from './bundleFetch.js';
4
+ export { BrowserBundleStore } from './bundleStore.js';
5
+ export type { BrowserPresetInstallation } from './bundleStore.js';
6
+ export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
7
+ export type { BrowserPresetSnapshot, PreparedBrowserPreset } from './browserPreset.js';
8
+ export { preparePreset, verifyPreset, verifyPreparedPreset } from './preset.js';
9
+ export type { PresetSnapshot, PreparedPreset } from './preset.js';
10
+ export type { PreparePresetOptions } from './prepare.js';
11
+ export { installPageRuntimeHost, buildPageRuntimeRegistrationSource, buildPageRuntimeCallSource } from './pageRuntime.js';
12
+ export type { PageRuntimeBinding, PageRuntimeConfig } from './pageRuntime.js';
13
+ export { receiveBrowserPreset, readPresetTransferResource, PRESET_TRANSFER_CHUNK_BYTES, MAX_PRESET_TRANSFER_HEADER_BYTES } from './browserTransfer.js';
14
+ export type { BrowserPresetTransferHeader, PresetTransferRequest, PresetTransferTransport } from './browserTransfer.js';
15
+ export { prepareBrowserBundleSnapshot } from './nodeSnapshot.js';
16
+ export { buildBrowserCapabilityPage, buildBrowserPureCapabilityPage } from './browserSandbox.js';
17
+ export type { BrowserCapabilityHost, BrowserCapabilityInvocation, BrowserCapabilityRequest, BrowserCapabilityResponse } from './browserSandbox.js';
18
+ export { validateBrowserRequestUi, selectBrowserRequestUi, browserUiText, browserRequestUiLink, browserRequestLoginError, validateBrowserRequestDetail } from './requestUi.js';
19
+ export type { BrowserUiText, BrowserRequestUi, BrowserRequestDetail, SelectedBrowserRequestUi } from './requestUi.js';
20
+ export { validateBrowserView, selectBrowserView, buildBrowserViewPage } from './browserViews.js';
21
+ export type { BrowserViewSlot, BrowserViewDefinition, BrowserViewAction, BrowserViewTransport, SelectedBrowserView, BrowserViewBootstrap } from './browserViews.js';
22
+ export { browserTransportUrl, browserTransportPolicies } from './browserTransport.js';
@@ -0,0 +1,12 @@
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 { createBundleFetchLoader } from './bundleFetch.js';
3
+ export { BrowserBundleStore } from './bundleStore.js';
4
+ export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
5
+ export { preparePreset, verifyPreset, verifyPreparedPreset } from './preset.js';
6
+ export { installPageRuntimeHost, buildPageRuntimeRegistrationSource, buildPageRuntimeCallSource } from './pageRuntime.js';
7
+ export { receiveBrowserPreset, readPresetTransferResource, PRESET_TRANSFER_CHUNK_BYTES, MAX_PRESET_TRANSFER_HEADER_BYTES } from './browserTransfer.js';
8
+ export { prepareBrowserBundleSnapshot } from './nodeSnapshot.js';
9
+ export { buildBrowserCapabilityPage, buildBrowserPureCapabilityPage } from './browserSandbox.js';
10
+ export { validateBrowserRequestUi, selectBrowserRequestUi, browserUiText, browserRequestUiLink, browserRequestLoginError, validateBrowserRequestDetail } from './requestUi.js';
11
+ export { validateBrowserView, selectBrowserView, buildBrowserViewPage } from './browserViews.js';
12
+ export { browserTransportUrl, browserTransportPolicies } from './browserTransport.js';
@@ -0,0 +1,5 @@
1
+ export { PresetError, validatePreset, clonePresetDefaults } from './manifest.js';
2
+ export { mergePresetDefaults, resolvePreset } from './resolve.js';
3
+ export type * from './types.js';
4
+ export { prepareNodeBundleSnapshot, prepareNodeCapabilitySnapshot, verifyNodeCapabilitySnapshot, verifyNodeCapabilityActivation, validateNodeModuleConfig, resolvePresetModuleConfig, MAX_NODE_SELECTION_BYTES } from './nodeSnapshot.js';
5
+ export type { NodeCapabilitySelection, NodeCapabilitySnapshot, NodeCapabilityActivation } from './nodeSnapshot.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { PresetError, validatePreset, clonePresetDefaults } from './manifest.js';
2
+ export { mergePresetDefaults, resolvePreset } from './resolve.js';
3
+ export { prepareNodeBundleSnapshot, prepareNodeCapabilitySnapshot, verifyNodeCapabilitySnapshot, verifyNodeCapabilityActivation, validateNodeModuleConfig, resolvePresetModuleConfig, MAX_NODE_SELECTION_BYTES } from './nodeSnapshot.js';
@@ -0,0 +1,24 @@
1
+ export interface InstallationGuide {
2
+ url: string;
3
+ registry: string;
4
+ }
5
+ export interface InstallationPreset {
6
+ label: string;
7
+ source: string;
8
+ origins: string[];
9
+ }
10
+ export interface InstallationConfig {
11
+ schemaVersion: 1;
12
+ guide?: InstallationGuide;
13
+ presets?: Record<string, InstallationPreset>;
14
+ legacyPreset?: string;
15
+ }
16
+ /** One validated catalog is used by the website and extension; parameters cannot add install sources. */
17
+ export declare function parseInstallationConfig(value: unknown, publicOrigin: string): InstallationConfig;
18
+ /** Explicit host scopes retain their selected schemes; download sources remain HTTPS. */
19
+ export declare function validateInstallationOrigin(value: unknown): string;
20
+ /** Match both scheme and host against a catalog scope before loading a resource. */
21
+ export declare function matchesInstallationOrigin(scope: string, target: URL): boolean;
22
+ export declare const INSTALLATION_CONFIG_MAX_BYTES = 65536;
23
+ export declare function readInstallationJson(response: Response, maxBytes: number): Promise<unknown>;
24
+ export declare function loadInstallationConfig(url: string | URL, publicOrigin: string, signal?: AbortSignal): Promise<InstallationConfig>;