@timurproko/a1 0.1.8-dev.322 → 0.1.8-dev.335
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/README.md +22 -11
- package/dist/cli/dispatch.js +1 -1
- package/dist/features/owned-ui/project-trust-prompt.js +1 -1
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/service.d.ts +31 -1
- package/dist/features/prompt-history/service.js +294 -129
- package/dist/features/prompt-history/store.d.ts +2 -1
- package/dist/features/prompt-history/store.js +17 -4
- package/dist/features/prompt-history/worker.js +6 -3
- package/dist/foundation/launch-context/index.d.ts +36 -6
- package/dist/foundation/launch-context/index.js +64 -11
- package/dist/foundation/launch-guardian/main.js +2 -1
- package/dist/foundation/release/bootstrap.d.ts +7 -0
- package/dist/foundation/release/bootstrap.js +95 -19
- package/dist/foundation/release/cohort-state.js +0 -3
- package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
- package/dist/foundation/release/dependency-certification-retention.js +88 -0
- package/dist/foundation/release/dependency-certification.d.ts +24 -0
- package/dist/foundation/release/dependency-certification.js +208 -0
- package/dist/foundation/release/dependency-layer.d.ts +3 -4
- package/dist/foundation/release/dependency-layer.js +6 -37
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/release-gc.js +72 -44
- package/dist/foundation/release/release-store.d.ts +2 -0
- package/dist/foundation/release/release-store.js +4 -3
- package/dist/foundation/release/restart-certification.js +16 -5
- package/dist/foundation/release/update-launch.d.ts +6 -0
- package/dist/foundation/release/update-launch.js +17 -0
- package/dist/foundation/release/update.d.ts +2 -0
- package/dist/foundation/release/update.js +21 -27
- package/dist/foundation/release/warmup.js +16 -5
- package/dist/foundation/supervision/main.js +5 -2
- package/dist/foundation/supervision/server.js +40 -13
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
- package/dist/integrations/pi/engine/adapter.d.ts +20 -0
- package/dist/integrations/pi/engine/adapter.js +223 -87
- package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
- package/dist/integrations/pi/engine/pending-delivery.js +149 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
- package/dist/integrations/pi/session-ui/session-shell.js +9 -6
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
- package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
- package/dist/integrations/pi/session-ui/text-paste.js +16 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.json +1 -1
- package/docs/architecture/internal-naming.md +5 -3
- package/docs/architecture/toolchain.md +1 -1
- package/docs/ci-release-runbook.md +45 -7
- package/docs/manual-code-streaming-cleanup.md +88 -0
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const DEPENDENCY_CERTIFICATIONS_DIRECTORY = "dependency-certifications";
|
|
2
|
+
type LayerIdentity = {
|
|
3
|
+
readonly layerId: string;
|
|
4
|
+
readonly contentDigest: string;
|
|
5
|
+
};
|
|
6
|
+
export interface ReadDependencyCertificationOptions {
|
|
7
|
+
/** Only authenticated parent-started supervisors may upgrade missing platform evidence. */
|
|
8
|
+
readonly allowLegacyParentCertification?: boolean;
|
|
9
|
+
/** Cleanup must validate canonical evidence without migrating or falling back. */
|
|
10
|
+
readonly canonicalOnly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Derive the canonical record path without accepting path components as layer identities. */
|
|
13
|
+
export declare function dependencyLayerCertificationPath(dataDir: string, layerId: string): string;
|
|
14
|
+
/** Keep the historical filename solely for compatibility reads and managed cleanup. */
|
|
15
|
+
export declare function legacyDependencyLayerCertificationPath(dataDir: string, layerId: string): string;
|
|
16
|
+
/** Validate the dedicated directory before reading, creating, or deleting any contained record. */
|
|
17
|
+
export declare function dependencyCertificationDirectory(dataDir: string, create?: boolean): Promise<string | null>;
|
|
18
|
+
/** Resolve only direct regular managed records; callers never follow directory or record links. */
|
|
19
|
+
export declare function managedDependencyCertificationPath(dataDir: string, layerId: string, legacy?: boolean): Promise<string | null>;
|
|
20
|
+
/** Prefer canonical evidence; copy validated legacy evidence without changing its sealed source. */
|
|
21
|
+
export declare function readDependencyCertification(dataDir: string, identity: LayerIdentity, options?: ReadDependencyCertificationOptions): Promise<void>;
|
|
22
|
+
/** Publish certification after materialization/full verification, preserving an already-valid winner. */
|
|
23
|
+
export declare function writeDependencyCertification(dataDir: string, identity: LayerIdentity): Promise<void>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
|
+
import { immutablePlatformPolicy } from "./immutable-platform.js";
|
|
6
|
+
export const DEPENDENCY_CERTIFICATIONS_DIRECTORY = "dependency-certifications";
|
|
7
|
+
/** Derive the canonical record path without accepting path components as layer identities. */
|
|
8
|
+
export function dependencyLayerCertificationPath(dataDir, layerId) {
|
|
9
|
+
assertLayerId(layerId);
|
|
10
|
+
return resolve(dataDir, DEPENDENCY_CERTIFICATIONS_DIRECTORY, `${layerId}.json`);
|
|
11
|
+
}
|
|
12
|
+
/** Keep the historical filename solely for compatibility reads and managed cleanup. */
|
|
13
|
+
export function legacyDependencyLayerCertificationPath(dataDir, layerId) {
|
|
14
|
+
assertLayerId(layerId);
|
|
15
|
+
return resolve(dataDir, `dependency-layer-certification-${layerId}.json`);
|
|
16
|
+
}
|
|
17
|
+
/** Validate the dedicated directory before reading, creating, or deleting any contained record. */
|
|
18
|
+
export async function dependencyCertificationDirectory(dataDir, create = false) {
|
|
19
|
+
const canonicalData = await realpath(dataDir);
|
|
20
|
+
const directory = resolve(canonicalData, DEPENDENCY_CERTIFICATIONS_DIRECTORY);
|
|
21
|
+
if (create)
|
|
22
|
+
await mkdir(directory, { mode: 0o700 }).catch(error => {
|
|
23
|
+
if (!hasCode(error, "EEXIST"))
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
const metadata = await lstat(directory).catch(missingOrThrow);
|
|
27
|
+
if (metadata === null)
|
|
28
|
+
return null;
|
|
29
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(directory) !== directory) {
|
|
30
|
+
throw new Error("dependency certification directory is not a managed non-link directory");
|
|
31
|
+
}
|
|
32
|
+
return directory;
|
|
33
|
+
}
|
|
34
|
+
/** Resolve only direct regular managed records; callers never follow directory or record links. */
|
|
35
|
+
export async function managedDependencyCertificationPath(dataDir, layerId, legacy = false) {
|
|
36
|
+
const canonicalData = await realpath(dataDir);
|
|
37
|
+
const path = legacy ? legacyDependencyLayerCertificationPath(canonicalData, layerId) : dependencyLayerCertificationPath(canonicalData, layerId);
|
|
38
|
+
if (!legacy && await dependencyCertificationDirectory(canonicalData) === null)
|
|
39
|
+
return null;
|
|
40
|
+
const metadata = await lstat(path).catch(missingOrThrow);
|
|
41
|
+
if (metadata === null)
|
|
42
|
+
return null;
|
|
43
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || await realpath(path) !== path) {
|
|
44
|
+
throw new Error(`dependency certification is not a managed regular file: ${path}`);
|
|
45
|
+
}
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
/** Prefer canonical evidence; copy validated legacy evidence without changing its sealed source. */
|
|
49
|
+
export async function readDependencyCertification(dataDir, identity, options = {}) {
|
|
50
|
+
const canonical = await managedDependencyCertificationPath(dataDir, identity.layerId);
|
|
51
|
+
const source = canonical ?? (options.canonicalOnly ? null : await managedDependencyCertificationPath(dataDir, identity.layerId, true));
|
|
52
|
+
if (source === null)
|
|
53
|
+
throw new Error(`dependency layer certification is absent: ${identity.layerId}`);
|
|
54
|
+
const document = JSON.parse(await readFile(source, "utf8"));
|
|
55
|
+
const legacyPlatform = validateCertification(document, identity, options.allowLegacyParentCertification === true);
|
|
56
|
+
if (!legacyPlatform && ((await lstat(source)).mode & 0o222) !== 0)
|
|
57
|
+
throw new Error("dependency certification is writable");
|
|
58
|
+
if (legacyPlatform) {
|
|
59
|
+
// Compatibility: only the authenticated parent path may replace older platform-less evidence.
|
|
60
|
+
await writeDependencyCertification(dataDir, identity);
|
|
61
|
+
}
|
|
62
|
+
else if (canonical === null) {
|
|
63
|
+
await publishCertification(dataDir, identity, document, false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Publish certification after materialization/full verification, preserving an already-valid winner. */
|
|
67
|
+
export async function writeDependencyCertification(dataDir, identity) {
|
|
68
|
+
await publishCertification(dataDir, identity, {
|
|
69
|
+
schema: PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema,
|
|
70
|
+
layerId: identity.layerId,
|
|
71
|
+
contentDigest: identity.contentDigest,
|
|
72
|
+
platform: process.platform,
|
|
73
|
+
platformPolicy: immutablePlatformPolicy(),
|
|
74
|
+
certifiedAt: new Date().toISOString(),
|
|
75
|
+
}, true);
|
|
76
|
+
}
|
|
77
|
+
async function publishCertification(dataDir, identity, document, replaceInvalid) {
|
|
78
|
+
const directory = (await dependencyCertificationDirectory(dataDir, true));
|
|
79
|
+
const path = dependencyLayerCertificationPath(dirname(directory), identity.layerId);
|
|
80
|
+
const lockPath = `${path}.lock`;
|
|
81
|
+
const releaseLock = await acquirePublicationLock(lockPath);
|
|
82
|
+
try {
|
|
83
|
+
await publishLockedCertification(dataDir, identity, document, replaceInvalid, path);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await releaseLock();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function publishLockedCertification(dataDir, identity, document, replaceInvalid, path) {
|
|
90
|
+
const existing = await managedDependencyCertificationPath(dataDir, identity.layerId);
|
|
91
|
+
if (existing !== null) {
|
|
92
|
+
const source = await readFile(existing, "utf8");
|
|
93
|
+
const metadata = await lstat(existing);
|
|
94
|
+
try {
|
|
95
|
+
validateCertification(JSON.parse(source), identity);
|
|
96
|
+
// Invariant: full verification restores writable evidence; ordinary valid reuse is read-only.
|
|
97
|
+
if ((metadata.mode & 0o222) === 0)
|
|
98
|
+
return;
|
|
99
|
+
if (!replaceInvalid)
|
|
100
|
+
throw new Error("dependency certification is writable");
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (!replaceInvalid)
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
await rm(existing, { force: true });
|
|
107
|
+
}
|
|
108
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
109
|
+
try {
|
|
110
|
+
await writeFile(temporary, JSON.stringify(document, null, 2), { flag: "wx", mode: 0o400 });
|
|
111
|
+
await chmod(temporary, 0o400);
|
|
112
|
+
await dependencyCertificationDirectory(dataDir);
|
|
113
|
+
// Concurrency: the per-layer lease protects the winner check and atomic publication together.
|
|
114
|
+
// Rename publishes final read-only metadata; no later temporary unlink changes sealed evidence.
|
|
115
|
+
await rename(temporary, path);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await rm(temporary, { force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function acquirePublicationLock(path) {
|
|
122
|
+
const token = randomUUID();
|
|
123
|
+
const candidate = `${path}.candidate-${token}`;
|
|
124
|
+
await mkdir(candidate, { mode: 0o700 });
|
|
125
|
+
try {
|
|
126
|
+
await writeFile(resolve(candidate, "owner.json"), JSON.stringify({ pid: process.pid, token }), { flag: "wx", mode: 0o600 });
|
|
127
|
+
const deadline = Date.now() + 5_000;
|
|
128
|
+
while (true) {
|
|
129
|
+
try {
|
|
130
|
+
// Concurrency: a nonempty directory cannot replace another nonempty directory on either
|
|
131
|
+
// platform. Publishing the complete owner atomically also avoids partially written leases.
|
|
132
|
+
await rename(candidate, path);
|
|
133
|
+
return async () => {
|
|
134
|
+
const released = `${path}.released-${token}`;
|
|
135
|
+
await rename(path, released);
|
|
136
|
+
await rm(released, { recursive: true, force: true });
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].some(code => hasCode(error, code)))
|
|
141
|
+
throw error;
|
|
142
|
+
const metadata = await lstat(path).catch(missingOrThrow);
|
|
143
|
+
if (metadata !== null) {
|
|
144
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
145
|
+
throw new Error("dependency certification publication lock is not a managed directory");
|
|
146
|
+
const ownerPath = resolve(path, "owner.json");
|
|
147
|
+
const ownerMetadata = await lstat(ownerPath).catch(missingOrThrow);
|
|
148
|
+
if (ownerMetadata !== null) {
|
|
149
|
+
if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink())
|
|
150
|
+
throw new Error("dependency certification publication owner is not a regular file");
|
|
151
|
+
const owner = await readFile(ownerPath, "utf8").then(source => JSON.parse(source)).catch(error => {
|
|
152
|
+
if (hasCode(error, "ENOENT"))
|
|
153
|
+
return null;
|
|
154
|
+
throw error;
|
|
155
|
+
});
|
|
156
|
+
if (owner !== null && Number.isSafeInteger(owner.pid) && owner.pid > 0 && /^[a-f0-9-]{36}$/.test(owner.token)) {
|
|
157
|
+
let abandoned = false;
|
|
158
|
+
try {
|
|
159
|
+
process.kill(owner.pid, 0);
|
|
160
|
+
}
|
|
161
|
+
catch (probeError) {
|
|
162
|
+
abandoned = hasCode(probeError, "ESRCH");
|
|
163
|
+
}
|
|
164
|
+
if (abandoned) {
|
|
165
|
+
// Concurrency: retain this generation's nonempty tombstone. A delayed second reclaimer
|
|
166
|
+
// cannot overwrite it and accidentally steal a newly acquired replacement lease.
|
|
167
|
+
await rename(path, `${path}.abandoned-${owner.token}`).catch(error => {
|
|
168
|
+
if (!["ENOENT", "EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].some(code => hasCode(error, code)))
|
|
169
|
+
throw error;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (Date.now() >= deadline)
|
|
176
|
+
throw new Error("dependency certification publication is busy");
|
|
177
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 20));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await rm(candidate, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function validateCertification(value, identity, allowLegacyPlatform = false) {
|
|
186
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
187
|
+
throw new Error(`dependency layer certification differs from manifest: ${identity.layerId}`);
|
|
188
|
+
const certification = value;
|
|
189
|
+
const identityMatches = certification.schema === PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema
|
|
190
|
+
&& certification.layerId === identity.layerId && certification.contentDigest === identity.contentDigest;
|
|
191
|
+
const currentPlatform = certification.platform === process.platform && certification.platformPolicy === immutablePlatformPolicy();
|
|
192
|
+
const legacyPlatform = allowLegacyPlatform && certification.platform === undefined && certification.platformPolicy === undefined;
|
|
193
|
+
if (!identityMatches || (!currentPlatform && !legacyPlatform))
|
|
194
|
+
throw new Error(`dependency layer certification differs from manifest: ${identity.layerId}`);
|
|
195
|
+
return legacyPlatform;
|
|
196
|
+
}
|
|
197
|
+
function assertLayerId(layerId) {
|
|
198
|
+
if (!/^dependencies-[a-f0-9]{32}$/.test(layerId))
|
|
199
|
+
throw new Error(`invalid dependency layer identity: ${layerId}`);
|
|
200
|
+
}
|
|
201
|
+
function hasCode(error, code) {
|
|
202
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
203
|
+
}
|
|
204
|
+
function missingOrThrow(error) {
|
|
205
|
+
if (hasCode(error, "ENOENT"))
|
|
206
|
+
return null;
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ReadDependencyCertificationOptions } from "./dependency-certification.js";
|
|
2
|
+
export { dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath } from "./dependency-certification.js";
|
|
1
3
|
import { type ReleaseFileIdentity } from "./release.js";
|
|
2
4
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
3
5
|
export declare const DEPENDENCY_LAYER_SCHEMA: string;
|
|
@@ -37,9 +39,7 @@ export interface DependencyLayerOperationEvent {
|
|
|
37
39
|
readonly path: string;
|
|
38
40
|
readonly bytes: number;
|
|
39
41
|
}
|
|
40
|
-
export interface ReadCertifiedDependencyLayerOptions {
|
|
41
|
-
/** Accept and replace certification written by the immediately preceding updater format. */
|
|
42
|
-
readonly allowLegacyParentCertification?: boolean;
|
|
42
|
+
export interface ReadCertifiedDependencyLayerOptions extends ReadDependencyCertificationOptions {
|
|
43
43
|
}
|
|
44
44
|
export interface SelectedRuntimePayload {
|
|
45
45
|
readonly paths: readonly string[];
|
|
@@ -79,4 +79,3 @@ export declare function readCertifiedDependencyLayer(dataDir: string, layerId: s
|
|
|
79
79
|
/** Fully verify a layer when certification is absent or explicit tamper evidence is required. */
|
|
80
80
|
export declare function verifyDependencyLayer(dataDir: string, reference: DependencyLayerReference, onOperation?: (event: DependencyLayerOperationEvent) => void): Promise<Omit<MaterializedDependencyLayer, "reused">>;
|
|
81
81
|
export declare function dependencyReference(layer: MaterializedDependencyLayer): DependencyLayerReference;
|
|
82
|
-
export declare function dependencyLayerCertificationPath(dataDir: string, layerId: string): string;
|
|
@@ -2,7 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
5
|
-
import { assertImmutableFileMode
|
|
5
|
+
import { assertImmutableFileMode } from "./immutable-platform.js";
|
|
6
|
+
import { readDependencyCertification, writeDependencyCertification as writeLayerCertification } from "./dependency-certification.js";
|
|
7
|
+
export { dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath } from "./dependency-certification.js";
|
|
6
8
|
import { digestManifestFiles, releaseFileIdentity, resolveWithin } from "./release.js";
|
|
7
9
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
8
10
|
const LAYER_IO_CONCURRENCY = 32;
|
|
@@ -281,20 +283,9 @@ export async function readCertifiedDependencyLayer(dataDir, layerId, expected, o
|
|
|
281
283
|
if (expected && (expected.layerId !== manifest.layerId || expected.contentDigest !== manifest.contentDigest)) {
|
|
282
284
|
throw new Error(`dependency layer identity mismatch: ${layerId}`);
|
|
283
285
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
286
|
+
if (manifest.layerId !== layerId)
|
|
287
|
+
throw new Error(`dependency layer identity mismatch: ${layerId}`);
|
|
288
|
+
await readDependencyCertification(dataDir, manifest, options);
|
|
298
289
|
return { ...manifest, layerRoot };
|
|
299
290
|
}
|
|
300
291
|
/** Fully verify a layer when certification is absent or explicit tamper evidence is required. */
|
|
@@ -326,12 +317,6 @@ export async function verifyDependencyLayer(dataDir, reference, onOperation) {
|
|
|
326
317
|
export function dependencyReference(layer) {
|
|
327
318
|
return { layerId: layer.layerId, contentDigest: layer.contentDigest, binding: "node_modules" };
|
|
328
319
|
}
|
|
329
|
-
export function dependencyLayerCertificationPath(dataDir, layerId) {
|
|
330
|
-
return certificationPath(dataDir, layerId);
|
|
331
|
-
}
|
|
332
|
-
function certificationPath(dataDir, layerId) {
|
|
333
|
-
return resolve(dataDir, `dependency-layer-certification-${layerId}.json`);
|
|
334
|
-
}
|
|
335
320
|
async function reuseDependencyLayer(dataDir, identity) {
|
|
336
321
|
try {
|
|
337
322
|
return await readCertifiedDependencyLayer(dataDir, identity.layerId, identity);
|
|
@@ -343,22 +328,6 @@ async function reuseDependencyLayer(dataDir, identity) {
|
|
|
343
328
|
return verified;
|
|
344
329
|
}
|
|
345
330
|
}
|
|
346
|
-
async function writeLayerCertification(dataDir, identity) {
|
|
347
|
-
const path = certificationPath(dataDir, identity.layerId);
|
|
348
|
-
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
349
|
-
await writeFile(temporary, JSON.stringify({
|
|
350
|
-
schema: PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema,
|
|
351
|
-
layerId: identity.layerId,
|
|
352
|
-
contentDigest: identity.contentDigest,
|
|
353
|
-
platform: process.platform,
|
|
354
|
-
platformPolicy: immutablePlatformPolicy(),
|
|
355
|
-
certifiedAt: new Date().toISOString(),
|
|
356
|
-
}, null, 2), { flag: "wx", mode: 0o400 });
|
|
357
|
-
await chmod(path, 0o600).catch(() => { });
|
|
358
|
-
await rm(path, { force: true });
|
|
359
|
-
await rename(temporary, path);
|
|
360
|
-
await chmod(path, 0o400);
|
|
361
|
-
}
|
|
362
331
|
function validateLayerManifest(value) {
|
|
363
332
|
if (value.schema !== DEPENDENCY_LAYER_SCHEMA || !/^dependencies-[a-f0-9]{32}$/.test(value.layerId)
|
|
364
333
|
|| !/^[a-f0-9]{64}$/.test(value.contentDigest) || !Array.isArray(value.files) || value.files.length === 0
|
|
@@ -10,6 +10,7 @@ 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-launch.js";
|
|
13
14
|
export * from "./update-recovery.js";
|
|
14
15
|
export * from "./update-transaction.js";
|
|
15
16
|
export * from "./warmup.js";
|
|
@@ -10,6 +10,7 @@ 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-launch.js";
|
|
13
14
|
export * from "./update-recovery.js";
|
|
14
15
|
export * from "./update-transaction.js";
|
|
15
16
|
export * from "./warmup.js";
|
|
@@ -7,7 +7,9 @@ import { CohortStateStore, planProtectedReleases, } from "./cohort-state.js";
|
|
|
7
7
|
import { liveReleaseIds } from "./endpoints.js";
|
|
8
8
|
import { resolveProductPaths } from "../lifecycle/index.js";
|
|
9
9
|
import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
|
|
10
|
-
import { DEPENDENCY_LAYER_MANIFEST,
|
|
10
|
+
import { DEPENDENCY_LAYER_MANIFEST, readCertifiedDependencyLayer } from "./dependency-layer.js";
|
|
11
|
+
import { dependencyCertificationDirectory, managedDependencyCertificationPath } from "./dependency-certification.js";
|
|
12
|
+
import { dependencyCertificationProtection } from "./dependency-certification-retention.js";
|
|
11
13
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
12
14
|
import { cleanupUpdateRecoveryCapsules } from "./update-recovery.js";
|
|
13
15
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
@@ -483,13 +485,19 @@ async function removeAbandonedArtifact(dataDir, path, operations) {
|
|
|
483
485
|
throw new Error("abandoned artifact is outside managed candidate, trash, or certification paths");
|
|
484
486
|
}
|
|
485
487
|
async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, options) {
|
|
488
|
+
const transactionStore = options.transactionStore ?? new UpdateTransactionStore(dataDir);
|
|
489
|
+
if ((await transactionStore.read())?.status === "active")
|
|
490
|
+
return { attempted: 0, completed: 0 };
|
|
491
|
+
const protection = await dependencyCertificationProtection(dataDir, Object.keys((await store.read()).releases));
|
|
492
|
+
if (protection.uncertain)
|
|
493
|
+
return { attempted: 0, completed: 0 };
|
|
494
|
+
const { referenced } = protection;
|
|
486
495
|
const layersRoot = resolve(dataDir, "dependency-layers");
|
|
487
496
|
const layersMetadata = await lstat(layersRoot).catch(error => missingOrThrow(error));
|
|
488
497
|
if (layersMetadata === null)
|
|
489
|
-
return
|
|
498
|
+
return await collectDependencyCertificationRecords(store, dataDir, protection, maxItems, options);
|
|
490
499
|
if (!layersMetadata.isDirectory() || layersMetadata.isSymbolicLink())
|
|
491
500
|
throw new Error("dependency-layer store is not a managed directory");
|
|
492
|
-
const referenced = await referencedDependencyLayerIds(dataDir);
|
|
493
501
|
const trashRoot = resolve(layersRoot, ".trash");
|
|
494
502
|
await mkdir(trashRoot, { recursive: true, mode: 0o700 });
|
|
495
503
|
let attempted = 0;
|
|
@@ -514,7 +522,12 @@ async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, opt
|
|
|
514
522
|
for (const entry of await readdir(trashRoot, { withFileTypes: true })) {
|
|
515
523
|
candidates.push({ path: resolve(trashRoot, entry.name), layerId: entry.name.split("--", 1)[0] ?? null, inTrash: true });
|
|
516
524
|
}
|
|
517
|
-
|
|
525
|
+
const certificates = await collectDependencyCertificationRecords(store, dataDir, protection, Math.min(1, maxItems), options);
|
|
526
|
+
attempted += certificates.attempted;
|
|
527
|
+
completed += certificates.completed;
|
|
528
|
+
for (const candidate of candidates.slice(0, maxItems - attempted)) {
|
|
529
|
+
if ((await transactionStore.read())?.status === "active")
|
|
530
|
+
break;
|
|
518
531
|
if (candidate.layerId !== null && referenced.has(candidate.layerId))
|
|
519
532
|
continue;
|
|
520
533
|
attempted += 1;
|
|
@@ -541,7 +554,11 @@ async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, opt
|
|
|
541
554
|
}
|
|
542
555
|
await (options.operations?.remove ?? rm)(removalPath, { recursive: true, force: false, maxRetries: 0 });
|
|
543
556
|
if (candidate.layerId !== null && /^dependencies-[a-f0-9]{32}$/.test(candidate.layerId)) {
|
|
544
|
-
|
|
557
|
+
for (const legacy of [false, true]) {
|
|
558
|
+
const certificate = await managedDependencyCertificationPath(dataDir, candidate.layerId, legacy);
|
|
559
|
+
if (certificate !== null)
|
|
560
|
+
await (options.operations?.remove ?? rm)(certificate, { force: true });
|
|
561
|
+
}
|
|
545
562
|
}
|
|
546
563
|
completed += 1;
|
|
547
564
|
}
|
|
@@ -551,22 +568,58 @@ async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, opt
|
|
|
551
568
|
}
|
|
552
569
|
}
|
|
553
570
|
if (attempted < maxItems) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
571
|
+
const remaining = await collectDependencyCertificationRecords(store, dataDir, protection, maxItems - attempted, options);
|
|
572
|
+
attempted += remaining.attempted;
|
|
573
|
+
completed += remaining.completed;
|
|
574
|
+
}
|
|
575
|
+
return { attempted, completed };
|
|
576
|
+
}
|
|
577
|
+
async function collectDependencyCertificationRecords(store, dataDir, protection, maxItems, options) {
|
|
578
|
+
const candidates = new Map();
|
|
579
|
+
for (const entry of await readdir(dataDir, { withFileTypes: true })) {
|
|
580
|
+
const match = /^dependency-layer-certification-(dependencies-[a-f0-9]{32})\.json$/.exec(entry.name);
|
|
581
|
+
if (match && entry.isFile())
|
|
582
|
+
candidates.set(resolve(dataDir, entry.name), { layerId: match[1], legacy: true });
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const directory = await dependencyCertificationDirectory(dataDir);
|
|
586
|
+
if (directory !== null) {
|
|
587
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
588
|
+
const match = /^(dependencies-[a-f0-9]{32})\.json$/.exec(entry.name);
|
|
589
|
+
if (match && entry.isFile())
|
|
590
|
+
candidates.set(resolve(directory, entry.name), { layerId: match[1], legacy: false });
|
|
564
591
|
}
|
|
565
|
-
|
|
566
|
-
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
await store.recordCleanupFailure("dependency-certifications", "certification-discovery", error);
|
|
596
|
+
return { attempted: 0, completed: 0 };
|
|
597
|
+
}
|
|
598
|
+
const transactionStore = options.transactionStore ?? new UpdateTransactionStore(dataDir);
|
|
599
|
+
let attempted = 0;
|
|
600
|
+
let completed = 0;
|
|
601
|
+
for (const candidate of sortArtifacts([...candidates.keys()], await store.read())) {
|
|
602
|
+
if (attempted >= maxItems || (await transactionStore.read())?.status === "active")
|
|
603
|
+
break;
|
|
604
|
+
const { layerId, legacy } = candidates.get(candidate);
|
|
605
|
+
if (legacy ? protection.legacyRequired.has(layerId) : protection.referenced.has(layerId))
|
|
606
|
+
continue;
|
|
607
|
+
const layerExists = await lstat(resolve(dataDir, "dependency-layers", layerId)).catch(missingOrThrow) !== null;
|
|
608
|
+
if (!legacy && layerExists)
|
|
609
|
+
continue;
|
|
610
|
+
try {
|
|
611
|
+
if (legacy && (layerExists || protection.referenced.has(layerId))) {
|
|
612
|
+
// Security: a duplicate is disposable only with validated canonical evidence, never fallback.
|
|
613
|
+
await readCertifiedDependencyLayer(dataDir, layerId, undefined, { canonicalOnly: true });
|
|
567
614
|
}
|
|
568
|
-
|
|
569
|
-
|
|
615
|
+
attempted += 1;
|
|
616
|
+
const path = await managedDependencyCertificationPath(dataDir, layerId, legacy);
|
|
617
|
+
if (path !== null)
|
|
618
|
+
await (options.operations?.remove ?? rm)(path, { force: true });
|
|
619
|
+
completed += 1;
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
await store.recordCleanupFailure(artifactName(candidate), "dependency-layer-certification", error);
|
|
570
623
|
}
|
|
571
624
|
}
|
|
572
625
|
return { attempted, completed };
|
|
@@ -585,31 +638,6 @@ async function protectedCompileCachePaths(dataDir, state) {
|
|
|
585
638
|
}
|
|
586
639
|
return paths;
|
|
587
640
|
}
|
|
588
|
-
async function referencedDependencyLayerIds(dataDir) {
|
|
589
|
-
const referenced = new Set();
|
|
590
|
-
const releasesRoot = resolve(dataDir, "releases");
|
|
591
|
-
const roots = [];
|
|
592
|
-
for (const entry of await readdir(releasesRoot, { withFileTypes: true }).catch(() => [])) {
|
|
593
|
-
if (entry.name === ".trash") {
|
|
594
|
-
for (const trash of await readdir(resolve(releasesRoot, entry.name), { withFileTypes: true }).catch(() => [])) {
|
|
595
|
-
if (trash.isDirectory())
|
|
596
|
-
roots.push(resolve(releasesRoot, entry.name, trash.name));
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
else if (entry.isDirectory() && !entry.name.startsWith(".candidate-"))
|
|
600
|
-
roots.push(resolve(releasesRoot, entry.name));
|
|
601
|
-
}
|
|
602
|
-
for (const root of roots) {
|
|
603
|
-
try {
|
|
604
|
-
const manifest = JSON.parse(await readFile(resolve(root, RELEASE_MANIFEST_FILENAME), "utf8"));
|
|
605
|
-
for (const layer of manifest.dependencyLayers ?? [])
|
|
606
|
-
if (typeof layer.layerId === "string")
|
|
607
|
-
referenced.add(layer.layerId);
|
|
608
|
-
}
|
|
609
|
-
catch { }
|
|
610
|
-
}
|
|
611
|
-
return referenced;
|
|
612
|
-
}
|
|
613
641
|
function assertDirectChild(parent, child) {
|
|
614
642
|
const fromParent = relative(parent, child);
|
|
615
643
|
if (fromParent.length === 0 || fromParent === ".." || fromParent.startsWith(`..${sep}`) || isAbsolute(fromParent) || fromParent.includes(sep)) {
|
|
@@ -28,6 +28,8 @@ export interface CertifiedReleaseRecord {
|
|
|
28
28
|
readonly releaseRoot: string;
|
|
29
29
|
readonly packageVersion?: string;
|
|
30
30
|
readonly contentDigest: string;
|
|
31
|
+
/** Absent on records written before releases declared a handoff contract. */
|
|
32
|
+
readonly launchContract?: string;
|
|
31
33
|
}
|
|
32
34
|
/** Process-authority compatibility controls for metadata-only release loading. */
|
|
33
35
|
export type ReadCertifiedReleaseManifestOptions = ReadCertifiedDependencyLayerOptions;
|
|
@@ -3,7 +3,7 @@ import { chmod, lstat, mkdir, readFile, realpath, rename, rm, symlink, writeFile
|
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { PRODUCT_PACKAGE_NAME, createReleaseIdentity, discoverReleasePayload, releaseFileIdentity, resolveWithin, } from "./release.js";
|
|
5
5
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
|
-
import { assertCurrentLaunchContract, readLaunchContext } from "../launch-context/index.js";
|
|
6
|
+
import { assertCurrentLaunchContract, isCurrentLaunchContract, readLaunchContext } from "../launch-context/index.js";
|
|
7
7
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
8
8
|
import { assertImmutableFileMode } from "./immutable-platform.js";
|
|
9
9
|
import { dependencyReference, materializeDependencyLayer, readCertifiedDependencyLayer, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
|
|
@@ -156,7 +156,6 @@ export async function assertImmutableExecutionRoot(release, dataDir) {
|
|
|
156
156
|
throw new Error(PRODUCT_TEXT.diagnostic("persistent process selected a different immutable release root"));
|
|
157
157
|
}
|
|
158
158
|
export async function resolveReleaseEntryPoint(release, entryPoint) {
|
|
159
|
-
assertCurrentLaunchContract(release);
|
|
160
159
|
const normalized = entryPoint.split("\\").join("/").replace(/^\.\//, "");
|
|
161
160
|
if (!release.files.some(file => file.path === normalized))
|
|
162
161
|
throw new Error(`entry point is not in the verified release manifest: ${entryPoint}`);
|
|
@@ -203,7 +202,9 @@ function certificationReadyRelease(release) {
|
|
|
203
202
|
return release;
|
|
204
203
|
}
|
|
205
204
|
function validateManifest(value) {
|
|
206
|
-
|
|
205
|
+
if (value.launchContract !== undefined && !isCurrentLaunchContract(value)) {
|
|
206
|
+
throw new Error(PRODUCT_TEXT.diagnostic(`release manifest declares an unsupported launch contract: ${String(value.launchContract)}`));
|
|
207
|
+
}
|
|
207
208
|
if (value.packageName !== PRODUCT_PACKAGE_NAME || typeof value.packageVersion !== "string")
|
|
208
209
|
throw new Error(PRODUCT_TEXT.diagnostic("release manifest metadata is invalid"));
|
|
209
210
|
if (!/^[a-f0-9]{64}$/.test(value.contentDigest) || !/^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/.test(value.releaseId))
|
|
@@ -2,8 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
|
-
import {
|
|
6
|
-
import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
|
|
5
|
+
import { PRIVATE_LAUNCH_CONTRACT, launchContractTarget } from "../launch-context/index.js";
|
|
6
|
+
import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath, readCertifiedDependencyLayer } from "./dependency-layer.js";
|
|
7
|
+
import { managedDependencyCertificationPath } from "./dependency-certification.js";
|
|
7
8
|
import { immutablePlatformPolicy } from "./immutable-platform.js";
|
|
8
9
|
import { PRODUCT_PACKAGE_NAME } from "./release.js";
|
|
9
10
|
import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
|
|
@@ -12,7 +13,6 @@ const RESTART_SEAL_VERSION = 1;
|
|
|
12
13
|
const REQUIRED_LAUNCH_ENTRIES = ["bin/supervisor.js", "bin/guardian.js"];
|
|
13
14
|
/** Build compact restart authority after complete content verification established read-only payload files. */
|
|
14
15
|
export async function createRestartSeal(release, dataDir) {
|
|
15
|
-
assertCurrentLaunchContract(release);
|
|
16
16
|
const policy = immutablePlatformPolicy();
|
|
17
17
|
if (policy === null)
|
|
18
18
|
return null;
|
|
@@ -29,6 +29,7 @@ export async function createRestartSeal(release, dataDir) {
|
|
|
29
29
|
const dependencyLayers = await Promise.all((release.dependencyLayers ?? []).map(async (reference) => {
|
|
30
30
|
if (layersRoot === null)
|
|
31
31
|
throw new Error("restart certification has no dependency-layer root");
|
|
32
|
+
await readCertifiedDependencyLayer(canonicalData, reference.layerId, reference);
|
|
32
33
|
const layerRoot = await realpath(resolve(layersRoot, reference.layerId));
|
|
33
34
|
assertDirectChild(layersRoot, layerRoot, reference.layerId, "dependency layer");
|
|
34
35
|
const bindingPath = resolve(releaseRoot, reference.binding);
|
|
@@ -47,7 +48,7 @@ export async function createRestartSeal(release, dataDir) {
|
|
|
47
48
|
}));
|
|
48
49
|
const core = {
|
|
49
50
|
version: RESTART_SEAL_VERSION,
|
|
50
|
-
launchContract: release
|
|
51
|
+
launchContract: launchContractTarget(release),
|
|
51
52
|
platform: process.platform,
|
|
52
53
|
platformPolicy: policy,
|
|
53
54
|
releaseId: release.releaseId,
|
|
@@ -84,7 +85,11 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
|
|
|
84
85
|
throw new Error("restart certification differs from the approved release record");
|
|
85
86
|
}
|
|
86
87
|
const seal = normalizeRestartSeal(certification.restartSeal);
|
|
87
|
-
|
|
88
|
+
// Invariant: a seal is restart authority only for a handoff this build can still
|
|
89
|
+
// perform, which is the contract it emits or the pre-cutover one it retains.
|
|
90
|
+
if (seal.launchContract !== PRIVATE_LAUNCH_CONTRACT && seal.launchContract !== "superseded") {
|
|
91
|
+
throw new Error(`restart seal declares an unsupported launch contract: ${String(seal.launchContract)}`);
|
|
92
|
+
}
|
|
88
93
|
if (seal.platform !== process.platform || seal.platformPolicy !== immutablePlatformPolicy()) {
|
|
89
94
|
throw new Error("restart certification platform immutability evidence is unsupported");
|
|
90
95
|
}
|
|
@@ -123,6 +128,12 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
|
|
|
123
128
|
assertDirectChild(layersRoot, layerRoot, layer.layerId, "dependency layer");
|
|
124
129
|
await assertPathEvidence(layer.layerRoot, onEvent);
|
|
125
130
|
await assertPathEvidence(layer.manifest, onEvent);
|
|
131
|
+
const legacyPath = legacyDependencyLayerCertificationPath(canonicalData, layer.layerId);
|
|
132
|
+
const canonicalPath = dependencyLayerCertificationPath(canonicalData, layer.layerId);
|
|
133
|
+
if (layer.certification.path !== legacyPath && layer.certification.path !== canonicalPath) {
|
|
134
|
+
throw new Error("restart dependency certification path is outside managed storage");
|
|
135
|
+
}
|
|
136
|
+
await managedDependencyCertificationPath(canonicalData, layer.layerId, layer.certification.path === legacyPath);
|
|
126
137
|
await assertPathEvidence(layer.certification, onEvent);
|
|
127
138
|
await assertPathEvidence(layer.bindingEvidence, onEvent, true);
|
|
128
139
|
const bindingTarget = await observedRealpath(layer.bindingEvidence.path, onEvent);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CohortState, ReleaseRecord } from "./cohort-state.js";
|
|
2
|
+
import type { UpdateTransaction } from "./update-transaction.js";
|
|
3
|
+
/** Keep interactive launches on the last successful release until the update commits success. */
|
|
4
|
+
export declare function selectUpdateLaunchRelease(state: CohortState, transaction: UpdateTransaction | null): ReleaseRecord | null;
|
|
5
|
+
/** Pause cohort retirement during preparation; afterward only the successful launch release admits new sessions. */
|
|
6
|
+
export declare function selectSupervisorLaunchReleaseId(state: CohortState, transaction: UpdateTransaction | null): string | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Keep interactive launches on the last successful release until the update commits success. */
|
|
2
|
+
export function selectUpdateLaunchRelease(state, transaction) {
|
|
3
|
+
if (transaction === null || transaction.status === "completed")
|
|
4
|
+
return null;
|
|
5
|
+
const prior = transaction.priorActiveReleaseId === null ? undefined : state.releases[transaction.priorActiveReleaseId];
|
|
6
|
+
if (!prior || prior.approval !== "approved") {
|
|
7
|
+
throw new Error("unfinished update has no verified previous release available for launch; resume the update");
|
|
8
|
+
}
|
|
9
|
+
return prior;
|
|
10
|
+
}
|
|
11
|
+
/** Pause cohort retirement during preparation; afterward only the successful launch release admits new sessions. */
|
|
12
|
+
export function selectSupervisorLaunchReleaseId(state, transaction) {
|
|
13
|
+
// Concurrency: both the previous cohort and the warming candidate must remain available until cutover.
|
|
14
|
+
if (transaction?.status === "active")
|
|
15
|
+
return null;
|
|
16
|
+
return selectUpdateLaunchRelease(state, transaction)?.releaseId ?? state.references.active;
|
|
17
|
+
}
|