@moinulmoin/eden 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +42 -0
- package/README.md +20 -0
- package/dist/eve-control-plane.d.ts +228 -0
- package/dist/eve-control-plane.js +2007 -0
- package/dist/eve-packaging.d.ts +261 -0
- package/dist/eve-packaging.js +2056 -0
- package/dist/eve-runtime-config.d.ts +126 -0
- package/dist/eve-runtime-config.js +446 -0
- package/dist/eve-runtime-image.d.ts +121 -0
- package/dist/eve-runtime-image.js +1134 -0
- package/dist/eve.d.ts +49 -0
- package/dist/eve.js +234 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +6840 -0
- package/package.json +56 -0
|
@@ -0,0 +1,2056 @@
|
|
|
1
|
+
import { createHash, } from "node:crypto";
|
|
2
|
+
import { execFile, } from "node:child_process";
|
|
3
|
+
import { chmod, lstat, mkdir, readFile, readlink, readdir, realpath, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { promisify, } from "node:util";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export class EvePackagingError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
subject;
|
|
10
|
+
remediation;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
super(options.reason);
|
|
13
|
+
this.name = "EvePackagingError";
|
|
14
|
+
this.code = options.code;
|
|
15
|
+
this.subject = options.subject;
|
|
16
|
+
this.remediation = options.remediation;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const INSTALL_COMMAND = [
|
|
20
|
+
"corepack",
|
|
21
|
+
"pnpm",
|
|
22
|
+
"install",
|
|
23
|
+
"--frozen-lockfile",
|
|
24
|
+
];
|
|
25
|
+
const BUILD_COMMAND = ["./node_modules/.bin/eve", "build"];
|
|
26
|
+
const START_COMMAND = [
|
|
27
|
+
"./node_modules/.bin/eve",
|
|
28
|
+
"start",
|
|
29
|
+
"--host",
|
|
30
|
+
"0.0.0.0",
|
|
31
|
+
"--port",
|
|
32
|
+
"8080",
|
|
33
|
+
];
|
|
34
|
+
const EVE_ENTRYPOINT = ".output/server/index.mjs";
|
|
35
|
+
const EVE_EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
36
|
+
".eden",
|
|
37
|
+
".git",
|
|
38
|
+
".hg",
|
|
39
|
+
".next",
|
|
40
|
+
".nuxt",
|
|
41
|
+
".pnpm-store",
|
|
42
|
+
".cache",
|
|
43
|
+
".npm",
|
|
44
|
+
".yarn",
|
|
45
|
+
".output",
|
|
46
|
+
".turbo",
|
|
47
|
+
".wrangler",
|
|
48
|
+
"coverage",
|
|
49
|
+
"node_modules",
|
|
50
|
+
]);
|
|
51
|
+
const EVE_EXCLUDED_FILE_NAMES = new Set([
|
|
52
|
+
".DS_Store",
|
|
53
|
+
".npmrc",
|
|
54
|
+
".pnpmrc",
|
|
55
|
+
".pypirc",
|
|
56
|
+
".bunfig.toml",
|
|
57
|
+
".yarnrc",
|
|
58
|
+
".yarnrc.yml",
|
|
59
|
+
"credentials.json",
|
|
60
|
+
"service-account.json",
|
|
61
|
+
]);
|
|
62
|
+
const EVE_COMPETING_LOCKFILES = new Set([
|
|
63
|
+
"bun.lock",
|
|
64
|
+
"bun.lockb",
|
|
65
|
+
"npm-shrinkwrap.json",
|
|
66
|
+
"package-lock.json",
|
|
67
|
+
"yarn.lock",
|
|
68
|
+
]);
|
|
69
|
+
const HOST_ENVIRONMENT = {
|
|
70
|
+
HOST: "0.0.0.0",
|
|
71
|
+
NITRO_HOST: "0.0.0.0",
|
|
72
|
+
PORT: "8080",
|
|
73
|
+
NITRO_PORT: "8080",
|
|
74
|
+
NODE_ENV: "production",
|
|
75
|
+
};
|
|
76
|
+
function sha256(value) {
|
|
77
|
+
return createHash("sha256").update(value).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
function jsonBytes(value) {
|
|
80
|
+
return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
|
|
81
|
+
}
|
|
82
|
+
function isWithin(root, candidate) {
|
|
83
|
+
const normalizedRoot = root.endsWith("/") ? root.slice(0, -1) : root;
|
|
84
|
+
return candidate === normalizedRoot ||
|
|
85
|
+
candidate.startsWith(`${normalizedRoot}/`);
|
|
86
|
+
}
|
|
87
|
+
function safeRelativePath(root, candidate) {
|
|
88
|
+
return relative(root, candidate).split("\\").join("/");
|
|
89
|
+
}
|
|
90
|
+
function isAllowedSystemAlias(path) {
|
|
91
|
+
const normalized = resolve(path);
|
|
92
|
+
return normalized === "/var" || normalized === "/tmp";
|
|
93
|
+
}
|
|
94
|
+
function isPathExcluded(relativePath, runtimeEnvRelativePath) {
|
|
95
|
+
const parts = relativePath.split("/");
|
|
96
|
+
const name = parts[parts.length - 1];
|
|
97
|
+
const excludedDirectory = parts.find((part) => EVE_EXCLUDED_DIRECTORY_NAMES.has(part));
|
|
98
|
+
if (excludedDirectory !== undefined) {
|
|
99
|
+
return {
|
|
100
|
+
excluded: true,
|
|
101
|
+
category: excludedDirectory === ".eden" || excludedDirectory === ".git"
|
|
102
|
+
? "generated-state"
|
|
103
|
+
: excludedDirectory === "node_modules"
|
|
104
|
+
? "node_modules"
|
|
105
|
+
: "build-cache",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (runtimeEnvRelativePath !== undefined && relativePath === runtimeEnvRelativePath) {
|
|
109
|
+
return { excluded: true, category: "runtime-env" };
|
|
110
|
+
}
|
|
111
|
+
if (name !== undefined && name.startsWith(".env")) {
|
|
112
|
+
return { excluded: true, category: "runtime-env" };
|
|
113
|
+
}
|
|
114
|
+
if ((name !== undefined && EVE_EXCLUDED_FILE_NAMES.has(name)) ||
|
|
115
|
+
(name !== undefined && (name.endsWith(".pem") || name.endsWith(".key")))) {
|
|
116
|
+
return { excluded: true, category: "credentials" };
|
|
117
|
+
}
|
|
118
|
+
if (name !== undefined &&
|
|
119
|
+
(name.endsWith(".swp") || name.endsWith(".swo") || name.endsWith(".tmp"))) {
|
|
120
|
+
return { excluded: true, category: "temporary-state" };
|
|
121
|
+
}
|
|
122
|
+
return { excluded: false, category: undefined };
|
|
123
|
+
}
|
|
124
|
+
async function readStableFile(path, containmentRoot) {
|
|
125
|
+
const canonicalRoot = containmentRoot === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: await realpath(containmentRoot).catch(() => undefined);
|
|
128
|
+
const parentBefore = containmentRoot === undefined
|
|
129
|
+
? undefined
|
|
130
|
+
: await realpath(dirname(path)).catch(() => undefined);
|
|
131
|
+
if (containmentRoot !== undefined &&
|
|
132
|
+
(canonicalRoot === undefined ||
|
|
133
|
+
parentBefore === undefined ||
|
|
134
|
+
!isWithin(canonicalRoot, resolve(parentBefore)))) {
|
|
135
|
+
throw new EvePackagingError({
|
|
136
|
+
code: "SOURCE_RACE",
|
|
137
|
+
subject: safeRelativePath(containmentRoot ?? dirname(path), path),
|
|
138
|
+
reason: "The observed file parent escaped its Eden-owned containment root.",
|
|
139
|
+
remediation: "Retry only after the selected source tree is quiescent.",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const before = await lstat(path).catch(() => undefined);
|
|
143
|
+
if (before === undefined ||
|
|
144
|
+
!before.isFile() ||
|
|
145
|
+
before.isSymbolicLink()) {
|
|
146
|
+
throw new EvePackagingError({
|
|
147
|
+
code: "ROOT_INVALID",
|
|
148
|
+
subject: safeRelativePath(dirname(path), path),
|
|
149
|
+
reason: "The Eve build input is missing, symlinked, or not a regular file.",
|
|
150
|
+
remediation: "Replace the input with a readable regular file inside the selected project root.",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const bytes = await readFile(path);
|
|
154
|
+
const secondRead = await readFile(path).catch(() => undefined);
|
|
155
|
+
const after = await lstat(path).catch(() => undefined);
|
|
156
|
+
const parentAfter = containmentRoot === undefined
|
|
157
|
+
? undefined
|
|
158
|
+
: await realpath(dirname(path)).catch(() => undefined);
|
|
159
|
+
if (secondRead === undefined ||
|
|
160
|
+
!bytes.equals(secondRead) ||
|
|
161
|
+
after === undefined ||
|
|
162
|
+
!after.isFile() ||
|
|
163
|
+
after.isSymbolicLink() ||
|
|
164
|
+
after.dev !== before.dev ||
|
|
165
|
+
after.ino !== before.ino ||
|
|
166
|
+
after.size !== before.size ||
|
|
167
|
+
after.mode !== before.mode ||
|
|
168
|
+
(containmentRoot !== undefined && parentAfter !== parentBefore)) {
|
|
169
|
+
throw new EvePackagingError({
|
|
170
|
+
code: "SOURCE_RACE",
|
|
171
|
+
subject: basename(path),
|
|
172
|
+
reason: "An Eve build input changed while it was being observed.",
|
|
173
|
+
remediation: "Retry only after the selected project and its lockfile are quiescent.",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
bytes,
|
|
178
|
+
mode: before.mode & 0o777,
|
|
179
|
+
sha256: sha256(bytes),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function captureInputs(root, runtimeConfig, artifactRoot, runtimeInputIdentity = runtimeConfig?.inputIdentity, requestedRoot = root) {
|
|
183
|
+
const runtimeEnvRelativePath = (() => {
|
|
184
|
+
if (runtimeConfig?.envFilePath === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const candidate = resolve(runtimeConfig.envFilePath);
|
|
187
|
+
if (isWithin(root, candidate))
|
|
188
|
+
return safeRelativePath(root, candidate);
|
|
189
|
+
if (isWithin(resolve(requestedRoot), candidate)) {
|
|
190
|
+
return safeRelativePath(resolve(requestedRoot), candidate);
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
})();
|
|
194
|
+
const files = [];
|
|
195
|
+
const excludedRelativePaths = [];
|
|
196
|
+
const visit = async (directory, relativeDirectory) => {
|
|
197
|
+
const directoryDetails = await lstat(directory).catch(() => undefined);
|
|
198
|
+
const canonicalDirectory = await realpath(directory).catch(() => undefined);
|
|
199
|
+
if (directoryDetails === undefined ||
|
|
200
|
+
!directoryDetails.isDirectory() ||
|
|
201
|
+
directoryDetails.isSymbolicLink() ||
|
|
202
|
+
canonicalDirectory === undefined ||
|
|
203
|
+
!isWithin(root, resolve(canonicalDirectory))) {
|
|
204
|
+
throw new EvePackagingError({
|
|
205
|
+
code: "SOURCE_RACE",
|
|
206
|
+
subject: relativeDirectory || ".",
|
|
207
|
+
reason: "The selected Eve source directory was replaced or escaped while it was being observed.",
|
|
208
|
+
remediation: "Retry only after the selected project tree is quiescent.",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
212
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const relativePath = relativeDirectory.length === 0
|
|
215
|
+
? entry.name
|
|
216
|
+
: `${relativeDirectory}/${entry.name}`;
|
|
217
|
+
const candidate = join(directory, entry.name);
|
|
218
|
+
const absoluteCandidate = resolve(candidate);
|
|
219
|
+
if (isWithin(artifactRoot, absoluteCandidate)) {
|
|
220
|
+
excludedRelativePaths.push(relativePath);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const exclusion = isPathExcluded(relativePath, runtimeEnvRelativePath);
|
|
224
|
+
if (exclusion.excluded) {
|
|
225
|
+
excludedRelativePaths.push(relativePath);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (entry.isSymbolicLink()) {
|
|
229
|
+
throw new EvePackagingError({
|
|
230
|
+
code: relativePath === "pnpm-lock.yaml"
|
|
231
|
+
? "DEPENDENCY_AMBIGUITY"
|
|
232
|
+
: "ROOT_INVALID",
|
|
233
|
+
subject: relativePath,
|
|
234
|
+
reason: relativePath === "pnpm-lock.yaml"
|
|
235
|
+
? "The root pnpm-lock.yaml must be a regular file, not a symbolic link."
|
|
236
|
+
: "Eve build inputs may not contain symbolic links.",
|
|
237
|
+
remediation: relativePath === "pnpm-lock.yaml"
|
|
238
|
+
? "Copy the matching lockfile into the selected project root and retry."
|
|
239
|
+
: "Copy the file into the selected project root and retry.",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (entry.isDirectory()) {
|
|
243
|
+
await visit(candidate, relativePath);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!entry.isFile()) {
|
|
247
|
+
throw new EvePackagingError({
|
|
248
|
+
code: "ROOT_INVALID",
|
|
249
|
+
subject: relativePath,
|
|
250
|
+
reason: "Eve build inputs may not contain devices, sockets, FIFOs, or other special files.",
|
|
251
|
+
remediation: "Remove the special file from the selected project inputs and retry.",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
let stable;
|
|
255
|
+
try {
|
|
256
|
+
stable = await readStableFile(candidate, root);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (relativePath === "pnpm-lock.yaml" &&
|
|
260
|
+
error instanceof EvePackagingError &&
|
|
261
|
+
error.code === "ROOT_INVALID") {
|
|
262
|
+
throw new EvePackagingError({
|
|
263
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
264
|
+
subject: relativePath,
|
|
265
|
+
reason: "The root pnpm-lock.yaml is unreadable or changed while it was observed.",
|
|
266
|
+
remediation: "Provide one readable regular lockfile generated by the declared exact pnpm version.",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
files.push({
|
|
272
|
+
relativePath,
|
|
273
|
+
sha256: stable.sha256,
|
|
274
|
+
byteLength: stable.bytes.byteLength,
|
|
275
|
+
mode: stable.mode,
|
|
276
|
+
bytes: stable.bytes,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
await visit(root, "");
|
|
281
|
+
files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
282
|
+
excludedRelativePaths.sort();
|
|
283
|
+
const sourceDigest = sha256(jsonBytes({
|
|
284
|
+
files: files.map((file) => ({
|
|
285
|
+
relativePath: file.relativePath,
|
|
286
|
+
sha256: file.sha256,
|
|
287
|
+
byteLength: file.byteLength,
|
|
288
|
+
mode: file.mode,
|
|
289
|
+
})),
|
|
290
|
+
runtimeConfigInputIdentity: runtimeInputIdentity,
|
|
291
|
+
}));
|
|
292
|
+
return {
|
|
293
|
+
files,
|
|
294
|
+
excludedRelativePaths,
|
|
295
|
+
sourceDigest,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function sourceInputsEqual(left, right) {
|
|
299
|
+
if (left.sourceDigest !== right.sourceDigest)
|
|
300
|
+
return false;
|
|
301
|
+
if (left.files.length !== right.files.length)
|
|
302
|
+
return false;
|
|
303
|
+
return left.files.every((file, index) => {
|
|
304
|
+
const other = right.files[index];
|
|
305
|
+
return other !== undefined &&
|
|
306
|
+
file.relativePath === other.relativePath &&
|
|
307
|
+
file.sha256 === other.sha256 &&
|
|
308
|
+
file.byteLength === other.byteLength &&
|
|
309
|
+
file.mode === other.mode;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function parsePnpmVersion(packageManager) {
|
|
313
|
+
if (typeof packageManager !== "string") {
|
|
314
|
+
throw new EvePackagingError({
|
|
315
|
+
code: "UNSUPPORTED_TOOLCHAIN",
|
|
316
|
+
subject: "package.json.packageManager",
|
|
317
|
+
reason: "The Eve MVP requires packageManager to pin an exact pnpm version.",
|
|
318
|
+
remediation: 'Set package.json.packageManager to an exact value such as "pnpm@11.21.0".',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const match = /^pnpm@((?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))$/u.exec(packageManager);
|
|
322
|
+
if (match === null) {
|
|
323
|
+
throw new EvePackagingError({
|
|
324
|
+
code: "UNSUPPORTED_TOOLCHAIN",
|
|
325
|
+
subject: "package.json.packageManager",
|
|
326
|
+
reason: "The Eve MVP supports only an exact pnpm@<major>.<minor>.<patch> pin.",
|
|
327
|
+
remediation: 'Replace ranges, tags, and other package managers with an exact pnpm pin.',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return match[1];
|
|
331
|
+
}
|
|
332
|
+
function lockfileMajor(lockfile) {
|
|
333
|
+
const text = lockfile.toString("utf8");
|
|
334
|
+
const match = /^\s*lockfileVersion:\s*['"]?([0-9]+)(?:\.[0-9]+)?['"]?\s*$/mu.exec(text);
|
|
335
|
+
return match === null ? undefined : Number(match[1]);
|
|
336
|
+
}
|
|
337
|
+
function lockfileMatchesPnpm(version, lockfile) {
|
|
338
|
+
const [majorText] = version.split(".");
|
|
339
|
+
const pnpmMajor = Number(majorText);
|
|
340
|
+
const declaredLockfileMajor = lockfileMajor(lockfile);
|
|
341
|
+
if (declaredLockfileMajor === undefined)
|
|
342
|
+
return false;
|
|
343
|
+
if (pnpmMajor >= 9)
|
|
344
|
+
return declaredLockfileMajor === 9;
|
|
345
|
+
if (pnpmMajor >= 7)
|
|
346
|
+
return declaredLockfileMajor === 6;
|
|
347
|
+
return declaredLockfileMajor === 5;
|
|
348
|
+
}
|
|
349
|
+
function packageJsonObject(bytes) {
|
|
350
|
+
let parsed;
|
|
351
|
+
try {
|
|
352
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
throw new EvePackagingError({
|
|
356
|
+
code: "ROOT_INVALID",
|
|
357
|
+
subject: "package.json",
|
|
358
|
+
reason: "The selected package.json is not valid JSON.",
|
|
359
|
+
remediation: "Fix package.json and retry with the same explicit project root.",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (typeof parsed !== "object" ||
|
|
363
|
+
parsed === null ||
|
|
364
|
+
Array.isArray(parsed)) {
|
|
365
|
+
throw new EvePackagingError({
|
|
366
|
+
code: "ROOT_INVALID",
|
|
367
|
+
subject: "package.json",
|
|
368
|
+
reason: "The selected package.json must contain a JSON object.",
|
|
369
|
+
remediation: "Provide a regular Eve project manifest with a non-empty name.",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return parsed;
|
|
373
|
+
}
|
|
374
|
+
function validateProjectContract(inputs) {
|
|
375
|
+
const packageFile = inputs.files.find((file) => file.relativePath === "package.json");
|
|
376
|
+
if (packageFile === undefined) {
|
|
377
|
+
throw new EvePackagingError({
|
|
378
|
+
code: "ROOT_INVALID",
|
|
379
|
+
subject: "package.json",
|
|
380
|
+
reason: "The selected Eve project root must contain a regular package.json.",
|
|
381
|
+
remediation: "Select the actual Eve application root instead of an ancestor or nested directory.",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const packageValue = packageJsonObject(packageFile.bytes);
|
|
385
|
+
const projectId = packageValue.name;
|
|
386
|
+
if (typeof projectId !== "string" || projectId.trim().length === 0) {
|
|
387
|
+
throw new EvePackagingError({
|
|
388
|
+
code: "ROOT_INVALID",
|
|
389
|
+
subject: "package.json.name",
|
|
390
|
+
reason: "The selected package.json must define a non-empty project identity.",
|
|
391
|
+
remediation: "Add a non-empty package.json name and retry.",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const packageManagerVersion = parsePnpmVersion(packageValue.packageManager);
|
|
395
|
+
const lockFiles = inputs.files.filter((file) => file.relativePath === "pnpm-lock.yaml" ||
|
|
396
|
+
file.relativePath.endsWith("/pnpm-lock.yaml"));
|
|
397
|
+
if (lockFiles.length !== 1 || lockFiles[0]?.relativePath !== "pnpm-lock.yaml") {
|
|
398
|
+
throw new EvePackagingError({
|
|
399
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
400
|
+
subject: "pnpm-lock.yaml",
|
|
401
|
+
reason: "The selected Eve root must contain exactly one regular root pnpm-lock.yaml.",
|
|
402
|
+
remediation: "Remove competing or nested lockfiles and retry with the matching root lockfile.",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const competing = inputs.files.find((file) => EVE_COMPETING_LOCKFILES.has(basename(file.relativePath)));
|
|
406
|
+
if (competing !== undefined) {
|
|
407
|
+
throw new EvePackagingError({
|
|
408
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
409
|
+
subject: competing.relativePath,
|
|
410
|
+
reason: "A competing package-manager lockfile was found at the selected Eve root.",
|
|
411
|
+
remediation: "Keep only the pinned pnpm toolchain and its matching root lockfile.",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const lockfile = lockFiles[0];
|
|
415
|
+
if (lockfile === undefined || !lockfileMatchesPnpm(packageManagerVersion, lockfile.bytes)) {
|
|
416
|
+
throw new EvePackagingError({
|
|
417
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
418
|
+
subject: "pnpm-lock.yaml",
|
|
419
|
+
reason: "The root pnpm-lock.yaml is malformed or does not match the exact packageManager pin.",
|
|
420
|
+
remediation: "Regenerate the lockfile with the declared pnpm version, without changing it during packaging.",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
projectId,
|
|
425
|
+
packageManagerVersion,
|
|
426
|
+
packageJsonSha256: packageFile.sha256,
|
|
427
|
+
lockfileSha256: lockfile.sha256,
|
|
428
|
+
lockfileBytes: lockfile.bytes,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function assertCanonicalRoot(projectRoot) {
|
|
432
|
+
if (typeof projectRoot !== "string" || projectRoot.length === 0) {
|
|
433
|
+
throw new EvePackagingError({
|
|
434
|
+
code: "ROOT_INVALID",
|
|
435
|
+
subject: "project",
|
|
436
|
+
reason: "The Eve project root must be an explicit non-empty path.",
|
|
437
|
+
remediation: "Pass the canonical application directory explicitly.",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const requestedRoot = isAbsolute(projectRoot)
|
|
441
|
+
? resolve(projectRoot)
|
|
442
|
+
: resolve(process.cwd(), projectRoot);
|
|
443
|
+
const details = await lstat(requestedRoot).catch(() => undefined);
|
|
444
|
+
if (details === undefined ||
|
|
445
|
+
!details.isDirectory() ||
|
|
446
|
+
details.isSymbolicLink()) {
|
|
447
|
+
throw new EvePackagingError({
|
|
448
|
+
code: "ROOT_INVALID",
|
|
449
|
+
subject: "project",
|
|
450
|
+
reason: "The selected Eve project root must be an existing canonical directory.",
|
|
451
|
+
remediation: "Pass the actual readable application directory, not a file, symlink, ancestor, or missing path.",
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const canonicalRoot = await realpath(requestedRoot).catch(() => undefined);
|
|
455
|
+
if (canonicalRoot === undefined) {
|
|
456
|
+
throw new EvePackagingError({
|
|
457
|
+
code: "ROOT_INVALID",
|
|
458
|
+
subject: "project",
|
|
459
|
+
reason: "The selected Eve project root is not canonical.",
|
|
460
|
+
remediation: "Resolve the path and pass the real directory explicitly.",
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
await readdir(canonicalRoot).catch(() => {
|
|
464
|
+
throw new EvePackagingError({
|
|
465
|
+
code: "ROOT_INVALID",
|
|
466
|
+
subject: "project",
|
|
467
|
+
reason: "The selected Eve project root is not readable.",
|
|
468
|
+
remediation: "Grant read access to the explicit project root and retry.",
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
return { requestedRoot, canonicalRoot };
|
|
472
|
+
}
|
|
473
|
+
async function assertRequiredRootInputs(root) {
|
|
474
|
+
const packageDetails = await lstat(join(root, "package.json")).catch(() => undefined);
|
|
475
|
+
if (packageDetails === undefined ||
|
|
476
|
+
!packageDetails.isFile() ||
|
|
477
|
+
packageDetails.isSymbolicLink()) {
|
|
478
|
+
throw new EvePackagingError({
|
|
479
|
+
code: "ROOT_INVALID",
|
|
480
|
+
subject: "package.json",
|
|
481
|
+
reason: "The selected Eve root must contain a regular package.json before any build input is walked.",
|
|
482
|
+
remediation: "Select the actual Eve application root instead of an ancestor or nested directory.",
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function prepareOutputParentChain(path) {
|
|
487
|
+
const absolute = resolve(path);
|
|
488
|
+
const parts = absolute.split("/").filter(Boolean);
|
|
489
|
+
let current = absolute.startsWith("/") ? "/" : "";
|
|
490
|
+
let missingStart = parts.length;
|
|
491
|
+
for (const [index, part] of parts.entries()) {
|
|
492
|
+
const candidate = current === "/" ? `/${part}` : join(current, part);
|
|
493
|
+
const details = await lstat(candidate).catch(() => undefined);
|
|
494
|
+
if (details === undefined) {
|
|
495
|
+
missingStart = index;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
if (details.isSymbolicLink()) {
|
|
499
|
+
if (!isAllowedSystemAlias(candidate)) {
|
|
500
|
+
throw new EvePackagingError({
|
|
501
|
+
code: "ROOT_INVALID",
|
|
502
|
+
subject: path,
|
|
503
|
+
reason: "The Eden-owned artifact parent contains an unsafe symbolic link.",
|
|
504
|
+
remediation: "Use a regular artifact directory and retry.",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
current = candidate;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (!details.isDirectory()) {
|
|
511
|
+
throw new EvePackagingError({
|
|
512
|
+
code: "ROOT_INVALID",
|
|
513
|
+
subject: path,
|
|
514
|
+
reason: "The Eden-owned artifact parent is not a directory.",
|
|
515
|
+
remediation: "Choose a writable regular artifact parent and retry.",
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
current = candidate;
|
|
519
|
+
}
|
|
520
|
+
const canonicalBase = await realpath(current).catch(() => undefined);
|
|
521
|
+
if (canonicalBase === undefined) {
|
|
522
|
+
throw new EvePackagingError({
|
|
523
|
+
code: "ROOT_INVALID",
|
|
524
|
+
subject: path,
|
|
525
|
+
reason: "The Eden-owned artifact parent could not be resolved safely.",
|
|
526
|
+
remediation: "Choose a writable artifact parent and retry.",
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (missingStart === parts.length)
|
|
530
|
+
return canonicalBase;
|
|
531
|
+
let created = canonicalBase;
|
|
532
|
+
for (const part of parts.slice(missingStart)) {
|
|
533
|
+
created = join(created, part);
|
|
534
|
+
await mkdir(created);
|
|
535
|
+
const details = await lstat(created).catch(() => undefined);
|
|
536
|
+
const canonicalCreated = await realpath(created).catch(() => undefined);
|
|
537
|
+
if (details === undefined ||
|
|
538
|
+
!details.isDirectory() ||
|
|
539
|
+
details.isSymbolicLink() ||
|
|
540
|
+
canonicalCreated !== created) {
|
|
541
|
+
throw new EvePackagingError({
|
|
542
|
+
code: "ROOT_INVALID",
|
|
543
|
+
subject: path,
|
|
544
|
+
reason: "The Eden-owned artifact parent could not be created as regular directories.",
|
|
545
|
+
remediation: "Choose a writable artifact parent and retry.",
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return created;
|
|
550
|
+
}
|
|
551
|
+
async function createGenerationRoot(artifactRoot, projectRoot) {
|
|
552
|
+
const generationRoot = resolve(artifactRoot);
|
|
553
|
+
if (generationRoot === projectRoot) {
|
|
554
|
+
throw new EvePackagingError({
|
|
555
|
+
code: "ROOT_INVALID",
|
|
556
|
+
subject: "artifactRoot",
|
|
557
|
+
reason: "Generated Eve artifacts may not overwrite the selected project root.",
|
|
558
|
+
remediation: "Use a new Eden-owned generation directory outside authored source.",
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (isWithin(projectRoot, generationRoot)) {
|
|
562
|
+
const first = safeRelativePath(projectRoot, generationRoot).split("/")[0];
|
|
563
|
+
if (first !== ".eden") {
|
|
564
|
+
throw new EvePackagingError({
|
|
565
|
+
code: "ROOT_INVALID",
|
|
566
|
+
subject: "artifactRoot",
|
|
567
|
+
reason: "Generated Eve artifacts may only be nested under the project-local .eden directory.",
|
|
568
|
+
remediation: "Use .eden/eve-deploy/generations/<new-id> or an external Eden-owned directory.",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const parent = dirname(generationRoot);
|
|
573
|
+
const canonicalParent = await prepareOutputParentChain(parent);
|
|
574
|
+
const canonicalGenerationRoot = join(canonicalParent, basename(generationRoot));
|
|
575
|
+
if (isWithin(projectRoot, canonicalGenerationRoot)) {
|
|
576
|
+
const first = safeRelativePath(projectRoot, canonicalGenerationRoot).split("/")[0];
|
|
577
|
+
if (first !== ".eden") {
|
|
578
|
+
throw new EvePackagingError({
|
|
579
|
+
code: "ROOT_INVALID",
|
|
580
|
+
subject: "artifactRoot",
|
|
581
|
+
reason: "The resolved Eden artifact path would overwrite authored project files.",
|
|
582
|
+
remediation: "Use a generation directory under .eden or outside the canonical project root.",
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const existing = await lstat(canonicalGenerationRoot).catch(() => undefined);
|
|
587
|
+
if (existing !== undefined) {
|
|
588
|
+
throw new EvePackagingError({
|
|
589
|
+
code: "ROOT_INVALID",
|
|
590
|
+
subject: "artifactRoot",
|
|
591
|
+
reason: "The requested Eve generation directory already exists.",
|
|
592
|
+
remediation: "Create a new generation directory and preserve the prior generation.",
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const parentBeforeCreate = await realpath(canonicalParent).catch(() => undefined);
|
|
596
|
+
if (parentBeforeCreate !== canonicalParent) {
|
|
597
|
+
throw new EvePackagingError({
|
|
598
|
+
code: "ROOT_INVALID",
|
|
599
|
+
subject: "artifactRoot",
|
|
600
|
+
reason: "The Eden-owned artifact parent changed before generation creation.",
|
|
601
|
+
remediation: "Use a stable regular artifact parent and retry.",
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
await mkdir(canonicalGenerationRoot);
|
|
605
|
+
const parentAfterCreate = await realpath(canonicalParent).catch(() => undefined);
|
|
606
|
+
if (parentAfterCreate !== canonicalParent) {
|
|
607
|
+
throw new EvePackagingError({
|
|
608
|
+
code: "ROOT_INVALID",
|
|
609
|
+
subject: "artifactRoot",
|
|
610
|
+
reason: "The Eden-owned artifact parent changed during generation creation.",
|
|
611
|
+
remediation: "Use a stable regular artifact parent and retry.",
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const created = await lstat(canonicalGenerationRoot).catch(() => undefined);
|
|
615
|
+
if (created === undefined ||
|
|
616
|
+
!created.isDirectory() ||
|
|
617
|
+
created.isSymbolicLink()) {
|
|
618
|
+
throw new EvePackagingError({
|
|
619
|
+
code: "ROOT_INVALID",
|
|
620
|
+
subject: "artifactRoot",
|
|
621
|
+
reason: "The Eden-owned generation directory could not be created safely.",
|
|
622
|
+
remediation: "Choose a writable regular directory and retry.",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return canonicalGenerationRoot;
|
|
626
|
+
}
|
|
627
|
+
async function ensureSnapshotDirectory(snapshotRoot, directory) {
|
|
628
|
+
const relativeDirectory = safeRelativePath(snapshotRoot, resolve(directory));
|
|
629
|
+
if (relativeDirectory === ".." ||
|
|
630
|
+
relativeDirectory.startsWith("../") ||
|
|
631
|
+
relativeDirectory.includes("/../")) {
|
|
632
|
+
throw new EvePackagingError({
|
|
633
|
+
code: "SOURCE_RACE",
|
|
634
|
+
subject: relativeDirectory,
|
|
635
|
+
reason: "The immutable Eve snapshot directory escaped its Eden-owned root.",
|
|
636
|
+
remediation: "Discard the mixed-generation candidate and retry from a clean snapshot.",
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const rootDetails = await lstat(snapshotRoot).catch(() => undefined);
|
|
640
|
+
const rootCanonical = await realpath(snapshotRoot).catch(() => undefined);
|
|
641
|
+
if (rootDetails === undefined ||
|
|
642
|
+
!rootDetails.isDirectory() ||
|
|
643
|
+
rootDetails.isSymbolicLink() ||
|
|
644
|
+
rootCanonical === undefined ||
|
|
645
|
+
!isWithin(snapshotRoot, resolve(rootCanonical))) {
|
|
646
|
+
throw new EvePackagingError({
|
|
647
|
+
code: "SOURCE_RACE",
|
|
648
|
+
subject: ".",
|
|
649
|
+
reason: "The immutable Eve snapshot root was replaced or escaped during creation.",
|
|
650
|
+
remediation: "Discard the mixed-generation candidate and retry from a clean snapshot.",
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
let current = snapshotRoot;
|
|
654
|
+
let currentCanonical = rootCanonical;
|
|
655
|
+
for (const part of relativeDirectory.split("/").filter(Boolean)) {
|
|
656
|
+
const currentObserved = await realpath(current).catch(() => undefined);
|
|
657
|
+
if (currentObserved !== currentCanonical) {
|
|
658
|
+
throw new EvePackagingError({
|
|
659
|
+
code: "SOURCE_RACE",
|
|
660
|
+
subject: safeRelativePath(snapshotRoot, current),
|
|
661
|
+
reason: "The immutable Eve snapshot parent was replaced during creation.",
|
|
662
|
+
remediation: "Discard the mixed-generation candidate and retry from a clean snapshot.",
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
const next = join(current, part);
|
|
666
|
+
const expectedCanonical = join(currentCanonical, part);
|
|
667
|
+
const existing = await lstat(next).catch(() => undefined);
|
|
668
|
+
if (existing === undefined) {
|
|
669
|
+
await mkdir(next);
|
|
670
|
+
}
|
|
671
|
+
const details = await lstat(next).catch(() => undefined);
|
|
672
|
+
const canonical = await realpath(next).catch(() => undefined);
|
|
673
|
+
if (details === undefined ||
|
|
674
|
+
!details.isDirectory() ||
|
|
675
|
+
details.isSymbolicLink() ||
|
|
676
|
+
canonical === undefined ||
|
|
677
|
+
canonical !== expectedCanonical ||
|
|
678
|
+
!isWithin(rootCanonical, resolve(canonical))) {
|
|
679
|
+
throw new EvePackagingError({
|
|
680
|
+
code: "SOURCE_RACE",
|
|
681
|
+
subject: safeRelativePath(snapshotRoot, next),
|
|
682
|
+
reason: "The immutable Eve snapshot directory was replaced or escaped during creation.",
|
|
683
|
+
remediation: "Discard the mixed-generation candidate and retry from a clean snapshot.",
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
current = next;
|
|
687
|
+
currentCanonical = canonical;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function copySnapshot(snapshotRoot, inputs) {
|
|
691
|
+
for (const file of inputs.files) {
|
|
692
|
+
const destination = join(snapshotRoot, file.relativePath);
|
|
693
|
+
const parent = dirname(destination);
|
|
694
|
+
await ensureSnapshotDirectory(snapshotRoot, parent);
|
|
695
|
+
const canonicalRoot = await realpath(snapshotRoot).catch(() => undefined);
|
|
696
|
+
const canonicalParent = await realpath(parent).catch(() => undefined);
|
|
697
|
+
if (canonicalRoot === undefined ||
|
|
698
|
+
canonicalParent === undefined ||
|
|
699
|
+
!isWithin(canonicalRoot, resolve(canonicalParent))) {
|
|
700
|
+
throw new EvePackagingError({
|
|
701
|
+
code: "SOURCE_RACE",
|
|
702
|
+
subject: file.relativePath,
|
|
703
|
+
reason: "The immutable Eve snapshot parent could not be proven contained before writing.",
|
|
704
|
+
remediation: "Discard the mixed-generation candidate and retry from a clean snapshot.",
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
await writeFile(destination, file.bytes, {
|
|
708
|
+
flag: "wx",
|
|
709
|
+
mode: file.mode,
|
|
710
|
+
});
|
|
711
|
+
const afterParent = await realpath(parent).catch(() => undefined);
|
|
712
|
+
if (afterParent !== canonicalParent) {
|
|
713
|
+
throw new EvePackagingError({
|
|
714
|
+
code: "SOURCE_RACE",
|
|
715
|
+
subject: file.relativePath,
|
|
716
|
+
reason: "The immutable Eve snapshot parent changed during writing.",
|
|
717
|
+
remediation: "Discard the mixed-generation candidate and retry from a clean snapshot.",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
await chmod(destination, file.mode).catch(() => undefined);
|
|
721
|
+
const copied = await readStableFile(destination, snapshotRoot);
|
|
722
|
+
if (copied.sha256 !== file.sha256 || copied.bytes.byteLength !== file.byteLength) {
|
|
723
|
+
throw new EvePackagingError({
|
|
724
|
+
code: "SOURCE_RACE",
|
|
725
|
+
subject: file.relativePath,
|
|
726
|
+
reason: "The immutable Eve snapshot did not retain the observed input bytes.",
|
|
727
|
+
remediation: "Retry after the selected project is quiescent.",
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async function verifySnapshotInputs(snapshotRoot, inputs) {
|
|
733
|
+
const generatedRoots = new Set([".dockerignore", ".output", "node_modules"]);
|
|
734
|
+
const observedInputPaths = [];
|
|
735
|
+
const visit = async (directory, relativeDirectory) => {
|
|
736
|
+
const directoryDetails = await lstat(directory).catch(() => undefined);
|
|
737
|
+
const directoryCanonical = await realpath(directory).catch(() => undefined);
|
|
738
|
+
if (directoryDetails === undefined ||
|
|
739
|
+
!directoryDetails.isDirectory() ||
|
|
740
|
+
directoryDetails.isSymbolicLink() ||
|
|
741
|
+
directoryCanonical === undefined ||
|
|
742
|
+
!isWithin(snapshotRoot, resolve(directoryCanonical))) {
|
|
743
|
+
throw new EvePackagingError({
|
|
744
|
+
code: "SOURCE_RACE",
|
|
745
|
+
subject: relativeDirectory || ".",
|
|
746
|
+
reason: "The immutable Eve snapshot directory escaped or changed during verification.",
|
|
747
|
+
remediation: "Discard the mixed-generation candidate and retry from quiescent source bytes.",
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
751
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
752
|
+
for (const entry of entries) {
|
|
753
|
+
const relativePath = relativeDirectory.length === 0
|
|
754
|
+
? entry.name
|
|
755
|
+
: `${relativeDirectory}/${entry.name}`;
|
|
756
|
+
const firstSegment = relativePath.split("/")[0];
|
|
757
|
+
if (firstSegment !== undefined && generatedRoots.has(firstSegment)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const candidate = join(directory, entry.name);
|
|
761
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
762
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
763
|
+
throw new EvePackagingError({
|
|
764
|
+
code: "SOURCE_RACE",
|
|
765
|
+
subject: relativePath,
|
|
766
|
+
reason: "The immutable Eve snapshot gained an unsafe or unsupported input.",
|
|
767
|
+
remediation: "Discard the mixed-generation candidate and retry from quiescent source bytes.",
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
observedInputPaths.push(relativePath);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
await visit(candidate, relativePath);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
await visit(snapshotRoot, "");
|
|
777
|
+
const expectedInputPaths = inputs.files.map((file) => file.relativePath).sort();
|
|
778
|
+
observedInputPaths.sort();
|
|
779
|
+
if (observedInputPaths.length !== expectedInputPaths.length ||
|
|
780
|
+
observedInputPaths.some((path, index) => path !== expectedInputPaths[index])) {
|
|
781
|
+
throw new EvePackagingError({
|
|
782
|
+
code: "SOURCE_RACE",
|
|
783
|
+
subject: "snapshot input set",
|
|
784
|
+
reason: "The immutable Eve snapshot gained or lost authored input paths during installation or build.",
|
|
785
|
+
remediation: "Discard the mixed-generation candidate and retry from quiescent source bytes.",
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
for (const file of inputs.files) {
|
|
789
|
+
try {
|
|
790
|
+
const observed = await readStableFile(join(snapshotRoot, file.relativePath), snapshotRoot);
|
|
791
|
+
if (observed.sha256 !== file.sha256 ||
|
|
792
|
+
observed.bytes.byteLength !== file.byteLength ||
|
|
793
|
+
observed.mode !== file.mode) {
|
|
794
|
+
throw new EvePackagingError({
|
|
795
|
+
code: "SOURCE_RACE",
|
|
796
|
+
subject: file.relativePath,
|
|
797
|
+
reason: "The immutable Eve snapshot changed during installation or build.",
|
|
798
|
+
remediation: "Discard the mixed-generation candidate and retry from quiescent source bytes.",
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
if (error instanceof EvePackagingError) {
|
|
804
|
+
if (error.code === "SOURCE_RACE")
|
|
805
|
+
throw error;
|
|
806
|
+
throw new EvePackagingError({
|
|
807
|
+
code: "SOURCE_RACE",
|
|
808
|
+
subject: file.relativePath,
|
|
809
|
+
reason: "The immutable Eve snapshot changed during installation or build.",
|
|
810
|
+
remediation: "Discard the mixed-generation candidate and retry from quiescent source bytes.",
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async function snapshotDigest(snapshotRoot) {
|
|
818
|
+
const files = [];
|
|
819
|
+
const visit = async (directory, relativeDirectory) => {
|
|
820
|
+
const directoryDetails = await lstat(directory).catch(() => undefined);
|
|
821
|
+
const directoryCanonical = await realpath(directory).catch(() => undefined);
|
|
822
|
+
if (directoryDetails === undefined ||
|
|
823
|
+
!directoryDetails.isDirectory() ||
|
|
824
|
+
directoryDetails.isSymbolicLink() ||
|
|
825
|
+
directoryCanonical === undefined ||
|
|
826
|
+
!isWithin(snapshotRoot, resolve(directoryCanonical))) {
|
|
827
|
+
throw new EvePackagingError({
|
|
828
|
+
code: "SOURCE_RACE",
|
|
829
|
+
subject: relativeDirectory || ".",
|
|
830
|
+
reason: "The immutable Eve snapshot directory escaped or changed during digest capture.",
|
|
831
|
+
remediation: "Discard the mixed-generation candidate and retry from a clean project snapshot.",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
835
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
836
|
+
for (const entry of entries) {
|
|
837
|
+
const relativePath = relativeDirectory.length === 0
|
|
838
|
+
? entry.name
|
|
839
|
+
: `${relativeDirectory}/${entry.name}`;
|
|
840
|
+
const candidate = join(directory, entry.name);
|
|
841
|
+
if (entry.isSymbolicLink()) {
|
|
842
|
+
const resolved = await realpath(candidate).catch(() => undefined);
|
|
843
|
+
if (resolved === undefined ||
|
|
844
|
+
!isWithin(snapshotRoot, resolve(resolved))) {
|
|
845
|
+
throw new EvePackagingError({
|
|
846
|
+
code: "SOURCE_RACE",
|
|
847
|
+
subject: relativePath,
|
|
848
|
+
reason: "The immutable Eve snapshot contains a symbolic link that escapes its dependency tree.",
|
|
849
|
+
remediation: "Discard the candidate and retry from a clean project snapshot.",
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
files.push({
|
|
853
|
+
relativePath,
|
|
854
|
+
sha256: `link:${await readlink(candidate)}`,
|
|
855
|
+
byteLength: 0,
|
|
856
|
+
});
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (entry.isDirectory()) {
|
|
860
|
+
await visit(candidate, relativePath);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (!entry.isFile()) {
|
|
864
|
+
throw new EvePackagingError({
|
|
865
|
+
code: "SOURCE_RACE",
|
|
866
|
+
subject: relativePath,
|
|
867
|
+
reason: "The immutable Eve snapshot contains an unsupported file type.",
|
|
868
|
+
remediation: "Discard the candidate and retry from a regular-file project tree.",
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
const stable = await readStableFile(candidate, snapshotRoot);
|
|
872
|
+
files.push({
|
|
873
|
+
relativePath,
|
|
874
|
+
sha256: stable.sha256,
|
|
875
|
+
byteLength: stable.bytes.byteLength,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
await visit(snapshotRoot, "");
|
|
880
|
+
return sha256(jsonBytes(files));
|
|
881
|
+
}
|
|
882
|
+
async function writeJson(path, value) {
|
|
883
|
+
await writeFile(path, jsonBytes(value), {
|
|
884
|
+
flag: "wx",
|
|
885
|
+
encoding: "utf8",
|
|
886
|
+
mode: 0o600,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
async function writeDockerBuildFiles(options) {
|
|
890
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(options.nodeImage.digest) ||
|
|
891
|
+
!/^node:24\.17\.0(?:-[a-z0-9][a-z0-9._-]*)?$/u.test(options.nodeImage.reference)) {
|
|
892
|
+
throw new EvePackagingError({
|
|
893
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
894
|
+
subject: "node-24-image",
|
|
895
|
+
reason: "The Linux/amd64 builder requires an immutable Node 24 image digest.",
|
|
896
|
+
remediation: "Supply a verified Node 24 image reference and sha256 digest.",
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
const image = `${options.nodeImage.reference}@${options.nodeImage.digest}`;
|
|
900
|
+
const dockerfile = `# syntax=docker/dockerfile:1
|
|
901
|
+
FROM --platform=linux/amd64 ${image} AS builder
|
|
902
|
+
WORKDIR /workspace
|
|
903
|
+
COPY package.json pnpm-lock.yaml ./
|
|
904
|
+
RUN corepack enable \\
|
|
905
|
+
&& corepack prepare pnpm@${options.packageManagerVersion} --activate \\
|
|
906
|
+
&& test "$(corepack pnpm --version)" = "${options.packageManagerVersion}" \\
|
|
907
|
+
&& test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}" \\
|
|
908
|
+
&& corepack pnpm install --frozen-lockfile --config.node-linker=hoisted
|
|
909
|
+
RUN test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}"
|
|
910
|
+
COPY . ./
|
|
911
|
+
RUN test -x ./node_modules/.bin/eve \\
|
|
912
|
+
&& ./node_modules/.bin/eve build
|
|
913
|
+
|
|
914
|
+
FROM builder AS runtime-deps
|
|
915
|
+
RUN rm -rf node_modules \\
|
|
916
|
+
&& corepack pnpm install --frozen-lockfile --prod --config.node-linker=hoisted \\
|
|
917
|
+
&& for l in node_modules/.bin/*; do t=$(readlink "$l") || continue; case "$t" in /*) ln -sfn "$(realpath --relative-to=node_modules/.bin "$t")" "$l" ;; esac; done
|
|
918
|
+
|
|
919
|
+
FROM --platform=linux/amd64 ${image} AS runtime
|
|
920
|
+
WORKDIR /app
|
|
921
|
+
ENV HOST=0.0.0.0 \\
|
|
922
|
+
NITRO_HOST=0.0.0.0 \\
|
|
923
|
+
PORT=8080 \\
|
|
924
|
+
NITRO_PORT=8080 \\
|
|
925
|
+
NODE_ENV=production
|
|
926
|
+
COPY --from=builder /workspace/.output /app/.output
|
|
927
|
+
COPY --from=runtime-deps /workspace/node_modules /app/node_modules
|
|
928
|
+
EXPOSE 8080
|
|
929
|
+
ENTRYPOINT ["./node_modules/.bin/eve", "start", "--host", "0.0.0.0", "--port", "8080"]
|
|
930
|
+
`;
|
|
931
|
+
const dockerignore = `node_modules
|
|
932
|
+
.output
|
|
933
|
+
.eden
|
|
934
|
+
.git
|
|
935
|
+
.env*
|
|
936
|
+
.npmrc
|
|
937
|
+
.yarnrc*
|
|
938
|
+
*.pem
|
|
939
|
+
*.key
|
|
940
|
+
`;
|
|
941
|
+
await writeFile(options.dockerfilePath, dockerfile, {
|
|
942
|
+
flag: "wx",
|
|
943
|
+
encoding: "utf8",
|
|
944
|
+
mode: 0o600,
|
|
945
|
+
});
|
|
946
|
+
await writeFile(join(options.snapshotRoot, ".dockerignore"), dockerignore, {
|
|
947
|
+
flag: "wx",
|
|
948
|
+
encoding: "utf8",
|
|
949
|
+
mode: 0o600,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
async function resolveProjectLocalEve(snapshotRoot) {
|
|
953
|
+
const binPath = join(snapshotRoot, "node_modules/.bin/eve");
|
|
954
|
+
const binDetails = await lstat(binPath).catch(() => undefined);
|
|
955
|
+
if (binDetails === undefined ||
|
|
956
|
+
(!binDetails.isFile() && !binDetails.isSymbolicLink())) {
|
|
957
|
+
throw new EvePackagingError({
|
|
958
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
959
|
+
subject: "node_modules/.bin/eve",
|
|
960
|
+
reason: "The installed project-local Eve executable is missing.",
|
|
961
|
+
remediation: "Declare Eve in the project dependencies and use the frozen pnpm install.",
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const resolved = await realpath(binPath).catch(() => undefined);
|
|
965
|
+
const nodeModulesRoot = resolve(join(snapshotRoot, "node_modules"));
|
|
966
|
+
if (resolved === undefined ||
|
|
967
|
+
!isWithin(nodeModulesRoot, resolve(resolved))) {
|
|
968
|
+
throw new EvePackagingError({
|
|
969
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
970
|
+
subject: "node_modules/.bin/eve",
|
|
971
|
+
reason: "The Eve executable resolves outside the isolated project dependency tree.",
|
|
972
|
+
remediation: "Remove the global or escaping Eve executable and install Eve locally.",
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
const resolvedDetails = await lstat(resolved).catch(() => undefined);
|
|
976
|
+
if (resolvedDetails === undefined ||
|
|
977
|
+
!resolvedDetails.isFile() ||
|
|
978
|
+
(resolvedDetails.mode & 0o111) === 0) {
|
|
979
|
+
throw new EvePackagingError({
|
|
980
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
981
|
+
subject: "node_modules/.bin/eve",
|
|
982
|
+
reason: "The project-local Eve executable is not a readable executable file.",
|
|
983
|
+
remediation: "Install a regular executable Eve package through the pinned lockfile and retry.",
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
let current = dirname(resolved);
|
|
987
|
+
while (isWithin(nodeModulesRoot, current) && current !== nodeModulesRoot) {
|
|
988
|
+
const packagePath = join(current, "package.json");
|
|
989
|
+
const packageDetails = await lstat(packagePath).catch(() => undefined);
|
|
990
|
+
if (packageDetails?.isFile() === true && !packageDetails.isSymbolicLink()) {
|
|
991
|
+
let packageValue;
|
|
992
|
+
try {
|
|
993
|
+
packageValue = JSON.parse((await readFile(packagePath)).toString("utf8"));
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
packageValue = undefined;
|
|
997
|
+
}
|
|
998
|
+
if (typeof packageValue === "object" &&
|
|
999
|
+
packageValue !== null &&
|
|
1000
|
+
!Array.isArray(packageValue) &&
|
|
1001
|
+
packageValue.name === "eve" &&
|
|
1002
|
+
typeof packageValue.version === "string" &&
|
|
1003
|
+
packageValue.version.length > 0) {
|
|
1004
|
+
return {
|
|
1005
|
+
version: packageValue.version,
|
|
1006
|
+
path: "node_modules/.bin/eve",
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
current = dirname(current);
|
|
1011
|
+
}
|
|
1012
|
+
const directPackagePath = await realpath(join(nodeModulesRoot, "eve", "package.json")).catch(() => undefined);
|
|
1013
|
+
if (directPackagePath !== undefined &&
|
|
1014
|
+
isWithin(nodeModulesRoot, directPackagePath)) {
|
|
1015
|
+
let directValue;
|
|
1016
|
+
try {
|
|
1017
|
+
directValue = JSON.parse((await readFile(directPackagePath)).toString("utf8"));
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
directValue = undefined;
|
|
1021
|
+
}
|
|
1022
|
+
if (typeof directValue === "object" &&
|
|
1023
|
+
directValue !== null &&
|
|
1024
|
+
!Array.isArray(directValue) &&
|
|
1025
|
+
directValue.name === "eve" &&
|
|
1026
|
+
typeof directValue.version ===
|
|
1027
|
+
"string" &&
|
|
1028
|
+
directValue.version.length > 0) {
|
|
1029
|
+
return {
|
|
1030
|
+
version: directValue.version,
|
|
1031
|
+
path: "node_modules/.bin/eve",
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
throw new EvePackagingError({
|
|
1036
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
1037
|
+
subject: "node_modules/.bin/eve",
|
|
1038
|
+
reason: "The project-local Eve executable has no readable installed package version.",
|
|
1039
|
+
remediation: "Install Eve through the pinned lockfile and retry.",
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
async function scanGeneratedOutput(snapshotRoot) {
|
|
1043
|
+
const readGeneratedFile = async (path, subject) => {
|
|
1044
|
+
try {
|
|
1045
|
+
return await readStableFile(path, snapshotRoot);
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
throw new EvePackagingError({
|
|
1049
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1050
|
+
subject,
|
|
1051
|
+
reason: "The generated Eve output is unreadable or changed during validation.",
|
|
1052
|
+
remediation: "Regenerate the Eve output inside the immutable snapshot and retry.",
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
const outputRoot = join(snapshotRoot, ".output");
|
|
1057
|
+
const outputDetails = await lstat(outputRoot).catch(() => undefined);
|
|
1058
|
+
const outputCanonical = await realpath(outputRoot).catch(() => undefined);
|
|
1059
|
+
if (outputDetails === undefined ||
|
|
1060
|
+
!outputDetails.isDirectory() ||
|
|
1061
|
+
outputDetails.isSymbolicLink() ||
|
|
1062
|
+
outputCanonical === undefined ||
|
|
1063
|
+
!isWithin(snapshotRoot, resolve(outputCanonical))) {
|
|
1064
|
+
throw new EvePackagingError({
|
|
1065
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1066
|
+
subject: ".output",
|
|
1067
|
+
reason: "The project-local Eve build output is missing or escapes the immutable snapshot.",
|
|
1068
|
+
remediation: "Regenerate .output as a regular directory inside the isolated build snapshot.",
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const entrypoint = join(snapshotRoot, EVE_ENTRYPOINT);
|
|
1072
|
+
const entryDetails = await lstat(entrypoint).catch(() => undefined);
|
|
1073
|
+
if (entryDetails === undefined ||
|
|
1074
|
+
!entryDetails.isFile() ||
|
|
1075
|
+
entryDetails.isSymbolicLink()) {
|
|
1076
|
+
throw new EvePackagingError({
|
|
1077
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1078
|
+
subject: EVE_ENTRYPOINT,
|
|
1079
|
+
reason: "The project-local Eve build did not produce a regular Nitro server entrypoint.",
|
|
1080
|
+
remediation: "Fix the project-local Eve build so .output/server/index.mjs is generated inside the snapshot.",
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
const files = [];
|
|
1084
|
+
const visit = async (directory, relativeDirectory) => {
|
|
1085
|
+
const directoryDetails = await lstat(directory).catch(() => undefined);
|
|
1086
|
+
const directoryCanonical = await realpath(directory).catch(() => undefined);
|
|
1087
|
+
if (directoryDetails === undefined ||
|
|
1088
|
+
!directoryDetails.isDirectory() ||
|
|
1089
|
+
directoryDetails.isSymbolicLink() ||
|
|
1090
|
+
directoryCanonical === undefined ||
|
|
1091
|
+
!isWithin(snapshotRoot, resolve(directoryCanonical))) {
|
|
1092
|
+
throw new EvePackagingError({
|
|
1093
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1094
|
+
subject: `.output/${relativeDirectory}`,
|
|
1095
|
+
reason: "Generated Eve output escaped or changed during validation.",
|
|
1096
|
+
remediation: "Regenerate output as regular files inside the immutable build snapshot.",
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1100
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1101
|
+
for (const entry of entries) {
|
|
1102
|
+
const relativePath = relativeDirectory.length === 0
|
|
1103
|
+
? entry.name
|
|
1104
|
+
: `${relativeDirectory}/${entry.name}`;
|
|
1105
|
+
const candidate = join(directory, entry.name);
|
|
1106
|
+
if (entry.isSymbolicLink()) {
|
|
1107
|
+
throw new EvePackagingError({
|
|
1108
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1109
|
+
subject: `.output/${relativePath}`,
|
|
1110
|
+
reason: "Generated Eve output contains a symbolic-link escape or unsupported link.",
|
|
1111
|
+
remediation: "Regenerate output as regular files inside the immutable build snapshot.",
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
if (entry.isDirectory()) {
|
|
1115
|
+
await visit(candidate, relativePath);
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (!entry.isFile()) {
|
|
1119
|
+
throw new EvePackagingError({
|
|
1120
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1121
|
+
subject: `.output/${relativePath}`,
|
|
1122
|
+
reason: "Generated Eve output contains an unsupported file type.",
|
|
1123
|
+
remediation: "Remove special files from the Eve build output and retry.",
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
const stable = await readGeneratedFile(candidate, `.output/${relativePath}`);
|
|
1127
|
+
files.push({
|
|
1128
|
+
relativePath: `.output/${relativePath}`,
|
|
1129
|
+
sha256: stable.sha256,
|
|
1130
|
+
byteLength: stable.bytes.byteLength,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
await visit(outputRoot, "");
|
|
1135
|
+
const entryStable = await readGeneratedFile(entrypoint, EVE_ENTRYPOINT);
|
|
1136
|
+
if (entryStable.bytes.byteLength === 0) {
|
|
1137
|
+
throw new EvePackagingError({
|
|
1138
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1139
|
+
subject: EVE_ENTRYPOINT,
|
|
1140
|
+
reason: "The project-local Eve build produced an empty Nitro server entrypoint.",
|
|
1141
|
+
remediation: "Regenerate .output/server/index.mjs from the project-local Eve build.",
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
await execFileAsync(process.execPath, ["--check", entrypoint], {
|
|
1146
|
+
cwd: snapshotRoot,
|
|
1147
|
+
env: {
|
|
1148
|
+
PATH: process.env.PATH,
|
|
1149
|
+
NODE_PATH: undefined,
|
|
1150
|
+
NODE_OPTIONS: undefined,
|
|
1151
|
+
},
|
|
1152
|
+
maxBuffer: 1024 * 1024,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
throw new EvePackagingError({
|
|
1157
|
+
code: "UNSUPPORTED_EVE_OUTPUT",
|
|
1158
|
+
subject: EVE_ENTRYPOINT,
|
|
1159
|
+
reason: "The generated Nitro server entrypoint is not valid JavaScript.",
|
|
1160
|
+
remediation: "Fix the project-local Eve build so .output/server/index.mjs passes Node syntax validation.",
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
entrypointPath: EVE_ENTRYPOINT,
|
|
1165
|
+
sha256: entryStable.sha256,
|
|
1166
|
+
regularFile: true,
|
|
1167
|
+
symlinkEscape: false,
|
|
1168
|
+
outputDigest: sha256(jsonBytes(files)),
|
|
1169
|
+
fileCount: files.length,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async function assertGeneratedTreesExcludeSecrets(snapshotRoot) {
|
|
1173
|
+
const roots = [
|
|
1174
|
+
join(snapshotRoot, "node_modules"),
|
|
1175
|
+
join(snapshotRoot, ".output"),
|
|
1176
|
+
];
|
|
1177
|
+
const visit = async (directory, relativeDirectory) => {
|
|
1178
|
+
const directoryDetails = await lstat(directory).catch(() => undefined);
|
|
1179
|
+
const directoryCanonical = await realpath(directory).catch(() => undefined);
|
|
1180
|
+
if (directoryDetails === undefined ||
|
|
1181
|
+
!directoryDetails.isDirectory() ||
|
|
1182
|
+
directoryDetails.isSymbolicLink() ||
|
|
1183
|
+
directoryCanonical === undefined ||
|
|
1184
|
+
!isWithin(snapshotRoot, resolve(directoryCanonical))) {
|
|
1185
|
+
throw new EvePackagingError({
|
|
1186
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1187
|
+
subject: relativeDirectory,
|
|
1188
|
+
reason: "The generated Eve runtime closure escaped or changed during secret validation.",
|
|
1189
|
+
remediation: "Regenerate dependencies and output inside the immutable project snapshot.",
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1193
|
+
for (const entry of entries) {
|
|
1194
|
+
const relativePath = relativeDirectory.length === 0
|
|
1195
|
+
? entry.name
|
|
1196
|
+
: `${relativeDirectory}/${entry.name}`;
|
|
1197
|
+
const candidate = join(directory, entry.name);
|
|
1198
|
+
if (entry.name.startsWith(".env") ||
|
|
1199
|
+
EVE_EXCLUDED_FILE_NAMES.has(entry.name) ||
|
|
1200
|
+
entry.name.endsWith(".pem") ||
|
|
1201
|
+
entry.name.endsWith(".key")) {
|
|
1202
|
+
throw new EvePackagingError({
|
|
1203
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1204
|
+
subject: relativePath,
|
|
1205
|
+
reason: "The generated Eve runtime closure contains a credential or environment file.",
|
|
1206
|
+
remediation: "Remove credential material from the project dependency/output closure and retry.",
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (entry.isSymbolicLink()) {
|
|
1210
|
+
const resolved = await realpath(candidate).catch(() => undefined);
|
|
1211
|
+
if (resolved === undefined ||
|
|
1212
|
+
!isWithin(snapshotRoot, resolve(resolved))) {
|
|
1213
|
+
throw new EvePackagingError({
|
|
1214
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1215
|
+
subject: relativePath,
|
|
1216
|
+
reason: "The generated Eve runtime closure contains an escaping symbolic link.",
|
|
1217
|
+
remediation: "Regenerate dependencies and output without links outside the immutable snapshot.",
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (entry.isDirectory()) {
|
|
1223
|
+
await visit(candidate, relativePath);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
for (const root of roots) {
|
|
1228
|
+
const details = await lstat(root).catch(() => undefined);
|
|
1229
|
+
if (details?.isSymbolicLink()) {
|
|
1230
|
+
throw new EvePackagingError({
|
|
1231
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1232
|
+
subject: relative(snapshotRoot, root),
|
|
1233
|
+
reason: "The generated Eve runtime closure root is a symbolic link.",
|
|
1234
|
+
remediation: "Regenerate dependencies and output as regular directories inside the immutable snapshot.",
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
if (details?.isDirectory() === true) {
|
|
1238
|
+
const canonical = await realpath(root).catch(() => undefined);
|
|
1239
|
+
if (canonical === undefined ||
|
|
1240
|
+
!isWithin(snapshotRoot, resolve(canonical))) {
|
|
1241
|
+
throw new EvePackagingError({
|
|
1242
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1243
|
+
subject: relative(snapshotRoot, root),
|
|
1244
|
+
reason: "The generated Eve runtime closure root escapes the immutable snapshot.",
|
|
1245
|
+
remediation: "Regenerate dependencies and output inside the isolated project snapshot.",
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
await visit(root, relative(root, root));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function safeError(error) {
|
|
1253
|
+
return {
|
|
1254
|
+
code: error.code,
|
|
1255
|
+
subject: error.subject,
|
|
1256
|
+
reason: error.message,
|
|
1257
|
+
remediation: error.remediation,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function classifyDockerBuildFailure(error) {
|
|
1261
|
+
const stderr = typeof error === "object" &&
|
|
1262
|
+
error !== null &&
|
|
1263
|
+
"stderr" in error &&
|
|
1264
|
+
typeof error.stderr === "string"
|
|
1265
|
+
? error.stderr
|
|
1266
|
+
: "";
|
|
1267
|
+
if (/ERR_PNPM_(?:OUTDATED_LOCKFILE|LOCKFILE_MISSING_DEPENDENCY)/u.test(stderr) ||
|
|
1268
|
+
/frozen-lockfile|pnpm --version/u.test(stderr)) {
|
|
1269
|
+
return new EvePackagingError({
|
|
1270
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
1271
|
+
subject: "frozen pnpm install",
|
|
1272
|
+
reason: "The isolated pinned pnpm install could not complete without changing or bypassing the lockfile.",
|
|
1273
|
+
remediation: "Regenerate pnpm-lock.yaml with the declared pnpm version and retry without relaxing frozen mode.",
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (/node_modules\/\.bin\/eve|test -x/u.test(stderr)) {
|
|
1277
|
+
return new EvePackagingError({
|
|
1278
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
1279
|
+
subject: "node_modules/.bin/eve",
|
|
1280
|
+
reason: "The isolated build could not resolve the project-local Eve executable.",
|
|
1281
|
+
remediation: "Declare Eve as a project dependency and retry with the frozen lockfile.",
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
if (/eve build/u.test(stderr)) {
|
|
1285
|
+
return new EvePackagingError({
|
|
1286
|
+
code: "EVE_BUILD_FAILED",
|
|
1287
|
+
subject: "eve build",
|
|
1288
|
+
reason: "The project-local Eve build failed inside the isolated Linux/amd64 builder.",
|
|
1289
|
+
remediation: "Fix the project-local Eve build and retry without changing the authored project during packaging.",
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
return new EvePackagingError({
|
|
1293
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
1294
|
+
subject: "Docker/OrbStack",
|
|
1295
|
+
reason: "The isolated Linux/amd64 Eve builder was unavailable or failed before a verified image was produced.",
|
|
1296
|
+
remediation: "Start Docker/OrbStack with BuildKit and retry without changing the project inputs.",
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
function blockedResult(error, extra = {}) {
|
|
1300
|
+
return {
|
|
1301
|
+
schemaVersion: 1,
|
|
1302
|
+
worker: "eve-packaging-worker",
|
|
1303
|
+
operation: "local-package",
|
|
1304
|
+
status: "blocked",
|
|
1305
|
+
returnCode: error.code,
|
|
1306
|
+
deployable: false,
|
|
1307
|
+
project: extra.project ?? null,
|
|
1308
|
+
candidate: null,
|
|
1309
|
+
snapshot: null,
|
|
1310
|
+
toolchain: null,
|
|
1311
|
+
image: null,
|
|
1312
|
+
runtime: null,
|
|
1313
|
+
secrets: {
|
|
1314
|
+
runtimeVariableNames: [],
|
|
1315
|
+
valuesRecorded: false,
|
|
1316
|
+
excludedFromSnapshot: true,
|
|
1317
|
+
excludedFromBuildEnvironment: true,
|
|
1318
|
+
excludedFromDockerContext: true,
|
|
1319
|
+
excludedFromImage: true,
|
|
1320
|
+
excludedFromHistory: true,
|
|
1321
|
+
excludedFromManifestsAndLogs: true,
|
|
1322
|
+
redactionRegisteredBeforeChildren: false,
|
|
1323
|
+
},
|
|
1324
|
+
checks: extra.checks ?? [{
|
|
1325
|
+
id: error.code,
|
|
1326
|
+
status: "blocked",
|
|
1327
|
+
subject: error.subject,
|
|
1328
|
+
reason: error.message,
|
|
1329
|
+
remediation: error.remediation,
|
|
1330
|
+
}],
|
|
1331
|
+
candidateImageId: null,
|
|
1332
|
+
candidateImageRetainedLocally: false,
|
|
1333
|
+
writtenPaths: extra.writtenPaths ?? [],
|
|
1334
|
+
error: safeError(error),
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
async function currentInputIdentity(runtimeConfig) {
|
|
1338
|
+
const token = (value) => {
|
|
1339
|
+
if (typeof value === "string")
|
|
1340
|
+
return value;
|
|
1341
|
+
return value?.token;
|
|
1342
|
+
};
|
|
1343
|
+
if (runtimeConfig?.readInputIdentity !== undefined) {
|
|
1344
|
+
return token(await runtimeConfig.readInputIdentity());
|
|
1345
|
+
}
|
|
1346
|
+
return token(runtimeConfig?.inputIdentity);
|
|
1347
|
+
}
|
|
1348
|
+
async function sourceRaceErrorIfChanged(root, initial, initialInputIdentity, runtimeConfig, artifactRoot, requestedRoot) {
|
|
1349
|
+
const requestedDetails = await lstat(requestedRoot).catch(() => undefined);
|
|
1350
|
+
const requestedCanonical = await realpath(requestedRoot).catch(() => undefined);
|
|
1351
|
+
if (requestedDetails === undefined ||
|
|
1352
|
+
!requestedDetails.isDirectory() ||
|
|
1353
|
+
requestedDetails.isSymbolicLink() ||
|
|
1354
|
+
requestedCanonical !== root) {
|
|
1355
|
+
return new EvePackagingError({
|
|
1356
|
+
code: "SOURCE_RACE",
|
|
1357
|
+
subject: "project root",
|
|
1358
|
+
reason: "The explicitly selected Eve project root changed during packaging.",
|
|
1359
|
+
remediation: "Retry only after the selected canonical project root is quiescent.",
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
let latestIdentity;
|
|
1363
|
+
try {
|
|
1364
|
+
latestIdentity = await currentInputIdentity(runtimeConfig);
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
return new EvePackagingError({
|
|
1368
|
+
code: "SOURCE_RACE",
|
|
1369
|
+
subject: "runtime configuration identity",
|
|
1370
|
+
reason: "The explicit environment identity could not be revalidated safely.",
|
|
1371
|
+
remediation: "Retry after the deployment-safety runtime-config seam is stable.",
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
let latest;
|
|
1375
|
+
try {
|
|
1376
|
+
latest = await captureInputs(root, runtimeConfig, artifactRoot, latestIdentity, requestedRoot);
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
if (error instanceof EvePackagingError) {
|
|
1380
|
+
return new EvePackagingError({
|
|
1381
|
+
code: "SOURCE_RACE",
|
|
1382
|
+
subject: error.subject,
|
|
1383
|
+
reason: "The selected Eve inputs changed or became unsafe during revalidation.",
|
|
1384
|
+
remediation: "Retry only after the selected project and its lockfile are quiescent.",
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return new EvePackagingError({
|
|
1388
|
+
code: "SOURCE_RACE",
|
|
1389
|
+
subject: "project inputs",
|
|
1390
|
+
reason: "The selected Eve inputs could not be revalidated safely.",
|
|
1391
|
+
remediation: "Retry only after the selected project and its lockfile are quiescent.",
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
if (initialInputIdentity !== latestIdentity ||
|
|
1395
|
+
!sourceInputsEqual(initial, latest)) {
|
|
1396
|
+
return new EvePackagingError({
|
|
1397
|
+
code: "SOURCE_RACE",
|
|
1398
|
+
subject: "project inputs",
|
|
1399
|
+
reason: "The selected Eve source, lockfile, configuration, or explicit environment identity changed during packaging.",
|
|
1400
|
+
remediation: "Retry only after all selected inputs are quiescent; the mixed-generation candidate was discarded.",
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return undefined;
|
|
1404
|
+
}
|
|
1405
|
+
export async function buildEveProjectSnapshot(options) {
|
|
1406
|
+
let roots;
|
|
1407
|
+
let initialInputs;
|
|
1408
|
+
let contract;
|
|
1409
|
+
let initialRuntimeInputIdentity;
|
|
1410
|
+
try {
|
|
1411
|
+
roots = await assertCanonicalRoot(options.projectRoot);
|
|
1412
|
+
await assertRequiredRootInputs(roots.canonicalRoot);
|
|
1413
|
+
if (options.runtimeConfig !== undefined &&
|
|
1414
|
+
options.runtimeConfig.redactionRegistered !== true) {
|
|
1415
|
+
throw new EvePackagingError({
|
|
1416
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1417
|
+
subject: "runtime configuration",
|
|
1418
|
+
reason: "Runtime configuration redaction was not registered before the isolated builder would start.",
|
|
1419
|
+
remediation: "Register the deployment-safety redaction handle before packaging and retry.",
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
if (options.runtimeConfig?.envFilePath !== undefined &&
|
|
1423
|
+
options.runtimeConfig.inputIdentity === undefined &&
|
|
1424
|
+
options.runtimeConfig.readInputIdentity === undefined) {
|
|
1425
|
+
throw new EvePackagingError({
|
|
1426
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1427
|
+
subject: "runtime configuration identity",
|
|
1428
|
+
reason: "An explicit environment file was supplied without a deployment-safety input identity.",
|
|
1429
|
+
remediation: "Pass the validated redacted environment identity from deployment-safety before packaging.",
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
const runtimeVariableNames = options.runtimeConfig?.variableNames ?? [];
|
|
1433
|
+
if (runtimeVariableNames.some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) ||
|
|
1434
|
+
new Set(runtimeVariableNames).size !== runtimeVariableNames.length) {
|
|
1435
|
+
throw new EvePackagingError({
|
|
1436
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1437
|
+
subject: "runtime variable names",
|
|
1438
|
+
reason: "The runtime-config seam returned an invalid or duplicate variable name.",
|
|
1439
|
+
remediation: "Return only validated non-secret environment variable names and retry.",
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
initialRuntimeInputIdentity = await currentInputIdentity(options.runtimeConfig);
|
|
1443
|
+
if (options.runtimeConfig?.envFilePath !== undefined &&
|
|
1444
|
+
initialRuntimeInputIdentity === undefined) {
|
|
1445
|
+
throw new EvePackagingError({
|
|
1446
|
+
code: "SECRET_EXCLUSION_FAILED",
|
|
1447
|
+
subject: "runtime configuration identity",
|
|
1448
|
+
reason: "The explicit environment file identity was unavailable.",
|
|
1449
|
+
remediation: "Revalidate the redacted environment input through deployment-safety before packaging.",
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
const artifactRoot = resolve(options.artifactRoot);
|
|
1453
|
+
initialInputs = await captureInputs(roots.canonicalRoot, options.runtimeConfig, artifactRoot, initialRuntimeInputIdentity, roots.requestedRoot);
|
|
1454
|
+
contract = validateProjectContract(initialInputs);
|
|
1455
|
+
}
|
|
1456
|
+
catch (error) {
|
|
1457
|
+
if (error instanceof EvePackagingError)
|
|
1458
|
+
return blockedResult(error);
|
|
1459
|
+
return blockedResult(new EvePackagingError({
|
|
1460
|
+
code: "ROOT_INVALID",
|
|
1461
|
+
subject: "project",
|
|
1462
|
+
reason: "The Eve project could not be inspected safely.",
|
|
1463
|
+
remediation: "Retry with a readable canonical project root.",
|
|
1464
|
+
}));
|
|
1465
|
+
}
|
|
1466
|
+
let generationRoot;
|
|
1467
|
+
try {
|
|
1468
|
+
generationRoot = await createGenerationRoot(options.artifactRoot, roots.canonicalRoot);
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
if (error instanceof EvePackagingError)
|
|
1472
|
+
return blockedResult(error);
|
|
1473
|
+
return blockedResult(new EvePackagingError({
|
|
1474
|
+
code: "ROOT_INVALID",
|
|
1475
|
+
subject: "artifactRoot",
|
|
1476
|
+
reason: "The Eden-owned generation directory could not be prepared safely.",
|
|
1477
|
+
remediation: "Use a new writable generation directory.",
|
|
1478
|
+
}));
|
|
1479
|
+
}
|
|
1480
|
+
const snapshotRoot = join(generationRoot, "container/snapshot");
|
|
1481
|
+
const inputManifestPath = join(generationRoot, "input-manifest.json");
|
|
1482
|
+
const dockerfilePath = join(generationRoot, "container/Dockerfile");
|
|
1483
|
+
const writtenPaths = [
|
|
1484
|
+
generationRoot,
|
|
1485
|
+
inputManifestPath,
|
|
1486
|
+
snapshotRoot,
|
|
1487
|
+
];
|
|
1488
|
+
const runtimeVariableNames = [...(options.runtimeConfig?.variableNames ?? [])]
|
|
1489
|
+
.sort();
|
|
1490
|
+
let buildResult;
|
|
1491
|
+
const inputManifest = {
|
|
1492
|
+
version: 1,
|
|
1493
|
+
requestedRoot: roots.requestedRoot,
|
|
1494
|
+
canonicalRoot: roots.canonicalRoot,
|
|
1495
|
+
projectId: contract.projectId,
|
|
1496
|
+
packageManager: "pnpm",
|
|
1497
|
+
packageManagerVersion: contract.packageManagerVersion,
|
|
1498
|
+
packageJsonSha256: contract.packageJsonSha256,
|
|
1499
|
+
lockfileSha256: contract.lockfileSha256,
|
|
1500
|
+
sourceDigest: initialInputs.sourceDigest,
|
|
1501
|
+
files: initialInputs.files.map((file) => ({
|
|
1502
|
+
relativePath: file.relativePath,
|
|
1503
|
+
sha256: file.sha256,
|
|
1504
|
+
byteLength: file.byteLength,
|
|
1505
|
+
mode: file.mode,
|
|
1506
|
+
})),
|
|
1507
|
+
excludedRelativePaths: initialInputs.excludedRelativePaths,
|
|
1508
|
+
...(initialRuntimeInputIdentity === undefined
|
|
1509
|
+
? {}
|
|
1510
|
+
: { runtimeConfigInputIdentity: initialRuntimeInputIdentity }),
|
|
1511
|
+
runtimeVariableNames,
|
|
1512
|
+
};
|
|
1513
|
+
const discardBuiltCandidate = async () => {
|
|
1514
|
+
if (buildResult === undefined || options.builder.discard === undefined) {
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
await options.builder.discard(buildResult);
|
|
1519
|
+
}
|
|
1520
|
+
catch {
|
|
1521
|
+
return new EvePackagingError({
|
|
1522
|
+
code: "CLEANUP_UNVERIFIED",
|
|
1523
|
+
subject: "candidate image",
|
|
1524
|
+
reason: "The failed Eve candidate could not be removed with exact ownership proof.",
|
|
1525
|
+
remediation: "Inspect the recorded candidate image identity before retrying; no broad Docker cleanup was attempted.",
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
buildResult = undefined;
|
|
1529
|
+
return undefined;
|
|
1530
|
+
};
|
|
1531
|
+
try {
|
|
1532
|
+
await mkdir(dirname(inputManifestPath), { recursive: true });
|
|
1533
|
+
await mkdir(snapshotRoot, { recursive: true });
|
|
1534
|
+
await writeJson(inputManifestPath, inputManifest);
|
|
1535
|
+
await copySnapshot(snapshotRoot, initialInputs);
|
|
1536
|
+
const afterCopyRace = await sourceRaceErrorIfChanged(roots.canonicalRoot, initialInputs, initialRuntimeInputIdentity, options.runtimeConfig, resolve(options.artifactRoot), roots.requestedRoot);
|
|
1537
|
+
if (afterCopyRace !== undefined)
|
|
1538
|
+
return blockedResult(afterCopyRace, { writtenPaths });
|
|
1539
|
+
const nodeImage = options.nodeImage ?? options.builder.nodeImage;
|
|
1540
|
+
let actualDockerfilePath;
|
|
1541
|
+
if (nodeImage !== undefined) {
|
|
1542
|
+
await writeDockerBuildFiles({
|
|
1543
|
+
snapshotRoot,
|
|
1544
|
+
dockerfilePath,
|
|
1545
|
+
nodeImage,
|
|
1546
|
+
packageManagerVersion: contract.packageManagerVersion,
|
|
1547
|
+
lockfileSha256: contract.lockfileSha256,
|
|
1548
|
+
});
|
|
1549
|
+
actualDockerfilePath = dockerfilePath;
|
|
1550
|
+
writtenPaths.push(dockerfilePath, join(snapshotRoot, ".dockerignore"));
|
|
1551
|
+
}
|
|
1552
|
+
buildResult = await options.builder.build({
|
|
1553
|
+
generationRoot,
|
|
1554
|
+
snapshotRoot,
|
|
1555
|
+
inputManifestPath,
|
|
1556
|
+
dockerfilePath: actualDockerfilePath,
|
|
1557
|
+
packageManagerVersion: contract.packageManagerVersion,
|
|
1558
|
+
installCommand: INSTALL_COMMAND,
|
|
1559
|
+
buildCommand: BUILD_COMMAND,
|
|
1560
|
+
platform: "linux/amd64",
|
|
1561
|
+
sourceDigest: initialInputs.sourceDigest,
|
|
1562
|
+
buildContext: "immutable-snapshot",
|
|
1563
|
+
});
|
|
1564
|
+
if (buildResult.imageId !== undefined &&
|
|
1565
|
+
buildResult.imagePlatform !== "linux/amd64") {
|
|
1566
|
+
throw new EvePackagingError({
|
|
1567
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
1568
|
+
subject: "linux/amd64 image",
|
|
1569
|
+
reason: "The builder returned an image without verified linux/amd64 metadata.",
|
|
1570
|
+
remediation: "Inspect the final image and return its verified Linux/amd64 identity.",
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
try {
|
|
1574
|
+
await verifySnapshotInputs(snapshotRoot, initialInputs);
|
|
1575
|
+
}
|
|
1576
|
+
catch (error) {
|
|
1577
|
+
const cleanupError = await discardBuiltCandidate();
|
|
1578
|
+
if (cleanupError !== undefined)
|
|
1579
|
+
return blockedResult(cleanupError, { writtenPaths });
|
|
1580
|
+
if (error instanceof EvePackagingError) {
|
|
1581
|
+
return blockedResult(error, { writtenPaths });
|
|
1582
|
+
}
|
|
1583
|
+
throw error;
|
|
1584
|
+
}
|
|
1585
|
+
const afterBuildRace = await sourceRaceErrorIfChanged(roots.canonicalRoot, initialInputs, initialRuntimeInputIdentity, options.runtimeConfig, resolve(options.artifactRoot), roots.requestedRoot);
|
|
1586
|
+
if (afterBuildRace !== undefined) {
|
|
1587
|
+
const cleanupError = await discardBuiltCandidate();
|
|
1588
|
+
return blockedResult(cleanupError ?? afterBuildRace, { writtenPaths });
|
|
1589
|
+
}
|
|
1590
|
+
const snapshotLockfile = await readStableFile(join(snapshotRoot, "pnpm-lock.yaml"), snapshotRoot);
|
|
1591
|
+
if (snapshotLockfile.sha256 !== contract.lockfileSha256 ||
|
|
1592
|
+
!snapshotLockfile.bytes.equals(contract.lockfileBytes)) {
|
|
1593
|
+
const dependencyError = new EvePackagingError({
|
|
1594
|
+
code: "DEPENDENCY_AMBIGUITY",
|
|
1595
|
+
subject: "pnpm-lock.yaml",
|
|
1596
|
+
reason: "The frozen install changed the captured lockfile bytes.",
|
|
1597
|
+
remediation: "Use the exact declared pnpm version with pnpm install --frozen-lockfile.",
|
|
1598
|
+
});
|
|
1599
|
+
const cleanupError = await discardBuiltCandidate();
|
|
1600
|
+
return blockedResult(cleanupError ?? dependencyError, { writtenPaths });
|
|
1601
|
+
}
|
|
1602
|
+
const eve = await resolveProjectLocalEve(snapshotRoot);
|
|
1603
|
+
const output = await scanGeneratedOutput(snapshotRoot);
|
|
1604
|
+
await assertGeneratedTreesExcludeSecrets(snapshotRoot);
|
|
1605
|
+
const snapshotSourceDigest = await snapshotDigest(snapshotRoot);
|
|
1606
|
+
const runtimeManifestPath = join(generationRoot, "runtime-manifest.json");
|
|
1607
|
+
await writeJson(runtimeManifestPath, {
|
|
1608
|
+
version: 1,
|
|
1609
|
+
generationId: basename(generationRoot),
|
|
1610
|
+
packageManager: "pnpm",
|
|
1611
|
+
packageManagerVersion: contract.packageManagerVersion,
|
|
1612
|
+
installCommand: INSTALL_COMMAND,
|
|
1613
|
+
buildCommand: BUILD_COMMAND,
|
|
1614
|
+
startCommand: START_COMMAND,
|
|
1615
|
+
sourceDigest: initialInputs.sourceDigest,
|
|
1616
|
+
snapshotDigest: snapshotSourceDigest,
|
|
1617
|
+
generatedOutput: output,
|
|
1618
|
+
artifactPath: EVE_ENTRYPOINT,
|
|
1619
|
+
platform: "linux/amd64",
|
|
1620
|
+
runtimeVariableNames,
|
|
1621
|
+
});
|
|
1622
|
+
writtenPaths.push(runtimeManifestPath);
|
|
1623
|
+
let image = null;
|
|
1624
|
+
if (buildResult.imageId !== undefined) {
|
|
1625
|
+
image = {
|
|
1626
|
+
dockerfilePath: actualDockerfilePath ?? null,
|
|
1627
|
+
platform: "linux/amd64",
|
|
1628
|
+
builderImage: nodeImage === undefined
|
|
1629
|
+
? null
|
|
1630
|
+
: `${nodeImage.reference}@${nodeImage.digest}`,
|
|
1631
|
+
runtimeImage: nodeImage === undefined
|
|
1632
|
+
? null
|
|
1633
|
+
: `${nodeImage.reference}@${nodeImage.digest}`,
|
|
1634
|
+
imageId: buildResult.imageId,
|
|
1635
|
+
imageReference: buildResult.imageReference ?? null,
|
|
1636
|
+
imageDigest: buildResult.imageDigest ?? buildResult.imageId,
|
|
1637
|
+
launchCommand: START_COMMAND,
|
|
1638
|
+
workingDirectory: "/app",
|
|
1639
|
+
hostEnvironment: HOST_ENVIRONMENT,
|
|
1640
|
+
generatedOutput: output,
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
const snapshot = {
|
|
1644
|
+
generationId: basename(generationRoot),
|
|
1645
|
+
path: snapshotRoot,
|
|
1646
|
+
sha256: snapshotSourceDigest,
|
|
1647
|
+
includedFileCount: initialInputs.files.length,
|
|
1648
|
+
excludedCategories: [...new Set(initialInputs.excludedRelativePaths.map((path) => isPathExcluded(path, undefined).category ?? "generated-state"))].sort(),
|
|
1649
|
+
sourceRaceChecked: true,
|
|
1650
|
+
};
|
|
1651
|
+
if ((buildResult.imageId !== undefined &&
|
|
1652
|
+
!/^sha256:[0-9a-f]{64}$/u.test(buildResult.imageId)) ||
|
|
1653
|
+
(buildResult.imageId !== undefined &&
|
|
1654
|
+
buildResult.imagePlatform !== "linux/amd64")) {
|
|
1655
|
+
const imageError = new EvePackagingError({
|
|
1656
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
1657
|
+
subject: "linux/amd64 image",
|
|
1658
|
+
reason: "The local Eve image metadata was present but did not include a verified Linux/amd64 identity.",
|
|
1659
|
+
remediation: "Inspect the image identity or return the build candidate to the image-runtime worker.",
|
|
1660
|
+
});
|
|
1661
|
+
const cleanupError = await discardBuiltCandidate();
|
|
1662
|
+
return blockedResult(cleanupError ?? imageError, { writtenPaths });
|
|
1663
|
+
}
|
|
1664
|
+
const project = {
|
|
1665
|
+
requestedRoot: roots.requestedRoot,
|
|
1666
|
+
canonicalRoot: roots.canonicalRoot,
|
|
1667
|
+
projectId: contract.projectId,
|
|
1668
|
+
packageJson: {
|
|
1669
|
+
path: "package.json",
|
|
1670
|
+
sha256: contract.packageJsonSha256,
|
|
1671
|
+
},
|
|
1672
|
+
lockfile: {
|
|
1673
|
+
path: "pnpm-lock.yaml",
|
|
1674
|
+
sha256: contract.lockfileSha256,
|
|
1675
|
+
},
|
|
1676
|
+
sourceDigest: initialInputs.sourceDigest,
|
|
1677
|
+
inputManifestPath,
|
|
1678
|
+
};
|
|
1679
|
+
const candidate = {
|
|
1680
|
+
generationId: basename(generationRoot),
|
|
1681
|
+
generationRoot,
|
|
1682
|
+
snapshotRoot,
|
|
1683
|
+
inputManifestPath,
|
|
1684
|
+
packageManager: "pnpm",
|
|
1685
|
+
packageManagerVersion: contract.packageManagerVersion,
|
|
1686
|
+
installCommand: INSTALL_COMMAND,
|
|
1687
|
+
buildCommand: BUILD_COMMAND,
|
|
1688
|
+
eveExecutable: "node_modules/.bin/eve",
|
|
1689
|
+
eveVersion: eve.version,
|
|
1690
|
+
packageJsonSha256: contract.packageJsonSha256,
|
|
1691
|
+
lockfileSha256: contract.lockfileSha256,
|
|
1692
|
+
sourceDigest: initialInputs.sourceDigest,
|
|
1693
|
+
snapshotDigest: snapshotSourceDigest,
|
|
1694
|
+
generatedOutput: output,
|
|
1695
|
+
...(initialRuntimeInputIdentity === undefined
|
|
1696
|
+
? {}
|
|
1697
|
+
: { runtimeConfigInputIdentity: initialRuntimeInputIdentity }),
|
|
1698
|
+
runtimeVariableNames,
|
|
1699
|
+
};
|
|
1700
|
+
const handoffRace = await sourceRaceErrorIfChanged(roots.canonicalRoot, initialInputs, initialRuntimeInputIdentity, options.runtimeConfig, resolve(options.artifactRoot), roots.requestedRoot);
|
|
1701
|
+
if (handoffRace !== undefined) {
|
|
1702
|
+
const cleanupError = await discardBuiltCandidate();
|
|
1703
|
+
return blockedResult(cleanupError ?? handoffRace, { writtenPaths });
|
|
1704
|
+
}
|
|
1705
|
+
return {
|
|
1706
|
+
schemaVersion: 1,
|
|
1707
|
+
worker: "eve-packaging-worker",
|
|
1708
|
+
operation: "local-package",
|
|
1709
|
+
status: "ready",
|
|
1710
|
+
returnCode: "EVE_PACKAGE_READY",
|
|
1711
|
+
deployable: true,
|
|
1712
|
+
project,
|
|
1713
|
+
snapshot,
|
|
1714
|
+
toolchain: {
|
|
1715
|
+
nodeVersion: "24.17.0",
|
|
1716
|
+
packageManager: "pnpm",
|
|
1717
|
+
packageManagerVersion: contract.packageManagerVersion,
|
|
1718
|
+
installCommand: INSTALL_COMMAND,
|
|
1719
|
+
buildCommand: BUILD_COMMAND,
|
|
1720
|
+
startCommand: START_COMMAND,
|
|
1721
|
+
eveExecutable: eve.path,
|
|
1722
|
+
eveVersion: eve.version,
|
|
1723
|
+
lockfileUnchanged: true,
|
|
1724
|
+
nativeBuildPlatform: "linux/amd64",
|
|
1725
|
+
},
|
|
1726
|
+
image,
|
|
1727
|
+
candidate,
|
|
1728
|
+
runtime: null,
|
|
1729
|
+
secrets: {
|
|
1730
|
+
runtimeVariableNames,
|
|
1731
|
+
valuesRecorded: false,
|
|
1732
|
+
excludedFromSnapshot: true,
|
|
1733
|
+
excludedFromBuildEnvironment: true,
|
|
1734
|
+
excludedFromDockerContext: true,
|
|
1735
|
+
excludedFromImage: true,
|
|
1736
|
+
excludedFromHistory: true,
|
|
1737
|
+
excludedFromManifestsAndLogs: true,
|
|
1738
|
+
redactionRegisteredBeforeChildren: options.runtimeConfig?.redactionRegistered ?? false,
|
|
1739
|
+
},
|
|
1740
|
+
checks: [
|
|
1741
|
+
{
|
|
1742
|
+
id: "VAL-CLI-004",
|
|
1743
|
+
status: "pass",
|
|
1744
|
+
subject: "project-root",
|
|
1745
|
+
reason: "The explicit project root is canonical and readable.",
|
|
1746
|
+
remediation: null,
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
id: "VAL-BUILD-001",
|
|
1750
|
+
status: "pass",
|
|
1751
|
+
subject: "package-manager",
|
|
1752
|
+
reason: "The exact pnpm pin and one matching regular root lockfile were verified.",
|
|
1753
|
+
remediation: null,
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
id: "VAL-BUILD-002",
|
|
1757
|
+
status: "pass",
|
|
1758
|
+
subject: "frozen-install",
|
|
1759
|
+
reason: "The isolated builder was given the exact frozen install command.",
|
|
1760
|
+
remediation: null,
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
id: "VAL-BUILD-003",
|
|
1764
|
+
status: "pass",
|
|
1765
|
+
subject: "immutable-snapshot",
|
|
1766
|
+
reason: "The build consumed one Eden-owned snapshot and source races were checked.",
|
|
1767
|
+
remediation: null,
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
id: "VAL-BUILD-004",
|
|
1771
|
+
status: "pass",
|
|
1772
|
+
subject: "project-local-eve",
|
|
1773
|
+
reason: "The resolved Eve executable is inside the isolated project dependency tree.",
|
|
1774
|
+
remediation: null,
|
|
1775
|
+
},
|
|
1776
|
+
],
|
|
1777
|
+
candidateImageId: buildResult.imageId ?? null,
|
|
1778
|
+
candidateImageRetainedLocally: buildResult.imageId !== undefined,
|
|
1779
|
+
writtenPaths,
|
|
1780
|
+
error: null,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
catch (error) {
|
|
1784
|
+
const cleanupError = await discardBuiltCandidate();
|
|
1785
|
+
if (cleanupError !== undefined) {
|
|
1786
|
+
return blockedResult(cleanupError, { writtenPaths });
|
|
1787
|
+
}
|
|
1788
|
+
if (error instanceof EvePackagingError) {
|
|
1789
|
+
return blockedResult(error, { writtenPaths });
|
|
1790
|
+
}
|
|
1791
|
+
return blockedResult(new EvePackagingError({
|
|
1792
|
+
code: "EVE_BUILD_FAILED",
|
|
1793
|
+
subject: "eve build",
|
|
1794
|
+
reason: "The project-local Eve build did not complete successfully.",
|
|
1795
|
+
remediation: "Inspect the project-local Eve build in the isolated builder and retry after fixing it.",
|
|
1796
|
+
}), { writtenPaths });
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
export async function revalidateEveProjectCandidateInputs(candidate, runtimeConfig) {
|
|
1800
|
+
const fail = () => {
|
|
1801
|
+
throw new EvePackagingError({
|
|
1802
|
+
code: "SOURCE_RACE",
|
|
1803
|
+
subject: "project inputs",
|
|
1804
|
+
reason: "The selected Eve source, lockfile, configuration, or explicit environment identity changed after the candidate was built.",
|
|
1805
|
+
remediation: "Discard the stale candidate and retry only after all selected inputs are quiescent.",
|
|
1806
|
+
});
|
|
1807
|
+
};
|
|
1808
|
+
let manifestValue;
|
|
1809
|
+
try {
|
|
1810
|
+
manifestValue = JSON.parse(await readFile(candidate.inputManifestPath, "utf8"));
|
|
1811
|
+
}
|
|
1812
|
+
catch {
|
|
1813
|
+
fail();
|
|
1814
|
+
}
|
|
1815
|
+
if (typeof manifestValue !== "object" ||
|
|
1816
|
+
manifestValue === null ||
|
|
1817
|
+
Array.isArray(manifestValue)) {
|
|
1818
|
+
fail();
|
|
1819
|
+
}
|
|
1820
|
+
const manifest = manifestValue;
|
|
1821
|
+
if (typeof manifest.requestedRoot !== "string" ||
|
|
1822
|
+
typeof manifest.canonicalRoot !== "string" ||
|
|
1823
|
+
typeof manifest.sourceDigest !== "string" ||
|
|
1824
|
+
typeof manifest.packageJsonSha256 !== "string" ||
|
|
1825
|
+
typeof manifest.lockfileSha256 !== "string") {
|
|
1826
|
+
fail();
|
|
1827
|
+
}
|
|
1828
|
+
const requestedRoot = manifest.requestedRoot;
|
|
1829
|
+
const canonicalRoot = manifest.canonicalRoot;
|
|
1830
|
+
const sourceDigest = manifest.sourceDigest;
|
|
1831
|
+
const packageJsonSha256 = manifest.packageJsonSha256;
|
|
1832
|
+
const lockfileSha256 = manifest.lockfileSha256;
|
|
1833
|
+
const requestedDetails = await lstat(requestedRoot).catch(() => undefined);
|
|
1834
|
+
const requestedCanonical = await realpath(requestedRoot).catch(() => undefined);
|
|
1835
|
+
if (requestedDetails === undefined ||
|
|
1836
|
+
!requestedDetails.isDirectory() ||
|
|
1837
|
+
requestedDetails.isSymbolicLink() ||
|
|
1838
|
+
requestedCanonical !== canonicalRoot) {
|
|
1839
|
+
fail();
|
|
1840
|
+
}
|
|
1841
|
+
const latestIdentity = await currentInputIdentity(runtimeConfig).catch(() => undefined);
|
|
1842
|
+
const expectedIdentity = candidate.runtimeConfigInputIdentity;
|
|
1843
|
+
if (latestIdentity !== expectedIdentity ||
|
|
1844
|
+
(typeof manifest.runtimeConfigInputIdentity === "string" &&
|
|
1845
|
+
latestIdentity !== manifest.runtimeConfigInputIdentity)) {
|
|
1846
|
+
fail();
|
|
1847
|
+
}
|
|
1848
|
+
let latest;
|
|
1849
|
+
try {
|
|
1850
|
+
latest = await captureInputs(canonicalRoot, runtimeConfig, resolve(candidate.generationRoot), latestIdentity, requestedRoot);
|
|
1851
|
+
}
|
|
1852
|
+
catch {
|
|
1853
|
+
fail();
|
|
1854
|
+
}
|
|
1855
|
+
const observedLatest = latest ?? fail();
|
|
1856
|
+
const packageFile = observedLatest.files.find((file) => file.relativePath === "package.json");
|
|
1857
|
+
const lockfile = observedLatest.files.find((file) => file.relativePath === "pnpm-lock.yaml");
|
|
1858
|
+
if (packageFile === undefined ||
|
|
1859
|
+
lockfile === undefined ||
|
|
1860
|
+
packageFile.sha256 !== candidate.packageJsonSha256 ||
|
|
1861
|
+
packageFile.sha256 !== packageJsonSha256 ||
|
|
1862
|
+
lockfile.sha256 !== candidate.lockfileSha256 ||
|
|
1863
|
+
lockfile.sha256 !== lockfileSha256 ||
|
|
1864
|
+
observedLatest.sourceDigest !== candidate.sourceDigest ||
|
|
1865
|
+
observedLatest.sourceDigest !== sourceDigest) {
|
|
1866
|
+
fail();
|
|
1867
|
+
}
|
|
1868
|
+
let latestSnapshotDigest;
|
|
1869
|
+
try {
|
|
1870
|
+
latestSnapshotDigest = await snapshotDigest(candidate.snapshotRoot);
|
|
1871
|
+
}
|
|
1872
|
+
catch {
|
|
1873
|
+
fail();
|
|
1874
|
+
}
|
|
1875
|
+
if (latestSnapshotDigest === undefined ||
|
|
1876
|
+
latestSnapshotDigest !== candidate.snapshotDigest) {
|
|
1877
|
+
fail();
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
async function dockerIdentityAbsent(dockerCommand, args, options) {
|
|
1881
|
+
try {
|
|
1882
|
+
const result = await execFileAsync(dockerCommand, [...args], options);
|
|
1883
|
+
return result.stdout.trim().length === 0;
|
|
1884
|
+
}
|
|
1885
|
+
catch (error) {
|
|
1886
|
+
const stderr = typeof error === "object" &&
|
|
1887
|
+
error !== null &&
|
|
1888
|
+
"stderr" in error &&
|
|
1889
|
+
typeof error.stderr === "string"
|
|
1890
|
+
? error.stderr
|
|
1891
|
+
: "";
|
|
1892
|
+
return /(?:no such|not found|does not exist)/iu.test(stderr);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
export function createDockerEveProjectBuilder(options) {
|
|
1896
|
+
const dockerCommand = options.dockerCommand ?? "docker";
|
|
1897
|
+
return {
|
|
1898
|
+
nodeImage: options.nodeImage,
|
|
1899
|
+
async build(request) {
|
|
1900
|
+
if (request.dockerfilePath === undefined) {
|
|
1901
|
+
throw new EvePackagingError({
|
|
1902
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
1903
|
+
subject: "Dockerfile",
|
|
1904
|
+
reason: "The isolated Docker builder requires a generated pinned Dockerfile.",
|
|
1905
|
+
remediation: "Supply a verified Node 24 image and use the generated packaging context.",
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
const imageIdFile = join(request.generationRoot, "image-id");
|
|
1909
|
+
const safeEnv = {
|
|
1910
|
+
PATH: process.env.PATH,
|
|
1911
|
+
HOME: process.env.HOME,
|
|
1912
|
+
TMPDIR: process.env.TMPDIR,
|
|
1913
|
+
LANG: process.env.LANG,
|
|
1914
|
+
LC_ALL: process.env.LC_ALL,
|
|
1915
|
+
DOCKER_HOST: process.env.DOCKER_HOST,
|
|
1916
|
+
DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
|
|
1917
|
+
};
|
|
1918
|
+
let containerId;
|
|
1919
|
+
let imageBuilt = false;
|
|
1920
|
+
let retainImage = false;
|
|
1921
|
+
let builtImageId;
|
|
1922
|
+
const cleanupOwnedResources = async () => {
|
|
1923
|
+
let cleanupFailed = false;
|
|
1924
|
+
if (containerId !== undefined) {
|
|
1925
|
+
await execFileAsync(dockerCommand, ["rm", "--force", containerId], { env: safeEnv, cwd: request.generationRoot }).catch(() => {
|
|
1926
|
+
cleanupFailed = true;
|
|
1927
|
+
});
|
|
1928
|
+
const containerAbsent = await dockerIdentityAbsent(dockerCommand, ["container", "inspect", containerId, "--format", "{{.Id}}"], { env: safeEnv, cwd: request.generationRoot });
|
|
1929
|
+
if (!containerAbsent) {
|
|
1930
|
+
cleanupFailed = true;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (imageBuilt && !retainImage && builtImageId === undefined) {
|
|
1934
|
+
cleanupFailed = true;
|
|
1935
|
+
}
|
|
1936
|
+
else if (imageBuilt && !retainImage && builtImageId !== undefined) {
|
|
1937
|
+
await execFileAsync(dockerCommand, ["image", "rm", "--force", builtImageId], { env: safeEnv, cwd: request.generationRoot }).catch(() => {
|
|
1938
|
+
cleanupFailed = true;
|
|
1939
|
+
});
|
|
1940
|
+
const imageAbsent = await dockerIdentityAbsent(dockerCommand, ["image", "inspect", builtImageId, "--format", "{{.Id}}"], { env: safeEnv, cwd: request.generationRoot });
|
|
1941
|
+
if (!imageAbsent) {
|
|
1942
|
+
cleanupFailed = true;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return cleanupFailed;
|
|
1946
|
+
};
|
|
1947
|
+
try {
|
|
1948
|
+
await execFileAsync(dockerCommand, ["version", "--format", "{{.Server.Version}}"], { env: safeEnv, cwd: request.generationRoot });
|
|
1949
|
+
await execFileAsync(dockerCommand, [
|
|
1950
|
+
"build",
|
|
1951
|
+
"--platform=linux/amd64",
|
|
1952
|
+
"--file",
|
|
1953
|
+
request.dockerfilePath,
|
|
1954
|
+
"--iidfile",
|
|
1955
|
+
imageIdFile,
|
|
1956
|
+
request.snapshotRoot,
|
|
1957
|
+
], { env: safeEnv, cwd: request.generationRoot, maxBuffer: 1024 * 1024 });
|
|
1958
|
+
imageBuilt = true;
|
|
1959
|
+
builtImageId = (await readFile(imageIdFile, "utf8")).trim();
|
|
1960
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(builtImageId)) {
|
|
1961
|
+
throw new EvePackagingError({
|
|
1962
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
1963
|
+
subject: "image identity",
|
|
1964
|
+
reason: "Docker did not return a verifiable immutable image identity.",
|
|
1965
|
+
remediation: "Use a BuildKit Docker/OrbStack builder that supports an iidfile.",
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
const imageDetails = await execFileAsync(dockerCommand, [
|
|
1969
|
+
"image",
|
|
1970
|
+
"inspect",
|
|
1971
|
+
builtImageId,
|
|
1972
|
+
"--format",
|
|
1973
|
+
"{{.Id}} {{.Os}} {{.Architecture}}",
|
|
1974
|
+
], { env: safeEnv, cwd: request.generationRoot });
|
|
1975
|
+
const [imageId, os, architecture] = imageDetails.stdout.trim().split(/\s+/u);
|
|
1976
|
+
if (imageId === undefined ||
|
|
1977
|
+
imageId !== builtImageId ||
|
|
1978
|
+
os !== "linux" ||
|
|
1979
|
+
architecture !== "amd64") {
|
|
1980
|
+
throw new EvePackagingError({
|
|
1981
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
1982
|
+
subject: "linux/amd64 image",
|
|
1983
|
+
reason: "The built Eve image did not report the required Linux/amd64 platform.",
|
|
1984
|
+
remediation: "Use a Docker/OrbStack builder that can produce and run linux/amd64 images.",
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
const created = await execFileAsync(dockerCommand, ["create", builtImageId], { env: safeEnv, cwd: request.generationRoot });
|
|
1988
|
+
containerId = created.stdout.trim();
|
|
1989
|
+
if (!/^[a-f0-9]+$/u.test(containerId)) {
|
|
1990
|
+
throw new EvePackagingError({
|
|
1991
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
1992
|
+
subject: "build container",
|
|
1993
|
+
reason: "The isolated Eve build container identity could not be verified.",
|
|
1994
|
+
remediation: "Retry with a Docker daemon that returns a stable container identity.",
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/.output`, join(request.snapshotRoot, ".output")], { env: safeEnv, cwd: request.generationRoot });
|
|
1998
|
+
await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/node_modules`, join(request.snapshotRoot, "node_modules")], { env: safeEnv, cwd: request.generationRoot });
|
|
1999
|
+
const result = {
|
|
2000
|
+
imageId: builtImageId,
|
|
2001
|
+
imagePlatform: "linux/amd64",
|
|
2002
|
+
imageReference: builtImageId,
|
|
2003
|
+
imageDigest: builtImageId,
|
|
2004
|
+
};
|
|
2005
|
+
retainImage = true;
|
|
2006
|
+
if (await cleanupOwnedResources()) {
|
|
2007
|
+
throw new EvePackagingError({
|
|
2008
|
+
code: "CLEANUP_UNVERIFIED",
|
|
2009
|
+
subject: "Docker build resources",
|
|
2010
|
+
reason: "Owned Docker cleanup could not be verified after the Eve build attempt.",
|
|
2011
|
+
remediation: "Inspect only the recorded container identity before retrying; no broad Docker cleanup was attempted.",
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
return result;
|
|
2015
|
+
}
|
|
2016
|
+
catch (error) {
|
|
2017
|
+
if (await cleanupOwnedResources()) {
|
|
2018
|
+
throw new EvePackagingError({
|
|
2019
|
+
code: "CLEANUP_UNVERIFIED",
|
|
2020
|
+
subject: "Docker build resources",
|
|
2021
|
+
reason: "Owned Docker cleanup could not be verified after the Eve build attempt.",
|
|
2022
|
+
remediation: "Inspect only the recorded image and container identities before retrying; no broad Docker cleanup was attempted.",
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
if (error instanceof EvePackagingError)
|
|
2026
|
+
throw error;
|
|
2027
|
+
throw classifyDockerBuildFailure(error);
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
2030
|
+
async discard(result) {
|
|
2031
|
+
const imageReference = result.imageReference;
|
|
2032
|
+
if (imageReference === undefined)
|
|
2033
|
+
return;
|
|
2034
|
+
const safeEnv = {
|
|
2035
|
+
PATH: process.env.PATH,
|
|
2036
|
+
HOME: process.env.HOME,
|
|
2037
|
+
TMPDIR: process.env.TMPDIR,
|
|
2038
|
+
LANG: process.env.LANG,
|
|
2039
|
+
LC_ALL: process.env.LC_ALL,
|
|
2040
|
+
DOCKER_HOST: process.env.DOCKER_HOST,
|
|
2041
|
+
DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
|
|
2042
|
+
};
|
|
2043
|
+
await execFileAsync(dockerCommand, ["image", "rm", "--force", imageReference], { env: safeEnv });
|
|
2044
|
+
const remainingImage = await execFileAsync(dockerCommand, ["image", "inspect", imageReference, "--format", "{{.Id}}"], { env: safeEnv }).catch(() => undefined);
|
|
2045
|
+
if (remainingImage !== undefined && remainingImage.stdout.trim().length > 0) {
|
|
2046
|
+
throw new EvePackagingError({
|
|
2047
|
+
code: "CLEANUP_UNVERIFIED",
|
|
2048
|
+
subject: imageReference,
|
|
2049
|
+
reason: "The failed Eve image remained after exact cleanup.",
|
|
2050
|
+
remediation: "Inspect the exact image identity manually; do not run broad Docker cleanup.",
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
//# sourceMappingURL=eve-packaging.js.map
|