@webskill/sdk 0.2.8 → 0.4.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/README.md +42 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +4 -9
- package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-KsujmL4b-Clx1kCnU.js} +278 -122
- package/dist/client-BCM6Z3yq-qUQNkKrK.js +7787 -0
- package/dist/{dist-BQzncxXg.js → dist-8oQRa8Xz.js} +212 -13
- package/dist/{dist-B9VLwOME.js → dist-C-Sh0MDU.js} +1019 -317
- package/dist/{dist-CtBLBbEz.js → dist-D9Lcn5Pp.js} +528 -838
- package/dist/governance.d.ts +46 -11
- package/dist/governance.js +152 -25
- package/dist/{index-7DVaZJU7.d.ts → index-CHXxDccV.d.ts} +62 -144
- package/dist/{index-QrHtAudz.d.ts → index-DLfR2Y6I.d.ts} +412 -21
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
- package/dist/node.d.ts +297 -4
- package/dist/node.js +281 -7
- package/dist/{openUiLibrary-B8-Cvou9-D3RsU2EB.js → openUiLibrary-YLS-cxyT-C96jWDQq.js} +6 -5
- package/dist/skillVersionStore-uyefLPR1-DXOzbksv.d.ts +158 -0
- package/dist/stdio-CFMoANJJ-BxrTeXh7.js +31 -0
- package/dist/{testing-BUoXvm1u.js → testing-DDCJWvgA.js} +8 -6
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/{types-AmKCKJn_-BogJPQHU.d.ts → types-7Wcg--Vh-1YlQ4jF9.d.ts} +219 -70
- package/dist/types-WovEf4ED-CZSDiiBU.js +6215 -0
- package/dist/ui-react.d.ts +329 -18
- package/dist/ui-react.js +3715 -3464
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-CNaUpasU-CfSRvqCZ.js → webskillLitCatalog-CSTbhBe_-CYIs5BX8.js} +312 -122
- package/package.json +4 -7
- package/dist/jsonRenderRegistry-9GrWP_hE-CQY8bT9w.js +0 -2468
- package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +0 -48
- package/dist/skillVersionStore-B7rGjtMi-BgnQho9v.d.ts +0 -365
|
@@ -24,6 +24,20 @@ function messageOf(e) {
|
|
|
24
24
|
const SKILL_MANIFEST_FILE = "webskill.skill-manifest.json";
|
|
25
25
|
/** managed root 下的锁定文件名(不参与 files 列表与 digest) */
|
|
26
26
|
const SKILLS_LOCKFILE = "skills.lock.json";
|
|
27
|
+
/** 技能目录内的签名文件名(不参与 files 列表与 digest) */
|
|
28
|
+
const SKILL_SIGNATURE_FILE = "webskill.skill-signature.json";
|
|
29
|
+
/**
|
|
30
|
+
* 不参与 digest 的技能目录内文件。
|
|
31
|
+
*
|
|
32
|
+
* 这份名单原本在 node / browser / console / core 各抄一份,签名文件一旦漏抄一处,
|
|
33
|
+
* 写签名就会改变该处算出的 digest、让签名当场自我失效——而且只在「先签盘上的包、
|
|
34
|
+
* 再从盘上验」时才暴露。收成一处是为了让「漏抄」这件事不再可能发生。
|
|
35
|
+
*/
|
|
36
|
+
const MANIFEST_EXCLUDED_FILES = /* @__PURE__ */ new Set([
|
|
37
|
+
SKILL_MANIFEST_FILE,
|
|
38
|
+
SKILLS_LOCKFILE,
|
|
39
|
+
SKILL_SIGNATURE_FILE
|
|
40
|
+
]);
|
|
27
41
|
/** 整体 digest:逐文件 "<path>:<sha256>\n" 拼接串(文件按 path 排序)的 sha256(hash 由调用方提供,可为 WebCrypto 异步实现) */
|
|
28
42
|
async function computeDigest(files, sha256) {
|
|
29
43
|
return sha256([...files].sort((a, b) => a.path.localeCompare(b.path)).map((f) => `${f.path}:${f.sha256}\n`).join(""));
|
|
@@ -53,7 +67,7 @@ function verifyManifest(manifest, actualHashes, actualFiles = [...actualHashes.k
|
|
|
53
67
|
if (actual === void 0) missing.push(file.path);
|
|
54
68
|
else if (actual !== file.sha256) mismatches.push(file.path);
|
|
55
69
|
}
|
|
56
|
-
const extras = actualFiles.filter((p) =>
|
|
70
|
+
const extras = actualFiles.filter((p) => !MANIFEST_EXCLUDED_FILES.has(p) && !listed.has(p)).sort();
|
|
57
71
|
return {
|
|
58
72
|
ok: mismatches.length === 0 && missing.length === 0 && extras.length === 0,
|
|
59
73
|
mismatches,
|
|
@@ -61,6 +75,202 @@ function verifyManifest(manifest, actualHashes, actualFiles = [...actualHashes.k
|
|
|
61
75
|
missing
|
|
62
76
|
};
|
|
63
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* 原子写文本:先写临时文件再 rename(进程崩溃不留下半写文件;lockfile 等账本场景)。
|
|
80
|
+
* 原子性强弱取决于 provider 的 rename 实现:NodeFS/MemoryFS 为真 rename;
|
|
81
|
+
* OPFS 无原生 move,rename 是 copy+delete(非原子,崩溃可能残留双份),
|
|
82
|
+
* 对崩溃一致性要求极高的场景在浏览器侧需知悉此前提。
|
|
83
|
+
*/
|
|
84
|
+
async function atomicWriteText(fs, path, content) {
|
|
85
|
+
const tmp = `${path}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
86
|
+
await fs.writeText(tmp, content);
|
|
87
|
+
await fs.rename(tmp, path);
|
|
88
|
+
}
|
|
89
|
+
/** 技能目录内的签名文件名(与 manifest 同级,不参与 files 列表与 digest) */
|
|
90
|
+
const SKILL_SIGNATURE_FILE$1 = "webskill.skill-signature.json";
|
|
91
|
+
const SIGNATURE_SCHEMA_VERSION = 1;
|
|
92
|
+
const encoder = new TextEncoder();
|
|
93
|
+
const toBase64Url = (bytes) => {
|
|
94
|
+
let binary = "";
|
|
95
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
96
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
97
|
+
};
|
|
98
|
+
const fromBase64Url = (value) => {
|
|
99
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
100
|
+
const binary = atob(padded);
|
|
101
|
+
const bytes = new Uint8Array(binary.length);
|
|
102
|
+
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
103
|
+
return bytes;
|
|
104
|
+
};
|
|
105
|
+
const toHex = (bytes) => [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
106
|
+
function subtle() {
|
|
107
|
+
const available = globalThis.crypto?.subtle;
|
|
108
|
+
if (!available) throw new WebSkillError("SIGNATURE_UNSUPPORTED", "WebCrypto (globalThis.crypto.subtle) is unavailable in this environment; skill signatures cannot be verified");
|
|
109
|
+
return available;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 签名载荷:固定键序手写拼装,**不依赖 JSON.stringify 的属性顺序**
|
|
113
|
+
* (对象字面量顺序与运行时插入顺序不是同一件事,签验两侧字节必须逐字一致)。
|
|
114
|
+
*
|
|
115
|
+
* 载荷带 skill/version 而非裸 digest:只签 digest 时,同内容不同名的两个包
|
|
116
|
+
* 签名可以互换,等于允许把 A 技能的签名挪到 B 技能上。
|
|
117
|
+
*/
|
|
118
|
+
function signaturePayloadBytes(manifest) {
|
|
119
|
+
const json = `{"schemaVersion":${JSON.stringify(1)},"algorithm":${JSON.stringify(manifest.integrity.algorithm)},"digest":${JSON.stringify(manifest.integrity.digest)},"skill":${JSON.stringify(manifest.name)},"version":${JSON.stringify(manifest.version ?? "")}}`;
|
|
120
|
+
return encoder.encode(json);
|
|
121
|
+
}
|
|
122
|
+
/** Ed25519 在部分浏览器仍在铺开:能力缺失是环境问题,不能报成数据问题 */
|
|
123
|
+
async function importEd25519(key, format, usage) {
|
|
124
|
+
try {
|
|
125
|
+
return await subtle().importKey(format, key, { name: "Ed25519" }, false, [usage]);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e instanceof WebSkillError) throw e;
|
|
128
|
+
throw new WebSkillError("SIGNATURE_UNSUPPORTED", `This environment cannot import Ed25519 keys via WebCrypto; skill signature ${usage === "sign" ? "signing" : "verification"} is unavailable`, e);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function keyIdOf(publicKey) {
|
|
132
|
+
const digest = await subtle().digest("SHA-256", publicKey);
|
|
133
|
+
return toHex(new Uint8Array(digest).slice(0, 16));
|
|
134
|
+
}
|
|
135
|
+
/** 结构校验:字段缺失/类型错/长度不对一律 SIGNATURE_MALFORMED */
|
|
136
|
+
function parseSignature(raw, origin) {
|
|
137
|
+
const bad = (why) => {
|
|
138
|
+
throw new WebSkillError("SIGNATURE_MALFORMED", `Skill signature at ${origin} is invalid: ${why}`);
|
|
139
|
+
};
|
|
140
|
+
if (typeof raw !== "object" || raw === null) return bad("not a JSON object");
|
|
141
|
+
const file = raw;
|
|
142
|
+
if (typeof file.schemaVersion !== "number") return bad("missing \"schemaVersion\"");
|
|
143
|
+
if (file.schemaVersion > 1) return bad(`schemaVersion ${file.schemaVersion} is newer than this build understands`);
|
|
144
|
+
if (file.algorithm !== "ed25519") return bad(`unsupported algorithm ${JSON.stringify(file.algorithm)}`);
|
|
145
|
+
for (const field of [
|
|
146
|
+
"keyId",
|
|
147
|
+
"publicKey",
|
|
148
|
+
"signature",
|
|
149
|
+
"signedAt"
|
|
150
|
+
]) if (typeof file[field] !== "string" || file[field] === "") return bad(`missing "${field}"`);
|
|
151
|
+
const signature = file;
|
|
152
|
+
let publicKey;
|
|
153
|
+
let signatureBytes;
|
|
154
|
+
try {
|
|
155
|
+
publicKey = fromBase64Url(signature.publicKey);
|
|
156
|
+
signatureBytes = fromBase64Url(signature.signature);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return bad(`"publicKey" or "signature" is not valid base64url: ${e instanceof Error ? e.message : String(e)}`);
|
|
159
|
+
}
|
|
160
|
+
if (publicKey.length !== 32) return bad(`"publicKey" must be 32 raw bytes, got ${publicKey.length}`);
|
|
161
|
+
if (signatureBytes.length !== 64) return bad(`"signature" must be 64 raw bytes, got ${signatureBytes.length}`);
|
|
162
|
+
return signature;
|
|
163
|
+
}
|
|
164
|
+
/** 从技能目录读签名文件;文件不存在返回 undefined(缺签名由策略层判定,不是错误) */
|
|
165
|
+
async function readSkillSignature(fs, skillRoot) {
|
|
166
|
+
const path = `${skillRoot}/${SKILL_SIGNATURE_FILE$1}`;
|
|
167
|
+
if (!await fs.exists(path)) return void 0;
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(await fs.readText(path));
|
|
171
|
+
} catch (e) {
|
|
172
|
+
throw new WebSkillError("SIGNATURE_MALFORMED", `Skill signature at ${path} is not valid JSON`, e);
|
|
173
|
+
}
|
|
174
|
+
return parseSignature(parsed, path);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 验签。四种失败各自一个错误码,因为宿主的处置完全不同:
|
|
178
|
+
* 缺签名要引导选策略,坏文件要提示重下,不受信的 key 要引导添加密钥,
|
|
179
|
+
* 而验签不通过是安全事件,不提供「继续」。
|
|
180
|
+
*
|
|
181
|
+
* 包内自带的 publicKey **不产生任何信任**——它只是让「摘要不匹配」与
|
|
182
|
+
* 「公钥不受信」两种失败可区分。信任判定只认信任库里的 keyId。
|
|
183
|
+
*/
|
|
184
|
+
async function verifySkillSignature(input) {
|
|
185
|
+
const { manifest, signature, trustedKeys, policy } = input;
|
|
186
|
+
const skill = JSON.stringify(manifest.name);
|
|
187
|
+
if (!signature) {
|
|
188
|
+
if (policy.unsigned === "deny") throw new WebSkillError("SIGNATURE_MISSING", `Skill ${skill} carries no ${SKILL_SIGNATURE_FILE$1} and the unsigned policy is "deny"`);
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
unsigned: true,
|
|
192
|
+
...policy.unsigned === "warn" ? { warning: `Skill ${skill} is not signed; it was allowed by the "warn" unsigned policy` } : {}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const trusted = await trustedKeys.get(signature.keyId);
|
|
196
|
+
if (!trusted) throw new WebSkillError("SIGNATURE_UNTRUSTED_KEY", `Skill ${skill} is signed by key ${signature.keyId}, which is not in the trusted key store`);
|
|
197
|
+
if (trusted.publicKey !== signature.publicKey) throw new WebSkillError("SIGNATURE_MISMATCH", `Skill ${skill} declares key ${signature.keyId} but its embedded public key differs from the trusted one`);
|
|
198
|
+
const key = await importEd25519(fromBase64Url(trusted.publicKey), "raw", "verify");
|
|
199
|
+
if (!await subtle().verify("Ed25519", key, fromBase64Url(signature.signature), signaturePayloadBytes(manifest))) throw new WebSkillError("SIGNATURE_MISMATCH", `Signature verification failed for skill ${skill}: the signed digest does not match this package`);
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
unsigned: false,
|
|
203
|
+
keyId: signature.keyId
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 为已生成 manifest 的技能产出签名。生产与测试走同一条路径——
|
|
208
|
+
* 只提供 CLI 会让测试无法覆盖真正被调用的代码。
|
|
209
|
+
*
|
|
210
|
+
* `publicKey` 必须显式传入:WebCrypto 没有「由私钥导出公钥」的 API,
|
|
211
|
+
* 密钥对的两半只能一起来。
|
|
212
|
+
*/
|
|
213
|
+
async function signSkill(input) {
|
|
214
|
+
const key = input.privateKey instanceof Uint8Array ? await importEd25519(input.privateKey, "pkcs8", "sign") : input.privateKey;
|
|
215
|
+
const signature = await subtle().sign("Ed25519", key, signaturePayloadBytes(input.manifest));
|
|
216
|
+
return {
|
|
217
|
+
schemaVersion: 1,
|
|
218
|
+
algorithm: "ed25519",
|
|
219
|
+
keyId: await keyIdOf(input.publicKey),
|
|
220
|
+
publicKey: toBase64Url(input.publicKey),
|
|
221
|
+
signature: toBase64Url(new Uint8Array(signature)),
|
|
222
|
+
signedAt: input.signedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* `FileSystemProvider` 后端的信任库:单个 JSON 文件,写入走原子替换。
|
|
227
|
+
*
|
|
228
|
+
* 「OPFS 还是 IndexedDB 还是宿主注入」不在这层回答——`FileSystemProvider`
|
|
229
|
+
* 已经是那层抽象,浏览器里落在哪由宿主注入的 fs 决定。
|
|
230
|
+
*/
|
|
231
|
+
var FsTrustedKeyStore = class {
|
|
232
|
+
#fs;
|
|
233
|
+
#path;
|
|
234
|
+
constructor(fs, path) {
|
|
235
|
+
this.#fs = fs;
|
|
236
|
+
this.#path = path;
|
|
237
|
+
}
|
|
238
|
+
async #read() {
|
|
239
|
+
if (!await this.#fs.exists(this.#path)) return [];
|
|
240
|
+
let parsed;
|
|
241
|
+
try {
|
|
242
|
+
parsed = JSON.parse(await this.#fs.readText(this.#path));
|
|
243
|
+
} catch (e) {
|
|
244
|
+
throw new WebSkillError("VALIDATION_FAILED", `Trusted key store at ${this.#path} is not valid JSON`, e);
|
|
245
|
+
}
|
|
246
|
+
const file = parsed;
|
|
247
|
+
if (!Array.isArray(file.keys)) throw new WebSkillError("VALIDATION_FAILED", `Trusted key store at ${this.#path} has no "keys" array`);
|
|
248
|
+
return file.keys;
|
|
249
|
+
}
|
|
250
|
+
async #write(keys) {
|
|
251
|
+
await atomicWriteText(this.#fs, this.#path, JSON.stringify({
|
|
252
|
+
schemaVersion: 1,
|
|
253
|
+
keys
|
|
254
|
+
}, null, 2));
|
|
255
|
+
}
|
|
256
|
+
async list() {
|
|
257
|
+
return this.#read();
|
|
258
|
+
}
|
|
259
|
+
async get(keyId) {
|
|
260
|
+
return (await this.#read()).find((key) => key.keyId === keyId);
|
|
261
|
+
}
|
|
262
|
+
async add(key) {
|
|
263
|
+
const keys = (await this.#read()).filter((existing) => existing.keyId !== key.keyId);
|
|
264
|
+
keys.push({
|
|
265
|
+
...key,
|
|
266
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
267
|
+
});
|
|
268
|
+
await this.#write(keys);
|
|
269
|
+
}
|
|
270
|
+
async remove(keyId) {
|
|
271
|
+
await this.#write((await this.#read()).filter((key) => key.keyId !== keyId));
|
|
272
|
+
}
|
|
273
|
+
};
|
|
64
274
|
const ROOT = "/";
|
|
65
275
|
/** 内存文件系统:路径一律规范化为 `/` 分隔,写入自动创建父目录 */
|
|
66
276
|
var MemoryFS = class {
|
|
@@ -204,17 +414,6 @@ var MemoryFS = class {
|
|
|
204
414
|
this.#entries.delete(src);
|
|
205
415
|
}
|
|
206
416
|
};
|
|
207
|
-
/**
|
|
208
|
-
* 原子写文本:先写临时文件再 rename(进程崩溃不留下半写文件;lockfile 等账本场景)。
|
|
209
|
-
* 原子性强弱取决于 provider 的 rename 实现:NodeFS/MemoryFS 为真 rename;
|
|
210
|
-
* OPFS 无原生 move,rename 是 copy+delete(非原子,崩溃可能残留双份),
|
|
211
|
-
* 对崩溃一致性要求极高的场景在浏览器侧需知悉此前提。
|
|
212
|
-
*/
|
|
213
|
-
async function atomicWriteText(fs, path, content) {
|
|
214
|
-
const tmp = `${path}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
215
|
-
await fs.writeText(tmp, content);
|
|
216
|
-
await fs.rename(tmp, path);
|
|
217
|
-
}
|
|
218
417
|
/** 统一分隔符为 `/`,去除 `.` 段与重复分隔符(不解析 `..`) */
|
|
219
418
|
function normalizePath(path) {
|
|
220
419
|
return path.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
@@ -836,4 +1035,4 @@ const xmlRenderer = {
|
|
|
836
1035
|
};
|
|
837
1036
|
|
|
838
1037
|
//#endregion
|
|
839
|
-
export {
|
|
1038
|
+
export { parseSkillMarkdown as A, unzipWithLimits as B, escapeXml as C, keyIdOf as D, jsonRenderer as E, renderCatalogJson as F, verifyManifest as H, resolveArchiveLimits as I, resolveInsideRoot as L, readResponseWithLimit as M, readSkillSignature as N, messageOf as O, renderAvailableSkillsXml as P, signSkill as R, computeDigest as S, isValidSkillName as T, verifySkillSignature as U, validateSkills as V, xmlRenderer as W, atomicWriteText as _, SIGNATURE_SCHEMA_VERSION as a, checkDependencyCycles as b, SKILL_NAME_MAX_LENGTH as c, SKILL_SIGNATURE_FILE as d, SkillDiscovery as f, assertSafePathSegment as g, assertRemoteUrlAllowed as h, MemoryFS as i, parseSkillPackManifest as j, normalizePath as k, SKILL_NAME_PATTERN as l, WebSkillError as m, FsTrustedKeyStore as n, SKILLS_LOCKFILE as o, SkillReader as p, MANIFEST_EXCLUDED_FILES as r, SKILL_MANIFEST_FILE as s, DEFAULT_ARCHIVE_LIMITS as t, SKILL_PACK_FILE as u, buildCatalog as v, exportSkills as w, checkSkillRules as x, buildManifest as y, signaturePayloadBytes as z };
|