@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.
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -1
- package/dist/package.js +1 -1
- package/dist/services/app-build.service.d.ts +1 -0
- package/dist/services/app-build.service.d.ts.map +1 -1
- package/dist/services/app-build.service.js +37 -2
- package/dist/services/app-build.service.js.map +1 -1
- package/dist/services/app-grant.service.d.ts.map +1 -1
- package/dist/services/app-grant.service.js +1 -4
- package/dist/services/app-grant.service.js.map +1 -1
- package/dist/services/app-installation-lifecycle.service.js +29 -2
- package/dist/services/app-installation-lifecycle.service.js.map +1 -1
- package/dist/services/app-manifest.service.d.ts +1 -0
- package/dist/services/app-manifest.service.d.ts.map +1 -1
- package/dist/services/app-manifest.service.js +27 -0
- package/dist/services/app-manifest.service.js.map +1 -1
- package/dist/services/app-permissions.service.d.ts.map +1 -1
- package/dist/services/app-permissions.service.js +0 -3
- package/dist/services/app-permissions.service.js.map +1 -1
- package/dist/services/app-registry-parser.service.js +200 -0
- package/dist/services/app-registry-parser.service.js.map +1 -0
- package/dist/services/app-registry.service.d.ts +4 -9
- package/dist/services/app-registry.service.d.ts.map +1 -1
- package/dist/services/app-registry.service.js +40 -164
- package/dist/services/app-registry.service.js.map +1 -1
- package/dist/services/app-runtime-cli.service.js +1 -1
- package/dist/services/app-runtime-cli.service.js.map +1 -1
- package/dist/services/app-runtime-options.service.d.ts.map +1 -1
- package/dist/services/app-runtime-options.service.js +2 -2
- package/dist/services/app-runtime-options.service.js.map +1 -1
- package/dist/services/app-runtime-toolchain.service.d.ts +5 -2
- package/dist/services/app-runtime-toolchain.service.d.ts.map +1 -1
- package/dist/services/app-runtime-toolchain.service.js +36 -4
- package/dist/services/app-runtime-toolchain.service.js.map +1 -1
- package/dist/services/app-rust-wasi-scaffold-template.service.d.ts +24 -0
- package/dist/services/app-rust-wasi-scaffold-template.service.d.ts.map +1 -0
- package/dist/services/app-rust-wasi-scaffold-template.service.js +394 -0
- package/dist/services/app-rust-wasi-scaffold-template.service.js.map +1 -0
- package/dist/services/app-scaffold.service.d.ts +4 -2
- package/dist/services/app-scaffold.service.d.ts.map +1 -1
- package/dist/services/app-scaffold.service.js +20 -4
- package/dist/services/app-scaffold.service.js.map +1 -1
- package/dist/services/platform-auth-state.service.js +1 -1
- package/dist/types/app-manifest.types.d.ts +8 -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 +8 -1
- package/dist/types/app-registry.types.d.ts.map +1 -1
- package/package.json +2 -1
- package/resources/rust-wasi/Cargo.lock +454 -0
- package/resources/wit/deps/clocks@0.2.6/monotonic-clock.wit +50 -0
- package/resources/wit/deps/clocks@0.2.6/timezone.wit +55 -0
- package/resources/wit/deps/clocks@0.2.6/wall-clock.wit +46 -0
- package/resources/wit/deps/clocks@0.2.6/world.wit +11 -0
- package/resources/wit/deps/config@0.2.0-draft-2024-09-27/package.wit +1 -0
- package/resources/wit/deps/config@0.2.0-draft-2024-09-27/store.wit +9 -0
- package/resources/wit/deps/http@0.2.6/handler.wit +49 -0
- package/resources/wit/deps/http@0.2.6/package.wit +1 -0
- package/resources/wit/deps/http@0.2.6/types.wit +688 -0
- package/resources/wit/deps/io@0.2.6/error.wit +34 -0
- package/resources/wit/deps/io@0.2.6/poll.wit +47 -0
- package/resources/wit/deps/io@0.2.6/streams.wit +290 -0
- package/resources/wit/deps/io@0.2.6/world.wit +10 -0
- package/resources/wit/deps/spin@2.0.0/package.wit +1 -0
- package/resources/wit/deps/spin@2.0.0/sqlite.wit +50 -0
- 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
|
|
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
|
|
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":";;;;;;;
|
|
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 {
|
|
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
|
-
|
|
9
|
+
parser;
|
|
15
10
|
constructor(appHomeService = new AppHomeService()) {
|
|
16
11
|
this.appHomeService = appHomeService;
|
|
17
|
-
this.
|
|
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.
|
|
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
|
|
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
|