@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/README.md +45 -2
- package/dist/browserControls.d.ts +29 -0
- package/dist/browserControls.js +54 -0
- package/dist/browserPreset.js +8 -3
- package/dist/browserViews.d.ts +1 -1
- package/dist/browserViews.js +5 -5
- package/dist/bundle.d.ts +4 -1
- package/dist/bundle.js +24 -5
- package/dist/bundleStore.js +80 -75
- package/dist/bundles.d.ts +7 -1
- package/dist/bundles.js +4 -1
- package/dist/{nodeSnapshot.d.ts → capabilitySnapshot.d.ts} +11 -11
- package/dist/{nodeSnapshot.js → capabilitySnapshot.js} +36 -35
- package/dist/extensionContributions.d.ts +29 -0
- package/dist/extensionContributions.js +57 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/installation.d.ts +5 -1
- package/dist/installation.js +19 -6
- package/dist/manifest.js +5 -5
- package/dist/networkRoutes.d.ts +12 -0
- package/dist/networkRoutes.js +32 -0
- package/dist/prepare.d.ts +6 -0
- package/dist/prepare.js +81 -16
- package/dist/preset.js +3 -1
- package/dist/requestUi.d.ts +3 -1
- package/dist/requestUi.js +2 -2
- package/dist/resolve.js +4 -0
- package/dist/toolboxCatalogs.d.ts +8 -0
- package/dist/toolboxCatalogs.js +34 -0
- package/dist/types.d.ts +2 -1
- package/installation-site.json +4 -0
- package/package.json +9 -3
package/dist/bundles.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { BundleError, MAX_BUNDLE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, MAX_BROWSER_STORAGE_BYTES, MAX_PAGE_RUNTIME_BYTES, MAX_REQUEST_UI_BYTES, bundleDigest, bundleSourceUrl, prepareBundle, readBundleJson, readBundleText, validateBundleManifest, verifyBundle, } from './bundle.js';
|
|
2
2
|
export type { BundleManifest, PreparedBundle, BundleLoader, BundleLoadOptions, PrepareBundleOptions, CapabilityRuntimeDeclaration } from './bundle.js';
|
|
3
|
+
export { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
|
|
4
|
+
export type { ExtensionContributions } from './extensionContributions.js';
|
|
3
5
|
export { createBundleFetchLoader } from './bundleFetch.js';
|
|
4
6
|
export { BrowserBundleStore } from './bundleStore.js';
|
|
5
7
|
export type { BrowserPresetInstallation } from './bundleStore.js';
|
|
@@ -12,7 +14,7 @@ export { installPageRuntimeHost, buildPageRuntimeRegistrationSource, buildPageRu
|
|
|
12
14
|
export type { PageRuntimeBinding, PageRuntimeConfig } from './pageRuntime.js';
|
|
13
15
|
export { receiveBrowserPreset, readPresetTransferResource, PRESET_TRANSFER_CHUNK_BYTES, MAX_PRESET_TRANSFER_HEADER_BYTES } from './browserTransfer.js';
|
|
14
16
|
export type { BrowserPresetTransferHeader, PresetTransferRequest, PresetTransferTransport } from './browserTransfer.js';
|
|
15
|
-
export { prepareBrowserBundleSnapshot } from './
|
|
17
|
+
export { prepareBrowserBundleSnapshot } from './capabilitySnapshot.js';
|
|
16
18
|
export { buildBrowserCapabilityPage, buildBrowserPureCapabilityPage } from './browserSandbox.js';
|
|
17
19
|
export type { BrowserCapabilityHost, BrowserCapabilityInvocation, BrowserCapabilityRequest, BrowserCapabilityResponse } from './browserSandbox.js';
|
|
18
20
|
export { validateBrowserRequestUi, selectBrowserRequestUi, browserUiText, browserRequestUiLink, browserRequestLoginError, validateBrowserRequestDetail } from './requestUi.js';
|
|
@@ -20,3 +22,7 @@ export type { BrowserUiText, BrowserRequestUi, BrowserRequestDetail, SelectedBro
|
|
|
20
22
|
export { validateBrowserView, selectBrowserView, buildBrowserViewPage } from './browserViews.js';
|
|
21
23
|
export type { BrowserViewSlot, BrowserViewDefinition, BrowserViewAction, BrowserViewTransport, SelectedBrowserView, BrowserViewBootstrap } from './browserViews.js';
|
|
22
24
|
export { browserTransportUrl, browserTransportPolicies } from './browserTransport.js';
|
|
25
|
+
export { validateBrowserControl, selectBrowserControl } from './browserControls.js';
|
|
26
|
+
export type { BrowserSegmentedControl, SelectedBrowserControl } from './browserControls.js';
|
|
27
|
+
export { parseNetworkRoutePlan, encodeNetworkRoutePlan, decodeNetworkRoutePlan } from './networkRoutes.js';
|
|
28
|
+
export type { NetworkHostRoute, NetworkRoutePlan } from './networkRoutes.js';
|
package/dist/bundles.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export { BundleError, MAX_BUNDLE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, MAX_BROWSER_STORAGE_BYTES, MAX_PAGE_RUNTIME_BYTES, MAX_REQUEST_UI_BYTES, bundleDigest, bundleSourceUrl, prepareBundle, readBundleJson, readBundleText, validateBundleManifest, verifyBundle, } from './bundle.js';
|
|
2
|
+
export { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
|
|
2
3
|
export { createBundleFetchLoader } from './bundleFetch.js';
|
|
3
4
|
export { BrowserBundleStore } from './bundleStore.js';
|
|
4
5
|
export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
|
|
5
6
|
export { preparePreset, verifyPreset, verifyPreparedPreset } from './preset.js';
|
|
6
7
|
export { installPageRuntimeHost, buildPageRuntimeRegistrationSource, buildPageRuntimeCallSource } from './pageRuntime.js';
|
|
7
8
|
export { receiveBrowserPreset, readPresetTransferResource, PRESET_TRANSFER_CHUNK_BYTES, MAX_PRESET_TRANSFER_HEADER_BYTES } from './browserTransfer.js';
|
|
8
|
-
export { prepareBrowserBundleSnapshot } from './
|
|
9
|
+
export { prepareBrowserBundleSnapshot } from './capabilitySnapshot.js';
|
|
9
10
|
export { buildBrowserCapabilityPage, buildBrowserPureCapabilityPage } from './browserSandbox.js';
|
|
10
11
|
export { validateBrowserRequestUi, selectBrowserRequestUi, browserUiText, browserRequestUiLink, browserRequestLoginError, validateBrowserRequestDetail } from './requestUi.js';
|
|
11
12
|
export { validateBrowserView, selectBrowserView, buildBrowserViewPage } from './browserViews.js';
|
|
12
13
|
export { browserTransportUrl, browserTransportPolicies } from './browserTransport.js';
|
|
14
|
+
export { validateBrowserControl, selectBrowserControl } from './browserControls.js';
|
|
15
|
+
export { parseNetworkRoutePlan, encodeNetworkRoutePlan, decodeNetworkRoutePlan } from './networkRoutes.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonObject } from './types.js';
|
|
2
|
-
export interface
|
|
2
|
+
export interface CapabilitySelection {
|
|
3
3
|
schemaVersion: 1;
|
|
4
4
|
modules: Record<string, {
|
|
5
5
|
sha256: string;
|
|
@@ -11,28 +11,28 @@ export interface NodeCapabilitySelection {
|
|
|
11
11
|
handler: string;
|
|
12
12
|
}>;
|
|
13
13
|
}
|
|
14
|
-
export interface
|
|
14
|
+
export interface CapabilitySnapshot extends CapabilitySelection {
|
|
15
15
|
revision: string;
|
|
16
16
|
}
|
|
17
|
-
export interface
|
|
17
|
+
export interface CapabilityActivation extends CapabilitySnapshot {
|
|
18
18
|
installationRevision?: string;
|
|
19
19
|
}
|
|
20
|
-
export declare const
|
|
20
|
+
export declare const MAX_CAPABILITY_SELECTION_BYTES = 262144;
|
|
21
21
|
/** Explicit module configuration survives periods where a preset disables its module. */
|
|
22
|
-
export declare function
|
|
22
|
+
export declare function validateCapabilityConfig(value: Record<string, JsonObject>): Record<string, JsonObject>;
|
|
23
23
|
/** Preset defaults are inherited; explicit host configuration stays a separate override. */
|
|
24
|
-
export declare function
|
|
24
|
+
export declare function resolveCapabilityConfig(defaults: JsonObject, overrides?: Record<string, JsonObject>): Record<string, JsonObject>;
|
|
25
25
|
/** Exact content and bindings are selected together; filesystem and code loading belong to hosts. */
|
|
26
|
-
export declare function
|
|
27
|
-
export declare function
|
|
26
|
+
export declare function prepareCapabilitySnapshot(input: CapabilitySelection): Promise<CapabilitySnapshot>;
|
|
27
|
+
export declare function verifyCapabilitySnapshot(input: CapabilitySnapshot): Promise<CapabilitySnapshot>;
|
|
28
28
|
/** The installer receipt and Node selection share one atomic filesystem pointer. */
|
|
29
|
-
export declare function
|
|
29
|
+
export declare function verifyCapabilityActivation(input: CapabilityActivation): Promise<CapabilityActivation>;
|
|
30
30
|
export declare function prepareNodeBundleSnapshot(bundles: import('./bundle.js').PreparedBundle[], host: string, mearlApiVersion: string, config?: Record<string, JsonObject>): Promise<{
|
|
31
|
-
snapshot:
|
|
31
|
+
snapshot: CapabilitySnapshot;
|
|
32
32
|
resources: Record<string, Uint8Array>;
|
|
33
33
|
}>;
|
|
34
34
|
/** Browser factories use the same exact module/configuration selection without a Node loader. */
|
|
35
35
|
export declare function prepareBrowserBundleSnapshot(bundles: import('./bundle.js').PreparedBundle[], host: string, mearlApiVersion: string, config?: Record<string, JsonObject>): Promise<{
|
|
36
|
-
snapshot:
|
|
36
|
+
snapshot: CapabilitySnapshot;
|
|
37
37
|
resources: Record<string, Uint8Array>;
|
|
38
38
|
}>;
|
|
@@ -1,95 +1,95 @@
|
|
|
1
1
|
import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_BYTES, bundleDigest, validateBundleManifest } from './bundle.js';
|
|
2
2
|
import { clonePresetDefaults, isJsonObject } from './manifest.js';
|
|
3
3
|
import { mergePresetDefaults } from './resolve.js';
|
|
4
|
-
export const
|
|
4
|
+
export const MAX_CAPABILITY_SELECTION_BYTES = 262_144;
|
|
5
5
|
const identifier = /^[a-z][a-z0-9-]{0,79}$/;
|
|
6
6
|
const hookName = /^[a-z][a-zA-Z0-9_.-]{0,127}$/;
|
|
7
7
|
const digest = /^[a-f0-9]{64}$/;
|
|
8
8
|
function invalid(message) { throw new BundleError('BUNDLE_INVALID', message); }
|
|
9
9
|
function fields(value, allowed) {
|
|
10
10
|
if (!isJsonObject(value) || Object.keys(value).some(key => !allowed.includes(key)))
|
|
11
|
-
invalid('Invalid
|
|
11
|
+
invalid('Invalid Capability selection structure');
|
|
12
12
|
}
|
|
13
13
|
/** Explicit module configuration survives periods where a preset disables its module. */
|
|
14
|
-
export function
|
|
14
|
+
export function validateCapabilityConfig(value) {
|
|
15
15
|
const configs = clonePresetDefaults(value);
|
|
16
|
-
if (Object.keys(configs).length > 64 || new TextEncoder().encode(JSON.stringify(configs)).byteLength >
|
|
17
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
16
|
+
if (Object.keys(configs).length > 64 || new TextEncoder().encode(JSON.stringify(configs)).byteLength > MAX_CAPABILITY_SELECTION_BYTES)
|
|
17
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability module configuration exceeds its limit');
|
|
18
18
|
for (const [id, config] of Object.entries(configs))
|
|
19
19
|
if (!identifier.test(id) || !isJsonObject(config))
|
|
20
|
-
invalid('
|
|
20
|
+
invalid('Capability module configuration must map IDs to JSON objects');
|
|
21
21
|
return configs;
|
|
22
22
|
}
|
|
23
23
|
/** Preset defaults are inherited; explicit host configuration stays a separate override. */
|
|
24
|
-
export function
|
|
24
|
+
export function resolveCapabilityConfig(defaults, overrides = {}) {
|
|
25
25
|
const value = defaults.capabilities === undefined ? {} : defaults.capabilities;
|
|
26
26
|
if (!isJsonObject(value))
|
|
27
27
|
invalid('Capability defaults must map module IDs to configuration objects');
|
|
28
|
-
return
|
|
28
|
+
return validateCapabilityConfig(mergePresetDefaults(validateCapabilityConfig(value), validateCapabilityConfig(overrides)));
|
|
29
29
|
}
|
|
30
30
|
/** Exact content and bindings are selected together; filesystem and code loading belong to hosts. */
|
|
31
|
-
export async function
|
|
31
|
+
export async function prepareCapabilitySnapshot(input) {
|
|
32
32
|
const value = clonePresetDefaults(input);
|
|
33
33
|
fields(value, ['schemaVersion', 'modules', 'bindings']);
|
|
34
34
|
if (value.schemaVersion !== 1 || !isJsonObject(value.modules) || !isJsonObject(value.bindings))
|
|
35
|
-
invalid('Invalid
|
|
35
|
+
invalid('Invalid Capability selection');
|
|
36
36
|
if (Object.keys(value.modules).length > 64 || Object.keys(value.bindings).length > 256)
|
|
37
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
37
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability selection exceeds the entry limit');
|
|
38
38
|
const modules = {};
|
|
39
39
|
let total = 0;
|
|
40
40
|
for (const id of Object.keys(value.modules).sort()) {
|
|
41
41
|
if (!identifier.test(id))
|
|
42
|
-
invalid('Invalid
|
|
42
|
+
invalid('Invalid Capability module ID');
|
|
43
43
|
const entry = value.modules[id];
|
|
44
44
|
fields(entry, ['sha256', 'byteLength', 'config']);
|
|
45
45
|
if (typeof entry.sha256 !== 'string' || !digest.test(entry.sha256) || typeof entry.byteLength !== 'number' || !Number.isSafeInteger(entry.byteLength) || entry.byteLength <= 0 || entry.byteLength > MAX_BUNDLE_BYTES)
|
|
46
|
-
invalid('Invalid
|
|
46
|
+
invalid('Invalid Capability module resource');
|
|
47
47
|
if (entry.config !== undefined && !isJsonObject(entry.config))
|
|
48
|
-
invalid('
|
|
48
|
+
invalid('Capability module configuration must be a JSON object');
|
|
49
49
|
total += entry.byteLength;
|
|
50
50
|
if (total > MAX_BROWSER_STORAGE_BYTES)
|
|
51
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
51
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability resources exceed 64 MiB');
|
|
52
52
|
modules[id] = { sha256: entry.sha256, byteLength: entry.byteLength, ...(entry.config === undefined ? {} : { config: clonePresetDefaults(entry.config) }) };
|
|
53
53
|
}
|
|
54
54
|
const bindings = {};
|
|
55
55
|
for (const name of Object.keys(value.bindings).sort()) {
|
|
56
56
|
if (!hookName.test(name))
|
|
57
|
-
invalid('Invalid
|
|
57
|
+
invalid('Invalid Capability hook name');
|
|
58
58
|
const binding = value.bindings[name];
|
|
59
59
|
fields(binding, ['module', 'handler']);
|
|
60
60
|
if (typeof binding.module !== 'string' || !Object.hasOwn(modules, binding.module) || typeof binding.handler !== 'string' || !hookName.test(binding.handler))
|
|
61
|
-
invalid('
|
|
61
|
+
invalid('Capability hook must bind a declared module handler');
|
|
62
62
|
bindings[name] = { module: binding.module, handler: binding.handler };
|
|
63
63
|
}
|
|
64
64
|
const selection = { schemaVersion: 1, modules, bindings };
|
|
65
|
-
if (new TextEncoder().encode(JSON.stringify(selection)).byteLength + 80 >
|
|
66
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
65
|
+
if (new TextEncoder().encode(JSON.stringify(selection)).byteLength + 80 > MAX_CAPABILITY_SELECTION_BYTES)
|
|
66
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability selection metadata exceeds 256 KiB');
|
|
67
67
|
return { ...selection, revision: await bundleDigest(new TextEncoder().encode(JSON.stringify(selection))) };
|
|
68
68
|
}
|
|
69
|
-
export async function
|
|
69
|
+
export async function verifyCapabilitySnapshot(input) {
|
|
70
70
|
const value = clonePresetDefaults(input);
|
|
71
71
|
fields(value, ['schemaVersion', 'modules', 'bindings', 'revision']);
|
|
72
72
|
if (typeof value.revision !== 'string' || !digest.test(value.revision))
|
|
73
|
-
invalid('Invalid
|
|
73
|
+
invalid('Invalid Capability revision');
|
|
74
74
|
const { revision, ...selection } = value;
|
|
75
|
-
const prepared = await
|
|
75
|
+
const prepared = await prepareCapabilitySnapshot(selection);
|
|
76
76
|
if (prepared.revision !== revision)
|
|
77
|
-
throw new BundleError('BUNDLE_INTEGRITY', '
|
|
77
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Capability selection revision does not match');
|
|
78
78
|
return prepared;
|
|
79
79
|
}
|
|
80
80
|
/** The installer receipt and Node selection share one atomic filesystem pointer. */
|
|
81
|
-
export async function
|
|
81
|
+
export async function verifyCapabilityActivation(input) {
|
|
82
82
|
const value = clonePresetDefaults(input);
|
|
83
83
|
const { installationRevision, ...selection } = value;
|
|
84
84
|
if (installationRevision !== undefined && (typeof installationRevision !== 'string' || !digest.test(installationRevision)))
|
|
85
85
|
invalid('Invalid capability installation revision');
|
|
86
|
-
const snapshot = await
|
|
86
|
+
const snapshot = await verifyCapabilitySnapshot(selection);
|
|
87
87
|
return { ...snapshot, ...(installationRevision === undefined ? {} : { installationRevision: installationRevision }) };
|
|
88
88
|
}
|
|
89
89
|
async function prepareRuntimeBundleSnapshot(bundles, kind, host, mearlApiVersion, config) {
|
|
90
90
|
if (!identifier.test(host))
|
|
91
|
-
invalid('Invalid
|
|
92
|
-
const configs =
|
|
91
|
+
invalid('Invalid capability runtime host');
|
|
92
|
+
const configs = validateCapabilityConfig(config);
|
|
93
93
|
const modules = {};
|
|
94
94
|
const bindings = {};
|
|
95
95
|
const resources = {};
|
|
@@ -101,20 +101,21 @@ async function prepareRuntimeBundleSnapshot(bundles, kind, host, mearlApiVersion
|
|
|
101
101
|
if (!runtime)
|
|
102
102
|
continue;
|
|
103
103
|
if (Object.hasOwn(modules, manifest.id))
|
|
104
|
-
invalid('Duplicate
|
|
104
|
+
invalid('Duplicate Capability module ID');
|
|
105
105
|
const metadata = manifest.resources[runtime.entry];
|
|
106
106
|
if (!Object.hasOwn(bundle.resources, runtime.entry))
|
|
107
|
-
invalid('
|
|
107
|
+
invalid('Capability module resource is missing');
|
|
108
108
|
total += metadata.byteLength;
|
|
109
109
|
if (pending.length >= 64 || total > MAX_BROWSER_STORAGE_BYTES)
|
|
110
|
-
throw new BundleError('BUNDLE_QUOTA', '
|
|
110
|
+
throw new BundleError('BUNDLE_QUOTA', 'Capability resources exceed the selection limit');
|
|
111
111
|
pending.push({ id: manifest.id, bytes: bundle.resources[runtime.entry], sha256: metadata.sha256 });
|
|
112
112
|
if (pending[pending.length - 1].bytes.byteLength !== metadata.byteLength)
|
|
113
|
-
invalid('
|
|
114
|
-
|
|
113
|
+
invalid('Capability module length does not match');
|
|
114
|
+
const effectiveConfig = mergePresetDefaults(manifest.defaults ?? {}, configs[manifest.id] ?? {});
|
|
115
|
+
modules[manifest.id] = { ...metadata, ...(Object.keys(effectiveConfig).length ? { config: effectiveConfig } : {}) };
|
|
115
116
|
for (const [name, handler] of Object.entries(runtime.bindings)) {
|
|
116
117
|
if (Object.hasOwn(bindings, name))
|
|
117
|
-
invalid('
|
|
118
|
+
invalid('Capability hook has multiple providers');
|
|
118
119
|
bindings[name] = { module: manifest.id, handler };
|
|
119
120
|
}
|
|
120
121
|
}
|
|
@@ -122,10 +123,10 @@ async function prepareRuntimeBundleSnapshot(bundles, kind, host, mearlApiVersion
|
|
|
122
123
|
const captured = pending.map(resource => ({ ...resource, bytes: Uint8Array.from(resource.bytes) }));
|
|
123
124
|
for (const resource of captured) {
|
|
124
125
|
if (await bundleDigest(resource.bytes) !== resource.sha256)
|
|
125
|
-
throw new BundleError('BUNDLE_INTEGRITY', `
|
|
126
|
+
throw new BundleError('BUNDLE_INTEGRITY', `Capability module digest does not match: ${resource.id}`);
|
|
126
127
|
resources[resource.sha256] = resource.bytes;
|
|
127
128
|
}
|
|
128
|
-
return { snapshot: await
|
|
129
|
+
return { snapshot: await prepareCapabilitySnapshot({ schemaVersion: 1, modules, bindings }), resources };
|
|
129
130
|
}
|
|
130
131
|
export function prepareNodeBundleSnapshot(bundles, host, mearlApiVersion, config = {}) {
|
|
131
132
|
return prepareRuntimeBundleSnapshot(bundles, 'nodeRuntimes', host, mearlApiVersion, config);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type PreparedBundle } from './bundle.js';
|
|
2
|
+
import { type BrowserUiText } from './requestUi.js';
|
|
3
|
+
export interface ExtensionContributions {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
operations?: Record<string, {
|
|
6
|
+
hook: string;
|
|
7
|
+
setting?: string;
|
|
8
|
+
}>;
|
|
9
|
+
logins?: Record<string, {
|
|
10
|
+
url: string;
|
|
11
|
+
title: BrowserUiText;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export declare function validateExtensionContributions(input: unknown): ExtensionContributions;
|
|
15
|
+
/** Definitions belong to packages; preset defaults cannot redirect their handlers. */
|
|
16
|
+
export declare function selectExtensionContributions(bundles: PreparedBundle[]): {
|
|
17
|
+
operations: Record<string, {
|
|
18
|
+
hook: string;
|
|
19
|
+
setting?: string;
|
|
20
|
+
}>;
|
|
21
|
+
logins: Record<string, {
|
|
22
|
+
url: string;
|
|
23
|
+
title: BrowserUiText;
|
|
24
|
+
}>;
|
|
25
|
+
owners: Record<string, {
|
|
26
|
+
bundleId: string;
|
|
27
|
+
revision: string;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
@@ -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,
|
|
5
|
-
export type {
|
|
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,
|
|
3
|
+
export { prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilitySnapshot, verifyCapabilityActivation, validateCapabilityConfig, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from './capabilitySnapshot.js';
|
|
4
|
+
export { parseToolboxCatalogs } from './toolboxCatalogs.js';
|
package/dist/installation.d.ts
CHANGED
|
@@ -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;
|
package/dist/installation.js
CHANGED
|
@@ -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'
|
|
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', '
|
|
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 ('
|
|
90
|
-
stringArray(value.
|
|
91
|
-
if (value.
|
|
92
|
-
invalid(`${path}.
|
|
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 & {
|