@mearl/preset 0.0.0-stage → 3.0.0-beta.0
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/LICENSE +201 -0
- package/README.md +57 -2
- package/dist/browserPreset.d.ts +22 -0
- package/dist/browserPreset.js +52 -0
- package/dist/browserSandbox.d.ts +47 -0
- package/dist/browserSandbox.js +141 -0
- package/dist/browserTransfer.d.ts +42 -0
- package/dist/browserTransfer.js +102 -0
- package/dist/browserTransport.d.ts +8 -0
- package/dist/browserTransport.js +64 -0
- package/dist/browserViews.d.ts +44 -0
- package/dist/browserViews.js +204 -0
- package/dist/bundle.d.ts +59 -0
- package/dist/bundle.js +219 -0
- package/dist/bundleFetch.d.ts +6 -0
- package/dist/bundleFetch.js +42 -0
- package/dist/bundleStore.d.ts +70 -0
- package/dist/bundleStore.js +478 -0
- package/dist/bundles.d.ts +22 -0
- package/dist/bundles.js +12 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/installation.d.ts +24 -0
- package/dist/installation.js +116 -0
- package/dist/manifest.d.ts +9 -0
- package/dist/manifest.js +145 -0
- package/dist/nodeSnapshot.d.ts +38 -0
- package/dist/nodeSnapshot.js +136 -0
- package/dist/pageRuntime.d.ts +19 -0
- package/dist/pageRuntime.js +47 -0
- package/dist/prepare.d.ts +34 -0
- package/dist/prepare.js +112 -0
- package/dist/preset.d.ts +24 -0
- package/dist/preset.js +53 -0
- package/dist/requestUi.d.ts +59 -0
- package/dist/requestUi.js +102 -0
- package/dist/resolve.d.ts +5 -0
- package/dist/resolve.js +134 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/package.json +35 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function record(value, allowed) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !allowed.includes(key))) {
|
|
3
|
+
throw new Error('Invalid installation configuration');
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function httpsUrl(value) {
|
|
8
|
+
if (typeof value !== 'string' || value.length > 4096)
|
|
9
|
+
throw new Error('Invalid installation URL');
|
|
10
|
+
const url = new URL(value);
|
|
11
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.hash)
|
|
12
|
+
throw new Error('Invalid installation URL');
|
|
13
|
+
return url;
|
|
14
|
+
}
|
|
15
|
+
/** One validated catalog is used by the website and extension; parameters cannot add install sources. */
|
|
16
|
+
export function parseInstallationConfig(value, publicOrigin) {
|
|
17
|
+
const input = record(value, ['schemaVersion', 'guide', 'presets', 'legacyPreset']);
|
|
18
|
+
if (input.schemaVersion !== 1)
|
|
19
|
+
throw new Error('Invalid installation schema');
|
|
20
|
+
const result = { schemaVersion: 1 };
|
|
21
|
+
if (input.guide !== undefined) {
|
|
22
|
+
const guide = record(input.guide, ['url', 'registry']);
|
|
23
|
+
const url = httpsUrl(guide.url);
|
|
24
|
+
const registry = httpsUrl(guide.registry);
|
|
25
|
+
if (url.origin === publicOrigin || registry.origin === publicOrigin || registry.search || !registry.pathname.endsWith('/'))
|
|
26
|
+
throw new Error('Invalid registry URL');
|
|
27
|
+
result.guide = { url: url.href, registry: registry.href };
|
|
28
|
+
}
|
|
29
|
+
if (input.presets !== undefined) {
|
|
30
|
+
if (!result.guide || !input.presets || typeof input.presets !== 'object' || Array.isArray(input.presets))
|
|
31
|
+
throw new Error('A preset catalog requires its guide');
|
|
32
|
+
const entries = Object.entries(input.presets);
|
|
33
|
+
if (entries.length > 32)
|
|
34
|
+
throw new Error('Installation catalog exceeds 32 presets');
|
|
35
|
+
result.presets = Object.fromEntries(entries.map(([id, value]) => {
|
|
36
|
+
if (!/^[a-z][a-z0-9-]{0,79}$/.test(id))
|
|
37
|
+
throw new Error('Invalid installation preset ID');
|
|
38
|
+
const entry = record(value, ['label', 'source', 'origins']);
|
|
39
|
+
if (typeof entry.label !== 'string' || !entry.label.trim() || entry.label.length > 120 || /[\u0000-\u001f\u007f]/.test(entry.label))
|
|
40
|
+
throw new Error('Invalid preset label');
|
|
41
|
+
const source = httpsUrl(entry.source).href;
|
|
42
|
+
if (!Array.isArray(entry.origins) || entry.origins.length > 64)
|
|
43
|
+
throw new Error('Expected preset permission origins');
|
|
44
|
+
const origins = [...new Set([`${new URL(source).origin}/*`, ...entry.origins.map(validateInstallationOrigin)])];
|
|
45
|
+
if (origins.length > 64)
|
|
46
|
+
throw new Error('Installation permission origins exceed 64 scopes');
|
|
47
|
+
return [id, { label: entry.label, source, origins }];
|
|
48
|
+
}));
|
|
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
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/** Explicit host scopes retain their selected schemes; download sources remain HTTPS. */
|
|
58
|
+
export function validateInstallationOrigin(value) {
|
|
59
|
+
if (typeof value !== 'string' || value.length > 4096 || !/^(https?|\*):\/\//.test(value) || !value.endsWith('/*'))
|
|
60
|
+
throw new Error('Invalid installation permission origin');
|
|
61
|
+
const scheme = value.slice(0, value.indexOf(':'));
|
|
62
|
+
const wildcard = /^(?:https?|\*):\/\/\*\./.test(value);
|
|
63
|
+
const normalized = value.replace(/^\*:/, 'https:').replace(/^(https?:\/\/)\*\./, '$1');
|
|
64
|
+
const url = new URL(normalized);
|
|
65
|
+
if (url.username || url.password || url.hash || url.hostname.includes('*') || url.pathname !== '/*' || url.search || !url.hostname || (wildcard && !url.hostname.includes('.')))
|
|
66
|
+
throw new Error('Invalid installation permission origin');
|
|
67
|
+
return `${scheme}://${wildcard ? '*.' : ''}${url.host}/*`;
|
|
68
|
+
}
|
|
69
|
+
/** Match both scheme and host against a catalog scope before loading a resource. */
|
|
70
|
+
export function matchesInstallationOrigin(scope, target) {
|
|
71
|
+
const value = validateInstallationOrigin(scope);
|
|
72
|
+
const scheme = value.slice(0, value.indexOf(':'));
|
|
73
|
+
if (scheme === '*' ? !['http:', 'https:'].includes(target.protocol) : `${scheme}:` !== target.protocol)
|
|
74
|
+
return false;
|
|
75
|
+
const wildcard = /^(?:https?|\*):\/\/\*\./.test(value);
|
|
76
|
+
const origin = new URL(value.replace(/^\*:/, 'https:').replace(/^(https?:\/\/)\*\./, '$1'));
|
|
77
|
+
return (!origin.port || origin.port === target.port) && (target.hostname === origin.hostname || wildcard && target.hostname.endsWith(`.${origin.hostname}`));
|
|
78
|
+
}
|
|
79
|
+
export const INSTALLATION_CONFIG_MAX_BYTES = 65_536;
|
|
80
|
+
export async function readInstallationJson(response, maxBytes) {
|
|
81
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > INSTALLATION_CONFIG_MAX_BYTES)
|
|
82
|
+
throw new Error('Invalid installation response size limit');
|
|
83
|
+
const reader = response.body?.getReader();
|
|
84
|
+
if (!reader)
|
|
85
|
+
throw new Error('Missing installation response');
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let length = 0;
|
|
88
|
+
try {
|
|
89
|
+
for (;;) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done)
|
|
92
|
+
break;
|
|
93
|
+
length += value.byteLength;
|
|
94
|
+
if (length > maxBytes)
|
|
95
|
+
throw new Error('Installation response is too large');
|
|
96
|
+
chunks.push(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await reader.cancel().catch(() => { });
|
|
101
|
+
reader.releaseLock();
|
|
102
|
+
}
|
|
103
|
+
const bytes = new Uint8Array(length);
|
|
104
|
+
let offset = 0;
|
|
105
|
+
for (const chunk of chunks) {
|
|
106
|
+
bytes.set(chunk, offset);
|
|
107
|
+
offset += chunk.length;
|
|
108
|
+
}
|
|
109
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
110
|
+
}
|
|
111
|
+
export async function loadInstallationConfig(url, publicOrigin, signal) {
|
|
112
|
+
const response = await fetch(url, { cache: 'no-store', credentials: 'omit', redirect: 'error', signal });
|
|
113
|
+
if (!response.ok || !response.headers.get('content-type')?.includes('application/json'))
|
|
114
|
+
throw new Error('Installation configuration unavailable');
|
|
115
|
+
return parseInstallationConfig(await readInstallationJson(response, INSTALLATION_CONFIG_MAX_BYTES), publicOrigin);
|
|
116
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { JsonObject, PresetManifest } from './types.js';
|
|
2
|
+
export declare class PresetError extends Error {
|
|
3
|
+
readonly code: 'PRESET_INVALID' | 'PRESET_CYCLE' | 'PRESET_INCOMPATIBLE' | 'PRESET_DEPENDENCY';
|
|
4
|
+
constructor(code: 'PRESET_INVALID' | 'PRESET_CYCLE' | 'PRESET_INCOMPATIBLE' | 'PRESET_DEPENDENCY', message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function isJsonObject(value: unknown): value is Record<string, unknown>;
|
|
7
|
+
/** Validates and clones JSON configuration without executing or evaluating it. */
|
|
8
|
+
export declare function clonePresetDefaults(value: unknown): JsonObject;
|
|
9
|
+
export declare function validatePreset(value: unknown): PresetManifest;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { valid, validRange } from 'semver';
|
|
2
|
+
export class PresetError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(`${code}: ${message}`);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'PresetError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const unsafeKeys = new Set(['__proto__', 'constructor', 'prototype']);
|
|
11
|
+
const componentId = /^[a-z][a-z0-9-]{0,79}$/;
|
|
12
|
+
function invalid(path, reason) {
|
|
13
|
+
throw new PresetError('PRESET_INVALID', `${path}: ${reason}`);
|
|
14
|
+
}
|
|
15
|
+
export function isJsonObject(value) {
|
|
16
|
+
return value !== null && typeof value === 'object' &&
|
|
17
|
+
!Array.isArray(value) &&
|
|
18
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
19
|
+
}
|
|
20
|
+
function object(value, path) {
|
|
21
|
+
if (!isJsonObject(value))
|
|
22
|
+
invalid(path, 'expected a plain object');
|
|
23
|
+
for (const key of Object.keys(value))
|
|
24
|
+
if (unsafeKeys.has(key))
|
|
25
|
+
invalid(path, `unsafe key ${key}`);
|
|
26
|
+
}
|
|
27
|
+
function fields(value, path, names) {
|
|
28
|
+
object(value, path);
|
|
29
|
+
for (const key of Object.keys(value)) {
|
|
30
|
+
if (!names.includes(key))
|
|
31
|
+
invalid(`${path}.${key}`, 'unknown field');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function text(value, path) {
|
|
35
|
+
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
36
|
+
invalid(path, 'expected a non-empty trimmed string');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function id(value, path) {
|
|
40
|
+
if (typeof value !== 'string' || !componentId.test(value))
|
|
41
|
+
invalid(path, 'invalid ID');
|
|
42
|
+
}
|
|
43
|
+
function range(value, path) {
|
|
44
|
+
text(value, path);
|
|
45
|
+
if (validRange(value) === null)
|
|
46
|
+
invalid(path, 'invalid version range');
|
|
47
|
+
}
|
|
48
|
+
function stringArray(value, path) {
|
|
49
|
+
if (!Array.isArray(value))
|
|
50
|
+
invalid(path, 'expected an array');
|
|
51
|
+
for (const [index, item] of value.entries())
|
|
52
|
+
text(item, `${path}[${index}]`);
|
|
53
|
+
if (new Set(value).size !== value.length)
|
|
54
|
+
invalid(path, 'duplicate values');
|
|
55
|
+
}
|
|
56
|
+
function json(value, path, depth = 0) {
|
|
57
|
+
if (depth > 32)
|
|
58
|
+
invalid(path, 'JSON nesting exceeds 32 levels');
|
|
59
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
60
|
+
return value;
|
|
61
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
62
|
+
return value;
|
|
63
|
+
if (Array.isArray(value))
|
|
64
|
+
return Array.from(value, (item, index) => json(item, `${path}[${index}]`, depth + 1));
|
|
65
|
+
object(value, path);
|
|
66
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, json(item, `${path}.${key}`, depth + 1)]));
|
|
67
|
+
}
|
|
68
|
+
/** Validates and clones JSON configuration without executing or evaluating it. */
|
|
69
|
+
export function clonePresetDefaults(value) {
|
|
70
|
+
object(value, 'defaults');
|
|
71
|
+
return json(value, 'defaults');
|
|
72
|
+
}
|
|
73
|
+
function component(value, path) {
|
|
74
|
+
fields(value, path, ['source', 'version', 'enabled', 'runtimes', 'dependsOn']);
|
|
75
|
+
if ('source' in value)
|
|
76
|
+
text(value.source, `${path}.source`);
|
|
77
|
+
if ('version' in value)
|
|
78
|
+
range(value.version, `${path}.version`);
|
|
79
|
+
if ('enabled' in value && typeof value.enabled !== 'boolean')
|
|
80
|
+
invalid(`${path}.enabled`, 'expected a boolean');
|
|
81
|
+
if ('runtimes' in value) {
|
|
82
|
+
stringArray(value.runtimes, `${path}.runtimes`);
|
|
83
|
+
if (value.runtimes.length === 0 || value.runtimes.some(item => item !== 'browser' && item !== 'node')) {
|
|
84
|
+
invalid(`${path}.runtimes`, 'expected browser and/or node');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if ('dependsOn' in value) {
|
|
88
|
+
stringArray(value.dependsOn, `${path}.dependsOn`);
|
|
89
|
+
for (const item of value.dependsOn)
|
|
90
|
+
id(item, `${path}.dependsOn`);
|
|
91
|
+
}
|
|
92
|
+
return json(value, path);
|
|
93
|
+
}
|
|
94
|
+
export function validatePreset(value) {
|
|
95
|
+
fields(value, 'preset', ['schemaVersion', 'id', 'version', 'extends', 'requires', 'packages', 'skills', 'providers', 'appProfiles', 'defaults', 'permissions']);
|
|
96
|
+
if (value.schemaVersion !== 1)
|
|
97
|
+
invalid('schemaVersion', 'only version 1 is supported');
|
|
98
|
+
id(value.id, 'id');
|
|
99
|
+
text(value.version, 'version');
|
|
100
|
+
if (valid(value.version) !== value.version)
|
|
101
|
+
invalid('version', 'expected an exact semantic version');
|
|
102
|
+
const result = { schemaVersion: 1, id: value.id, version: value.version };
|
|
103
|
+
if ('extends' in value) {
|
|
104
|
+
fields(value.extends, 'extends', ['source', 'version']);
|
|
105
|
+
text(value.extends.source, 'extends.source');
|
|
106
|
+
if ('version' in value.extends)
|
|
107
|
+
range(value.extends.version, 'extends.version');
|
|
108
|
+
result.extends = { source: value.extends.source, ...(value.extends.version === undefined ? {} : { version: value.extends.version }) };
|
|
109
|
+
}
|
|
110
|
+
if ('requires' in value) {
|
|
111
|
+
fields(value.requires, 'requires', ['mearlApi']);
|
|
112
|
+
if ('mearlApi' in value.requires)
|
|
113
|
+
range(value.requires.mearlApi, 'requires.mearlApi');
|
|
114
|
+
result.requires = { ...(value.requires.mearlApi === undefined ? {} : { mearlApi: value.requires.mearlApi }) };
|
|
115
|
+
}
|
|
116
|
+
for (const kind of ['packages', 'skills', 'providers']) {
|
|
117
|
+
if (!(kind in value))
|
|
118
|
+
continue;
|
|
119
|
+
const declarations = value[kind];
|
|
120
|
+
object(declarations, kind);
|
|
121
|
+
result[kind] = Object.fromEntries(Object.entries(declarations).map(([key, item]) => {
|
|
122
|
+
id(key, `${kind}.${key}`);
|
|
123
|
+
return [key, component(item, `${kind}.${key}`)];
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
if ('appProfiles' in value) {
|
|
127
|
+
object(value.appProfiles, 'appProfiles');
|
|
128
|
+
result.appProfiles = Object.fromEntries(Object.entries(value.appProfiles).map(([key, target]) => {
|
|
129
|
+
id(key, `appProfiles.${key}`);
|
|
130
|
+
text(target, `appProfiles.${key}`);
|
|
131
|
+
return [key, target];
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
if ('defaults' in value)
|
|
135
|
+
result.defaults = clonePresetDefaults(value.defaults);
|
|
136
|
+
if ('permissions' in value) {
|
|
137
|
+
fields(value.permissions, 'permissions', ['origins']);
|
|
138
|
+
if ('origins' in value.permissions)
|
|
139
|
+
stringArray(value.permissions.origins, 'permissions.origins');
|
|
140
|
+
result.permissions = { origins: [...(value.permissions.origins ?? [])] };
|
|
141
|
+
}
|
|
142
|
+
if (JSON.stringify(result).length > 262_144)
|
|
143
|
+
invalid('preset', 'manifest exceeds 256 Ki UTF-16 code units');
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { JsonObject } from './types.js';
|
|
2
|
+
export interface NodeCapabilitySelection {
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
modules: Record<string, {
|
|
5
|
+
sha256: string;
|
|
6
|
+
byteLength: number;
|
|
7
|
+
config?: JsonObject;
|
|
8
|
+
}>;
|
|
9
|
+
bindings: Record<string, {
|
|
10
|
+
module: string;
|
|
11
|
+
handler: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export interface NodeCapabilitySnapshot extends NodeCapabilitySelection {
|
|
15
|
+
revision: string;
|
|
16
|
+
}
|
|
17
|
+
export interface NodeCapabilityActivation extends NodeCapabilitySnapshot {
|
|
18
|
+
installationRevision?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const MAX_NODE_SELECTION_BYTES = 262144;
|
|
21
|
+
/** Explicit module configuration survives periods where a preset disables its module. */
|
|
22
|
+
export declare function validateNodeModuleConfig(value: Record<string, JsonObject>): Record<string, JsonObject>;
|
|
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>;
|
|
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>;
|
|
28
|
+
/** The installer receipt and Node selection share one atomic filesystem pointer. */
|
|
29
|
+
export declare function verifyNodeCapabilityActivation(input: NodeCapabilityActivation): Promise<NodeCapabilityActivation>;
|
|
30
|
+
export declare function prepareNodeBundleSnapshot(bundles: import('./bundle.js').PreparedBundle[], host: string, mearlApiVersion: string, config?: Record<string, JsonObject>): Promise<{
|
|
31
|
+
snapshot: NodeCapabilitySnapshot;
|
|
32
|
+
resources: Record<string, Uint8Array>;
|
|
33
|
+
}>;
|
|
34
|
+
/** Browser factories use the same exact module/configuration selection without a Node loader. */
|
|
35
|
+
export declare function prepareBrowserBundleSnapshot(bundles: import('./bundle.js').PreparedBundle[], host: string, mearlApiVersion: string, config?: Record<string, JsonObject>): Promise<{
|
|
36
|
+
snapshot: NodeCapabilitySnapshot;
|
|
37
|
+
resources: Record<string, Uint8Array>;
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_BYTES, bundleDigest, validateBundleManifest } from './bundle.js';
|
|
2
|
+
import { clonePresetDefaults, isJsonObject } from './manifest.js';
|
|
3
|
+
import { mergePresetDefaults } from './resolve.js';
|
|
4
|
+
export const MAX_NODE_SELECTION_BYTES = 262_144;
|
|
5
|
+
const identifier = /^[a-z][a-z0-9-]{0,79}$/;
|
|
6
|
+
const hookName = /^[a-z][a-zA-Z0-9_.-]{0,127}$/;
|
|
7
|
+
const digest = /^[a-f0-9]{64}$/;
|
|
8
|
+
function invalid(message) { throw new BundleError('BUNDLE_INVALID', message); }
|
|
9
|
+
function fields(value, allowed) {
|
|
10
|
+
if (!isJsonObject(value) || Object.keys(value).some(key => !allowed.includes(key)))
|
|
11
|
+
invalid('Invalid Node capability selection structure');
|
|
12
|
+
}
|
|
13
|
+
/** Explicit module configuration survives periods where a preset disables its module. */
|
|
14
|
+
export function validateNodeModuleConfig(value) {
|
|
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');
|
|
18
|
+
for (const [id, config] of Object.entries(configs))
|
|
19
|
+
if (!identifier.test(id) || !isJsonObject(config))
|
|
20
|
+
invalid('Node module configuration must map IDs to JSON objects');
|
|
21
|
+
return configs;
|
|
22
|
+
}
|
|
23
|
+
/** Preset defaults are inherited; explicit host configuration stays a separate override. */
|
|
24
|
+
export function resolvePresetModuleConfig(defaults, overrides = {}) {
|
|
25
|
+
const value = defaults.capabilities === undefined ? {} : defaults.capabilities;
|
|
26
|
+
if (!isJsonObject(value))
|
|
27
|
+
invalid('Capability defaults must map module IDs to configuration objects');
|
|
28
|
+
return validateNodeModuleConfig(mergePresetDefaults(validateNodeModuleConfig(value), validateNodeModuleConfig(overrides)));
|
|
29
|
+
}
|
|
30
|
+
/** Exact content and bindings are selected together; filesystem and code loading belong to hosts. */
|
|
31
|
+
export async function prepareNodeCapabilitySnapshot(input) {
|
|
32
|
+
const value = clonePresetDefaults(input);
|
|
33
|
+
fields(value, ['schemaVersion', 'modules', 'bindings']);
|
|
34
|
+
if (value.schemaVersion !== 1 || !isJsonObject(value.modules) || !isJsonObject(value.bindings))
|
|
35
|
+
invalid('Invalid Node capability selection');
|
|
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');
|
|
38
|
+
const modules = {};
|
|
39
|
+
let total = 0;
|
|
40
|
+
for (const id of Object.keys(value.modules).sort()) {
|
|
41
|
+
if (!identifier.test(id))
|
|
42
|
+
invalid('Invalid Node module ID');
|
|
43
|
+
const entry = value.modules[id];
|
|
44
|
+
fields(entry, ['sha256', 'byteLength', 'config']);
|
|
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');
|
|
47
|
+
if (entry.config !== undefined && !isJsonObject(entry.config))
|
|
48
|
+
invalid('Node module configuration must be a JSON object');
|
|
49
|
+
total += entry.byteLength;
|
|
50
|
+
if (total > MAX_BROWSER_STORAGE_BYTES)
|
|
51
|
+
throw new BundleError('BUNDLE_QUOTA', 'Node capability resources exceed 64 MiB');
|
|
52
|
+
modules[id] = { sha256: entry.sha256, byteLength: entry.byteLength, ...(entry.config === undefined ? {} : { config: clonePresetDefaults(entry.config) }) };
|
|
53
|
+
}
|
|
54
|
+
const bindings = {};
|
|
55
|
+
for (const name of Object.keys(value.bindings).sort()) {
|
|
56
|
+
if (!hookName.test(name))
|
|
57
|
+
invalid('Invalid Node hook name');
|
|
58
|
+
const binding = value.bindings[name];
|
|
59
|
+
fields(binding, ['module', 'handler']);
|
|
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');
|
|
62
|
+
bindings[name] = { module: binding.module, handler: binding.handler };
|
|
63
|
+
}
|
|
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');
|
|
67
|
+
return { ...selection, revision: await bundleDigest(new TextEncoder().encode(JSON.stringify(selection))) };
|
|
68
|
+
}
|
|
69
|
+
export async function verifyNodeCapabilitySnapshot(input) {
|
|
70
|
+
const value = clonePresetDefaults(input);
|
|
71
|
+
fields(value, ['schemaVersion', 'modules', 'bindings', 'revision']);
|
|
72
|
+
if (typeof value.revision !== 'string' || !digest.test(value.revision))
|
|
73
|
+
invalid('Invalid Node capability revision');
|
|
74
|
+
const { revision, ...selection } = value;
|
|
75
|
+
const prepared = await prepareNodeCapabilitySnapshot(selection);
|
|
76
|
+
if (prepared.revision !== revision)
|
|
77
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Node capability selection revision does not match');
|
|
78
|
+
return prepared;
|
|
79
|
+
}
|
|
80
|
+
/** The installer receipt and Node selection share one atomic filesystem pointer. */
|
|
81
|
+
export async function verifyNodeCapabilityActivation(input) {
|
|
82
|
+
const value = clonePresetDefaults(input);
|
|
83
|
+
const { installationRevision, ...selection } = value;
|
|
84
|
+
if (installationRevision !== undefined && (typeof installationRevision !== 'string' || !digest.test(installationRevision)))
|
|
85
|
+
invalid('Invalid capability installation revision');
|
|
86
|
+
const snapshot = await verifyNodeCapabilitySnapshot(selection);
|
|
87
|
+
return { ...snapshot, ...(installationRevision === undefined ? {} : { installationRevision: installationRevision }) };
|
|
88
|
+
}
|
|
89
|
+
async function prepareRuntimeBundleSnapshot(bundles, kind, host, mearlApiVersion, config) {
|
|
90
|
+
if (!identifier.test(host))
|
|
91
|
+
invalid('Invalid Node runtime host');
|
|
92
|
+
const configs = validateNodeModuleConfig(config);
|
|
93
|
+
const modules = {};
|
|
94
|
+
const bindings = {};
|
|
95
|
+
const resources = {};
|
|
96
|
+
const pending = [];
|
|
97
|
+
let total = 0;
|
|
98
|
+
for (const bundle of bundles) {
|
|
99
|
+
const manifest = validateBundleManifest(bundle.manifest, mearlApiVersion);
|
|
100
|
+
const runtime = manifest.contributions?.[kind]?.[host];
|
|
101
|
+
if (!runtime)
|
|
102
|
+
continue;
|
|
103
|
+
if (Object.hasOwn(modules, manifest.id))
|
|
104
|
+
invalid('Duplicate Node module ID');
|
|
105
|
+
const metadata = manifest.resources[runtime.entry];
|
|
106
|
+
if (!Object.hasOwn(bundle.resources, runtime.entry))
|
|
107
|
+
invalid('Node module resource is missing');
|
|
108
|
+
total += metadata.byteLength;
|
|
109
|
+
if (pending.length >= 64 || total > MAX_BROWSER_STORAGE_BYTES)
|
|
110
|
+
throw new BundleError('BUNDLE_QUOTA', 'Node capability resources exceed the selection limit');
|
|
111
|
+
pending.push({ id: manifest.id, bytes: bundle.resources[runtime.entry], sha256: metadata.sha256 });
|
|
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]) } : {}) };
|
|
115
|
+
for (const [name, handler] of Object.entries(runtime.bindings)) {
|
|
116
|
+
if (Object.hasOwn(bindings, name))
|
|
117
|
+
invalid('Node hook has multiple providers');
|
|
118
|
+
bindings[name] = { module: manifest.id, handler };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Capture the complete accepted batch before the first asynchronous digest.
|
|
122
|
+
const captured = pending.map(resource => ({ ...resource, bytes: Uint8Array.from(resource.bytes) }));
|
|
123
|
+
for (const resource of captured) {
|
|
124
|
+
if (await bundleDigest(resource.bytes) !== resource.sha256)
|
|
125
|
+
throw new BundleError('BUNDLE_INTEGRITY', `Node module digest does not match: ${resource.id}`);
|
|
126
|
+
resources[resource.sha256] = resource.bytes;
|
|
127
|
+
}
|
|
128
|
+
return { snapshot: await prepareNodeCapabilitySnapshot({ schemaVersion: 1, modules, bindings }), resources };
|
|
129
|
+
}
|
|
130
|
+
export function prepareNodeBundleSnapshot(bundles, host, mearlApiVersion, config = {}) {
|
|
131
|
+
return prepareRuntimeBundleSnapshot(bundles, 'nodeRuntimes', host, mearlApiVersion, config);
|
|
132
|
+
}
|
|
133
|
+
/** Browser factories use the same exact module/configuration selection without a Node loader. */
|
|
134
|
+
export function prepareBrowserBundleSnapshot(bundles, host, mearlApiVersion, config = {}) {
|
|
135
|
+
return prepareRuntimeBundleSnapshot(bundles, 'browserRuntimes', host, mearlApiVersion, config);
|
|
136
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A content-addressed MAIN world installer, delivered through a prepared debugger bootstrap. */
|
|
2
|
+
export interface PageRuntimeBinding {
|
|
3
|
+
id: string;
|
|
4
|
+
sha256: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PageRuntimeConfig {
|
|
7
|
+
pageRuntime?: PageRuntimeBinding;
|
|
8
|
+
}
|
|
9
|
+
type Installer = (config: PageRuntimeConfig) => void;
|
|
10
|
+
interface PageRuntimeHost {
|
|
11
|
+
install<Config extends PageRuntimeConfig>(config: Config, fallback?: (config: Config) => void): void;
|
|
12
|
+
register(sha256: string, installer: Installer): void;
|
|
13
|
+
}
|
|
14
|
+
/** Self-contained because the debugger serializes this function into each document. */
|
|
15
|
+
export declare function installPageRuntimeHost(): PageRuntimeHost;
|
|
16
|
+
/** The returned source is executed only in the controlled page, never in the extension host. */
|
|
17
|
+
export declare function buildPageRuntimeRegistrationSource(binding: PageRuntimeBinding, source: string, installerGlobal: string): string;
|
|
18
|
+
export declare function buildPageRuntimeCallSource(configExpression: string): string;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Self-contained because the debugger serializes this function into each document. */
|
|
2
|
+
export function installPageRuntimeHost() {
|
|
3
|
+
const root = window;
|
|
4
|
+
if (root.__mearlPageRuntimeHost__)
|
|
5
|
+
return root.__mearlPageRuntimeHost__;
|
|
6
|
+
const installers = new Map();
|
|
7
|
+
const pending = new Map();
|
|
8
|
+
const host = {
|
|
9
|
+
install(config, fallback) {
|
|
10
|
+
const binding = config.pageRuntime;
|
|
11
|
+
if (!binding) {
|
|
12
|
+
fallback?.(config);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const installer = installers.get(binding.sha256);
|
|
16
|
+
if (installer) {
|
|
17
|
+
installer(config);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const waiting = pending.get(binding.sha256) ?? [];
|
|
21
|
+
waiting.push(config);
|
|
22
|
+
pending.set(binding.sha256, waiting);
|
|
23
|
+
},
|
|
24
|
+
register(sha256, installer) {
|
|
25
|
+
if (typeof installer !== 'function')
|
|
26
|
+
throw new Error('A page runtime must export an install function');
|
|
27
|
+
if (installers.has(sha256))
|
|
28
|
+
return;
|
|
29
|
+
installers.set(sha256, installer);
|
|
30
|
+
const waiting = pending.get(sha256) ?? [];
|
|
31
|
+
pending.delete(sha256);
|
|
32
|
+
for (const config of waiting)
|
|
33
|
+
installer(config);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(root, '__mearlPageRuntimeHost__', { configurable: true, value: host });
|
|
37
|
+
return host;
|
|
38
|
+
}
|
|
39
|
+
/** The returned source is executed only in the controlled page, never in the extension host. */
|
|
40
|
+
export function buildPageRuntimeRegistrationSource(binding, source, installerGlobal) {
|
|
41
|
+
if (!/^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test(binding.id) || !/^[a-f0-9]{64}$/.test(binding.sha256) || !source.trim())
|
|
42
|
+
throw new Error('Invalid page runtime binding');
|
|
43
|
+
return `;(() => { const host=(${installPageRuntimeHost.toString()})(); const module={exports:{}};\n(function(module,exports){\n${source}\n})(module,module.exports);\nhost.register(${JSON.stringify(binding.sha256)},module.exports.install); Object.defineProperty(window,${JSON.stringify(installerGlobal)},{configurable:true,value:config=>host.install(config)}); })();`;
|
|
44
|
+
}
|
|
45
|
+
export function buildPageRuntimeCallSource(configExpression) {
|
|
46
|
+
return `(${installPageRuntimeHost.toString()})().install(${configExpression});`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type BundleLoader, type PreparedBundle } from './bundle.js';
|
|
2
|
+
import type { PresetManifest, PresetRuntime, ResolvedComponent, ResolvedPreset } from './types.js';
|
|
3
|
+
export interface PreparePresetOptions {
|
|
4
|
+
load: BundleLoader;
|
|
5
|
+
mearlApiVersion: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
/** One budget covers inheritance manifests, package manifests and resource bytes. */
|
|
8
|
+
maxDownloadBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PresetGraphSnapshot {
|
|
11
|
+
source: string;
|
|
12
|
+
manifests: Record<string, PresetManifest>;
|
|
13
|
+
bundleRevisions: Record<string, string>;
|
|
14
|
+
revision: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function presetSnapshotRevision(value: object): Promise<string>;
|
|
17
|
+
/** Each execution location must be able to satisfy its own package dependencies. */
|
|
18
|
+
export declare function presetPackages(preset: ResolvedPreset, runtimes: PresetRuntime[]): Array<[string, ResolvedComponent]>;
|
|
19
|
+
/** Shared preparation performs no installation, code execution or permission changes. */
|
|
20
|
+
export declare function preparePresetPackages(source: string, options: PreparePresetOptions, runtimes: PresetRuntime[]): Promise<{
|
|
21
|
+
source: string;
|
|
22
|
+
manifests: Record<string, PresetManifest>;
|
|
23
|
+
preset: ResolvedPreset;
|
|
24
|
+
bundles: PreparedBundle[];
|
|
25
|
+
}>;
|
|
26
|
+
/** The exact graph is checked offline; installed resources are verified by the target store. */
|
|
27
|
+
export declare function verifyPresetGraph(value: PresetGraphSnapshot & {
|
|
28
|
+
schemaVersion?: 1;
|
|
29
|
+
}, mearlApiVersion: string, runtimes: PresetRuntime[], full: boolean): Promise<{
|
|
30
|
+
snapshot: PresetGraphSnapshot & {
|
|
31
|
+
schemaVersion?: 1;
|
|
32
|
+
};
|
|
33
|
+
preset: ResolvedPreset;
|
|
34
|
+
}>;
|