@mearl/preset 3.0.0-beta.1 → 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.
@@ -11,8 +11,8 @@ function installationMetadata(value) {
11
11
  }
12
12
  const STORE = 'bundles';
13
13
  const PRESET = 'preset';
14
- const RUNTIMES = 'pageRuntimes';
15
- const PINS = 'pageRuntimePins';
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 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'))];
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 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));
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 runtimeResources(bundles) {
51
+ function bundleObjects(bundles) {
45
52
  const result = new Map();
46
53
  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
- }
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 = records.reduce((sum, record) => sum + record.byteLength, retained);
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-capability-bundles', 4);
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(RUNTIMES))
81
- request.result.createObjectStore(RUNTIMES, { keyPath: 'sha256' });
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 bundle = await verifyBundle(record.bundle, this.options.mearlApiVersion);
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(RUNTIMES, 'readonly');
195
- const request = transaction.objectStore(RUNTIMES).get(sha256);
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, RUNTIMES, PINS], 'readwrite');
232
- const runtimeStore = transaction.objectStore(RUNTIMES);
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(referencedRuntimes(records.result, available, [...pins.values()]).map(runtime => runtime.sha256));
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, RUNTIMES, PINS], 'readwrite');
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(RUNTIMES);
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({ 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));
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 = referencedRuntimes(result, [...cache.values()], pins.result);
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
- 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);
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, RUNTIMES, PINS], 'readwrite');
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(RUNTIMES);
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 = referencedRuntimes([...others, next], [...cache.values()], pins.result);
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(referencedRuntimes(records.filter(record => record.id !== id), retained.result, pins.result).map(runtime => runtime.sha256));
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 './nodeSnapshot.js';
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 './nodeSnapshot.js';
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 NodeCapabilitySelection {
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 NodeCapabilitySnapshot extends NodeCapabilitySelection {
14
+ export interface CapabilitySnapshot extends CapabilitySelection {
15
15
  revision: string;
16
16
  }
17
- export interface NodeCapabilityActivation extends NodeCapabilitySnapshot {
17
+ export interface CapabilityActivation extends CapabilitySnapshot {
18
18
  installationRevision?: string;
19
19
  }
20
- export declare const MAX_NODE_SELECTION_BYTES = 262144;
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 validateNodeModuleConfig(value: Record<string, JsonObject>): Record<string, JsonObject>;
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 resolvePresetModuleConfig(defaults: JsonObject, overrides?: Record<string, JsonObject>): Record<string, JsonObject>;
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 prepareNodeCapabilitySnapshot(input: NodeCapabilitySelection): Promise<NodeCapabilitySnapshot>;
27
- export declare function verifyNodeCapabilitySnapshot(input: NodeCapabilitySnapshot): Promise<NodeCapabilitySnapshot>;
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 verifyNodeCapabilityActivation(input: NodeCapabilityActivation): Promise<NodeCapabilityActivation>;
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: NodeCapabilitySnapshot;
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: NodeCapabilitySnapshot;
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 MAX_NODE_SELECTION_BYTES = 262_144;
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 Node capability selection structure');
11
+ invalid('Invalid Capability selection structure');
12
12
  }
13
13
  /** Explicit module configuration survives periods where a preset disables its module. */
14
- export function validateNodeModuleConfig(value) {
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 > MAX_NODE_SELECTION_BYTES)
17
- throw new BundleError('BUNDLE_QUOTA', 'Node module configuration exceeds its limit');
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('Node module configuration must map IDs to JSON objects');
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 resolvePresetModuleConfig(defaults, overrides = {}) {
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 validateNodeModuleConfig(mergePresetDefaults(validateNodeModuleConfig(value), validateNodeModuleConfig(overrides)));
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 prepareNodeCapabilitySnapshot(input) {
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 Node capability selection');
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', 'Node capability selection exceeds the entry limit');
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 Node module ID');
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 Node module resource');
46
+ invalid('Invalid Capability module resource');
47
47
  if (entry.config !== undefined && !isJsonObject(entry.config))
48
- invalid('Node module configuration must be a JSON object');
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', 'Node capability resources exceed 64 MiB');
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 Node hook name');
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('Node hook must bind a declared module handler');
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 > MAX_NODE_SELECTION_BYTES)
66
- throw new BundleError('BUNDLE_QUOTA', 'Node capability selection metadata exceeds 256 KiB');
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 verifyNodeCapabilitySnapshot(input) {
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 Node capability revision');
73
+ invalid('Invalid Capability revision');
74
74
  const { revision, ...selection } = value;
75
- const prepared = await prepareNodeCapabilitySnapshot(selection);
75
+ const prepared = await prepareCapabilitySnapshot(selection);
76
76
  if (prepared.revision !== revision)
77
- throw new BundleError('BUNDLE_INTEGRITY', 'Node capability selection revision does not match');
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 verifyNodeCapabilityActivation(input) {
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 verifyNodeCapabilitySnapshot(selection);
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 Node runtime host');
92
- const configs = validateNodeModuleConfig(config);
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 Node module ID');
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('Node module resource is missing');
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', 'Node capability resources exceed the selection limit');
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('Node module length does not match');
114
- modules[manifest.id] = { ...metadata, ...(configs[manifest.id] ? { config: clonePresetDefaults(configs[manifest.id]) } : {}) };
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('Node hook has multiple providers');
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', `Node module digest does not match: ${resource.id}`);
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 prepareNodeCapabilitySnapshot({ schemaVersion: 1, modules, bindings }), resources };
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
+ };