@nextclaw/app-runtime 0.9.15 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/controllers/publish.controller.js +7 -0
  2. package/dist/controllers/publish.controller.js.map +1 -1
  3. package/dist/index.d.ts +8 -3
  4. package/dist/index.js +6 -1
  5. package/dist/package.js +1 -1
  6. package/dist/services/app-artifact-validation.service.d.ts +46 -0
  7. package/dist/services/app-artifact-validation.service.d.ts.map +1 -0
  8. package/dist/services/app-artifact-validation.service.js +250 -0
  9. package/dist/services/app-artifact-validation.service.js.map +1 -0
  10. package/dist/services/app-bundle.service.d.ts +4 -8
  11. package/dist/services/app-bundle.service.d.ts.map +1 -1
  12. package/dist/services/app-bundle.service.js +6 -65
  13. package/dist/services/app-bundle.service.js.map +1 -1
  14. package/dist/services/app-home.service.d.ts +4 -0
  15. package/dist/services/app-home.service.d.ts.map +1 -1
  16. package/dist/services/app-home.service.js +15 -1
  17. package/dist/services/app-home.service.js.map +1 -1
  18. package/dist/services/app-install-source.service.js +14 -1
  19. package/dist/services/app-install-source.service.js.map +1 -1
  20. package/dist/services/app-installation-filesystem.service.js +68 -0
  21. package/dist/services/app-installation-filesystem.service.js.map +1 -0
  22. package/dist/services/app-installation-integrity.service.d.ts +20 -0
  23. package/dist/services/app-installation-integrity.service.d.ts.map +1 -0
  24. package/dist/services/app-installation-integrity.service.js +84 -0
  25. package/dist/services/app-installation-integrity.service.js.map +1 -0
  26. package/dist/services/app-installation-lifecycle.service.js +110 -0
  27. package/dist/services/app-installation-lifecycle.service.js.map +1 -0
  28. package/dist/services/app-installation.service.d.ts +21 -11
  29. package/dist/services/app-installation.service.d.ts.map +1 -1
  30. package/dist/services/app-installation.service.js +262 -280
  31. package/dist/services/app-installation.service.js.map +1 -1
  32. package/dist/services/app-instance-storage.service.d.ts +49 -0
  33. package/dist/services/app-instance-storage.service.d.ts.map +1 -0
  34. package/dist/services/app-instance-storage.service.js +256 -0
  35. package/dist/services/app-instance-storage.service.js.map +1 -0
  36. package/dist/services/app-manifest.service.d.ts +5 -1
  37. package/dist/services/app-manifest.service.d.ts.map +1 -1
  38. package/dist/services/app-manifest.service.js +59 -3
  39. package/dist/services/app-manifest.service.js.map +1 -1
  40. package/dist/services/app-publish-validation.service.d.ts +1 -1
  41. package/dist/services/app-publish.service.d.ts +2 -1
  42. package/dist/services/app-publish.service.d.ts.map +1 -1
  43. package/dist/services/app-publish.service.js +3 -2
  44. package/dist/services/app-publish.service.js.map +1 -1
  45. package/dist/services/app-registry.service.d.ts +13 -3
  46. package/dist/services/app-registry.service.d.ts.map +1 -1
  47. package/dist/services/app-registry.service.js +143 -13
  48. package/dist/services/app-registry.service.js.map +1 -1
  49. package/dist/services/file-lock.service.d.ts +20 -0
  50. package/dist/services/file-lock.service.d.ts.map +1 -0
  51. package/dist/services/file-lock.service.js +156 -0
  52. package/dist/services/file-lock.service.js.map +1 -0
  53. package/dist/types/app-installation.types.d.ts +8 -1
  54. package/dist/types/app-installation.types.d.ts.map +1 -1
  55. package/dist/types/app-manifest.types.d.ts +22 -1
  56. package/dist/types/app-manifest.types.d.ts.map +1 -1
  57. package/dist/types/app-manifest.types.js.map +1 -1
  58. package/dist/types/app-publish.types.d.ts +4 -1
  59. package/dist/types/app-publish.types.d.ts.map +1 -1
  60. package/dist/types/app-publish.types.js.map +1 -1
  61. package/dist/types/app-registry.types.d.ts +7 -1
  62. package/dist/types/app-registry.types.d.ts.map +1 -1
  63. package/dist/types/app-remote-registry.types.d.ts +1 -1
  64. package/dist/types/app-storage.types.d.ts +37 -0
  65. package/dist/types/app-storage.types.d.ts.map +1 -0
  66. package/dist/types/app-storage.types.js +7 -0
  67. package/dist/types/app-storage.types.js.map +1 -0
  68. package/package.json +6 -1
@@ -1,19 +1,31 @@
1
1
  import { AppManifestService } from "./app-manifest.service.js";
2
- import { isAppComponentManifestBundle, isAppStandaloneManifestBundle } from "../types/app-manifest.types.js";
2
+ import { isAppComponentManifestBundle } from "../types/app-manifest.types.js";
3
3
  import { AppBundleService } from "./app-bundle.service.js";
4
4
  import { AppHomeService } from "./app-home.service.js";
