@mearl/preset 3.0.0-beta.0 → 3.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -18
- package/README.zh-CN.md +106 -0
- 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 +6 -2
- package/dist/index.js +3 -1
- package/dist/installation.d.ts +5 -1
- package/dist/installation.js +19 -6
- package/dist/management.d.ts +10 -0
- package/dist/management.js +36 -0
- package/dist/manifest.js +26 -8
- 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 +7 -0
- package/dist/toolboxCatalogs.d.ts +8 -0
- package/dist/toolboxCatalogs.js +34 -0
- package/dist/types.d.ts +4 -1
- package/installation-site.json +4 -0
- package/package.json +13 -2
|
@@ -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,9 @@
|
|
|
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 {
|
|
5
|
-
export type {
|
|
4
|
+
export { parseSetupManagement, setupManagementCommand, setupRuntimeCompatible } from './management.js';
|
|
5
|
+
export type { SetupManagement } from './management.js';
|
|
6
|
+
export { prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilitySnapshot, verifyCapabilityActivation, validateCapabilityConfig, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from './capabilitySnapshot.js';
|
|
7
|
+
export type { CapabilitySelection, CapabilitySnapshot, CapabilityActivation } from './capabilitySnapshot.js';
|
|
8
|
+
export { parseToolboxCatalogs } from './toolboxCatalogs.js';
|
|
9
|
+
export type { ToolboxCatalogConfig, ToolboxCatalogConfigs } from './toolboxCatalogs.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { PresetError, validatePreset, clonePresetDefaults } from './manifest.js';
|
|
2
2
|
export { mergePresetDefaults, resolvePreset } from './resolve.js';
|
|
3
|
-
export {
|
|
3
|
+
export { parseSetupManagement, setupManagementCommand, setupRuntimeCompatible } from './management.js';
|
|
4
|
+
export { prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilitySnapshot, verifyCapabilityActivation, validateCapabilityConfig, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from './capabilitySnapshot.js';
|
|
5
|
+
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SetupManagement {
|
|
2
|
+
package: string;
|
|
3
|
+
version: string;
|
|
4
|
+
registry?: string;
|
|
5
|
+
runtime?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Declarative installation guidance; never accepts commands, arguments or executable hooks. */
|
|
8
|
+
export declare function parseSetupManagement(value: unknown): SetupManagement | undefined;
|
|
9
|
+
export declare function setupManagementCommand(setup: SetupManagement): string;
|
|
10
|
+
export declare function setupRuntimeCompatible(setup: SetupManagement, version: string, targetLine?: string): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { valid, validRange, satisfies, subset } from 'semver';
|
|
2
|
+
import { isJsonObject, PresetError } from './manifest.js';
|
|
3
|
+
/** Declarative installation guidance; never accepts commands, arguments or executable hooks. */
|
|
4
|
+
export function parseSetupManagement(value) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return;
|
|
7
|
+
const invalid = () => { throw new PresetError('PRESET_INVALID', 'Invalid defaults.management.setup declaration'); };
|
|
8
|
+
if (!isJsonObject(value) || Object.keys(value).some(key => key !== 'setup') || !isJsonObject(value.setup))
|
|
9
|
+
return invalid();
|
|
10
|
+
const setup = value.setup;
|
|
11
|
+
if (Object.keys(setup).some(key => !['package', 'version', 'registry', 'runtime'].includes(key)) ||
|
|
12
|
+
typeof setup.package !== 'string' || setup.package.length > 214 || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(setup.package) ||
|
|
13
|
+
typeof setup.version !== 'string' || setup.version.length > 64 || !(valid(setup.version) === setup.version || /^[a-z][a-z0-9.-]*$/.test(setup.version)))
|
|
14
|
+
return invalid();
|
|
15
|
+
if (setup.registry !== undefined) {
|
|
16
|
+
if (typeof setup.registry !== 'string' || setup.registry.length > 2048 || !/^https:\/\/[a-zA-Z0-9./:_-]+$/.test(setup.registry))
|
|
17
|
+
return invalid();
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(setup.registry);
|
|
20
|
+
if (!url.hostname || url.username || url.password || url.search || url.hash)
|
|
21
|
+
return invalid();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return invalid();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (setup.runtime !== undefined && (typeof setup.runtime !== 'string' || setup.runtime.length > 128 || !validRange(setup.runtime)))
|
|
28
|
+
return invalid();
|
|
29
|
+
return { ...setup };
|
|
30
|
+
}
|
|
31
|
+
export function setupManagementCommand(setup) {
|
|
32
|
+
return `npx${setup.registry ? ` --registry=${setup.registry}` : ''} ${setup.package}@${setup.version}`;
|
|
33
|
+
}
|
|
34
|
+
export function setupRuntimeCompatible(setup, version, targetLine) {
|
|
35
|
+
return !setup.runtime || (satisfies(version, setup.runtime, { includePrerelease: true }) && (!targetLine || subset(targetLine, setup.runtime, { includePrerelease: true })));
|
|
36
|
+
}
|
package/dist/manifest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateInstallationOrigin } from './installation.js';
|
|
1
2
|
import { valid, validRange } from 'semver';
|
|
2
3
|
export class PresetError extends Error {
|
|
3
4
|
code;
|
|
@@ -70,18 +71,25 @@ export function clonePresetDefaults(value) {
|
|
|
70
71
|
object(value, 'defaults');
|
|
71
72
|
return json(value, 'defaults');
|
|
72
73
|
}
|
|
73
|
-
function component(value, path) {
|
|
74
|
-
fields(value, path, ['source', 'version', 'enabled', '
|
|
74
|
+
function component(value, path, kind) {
|
|
75
|
+
fields(value, path, ['source', 'version', 'enabled', 'targets', 'dependsOn', 'migrateFrom']);
|
|
75
76
|
if ('source' in value)
|
|
76
77
|
text(value.source, `${path}.source`);
|
|
77
78
|
if ('version' in value)
|
|
78
79
|
range(value.version, `${path}.version`);
|
|
80
|
+
if ('migrateFrom' in value) {
|
|
81
|
+
if (kind !== 'skills')
|
|
82
|
+
invalid(`${path}.migrateFrom`, 'only Skills support source migration');
|
|
83
|
+
stringArray(value.migrateFrom, `${path}.migrateFrom`);
|
|
84
|
+
if (value.migrateFrom.length > 16)
|
|
85
|
+
invalid(`${path}.migrateFrom`, 'exceeds 16 sources');
|
|
86
|
+
}
|
|
79
87
|
if ('enabled' in value && typeof value.enabled !== 'boolean')
|
|
80
88
|
invalid(`${path}.enabled`, 'expected a boolean');
|
|
81
|
-
if ('
|
|
82
|
-
stringArray(value.
|
|
83
|
-
if (value.
|
|
84
|
-
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');
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
if ('dependsOn' in value) {
|
|
@@ -120,7 +128,7 @@ export function validatePreset(value) {
|
|
|
120
128
|
object(declarations, kind);
|
|
121
129
|
result[kind] = Object.fromEntries(Object.entries(declarations).map(([key, item]) => {
|
|
122
130
|
id(key, `${kind}.${key}`);
|
|
123
|
-
return [key, component(item, `${kind}.${key}
|
|
131
|
+
return [key, component(item, `${kind}.${key}`, kind)];
|
|
124
132
|
}));
|
|
125
133
|
}
|
|
126
134
|
if ('appProfiles' in value) {
|
|
@@ -137,7 +145,17 @@ export function validatePreset(value) {
|
|
|
137
145
|
fields(value.permissions, 'permissions', ['origins']);
|
|
138
146
|
if ('origins' in value.permissions)
|
|
139
147
|
stringArray(value.permissions.origins, 'permissions.origins');
|
|
140
|
-
|
|
148
|
+
const origins = value.permissions.origins ?? [];
|
|
149
|
+
if (origins.length > 64)
|
|
150
|
+
invalid('permissions.origins', 'exceeds 64 scopes');
|
|
151
|
+
result.permissions = { origins: [...new Set(origins.map((origin, index) => {
|
|
152
|
+
try {
|
|
153
|
+
return validateInstallationOrigin(origin);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return invalid(`permissions.origins[${index}]`, 'expected an explicit HTTP host scope');
|
|
157
|
+
}
|
|
158
|
+
}))] };
|
|
141
159
|
}
|
|
142
160
|
if (JSON.stringify(result).length > 262_144)
|
|
143
161
|
invalid('preset', 'manifest exceeds 256 Ki UTF-16 code units');
|
|
@@ -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.
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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');
|
package/dist/preset.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { browserPresetSnapshot } from './browserPreset.js';
|
|
2
|
-
import { preparePresetPackages, presetSnapshotRevision, verifyPresetGraph } from './prepare.js';
|
|
2
|
+
import { preparePresetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets } from './prepare.js';
|
|
3
3
|
import { satisfies } from 'semver';
|
|
4
4
|
import { BundleError, MAX_BROWSER_STORAGE_BYTES, verifyBundle } from './bundle.js';
|
|
5
5
|
export async function preparePreset(source, options) {
|
|
@@ -9,6 +9,7 @@ export async function preparePreset(source, options) {
|
|
|
9
9
|
source: prepared.source,
|
|
10
10
|
manifests: prepared.manifests,
|
|
11
11
|
bundleRevisions: Object.fromEntries(prepared.bundles.map(bundle => [bundle.manifest.id, bundle.revision])),
|
|
12
|
+
packageTargets: prepared.packageTargets,
|
|
12
13
|
};
|
|
13
14
|
const snapshot = { ...data, revision: await presetSnapshotRevision(data) };
|
|
14
15
|
const browser = await browserPresetSnapshot(prepared.source, prepared.manifests, prepared.preset, prepared.bundles);
|
|
@@ -44,6 +45,7 @@ export async function verifyPreparedPreset(value, mearlApiVersion) {
|
|
|
44
45
|
Promise.all(value.bundles.map(bundle => verifyBundle(bundle, mearlApiVersion))),
|
|
45
46
|
]);
|
|
46
47
|
const { snapshot, preset } = graph;
|
|
48
|
+
verifyPackageTargets(preset, bundles);
|
|
47
49
|
if (bundles.length !== Object.keys(snapshot.bundleRevisions).length || new Set(bundles.map(bundle => bundle.manifest.id)).size !== bundles.length || bundles.some(bundle => {
|
|
48
50
|
const entry = preset.packages[bundle.manifest.id];
|
|
49
51
|
return snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision || entry?.source !== bundle.source || (entry.version && !satisfies(bundle.manifest.version, entry.version));
|
package/dist/requestUi.d.ts
CHANGED
|
@@ -49,7 +49,9 @@ export interface SelectedBrowserRequestUi {
|
|
|
49
49
|
definition: BrowserRequestUi;
|
|
50
50
|
}
|
|
51
51
|
export declare function validateBrowserUiText(value: unknown): void;
|
|
52
|
-
export declare function browserUiHttpUrl(value: unknown
|
|
52
|
+
export declare function browserUiHttpUrl(value: unknown, options?: {
|
|
53
|
+
allowFragment?: boolean;
|
|
54
|
+
}): string;
|
|
53
55
|
export declare function validateBrowserRequestUi(input: unknown): BrowserRequestUi;
|
|
54
56
|
/** Contribution validation is pure; factories and UI side effects belong to the host. */
|
|
55
57
|
export declare function selectBrowserRequestUi(bundles: PreparedBundle[], apiVersion: string): SelectedBrowserRequestUi | undefined;
|
package/dist/requestUi.js
CHANGED
|
@@ -10,7 +10,7 @@ export function validateBrowserUiText(value) {
|
|
|
10
10
|
if (typeof value.en !== 'string' || !value.en.trim() || Object.values(value).some(entry => typeof entry !== 'string' || entry.length > 1024 || !entry.trim()))
|
|
11
11
|
invalid('Request UI text requires bounded translations');
|
|
12
12
|
}
|
|
13
|
-
export function browserUiHttpUrl(value) {
|
|
13
|
+
export function browserUiHttpUrl(value, options = {}) {
|
|
14
14
|
if (typeof value !== 'string' || value.length > 4096)
|
|
15
15
|
invalid('Invalid request UI URL');
|
|
16
16
|
let url;
|
|
@@ -20,7 +20,7 @@ export function browserUiHttpUrl(value) {
|
|
|
20
20
|
catch {
|
|
21
21
|
invalid('Invalid request UI URL');
|
|
22
22
|
}
|
|
23
|
-
if (url.username || url.password || url.hash || url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))
|
|
23
|
+
if (url.username || url.password || !options.allowFragment && url.hash || url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))
|
|
24
24
|
invalid('Request UI requires credential-free HTTPS or loopback URLs');
|
|
25
25
|
return url.href;
|
|
26
26
|
}
|
package/dist/resolve.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseToolboxCatalogs } from './toolboxCatalogs.js';
|
|
2
|
+
import { parseSetupManagement } from './management.js';
|
|
1
3
|
import { satisfies, valid } from 'semver';
|
|
2
4
|
import { clonePresetDefaults, isJsonObject, PresetError, validatePreset } from './manifest.js';
|
|
3
5
|
const kinds = ['packages', 'skills', 'providers'];
|
|
@@ -118,6 +120,7 @@ export async function resolvePreset(source, options) {
|
|
|
118
120
|
...result[kind][id],
|
|
119
121
|
...patch,
|
|
120
122
|
...(patch.source === undefined ? {} : { source: componentSource(kind, patch.source, url) }),
|
|
123
|
+
...(patch.migrateFrom === undefined ? {} : { migrateFrom: patch.migrateFrom.map(source => componentSource(kind, source, url)) }),
|
|
121
124
|
declaredBy: url,
|
|
122
125
|
};
|
|
123
126
|
}
|
|
@@ -126,6 +129,10 @@ export async function resolvePreset(source, options) {
|
|
|
126
129
|
return result;
|
|
127
130
|
};
|
|
128
131
|
const result = await load(sourceUrl(source));
|
|
132
|
+
parseSetupManagement(result.defaults.management);
|
|
133
|
+
const extension = result.defaults.extension;
|
|
134
|
+
if (isJsonObject(extension))
|
|
135
|
+
parseToolboxCatalogs(extension.toolCatalogs);
|
|
129
136
|
// Dependency validity is checked on the final graph, so children may complete or disable parent entries.
|
|
130
137
|
for (const kind of kinds)
|
|
131
138
|
validateDependencies(kind, result[kind]);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ToolboxCatalogConfig {
|
|
2
|
+
url?: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export type ToolboxCatalogConfigs = Record<string, ToolboxCatalogConfig>;
|
|
7
|
+
/** Stable keys let child presets patch or disable a parent directory through defaults merging. */
|
|
8
|
+
export declare function parseToolboxCatalogs(input: unknown): ToolboxCatalogConfigs;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isJsonObject, PresetError } from './manifest.js';
|
|
2
|
+
/** Stable keys let child presets patch or disable a parent directory through defaults merging. */
|
|
3
|
+
export function parseToolboxCatalogs(input) {
|
|
4
|
+
if (input === undefined)
|
|
5
|
+
return {};
|
|
6
|
+
function invalid() { throw new PresetError('PRESET_INVALID', 'Invalid extension.toolCatalogs configuration'); }
|
|
7
|
+
if (!isJsonObject(input) || Object.keys(input).length > 16)
|
|
8
|
+
invalid();
|
|
9
|
+
return Object.fromEntries(Object.entries(input).map(([id, entry]) => {
|
|
10
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(id) || id === 'official' || !isJsonObject(entry) || Object.keys(entry).some(key => !['url', 'label', 'enabled'].includes(key)))
|
|
11
|
+
invalid();
|
|
12
|
+
if (entry.enabled !== undefined && typeof entry.enabled !== 'boolean')
|
|
13
|
+
invalid();
|
|
14
|
+
if (entry.label !== undefined && (typeof entry.label !== 'string' || !entry.label.trim() || entry.label.length > 120))
|
|
15
|
+
invalid();
|
|
16
|
+
let url;
|
|
17
|
+
if (entry.url !== undefined) {
|
|
18
|
+
if (typeof entry.url !== 'string' || entry.url.length > 2048)
|
|
19
|
+
invalid();
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(entry.url);
|
|
22
|
+
if (parsed.username || parsed.password || parsed.hash || !(parsed.protocol === 'https:' || parsed.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname)))
|
|
23
|
+
invalid();
|
|
24
|
+
url = parsed.href;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
invalid();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (entry.enabled !== false)
|
|
31
|
+
invalid();
|
|
32
|
+
return [id, { ...(url ? { url } : {}), ...(entry.label !== undefined ? { label: entry.label } : {}), ...(entry.enabled !== undefined ? { enabled: entry.enabled } : {}) }];
|
|
33
|
+
}));
|
|
34
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,8 +8,11 @@ export interface PresetComponent {
|
|
|
8
8
|
/** Bundle URL for packages; installer-specific source for skills and providers. */
|
|
9
9
|
source?: string;
|
|
10
10
|
version?: string;
|
|
11
|
+
/** Previous Skill sources eligible for migration only when setup owns the installation. */
|
|
12
|
+
migrateFrom?: string[];
|
|
11
13
|
enabled?: boolean;
|
|
12
|
-
|
|
14
|
+
/** Optional activation subset; supported locations are defined by the package. */
|
|
15
|
+
targets?: PresetRuntime[];
|
|
13
16
|
dependsOn?: string[];
|
|
14
17
|
}
|
|
15
18
|
export interface PresetReference {
|