@nextclaw/app-runtime 0.14.1 → 0.16.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.
Files changed (66) hide show
  1. package/dist/index.d.ts +5 -4
  2. package/dist/index.js +2 -1
  3. package/dist/package.js +1 -1
  4. package/dist/services/app-build.service.d.ts +1 -0
  5. package/dist/services/app-build.service.d.ts.map +1 -1
  6. package/dist/services/app-build.service.js +37 -2
  7. package/dist/services/app-build.service.js.map +1 -1
  8. package/dist/services/app-grant.service.d.ts.map +1 -1
  9. package/dist/services/app-grant.service.js +1 -4
  10. package/dist/services/app-grant.service.js.map +1 -1
  11. package/dist/services/app-installation-lifecycle.service.js +29 -2
  12. package/dist/services/app-installation-lifecycle.service.js.map +1 -1
  13. package/dist/services/app-manifest.service.d.ts +1 -0
  14. package/dist/services/app-manifest.service.d.ts.map +1 -1
  15. package/dist/services/app-manifest.service.js +27 -0
  16. package/dist/services/app-manifest.service.js.map +1 -1
  17. package/dist/services/app-permissions.service.d.ts.map +1 -1
  18. package/dist/services/app-permissions.service.js +0 -3
  19. package/dist/services/app-permissions.service.js.map +1 -1
  20. package/dist/services/app-registry-parser.service.js +200 -0
  21. package/dist/services/app-registry-parser.service.js.map +1 -0
  22. package/dist/services/app-registry.service.d.ts +4 -9
  23. package/dist/services/app-registry.service.d.ts.map +1 -1
  24. package/dist/services/app-registry.service.js +40 -164
  25. package/dist/services/app-registry.service.js.map +1 -1
  26. package/dist/services/app-runtime-cli.service.js +1 -1
  27. package/dist/services/app-runtime-cli.service.js.map +1 -1
  28. package/dist/services/app-runtime-options.service.d.ts.map +1 -1
  29. package/dist/services/app-runtime-options.service.js +2 -2
  30. package/dist/services/app-runtime-options.service.js.map +1 -1
  31. package/dist/services/app-runtime-toolchain.service.d.ts +5 -2
  32. package/dist/services/app-runtime-toolchain.service.d.ts.map +1 -1
  33. package/dist/services/app-runtime-toolchain.service.js +36 -4
  34. package/dist/services/app-runtime-toolchain.service.js.map +1 -1
  35. package/dist/services/app-rust-wasi-scaffold-template.service.d.ts +24 -0
  36. package/dist/services/app-rust-wasi-scaffold-template.service.d.ts.map +1 -0
  37. package/dist/services/app-rust-wasi-scaffold-template.service.js +394 -0
  38. package/dist/services/app-rust-wasi-scaffold-template.service.js.map +1 -0
  39. package/dist/services/app-scaffold.service.d.ts +4 -2
  40. package/dist/services/app-scaffold.service.d.ts.map +1 -1
  41. package/dist/services/app-scaffold.service.js +20 -4
  42. package/dist/services/app-scaffold.service.js.map +1 -1
  43. package/dist/services/platform-auth-state.service.js +1 -1
  44. package/dist/types/app-manifest.types.d.ts +8 -1
  45. package/dist/types/app-manifest.types.d.ts.map +1 -1
  46. package/dist/types/app-manifest.types.js.map +1 -1
  47. package/dist/types/app-registry.types.d.ts +8 -1
  48. package/dist/types/app-registry.types.d.ts.map +1 -1
  49. package/package.json +2 -1
  50. package/resources/rust-wasi/Cargo.lock +454 -0
  51. package/resources/wit/deps/clocks@0.2.6/monotonic-clock.wit +50 -0
  52. package/resources/wit/deps/clocks@0.2.6/timezone.wit +55 -0
  53. package/resources/wit/deps/clocks@0.2.6/wall-clock.wit +46 -0
  54. package/resources/wit/deps/clocks@0.2.6/world.wit +11 -0
  55. package/resources/wit/deps/config@0.2.0-draft-2024-09-27/package.wit +1 -0
  56. package/resources/wit/deps/config@0.2.0-draft-2024-09-27/store.wit +9 -0
  57. package/resources/wit/deps/http@0.2.6/handler.wit +49 -0
  58. package/resources/wit/deps/http@0.2.6/package.wit +1 -0
  59. package/resources/wit/deps/http@0.2.6/types.wit +688 -0
  60. package/resources/wit/deps/io@0.2.6/error.wit +34 -0
  61. package/resources/wit/deps/io@0.2.6/poll.wit +47 -0
  62. package/resources/wit/deps/io@0.2.6/streams.wit +290 -0
  63. package/resources/wit/deps/io@0.2.6/world.wit +10 -0
  64. package/resources/wit/deps/spin@2.0.0/package.wit +1 -0
  65. package/resources/wit/deps/spin@2.0.0/sqlite.wit +50 -0
  66. package/resources/wit/portable-service.wit +120 -0
