@nextclaw/app-runtime 0.16.1 → 0.16.2
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/dist/package.js +1 -1
- package/dist/services/app-artifact-validation.service.d.ts +1 -0
- package/dist/services/app-artifact-validation.service.d.ts.map +1 -1
- package/dist/services/app-artifact-validation.service.js +9 -13
- package/dist/services/app-artifact-validation.service.js.map +1 -1
- package/dist/services/app-component-contract.service.d.ts +37 -0
- package/dist/services/app-component-contract.service.d.ts.map +1 -0
- package/dist/services/app-component-contract.service.js +114 -0
- package/dist/services/app-component-contract.service.js.map +1 -0
- package/dist/services/app-manifest.service.d.ts +3 -2
- package/dist/services/app-manifest.service.d.ts.map +1 -1
- package/dist/services/app-manifest.service.js +11 -16
- package/dist/services/app-manifest.service.js.map +1 -1
- package/dist/services/app-rust-wasi-scaffold-template.service.d.ts +1 -0
- package/dist/services/app-rust-wasi-scaffold-template.service.d.ts.map +1 -1
- package/dist/services/app-rust-wasi-scaffold-template.service.js +35 -20
- package/dist/services/app-rust-wasi-scaffold-template.service.js.map +1 -1
- package/dist/utils/app-service-component-contract.utils.js +45 -0
- package/dist/utils/app-service-component-contract.utils.js.map +1 -0
- package/package.json +1 -1
- package/resources/wit/deps/keyvalue@0.2.0-draft2/store.wit +36 -0
- package/resources/wit/portable-service.wit +2 -0
package/dist/package.js
CHANGED
|
@@ -19,6 +19,7 @@ type AppArtifactValidationResult = {
|
|
|
19
19
|
};
|
|
20
20
|
declare class AppArtifactValidationService {
|
|
21
21
|
private readonly platformTargetService;
|
|
22
|
+
private readonly componentContractService;
|
|
22
23
|
private readonly serviceLaunchService;
|
|
23
24
|
validate: (params: {
|
|
24
25
|
bytes: Uint8Array;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-artifact-validation.service.d.ts","names":[],"sources":["../../src/services/app-artifact-validation.service.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"app-artifact-validation.service.d.ts","names":[],"sources":["../../src/services/app-artifact-validation.service.ts"],"mappings":";;;;KAsCY,gCAAA;EACV,KAAA;EACA,IAAA;EACA,OAAA;EACA,gBAAA,EAAkB,mBAAA;EAClB,QAAA;EACA,MAAA,GAAS,iBAAA;AAAA;AAAA,KAGC,2BAAA;EACV,OAAA,EAAS,MAAA,SAAe,UAAA;EACxB,cAAA;EACA,QAAA,EAAU,iBAAA;EACV,SAAA,EAAW,kBAAA;EACX,QAAA,EAAU,MAAA;AAAA;AAAA,cAQC,4BAAA;EAAA,iBACM,qBAAA;EAAA,iBACA,wBAAA;EAAA,iBAGA,oBAAA;EAIjB,QAAA,GAAkB,MAAA;IAChB,KAAA,EAAO,UAAA;IACP,QAAA,GAAW,gCAAA;EAAA,MACT,OAAA,CAAQ,2BAAA;EAAA,QA0BJ,kBAAA;EAAA,QAiCA,oBAAA;EAAA,QAmCA,gBAAA;EAAA,QAgEA,cAAA;EAAA,QAiBA,iBAAA;EAAA,QAsBA,gBAAA;EAAA,QAcA,cAAA;EAAA,QAiBA,gCAAA;EAAA,QAuBA,YAAA;EAAA,QAmCA,aAAA;EAAA,QAcA,eAAA;EAAA,QA4BA,sBAAA;EAAA,QAqCA,qBAAA;EAAA,QAiBA,sBAAA;EAAA,QA0DA,aAAA;EAAA,QAkBA,UAAA;EAAA,QAQA,MAAA;EAAA,QAUA,WAAA;EAAA,QAOA,YAAA;EAAA,QAsBA,QAAA;AAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AppPlatformTargetService } from "./app-platform-target.service.js";
|
|
2
|
+
import { AppComponentContractService } from "./app-component-contract.service.js";
|
|
2
3
|
import { AppServiceLaunchService } from "./app-service-launch.service.js";
|
|
3
4
|
import { strFromU8, unzipSync } from "fflate";
|
|
4
5
|
//#region src/services/app-artifact-validation.service.ts
|
|
@@ -17,6 +18,7 @@ const ZIP_MAX_COMMENT_BYTES = 65535;
|
|
|
17
18
|
const ZIP_CENTRAL_FILE_HEADER_BYTES = 46;
|
|
18
19
|
var AppArtifactValidationService = class {
|
|
19
20
|
platformTargetService = new AppPlatformTargetService();
|
|
21
|
+
componentContractService = new AppComponentContractService(this.platformTargetService);
|
|
20
22
|
serviceLaunchService = new AppServiceLaunchService(this.platformTargetService);
|
|
21
23
|
validate = async (params) => {
|
|
22
24
|
const { bytes, expected } = params;
|
|
@@ -189,19 +191,13 @@ var AppArtifactValidationService = class {
|
|
|
189
191
|
const { archive, manifest, metadata } = params;
|
|
190
192
|
const identity = manifest;
|
|
191
193
|
if (identity.id !== metadata.appId || identity.name !== metadata.name || identity.version !== metadata.version) throw new Error("bundle metadata 与 manifest.json 身份不一致。");
|
|
192
|
-
if (identity.schemaVersion === 2) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (kind !== "panel" && kind !== "service" || typeof componentPath !== "string") throw new Error(`manifest.components[${index}] 无效。`);
|
|
200
|
-
const manifestPath = `${this.normalizeEntry(componentPath)}/${kind === "panel" ? "panel-app.json" : "service-app.json"}`;
|
|
201
|
-
if (!archive[manifestPath]) throw new Error(`bundle 缺少组件 manifest:${manifestPath}`);
|
|
202
|
-
}
|
|
203
|
-
if (!archive["marketplace.json"]) throw new Error("schema v2 bundle 缺少 marketplace.json。");
|
|
204
|
-
} else if (identity.schemaVersion === 1) {
|
|
194
|
+
if (identity.schemaVersion === 2) this.componentContractService.assertArtifact({
|
|
195
|
+
archive,
|
|
196
|
+
identity,
|
|
197
|
+
manifest,
|
|
198
|
+
distributionMode: metadata.distributionMode
|
|
199
|
+
});
|
|
200
|
+
else if (identity.schemaVersion === 1) {
|
|
205
201
|
this.assertStandaloneEntry(archive, identity.main, "main.entry");
|
|
206
202
|
this.assertStandaloneEntry(archive, identity.ui, "ui.entry");
|
|
207
203
|
} else throw new Error("manifest.schemaVersion 必须是 1 或 2。");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-artifact-validation.service.js","names":[],"sources":["../../src/services/app-artifact-validation.service.ts"],"sourcesContent":["import { strFromU8, unzipSync } from \"fflate\";\nimport { AppPlatformTargetService } from \"#app-runtime/services/app-platform-target.service.js\";\nimport { AppServiceLaunchService } from \"#app-runtime/services/app-service-launch.service.js\";\nimport type { AppArtifactTarget } from \"#app-runtime/types/app-manifest.types.js\";\nimport type {\n AppBundleChecksums,\n AppBundleMetadata,\n AppDistributionMode,\n} from \"#app-runtime/types/app-bundle.types.js\";\n\nconst MAX_COMPRESSED_BYTES = 25 * 1024 * 1024;\nconst MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;\nconst MAX_FILE_BYTES = 25 * 1024 * 1024;\nconst MAX_FILE_COUNT = 2_000;\nconst MAX_COMPRESSION_RATIO = 200;\nconst CHECKSUMS_PATH = \".napp/checksums.json\";\nconst BUNDLE_METADATA_PATH = \".napp/bundle.json\";\nconst ENTRY_MANIFEST_PATH = \"manifest.json\";\nconst ZIP_EOCD_SIGNATURE = 0x06054b50;\nconst ZIP_CENTRAL_FILE_SIGNATURE = 0x02014b50;\nconst ZIP_EOCD_MIN_BYTES = 22;\nconst ZIP_MAX_COMMENT_BYTES = 0xffff;\nconst ZIP_CENTRAL_FILE_HEADER_BYTES = 46;\n\ntype ArtifactManifestIdentity = {\n schemaVersion?: unknown;\n id?: unknown;\n name?: unknown;\n version?: unknown;\n icon?: unknown;\n main?: unknown;\n ui?: unknown;\n components?: unknown;\n};\n\nexport type AppArtifactValidationExpectation = {\n appId: string;\n name: string;\n version: string;\n distributionMode: AppDistributionMode;\n manifest?: unknown;\n target?: AppArtifactTarget;\n};\n\nexport type AppArtifactValidationResult = {\n archive: Record<string, Uint8Array>;\n artifactSha256: string;\n metadata: AppBundleMetadata;\n checksums: AppBundleChecksums;\n manifest: Record<string, unknown>;\n};\n\ntype ZipCentralEntry = {\n name: string;\n uncompressedSize: number;\n};\n\nexport class AppArtifactValidationService {\n private readonly platformTargetService = new AppPlatformTargetService();\n private readonly serviceLaunchService = new AppServiceLaunchService(\n this.platformTargetService,\n );\n\n validate = async (params: {\n bytes: Uint8Array;\n expected?: AppArtifactValidationExpectation;\n }): Promise<AppArtifactValidationResult> => {\n const { bytes, expected } = params;\n if (bytes.byteLength === 0 || bytes.byteLength > MAX_COMPRESSED_BYTES) {\n throw new Error(\n `bundle 压缩体积必须在 1 到 ${MAX_COMPRESSED_BYTES} bytes 之间。`,\n );\n }\n const artifactSha256 = await this.sha256(bytes);\n const centralEntries = this.readCentralEntries(bytes);\n const archive = this.normalizeArchive(unzipSync(bytes));\n this.assertCentralEntriesMatchArchive(centralEntries, archive);\n const metadata = this.readMetadata(archive);\n const checksums = this.readChecksums(archive);\n await this.verifyChecksums(archive, checksums);\n const manifest = this.readJsonEntry<Record<string, unknown>>(\n archive,\n ENTRY_MANIFEST_PATH,\n \"manifest.json\",\n );\n this.assertManifestContract({ archive, manifest, metadata });\n if (expected) {\n this.assertExpectedContract({ archive, expected, manifest, metadata });\n }\n return { archive, artifactSha256, metadata, checksums, manifest };\n };\n\n private readCentralEntries = (bytes: Uint8Array): ZipCentralEntry[] => {\n const { centralDirectoryOffset, entryCount, eocdOffset, view } =\n this.readCentralDirectory(bytes);\n const entries: ZipCentralEntry[] = [];\n const names = new Set<string>();\n let cursor = centralDirectoryOffset;\n let totalUncompressedBytes = 0;\n for (let index = 0; index < entryCount; index += 1) {\n const { entry, entryEnd } = this.readCentralEntry({\n bytes,\n cursor,\n eocdOffset,\n view,\n });\n if (names.has(entry.name)) {\n throw new Error(`bundle 包含重复路径:${entry.name}`);\n }\n names.add(entry.name);\n totalUncompressedBytes += entry.uncompressedSize;\n if (totalUncompressedBytes > MAX_UNCOMPRESSED_BYTES) {\n throw new Error(\n `bundle 解压后超过 ${MAX_UNCOMPRESSED_BYTES} bytes 上限。`,\n );\n }\n entries.push(entry);\n cursor = entryEnd;\n }\n if (cursor !== eocdOffset) {\n throw new Error(\"bundle central directory 未被精确消费。\");\n }\n return entries;\n };\n\n private readCentralDirectory = (bytes: Uint8Array) => {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const eocdOffset = this.findEocdOffset(view);\n const diskNumber = view.getUint16(eocdOffset + 4, true);\n const centralDirectoryDisk = view.getUint16(eocdOffset + 6, true);\n const entryCountOnDisk = view.getUint16(eocdOffset + 8, true);\n const entryCount = view.getUint16(eocdOffset + 10, true);\n const centralDirectorySize = view.getUint32(eocdOffset + 12, true);\n const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true);\n const commentLength = view.getUint16(eocdOffset + 20, true);\n if (\n diskNumber !== 0 ||\n centralDirectoryDisk !== 0 ||\n entryCountOnDisk !== entryCount\n ) {\n throw new Error(\"bundle 不支持跨磁盘 ZIP。\");\n }\n if (\n entryCount === 0xffff ||\n centralDirectorySize === 0xffffffff ||\n centralDirectoryOffset === 0xffffffff\n ) {\n throw new Error(\"bundle 不支持 Zip64。\");\n }\n if (\n entryCount === 0 ||\n entryCount > MAX_FILE_COUNT ||\n eocdOffset + ZIP_EOCD_MIN_BYTES + commentLength !== bytes.byteLength ||\n centralDirectoryOffset + centralDirectorySize !== eocdOffset\n ) {\n throw new Error(\"bundle central directory 无效。\");\n }\n return { centralDirectoryOffset, entryCount, eocdOffset, view };\n };\n\n private readCentralEntry = (params: {\n bytes: Uint8Array;\n cursor: number;\n eocdOffset: number;\n view: DataView;\n }): { entry: ZipCentralEntry; entryEnd: number } => {\n const { bytes, cursor, eocdOffset, view } = params;\n if (\n cursor + ZIP_CENTRAL_FILE_HEADER_BYTES > eocdOffset ||\n view.getUint32(cursor, true) !== ZIP_CENTRAL_FILE_SIGNATURE\n ) {\n throw new Error(\"bundle central directory entry 无效。\");\n }\n const versionMadeBy = view.getUint16(cursor + 4, true);\n const flags = view.getUint16(cursor + 8, true);\n const compressionMethod = view.getUint16(cursor + 10, true);\n const compressedSize = view.getUint32(cursor + 20, true);\n const uncompressedSize = view.getUint32(cursor + 24, true);\n const nameLength = view.getUint16(cursor + 28, true);\n const extraLength = view.getUint16(cursor + 30, true);\n const entryCommentLength = view.getUint16(cursor + 32, true);\n const externalAttributes = view.getUint32(cursor + 38, true);\n const entryEnd =\n cursor +\n ZIP_CENTRAL_FILE_HEADER_BYTES +\n nameLength +\n extraLength +\n entryCommentLength;\n if (entryEnd > eocdOffset || nameLength === 0) {\n throw new Error(\"bundle central directory entry 长度无效。\");\n }\n if ((flags & 0x1) !== 0 || (flags & 0x40) !== 0) {\n throw new Error(\"bundle 不允许加密文件。\");\n }\n if (compressionMethod !== 0 && compressionMethod !== 8) {\n throw new Error(`bundle 不支持压缩方法 ${compressionMethod}。`);\n }\n const rawName = bytes.subarray(\n cursor + ZIP_CENTRAL_FILE_HEADER_BYTES,\n cursor + ZIP_CENTRAL_FILE_HEADER_BYTES + nameLength,\n );\n const name = this.normalizeEntry(this.decodeUtf8(rawName));\n this.assertRegularFile({\n externalAttributes,\n hostSystem: versionMadeBy >>> 8,\n name,\n });\n if (uncompressedSize > MAX_FILE_BYTES) {\n throw new Error(\n `bundle 单文件超过 ${MAX_FILE_BYTES} bytes 上限:${name}`,\n );\n }\n if (\n uncompressedSize > 1024 * 1024 &&\n (compressedSize === 0 ||\n uncompressedSize / compressedSize > MAX_COMPRESSION_RATIO)\n ) {\n throw new Error(\n `bundle 文件压缩比超过 ${MAX_COMPRESSION_RATIO}:1 上限:${name}`,\n );\n }\n return { entry: { name, uncompressedSize }, entryEnd };\n };\n\n private findEocdOffset = (view: DataView): number => {\n const minimumOffset = Math.max(\n 0,\n view.byteLength - ZIP_EOCD_MIN_BYTES - ZIP_MAX_COMMENT_BYTES,\n );\n for (\n let offset = view.byteLength - ZIP_EOCD_MIN_BYTES;\n offset >= minimumOffset;\n offset -= 1\n ) {\n if (view.getUint32(offset, true) === ZIP_EOCD_SIGNATURE) {\n return offset;\n }\n }\n throw new Error(\"bundle 缺少有效 ZIP end-of-central-directory。\");\n };\n\n private assertRegularFile = (params: {\n externalAttributes: number;\n hostSystem: number;\n name: string;\n }): void => {\n const { externalAttributes, hostSystem, name } = params;\n if (name.endsWith(\"/\")) {\n throw new Error(`bundle 不允许目录 entry:${name}`);\n }\n if (hostSystem === 3) {\n const unixMode = externalAttributes >>> 16;\n const fileType = unixMode & 0xf000;\n if (fileType !== 0 && fileType !== 0x8000) {\n throw new Error(`bundle 只允许普通文件:${name}`);\n }\n return;\n }\n if ((externalAttributes & 0x10) !== 0) {\n throw new Error(`bundle 只允许普通文件:${name}`);\n }\n };\n\n private normalizeArchive = (\n archive: Record<string, Uint8Array>,\n ): Record<string, Uint8Array> => {\n const normalized: Record<string, Uint8Array> = {};\n for (const [rawName, fileBytes] of Object.entries(archive)) {\n const name = this.normalizeEntry(rawName);\n if (Object.hasOwn(normalized, name)) {\n throw new Error(`bundle 包含重复路径:${name}`);\n }\n normalized[name] = fileBytes;\n }\n return normalized;\n };\n\n private normalizeEntry = (rawName: string): string => {\n const normalized = rawName.replace(/\\\\/g, \"/\");\n const segments = normalized.split(\"/\");\n if (\n !normalized ||\n normalized.startsWith(\"/\") ||\n normalized.includes(\"\\0\") ||\n /^[A-Za-z]:/.test(normalized) ||\n segments.some(\n (segment) => !segment || segment === \".\" || segment === \"..\",\n )\n ) {\n throw new Error(`bundle 内包含非法路径:${rawName}`);\n }\n return segments.join(\"/\");\n };\n\n private assertCentralEntriesMatchArchive = (\n entries: ZipCentralEntry[],\n archive: Record<string, Uint8Array>,\n ): void => {\n const centralPaths = entries\n .map((entry) => entry.name)\n .sort((left, right) => left.localeCompare(right));\n const archivePaths = Object.keys(archive).sort((left, right) =>\n left.localeCompare(right),\n );\n if (\n centralPaths.length !== archivePaths.length ||\n centralPaths.some((entry, index) => entry !== archivePaths[index])\n ) {\n throw new Error(\"bundle 解压结果与 central directory 不一致。\");\n }\n for (const entry of entries) {\n if (archive[entry.name]?.byteLength !== entry.uncompressedSize) {\n throw new Error(`bundle 解压后文件大小不一致:${entry.name}`);\n }\n }\n };\n\n private readMetadata = (\n archive: Record<string, Uint8Array>,\n ): AppBundleMetadata => {\n const raw = this.readJsonEntry<Partial<AppBundleMetadata>>(\n archive,\n BUNDLE_METADATA_PATH,\n \"bundle metadata\",\n );\n const distributionMode = raw.distributionMode ?? \"bundle\";\n if (\n raw.bundleFormatVersion !== 1 ||\n (distributionMode !== \"bundle\" && distributionMode !== \"source\") ||\n typeof raw.appId !== \"string\" ||\n !raw.appId ||\n typeof raw.name !== \"string\" ||\n !raw.name ||\n typeof raw.version !== \"string\" ||\n !raw.version ||\n raw.entryManifest !== ENTRY_MANIFEST_PATH ||\n raw.checksumsFile !== CHECKSUMS_PATH\n ) {\n throw new Error(\"bundle metadata 无效。\");\n }\n return {\n ...raw,\n distributionMode,\n target: raw.target === undefined\n ? undefined\n : this.platformTargetService.parseArtifactTarget(\n raw.target,\n \"bundle metadata.target\",\n ),\n } as AppBundleMetadata;\n };\n\n private readChecksums = (\n archive: Record<string, Uint8Array>,\n ): AppBundleChecksums => {\n const raw = this.readJsonEntry<Partial<AppBundleChecksums>>(\n archive,\n CHECKSUMS_PATH,\n \"bundle checksums\",\n );\n if (raw.algorithm !== \"sha256\" || !this.isRecord(raw.files)) {\n throw new Error(\"bundle checksums 无效。\");\n }\n return raw as AppBundleChecksums;\n };\n\n private verifyChecksums = async (\n archive: Record<string, Uint8Array>,\n checksums: AppBundleChecksums,\n ): Promise<void> => {\n const actualPaths = Object.keys(archive)\n .filter((entry) => entry !== CHECKSUMS_PATH)\n .sort((left, right) => left.localeCompare(right));\n const checksumPaths = Object.keys(checksums.files)\n .map((entry) => this.normalizeEntry(entry))\n .sort((left, right) => left.localeCompare(right));\n if (\n actualPaths.length !== checksumPaths.length ||\n actualPaths.some((entry, index) => entry !== checksumPaths[index])\n ) {\n throw new Error(\"bundle checksums 必须精确覆盖所有 artifact 文件。\");\n }\n for (const relativePath of actualPaths) {\n const expectedHash = checksums.files[relativePath];\n if (!expectedHash || !/^[a-f0-9]{64}$/.test(expectedHash)) {\n throw new Error(`bundle checksum 格式无效:${relativePath}`);\n }\n const actualHash = await this.sha256(archive[relativePath] as Uint8Array);\n if (actualHash !== expectedHash) {\n throw new Error(`bundle checksum 校验失败:${relativePath}`);\n }\n }\n };\n\n private assertManifestContract = (params: {\n archive: Record<string, Uint8Array>;\n manifest: Record<string, unknown>;\n metadata: AppBundleMetadata;\n }): void => {\n const { archive, manifest, metadata } = params;\n const identity = manifest as ArtifactManifestIdentity;\n if (\n identity.id !== metadata.appId ||\n identity.name !== metadata.name ||\n identity.version !== metadata.version\n ) {\n throw new Error(\"bundle metadata 与 manifest.json 身份不一致。\");\n }\n if (identity.schemaVersion === 2) {\n if (metadata.distributionMode !== \"bundle\") {\n throw new Error(\"schema v2 组合包只支持 bundle 分发。\");\n }\n if (\n !Array.isArray(identity.components) ||\n identity.components.length === 0\n ) {\n throw new Error(\"schema v2 manifest.components 必须是非空数组。\");\n }\n for (const [index, rawComponent] of identity.components.entries()) {\n if (!this.isRecord(rawComponent)) {\n throw new Error(`manifest.components[${index}] 必须是对象。`);\n }\n const kind = rawComponent.kind;\n const componentPath = rawComponent.path;\n if (\n (kind !== \"panel\" && kind !== \"service\") ||\n typeof componentPath !== \"string\"\n ) {\n throw new Error(`manifest.components[${index}] 无效。`);\n }\n const normalizedPath = this.normalizeEntry(componentPath);\n const manifestPath = `${normalizedPath}/${kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\"}`;\n if (!archive[manifestPath]) {\n throw new Error(`bundle 缺少组件 manifest:${manifestPath}`);\n }\n }\n if (!archive[\"marketplace.json\"]) {\n throw new Error(\"schema v2 bundle 缺少 marketplace.json。\");\n }\n } else if (identity.schemaVersion === 1) {\n this.assertStandaloneEntry(archive, identity.main, \"main.entry\");\n this.assertStandaloneEntry(archive, identity.ui, \"ui.entry\");\n } else {\n throw new Error(\"manifest.schemaVersion 必须是 1 或 2。\");\n }\n if (identity.icon !== undefined) {\n if (\n typeof identity.icon !== \"string\" ||\n !archive[this.normalizeEntry(identity.icon)]\n ) {\n throw new Error(\"bundle 缺少 manifest.icon 声明的文件。\");\n }\n }\n };\n\n private assertStandaloneEntry = (\n archive: Record<string, Uint8Array>,\n rawContainer: unknown,\n pathLabel: string,\n ): void => {\n if (\n !this.isRecord(rawContainer) ||\n typeof rawContainer.entry !== \"string\"\n ) {\n throw new Error(`manifest.${pathLabel} 无效。`);\n }\n const entry = this.normalizeEntry(rawContainer.entry);\n if (!archive[entry]) {\n throw new Error(`bundle 缺少 manifest.${pathLabel} 文件:${entry}`);\n }\n };\n\n private assertExpectedContract = (params: {\n archive: Record<string, Uint8Array>;\n expected: AppArtifactValidationExpectation;\n manifest: Record<string, unknown>;\n metadata: AppBundleMetadata;\n }): void => {\n const { archive, expected, manifest, metadata } = params;\n if (\n expected.appId !== metadata.appId ||\n expected.name !== metadata.name ||\n expected.version !== metadata.version ||\n expected.distributionMode !== metadata.distributionMode\n ) {\n throw new Error(\"artifact metadata 与 publish payload 不一致。\");\n }\n if (\n expected.target !== undefined &&\n (\n metadata.target === undefined ||\n this.platformTargetService.toTargetKey(expected.target) !==\n this.platformTargetService.toTargetKey(metadata.target)\n )\n ) {\n throw new Error(\"artifact target metadata 与 publish payload 不一致。\");\n }\n if (\n expected.manifest !== undefined &&\n !this.isJsonSubset(this.toJsonValue(expected.manifest), manifest)\n ) {\n throw new Error(\n \"artifact manifest.json 与 publish payload.manifest 不一致。\",\n );\n }\n if (\n expected.target?.kind === \"native\" &&\n manifest.schemaVersion === 2 &&\n Array.isArray(manifest.components)\n ) {\n for (const rawComponent of manifest.components) {\n if (!this.isRecord(rawComponent) || rawComponent.kind !== \"service\") {\n continue;\n }\n const componentPath = typeof rawComponent.path === \"string\"\n ? this.normalizeEntry(rawComponent.path)\n : undefined;\n if (!componentPath) {\n throw new Error(\"service component path 无效。\");\n }\n const serviceManifest = this.readJsonEntry<Record<string, unknown>>(\n archive,\n `${componentPath}/service-app.json`,\n `${componentPath}/service-app.json`,\n );\n this.serviceLaunchService.resolve(serviceManifest, expected.target);\n }\n }\n };\n\n private readJsonEntry = <T>(\n archive: Record<string, Uint8Array>,\n entryPath: string,\n label: string,\n ): T => {\n const bytes = archive[entryPath];\n if (!bytes) {\n throw new Error(`bundle 缺少 ${label}。`);\n }\n try {\n return JSON.parse(strFromU8(bytes)) as T;\n } catch (error) {\n throw new Error(\n `无法读取 ${label}:${error instanceof Error ? error.message : String(error)}`,\n );\n }\n };\n\n private decodeUtf8 = (bytes: Uint8Array): string => {\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n throw new Error(\"bundle 文件名不是有效 UTF-8。\");\n }\n };\n\n private sha256 = async (bytes: Uint8Array): Promise<string> => {\n const digest = await globalThis.crypto.subtle.digest(\n \"SHA-256\",\n new Uint8Array(bytes),\n );\n return [...new Uint8Array(digest)]\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n };\n\n private toJsonValue = (value: unknown): unknown => {\n const serialized = JSON.stringify(value);\n return serialized === undefined\n ? null\n : (JSON.parse(serialized) as unknown);\n };\n\n private isJsonSubset = (expected: unknown, actual: unknown): boolean => {\n if (Array.isArray(expected)) {\n return (\n Array.isArray(actual) &&\n expected.length === actual.length &&\n expected.every((entry, index) =>\n this.isJsonSubset(entry, actual[index]),\n )\n );\n }\n if (this.isRecord(expected)) {\n return (\n this.isRecord(actual) &&\n Object.entries(expected).every(\n ([key, value]) =>\n Object.hasOwn(actual, key) && this.isJsonSubset(value, actual[key]),\n )\n );\n }\n return Object.is(expected, actual);\n };\n\n private isRecord = (value: unknown): value is Record<string, unknown> =>\n Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app-artifact-validation.service.js","names":[],"sources":["../../src/services/app-artifact-validation.service.ts"],"sourcesContent":["import { strFromU8, unzipSync } from \"fflate\";\nimport { AppComponentContractService } from \"#app-runtime/services/app-component-contract.service.js\";\nimport { AppPlatformTargetService } from \"#app-runtime/services/app-platform-target.service.js\";\nimport { AppServiceLaunchService } from \"#app-runtime/services/app-service-launch.service.js\";\nimport type { AppArtifactTarget } from \"#app-runtime/types/app-manifest.types.js\";\nimport type {\n AppBundleChecksums,\n AppBundleMetadata,\n AppDistributionMode,\n} from \"#app-runtime/types/app-bundle.types.js\";\n\nconst MAX_COMPRESSED_BYTES = 25 * 1024 * 1024;\nconst MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;\nconst MAX_FILE_BYTES = 25 * 1024 * 1024;\nconst MAX_FILE_COUNT = 2_000;\nconst MAX_COMPRESSION_RATIO = 200;\nconst CHECKSUMS_PATH = \".napp/checksums.json\";\nconst BUNDLE_METADATA_PATH = \".napp/bundle.json\";\nconst ENTRY_MANIFEST_PATH = \"manifest.json\";\nconst ZIP_EOCD_SIGNATURE = 0x06054b50;\nconst ZIP_CENTRAL_FILE_SIGNATURE = 0x02014b50;\nconst ZIP_EOCD_MIN_BYTES = 22;\nconst ZIP_MAX_COMMENT_BYTES = 0xffff;\nconst ZIP_CENTRAL_FILE_HEADER_BYTES = 46;\n\ntype ArtifactManifestIdentity = {\n schemaVersion?: unknown;\n id?: unknown;\n name?: unknown;\n version?: unknown;\n icon?: unknown;\n main?: unknown;\n ui?: unknown;\n components?: unknown;\n runtime?: unknown;\n distribution?: unknown;\n};\n\nexport type AppArtifactValidationExpectation = {\n appId: string;\n name: string;\n version: string;\n distributionMode: AppDistributionMode;\n manifest?: unknown;\n target?: AppArtifactTarget;\n};\n\nexport type AppArtifactValidationResult = {\n archive: Record<string, Uint8Array>;\n artifactSha256: string;\n metadata: AppBundleMetadata;\n checksums: AppBundleChecksums;\n manifest: Record<string, unknown>;\n};\n\ntype ZipCentralEntry = {\n name: string;\n uncompressedSize: number;\n};\n\nexport class AppArtifactValidationService {\n private readonly platformTargetService = new AppPlatformTargetService();\n private readonly componentContractService = new AppComponentContractService(\n this.platformTargetService,\n );\n private readonly serviceLaunchService = new AppServiceLaunchService(\n this.platformTargetService,\n );\n\n validate = async (params: {\n bytes: Uint8Array;\n expected?: AppArtifactValidationExpectation;\n }): Promise<AppArtifactValidationResult> => {\n const { bytes, expected } = params;\n if (bytes.byteLength === 0 || bytes.byteLength > MAX_COMPRESSED_BYTES) {\n throw new Error(\n `bundle 压缩体积必须在 1 到 ${MAX_COMPRESSED_BYTES} bytes 之间。`,\n );\n }\n const artifactSha256 = await this.sha256(bytes);\n const centralEntries = this.readCentralEntries(bytes);\n const archive = this.normalizeArchive(unzipSync(bytes));\n this.assertCentralEntriesMatchArchive(centralEntries, archive);\n const metadata = this.readMetadata(archive);\n const checksums = this.readChecksums(archive);\n await this.verifyChecksums(archive, checksums);\n const manifest = this.readJsonEntry<Record<string, unknown>>(\n archive,\n ENTRY_MANIFEST_PATH,\n \"manifest.json\",\n );\n this.assertManifestContract({ archive, manifest, metadata });\n if (expected) {\n this.assertExpectedContract({ archive, expected, manifest, metadata });\n }\n return { archive, artifactSha256, metadata, checksums, manifest };\n };\n\n private readCentralEntries = (bytes: Uint8Array): ZipCentralEntry[] => {\n const { centralDirectoryOffset, entryCount, eocdOffset, view } =\n this.readCentralDirectory(bytes);\n const entries: ZipCentralEntry[] = [];\n const names = new Set<string>();\n let cursor = centralDirectoryOffset;\n let totalUncompressedBytes = 0;\n for (let index = 0; index < entryCount; index += 1) {\n const { entry, entryEnd } = this.readCentralEntry({\n bytes,\n cursor,\n eocdOffset,\n view,\n });\n if (names.has(entry.name)) {\n throw new Error(`bundle 包含重复路径:${entry.name}`);\n }\n names.add(entry.name);\n totalUncompressedBytes += entry.uncompressedSize;\n if (totalUncompressedBytes > MAX_UNCOMPRESSED_BYTES) {\n throw new Error(\n `bundle 解压后超过 ${MAX_UNCOMPRESSED_BYTES} bytes 上限。`,\n );\n }\n entries.push(entry);\n cursor = entryEnd;\n }\n if (cursor !== eocdOffset) {\n throw new Error(\"bundle central directory 未被精确消费。\");\n }\n return entries;\n };\n\n private readCentralDirectory = (bytes: Uint8Array) => {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const eocdOffset = this.findEocdOffset(view);\n const diskNumber = view.getUint16(eocdOffset + 4, true);\n const centralDirectoryDisk = view.getUint16(eocdOffset + 6, true);\n const entryCountOnDisk = view.getUint16(eocdOffset + 8, true);\n const entryCount = view.getUint16(eocdOffset + 10, true);\n const centralDirectorySize = view.getUint32(eocdOffset + 12, true);\n const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true);\n const commentLength = view.getUint16(eocdOffset + 20, true);\n if (\n diskNumber !== 0 ||\n centralDirectoryDisk !== 0 ||\n entryCountOnDisk !== entryCount\n ) {\n throw new Error(\"bundle 不支持跨磁盘 ZIP。\");\n }\n if (\n entryCount === 0xffff ||\n centralDirectorySize === 0xffffffff ||\n centralDirectoryOffset === 0xffffffff\n ) {\n throw new Error(\"bundle 不支持 Zip64。\");\n }\n if (\n entryCount === 0 ||\n entryCount > MAX_FILE_COUNT ||\n eocdOffset + ZIP_EOCD_MIN_BYTES + commentLength !== bytes.byteLength ||\n centralDirectoryOffset + centralDirectorySize !== eocdOffset\n ) {\n throw new Error(\"bundle central directory 无效。\");\n }\n return { centralDirectoryOffset, entryCount, eocdOffset, view };\n };\n\n private readCentralEntry = (params: {\n bytes: Uint8Array;\n cursor: number;\n eocdOffset: number;\n view: DataView;\n }): { entry: ZipCentralEntry; entryEnd: number } => {\n const { bytes, cursor, eocdOffset, view } = params;\n if (\n cursor + ZIP_CENTRAL_FILE_HEADER_BYTES > eocdOffset ||\n view.getUint32(cursor, true) !== ZIP_CENTRAL_FILE_SIGNATURE\n ) {\n throw new Error(\"bundle central directory entry 无效。\");\n }\n const versionMadeBy = view.getUint16(cursor + 4, true);\n const flags = view.getUint16(cursor + 8, true);\n const compressionMethod = view.getUint16(cursor + 10, true);\n const compressedSize = view.getUint32(cursor + 20, true);\n const uncompressedSize = view.getUint32(cursor + 24, true);\n const nameLength = view.getUint16(cursor + 28, true);\n const extraLength = view.getUint16(cursor + 30, true);\n const entryCommentLength = view.getUint16(cursor + 32, true);\n const externalAttributes = view.getUint32(cursor + 38, true);\n const entryEnd =\n cursor +\n ZIP_CENTRAL_FILE_HEADER_BYTES +\n nameLength +\n extraLength +\n entryCommentLength;\n if (entryEnd > eocdOffset || nameLength === 0) {\n throw new Error(\"bundle central directory entry 长度无效。\");\n }\n if ((flags & 0x1) !== 0 || (flags & 0x40) !== 0) {\n throw new Error(\"bundle 不允许加密文件。\");\n }\n if (compressionMethod !== 0 && compressionMethod !== 8) {\n throw new Error(`bundle 不支持压缩方法 ${compressionMethod}。`);\n }\n const rawName = bytes.subarray(\n cursor + ZIP_CENTRAL_FILE_HEADER_BYTES,\n cursor + ZIP_CENTRAL_FILE_HEADER_BYTES + nameLength,\n );\n const name = this.normalizeEntry(this.decodeUtf8(rawName));\n this.assertRegularFile({\n externalAttributes,\n hostSystem: versionMadeBy >>> 8,\n name,\n });\n if (uncompressedSize > MAX_FILE_BYTES) {\n throw new Error(\n `bundle 单文件超过 ${MAX_FILE_BYTES} bytes 上限:${name}`,\n );\n }\n if (\n uncompressedSize > 1024 * 1024 &&\n (compressedSize === 0 ||\n uncompressedSize / compressedSize > MAX_COMPRESSION_RATIO)\n ) {\n throw new Error(\n `bundle 文件压缩比超过 ${MAX_COMPRESSION_RATIO}:1 上限:${name}`,\n );\n }\n return { entry: { name, uncompressedSize }, entryEnd };\n };\n\n private findEocdOffset = (view: DataView): number => {\n const minimumOffset = Math.max(\n 0,\n view.byteLength - ZIP_EOCD_MIN_BYTES - ZIP_MAX_COMMENT_BYTES,\n );\n for (\n let offset = view.byteLength - ZIP_EOCD_MIN_BYTES;\n offset >= minimumOffset;\n offset -= 1\n ) {\n if (view.getUint32(offset, true) === ZIP_EOCD_SIGNATURE) {\n return offset;\n }\n }\n throw new Error(\"bundle 缺少有效 ZIP end-of-central-directory。\");\n };\n\n private assertRegularFile = (params: {\n externalAttributes: number;\n hostSystem: number;\n name: string;\n }): void => {\n const { externalAttributes, hostSystem, name } = params;\n if (name.endsWith(\"/\")) {\n throw new Error(`bundle 不允许目录 entry:${name}`);\n }\n if (hostSystem === 3) {\n const unixMode = externalAttributes >>> 16;\n const fileType = unixMode & 0xf000;\n if (fileType !== 0 && fileType !== 0x8000) {\n throw new Error(`bundle 只允许普通文件:${name}`);\n }\n return;\n }\n if ((externalAttributes & 0x10) !== 0) {\n throw new Error(`bundle 只允许普通文件:${name}`);\n }\n };\n\n private normalizeArchive = (\n archive: Record<string, Uint8Array>,\n ): Record<string, Uint8Array> => {\n const normalized: Record<string, Uint8Array> = {};\n for (const [rawName, fileBytes] of Object.entries(archive)) {\n const name = this.normalizeEntry(rawName);\n if (Object.hasOwn(normalized, name)) {\n throw new Error(`bundle 包含重复路径:${name}`);\n }\n normalized[name] = fileBytes;\n }\n return normalized;\n };\n\n private normalizeEntry = (rawName: string): string => {\n const normalized = rawName.replace(/\\\\/g, \"/\");\n const segments = normalized.split(\"/\");\n if (\n !normalized ||\n normalized.startsWith(\"/\") ||\n normalized.includes(\"\\0\") ||\n /^[A-Za-z]:/.test(normalized) ||\n segments.some(\n (segment) => !segment || segment === \".\" || segment === \"..\",\n )\n ) {\n throw new Error(`bundle 内包含非法路径:${rawName}`);\n }\n return segments.join(\"/\");\n };\n\n private assertCentralEntriesMatchArchive = (\n entries: ZipCentralEntry[],\n archive: Record<string, Uint8Array>,\n ): void => {\n const centralPaths = entries\n .map((entry) => entry.name)\n .sort((left, right) => left.localeCompare(right));\n const archivePaths = Object.keys(archive).sort((left, right) =>\n left.localeCompare(right),\n );\n if (\n centralPaths.length !== archivePaths.length ||\n centralPaths.some((entry, index) => entry !== archivePaths[index])\n ) {\n throw new Error(\"bundle 解压结果与 central directory 不一致。\");\n }\n for (const entry of entries) {\n if (archive[entry.name]?.byteLength !== entry.uncompressedSize) {\n throw new Error(`bundle 解压后文件大小不一致:${entry.name}`);\n }\n }\n };\n\n private readMetadata = (\n archive: Record<string, Uint8Array>,\n ): AppBundleMetadata => {\n const raw = this.readJsonEntry<Partial<AppBundleMetadata>>(\n archive,\n BUNDLE_METADATA_PATH,\n \"bundle metadata\",\n );\n const distributionMode = raw.distributionMode ?? \"bundle\";\n if (\n raw.bundleFormatVersion !== 1 ||\n (distributionMode !== \"bundle\" && distributionMode !== \"source\") ||\n typeof raw.appId !== \"string\" ||\n !raw.appId ||\n typeof raw.name !== \"string\" ||\n !raw.name ||\n typeof raw.version !== \"string\" ||\n !raw.version ||\n raw.entryManifest !== ENTRY_MANIFEST_PATH ||\n raw.checksumsFile !== CHECKSUMS_PATH\n ) {\n throw new Error(\"bundle metadata 无效。\");\n }\n return {\n ...raw,\n distributionMode,\n target: raw.target === undefined\n ? undefined\n : this.platformTargetService.parseArtifactTarget(\n raw.target,\n \"bundle metadata.target\",\n ),\n } as AppBundleMetadata;\n };\n\n private readChecksums = (\n archive: Record<string, Uint8Array>,\n ): AppBundleChecksums => {\n const raw = this.readJsonEntry<Partial<AppBundleChecksums>>(\n archive,\n CHECKSUMS_PATH,\n \"bundle checksums\",\n );\n if (raw.algorithm !== \"sha256\" || !this.isRecord(raw.files)) {\n throw new Error(\"bundle checksums 无效。\");\n }\n return raw as AppBundleChecksums;\n };\n\n private verifyChecksums = async (\n archive: Record<string, Uint8Array>,\n checksums: AppBundleChecksums,\n ): Promise<void> => {\n const actualPaths = Object.keys(archive)\n .filter((entry) => entry !== CHECKSUMS_PATH)\n .sort((left, right) => left.localeCompare(right));\n const checksumPaths = Object.keys(checksums.files)\n .map((entry) => this.normalizeEntry(entry))\n .sort((left, right) => left.localeCompare(right));\n if (\n actualPaths.length !== checksumPaths.length ||\n actualPaths.some((entry, index) => entry !== checksumPaths[index])\n ) {\n throw new Error(\"bundle checksums 必须精确覆盖所有 artifact 文件。\");\n }\n for (const relativePath of actualPaths) {\n const expectedHash = checksums.files[relativePath];\n if (!expectedHash || !/^[a-f0-9]{64}$/.test(expectedHash)) {\n throw new Error(`bundle checksum 格式无效:${relativePath}`);\n }\n const actualHash = await this.sha256(archive[relativePath] as Uint8Array);\n if (actualHash !== expectedHash) {\n throw new Error(`bundle checksum 校验失败:${relativePath}`);\n }\n }\n };\n\n private assertManifestContract = (params: {\n archive: Record<string, Uint8Array>;\n manifest: Record<string, unknown>;\n metadata: AppBundleMetadata;\n }): void => {\n const { archive, manifest, metadata } = params;\n const identity = manifest as ArtifactManifestIdentity;\n if (\n identity.id !== metadata.appId ||\n identity.name !== metadata.name ||\n identity.version !== metadata.version\n ) {\n throw new Error(\"bundle metadata 与 manifest.json 身份不一致。\");\n }\n if (identity.schemaVersion === 2) {\n this.componentContractService.assertArtifact({\n archive,\n identity,\n manifest,\n distributionMode: metadata.distributionMode,\n });\n } else if (identity.schemaVersion === 1) {\n this.assertStandaloneEntry(archive, identity.main, \"main.entry\");\n this.assertStandaloneEntry(archive, identity.ui, \"ui.entry\");\n } else {\n throw new Error(\"manifest.schemaVersion 必须是 1 或 2。\");\n }\n if (identity.icon !== undefined) {\n if (\n typeof identity.icon !== \"string\" ||\n !archive[this.normalizeEntry(identity.icon)]\n ) {\n throw new Error(\"bundle 缺少 manifest.icon 声明的文件。\");\n }\n }\n };\n\n private assertStandaloneEntry = (\n archive: Record<string, Uint8Array>,\n rawContainer: unknown,\n pathLabel: string,\n ): void => {\n if (\n !this.isRecord(rawContainer) ||\n typeof rawContainer.entry !== \"string\"\n ) {\n throw new Error(`manifest.${pathLabel} 无效。`);\n }\n const entry = this.normalizeEntry(rawContainer.entry);\n if (!archive[entry]) {\n throw new Error(`bundle 缺少 manifest.${pathLabel} 文件:${entry}`);\n }\n };\n\n private assertExpectedContract = (params: {\n archive: Record<string, Uint8Array>;\n expected: AppArtifactValidationExpectation;\n manifest: Record<string, unknown>;\n metadata: AppBundleMetadata;\n }): void => {\n const { archive, expected, manifest, metadata } = params;\n if (\n expected.appId !== metadata.appId ||\n expected.name !== metadata.name ||\n expected.version !== metadata.version ||\n expected.distributionMode !== metadata.distributionMode\n ) {\n throw new Error(\"artifact metadata 与 publish payload 不一致。\");\n }\n if (\n expected.target !== undefined &&\n (\n metadata.target === undefined ||\n this.platformTargetService.toTargetKey(expected.target) !==\n this.platformTargetService.toTargetKey(metadata.target)\n )\n ) {\n throw new Error(\"artifact target metadata 与 publish payload 不一致。\");\n }\n if (\n expected.manifest !== undefined &&\n !this.isJsonSubset(this.toJsonValue(expected.manifest), manifest)\n ) {\n throw new Error(\n \"artifact manifest.json 与 publish payload.manifest 不一致。\",\n );\n }\n if (\n expected.target?.kind === \"native\" &&\n manifest.schemaVersion === 2 &&\n Array.isArray(manifest.components)\n ) {\n for (const rawComponent of manifest.components) {\n if (!this.isRecord(rawComponent) || rawComponent.kind !== \"service\") {\n continue;\n }\n const componentPath = typeof rawComponent.path === \"string\"\n ? this.normalizeEntry(rawComponent.path)\n : undefined;\n if (!componentPath) {\n throw new Error(\"service component path 无效。\");\n }\n const serviceManifest = this.readJsonEntry<Record<string, unknown>>(\n archive,\n `${componentPath}/service-app.json`,\n `${componentPath}/service-app.json`,\n );\n this.serviceLaunchService.resolve(serviceManifest, expected.target);\n }\n }\n };\n\n private readJsonEntry = <T>(\n archive: Record<string, Uint8Array>,\n entryPath: string,\n label: string,\n ): T => {\n const bytes = archive[entryPath];\n if (!bytes) {\n throw new Error(`bundle 缺少 ${label}。`);\n }\n try {\n return JSON.parse(strFromU8(bytes)) as T;\n } catch (error) {\n throw new Error(\n `无法读取 ${label}:${error instanceof Error ? error.message : String(error)}`,\n );\n }\n };\n\n private decodeUtf8 = (bytes: Uint8Array): string => {\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n throw new Error(\"bundle 文件名不是有效 UTF-8。\");\n }\n };\n\n private sha256 = async (bytes: Uint8Array): Promise<string> => {\n const digest = await globalThis.crypto.subtle.digest(\n \"SHA-256\",\n new Uint8Array(bytes),\n );\n return [...new Uint8Array(digest)]\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n };\n\n private toJsonValue = (value: unknown): unknown => {\n const serialized = JSON.stringify(value);\n return serialized === undefined\n ? null\n : (JSON.parse(serialized) as unknown);\n };\n\n private isJsonSubset = (expected: unknown, actual: unknown): boolean => {\n if (Array.isArray(expected)) {\n return (\n Array.isArray(actual) &&\n expected.length === actual.length &&\n expected.every((entry, index) =>\n this.isJsonSubset(entry, actual[index]),\n )\n );\n }\n if (this.isRecord(expected)) {\n return (\n this.isRecord(actual) &&\n Object.entries(expected).every(\n ([key, value]) =>\n Object.hasOwn(actual, key) && this.isJsonSubset(value, actual[key]),\n )\n );\n }\n return Object.is(expected, actual);\n };\n\n private isRecord = (value: unknown): value is Record<string, unknown> =>\n Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AppComponentManifest, AppComponentReference, AppDistributionDeclaration, AppRuntimeProfile } from "../types/app-manifest.types.js";
|
|
2
|
+
import { AppPlatformTargetService } from "./app-platform-target.service.js";
|
|
3
|
+
|
|
4
|
+
//#region src/services/app-component-contract.service.d.ts
|
|
5
|
+
type AppArtifactComponentIdentity = {
|
|
6
|
+
components?: unknown;
|
|
7
|
+
runtime?: unknown;
|
|
8
|
+
distribution?: unknown;
|
|
9
|
+
};
|
|
10
|
+
declare class AppComponentContractService {
|
|
11
|
+
private readonly platformTargetService;
|
|
12
|
+
constructor(platformTargetService?: AppPlatformTargetService);
|
|
13
|
+
loadDirectoryComponent: (params: {
|
|
14
|
+
component: AppComponentReference;
|
|
15
|
+
componentDirectory: string;
|
|
16
|
+
runtimeProfile: AppRuntimeProfile;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
}>;
|
|
20
|
+
assertRuntimeDistribution: (runtime: AppComponentManifest["runtime"], distribution: AppDistributionDeclaration | undefined) => void;
|
|
21
|
+
assertArtifact: (params: {
|
|
22
|
+
archive: Record<string, Uint8Array>;
|
|
23
|
+
identity: AppArtifactComponentIdentity;
|
|
24
|
+
manifest: Record<string, unknown>;
|
|
25
|
+
distributionMode: string;
|
|
26
|
+
}) => void;
|
|
27
|
+
private assertArtifactComponent;
|
|
28
|
+
private readComponentId;
|
|
29
|
+
private readArtifactRuntimeProfile;
|
|
30
|
+
private readJsonEntry;
|
|
31
|
+
private normalizeEntry;
|
|
32
|
+
private assertRecord;
|
|
33
|
+
private isRecord;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { AppComponentContractService };
|
|
37
|
+
//# sourceMappingURL=app-component-contract.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-component-contract.service.d.ts","names":[],"sources":["../../src/services/app-component-contract.service.ts"],"mappings":";;;;KAiBY,4BAAA;EACV,UAAA;EACA,OAAA;EACA,YAAA;AAAA;AAAA,cAGW,2BAAA;EAAA,iBAEQ,qBAAA;cAAA,qBAAA,GAAqB,wBAAA;EAGxC,sBAAA,GAAgC,MAAA;IAC9B,SAAA,EAAW,qBAAA;IACX,kBAAA;IACA,cAAA,EAAgB,iBAAA;EAAA,MACd,OAAA;IAAU,EAAA;EAAA;EAcd,yBAAA,GACE,OAAA,EAAS,oBAAA,aACT,YAAA,EAAc,0BAAA;EAUhB,cAAA,GAAkB,MAAA;IAChB,OAAA,EAAS,MAAA,SAAe,UAAA;IACxB,QAAA,EAAU,4BAAA;IACV,QAAA,EAAU,MAAA;IACV,gBAAA;EAAA;EAAA,QAoBM,uBAAA;EAAA,QAoCA,eAAA;EAAA,QAyBA,0BAAA;EAAA,QAkCA,aAAA;EAAA,QASA,cAAA;EAAA,QAaA,YAAA;EAAA,QAKA,QAAA;AAAA"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { AppPlatformTargetService } from "./app-platform-target.service.js";
|
|
2
|
+
import { assertWasmComponentBinary, parseAppServiceComponentContract } from "../utils/app-service-component-contract.utils.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { strFromU8 } from "fflate";
|
|
6
|
+
//#region src/services/app-component-contract.service.ts
|
|
7
|
+
const COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8
|
+
var AppComponentContractService = class {
|
|
9
|
+
constructor(platformTargetService = new AppPlatformTargetService()) {
|
|
10
|
+
this.platformTargetService = platformTargetService;
|
|
11
|
+
}
|
|
12
|
+
loadDirectoryComponent = async (params) => {
|
|
13
|
+
const { component, componentDirectory, runtimeProfile } = params;
|
|
14
|
+
const manifestFile = component.kind === "panel" ? "panel-app.json" : "service-app.json";
|
|
15
|
+
const label = `${component.path}/${manifestFile}`;
|
|
16
|
+
const manifest = this.assertRecord(JSON.parse(await readFile(path.join(componentDirectory, manifestFile), "utf8")), label);
|
|
17
|
+
if (component.kind === "service") parseAppServiceComponentContract({
|
|
18
|
+
manifest,
|
|
19
|
+
runtimeProfile,
|
|
20
|
+
label
|
|
21
|
+
});
|
|
22
|
+
return { id: this.readComponentId({
|
|
23
|
+
component,
|
|
24
|
+
componentDirectory,
|
|
25
|
+
manifest,
|
|
26
|
+
label
|
|
27
|
+
}) };
|
|
28
|
+
};
|
|
29
|
+
assertRuntimeDistribution = (runtime, distribution) => {
|
|
30
|
+
if (runtime?.profile === "wasi" && this.platformTargetService.resolveDistribution(distribution).mode !== "universal") throw new Error("runtime.profile=wasi 只支持 distribution.mode=universal。");
|
|
31
|
+
};
|
|
32
|
+
assertArtifact = (params) => {
|
|
33
|
+
const { archive, identity, manifest, distributionMode } = params;
|
|
34
|
+
if (distributionMode !== "bundle") throw new Error("schema v2 组合包只支持 bundle 分发。");
|
|
35
|
+
if (!Array.isArray(identity.components) || identity.components.length === 0) throw new Error("schema v2 manifest.components 必须是非空数组。");
|
|
36
|
+
const runtimeProfile = this.readArtifactRuntimeProfile(manifest);
|
|
37
|
+
const distribution = this.platformTargetService.parseDistribution(identity.distribution);
|
|
38
|
+
this.assertRuntimeDistribution({ profile: runtimeProfile }, distribution);
|
|
39
|
+
for (const [index, rawComponent] of identity.components.entries()) this.assertArtifactComponent({
|
|
40
|
+
archive,
|
|
41
|
+
index,
|
|
42
|
+
rawComponent,
|
|
43
|
+
runtimeProfile
|
|
44
|
+
});
|
|
45
|
+
if (!archive["marketplace.json"]) throw new Error("schema v2 bundle 缺少 marketplace.json。");
|
|
46
|
+
};
|
|
47
|
+
assertArtifactComponent = (params) => {
|
|
48
|
+
const { archive, index, rawComponent, runtimeProfile } = params;
|
|
49
|
+
if (!this.isRecord(rawComponent)) throw new Error(`manifest.components[${index}] 必须是对象。`);
|
|
50
|
+
const { kind, path: componentPath } = rawComponent;
|
|
51
|
+
if (kind !== "panel" && kind !== "service" || typeof componentPath !== "string") throw new Error(`manifest.components[${index}] 无效。`);
|
|
52
|
+
const normalizedPath = this.normalizeEntry(componentPath);
|
|
53
|
+
const manifestPath = `${normalizedPath}/${kind === "panel" ? "panel-app.json" : "service-app.json"}`;
|
|
54
|
+
const manifestBytes = archive[manifestPath];
|
|
55
|
+
if (!manifestBytes) throw new Error(`bundle 缺少组件 manifest:${manifestPath}`);
|
|
56
|
+
if (kind !== "service") return;
|
|
57
|
+
const contract = parseAppServiceComponentContract({
|
|
58
|
+
manifest: this.readJsonEntry(manifestBytes, manifestPath),
|
|
59
|
+
runtimeProfile,
|
|
60
|
+
label: manifestPath
|
|
61
|
+
});
|
|
62
|
+
if (!contract.componentEntry) return;
|
|
63
|
+
const componentEntryPath = `${normalizedPath}/${contract.componentEntry}`;
|
|
64
|
+
const componentBytes = archive[componentEntryPath];
|
|
65
|
+
if (!componentBytes) throw new Error(`bundle 缺少 Portable Component:${componentEntryPath}`);
|
|
66
|
+
assertWasmComponentBinary(componentBytes, componentEntryPath);
|
|
67
|
+
};
|
|
68
|
+
readComponentId = (params) => {
|
|
69
|
+
const { component, componentDirectory, manifest, label } = params;
|
|
70
|
+
const id = typeof manifest.id === "string" ? manifest.id.trim() : "";
|
|
71
|
+
if (!id) throw new Error(`${label}.id 必须是非空字符串。`);
|
|
72
|
+
if (!COMPONENT_ID_PATTERN.test(id)) throw new Error(`组件 id 必须是 kebab-case:${id}`);
|
|
73
|
+
const directoryName = path.basename(componentDirectory);
|
|
74
|
+
const expectedId = component.kind === "panel" ? directoryName.replace(/\.panel$/, "") : directoryName;
|
|
75
|
+
if (component.kind === "panel" && !directoryName.endsWith(".panel")) throw new Error(`Panel component 目录必须以 .panel 结尾:${component.path}`);
|
|
76
|
+
if (id !== expectedId) throw new Error(`组件 id 必须与目录名一致:期望 ${expectedId},实际 ${id}`);
|
|
77
|
+
return id;
|
|
78
|
+
};
|
|
79
|
+
readArtifactRuntimeProfile = (manifest) => {
|
|
80
|
+
const components = manifest.components;
|
|
81
|
+
if (!Array.isArray(components)) throw new Error("schema v2 manifest.components 必须是数组。");
|
|
82
|
+
const hasService = components.some((component) => this.isRecord(component) && component.kind === "service");
|
|
83
|
+
const runtime = manifest.runtime;
|
|
84
|
+
const explicitProfile = runtime === void 0 ? void 0 : this.isRecord(runtime) && typeof runtime.profile === "string" ? runtime.profile : null;
|
|
85
|
+
if (explicitProfile !== void 0 && explicitProfile !== "panel-only" && explicitProfile !== "wasi" && explicitProfile !== "native-process") throw new Error("manifest.runtime.profile 必须是 panel-only、wasi 或 native-process。");
|
|
86
|
+
const profile = explicitProfile ?? (hasService ? "native-process" : "panel-only");
|
|
87
|
+
if (profile === "panel-only" && hasService) throw new Error("runtime.profile=panel-only 不能包含 Service component。");
|
|
88
|
+
if (profile !== "panel-only" && !hasService) throw new Error(`${profile} runtime 必须包含 Service component。`);
|
|
89
|
+
return profile;
|
|
90
|
+
};
|
|
91
|
+
readJsonEntry = (bytes, label) => {
|
|
92
|
+
try {
|
|
93
|
+
return this.assertRecord(JSON.parse(strFromU8(bytes)), label);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof SyntaxError) throw new Error(`${label} 不是有效 JSON。`);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
normalizeEntry = (rawName) => {
|
|
100
|
+
const normalized = rawName.replace(/\\/g, "/");
|
|
101
|
+
const segments = normalized.split("/");
|
|
102
|
+
if (!normalized || normalized.startsWith("/") || normalized.includes("\0") || /^[A-Za-z]:/.test(normalized) || segments.some((segment) => !segment || segment === "." || segment === "..")) throw new Error(`bundle 内包含非法路径:${rawName}`);
|
|
103
|
+
return segments.join("/");
|
|
104
|
+
};
|
|
105
|
+
assertRecord = (value, label) => {
|
|
106
|
+
if (!this.isRecord(value)) throw new Error(`${label} 必须是对象。`);
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
export { AppComponentContractService };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=app-component-contract.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-component-contract.service.js","names":[],"sources":["../../src/services/app-component-contract.service.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { strFromU8 } from \"fflate\";\nimport { AppPlatformTargetService } from \"#app-runtime/services/app-platform-target.service.js\";\nimport type {\n AppComponentManifest,\n AppComponentReference,\n AppDistributionDeclaration,\n AppRuntimeProfile,\n} from \"#app-runtime/types/app-manifest.types.js\";\nimport {\n assertWasmComponentBinary,\n parseAppServiceComponentContract,\n} from \"#app-runtime/utils/app-service-component-contract.utils.js\";\n\nconst COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport type AppArtifactComponentIdentity = {\n components?: unknown;\n runtime?: unknown;\n distribution?: unknown;\n};\n\nexport class AppComponentContractService {\n constructor(\n private readonly platformTargetService = new AppPlatformTargetService(),\n ) {}\n\n loadDirectoryComponent = async (params: {\n component: AppComponentReference;\n componentDirectory: string;\n runtimeProfile: AppRuntimeProfile;\n }): Promise<{ id: string }> => {\n const { component, componentDirectory, runtimeProfile } = params;\n const manifestFile = component.kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\";\n const label = `${component.path}/${manifestFile}`;\n const manifest = this.assertRecord(\n JSON.parse(await readFile(path.join(componentDirectory, manifestFile), \"utf8\")) as unknown,\n label,\n );\n if (component.kind === \"service\") {\n parseAppServiceComponentContract({ manifest, runtimeProfile, label });\n }\n return { id: this.readComponentId({ component, componentDirectory, manifest, label }) };\n };\n\n assertRuntimeDistribution = (\n runtime: AppComponentManifest[\"runtime\"],\n distribution: AppDistributionDeclaration | undefined,\n ): void => {\n if (\n runtime?.profile === \"wasi\" &&\n this.platformTargetService.resolveDistribution(distribution).mode !== \"universal\"\n ) {\n throw new Error(\"runtime.profile=wasi 只支持 distribution.mode=universal。\");\n }\n };\n\n assertArtifact = (params: {\n archive: Record<string, Uint8Array>;\n identity: AppArtifactComponentIdentity;\n manifest: Record<string, unknown>;\n distributionMode: string;\n }): void => {\n const { archive, identity, manifest, distributionMode } = params;\n if (distributionMode !== \"bundle\") {\n throw new Error(\"schema v2 组合包只支持 bundle 分发。\");\n }\n if (!Array.isArray(identity.components) || identity.components.length === 0) {\n throw new Error(\"schema v2 manifest.components 必须是非空数组。\");\n }\n const runtimeProfile = this.readArtifactRuntimeProfile(manifest);\n const distribution = this.platformTargetService.parseDistribution(identity.distribution);\n this.assertRuntimeDistribution({ profile: runtimeProfile }, distribution);\n for (const [index, rawComponent] of identity.components.entries()) {\n this.assertArtifactComponent({ archive, index, rawComponent, runtimeProfile });\n }\n if (!archive[\"marketplace.json\"]) {\n throw new Error(\"schema v2 bundle 缺少 marketplace.json。\");\n }\n };\n\n private assertArtifactComponent = (params: {\n archive: Record<string, Uint8Array>;\n index: number;\n rawComponent: unknown;\n runtimeProfile: AppRuntimeProfile;\n }): void => {\n const { archive, index, rawComponent, runtimeProfile } = params;\n if (!this.isRecord(rawComponent)) {\n throw new Error(`manifest.components[${index}] 必须是对象。`);\n }\n const { kind, path: componentPath } = rawComponent;\n if ((kind !== \"panel\" && kind !== \"service\") || typeof componentPath !== \"string\") {\n throw new Error(`manifest.components[${index}] 无效。`);\n }\n const normalizedPath = this.normalizeEntry(componentPath);\n const manifestPath = `${normalizedPath}/${kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\"}`;\n const manifestBytes = archive[manifestPath];\n if (!manifestBytes) {\n throw new Error(`bundle 缺少组件 manifest:${manifestPath}`);\n }\n if (kind !== \"service\") return;\n const serviceManifest = this.readJsonEntry(manifestBytes, manifestPath);\n const contract = parseAppServiceComponentContract({\n manifest: serviceManifest,\n runtimeProfile,\n label: manifestPath,\n });\n if (!contract.componentEntry) return;\n const componentEntryPath = `${normalizedPath}/${contract.componentEntry}`;\n const componentBytes = archive[componentEntryPath];\n if (!componentBytes) {\n throw new Error(`bundle 缺少 Portable Component:${componentEntryPath}`);\n }\n assertWasmComponentBinary(componentBytes, componentEntryPath);\n };\n\n private readComponentId = (params: {\n component: AppComponentReference;\n componentDirectory: string;\n manifest: Record<string, unknown>;\n label: string;\n }): string => {\n const { component, componentDirectory, manifest, label } = params;\n const id = typeof manifest.id === \"string\" ? manifest.id.trim() : \"\";\n if (!id) throw new Error(`${label}.id 必须是非空字符串。`);\n if (!COMPONENT_ID_PATTERN.test(id)) {\n throw new Error(`组件 id 必须是 kebab-case:${id}`);\n }\n const directoryName = path.basename(componentDirectory);\n const expectedId = component.kind === \"panel\"\n ? directoryName.replace(/\\.panel$/, \"\")\n : directoryName;\n if (component.kind === \"panel\" && !directoryName.endsWith(\".panel\")) {\n throw new Error(`Panel component 目录必须以 .panel 结尾:${component.path}`);\n }\n if (id !== expectedId) {\n throw new Error(`组件 id 必须与目录名一致:期望 ${expectedId},实际 ${id}`);\n }\n return id;\n };\n\n private readArtifactRuntimeProfile = (\n manifest: Record<string, unknown>,\n ): AppRuntimeProfile => {\n const components = manifest.components;\n if (!Array.isArray(components)) {\n throw new Error(\"schema v2 manifest.components 必须是数组。\");\n }\n const hasService = components.some((component) =>\n this.isRecord(component) && component.kind === \"service\"\n );\n const runtime = manifest.runtime;\n const explicitProfile = runtime === undefined\n ? undefined\n : this.isRecord(runtime) && typeof runtime.profile === \"string\"\n ? runtime.profile\n : null;\n if (\n explicitProfile !== undefined &&\n explicitProfile !== \"panel-only\" &&\n explicitProfile !== \"wasi\" &&\n explicitProfile !== \"native-process\"\n ) {\n throw new Error(\"manifest.runtime.profile 必须是 panel-only、wasi 或 native-process。\");\n }\n const profile = explicitProfile ?? (hasService ? \"native-process\" : \"panel-only\");\n if (profile === \"panel-only\" && hasService) {\n throw new Error(\"runtime.profile=panel-only 不能包含 Service component。\");\n }\n if (profile !== \"panel-only\" && !hasService) {\n throw new Error(`${profile} runtime 必须包含 Service component。`);\n }\n return profile;\n };\n\n private readJsonEntry = (bytes: Uint8Array, label: string): Record<string, unknown> => {\n try {\n return this.assertRecord(JSON.parse(strFromU8(bytes)) as unknown, label);\n } catch (error) {\n if (error instanceof SyntaxError) throw new Error(`${label} 不是有效 JSON。`);\n throw error;\n }\n };\n\n private normalizeEntry = (rawName: string): string => {\n const normalized = rawName.replace(/\\\\/g, \"/\");\n const segments = normalized.split(\"/\");\n if (\n !normalized || normalized.startsWith(\"/\") || normalized.includes(\"\\0\") ||\n /^[A-Za-z]:/.test(normalized) ||\n segments.some((segment) => !segment || segment === \".\" || segment === \"..\")\n ) {\n throw new Error(`bundle 内包含非法路径:${rawName}`);\n }\n return segments.join(\"/\");\n };\n\n private assertRecord = (value: unknown, label: string): Record<string, unknown> => {\n if (!this.isRecord(value)) throw new Error(`${label} 必须是对象。`);\n return value;\n };\n\n private isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { AppComponentManifest, AppManifestBundle, AppManifestSummary, AppPlatformSecuritySummary } from "../types/app-manifest.types.js";
|
|
2
2
|
import { AppPlatformTargetService } from "./app-platform-target.service.js";
|
|
3
|
+
import { AppComponentContractService } from "./app-component-contract.service.js";
|
|
3
4
|
|
|
4
5
|
//#region src/services/app-manifest.service.d.ts
|
|
5
6
|
declare class AppManifestService {
|
|
6
7
|
private readonly platformTargetService;
|
|
7
|
-
|
|
8
|
+
private readonly componentContractService;
|
|
9
|
+
constructor(platformTargetService?: AppPlatformTargetService, componentContractService?: AppComponentContractService);
|
|
8
10
|
load: (appDirectory: string) => Promise<AppManifestBundle>;
|
|
9
11
|
summarize: (bundle: AppManifestBundle) => AppManifestSummary;
|
|
10
12
|
private loadStandaloneBundle;
|
|
@@ -20,7 +22,6 @@ declare class AppManifestService {
|
|
|
20
22
|
private parseRuntime;
|
|
21
23
|
private parseAppStorage;
|
|
22
24
|
private assertRuntimeMatchesComponents;
|
|
23
|
-
private readComponentId;
|
|
24
25
|
private parsePermissions;
|
|
25
26
|
private parseDocumentAccess;
|
|
26
27
|
private parseSecretSlots;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-manifest.service.d.ts","names":[],"sources":["../../src/services/app-manifest.service.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-manifest.service.d.ts","names":[],"sources":["../../src/services/app-manifest.service.ts"],"mappings":";;;;;cAuBa,kBAAA;EAAA,iBAEQ,qBAAA;EAAA,iBACA,wBAAA;cADA,qBAAA,GAAqB,wBAAA,EACrB,wBAAA,GAAwB,2BAAA;EAK3C,IAAA,GAAc,YAAA,aAAuB,OAAA,CAAQ,iBAAA;EAU7C,SAAA,GAAa,MAAA,EAAQ,iBAAA,KAAoB,kBAAA;EAAA,QAwCjC,oBAAA;EAAA,QA8BA,mBAAA;EAAA,QAkEA,aAAA;EAkCR,uBAAA,GACE,QAAA,EAAU,oBAAA,KACT,0BAAA;EAAA,QAgCK,mBAAA;EAAA,QAcA,SAAA;EAAA,QAoBA,OAAA;EAAA,QAKA,eAAA;EAAA,QA+BA,YAAA;EAAA,QAUA,iBAAA;EAAA,QAeA,YAAA;EAAA,QAcA,eAAA;EAAA,QAkBA,8BAAA;EAAA,QAsBA,gBAAA;EAAA,QAcA,mBAAA;EAAA,QAwBA,gBAAA;EAAA,QAoCA,gBAAA;EAAA,QAUA,YAAA;EAAA,QAQA,iBAAA;EAAA,QAeA,kBAAA;EAAA,QAaA,uBAAA;EAAA,QAcA,aAAA;EAAA,QAUA,qBAAA;EAAA,QAWA,YAAA;EAAA,QAOA,kBAAA;EAAA,QAOA,kBAAA;EAAA,QAIA,UAAA;EAAA,QAOA,WAAA;AAAA"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { AppPlatformTargetService } from "./app-platform-target.service.js";
|
|
2
|
+
import { AppComponentContractService } from "./app-component-contract.service.js";
|
|
2
3
|
import { access, lstat, readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
//#region src/services/app-manifest.service.ts
|
|
5
6
|
const PACKAGE_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
6
|
-
const COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
7
|
const SECRET_SLOT_ID_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
|
|
8
8
|
var AppManifestService = class {
|
|
9
|
-
constructor(platformTargetService = new AppPlatformTargetService()) {
|
|
9
|
+
constructor(platformTargetService = new AppPlatformTargetService(), componentContractService = new AppComponentContractService(platformTargetService)) {
|
|
10
10
|
this.platformTargetService = platformTargetService;
|
|
11
|
+
this.componentContractService = componentContractService;
|
|
11
12
|
}
|
|
12
13
|
load = async (appDirectory) => {
|
|
13
14
|
const normalizedDirectory = path.resolve(appDirectory);
|
|
@@ -69,7 +70,11 @@ var AppManifestService = class {
|
|
|
69
70
|
const components = [];
|
|
70
71
|
for (const [index, component] of manifest.components.entries()) {
|
|
71
72
|
const componentDirectory = await this.assertResolvedDirectory(appDirectory, component.path, `components[${index}].path`);
|
|
72
|
-
const componentId = await this.
|
|
73
|
+
const { id: componentId } = await this.componentContractService.loadDirectoryComponent({
|
|
74
|
+
component,
|
|
75
|
+
componentDirectory,
|
|
76
|
+
runtimeProfile: manifest.runtime?.profile ?? "native-process"
|
|
77
|
+
});
|
|
73
78
|
const expectedPrefix = `${manifest.id.replace(/[^a-z0-9]+/g, "-")}-`;
|
|
74
79
|
if (!componentId.startsWith(expectedPrefix)) throw new Error(`组件 ${componentId} 必须使用包前缀 ${expectedPrefix}。`);
|
|
75
80
|
components.push({
|
|
@@ -112,13 +117,15 @@ var AppManifestService = class {
|
|
|
112
117
|
const components = this.parseComponents(candidate.components);
|
|
113
118
|
const runtime = this.parseRuntime(candidate.runtime);
|
|
114
119
|
this.assertRuntimeMatchesComponents(runtime, components);
|
|
120
|
+
const distribution = this.platformTargetService.parseDistribution(candidate.distribution);
|
|
121
|
+
this.componentContractService.assertRuntimeDistribution(runtime, distribution);
|
|
115
122
|
return {
|
|
116
123
|
schemaVersion: 2,
|
|
117
124
|
...common,
|
|
118
125
|
engines: this.parseEngines(candidate.engines),
|
|
119
126
|
presentation: this.parsePresentation(candidate.presentation),
|
|
120
127
|
runtime,
|
|
121
|
-
distribution
|
|
128
|
+
distribution,
|
|
122
129
|
storage: this.parseAppStorage(candidate.storage),
|
|
123
130
|
permissions: this.parsePermissions(candidate.permissions),
|
|
124
131
|
components
|
|
@@ -232,18 +239,6 @@ var AppManifestService = class {
|
|
|
232
239
|
}
|
|
233
240
|
if (runtime.profile !== "panel-only" && !hasService) throw new Error(`${runtime.profile} runtime 必须包含 Service component。`);
|
|
234
241
|
};
|
|
235
|
-
readComponentId = async (component, componentDirectory) => {
|
|
236
|
-
const manifestFile = component.kind === "panel" ? "panel-app.json" : "service-app.json";
|
|
237
|
-
const raw = JSON.parse(await readFile(path.join(componentDirectory, manifestFile), "utf-8"));
|
|
238
|
-
const manifest = this.assertObject(raw, `${component.path}/${manifestFile}`);
|
|
239
|
-
const id = this.readRequiredString(manifest.id, `${component.path}/${manifestFile}.id`);
|
|
240
|
-
if (!COMPONENT_ID_PATTERN.test(id)) throw new Error(`组件 id 必须是 kebab-case:${id}`);
|
|
241
|
-
const directoryName = path.basename(componentDirectory);
|
|
242
|
-
const expectedId = component.kind === "panel" ? directoryName.replace(/\.panel$/, "") : directoryName;
|
|
243
|
-
if (component.kind === "panel" && !directoryName.endsWith(".panel")) throw new Error(`Panel component 目录必须以 .panel 结尾:${component.path}`);
|
|
244
|
-
if (id !== expectedId) throw new Error(`组件 id 必须与目录名一致:期望 ${expectedId},实际 ${id}`);
|
|
245
|
-
return id;
|
|
246
|
-
};
|
|
247
242
|
parsePermissions = (rawPermissions) => {
|
|
248
243
|
if (rawPermissions === void 0) return;
|
|
249
244
|
const candidate = this.assertObject(rawPermissions, "permissions");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-manifest.service.js","names":[],"sources":["../../src/services/app-manifest.service.ts"],"sourcesContent":["import { access, lstat, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { AppPlatformTargetService } from \"#app-runtime/services/app-platform-target.service.js\";\nimport type {\n AppComponentManifest,\n AppComponentManifestBundle,\n AppComponentReference,\n AppDocumentAccessScope,\n AppManifest,\n AppManifestBundle,\n AppManifestSummary,\n AppPermissions,\n AppPlatformSecuritySummary,\n AppResolvedComponent,\n AppSecretSlot,\n AppStandaloneManifest,\n AppStandaloneManifestBundle,\n} from \"#app-runtime/types/app-manifest.types.js\";\n\nconst PACKAGE_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;\nconst COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst SECRET_SLOT_ID_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;\n\nexport class AppManifestService {\n constructor(\n private readonly platformTargetService = new AppPlatformTargetService(),\n ) {}\n\n load = async (appDirectory: string): Promise<AppManifestBundle> => {\n const normalizedDirectory = path.resolve(appDirectory);\n const manifestPath = path.join(normalizedDirectory, \"manifest.json\");\n const rawManifest = JSON.parse(await readFile(manifestPath, \"utf-8\")) as unknown;\n const manifest = this.parseManifest(rawManifest);\n return manifest.schemaVersion === 1\n ? await this.loadStandaloneBundle(normalizedDirectory, manifestPath, manifest)\n : await this.loadComponentBundle(normalizedDirectory, manifestPath, manifest);\n };\n\n summarize = (bundle: AppManifestBundle): AppManifestSummary => {\n if (bundle.manifest.schemaVersion === 2) {\n const componentBundle = bundle as AppComponentManifestBundle;\n return {\n schemaVersion: 2,\n id: componentBundle.manifest.id,\n name: componentBundle.manifest.name,\n version: componentBundle.manifest.version,\n description: componentBundle.manifest.description,\n manifestPath: componentBundle.manifestPath,\n iconPath: componentBundle.iconPath,\n primaryPanelId: componentBundle.primaryPanelId,\n components: componentBundle.components,\n distribution: this.platformTargetService.resolveDistribution(\n componentBundle.manifest.distribution,\n ),\n security: this.resolvePlatformSecurity(componentBundle.manifest),\n };\n }\n const standaloneBundle = bundle as AppStandaloneManifestBundle;\n const permissions = standaloneBundle.manifest.permissions ?? {};\n return {\n schemaVersion: 1,\n id: standaloneBundle.manifest.id,\n name: standaloneBundle.manifest.name,\n version: standaloneBundle.manifest.version,\n description: standaloneBundle.manifest.description,\n mainKind: standaloneBundle.manifest.main.kind,\n action:\n standaloneBundle.manifest.main.kind === \"wasm\"\n ? standaloneBundle.manifest.main.action\n : undefined,\n manifestPath: standaloneBundle.manifestPath,\n mainEntryPath: standaloneBundle.mainEntryPath,\n uiEntryPath: standaloneBundle.uiEntryPath,\n iconPath: standaloneBundle.iconPath,\n permissions,\n };\n };\n\n private loadStandaloneBundle = async (\n appDirectory: string,\n manifestPath: string,\n manifest: AppStandaloneManifest,\n ): Promise<AppStandaloneManifestBundle> => {\n const mainEntryPath = await this.assertResolvedFile(\n appDirectory,\n manifest.main.entry,\n \"main.entry\",\n );\n const uiEntryPath = await this.assertResolvedFile(\n appDirectory,\n manifest.ui.entry,\n \"ui.entry\",\n );\n const iconPath = manifest.icon\n ? await this.assertResolvedFile(appDirectory, manifest.icon, \"icon\")\n : undefined;\n return {\n appDirectory,\n manifestPath,\n manifest,\n mainEntryPath,\n uiEntryPath,\n uiDirectoryPath: path.dirname(uiEntryPath),\n assetsDirectoryPath: path.join(appDirectory, \"assets\"),\n iconPath,\n };\n };\n\n private loadComponentBundle = async (\n appDirectory: string,\n manifestPath: string,\n manifest: AppComponentManifest,\n ): Promise<AppComponentManifestBundle> => {\n const components: AppResolvedComponent[] = [];\n for (const [index, component] of manifest.components.entries()) {\n const componentDirectory = await this.assertResolvedDirectory(\n appDirectory,\n component.path,\n `components[${index}].path`,\n );\n const componentId = await this.readComponentId(component, componentDirectory);\n const expectedPrefix = `${manifest.id.replace(/[^a-z0-9]+/g, \"-\")}-`;\n if (!componentId.startsWith(expectedPrefix)) {\n throw new Error(\n `组件 ${componentId} 必须使用包前缀 ${expectedPrefix}。`,\n );\n }\n components.push({\n ...component,\n id: componentId,\n componentDirectory,\n manifestPath: path.join(\n componentDirectory,\n component.kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\",\n ),\n });\n }\n const duplicateId = components.find(\n (component, index) => components.findIndex((entry) => entry.id === component.id) !== index,\n );\n if (duplicateId) {\n throw new Error(`components 包含重复组件 id:${duplicateId.id}`);\n }\n const panelIds = components\n .filter((component) => component.kind === \"panel\")\n .map((component) => component.id);\n const declaredPrimaryPanel = manifest.presentation?.primaryPanel;\n if (declaredPrimaryPanel && !panelIds.includes(declaredPrimaryPanel)) {\n throw new Error(\"presentation.primaryPanel 必须引用真实 Panel component id。\");\n }\n if (panelIds.length > 1 && !declaredPrimaryPanel) {\n throw new Error(\"包含多个 Panel 时必须声明 presentation.primaryPanel。\");\n }\n if (panelIds.length === 0 && declaredPrimaryPanel) {\n throw new Error(\"不含 Panel 的包不能声明 presentation.primaryPanel。\");\n }\n const iconPath = manifest.icon\n ? await this.assertResolvedFile(appDirectory, manifest.icon, \"icon\")\n : undefined;\n return {\n appDirectory,\n manifestPath,\n manifest,\n components,\n assetsDirectoryPath: path.join(appDirectory, \"assets\"),\n iconPath,\n primaryPanelId: declaredPrimaryPanel ?? panelIds[0],\n };\n };\n\n private parseManifest = (rawManifest: unknown): AppManifest => {\n const candidate = this.assertObject(rawManifest, \"manifest.json\");\n const schemaVersion = this.readNumber(candidate.schemaVersion, \"schemaVersion\");\n if (schemaVersion !== 1 && schemaVersion !== 2) {\n throw new Error(\"当前只支持 schemaVersion = 1 或 2。\");\n }\n const common = this.parseCommonManifest(candidate);\n if (schemaVersion === 1) {\n return {\n schemaVersion: 1,\n ...common,\n main: this.parseMain(candidate.main),\n ui: this.parseUi(candidate.ui),\n permissions: this.parsePermissions(candidate.permissions),\n };\n }\n const components = this.parseComponents(candidate.components);\n const runtime = this.parseRuntime(candidate.runtime);\n this.assertRuntimeMatchesComponents(runtime, components);\n return {\n schemaVersion: 2,\n ...common,\n engines: this.parseEngines(candidate.engines),\n presentation: this.parsePresentation(candidate.presentation),\n runtime,\n distribution: this.platformTargetService.parseDistribution(candidate.distribution),\n storage: this.parseAppStorage(candidate.storage),\n permissions: this.parsePermissions(candidate.permissions),\n components,\n };\n };\n\n resolvePlatformSecurity = (\n manifest: AppComponentManifest,\n ): AppPlatformSecuritySummary => {\n const hasServiceComponents = manifest.components.some(\n (component) => component.kind === \"service\",\n );\n const runtimeProfile = manifest.runtime?.profile ?? (\n hasServiceComponents ? \"native-process\" : \"panel-only\"\n );\n const isolation = runtimeProfile === \"panel-only\"\n ? \"sandboxed\"\n : runtimeProfile === \"wasi\"\n ? \"host-mediated\"\n : \"full-user\";\n const declaredPermissions = manifest.permissions ?? {};\n const permissions: AppPermissions = runtimeProfile === \"native-process\"\n ? {\n ...declaredPermissions,\n storage: declaredPermissions.storage ?? true,\n capabilities: {\n ...declaredPermissions.capabilities,\n nativeProcess: true,\n },\n }\n : declaredPermissions;\n return {\n runtimeProfile,\n isolation,\n hasServiceComponents,\n inferred: manifest.runtime === undefined,\n permissions,\n };\n };\n\n private parseCommonManifest = (candidate: Record<string, unknown>) => {\n const id = this.readRequiredString(candidate.id, \"id\");\n if (!PACKAGE_ID_PATTERN.test(id)) {\n throw new Error(\"id 必须由小写字母、数字、点或连字符组成。\");\n }\n return {\n id,\n name: this.readRequiredString(candidate.name, \"name\"),\n version: this.readRequiredString(candidate.version, \"version\"),\n description: this.readOptionalString(candidate.description, \"description\"),\n icon: this.readOptionalString(candidate.icon, \"icon\"),\n };\n };\n\n private parseMain = (rawMain: unknown): AppStandaloneManifest[\"main\"] => {\n const candidate = this.assertObject(rawMain, \"main\");\n const kind = this.readRequiredString(candidate.kind, \"main.kind\");\n if (kind === \"wasm\") {\n return {\n kind,\n entry: this.readRequiredString(candidate.entry, \"main.entry\"),\n export: this.readRequiredString(candidate.export, \"main.export\"),\n action: this.readRequiredString(candidate.action, \"main.action\"),\n };\n }\n if (kind === \"wasi-http-component\") {\n return {\n kind,\n entry: this.readRequiredString(candidate.entry, \"main.entry\"),\n };\n }\n throw new Error(\"当前 main.kind 只支持 wasm 或 wasi-http-component。\");\n };\n\n private parseUi = (rawUi: unknown): AppStandaloneManifest[\"ui\"] => {\n const candidate = this.assertObject(rawUi, \"ui\");\n return { entry: this.readRequiredString(candidate.entry, \"ui.entry\") };\n };\n\n private parseComponents = (rawComponents: unknown): AppComponentReference[] => {\n if (!Array.isArray(rawComponents) || rawComponents.length === 0) {\n throw new Error(\"components 必须是非空数组。\");\n }\n const components = rawComponents.map((rawComponent, index) => {\n const candidate = this.assertObject(rawComponent, `components[${index}]`);\n const kind = this.readRequiredString(candidate.kind, `components[${index}].kind`);\n if (kind !== \"panel\" && kind !== \"service\") {\n throw new Error(`components[${index}].kind 只支持 panel 或 service。`);\n }\n const normalizedKind: AppComponentReference[\"kind\"] = kind;\n return {\n kind: normalizedKind,\n path: this.normalizeRelativePath(\n this.readRequiredString(candidate.path, `components[${index}].path`),\n `components[${index}].path`,\n ),\n };\n });\n for (let leftIndex = 0; leftIndex < components.length; leftIndex += 1) {\n for (let rightIndex = leftIndex + 1; rightIndex < components.length; rightIndex += 1) {\n const left = components[leftIndex]?.path;\n const right = components[rightIndex]?.path;\n if (left && right && (left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`))) {\n throw new Error(`components path 不能重复或重叠:${left} / ${right}`);\n }\n }\n }\n return components;\n };\n\n private parseEngines = (\n rawEngines: unknown,\n ): AppComponentManifest[\"engines\"] => {\n if (rawEngines === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawEngines, \"engines\");\n return { nextclaw: this.readOptionalString(candidate.nextclaw, \"engines.nextclaw\") };\n };\n\n private parsePresentation = (\n rawPresentation: unknown,\n ): AppComponentManifest[\"presentation\"] => {\n if (rawPresentation === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawPresentation, \"presentation\");\n return {\n primaryPanel: this.readOptionalString(\n candidate.primaryPanel,\n \"presentation.primaryPanel\",\n ),\n };\n };\n\n private parseRuntime = (\n rawRuntime: unknown,\n ): AppComponentManifest[\"runtime\"] => {\n if (rawRuntime === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawRuntime, \"runtime\");\n const profile = this.readRequiredString(candidate.profile, \"runtime.profile\");\n if (profile !== \"panel-only\" && profile !== \"wasi\" && profile !== \"native-process\") {\n throw new Error(\"runtime.profile 只支持 panel-only、wasi 或 native-process。\");\n }\n return { profile };\n };\n\n private parseAppStorage = (\n rawStorage: unknown,\n ): AppComponentManifest[\"storage\"] => {\n if (rawStorage === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawStorage, \"storage\");\n const scope = this.readRequiredString(candidate.scope, \"storage.scope\");\n if (scope !== \"global\") {\n throw new Error(\"当前 storage.scope 只支持 global。\");\n }\n const schemaVersion = this.readNumber(candidate.schemaVersion, \"storage.schemaVersion\");\n if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) {\n throw new Error(\"storage.schemaVersion 必须是正整数。\");\n }\n return { scope, schemaVersion };\n };\n\n private assertRuntimeMatchesComponents = (\n runtime: AppComponentManifest[\"runtime\"],\n components: AppComponentReference[],\n ): void => {\n if (!runtime) {\n return;\n }\n const hasService = components.some((component) => component.kind === \"service\");\n if (runtime.profile === \"panel-only\" && hasService) {\n throw new Error(\"runtime.profile=panel-only 不能包含 Service component。\");\n }\n if (runtime.profile === \"wasi\") {\n if (!hasService) {\n throw new Error(\"wasi runtime 必须包含 Service component。\");\n }\n return;\n }\n if (runtime.profile !== \"panel-only\" && !hasService) {\n throw new Error(`${runtime.profile} runtime 必须包含 Service component。`);\n }\n };\n\n private readComponentId = async (\n component: AppComponentReference,\n componentDirectory: string,\n ): Promise<string> => {\n const manifestFile = component.kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\";\n const raw = JSON.parse(\n await readFile(path.join(componentDirectory, manifestFile), \"utf-8\"),\n ) as unknown;\n const manifest = this.assertObject(raw, `${component.path}/${manifestFile}`);\n const id = this.readRequiredString(manifest.id, `${component.path}/${manifestFile}.id`);\n if (!COMPONENT_ID_PATTERN.test(id)) {\n throw new Error(`组件 id 必须是 kebab-case:${id}`);\n }\n const directoryName = path.basename(componentDirectory);\n const expectedId = component.kind === \"panel\"\n ? directoryName.replace(/\\.panel$/, \"\")\n : directoryName;\n if (component.kind === \"panel\" && !directoryName.endsWith(\".panel\")) {\n throw new Error(`Panel component 目录必须以 .panel 结尾:${component.path}`);\n }\n if (id !== expectedId) {\n throw new Error(`组件 id 必须与目录名一致:期望 ${expectedId},实际 ${id}`);\n }\n return id;\n };\n\n private parsePermissions = (rawPermissions: unknown): AppPermissions | undefined => {\n if (rawPermissions === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawPermissions, \"permissions\");\n return {\n documentAccess: this.parseDocumentAccess(candidate.documentAccess),\n secrets: this.parseSecretSlots(candidate.secrets),\n allowedDomains: this.parseStringArray(candidate.allowedDomains, \"permissions.allowedDomains\"),\n storage: this.parseStorage(candidate.storage),\n capabilities: this.parseCapabilities(candidate.capabilities),\n };\n };\n\n private parseDocumentAccess = (rawDocumentAccess: unknown): AppDocumentAccessScope[] | undefined => {\n if (rawDocumentAccess === undefined) {\n return undefined;\n }\n if (!Array.isArray(rawDocumentAccess)) {\n throw new Error(\"permissions.documentAccess 必须是数组。\");\n }\n return rawDocumentAccess.map((scope, index) => {\n const candidate = this.assertObject(scope, `permissions.documentAccess[${index}]`);\n const mode = this.readRequiredString(candidate.mode, `permissions.documentAccess[${index}].mode`);\n if (mode !== \"read\" && mode !== \"read-write\") {\n throw new Error(`permissions.documentAccess[${index}].mode 只支持 read 或 read-write。`);\n }\n return {\n id: this.readRequiredString(candidate.id, `permissions.documentAccess[${index}].id`),\n mode,\n description: this.readOptionalString(\n candidate.description,\n `permissions.documentAccess[${index}].description`,\n ),\n };\n });\n };\n\n private parseSecretSlots = (rawSecrets: unknown): AppSecretSlot[] | undefined => {\n if (rawSecrets === undefined) {\n return undefined;\n }\n if (!Array.isArray(rawSecrets)) {\n throw new Error(\"permissions.secrets 必须是数组。\");\n }\n const slots = rawSecrets.map((rawSlot, index) => {\n const field = `permissions.secrets[${index}]`;\n const candidate = this.assertObject(rawSlot, field);\n const unsupportedField = Object.keys(candidate).find(\n (key) => ![\"id\", \"title\", \"description\", \"required\"].includes(key),\n );\n if (unsupportedField) {\n throw new Error(`${field} 只能声明 id、title、description 和 required。`);\n }\n const id = this.readRequiredString(candidate.id, `${field}.id`);\n if (!SECRET_SLOT_ID_PATTERN.test(id)) {\n throw new Error(`${field}.id 必须是稳定的 lowercase slot id。`);\n }\n return {\n id,\n title: this.readRequiredString(candidate.title, `${field}.title`),\n description: this.readRequiredString(candidate.description, `${field}.description`),\n required: this.readBoolean(candidate.required, `${field}.required`),\n };\n });\n const duplicate = slots.find(\n (slot, index) => slots.findIndex((entry) => entry.id === slot.id) !== index,\n );\n if (duplicate) {\n throw new Error(`permissions.secrets 包含重复 slot id:${duplicate.id}`);\n }\n return slots;\n };\n\n private parseStringArray = (rawValue: unknown, fieldName: string): string[] | undefined => {\n if (rawValue === undefined) {\n return undefined;\n }\n if (!Array.isArray(rawValue)) {\n throw new Error(`${fieldName} 必须是字符串数组。`);\n }\n return rawValue.map((item, index) => this.readRequiredString(item, `${fieldName}[${index}]`));\n };\n\n private parseStorage = (rawStorage: unknown): AppPermissions[\"storage\"] | undefined => {\n if (rawStorage === undefined || typeof rawStorage === \"boolean\") {\n return rawStorage;\n }\n const candidate = this.assertObject(rawStorage, \"permissions.storage\");\n return { namespace: this.readOptionalString(candidate.namespace, \"permissions.storage.namespace\") };\n };\n\n private parseCapabilities = (rawCapabilities: unknown): AppPermissions[\"capabilities\"] | undefined => {\n if (rawCapabilities === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawCapabilities, \"permissions.capabilities\");\n return {\n hostBridge: candidate.hostBridge === undefined\n ? undefined\n : this.readBoolean(candidate.hostBridge, \"permissions.capabilities.hostBridge\"),\n nativeProcess: candidate.nativeProcess === undefined\n ? undefined\n : this.readBoolean(candidate.nativeProcess, \"permissions.capabilities.nativeProcess\"),\n };\n };\n\n private assertResolvedFile = async (\n appDirectory: string,\n relativePath: string,\n fieldName: string,\n ): Promise<string> => {\n const resolvedPath = this.resolveInside(appDirectory, relativePath, fieldName);\n const stats = await lstat(resolvedPath);\n if (!stats.isFile() || stats.isSymbolicLink()) {\n throw new Error(`${fieldName} 必须指向包内普通文件。`);\n }\n return resolvedPath;\n };\n\n private assertResolvedDirectory = async (\n appDirectory: string,\n relativePath: string,\n fieldName: string,\n ): Promise<string> => {\n const resolvedPath = this.resolveInside(appDirectory, relativePath, fieldName);\n const stats = await lstat(resolvedPath);\n if (!stats.isDirectory() || stats.isSymbolicLink()) {\n throw new Error(`${fieldName} 必须指向包内普通目录。`);\n }\n await access(resolvedPath);\n return resolvedPath;\n };\n\n private resolveInside = (appDirectory: string, relativePath: string, fieldName: string): string => {\n const normalizedPath = this.normalizeRelativePath(relativePath, fieldName);\n const resolvedPath = path.resolve(appDirectory, normalizedPath);\n const relative = path.relative(appDirectory, resolvedPath);\n if (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n throw new Error(`${fieldName} 不能指向应用目录之外。`);\n }\n return resolvedPath;\n };\n\n private normalizeRelativePath = (relativePath: string, fieldName: string): string => {\n if (path.isAbsolute(relativePath) || relativePath.includes(\"\\0\")) {\n throw new Error(`${fieldName} 必须使用包内相对路径。`);\n }\n const segments = relativePath.replace(/\\\\/g, \"/\").split(\"/\");\n if (segments.some((segment) => !segment || segment === \".\" || segment === \"..\")) {\n throw new Error(`${fieldName} 包含非法路径片段。`);\n }\n return segments.join(\"/\");\n };\n\n private assertObject = (value: unknown, fieldName: string): Record<string, unknown> => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(`${fieldName} 必须是对象。`);\n }\n return value as Record<string, unknown>;\n };\n\n private readRequiredString = (value: unknown, fieldName: string): string => {\n if (typeof value !== \"string\" || !value.trim()) {\n throw new Error(`${fieldName} 必须是非空字符串。`);\n }\n return value.trim();\n };\n\n private readOptionalString = (value: unknown, fieldName: string): string | undefined => {\n return value === undefined ? undefined : this.readRequiredString(value, fieldName);\n };\n\n private readNumber = (value: unknown, fieldName: string): number => {\n if (typeof value !== \"number\" || Number.isNaN(value)) {\n throw new Error(`${fieldName} 必须是数字。`);\n }\n return value;\n };\n\n private readBoolean = (value: unknown, fieldName: string): boolean => {\n if (typeof value !== \"boolean\") {\n throw new Error(`${fieldName} 必须是布尔值。`);\n }\n return value;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app-manifest.service.js","names":[],"sources":["../../src/services/app-manifest.service.ts"],"sourcesContent":["import { access, lstat, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { AppComponentContractService } from \"#app-runtime/services/app-component-contract.service.js\";\nimport { AppPlatformTargetService } from \"#app-runtime/services/app-platform-target.service.js\";\nimport type {\n AppComponentManifest,\n AppComponentManifestBundle,\n AppComponentReference,\n AppDocumentAccessScope,\n AppManifest,\n AppManifestBundle,\n AppManifestSummary,\n AppPermissions,\n AppPlatformSecuritySummary,\n AppResolvedComponent,\n AppSecretSlot,\n AppStandaloneManifest,\n AppStandaloneManifestBundle,\n} from \"#app-runtime/types/app-manifest.types.js\";\n\nconst PACKAGE_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;\nconst SECRET_SLOT_ID_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;\n\nexport class AppManifestService {\n constructor(\n private readonly platformTargetService = new AppPlatformTargetService(),\n private readonly componentContractService = new AppComponentContractService(\n platformTargetService,\n ),\n ) {}\n\n load = async (appDirectory: string): Promise<AppManifestBundle> => {\n const normalizedDirectory = path.resolve(appDirectory);\n const manifestPath = path.join(normalizedDirectory, \"manifest.json\");\n const rawManifest = JSON.parse(await readFile(manifestPath, \"utf-8\")) as unknown;\n const manifest = this.parseManifest(rawManifest);\n return manifest.schemaVersion === 1\n ? await this.loadStandaloneBundle(normalizedDirectory, manifestPath, manifest)\n : await this.loadComponentBundle(normalizedDirectory, manifestPath, manifest);\n };\n\n summarize = (bundle: AppManifestBundle): AppManifestSummary => {\n if (bundle.manifest.schemaVersion === 2) {\n const componentBundle = bundle as AppComponentManifestBundle;\n return {\n schemaVersion: 2,\n id: componentBundle.manifest.id,\n name: componentBundle.manifest.name,\n version: componentBundle.manifest.version,\n description: componentBundle.manifest.description,\n manifestPath: componentBundle.manifestPath,\n iconPath: componentBundle.iconPath,\n primaryPanelId: componentBundle.primaryPanelId,\n components: componentBundle.components,\n distribution: this.platformTargetService.resolveDistribution(\n componentBundle.manifest.distribution,\n ),\n security: this.resolvePlatformSecurity(componentBundle.manifest),\n };\n }\n const standaloneBundle = bundle as AppStandaloneManifestBundle;\n const permissions = standaloneBundle.manifest.permissions ?? {};\n return {\n schemaVersion: 1,\n id: standaloneBundle.manifest.id,\n name: standaloneBundle.manifest.name,\n version: standaloneBundle.manifest.version,\n description: standaloneBundle.manifest.description,\n mainKind: standaloneBundle.manifest.main.kind,\n action:\n standaloneBundle.manifest.main.kind === \"wasm\"\n ? standaloneBundle.manifest.main.action\n : undefined,\n manifestPath: standaloneBundle.manifestPath,\n mainEntryPath: standaloneBundle.mainEntryPath,\n uiEntryPath: standaloneBundle.uiEntryPath,\n iconPath: standaloneBundle.iconPath,\n permissions,\n };\n };\n\n private loadStandaloneBundle = async (\n appDirectory: string,\n manifestPath: string,\n manifest: AppStandaloneManifest,\n ): Promise<AppStandaloneManifestBundle> => {\n const mainEntryPath = await this.assertResolvedFile(\n appDirectory,\n manifest.main.entry,\n \"main.entry\",\n );\n const uiEntryPath = await this.assertResolvedFile(\n appDirectory,\n manifest.ui.entry,\n \"ui.entry\",\n );\n const iconPath = manifest.icon\n ? await this.assertResolvedFile(appDirectory, manifest.icon, \"icon\")\n : undefined;\n return {\n appDirectory,\n manifestPath,\n manifest,\n mainEntryPath,\n uiEntryPath,\n uiDirectoryPath: path.dirname(uiEntryPath),\n assetsDirectoryPath: path.join(appDirectory, \"assets\"),\n iconPath,\n };\n };\n\n private loadComponentBundle = async (\n appDirectory: string,\n manifestPath: string,\n manifest: AppComponentManifest,\n ): Promise<AppComponentManifestBundle> => {\n const components: AppResolvedComponent[] = [];\n for (const [index, component] of manifest.components.entries()) {\n const componentDirectory = await this.assertResolvedDirectory(\n appDirectory,\n component.path,\n `components[${index}].path`,\n );\n const { id: componentId } = await this.componentContractService.loadDirectoryComponent({\n component,\n componentDirectory,\n runtimeProfile: manifest.runtime?.profile ?? \"native-process\",\n });\n const expectedPrefix = `${manifest.id.replace(/[^a-z0-9]+/g, \"-\")}-`;\n if (!componentId.startsWith(expectedPrefix)) {\n throw new Error(\n `组件 ${componentId} 必须使用包前缀 ${expectedPrefix}。`,\n );\n }\n components.push({\n ...component,\n id: componentId,\n componentDirectory,\n manifestPath: path.join(\n componentDirectory,\n component.kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\",\n ),\n });\n }\n const duplicateId = components.find(\n (component, index) => components.findIndex((entry) => entry.id === component.id) !== index,\n );\n if (duplicateId) {\n throw new Error(`components 包含重复组件 id:${duplicateId.id}`);\n }\n const panelIds = components\n .filter((component) => component.kind === \"panel\")\n .map((component) => component.id);\n const declaredPrimaryPanel = manifest.presentation?.primaryPanel;\n if (declaredPrimaryPanel && !panelIds.includes(declaredPrimaryPanel)) {\n throw new Error(\"presentation.primaryPanel 必须引用真实 Panel component id。\");\n }\n if (panelIds.length > 1 && !declaredPrimaryPanel) {\n throw new Error(\"包含多个 Panel 时必须声明 presentation.primaryPanel。\");\n }\n if (panelIds.length === 0 && declaredPrimaryPanel) {\n throw new Error(\"不含 Panel 的包不能声明 presentation.primaryPanel。\");\n }\n const iconPath = manifest.icon\n ? await this.assertResolvedFile(appDirectory, manifest.icon, \"icon\")\n : undefined;\n return {\n appDirectory,\n manifestPath,\n manifest,\n components,\n assetsDirectoryPath: path.join(appDirectory, \"assets\"),\n iconPath,\n primaryPanelId: declaredPrimaryPanel ?? panelIds[0],\n };\n };\n\n private parseManifest = (rawManifest: unknown): AppManifest => {\n const candidate = this.assertObject(rawManifest, \"manifest.json\");\n const schemaVersion = this.readNumber(candidate.schemaVersion, \"schemaVersion\");\n if (schemaVersion !== 1 && schemaVersion !== 2) {\n throw new Error(\"当前只支持 schemaVersion = 1 或 2。\");\n }\n const common = this.parseCommonManifest(candidate);\n if (schemaVersion === 1) {\n return {\n schemaVersion: 1,\n ...common,\n main: this.parseMain(candidate.main),\n ui: this.parseUi(candidate.ui),\n permissions: this.parsePermissions(candidate.permissions),\n };\n }\n const components = this.parseComponents(candidate.components);\n const runtime = this.parseRuntime(candidate.runtime);\n this.assertRuntimeMatchesComponents(runtime, components);\n const distribution = this.platformTargetService.parseDistribution(candidate.distribution);\n this.componentContractService.assertRuntimeDistribution(runtime, distribution);\n return {\n schemaVersion: 2,\n ...common,\n engines: this.parseEngines(candidate.engines),\n presentation: this.parsePresentation(candidate.presentation),\n runtime,\n distribution,\n storage: this.parseAppStorage(candidate.storage),\n permissions: this.parsePermissions(candidate.permissions),\n components,\n };\n };\n\n resolvePlatformSecurity = (\n manifest: AppComponentManifest,\n ): AppPlatformSecuritySummary => {\n const hasServiceComponents = manifest.components.some(\n (component) => component.kind === \"service\",\n );\n const runtimeProfile = manifest.runtime?.profile ?? (\n hasServiceComponents ? \"native-process\" : \"panel-only\"\n );\n const isolation = runtimeProfile === \"panel-only\"\n ? \"sandboxed\"\n : runtimeProfile === \"wasi\"\n ? \"host-mediated\"\n : \"full-user\";\n const declaredPermissions = manifest.permissions ?? {};\n const permissions: AppPermissions = runtimeProfile === \"native-process\"\n ? {\n ...declaredPermissions,\n storage: declaredPermissions.storage ?? true,\n capabilities: {\n ...declaredPermissions.capabilities,\n nativeProcess: true,\n },\n }\n : declaredPermissions;\n return {\n runtimeProfile,\n isolation,\n hasServiceComponents,\n inferred: manifest.runtime === undefined,\n permissions,\n };\n };\n\n private parseCommonManifest = (candidate: Record<string, unknown>) => {\n const id = this.readRequiredString(candidate.id, \"id\");\n if (!PACKAGE_ID_PATTERN.test(id)) {\n throw new Error(\"id 必须由小写字母、数字、点或连字符组成。\");\n }\n return {\n id,\n name: this.readRequiredString(candidate.name, \"name\"),\n version: this.readRequiredString(candidate.version, \"version\"),\n description: this.readOptionalString(candidate.description, \"description\"),\n icon: this.readOptionalString(candidate.icon, \"icon\"),\n };\n };\n\n private parseMain = (rawMain: unknown): AppStandaloneManifest[\"main\"] => {\n const candidate = this.assertObject(rawMain, \"main\");\n const kind = this.readRequiredString(candidate.kind, \"main.kind\");\n if (kind === \"wasm\") {\n return {\n kind,\n entry: this.readRequiredString(candidate.entry, \"main.entry\"),\n export: this.readRequiredString(candidate.export, \"main.export\"),\n action: this.readRequiredString(candidate.action, \"main.action\"),\n };\n }\n if (kind === \"wasi-http-component\") {\n return {\n kind,\n entry: this.readRequiredString(candidate.entry, \"main.entry\"),\n };\n }\n throw new Error(\"当前 main.kind 只支持 wasm 或 wasi-http-component。\");\n };\n\n private parseUi = (rawUi: unknown): AppStandaloneManifest[\"ui\"] => {\n const candidate = this.assertObject(rawUi, \"ui\");\n return { entry: this.readRequiredString(candidate.entry, \"ui.entry\") };\n };\n\n private parseComponents = (rawComponents: unknown): AppComponentReference[] => {\n if (!Array.isArray(rawComponents) || rawComponents.length === 0) {\n throw new Error(\"components 必须是非空数组。\");\n }\n const components = rawComponents.map((rawComponent, index) => {\n const candidate = this.assertObject(rawComponent, `components[${index}]`);\n const kind = this.readRequiredString(candidate.kind, `components[${index}].kind`);\n if (kind !== \"panel\" && kind !== \"service\") {\n throw new Error(`components[${index}].kind 只支持 panel 或 service。`);\n }\n const normalizedKind: AppComponentReference[\"kind\"] = kind;\n return {\n kind: normalizedKind,\n path: this.normalizeRelativePath(\n this.readRequiredString(candidate.path, `components[${index}].path`),\n `components[${index}].path`,\n ),\n };\n });\n for (let leftIndex = 0; leftIndex < components.length; leftIndex += 1) {\n for (let rightIndex = leftIndex + 1; rightIndex < components.length; rightIndex += 1) {\n const left = components[leftIndex]?.path;\n const right = components[rightIndex]?.path;\n if (left && right && (left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`))) {\n throw new Error(`components path 不能重复或重叠:${left} / ${right}`);\n }\n }\n }\n return components;\n };\n\n private parseEngines = (\n rawEngines: unknown,\n ): AppComponentManifest[\"engines\"] => {\n if (rawEngines === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawEngines, \"engines\");\n return { nextclaw: this.readOptionalString(candidate.nextclaw, \"engines.nextclaw\") };\n };\n\n private parsePresentation = (\n rawPresentation: unknown,\n ): AppComponentManifest[\"presentation\"] => {\n if (rawPresentation === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawPresentation, \"presentation\");\n return {\n primaryPanel: this.readOptionalString(\n candidate.primaryPanel,\n \"presentation.primaryPanel\",\n ),\n };\n };\n\n private parseRuntime = (\n rawRuntime: unknown,\n ): AppComponentManifest[\"runtime\"] => {\n if (rawRuntime === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawRuntime, \"runtime\");\n const profile = this.readRequiredString(candidate.profile, \"runtime.profile\");\n if (profile !== \"panel-only\" && profile !== \"wasi\" && profile !== \"native-process\") {\n throw new Error(\"runtime.profile 只支持 panel-only、wasi 或 native-process。\");\n }\n return { profile };\n };\n\n private parseAppStorage = (\n rawStorage: unknown,\n ): AppComponentManifest[\"storage\"] => {\n if (rawStorage === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawStorage, \"storage\");\n const scope = this.readRequiredString(candidate.scope, \"storage.scope\");\n if (scope !== \"global\") {\n throw new Error(\"当前 storage.scope 只支持 global。\");\n }\n const schemaVersion = this.readNumber(candidate.schemaVersion, \"storage.schemaVersion\");\n if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) {\n throw new Error(\"storage.schemaVersion 必须是正整数。\");\n }\n return { scope, schemaVersion };\n };\n\n private assertRuntimeMatchesComponents = (\n runtime: AppComponentManifest[\"runtime\"],\n components: AppComponentReference[],\n ): void => {\n if (!runtime) {\n return;\n }\n const hasService = components.some((component) => component.kind === \"service\");\n if (runtime.profile === \"panel-only\" && hasService) {\n throw new Error(\"runtime.profile=panel-only 不能包含 Service component。\");\n }\n if (runtime.profile === \"wasi\") {\n if (!hasService) {\n throw new Error(\"wasi runtime 必须包含 Service component。\");\n }\n return;\n }\n if (runtime.profile !== \"panel-only\" && !hasService) {\n throw new Error(`${runtime.profile} runtime 必须包含 Service component。`);\n }\n };\n\n private parsePermissions = (rawPermissions: unknown): AppPermissions | undefined => {\n if (rawPermissions === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawPermissions, \"permissions\");\n return {\n documentAccess: this.parseDocumentAccess(candidate.documentAccess),\n secrets: this.parseSecretSlots(candidate.secrets),\n allowedDomains: this.parseStringArray(candidate.allowedDomains, \"permissions.allowedDomains\"),\n storage: this.parseStorage(candidate.storage),\n capabilities: this.parseCapabilities(candidate.capabilities),\n };\n };\n\n private parseDocumentAccess = (rawDocumentAccess: unknown): AppDocumentAccessScope[] | undefined => {\n if (rawDocumentAccess === undefined) {\n return undefined;\n }\n if (!Array.isArray(rawDocumentAccess)) {\n throw new Error(\"permissions.documentAccess 必须是数组。\");\n }\n return rawDocumentAccess.map((scope, index) => {\n const candidate = this.assertObject(scope, `permissions.documentAccess[${index}]`);\n const mode = this.readRequiredString(candidate.mode, `permissions.documentAccess[${index}].mode`);\n if (mode !== \"read\" && mode !== \"read-write\") {\n throw new Error(`permissions.documentAccess[${index}].mode 只支持 read 或 read-write。`);\n }\n return {\n id: this.readRequiredString(candidate.id, `permissions.documentAccess[${index}].id`),\n mode,\n description: this.readOptionalString(\n candidate.description,\n `permissions.documentAccess[${index}].description`,\n ),\n };\n });\n };\n\n private parseSecretSlots = (rawSecrets: unknown): AppSecretSlot[] | undefined => {\n if (rawSecrets === undefined) {\n return undefined;\n }\n if (!Array.isArray(rawSecrets)) {\n throw new Error(\"permissions.secrets 必须是数组。\");\n }\n const slots = rawSecrets.map((rawSlot, index) => {\n const field = `permissions.secrets[${index}]`;\n const candidate = this.assertObject(rawSlot, field);\n const unsupportedField = Object.keys(candidate).find(\n (key) => ![\"id\", \"title\", \"description\", \"required\"].includes(key),\n );\n if (unsupportedField) {\n throw new Error(`${field} 只能声明 id、title、description 和 required。`);\n }\n const id = this.readRequiredString(candidate.id, `${field}.id`);\n if (!SECRET_SLOT_ID_PATTERN.test(id)) {\n throw new Error(`${field}.id 必须是稳定的 lowercase slot id。`);\n }\n return {\n id,\n title: this.readRequiredString(candidate.title, `${field}.title`),\n description: this.readRequiredString(candidate.description, `${field}.description`),\n required: this.readBoolean(candidate.required, `${field}.required`),\n };\n });\n const duplicate = slots.find(\n (slot, index) => slots.findIndex((entry) => entry.id === slot.id) !== index,\n );\n if (duplicate) {\n throw new Error(`permissions.secrets 包含重复 slot id:${duplicate.id}`);\n }\n return slots;\n };\n\n private parseStringArray = (rawValue: unknown, fieldName: string): string[] | undefined => {\n if (rawValue === undefined) {\n return undefined;\n }\n if (!Array.isArray(rawValue)) {\n throw new Error(`${fieldName} 必须是字符串数组。`);\n }\n return rawValue.map((item, index) => this.readRequiredString(item, `${fieldName}[${index}]`));\n };\n\n private parseStorage = (rawStorage: unknown): AppPermissions[\"storage\"] | undefined => {\n if (rawStorage === undefined || typeof rawStorage === \"boolean\") {\n return rawStorage;\n }\n const candidate = this.assertObject(rawStorage, \"permissions.storage\");\n return { namespace: this.readOptionalString(candidate.namespace, \"permissions.storage.namespace\") };\n };\n\n private parseCapabilities = (rawCapabilities: unknown): AppPermissions[\"capabilities\"] | undefined => {\n if (rawCapabilities === undefined) {\n return undefined;\n }\n const candidate = this.assertObject(rawCapabilities, \"permissions.capabilities\");\n return {\n hostBridge: candidate.hostBridge === undefined\n ? undefined\n : this.readBoolean(candidate.hostBridge, \"permissions.capabilities.hostBridge\"),\n nativeProcess: candidate.nativeProcess === undefined\n ? undefined\n : this.readBoolean(candidate.nativeProcess, \"permissions.capabilities.nativeProcess\"),\n };\n };\n\n private assertResolvedFile = async (\n appDirectory: string,\n relativePath: string,\n fieldName: string,\n ): Promise<string> => {\n const resolvedPath = this.resolveInside(appDirectory, relativePath, fieldName);\n const stats = await lstat(resolvedPath);\n if (!stats.isFile() || stats.isSymbolicLink()) {\n throw new Error(`${fieldName} 必须指向包内普通文件。`);\n }\n return resolvedPath;\n };\n\n private assertResolvedDirectory = async (\n appDirectory: string,\n relativePath: string,\n fieldName: string,\n ): Promise<string> => {\n const resolvedPath = this.resolveInside(appDirectory, relativePath, fieldName);\n const stats = await lstat(resolvedPath);\n if (!stats.isDirectory() || stats.isSymbolicLink()) {\n throw new Error(`${fieldName} 必须指向包内普通目录。`);\n }\n await access(resolvedPath);\n return resolvedPath;\n };\n\n private resolveInside = (appDirectory: string, relativePath: string, fieldName: string): string => {\n const normalizedPath = this.normalizeRelativePath(relativePath, fieldName);\n const resolvedPath = path.resolve(appDirectory, normalizedPath);\n const relative = path.relative(appDirectory, resolvedPath);\n if (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n throw new Error(`${fieldName} 不能指向应用目录之外。`);\n }\n return resolvedPath;\n };\n\n private normalizeRelativePath = (relativePath: string, fieldName: string): string => {\n if (path.isAbsolute(relativePath) || relativePath.includes(\"\\0\")) {\n throw new Error(`${fieldName} 必须使用包内相对路径。`);\n }\n const segments = relativePath.replace(/\\\\/g, \"/\").split(\"/\");\n if (segments.some((segment) => !segment || segment === \".\" || segment === \"..\")) {\n throw new Error(`${fieldName} 包含非法路径片段。`);\n }\n return segments.join(\"/\");\n };\n\n private assertObject = (value: unknown, fieldName: string): Record<string, unknown> => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(`${fieldName} 必须是对象。`);\n }\n return value as Record<string, unknown>;\n };\n\n private readRequiredString = (value: unknown, fieldName: string): string => {\n if (typeof value !== \"string\" || !value.trim()) {\n throw new Error(`${fieldName} 必须是非空字符串。`);\n }\n return value.trim();\n };\n\n private readOptionalString = (value: unknown, fieldName: string): string | undefined => {\n return value === undefined ? undefined : this.readRequiredString(value, fieldName);\n };\n\n private readNumber = (value: unknown, fieldName: string): number => {\n if (typeof value !== \"number\" || Number.isNaN(value)) {\n throw new Error(`${fieldName} 必须是数字。`);\n }\n return value;\n };\n\n private readBoolean = (value: unknown, fieldName: string): boolean => {\n if (typeof value !== \"boolean\") {\n throw new Error(`${fieldName} 必须是布尔值。`);\n }\n return value;\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-rust-wasi-scaffold-template.service.d.ts","names":[],"sources":["../../src/services/app-rust-wasi-scaffold-template.service.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"app-rust-wasi-scaffold-template.service.d.ts","names":[],"sources":["../../src/services/app-rust-wasi-scaffold-template.service.ts"],"mappings":";;;cAuBa,kCAAA;EACX,UAAA,GAAc,MAAA;IAAU,KAAA;IAAe,OAAA;EAAA,MAAoB,eAAA;EAAA,QA4CnD,YAAA;EAAA,QAKA,aAAA;EAAA,QAwBA,kBAAA;EAAA,QASA,oBAAA;EAAA,QAoBA,wBAAA;EAAA,QAkBA,cAAA;EAAA,QA6BA,eAAA;EAAA,QAuFA,WAAA;EAAA,QAwCA,cAAA;EAAA,QA6BA,gBAAA;EAAA,QA4BA,wBAAA;EAAA,QAmBA,aAAA;EAAA,QAMA,YAAA;AAAA"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
//#region src/services/app-rust-wasi-scaffold-template.service.ts
|
|
3
3
|
const RUST_WASI_GUEST_CRATE_NAME = "nextclaw-rust-wasi-guest";
|
|
4
|
-
const
|
|
5
|
-
const STANDARD_PORTABLE_WIT_DEPS = [
|
|
4
|
+
const STANDARD_PORTABLE_WIT_FILES = [
|
|
6
5
|
"deps/http@0.2.6/package.wit",
|
|
7
6
|
"deps/http@0.2.6/handler.wit",
|
|
8
7
|
"deps/http@0.2.6/types.wit",
|
|
@@ -16,13 +15,10 @@ const STANDARD_PORTABLE_WIT_DEPS = [
|
|
|
16
15
|
"deps/clocks@0.2.6/world.wit",
|
|
17
16
|
"deps/config@0.2.0-draft-2024-09-27/package.wit",
|
|
18
17
|
"deps/config@0.2.0-draft-2024-09-27/store.wit",
|
|
18
|
+
"deps/keyvalue@0.2.0-draft2/store.wit",
|
|
19
19
|
"deps/spin@2.0.0/package.wit",
|
|
20
20
|
"deps/spin@2.0.0/sqlite.wit"
|
|
21
|
-
]
|
|
22
|
-
relativePath: `guest/wit/${relativePath}`,
|
|
23
|
-
content: readFileSync(new URL(`../../resources/wit/${relativePath}`, import.meta.url), "utf8")
|
|
24
|
-
}));
|
|
25
|
-
const RUST_WASI_CARGO_LOCK = readFileSync(new URL("../../resources/rust-wasi/Cargo.lock", import.meta.url), "utf8");
|
|
21
|
+
];
|
|
26
22
|
var AppRustWasiScaffoldTemplateService = class {
|
|
27
23
|
buildFiles = (params) => {
|
|
28
24
|
const { appId, appName } = params;
|
|
@@ -64,7 +60,7 @@ var AppRustWasiScaffoldTemplateService = class {
|
|
|
64
60
|
},
|
|
65
61
|
{
|
|
66
62
|
relativePath: "guest/Cargo.lock",
|
|
67
|
-
content:
|
|
63
|
+
content: this.readResource("rust-wasi/Cargo.lock")
|
|
68
64
|
},
|
|
69
65
|
{
|
|
70
66
|
relativePath: "guest/src/lib.rs",
|
|
@@ -72,9 +68,12 @@ var AppRustWasiScaffoldTemplateService = class {
|
|
|
72
68
|
},
|
|
73
69
|
{
|
|
74
70
|
relativePath: "guest/wit/portable-service.wit",
|
|
75
|
-
content:
|
|
71
|
+
content: this.readResource("wit/portable-service.wit")
|
|
76
72
|
},
|
|
77
|
-
...
|
|
73
|
+
...STANDARD_PORTABLE_WIT_FILES.map((relativePath) => ({
|
|
74
|
+
relativePath: `guest/wit/${relativePath}`,
|
|
75
|
+
content: this.readResource(`wit/${relativePath}`)
|
|
76
|
+
})),
|
|
78
77
|
{
|
|
79
78
|
relativePath: "tests/service-smoke.json",
|
|
80
79
|
content: `${JSON.stringify(this.buildServiceSmokeFixture(serviceId), null, 2)}\n`
|
|
@@ -85,6 +84,7 @@ var AppRustWasiScaffoldTemplateService = class {
|
|
|
85
84
|
}
|
|
86
85
|
];
|
|
87
86
|
};
|
|
87
|
+
readResource = (relativePath) => readFileSync(new URL(`../../resources/${relativePath}`, import.meta.url), "utf8");
|
|
88
88
|
buildManifest = (appId, appName, panelId, serviceId) => ({
|
|
89
89
|
schemaVersion: 2,
|
|
90
90
|
id: appId,
|
|
@@ -120,7 +120,7 @@ var AppRustWasiScaffoldTemplateService = class {
|
|
|
120
120
|
buildServiceManifest = (serviceId) => ({
|
|
121
121
|
id: serviceId,
|
|
122
122
|
title: "Rust/WASI 持久计数组件",
|
|
123
|
-
description: "
|
|
123
|
+
description: "通过标准 WASI key-value 读取和增加持久计数。",
|
|
124
124
|
protocol: "wasi-component",
|
|
125
125
|
component: { entry: "service.wasm" },
|
|
126
126
|
actions: {
|
|
@@ -152,14 +152,14 @@ var AppRustWasiScaffoldTemplateService = class {
|
|
|
152
152
|
input: { step: 3 },
|
|
153
153
|
expect: {
|
|
154
154
|
counter: 3,
|
|
155
|
-
persistedBy: "
|
|
155
|
+
persistedBy: "wasi:keyvalue/store"
|
|
156
156
|
}
|
|
157
157
|
}, {
|
|
158
158
|
action: "counter_read",
|
|
159
159
|
input: {},
|
|
160
160
|
expect: {
|
|
161
161
|
counter: 3,
|
|
162
|
-
persistedBy: "
|
|
162
|
+
persistedBy: "wasi:keyvalue/store"
|
|
163
163
|
}
|
|
164
164
|
}]
|
|
165
165
|
});
|
|
@@ -188,6 +188,7 @@ world = "service-app"
|
|
|
188
188
|
"wasi:http" = { path = "wit/deps/http@0.2.6" }
|
|
189
189
|
"wasi:io" = { path = "wit/deps/io@0.2.6" }
|
|
190
190
|
"wasi:clocks" = { path = "wit/deps/clocks@0.2.6" }
|
|
191
|
+
"wasi:keyvalue" = { path = "wit/deps/keyvalue@0.2.0-draft2" }
|
|
191
192
|
"wasi:config" = { path = "wit/deps/config@0.2.0-draft-2024-09-27" }
|
|
192
193
|
`;
|
|
193
194
|
buildRustSource = () => `wit_bindgen::generate!({
|
|
@@ -200,6 +201,9 @@ world = "service-app"
|
|
|
200
201
|
use exports::nextclaw::portable_service::service::{Action, Guest};
|
|
201
202
|
use nextclaw::portable_service::host;
|
|
202
203
|
use serde_json::{Value, json};
|
|
204
|
+
use wasi::keyvalue::store as keyvalue_store;
|
|
205
|
+
|
|
206
|
+
const STORE_NAME: &str = "default";
|
|
203
207
|
|
|
204
208
|
struct Component;
|
|
205
209
|
|
|
@@ -209,12 +213,12 @@ impl Guest for Component {
|
|
|
209
213
|
Action {
|
|
210
214
|
name: "counter_read".into(),
|
|
211
215
|
title: "读取持久计数".into(),
|
|
212
|
-
description: "
|
|
216
|
+
description: "从标准 WASI key-value 存储读取计数。".into(),
|
|
213
217
|
},
|
|
214
218
|
Action {
|
|
215
219
|
name: "counter_increment".into(),
|
|
216
220
|
title: "增加持久计数".into(),
|
|
217
|
-
description: "在 Rust/WASM
|
|
221
|
+
description: "在 Rust/WASM 中计算,并通过标准 WASI key-value 持久化。".into(),
|
|
218
222
|
},
|
|
219
223
|
]
|
|
220
224
|
}
|
|
@@ -230,7 +234,10 @@ impl Guest for Component {
|
|
|
230
234
|
return Err("INVALID_INPUT: step must be between 1 and 100".into());
|
|
231
235
|
}
|
|
232
236
|
let counter = read_counter()?.saturating_add(step);
|
|
233
|
-
|
|
237
|
+
let bucket = keyvalue_store::open(STORE_NAME).map_err(format_keyvalue_error)?;
|
|
238
|
+
bucket
|
|
239
|
+
.set("counter", counter.to_string().as_bytes())
|
|
240
|
+
.map_err(format_keyvalue_error)?;
|
|
234
241
|
Ok(counter_result(counter))
|
|
235
242
|
}
|
|
236
243
|
_ => Err(format!("UNKNOWN_ACTION: {action}")),
|
|
@@ -251,20 +258,28 @@ impl Guest for Component {
|
|
|
251
258
|
}
|
|
252
259
|
|
|
253
260
|
fn read_counter() -> Result<i64, String> {
|
|
254
|
-
|
|
261
|
+
let bucket = keyvalue_store::open(STORE_NAME).map_err(format_keyvalue_error)?;
|
|
262
|
+
Ok(bucket
|
|
263
|
+
.get("counter")
|
|
264
|
+
.map_err(format_keyvalue_error)?
|
|
265
|
+
.and_then(|value| String::from_utf8(value).ok())
|
|
255
266
|
.and_then(|value| value.parse().ok())
|
|
256
267
|
.unwrap_or(0))
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
fn counter_result(counter: i64) -> String {
|
|
260
|
-
json!({ "counter": counter, "persistedBy": "
|
|
271
|
+
json!({ "counter": counter, "persistedBy": "wasi:keyvalue/store" }).to_string()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn format_keyvalue_error(_error: keyvalue_store::Error) -> String {
|
|
275
|
+
"WASI_KEYVALUE_ERROR: application storage is unavailable".into()
|
|
261
276
|
}
|
|
262
277
|
|
|
263
278
|
export!(Component with_types_in self);
|
|
264
279
|
`;
|
|
265
280
|
buildReadme = (appName, serviceId) => `# ${appName}
|
|
266
281
|
|
|
267
|
-
这是一个可以独立构建的 Rust/WASI Component App。Panel 和 Agent 调用同一组 Service Action
|
|
282
|
+
这是一个可以独立构建的 Rust/WASI Component App。Panel 和 Agent 调用同一组 Service Action,计数通过标准 WASI key-value 持久保存。
|
|
268
283
|
|
|
269
284
|
## 1. 准备 Rust 工具链
|
|
270
285
|
|
|
@@ -320,7 +335,7 @@ nextclaw app install ./${this.normalizeSlug(appName)}.napp
|
|
|
320
335
|
<body>
|
|
321
336
|
<main>
|
|
322
337
|
<h1>${appName}</h1>
|
|
323
|
-
<p>这个数字由 Rust/WASI Component
|
|
338
|
+
<p>这个数字由 Rust/WASI Component 计算,并通过标准 WASI key-value 持久保存。</p>
|
|
324
339
|
<output id="counter">…</output>
|
|
325
340
|
<button id="increment" type="button">增加 1</button>
|
|
326
341
|
<p id="error" role="alert"></p>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-rust-wasi-scaffold-template.service.js","names":[],"sources":["../../src/services/app-rust-wasi-scaffold-template.service.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport type { AppScaffoldFile } from \"./app-ts-http-scaffold-template.service.js\";\n\nconst RUST_WASI_GUEST_CRATE_NAME = \"nextclaw-rust-wasi-guest\";\nconst PORTABLE_SERVICE_WIT = readFileSync(\n new URL(\"../../resources/wit/portable-service.wit\", import.meta.url),\n \"utf8\",\n);\nconst STANDARD_PORTABLE_WIT_FILES = [\n \"deps/http@0.2.6/package.wit\",\n \"deps/http@0.2.6/handler.wit\",\n \"deps/http@0.2.6/types.wit\",\n \"deps/io@0.2.6/error.wit\",\n \"deps/io@0.2.6/poll.wit\",\n \"deps/io@0.2.6/streams.wit\",\n \"deps/io@0.2.6/world.wit\",\n \"deps/clocks@0.2.6/monotonic-clock.wit\",\n \"deps/clocks@0.2.6/timezone.wit\",\n \"deps/clocks@0.2.6/wall-clock.wit\",\n \"deps/clocks@0.2.6/world.wit\",\n \"deps/config@0.2.0-draft-2024-09-27/package.wit\",\n \"deps/config@0.2.0-draft-2024-09-27/store.wit\",\n \"deps/spin@2.0.0/package.wit\",\n \"deps/spin@2.0.0/sqlite.wit\",\n] as const;\nconst STANDARD_PORTABLE_WIT_DEPS: AppScaffoldFile[] = STANDARD_PORTABLE_WIT_FILES.map((relativePath) => ({\n relativePath: `guest/wit/${relativePath}`,\n content: readFileSync(new URL(`../../resources/wit/${relativePath}`, import.meta.url), \"utf8\"),\n}));\nconst RUST_WASI_CARGO_LOCK = readFileSync(\n new URL(\"../../resources/rust-wasi/Cargo.lock\", import.meta.url),\n \"utf8\",\n);\n\nexport class AppRustWasiScaffoldTemplateService {\n buildFiles = (params: { appId: string; appName: string }): AppScaffoldFile[] => {\n const { appId, appName } = params;\n const packageSlug = appId.replace(/[^a-z0-9]+/g, \"-\");\n const panelId = `${packageSlug}-panel`;\n const serviceId = `${packageSlug}-service`;\n return [\n {\n relativePath: \"manifest.json\",\n content: `${JSON.stringify(this.buildManifest(appId, appName, panelId, serviceId), null, 2)}\\n`,\n },\n {\n relativePath: \"marketplace.json\",\n content: `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\\n`,\n },\n { relativePath: \"README.md\", content: this.buildReadme(appName, serviceId) },\n {\n relativePath: `panels/${panelId}.panel/panel-app.json`,\n content: `${JSON.stringify(this.buildPanelManifest(panelId, appName, serviceId), null, 2)}\\n`,\n },\n { relativePath: `panels/${panelId}.panel/index.html`, content: this.buildPanelHtml(appName) },\n { relativePath: `panels/${panelId}.panel/app.js`, content: this.buildPanelScript(serviceId) },\n {\n relativePath: `service-components/${serviceId}/service-app.json`,\n content: `${JSON.stringify(this.buildServiceManifest(serviceId), null, 2)}\\n`,\n },\n { relativePath: \"guest/Cargo.toml\", content: this.buildCargoToml() },\n { relativePath: \"guest/Cargo.lock\", content: RUST_WASI_CARGO_LOCK },\n { relativePath: \"guest/src/lib.rs\", content: this.buildRustSource() },\n { relativePath: \"guest/wit/portable-service.wit\", content: PORTABLE_SERVICE_WIT },\n ...STANDARD_PORTABLE_WIT_DEPS,\n {\n relativePath: \"tests/service-smoke.json\",\n content: `${JSON.stringify(this.buildServiceSmokeFixture(serviceId), null, 2)}\\n`,\n },\n { relativePath: \"assets/icon.svg\", content: this.buildIconSvg() },\n ];\n };\n\n private buildManifest = (\n appId: string,\n appName: string,\n panelId: string,\n serviceId: string,\n ) => ({\n schemaVersion: 2,\n id: appId,\n name: appName,\n version: \"0.1.0\",\n description: `${appName},由 Rust/WASI Component 保存持久计数。`,\n icon: \"assets/icon.svg\",\n engines: { nextclaw: \">=0.45.4\" },\n presentation: { primaryPanel: panelId },\n runtime: { profile: \"wasi\" },\n distribution: { mode: \"universal\" },\n storage: { scope: \"global\", schemaVersion: 1 },\n permissions: { storage: { namespace: appId.replace(/\\./g, \"-\") } },\n components: [\n { kind: \"panel\", path: `panels/${panelId}.panel` },\n { kind: \"service\", path: `service-components/${serviceId}` },\n ],\n });\n\n private buildPanelManifest = (panelId: string, appName: string, serviceId: string) => ({\n id: panelId,\n title: appName,\n description: \"读取并增加由 Rust/WASI Component 持久保存的计数。\",\n icon: \"🦀\",\n entry: \"index.html\",\n actions: [`${serviceId}.counter_read`, `${serviceId}.counter_increment`],\n });\n\n private buildServiceManifest = (serviceId: string) => ({\n id: serviceId,\n title: \"Rust/WASI 持久计数组件\",\n description: \"通过宿主 KV 读取和增加持久计数。\",\n protocol: \"wasi-component\",\n component: { entry: \"service.wasm\" },\n actions: {\n counter_read: { risk: \"read\", title: \"读取持久计数\" },\n counter_increment: {\n risk: \"write\",\n title: \"增加持久计数\",\n inputSchema: {\n type: \"object\",\n properties: { step: { type: \"integer\", minimum: 1, maximum: 100 } },\n additionalProperties: false,\n },\n },\n },\n });\n\n private buildServiceSmokeFixture = (serviceId: string) => ({\n schemaVersion: 1,\n component: serviceId,\n resetData: true,\n steps: [\n {\n action: \"counter_increment\",\n input: { step: 3 },\n expect: { counter: 3, persistedBy: \"host.kv\" },\n },\n {\n action: \"counter_read\",\n input: {},\n expect: { counter: 3, persistedBy: \"host.kv\" },\n },\n ],\n });\n\n private buildCargoToml = (): string => `[package]\nname = \"${RUST_WASI_GUEST_CRATE_NAME}\"\nversion = \"0.1.0\"\nedition = \"2024\"\npublish = false\n\n[dependencies]\nserde_json = \"1.0\"\nwit-bindgen = \"0.44.0\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[package.metadata.component]\npackage = \"nextclaw:portable-service\"\n\n[package.metadata.component.target]\npath = \"wit\"\nworld = \"service-app\"\n\n[package.metadata.component.target.dependencies]\n\"fermyon:spin\" = { path = \"wit/deps/spin@2.0.0\" }\n\"wasi:http\" = { path = \"wit/deps/http@0.2.6\" }\n\"wasi:io\" = { path = \"wit/deps/io@0.2.6\" }\n\"wasi:clocks\" = { path = \"wit/deps/clocks@0.2.6\" }\n\"wasi:config\" = { path = \"wit/deps/config@0.2.0-draft-2024-09-27\" }\n`;\n\n private buildRustSource = (): string => `wit_bindgen::generate!({\n path: \"wit\",\n world: \"service-app\",\n // Generate standard WASI HTTP's transitive interfaces for later use by a Guest.\n generate_all,\n});\n\nuse exports::nextclaw::portable_service::service::{Action, Guest};\nuse nextclaw::portable_service::host;\nuse serde_json::{Value, json};\n\nstruct Component;\n\nimpl Guest for Component {\n fn list_actions() -> Vec<Action> {\n vec![\n Action {\n name: \"counter_read\".into(),\n title: \"读取持久计数\".into(),\n description: \"从宿主管理的 KV 存储读取计数。\".into(),\n },\n Action {\n name: \"counter_increment\".into(),\n title: \"增加持久计数\".into(),\n description: \"在 Rust/WASM 中计算,并通过宿主 KV 持久化。\".into(),\n },\n ]\n }\n\n fn invoke(action: String, input_json: String) -> Result<String, String> {\n host::log(host::LogLevel::Info, &format!(\"invoking {action}\"));\n let input: Value = serde_json::from_str(&input_json).unwrap_or_else(|_| json!({}));\n match action.as_str() {\n \"counter_read\" => Ok(counter_result(read_counter()?)),\n \"counter_increment\" => {\n let step = input.get(\"step\").and_then(Value::as_i64).unwrap_or(1);\n if !(1..=100).contains(&step) {\n return Err(\"INVALID_INPUT: step must be between 1 and 100\".into());\n }\n let counter = read_counter()?.saturating_add(step);\n host::kv_set(\"counter\", &counter.to_string())?;\n Ok(counter_result(counter))\n }\n _ => Err(format!(\"UNKNOWN_ACTION: {action}\")),\n }\n }\n\n fn start(_config_json: String) -> Result<String, String> {\n Ok(json!({ \"started\": true, \"mode\": \"action\" }).to_string())\n }\n\n fn handle_event(_event_json: String) -> Result<String, String> {\n Err(\"UNSUPPORTED_LIFECYCLE: action component does not accept resident events\".into())\n }\n\n fn stop(_reason_json: String) -> Result<String, String> {\n Ok(json!({ \"stopped\": true, \"mode\": \"action\" }).to_string())\n }\n}\n\nfn read_counter() -> Result<i64, String> {\n Ok(host::kv_get(\"counter\")?\n .and_then(|value| value.parse().ok())\n .unwrap_or(0))\n}\n\nfn counter_result(counter: i64) -> String {\n json!({ \"counter\": counter, \"persistedBy\": \"host.kv\" }).to_string()\n}\n\nexport!(Component with_types_in self);\n`;\n\n private buildReadme = (appName: string, serviceId: string): string => `# ${appName}\n\n这是一个可以独立构建的 Rust/WASI Component App。Panel 和 Agent 调用同一组 Service Action,计数通过 NextClaw 宿主 KV 持久保存。\n\n## 1. 准备 Rust 工具链\n\n\\`\\`\\`bash\nrustup target add wasm32-wasip2\n\\`\\`\\`\n\n## 2. 构建 Component\n\n\\`\\`\\`bash\ncd guest\ncargo build --release --target wasm32-wasip2\ncp target/wasm32-wasip2/release/nextclaw_rust_wasi_guest.wasm ../service-components/${serviceId}/service.wasm\ncd ..\n\\`\\`\\`\n\n项目内的 \\`guest/wit/portable-service.wit\\` 是当前 Service Action 与宿主能力合同;不需要 NextClaw 源码仓库。\n\n## 3. 检查和调试\n\n\\`\\`\\`bash\nnextclaw app check .\nnextclaw app dev .\nnextclaw app call . counter_read\nnextclaw app call . counter_increment --input '{\"step\":2}'\n\\`\\`\\`\n\n如果一个包包含多个 Service Component,请增加 \\`--component <component-id>\\`。\n\n## 4. 打包和安装\n\n\\`\\`\\`bash\nnextclaw app pack . --target universal --out ${this.normalizeSlug(appName)}.napp\nnextclaw app install ./${this.normalizeSlug(appName)}.napp\n\\`\\`\\`\n`;\n\n private buildPanelHtml = (appName: string): string => `<!doctype html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${appName}</title>\n <style>\n :root { font-family: ui-sans-serif, system-ui, sans-serif; color: #172033; background: #f5f7fb; }\n body { margin: 0; min-height: 100vh; display: grid; place-items: center; }\n main { width: min(420px, calc(100vw - 40px)); padding: 28px; border-radius: 24px; background: white; box-shadow: 0 18px 60px #24324a1f; }\n p { color: #667085; }\n output { display: block; margin: 24px 0; font-size: 64px; font-weight: 700; }\n button { border: 0; border-radius: 999px; padding: 12px 18px; background: #172033; color: white; cursor: pointer; }\n #error { color: #b42318; }\n </style>\n </head>\n <body>\n <main>\n <h1>${appName}</h1>\n <p>这个数字由 Rust/WASI Component 计算,并通过宿主 KV 持久保存。</p>\n <output id=\"counter\">…</output>\n <button id=\"increment\" type=\"button\">增加 1</button>\n <p id=\"error\" role=\"alert\"></p>\n </main>\n <script src=\"app.js\"></script>\n </body>\n</html>\n`;\n\n private buildPanelScript = (serviceId: string): string => `const counter = document.querySelector(\"#counter\");\nconst error = document.querySelector(\"#error\");\nconst increment = document.querySelector(\"#increment\");\n\nasync function readCounter() {\n error.textContent = \"\";\n try {\n const result = await window.nextclaw.serviceActions.invoke(\"${serviceId}.counter_read\", {});\n counter.textContent = String(result.counter ?? 0);\n } catch (cause) {\n error.textContent = cause instanceof Error ? cause.message : String(cause);\n }\n}\n\nasync function incrementCounter() {\n error.textContent = \"\";\n try {\n const result = await window.nextclaw.serviceActions.invoke(\"${serviceId}.counter_increment\", { step: 1 });\n counter.textContent = String(result.counter ?? 0);\n } catch (cause) {\n error.textContent = cause instanceof Error ? cause.message : String(cause);\n }\n}\n\nincrement.addEventListener(\"click\", incrementCounter);\nvoid readCounter();\n`;\n\n private buildMarketplaceMetadata = (appName: string) => ({\n slug: this.normalizeSlug(appName),\n summary: `${appName} Rust/WASI Component 示例。`,\n summaryI18n: {\n zh: `${appName} Rust/WASI Component 示例。`,\n en: `${appName}, a Rust/WASI Component example.`,\n },\n description: \"A minimal Rust/WASI Component App with a Panel and host-managed persistent KV.\",\n descriptionI18n: {\n zh: \"一个包含 Panel 和宿主持久 KV 的最小 Rust/WASI Component App。\",\n en: \"A minimal Rust/WASI Component App with a Panel and host-managed persistent KV.\",\n },\n author: \"NextClaw\",\n tags: [\"starter\", \"rust\", \"wasi-component\", \"official\"],\n sourceRepo: \"https://github.com/Peiiii/nextclaw\",\n homepage: \"https://nextclaw.io\",\n featured: false,\n });\n\n private normalizeSlug = (value: string): string => value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\") || \"rust-wasi-app\";\n\n private buildIconSvg = (): string => `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\">\n <rect width=\"128\" height=\"128\" rx=\"30\" fill=\"#172033\"/>\n <path d=\"M31 40h66v48H31z\" fill=\"#fff\" opacity=\".12\"/>\n <path d=\"M43 52h42v8H43zm0 16h28v8H43z\" fill=\"#fff\"/>\n <circle cx=\"88\" cy=\"76\" r=\"12\" fill=\"#f59e0b\"/>\n</svg>\n`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app-rust-wasi-scaffold-template.service.js","names":[],"sources":["../../src/services/app-rust-wasi-scaffold-template.service.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport type { AppScaffoldFile } from \"./app-ts-http-scaffold-template.service.js\";\n\nconst RUST_WASI_GUEST_CRATE_NAME = \"nextclaw-rust-wasi-guest\";\nconst STANDARD_PORTABLE_WIT_FILES = [\n \"deps/http@0.2.6/package.wit\",\n \"deps/http@0.2.6/handler.wit\",\n \"deps/http@0.2.6/types.wit\",\n \"deps/io@0.2.6/error.wit\",\n \"deps/io@0.2.6/poll.wit\",\n \"deps/io@0.2.6/streams.wit\",\n \"deps/io@0.2.6/world.wit\",\n \"deps/clocks@0.2.6/monotonic-clock.wit\",\n \"deps/clocks@0.2.6/timezone.wit\",\n \"deps/clocks@0.2.6/wall-clock.wit\",\n \"deps/clocks@0.2.6/world.wit\",\n \"deps/config@0.2.0-draft-2024-09-27/package.wit\",\n \"deps/config@0.2.0-draft-2024-09-27/store.wit\",\n \"deps/keyvalue@0.2.0-draft2/store.wit\",\n \"deps/spin@2.0.0/package.wit\",\n \"deps/spin@2.0.0/sqlite.wit\",\n] as const;\n\nexport class AppRustWasiScaffoldTemplateService {\n buildFiles = (params: { appId: string; appName: string }): AppScaffoldFile[] => {\n const { appId, appName } = params;\n const packageSlug = appId.replace(/[^a-z0-9]+/g, \"-\");\n const panelId = `${packageSlug}-panel`;\n const serviceId = `${packageSlug}-service`;\n return [\n {\n relativePath: \"manifest.json\",\n content: `${JSON.stringify(this.buildManifest(appId, appName, panelId, serviceId), null, 2)}\\n`,\n },\n {\n relativePath: \"marketplace.json\",\n content: `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\\n`,\n },\n { relativePath: \"README.md\", content: this.buildReadme(appName, serviceId) },\n {\n relativePath: `panels/${panelId}.panel/panel-app.json`,\n content: `${JSON.stringify(this.buildPanelManifest(panelId, appName, serviceId), null, 2)}\\n`,\n },\n { relativePath: `panels/${panelId}.panel/index.html`, content: this.buildPanelHtml(appName) },\n { relativePath: `panels/${panelId}.panel/app.js`, content: this.buildPanelScript(serviceId) },\n {\n relativePath: `service-components/${serviceId}/service-app.json`,\n content: `${JSON.stringify(this.buildServiceManifest(serviceId), null, 2)}\\n`,\n },\n { relativePath: \"guest/Cargo.toml\", content: this.buildCargoToml() },\n { relativePath: \"guest/Cargo.lock\", content: this.readResource(\"rust-wasi/Cargo.lock\") },\n { relativePath: \"guest/src/lib.rs\", content: this.buildRustSource() },\n {\n relativePath: \"guest/wit/portable-service.wit\",\n content: this.readResource(\"wit/portable-service.wit\"),\n },\n ...STANDARD_PORTABLE_WIT_FILES.map((relativePath) => ({\n relativePath: `guest/wit/${relativePath}`,\n content: this.readResource(`wit/${relativePath}`),\n })),\n {\n relativePath: \"tests/service-smoke.json\",\n content: `${JSON.stringify(this.buildServiceSmokeFixture(serviceId), null, 2)}\\n`,\n },\n { relativePath: \"assets/icon.svg\", content: this.buildIconSvg() },\n ];\n };\n\n private readResource = (relativePath: string): string => readFileSync(\n new URL(`../../resources/${relativePath}`, import.meta.url),\n \"utf8\",\n );\n\n private buildManifest = (\n appId: string,\n appName: string,\n panelId: string,\n serviceId: string,\n ) => ({\n schemaVersion: 2,\n id: appId,\n name: appName,\n version: \"0.1.0\",\n description: `${appName},由 Rust/WASI Component 保存持久计数。`,\n icon: \"assets/icon.svg\",\n engines: { nextclaw: \">=0.45.4\" },\n presentation: { primaryPanel: panelId },\n runtime: { profile: \"wasi\" },\n distribution: { mode: \"universal\" },\n storage: { scope: \"global\", schemaVersion: 1 },\n permissions: { storage: { namespace: appId.replace(/\\./g, \"-\") } },\n components: [\n { kind: \"panel\", path: `panels/${panelId}.panel` },\n { kind: \"service\", path: `service-components/${serviceId}` },\n ],\n });\n\n private buildPanelManifest = (panelId: string, appName: string, serviceId: string) => ({\n id: panelId,\n title: appName,\n description: \"读取并增加由 Rust/WASI Component 持久保存的计数。\",\n icon: \"🦀\",\n entry: \"index.html\",\n actions: [`${serviceId}.counter_read`, `${serviceId}.counter_increment`],\n });\n\n private buildServiceManifest = (serviceId: string) => ({\n id: serviceId,\n title: \"Rust/WASI 持久计数组件\",\n description: \"通过标准 WASI key-value 读取和增加持久计数。\",\n protocol: \"wasi-component\",\n component: { entry: \"service.wasm\" },\n actions: {\n counter_read: { risk: \"read\", title: \"读取持久计数\" },\n counter_increment: {\n risk: \"write\",\n title: \"增加持久计数\",\n inputSchema: {\n type: \"object\",\n properties: { step: { type: \"integer\", minimum: 1, maximum: 100 } },\n additionalProperties: false,\n },\n },\n },\n });\n\n private buildServiceSmokeFixture = (serviceId: string) => ({\n schemaVersion: 1,\n component: serviceId,\n resetData: true,\n steps: [\n {\n action: \"counter_increment\",\n input: { step: 3 },\n expect: { counter: 3, persistedBy: \"wasi:keyvalue/store\" },\n },\n {\n action: \"counter_read\",\n input: {},\n expect: { counter: 3, persistedBy: \"wasi:keyvalue/store\" },\n },\n ],\n });\n\n private buildCargoToml = (): string => `[package]\nname = \"${RUST_WASI_GUEST_CRATE_NAME}\"\nversion = \"0.1.0\"\nedition = \"2024\"\npublish = false\n\n[dependencies]\nserde_json = \"1.0\"\nwit-bindgen = \"0.44.0\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[package.metadata.component]\npackage = \"nextclaw:portable-service\"\n\n[package.metadata.component.target]\npath = \"wit\"\nworld = \"service-app\"\n\n[package.metadata.component.target.dependencies]\n\"fermyon:spin\" = { path = \"wit/deps/spin@2.0.0\" }\n\"wasi:http\" = { path = \"wit/deps/http@0.2.6\" }\n\"wasi:io\" = { path = \"wit/deps/io@0.2.6\" }\n\"wasi:clocks\" = { path = \"wit/deps/clocks@0.2.6\" }\n\"wasi:keyvalue\" = { path = \"wit/deps/keyvalue@0.2.0-draft2\" }\n\"wasi:config\" = { path = \"wit/deps/config@0.2.0-draft-2024-09-27\" }\n`;\n\n private buildRustSource = (): string => `wit_bindgen::generate!({\n path: \"wit\",\n world: \"service-app\",\n // Generate standard WASI HTTP's transitive interfaces for later use by a Guest.\n generate_all,\n});\n\nuse exports::nextclaw::portable_service::service::{Action, Guest};\nuse nextclaw::portable_service::host;\nuse serde_json::{Value, json};\nuse wasi::keyvalue::store as keyvalue_store;\n\nconst STORE_NAME: &str = \"default\";\n\nstruct Component;\n\nimpl Guest for Component {\n fn list_actions() -> Vec<Action> {\n vec![\n Action {\n name: \"counter_read\".into(),\n title: \"读取持久计数\".into(),\n description: \"从标准 WASI key-value 存储读取计数。\".into(),\n },\n Action {\n name: \"counter_increment\".into(),\n title: \"增加持久计数\".into(),\n description: \"在 Rust/WASM 中计算,并通过标准 WASI key-value 持久化。\".into(),\n },\n ]\n }\n\n fn invoke(action: String, input_json: String) -> Result<String, String> {\n host::log(host::LogLevel::Info, &format!(\"invoking {action}\"));\n let input: Value = serde_json::from_str(&input_json).unwrap_or_else(|_| json!({}));\n match action.as_str() {\n \"counter_read\" => Ok(counter_result(read_counter()?)),\n \"counter_increment\" => {\n let step = input.get(\"step\").and_then(Value::as_i64).unwrap_or(1);\n if !(1..=100).contains(&step) {\n return Err(\"INVALID_INPUT: step must be between 1 and 100\".into());\n }\n let counter = read_counter()?.saturating_add(step);\n let bucket = keyvalue_store::open(STORE_NAME).map_err(format_keyvalue_error)?;\n bucket\n .set(\"counter\", counter.to_string().as_bytes())\n .map_err(format_keyvalue_error)?;\n Ok(counter_result(counter))\n }\n _ => Err(format!(\"UNKNOWN_ACTION: {action}\")),\n }\n }\n\n fn start(_config_json: String) -> Result<String, String> {\n Ok(json!({ \"started\": true, \"mode\": \"action\" }).to_string())\n }\n\n fn handle_event(_event_json: String) -> Result<String, String> {\n Err(\"UNSUPPORTED_LIFECYCLE: action component does not accept resident events\".into())\n }\n\n fn stop(_reason_json: String) -> Result<String, String> {\n Ok(json!({ \"stopped\": true, \"mode\": \"action\" }).to_string())\n }\n}\n\nfn read_counter() -> Result<i64, String> {\n let bucket = keyvalue_store::open(STORE_NAME).map_err(format_keyvalue_error)?;\n Ok(bucket\n .get(\"counter\")\n .map_err(format_keyvalue_error)?\n .and_then(|value| String::from_utf8(value).ok())\n .and_then(|value| value.parse().ok())\n .unwrap_or(0))\n}\n\nfn counter_result(counter: i64) -> String {\n json!({ \"counter\": counter, \"persistedBy\": \"wasi:keyvalue/store\" }).to_string()\n}\n\nfn format_keyvalue_error(_error: keyvalue_store::Error) -> String {\n \"WASI_KEYVALUE_ERROR: application storage is unavailable\".into()\n}\n\nexport!(Component with_types_in self);\n`;\n\n private buildReadme = (appName: string, serviceId: string): string => `# ${appName}\n\n这是一个可以独立构建的 Rust/WASI Component App。Panel 和 Agent 调用同一组 Service Action,计数通过标准 WASI key-value 持久保存。\n\n## 1. 准备 Rust 工具链\n\n\\`\\`\\`bash\nrustup target add wasm32-wasip2\n\\`\\`\\`\n\n## 2. 构建 Component\n\n\\`\\`\\`bash\ncd guest\ncargo build --release --target wasm32-wasip2\ncp target/wasm32-wasip2/release/nextclaw_rust_wasi_guest.wasm ../service-components/${serviceId}/service.wasm\ncd ..\n\\`\\`\\`\n\n项目内的 \\`guest/wit/portable-service.wit\\` 是当前 Service Action 与宿主能力合同;不需要 NextClaw 源码仓库。\n\n## 3. 检查和调试\n\n\\`\\`\\`bash\nnextclaw app check .\nnextclaw app dev .\nnextclaw app call . counter_read\nnextclaw app call . counter_increment --input '{\"step\":2}'\n\\`\\`\\`\n\n如果一个包包含多个 Service Component,请增加 \\`--component <component-id>\\`。\n\n## 4. 打包和安装\n\n\\`\\`\\`bash\nnextclaw app pack . --target universal --out ${this.normalizeSlug(appName)}.napp\nnextclaw app install ./${this.normalizeSlug(appName)}.napp\n\\`\\`\\`\n`;\n\n private buildPanelHtml = (appName: string): string => `<!doctype html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${appName}</title>\n <style>\n :root { font-family: ui-sans-serif, system-ui, sans-serif; color: #172033; background: #f5f7fb; }\n body { margin: 0; min-height: 100vh; display: grid; place-items: center; }\n main { width: min(420px, calc(100vw - 40px)); padding: 28px; border-radius: 24px; background: white; box-shadow: 0 18px 60px #24324a1f; }\n p { color: #667085; }\n output { display: block; margin: 24px 0; font-size: 64px; font-weight: 700; }\n button { border: 0; border-radius: 999px; padding: 12px 18px; background: #172033; color: white; cursor: pointer; }\n #error { color: #b42318; }\n </style>\n </head>\n <body>\n <main>\n <h1>${appName}</h1>\n <p>这个数字由 Rust/WASI Component 计算,并通过标准 WASI key-value 持久保存。</p>\n <output id=\"counter\">…</output>\n <button id=\"increment\" type=\"button\">增加 1</button>\n <p id=\"error\" role=\"alert\"></p>\n </main>\n <script src=\"app.js\"></script>\n </body>\n</html>\n`;\n\n private buildPanelScript = (serviceId: string): string => `const counter = document.querySelector(\"#counter\");\nconst error = document.querySelector(\"#error\");\nconst increment = document.querySelector(\"#increment\");\n\nasync function readCounter() {\n error.textContent = \"\";\n try {\n const result = await window.nextclaw.serviceActions.invoke(\"${serviceId}.counter_read\", {});\n counter.textContent = String(result.counter ?? 0);\n } catch (cause) {\n error.textContent = cause instanceof Error ? cause.message : String(cause);\n }\n}\n\nasync function incrementCounter() {\n error.textContent = \"\";\n try {\n const result = await window.nextclaw.serviceActions.invoke(\"${serviceId}.counter_increment\", { step: 1 });\n counter.textContent = String(result.counter ?? 0);\n } catch (cause) {\n error.textContent = cause instanceof Error ? cause.message : String(cause);\n }\n}\n\nincrement.addEventListener(\"click\", incrementCounter);\nvoid readCounter();\n`;\n\n private buildMarketplaceMetadata = (appName: string) => ({\n slug: this.normalizeSlug(appName),\n summary: `${appName} Rust/WASI Component 示例。`,\n summaryI18n: {\n zh: `${appName} Rust/WASI Component 示例。`,\n en: `${appName}, a Rust/WASI Component example.`,\n },\n description: \"A minimal Rust/WASI Component App with a Panel and host-managed persistent KV.\",\n descriptionI18n: {\n zh: \"一个包含 Panel 和宿主持久 KV 的最小 Rust/WASI Component App。\",\n en: \"A minimal Rust/WASI Component App with a Panel and host-managed persistent KV.\",\n },\n author: \"NextClaw\",\n tags: [\"starter\", \"rust\", \"wasi-component\", \"official\"],\n sourceRepo: \"https://github.com/Peiiii/nextclaw\",\n homepage: \"https://nextclaw.io\",\n featured: false,\n });\n\n private normalizeSlug = (value: string): string => value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\") || \"rust-wasi-app\";\n\n private buildIconSvg = (): string => `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\">\n <rect width=\"128\" height=\"128\" rx=\"30\" fill=\"#172033\"/>\n <path d=\"M31 40h66v48H31z\" fill=\"#fff\" opacity=\".12\"/>\n <path d=\"M43 52h42v8H43zm0 16h28v8H43z\" fill=\"#fff\"/>\n <circle cx=\"88\" cy=\"76\" r=\"12\" fill=\"#f59e0b\"/>\n</svg>\n`;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/utils/app-service-component-contract.utils.ts
|
|
2
|
+
function parseAppServiceComponentContract(params) {
|
|
3
|
+
const { label, runtimeProfile } = params;
|
|
4
|
+
const manifest = assertRecord(params.manifest, label);
|
|
5
|
+
const protocol = manifest.protocol === void 0 ? "mcp" : readRequiredString(manifest.protocol, `${label}.protocol`);
|
|
6
|
+
if (protocol !== "mcp" && protocol !== "wasi-component") throw new Error(`${label}.protocol 只支持 mcp 或 wasi-component。`);
|
|
7
|
+
if (runtimeProfile === "wasi" && protocol !== "wasi-component") throw new Error("runtime.profile=wasi 的所有 Service component 必须使用 wasi-component protocol。");
|
|
8
|
+
if (runtimeProfile !== "wasi" && protocol === "wasi-component") throw new Error("wasi-component Service 必须声明根 manifest runtime.profile=wasi。");
|
|
9
|
+
if (protocol === "mcp") return { protocol };
|
|
10
|
+
return {
|
|
11
|
+
protocol,
|
|
12
|
+
componentEntry: normalizeComponentEntry(readRequiredString(assertRecord(manifest.component, `${label}.component`).entry, `${label}.component.entry`), `${label}.component.entry`)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function assertWasmComponentBinary(bytes, label) {
|
|
16
|
+
const componentHeader = [
|
|
17
|
+
0,
|
|
18
|
+
97,
|
|
19
|
+
115,
|
|
20
|
+
109,
|
|
21
|
+
13,
|
|
22
|
+
0,
|
|
23
|
+
1,
|
|
24
|
+
0
|
|
25
|
+
];
|
|
26
|
+
if (bytes.byteLength < componentHeader.length || componentHeader.some((byte, index) => bytes[index] !== byte)) throw new Error(`${label} 不是 WebAssembly Component binary。`);
|
|
27
|
+
}
|
|
28
|
+
function normalizeComponentEntry(value, label) {
|
|
29
|
+
const normalized = value.replace(/\\/g, "/");
|
|
30
|
+
const segments = normalized.split("/");
|
|
31
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || !normalized.endsWith(".wasm") || segments.some((segment) => !segment || segment === "." || segment === "..")) throw new Error(`${label} 必须是安全的 package-relative .wasm 路径。`);
|
|
32
|
+
return segments.join("/");
|
|
33
|
+
}
|
|
34
|
+
function assertRecord(value, label) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} 必须是对象。`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function readRequiredString(value, label) {
|
|
39
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} 必须是非空字符串。`);
|
|
40
|
+
return value.trim();
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { assertWasmComponentBinary, parseAppServiceComponentContract };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=app-service-component-contract.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-service-component-contract.utils.js","names":[],"sources":["../../src/utils/app-service-component-contract.utils.ts"],"sourcesContent":["import type { AppRuntimeProfile } from \"#app-runtime/types/app-manifest.types.js\";\n\nexport type AppServiceComponentContract = {\n protocol: \"mcp\" | \"wasi-component\";\n componentEntry?: string;\n};\n\nexport function parseAppServiceComponentContract(params: {\n manifest: unknown;\n runtimeProfile: AppRuntimeProfile;\n label: string;\n}): AppServiceComponentContract {\n const { label, runtimeProfile } = params;\n const manifest = assertRecord(params.manifest, label);\n const protocol = manifest.protocol === undefined\n ? \"mcp\"\n : readRequiredString(manifest.protocol, `${label}.protocol`);\n if (protocol !== \"mcp\" && protocol !== \"wasi-component\") {\n throw new Error(`${label}.protocol 只支持 mcp 或 wasi-component。`);\n }\n if (runtimeProfile === \"wasi\" && protocol !== \"wasi-component\") {\n throw new Error(\"runtime.profile=wasi 的所有 Service component 必须使用 wasi-component protocol。\");\n }\n if (runtimeProfile !== \"wasi\" && protocol === \"wasi-component\") {\n throw new Error(\"wasi-component Service 必须声明根 manifest runtime.profile=wasi。\");\n }\n if (protocol === \"mcp\") {\n return { protocol };\n }\n const component = assertRecord(manifest.component, `${label}.component`);\n const rawEntry = readRequiredString(component.entry, `${label}.component.entry`);\n const componentEntry = normalizeComponentEntry(rawEntry, `${label}.component.entry`);\n return { protocol, componentEntry };\n}\n\nexport function assertWasmComponentBinary(bytes: Uint8Array, label: string): void {\n const componentHeader = [0x00, 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00];\n if (\n bytes.byteLength < componentHeader.length ||\n componentHeader.some((byte, index) => bytes[index] !== byte)\n ) {\n throw new Error(`${label} 不是 WebAssembly Component binary。`);\n }\n}\n\nfunction normalizeComponentEntry(value: string, label: string): string {\n const normalized = value.replace(/\\\\/g, \"/\");\n const segments = normalized.split(\"/\");\n if (\n normalized.startsWith(\"/\") ||\n /^[A-Za-z]:/.test(normalized) ||\n !normalized.endsWith(\".wasm\") ||\n segments.some((segment) => !segment || segment === \".\" || segment === \"..\")\n ) {\n throw new Error(`${label} 必须是安全的 package-relative .wasm 路径。`);\n }\n return segments.join(\"/\");\n}\n\nfunction assertRecord(value: unknown, label: string): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(`${label} 必须是对象。`);\n }\n return value as Record<string, unknown>;\n}\n\nfunction readRequiredString(value: unknown, label: string): string {\n if (typeof value !== \"string\" || !value.trim()) {\n throw new Error(`${label} 必须是非空字符串。`);\n }\n return value.trim();\n}\n"],"mappings":";AAOA,SAAgB,iCAAiC,QAIjB;CAC9B,MAAM,EAAE,OAAO,mBAAmB;CAClC,MAAM,WAAW,aAAa,OAAO,UAAU,MAAM;CACrD,MAAM,WAAW,SAAS,aAAa,KAAA,IACnC,QACA,mBAAmB,SAAS,UAAU,GAAG,MAAM,WAAW;AAC9D,KAAI,aAAa,SAAS,aAAa,iBACrC,OAAM,IAAI,MAAM,GAAG,MAAM,qCAAqC;AAEhE,KAAI,mBAAmB,UAAU,aAAa,iBAC5C,OAAM,IAAI,MAAM,2EAA2E;AAE7F,KAAI,mBAAmB,UAAU,aAAa,iBAC5C,OAAM,IAAI,MAAM,8DAA8D;AAEhF,KAAI,aAAa,MACf,QAAO,EAAE,UAAU;AAKrB,QAAO;EAAE;EAAU,gBADI,wBADN,mBADC,aAAa,SAAS,WAAW,GAAG,MAAM,YAAY,CAC1B,OAAO,GAAG,MAAM,kBAAkB,EACvB,GAAG,MAAM,kBAAkB;EACjD;;AAGrC,SAAgB,0BAA0B,OAAmB,OAAqB;CAChF,MAAM,kBAAkB;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAK;AACxE,KACE,MAAM,aAAa,gBAAgB,UACnC,gBAAgB,MAAM,MAAM,UAAU,MAAM,WAAW,KAAK,CAE5D,OAAM,IAAI,MAAM,GAAG,MAAM,mCAAmC;;AAIhE,SAAS,wBAAwB,OAAe,OAAuB;CACrE,MAAM,aAAa,MAAM,QAAQ,OAAO,IAAI;CAC5C,MAAM,WAAW,WAAW,MAAM,IAAI;AACtC,KACE,WAAW,WAAW,IAAI,IAC1B,aAAa,KAAK,WAAW,IAC7B,CAAC,WAAW,SAAS,QAAQ,IAC7B,SAAS,MAAM,YAAY,CAAC,WAAW,YAAY,OAAO,YAAY,KAAK,CAE3E,OAAM,IAAI,MAAM,GAAG,MAAM,oCAAoC;AAE/D,QAAO,SAAS,KAAK,IAAI;;AAG3B,SAAS,aAAa,OAAgB,OAAwC;AAC5E,KAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7D,OAAM,IAAI,MAAM,GAAG,MAAM,SAAS;AAEpC,QAAO;;AAGT,SAAS,mBAAmB,OAAgB,OAAuB;AACjE,KAAI,OAAO,UAAU,YAAY,CAAC,MAAM,MAAM,CAC5C,OAAM,IAAI,MAAM,GAAG,MAAM,YAAY;AAEvC,QAAO,MAAM,MAAM"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package wasi:keyvalue@0.2.0-draft2;
|
|
2
|
+
|
|
3
|
+
/// A keyvalue interface that provides eventually consistent key-value operations.
|
|
4
|
+
interface store {
|
|
5
|
+
/// The set of errors which may be raised by functions in this package.
|
|
6
|
+
variant error {
|
|
7
|
+
/// The host does not recognize the store identifier requested.
|
|
8
|
+
no-such-store,
|
|
9
|
+
/// The requesting component does not have access to the specified store.
|
|
10
|
+
access-denied,
|
|
11
|
+
/// Some implementation-specific error has occurred.
|
|
12
|
+
other(string),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// A response to a `list-keys` operation.
|
|
16
|
+
record key-response {
|
|
17
|
+
keys: list<string>,
|
|
18
|
+
cursor: option<string>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Get the bucket with the specified identifier.
|
|
22
|
+
open: func(identifier: string) -> result<bucket, error>;
|
|
23
|
+
|
|
24
|
+
resource bucket {
|
|
25
|
+
/// Get the value associated with the specified key.
|
|
26
|
+
get: func(key: string) -> result<option<list<u8>>, error>;
|
|
27
|
+
/// Set or replace the value associated with the specified key.
|
|
28
|
+
set: func(key: string, value: list<u8>) -> result<_, error>;
|
|
29
|
+
/// Delete the key-value pair associated with the specified key.
|
|
30
|
+
delete: func(key: string) -> result<_, error>;
|
|
31
|
+
/// Check if the specified key exists.
|
|
32
|
+
exists: func(key: string) -> result<bool, error>;
|
|
33
|
+
/// Return a page of keys and an optional continuation cursor.
|
|
34
|
+
list-keys: func(cursor: option<string>) -> result<key-response, error>;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -97,6 +97,7 @@ interface resident-v2 {
|
|
|
97
97
|
world service-app {
|
|
98
98
|
import host;
|
|
99
99
|
import wasi:http/outgoing-handler@0.2.6;
|
|
100
|
+
import wasi:keyvalue/store@0.2.0-draft2;
|
|
100
101
|
import wasi:config/store@0.2.0-draft-2024-09-27;
|
|
101
102
|
export service;
|
|
102
103
|
}
|
|
@@ -115,6 +116,7 @@ world service-app-sqlite {
|
|
|
115
116
|
world service-app-v2 {
|
|
116
117
|
import host;
|
|
117
118
|
import wasi:http/outgoing-handler@0.2.6;
|
|
119
|
+
import wasi:keyvalue/store@0.2.0-draft2;
|
|
118
120
|
import wasi:config/store@0.2.0-draft-2024-09-27;
|
|
119
121
|
export resident-v2;
|
|
120
122
|
}
|