@nextclaw/app-runtime 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/package.js +1 -1
- package/dist/services/app-home.service.d.ts +4 -0
- package/dist/services/app-home.service.d.ts.map +1 -1
- package/dist/services/app-home.service.js +15 -1
- package/dist/services/app-home.service.js.map +1 -1
- package/dist/services/app-install-source.service.js +14 -1
- package/dist/services/app-install-source.service.js.map +1 -1
- package/dist/services/app-installation-filesystem.service.js +68 -0
- package/dist/services/app-installation-filesystem.service.js.map +1 -0
- package/dist/services/app-installation-integrity.service.d.ts +20 -0
- package/dist/services/app-installation-integrity.service.d.ts.map +1 -0
- package/dist/services/app-installation-integrity.service.js +84 -0
- package/dist/services/app-installation-integrity.service.js.map +1 -0
- package/dist/services/app-installation-lifecycle.service.js +110 -0
- package/dist/services/app-installation-lifecycle.service.js.map +1 -0
- package/dist/services/app-installation.service.d.ts +21 -11
- package/dist/services/app-installation.service.d.ts.map +1 -1
- package/dist/services/app-installation.service.js +262 -280
- package/dist/services/app-installation.service.js.map +1 -1
- package/dist/services/app-instance-storage.service.d.ts +49 -0
- package/dist/services/app-instance-storage.service.d.ts.map +1 -0
- package/dist/services/app-instance-storage.service.js +256 -0
- package/dist/services/app-instance-storage.service.js.map +1 -0
- package/dist/services/app-manifest.service.d.ts +5 -1
- package/dist/services/app-manifest.service.d.ts.map +1 -1
- package/dist/services/app-manifest.service.js +59 -3
- package/dist/services/app-manifest.service.js.map +1 -1
- package/dist/services/app-publish.service.js +1 -1
- package/dist/services/app-publish.service.js.map +1 -1
- package/dist/services/app-registry.service.d.ts +13 -3
- package/dist/services/app-registry.service.d.ts.map +1 -1
- package/dist/services/app-registry.service.js +143 -13
- package/dist/services/app-registry.service.js.map +1 -1
- package/dist/services/file-lock.service.d.ts +20 -0
- package/dist/services/file-lock.service.d.ts.map +1 -0
- package/dist/services/file-lock.service.js +156 -0
- package/dist/services/file-lock.service.js.map +1 -0
- package/dist/types/app-installation.types.d.ts +7 -0
- package/dist/types/app-installation.types.d.ts.map +1 -1
- package/dist/types/app-manifest.types.d.ts +22 -1
- package/dist/types/app-manifest.types.d.ts.map +1 -1
- package/dist/types/app-manifest.types.js.map +1 -1
- package/dist/types/app-registry.types.d.ts +7 -1
- package/dist/types/app-registry.types.d.ts.map +1 -1
- package/dist/types/app-storage.types.d.ts +37 -0
- package/dist/types/app-storage.types.d.ts.map +1 -0
- package/dist/types/app-storage.types.js +7 -0
- package/dist/types/app-storage.types.js.map +1 -0
- package/package.json +1 -1
|
@@ -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 type {\n AppComponentManifest,\n AppComponentManifestBundle,\n AppComponentReference,\n AppDocumentAccessScope,\n AppManifest,\n AppManifestBundle,\n AppManifestSummary,\n AppPermissions,\n AppResolvedComponent,\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]+)*$/;\n\nexport class AppManifestService {\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 };\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 return {\n schemaVersion: 2,\n ...common,\n engines: this.parseEngines(candidate.engines),\n presentation: this.parsePresentation(candidate.presentation),\n components: this.parseComponents(candidate.components),\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 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 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 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 };\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":";;;AAgBA,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAE7B,IAAa,qBAAb,MAAgC;CAC9B,OAAO,OAAO,iBAAqD;EACjE,MAAM,sBAAsB,KAAK,QAAQ,aAAa;EACtD,MAAM,eAAe,KAAK,KAAK,qBAAqB,gBAAgB;EACpE,MAAM,cAAc,KAAK,MAAM,MAAM,SAAS,cAAc,QAAQ,CAAC;EACrE,MAAM,WAAW,KAAK,cAAc,YAAY;AAChD,SAAO,SAAS,kBAAkB,IAC9B,MAAM,KAAK,qBAAqB,qBAAqB,cAAc,SAAS,GAC5E,MAAM,KAAK,oBAAoB,qBAAqB,cAAc,SAAS;;CAGjF,aAAa,WAAkD;AAC7D,MAAI,OAAO,SAAS,kBAAkB,GAAG;GACvC,MAAM,kBAAkB;AACxB,UAAO;IACL,eAAe;IACf,IAAI,gBAAgB,SAAS;IAC7B,MAAM,gBAAgB,SAAS;IAC/B,SAAS,gBAAgB,SAAS;IAClC,aAAa,gBAAgB,SAAS;IACtC,cAAc,gBAAgB;IAC9B,UAAU,gBAAgB;IAC1B,gBAAgB,gBAAgB;IAChC,YAAY,gBAAgB;IAC7B;;EAEH,MAAM,mBAAmB;EACzB,MAAM,cAAc,iBAAiB,SAAS,eAAe,EAAE;AAC/D,SAAO;GACL,eAAe;GACf,IAAI,iBAAiB,SAAS;GAC9B,MAAM,iBAAiB,SAAS;GAChC,SAAS,iBAAiB,SAAS;GACnC,aAAa,iBAAiB,SAAS;GACvC,UAAU,iBAAiB,SAAS,KAAK;GACzC,QACE,iBAAiB,SAAS,KAAK,SAAS,SACpC,iBAAiB,SAAS,KAAK,SAC/B,KAAA;GACN,cAAc,iBAAiB;GAC/B,eAAe,iBAAiB;GAChC,aAAa,iBAAiB;GAC9B,UAAU,iBAAiB;GAC3B;GACD;;CAGH,uBAA+B,OAC7B,cACA,cACA,aACyC;EACzC,MAAM,gBAAgB,MAAM,KAAK,mBAC/B,cACA,SAAS,KAAK,OACd,aACD;EACD,MAAM,cAAc,MAAM,KAAK,mBAC7B,cACA,SAAS,GAAG,OACZ,WACD;EACD,MAAM,WAAW,SAAS,OACtB,MAAM,KAAK,mBAAmB,cAAc,SAAS,MAAM,OAAO,GAClE,KAAA;AACJ,SAAO;GACL;GACA;GACA;GACA;GACA;GACA,iBAAiB,KAAK,QAAQ,YAAY;GAC1C,qBAAqB,KAAK,KAAK,cAAc,SAAS;GACtD;GACD;;CAGH,sBAA8B,OAC5B,cACA,cACA,aACwC;EACxC,MAAM,aAAqC,EAAE;AAC7C,OAAK,MAAM,CAAC,OAAO,cAAc,SAAS,WAAW,SAAS,EAAE;GAC9D,MAAM,qBAAqB,MAAM,KAAK,wBACpC,cACA,UAAU,MACV,cAAc,MAAM,QACrB;GACD,MAAM,cAAc,MAAM,KAAK,gBAAgB,WAAW,mBAAmB;GAC7E,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,eAAe,IAAI,CAAC;AAClE,OAAI,CAAC,YAAY,WAAW,eAAe,CACzC,OAAM,IAAI,MACR,MAAM,YAAY,WAAW,eAAe,GAC7C;AAEH,cAAW,KAAK;IACd,GAAG;IACH,IAAI;IACJ;IACA,cAAc,KAAK,KACjB,oBACA,UAAU,SAAS,UAAU,mBAAmB,mBACjD;IACF,CAAC;;EAEJ,MAAM,cAAc,WAAW,MAC5B,WAAW,UAAU,WAAW,WAAW,UAAU,MAAM,OAAO,UAAU,GAAG,KAAK,MACtF;AACD,MAAI,YACF,OAAM,IAAI,MAAM,wBAAwB,YAAY,KAAK;EAE3D,MAAM,WAAW,WACd,QAAQ,cAAc,UAAU,SAAS,QAAQ,CACjD,KAAK,cAAc,UAAU,GAAG;EACnC,MAAM,uBAAuB,SAAS,cAAc;AACpD,MAAI,wBAAwB,CAAC,SAAS,SAAS,qBAAqB,CAClE,OAAM,IAAI,MAAM,uDAAuD;AAEzE,MAAI,SAAS,SAAS,KAAK,CAAC,qBAC1B,OAAM,IAAI,MAAM,8CAA8C;AAEhE,MAAI,SAAS,WAAW,KAAK,qBAC3B,OAAM,IAAI,MAAM,6CAA6C;EAE/D,MAAM,WAAW,SAAS,OACtB,MAAM,KAAK,mBAAmB,cAAc,SAAS,MAAM,OAAO,GAClE,KAAA;AACJ,SAAO;GACL;GACA;GACA;GACA;GACA,qBAAqB,KAAK,KAAK,cAAc,SAAS;GACtD;GACA,gBAAgB,wBAAwB,SAAS;GAClD;;CAGH,iBAAyB,gBAAsC;EAC7D,MAAM,YAAY,KAAK,aAAa,aAAa,gBAAgB;EACjE,MAAM,gBAAgB,KAAK,WAAW,UAAU,eAAe,gBAAgB;AAC/E,MAAI,kBAAkB,KAAK,kBAAkB,EAC3C,OAAM,IAAI,MAAM,+BAA+B;EAEjD,MAAM,SAAS,KAAK,oBAAoB,UAAU;AAClD,MAAI,kBAAkB,EACpB,QAAO;GACL,eAAe;GACf,GAAG;GACH,MAAM,KAAK,UAAU,UAAU,KAAK;GACpC,IAAI,KAAK,QAAQ,UAAU,GAAG;GAC9B,aAAa,KAAK,iBAAiB,UAAU,YAAY;GAC1D;AAEH,SAAO;GACL,eAAe;GACf,GAAG;GACH,SAAS,KAAK,aAAa,UAAU,QAAQ;GAC7C,cAAc,KAAK,kBAAkB,UAAU,aAAa;GAC5D,YAAY,KAAK,gBAAgB,UAAU,WAAW;GACvD;;CAGH,uBAA+B,cAAuC;EACpE,MAAM,KAAK,KAAK,mBAAmB,UAAU,IAAI,KAAK;AACtD,MAAI,CAAC,mBAAmB,KAAK,GAAG,CAC9B,OAAM,IAAI,MAAM,yBAAyB;AAE3C,SAAO;GACL;GACA,MAAM,KAAK,mBAAmB,UAAU,MAAM,OAAO;GACrD,SAAS,KAAK,mBAAmB,UAAU,SAAS,UAAU;GAC9D,aAAa,KAAK,mBAAmB,UAAU,aAAa,cAAc;GAC1E,MAAM,KAAK,mBAAmB,UAAU,MAAM,OAAO;GACtD;;CAGH,aAAqB,YAAoD;EACvE,MAAM,YAAY,KAAK,aAAa,SAAS,OAAO;EACpD,MAAM,OAAO,KAAK,mBAAmB,UAAU,MAAM,YAAY;AACjE,MAAI,SAAS,OACX,QAAO;GACL;GACA,OAAO,KAAK,mBAAmB,UAAU,OAAO,aAAa;GAC7D,QAAQ,KAAK,mBAAmB,UAAU,QAAQ,cAAc;GAChE,QAAQ,KAAK,mBAAmB,UAAU,QAAQ,cAAc;GACjE;AAEH,MAAI,SAAS,sBACX,QAAO;GACL;GACA,OAAO,KAAK,mBAAmB,UAAU,OAAO,aAAa;GAC9D;AAEH,QAAM,IAAI,MAAM,+CAA+C;;CAGjE,WAAmB,UAAgD;EACjE,MAAM,YAAY,KAAK,aAAa,OAAO,KAAK;AAChD,SAAO,EAAE,OAAO,KAAK,mBAAmB,UAAU,OAAO,WAAW,EAAE;;CAGxE,mBAA2B,kBAAoD;AAC7E,MAAI,CAAC,MAAM,QAAQ,cAAc,IAAI,cAAc,WAAW,EAC5D,OAAM,IAAI,MAAM,sBAAsB;EAExC,MAAM,aAAa,cAAc,KAAK,cAAc,UAAU;GAC5D,MAAM,YAAY,KAAK,aAAa,cAAc,cAAc,MAAM,GAAG;GACzE,MAAM,OAAO,KAAK,mBAAmB,UAAU,MAAM,cAAc,MAAM,QAAQ;AACjF,OAAI,SAAS,WAAW,SAAS,UAC/B,OAAM,IAAI,MAAM,cAAc,MAAM,6BAA6B;AAGnE,UAAO;IAD+C;IAGpD,MAAM,KAAK,sBACT,KAAK,mBAAmB,UAAU,MAAM,cAAc,MAAM,QAAQ,EACpE,cAAc,MAAM,QACrB;IACF;IACD;AACF,OAAK,IAAI,YAAY,GAAG,YAAY,WAAW,QAAQ,aAAa,EAClE,MAAK,IAAI,aAAa,YAAY,GAAG,aAAa,WAAW,QAAQ,cAAc,GAAG;GACpF,MAAM,OAAO,WAAW,YAAY;GACpC,MAAM,QAAQ,WAAW,aAAa;AACtC,OAAI,QAAQ,UAAU,SAAS,SAAS,KAAK,WAAW,GAAG,MAAM,GAAG,IAAI,MAAM,WAAW,GAAG,KAAK,GAAG,EAClG,OAAM,IAAI,MAAM,2BAA2B,KAAK,KAAK,QAAQ;;AAInE,SAAO;;CAGT,gBACE,eACoC;AACpC,MAAI,eAAe,KAAA,EACjB;EAEF,MAAM,YAAY,KAAK,aAAa,YAAY,UAAU;AAC1D,SAAO,EAAE,UAAU,KAAK,mBAAmB,UAAU,UAAU,mBAAmB,EAAE;;CAGtF,qBACE,oBACyC;AACzC,MAAI,oBAAoB,KAAA,EACtB;EAEF,MAAM,YAAY,KAAK,aAAa,iBAAiB,eAAe;AACpE,SAAO,EACL,cAAc,KAAK,mBACjB,UAAU,cACV,4BACD,EACF;;CAGH,kBAA0B,OACxB,WACA,uBACoB;EACpB,MAAM,eAAe,UAAU,SAAS,UAAU,mBAAmB;EACrE,MAAM,MAAM,KAAK,MACf,MAAM,SAAS,KAAK,KAAK,oBAAoB,aAAa,EAAE,QAAQ,CACrE;EACD,MAAM,WAAW,KAAK,aAAa,KAAK,GAAG,UAAU,KAAK,GAAG,eAAe;EAC5E,MAAM,KAAK,KAAK,mBAAmB,SAAS,IAAI,GAAG,UAAU,KAAK,GAAG,aAAa,KAAK;AACvF,MAAI,CAAC,qBAAqB,KAAK,GAAG,CAChC,OAAM,IAAI,MAAM,wBAAwB,KAAK;EAE/C,MAAM,gBAAgB,KAAK,SAAS,mBAAmB;EACvD,MAAM,aAAa,UAAU,SAAS,UAClC,cAAc,QAAQ,YAAY,GAAG,GACrC;AACJ,MAAI,UAAU,SAAS,WAAW,CAAC,cAAc,SAAS,SAAS,CACjE,OAAM,IAAI,MAAM,mCAAmC,UAAU,OAAO;AAEtE,MAAI,OAAO,WACT,OAAM,IAAI,MAAM,qBAAqB,WAAW,MAAM,KAAK;AAE7D,SAAO;;CAGT,oBAA4B,mBAAwD;AAClF,MAAI,mBAAmB,KAAA,EACrB;EAEF,MAAM,YAAY,KAAK,aAAa,gBAAgB,cAAc;AAClE,SAAO;GACL,gBAAgB,KAAK,oBAAoB,UAAU,eAAe;GAClE,gBAAgB,KAAK,iBAAiB,UAAU,gBAAgB,6BAA6B;GAC7F,SAAS,KAAK,aAAa,UAAU,QAAQ;GAC7C,cAAc,KAAK,kBAAkB,UAAU,aAAa;GAC7D;;CAGH,uBAA+B,sBAAqE;AAClG,MAAI,sBAAsB,KAAA,EACxB;AAEF,MAAI,CAAC,MAAM,QAAQ,kBAAkB,CACnC,OAAM,IAAI,MAAM,oCAAoC;AAEtD,SAAO,kBAAkB,KAAK,OAAO,UAAU;GAC7C,MAAM,YAAY,KAAK,aAAa,OAAO,8BAA8B,MAAM,GAAG;GAClF,MAAM,OAAO,KAAK,mBAAmB,UAAU,MAAM,8BAA8B,MAAM,QAAQ;AACjG,OAAI,SAAS,UAAU,SAAS,aAC9B,OAAM,IAAI,MAAM,8BAA8B,MAAM,+BAA+B;AAErF,UAAO;IACL,IAAI,KAAK,mBAAmB,UAAU,IAAI,8BAA8B,MAAM,MAAM;IACpF;IACA,aAAa,KAAK,mBAChB,UAAU,aACV,8BAA8B,MAAM,eACrC;IACF;IACD;;CAGJ,oBAA4B,UAAmB,cAA4C;AACzF,MAAI,aAAa,KAAA,EACf;AAEF,MAAI,CAAC,MAAM,QAAQ,SAAS,CAC1B,OAAM,IAAI,MAAM,GAAG,UAAU,YAAY;AAE3C,SAAO,SAAS,KAAK,MAAM,UAAU,KAAK,mBAAmB,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,CAAC;;CAG/F,gBAAwB,eAA+D;AACrF,MAAI,eAAe,KAAA,KAAa,OAAO,eAAe,UACpD,QAAO;EAET,MAAM,YAAY,KAAK,aAAa,YAAY,sBAAsB;AACtE,SAAO,EAAE,WAAW,KAAK,mBAAmB,UAAU,WAAW,gCAAgC,EAAE;;CAGrG,qBAA6B,oBAAyE;AACpG,MAAI,oBAAoB,KAAA,EACtB;EAEF,MAAM,YAAY,KAAK,aAAa,iBAAiB,2BAA2B;AAChF,SAAO,EACL,YAAY,UAAU,eAAe,KAAA,IACjC,KAAA,IACA,KAAK,YAAY,UAAU,YAAY,sCAAsC,EAClF;;CAGH,qBAA6B,OAC3B,cACA,cACA,cACoB;EACpB,MAAM,eAAe,KAAK,cAAc,cAAc,cAAc,UAAU;EAC9E,MAAM,QAAQ,MAAM,MAAM,aAAa;AACvC,MAAI,CAAC,MAAM,QAAQ,IAAI,MAAM,gBAAgB,CAC3C,OAAM,IAAI,MAAM,GAAG,UAAU,cAAc;AAE7C,SAAO;;CAGT,0BAAkC,OAChC,cACA,cACA,cACoB;EACpB,MAAM,eAAe,KAAK,cAAc,cAAc,cAAc,UAAU;EAC9E,MAAM,QAAQ,MAAM,MAAM,aAAa;AACvC,MAAI,CAAC,MAAM,aAAa,IAAI,MAAM,gBAAgB,CAChD,OAAM,IAAI,MAAM,GAAG,UAAU,cAAc;AAE7C,QAAM,OAAO,aAAa;AAC1B,SAAO;;CAGT,iBAAyB,cAAsB,cAAsB,cAA8B;EACjG,MAAM,iBAAiB,KAAK,sBAAsB,cAAc,UAAU;EAC1E,MAAM,eAAe,KAAK,QAAQ,cAAc,eAAe;EAC/D,MAAM,WAAW,KAAK,SAAS,cAAc,aAAa;AAC1D,MAAI,SAAS,WAAW,KAAK,IAAI,KAAK,WAAW,SAAS,CACxD,OAAM,IAAI,MAAM,GAAG,UAAU,cAAc;AAE7C,SAAO;;CAGT,yBAAiC,cAAsB,cAA8B;AACnF,MAAI,KAAK,WAAW,aAAa,IAAI,aAAa,SAAS,KAAK,CAC9D,OAAM,IAAI,MAAM,GAAG,UAAU,cAAc;EAE7C,MAAM,WAAW,aAAa,QAAQ,OAAO,IAAI,CAAC,MAAM,IAAI;AAC5D,MAAI,SAAS,MAAM,YAAY,CAAC,WAAW,YAAY,OAAO,YAAY,KAAK,CAC7E,OAAM,IAAI,MAAM,GAAG,UAAU,YAAY;AAE3C,SAAO,SAAS,KAAK,IAAI;;CAG3B,gBAAwB,OAAgB,cAA+C;AACrF,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7D,OAAM,IAAI,MAAM,GAAG,UAAU,SAAS;AAExC,SAAO;;CAGT,sBAA8B,OAAgB,cAA8B;AAC1E,MAAI,OAAO,UAAU,YAAY,CAAC,MAAM,MAAM,CAC5C,OAAM,IAAI,MAAM,GAAG,UAAU,YAAY;AAE3C,SAAO,MAAM,MAAM;;CAGrB,sBAA8B,OAAgB,cAA0C;AACtF,SAAO,UAAU,KAAA,IAAY,KAAA,IAAY,KAAK,mBAAmB,OAAO,UAAU;;CAGpF,cAAsB,OAAgB,cAA8B;AAClE,MAAI,OAAO,UAAU,YAAY,OAAO,MAAM,MAAM,CAClD,OAAM,IAAI,MAAM,GAAG,UAAU,SAAS;AAExC,SAAO;;CAGT,eAAuB,OAAgB,cAA+B;AACpE,MAAI,OAAO,UAAU,UACnB,OAAM,IAAI,MAAM,GAAG,UAAU,UAAU;AAEzC,SAAO"}
|
|
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 type {\n AppComponentManifest,\n AppComponentManifestBundle,\n AppComponentReference,\n AppDocumentAccessScope,\n AppManifest,\n AppManifestBundle,\n AppManifestSummary,\n AppPermissions,\n AppPlatformSecuritySummary,\n AppResolvedComponent,\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]+)*$/;\n\nexport class AppManifestService {\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 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 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 !== \"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 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 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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"}
|
|
@@ -62,7 +62,7 @@ var AppPublishService = class {
|
|
|
62
62
|
visuals: metadata.visuals,
|
|
63
63
|
distributionMode,
|
|
64
64
|
manifest: manifestBundle.manifest,
|
|
65
|
-
permissions: manifestBundle.manifest.schemaVersion === 1 ? manifestBundle.manifest.permissions ?? {} :
|
|
65
|
+
permissions: manifestBundle.manifest.schemaVersion === 1 ? manifestBundle.manifest.permissions ?? {} : this.manifestService.resolvePlatformSecurity(manifestBundle.manifest).permissions,
|
|
66
66
|
bundleBase64: bundleBytes.toString("base64"),
|
|
67
67
|
bundleSha256,
|
|
68
68
|
files: publishFiles.map((file) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-publish.service.js","names":[],"sources":["../../src/services/app-publish.service.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { AppBundleService } from \"#app-runtime/services/app-bundle.service.js\";\nimport type { AppDistributionMode } from \"#app-runtime/types/app-bundle.types.js\";\nimport { AppManifestService } from \"#app-runtime/services/app-manifest.service.js\";\nimport { AppMarketplaceClientService } from \"./app-marketplace-client.service.js\";\nimport { AppMarketplaceMetadataService } from \"./app-marketplace-metadata.service.js\";\nimport type { AppPublishPayload, AppPublishResult } from \"#app-runtime/types/app-publish.types.js\";\nimport { PlatformAuthStateService } from \"./platform-auth-state.service.js\";\n\nconst DEFAULT_PLATFORM_API_BASE = \"https://ai-gateway-api.nextclaw.io\";\n\ntype ResolvedPublishActor = {\n token: string;\n publisher: NonNullable<AppPublishPayload[\"publisher\"]>;\n};\n\nexport class AppPublishService {\n constructor(\n private readonly manifestService: AppManifestService = new AppManifestService(),\n private readonly bundleService: AppBundleService = new AppBundleService(),\n private readonly metadataService: AppMarketplaceMetadataService = new AppMarketplaceMetadataService(),\n private readonly marketplaceClient: AppMarketplaceClientService = new AppMarketplaceClientService(),\n private readonly authStateService: PlatformAuthStateService = new PlatformAuthStateService(),\n ) {}\n\n publish = async (params: {\n appDirectory: string;\n metadataPath?: string;\n bundleOutputPath?: string;\n apiBaseUrl?: string;\n token?: string;\n mode?: AppDistributionMode;\n }): Promise<AppPublishResult> => {\n const {\n appDirectory: inputAppDirectory,\n metadataPath,\n bundleOutputPath,\n apiBaseUrl,\n token,\n } = params;\n const appDirectory = path.resolve(inputAppDirectory);\n const manifestBundle = await this.manifestService.load(appDirectory);\n const distributionMode = params.mode ??\n (manifestBundle.manifest.schemaVersion === 2 ? \"bundle\" : \"source\");\n const metadata = await this.metadataService.load({\n appDirectory,\n manifest: manifestBundle.manifest,\n metadataPath,\n });\n const actor = await this.resolvePublishActor({\n apiBaseUrl,\n explicitToken: token,\n appId: manifestBundle.manifest.id,\n });\n const bundle = await this.bundleService.packAppDirectory({\n appDirectory,\n outputPath: bundleOutputPath,\n mode: distributionMode,\n });\n const bundleBytes = Buffer.from(await readFile(bundle.bundlePath));\n const bundleSha256 = createHash(\"sha256\").update(bundleBytes).digest(\"hex\");\n const publishFiles = await this.metadataService.collectPublishFiles({\n appDirectory,\n iconPath: manifestBundle.manifest.icon,\n metadataPath,\n visuals: metadata.visuals,\n });\n const payload: AppPublishPayload = {\n slug: metadata.slug,\n appId: manifestBundle.manifest.id,\n name: manifestBundle.manifest.name,\n version: manifestBundle.manifest.version,\n summary: metadata.summary,\n summaryI18n: metadata.summaryI18n,\n description: metadata.description ?? manifestBundle.manifest.description,\n descriptionI18n: metadata.descriptionI18n,\n author: metadata.author,\n tags: metadata.tags,\n sourceRepo: metadata.sourceRepo,\n homepage: metadata.homepage,\n featured: metadata.featured ?? false,\n publisher: actor.publisher,\n visuals: metadata.visuals,\n distributionMode,\n manifest: manifestBundle.manifest,\n permissions: manifestBundle.manifest.schemaVersion === 1\n ? manifestBundle.manifest.permissions ?? {}\n : {},\n bundleBase64: bundleBytes.toString(\"base64\"),\n bundleSha256,\n files: publishFiles.map((file) => ({\n path: file.path,\n contentBase64: file.bytes.toString(\"base64\"),\n })),\n };\n const result = await this.marketplaceClient.publish({\n payload,\n apiBaseUrl,\n token: actor.token,\n });\n return {\n ...result,\n distribution: {\n path: bundle.bundlePath,\n sha256: bundleSha256,\n mode: distributionMode,\n },\n };\n };\n\n private resolvePublishActor = async (params: {\n apiBaseUrl?: string;\n explicitToken?: string;\n appId: string;\n }): Promise<ResolvedPublishActor> => {\n const explicitToken = params.explicitToken?.trim();\n const envAdminToken = process.env.NEXTCLAW_MARKETPLACE_ADMIN_TOKEN?.trim();\n if (explicitToken) {\n const token = explicitToken;\n if (!token) {\n throw new Error(\"缺少 publish token。\");\n }\n const me = await this.fetchCurrentPlatformUser({\n token,\n platformApiBase: this.resolvePlatformApiBase(),\n });\n return {\n token,\n publisher: this.buildUserPublisher(me, params.appId),\n };\n }\n\n const authState = this.authStateService.readCurrentAuthState();\n const platformToken = authState.token?.trim();\n if (platformToken) {\n const me = await this.fetchCurrentPlatformUser({\n token: platformToken,\n platformApiBase: this.resolvePlatformApiBase(authState.apiBaseUrl),\n });\n return {\n token: platformToken,\n publisher: this.buildUserPublisher(me, params.appId),\n };\n }\n\n if (envAdminToken) {\n return {\n token: envAdminToken,\n publisher: this.buildOfficialPublisher(),\n };\n }\n\n throw new Error(\"发布需要 NextClaw 平台登录态。请先运行 nextclaw login,或传入 --token。\");\n };\n\n private fetchCurrentPlatformUser = async (params: {\n token: string;\n platformApiBase: string;\n }): Promise<{\n id: string;\n username: string | null;\n role: \"admin\" | \"user\";\n }> => {\n const response = await fetch(`${params.platformApiBase}/platform/auth/me`, {\n headers: {\n authorization: `Bearer ${params.token}`,\n accept: \"application/json\",\n },\n });\n const payload = await response.json().catch(() => null);\n if (!response.ok) {\n const message =\n typeof payload === \"object\" &&\n payload &&\n \"error\" in payload &&\n typeof (payload as { error?: { message?: unknown } }).error?.message === \"string\"\n ? (payload as { error: { message: string } }).error.message\n : `${response.status} ${response.statusText}`;\n throw new Error(`读取 NextClaw 登录态失败:${message}`);\n }\n const user = typeof payload === \"object\" &&\n payload &&\n \"data\" in payload &&\n typeof (payload as { data?: { user?: unknown } }).data?.user === \"object\" &&\n (payload as { data: { user: Record<string, unknown> } }).data.user\n ? (payload as { data: { user: Record<string, unknown> } }).data.user\n : null;\n const id = typeof user?.id === \"string\" ? user.id.trim() : \"\";\n const username = typeof user?.username === \"string\" ? user.username.trim() : \"\";\n const role = user?.role === \"admin\" ? \"admin\" : \"user\";\n if (!id) {\n throw new Error(\"平台登录态缺少用户 id。\");\n }\n return {\n id,\n username: username || null,\n role,\n };\n };\n\n private resolvePlatformApiBase = (configuredApiBase?: string): string => {\n const source = configuredApiBase?.trim() || process.env.NEXTCLAW_PLATFORM_API_BASE?.trim() || DEFAULT_PLATFORM_API_BASE;\n const normalized = new URL(source);\n normalized.pathname = normalized.pathname.replace(/\\/v1\\/?$/, \"/\");\n return normalized.toString().replace(/\\/+$/, \"\");\n };\n\n private buildUserPublisher = (\n user: { id: string; username: string | null; role: \"admin\" | \"user\" },\n appId: string,\n ): NonNullable<AppPublishPayload[\"publisher\"]> => {\n const isOfficialScope = appId.startsWith(\"nextclaw.\");\n if (isOfficialScope && user.role === \"admin\") {\n return this.buildOfficialPublisher();\n }\n if (!user.username) {\n throw new Error(\"当前 NextClaw 账号还没有 username,无法发布个人 scope app。请先在平台账号页设置用户名。\");\n }\n return {\n id: user.username,\n name: user.username,\n url: `https://platform.nextclaw.io/account`,\n };\n };\n\n private buildOfficialPublisher = (): NonNullable<AppPublishPayload[\"publisher\"]> => {\n return {\n id: \"nextclaw\",\n name: \"NextClaw\",\n url: \"https://nextclaw.io\",\n };\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app-publish.service.js","names":[],"sources":["../../src/services/app-publish.service.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { AppBundleService } from \"#app-runtime/services/app-bundle.service.js\";\nimport type { AppDistributionMode } from \"#app-runtime/types/app-bundle.types.js\";\nimport { AppManifestService } from \"#app-runtime/services/app-manifest.service.js\";\nimport { AppMarketplaceClientService } from \"./app-marketplace-client.service.js\";\nimport { AppMarketplaceMetadataService } from \"./app-marketplace-metadata.service.js\";\nimport type { AppPublishPayload, AppPublishResult } from \"#app-runtime/types/app-publish.types.js\";\nimport { PlatformAuthStateService } from \"./platform-auth-state.service.js\";\n\nconst DEFAULT_PLATFORM_API_BASE = \"https://ai-gateway-api.nextclaw.io\";\n\ntype ResolvedPublishActor = {\n token: string;\n publisher: NonNullable<AppPublishPayload[\"publisher\"]>;\n};\n\nexport class AppPublishService {\n constructor(\n private readonly manifestService: AppManifestService = new AppManifestService(),\n private readonly bundleService: AppBundleService = new AppBundleService(),\n private readonly metadataService: AppMarketplaceMetadataService = new AppMarketplaceMetadataService(),\n private readonly marketplaceClient: AppMarketplaceClientService = new AppMarketplaceClientService(),\n private readonly authStateService: PlatformAuthStateService = new PlatformAuthStateService(),\n ) {}\n\n publish = async (params: {\n appDirectory: string;\n metadataPath?: string;\n bundleOutputPath?: string;\n apiBaseUrl?: string;\n token?: string;\n mode?: AppDistributionMode;\n }): Promise<AppPublishResult> => {\n const {\n appDirectory: inputAppDirectory,\n metadataPath,\n bundleOutputPath,\n apiBaseUrl,\n token,\n } = params;\n const appDirectory = path.resolve(inputAppDirectory);\n const manifestBundle = await this.manifestService.load(appDirectory);\n const distributionMode = params.mode ??\n (manifestBundle.manifest.schemaVersion === 2 ? \"bundle\" : \"source\");\n const metadata = await this.metadataService.load({\n appDirectory,\n manifest: manifestBundle.manifest,\n metadataPath,\n });\n const actor = await this.resolvePublishActor({\n apiBaseUrl,\n explicitToken: token,\n appId: manifestBundle.manifest.id,\n });\n const bundle = await this.bundleService.packAppDirectory({\n appDirectory,\n outputPath: bundleOutputPath,\n mode: distributionMode,\n });\n const bundleBytes = Buffer.from(await readFile(bundle.bundlePath));\n const bundleSha256 = createHash(\"sha256\").update(bundleBytes).digest(\"hex\");\n const publishFiles = await this.metadataService.collectPublishFiles({\n appDirectory,\n iconPath: manifestBundle.manifest.icon,\n metadataPath,\n visuals: metadata.visuals,\n });\n const payload: AppPublishPayload = {\n slug: metadata.slug,\n appId: manifestBundle.manifest.id,\n name: manifestBundle.manifest.name,\n version: manifestBundle.manifest.version,\n summary: metadata.summary,\n summaryI18n: metadata.summaryI18n,\n description: metadata.description ?? manifestBundle.manifest.description,\n descriptionI18n: metadata.descriptionI18n,\n author: metadata.author,\n tags: metadata.tags,\n sourceRepo: metadata.sourceRepo,\n homepage: metadata.homepage,\n featured: metadata.featured ?? false,\n publisher: actor.publisher,\n visuals: metadata.visuals,\n distributionMode,\n manifest: manifestBundle.manifest,\n permissions: manifestBundle.manifest.schemaVersion === 1\n ? manifestBundle.manifest.permissions ?? {}\n : this.manifestService.resolvePlatformSecurity(manifestBundle.manifest).permissions,\n bundleBase64: bundleBytes.toString(\"base64\"),\n bundleSha256,\n files: publishFiles.map((file) => ({\n path: file.path,\n contentBase64: file.bytes.toString(\"base64\"),\n })),\n };\n const result = await this.marketplaceClient.publish({\n payload,\n apiBaseUrl,\n token: actor.token,\n });\n return {\n ...result,\n distribution: {\n path: bundle.bundlePath,\n sha256: bundleSha256,\n mode: distributionMode,\n },\n };\n };\n\n private resolvePublishActor = async (params: {\n apiBaseUrl?: string;\n explicitToken?: string;\n appId: string;\n }): Promise<ResolvedPublishActor> => {\n const explicitToken = params.explicitToken?.trim();\n const envAdminToken = process.env.NEXTCLAW_MARKETPLACE_ADMIN_TOKEN?.trim();\n if (explicitToken) {\n const token = explicitToken;\n if (!token) {\n throw new Error(\"缺少 publish token。\");\n }\n const me = await this.fetchCurrentPlatformUser({\n token,\n platformApiBase: this.resolvePlatformApiBase(),\n });\n return {\n token,\n publisher: this.buildUserPublisher(me, params.appId),\n };\n }\n\n const authState = this.authStateService.readCurrentAuthState();\n const platformToken = authState.token?.trim();\n if (platformToken) {\n const me = await this.fetchCurrentPlatformUser({\n token: platformToken,\n platformApiBase: this.resolvePlatformApiBase(authState.apiBaseUrl),\n });\n return {\n token: platformToken,\n publisher: this.buildUserPublisher(me, params.appId),\n };\n }\n\n if (envAdminToken) {\n return {\n token: envAdminToken,\n publisher: this.buildOfficialPublisher(),\n };\n }\n\n throw new Error(\"发布需要 NextClaw 平台登录态。请先运行 nextclaw login,或传入 --token。\");\n };\n\n private fetchCurrentPlatformUser = async (params: {\n token: string;\n platformApiBase: string;\n }): Promise<{\n id: string;\n username: string | null;\n role: \"admin\" | \"user\";\n }> => {\n const response = await fetch(`${params.platformApiBase}/platform/auth/me`, {\n headers: {\n authorization: `Bearer ${params.token}`,\n accept: \"application/json\",\n },\n });\n const payload = await response.json().catch(() => null);\n if (!response.ok) {\n const message =\n typeof payload === \"object\" &&\n payload &&\n \"error\" in payload &&\n typeof (payload as { error?: { message?: unknown } }).error?.message === \"string\"\n ? (payload as { error: { message: string } }).error.message\n : `${response.status} ${response.statusText}`;\n throw new Error(`读取 NextClaw 登录态失败:${message}`);\n }\n const user = typeof payload === \"object\" &&\n payload &&\n \"data\" in payload &&\n typeof (payload as { data?: { user?: unknown } }).data?.user === \"object\" &&\n (payload as { data: { user: Record<string, unknown> } }).data.user\n ? (payload as { data: { user: Record<string, unknown> } }).data.user\n : null;\n const id = typeof user?.id === \"string\" ? user.id.trim() : \"\";\n const username = typeof user?.username === \"string\" ? user.username.trim() : \"\";\n const role = user?.role === \"admin\" ? \"admin\" : \"user\";\n if (!id) {\n throw new Error(\"平台登录态缺少用户 id。\");\n }\n return {\n id,\n username: username || null,\n role,\n };\n };\n\n private resolvePlatformApiBase = (configuredApiBase?: string): string => {\n const source = configuredApiBase?.trim() || process.env.NEXTCLAW_PLATFORM_API_BASE?.trim() || DEFAULT_PLATFORM_API_BASE;\n const normalized = new URL(source);\n normalized.pathname = normalized.pathname.replace(/\\/v1\\/?$/, \"/\");\n return normalized.toString().replace(/\\/+$/, \"\");\n };\n\n private buildUserPublisher = (\n user: { id: string; username: string | null; role: \"admin\" | \"user\" },\n appId: string,\n ): NonNullable<AppPublishPayload[\"publisher\"]> => {\n const isOfficialScope = appId.startsWith(\"nextclaw.\");\n if (isOfficialScope && user.role === \"admin\") {\n return this.buildOfficialPublisher();\n }\n if (!user.username) {\n throw new Error(\"当前 NextClaw 账号还没有 username,无法发布个人 scope app。请先在平台账号页设置用户名。\");\n }\n return {\n id: user.username,\n name: user.username,\n url: `https://platform.nextclaw.io/account`,\n };\n };\n\n private buildOfficialPublisher = (): NonNullable<AppPublishPayload[\"publisher\"]> => {\n return {\n id: \"nextclaw\",\n name: \"NextClaw\",\n url: \"https://nextclaw.io\",\n };\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { AppPermissions, AppResolvedComponent } from "../types/app-manifest.types.js";
|
|
1
|
+
import { AppPermissions, AppPlatformSecuritySummary, AppResolvedComponent } from "../types/app-manifest.types.js";
|
|
2
2
|
import { AppDocumentGrantMap } from "../types/app-permissions.types.js";
|
|
3
3
|
import { AppHomeService } from "./app-home.service.js";
|
|
4
|
+
import { AppInstanceRecord } from "../types/app-storage.types.js";
|
|
4
5
|
import { AppInstallSourceKind, AppRegistry, AppRegistryAppRecord, AppRegistryInstalledVersion } from "../types/app-registry.types.js";
|
|
5
6
|
|
|
6
7
|
//#region src/services/app-registry.service.d.ts
|
|
7
8
|
declare class AppRegistryService {
|
|
8
9
|
private readonly appHomeService;
|
|
9
|
-
private
|
|
10
|
+
private readonly instanceStorageService;
|
|
11
|
+
private readonly fileLockService;
|
|
10
12
|
constructor(appHomeService?: AppHomeService);
|
|
11
13
|
load: () => Promise<AppRegistry>;
|
|
12
14
|
save: (registry: AppRegistry) => Promise<void>;
|
|
@@ -20,7 +22,7 @@ declare class AppRegistryService {
|
|
|
20
22
|
description?: string;
|
|
21
23
|
version: string;
|
|
22
24
|
installDirectory: string;
|
|
23
|
-
|
|
25
|
+
defaultInstance: AppInstanceRecord;
|
|
24
26
|
sourceKind: AppInstallSourceKind;
|
|
25
27
|
sourceRef: string;
|
|
26
28
|
installedAt: string;
|
|
@@ -33,10 +35,15 @@ declare class AppRegistryService {
|
|
|
33
35
|
manifestSchemaVersion: 1 | 2;
|
|
34
36
|
components?: AppResolvedComponent[];
|
|
35
37
|
primaryPanelId?: string;
|
|
38
|
+
security?: AppPlatformSecuritySummary;
|
|
39
|
+
dataSchemaVersion: number;
|
|
40
|
+
contentSha256?: string;
|
|
36
41
|
enabled?: boolean;
|
|
42
|
+
activate?: boolean;
|
|
37
43
|
}) => Promise<AppRegistryAppRecord>;
|
|
38
44
|
setEnabled: (appId: string, enabled: boolean) => Promise<AppRegistryAppRecord>;
|
|
39
45
|
activateVersion: (appId: string, version: string) => Promise<AppRegistryAppRecord>;
|
|
46
|
+
setVersionContentDigest: (appId: string, version: string, contentSha256: string) => Promise<AppRegistryAppRecord>;
|
|
40
47
|
updateGrants: (appId: string, grants: AppDocumentGrantMap) => Promise<AppRegistryAppRecord>;
|
|
41
48
|
setDocumentGrant: (appId: string, scopeId: string, directoryPath: string) => Promise<AppRegistryAppRecord>;
|
|
42
49
|
removeDocumentGrant: (appId: string, scopeId: string) => Promise<boolean>;
|
|
@@ -46,6 +53,9 @@ declare class AppRegistryService {
|
|
|
46
53
|
private updateApp;
|
|
47
54
|
private withMutation;
|
|
48
55
|
private parseRegistry;
|
|
56
|
+
private parseDefaultInstance;
|
|
57
|
+
private assertExactPath;
|
|
58
|
+
private parseComponents;
|
|
49
59
|
private assertRecord;
|
|
50
60
|
private requireString;
|
|
51
61
|
private isMissingFileError;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-registry.service.d.ts","names":[],"sources":["../../src/services/app-registry.service.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-registry.service.d.ts","names":[],"sources":["../../src/services/app-registry.service.ts"],"mappings":";;;;;;;cAuBa,kBAAA;EAAA,iBAIkB,cAAA;EAAA,iBAHZ,sBAAA;EAAA,iBACA,eAAA;cAEY,cAAA,GAAgB,cAAA;EAI7C,IAAA,QAAiB,OAAA,CAAQ,WAAA;EAazB,IAAA,GAAc,QAAA,EAAU,WAAA,KAAc,OAAA;EAAA,QAI9B,YAAA;EAsBR,QAAA,QAAqB,OAAA,CAAQ,oBAAA;EAK7B,MAAA,GAAgB,KAAA,aAAgB,OAAA,CAAQ,oBAAA;EAKxC,gBAAA,GAA0B,KAAA,aAAgB,OAAA,CAAQ,2BAAA;EAKlD,kBAAA,GAA4B,MAAA;IAC1B,KAAA;IACA,IAAA;IACA,WAAA;IACA,OAAA;IACA,gBAAA;IACA,eAAA,EAAiB,iBAAA;IACjB,UAAA,EAAY,oBAAA;IACZ,SAAA;IACA,WAAA;IACA,gBAAA,GAAmB,2BAAA;IACnB,WAAA,EAAa,cAAA;IACb,WAAA;IACA,SAAA;IACA,MAAA;IACA,SAAA,GAAY,2BAAA;IACZ,qBAAA;IACA,UAAA,GAAa,oBAAA;IACb,cAAA;IACA,QAAA,GAAW,0BAAA;IACX,iBAAA;IACA,aAAA;IACA,OAAA;IACA,QAAA;EAAA,MACE,OAAA,CAAQ,oBAAA;EAoDZ,UAAA,GAAoB,KAAA,UAAe,OAAA,cAAmB,OAAA,CAAQ,oBAAA;EAI9D,eAAA,GACE,KAAA,UACA,OAAA,aACC,OAAA,CAAQ,oBAAA;EASX,uBAAA,GACE,KAAA,UACA,OAAA,UACA,aAAA,aACC,OAAA,CAAQ,oBAAA;EAmBX,YAAA,GACE,KAAA,UACA,MAAA,EAAQ,mBAAA,KACP,OAAA,CAAQ,oBAAA;EAOX,gBAAA,GACE,KAAA,UACA,OAAA,UACA,aAAA,aACC,OAAA,CAAQ,oBAAA;EAIX,mBAAA,GAA6B,KAAA,UAAe,OAAA,aAAkB,OAAA;EAc9D,SAAA,GAAmB,KAAA,aAAgB,OAAA,CAAQ,oBAAA;EAa3C,mBAAA,GAA6B,KAAA,aAAgB,OAAA;EAK7C,oBAAA,GAA8B,KAAA,UAAe,UAAA,cAAsB,OAAA;EAAA,QAY3D,SAAA;EAAA,QAiBA,YAAA;EAAA,QAKA,aAAA;EAAA,QA+FA,oBAAA;EAAA,QAyGA,eAAA;EAAA,QAMA,eAAA;EAAA,QA6DA,YAAA;EAAA,QAOA,aAAA;EAAA,QAOA,kBAAA;AAAA"}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { AppHomeService } from "./app-home.service.js";
|
|
2
|
+
import { FileLockService } from "./file-lock.service.js";
|
|
3
|
+
import { AppInstanceStorageService } from "./app-instance-storage.service.js";
|
|
2
4
|
import { open, readFile, rename, rm } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
//#region src/services/app-registry.service.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
const SAFE_APP_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
8
|
+
const SAFE_VERSION_PATTERN = /^[0-9A-Za-z]+(?:[._+-][0-9A-Za-z]+)*$/;
|
|
9
|
+
const SAFE_COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
var AppRegistryService = class {
|
|
11
|
+
instanceStorageService;
|
|
12
|
+
fileLockService = new FileLockService();
|
|
7
13
|
constructor(appHomeService = new AppHomeService()) {
|
|
8
14
|
this.appHomeService = appHomeService;
|
|
15
|
+
this.instanceStorageService = new AppInstanceStorageService(appHomeService);
|
|
9
16
|
}
|
|
10
17
|
load = async () => {
|
|
11
18
|
await this.appHomeService.ensureBaseDirectories();
|
|
@@ -57,13 +64,17 @@ var AppRegistryService = class AppRegistryService {
|
|
|
57
64
|
return await this.withMutation(async () => {
|
|
58
65
|
const registry = await this.load();
|
|
59
66
|
const currentRecord = registry.apps[params.appId];
|
|
67
|
+
const currentPublisher = currentRecord?.publisher ?? currentRecord?.installedVersions[currentRecord.activeVersion]?.publisher;
|
|
68
|
+
if (currentPublisher && currentPublisher.id !== params.publisher?.id) throw new Error(`应用 ${params.appId} 已绑定发布者 ${currentPublisher.id},拒绝由 ${params.publisher?.id ?? "未验证本地来源"} 覆盖。`);
|
|
60
69
|
const nextRecord = {
|
|
61
70
|
appId: params.appId,
|
|
62
71
|
name: params.name,
|
|
63
72
|
description: params.description,
|
|
64
|
-
|
|
73
|
+
publisher: currentPublisher ?? params.publisher,
|
|
74
|
+
activeVersion: params.activate === false && currentRecord ? currentRecord.activeVersion : params.version,
|
|
65
75
|
enabled: params.enabled ?? currentRecord?.enabled ?? params.manifestSchemaVersion === 1,
|
|
66
|
-
dataDirectory: params.dataDirectory,
|
|
76
|
+
dataDirectory: params.defaultInstance.storage.dataDirectory,
|
|
77
|
+
defaultInstance: params.defaultInstance,
|
|
67
78
|
installedVersions: {
|
|
68
79
|
...currentRecord?.installedVersions ?? {},
|
|
69
80
|
[params.version]: {
|
|
@@ -80,7 +91,10 @@ var AppRegistryService = class AppRegistryService {
|
|
|
80
91
|
publisher: params.publisher,
|
|
81
92
|
manifestSchemaVersion: params.manifestSchemaVersion,
|
|
82
93
|
components: params.components,
|
|
83
|
-
primaryPanelId: params.primaryPanelId
|
|
94
|
+
primaryPanelId: params.primaryPanelId,
|
|
95
|
+
security: params.security,
|
|
96
|
+
dataSchemaVersion: params.dataSchemaVersion,
|
|
97
|
+
contentSha256: params.contentSha256
|
|
84
98
|
}
|
|
85
99
|
},
|
|
86
100
|
grants: currentRecord?.grants ?? {}
|
|
@@ -105,6 +119,23 @@ var AppRegistryService = class AppRegistryService {
|
|
|
105
119
|
};
|
|
106
120
|
});
|
|
107
121
|
};
|
|
122
|
+
setVersionContentDigest = async (appId, version, contentSha256) => {
|
|
123
|
+
return await this.updateApp(appId, (record) => {
|
|
124
|
+
const installedVersion = record.installedVersions[version];
|
|
125
|
+
if (!installedVersion) throw new Error(`应用 ${appId} 未安装版本 ${version}。`);
|
|
126
|
+
if (installedVersion.contentSha256 && installedVersion.contentSha256 !== contentSha256) throw new Error(`应用 ${appId}@${version} 已存在不同的代码完整性摘要。`);
|
|
127
|
+
return {
|
|
128
|
+
...record,
|
|
129
|
+
installedVersions: {
|
|
130
|
+
...record.installedVersions,
|
|
131
|
+
[version]: {
|
|
132
|
+
...installedVersion,
|
|
133
|
+
contentSha256
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
};
|
|
108
139
|
updateGrants = async (appId, grants) => {
|
|
109
140
|
return await this.updateApp(appId, (record) => ({
|
|
110
141
|
...record,
|
|
@@ -166,13 +197,7 @@ var AppRegistryService = class AppRegistryService {
|
|
|
166
197
|
};
|
|
167
198
|
withMutation = async (operation) => {
|
|
168
199
|
const registryPath = path.resolve(this.appHomeService.getRegistryPath());
|
|
169
|
-
|
|
170
|
-
AppRegistryService.mutationQueues.set(registryPath, current);
|
|
171
|
-
try {
|
|
172
|
-
return await current;
|
|
173
|
-
} finally {
|
|
174
|
-
if (AppRegistryService.mutationQueues.get(registryPath) === current) AppRegistryService.mutationQueues.delete(registryPath);
|
|
175
|
-
}
|
|
200
|
+
return await this.fileLockService.withLock(`${registryPath}.lock`, operation);
|
|
176
201
|
};
|
|
177
202
|
parseRegistry = (rawRegistry) => {
|
|
178
203
|
const candidate = this.assertRecord(rawRegistry, "registry.json");
|
|
@@ -180,24 +205,37 @@ var AppRegistryService = class AppRegistryService {
|
|
|
180
205
|
const rawApps = this.assertRecord(candidate.apps, "registry.apps");
|
|
181
206
|
const apps = {};
|
|
182
207
|
for (const [appId, rawApp] of Object.entries(rawApps)) {
|
|
208
|
+
if (!SAFE_APP_ID_PATTERN.test(appId)) throw new Error(`registry.apps 包含不安全的 appId:${appId}`);
|
|
183
209
|
const app = this.assertRecord(rawApp, `registry.apps.${appId}`);
|
|
184
210
|
const installedVersions = {};
|
|
185
211
|
const rawVersions = this.assertRecord(app.installedVersions, `registry.apps.${appId}.installedVersions`);
|
|
186
212
|
for (const [version, rawVersion] of Object.entries(rawVersions)) {
|
|
213
|
+
if (!SAFE_VERSION_PATTERN.test(version)) throw new Error(`registry.apps.${appId} 包含不安全的版本:${version}`);
|
|
187
214
|
const versionRecord = this.assertRecord(rawVersion, `registry.apps.${appId}.installedVersions.${version}`);
|
|
215
|
+
const installDirectory = this.requireString(versionRecord.installDirectory, `registry.apps.${appId}.installedVersions.${version}.installDirectory`);
|
|
216
|
+
this.assertExactPath(installDirectory, this.appHomeService.getInstallDirectory(appId, version), `registry.apps.${appId}.installedVersions.${version}.installDirectory`);
|
|
188
217
|
installedVersions[version] = {
|
|
189
218
|
...versionRecord,
|
|
190
219
|
version,
|
|
191
|
-
|
|
220
|
+
installDirectory: path.resolve(installDirectory),
|
|
221
|
+
manifestSchemaVersion: versionRecord.manifestSchemaVersion === 2 ? 2 : 1,
|
|
222
|
+
components: this.parseComponents(versionRecord.components, installDirectory, `registry.apps.${appId}.installedVersions.${version}.components`),
|
|
223
|
+
dataSchemaVersion: typeof versionRecord.dataSchemaVersion === "number" && Number.isSafeInteger(versionRecord.dataSchemaVersion) && versionRecord.dataSchemaVersion > 0 ? versionRecord.dataSchemaVersion : 1
|
|
192
224
|
};
|
|
193
225
|
}
|
|
194
226
|
const activeVersion = this.requireString(app.activeVersion, `registry.apps.${appId}.activeVersion`);
|
|
195
227
|
if (!installedVersions[activeVersion]) throw new Error(`registry.apps.${appId} 缺少 activeVersion ${activeVersion}。`);
|
|
228
|
+
const dataDirectory = this.requireString(app.dataDirectory, `registry.apps.${appId}.dataDirectory`);
|
|
229
|
+
const firstInstalledAt = Object.values(installedVersions).map((version) => version.installedAt).filter((value) => typeof value === "string").sort()[0] ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
230
|
+
const defaultInstance = this.parseDefaultInstance(app.defaultInstance, appId, dataDirectory, firstInstalledAt);
|
|
196
231
|
apps[appId] = {
|
|
197
232
|
...app,
|
|
198
233
|
appId,
|
|
234
|
+
publisher: app.publisher ?? installedVersions[activeVersion]?.publisher,
|
|
199
235
|
enabled: typeof app.enabled === "boolean" ? app.enabled : true,
|
|
200
236
|
activeVersion,
|
|
237
|
+
dataDirectory: defaultInstance.storage.dataDirectory,
|
|
238
|
+
defaultInstance,
|
|
201
239
|
installedVersions,
|
|
202
240
|
grants: app.grants && typeof app.grants === "object" ? app.grants : {}
|
|
203
241
|
};
|
|
@@ -212,6 +250,98 @@ var AppRegistryService = class AppRegistryService {
|
|
|
212
250
|
})) : {}
|
|
213
251
|
};
|
|
214
252
|
};
|
|
253
|
+
parseDefaultInstance = (rawInstance, appId, dataDirectory, createdAt) => {
|
|
254
|
+
if (!rawInstance || typeof rawInstance !== "object" || Array.isArray(rawInstance)) {
|
|
255
|
+
this.assertExactPath(dataDirectory, this.appHomeService.getAppDataDirectory(appId), `registry.apps.${appId}.dataDirectory`);
|
|
256
|
+
return this.instanceStorageService.buildLegacyDefaultInstance({
|
|
257
|
+
appId,
|
|
258
|
+
dataDirectory,
|
|
259
|
+
createdAt
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const instance = rawInstance;
|
|
263
|
+
if (instance.id !== "default" || !instance.storage || typeof instance.storage !== "object" || instance.storage.layout !== "legacy" && instance.storage.layout !== "instance-v1") throw new Error(`registry.apps.${appId}.defaultInstance 无效。`);
|
|
264
|
+
if (instance.publisherId !== void 0 && typeof instance.publisherId !== "string") throw new Error(`registry.apps.${appId}.defaultInstance.publisherId 无效。`);
|
|
265
|
+
const dataSchemaVersion = typeof instance.dataSchemaVersion === "number" && Number.isSafeInteger(instance.dataSchemaVersion) && instance.dataSchemaVersion > 0 ? instance.dataSchemaVersion : 1;
|
|
266
|
+
const instanceCreatedAt = typeof instance.createdAt === "string" ? instance.createdAt : createdAt;
|
|
267
|
+
const instanceDirectory = path.resolve(this.appHomeService.getAppInstanceDirectory(appId, "default"));
|
|
268
|
+
const expectedStorage = {
|
|
269
|
+
layout: "instance-v1",
|
|
270
|
+
layoutVersion: 1,
|
|
271
|
+
instanceId: "default",
|
|
272
|
+
instanceDirectory,
|
|
273
|
+
dataDirectory: path.join(instanceDirectory, "data"),
|
|
274
|
+
configDirectory: path.join(instanceDirectory, "config"),
|
|
275
|
+
stateDirectory: path.join(instanceDirectory, "state"),
|
|
276
|
+
cacheDirectory: path.join(instanceDirectory, "cache"),
|
|
277
|
+
temporaryDirectory: path.join(instanceDirectory, "tmp"),
|
|
278
|
+
logsDirectory: path.join(instanceDirectory, "logs")
|
|
279
|
+
};
|
|
280
|
+
if (instance.storage.layout === "legacy") {
|
|
281
|
+
const expectedLegacyDataDirectory = this.appHomeService.getAppDataDirectory(appId);
|
|
282
|
+
this.assertExactPath(dataDirectory, expectedLegacyDataDirectory, `registry.apps.${appId}.dataDirectory`);
|
|
283
|
+
return {
|
|
284
|
+
id: "default",
|
|
285
|
+
publisherId: instance.publisherId,
|
|
286
|
+
storage: {
|
|
287
|
+
...expectedStorage,
|
|
288
|
+
layout: "legacy",
|
|
289
|
+
dataDirectory: path.resolve(expectedLegacyDataDirectory)
|
|
290
|
+
},
|
|
291
|
+
dataSchemaVersion,
|
|
292
|
+
createdAt: instanceCreatedAt,
|
|
293
|
+
migratedAt: instance.migratedAt,
|
|
294
|
+
legacyDataDirectory: path.resolve(expectedLegacyDataDirectory)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
for (const [field, expectedPath] of Object.entries({
|
|
298
|
+
instanceDirectory: expectedStorage.instanceDirectory,
|
|
299
|
+
dataDirectory: expectedStorage.dataDirectory,
|
|
300
|
+
configDirectory: expectedStorage.configDirectory,
|
|
301
|
+
stateDirectory: expectedStorage.stateDirectory,
|
|
302
|
+
cacheDirectory: expectedStorage.cacheDirectory,
|
|
303
|
+
temporaryDirectory: expectedStorage.temporaryDirectory,
|
|
304
|
+
logsDirectory: expectedStorage.logsDirectory
|
|
305
|
+
})) this.assertExactPath(instance.storage[field], expectedPath, `registry.apps.${appId}.defaultInstance.storage.${field}`);
|
|
306
|
+
this.assertExactPath(dataDirectory, expectedStorage.dataDirectory, `registry.apps.${appId}.dataDirectory`);
|
|
307
|
+
return {
|
|
308
|
+
id: "default",
|
|
309
|
+
publisherId: instance.publisherId,
|
|
310
|
+
storage: expectedStorage,
|
|
311
|
+
dataSchemaVersion,
|
|
312
|
+
createdAt: instanceCreatedAt,
|
|
313
|
+
migratedAt: instance.migratedAt,
|
|
314
|
+
legacyDataDirectory: instance.legacyDataDirectory
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
assertExactPath = (actual, expected, field) => {
|
|
318
|
+
if (typeof actual !== "string" || path.resolve(actual) !== path.resolve(expected)) throw new Error(`${field} 必须位于受管路径 ${path.resolve(expected)}。`);
|
|
319
|
+
};
|
|
320
|
+
parseComponents = (rawComponents, installDirectory, field) => {
|
|
321
|
+
if (rawComponents === void 0) return;
|
|
322
|
+
if (!Array.isArray(rawComponents)) throw new Error(`${field} 必须是数组。`);
|
|
323
|
+
return rawComponents.map((rawComponent, index) => {
|
|
324
|
+
const component = this.assertRecord(rawComponent, `${field}[${index}]`);
|
|
325
|
+
const kind = component.kind;
|
|
326
|
+
if (kind !== "panel" && kind !== "service") throw new Error(`${field}[${index}].kind 无效。`);
|
|
327
|
+
const id = this.requireString(component.id, `${field}[${index}].id`);
|
|
328
|
+
if (!SAFE_COMPONENT_ID_PATTERN.test(id)) throw new Error(`${field}[${index}].id 不安全。`);
|
|
329
|
+
const relativeComponentPath = this.requireString(component.path, `${field}[${index}].path`);
|
|
330
|
+
const expectedComponentDirectory = path.resolve(installDirectory, relativeComponentPath);
|
|
331
|
+
const relativeToInstall = path.relative(path.resolve(installDirectory), expectedComponentDirectory);
|
|
332
|
+
if (!relativeToInstall || relativeToInstall.startsWith("..") || path.isAbsolute(relativeToInstall)) throw new Error(`${field}[${index}].path 必须位于版本目录内。`);
|
|
333
|
+
const expectedManifestPath = path.join(expectedComponentDirectory, kind === "panel" ? "panel-app.json" : "service-app.json");
|
|
334
|
+
this.assertExactPath(component.componentDirectory, expectedComponentDirectory, `${field}[${index}].componentDirectory`);
|
|
335
|
+
this.assertExactPath(component.manifestPath, expectedManifestPath, `${field}[${index}].manifestPath`);
|
|
336
|
+
return {
|
|
337
|
+
kind,
|
|
338
|
+
id,
|
|
339
|
+
path: relativeComponentPath,
|
|
340
|
+
componentDirectory: expectedComponentDirectory,
|
|
341
|
+
manifestPath: expectedManifestPath
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
};
|
|
215
345
|
assertRecord = (value, field) => {
|
|
216
346
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} 必须是对象。`);
|
|
217
347
|
return value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-registry.service.js","names":[],"sources":["../../src/services/app-registry.service.ts"],"sourcesContent":["import { open, readFile, rename, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { AppPermissions, AppResolvedComponent } from \"#app-runtime/types/app-manifest.types.js\";\nimport type { AppDocumentGrantMap } from \"#app-runtime/types/app-permissions.types.js\";\nimport { AppHomeService } from \"#app-runtime/services/app-home.service.js\";\nimport type {\n AppInstallSourceKind,\n AppRegistry,\n AppRegistryAppRecord,\n AppRegistryInstalledVersion,\n} from \"#app-runtime/types/app-registry.types.js\";\n\nexport class AppRegistryService {\n private static readonly mutationQueues = new Map<string, Promise<unknown>>();\n\n constructor(private readonly appHomeService: AppHomeService = new AppHomeService()) {}\n\n load = async (): Promise<AppRegistry> => {\n await this.appHomeService.ensureBaseDirectories();\n try {\n const raw = await readFile(this.appHomeService.getRegistryPath(), \"utf-8\");\n return this.parseRegistry(JSON.parse(raw) as unknown);\n } catch (error) {\n if (this.isMissingFileError(error)) {\n return { schemaVersion: 1, apps: {}, suppressedBuiltIns: {} };\n }\n throw error;\n }\n };\n\n save = async (registry: AppRegistry): Promise<void> => {\n await this.withMutation(async () => await this.saveUnlocked(registry));\n };\n\n private saveUnlocked = async (registry: AppRegistry): Promise<void> => {\n await this.appHomeService.ensureBaseDirectories();\n const registryPath = this.appHomeService.getRegistryPath();\n const temporaryPath = path.join(\n path.dirname(registryPath),\n `.${path.basename(registryPath)}.${process.pid}.${Date.now()}.tmp`,\n );\n const handle = await open(temporaryPath, \"wx\", 0o600);\n try {\n await handle.writeFile(`${JSON.stringify(registry, null, 2)}\\n`, \"utf-8\");\n await handle.sync();\n } finally {\n await handle.close();\n }\n try {\n await rename(temporaryPath, registryPath);\n } catch (error) {\n await rm(temporaryPath, { force: true });\n throw error;\n }\n };\n\n listApps = async (): Promise<AppRegistryAppRecord[]> => {\n const registry = await this.load();\n return Object.values(registry.apps).sort((left, right) => left.appId.localeCompare(right.appId));\n };\n\n getApp = async (appId: string): Promise<AppRegistryAppRecord | undefined> => {\n const registry = await this.load();\n return registry.apps[appId];\n };\n\n getActiveVersion = async (appId: string): Promise<AppRegistryInstalledVersion | undefined> => {\n const appRecord = await this.getApp(appId);\n return appRecord?.installedVersions[appRecord.activeVersion];\n };\n\n upsertInstallation = async (params: {\n appId: string;\n name: string;\n description?: string;\n version: string;\n installDirectory: string;\n dataDirectory: string;\n sourceKind: AppInstallSourceKind;\n sourceRef: string;\n installedAt: string;\n distributionMode?: AppRegistryInstalledVersion[\"distributionMode\"];\n permissions: AppPermissions;\n registryUrl?: string;\n bundleUrl?: string;\n sha256?: string;\n publisher?: AppRegistryInstalledVersion[\"publisher\"];\n manifestSchemaVersion: 1 | 2;\n components?: AppResolvedComponent[];\n primaryPanelId?: string;\n enabled?: boolean;\n }): Promise<AppRegistryAppRecord> => {\n return await this.withMutation(async () => {\n const registry = await this.load();\n const currentRecord = registry.apps[params.appId];\n const nextRecord: AppRegistryAppRecord = {\n appId: params.appId,\n name: params.name,\n description: params.description,\n activeVersion: params.version,\n enabled: params.enabled ?? currentRecord?.enabled ?? params.manifestSchemaVersion === 1,\n dataDirectory: params.dataDirectory,\n installedVersions: {\n ...(currentRecord?.installedVersions ?? {}),\n [params.version]: {\n version: params.version,\n installDirectory: params.installDirectory,\n sourceKind: params.sourceKind,\n sourceRef: params.sourceRef,\n installedAt: params.installedAt,\n distributionMode: params.distributionMode,\n permissions: params.permissions,\n registryUrl: params.registryUrl,\n bundleUrl: params.bundleUrl,\n sha256: params.sha256,\n publisher: params.publisher,\n manifestSchemaVersion: params.manifestSchemaVersion,\n components: params.components,\n primaryPanelId: params.primaryPanelId,\n },\n },\n grants: currentRecord?.grants ?? {},\n };\n registry.apps[params.appId] = nextRecord;\n await this.saveUnlocked(registry);\n return nextRecord;\n });\n };\n\n setEnabled = async (appId: string, enabled: boolean): Promise<AppRegistryAppRecord> => {\n return await this.updateApp(appId, (record) => ({ ...record, enabled }));\n };\n\n activateVersion = async (\n appId: string,\n version: string,\n ): Promise<AppRegistryAppRecord> => {\n return await this.updateApp(appId, (record) => {\n if (!record.installedVersions[version]) {\n throw new Error(`应用 ${appId} 未安装版本 ${version}。`);\n }\n return { ...record, activeVersion: version };\n });\n };\n\n updateGrants = async (\n appId: string,\n grants: AppDocumentGrantMap,\n ): Promise<AppRegistryAppRecord> => {\n return await this.updateApp(appId, (record) => ({\n ...record,\n grants: { ...record.grants, ...grants },\n }));\n };\n\n setDocumentGrant = async (\n appId: string,\n scopeId: string,\n directoryPath: string,\n ): Promise<AppRegistryAppRecord> => {\n return await this.updateGrants(appId, { [scopeId]: directoryPath });\n };\n\n removeDocumentGrant = async (appId: string, scopeId: string): Promise<boolean> => {\n let removed = false;\n await this.updateApp(appId, (record) => {\n if (!(scopeId in record.grants)) {\n return record;\n }\n const grants = { ...record.grants };\n delete grants[scopeId];\n removed = true;\n return { ...record, grants };\n });\n return removed;\n };\n\n removeApp = async (appId: string): Promise<AppRegistryAppRecord | undefined> => {\n return await this.withMutation(async () => {\n const registry = await this.load();\n const appRecord = registry.apps[appId];\n if (!appRecord) {\n return undefined;\n }\n delete registry.apps[appId];\n await this.saveUnlocked(registry);\n return appRecord;\n });\n };\n\n isBuiltInSuppressed = async (appId: string): Promise<boolean> => {\n const registry = await this.load();\n return Boolean(registry.suppressedBuiltIns[appId]);\n };\n\n setBuiltInSuppressed = async (appId: string, suppressed: boolean): Promise<void> => {\n await this.withMutation(async () => {\n const registry = await this.load();\n if (suppressed) {\n registry.suppressedBuiltIns[appId] = { suppressedAt: new Date().toISOString() };\n } else {\n delete registry.suppressedBuiltIns[appId];\n }\n await this.saveUnlocked(registry);\n });\n };\n\n private updateApp = async (\n appId: string,\n update: (record: AppRegistryAppRecord) => AppRegistryAppRecord,\n ): Promise<AppRegistryAppRecord> => {\n return await this.withMutation(async () => {\n const registry = await this.load();\n const appRecord = registry.apps[appId];\n if (!appRecord) {\n throw new Error(`未找到已安装应用:${appId}`);\n }\n const nextRecord = update(appRecord);\n registry.apps[appId] = nextRecord;\n await this.saveUnlocked(registry);\n return nextRecord;\n });\n };\n\n private withMutation = async <T>(operation: () => Promise<T>): Promise<T> => {\n const registryPath = path.resolve(this.appHomeService.getRegistryPath());\n const previous = AppRegistryService.mutationQueues.get(registryPath) ?? Promise.resolve();\n const current = previous.catch(() => undefined).then(operation);\n AppRegistryService.mutationQueues.set(registryPath, current);\n try {\n return await current;\n } finally {\n if (AppRegistryService.mutationQueues.get(registryPath) === current) {\n AppRegistryService.mutationQueues.delete(registryPath);\n }\n }\n };\n\n private parseRegistry = (rawRegistry: unknown): AppRegistry => {\n const candidate = this.assertRecord(rawRegistry, \"registry.json\");\n if (candidate.schemaVersion !== 1) {\n throw new Error(\"当前只支持 registry schemaVersion = 1。\");\n }\n const rawApps = this.assertRecord(candidate.apps, \"registry.apps\");\n const apps: Record<string, AppRegistryAppRecord> = {};\n for (const [appId, rawApp] of Object.entries(rawApps)) {\n const app = this.assertRecord(rawApp, `registry.apps.${appId}`) as Partial<AppRegistryAppRecord>;\n const installedVersions: Record<string, AppRegistryInstalledVersion> = {};\n const rawVersions = this.assertRecord(\n app.installedVersions,\n `registry.apps.${appId}.installedVersions`,\n );\n for (const [version, rawVersion] of Object.entries(rawVersions)) {\n const versionRecord = this.assertRecord(\n rawVersion,\n `registry.apps.${appId}.installedVersions.${version}`,\n ) as Partial<AppRegistryInstalledVersion>;\n installedVersions[version] = {\n ...(versionRecord as AppRegistryInstalledVersion),\n version,\n manifestSchemaVersion: versionRecord.manifestSchemaVersion === 2 ? 2 : 1,\n };\n }\n const activeVersion = this.requireString(app.activeVersion, `registry.apps.${appId}.activeVersion`);\n if (!installedVersions[activeVersion]) {\n throw new Error(`registry.apps.${appId} 缺少 activeVersion ${activeVersion}。`);\n }\n apps[appId] = {\n ...(app as AppRegistryAppRecord),\n appId,\n enabled: typeof app.enabled === \"boolean\" ? app.enabled : true,\n activeVersion,\n installedVersions,\n grants: app.grants && typeof app.grants === \"object\" ? app.grants : {},\n };\n }\n const suppressedBuiltIns = candidate.suppressedBuiltIns &&\n typeof candidate.suppressedBuiltIns === \"object\" &&\n !Array.isArray(candidate.suppressedBuiltIns)\n ? Object.fromEntries(Object.entries(candidate.suppressedBuiltIns).flatMap(([appId, raw]) => {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n return [];\n }\n const suppressedAt = (raw as { suppressedAt?: unknown }).suppressedAt;\n return typeof suppressedAt === \"string\" ? [[appId, { suppressedAt }]] : [];\n }))\n : {};\n return { schemaVersion: 1, apps, suppressedBuiltIns };\n };\n\n private assertRecord = (value: unknown, field: string): Record<string, unknown> => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(`${field} 必须是对象。`);\n }\n return value as Record<string, unknown>;\n };\n\n private requireString = (value: unknown, field: string): string => {\n if (typeof value !== \"string\" || !value.trim()) {\n throw new Error(`${field} 必须是非空字符串。`);\n }\n return value;\n };\n\n private isMissingFileError = (error: unknown): boolean =>\n typeof error === \"object\" && error !== null &&\n \"code\" in error && (error as { code?: unknown }).code === \"ENOENT\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app-registry.service.js","names":[],"sources":["../../src/services/app-registry.service.ts"],"sourcesContent":["import { open, readFile, rename, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type {\n AppPermissions,\n AppPlatformSecuritySummary,\n AppResolvedComponent,\n} from \"#app-runtime/types/app-manifest.types.js\";\nimport type { AppDocumentGrantMap } from \"#app-runtime/types/app-permissions.types.js\";\nimport { AppHomeService } from \"#app-runtime/services/app-home.service.js\";\nimport { AppInstanceStorageService } from \"#app-runtime/services/app-instance-storage.service.js\";\nimport { FileLockService } from \"#app-runtime/services/file-lock.service.js\";\nimport type { AppInstanceRecord } from \"#app-runtime/types/app-storage.types.js\";\nimport type {\n AppInstallSourceKind,\n AppRegistry,\n AppRegistryAppRecord,\n AppRegistryInstalledVersion,\n} from \"#app-runtime/types/app-registry.types.js\";\n\nconst SAFE_APP_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;\nconst SAFE_VERSION_PATTERN = /^[0-9A-Za-z]+(?:[._+-][0-9A-Za-z]+)*$/;\nconst SAFE_COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport class AppRegistryService {\n private readonly instanceStorageService: AppInstanceStorageService;\n private readonly fileLockService = new FileLockService();\n\n constructor(private readonly appHomeService: AppHomeService = new AppHomeService()) {\n this.instanceStorageService = new AppInstanceStorageService(appHomeService);\n }\n\n load = async (): Promise<AppRegistry> => {\n await this.appHomeService.ensureBaseDirectories();\n try {\n const raw = await readFile(this.appHomeService.getRegistryPath(), \"utf-8\");\n return this.parseRegistry(JSON.parse(raw) as unknown);\n } catch (error) {\n if (this.isMissingFileError(error)) {\n return { schemaVersion: 1, apps: {}, suppressedBuiltIns: {} };\n }\n throw error;\n }\n };\n\n save = async (registry: AppRegistry): Promise<void> => {\n await this.withMutation(async () => await this.saveUnlocked(registry));\n };\n\n private saveUnlocked = async (registry: AppRegistry): Promise<void> => {\n await this.appHomeService.ensureBaseDirectories();\n const registryPath = this.appHomeService.getRegistryPath();\n const temporaryPath = path.join(\n path.dirname(registryPath),\n `.${path.basename(registryPath)}.${process.pid}.${Date.now()}.tmp`,\n );\n const handle = await open(temporaryPath, \"wx\", 0o600);\n try {\n await handle.writeFile(`${JSON.stringify(registry, null, 2)}\\n`, \"utf-8\");\n await handle.sync();\n } finally {\n await handle.close();\n }\n try {\n await rename(temporaryPath, registryPath);\n } catch (error) {\n await rm(temporaryPath, { force: true });\n throw error;\n }\n };\n\n listApps = async (): Promise<AppRegistryAppRecord[]> => {\n const registry = await this.load();\n return Object.values(registry.apps).sort((left, right) => left.appId.localeCompare(right.appId));\n };\n\n getApp = async (appId: string): Promise<AppRegistryAppRecord | undefined> => {\n const registry = await this.load();\n return registry.apps[appId];\n };\n\n getActiveVersion = async (appId: string): Promise<AppRegistryInstalledVersion | undefined> => {\n const appRecord = await this.getApp(appId);\n return appRecord?.installedVersions[appRecord.activeVersion];\n };\n\n upsertInstallation = async (params: {\n appId: string;\n name: string;\n description?: string;\n version: string;\n installDirectory: string;\n defaultInstance: AppInstanceRecord;\n sourceKind: AppInstallSourceKind;\n sourceRef: string;\n installedAt: string;\n distributionMode?: AppRegistryInstalledVersion[\"distributionMode\"];\n permissions: AppPermissions;\n registryUrl?: string;\n bundleUrl?: string;\n sha256?: string;\n publisher?: AppRegistryInstalledVersion[\"publisher\"];\n manifestSchemaVersion: 1 | 2;\n components?: AppResolvedComponent[];\n primaryPanelId?: string;\n security?: AppPlatformSecuritySummary;\n dataSchemaVersion: number;\n contentSha256?: string;\n enabled?: boolean;\n activate?: boolean;\n }): Promise<AppRegistryAppRecord> => {\n return await this.withMutation(async () => {\n const registry = await this.load();\n const currentRecord = registry.apps[params.appId];\n const currentPublisher = currentRecord?.publisher ??\n currentRecord?.installedVersions[currentRecord.activeVersion]?.publisher;\n if (currentPublisher && currentPublisher.id !== params.publisher?.id) {\n throw new Error(\n `应用 ${params.appId} 已绑定发布者 ${currentPublisher.id},拒绝由 ${params.publisher?.id ?? \"未验证本地来源\"} 覆盖。`,\n );\n }\n const nextRecord: AppRegistryAppRecord = {\n appId: params.appId,\n name: params.name,\n description: params.description,\n publisher: currentPublisher ?? params.publisher,\n activeVersion: params.activate === false && currentRecord\n ? currentRecord.activeVersion\n : params.version,\n enabled: params.enabled ?? currentRecord?.enabled ?? params.manifestSchemaVersion === 1,\n dataDirectory: params.defaultInstance.storage.dataDirectory,\n defaultInstance: params.defaultInstance,\n installedVersions: {\n ...(currentRecord?.installedVersions ?? {}),\n [params.version]: {\n version: params.version,\n installDirectory: params.installDirectory,\n sourceKind: params.sourceKind,\n sourceRef: params.sourceRef,\n installedAt: params.installedAt,\n distributionMode: params.distributionMode,\n permissions: params.permissions,\n registryUrl: params.registryUrl,\n bundleUrl: params.bundleUrl,\n sha256: params.sha256,\n publisher: params.publisher,\n manifestSchemaVersion: params.manifestSchemaVersion,\n components: params.components,\n primaryPanelId: params.primaryPanelId,\n security: params.security,\n dataSchemaVersion: params.dataSchemaVersion,\n contentSha256: params.contentSha256,\n },\n },\n grants: currentRecord?.grants ?? {},\n };\n registry.apps[params.appId] = nextRecord;\n await this.saveUnlocked(registry);\n return nextRecord;\n });\n };\n\n setEnabled = async (appId: string, enabled: boolean): Promise<AppRegistryAppRecord> => {\n return await this.updateApp(appId, (record) => ({ ...record, enabled }));\n };\n\n activateVersion = async (\n appId: string,\n version: string,\n ): Promise<AppRegistryAppRecord> => {\n return await this.updateApp(appId, (record) => {\n if (!record.installedVersions[version]) {\n throw new Error(`应用 ${appId} 未安装版本 ${version}。`);\n }\n return { ...record, activeVersion: version };\n });\n };\n\n setVersionContentDigest = async (\n appId: string,\n version: string,\n contentSha256: string,\n ): Promise<AppRegistryAppRecord> => {\n return await this.updateApp(appId, (record) => {\n const installedVersion = record.installedVersions[version];\n if (!installedVersion) {\n throw new Error(`应用 ${appId} 未安装版本 ${version}。`);\n }\n if (installedVersion.contentSha256 && installedVersion.contentSha256 !== contentSha256) {\n throw new Error(`应用 ${appId}@${version} 已存在不同的代码完整性摘要。`);\n }\n return {\n ...record,\n installedVersions: {\n ...record.installedVersions,\n [version]: { ...installedVersion, contentSha256 },\n },\n };\n });\n };\n\n updateGrants = async (\n appId: string,\n grants: AppDocumentGrantMap,\n ): Promise<AppRegistryAppRecord> => {\n return await this.updateApp(appId, (record) => ({\n ...record,\n grants: { ...record.grants, ...grants },\n }));\n };\n\n setDocumentGrant = async (\n appId: string,\n scopeId: string,\n directoryPath: string,\n ): Promise<AppRegistryAppRecord> => {\n return await this.updateGrants(appId, { [scopeId]: directoryPath });\n };\n\n removeDocumentGrant = async (appId: string, scopeId: string): Promise<boolean> => {\n let removed = false;\n await this.updateApp(appId, (record) => {\n if (!(scopeId in record.grants)) {\n return record;\n }\n const grants = { ...record.grants };\n delete grants[scopeId];\n removed = true;\n return { ...record, grants };\n });\n return removed;\n };\n\n removeApp = async (appId: string): Promise<AppRegistryAppRecord | undefined> => {\n return await this.withMutation(async () => {\n const registry = await this.load();\n const appRecord = registry.apps[appId];\n if (!appRecord) {\n return undefined;\n }\n delete registry.apps[appId];\n await this.saveUnlocked(registry);\n return appRecord;\n });\n };\n\n isBuiltInSuppressed = async (appId: string): Promise<boolean> => {\n const registry = await this.load();\n return Boolean(registry.suppressedBuiltIns[appId]);\n };\n\n setBuiltInSuppressed = async (appId: string, suppressed: boolean): Promise<void> => {\n await this.withMutation(async () => {\n const registry = await this.load();\n if (suppressed) {\n registry.suppressedBuiltIns[appId] = { suppressedAt: new Date().toISOString() };\n } else {\n delete registry.suppressedBuiltIns[appId];\n }\n await this.saveUnlocked(registry);\n });\n };\n\n private updateApp = async (\n appId: string,\n update: (record: AppRegistryAppRecord) => AppRegistryAppRecord,\n ): Promise<AppRegistryAppRecord> => {\n return await this.withMutation(async () => {\n const registry = await this.load();\n const appRecord = registry.apps[appId];\n if (!appRecord) {\n throw new Error(`未找到已安装应用:${appId}`);\n }\n const nextRecord = update(appRecord);\n registry.apps[appId] = nextRecord;\n await this.saveUnlocked(registry);\n return nextRecord;\n });\n };\n\n private withMutation = async <T>(operation: () => Promise<T>): Promise<T> => {\n const registryPath = path.resolve(this.appHomeService.getRegistryPath());\n return await this.fileLockService.withLock(`${registryPath}.lock`, operation);\n };\n\n private parseRegistry = (rawRegistry: unknown): AppRegistry => {\n const candidate = this.assertRecord(rawRegistry, \"registry.json\");\n if (candidate.schemaVersion !== 1) {\n throw new Error(\"当前只支持 registry schemaVersion = 1。\");\n }\n const rawApps = this.assertRecord(candidate.apps, \"registry.apps\");\n const apps: Record<string, AppRegistryAppRecord> = {};\n for (const [appId, rawApp] of Object.entries(rawApps)) {\n if (!SAFE_APP_ID_PATTERN.test(appId)) {\n throw new Error(`registry.apps 包含不安全的 appId:${appId}`);\n }\n const app = this.assertRecord(rawApp, `registry.apps.${appId}`) as Partial<AppRegistryAppRecord>;\n const installedVersions: Record<string, AppRegistryInstalledVersion> = {};\n const rawVersions = this.assertRecord(\n app.installedVersions,\n `registry.apps.${appId}.installedVersions`,\n );\n for (const [version, rawVersion] of Object.entries(rawVersions)) {\n if (!SAFE_VERSION_PATTERN.test(version)) {\n throw new Error(`registry.apps.${appId} 包含不安全的版本:${version}`);\n }\n const versionRecord = this.assertRecord(\n rawVersion,\n `registry.apps.${appId}.installedVersions.${version}`,\n ) as Partial<AppRegistryInstalledVersion>;\n const installDirectory = this.requireString(\n versionRecord.installDirectory,\n `registry.apps.${appId}.installedVersions.${version}.installDirectory`,\n );\n this.assertExactPath(\n installDirectory,\n this.appHomeService.getInstallDirectory(appId, version),\n `registry.apps.${appId}.installedVersions.${version}.installDirectory`,\n );\n installedVersions[version] = {\n ...(versionRecord as AppRegistryInstalledVersion),\n version,\n installDirectory: path.resolve(installDirectory),\n manifestSchemaVersion: versionRecord.manifestSchemaVersion === 2 ? 2 : 1,\n components: this.parseComponents(\n versionRecord.components,\n installDirectory,\n `registry.apps.${appId}.installedVersions.${version}.components`,\n ),\n dataSchemaVersion: typeof versionRecord.dataSchemaVersion === \"number\" &&\n Number.isSafeInteger(versionRecord.dataSchemaVersion) &&\n versionRecord.dataSchemaVersion > 0\n ? versionRecord.dataSchemaVersion\n : 1,\n };\n }\n const activeVersion = this.requireString(app.activeVersion, `registry.apps.${appId}.activeVersion`);\n if (!installedVersions[activeVersion]) {\n throw new Error(`registry.apps.${appId} 缺少 activeVersion ${activeVersion}。`);\n }\n const dataDirectory = this.requireString(\n app.dataDirectory,\n `registry.apps.${appId}.dataDirectory`,\n );\n const firstInstalledAt = Object.values(installedVersions)\n .map((version) => version.installedAt)\n .filter((value): value is string => typeof value === \"string\")\n .sort()[0] ?? new Date(0).toISOString();\n const defaultInstance = this.parseDefaultInstance(\n app.defaultInstance,\n appId,\n dataDirectory,\n firstInstalledAt,\n );\n apps[appId] = {\n ...(app as AppRegistryAppRecord),\n appId,\n publisher: app.publisher ?? installedVersions[activeVersion]?.publisher,\n enabled: typeof app.enabled === \"boolean\" ? app.enabled : true,\n activeVersion,\n dataDirectory: defaultInstance.storage.dataDirectory,\n defaultInstance,\n installedVersions,\n grants: app.grants && typeof app.grants === \"object\" ? app.grants : {},\n };\n }\n const suppressedBuiltIns = candidate.suppressedBuiltIns &&\n typeof candidate.suppressedBuiltIns === \"object\" &&\n !Array.isArray(candidate.suppressedBuiltIns)\n ? Object.fromEntries(Object.entries(candidate.suppressedBuiltIns).flatMap(([appId, raw]) => {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n return [];\n }\n const suppressedAt = (raw as { suppressedAt?: unknown }).suppressedAt;\n return typeof suppressedAt === \"string\" ? [[appId, { suppressedAt }]] : [];\n }))\n : {};\n return { schemaVersion: 1, apps, suppressedBuiltIns };\n };\n\n private parseDefaultInstance = (\n rawInstance: unknown,\n appId: string,\n dataDirectory: string,\n createdAt: string,\n ): AppInstanceRecord => {\n if (!rawInstance || typeof rawInstance !== \"object\" || Array.isArray(rawInstance)) {\n this.assertExactPath(\n dataDirectory,\n this.appHomeService.getAppDataDirectory(appId),\n `registry.apps.${appId}.dataDirectory`,\n );\n return this.instanceStorageService.buildLegacyDefaultInstance({\n appId,\n dataDirectory,\n createdAt,\n });\n }\n const instance = rawInstance as Partial<AppInstanceRecord>;\n if (\n instance.id !== \"default\" ||\n !instance.storage ||\n typeof instance.storage !== \"object\" ||\n (instance.storage.layout !== \"legacy\" && instance.storage.layout !== \"instance-v1\")\n ) {\n throw new Error(`registry.apps.${appId}.defaultInstance 无效。`);\n }\n if (instance.publisherId !== undefined && typeof instance.publisherId !== \"string\") {\n throw new Error(`registry.apps.${appId}.defaultInstance.publisherId 无效。`);\n }\n const dataSchemaVersion = typeof instance.dataSchemaVersion === \"number\" &&\n Number.isSafeInteger(instance.dataSchemaVersion) &&\n instance.dataSchemaVersion > 0\n ? instance.dataSchemaVersion\n : 1;\n const instanceCreatedAt = typeof instance.createdAt === \"string\"\n ? instance.createdAt\n : createdAt;\n const instanceDirectory = path.resolve(\n this.appHomeService.getAppInstanceDirectory(appId, \"default\"),\n );\n const expectedStorage = {\n layout: \"instance-v1\" as const,\n layoutVersion: 1 as const,\n instanceId: \"default\",\n instanceDirectory,\n dataDirectory: path.join(instanceDirectory, \"data\"),\n configDirectory: path.join(instanceDirectory, \"config\"),\n stateDirectory: path.join(instanceDirectory, \"state\"),\n cacheDirectory: path.join(instanceDirectory, \"cache\"),\n temporaryDirectory: path.join(instanceDirectory, \"tmp\"),\n logsDirectory: path.join(instanceDirectory, \"logs\"),\n };\n if (instance.storage.layout === \"legacy\") {\n const expectedLegacyDataDirectory = this.appHomeService.getAppDataDirectory(appId);\n this.assertExactPath(\n dataDirectory,\n expectedLegacyDataDirectory,\n `registry.apps.${appId}.dataDirectory`,\n );\n return {\n id: \"default\",\n publisherId: instance.publisherId,\n storage: {\n ...expectedStorage,\n layout: \"legacy\",\n dataDirectory: path.resolve(expectedLegacyDataDirectory),\n },\n dataSchemaVersion,\n createdAt: instanceCreatedAt,\n migratedAt: instance.migratedAt,\n legacyDataDirectory: path.resolve(expectedLegacyDataDirectory),\n };\n }\n for (const [field, expectedPath] of Object.entries({\n instanceDirectory: expectedStorage.instanceDirectory,\n dataDirectory: expectedStorage.dataDirectory,\n configDirectory: expectedStorage.configDirectory,\n stateDirectory: expectedStorage.stateDirectory,\n cacheDirectory: expectedStorage.cacheDirectory,\n temporaryDirectory: expectedStorage.temporaryDirectory,\n logsDirectory: expectedStorage.logsDirectory,\n })) {\n this.assertExactPath(\n instance.storage[field as keyof typeof instance.storage] as string,\n expectedPath,\n `registry.apps.${appId}.defaultInstance.storage.${field}`,\n );\n }\n this.assertExactPath(\n dataDirectory,\n expectedStorage.dataDirectory,\n `registry.apps.${appId}.dataDirectory`,\n );\n return {\n id: \"default\",\n publisherId: instance.publisherId,\n storage: expectedStorage,\n dataSchemaVersion,\n createdAt: instanceCreatedAt,\n migratedAt: instance.migratedAt,\n legacyDataDirectory: instance.legacyDataDirectory,\n };\n };\n\n private assertExactPath = (actual: unknown, expected: string, field: string): void => {\n if (typeof actual !== \"string\" || path.resolve(actual) !== path.resolve(expected)) {\n throw new Error(`${field} 必须位于受管路径 ${path.resolve(expected)}。`);\n }\n };\n\n private parseComponents = (\n rawComponents: unknown,\n installDirectory: string,\n field: string,\n ): AppResolvedComponent[] | undefined => {\n if (rawComponents === undefined) {\n return undefined;\n }\n if (!Array.isArray(rawComponents)) {\n throw new Error(`${field} 必须是数组。`);\n }\n return rawComponents.map((rawComponent, index) => {\n const component = this.assertRecord(rawComponent, `${field}[${index}]`);\n const kind = component.kind;\n if (kind !== \"panel\" && kind !== \"service\") {\n throw new Error(`${field}[${index}].kind 无效。`);\n }\n const id = this.requireString(component.id, `${field}[${index}].id`);\n if (!SAFE_COMPONENT_ID_PATTERN.test(id)) {\n throw new Error(`${field}[${index}].id 不安全。`);\n }\n const relativeComponentPath = this.requireString(\n component.path,\n `${field}[${index}].path`,\n );\n const expectedComponentDirectory = path.resolve(installDirectory, relativeComponentPath);\n const relativeToInstall = path.relative(\n path.resolve(installDirectory),\n expectedComponentDirectory,\n );\n if (\n !relativeToInstall ||\n relativeToInstall.startsWith(\"..\") ||\n path.isAbsolute(relativeToInstall)\n ) {\n throw new Error(`${field}[${index}].path 必须位于版本目录内。`);\n }\n const expectedManifestPath = path.join(\n expectedComponentDirectory,\n kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\",\n );\n this.assertExactPath(\n component.componentDirectory,\n expectedComponentDirectory,\n `${field}[${index}].componentDirectory`,\n );\n this.assertExactPath(\n component.manifestPath,\n expectedManifestPath,\n `${field}[${index}].manifestPath`,\n );\n return {\n kind,\n id,\n path: relativeComponentPath,\n componentDirectory: expectedComponentDirectory,\n manifestPath: expectedManifestPath,\n };\n });\n };\n\n private assertRecord = (value: unknown, field: string): Record<string, unknown> => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(`${field} 必须是对象。`);\n }\n return value as Record<string, unknown>;\n };\n\n private requireString = (value: unknown, field: string): string => {\n if (typeof value !== \"string\" || !value.trim()) {\n throw new Error(`${field} 必须是非空字符串。`);\n }\n return value;\n };\n\n private isMissingFileError = (error: unknown): boolean =>\n typeof error === \"object\" && error !== null &&\n \"code\" in error && (error as { code?: unknown }).code === \"ENOENT\";\n}\n"],"mappings":"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"}
|