5
+ import { FileLockService } from "./file-lock.service.js";
6
+ import { AppInstanceStorageService } from "./app-instance-storage.service.js";
7
+ import { AppInstallationIntegrityService } from "./app-installation-integrity.service.js";
8
+ import { AppInstallationFilesystemService } from "./app-installation-filesystem.service.js";
9
+ import { AppInstallationLifecycleService } from "./app-installation-lifecycle.service.js";
5
10
  import { AppBuildService } from "./app-build.service.js";
6
11
  import { AppRegistryConfigService } from "./app-registry-config.service.js";
7
12
  import { AppRemoteRegistryClientService } from "./app-remote-registry-client.service.js";
8
13
  import { AppRegistryService } from "./app-registry.service.js";
9
14
  import { AppInstallSourceService } from "./app-install-source.service.js";
10
- import { randomUUID } from "node:crypto";
11
- import { access, cp, mkdir, readdir, rename, rm } from "node:fs/promises";
15
+ import { rm } from "node:fs/promises";
12
16
  import path from "node:path";
17
+ import { AsyncLocalStorage } from "node:async_hooks";
13
18
  //#region src/services/app-installation.service.ts
19
+ const SAFE_APP_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
14
20
  var AppInstallationService = class {
15
21
  installSourceService;
16
- constructor(appHomeService = new AppHomeService(), bundleService = new AppBundleService(), manifestService = new AppManifestService(), buildService = new AppBuildService(), registryService = new AppRegistryService(appHomeService), registryConfigService = new AppRegistryConfigService(appHomeService), remoteRegistryClient = new AppRemoteRegistryClientService(new AppRegistryConfigService(appHomeService))) {
22
+ instanceStorageService;
23
+ fileLockService = new FileLockService();
24
+ integrityService;
25
+ filesystemService;
26
+ lifecycleService;
27
+ appOperationContext = new AsyncLocalStorage();
28
+ constructor(appHomeService = new AppHomeService(), bundleService = new AppBundleService(), manifestService = new AppManifestService(), buildService = new AppBuildService(), registryService = new AppRegistryService(appHomeService), registryConfigService = new AppRegistryConfigService(appHomeService), remoteRegistryClient = new AppRemoteRegistryClientService(new AppRegistryConfigService(appHomeService)), instanceStorageService) {
17
29
  this.appHomeService = appHomeService;
18
30
  this.bundleService = bundleService;
19
31
  this.manifestService = manifestService;
@@ -21,98 +33,49 @@ var AppInstallationService = class {
21
33
  this.registryService = registryService;
22
34
  this.registryConfigService = registryConfigService;
23
35
  this.remoteRegistryClient = remoteRegistryClient;
24
- this.installSourceService = new AppInstallSourceService(this.manifestService, this.remoteRegistryClient);
36
+ this.installSourceService = new AppInstallSourceService(this.manifestService, this.remoteRegistryClient, this.bundleService);
37
+ this.instanceStorageService = instanceStorageService ?? new AppInstanceStorageService(appHomeService);
38
+ this.integrityService = new AppInstallationIntegrityService(manifestService);
39
+ this.filesystemService = new AppInstallationFilesystemService({
40
+ buildService,
41
+ integrityService: this.integrityService,
42
+ manifestService
43
+ });
44
+ this.lifecycleService = new AppInstallationLifecycleService({
45
+ registryService,
46
+ registryConfigService,
47
+ remoteRegistryClient,
48
+ instanceStorageService: this.instanceStorageService,
49
+ integrityService: this.integrityService,
50
+ install: this.install,
51
+ rollback: this.rollback
52
+ });
25
53
  }
26
- install = async (appSource, options) => {
27
- const { onProgress, registryUrl } = options ?? {};
54
+ install = async (appSource, options = {}) => {
55
+ const { onProgress, registryUrl } = options;
28
56
  await onProgress?.("resolving");
29
57
  const source = await this.installSourceService.resolve(appSource, registryUrl);
30
58
  const tempDirectory = await this.appHomeService.createTemporaryDirectory("napp-install-");
31
59
  try {
32
- if (source.kind === "registry") await onProgress?.("downloading");
33
- const bundlePath = source.kind === "directory" ? (await this.bundleService.packAppDirectory({
34
- appDirectory: source.appDirectory,
35
- outputPath: path.join(tempDirectory, "app.napp")
36
- })).bundlePath : source.kind === "bundle" ? source.bundlePath : (await this.remoteRegistryClient.downloadBundle({
37
- resolution: source.registryResolution,
38
- targetDirectory: tempDirectory
39
- })).bundlePath;
60
+ const bundlePath = await this.installSourceService.materializeBundle(source, tempDirectory, onProgress);
40
61
  await onProgress?.("verifying");
41
62
  const extractedDirectory = path.join(tempDirectory, "bundle");
42
63
  const extractedMetadata = await this.bundleService.extractBundle({
43
64
  bundlePath,
44
65
  targetDirectory: extractedDirectory
45
66
  });
46
- await this.materializeDistribution({
67
+ await this.filesystemService.materializeDistribution({
47
68
  appDirectory: extractedDirectory,
48
69
  distributionMode: extractedMetadata.metadata.distributionMode
49
70
  });
50
71
  const manifestBundle = await this.manifestService.load(extractedDirectory);
51
- const installDirectory = this.appHomeService.getInstallDirectory(manifestBundle.manifest.id, manifestBundle.manifest.version);
52
- if (source.kind === "registry" && manifestBundle.manifest.id !== source.registryResolution.appId) throw new Error(`bundle manifest.appId 与 registry 请求不一致:期望 ${source.registryResolution.appId},实际 ${manifestBundle.manifest.id}`);
53
- await onProgress?.("installing");
54
- const dataDirectory = this.appHomeService.getAppDataDirectory(manifestBundle.manifest.id);
55
- await this.copyToImmutableInstallDirectory({
56
- appId: manifestBundle.manifest.id,
57
- appVersion: manifestBundle.manifest.version,
72
+ return await this.installPreparedBundle({
73
+ source,
58
74
  extractedDirectory,
59
- installDirectory
75
+ extractedMetadata,
76
+ manifestBundle,
77
+ options
60
78
  });
61
- await mkdir(dataDirectory, { recursive: true });
62
- await onProgress?.("finalizing");
63
- let registryRecord;
64
- try {
65
- registryRecord = await this.registryService.upsertInstallation({
66
- appId: manifestBundle.manifest.id,
67
- name: manifestBundle.manifest.name,
68
- description: manifestBundle.manifest.description,
69
- version: manifestBundle.manifest.version,
70
- installDirectory,
71
- dataDirectory,
72
- sourceKind: source.kind,
73
- distributionMode: source.kind === "registry" ? source.registryResolution.distributionMode : extractedMetadata.metadata.distributionMode,
74
- sourceRef: source.sourceRef,
75
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
76
- permissions: manifestBundle.manifest.schemaVersion === 1 ? manifestBundle.manifest.permissions ?? {} : {},
77
- registryUrl: source.kind === "registry" ? source.registryResolution.registryUrl : void 0,
78
- bundleUrl: source.kind === "registry" ? source.registryResolution.bundleUrl : void 0,
79
- sha256: source.kind === "registry" ? source.registryResolution.sha256 : void 0,
80
- publisher: source.kind === "registry" ? source.registryResolution.publisher : void 0,
81
- manifestSchemaVersion: manifestBundle.manifest.schemaVersion,
82
- components: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.components.map((component) => ({
83
- ...component,
84
- componentDirectory: path.join(installDirectory, component.path),
85
- manifestPath: path.join(installDirectory, component.path, component.kind === "panel" ? "panel-app.json" : "service-app.json")
86
- })) : void 0,
87
- primaryPanelId: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.primaryPanelId : void 0
88
- });
89
- } catch (error) {
90
- await rm(installDirectory, {
91
- recursive: true,
92
- force: true
93
- });
94
- throw error;
95
- }
96
- const activeVersionRecord = registryRecord.installedVersions[registryRecord.activeVersion];
97
- return {
98
- appId: registryRecord.appId,
99
- name: registryRecord.name,
100
- version: registryRecord.activeVersion,
101
- installDirectory,
102
- dataDirectory,
103
- sourceKind: source.kind,
104
- sourceRef: source.sourceRef,
105
- distributionMode: registryRecord.installedVersions[registryRecord.activeVersion]?.distributionMode,
106
- permissions: registryRecord.installedVersions[registryRecord.activeVersion]?.permissions ?? {},
107
- registryUrl: registryRecord.installedVersions[registryRecord.activeVersion]?.registryUrl,
108
- bundleUrl: registryRecord.installedVersions[registryRecord.activeVersion]?.bundleUrl,
109
- sha256: registryRecord.installedVersions[registryRecord.activeVersion]?.sha256,
110
- publisher: registryRecord.installedVersions[registryRecord.activeVersion]?.publisher,
111
- enabled: registryRecord.enabled,
112
- manifestSchemaVersion: activeVersionRecord?.manifestSchemaVersion ?? 1,
113
- components: activeVersionRecord?.components,
114
- primaryPanelId: activeVersionRecord?.primaryPanelId
115
- };
116
79
  } finally {
117
80
  await rm(tempDirectory, {
118
81
  recursive: true,
@@ -120,117 +83,104 @@ var AppInstallationService = class {
120
83
  });
121
84
  }
122
85
  };
123
- update = async (appId, options) => {
124
- const { onProgress, registryUrl: requestedRegistryUrl, version } = options ?? {};
125
- await onProgress?.("resolving");
126
- const appRecord = await this.registryService.getApp(appId);
127
- if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
128
- const activeVersionRecord = appRecord.installedVersions[appRecord.activeVersion];
129
- const registryUrl = requestedRegistryUrl ?? activeVersionRecord?.registryUrl ?? (await this.registryConfigService.getSnapshot()).currentUrl;
130
- const resolution = await this.remoteRegistryClient.resolve({
86
+ installPreparedBundle = async (prepared) => {
87
+ const appId = prepared.manifestBundle.manifest.id;
88
+ return await this.withAppOperation(appId, async () => await this.commitPreparedInstall(prepared));
89
+ };
90
+ commitPreparedInstall = async (prepared) => {
91
+ const { extractedDirectory, extractedMetadata, manifestBundle, options, source } = prepared;
92
+ const { onProgress } = options;
93
+ const { id: appId, version } = manifestBundle.manifest;
94
+ const installDirectory = this.appHomeService.getInstallDirectory(appId, version);
95
+ if (source.kind === "registry" && appId !== source.registryResolution.appId) throw new Error(`bundle manifest.appId 与 registry 请求不一致:期望 ${source.registryResolution.appId},实际 ${appId}`);
96
+ await onProgress?.("installing");
97
+ const existingRecord = await this.registryService.getApp(appId);
98
+ const publisher = source.kind === "registry" ? source.registryResolution.publisher : options.trustedPublisher;
99
+ const currentPublisher = existingRecord?.publisher ?? existingRecord?.installedVersions[existingRecord.activeVersion]?.publisher;
100
+ if (currentPublisher && currentPublisher.id !== publisher?.id) throw new Error(`应用 ${appId} 已绑定发布者 ${currentPublisher.id},拒绝由 ${publisher?.id ?? "未验证本地来源"} 覆盖。`);
101
+ const dataSchemaVersion = manifestBundle.manifest.schemaVersion === 2 ? manifestBundle.manifest.storage?.schemaVersion ?? 1 : 1;
102
+ if (existingRecord?.defaultInstance.dataSchemaVersion !== void 0 && existingRecord.defaultInstance.dataSchemaVersion !== dataSchemaVersion) throw new Error(`应用 ${appId} 数据 schema 从 ${existingRecord.defaultInstance.dataSchemaVersion} 升级到 ${dataSchemaVersion},但包内没有受支持的迁移合同。更新已停止,现有数据和版本保持不变。`);
103
+ const instanceDirectory = this.appHomeService.getAppInstanceDirectory(appId, "default");
104
+ const instanceExisted = await this.integrityService.pathExists(instanceDirectory);
105
+ const contentSha256 = await this.filesystemService.copyToImmutableInstallDirectory({
131
106
  appId,
132
- version,
133
- registryUrl
107
+ appVersion: version,
108
+ extractedDirectory,
109
+ installDirectory
134
110
  });
135
- if (resolution.version === appRecord.activeVersion) {
136
- if (!activeVersionRecord) throw new Error(`已安装应用缺少激活版本:${appId}`);
137
- return {
138
- appId: appRecord.appId,
139
- name: appRecord.name,
140
- version: appRecord.activeVersion,
141
- previousVersion: appRecord.activeVersion,
142
- installDirectory: activeVersionRecord.installDirectory,
143
- dataDirectory: appRecord.dataDirectory,
144
- sourceKind: activeVersionRecord.sourceKind,
145
- sourceRef: activeVersionRecord.sourceRef,
146
- permissions: activeVersionRecord.permissions,
147
- registryUrl: activeVersionRecord.registryUrl,
148
- bundleUrl: activeVersionRecord.bundleUrl,
149
- sha256: activeVersionRecord.sha256,
150
- publisher: activeVersionRecord.publisher,
151
- enabled: appRecord.enabled,
152
- manifestSchemaVersion: activeVersionRecord.manifestSchemaVersion,
153
- components: activeVersionRecord.components,
154
- primaryPanelId: activeVersionRecord.primaryPanelId,
155
- updated: false
156
- };
157
- }
158
- const installedTarget = appRecord.installedVersions[resolution.version];
159
- if (installedTarget) {
160
- await onProgress?.("verifying");
161
- await onProgress?.("installing");
162
- await this.rollback(appId, resolution.version);
163
- await onProgress?.("finalizing");
164
- return {
165
- appId: appRecord.appId,
166
- name: appRecord.name,
167
- version: resolution.version,
168
- previousVersion: appRecord.activeVersion,
169
- installDirectory: installedTarget.installDirectory,
170
- dataDirectory: appRecord.dataDirectory,
171
- sourceKind: installedTarget.sourceKind,
172
- distributionMode: installedTarget.distributionMode,
173
- sourceRef: installedTarget.sourceRef,
174
- permissions: installedTarget.permissions,
175
- registryUrl: installedTarget.registryUrl,
176
- bundleUrl: installedTarget.bundleUrl,
177
- sha256: installedTarget.sha256,
178
- publisher: installedTarget.publisher,
179
- enabled: appRecord.enabled,
180
- manifestSchemaVersion: installedTarget.manifestSchemaVersion,
181
- components: installedTarget.components,
182
- primaryPanelId: installedTarget.primaryPanelId,
183
- updated: true
184
- };
185
- }
186
- return {
187
- ...await this.install(`${appId}@${resolution.version}`, {
188
- registryUrl,
189
- onProgress
190
- }),
191
- previousVersion: appRecord.activeVersion,
192
- updated: true
193
- };
194
- };
195
- uninstall = async (appId, purgeData) => {
196
- const appRecord = await this.registryService.getApp(appId);
197
- if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
198
- const removedVersions = Object.keys(appRecord.installedVersions).sort((left, right) => left.localeCompare(right));
199
- const stagedPaths = [];
200
- const stagePath = async (originalPath) => {
201
- if (!await this.pathExists(originalPath)) return;
202
- const stagedPath = `${originalPath}.uninstalling-${randomUUID()}`;
203
- await rename(originalPath, stagedPath);
204
- stagedPaths.push({
205
- originalPath,
206
- stagedPath
207
- });
208
- };
209
- const restoreStagedPaths = async () => {
210
- for (const entry of [...stagedPaths].reverse()) if (await this.pathExists(entry.stagedPath)) await rename(entry.stagedPath, entry.originalPath);
211
- };
111
+ const defaultInstance = await this.instanceStorageService.materializeDefaultInstance({
112
+ appId,
113
+ publisherId: (currentPublisher ?? publisher)?.id,
114
+ legacyDataDirectory: existingRecord?.defaultInstance.storage.layout === "legacy" ? existingRecord.dataDirectory : void 0,
115
+ dataSchemaVersion
116
+ });
117
+ await onProgress?.("finalizing");
118
+ let registryRecord;
212
119
  try {
213
- for (const versionRecord of Object.values(appRecord.installedVersions)) await stagePath(versionRecord.installDirectory);
214
- if (purgeData) await stagePath(appRecord.dataDirectory);
215
- if (!await this.registryService.removeApp(appId)) throw new Error(`卸载过程中应用记录已发生变化:${appId}`);
120
+ registryRecord = await this.registryService.upsertInstallation({
121
+ appId,
122
+ name: manifestBundle.manifest.name,
123
+ description: manifestBundle.manifest.description,
124
+ version,
125
+ installDirectory,
126
+ defaultInstance,
127
+ sourceKind: source.kind,
128
+ distributionMode: source.kind === "registry" ? source.registryResolution.distributionMode : extractedMetadata.metadata.distributionMode,
129
+ sourceRef: source.sourceRef,
130
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
131
+ permissions: manifestBundle.manifest.schemaVersion === 1 ? manifestBundle.manifest.permissions ?? {} : this.manifestService.resolvePlatformSecurity(manifestBundle.manifest).permissions,
132
+ registryUrl: source.kind === "registry" ? source.registryResolution.registryUrl : void 0,
133
+ bundleUrl: source.kind === "registry" ? source.registryResolution.bundleUrl : void 0,
134
+ sha256: source.kind === "registry" ? source.registryResolution.sha256 : void 0,
135
+ publisher,
136
+ manifestSchemaVersion: manifestBundle.manifest.schemaVersion,
137
+ components: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.components.map((component) => ({
138
+ ...component,
139
+ componentDirectory: path.join(installDirectory, component.path),
140
+ manifestPath: path.join(installDirectory, component.path, component.kind === "panel" ? "panel-app.json" : "service-app.json")
141
+ })) : void 0,
142
+ primaryPanelId: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.primaryPanelId : void 0,
143
+ security: isAppComponentManifestBundle(manifestBundle) ? this.manifestService.resolvePlatformSecurity(manifestBundle.manifest) : void 0,
144
+ dataSchemaVersion,
145
+ contentSha256,
146
+ activate: options.activate
147
+ });
216
148
  } catch (error) {
217
- try {
218
- await restoreStagedPaths();
219
- } catch (restoreError) {
220
- throw new AggregateError([error, restoreError], `卸载 ${appId} 失败,且无法完全恢复已暂存文件。`);
221
- }
149
+ await this.integrityService.removeDirectory(installDirectory);
150
+ if (!instanceExisted) await this.instanceStorageService.rollbackNewInstance({
151
+ instance: defaultInstance,
152
+ legacyDataDirectory: existingRecord?.defaultInstance.storage.layout === "legacy" ? existingRecord.dataDirectory : void 0
153
+ });
222
154
  throw error;
223
155
  }
224
- await Promise.all(stagedPaths.map((entry) => rm(entry.stagedPath, {
225
- recursive: true,
226
- force: true
227
- })));
156
+ const installedVersion = registryRecord.installedVersions[version];
228
157
  return {
229
- appId,
230
- removedVersions,
231
- dataRemoved: purgeData
158
+ appId: registryRecord.appId,
159
+ name: registryRecord.name,
160
+ version,
161
+ installDirectory,
162
+ dataDirectory: defaultInstance.storage.dataDirectory,
163
+ instance: registryRecord.defaultInstance,
164
+ sourceKind: source.kind,
165
+ sourceRef: source.sourceRef,
166
+ distributionMode: installedVersion?.distributionMode,
167
+ permissions: installedVersion?.permissions ?? {},
168
+ registryUrl: installedVersion?.registryUrl,
169
+ bundleUrl: installedVersion?.bundleUrl,
170
+ sha256: installedVersion?.sha256,
171
+ publisher: installedVersion?.publisher,
172
+ enabled: registryRecord.enabled,
173
+ manifestSchemaVersion: installedVersion?.manifestSchemaVersion ?? 1,
174
+ components: installedVersion?.components,
175
+ primaryPanelId: installedVersion?.primaryPanelId
232
176
  };
233
177
  };
178
+ update = async (appId, options) => {
179
+ return await this.withAppOperation(appId, async () => await this.lifecycleService.update(appId, options));
180
+ };
181
+ uninstall = async (appId, purgeData) => {
182
+ return await this.withAppOperation(appId, async () => await this.lifecycleService.uninstall(appId, purgeData));
183
+ };
234
184
  list = async () => {
235
185
  return (await this.registryService.listApps()).map((appRecord) => ({
236
186
  appId: appRecord.appId,
@@ -245,12 +195,74 @@ var AppInstallationService = class {
245
195
  };
246
196
  reconcileFilesystem = async () => {
247
197
  await this.appHomeService.ensureBaseDirectories();
248
- const appRecords = await this.registryService.listApps();
198
+ let appRecords = await this.registryService.listApps();
199
+ for (const appRecord of appRecords) await this.withAppOperation(appRecord.appId, async () => {
200
+ for (const versionRecord of Object.values(appRecord.installedVersions)) {
201
+ if (!await this.integrityService.pathExists(versionRecord.installDirectory)) continue;
202
+ if (!versionRecord.contentSha256) {
203
+ const contentSha256 = await this.integrityService.calculateDigest(versionRecord.installDirectory);
204
+ await this.registryService.setVersionContentDigest(appRecord.appId, versionRecord.version, contentSha256);
205
+ }
206
+ await this.integrityService.protectDirectory(versionRecord.installDirectory);
207
+ }
208
+ });
209
+ appRecords = await this.registryService.listApps();
249
210
  const referencedInstallPaths = new Set(appRecords.flatMap((record) => Object.values(record.installedVersions).map((version) => path.resolve(version.installDirectory))));
250
- const referencedDataPaths = new Set(appRecords.map((record) => path.resolve(record.dataDirectory)));
251
- const packageDirectories = await this.readDirectories(this.appHomeService.getPackagesDirectory());
252
- for (const appDirectory of packageDirectories) await this.reconcileGeneratedSiblings(appDirectory, referencedInstallPaths);
253
- await this.reconcileGeneratedSiblings(this.appHomeService.getDataDirectory(), referencedDataPaths);
211
+ const referencedDataPaths = new Set(appRecords.map((record) => path.resolve(record.defaultInstance.storage.layout === "instance-v1" ? record.defaultInstance.storage.instanceDirectory : record.dataDirectory)));
212
+ const packageDirectories = await this.filesystemService.readDirectories(this.appHomeService.getPackagesDirectory());
213
+ for (const appDirectory of packageDirectories) await this.filesystemService.reconcileGeneratedSiblings(appDirectory, referencedInstallPaths);
214
+ await this.filesystemService.reconcileGeneratedSiblings(this.appHomeService.getDataDirectory(), referencedDataPaths);
215
+ for (const appRecord of appRecords) {
216
+ if (appRecord.defaultInstance.storage.layout !== "instance-v1") continue;
217
+ await this.filesystemService.reconcileGeneratedSiblings(appRecord.defaultInstance.storage.instanceDirectory, new Set([path.resolve(appRecord.defaultInstance.storage.dataDirectory)]));
218
+ }
219
+ for (const appRecord of appRecords) {
220
+ if (appRecord.defaultInstance.storage.layout !== "legacy") continue;
221
+ await this.withAppOperation(appRecord.appId, async () => {
222
+ const currentRecord = await this.registryService.getApp(appRecord.appId);
223
+ if (!currentRecord || currentRecord.defaultInstance.storage.layout !== "legacy") return;
224
+ const activeVersion = currentRecord.installedVersions[currentRecord.activeVersion];
225
+ if (!activeVersion) throw new Error(`已安装应用缺少激活版本:${currentRecord.appId}`);
226
+ const instance = await this.instanceStorageService.materializeDefaultInstance({
227
+ appId: currentRecord.appId,
228
+ publisherId: currentRecord.publisher?.id,
229
+ legacyDataDirectory: currentRecord.dataDirectory,
230
+ dataSchemaVersion: currentRecord.defaultInstance.dataSchemaVersion
231
+ });
232
+ try {
233
+ await this.registryService.upsertInstallation({
234
+ appId: currentRecord.appId,
235
+ name: currentRecord.name,
236
+ description: currentRecord.description,
237
+ version: currentRecord.activeVersion,
238
+ installDirectory: activeVersion.installDirectory,
239
+ defaultInstance: instance,
240
+ sourceKind: activeVersion.sourceKind,
241
+ sourceRef: activeVersion.sourceRef,
242
+ installedAt: activeVersion.installedAt,
243
+ distributionMode: activeVersion.distributionMode,
244
+ permissions: activeVersion.permissions,
245
+ registryUrl: activeVersion.registryUrl,
246
+ bundleUrl: activeVersion.bundleUrl,
247
+ sha256: activeVersion.sha256,
248
+ publisher: currentRecord.publisher ?? activeVersion.publisher,
249
+ manifestSchemaVersion: activeVersion.manifestSchemaVersion,
250
+ components: activeVersion.components,
251
+ primaryPanelId: activeVersion.primaryPanelId,
252
+ security: activeVersion.security,
253
+ dataSchemaVersion: activeVersion.dataSchemaVersion,
254
+ contentSha256: activeVersion.contentSha256,
255
+ enabled: currentRecord.enabled
256
+ });
257
+ } catch (error) {
258
+ await this.instanceStorageService.rollbackNewInstance({
259
+ instance,
260
+ legacyDataDirectory: currentRecord.dataDirectory
261
+ });
262
+ throw error;
263
+ }
264
+ });
265
+ }
254
266
  };
255
267
  info = async (appId) => {
256
268
  const appRecord = await this.registryService.getApp(appId);
@@ -263,6 +275,9 @@ var AppInstallationService = class {
263
275
  activeVersion: appRecord.activeVersion,
264
276
  enabled: appRecord.enabled,
265
277
  dataDirectory: appRecord.dataDirectory,
278
+ instance: appRecord.defaultInstance,
279
+ storage: appRecord.defaultInstance.storage,
280
+ storageUsage: await this.instanceStorageService.measureUsage(appRecord.defaultInstance.storage),
266
281
  installedVersions: installedVersions.map((versionRecord) => ({
267
282
  version: versionRecord.version,
268
283
  installDirectory: versionRecord.installDirectory,
@@ -277,41 +292,73 @@ var AppInstallationService = class {
277
292
  publisher: versionRecord.publisher,
278
293
  manifestSchemaVersion: versionRecord.manifestSchemaVersion,
279
294
  components: versionRecord.components,
280
- primaryPanelId: versionRecord.primaryPanelId
295
+ primaryPanelId: versionRecord.primaryPanelId,
296
+ contentSha256: versionRecord.contentSha256
281
297
  })),
282
298
  grants: appRecord.grants
283
299
  };
284
300
  };
285
301
  setEnabled = async (appId, enabled) => {
286
- const appRecord = await this.registryService.setEnabled(appId, enabled);
287
- return {
288
- appId: appRecord.appId,
289
- activeVersion: appRecord.activeVersion,
290
- enabled: appRecord.enabled
291
- };
302
+ return await this.withAppOperation(appId, async () => {
303
+ const appRecord = await this.registryService.setEnabled(appId, enabled);
304
+ return {
305
+ appId: appRecord.appId,
306
+ activeVersion: appRecord.activeVersion,
307
+ enabled: appRecord.enabled
308
+ };
309
+ });
292
310
  };
293
311
  rollback = async (appId, version) => {
294
- const currentRecord = await this.registryService.getApp(appId);
295
- if (!currentRecord) throw new Error(`未找到已安装应用:${appId}`);
296
- if (currentRecord.activeVersion === version) return {
297
- appId,
298
- activeVersion: version,
299
- previousVersion: version,
300
- enabled: currentRecord.enabled,
301
- rolledBack: false
302
- };
303
- const targetVersion = currentRecord.installedVersions[version];
304
- if (!targetVersion) throw new Error(`应用 ${appId} 未安装版本 ${version}。`);
305
- const targetManifest = await this.manifestService.load(targetVersion.installDirectory);
306
- if (targetManifest.manifest.id !== appId || targetManifest.manifest.version !== version) throw new Error(`回滚目标 ${appId}@${version} 校验失败。`);
307
- const appRecord = await this.registryService.activateVersion(appId, version);
308
- return {
312
+ return await this.withAppOperation(appId, async () => {
313
+ const currentRecord = await this.registryService.getApp(appId);
314
+ if (!currentRecord) throw new Error(`未找到已安装应用:${appId}`);
315
+ if (currentRecord.activeVersion === version) return {
316
+ appId,
317
+ activeVersion: version,
318
+ previousVersion: version,
319
+ enabled: currentRecord.enabled,
320
+ rolledBack: false
321
+ };
322
+ const targetVersion = currentRecord.installedVersions[version];
323
+ if (!targetVersion) throw new Error(`应用 ${appId} 未安装版本 ${version}。`);
324
+ if (targetVersion.dataSchemaVersion !== currentRecord.defaultInstance.dataSchemaVersion) throw new Error(`应用 ${appId}@${version} 需要数据 schema ${targetVersion.dataSchemaVersion},当前实例为 ${currentRecord.defaultInstance.dataSchemaVersion}。没有可用 checkpoint,已阻止回滚。`);
325
+ await this.assertVersionIntegrity(appId, version);
326
+ const appRecord = await this.registryService.activateVersion(appId, version);
327
+ return {
328
+ appId,
329
+ activeVersion: appRecord.activeVersion,
330
+ previousVersion: currentRecord.activeVersion,
331
+ enabled: appRecord.enabled,
332
+ rolledBack: true
333
+ };
334
+ });
335
+ };
336
+ assertVersionIntegrity = async (appId, version) => {
337
+ const appRecord = await this.registryService.getApp(appId);
338
+ if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
339
+ const targetVersion = version ?? appRecord.activeVersion;
340
+ const versionRecord = appRecord.installedVersions[targetVersion];
341
+ if (!versionRecord) throw new Error(`应用 ${appId} 未安装版本 ${targetVersion}。`);
342
+ const contentSha256 = await this.integrityService.assertVersion({
309
343
  appId,
310
- activeVersion: appRecord.activeVersion,
311
- previousVersion: currentRecord.activeVersion,
312
- enabled: appRecord.enabled,
313
- rolledBack: true
314
- };
344
+ version: targetVersion,
345
+ versionRecord
346
+ });
347
+ if (!versionRecord.contentSha256) {
348
+ await this.registryService.setVersionContentDigest(appId, targetVersion, contentSha256);
349
+ await this.integrityService.protectDirectory(versionRecord.installDirectory);
350
+ }
351
+ };
352
+ withAppOperation = async (appId, operation) => {
353
+ if (!SAFE_APP_ID_PATTERN.test(appId)) throw new Error(`appId 不是安全的 App 操作标识:${appId}`);
354
+ const lockPath = path.resolve(this.appHomeService.getAppOperationLockPath(appId));
355
+ const activeLocks = this.appOperationContext.getStore();
356
+ if (activeLocks?.has(lockPath)) return await operation();
357
+ return await this.fileLockService.withLock(lockPath, async () => {
358
+ const nextLocks = new Set(activeLocks ?? []);
359
+ nextLocks.add(lockPath);
360
+ return await this.appOperationContext.run(nextLocks, operation);
361
+ });
315
362
  };
316
363
  resolveLaunch = async (appReference, explicitDocumentGrantMap) => {
317
364
  const sourceType = await this.installSourceService.detectLocal(appReference, false);
@@ -323,10 +370,12 @@ var AppInstallationService = class {
323
370
  if (!appRecord) throw new Error(`未找到应用目录,也未找到已安装应用:${appReference}`);
324
371
  const activeVersion = appRecord.installedVersions[appRecord.activeVersion];
325
372
  if (!activeVersion) throw new Error(`已安装应用缺少激活版本:${appReference}`);
373
+ await this.assertVersionIntegrity(appRecord.appId, appRecord.activeVersion);
326
374
  return {
327
375
  appDirectory: activeVersion.installDirectory,
328
376
  appId: appRecord.appId,
329
377
  dataDirectory: appRecord.dataDirectory,
378
+ storage: appRecord.defaultInstance.storage,
330
379
  documentGrantMap: {
331
380
  ...appRecord.grants,
332
381
  ...explicitDocumentGrantMap
@@ -337,73 +386,6 @@ var AppInstallationService = class {
337
386
  if (!appId || Object.keys(documentGrantMap).length === 0) return;
338
387
  await this.registryService.updateGrants(appId, documentGrantMap);
339
388
  };
340
- copyToImmutableInstallDirectory = async (params) => {
341
- const { appId, appVersion, extractedDirectory, installDirectory } = params;
342
- if (await this.pathExists(installDirectory)) throw new Error(`应用版本目录已存在,不能覆盖不可变版本:${appId}@${appVersion}`);
343
- await mkdir(path.dirname(installDirectory), { recursive: true });
344
- const stagedInstallDirectory = `${installDirectory}.staging-${randomUUID()}`;
345
- try {
346
- await cp(extractedDirectory, stagedInstallDirectory, { recursive: true });
347
- const stagedManifest = await this.manifestService.load(stagedInstallDirectory);
348
- if (stagedManifest.manifest.id !== appId || stagedManifest.manifest.version !== appVersion) throw new Error("staging manifest 与已验证 bundle 身份不一致。");
349
- await rename(stagedInstallDirectory, installDirectory);
350
- } catch (error) {
351
- await rm(stagedInstallDirectory, {
352
- recursive: true,
353
- force: true
354
- });
355
- throw error;
356
- }
357
- };
358
- materializeDistribution = async (params) => {
359
- if (params.distributionMode !== "source") return;
360
- const manifestBundle = await this.manifestService.load(params.appDirectory);
361
- if (!isAppStandaloneManifestBundle(manifestBundle)) throw new Error("schema v2 组合包不允许运行安装期 build。");
362
- if (manifestBundle.manifest.main.kind !== "wasi-http-component") return;
363
- await this.buildService.build({
364
- appDirectory: params.appDirectory,
365
- install: true
366
- });
367
- };
368
- reconcileGeneratedSiblings = async (parentDirectory, referencedPaths) => {
369
- for (const entry of await this.readDirectories(parentDirectory)) {
370
- const entryName = path.basename(entry);
371
- const stagingMarker = ".staging-";
372
- const uninstallingMarker = ".uninstalling-";
373
- if (entryName.includes(stagingMarker)) {
374
- await rm(entry, {
375
- recursive: true,
376
- force: true
377
- });
378
- continue;
379
- }
380
- const markerIndex = entryName.indexOf(uninstallingMarker);
381
- if (markerIndex < 0) continue;
382
- const originalPath = path.join(parentDirectory, entryName.slice(0, markerIndex));
383
- if (referencedPaths.has(path.resolve(originalPath)) && !await this.pathExists(originalPath)) await rename(entry, originalPath);
384
- else await rm(entry, {
385
- recursive: true,
386
- force: true
387
- });
388
- }
389
- };
390
- readDirectories = async (directory) => {
391
- try {
392
- return (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path.join(directory, entry.name));
393
- } catch (error) {
394
- if (this.isMissingFileError(error)) return [];
395
- throw error;
396
- }
397
- };
398
- pathExists = async (targetPath) => {
399
- try {
400
- await access(targetPath);
401
- return true;
402
- } catch {
403
- return false;
404
- }
405
- };
406
- isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
407
389
  };
408
390
  //#endregion
409
391
  export { AppInstallationService };