@mearl/preset 3.0.0-beta.5 → 3.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
+ };
@@ -0,0 +1,57 @@
1
+ import { BundleError, readBundleJson } from './bundle.js';
2
+ import { clonePresetDefaults, isJsonObject } from './manifest.js';
3
+ import { browserUiHttpUrl, validateBrowserUiText } from './requestUi.js';
4
+ export function validateExtensionContributions(input) {
5
+ const value = clonePresetDefaults(input);
6
+ if (value.schemaVersion !== 1 || Object.keys(value).some(key => !['schemaVersion', 'operations', 'logins'].includes(key)))
7
+ throw new BundleError('BUNDLE_INVALID', 'Invalid extension contributions');
8
+ for (const kind of ['operations', 'logins']) {
9
+ const entries = value[kind];
10
+ if (entries === undefined)
11
+ continue;
12
+ if (!isJsonObject(entries) || Object.keys(entries).length > 64)
13
+ throw new BundleError('BUNDLE_QUOTA', 'Extension contributions exceed 64 entries');
14
+ for (const [name, entry] of Object.entries(entries)) {
15
+ if (!/^[a-z][a-zA-Z0-9_.-]{0,95}$/.test(name) || !isJsonObject(entry))
16
+ throw new BundleError('BUNDLE_INVALID', 'Invalid extension contribution name');
17
+ if (kind === 'operations') {
18
+ if (Object.keys(entry).some(key => !['hook', 'setting'].includes(key)) || typeof entry.hook !== 'string' || !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(entry.hook) || entry.setting !== undefined && (typeof entry.setting !== 'string' || !/^[a-z][a-zA-Z0-9]{0,95}Enabled$/.test(entry.setting) || ['agentEnabled', 'sseMonitoringEnabled', 'agentFloatingButtonEnabled'].includes(entry.setting)))
19
+ throw new BundleError('BUNDLE_INVALID', 'Invalid extension operation');
20
+ }
21
+ else {
22
+ if (Object.keys(entry).some(key => !['url', 'title'].includes(key)))
23
+ throw new BundleError('BUNDLE_INVALID', 'Invalid extension login');
24
+ entry.url = browserUiHttpUrl(entry.url, { allowFragment: true });
25
+ validateBrowserUiText(entry.title);
26
+ }
27
+ }
28
+ }
29
+ return value;
30
+ }
31
+ /** Definitions belong to packages; preset defaults cannot redirect their handlers. */
32
+ export function selectExtensionContributions(bundles) {
33
+ const operations = {};
34
+ const logins = {};
35
+ const owners = {};
36
+ for (const bundle of bundles) {
37
+ const path = bundle.manifest.contributions?.extension;
38
+ if (!path)
39
+ continue;
40
+ const definition = validateExtensionContributions(readBundleJson(bundle, path));
41
+ const bindings = bundle.manifest.contributions?.browserRuntimes?.extension?.bindings ?? {};
42
+ for (const [name, operation] of Object.entries(definition.operations ?? {})) {
43
+ if (Object.hasOwn(operations, name))
44
+ throw new BundleError('BUNDLE_CONFLICT', `Multiple providers for extension operation: ${name}`);
45
+ if (!Object.hasOwn(bindings, operation.hook))
46
+ throw new BundleError('BUNDLE_INVALID', 'Extension operation must use its own runtime');
47
+ operations[name] = operation;
48
+ owners[name] = { bundleId: bundle.manifest.id, revision: bundle.revision };
49
+ }
50
+ for (const [name, login] of Object.entries(definition.logins ?? {})) {
51
+ if (Object.hasOwn(logins, name))
52
+ throw new BundleError('BUNDLE_CONFLICT', `Multiple providers for extension login: ${name}`);
53
+ logins[name] = login;
54
+ }
55
+ }
56
+ return { operations, logins, owners };
57
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { PresetError, validatePreset, clonePresetDefaults } from './manifest.js';
2
2
  export { mergePresetDefaults, resolvePreset } from './resolve.js';
3
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';
4
+ export { prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilitySnapshot, verifyCapabilityActivation, validateCapabilityConfig, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from './capabilitySnapshot.js';
5
+ export type { CapabilitySelection, CapabilitySnapshot, CapabilityActivation } from './capabilitySnapshot.js';
6
+ export { parseToolboxCatalogs } from './toolboxCatalogs.js';
7
+ export type { ToolboxCatalogConfig, ToolboxCatalogConfigs } from './toolboxCatalogs.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { PresetError, validatePreset, clonePresetDefaults } from './manifest.js';
2
2
  export { mergePresetDefaults, resolvePreset } from './resolve.js';
3
- export { prepareNodeBundleSnapshot, prepareNodeCapabilitySnapshot, verifyNodeCapabilitySnapshot, verifyNodeCapabilityActivation, validateNodeModuleConfig, resolvePresetModuleConfig, MAX_NODE_SELECTION_BYTES } from './nodeSnapshot.js';
3
+ export { prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilitySnapshot, verifyCapabilityActivation, validateCapabilityConfig, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from './capabilitySnapshot.js';
4
+ export { parseToolboxCatalogs } from './toolboxCatalogs.js';
package/dist/manifest.js CHANGED
@@ -72,7 +72,7 @@ export function clonePresetDefaults(value) {
72
72
  return json(value, 'defaults');
73
73
  }
74
74
  function component(value, path, kind) {
75
- fields(value, path, ['source', 'version', 'enabled', 'runtimes', 'dependsOn', 'migrateFrom']);
75
+ fields(value, path, ['source', 'version', 'enabled', 'targets', 'dependsOn', 'migrateFrom']);
76
76
  if ('source' in value)
77
77
  text(value.source, `${path}.source`);
78
78
  if ('version' in value)
@@ -86,10 +86,10 @@ function component(value, path, kind) {
86
86
  }
87
87
  if ('enabled' in value && typeof value.enabled !== 'boolean')
88
88
  invalid(`${path}.enabled`, 'expected a boolean');
89
- if ('runtimes' in value) {
90
- stringArray(value.runtimes, `${path}.runtimes`);
91
- if (value.runtimes.length === 0 || value.runtimes.some(item => item !== 'browser' && item !== 'node')) {
92
- invalid(`${path}.runtimes`, 'expected browser and/or node');
89
+ if ('targets' in value) {
90
+ stringArray(value.targets, `${path}.targets`);
91
+ if (value.targets.length === 0 || value.targets.some(item => item !== 'browser' && item !== 'node')) {
92
+ invalid(`${path}.targets`, 'expected browser and/or node');
93
93
  }
94
94
  }
95
95
  if ('dependsOn' in value) {
@@ -0,0 +1,12 @@
1
+ export interface NetworkHostRoute {
2
+ id?: string;
3
+ fromHost: string;
4
+ toHost: string;
5
+ }
6
+ export interface NetworkRoutePlan {
7
+ routes: NetworkHostRoute[];
8
+ }
9
+ /** Shared by Node, the extension host and the self-contained document-start runtime. */
10
+ export declare function parseNetworkRoutePlan(input: unknown): NetworkRoutePlan;
11
+ export declare function encodeNetworkRoutePlan(input: NetworkRoutePlan): string;
12
+ export declare function decodeNetworkRoutePlan(value: string): NetworkRoutePlan;
@@ -0,0 +1,32 @@
1
+ /** Shared by Node, the extension host and the self-contained document-start runtime. */
2
+ export function parseNetworkRoutePlan(input) {
3
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).join(',') !== 'routes')
4
+ throw new Error('Invalid network route plan');
5
+ const routes = input.routes;
6
+ if (!Array.isArray(routes) || routes.length > 32)
7
+ throw new Error('Invalid network routes');
8
+ const sources = new Set();
9
+ const ids = new Set();
10
+ const hostname = (value) => typeof value === 'string' && value.length <= 253 && value.includes('.') && value.split('.').every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
11
+ for (const route of routes) {
12
+ if (!route || typeof route !== 'object' || Array.isArray(route) || Object.keys(route).some(key => !['id', 'fromHost', 'toHost'].includes(key)) || route.id !== undefined && (typeof route.id !== 'string' || !/^[a-z][a-zA-Z0-9_.-]{0,79}$/.test(route.id)) || !hostname(route.fromHost) || !hostname(route.toHost) || route.fromHost === route.toHost || sources.has(route.fromHost) || route.id !== undefined && ids.has(route.id))
13
+ throw new Error('Invalid or ambiguous network route');
14
+ sources.add(route.fromHost);
15
+ if (route.id)
16
+ ids.add(route.id);
17
+ }
18
+ if (routes.reduce((size, route) => size + route.toHost.length, 0) > 1024)
19
+ throw new Error('Network route plan is too large');
20
+ const plan = { routes: routes.map(({ id, fromHost, toHost }) => ({ ...(id ? { id } : {}), fromHost, toHost })) };
21
+ if (JSON.stringify(plan).length > 12288)
22
+ throw new Error('Network route header is too large');
23
+ return plan;
24
+ }
25
+ export function encodeNetworkRoutePlan(input) {
26
+ return btoa(JSON.stringify(parseNetworkRoutePlan(input)));
27
+ }
28
+ export function decodeNetworkRoutePlan(value) {
29
+ if (value.length > 16384)
30
+ throw new Error('Network route header is too large');
31
+ return parseNetworkRoutePlan(JSON.parse(atob(value)));
32
+ }
package/dist/prepare.d.ts CHANGED
@@ -11,17 +11,23 @@ export interface PresetGraphSnapshot {
11
11
  source: string;
12
12
  manifests: Record<string, PresetManifest>;
13
13
  bundleRevisions: Record<string, string>;
14
+ packageTargets: Record<string, PresetRuntime[]>;
14
15
  revision: string;
15
16
  }
16
17
  export declare function presetSnapshotRevision(value: object): Promise<string>;
17
18
  /** Each execution location must be able to satisfy its own package dependencies. */
18
19
  export declare function presetPackages(preset: ResolvedPreset, runtimes: PresetRuntime[]): Array<[string, ResolvedComponent]>;
20
+ export declare function bundleTargets(bundle: Pick<PreparedBundle, 'manifest'>): PresetRuntime[];
21
+ /** Resolved activation choices are derived data; source manifests remain unchanged. */
22
+ export declare function applyPackageTargets(preset: ResolvedPreset, targets: Record<string, PresetRuntime[]>): ResolvedPreset;
23
+ export declare function verifyPackageTargets(preset: ResolvedPreset, bundles: PreparedBundle[]): void;
19
24
  /** Shared preparation performs no installation, code execution or permission changes. */
20
25
  export declare function preparePresetPackages(source: string, options: PreparePresetOptions, runtimes: PresetRuntime[]): Promise<{
21
26
  source: string;
22
27
  manifests: Record<string, PresetManifest>;
23
28
  preset: ResolvedPreset;
24
29
  bundles: PreparedBundle[];
30
+ packageTargets: Record<string, PresetRuntime[]>;
25
31
  }>;
26
32
  /** The exact graph is checked offline; installed resources are verified by the target store. */
27
33
  export declare function verifyPresetGraph(value: PresetGraphSnapshot & {
package/dist/prepare.js CHANGED
@@ -1,16 +1,21 @@
1
- import { BundleError, MAX_BROWSER_STORAGE_BYTES, bundleDigest, bundleSourceUrl, prepareBundle } from './bundle.js';
1
+ import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, bundleDigest, bundleSourceUrl, prepareBundle, validateBundleManifest } from './bundle.js';
2
+ import { satisfies } from 'semver';
2
3
  import { clonePresetDefaults, validatePreset } from './manifest.js';
3
4
  import { resolvePreset } from './resolve.js';
4
5
  export async function presetSnapshotRevision(value) {
5
6
  return bundleDigest(new TextEncoder().encode(JSON.stringify(value)));
6
7
  }
8
+ /** Select candidates without assuming that inferred locations have been discovered. */
9
+ function selectPresetPackages(preset, runtimes) {
10
+ const entries = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false && (entry.targets ?? ['browser', 'node']).some(runtime => runtimes.includes(runtime)));
11
+ if (entries.length > 64)
12
+ throw new BundleError('BUNDLE_QUOTA', 'A preset can activate at most 64 packages');
13
+ return entries;
14
+ }
7
15
  /** Each execution location must be able to satisfy its own package dependencies. */
8
16
  export function presetPackages(preset, runtimes) {
9
- if (runtimes.includes('node') && runtimes.includes('browser') && Object.values(preset.packages).some(entry => entry.enabled !== false && !entry.runtimes?.length)) {
10
- throw new BundleError('BUNDLE_INCOMPATIBLE', 'Every enabled package must declare its browser and/or Node runtime');
11
- }
12
17
  for (const runtime of runtimes) {
13
- const entries = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false && entry.runtimes?.includes(runtime));
18
+ const entries = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false && (entry.targets ?? ['browser', 'node']).includes(runtime));
14
19
  const ids = new Set(entries.map(([id]) => id));
15
20
  for (const [id, entry] of entries) {
16
21
  if (entry.dependsOn?.some(dependency => !ids.has(dependency))) {
@@ -25,10 +30,37 @@ export function presetPackages(preset, runtimes) {
25
30
  }
26
31
  }
27
32
  }
28
- const entries = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false && entry.runtimes?.some(runtime => runtimes.includes(runtime)));
29
- if (entries.length > 64)
30
- throw new BundleError('BUNDLE_QUOTA', 'A preset can activate at most 64 packages');
31
- return entries;
33
+ return selectPresetPackages(preset, runtimes);
34
+ }
35
+ export function bundleTargets(bundle) {
36
+ const contributions = bundle.manifest.contributions;
37
+ const targets = [];
38
+ if (contributions && (Object.keys(contributions.browserRuntimes ?? {}).length || Object.keys(contributions.pageRuntimes ?? {}).length || contributions.appProfiles?.length || contributions.extension || contributions.requestUi || contributions.browserViews || contributions.browserControls))
39
+ targets.push('browser');
40
+ if (Object.keys(contributions?.nodeRuntimes ?? {}).length)
41
+ targets.push('node');
42
+ return targets.length ? targets : ['browser', 'node'];
43
+ }
44
+ /** Resolved activation choices are derived data; source manifests remain unchanged. */
45
+ export function applyPackageTargets(preset, targets) {
46
+ const enabled = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false);
47
+ if (enabled.length !== Object.keys(targets).length)
48
+ throw new BundleError('BUNDLE_INTEGRITY', 'Package target selection does not match the preset');
49
+ const packages = { ...preset.packages };
50
+ for (const [id, entry] of enabled) {
51
+ const selected = targets[id];
52
+ if (!Array.isArray(selected) || !selected.length || selected.length > 2 || new Set(selected).size !== selected.length || selected.some(target => target !== 'browser' && target !== 'node') || entry.targets && JSON.stringify(entry.targets) !== JSON.stringify(selected))
53
+ throw new BundleError('BUNDLE_INTEGRITY', 'Invalid package target selection');
54
+ packages[id] = { ...entry, targets: [...selected] };
55
+ }
56
+ return { ...preset, packages };
57
+ }
58
+ export function verifyPackageTargets(preset, bundles) {
59
+ for (const bundle of bundles) {
60
+ const available = bundleTargets(bundle);
61
+ if (preset.packages[bundle.manifest.id]?.targets?.some(target => !available.includes(target)))
62
+ throw new BundleError('BUNDLE_INCOMPATIBLE', 'Package does not support the selected target');
63
+ }
32
64
  }
33
65
  /** Shared preparation performs no installation, code execution or permission changes. */
34
66
  export async function preparePresetPackages(source, options, runtimes) {
@@ -47,7 +79,7 @@ export async function preparePresetPackages(source, options, runtimes) {
47
79
  return bytes;
48
80
  };
49
81
  const manifests = {};
50
- const preset = await resolvePreset(source, {
82
+ let preset = await resolvePreset(source, {
51
83
  mearlApiVersion: options.mearlApiVersion,
52
84
  signal: options.signal,
53
85
  load: async (url) => {
@@ -63,18 +95,49 @@ export async function preparePresetPackages(source, options, runtimes) {
63
95
  return manifest;
64
96
  },
65
97
  });
66
- const entries = presetPackages(preset, runtimes);
98
+ const targetsKnown = Object.values(preset.packages).every(entry => entry.enabled === false || Boolean(entry.targets?.length));
99
+ const entries = targetsKnown ? presetPackages(preset, runtimes) : selectPresetPackages(preset, runtimes);
67
100
  const bundles = [];
101
+ const pending = [];
102
+ const packageTargets = Object.fromEntries(Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false).map(([id, entry]) => [id, entry.targets ?? []]));
68
103
  for (const [id, entry] of entries) {
69
- bundles.push(await prepareBundle(entry.source, { ...options, load, expectedId: id, version: entry.version }));
104
+ const manifestSource = bundleSourceUrl(entry.source);
105
+ const bytes = Uint8Array.from(await load(manifestSource, { maxBytes: MAX_BUNDLE_MANIFEST_BYTES, signal: options.signal }));
106
+ let manifest;
107
+ try {
108
+ manifest = validateBundleManifest(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)), options.mearlApiVersion);
109
+ }
110
+ catch (error) {
111
+ if (error instanceof BundleError)
112
+ throw error;
113
+ throw new BundleError('BUNDLE_INVALID', error instanceof Error ? error.message : 'Invalid package manifest');
114
+ }
115
+ if (manifest.id !== id)
116
+ throw new BundleError('BUNDLE_INVALID', 'Package identity does not match the preset');
117
+ if (entry.version && !satisfies(manifest.version, entry.version))
118
+ throw new BundleError('BUNDLE_INCOMPATIBLE', 'Package version does not satisfy the preset');
119
+ const available = bundleTargets({ manifest });
120
+ const targets = entry.targets ?? available;
121
+ if (targets.some(target => !available.includes(target)))
122
+ throw new BundleError('BUNDLE_INCOMPATIBLE', 'Package does not support the selected target');
123
+ packageTargets[id] = targets;
124
+ pending.push({ id, entry, source: manifestSource, bytes });
125
+ }
126
+ preset = applyPackageTargets(preset, packageTargets);
127
+ const selected = new Set(presetPackages(preset, runtimes).map(([id]) => id));
128
+ for (const candidate of pending) {
129
+ if (!selected.has(candidate.id))
130
+ continue;
131
+ bundles.push(await prepareBundle(candidate.source, { ...options, load: (url, request) => url === candidate.source ? Promise.resolve(candidate.bytes) : load(url, request), expectedId: candidate.id, version: candidate.entry.version }));
70
132
  }
133
+ verifyPackageTargets(preset, bundles);
71
134
  options.signal?.throwIfAborted();
72
- return { source, manifests, preset, bundles };
135
+ return { source, manifests, preset, bundles: bundles.filter(bundle => selected.has(bundle.manifest.id)), packageTargets };
73
136
  }
74
137
  /** The exact graph is checked offline; installed resources are verified by the target store. */
75
138
  export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full) {
76
139
  const json = clonePresetDefaults(value);
77
- const allowed = ['source', 'manifests', 'bundleRevisions', 'revision', ...(full ? ['schemaVersion'] : [])];
140
+ const allowed = ['source', 'manifests', 'bundleRevisions', 'packageTargets', 'revision', ...(full ? ['schemaVersion'] : [])];
78
141
  if (Object.keys(json).some(key => !allowed.includes(key)) || (full && json.schemaVersion !== 1) ||
79
142
  typeof json.source !== 'string' || typeof json.revision !== 'string' || !/^[a-f0-9]{64}$/.test(json.revision)) {
80
143
  throw new BundleError('BUNDLE_INTEGRITY', 'Invalid stored preset identity');
@@ -93,10 +156,11 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
93
156
  throw new BundleError('BUNDLE_INTEGRITY', 'Invalid stored preset source');
94
157
  validated[url] = validatePreset(manifest);
95
158
  }
96
- const data = { ...(full ? { schemaVersion: 1 } : {}), source, manifests: validated, bundleRevisions };
159
+ const packageTargets = clonePresetDefaults(json.packageTargets);
160
+ const data = { ...(full ? { schemaVersion: 1 } : {}), source, manifests: validated, bundleRevisions, packageTargets };
97
161
  if (await presetSnapshotRevision(data) !== json.revision)
98
162
  throw new BundleError('BUNDLE_INTEGRITY', 'Stored preset revision does not match');
99
- const preset = await resolvePreset(source, {
163
+ let preset = await resolvePreset(source, {
100
164
  mearlApiVersion,
101
165
  load: async (url) => {
102
166
  if (!Object.hasOwn(validated, url))
@@ -104,6 +168,7 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
104
168
  return validated[url];
105
169
  },
106
170
  });
171
+ preset = applyPackageTargets(preset, packageTargets);
107
172
  const entries = presetPackages(preset, runtimes);
108
173
  if (preset.chain.length !== Object.keys(validated).length || entries.length !== Object.keys(bundleRevisions).length || entries.some(([id]) => !Object.hasOwn(bundleRevisions, id))) {
109
174
  throw new BundleError('BUNDLE_INTEGRITY', 'Stored preset package graph does not match');