@mearl/preset 3.0.0-beta.9 → 3.0.1

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.en.md ADDED
@@ -0,0 +1,110 @@
1
+ # @mearl/preset
2
+
3
+ [简体中文](README.md) | [English](README.en.md)
4
+
5
+ Use a Preset to distribute a configured set of Mearl capabilities, Skills, and Providers. This package resolves the manifest and prepares the resources that browser and Node hosts use.
6
+
7
+ See the [capability model](../../docs/capability-model.en.md) for 3.x declarations, configuration, catalogs, and object storage. Contributions infer supported targets; optional Preset `targets` selects a subset. Operations and logins belong to packages; configuration overrides belong to Presets and users.
8
+
9
+ See [browser bundles](../../docs/browser-bundles.en.md) for preparation and local storage, and [browser Presets](../../docs/browser-presets.en.md) for atomic activation and App implementation mappings. Both are exported through `@mearl/preset/bundles`.
10
+
11
+ Browser and Node share one inheritance resolution and resource download; see [complete resource preparation](../../docs/preset-preparation.en.md). `preparePreset` returns the full exact graph and a compatible browser projection. Node module selection reuses the same bytes. Preparation executes no code and installs no external environment.
12
+
13
+ The main entry shares preset validation and inheritance resolution across browsers and Node, without filesystem, network, credential, or browser permission side effects. Callers supply manifest loading. Optional `@mearl/preset/bundles` provides resource validation, bounded downloads, and explicitly invoked browser-local storage; see [browser bundles](../../docs/browser-bundles.en.md).
14
+
15
+ `@mearl/preset/pageRuntime` provides the page runtime host and source generation functions for standalone page injection scripts.
16
+
17
+ ```ts
18
+ import { resolvePreset, mergePresetDefaults } from '@mearl/preset';
19
+
20
+ const preset = await resolvePreset('https://example.com/presets/team.json', {
21
+ mearlApiVersion: '1.0.0',
22
+ async load(source) {
23
+ const response = await fetch(source);
24
+ if (!response.ok) throw new Error(`Preset load failed: ${response.status}`);
25
+ return response.json();
26
+ },
27
+ });
28
+
29
+ const effectiveSettings = mergePresetDefaults(preset.defaults, userSettings);
30
+ ```
31
+
32
+ Loaders check source policy, response size, redirects, authentication, and cancellation; the example only demonstrates the contract. Browser loaders should restrict URLs according to product source policies. The resolver does not automatically trust HTTP(S)/file URLs.
33
+
34
+ See [Node capabilities](../../docs/node-capabilities.en.md) for module snapshots and host loading contracts.
35
+
36
+ ## Manifest
37
+
38
+ ```json
39
+ {
40
+ "schemaVersion": 1,
41
+ "id": "team",
42
+ "version": "1.0.0",
43
+ "extends": { "source": "../base/preset.json", "version": "^1" },
44
+ "requires": { "mearlApi": "^1" },
45
+ "packages": {
46
+ "capture": { "source": "./capture/package.json", "targets": ["browser"] },
47
+ "unused": { "enabled": false }
48
+ },
49
+ "skills": {},
50
+ "providers": {},
51
+ "defaults": { "simulation": { "app": "sample" } },
52
+ "permissions": { "origins": ["https://*.example.com/*"] }
53
+ }
54
+ ```
55
+
56
+ - Single inheritance, at most 32 levels; cycles and unmet parent version/API requirements are rejected. `version` is exact semver; component `version` is a version requirement, with concrete installation versions resolved by installers.
57
+ - packages, skills, and providers merge by ID; child fields override the same ID, while arrays replace entirely. Enabled entries must ultimately have a source; `enabled:false` retains a disabled declaration.
58
+ - Component `dependsOn` references IDs of the same component type. The final inherited graph checks missing, disabled, and cyclic dependencies. This does not establish runtime compatibility or completed installation.
59
+ - source resolves relative to the declaring manifest. A child changing only the version does not alter an inherited source's base URL.
60
+ - Skills may declare up to 16 previous sources in `migrateFrom`, resolved relative to the declaring manifest. Children replace the whole list; an empty array clears it. Installers migrate only with a matching source receipt, valid Skill identity, and setup ownership of all associated plugins. Manual registrations, shared manual aliases, and other sources remain and report conflicts. Source matching still checks refs strictly.
61
+ - defaults recursively merges objects; arrays, scalars, and null replace values. User overrides are saved separately and applied to the resolved result.
62
+ - Permissions are aggregated and deduplicated as requirements only; browser installers obtain actual authorization separately.
63
+ - appProfiles maps App IDs to implementation identifiers. Later capability catalogs verify profile existence and package executability; the resolver does not claim simulation validation.
64
+ - Results retain the exact parent-to-child version chain and each component's latest declaration source. Loading failures do not produce partial success.
65
+ - Unknown fields, non-JSON configuration, and prototype-pollution keys are rejected. Total manifests are limited to 256 Ki UTF-16 code units; defaults nesting to 32 levels.
66
+
67
+ This package owns generic manifest parsing, validation, and resource graphs, without installation or UI. Browser hosts and setup own full installation, preset lock persistence, permissions, registration, and activation; see the [installation host](../../docs/preset-installation.en.md). Bundle-local storage transactionally replaces validated snapshots.
68
+
69
+ The Skill manager's optional `readSources(scope, cwd)` returns manager-verified source receipts matching setup's `TrackedSkillSource`. CLI and Native installation status share this reader. Managers persist, validate, and invalidate their supplementary records; public hosts still verify sources, refs, manifest identity, and managed ownership. Without it, the standard Skills lockfile is read.
70
+
71
+ ## Tool catalogs
72
+
73
+ Presets can register additional installable tool catalogs through `defaults.extension.toolCatalogs`:
74
+
75
+ ```json
76
+ {
77
+ "defaults": {
78
+ "extension": {
79
+ "toolCatalogs": {
80
+ "team": {
81
+ "url": "https://tools.example.com/toolbox.json",
82
+ "label": "Team"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ Use absolute HTTPS URLs (loopback HTTP is allowed for local development), with at most 16 catalogs. Use stable IDs; child presets can override only `url` or `label`, or disable a parent catalog with `enabled: false`. The official catalog always remains first; `official` is reserved. Declare cross-origin domains in `permissions.origins` and obtain access through the existing authorization flow.
91
+
92
+ Catalog JSON shares the official structure, limited to 256 entries and 256 KiB:
93
+
94
+ ```json
95
+ {
96
+ "tools": [{
97
+ "id": "plugin:team-tool",
98
+ "plugin": "team-tool",
99
+ "repository": "example/skills",
100
+ "skill": "team-tool",
101
+ "title": "Team tool",
102
+ "description": "A tool for the team",
103
+ "icon": "wrench"
104
+ }]
105
+ }
106
+ ```
107
+
108
+ `repository` also supports HTTPS/SSH Git sources and pinned refs such as `git@code.example.com:team/skills.git#release/v3`, passed unchanged to the preset-selected Skills manager. Manifests reject shell arguments, credentials, and local paths, and execute no embedded code. Listing tools does not install them.
109
+
110
+ Sources load concurrently and cache independently. Retry refreshes the current catalog; failure retains its cache without blocking other sources. Identical plugin IDs and sources deduplicate. Conflicting repositories or Skills for one ID retain the higher-priority catalog and report a conflict. Lower-priority install buttons wait for earlier catalogs to finish checking, so load order cannot change installation targets. Replaced or disabled catalogs and late responses do not remain in the current list.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @mearl/preset
2
2
 
