@timurproko/a1 0.1.8-dev.259 → 0.1.8-dev.269

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 (32) hide show
  1. package/bin/update-recovery.js +272 -0
  2. package/dist/foundation/release/bootstrap.d.ts +2 -0
  3. package/dist/foundation/release/bootstrap.js +4 -0
  4. package/dist/foundation/release/dependency-layer.d.ts +5 -1
  5. package/dist/foundation/release/dependency-layer.js +15 -6
  6. package/dist/foundation/release/index.d.ts +1 -0
  7. package/dist/foundation/release/index.js +1 -0
  8. package/dist/foundation/release/release-gc.js +2 -0
  9. package/dist/foundation/release/release-store.d.ts +4 -2
  10. package/dist/foundation/release/release-store.js +4 -4
  11. package/dist/foundation/release/update-recovery.d.ts +74 -0
  12. package/dist/foundation/release/update-recovery.js +352 -0
  13. package/dist/foundation/release/update-transaction.d.ts +10 -0
  14. package/dist/foundation/release/update-transaction.js +12 -1
  15. package/dist/foundation/release/update.d.ts +19 -1
  16. package/dist/foundation/release/update.js +67 -10
  17. package/dist/foundation/supervision/main.js +5 -2
  18. package/dist/integrations/pi/engine/conformance.d.ts +1 -1
  19. package/dist/integrations/pi/engine/conformance.js +9 -1
  20. package/dist/integrations/pi/engine/runtime-integration.js +10 -1
  21. package/dist/integrations/pi/engine/windows-filesystem-hygiene.d.ts +21 -0
  22. package/dist/integrations/pi/engine/windows-filesystem-hygiene.js +55 -0
  23. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
  24. package/dist/integrations/pi/session-ui/session-shell-root.js +29 -20
  25. package/dist/native/darwin-arm64/manifest.json +1 -1
  26. package/dist/native/linux-x64/manifest.json +1 -1
  27. package/dist/native/win32-x64/manifest.json +2 -2
  28. package/dist/native/win32-x64/process-guardian.exe +0 -0
  29. package/dist/runtime-payload-inventory.json +1 -0
  30. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  31. package/dist/ui/components/transcript-viewport.js +30 -6
  32. package/package.json +1 -1
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Security: this file intentionally depends only on Node built-ins: the updater copies it into a
4
+ // transaction capsule before npm is allowed to rename or remove the installed package.
5
+ import { spawn } from "node:child_process";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
8
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const SCHEMA = "a1-update-recovery-v1";
12
+ const [, , mode, manifestPath, separator, ...forwarded] = process.argv;
13
+
14
+ if ((mode !== "--worker" && mode !== "--launch") || !manifestPath || (mode === "--launch" && separator !== "--")) {
15
+ throw new Error("invalid A1 update recovery invocation");
16
+ }
17
+
18
+ const capsule = await readCapsule(manifestPath);
19
+ if (mode === "--worker") {
20
+ await runWorker(capsule);
21
+ } else {
22
+ process.exitCode = await runRecoveredCommand(capsule, forwarded);
23
+ }
24
+
25
+ async function runWorker(value) {
26
+ await rm(value.resultPath, { force: true });
27
+ await writeJson(value.ownerPath, {
28
+ schema: SCHEMA,
29
+ transactionId: value.transactionId,
30
+ pid: process.pid,
31
+ startIdentity: `${process.pid}:${Math.floor(Date.now() - process.uptime() * 1000)}`,
32
+ startedAt: new Date().toISOString(),
33
+ });
34
+ const heartbeat = setInterval(() => {
35
+ void writeJson(value.ownerPath, {
36
+ schema: SCHEMA,
37
+ transactionId: value.transactionId,
38
+ pid: process.pid,
39
+ startIdentity: `${process.pid}:${Math.floor(Date.now() - process.uptime() * 1000)}`,
40
+ heartbeatAt: new Date().toISOString(),
41
+ });
42
+ }, 500);
43
+ heartbeat.unref?.();
44
+ const child = spawn(process.execPath, [value.npmCli, ...value.npmArguments], {
45
+ detached: false,
46
+ windowsHide: true,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ const stdout = [];
50
+ const stderr = [];
51
+ child.stdout?.on("data", chunk => stdout.push(Buffer.from(chunk)));
52
+ child.stderr?.on("data", chunk => stderr.push(Buffer.from(chunk)));
53
+ let cancelled = false;
54
+ let forced = false;
55
+ let forceTimer = null;
56
+ const cancellationPoll = setInterval(async () => {
57
+ if (cancelled || !await exists(value.cancellationPath)) return;
58
+ cancelled = true;
59
+ child.kill("SIGTERM");
60
+ forceTimer = setTimeout(() => { forced = true; child.kill("SIGKILL"); }, 2_000);
61
+ forceTimer.unref?.();
62
+ }, 50);
63
+ cancellationPoll.unref?.();
64
+
65
+ let npmExitCode = null;
66
+ let spawnError = null;
67
+ try {
68
+ npmExitCode = await new Promise((resolvePromise, rejectPromise) => {
69
+ child.once("error", rejectPromise);
70
+ child.once("close", code => resolvePromise(code));
71
+ });
72
+ } catch (error) {
73
+ spawnError = error;
74
+ } finally {
75
+ clearInterval(cancellationPoll);
76
+ clearInterval(heartbeat);
77
+ if (forceTimer) clearTimeout(forceTimer);
78
+ }
79
+
80
+ const targetReady = await installedTargetIsCallable(value);
81
+ let launcherDisposition = "unavailable";
82
+ let outcome = "failed";
83
+ if (targetReady) {
84
+ launcherDisposition = "target";
85
+ outcome = "installed";
86
+ } else {
87
+ await writeRecoveryLaunchers(value);
88
+ if (await recoveryLaunchersAreCallable(value)) {
89
+ launcherDisposition = "recovery";
90
+ outcome = "recovery-launcher";
91
+ }
92
+ }
93
+ await writeJson(value.resultPath, {
94
+ schema: SCHEMA,
95
+ transactionId: value.transactionId,
96
+ outcome,
97
+ npmExitCode,
98
+ cancelled,
99
+ launcherDisposition,
100
+ stdout: bounded(Buffer.concat(stdout).toString("utf8")),
101
+ stderr: bounded(`${Buffer.concat(stderr).toString("utf8")}${spawnError ? `\n${String(spawnError)}` : ""}${forced ? "\nforced npm termination after cancellation" : ""}`.trim()),
102
+ completedAt: new Date().toISOString(),
103
+ });
104
+ if (launcherDisposition === "unavailable") process.exitCode = 1;
105
+ }
106
+
107
+ async function runRecoveredCommand(value, arguments_) {
108
+ let entry = await installedTargetEntry(value);
109
+ if (entry === null && arguments_[0] === "update") {
110
+ await rm(value.cancellationPath, { force: true });
111
+ await rm(value.resultPath, { force: true });
112
+ await rm(value.ownerPath, { force: true });
113
+ const worker = spawn(process.execPath, [fileURLToPath(import.meta.url), "--worker", manifestPath], {
114
+ detached: true,
115
+ windowsHide: true,
116
+ stdio: "ignore",
117
+ });
118
+ worker.unref();
119
+ const deadline = Date.now() + 15 * 60 * 1000;
120
+ const requestCancellation = signal => { void writeJson(value.cancellationPath, { schema: SCHEMA, transactionId: value.transactionId, signal, requestedAt: new Date().toISOString() }); };
121
+ const onSigint = () => requestCancellation("SIGINT");
122
+ const onSigterm = () => requestCancellation("SIGTERM");
123
+ process.on("SIGINT", onSigint);
124
+ process.on("SIGTERM", onSigterm);
125
+ try {
126
+ while (Date.now() < deadline && !await exists(value.resultPath)) await sleep(50);
127
+ } finally {
128
+ process.off("SIGINT", onSigint);
129
+ process.off("SIGTERM", onSigterm);
130
+ }
131
+ entry = await installedTargetEntry(value);
132
+ }
133
+ entry ??= resolve(value.priorReleaseRoot, "bin", "cli.js");
134
+ const child = spawn(process.execPath, [entry, ...arguments_], { stdio: "inherit", windowsHide: false });
135
+ return await new Promise(resolvePromise => {
136
+ child.once("error", error => { console.error(error instanceof Error ? error.message : String(error)); resolvePromise(1); });
137
+ child.once("close", code => resolvePromise(code ?? 1));
138
+ });
139
+ }
140
+
141
+ async function readCapsule(path) {
142
+ const value = JSON.parse(await readFile(path, "utf8"));
143
+ if (value.schema !== SCHEMA || typeof value.transactionId !== "string" || typeof value.packageName !== "string"
144
+ || typeof value.packageName !== "string" || !value.packageName.includes("/") || typeof value.targetVersion !== "string" || typeof value.packageRoot !== "string" || typeof value.globalRoot !== "string"
145
+ || typeof value.launcherRoot !== "string" || !Array.isArray(value.launchers) || typeof value.priorReleaseId !== "string" || typeof value.priorReleaseRoot !== "string"
146
+ || !/^[a-f0-9]{64}$/.test(value.priorContentDigest) || typeof value.releaseManifestName !== "string"
147
+ || typeof value.recoveryEntry !== "string" || typeof value.recoveryEntryDigest !== "string" || value.nodeExecutable !== process.execPath || typeof value.npmCli !== "string"
148
+ || !Array.isArray(value.npmArguments) || typeof value.resultPath !== "string" || typeof value.cancellationPath !== "string") {
149
+ throw new Error("invalid A1 update recovery capsule");
150
+ }
151
+ const lexicalCapsuleRoot = dirname(resolve(path));
152
+ if (lexicalCapsuleRoot.split(sep).at(-1) !== value.transactionId) throw new Error("A1 update recovery transaction path is invalid");
153
+ const capsuleRoot = await realpath(lexicalCapsuleRoot);
154
+ const entry = await realpath(value.recoveryEntry);
155
+ assertDirectChild(capsuleRoot, entry);
156
+ if (!samePath(entry, resolve(capsuleRoot, "recovery.js"))) throw new Error("A1 update recovery entry path is invalid");
157
+ const digest = createHash("sha256").update(await readFile(entry)).digest("hex");
158
+ if (digest !== value.recoveryEntryDigest) throw new Error("A1 update recovery payload digest mismatch");
159
+ const globalRoot = await realpath(value.globalRoot);
160
+ const packageRoot = await realpath(value.packageRoot).catch(() => resolve(value.packageRoot));
161
+ const expectedPackage = resolve(globalRoot, ...value.packageName.split("/"));
162
+ if (!samePath(packageRoot, expectedPackage) || !containedBy(globalRoot, packageRoot)) throw new Error("A1 update recovery package root escapes npm global root");
163
+ const expectedNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
164
+ if (JSON.stringify(value.npmArguments) !== JSON.stringify(expectedNpmArguments)) throw new Error("A1 update recovery npm arguments are invalid");
165
+ const launcherRoot = resolve(value.launcherRoot);
166
+ const expectedLaunchers = process.platform === "win32"
167
+ ? [resolve(launcherRoot, "a1"), resolve(launcherRoot, "a1.cmd"), resolve(launcherRoot, "a1.ps1")]
168
+ : [resolve(launcherRoot, "a1")];
169
+ if (JSON.stringify(value.launchers.map(path => resolve(path))) !== JSON.stringify(expectedLaunchers)) throw new Error("A1 update recovery launcher set is invalid");
170
+ for (const launcher of value.launchers) assertDirectChild(launcherRoot, resolve(launcher));
171
+ const expectedSidecars = [resolve(lexicalCapsuleRoot, "cancel.json"), resolve(lexicalCapsuleRoot, "result.json"), resolve(lexicalCapsuleRoot, "owner.json")];
172
+ if (![value.cancellationPath, value.resultPath, value.ownerPath].every((candidate, index) => samePath(candidate, expectedSidecars[index]))) {
173
+ throw new Error("A1 update recovery sidecar paths are invalid");
174
+ }
175
+ const dataDir = dirname(dirname(capsuleRoot));
176
+ const releasesRoot = await realpath(resolve(dataDir, "releases"));
177
+ const priorReleaseRoot = await realpath(value.priorReleaseRoot);
178
+ assertDirectChild(releasesRoot, priorReleaseRoot);
179
+ if (priorReleaseRoot.split(sep).at(-1) !== value.priorReleaseId) throw new Error("A1 update recovery prior release identity is invalid");
180
+ const priorManifest = JSON.parse(await readFile(resolve(priorReleaseRoot, value.releaseManifestName), "utf8"));
181
+ if (priorManifest.releaseId !== value.priorReleaseId || priorManifest.contentDigest !== value.priorContentDigest) {
182
+ throw new Error("A1 update recovery prior release manifest is invalid");
183
+ }
184
+ const npmMetadata = await lstat(value.npmCli);
185
+ if (!npmMetadata.isFile() || npmMetadata.isSymbolicLink()) throw new Error("A1 update recovery npm entry is invalid");
186
+ return value;
187
+ }
188
+
189
+ async function installedTargetEntry(value) {
190
+ try {
191
+ const manifest = JSON.parse(await readFile(resolve(value.packageRoot, "package.json"), "utf8"));
192
+ const entry = resolve(value.packageRoot, "bin", "cli.js");
193
+ if (manifest.name !== value.packageName || manifest.version !== value.targetVersion || !(await lstat(entry)).isFile()) return null;
194
+ return entry;
195
+ } catch { return null; }
196
+ }
197
+
198
+ async function installedTargetIsCallable(value) {
199
+ if (await installedTargetEntry(value) === null) return false;
200
+ const token = `node_modules/${value.packageName}/bin/cli.js`;
201
+ for (const path of value.launchers) {
202
+ try {
203
+ const metadata = await lstat(path);
204
+ if (!metadata.isFile() || metadata.isSymbolicLink()) return false;
205
+ if (process.platform !== "win32" && (metadata.mode & 0o111) === 0) return false;
206
+ const normalized = (await readFile(path, "utf8")).replaceAll("\\", "/");
207
+ if (!normalized.includes(token)) return false;
208
+ } catch { return false; }
209
+ }
210
+ return true;
211
+ }
212
+
213
+ async function writeRecoveryLaunchers(value) {
214
+ const node = value.nodeExecutable ?? process.execPath;
215
+ const entry = value.recoveryEntry;
216
+ const manifest = manifestPath;
217
+ const shell = `#!/bin/sh\nexec ${shellQuote(node)} ${shellQuote(entry)} --launch ${shellQuote(manifest)} -- "$@"\n`;
218
+ const command = `@ECHO off\r\n"${node}" "${entry}" --launch "${manifest}" -- %*\r\n`;
219
+ const powershell = `& '${psQuote(node)}' '${psQuote(entry)}' --launch '${psQuote(manifest)}' -- $args\nexit $LASTEXITCODE\n`;
220
+ const content = process.platform === "win32" ? [shell, command, powershell] : [shell];
221
+ await mkdir(value.launcherRoot, { recursive: true });
222
+ for (let index = 0; index < value.launchers.length; index += 1) await atomicWrite(value.launchers[index], content[index], 0o755);
223
+ }
224
+
225
+ async function recoveryLaunchersAreCallable(value) {
226
+ for (const path of value.launchers) {
227
+ try {
228
+ const metadata = await lstat(path);
229
+ if (!metadata.isFile() || metadata.isSymbolicLink()) return false;
230
+ if (process.platform !== "win32" && (metadata.mode & 0o111) === 0) return false;
231
+ if (!(await readFile(path, "utf8")).includes(value.recoveryEntry)) return false;
232
+ } catch { return false; }
233
+ }
234
+ return true;
235
+ }
236
+
237
+ async function atomicWrite(path, content, mode) {
238
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
239
+ await writeFile(temporary, content, { mode });
240
+ await chmod(temporary, mode);
241
+ await rm(path, { force: true });
242
+ await rename(temporary, path);
243
+ }
244
+
245
+ async function writeJson(path, value) {
246
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
247
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
248
+ const file = await open(temporary, "wx", 0o600);
249
+ try { await file.writeFile(JSON.stringify(value, null, 2)); await file.sync(); }
250
+ finally { await file.close(); }
251
+ await rm(path, { force: true });
252
+ await rename(temporary, path);
253
+ }
254
+
255
+ function samePath(left, right) {
256
+ return process.platform === "win32" ? resolve(left).toLowerCase() === resolve(right).toLowerCase() : resolve(left) === resolve(right);
257
+ }
258
+ function containedBy(parent, child) {
259
+ const fromParent = relative(parent, child);
260
+ return fromParent.length > 0 && fromParent !== ".." && !fromParent.startsWith(`..${sep}`) && !isAbsolute(fromParent);
261
+ }
262
+ function assertDirectChild(parent, child) {
263
+ const expectedParent = resolve(parent);
264
+ const actualParent = dirname(resolve(child));
265
+ const matches = process.platform === "win32" ? actualParent.toLowerCase() === expectedParent.toLowerCase() : actualParent === expectedParent;
266
+ if (!matches) throw new Error(`A1 update recovery path escapes its managed root: ${child}`);
267
+ }
268
+ function shellQuote(value) { return `'${String(value).replaceAll("'", `'"'"'`)}'`; }
269
+ function psQuote(value) { return String(value).replaceAll("'", "''"); }
270
+ function bounded(value) { return value.length <= 64 * 1024 ? value : value.slice(-64 * 1024); }
271
+ async function exists(path) { return await lstat(path).then(() => true).catch(() => false); }
272
+ async function sleep(ms) { await new Promise(resolvePromise => setTimeout(resolvePromise, ms)); }
@@ -27,6 +27,8 @@ export interface BootstrapOptions {
27
27
  }
28
28
  export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
29
29
  export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
30
+ /** Persist current-format evidence after an authenticated parent has certified the exact release. */
31
+ export declare function recordParentCertifiedRelease(release: MaterializedRelease, dataDir: string): Promise<string>;
30
32
  export interface SupervisorStartupAttempt extends SupervisorStartupAttemptIdentity {
31
33
  readonly childOutcome: Promise<{
32
34
  readonly exitCode: number | null;
@@ -193,6 +193,10 @@ export async function certifyMaterializedRelease(release, dataDir, verification
193
193
  if (!consumeMaterializationProof(release)) {
194
194
  await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
195
195
  }
196
+ return await recordParentCertifiedRelease(release, dataDir);
197
+ }
198
+ /** Persist current-format evidence after an authenticated parent has certified the exact release. */
199
+ export async function recordParentCertifiedRelease(release, dataDir) {
196
200
  const path = resolve(dataDir, `certification-${release.releaseId}.json`);
197
201
  const restartSeal = await createRestartSeal(release, dataDir);
198
202
  await chmod(path, 0o600).catch(() => { });
@@ -37,6 +37,10 @@ export interface DependencyLayerOperationEvent {
37
37
  readonly path: string;
38
38
  readonly bytes: number;
39
39
  }
40
+ export interface ReadCertifiedDependencyLayerOptions {
41
+ /** Accept and replace certification written by the immediately preceding updater format. */
42
+ readonly allowLegacyParentCertification?: boolean;
43
+ }
40
44
  export interface SelectedRuntimePayload {
41
45
  readonly paths: readonly string[];
42
46
  readonly inventory: RuntimePayloadInventory;
@@ -71,7 +75,7 @@ export interface MaterializeDependencyLayerOptions {
71
75
  /** Materialize or reuse an exact immutable dependency layer after one source-content pass. */
72
76
  export declare function materializeDependencyLayer(packageRoot: string, dataDir: string, paths: readonly string[], options: MaterializeDependencyLayerOptions): Promise<MaterializedDependencyLayer | null>;
73
77
  /** Read trusted layer certification and canonical metadata without rereading every payload byte. */
74
- export declare function readCertifiedDependencyLayer(dataDir: string, layerId: string, expected?: Pick<DependencyLayerIdentity, "layerId" | "contentDigest">): Promise<Omit<MaterializedDependencyLayer, "reused">>;
78
+ export declare function readCertifiedDependencyLayer(dataDir: string, layerId: string, expected?: Pick<DependencyLayerIdentity, "layerId" | "contentDigest">, options?: ReadCertifiedDependencyLayerOptions): Promise<Omit<MaterializedDependencyLayer, "reused">>;
75
79
  /** Fully verify a layer when certification is absent or explicit tamper evidence is required. */
76
80
  export declare function verifyDependencyLayer(dataDir: string, reference: DependencyLayerReference, onOperation?: (event: DependencyLayerOperationEvent) => void): Promise<Omit<MaterializedDependencyLayer, "reused">>;
77
81
  export declare function dependencyReference(layer: MaterializedDependencyLayer): DependencyLayerReference;
@@ -269,7 +269,7 @@ export async function materializeDependencyLayer(packageRoot, dataDir, paths, op
269
269
  }
270
270
  }
271
271
  /** Read trusted layer certification and canonical metadata without rereading every payload byte. */
272
- export async function readCertifiedDependencyLayer(dataDir, layerId, expected) {
272
+ export async function readCertifiedDependencyLayer(dataDir, layerId, expected, options = {}) {
273
273
  const layersRoot = await realpath(resolve(dataDir, "dependency-layers"));
274
274
  const layerRoot = await realpath(resolveWithin(layersRoot, layerId));
275
275
  assertDirectChild(layersRoot, layerRoot);
@@ -278,14 +278,23 @@ export async function readCertifiedDependencyLayer(dataDir, layerId, expected) {
278
278
  throw new Error(`dependency layer is not a managed non-link directory: ${layerId}`);
279
279
  const manifest = JSON.parse(await readFile(resolve(layerRoot, DEPENDENCY_LAYER_MANIFEST), "utf8"));
280
280
  validateLayerManifest(manifest);
281
- const certification = JSON.parse(await readFile(certificationPath(dataDir, layerId), "utf8"));
282
- if (certification.schema !== PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema || certification.layerId !== manifest.layerId || certification.contentDigest !== manifest.contentDigest
283
- || certification.platform !== process.platform || certification.platformPolicy !== immutablePlatformPolicy()) {
284
- throw new Error(`dependency layer certification differs from manifest: ${layerId}`);
285
- }
286
281
  if (expected && (expected.layerId !== manifest.layerId || expected.contentDigest !== manifest.contentDigest)) {
287
282
  throw new Error(`dependency layer identity mismatch: ${layerId}`);
288
283
  }
284
+ const certification = JSON.parse(await readFile(certificationPath(dataDir, layerId), "utf8"));
285
+ const identityMatches = certification.schema === PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema
286
+ && certification.layerId === manifest.layerId && certification.contentDigest === manifest.contentDigest;
287
+ const currentPlatformEvidence = certification.platform === process.platform && certification.platformPolicy === immutablePlatformPolicy();
288
+ // Compatibility: the updater that introduced layers certified these exact identities but did
289
+ // not record platform fields. Only an authenticated parent-started supervisor opts into this
290
+ // transition; durable/reuse readers remain strict and cannot treat the legacy marker as authority.
291
+ const legacyParentCertification = options.allowLegacyParentCertification === true
292
+ && certification.platform === undefined && certification.platformPolicy === undefined;
293
+ if (!identityMatches || (!currentPlatformEvidence && !legacyParentCertification)) {
294
+ throw new Error(`dependency layer certification differs from manifest: ${layerId}`);
295
+ }
296
+ if (legacyParentCertification)
297
+ await writeLayerCertification(dataDir, manifest);
289
298
  return { ...manifest, layerRoot };
290
299
  }
291
300
  /** Fully verify a layer when certification is absent or explicit tamper evidence is required. */
@@ -10,5 +10,6 @@ export * from "./release-store.js";
10
10
  export * from "./restart-certification.js";
11
11
  export * from "./stable-release.js";
12
12
  export * from "./update.js";
13
+ export * from "./update-recovery.js";
13
14
  export * from "./update-transaction.js";
14
15
  export * from "./warmup.js";
@@ -10,5 +10,6 @@ export * from "./release-store.js";
10
10
  export * from "./restart-certification.js";
11
11
  export * from "./stable-release.js";
12
12
  export * from "./update.js";
13
+ export * from "./update-recovery.js";
13
14
  export * from "./update-transaction.js";
14
15
  export * from "./warmup.js";
@@ -9,6 +9,7 @@ import { resolveProductPaths } from "../lifecycle/index.js";
9
9
  import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
10
10
  import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
11
11
  import { UpdateTransactionStore } from "./update-transaction.js";
12
+ import { cleanupUpdateRecoveryCapsules } from "./update-recovery.js";
12
13
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
13
14
  import { collectCompileCaches, startupCompileCachePath } from "../startup/index.js";
14
15
  const DEFAULT_CANDIDATE_AGE_MS = 60 * 60 * 1_000;
@@ -115,6 +116,7 @@ export async function runBoundedReleaseCleanup(dataDir, paths, options = {}) {
115
116
  }
116
117
  if (!activeTransaction && hasTime()) {
117
118
  await collectCompileCaches(dataDir, await protectedCompileCachePaths(dataDir, await store.read())).catch(() => { });
119
+ await cleanupUpdateRecoveryCapsules(dataDir).catch(() => { });
118
120
  }
119
121
  const remaining = Object.keys((await store.read()).cleanup.pending).length;
120
122
  return { planned: pending.length + artifacts.length, attempted, completed, remaining, durationMs: Math.max(0, now() - startedAt) };
@@ -1,5 +1,5 @@
1
1
  import { type ReleaseIdentity } from "./release.js";
2
- import { type RuntimePayloadInventory } from "./dependency-layer.js";
2
+ import { type ReadCertifiedDependencyLayerOptions, type RuntimePayloadInventory } from "./dependency-layer.js";
3
3
  export declare const RELEASE_MANIFEST_FILENAME: string;
4
4
  export interface MaterializedRelease extends ReleaseIdentity {
5
5
  readonly releaseRoot: string;
@@ -29,6 +29,8 @@ export interface CertifiedReleaseRecord {
29
29
  readonly packageVersion?: string;
30
30
  readonly contentDigest: string;
31
31
  }
32
+ /** Process-authority compatibility controls for metadata-only release loading. */
33
+ export type ReadCertifiedReleaseManifestOptions = ReadCertifiedDependencyLayerOptions;
32
34
  export declare function materializeRelease(packageRoot: string, dataDir: string, options?: MaterializeReleaseOptions): Promise<MaterializedRelease>;
33
35
  /** Consume proof that this exact object was freshly materialized or fully verified in this process. */
34
36
  export declare function consumeMaterializationProof(release: MaterializedRelease): boolean;
@@ -39,7 +41,7 @@ export declare function readMaterializedRelease(releaseRoot: string, selectedSto
39
41
  * establish one of those preconditions; untrusted releases require full
40
42
  * verification.
41
43
  */
42
- export declare function readCertifiedReleaseManifest(record: CertifiedReleaseRecord, selectedStoreRoot: string): Promise<MaterializedRelease>;
44
+ export declare function readCertifiedReleaseManifest(record: CertifiedReleaseRecord, selectedStoreRoot: string, options?: ReadCertifiedReleaseManifestOptions): Promise<MaterializedRelease>;
43
45
  export declare function verifyMaterializedRelease(releaseRoot: string, expected?: ReleaseIdentity, selectedStoreRoot?: string, options?: VerifyMaterializedReleaseOptions): Promise<MaterializedRelease>;
44
46
  export declare function assertImmutableExecutionRoot(release: MaterializedRelease, dataDir: string): Promise<void>;
45
47
  export declare function resolveReleaseEntryPoint(release: MaterializedRelease, entryPoint: string): Promise<string>;
@@ -106,7 +106,7 @@ export async function readMaterializedRelease(releaseRoot, selectedStoreRoot) {
106
106
  * establish one of those preconditions; untrusted releases require full
107
107
  * verification.
108
108
  */
109
- export async function readCertifiedReleaseManifest(record, selectedStoreRoot) {
109
+ export async function readCertifiedReleaseManifest(record, selectedStoreRoot, options = {}) {
110
110
  const canonical = await realpath(record.releaseRoot);
111
111
  const canonicalStoreRoot = await realpath(selectedStoreRoot);
112
112
  assertContained(canonicalStoreRoot, canonical, "release root is outside the selected release store");
@@ -118,7 +118,7 @@ export async function readCertifiedReleaseManifest(record, selectedStoreRoot) {
118
118
  }
119
119
  if (canonical.split(sep).at(-1) !== manifest.releaseId)
120
120
  throw new Error(`release directory does not match identity ${manifest.releaseId}`);
121
- await verifyReleaseDependencies(canonical, canonicalStoreRoot, manifest.dependencyLayers ?? [], false);
121
+ await verifyReleaseDependencies(canonical, canonicalStoreRoot, manifest.dependencyLayers ?? [], false, {}, options);
122
122
  return { ...manifest, releaseRoot: canonical };
123
123
  }
124
124
  export async function verifyMaterializedRelease(releaseRoot, expected, selectedStoreRoot, options = {}) {
@@ -162,7 +162,7 @@ export async function resolveReleaseEntryPoint(release, entryPoint) {
162
162
  assertContained(release.releaseRoot, canonical, "entry point resolves outside the selected release root");
163
163
  return canonical;
164
164
  }
165
- async function verifyReleaseDependencies(releaseRoot, storeRoot, references, fullVerification, options = {}) {
165
+ async function verifyReleaseDependencies(releaseRoot, storeRoot, references, fullVerification, options = {}, certificationOptions = {}) {
166
166
  if (references.length === 0)
167
167
  return;
168
168
  if (references.length !== 1)
@@ -171,7 +171,7 @@ async function verifyReleaseDependencies(releaseRoot, storeRoot, references, ful
171
171
  const reference = references[0];
172
172
  const layer = fullVerification
173
173
  ? await verifyDependencyLayer(dataDir, reference, event => options.onOperation?.({ operation: event.operation, path: event.path, bytes: event.bytes }))
174
- : await readCertifiedDependencyLayer(dataDir, reference.layerId, reference);
174
+ : await readCertifiedDependencyLayer(dataDir, reference.layerId, reference, certificationOptions);
175
175
  const binding = resolveWithin(releaseRoot, reference.binding);
176
176
  const metadata = await lstat(binding);
177
177
  if (!metadata.isSymbolicLink())
@@ -0,0 +1,74 @@
1
+ import type { UpdateTransaction, UpdateRecoveryState } from "./update-transaction.js";
2
+ export declare const UPDATE_RECOVERY_SCHEMA: "a1-update-recovery-v1";
3
+ export interface UpdateRecoveryCapsule {
4
+ readonly schema: typeof UPDATE_RECOVERY_SCHEMA;
5
+ readonly transactionId: string;
6
+ readonly packageName: string;
7
+ readonly targetVersion: string;
8
+ readonly packageRoot: string;
9
+ readonly globalRoot: string;
10
+ readonly launcherRoot: string;
11
+ readonly launchers: readonly string[];
12
+ readonly priorReleaseId: string;
13
+ readonly priorReleaseRoot: string;
14
+ readonly priorContentDigest: string;
15
+ readonly releaseManifestName: string;
16
+ readonly recoveryEntry: string;
17
+ readonly recoveryEntryDigest: string;
18
+ readonly nodeExecutable: string;
19
+ readonly npmCli: string;
20
+ readonly npmArguments: readonly string[];
21
+ readonly cancellationPath: string;
22
+ readonly resultPath: string;
23
+ readonly ownerPath: string;
24
+ readonly createdAt: string;
25
+ }
26
+ export interface UpdateRecoveryResult {
27
+ readonly schema: typeof UPDATE_RECOVERY_SCHEMA;
28
+ readonly transactionId: string;
29
+ readonly outcome: "installed" | "recovery-launcher" | "failed";
30
+ readonly npmExitCode: number | null;
31
+ readonly cancelled: boolean;
32
+ readonly launcherDisposition: "target" | "recovery" | "unavailable";
33
+ readonly stdout: string;
34
+ readonly stderr: string;
35
+ readonly completedAt: string;
36
+ }
37
+ export interface ProtectedPackageReplacementOptions {
38
+ readonly dataDir: string;
39
+ readonly globalRoot: string;
40
+ readonly packageRoot: string;
41
+ readonly transaction: UpdateTransaction;
42
+ readonly priorRelease: {
43
+ readonly releaseId: string;
44
+ readonly releaseRoot: string;
45
+ readonly contentDigest: string;
46
+ };
47
+ readonly output: {
48
+ stderr(message: string): void;
49
+ };
50
+ readonly environment?: NodeJS.ProcessEnv;
51
+ readonly platform?: NodeJS.Platform;
52
+ readonly timeoutMs?: number;
53
+ readonly workerSpawner?: (entry: string, manifestPath: string, environment: NodeJS.ProcessEnv) => Promise<void>;
54
+ readonly onRecoveryState?: (state: UpdateRecoveryState) => Promise<void>;
55
+ }
56
+ export interface ProtectedPackageReplacementResult extends UpdateRecoveryResult {
57
+ readonly recovery: UpdateRecoveryState;
58
+ }
59
+ /** Resolve the complete public launcher set npm owns for the active platform. */
60
+ export declare function updateLauncherPaths(globalRoot: string, platform?: NodeJS.Platform): readonly string[];
61
+ /** Validate transaction-scoped recovery authority without trusting npm temporary names. */
62
+ export declare function readUpdateRecoveryCapsule(manifestPath: string, platform?: NodeJS.Platform): Promise<UpdateRecoveryCapsule>;
63
+ /** Prepare durable recovery authority before npm can mutate the live launcher set. */
64
+ export declare function prepareUpdateRecoveryCapsule(options: ProtectedPackageReplacementOptions): Promise<{
65
+ capsule: UpdateRecoveryCapsule;
66
+ manifestPath: string;
67
+ }>;
68
+ /** Execute global replacement behind a detached owner and coordinate terminal cancellation. */
69
+ export declare function runProtectedPackageReplacement(options: ProtectedPackageReplacementOptions): Promise<ProtectedPackageReplacementResult>;
70
+ export declare function removeUpdateRecoveryCapsule(dataDir: string, transactionId: string): Promise<void>;
71
+ /** Classify a complete launcher set without following linked or mixed launcher files. */
72
+ export declare function inspectUpdateLauncherSet(capsule: UpdateRecoveryCapsule, platform?: NodeJS.Platform): Promise<"target" | "recovery" | "unavailable">;
73
+ /** Remove terminal recovery capsules only after no canonical launcher references them. */
74
+ export declare function cleanupUpdateRecoveryCapsules(dataDir: string): Promise<void>;