@mearl/preset 3.0.0-beta.1 → 3.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -18
- package/README.zh-CN.md +106 -0
- package/dist/browserControls.d.ts +29 -0
- package/dist/browserControls.js +54 -0
- package/dist/browserPreset.js +8 -3
- package/dist/browserViews.d.ts +1 -1
- package/dist/browserViews.js +5 -5
- package/dist/bundle.d.ts +4 -1
- package/dist/bundle.js +24 -5
- package/dist/bundleStore.js +80 -75
- package/dist/bundles.d.ts +7 -1
- package/dist/bundles.js +4 -1
- package/dist/{nodeSnapshot.d.ts → capabilitySnapshot.d.ts} +11 -11
- package/dist/{nodeSnapshot.js → capabilitySnapshot.js} +36 -35
- package/dist/extensionContributions.d.ts +29 -0
- package/dist/extensionContributions.js +57 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/installation.d.ts +5 -1
- package/dist/installation.js +19 -6
- package/dist/management.d.ts +10 -0
- package/dist/management.js +36 -0
- package/dist/manifest.js +14 -7
- package/dist/networkRoutes.d.ts +12 -0
- package/dist/networkRoutes.js +32 -0
- package/dist/prepare.d.ts +6 -0
- package/dist/prepare.js +81 -16
- package/dist/preset.js +3 -1
- package/dist/requestUi.d.ts +3 -1
- package/dist/requestUi.js +2 -2
- package/dist/resolve.js +7 -0
- package/dist/toolboxCatalogs.d.ts +8 -0
- package/dist/toolboxCatalogs.js +34 -0
- package/dist/types.d.ts +4 -1
- package/installation-site.json +4 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# @mearl/preset
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
See the [capability model](../../docs/capability-model.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.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
See [browser bundles](../../docs/browser-bundles.md) for preparation and local storage, and [browser Presets](../../docs/browser-presets.md) for atomic activation and App implementation mappings. Both are exported through `@mearl/preset/bundles`.
|
|
8
|
+
|
|
9
|
+
Browser and Node share one inheritance resolution and resource download; see [complete resource preparation](../../docs/preset-preparation.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.
|
|
10
|
+
|
|
11
|
+
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.md).
|
|
8
12
|
|
|
9
13
|
```ts
|
|
10
14
|
import { resolvePreset, mergePresetDefaults } from '@mearl/preset';
|
|
@@ -21,11 +25,11 @@ const preset = await resolvePreset('https://example.com/presets/team.json', {
|
|
|
21
25
|
const effectiveSettings = mergePresetDefaults(preset.defaults, userSettings);
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
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.
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
See [Node capabilities](../../docs/node-capabilities.md) for module snapshots and host loading contracts.
|
|
27
31
|
|
|
28
|
-
##
|
|
32
|
+
## Manifest
|
|
29
33
|
|
|
30
34
|
```json
|
|
31
35
|
{
|
|
@@ -35,7 +39,7 @@ Node 模块快照与宿主加载合同见 [Node 能力包](../../docs/node-capab
|
|
|
35
39
|
"extends": { "source": "../base/preset.json", "version": "^1" },
|
|
36
40
|
"requires": { "mearlApi": "^1" },
|
|
37
41
|
"packages": {
|
|
38
|
-
"capture": { "source": "./capture/package.json", "
|
|
42
|
+
"capture": { "source": "./capture/package.json", "targets": ["browser"] },
|
|
39
43
|
"unused": { "enabled": false }
|
|
40
44
|
},
|
|
41
45
|
"skills": {},
|
|
@@ -45,14 +49,58 @@ Node 模块快照与宿主加载合同见 [Node 能力包](../../docs/node-capab
|
|
|
45
49
|
}
|
|
46
50
|
```
|
|
47
51
|
|
|
48
|
-
-
|
|
49
|
-
- packages
|
|
50
|
-
-
|
|
51
|
-
- source
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
- 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.
|
|
53
|
+
- 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.
|
|
54
|
+
- 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.
|
|
55
|
+
- source resolves relative to the declaring manifest. A child changing only the version does not alter an inherited source's base URL.
|
|
56
|
+
- 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.
|
|
57
|
+
- defaults recursively merges objects; arrays, scalars, and null replace values. User overrides are saved separately and applied to the resolved result.
|
|
58
|
+
- Permissions are aggregated and deduplicated as requirements only; browser installers obtain actual authorization separately.
|
|
59
|
+
- appProfiles maps App IDs to implementation identifiers. Later capability catalogs verify profile existence and package executability; the resolver does not claim simulation validation.
|
|
60
|
+
- Results retain the exact parent-to-child version chain and each component's latest declaration source. Loading failures do not produce partial success.
|
|
61
|
+
- 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.
|
|
62
|
+
|
|
63
|
+
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.md). Bundle-local storage transactionally replaces validated snapshots.
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
## Tool catalogs
|
|
68
|
+
|
|
69
|
+
Presets can register additional installable tool catalogs through `defaults.extension.toolCatalogs`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"defaults": {
|
|
74
|
+
"extension": {
|
|
75
|
+
"toolCatalogs": {
|
|
76
|
+
"team": {
|
|
77
|
+
"url": "https://tools.example.com/toolbox.json",
|
|
78
|
+
"label": "Team"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
Catalog JSON shares the official structure, limited to 256 entries and 256 KiB:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"tools": [{
|
|
93
|
+
"id": "plugin:team-tool",
|
|
94
|
+
"plugin": "team-tool",
|
|
95
|
+
"repository": "example/skills",
|
|
96
|
+
"skill": "team-tool",
|
|
97
|
+
"title": "Team tool",
|
|
98
|
+
"description": "A tool for the team",
|
|
99
|
+
"icon": "wrench"
|
|
100
|
+
}]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`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.
|
|
105
|
+
|
|
106
|
+
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.zh-CN.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @mearl/preset
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
3.x 的声明、配置、能力目录及对象存储设计见 [能力包模型](../../docs/capability-model.zh-CN.md)。包支持的位置从贡献推导,Preset 的可选 `targets` 仅选择启用子集;操作和登录定义归包,配置覆盖归 Preset 与用户。
|
|
6
|
+
|
|
7
|
+
浏览器包的准备与本地存储见 [能力包](../../docs/browser-bundles.zh-CN.md),整套原子激活与 App 实现映射见 [浏览器 Preset](../../docs/browser-presets.zh-CN.md)。这两类能力通过 `@mearl/preset/bundles` 导出。
|
|
8
|
+
|
|
9
|
+
浏览器和 Node 共用一次继承解析及资源下载,见 [整套资源准备](../../docs/preset-preparation.zh-CN.md)。`preparePreset` 返回完整确切图与兼容的浏览器投影;Node 模块选择直接复用同批字节,准备过程不执行代码或安装外部环境。
|
|
10
|
+
|
|
11
|
+
主入口在浏览器和 Node 中共享 preset 清单校验与继承解析,没有文件系统、网络、凭据或浏览器权限副作用;清单加载由调用方提供。可选子入口 `@mearl/preset/bundles` 提供资源校验、受限下载和显式调用的浏览器本地存储,见 [能力包说明](../../docs/browser-bundles.zh-CN.md)。
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { resolvePreset, mergePresetDefaults } from '@mearl/preset';
|
|
15
|
+
|
|
16
|
+
const preset = await resolvePreset('https://example.com/presets/team.json', {
|
|
17
|
+
mearlApiVersion: '1.0.0',
|
|
18
|
+
async load(source) {
|
|
19
|
+
const response = await fetch(source);
|
|
20
|
+
if (!response.ok) throw new Error(`Preset load failed: ${response.status}`);
|
|
21
|
+
return response.json();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const effectiveSettings = mergePresetDefaults(preset.defaults, userSettings);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
加载器负责检查来源、响应大小、重定向、认证和取消请求;上例仅演示合同。浏览器加载器应根据产品来源策略限制 URL,解析器不会把 HTTP(S)/file 形式的 URL 自动视为可信。
|
|
29
|
+
|
|
30
|
+
Node 模块快照与宿主加载合同见 [Node 能力包](../../docs/node-capabilities.zh-CN.md)。
|
|
31
|
+
|
|
32
|
+
## 清单
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"schemaVersion": 1,
|
|
37
|
+
"id": "team",
|
|
38
|
+
"version": "1.0.0",
|
|
39
|
+
"extends": { "source": "../base/preset.json", "version": "^1" },
|
|
40
|
+
"requires": { "mearlApi": "^1" },
|
|
41
|
+
"packages": {
|
|
42
|
+
"capture": { "source": "./capture/package.json", "targets": ["browser"] },
|
|
43
|
+
"unused": { "enabled": false }
|
|
44
|
+
},
|
|
45
|
+
"skills": {},
|
|
46
|
+
"providers": {},
|
|
47
|
+
"defaults": { "simulation": { "app": "sample" } },
|
|
48
|
+
"permissions": { "origins": ["https://*.example.com/*"] }
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- 单继承,最多 32 层;拒绝循环和不满足的父版本/API 要求。`version` 是确切 semver,组件 `version` 是版本要求,具体安装版本留给安装器解析。
|
|
53
|
+
- packages、skills、providers 按 ID 合并,子级字段覆盖同 ID 声明;数组整体替换。启用项最终必须存在 source,`enabled:false` 保留停用声明。
|
|
54
|
+
- 组件 `dependsOn` 引用同类组件 ID;检查最终继承图中的缺失、停用和循环依赖。它不表示 runtime 兼容或安装已完成。
|
|
55
|
+
- source 相对声明它的清单解析;子级只修改版本时不会改变继承 source 的基准 URL。
|
|
56
|
+
- Skill 可声明 `migrateFrom` 旧来源列表(最多 16 项),相对地址按声明清单解析;子级整体覆盖,空数组清除。安装器仅在来源收据匹配旧来源、Skill 身份有效且所有关联插件均由 setup 托管时迁移。手动注册、共享的手动别名或其他来源保留并报告冲突;来源匹配仍严格检查 ref。
|
|
57
|
+
- defaults 递归合并对象,数组、标量和 null 替换;用户覆盖单独保存,在解析结果上应用。
|
|
58
|
+
- 权限汇总去重,结果仅表示需求;必须由浏览器安装器另行取得实际授权。
|
|
59
|
+
- appProfiles 是 App ID 到实现标识的覆盖映射。具体档案是否存在、所需包是否可执行,由后续能力目录校验,解析器不宣称已验证模拟效果。
|
|
60
|
+
- 结果保留父级到子级的确切版本链和各组件最近声明来源,加载失败不会生成部分成功的结果。
|
|
61
|
+
- 拒绝未知字段、非 JSON 配置和原型污染键。清单总量不超过 256 Ki UTF-16 code units,defaults 最大嵌套 32 层。
|
|
62
|
+
|
|
63
|
+
本包负责通用清单解析、校验和资源图,不执行安装或展示 UI。完整安装器、preset 锁文件持久化、权限、注册与激活由浏览器宿主及 setup 提供,见 [安装宿主](../../docs/preset-installation.zh-CN.md);能力包本地存储负责已校验快照的事务替换。
|
|
64
|
+
|
|
65
|
+
Skill manager 的可选 `readSources(scope, cwd)` 返回该管理器核验过的来源收据,结构与 setup 的 `TrackedSkillSource` 一致。CLI 与 Native 安装状态共用该读取接口;管理器负责持久化、校验和失效自身补充记录,公共宿主继续核对来源、ref、清单身份与托管归属。未提供时读取标准 Skills 锁文件。
|
|
66
|
+
|
|
67
|
+
## 工具目录
|
|
68
|
+
|
|
69
|
+
Preset 可通过 `defaults.extension.toolCatalogs` 注册额外的可安装工具目录:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"defaults": {
|
|
74
|
+
"extension": {
|
|
75
|
+
"toolCatalogs": {
|
|
76
|
+
"team": {
|
|
77
|
+
"url": "https://tools.example.com/toolbox.json",
|
|
78
|
+
"label": "Team"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
目录使用绝对 HTTPS URL(本机开发允许 loopback HTTP),最多 16 个。使用稳定的目录 ID,子 preset 可只覆盖 `url` 或 `label`,或设置 `enabled: false` 禁用父级目录。官方目录始终保留且优先,不能使用保留 ID `official`。需要跨域访问时在 preset 的 `permissions.origins` 声明相应域名,由现有授权流程取得权限。
|
|
87
|
+
|
|
88
|
+
目录 JSON 与官方目录使用同一结构,最多 256 项、256 KiB:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"tools": [{
|
|
93
|
+
"id": "plugin:team-tool",
|
|
94
|
+
"plugin": "team-tool",
|
|
95
|
+
"repository": "example/skills",
|
|
96
|
+
"skill": "team-tool",
|
|
97
|
+
"title": "Team tool",
|
|
98
|
+
"description": "A tool for the team",
|
|
99
|
+
"icon": "wrench"
|
|
100
|
+
}]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`repository` 也支持 HTTPS/SSH Git 来源和固定 ref(例如 `git@code.example.com:team/skills.git#release/v3`),安装时原样交给 preset 选择的 Skills 管理器。清单不接受 shell 参数、凭据或本地文件路径,也不执行其中的代码;列出工具不会自动安装。
|
|
105
|
+
|
|
106
|
+
各来源并发读取、独立缓存,重试时刷新当前目录;失败时保留该来源已有缓存,其他来源仍可使用。相同插件 ID 与安装来源去重;同 ID 指向不同仓库或 Skill 时保留优先目录并提示冲突。较低优先目录的安装按钮会等待前面的目录完成检查,避免加载顺序改变安装目标。更换或禁用目录后,旧来源和迟到响应不会留在当前列表。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type PreparedBundle } from './bundle.js';
|
|
2
|
+
import { type BrowserUiText } from './requestUi.js';
|
|
3
|
+
import { type NetworkRoutePlan } from './networkRoutes.js';
|
|
4
|
+
export interface BrowserSegmentedControl {
|
|
5
|
+
schemaVersion: 1;
|
|
6
|
+
id: string;
|
|
7
|
+
kind: 'segmented';
|
|
8
|
+
label: BrowserUiText;
|
|
9
|
+
description: BrowserUiText;
|
|
10
|
+
defaultValue: string;
|
|
11
|
+
aliases?: string[];
|
|
12
|
+
messages: {
|
|
13
|
+
changed: BrowserUiText;
|
|
14
|
+
failed: BrowserUiText;
|
|
15
|
+
};
|
|
16
|
+
options: Array<{
|
|
17
|
+
value: string;
|
|
18
|
+
label: BrowserUiText;
|
|
19
|
+
networkRoutes: NetworkRoutePlan;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface SelectedBrowserControl {
|
|
23
|
+
bundleId: string;
|
|
24
|
+
revision: string;
|
|
25
|
+
definition: BrowserSegmentedControl;
|
|
26
|
+
}
|
|
27
|
+
export declare function validateBrowserControl(input: unknown): BrowserSegmentedControl;
|
|
28
|
+
/** The network-route surface has one owner, so competing packages cannot silently overwrite it. */
|
|
29
|
+
export declare function selectBrowserControl(bundles: PreparedBundle[]): SelectedBrowserControl | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BundleError, readBundleJson } from './bundle.js';
|
|
2
|
+
import { validateBrowserUiText } from './requestUi.js';
|
|
3
|
+
import { parseNetworkRoutePlan } from './networkRoutes.js';
|
|
4
|
+
const id = /^[a-z][a-zA-Z0-9_.-]{0,79}$/;
|
|
5
|
+
function fields(value, names) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !names.includes(key)))
|
|
7
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid browser control');
|
|
8
|
+
}
|
|
9
|
+
export function validateBrowserControl(input) {
|
|
10
|
+
fields(input, ['schemaVersion', 'id', 'kind', 'label', 'description', 'defaultValue', 'aliases', 'messages', 'options']);
|
|
11
|
+
if (input.schemaVersion !== 1 || input.kind !== 'segmented' || typeof input.id !== 'string' || !id.test(input.id) || typeof input.defaultValue !== 'string' || !id.test(input.defaultValue))
|
|
12
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid segmented control');
|
|
13
|
+
validateBrowserUiText(input.label);
|
|
14
|
+
validateBrowserUiText(input.description);
|
|
15
|
+
fields(input.messages, ['changed', 'failed']);
|
|
16
|
+
validateBrowserUiText(input.messages.changed);
|
|
17
|
+
validateBrowserUiText(input.messages.failed);
|
|
18
|
+
if (input.aliases !== undefined && (!Array.isArray(input.aliases) || input.aliases.length > 8 || new Set(input.aliases).size !== input.aliases.length || input.aliases.some(alias => typeof alias !== 'string' || !id.test(alias))))
|
|
19
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid browser control aliases');
|
|
20
|
+
if (!Array.isArray(input.options) || input.options.length < 2 || input.options.length > 8)
|
|
21
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid browser control options');
|
|
22
|
+
const values = new Set();
|
|
23
|
+
const options = input.options.map(option => {
|
|
24
|
+
fields(option, ['value', 'label', 'networkRoutes']);
|
|
25
|
+
if (typeof option.value !== 'string' || !id.test(option.value) || values.has(option.value))
|
|
26
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid browser control option');
|
|
27
|
+
values.add(option.value);
|
|
28
|
+
validateBrowserUiText(option.label);
|
|
29
|
+
try {
|
|
30
|
+
return { value: option.value, label: structuredClone(option.label), networkRoutes: parseNetworkRoutePlan(option.networkRoutes) };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid control network route plan');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
if (!values.has(input.defaultValue) || options.find(option => option.value === input.defaultValue).networkRoutes.routes.length)
|
|
37
|
+
throw new BundleError('BUNDLE_INVALID', 'The default option must clear network routes');
|
|
38
|
+
if (new TextEncoder().encode(JSON.stringify(input)).byteLength > 65536)
|
|
39
|
+
throw new BundleError('BUNDLE_QUOTA', 'Browser control exceeds 64 KiB');
|
|
40
|
+
return { schemaVersion: 1, id: input.id, kind: 'segmented', label: structuredClone(input.label), description: structuredClone(input.description), defaultValue: input.defaultValue, messages: structuredClone(input.messages), options, ...(input.aliases ? { aliases: [...input.aliases] } : {}) };
|
|
41
|
+
}
|
|
42
|
+
/** The network-route surface has one owner, so competing packages cannot silently overwrite it. */
|
|
43
|
+
export function selectBrowserControl(bundles) {
|
|
44
|
+
let selected;
|
|
45
|
+
for (const bundle of bundles) {
|
|
46
|
+
const resource = bundle.manifest.contributions?.browserControls;
|
|
47
|
+
if (!resource)
|
|
48
|
+
continue;
|
|
49
|
+
if (selected)
|
|
50
|
+
throw new BundleError('BUNDLE_CONFLICT', 'Browser network control has multiple providers');
|
|
51
|
+
selected = { bundleId: bundle.manifest.id, revision: bundle.revision, definition: validateBrowserControl(readBundleJson(bundle, resource)) };
|
|
52
|
+
}
|
|
53
|
+
return selected;
|
|
54
|
+
}
|
package/dist/browserPreset.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { selectBrowserControl } from './browserControls.js';
|
|
1
2
|
import { BundleError, MAX_BROWSER_STORAGE_BYTES, verifyBundle } from './bundle.js';
|
|
2
3
|
import { satisfies } from 'semver';
|
|
3
|
-
import { preparePresetPackages, presetPackages, presetSnapshotRevision, verifyPresetGraph } from './prepare.js';
|
|
4
|
+
import { preparePresetPackages, presetPackages, presetSnapshotRevision, verifyPresetGraph, verifyPackageTargets } from './prepare.js';
|
|
4
5
|
/** Core browser defaults also need synchronization when no browser package is declared. */
|
|
5
6
|
export function presetRequiresBrowser(preset) {
|
|
6
|
-
return Object.values(preset.packages).some(entry => entry.enabled !== false && entry.
|
|
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
8
|
}
|
|
8
9
|
/** Browser selection can be derived from the same accepted batch without another download. */
|
|
9
10
|
export async function browserPresetSnapshot(source, manifests, preset, bundles) {
|
|
10
11
|
const ids = new Set(presetPackages(preset, ['browser']).map(([id]) => id));
|
|
11
12
|
const selected = bundles.filter(bundle => ids.has(bundle.manifest.id));
|
|
12
|
-
|
|
13
|
+
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 };
|
|
13
16
|
return { snapshot: { ...data, revision: await presetSnapshotRevision(data) }, preset, bundles: selected };
|
|
14
17
|
}
|
|
15
18
|
export async function prepareBrowserPreset(source, options) {
|
|
@@ -48,5 +51,7 @@ export async function verifyPreparedBrowserPreset(value, mearlApiVersion) {
|
|
|
48
51
|
return graph.snapshot.bundleRevisions[bundle.manifest.id] !== bundle.revision || entry?.source !== bundle.source || (entry.version && !satisfies(bundle.manifest.version, entry.version));
|
|
49
52
|
}))
|
|
50
53
|
throw new BundleError('BUNDLE_INTEGRITY', 'Browser activation requires every exact package');
|
|
54
|
+
selectBrowserControl(bundles);
|
|
55
|
+
verifyPackageTargets(graph.preset, bundles);
|
|
51
56
|
return { ...graph, bundles };
|
|
52
57
|
}
|
package/dist/browserViews.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface BrowserViewDefinition {
|
|
|
16
16
|
entry: string;
|
|
17
17
|
actions: Record<string, BrowserViewAction>;
|
|
18
18
|
preferences?: Record<string, {
|
|
19
|
-
|
|
19
|
+
shared?: 'editor.highlight' | 'json.viewMode' | 'json.shortcuts' | 'code.shortcuts';
|
|
20
20
|
area?: 'local' | 'session';
|
|
21
21
|
}>;
|
|
22
22
|
transports?: Record<string, BrowserViewTransport>;
|
package/dist/browserViews.js
CHANGED
|
@@ -30,13 +30,13 @@ export function validateBrowserView(input) {
|
|
|
30
30
|
if (value.preferences !== undefined) {
|
|
31
31
|
const aliases = new Set();
|
|
32
32
|
for (const [, preference] of entries(value.preferences, 32)) {
|
|
33
|
-
fields(preference, ['
|
|
33
|
+
fields(preference, ['shared', 'area']);
|
|
34
34
|
if (preference.area !== undefined && preference.area !== 'local' && preference.area !== 'session')
|
|
35
35
|
invalid('Invalid browser view preference area');
|
|
36
|
-
if (preference.
|
|
37
|
-
if (typeof preference.
|
|
36
|
+
if (preference.shared !== undefined) {
|
|
37
|
+
if (typeof preference.shared !== 'string' || !['editor.highlight', 'json.viewMode', 'json.shortcuts', 'code.shortcuts'].includes(preference.shared) || aliases.has(preference.shared))
|
|
38
38
|
invalid('Invalid browser view preference alias');
|
|
39
|
-
aliases.add(preference.
|
|
39
|
+
aliases.add(preference.shared);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -200,5 +200,5 @@ export function buildBrowserViewPage(html) {
|
|
|
200
200
|
if (typeof html !== 'string' || !html.trim())
|
|
201
201
|
invalid('Browser view entry must not be empty');
|
|
202
202
|
const policy = "default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
|
|
203
|
-
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${policy}"><script>(${installBrowserViewBridge.toString()})();</script>${html}
|
|
203
|
+
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${policy}"><script>(${installBrowserViewBridge.toString()})();</script>${html}<style data-mearl-view-surface>html,body{background:transparent!important}</style>`;
|
|
204
204
|
}
|
package/dist/bundle.d.ts
CHANGED
|
@@ -14,17 +14,20 @@ export interface BundleManifest {
|
|
|
14
14
|
requires?: {
|
|
15
15
|
mearlApi: string;
|
|
16
16
|
};
|
|
17
|
+
defaults?: import('./types.js').JsonObject;
|
|
17
18
|
resources: Record<string, {
|
|
18
19
|
sha256: string;
|
|
19
20
|
byteLength: number;
|
|
20
21
|
}>;
|
|
21
22
|
contributions?: {
|
|
22
|
-
appProfiles
|
|
23
|
+
appProfiles?: string[];
|
|
23
24
|
pageRuntimes?: Record<string, string>;
|
|
24
25
|
nodeRuntimes?: Record<string, CapabilityRuntimeDeclaration>;
|
|
25
26
|
browserRuntimes?: Record<string, CapabilityRuntimeDeclaration>;
|
|
27
|
+
extension?: string;
|
|
26
28
|
requestUi?: string;
|
|
27
29
|
browserViews?: Record<string, string>;
|
|
30
|
+
browserControls?: string;
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
33
|
export interface CapabilityRuntimeDeclaration {
|
package/dist/bundle.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { satisfies, valid, validRange } from 'semver';
|
|
2
2
|
import { clonePresetDefaults } from './manifest.js';
|
|
3
|
+
import { validateBrowserControl } from './browserControls.js';
|
|
3
4
|
export const MAX_BUNDLE_BYTES = 16 * 1024 * 1024;
|
|
4
5
|
export const MAX_BUNDLE_MANIFEST_BYTES = 64 * 1024;
|
|
5
6
|
export const MAX_BROWSER_STORAGE_BYTES = 64 * 1024 * 1024;
|
|
@@ -47,7 +48,9 @@ export function validateBundleManifest(value, mearlApiVersion) {
|
|
|
47
48
|
catch {
|
|
48
49
|
invalid('Expected a JSON manifest');
|
|
49
50
|
}
|
|
50
|
-
fields(json, ['schemaVersion', 'id', 'version', 'requires', 'resources', 'contributions']);
|
|
51
|
+
fields(json, ['schemaVersion', 'id', 'version', 'requires', 'defaults', 'resources', 'contributions']);
|
|
52
|
+
if (json.defaults !== undefined)
|
|
53
|
+
json.defaults = clonePresetDefaults(json.defaults);
|
|
51
54
|
if (json.schemaVersion !== 1 || typeof json.id !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(json.id))
|
|
52
55
|
invalid('Invalid bundle identity');
|
|
53
56
|
if (typeof json.version !== 'string' || valid(json.version) !== json.version)
|
|
@@ -79,7 +82,17 @@ export function validateBundleManifest(value, mearlApiVersion) {
|
|
|
79
82
|
throw new BundleError('BUNDLE_QUOTA', 'Bundle resources exceed 16 MiB');
|
|
80
83
|
}
|
|
81
84
|
if (json.contributions !== undefined) {
|
|
82
|
-
fields(json.contributions, ['appProfiles', 'pageRuntimes', 'nodeRuntimes', 'browserRuntimes', 'requestUi', 'browserViews']);
|
|
85
|
+
fields(json.contributions, ['appProfiles', 'pageRuntimes', 'nodeRuntimes', 'browserRuntimes', 'extension', 'requestUi', 'browserViews', 'browserControls']);
|
|
86
|
+
const extension = json.contributions.extension;
|
|
87
|
+
if (extension !== undefined && (typeof extension !== 'string' || !Object.hasOwn(json.resources, extension)))
|
|
88
|
+
invalid('Extension contributions must reference a declared resource');
|
|
89
|
+
if (typeof extension === 'string' && json.resources[extension].byteLength > MAX_REQUEST_UI_BYTES)
|
|
90
|
+
throw new BundleError('BUNDLE_QUOTA', 'Extension contributions exceed 64 KiB');
|
|
91
|
+
const controls = json.contributions.browserControls;
|
|
92
|
+
if (controls !== undefined && (typeof controls !== 'string' || !Object.hasOwn(json.resources, controls)))
|
|
93
|
+
invalid('Browser controls must reference a declared resource');
|
|
94
|
+
if (typeof controls === 'string' && json.resources[controls].byteLength > MAX_REQUEST_UI_BYTES)
|
|
95
|
+
throw new BundleError('BUNDLE_QUOTA', 'Browser controls exceed 64 KiB');
|
|
83
96
|
if (json.contributions.requestUi !== undefined && (typeof json.contributions.requestUi !== 'string' || !Object.hasOwn(json.resources, json.contributions.requestUi)))
|
|
84
97
|
invalid('Request UI must reference a declared resource');
|
|
85
98
|
if (typeof json.contributions.requestUi === 'string' && json.resources[json.contributions.requestUi].byteLength > MAX_REQUEST_UI_BYTES)
|
|
@@ -93,7 +106,7 @@ export function validateBundleManifest(value, mearlApiVersion) {
|
|
|
93
106
|
throw new BundleError('BUNDLE_QUOTA', 'Browser view definition exceeds 64 KiB');
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
|
-
const profiles = json.contributions.appProfiles;
|
|
109
|
+
const profiles = json.contributions.appProfiles === undefined ? [] : json.contributions.appProfiles;
|
|
97
110
|
if (!Array.isArray(profiles) || profiles.some(name => typeof name !== 'string' || !Object.hasOwn(json.resources, name)) ||
|
|
98
111
|
new Set(profiles).size !== profiles.length)
|
|
99
112
|
invalid('App profiles must reference distinct declared resources');
|
|
@@ -188,7 +201,10 @@ export async function prepareBundle(source, options) {
|
|
|
188
201
|
resources[name] = resource;
|
|
189
202
|
}
|
|
190
203
|
options.signal?.throwIfAborted();
|
|
191
|
-
|
|
204
|
+
const prepared = { source, manifest, revision: await manifestRevision(source, manifest), resources };
|
|
205
|
+
if (manifest.contributions?.browserControls)
|
|
206
|
+
validateBrowserControl(readBundleJson(prepared, manifest.contributions.browserControls));
|
|
207
|
+
return prepared;
|
|
192
208
|
}
|
|
193
209
|
/** Revalidates persisted snapshots without network access and returns independent resource buffers. */
|
|
194
210
|
export async function verifyBundle(bundle, mearlApiVersion) {
|
|
@@ -215,5 +231,8 @@ export async function verifyBundle(bundle, mearlApiVersion) {
|
|
|
215
231
|
if (await bundleDigest(resources[name]) !== expected.sha256)
|
|
216
232
|
throw new BundleError('BUNDLE_INTEGRITY', `Stored resource integrity check failed: ${name}`);
|
|
217
233
|
}
|
|
218
|
-
|
|
234
|
+
const prepared = { source, manifest, revision, resources };
|
|
235
|
+
if (manifest.contributions?.browserControls)
|
|
236
|
+
validateBrowserControl(readBundleJson(prepared, manifest.contributions.browserControls));
|
|
237
|
+
return prepared;
|
|
219
238
|
}
|