3
+ [简体中文](README.md) | [English](README.en.md)
4
+
5
+ 用 Preset 将一组配置好的 Mearl 能力、Skills 和 Providers 一起分发。本包负责解析清单,并为浏览器与 Node 宿主准备资源。
6
+
3
7
  3.x 的声明、配置、能力目录及对象存储设计见 [能力包模型](../../docs/capability-model.md)。包支持的位置从贡献推导,Preset 的可选 `targets` 仅选择启用子集;操作和登录定义归包,配置覆盖归 Preset 与用户。
4
8
 
5
9
  浏览器包的准备与本地存储见 [能力包](../../docs/browser-bundles.md),整套原子激活与 App 实现映射见 [浏览器 Preset](../../docs/browser-presets.md)。这两类能力通过 `@mearl/preset/bundles` 导出。
@@ -8,6 +12,8 @@
8
12
 
9
13
  主入口在浏览器和 Node 中共享 preset 清单校验与继承解析,没有文件系统、网络、凭据或浏览器权限副作用;清单加载由调用方提供。可选子入口 `@mearl/preset/bundles` 提供资源校验、受限下载和显式调用的浏览器本地存储,见 [能力包说明](../../docs/browser-bundles.md)。
10
14
 
15
+ `@mearl/preset/pageRuntime` 为独立的页面注入脚本提供页面运行时宿主及注入源码生成函数。
16
+
11
17
  ```ts
12
18
  import { resolvePreset, mergePresetDefaults } from '@mearl/preset';
13
19
 
@@ -54,7 +60,7 @@ Node 模块快照与宿主加载合同见 [Node 能力包](../../docs/node-capab
54
60
  - Skill 可声明 `migrateFrom` 旧来源列表(最多 16 项),相对地址按声明清单解析;子级整体覆盖,空数组清除。安装器仅在来源收据匹配旧来源、Skill 身份有效且所有关联插件均由 setup 托管时迁移。手动注册、共享的手动别名或其他来源保留并报告冲突;来源匹配仍严格检查 ref。
55
61
  - defaults 递归合并对象,数组、标量和 null 替换;用户覆盖单独保存,在解析结果上应用。
56
62
  - 权限汇总去重,结果仅表示需求;必须由浏览器安装器另行取得实际授权。
57
- - appProfiles 是 App ID 到实现标识的覆盖映射。具体档案是否存在、所需包是否可执行,由后续能力目录校验,解析器不宣称已验证模拟效果。
63
+ - appProfiles 是 App ID 到实现标识的覆盖映射。具体端模拟配置是否存在、所需包是否可执行,由后续能力目录校验,解析器不宣称已验证模拟效果。
58
64
  - 结果保留父级到子级的确切版本链和各组件最近声明来源,加载失败不会生成部分成功的结果。
59
65
  - 拒绝未知字段、非 JSON 配置和原型污染键。清单总量不超过 256 Ki UTF-16 code units,defaults 最大嵌套 32 层。
60
66
 
@@ -1,19 +1,32 @@
1
1
  import { selectBrowserControl } from './browserControls.js';
2
2
  import { BundleError, MAX_BROWSER_STORAGE_BYTES, verifyBundle } from './bundle.js';
3
- import { satisfies } from 'semver';
4
- import { preparePresetPackages, presetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets } from './prepare.js';
3
+ import satisfies from 'semver/functions/satisfies.js';
4
+ import { preparePresetPackages, presetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets, } from './prepare.js';
5
5
  /** Core browser defaults also need synchronization when no browser package is declared. */
6
6
  export function presetRequiresBrowser(preset) {
7
- return Object.values(preset.packages).some(entry => entry.enabled !== false && entry.targets?.includes('browser')) || Object.keys(preset.appProfiles).length > 0 || Object.keys(preset.defaults).some(key => key === 'simulation' || key === 'extension' || key === 'services');
7
+ return (Object.values(preset.packages).some(entry => entry.enabled !== false && entry.targets?.includes('browser')) ||
8
+ Object.keys(preset.appProfiles).length > 0 ||
9
+ Object.keys(preset.defaults).some(key => key === 'simulation' || key === 'extension' || key === 'services'));
8
10
  }
9
11
  /** Browser selection can be derived from the same accepted batch without another download. */
10
12
  export async function browserPresetSnapshot(source, manifests, preset, bundles) {
11
13
  const ids = new Set(presetPackages(preset, ['browser']).map(([id]) => id));
12
14
  const selected = bundles.filter(bundle => ids.has(bundle.manifest.id));
13
15
  selectBrowserControl(selected);
14
- const packageTargets = Object.fromEntries(Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false).map(([id, entry]) => [id, entry.targets]));
15
- const data = { source, manifests, bundleRevisions: Object.fromEntries(selected.map(bundle => [bundle.manifest.id, bundle.revision])), packageTargets };
16
- return { snapshot: { ...data, revision: await presetSnapshotRevision(data) }, preset, bundles: selected };
16
+ const packageTargets = Object.fromEntries(Object.entries(preset.packages)
17
+ .filter(([, entry]) => entry.enabled !== false)
18
+ .map(([id, entry]) => [id, entry.targets]));
19
+ const data = {
20
+ source,
21
+ manifests,
22
+ bundleRevisions: Object.fromEntries(selected.map(bundle => [bundle.manifest.id, bundle.revision])),
23
+ packageTargets,
24
+ };
25
+ return {
26
+ snapshot: { ...data, revision: await presetSnapshotRevision(data) },
27
+ preset,
28
+ bundles: selected,
29
+ };
17
30
  }
18
31
  export async function prepareBrowserPreset(source, options) {
19
32
  const prepared = await preparePresetPackages(source, options, ['browser']);
@@ -29,7 +42,9 @@ export async function verifyPreparedBrowserPreset(value, mearlApiVersion) {
29
42
  throw new BundleError('BUNDLE_QUOTA', 'Browser preset package count exceeds 64');
30
43
  let total = 0;
31
44
  for (const bundle of value.bundles) {
32
- if (!bundle?.resources || typeof bundle.resources !== 'object' || Array.isArray(bundle.resources))
45
+ if (!bundle?.resources ||
46
+ typeof bundle.resources !== 'object' ||
47
+ Array.isArray(bundle.resources))
33
48
  throw new BundleError('BUNDLE_INVALID', 'Expected package resource buffers');
34
49
  const resources = Object.values(bundle.resources);
35
50
  if (resources.length > 64)
@@ -46,10 +61,14 @@ export async function verifyPreparedBrowserPreset(value, mearlApiVersion) {
46
61
  verifyBrowserPreset(value.snapshot, mearlApiVersion),
47
62
  Promise.all(value.bundles.map(bundle => verifyBundle(bundle, mearlApiVersion))),
48
63
  ]);
49
- if (bundles.length !== Object.keys(graph.snapshot.bundleRevisions).length || new Set(bundles.map(bundle => bundle.manifest.id)).size !== bundles.length || bundles.some(bundle => {
50
- const entry = graph.preset.packages[bundle.manifest.id];
51
- return graph.snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision || entry?.source !== bundle.source || (entry.version && !satisfies(bundle.manifest.version, entry.version));
52
- }))
64
+ if (bundles.length !== Object.keys(graph.snapshot.bundleRevisions).length ||
65
+ new Set(bundles.map(bundle => bundle.manifest.id)).size !== bundles.length ||
66
+ bundles.some(bundle => {
67
+ const entry = graph.preset.packages[bundle.manifest.id];
68
+ return (graph.snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision ||
69
+ entry?.source !== bundle.source ||
70
+ (entry.version && !satisfies(bundle.manifest.version, entry.version)));
71
+ }))
53
72
  throw new BundleError('BUNDLE_INTEGRITY', 'Browser activation requires every exact package');
54
73
  selectBrowserControl(bundles);
55
74
  verifyPackageTargets(graph.preset, bundles);
package/dist/bundle.d.ts CHANGED
@@ -48,7 +48,7 @@ export type BundleLoader = (source: string, options: BundleLoadOptions) => Promi
48
48
  export interface PrepareBundleOptions {
49
49
  load: BundleLoader;
50
50
  mearlApiVersion: string;
51
- expectedId: string;
51
+ expectedId?: string;
52
52
  version?: string;
53
53
  signal?: AbortSignal;
54
54
  }
@@ -58,5 +58,7 @@ export declare function bundleDigest(bytes: Uint8Array): Promise<string>;
58
58
  export declare function readBundleJson(bundle: PreparedBundle, name: string): unknown;
59
59
  export declare function readBundleText(bundle: PreparedBundle, name: string): string;
60
60
  export declare function prepareBundle(source: string, options: PrepareBundleOptions): Promise<PreparedBundle>;
61
+ /** Loads the resources declared by a validated manifest at its canonical source. */
62
+ export declare function loadBundleResources(source: string, manifest: BundleManifest, options: Pick<PrepareBundleOptions, 'load' | 'signal'>): Promise<PreparedBundle>;
61
63
  /** Revalidates persisted snapshots without network access and returns independent resource buffers. */
62
64
  export declare function verifyBundle(bundle: PreparedBundle, mearlApiVersion: string): Promise<PreparedBundle>;
package/dist/bundle.js CHANGED
@@ -1,4 +1,6 @@
1
- import { satisfies, valid, validRange } from 'semver';
1
+ import satisfies from 'semver/functions/satisfies.js';
2
+ import valid from 'semver/functions/valid.js';
3
+ import validRange from 'semver/ranges/valid.js';
2
4
  import { clonePresetDefaults } from './manifest.js';
3
5
  import { validateBrowserControl } from './browserControls.js';
4
6
  export const MAX_BUNDLE_BYTES = 16 * 1024 * 1024;
@@ -18,7 +20,9 @@ function invalid(message) {
18
20
  throw new BundleError('BUNDLE_INVALID', message);
19
21
  }
20
22
  function fields(value, names) {
21
- if (!value || typeof value !== 'object' || Array.isArray(value) ||
23
+ if (!value ||
24
+ typeof value !== 'object' ||
25
+ Array.isArray(value) ||
22
26
  Object.keys(value).some(key => !names.includes(key)))
23
27
  invalid('Unexpected manifest structure');
24
28
  }
@@ -48,10 +52,20 @@ export function validateBundleManifest(value, mearlApiVersion) {
48
52
  catch {
49
53
  invalid('Expected a JSON manifest');
50
54
  }
51
- fields(json, ['schemaVersion', 'id', 'version', 'requires', 'defaults', 'resources', 'contributions']);
55
+ fields(json, [
56
+ 'schemaVersion',
57
+ 'id',
58
+ 'version',
59
+ 'requires',
60
+ 'defaults',
61
+ 'resources',
62
+ 'contributions',
63
+ ]);
52
64
  if (json.defaults !== undefined)
53
65
  json.defaults = clonePresetDefaults(json.defaults);
54
- if (json.schemaVersion !== 1 || typeof json.id !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(json.id))
66
+ if (json.schemaVersion !== 1 ||
67
+ typeof json.id !== 'string' ||
68
+ !/^[a-z][a-z0-9-]{0,79}$/.test(json.id))
55
69
  invalid('Invalid bundle identity');
56
70
  if (typeof json.version !== 'string' || valid(json.version) !== json.version)
57
71
  invalid('Expected an exact bundle version');
@@ -74,45 +88,77 @@ export function validateBundleManifest(value, mearlApiVersion) {
74
88
  for (const [name, resource] of resources) {
75
89
  path(name);
76
90
  fields(resource, ['sha256', 'byteLength']);
77
- if (typeof resource.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(resource.sha256) ||
78
- typeof resource.byteLength !== 'number' || !Number.isSafeInteger(resource.byteLength) || resource.byteLength < 0)
91
+ if (typeof resource.sha256 !== 'string' ||
92
+ !/^[a-f0-9]{64}$/.test(resource.sha256) ||
93
+ typeof resource.byteLength !== 'number' ||
94
+ !Number.isSafeInteger(resource.byteLength) ||
95
+ resource.byteLength < 0)
79
96
  invalid('Invalid resource metadata');
80
97
  total += resource.byteLength;
81
98
  if (total > MAX_BUNDLE_BYTES)
82
99
  throw new BundleError('BUNDLE_QUOTA', 'Bundle resources exceed 16 MiB');
83
100
  }
84
101
  if (json.contributions !== undefined) {
85
- fields(json.contributions, ['appProfiles', 'pageRuntimes', 'nodeRuntimes', 'browserRuntimes', 'extension', 'requestUi', 'browserViews', 'browserControls']);
102
+ fields(json.contributions, [
103
+ 'appProfiles',
104
+ 'pageRuntimes',
105
+ 'nodeRuntimes',
106
+ 'browserRuntimes',
107
+ 'extension',
108
+ 'requestUi',
109
+ 'browserViews',
110
+ 'browserControls',
111
+ ]);
86
112
  const extension = json.contributions.extension;
87
- if (extension !== undefined && (typeof extension !== 'string' || !Object.hasOwn(json.resources, extension)))
113
+ if (extension !== undefined &&
114
+ (typeof extension !== 'string' || !Object.hasOwn(json.resources, extension)))
88
115
  invalid('Extension contributions must reference a declared resource');
89
- if (typeof extension === 'string' && json.resources[extension].byteLength > MAX_REQUEST_UI_BYTES)
116
+ if (typeof extension === 'string' &&
117
+ json.resources[extension].byteLength > MAX_REQUEST_UI_BYTES)
90
118
  throw new BundleError('BUNDLE_QUOTA', 'Extension contributions exceed 64 KiB');
91
119
  const controls = json.contributions.browserControls;
92
- if (controls !== undefined && (typeof controls !== 'string' || !Object.hasOwn(json.resources, controls)))
120
+ if (controls !== undefined &&
121
+ (typeof controls !== 'string' || !Object.hasOwn(json.resources, controls)))
93
122
  invalid('Browser controls must reference a declared resource');
94
- if (typeof controls === 'string' && json.resources[controls].byteLength > MAX_REQUEST_UI_BYTES)
123
+ if (typeof controls === 'string' &&
124
+ json.resources[controls].byteLength > MAX_REQUEST_UI_BYTES)
95
125
  throw new BundleError('BUNDLE_QUOTA', 'Browser controls exceed 64 KiB');
96
- if (json.contributions.requestUi !== undefined && (typeof json.contributions.requestUi !== 'string' || !Object.hasOwn(json.resources, json.contributions.requestUi)))
126
+ if (json.contributions.requestUi !== undefined &&
127
+ (typeof json.contributions.requestUi !== 'string' ||
128
+ !Object.hasOwn(json.resources, json.contributions.requestUi)))
97
129
  invalid('Request UI must reference a declared resource');
98
- if (typeof json.contributions.requestUi === 'string' && json.resources[json.contributions.requestUi].byteLength > MAX_REQUEST_UI_BYTES)
130
+ if (typeof json.contributions.requestUi === 'string' &&
131
+ json.resources[json.contributions.requestUi].byteLength >
132
+ MAX_REQUEST_UI_BYTES)
99
133
  throw new BundleError('BUNDLE_QUOTA', 'Request UI exceeds 64 KiB');
100
134
  if (json.contributions.browserViews !== undefined) {
101
- fields(json.contributions.browserViews, ['requestImport', 'requestCases', 'captureLauncher', 'captureController']);
135
+ fields(json.contributions.browserViews, [
136
+ 'requestImport',
137
+ 'requestCases',
138
+ 'captureLauncher',
139
+ 'captureController',
140
+ ]);
102
141
  for (const resource of Object.values(json.contributions.browserViews)) {
103
142
  if (typeof resource !== 'string' || !Object.hasOwn(json.resources, resource))
104
143
  invalid('Browser views must reference declared resources');
105
- if (json.resources[resource].byteLength > MAX_REQUEST_UI_BYTES)
144
+ if (json.resources[resource].byteLength >
145
+ MAX_REQUEST_UI_BYTES)
106
146
  throw new BundleError('BUNDLE_QUOTA', 'Browser view definition exceeds 64 KiB');
107
147
  }
108
148
  }
109
149
  const profiles = json.contributions.appProfiles === undefined ? [] : json.contributions.appProfiles;
110
- if (!Array.isArray(profiles) || profiles.some(name => typeof name !== 'string' || !Object.hasOwn(json.resources, name)) ||
150
+ if (!Array.isArray(profiles) ||
151
+ profiles.some(name => typeof name !== 'string' || !Object.hasOwn(json.resources, name)) ||
111
152
  new Set(profiles).size !== profiles.length)
112
153
  invalid('App profiles must reference distinct declared resources');
113
154
  const runtimes = json.contributions.pageRuntimes;
114
155
  if (runtimes !== undefined) {
115
- if (!runtimes || typeof runtimes !== 'object' || Array.isArray(runtimes) || Object.entries(runtimes).some(([id, name]) => !/^[a-z][a-z0-9-]{0,79}$/.test(id) || typeof name !== 'string' || !Object.hasOwn(json.resources, name)))
156
+ if (!runtimes ||
157
+ typeof runtimes !== 'object' ||
158
+ Array.isArray(runtimes) ||
159
+ Object.entries(runtimes).some(([id, name]) => !/^[a-z][a-z0-9-]{0,79}$/.test(id) ||
160
+ typeof name !== 'string' ||
161
+ !Object.hasOwn(json.resources, name)))
116
162
  invalid('Page runtimes must reference declared resources');
117
163
  for (const name of Object.values(runtimes)) {
118
164
  const resource = json.resources[name];
@@ -125,15 +171,26 @@ export function validateBundleManifest(value, mearlApiVersion) {
125
171
  const label = kind === 'nodeRuntimes' ? 'Node' : 'Browser';
126
172
  if (runtimes === undefined)
127
173
  continue;
128
- if (!runtimes || typeof runtimes !== 'object' || Array.isArray(runtimes) || Object.keys(runtimes).length > 16)
174
+ if (!runtimes ||
175
+ typeof runtimes !== 'object' ||
176
+ Array.isArray(runtimes) ||
177
+ Object.keys(runtimes).length > 16)
129
178
  invalid(`Invalid ${label} runtime declarations`);
130
179
  for (const [host, declaration] of Object.entries(runtimes)) {
131
180
  if (!/^[a-z][a-z0-9-]{0,79}$/.test(host))
132
181
  invalid(`Invalid ${label} runtime host`);
133
182
  fields(declaration, ['entry', 'bindings']);
134
- if (typeof declaration.entry !== 'string' || !Object.hasOwn(json.resources, declaration.entry) || !declaration.entry.endsWith('.cjs'))
183
+ if (typeof declaration.entry !== 'string' ||
184
+ !Object.hasOwn(json.resources, declaration.entry) ||
185
+ !declaration.entry.endsWith('.cjs'))
135
186
  invalid(`${label} runtime entry must reference a declared CommonJS resource`);
136
- if (!declaration.bindings || typeof declaration.bindings !== 'object' || Array.isArray(declaration.bindings) || Object.keys(declaration.bindings).length > 256 || Object.entries(declaration.bindings).some(([hook, handler]) => !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(hook) || typeof handler !== 'string' || !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(handler)))
187
+ if (!declaration.bindings ||
188
+ typeof declaration.bindings !== 'object' ||
189
+ Array.isArray(declaration.bindings) ||
190
+ Object.keys(declaration.bindings).length > 256 ||
191
+ Object.entries(declaration.bindings).some(([hook, handler]) => !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(hook) ||
192
+ typeof handler !== 'string' ||
193
+ !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(handler)))
137
194
  invalid(`Invalid ${label} runtime bindings`);
138
195
  }
139
196
  }
@@ -143,7 +200,12 @@ export function validateBundleManifest(value, mearlApiVersion) {
143
200
  return json;
144
201
  }
145
202
  export async function bundleDigest(bytes) {
146
- const digest = await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes).buffer);
203
+ const buffer = bytes.buffer;
204
+ // WebCrypto snapshots fixed, non-shared input views before hashing asynchronously.
205
+ const data = buffer instanceof ArrayBuffer && !('resizable' in buffer && buffer.resizable)
206
+ ? new Uint8Array(buffer, bytes.byteOffset, bytes.byteLength)
207
+ : Uint8Array.from(bytes);
208
+ const digest = await crypto.subtle.digest('SHA-256', data);
147
209
  return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
148
210
  }
149
211
  async function manifestRevision(source, manifest) {
@@ -172,7 +234,8 @@ export function readBundleText(bundle, name) {
172
234
  export async function prepareBundle(source, options) {
173
235
  options.signal?.throwIfAborted();
174
236
  source = bundleSourceUrl(source);
175
- if (options.version !== undefined && (!options.version.trim() || validRange(options.version) === null))
237
+ if (options.version !== undefined &&
238
+ (!options.version.trim() || validRange(options.version) === null))
176
239
  invalid('Invalid requested bundle version');
177
240
  const bytes = Uint8Array.from(await options.load(source, { maxBytes: MAX_BUNDLE_MANIFEST_BYTES, signal: options.signal }));
178
241
  options.signal?.throwIfAborted();
@@ -186,22 +249,35 @@ export async function prepareBundle(source, options) {
186
249
  invalid('Expected a UTF-8 JSON manifest');
187
250
  }
188
251
  const manifest = validateBundleManifest(value, options.mearlApiVersion);
189
- if (manifest.id !== options.expectedId)
252
+ if (options.expectedId !== undefined && manifest.id !== options.expectedId)
190
253
  invalid('Bundle identity does not match the requested package');
191
254
  if (options.version && !satisfies(manifest.version, options.version))
192
255
  throw new BundleError('BUNDLE_INCOMPATIBLE', 'Bundle version does not satisfy the requested range');
256
+ return loadBundleResources(source, manifest, options);
257
+ }
258
+ /** Loads the resources declared by a validated manifest at its canonical source. */
259
+ export async function loadBundleResources(source, manifest, options) {
193
260
  const resources = {};
194
261
  for (const [name, expected] of Object.entries(manifest.resources)) {
195
262
  options.signal?.throwIfAborted();
196
- const resource = Uint8Array.from(await options.load(new URL(name, source).href, { maxBytes: expected.byteLength, signal: options.signal }));
263
+ const resource = Uint8Array.from(await options.load(new URL(name, source).href, {
264
+ maxBytes: expected.byteLength,
265
+ signal: options.signal,
266
+ }));
197
267
  options.signal?.throwIfAborted();
198
- if (resource.byteLength !== expected.byteLength || await bundleDigest(resource) !== expected.sha256) {
268
+ if (resource.byteLength !== expected.byteLength ||
269
+ (await bundleDigest(resource)) !== expected.sha256) {
199
270
  throw new BundleError('BUNDLE_INTEGRITY', `Resource integrity check failed: ${name}`);
200
271
  }
201
272
  resources[name] = resource;
202
273
  }
203
274
  options.signal?.throwIfAborted();
204
- const prepared = { source, manifest, revision: await manifestRevision(source, manifest), resources };
275
+ const prepared = {
276
+ source,
277
+ manifest,
278
+ revision: await manifestRevision(source, manifest),
279
+ resources,
280
+ };
205
281
  if (manifest.contributions?.browserControls)
206
282
  validateBrowserControl(readBundleJson(prepared, manifest.contributions.browserControls));
207
283
  return prepared;
@@ -211,7 +287,8 @@ export async function verifyBundle(bundle, mearlApiVersion) {
211
287
  const source = bundleSourceUrl(bundle.source);
212
288
  const manifest = validateBundleManifest(bundle.manifest, mearlApiVersion);
213
289
  const revision = bundle.revision;
214
- if (!bundle.resources || typeof bundle.resources !== 'object' ||
290
+ if (!bundle.resources ||
291
+ typeof bundle.resources !== 'object' ||
215
292
  Object.keys(bundle.resources).length !== Object.keys(manifest.resources).length)
216
293
  invalid('Stored resource set does not match the manifest');
217
294
  const resources = {};
@@ -225,10 +302,10 @@ export async function verifyBundle(bundle, mearlApiVersion) {
225
302
  }
226
303
  resources[name] = bytes;
227
304
  }
228
- if (await manifestRevision(source, manifest) !== revision)
305
+ if ((await manifestRevision(source, manifest)) !== revision)
229
306
  throw new BundleError('BUNDLE_INTEGRITY', 'Stored manifest revision does not match');
230
307
  for (const [name, expected] of Object.entries(manifest.resources)) {
231
- if (await bundleDigest(resources[name]) !== expected.sha256)
308
+ if ((await bundleDigest(resources[name])) !== expected.sha256)
232
309
  throw new BundleError('BUNDLE_INTEGRITY', `Stored resource integrity check failed: ${name}`);
233
310
  }
234
311
  const prepared = { source, manifest, revision, resources };
@@ -292,9 +292,9 @@ export class BrowserBundleStore {
292
292
  /** Pins the verified selection while reading its package from the same IndexedDB snapshot. */
293
293
  async profileBinding(name) {
294
294
  const activation = await this.activePreset();
295
- const target = activation?.preset.appProfiles[name];
296
- if (!activation || !target)
295
+ if (!activation || !Object.hasOwn(activation.preset.appProfiles, name))
297
296
  return undefined;
297
+ const target = activation.preset.appProfiles[name];
298
298
  const id = target.split('/')[0];
299
299
  const database = await this.open();
300
300
  let objects = new Map();
package/dist/bundles.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { BundleManifest, PreparedBundle, BundleLoader, BundleLoadOptions, P
3
3
  export { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
4
4
  export type { ExtensionContributions } from './extensionContributions.js';
5
5
  export { createBundleFetchLoader } from './bundleFetch.js';
6
+ export { parseProfileSource, resolveProfileSource } from './profileSource.js';
6
7
  export { BrowserBundleStore } from './bundleStore.js';
7
8
  export type { BrowserPresetInstallation } from './bundleStore.js';
8
9
  export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
package/dist/bundles.js CHANGED
@@ -1,6 +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 { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
3
3
  export { createBundleFetchLoader } from './bundleFetch.js';
4
+ export { parseProfileSource, resolveProfileSource } from './profileSource.js';
4
5
  export { BrowserBundleStore } from './bundleStore.js';
5
6
  export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
6
7
  export { preparePreset, verifyPreset, verifyPreparedPreset } from './preset.js';
@@ -1,19 +1,32 @@
1
- import { valid, validRange, satisfies, subset } from 'semver';
1
+ import satisfies from 'semver/functions/satisfies.js';
2
+ import valid from 'semver/functions/valid.js';
3
+ import subset from 'semver/ranges/subset.js';
4
+ import validRange from 'semver/ranges/valid.js';
2
5
  import { isJsonObject, PresetError } from './manifest.js';
3
6
  /** Declarative installation guidance; never accepts commands, arguments or executable hooks. */
4
7
  export function parseSetupManagement(value) {
5
8
  if (value === undefined)
6
9
  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))
10
+ const invalid = () => {
11
+ throw new PresetError('PRESET_INVALID', 'Invalid defaults.management.setup declaration');
12
+ };
13
+ if (!isJsonObject(value) ||
14
+ Object.keys(value).some(key => key !== 'setup') ||
15
+ !isJsonObject(value.setup))
9
16
  return invalid();
10
17
  const setup = value.setup;
11
18
  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)))
19
+ typeof setup.package !== 'string' ||
20
+ setup.package.length > 214 ||
21
+ !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(setup.package) ||
22
+ typeof setup.version !== 'string' ||
23
+ setup.version.length > 64 ||
24
+ !(valid(setup.version) === setup.version || /^[a-z][a-z0-9.-]*$/.test(setup.version)))
14
25
  return invalid();
15
26
  if (setup.registry !== undefined) {
16
- if (typeof setup.registry !== 'string' || setup.registry.length > 2048 || !/^https:\/\/[a-zA-Z0-9./:_-]+$/.test(setup.registry))
27
+ if (typeof setup.registry !== 'string' ||
28
+ setup.registry.length > 2048 ||
29
+ !/^https:\/\/[a-zA-Z0-9./:_-]+$/.test(setup.registry))
17
30
  return invalid();
18
31
  try {
19
32
  const url = new URL(setup.registry);
@@ -24,7 +37,8 @@ export function parseSetupManagement(value) {
24
37
  return invalid();
25
38
  }
26
39
  }
27
- if (setup.runtime !== undefined && (typeof setup.runtime !== 'string' || setup.runtime.length > 128 || !validRange(setup.runtime)))
40
+ if (setup.runtime !== undefined &&
41
+ (typeof setup.runtime !== 'string' || setup.runtime.length > 128 || !validRange(setup.runtime)))
28
42
  return invalid();
29
43
  return { ...setup };
30
44
  }
@@ -32,5 +46,7 @@ export function setupManagementCommand(setup) {
32
46
  return `npx${setup.registry ? ` --registry=${setup.registry}` : ''} ${setup.package}@${setup.version}`;
33
47
  }
34
48
  export function setupRuntimeCompatible(setup, version, targetLine) {
35
- return !setup.runtime || (satisfies(version, setup.runtime, { includePrerelease: true }) && (!targetLine || subset(targetLine, setup.runtime, { includePrerelease: true })));
49
+ return (!setup.runtime ||
50
+ (satisfies(version, setup.runtime, { includePrerelease: true }) &&
51
+ (!targetLine || subset(targetLine, setup.runtime, { includePrerelease: true }))));
36
52
  }
package/dist/manifest.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { validateInstallationOrigin } from './installation.js';
2
- import { valid, validRange } from 'semver';
2
+ import valid from 'semver/functions/valid.js';
3
+ import validRange from 'semver/ranges/valid.js';
3
4
  export class PresetError extends Error {
4
5
  code;
5
6
  constructor(code, message) {
@@ -14,9 +15,10 @@ function invalid(path, reason) {
14
15
  throw new PresetError('PRESET_INVALID', `${path}: ${reason}`);
15
16
  }
16
17
  export function isJsonObject(value) {
17
- return value !== null && typeof value === 'object' &&
18
+ return (value !== null &&
19
+ typeof value === 'object' &&
18
20
  !Array.isArray(value) &&
19
- (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
21
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null));
20
22
  }
21
23
  function object(value, path) {
22
24
  if (!isJsonObject(value))
@@ -88,7 +90,8 @@ function component(value, path, kind) {
88
90
  invalid(`${path}.enabled`, 'expected a boolean');
89
91
  if ('targets' in value) {
90
92
  stringArray(value.targets, `${path}.targets`);
91
- if (value.targets.length === 0 || value.targets.some(item => item !== 'browser' && item !== 'node')) {
93
+ if (value.targets.length === 0 ||
94
+ value.targets.some(item => item !== 'browser' && item !== 'node')) {
92
95
  invalid(`${path}.targets`, 'expected browser and/or node');
93
96
  }
94
97
  }
@@ -100,7 +103,19 @@ function component(value, path, kind) {
100
103
  return json(value, path);
101
104
  }
102
105
  export function validatePreset(value) {
103
- fields(value, 'preset', ['schemaVersion', 'id', 'version', 'extends', 'requires', 'packages', 'skills', 'providers', 'appProfiles', 'defaults', 'permissions']);
106
+ fields(value, 'preset', [
107
+ 'schemaVersion',
108
+ 'id',
109
+ 'version',
110
+ 'extends',
111
+ 'requires',
112
+ 'packages',
113
+ 'skills',
114
+ 'providers',
115
+ 'appProfiles',
116
+ 'defaults',
117
+ 'permissions',
118
+ ]);
104
119
  if (value.schemaVersion !== 1)
105
120
  invalid('schemaVersion', 'only version 1 is supported');
106
121
  id(value.id, 'id');
@@ -113,13 +128,20 @@ export function validatePreset(value) {
113
128
  text(value.extends.source, 'extends.source');
114
129
  if ('version' in value.extends)
115
130
  range(value.extends.version, 'extends.version');
116
- result.extends = { source: value.extends.source, ...(value.extends.version === undefined ? {} : { version: value.extends.version }) };
131
+ result.extends = {
132
+ source: value.extends.source,
133
+ ...(value.extends.version === undefined ? {} : { version: value.extends.version }),
134
+ };
117
135
  }
118
136
  if ('requires' in value) {
119
137
  fields(value.requires, 'requires', ['mearlApi']);
120
138
  if ('mearlApi' in value.requires)
121
139
  range(value.requires.mearlApi, 'requires.mearlApi');
122
- result.requires = { ...(value.requires.mearlApi === undefined ? {} : { mearlApi: value.requires.mearlApi }) };
140
+ result.requires = {
141
+ ...(value.requires.mearlApi === undefined
142
+ ? {}
143
+ : { mearlApi: value.requires.mearlApi }),
144
+ };
123
145
  }
124
146
  for (const kind of ['packages', 'skills', 'providers']) {
125
147
  if (!(kind in value))
@@ -148,14 +170,18 @@ export function validatePreset(value) {
148
170
  const origins = value.permissions.origins ?? [];
149
171
  if (origins.length > 64)
150
172
  invalid('permissions.origins', 'exceeds 64 scopes');
151
- result.permissions = { origins: [...new Set(origins.map((origin, index) => {
173
+ result.permissions = {
174
+ origins: [
175
+ ...new Set(origins.map((origin, index) => {
152
176
  try {
153
177
  return validateInstallationOrigin(origin);
154
178
  }
155
179
  catch {
156
180
  return invalid(`permissions.origins[${index}]`, 'expected an explicit HTTP host scope');
157
181
  }
158
- }))] };
182
+ })),
183
+ ],
184
+ };
159
185
  }
160
186
  if (JSON.stringify(result).length > 262_144)
161
187
  invalid('preset', 'manifest exceeds 256 Ki UTF-16 code units');
package/dist/prepare.js CHANGED
@@ -1,5 +1,5 @@
1
- import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, bundleDigest, bundleSourceUrl, prepareBundle, validateBundleManifest } from './bundle.js';
2
- import { satisfies } from 'semver';
1
+ import { BundleError, MAX_BROWSER_STORAGE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, bundleDigest, bundleSourceUrl, loadBundleResources, validateBundleManifest, } from './bundle.js';
2
+ import satisfies from 'semver/functions/satisfies.js';
3
3
  import { clonePresetDefaults, validatePreset } from './manifest.js';
4
4
  import { resolvePreset } from './resolve.js';
5
5
  export async function presetSnapshotRevision(value) {
@@ -7,7 +7,8 @@ export async function presetSnapshotRevision(value) {
7
7
  }
8
8
  /** Select candidates without assuming that inferred locations have been discovered. */
9
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)));
10
+ const entries = Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false &&
11
+ (entry.targets ?? ['browser', 'node']).some(runtime => runtimes.includes(runtime)));
11
12
  if (entries.length > 64)
12
13
  throw new BundleError('BUNDLE_QUOTA', 'A preset can activate at most 64 packages');
13
14
  return entries;
@@ -35,7 +36,14 @@ export function presetPackages(preset, runtimes) {
35
36
  export function bundleTargets(bundle) {
36
37
  const contributions = bundle.manifest.contributions;
37
38
  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
+ if (contributions &&
40
+ (Object.keys(contributions.browserRuntimes ?? {}).length ||
41
+ Object.keys(contributions.pageRuntimes ?? {}).length ||
42
+ contributions.appProfiles?.length ||
43
+ contributions.extension ||
44
+ contributions.requestUi ||
45
+ contributions.browserViews ||
46
+ contributions.browserControls))
39
47
  targets.push('browser');
40
48
  if (Object.keys(contributions?.nodeRuntimes ?? {}).length)
41
49
  targets.push('node');
@@ -49,7 +57,12 @@ export function applyPackageTargets(preset, targets) {
49
57
  const packages = { ...preset.packages };
50
58
  for (const [id, entry] of enabled) {
51
59
  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))
60
+ if (!Array.isArray(selected) ||
61
+ !selected.length ||
62
+ selected.length > 2 ||
63
+ new Set(selected).size !== selected.length ||
64
+ selected.some(target => target !== 'browser' && target !== 'node') ||
65
+ (entry.targets && JSON.stringify(entry.targets) !== JSON.stringify(selected)))
53
66
  throw new BundleError('BUNDLE_INTEGRITY', 'Invalid package target selection');
54
67
  packages[id] = { ...entry, targets: [...selected] };
55
68
  }
@@ -96,10 +109,14 @@ export async function preparePresetPackages(source, options, runtimes) {
96
109
  },
97
110
  });
98
111
  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);
112
+ const entries = targetsKnown
113
+ ? presetPackages(preset, runtimes)
114
+ : selectPresetPackages(preset, runtimes);
100
115
  const bundles = [];
101
116
  const pending = [];
102
- const packageTargets = Object.fromEntries(Object.entries(preset.packages).filter(([, entry]) => entry.enabled !== false).map(([id, entry]) => [id, entry.targets ?? []]));
117
+ const packageTargets = Object.fromEntries(Object.entries(preset.packages)
118
+ .filter(([, entry]) => entry.enabled !== false)
119
+ .map(([id, entry]) => [id, entry.targets ?? []]));
103
120
  for (const [id, entry] of entries) {
104
121
  const manifestSource = bundleSourceUrl(entry.source);
105
122
  const bytes = Uint8Array.from(await load(manifestSource, { maxBytes: MAX_BUNDLE_MANIFEST_BYTES, signal: options.signal }));
@@ -121,25 +138,38 @@ export async function preparePresetPackages(source, options, runtimes) {
121
138
  if (targets.some(target => !available.includes(target)))
122
139
  throw new BundleError('BUNDLE_INCOMPATIBLE', 'Package does not support the selected target');
123
140
  packageTargets[id] = targets;
124
- pending.push({ id, entry, source: manifestSource, bytes });
141
+ pending.push({ source: manifestSource, manifest, bytes });
125
142
  }
126
143
  preset = applyPackageTargets(preset, packageTargets);
127
144
  const selected = new Set(presetPackages(preset, runtimes).map(([id]) => id));
128
- for (const candidate of pending) {
129
- if (!selected.has(candidate.id))
145
+ for (const { source: manifestSource, manifest, bytes } of pending) {
146
+ if (!selected.has(manifest.id))
130
147
  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 }));
148
+ bundles.push(await loadBundleResources(manifestSource, manifest, {
149
+ // A resource at the manifest URL belongs to the same captured response.
150
+ load: (url, request) => url === manifestSource ? Promise.resolve(bytes) : load(url, request),
151
+ signal: options.signal,
152
+ }));
132
153
  }
133
- verifyPackageTargets(preset, bundles);
134
154
  options.signal?.throwIfAborted();
135
- return { source, manifests, preset, bundles: bundles.filter(bundle => selected.has(bundle.manifest.id)), packageTargets };
155
+ return { source, manifests, preset, bundles, packageTargets };
136
156
  }
137
157
  /** The exact graph is checked offline; installed resources are verified by the target store. */
138
158
  export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full) {
139
159
  const json = clonePresetDefaults(value);
140
- const allowed = ['source', 'manifests', 'bundleRevisions', 'packageTargets', 'revision', ...(full ? ['schemaVersion'] : [])];
141
- if (Object.keys(json).some(key => !allowed.includes(key)) || (full && json.schemaVersion !== 1) ||
142
- typeof json.source !== 'string' || typeof json.revision !== 'string' || !/^[a-f0-9]{64}$/.test(json.revision)) {
160
+ const allowed = [
161
+ 'source',
162
+ 'manifests',
163
+ 'bundleRevisions',
164
+ 'packageTargets',
165
+ 'revision',
166
+ ...(full ? ['schemaVersion'] : []),
167
+ ];
168
+ if (Object.keys(json).some(key => !allowed.includes(key)) ||
169
+ (full && json.schemaVersion !== 1) ||
170
+ typeof json.source !== 'string' ||
171
+ typeof json.revision !== 'string' ||
172
+ !/^[a-f0-9]{64}$/.test(json.revision)) {
143
173
  throw new BundleError('BUNDLE_INTEGRITY', 'Invalid stored preset identity');
144
174
  }
145
175
  const source = bundleSourceUrl(json.source);
@@ -147,7 +177,8 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
147
177
  if (Object.keys(manifests).length > 32)
148
178
  throw new BundleError('BUNDLE_QUOTA', 'Stored preset inheritance exceeds 32 manifests');
149
179
  const bundleRevisions = clonePresetDefaults(json.bundleRevisions);
150
- if (Object.keys(bundleRevisions).length > 64 || Object.values(bundleRevisions).some(revision => typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))) {
180
+ if (Object.keys(bundleRevisions).length > 64 ||
181
+ Object.values(bundleRevisions).some(revision => typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))) {
151
182
  throw new BundleError('BUNDLE_INTEGRITY', 'Invalid stored package revisions');
152
183
  }
153
184
  const validated = {};
@@ -157,8 +188,14 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
157
188
  validated[url] = validatePreset(manifest);
158
189
  }
159
190
  const packageTargets = clonePresetDefaults(json.packageTargets);
160
- const data = { ...(full ? { schemaVersion: 1 } : {}), source, manifests: validated, bundleRevisions, packageTargets };
161
- if (await presetSnapshotRevision(data) !== json.revision)
191
+ const data = {
192
+ ...(full ? { schemaVersion: 1 } : {}),
193
+ source,
194
+ manifests: validated,
195
+ bundleRevisions,
196
+ packageTargets,
197
+ };
198
+ if ((await presetSnapshotRevision(data)) !== json.revision)
162
199
  throw new BundleError('BUNDLE_INTEGRITY', 'Stored preset revision does not match');
163
200
  let preset = await resolvePreset(source, {
164
201
  mearlApiVersion,
@@ -170,7 +207,9 @@ export async function verifyPresetGraph(value, mearlApiVersion, runtimes, full)
170
207
  });
171
208
  preset = applyPackageTargets(preset, packageTargets);
172
209
  const entries = presetPackages(preset, runtimes);
173
- if (preset.chain.length !== Object.keys(validated).length || entries.length !== Object.keys(bundleRevisions).length || entries.some(([id]) => !Object.hasOwn(bundleRevisions, id))) {
210
+ if (preset.chain.length !== Object.keys(validated).length ||
211
+ entries.length !== Object.keys(bundleRevisions).length ||
212
+ entries.some(([id]) => !Object.hasOwn(bundleRevisions, id))) {
174
213
  throw new BundleError('BUNDLE_INTEGRITY', 'Stored preset package graph does not match');
175
214
  }
176
215
  return { snapshot: { ...data, revision: json.revision }, preset };
package/dist/preset.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { browserPresetSnapshot } from './browserPreset.js';
2
- import { preparePresetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets } from './prepare.js';
3
- import { satisfies } from 'semver';
2
+ import { preparePresetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets, } from './prepare.js';
3
+ import satisfies from 'semver/functions/satisfies.js';
4
4
  import { BundleError, MAX_BROWSER_STORAGE_BYTES, verifyBundle } from './bundle.js';
5
5
  export async function preparePreset(source, options) {
6
6
  const prepared = await preparePresetPackages(source, options, ['browser', 'node']);
@@ -27,7 +27,9 @@ export async function verifyPreparedPreset(value, mearlApiVersion) {
27
27
  throw new BundleError('BUNDLE_QUOTA', 'Preset package count exceeds 64');
28
28
  let total = 0;
29
29
  for (const bundle of value.bundles) {
30
- if (!bundle?.resources || typeof bundle.resources !== 'object' || Array.isArray(bundle.resources))
30
+ if (!bundle?.resources ||
31
+ typeof bundle.resources !== 'object' ||
32
+ Array.isArray(bundle.resources))
31
33
  throw new BundleError('BUNDLE_INVALID', 'Expected package resource buffers');
32
34
  const resources = Object.values(bundle.resources);
33
35
  if (resources.length > 64)
@@ -46,10 +48,19 @@ export async function verifyPreparedPreset(value, mearlApiVersion) {
46
48
  ]);
47
49
  const { snapshot, preset } = graph;
48
50
  verifyPackageTargets(preset, bundles);
49
- if (bundles.length !== Object.keys(snapshot.bundleRevisions).length || new Set(bundles.map(bundle => bundle.manifest.id)).size !== bundles.length || bundles.some(bundle => {
50
- const entry = preset.packages[bundle.manifest.id];
51
- return snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision || entry?.source !== bundle.source || (entry.version && !satisfies(bundle.manifest.version, entry.version));
52
- }))
51
+ if (bundles.length !== Object.keys(snapshot.bundleRevisions).length ||
52
+ new Set(bundles.map(bundle => bundle.manifest.id)).size !== bundles.length ||
53
+ bundles.some(bundle => {
54
+ const entry = preset.packages[bundle.manifest.id];
55
+ return (snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision ||
56
+ entry?.source !== bundle.source ||
57
+ (entry.version && !satisfies(bundle.manifest.version, entry.version)));
58
+ }))
53
59
  throw new BundleError('BUNDLE_INTEGRITY', 'Preset installation requires every exact package');
54
- return { snapshot, preset, bundles, browser: await browserPresetSnapshot(snapshot.source, snapshot.manifests, preset, bundles) };
60
+ return {
61
+ snapshot,
62
+ preset,
63
+ bundles,
64
+ browser: await browserPresetSnapshot(snapshot.source, snapshot.manifests, preset, bundles),
65
+ };
55
66
  }
@@ -0,0 +1,9 @@
1
+ export declare function parseProfileSource(input: string): {
2
+ origin: string;
3
+ url: string;
4
+ github?: {
5
+ repository: string;
6
+ ref: string;
7
+ };
8
+ };
9
+ export declare function resolveProfileSource(input: string, fetcher?: typeof fetch): Promise<string>;
@@ -0,0 +1,44 @@
1
+ import { BundleError } from './bundle.js';
2
+ import { createBundleFetchLoader } from './bundleFetch.js';
3
+ const githubReference = /^([A-Za-z0-9-]+)\/([A-Za-z0-9_.-]+)(?:@([A-Za-z0-9._/-]+))?$/;
4
+ export function parseProfileSource(input) {
5
+ if (typeof input !== 'string' || input.length > 2048)
6
+ throw new BundleError('BUNDLE_INVALID', 'Invalid App profile source');
7
+ const match = githubReference.exec(input.trim());
8
+ if (match && match[2] !== '.' && match[2] !== '..') {
9
+ return {
10
+ origin: 'https://raw.githubusercontent.com',
11
+ url: input.trim(),
12
+ github: { repository: `${match[1]}/${match[2]}`, ref: match[3] ?? 'HEAD' },
13
+ };
14
+ }
15
+ let url;
16
+ try {
17
+ url = new URL(input);
18
+ }
19
+ catch {
20
+ throw new BundleError('BUNDLE_INVALID', 'Use a GitHub owner/repo or an HTTPS bundle manifest URL');
21
+ }
22
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash) {
23
+ throw new BundleError('BUNDLE_INVALID', 'App profile bundle URLs require HTTPS without credentials or a fragment');
24
+ }
25
+ return { origin: url.origin, url: url.href };
26
+ }
27
+ export async function resolveProfileSource(input, fetcher = fetch) {
28
+ const { url, github } = parseProfileSource(input);
29
+ if (!github)
30
+ return url;
31
+ const { repository, ref } = github;
32
+ let sha = ref;
33
+ if (!/^[a-f0-9]{40}$/.test(sha)) {
34
+ const load = createBundleFetchLoader({ fetch: fetcher });
35
+ const bytes = await load(`https://api.github.com/repos/${repository}/commits/${encodeURIComponent(ref)}`, {
36
+ maxBytes: 2 * 1024 * 1024,
37
+ signal: AbortSignal.timeout(10000),
38
+ });
39
+ sha = JSON.parse(new TextDecoder().decode(bytes)).sha;
40
+ if (typeof sha !== 'string' || !/^[a-f0-9]{40}$/.test(sha))
41
+ throw new BundleError('BUNDLE_INVALID', 'GitHub did not return a commit ID');
42
+ }
43
+ return `https://raw.githubusercontent.com/${repository}/${sha}/bundle.json`;
44
+ }
package/dist/resolve.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { parseToolboxCatalogs } from './toolboxCatalogs.js';
2
2
  import { parseSetupManagement } from './management.js';