@@ -1 +1 @@
1
- {"version":3,"file":"app-permissions.service.js","names":[],"sources":["../../src/services/app-permissions.service.ts"],"sourcesContent":["import { access } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type {\n AppDocumentAccessScope,\n AppManifestBundle,\n} from \"#app-runtime/types/app-manifest.types.js\";\nimport { isAppStandaloneManifestBundle } from \"#app-runtime/types/app-manifest.types.js\";\nimport type {\n AppDocumentGrantMap,\n AppPermissionSummary,\n ResolvedDocumentGrant,\n ResolvedPermissions,\n} from \"#app-runtime/types/app-permissions.types.js\";\n\nexport class AppPermissionsService {\n resolve = async (\n bundle: AppManifestBundle,\n documentGrantMap: AppDocumentGrantMap,\n context?: {\n appId?: string;\n },\n ): Promise<ResolvedPermissions> => {\n if (!isAppStandaloneManifestBundle(bundle)) {\n throw new Error(\"schema v2 组合包权限由 Panel/Service runtime grant 管理。\");\n }\n const requestedPermissions = bundle.manifest.permissions ?? {};\n const documentAccess =\n requestedPermissions.documentAccess === undefined\n ? []\n : await Promise.all(\n requestedPermissions.documentAccess.map((scope) =>\n this.resolveDocumentGrant(scope, documentGrantMap, context?.appId),\n ),\n );\n\n return {\n documentAccess,\n allowedDomains: requestedPermissions.allowedDomains ?? [],\n storage: {\n enabled: requestedPermissions.storage !== undefined && requestedPermissions.storage !== false,\n namespace:\n typeof requestedPermissions.storage === \"object\"\n ? requestedPermissions.storage.namespace\n : undefined,\n },\n capabilities: {\n hostBridge: requestedPermissions.capabilities?.hostBridge !== false,\n },\n };\n };\n\n summarize = (\n bundle: AppManifestBundle,\n permissions: ResolvedPermissions,\n ): AppPermissionSummary => {\n if (!isAppStandaloneManifestBundle(bundle)) {\n throw new Error(\"schema v2 组合包权限由 Panel/Service runtime grant 管理。\");\n }\n return {\n requested: bundle.manifest.permissions ?? {},\n documentAccess: permissions.documentAccess,\n allowedDomains: permissions.allowedDomains,\n storage: permissions.storage,\n capabilities: permissions.capabilities,\n };\n };\n\n private resolveDocumentGrant = async (\n scope: AppDocumentAccessScope,\n documentGrantMap: AppDocumentGrantMap,\n appId?: string,\n ): Promise<ResolvedDocumentGrant> => {\n const grantedPath = documentGrantMap[scope.id];\n if (!grantedPath) {\n if (appId) {\n throw new Error(\n `缺少 documentAccess 授权:${scope.id}。请先执行 napp grant ${appId} --document ${scope.id}=/absolute/path`,\n );\n }\n throw new Error(\n `缺少 documentAccess 授权:${scope.id}。请通过 --document ${scope.id}=/absolute/path 提供授权。`,\n );\n }\n const normalizedPath = path.resolve(grantedPath);\n try {\n await access(normalizedPath);\n } catch {\n throw new Error(`documentAccess 授权路径不存在:${scope.id} -> ${normalizedPath}`);\n }\n return {\n id: scope.id,\n mode: scope.mode,\n description: scope.description,\n path: normalizedPath,\n };\n };\n}\n"],"mappings":";;;;AAcA,IAAa,wBAAb,MAAmC;CACjC,UAAU,OACR,QACA,kBACA,YAGiC;AACjC,MAAI,CAAC,8BAA8B,OAAO,CACxC,OAAM,IAAI,MAAM,mDAAmD;EAErE,MAAM,uBAAuB,OAAO,SAAS,eAAe,EAAE;AAU9D,SAAO;GACL,gBATA,qBAAqB,mBAAmB,KAAA,IACpC,EAAE,GACF,MAAM,QAAQ,IACZ,qBAAqB,eAAe,KAAK,UACvC,KAAK,qBAAqB,OAAO,kBAAkB,SAAS,MAAM,CACnE,CACF;GAIL,gBAAgB,qBAAqB,kBAAkB,EAAE;GACzD,SAAS;IACP,SAAS,qBAAqB,YAAY,KAAA,KAAa,qBAAqB,YAAY;IACxF,WACE,OAAO,qBAAqB,YAAY,WACpC,qBAAqB,QAAQ,YAC7B,KAAA;IACP;GACD,cAAc,EACZ,YAAY,qBAAqB,cAAc,eAAe,OAC/D;GACF;;CAGH,aACE,QACA,gBACyB;AACzB,MAAI,CAAC,8BAA8B,OAAO,CACxC,OAAM,IAAI,MAAM,mDAAmD;AAErE,SAAO;GACL,WAAW,OAAO,SAAS,eAAe,EAAE;GAC5C,gBAAgB,YAAY;GAC5B,gBAAgB,YAAY;GAC5B,SAAS,YAAY;GACrB,cAAc,YAAY;GAC3B;;CAGH,uBAA+B,OAC7B,OACA,kBACA,UACmC;EACnC,MAAM,cAAc,iBAAiB,MAAM;AAC3C,MAAI,CAAC,aAAa;AAChB,OAAI,MACF,OAAM,IAAI,MACR,wBAAwB,MAAM,GAAG,mBAAmB,MAAM,cAAc,MAAM,GAAG,iBAClF;AAEH,SAAM,IAAI,MACR,wBAAwB,MAAM,GAAG,kBAAkB,MAAM,GAAG,uBAC7D;;EAEH,MAAM,iBAAiB,KAAK,QAAQ,YAAY;AAChD,MAAI;AACF,SAAM,OAAO,eAAe;UACtB;AACN,SAAM,IAAI,MAAM,0BAA0B,MAAM,GAAG,MAAM,iBAAiB;;AAE5E,SAAO;GACL,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,aAAa,MAAM;GACnB,MAAM;GACP"}
1
+ {"version":3,"file":"app-permissions.service.js","names":[],"sources":["../../src/services/app-permissions.service.ts"],"sourcesContent":["import { access } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type {\n AppDocumentAccessScope,\n AppManifestBundle,\n} from \"#app-runtime/types/app-manifest.types.js\";\nimport type {\n AppDocumentGrantMap,\n AppPermissionSummary,\n ResolvedDocumentGrant,\n ResolvedPermissions,\n} from \"#app-runtime/types/app-permissions.types.js\";\n\nexport class AppPermissionsService {\n resolve = async (\n bundle: AppManifestBundle,\n documentGrantMap: AppDocumentGrantMap,\n context?: {\n appId?: string;\n },\n ): Promise<ResolvedPermissions> => {\n const requestedPermissions = bundle.manifest.permissions ?? {};\n const documentAccess =\n requestedPermissions.documentAccess === undefined\n ? []\n : await Promise.all(\n requestedPermissions.documentAccess.map((scope) =>\n this.resolveDocumentGrant(\n scope,\n documentGrantMap,\n context?.appId,\n ),\n ),\n );\n\n return {\n documentAccess,\n allowedDomains: requestedPermissions.allowedDomains ?? [],\n storage: {\n enabled:\n requestedPermissions.storage !== undefined &&\n requestedPermissions.storage !== false,\n namespace:\n typeof requestedPermissions.storage === \"object\"\n ? requestedPermissions.storage.namespace\n : undefined,\n },\n capabilities: {\n hostBridge: requestedPermissions.capabilities?.hostBridge !== false,\n },\n };\n };\n\n summarize = (\n bundle: AppManifestBundle,\n permissions: ResolvedPermissions,\n ): AppPermissionSummary => {\n return {\n requested: bundle.manifest.permissions ?? {},\n documentAccess: permissions.documentAccess,\n allowedDomains: permissions.allowedDomains,\n storage: permissions.storage,\n capabilities: permissions.capabilities,\n };\n };\n\n private resolveDocumentGrant = async (\n scope: AppDocumentAccessScope,\n documentGrantMap: AppDocumentGrantMap,\n appId?: string,\n ): Promise<ResolvedDocumentGrant> => {\n const grantedPath = documentGrantMap[scope.id];\n if (!grantedPath) {\n if (appId) {\n throw new Error(\n `缺少 documentAccess 授权:${scope.id}。请先执行 napp grant ${appId} --document ${scope.id}=/absolute/path`,\n );\n }\n throw new Error(\n `缺少 documentAccess 授权:${scope.id}。请通过 --document ${scope.id}=/absolute/path 提供授权。`,\n );\n }\n const normalizedPath = path.resolve(grantedPath);\n try {\n await access(normalizedPath);\n } catch {\n throw new Error(\n `documentAccess 授权路径不存在:${scope.id} -> ${normalizedPath}`,\n );\n }\n return {\n id: scope.id,\n mode: scope.mode,\n description: scope.description,\n path: normalizedPath,\n };\n };\n}\n"],"mappings":";;;AAaA,IAAa,wBAAb,MAAmC;CACjC,UAAU,OACR,QACA,kBACA,YAGiC;EACjC,MAAM,uBAAuB,OAAO,SAAS,eAAe,EAAE;AAc9D,SAAO;GACL,gBAbA,qBAAqB,mBAAmB,KAAA,IACpC,EAAE,GACF,MAAM,QAAQ,IACZ,qBAAqB,eAAe,KAAK,UACvC,KAAK,qBACH,OACA,kBACA,SAAS,MACV,CACF,CACF;GAIL,gBAAgB,qBAAqB,kBAAkB,EAAE;GACzD,SAAS;IACP,SACE,qBAAqB,YAAY,KAAA,KACjC,qBAAqB,YAAY;IACnC,WACE,OAAO,qBAAqB,YAAY,WACpC,qBAAqB,QAAQ,YAC7B,KAAA;IACP;GACD,cAAc,EACZ,YAAY,qBAAqB,cAAc,eAAe,OAC/D;GACF;;CAGH,aACE,QACA,gBACyB;AACzB,SAAO;GACL,WAAW,OAAO,SAAS,eAAe,EAAE;GAC5C,gBAAgB,YAAY;GAC5B,gBAAgB,YAAY;GAC5B,SAAS,YAAY;GACrB,cAAc,YAAY;GAC3B;;CAGH,uBAA+B,OAC7B,OACA,kBACA,UACmC;EACnC,MAAM,cAAc,iBAAiB,MAAM;AAC3C,MAAI,CAAC,aAAa;AAChB,OAAI,MACF,OAAM,IAAI,MACR,wBAAwB,MAAM,GAAG,mBAAmB,MAAM,cAAc,MAAM,GAAG,iBAClF;AAEH,SAAM,IAAI,MACR,wBAAwB,MAAM,GAAG,kBAAkB,MAAM,GAAG,uBAC7D;;EAEH,MAAM,iBAAiB,KAAK,QAAQ,YAAY;AAChD,MAAI;AACF,SAAM,OAAO,eAAe;UACtB;AACN,SAAM,IAAI,MACR,0BAA0B,MAAM,GAAG,MAAM,iBAC1C;;AAEH,SAAO;GACL,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,aAAa,MAAM;GACnB,MAAM;GACP"}
@@ -0,0 +1,200 @@
1
+ import { AppPlatformTargetService } from "./app-platform-target.service.js";
2
+ import { AppInstanceStorageService } from "./app-instance-storage.service.js";
3
+ import path from "node:path";
4
+ //#region src/services/app-registry-parser.service.ts
5
+ const SAFE_APP_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
6
+ const SAFE_VERSION_PATTERN = /^[0-9A-Za-z]+(?:[._+-][0-9A-Za-z]+)*$/;
7
+ const SAFE_COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ const SAFE_SECRET_SLOT_ID_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
9
+ var AppRegistryParserService = class {
10
+ instanceStorageService;
11
+ platformTargetService = new AppPlatformTargetService();
12
+ constructor(appHomeService) {
13
+ this.appHomeService = appHomeService;
14
+ this.instanceStorageService = new AppInstanceStorageService(appHomeService);
15
+ }
16
+ parse = (rawRegistry) => {
17
+ const candidate = this.assertRecord(rawRegistry, "registry.json");
18
+ if (candidate.schemaVersion !== 1) throw new Error("当前只支持 registry schemaVersion = 1。");
19
+ const rawApps = this.assertRecord(candidate.apps, "registry.apps");
20
+ const apps = {};
21
+ for (const [appId, rawApp] of Object.entries(rawApps)) {
22
+ if (!SAFE_APP_ID_PATTERN.test(appId)) throw new Error(`registry.apps 包含不安全的 appId:${appId}`);
23
+ const app = this.assertRecord(rawApp, `registry.apps.${appId}`);
24
+ const installedVersions = this.parseInstalledVersions(appId, app.installedVersions);
25
+ const activeVersion = this.requireString(app.activeVersion, `registry.apps.${appId}.activeVersion`);
26
+ if (!installedVersions[activeVersion]) throw new Error(`registry.apps.${appId} 缺少 activeVersion ${activeVersion}。`);
27
+ const dataDirectory = this.requireString(app.dataDirectory, `registry.apps.${appId}.dataDirectory`);
28
+ const firstInstalledAt = Object.values(installedVersions).map((version) => version.installedAt).filter((value) => typeof value === "string").sort()[0] ?? (/* @__PURE__ */ new Date(0)).toISOString();
29
+ const defaultInstance = this.parseDefaultInstance(app.defaultInstance, appId, dataDirectory, firstInstalledAt);
30
+ apps[appId] = {
31
+ ...app,
32
+ appId,
33
+ publisher: app.publisher ?? installedVersions[activeVersion]?.publisher,
34
+ enabled: typeof app.enabled === "boolean" ? app.enabled : true,
35
+ activeVersion,
36
+ dataDirectory: defaultInstance.storage.dataDirectory,
37
+ defaultInstance,
38
+ installedVersions,
39
+ grants: app.grants && typeof app.grants === "object" ? app.grants : {},
40
+ secretBindings: this.parseSecretBindings(app.secretBindings, `registry.apps.${appId}.secretBindings`)
41
+ };
42
+ }
43
+ return {
44
+ schemaVersion: 1,
45
+ apps,
46
+ suppressedBuiltIns: candidate.suppressedBuiltIns && typeof candidate.suppressedBuiltIns === "object" && !Array.isArray(candidate.suppressedBuiltIns) ? Object.fromEntries(Object.entries(candidate.suppressedBuiltIns).flatMap(([appId, raw]) => {
47
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
48
+ const suppressedAt = raw.suppressedAt;
49
+ return typeof suppressedAt === "string" ? [[appId, { suppressedAt }]] : [];
50
+ })) : {}
51
+ };
52
+ };
53
+ parseSecretSlotId = (slotId, field) => {
54
+ if (!SAFE_SECRET_SLOT_ID_PATTERN.test(slotId)) throw new Error(`${field} 必须是稳定的 lowercase slot id。`);
55
+ return slotId;
56
+ };
57
+ parseSecretBinding = (rawBinding, field) => {
58
+ const binding = this.assertRecord(rawBinding, field);
59
+ if (Object.prototype.hasOwnProperty.call(binding, "value")) throw new Error(`${field} 不能保存 Secret 明文。`);
60
+ if (Object.keys(binding).find((key) => ![
61
+ "source",
62
+ "provider",
63
+ "id"
64
+ ].includes(key))) throw new Error(`${field} 只允许保存 source、provider 和 id。`);
65
+ const source = this.requireString(binding.source, `${field}.source`);
66
+ if (source !== "env" && source !== "file" && source !== "exec") throw new Error(`${field}.source 只支持 env、file 或 exec。`);
67
+ return {
68
+ source,
69
+ provider: binding.provider === void 0 ? void 0 : this.requireString(binding.provider, `${field}.provider`),
70
+ id: this.requireString(binding.id, `${field}.id`)
71
+ };
72
+ };
73
+ retainDeclaredSecretBindings = (bindings, permissions) => {
74
+ const declared = new Set((permissions.secrets ?? []).map((slot) => slot.id));
75
+ return Object.fromEntries(Object.entries(bindings).filter(([slotId]) => declared.has(slotId)));
76
+ };
77
+ parseInstalledVersions = (appId, raw) => {
78
+ const installedVersions = {};
79
+ const rawVersions = this.assertRecord(raw, `registry.apps.${appId}.installedVersions`);
80
+ for (const [version, rawVersion] of Object.entries(rawVersions)) {
81
+ if (!SAFE_VERSION_PATTERN.test(version)) throw new Error(`registry.apps.${appId} 包含不安全的版本:${version}`);
82
+ const field = `registry.apps.${appId}.installedVersions.${version}`;
83
+ const versionRecord = this.assertRecord(rawVersion, field);
84
+ const installDirectory = this.requireString(versionRecord.installDirectory, `${field}.installDirectory`);
85
+ this.assertExactPath(installDirectory, this.appHomeService.getInstallDirectory(appId, version), `${field}.installDirectory`);
86
+ installedVersions[version] = {
87
+ ...versionRecord,
88
+ version,
89
+ installDirectory: path.resolve(installDirectory),
90
+ manifestSchemaVersion: versionRecord.manifestSchemaVersion === 2 ? 2 : 1,
91
+ target: versionRecord.target === void 0 ? void 0 : this.platformTargetService.parseArtifactTarget(versionRecord.target, `${field}.target`),
92
+ components: this.parseComponents(versionRecord.components, installDirectory, `${field}.components`),
93
+ dataSchemaVersion: typeof versionRecord.dataSchemaVersion === "number" && Number.isSafeInteger(versionRecord.dataSchemaVersion) && versionRecord.dataSchemaVersion > 0 ? versionRecord.dataSchemaVersion : 1
94
+ };
95
+ }
96
+ return installedVersions;
97
+ };
98
+ parseDefaultInstance = (raw, appId, dataDirectory, createdAt) => {
99
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
100
+ this.assertExactPath(dataDirectory, this.appHomeService.getAppDataDirectory(appId), `registry.apps.${appId}.dataDirectory`);
101
+ return this.instanceStorageService.buildLegacyDefaultInstance({
102
+ appId,
103
+ dataDirectory,
104
+ createdAt
105
+ });
106
+ }
107
+ const instance = raw;
108
+ 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 无效。`);
109
+ if (instance.publisherId !== void 0 && typeof instance.publisherId !== "string") throw new Error(`registry.apps.${appId}.defaultInstance.publisherId 无效。`);
110
+ const dataSchemaVersion = typeof instance.dataSchemaVersion === "number" && Number.isSafeInteger(instance.dataSchemaVersion) && instance.dataSchemaVersion > 0 ? instance.dataSchemaVersion : 1;
111
+ const instanceCreatedAt = typeof instance.createdAt === "string" ? instance.createdAt : createdAt;
112
+ const instanceDirectory = path.resolve(this.appHomeService.getAppInstanceDirectory(appId, "default"));
113
+ const expectedStorage = {
114
+ layout: "instance-v1",
115
+ layoutVersion: 1,
116
+ instanceId: "default",
117
+ instanceDirectory,
118
+ dataDirectory: path.join(instanceDirectory, "data"),
119
+ configDirectory: path.join(instanceDirectory, "config"),
120
+ stateDirectory: path.join(instanceDirectory, "state"),
121
+ cacheDirectory: path.join(instanceDirectory, "cache"),
122
+ temporaryDirectory: path.join(instanceDirectory, "tmp"),
123
+ logsDirectory: path.join(instanceDirectory, "logs")
124
+ };
125
+ if (instance.storage.layout === "legacy") {
126
+ const legacy = this.appHomeService.getAppDataDirectory(appId);
127
+ this.assertExactPath(dataDirectory, legacy, `registry.apps.${appId}.dataDirectory`);
128
+ return {
129
+ id: "default",
130
+ publisherId: instance.publisherId,
131
+ storage: {
132
+ ...expectedStorage,
133
+ layout: "legacy",
134
+ dataDirectory: path.resolve(legacy)
135
+ },
136
+ dataSchemaVersion,
137
+ createdAt: instanceCreatedAt,
138
+ migratedAt: instance.migratedAt,
139
+ legacyDataDirectory: path.resolve(legacy)
140
+ };
141
+ }
142
+ for (const [field, expectedPath] of Object.entries(expectedStorage).filter(([key]) => key.endsWith("Directory"))) this.assertExactPath(instance.storage[field], expectedPath, `registry.apps.${appId}.defaultInstance.storage.${field}`);
143
+ this.assertExactPath(dataDirectory, expectedStorage.dataDirectory, `registry.apps.${appId}.dataDirectory`);
144
+ return {
145
+ id: "default",
146
+ publisherId: instance.publisherId,
147
+ storage: expectedStorage,
148
+ dataSchemaVersion,
149
+ createdAt: instanceCreatedAt,
150
+ migratedAt: instance.migratedAt,
151
+ legacyDataDirectory: instance.legacyDataDirectory
152
+ };
153
+ };
154
+ parseSecretBindings = (raw, field) => {
155
+ if (raw === void 0) return {};
156
+ return Object.fromEntries(Object.entries(this.assertRecord(raw, field)).map(([slotId, binding]) => {
157
+ const normalized = this.parseSecretSlotId(slotId, `${field} slot id`);
158
+ return [normalized, this.parseSecretBinding(binding, `${field}.${normalized}`)];
159
+ }));
160
+ };
161
+ parseComponents = (raw, installDirectory, field) => {
162
+ if (raw === void 0) return void 0;
163
+ if (!Array.isArray(raw)) throw new Error(`${field} 必须是数组。`);
164
+ return raw.map((item, index) => {
165
+ const component = this.assertRecord(item, `${field}[${index}]`);
166
+ if (component.kind !== "panel" && component.kind !== "service") throw new Error(`${field}[${index}].kind 无效。`);
167
+ const id = this.requireString(component.id, `${field}[${index}].id`);
168
+ if (!SAFE_COMPONENT_ID_PATTERN.test(id)) throw new Error(`${field}[${index}].id 不安全。`);
169
+ const relativePath = this.requireString(component.path, `${field}[${index}].path`);
170
+ const directory = path.resolve(installDirectory, relativePath);
171
+ const relative = path.relative(path.resolve(installDirectory), directory);
172
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`${field}[${index}].path 必须位于版本目录内。`);
173
+ const manifestPath = path.join(directory, component.kind === "panel" ? "panel-app.json" : "service-app.json");
174
+ this.assertExactPath(component.componentDirectory, directory, `${field}[${index}].componentDirectory`);
175
+ this.assertExactPath(component.manifestPath, manifestPath, `${field}[${index}].manifestPath`);
176
+ return {
177
+ kind: component.kind,
178
+ id,
179
+ path: relativePath,
180
+ componentDirectory: directory,
181
+ manifestPath
182
+ };
183
+ });
184
+ };
185
+ assertExactPath = (actual, expected, field) => {
186
+ if (typeof actual !== "string" || path.resolve(actual) !== path.resolve(expected)) throw new Error(`${field} 必须位于受管路径 ${path.resolve(expected)}。`);
187
+ };
188
+ assertRecord = (value, field) => {
189
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} 必须是对象。`);
190
+ return value;
191
+ };
192
+ requireString = (value, field) => {
193
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${field} 必须是非空字符串。`);
194
+ return value;
195
+ };
196
+ };
197
+ //#endregion
198
+ export { AppRegistryParserService };
199
+
200
+ //# sourceMappingURL=app-registry-parser.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-registry-parser.service.js","names":[],"sources":["../../src/services/app-registry-parser.service.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { AppPermissions, AppResolvedComponent } from \"#app-runtime/types/app-manifest.types.js\";\nimport type { AppHomeService } from \"#app-runtime/services/app-home.service.js\";\nimport { AppInstanceStorageService } from \"#app-runtime/services/app-instance-storage.service.js\";\nimport { AppPlatformTargetService } from \"#app-runtime/services/app-platform-target.service.js\";\nimport type { AppInstanceRecord } from \"#app-runtime/types/app-storage.types.js\";\nimport type {\n AppRegistry,\n AppRegistryAppRecord,\n AppRegistryInstalledVersion,\n AppSecretBinding,\n AppSecretBindingMap,\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]+)*$/;\nconst SAFE_SECRET_SLOT_ID_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;\n\nexport class AppRegistryParserService {\n private readonly instanceStorageService: AppInstanceStorageService;\n private readonly platformTargetService = new AppPlatformTargetService();\n\n constructor(private readonly appHomeService: AppHomeService) {\n this.instanceStorageService = new AppInstanceStorageService(appHomeService);\n }\n\n parse = (rawRegistry: unknown): AppRegistry => {\n const candidate = this.assertRecord(rawRegistry, \"registry.json\");\n if (candidate.schemaVersion !== 1) throw new Error(\"当前只支持 registry schemaVersion = 1。\");\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)) throw new Error(`registry.apps 包含不安全的 appId:${appId}`);\n const app = this.assertRecord(rawApp, `registry.apps.${appId}`) as Partial<AppRegistryAppRecord>;\n const installedVersions = this.parseInstalledVersions(appId, app.installedVersions);\n const activeVersion = this.requireString(app.activeVersion, `registry.apps.${appId}.activeVersion`);\n if (!installedVersions[activeVersion]) throw new Error(`registry.apps.${appId} 缺少 activeVersion ${activeVersion}。`);\n const dataDirectory = this.requireString(app.dataDirectory, `registry.apps.${appId}.dataDirectory`);\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(app.defaultInstance, appId, dataDirectory, firstInstalledAt);\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 secretBindings: this.parseSecretBindings(app.secretBindings, `registry.apps.${appId}.secretBindings`),\n };\n }\n const suppressedBuiltIns = candidate.suppressedBuiltIns &&\n typeof candidate.suppressedBuiltIns === \"object\" && !Array.isArray(candidate.suppressedBuiltIns)\n ? Object.fromEntries(Object.entries(candidate.suppressedBuiltIns).flatMap(([appId, raw]) => {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return [];\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 parseSecretSlotId = (slotId: string, field: string): string => {\n if (!SAFE_SECRET_SLOT_ID_PATTERN.test(slotId)) throw new Error(`${field} 必须是稳定的 lowercase slot id。`);\n return slotId;\n };\n\n parseSecretBinding = (rawBinding: unknown, field: string): AppSecretBinding => {\n const binding = this.assertRecord(rawBinding, field);\n if (Object.prototype.hasOwnProperty.call(binding, \"value\")) throw new Error(`${field} 不能保存 Secret 明文。`);\n const unsupportedField = Object.keys(binding).find((key) => ![\"source\", \"provider\", \"id\"].includes(key));\n if (unsupportedField) throw new Error(`${field} 只允许保存 source、provider 和 id。`);\n const source = this.requireString(binding.source, `${field}.source`);\n if (source !== \"env\" && source !== \"file\" && source !== \"exec\") {\n throw new Error(`${field}.source 只支持 env、file 或 exec。`);\n }\n return {\n source,\n provider: binding.provider === undefined ? undefined : this.requireString(binding.provider, `${field}.provider`),\n id: this.requireString(binding.id, `${field}.id`),\n };\n };\n\n retainDeclaredSecretBindings = (bindings: AppSecretBindingMap, permissions: AppPermissions): AppSecretBindingMap => {\n const declared = new Set((permissions.secrets ?? []).map((slot) => slot.id));\n return Object.fromEntries(Object.entries(bindings).filter(([slotId]) => declared.has(slotId)));\n };\n\n private parseInstalledVersions = (appId: string, raw: unknown): Record<string, AppRegistryInstalledVersion> => {\n const installedVersions: Record<string, AppRegistryInstalledVersion> = {};\n const rawVersions = this.assertRecord(raw, `registry.apps.${appId}.installedVersions`);\n for (const [version, rawVersion] of Object.entries(rawVersions)) {\n if (!SAFE_VERSION_PATTERN.test(version)) throw new Error(`registry.apps.${appId} 包含不安全的版本:${version}`);\n const field = `registry.apps.${appId}.installedVersions.${version}`;\n const versionRecord = this.assertRecord(rawVersion, field) as Partial<AppRegistryInstalledVersion>;\n const installDirectory = this.requireString(versionRecord.installDirectory, `${field}.installDirectory`);\n this.assertExactPath(installDirectory, this.appHomeService.getInstallDirectory(appId, version), `${field}.installDirectory`);\n installedVersions[version] = {\n ...(versionRecord as AppRegistryInstalledVersion),\n version,\n installDirectory: path.resolve(installDirectory),\n manifestSchemaVersion: versionRecord.manifestSchemaVersion === 2 ? 2 : 1,\n target: versionRecord.target === undefined ? undefined : this.platformTargetService.parseArtifactTarget(versionRecord.target, `${field}.target`),\n components: this.parseComponents(versionRecord.components, installDirectory, `${field}.components`),\n dataSchemaVersion: typeof versionRecord.dataSchemaVersion === \"number\" &&\n Number.isSafeInteger(versionRecord.dataSchemaVersion) && versionRecord.dataSchemaVersion > 0\n ? versionRecord.dataSchemaVersion : 1,\n };\n }\n return installedVersions;\n };\n\n private parseDefaultInstance = (raw: unknown, appId: string, dataDirectory: string, createdAt: string): AppInstanceRecord => {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n this.assertExactPath(dataDirectory, this.appHomeService.getAppDataDirectory(appId), `registry.apps.${appId}.dataDirectory`);\n return this.instanceStorageService.buildLegacyDefaultInstance({ appId, dataDirectory, createdAt });\n }\n const instance = raw as Partial<AppInstanceRecord>;\n if (instance.id !== \"default\" || !instance.storage || typeof instance.storage !== \"object\" ||\n (instance.storage.layout !== \"legacy\" && instance.storage.layout !== \"instance-v1\")) {\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\" && Number.isSafeInteger(instance.dataSchemaVersion) && instance.dataSchemaVersion > 0\n ? instance.dataSchemaVersion : 1;\n const instanceCreatedAt = typeof instance.createdAt === \"string\" ? instance.createdAt : createdAt;\n const instanceDirectory = path.resolve(this.appHomeService.getAppInstanceDirectory(appId, \"default\"));\n const expectedStorage = {\n layout: \"instance-v1\" as const, layoutVersion: 1 as const, instanceId: \"default\", instanceDirectory,\n dataDirectory: path.join(instanceDirectory, \"data\"), configDirectory: path.join(instanceDirectory, \"config\"),\n stateDirectory: path.join(instanceDirectory, \"state\"), cacheDirectory: path.join(instanceDirectory, \"cache\"),\n temporaryDirectory: path.join(instanceDirectory, \"tmp\"), logsDirectory: path.join(instanceDirectory, \"logs\"),\n };\n if (instance.storage.layout === \"legacy\") {\n const legacy = this.appHomeService.getAppDataDirectory(appId);\n this.assertExactPath(dataDirectory, legacy, `registry.apps.${appId}.dataDirectory`);\n return { id: \"default\", publisherId: instance.publisherId, storage: { ...expectedStorage, layout: \"legacy\", dataDirectory: path.resolve(legacy) }, dataSchemaVersion, createdAt: instanceCreatedAt, migratedAt: instance.migratedAt, legacyDataDirectory: path.resolve(legacy) };\n }\n for (const [field, expectedPath] of Object.entries(expectedStorage).filter(([key]) => key.endsWith(\"Directory\"))) {\n this.assertExactPath(instance.storage[field as keyof typeof instance.storage], expectedPath as string, `registry.apps.${appId}.defaultInstance.storage.${field}`);\n }\n this.assertExactPath(dataDirectory, expectedStorage.dataDirectory, `registry.apps.${appId}.dataDirectory`);\n return { id: \"default\", publisherId: instance.publisherId, storage: expectedStorage, dataSchemaVersion, createdAt: instanceCreatedAt, migratedAt: instance.migratedAt, legacyDataDirectory: instance.legacyDataDirectory };\n };\n\n private parseSecretBindings = (raw: unknown, field: string): AppSecretBindingMap => {\n if (raw === undefined) return {};\n return Object.fromEntries(Object.entries(this.assertRecord(raw, field)).map(([slotId, binding]) => {\n const normalized = this.parseSecretSlotId(slotId, `${field} slot id`);\n return [normalized, this.parseSecretBinding(binding, `${field}.${normalized}`)];\n }));\n };\n\n private parseComponents = (raw: unknown, installDirectory: string, field: string): AppResolvedComponent[] | undefined => {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw)) throw new Error(`${field} 必须是数组。`);\n return raw.map((item, index) => {\n const component = this.assertRecord(item, `${field}[${index}]`);\n if (component.kind !== \"panel\" && component.kind !== \"service\") throw new Error(`${field}[${index}].kind 无效。`);\n const id = this.requireString(component.id, `${field}[${index}].id`);\n if (!SAFE_COMPONENT_ID_PATTERN.test(id)) throw new Error(`${field}[${index}].id 不安全。`);\n const relativePath = this.requireString(component.path, `${field}[${index}].path`);\n const directory = path.resolve(installDirectory, relativePath);\n const relative = path.relative(path.resolve(installDirectory), directory);\n if (!relative || relative.startsWith(\"..\") || path.isAbsolute(relative)) throw new Error(`${field}[${index}].path 必须位于版本目录内。`);\n const manifestPath = path.join(directory, component.kind === \"panel\" ? \"panel-app.json\" : \"service-app.json\");\n this.assertExactPath(component.componentDirectory, directory, `${field}[${index}].componentDirectory`);\n this.assertExactPath(component.manifestPath, manifestPath, `${field}[${index}].manifestPath`);\n return { kind: component.kind, id, path: relativePath, componentDirectory: directory, manifestPath };\n });\n };\n\n private assertExactPath = (actual: unknown, expected: string, field: string): void => {\n if (typeof actual !== \"string\" || path.resolve(actual) !== path.resolve(expected)) throw new Error(`${field} 必须位于受管路径 ${path.resolve(expected)}。`);\n };\n\n private assertRecord = (value: unknown, field: string): Record<string, unknown> => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`${field} 必须是对象。`);\n return value as Record<string, unknown>;\n };\n\n private requireString = (value: unknown, field: string): string => {\n if (typeof value !== \"string\" || !value.trim()) throw new Error(`${field} 必须是非空字符串。`);\n return value;\n };\n}\n"],"mappings":"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"}
@@ -2,14 +2,13 @@ import { AppPermissions, AppPlatformSecuritySummary, AppResolvedComponent } from
2
2
  import { AppDocumentGrantMap } from "../types/app-permissions.types.js";
3
3
  import { AppHomeService } from "./app-home.service.js";
4
4
  import { AppInstanceRecord } from "../types/app-storage.types.js";
5
- import { AppInstallSourceKind, AppRegistry, AppRegistryAppRecord, AppRegistryInstalledVersion } from "../types/app-registry.types.js";
5
+ import { AppInstallSourceKind, AppRegistry, AppRegistryAppRecord, AppRegistryInstalledVersion, AppSecretBinding } from "../types/app-registry.types.js";
6
6
 
7
7
  //#region src/services/app-registry.service.d.ts
8
8
  declare class AppRegistryService {
9
9
  private readonly appHomeService;
10
- private readonly instanceStorageService;
11
10
  private readonly fileLockService;
12
- private readonly platformTargetService;
11
+ private readonly parser;
13
12
  constructor(appHomeService?: AppHomeService);
14
13
  load: () => Promise<AppRegistry>;
15
14
  save: (registry: AppRegistry) => Promise<void>;
@@ -49,17 +48,13 @@ declare class AppRegistryService {
49
48
  updateGrants: (appId: string, grants: AppDocumentGrantMap) => Promise<AppRegistryAppRecord>;
50
49
  setDocumentGrant: (appId: string, scopeId: string, directoryPath: string) => Promise<AppRegistryAppRecord>;
51
50
  removeDocumentGrant: (appId: string, scopeId: string) => Promise<boolean>;
51
+ bindSecret: (appId: string, slotId: string, binding: AppSecretBinding) => Promise<AppRegistryAppRecord>;
52
+ unbindSecret: (appId: string, slotId: string) => Promise<boolean>;
52
53
  removeApp: (appId: string) => Promise<AppRegistryAppRecord | undefined>;
53
54
  isBuiltInSuppressed: (appId: string) => Promise<boolean>;
54
55
  setBuiltInSuppressed: (appId: string, suppressed: boolean) => Promise<void>;
55
56
  private updateApp;
56
57
  private withMutation;
57
- private parseRegistry;
58
- private parseDefaultInstance;
59
- private assertExactPath;
60
- private parseComponents;
61
- private assertRecord;
62
- private requireString;
63
58
  private isMissingFileError;
64
59
  }
65
60
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"app-registry.service.d.ts","names":[],"sources":["../../src/services/app-registry.service.ts"],"mappings":";;;;;;;cAwBa,kBAAA;EAAA,iBAKkB,cAAA;EAAA,iBAJZ,sBAAA;EAAA,iBACA,eAAA;EAAA,iBACA,qBAAA;cAEY,cAAA,GAAgB,cAAA;EAI7C,IAAA,QAAiB,OAAA,CAAQ,WAAA;EAYzB,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,MAAA,GAAS,2BAAA;IACT,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;EAqDZ,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,QAqGA,oBAAA;EAAA,QAyGA,eAAA;EAAA,QAMA,eAAA;EAAA,QA6DA,YAAA;EAAA,QAOA,aAAA;EAAA,QAOA,kBAAA;AAAA"}
1
+ {"version":3,"file":"app-registry.service.d.ts","names":[],"sources":["../../src/services/app-registry.service.ts"],"mappings":";;;;;;;cAgBa,kBAAA;EAAA,iBAIkB,cAAA;EAAA,iBAHZ,eAAA;EAAA,iBACA,MAAA;cAEY,cAAA,GAAgB,cAAA;EAI7C,IAAA,QAAiB,OAAA,CAAQ,WAAA;EAYzB,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,MAAA,GAAS,2BAAA;IACT,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;EAgEZ,UAAA,GAAoB,KAAA,UAAe,OAAA,cAAmB,OAAA,CAAQ,oBAAA;EAI9D,eAAA,GACE,KAAA,UACA,OAAA,aACC,OAAA,CAAQ,oBAAA;EAgBX,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,UAAA,GACE,KAAA,UACA,MAAA,UACA,OAAA,EAAS,gBAAA,KACR,OAAA,CAAQ,oBAAA;EAmBX,YAAA,GAAsB,KAAA,UAAe,MAAA,aAAiB,OAAA;EAetD,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,QAMA,kBAAA;AAAA"}
@@ -1,25 +1,20 @@
1
- import { AppPlatformTargetService } from "./app-platform-target.service.js";
2
1
  import { AppHomeService } from "./app-home.service.js";
3
2
  import { FileLockService } from "./file-lock.service.js";
4
- import { AppInstanceStorageService } from "./app-instance-storage.service.js";
3
+ import { AppRegistryParserService } from "./app-registry-parser.service.js";
5
4
  import { open, readFile, rename, rm } from "node:fs/promises";
6
5
  import path from "node:path";
7
6
  //#region src/services/app-registry.service.ts
8
- const SAFE_APP_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
9
- const SAFE_VERSION_PATTERN = /^[0-9A-Za-z]+(?:[._+-][0-9A-Za-z]+)*$/;
10
- const SAFE_COMPONENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
7
  var AppRegistryService = class {
12
- instanceStorageService;
13
8
  fileLockService = new FileLockService();
14
- platformTargetService = new AppPlatformTargetService();
9
+ parser;
15
10
  constructor(appHomeService = new AppHomeService()) {
16
11
  this.appHomeService = appHomeService;
17
- this.instanceStorageService = new AppInstanceStorageService(appHomeService);
12
+ this.parser = new AppRegistryParserService(appHomeService);
18
13
  }
19
14
  load = async () => {
20
15
  try {
21
16
  const raw = await readFile(this.appHomeService.getRegistryPath(), "utf-8");
22
- return this.parseRegistry(JSON.parse(raw));
17
+ return this.parser.parse(JSON.parse(raw));
23
18
  } catch (error) {
24
19
  if (this.isMissingFileError(error)) return {
25
20
  schemaVersion: 1,
@@ -67,12 +62,14 @@ var AppRegistryService = class {
67
62
  const currentRecord = registry.apps[params.appId];
68
63
  const currentPublisher = currentRecord?.publisher ?? currentRecord?.installedVersions[currentRecord.activeVersion]?.publisher;
69
64
  if (currentPublisher && currentPublisher.id !== params.publisher?.id) throw new Error(`应用 ${params.appId} 已绑定发布者 ${currentPublisher.id},拒绝由 ${params.publisher?.id ?? "未验证本地来源"} 覆盖。`);
65
+ const activeVersion = params.activate === false && currentRecord ? currentRecord.activeVersion : params.version;
66
+ const activePermissions = activeVersion === params.version ? params.permissions : currentRecord?.installedVersions[activeVersion]?.permissions ?? {};
70
67
  const nextRecord = {
71
68
  appId: params.appId,
72
69
  name: params.name,
73
70
  description: params.description,
74
71
  publisher: currentPublisher ?? params.publisher,
75
- activeVersion: params.activate === false && currentRecord ? currentRecord.activeVersion : params.version,
72
+ activeVersion,
76
73
  enabled: params.enabled ?? currentRecord?.enabled ?? params.manifestSchemaVersion === 1,
77
74
  dataDirectory: params.defaultInstance.storage.dataDirectory,
78
75
  defaultInstance: params.defaultInstance,
@@ -99,7 +96,8 @@ var AppRegistryService = class {
99
96
  contentSha256: params.contentSha256
100
97
  }
101
98
  },
102
- grants: currentRecord?.grants ?? {}
99
+ grants: currentRecord?.grants ?? {},
100
+ secretBindings: this.parser.retainDeclaredSecretBindings(currentRecord?.secretBindings ?? {}, activePermissions)
103
101
  };
104
102
  registry.apps[params.appId] = nextRecord;
105
103
  await this.saveUnlocked(registry);
@@ -117,7 +115,8 @@ var AppRegistryService = class {
117
115
  if (!record.installedVersions[version]) throw new Error(`应用 ${appId} 未安装版本 ${version}。`);
118
116
  return {
119
117
  ...record,
120
- activeVersion: version
118
+ activeVersion: version,
119
+ secretBindings: this.parser.retainDeclaredSecretBindings(record.secretBindings, record.installedVersions[version].permissions)
121
120
  };
122
121
  });
123
122
  };
@@ -164,6 +163,35 @@ var AppRegistryService = class {
164
163
  });
165
164
  return removed;
166
165
  };
166
+ bindSecret = async (appId, slotId, binding) => {
167
+ const normalizedSlotId = this.parser.parseSecretSlotId(slotId, "secret slot id");
168
+ const normalizedBinding = this.parser.parseSecretBinding(binding, `secret binding ${normalizedSlotId}`);
169
+ return await this.updateApp(appId, (record) => {
170
+ if (!(record.installedVersions[record.activeVersion]?.permissions.secrets ?? []).some((slot) => slot.id === normalizedSlotId)) throw new Error(`应用 ${appId} 未声明 Secret slot:${normalizedSlotId}`);
171
+ return {
172
+ ...record,
173
+ secretBindings: {
174
+ ...record.secretBindings,
175
+ [normalizedSlotId]: normalizedBinding
176
+ }
177
+ };
178
+ });
179
+ };
180
+ unbindSecret = async (appId, slotId) => {
181
+ const normalizedSlotId = this.parser.parseSecretSlotId(slotId, "secret slot id");
182
+ let removed = false;
183
+ await this.updateApp(appId, (record) => {
184
+ if (!(normalizedSlotId in record.secretBindings)) return record;
185
+ const secretBindings = { ...record.secretBindings };
186
+ delete secretBindings[normalizedSlotId];
187
+ removed = true;
188
+ return {
189
+ ...record,
190
+ secretBindings
191
+ };
192
+ });
193
+ return removed;
194
+ };
167
195
  removeApp = async (appId) => {
168
196
  return await this.withMutation(async () => {
169
197
  const registry = await this.load();
@@ -201,158 +229,6 @@ var AppRegistryService = class {
201
229
  const registryPath = path.resolve(this.appHomeService.getRegistryPath());
202
230
  return await this.fileLockService.withLock(`${registryPath}.lock`, operation);
203
231
  };
204
- parseRegistry = (rawRegistry) => {
205
- const candidate = this.assertRecord(rawRegistry, "registry.json");
206
- if (candidate.schemaVersion !== 1) throw new Error("当前只支持 registry schemaVersion = 1。");
207
- const rawApps = this.assertRecord(candidate.apps, "registry.apps");
208
- const apps = {};
209
- for (const [appId, rawApp] of Object.entries(rawApps)) {
210
- if (!SAFE_APP_ID_PATTERN.test(appId)) throw new Error(`registry.apps 包含不安全的 appId:${appId}`);
211
- const app = this.assertRecord(rawApp, `registry.apps.${appId}`);
212
- const installedVersions = {};
213
- const rawVersions = this.assertRecord(app.installedVersions, `registry.apps.${appId}.installedVersions`);
214
- for (const [version, rawVersion] of Object.entries(rawVersions)) {
215
- if (!SAFE_VERSION_PATTERN.test(version)) throw new Error(`registry.apps.${appId} 包含不安全的版本:${version}`);
216
- const versionRecord = this.assertRecord(rawVersion, `registry.apps.${appId}.installedVersions.${version}`);
217
- const installDirectory = this.requireString(versionRecord.installDirectory, `registry.apps.${appId}.installedVersions.${version}.installDirectory`);
218
- this.assertExactPath(installDirectory, this.appHomeService.getInstallDirectory(appId, version), `registry.apps.${appId}.installedVersions.${version}.installDirectory`);
219
- installedVersions[version] = {
220
- ...versionRecord,
221
- version,
222
- installDirectory: path.resolve(installDirectory),
223
- manifestSchemaVersion: versionRecord.manifestSchemaVersion === 2 ? 2 : 1,
224
- target: versionRecord.target === void 0 ? void 0 : this.platformTargetService.parseArtifactTarget(versionRecord.target, `registry.apps.${appId}.installedVersions.${version}.target`),
225
- components: this.parseComponents(versionRecord.components, installDirectory, `registry.apps.${appId}.installedVersions.${version}.components`),
226
- dataSchemaVersion: typeof versionRecord.dataSchemaVersion === "number" && Number.isSafeInteger(versionRecord.dataSchemaVersion) && versionRecord.dataSchemaVersion > 0 ? versionRecord.dataSchemaVersion : 1
227
- };
228
- }
229
- const activeVersion = this.requireString(app.activeVersion, `registry.apps.${appId}.activeVersion`);
230
- if (!installedVersions[activeVersion]) throw new Error(`registry.apps.${appId} 缺少 activeVersion ${activeVersion}。`);
231
- const dataDirectory = this.requireString(app.dataDirectory, `registry.apps.${appId}.dataDirectory`);
232
- const firstInstalledAt = Object.values(installedVersions).map((version) => version.installedAt).filter((value) => typeof value === "string").sort()[0] ?? (/* @__PURE__ */ new Date(0)).toISOString();
233
- const defaultInstance = this.parseDefaultInstance(app.defaultInstance, appId, dataDirectory, firstInstalledAt);
234
- apps[appId] = {
235
- ...app,
236
- appId,
237
- publisher: app.publisher ?? installedVersions[activeVersion]?.publisher,
238
- enabled: typeof app.enabled === "boolean" ? app.enabled : true,
239
- activeVersion,
240
- dataDirectory: defaultInstance.storage.dataDirectory,
241
- defaultInstance,
242
- installedVersions,
243
- grants: app.grants && typeof app.grants === "object" ? app.grants : {}
244
- };
245
- }
246
- return {
247
- schemaVersion: 1,
248
- apps,
249
- suppressedBuiltIns: candidate.suppressedBuiltIns && typeof candidate.suppressedBuiltIns === "object" && !Array.isArray(candidate.suppressedBuiltIns) ? Object.fromEntries(Object.entries(candidate.suppressedBuiltIns).flatMap(([appId, raw]) => {
250
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
251
- const suppressedAt = raw.suppressedAt;
252
- return typeof suppressedAt === "string" ? [[appId, { suppressedAt }]] : [];
253
- })) : {}
254
- };
255
- };
256
- parseDefaultInstance = (rawInstance, appId, dataDirectory, createdAt) => {
257
- if (!rawInstance || typeof rawInstance !== "object" || Array.isArray(rawInstance)) {
258
- this.assertExactPath(dataDirectory, this.appHomeService.getAppDataDirectory(appId), `registry.apps.${appId}.dataDirectory`);
259
- return this.instanceStorageService.buildLegacyDefaultInstance({
260
- appId,
261
- dataDirectory,
262
- createdAt
263
- });
264
- }
265
- const instance = rawInstance;
266
- 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 无效。`);
267
- if (instance.publisherId !== void 0 && typeof instance.publisherId !== "string") throw new Error(`registry.apps.${appId}.defaultInstance.publisherId 无效。`);
268
- const dataSchemaVersion = typeof instance.dataSchemaVersion === "number" && Number.isSafeInteger(instance.dataSchemaVersion) && instance.dataSchemaVersion > 0 ? instance.dataSchemaVersion : 1;
269
- const instanceCreatedAt = typeof instance.createdAt === "string" ? instance.createdAt : createdAt;
270
- const instanceDirectory = path.resolve(this.appHomeService.getAppInstanceDirectory(appId, "default"));
271
- const expectedStorage = {
272
- layout: "instance-v1",
273
- layoutVersion: 1,
274
- instanceId: "default",
275
- instanceDirectory,
276
- dataDirectory: path.join(instanceDirectory, "data"),
277
- configDirectory: path.join(instanceDirectory, "config"),
278
- stateDirectory: path.join(instanceDirectory, "state"),
279
- cacheDirectory: path.join(instanceDirectory, "cache"),
280
- temporaryDirectory: path.join(instanceDirectory, "tmp"),
281
- logsDirectory: path.join(instanceDirectory, "logs")
282
- };
283
- if (instance.storage.layout === "legacy") {
284
- const expectedLegacyDataDirectory = this.appHomeService.getAppDataDirectory(appId);
285
- this.assertExactPath(dataDirectory, expectedLegacyDataDirectory, `registry.apps.${appId}.dataDirectory`);
286
- return {
287
- id: "default",
288
- publisherId: instance.publisherId,
289
- storage: {
290
- ...expectedStorage,
291
- layout: "legacy",
292
- dataDirectory: path.resolve(expectedLegacyDataDirectory)
293
- },
294
- dataSchemaVersion,
295
- createdAt: instanceCreatedAt,
296
- migratedAt: instance.migratedAt,
297
- legacyDataDirectory: path.resolve(expectedLegacyDataDirectory)
298
- };
299
- }
300
- for (const [field, expectedPath] of Object.entries({
301
- instanceDirectory: expectedStorage.instanceDirectory,
302
- dataDirectory: expectedStorage.dataDirectory,
303
- configDirectory: expectedStorage.configDirectory,
304
- stateDirectory: expectedStorage.stateDirectory,
305
- cacheDirectory: expectedStorage.cacheDirectory,
306
- temporaryDirectory: expectedStorage.temporaryDirectory,
307
- logsDirectory: expectedStorage.logsDirectory
308
- })) this.assertExactPath(instance.storage[field], expectedPath, `registry.apps.${appId}.defaultInstance.storage.${field}`);
309
- this.assertExactPath(dataDirectory, expectedStorage.dataDirectory, `registry.apps.${appId}.dataDirectory`);
310
- return {
311
- id: "default",
312
- publisherId: instance.publisherId,
313
- storage: expectedStorage,
314
- dataSchemaVersion,
315
- createdAt: instanceCreatedAt,
316
- migratedAt: instance.migratedAt,
317
- legacyDataDirectory: instance.legacyDataDirectory
318
- };
319
- };
320
- assertExactPath = (actual, expected, field) => {
321
- if (typeof actual !== "string" || path.resolve(actual) !== path.resolve(expected)) throw new Error(`${field} 必须位于受管路径 ${path.resolve(expected)}。`);
322
- };
323
- parseComponents = (rawComponents, installDirectory, field) => {
324
- if (rawComponents === void 0) return;
325
- if (!Array.isArray(rawComponents)) throw new Error(`${field} 必须是数组。`);
326
- return rawComponents.map((rawComponent, index) => {
327
- const component = this.assertRecord(rawComponent, `${field}[${index}]`);
328
- const kind = component.kind;
329
- if (kind !== "panel" && kind !== "service") throw new Error(`${field}[${index}].kind 无效。`);
330
- const id = this.requireString(component.id, `${field}[${index}].id`);
331
- if (!SAFE_COMPONENT_ID_PATTERN.test(id)) throw new Error(`${field}[${index}].id 不安全。`);
332
- const relativeComponentPath = this.requireString(component.path, `${field}[${index}].path`);
333
- const expectedComponentDirectory = path.resolve(installDirectory, relativeComponentPath);
334
- const relativeToInstall = path.relative(path.resolve(installDirectory), expectedComponentDirectory);
335
- if (!relativeToInstall || relativeToInstall.startsWith("..") || path.isAbsolute(relativeToInstall)) throw new Error(`${field}[${index}].path 必须位于版本目录内。`);
336
- const expectedManifestPath = path.join(expectedComponentDirectory, kind === "panel" ? "panel-app.json" : "service-app.json");
337
- this.assertExactPath(component.componentDirectory, expectedComponentDirectory, `${field}[${index}].componentDirectory`);
338
- this.assertExactPath(component.manifestPath, expectedManifestPath, `${field}[${index}].manifestPath`);
339
- return {
340
- kind,
341
- id,
342
- path: relativeComponentPath,
343
- componentDirectory: expectedComponentDirectory,
344
- manifestPath: expectedManifestPath
345
- };
346
- });
347
- };
348
- assertRecord = (value, field) => {
349
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} 必须是对象。`);
350
- return value;
351
- };
352
- requireString = (value, field) => {
353
- if (typeof value !== "string" || !value.trim()) throw new Error(`${field} 必须是非空字符串。`);
354
- return value;
355
- };
356
232
  isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
357
233
  };
358
234
  //#endregion