@mearl/preset 3.0.0-beta.4 → 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';
@@ -11,7 +11,6 @@ export interface InstallationConfig {
11
11
  schemaVersion: 1;
12
12
  guide?: InstallationGuide;
13
13
  presets?: Record<string, InstallationPreset>;
14
- legacyPreset?: string;
15
14
  }
16
15
  /** One validated catalog is used by the website and extension; parameters cannot add install sources. */
17
16
  export declare function parseInstallationConfig(value: unknown, publicOrigin: string): InstallationConfig;
@@ -22,3 +21,8 @@ export declare function matchesInstallationOrigin(scope: string, target: URL): b
22
21
  export declare const INSTALLATION_CONFIG_MAX_BYTES = 65536;
23
22
  export declare function readInstallationJson(response: Response, maxBytes: number): Promise<unknown>;
24
23
  export declare function loadInstallationConfig(url: string | URL, publicOrigin: string, signal?: AbortSignal): Promise<InstallationConfig>;
24
+ export declare const PRESET_UPDATE_INTERVAL_MS: number;
25
+ export declare const PRESET_UPDATE_RETRY_MS: number;
26
+ export declare function trustedInstallationPreset(config: InstallationConfig, id: string | undefined, source: string | undefined): InstallationPreset | undefined;
27
+ /** An exact scope can be covered by a broader catalog scope, never the reverse. */
28
+ export declare function installationOriginCovered(origin: string, scopes: readonly string[]): boolean;
@@ -14,7 +14,7 @@ function httpsUrl(value) {
14
14
  }
15
15
  /** One validated catalog is used by the website and extension; parameters cannot add install sources. */
16
16
  export function parseInstallationConfig(value, publicOrigin) {
17
- const input = record(value, ['schemaVersion', 'guide', 'presets', 'legacyPreset']);
17
+ const input = record(value, ['schemaVersion', 'guide', 'presets']);
18
18
  if (input.schemaVersion !== 1)
19
19
  throw new Error('Invalid installation schema');
20
20
  const result = { schemaVersion: 1 };
@@ -47,11 +47,6 @@ export function parseInstallationConfig(value, publicOrigin) {
47
47
  return [id, { label: entry.label, source, origins }];
48
48
  }));
49
49
  }
50
- if (input.legacyPreset !== undefined) {
51
- if (typeof input.legacyPreset !== 'string' || !Object.hasOwn(result.presets ?? {}, input.legacyPreset))
52
- throw new Error('Invalid legacy installation preset');
53
- result.legacyPreset = input.legacyPreset;
54
- }
55
50
  return result;
56
51
  }
57
52
  /** Explicit host scopes retain their selected schemes; download sources remain HTTPS. */
@@ -114,3 +109,21 @@ export async function loadInstallationConfig(url, publicOrigin, signal) {
114
109
  throw new Error('Installation configuration unavailable');
115
110
  return parseInstallationConfig(await readInstallationJson(response, INSTALLATION_CONFIG_MAX_BYTES), publicOrigin);
116
111
  }
112
+ export const PRESET_UPDATE_INTERVAL_MS = 12 * 60 * 60_000;
113
+ export const PRESET_UPDATE_RETRY_MS = 60 * 60_000;
114
+ export function trustedInstallationPreset(config, id, source) {
115
+ return id && source && Object.hasOwn(config.presets ?? {}, id) && config.presets[id].source === source ? config.presets[id] : undefined;
116
+ }
117
+ /** An exact scope can be covered by a broader catalog scope, never the reverse. */
118
+ export function installationOriginCovered(origin, scopes) {
119
+ const target = validateInstallationOrigin(origin);
120
+ const wildcard = /^(?:https?|\*):\/\/\*\./.test(target);
121
+ const schemes = target.startsWith('*:') ? ['http:', 'https:'] : [target.slice(0, target.indexOf(':') + 1)];
122
+ return scopes.some(scope => {
123
+ const candidate = validateInstallationOrigin(scope);
124
+ if (wildcard && !/^(?:https?|\*):\/\/\*\./.test(candidate))
125
+ return false;
126
+ const base = target.replace(/^\*:/, 'https:').replace(/^(https?:\/\/)\*\./, '$1');
127
+ return schemes.every(scheme => { const url = new URL(base); url.protocol = scheme; return matchesInstallationOrigin(candidate, url); });
128
+ });
129
+ }
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 & {