3
- import { satisfies, valid } from 'semver';
3
+ import satisfies from 'semver/functions/satisfies.js';
4
+ import valid from 'semver/functions/valid.js';
4
5
  import { clonePresetDefaults, isJsonObject, PresetError, validatePreset } from './manifest.js';
5
6
  const kinds = ['packages', 'skills', 'providers'];
6
7
  /** Objects merge recursively; arrays, scalars and explicit null replace the previous value. */
7
8
  export function mergePresetDefaults(base, overrides) {
8
9
  const result = clonePresetDefaults(base);
9
10
  for (const [key, value] of Object.entries(clonePresetDefaults(overrides))) {
10
- result[key] = isJsonObject(value) && isJsonObject(result[key])
11
- ? mergePresetDefaults(result[key], value)
12
- : value;
11
+ result[key] =
12
+ isJsonObject(value) && isJsonObject(result[key])
13
+ ? mergePresetDefaults(result[key], value)
14
+ : value;
13
15
  }
14
16
  return result;
15
17
  }
@@ -93,7 +95,8 @@ export async function resolvePreset(source, options) {
93
95
  active.add(url);
94
96
  const manifest = validatePreset(await options.load(url));
95
97
  options.signal?.throwIfAborted();
96
- if (manifest.requires?.mearlApi && !satisfies(options.mearlApiVersion, manifest.requires.mearlApi)) {
98
+ if (manifest.requires?.mearlApi &&
99
+ !satisfies(options.mearlApiVersion, manifest.requires.mearlApi)) {
97
100
  throw new PresetError('PRESET_INCOMPATIBLE', `${manifest.id} requires Mearl API ${manifest.requires.mearlApi}`);
98
101
  }
99
102
  let parent;
@@ -106,21 +109,35 @@ export async function resolvePreset(source, options) {
106
109
  const result = {
107
110
  id: manifest.id,
108
111
  version: manifest.version,
109
- chain: [...(parent?.chain ?? []), { source: url, id: manifest.id, version: manifest.version }],
112
+ chain: [
113
+ ...(parent?.chain ?? []),
114
+ { source: url, id: manifest.id, version: manifest.version },
115
+ ],
110
116
  packages: { ...parent?.packages },
111
117
  skills: { ...parent?.skills },
112
118
  providers: { ...parent?.providers },
113
119
  appProfiles: { ...parent?.appProfiles, ...manifest.appProfiles },
114
120
  defaults: mergePresetDefaults(parent?.defaults ?? {}, manifest.defaults ?? {}),
115
- permissions: { origins: [...new Set([...(parent?.permissions.origins ?? []), ...(manifest.permissions?.origins ?? [])])] },
121
+ permissions: {
122
+ origins: [
123
+ ...new Set([
124
+ ...(parent?.permissions.origins ?? []),
125
+ ...(manifest.permissions?.origins ?? []),
126
+ ]),
127
+ ],
128
+ },
116
129
  };
117
130
  for (const kind of kinds) {
118
131
  for (const [id, patch] of Object.entries(manifest[kind] ?? {})) {
119
132
  result[kind][id] = {
120
133
  ...result[kind][id],
121
134
  ...patch,
122
- ...(patch.source === undefined ? {} : { source: componentSource(kind, patch.source, url) }),
123
- ...(patch.migrateFrom === undefined ? {} : { migrateFrom: patch.migrateFrom.map(source => componentSource(kind, source, url)) }),
135
+ ...(patch.source === undefined
136
+ ? {}
137
+ : { source: componentSource(kind, patch.source, url) }),
138
+ ...(patch.migrateFrom === undefined
139
+ ? {}
140
+ : { migrateFrom: patch.migrateFrom.map(source => componentSource(kind, source, url)) }),
124
141
  declaredBy: url,
125
142
  };
126
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/preset",
3
- "version": "3.0.0-beta.9",
3
+ "version": "3.0.1",
4
4
  "description": "Browser-compatible preset validation, inheritance and capability resolution for Mearl",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/installation.d.ts",
20
20
  "import": "./dist/installation.js"
21
21
  },
22
+ "./pageRuntime": {
23
+ "types": "./dist/pageRuntime.d.ts",
24
+ "import": "./dist/pageRuntime.js"
25
+ },
22
26
  "./installation-site.json": "./installation-site.json",
23
27
  "./networkRoutes": {
24
28
  "types": "./dist/networkRoutes.d.ts",
@@ -26,6 +30,7 @@
26
30
  }
27
31
  },
28
32
  "files": [
33
+ "README.en.md",
29
34
  "dist",
30
35
  "!dist/**/*.map",
31
36
  "installation-site.json"