@hue-run/sdk 0.3.1 → 0.4.1
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/CLI.md +270 -47
- package/ENVIRONMENTS.md +10 -0
- package/EVALUATIONS.md +1 -1
- package/README.md +20 -4
- package/dist/client.d.ts +5 -5
- package/dist/client.js +13 -6
- package/dist/environment/tools.d.ts +6 -1
- package/dist/environment/tools.js +7 -1
- package/dist/environment/types.d.ts +6 -1
- package/dist/evals/environment-target.js +1 -1
- package/dist/receipt.js +36 -8
- package/dist/setup/application.d.ts +74 -0
- package/dist/setup/application.js +766 -0
- package/dist/setup/backend.d.ts +229 -0
- package/dist/setup/backend.js +855 -0
- package/dist/setup/checkpoint.js +100 -30
- package/dist/setup/cli.js +20 -4
- package/dist/setup/configure.d.ts +13 -0
- package/dist/setup/configure.js +454 -0
- package/dist/setup/credential.d.ts +2 -0
- package/dist/setup/credential.js +9 -0
- package/dist/setup/detect.js +4 -1
- package/dist/setup/installation.d.ts +118 -0
- package/dist/setup/installation.js +605 -0
- package/dist/setup/lock.d.ts +2 -0
- package/dist/setup/lock.js +38 -0
- package/dist/setup/machine.d.ts +1 -10
- package/dist/setup/machine.js +8 -7
- package/dist/setup/render.d.ts +3 -1
- package/dist/setup/render.js +209 -6
- package/dist/setup/runner.d.ts +26 -76
- package/dist/setup/runner.js +320 -45
- package/dist/setup/socket.d.ts +7 -0
- package/dist/setup/socket.js +144 -0
- package/dist/setup/source.d.ts +9 -0
- package/dist/setup/source.js +269 -0
- package/dist/setup/types.d.ts +16 -9
- package/dist/setup/types.js +1 -1
- package/dist/setup.d.ts +6 -2
- package/dist/setup.js +3 -0
- package/dist/types.d.ts +24 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/setup-events.schema.json +16 -9
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import { randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, readdir, realpath, rename, unlink } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { connectOwnedApplication, requestOwnedApplication } from "./socket.js";
|
|
8
|
+
import { sdkVersion } from "../version.js";
|
|
9
|
+
import { inspectExpressSource, inspectFlaskSource } from "./source.js";
|
|
10
|
+
import { setupManagedDigest, } from "./installation.js";
|
|
11
|
+
const PYTHON_RUNTIME_VERSION = "0.2.2";
|
|
12
|
+
const EXPRESS_RUNTIME_DEPENDENCIES = {
|
|
13
|
+
"@hue-run/sdk": sdkVersion,
|
|
14
|
+
"@opentelemetry/api": "1.9.1",
|
|
15
|
+
"@opentelemetry/context-async-hooks": "2.11.0",
|
|
16
|
+
};
|
|
17
|
+
const MAX_SOURCE_BYTES = 1024 * 1024;
|
|
18
|
+
const START_MARKER = "Hue setup instrumentation (managed; do not edit)";
|
|
19
|
+
const END_MARKER = "End Hue setup instrumentation";
|
|
20
|
+
/** Never let Bun evaluate a project preload while checking runtime support. */
|
|
21
|
+
async function bunConfiguration(root) {
|
|
22
|
+
const refuse = () => new SetupApplicationActionRequired("custom-instrumentation", "Review Bun configuration and dotenv files before automatic setup; custom runtime/bootstrap ownership is unsupported.");
|
|
23
|
+
if ((await readdir(root)).some((name) => /^\.env(?:\.|$)/u.test(name)))
|
|
24
|
+
throw refuse();
|
|
25
|
+
const globals = new Set([
|
|
26
|
+
join(homedir(), ".bunfig.toml"),
|
|
27
|
+
...(process.env.XDG_CONFIG_HOME ? [join(process.env.XDG_CONFIG_HOME, ".bunfig.toml")] : []),
|
|
28
|
+
]);
|
|
29
|
+
for (const path of globals) {
|
|
30
|
+
try {
|
|
31
|
+
await lstat(path);
|
|
32
|
+
throw refuse();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== "ENOENT")
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let source;
|
|
40
|
+
try {
|
|
41
|
+
source = await safeRead(root, "bunfig.toml");
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ENOENT")
|
|
45
|
+
return "/dev/null";
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
// The pre-publication registry overlay is the sole supported local Bun setting.
|
|
49
|
+
// Unknown/quoted/dotted/multiline TOML never gets evaluated by Bun during preflight.
|
|
50
|
+
const match = /^\s*\[install\]\s*\n\s*registry\s*=\s*"([^"\\\r\n]+)"\s*$/u.exec(source);
|
|
51
|
+
if (!match)
|
|
52
|
+
throw refuse();
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(match[1]);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw refuse();
|
|
59
|
+
}
|
|
60
|
+
if (url.username ||
|
|
61
|
+
url.password ||
|
|
62
|
+
url.search ||
|
|
63
|
+
url.hash ||
|
|
64
|
+
url.pathname !== "/" ||
|
|
65
|
+
!(url.protocol === "https:" ||
|
|
66
|
+
(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
|
|
67
|
+
throw refuse();
|
|
68
|
+
return join(root, "bunfig.toml");
|
|
69
|
+
}
|
|
70
|
+
/** Stable reason an automatic application integration is not safe. */
|
|
71
|
+
export class SetupApplicationActionRequired extends Error {
|
|
72
|
+
code;
|
|
73
|
+
constructor(
|
|
74
|
+
/** Stable reason the caller must resolve before automatic integration continues. */
|
|
75
|
+
code, message) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.code = code;
|
|
78
|
+
this.name = "SetupApplicationActionRequired";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function inside(parent, child) {
|
|
82
|
+
const path = relative(parent, child);
|
|
83
|
+
return path === "" || (!path.startsWith("..") && !path.startsWith("/"));
|
|
84
|
+
}
|
|
85
|
+
async function safeDirectory(path) {
|
|
86
|
+
const directories = [];
|
|
87
|
+
let current = resolve(path);
|
|
88
|
+
for (;;) {
|
|
89
|
+
directories.unshift(current);
|
|
90
|
+
const parent = dirname(current);
|
|
91
|
+
if (parent === current)
|
|
92
|
+
break;
|
|
93
|
+
current = parent;
|
|
94
|
+
}
|
|
95
|
+
for (const directory of directories) {
|
|
96
|
+
const info = await lstat(directory);
|
|
97
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
98
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "The application path contains an unsafe directory. Select a regular project directory without symlinks.");
|
|
99
|
+
}
|
|
100
|
+
if ((await realpath(path)) !== resolve(path))
|
|
101
|
+
throw new Error("Unsafe setup application directory");
|
|
102
|
+
}
|
|
103
|
+
async function safeRead(root, relativePath) {
|
|
104
|
+
const path = resolve(root, relativePath);
|
|
105
|
+
if (!inside(root, path))
|
|
106
|
+
throw new Error("Unsafe setup application path");
|
|
107
|
+
await safeDirectory(dirname(path));
|
|
108
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
109
|
+
try {
|
|
110
|
+
const info = await handle.stat();
|
|
111
|
+
if (!info.isFile() || info.size > MAX_SOURCE_BYTES || (info.mode & 0o7000) !== 0)
|
|
112
|
+
throw new Error("Unsafe setup application file");
|
|
113
|
+
const source = await handle.readFile("utf8");
|
|
114
|
+
const after = await handle.stat();
|
|
115
|
+
await safeDirectory(dirname(path));
|
|
116
|
+
const named = await lstat(path);
|
|
117
|
+
if (info.dev !== named.dev ||
|
|
118
|
+
info.ino !== named.ino ||
|
|
119
|
+
!named.isFile() ||
|
|
120
|
+
info.mode !== named.mode ||
|
|
121
|
+
info.mtimeMs !== after.mtimeMs ||
|
|
122
|
+
info.ctimeMs !== after.ctimeMs ||
|
|
123
|
+
info.size !== after.size)
|
|
124
|
+
throw new Error("The setup application file changed while reading");
|
|
125
|
+
return source;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await handle.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function rejectMonorepoRoot(root) {
|
|
132
|
+
await safeDirectory(root);
|
|
133
|
+
for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json"]) {
|
|
134
|
+
try {
|
|
135
|
+
const handle = await open(join(root, marker), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
136
|
+
try {
|
|
137
|
+
if ((await handle.stat()).isFile())
|
|
138
|
+
throw new SetupApplicationActionRequired("ambiguous-project", "Run setup from one selected workspace package with --project; the repository root is ambiguous.");
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await handle.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error.code !== "ENOENT")
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function rejectNodeWorkspaceAncestors(root) {
|
|
151
|
+
// npm and Bun can discover an ancestor workspace and move installation/lockfile writes there.
|
|
152
|
+
for (let parent = dirname(root);; parent = dirname(parent)) {
|
|
153
|
+
try {
|
|
154
|
+
const source = await safeRead(parent, "package.json");
|
|
155
|
+
const manifest = JSON.parse(source);
|
|
156
|
+
if (!manifest ||
|
|
157
|
+
typeof manifest !== "object" ||
|
|
158
|
+
Array.isArray(manifest) ||
|
|
159
|
+
manifest.workspaces !== undefined)
|
|
160
|
+
throw new SetupApplicationActionRequired("ambiguous-project", "Automatic Express setup does not mutate workspace members. Select an independent npm or Bun application project.");
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error.code !== "ENOENT")
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
if (dirname(parent) === parent)
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function typescriptRuntimeDeclared(manifest) {
|
|
171
|
+
const refuse = () => new SetupApplicationActionRequired("custom-instrumentation", `The project declares a custom or malformed @hue-run/sdk dependency. Select ${sdkVersion} explicitly, review compatibility, and rerun setup.`);
|
|
172
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
|
|
173
|
+
throw refuse();
|
|
174
|
+
const declared = new Map();
|
|
175
|
+
for (const section of [
|
|
176
|
+
"dependencies",
|
|
177
|
+
"devDependencies",
|
|
178
|
+
"optionalDependencies",
|
|
179
|
+
"peerDependencies",
|
|
180
|
+
]) {
|
|
181
|
+
const dependencies = manifest[section];
|
|
182
|
+
if (dependencies === undefined)
|
|
183
|
+
continue;
|
|
184
|
+
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies))
|
|
185
|
+
throw refuse();
|
|
186
|
+
for (const [name, value] of Object.entries(dependencies)) {
|
|
187
|
+
if (name.startsWith("@opentelemetry/") && !Object.hasOwn(EXPRESS_RUNTIME_DEPENDENCIES, name))
|
|
188
|
+
throw refuse();
|
|
189
|
+
if (Object.hasOwn(EXPRESS_RUNTIME_DEPENDENCIES, name)) {
|
|
190
|
+
if (section === "peerDependencies" || section === "optionalDependencies")
|
|
191
|
+
throw refuse();
|
|
192
|
+
declared.set(name, [...(declared.get(name) ?? []), value]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const [name, version] of Object.entries(EXPRESS_RUNTIME_DEPENDENCIES)) {
|
|
197
|
+
const versions = declared.get(name) ?? [];
|
|
198
|
+
if (versions.length > 1 || (versions.length === 1 && versions[0] !== version))
|
|
199
|
+
throw refuse();
|
|
200
|
+
}
|
|
201
|
+
return Object.keys(EXPRESS_RUNTIME_DEPENDENCIES).every((name) => declared.has(name));
|
|
202
|
+
}
|
|
203
|
+
function applicationRequestUrl(path, origin) {
|
|
204
|
+
const url = new URL(path, origin);
|
|
205
|
+
if (path.length > 200 ||
|
|
206
|
+
!/^\/[A-Za-z0-9_~./-]*$/u.test(path) ||
|
|
207
|
+
path.includes("//") ||
|
|
208
|
+
url.origin !== origin ||
|
|
209
|
+
url.pathname !== path ||
|
|
210
|
+
url.search !== "" ||
|
|
211
|
+
url.hash !== "" ||
|
|
212
|
+
url.username !== "" ||
|
|
213
|
+
url.password !== "")
|
|
214
|
+
throw new SetupApplicationActionRequired("ambiguous-entrypoint", "The existing GET route must be one literal local pathname without redirects, dynamic segments or escaping.");
|
|
215
|
+
return url;
|
|
216
|
+
}
|
|
217
|
+
function syntax(source, language) {
|
|
218
|
+
let result;
|
|
219
|
+
try {
|
|
220
|
+
result = language === "typescript" ? inspectExpressSource(source) : inspectFlaskSource(source);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new SetupApplicationActionRequired("ambiguous-entrypoint", "Hue requires a supported runtime and unambiguous executable constructor, literal GET handler and telemetry ownership. Review this application before setup changes it.");
|
|
224
|
+
}
|
|
225
|
+
applicationRequestUrl(result.requestPath, "http://127.0.0.1:1");
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
function pythonProject(source) {
|
|
229
|
+
const refuse = () => new SetupApplicationActionRequired("custom-instrumentation", "Automatic Flask setup requires static project dependencies without build hooks, workspaces, custom Hue requirements or source overrides.");
|
|
230
|
+
// Parse a bounded, deliberately small TOML subset without executing Python or a build backend.
|
|
231
|
+
let clean = "";
|
|
232
|
+
let quote = "";
|
|
233
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
234
|
+
const char = source[index];
|
|
235
|
+
if (quote) {
|
|
236
|
+
if (char === "\n" || char === "\r")
|
|
237
|
+
throw refuse();
|
|
238
|
+
clean += char;
|
|
239
|
+
if (char === "\\" && quote === '"') {
|
|
240
|
+
clean += source[++index] ?? "";
|
|
241
|
+
}
|
|
242
|
+
else if (char === quote)
|
|
243
|
+
quote = "";
|
|
244
|
+
}
|
|
245
|
+
else if (char === '"' || char === "'") {
|
|
246
|
+
if (source.slice(index, index + 3) === char.repeat(3))
|
|
247
|
+
throw refuse();
|
|
248
|
+
quote = char;
|
|
249
|
+
clean += char;
|
|
250
|
+
}
|
|
251
|
+
else if (char === "#") {
|
|
252
|
+
while (index < source.length && source[index] !== "\n")
|
|
253
|
+
index += 1;
|
|
254
|
+
clean += "\n";
|
|
255
|
+
}
|
|
256
|
+
else
|
|
257
|
+
clean += char;
|
|
258
|
+
}
|
|
259
|
+
if (quote)
|
|
260
|
+
throw refuse();
|
|
261
|
+
const sections = [...clean.matchAll(/^\s*\[([^\n]+)\]\s*$/gmu)];
|
|
262
|
+
if (sections.some((section) => !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/u.test(section[1])))
|
|
263
|
+
throw refuse();
|
|
264
|
+
if (sections.some((section) => /^(?:build-system|tool\.uv\.(?:workspace|sources))(?:\.|$)/u.test(section[1])))
|
|
265
|
+
throw refuse();
|
|
266
|
+
const projects = sections.filter((section) => section[1] === "project");
|
|
267
|
+
if (projects.length !== 1 || /^\s*(?:dynamic|workspace|sources)\s*=/mu.test(clean))
|
|
268
|
+
throw refuse();
|
|
269
|
+
const project = projects[0];
|
|
270
|
+
const end = sections.find((section) => section.index > project.index)?.index ?? clean.length;
|
|
271
|
+
const body = clean.slice(project.index + project[0].length, end);
|
|
272
|
+
const declarations = [...body.matchAll(/^\s*dependencies\s*=\s*/gmu)];
|
|
273
|
+
if (declarations.length !== 1)
|
|
274
|
+
throw refuse();
|
|
275
|
+
let cursor = declarations[0].index + declarations[0][0].length;
|
|
276
|
+
if (body[cursor++] !== "[")
|
|
277
|
+
throw refuse();
|
|
278
|
+
const dependencies = [];
|
|
279
|
+
for (;;) {
|
|
280
|
+
while (/\s/u.test(body[cursor] ?? "x"))
|
|
281
|
+
cursor += 1;
|
|
282
|
+
if (body[cursor] === "]") {
|
|
283
|
+
cursor += 1;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
const delimiter = body[cursor++];
|
|
287
|
+
if (delimiter !== '"' && delimiter !== "'")
|
|
288
|
+
throw refuse();
|
|
289
|
+
let dependency = "";
|
|
290
|
+
while (body[cursor] !== delimiter) {
|
|
291
|
+
const char = body[cursor++];
|
|
292
|
+
if (char === undefined || char === "\\" || char === "\n" || char === "\r")
|
|
293
|
+
throw refuse();
|
|
294
|
+
dependency += char;
|
|
295
|
+
}
|
|
296
|
+
cursor += 1;
|
|
297
|
+
dependencies.push(dependency);
|
|
298
|
+
while (/\s/u.test(body[cursor] ?? "x"))
|
|
299
|
+
cursor += 1;
|
|
300
|
+
if (body[cursor] === ",")
|
|
301
|
+
cursor += 1;
|
|
302
|
+
else if (body[cursor] !== "]")
|
|
303
|
+
throw refuse();
|
|
304
|
+
}
|
|
305
|
+
if (/^[^\n]*\S/u.test(body.slice(cursor)))
|
|
306
|
+
throw refuse();
|
|
307
|
+
const packageName = (requirement) => /^\s*([A-Za-z0-9][A-Za-z0-9_.-]*)/u
|
|
308
|
+
.exec(requirement)?.[1]
|
|
309
|
+
?.toLowerCase()
|
|
310
|
+
.replaceAll(/[_.-]+/gu, "-");
|
|
311
|
+
const hue = dependencies.filter((dependency) => packageName(dependency) === "hue-run");
|
|
312
|
+
if (hue.length > 1 ||
|
|
313
|
+
(hue.length === 1 && !/^hue[-_.]run\s*==\s*0\.2\.2$/iu.test(hue[0].trim())))
|
|
314
|
+
throw refuse();
|
|
315
|
+
const allHueStrings = [...clean.matchAll(/(["'])\s*hue[-_.]run\b[^"']*\1/giu)];
|
|
316
|
+
if (allHueStrings.length !== hue.length ||
|
|
317
|
+
!dependencies.some((dependency) => packageName(dependency) === "flask"))
|
|
318
|
+
throw refuse();
|
|
319
|
+
return { dependencies, hasHue: hue.length === 1 };
|
|
320
|
+
}
|
|
321
|
+
async function flaskConfiguration(root) {
|
|
322
|
+
if (Object.keys(process.env).some((key) => key.startsWith("FLASK_") || key === "PYTHONPATH" || key === "PYTHONHOME") ||
|
|
323
|
+
(await readdir(root)).some((name) => name === ".env" || name.startsWith(".env.") || name === ".flaskenv"))
|
|
324
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "Review Flask runtime and dotenv configuration before setup; reloader or custom bootstrap ownership is unsupported.");
|
|
325
|
+
}
|
|
326
|
+
/** Statically recognizes the deliberately narrow automatic matrix without executing project code. */
|
|
327
|
+
export async function planSetupApplication(project) {
|
|
328
|
+
await rejectMonorepoRoot(project.root);
|
|
329
|
+
if (project.languages.length !== 1)
|
|
330
|
+
throw new SetupApplicationActionRequired("ambiguous-project", "Select one TypeScript or Python package root with --project; Hue will not choose a monorepo package or language automatically.");
|
|
331
|
+
if (project.languages[0] === "typescript") {
|
|
332
|
+
if (process.env.NODE_OPTIONS || process.env.BUN_OPTIONS)
|
|
333
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "Review runtime preload options before automatic setup; context ownership is ambiguous.");
|
|
334
|
+
const managers = project.packageManagers.filter((manager) => manager === "bun" || manager === "npm");
|
|
335
|
+
if (managers.length !== 1 || project.packageManagers.length !== 1)
|
|
336
|
+
throw new SetupApplicationActionRequired("unsupported-manager", "Select a single npm or Bun package root and rerun setup; Hue will not choose or rewrite an ambiguous lockfile.");
|
|
337
|
+
if (!project.frameworks.includes("express"))
|
|
338
|
+
throw new SetupApplicationActionRequired("unsupported-framework", "Automatic setup currently supports single-package Express servers only. Add Hue to one existing request path, then rerun hue resume.");
|
|
339
|
+
const manifest = JSON.parse(await safeRead(project.root, "package.json"));
|
|
340
|
+
typescriptRuntimeDeclared(manifest);
|
|
341
|
+
await rejectNodeWorkspaceAncestors(project.root);
|
|
342
|
+
if (manifest.workspaces !== undefined)
|
|
343
|
+
throw new SetupApplicationActionRequired("ambiguous-project", "Run setup from one selected workspace package with --project; the repository root is ambiguous.");
|
|
344
|
+
const start = manifest.scripts && typeof manifest.scripts === "object" && !Array.isArray(manifest.scripts)
|
|
345
|
+
? manifest.scripts.start
|
|
346
|
+
: undefined;
|
|
347
|
+
const match = typeof start === "string"
|
|
348
|
+
? /^(node(?: --enable-source-maps)?|bun) ([A-Za-z0-9_./-]+\.(?:ts|mts|js|mjs))$/u.exec(start)
|
|
349
|
+
: null;
|
|
350
|
+
if (!match)
|
|
351
|
+
throw new SetupApplicationActionRequired("ambiguous-entrypoint", "Define a start script consisting only of node or bun plus one server entrypoint, or integrate Hue into an existing request.");
|
|
352
|
+
const entrypoint = match[2];
|
|
353
|
+
const original = await safeRead(project.root, entrypoint);
|
|
354
|
+
const plan = {
|
|
355
|
+
language: "typescript",
|
|
356
|
+
manager: managers[0],
|
|
357
|
+
framework: "express",
|
|
358
|
+
runtime: match[1] === "bun" ? "bun" : "node",
|
|
359
|
+
sourceMaps: match[1] === "node --enable-source-maps",
|
|
360
|
+
entrypoint,
|
|
361
|
+
requestPath: "/",
|
|
362
|
+
entryDigest: setupManagedDigest(original),
|
|
363
|
+
};
|
|
364
|
+
const source = unmanagedApplicationSource(original, plan);
|
|
365
|
+
plan.requestPath = syntax(source, "typescript").requestPath;
|
|
366
|
+
if (plan.manager === "bun" || plan.runtime === "bun")
|
|
367
|
+
await bunConfiguration(project.root);
|
|
368
|
+
// Inspect only the selected runtime, never import the app or a user bootstrap.
|
|
369
|
+
// Fixed argv + bounded output; unsupported runtimes fail before installation/provisioning.
|
|
370
|
+
const runtime = spawnSync(plan.runtime === "bun" ? "bun" : "node", [
|
|
371
|
+
...(plan.runtime === "bun"
|
|
372
|
+
? ["--no-env-file", "--config=/dev/null"]
|
|
373
|
+
: ["--experimental-vm-modules"]),
|
|
374
|
+
"--input-type=module",
|
|
375
|
+
"-e",
|
|
376
|
+
'import { AsyncLocalStorage } from "node:async_hooks"; import { readFileSync } from "node:fs"; import * as module from "node:module"; import * as vm from "node:vm"; try { const input=JSON.parse(readFileSync(0,"utf8")); const major=Number(process.versions.node.split(".")[0]); if (process.versions.bun ? process.versions.bun !== "1.4.2" : ![22,24,26].includes(major)) process.exit(2); if(process.versions.bun) new Bun.Transpiler({loader:input.typescript?"ts":"js"}).transformSync(input.source); else new vm.SourceTextModule(input.typescript?module.stripTypeScriptTypes(input.source,{mode:"strip"}):input.source); const storage=new AsyncLocalStorage(); await storage.run(1, async()=>{await new Promise(r=>setImmediate(r));if(storage.getStore()!==1)process.exit(2)}); storage.disable(); } catch { process.exitCode=2; }',
|
|
377
|
+
], {
|
|
378
|
+
cwd: project.root,
|
|
379
|
+
timeout: 5000,
|
|
380
|
+
maxBuffer: 8192,
|
|
381
|
+
shell: false,
|
|
382
|
+
input: JSON.stringify({ source, typescript: /\.(?:ts|mts)$/u.test(entrypoint) }),
|
|
383
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
384
|
+
});
|
|
385
|
+
if (runtime.status !== 0)
|
|
386
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "The selected runtime cannot compile this entrypoint or safely support setup-owned asynchronous context. Review syntax/runtime compatibility before rerunning setup.");
|
|
387
|
+
return plan;
|
|
388
|
+
}
|
|
389
|
+
if (project.packageManagers.length !== 1 || project.packageManagers[0] !== "uv")
|
|
390
|
+
throw new SetupApplicationActionRequired("unsupported-manager", "Automatic Python setup currently supports one uv package only; Hue will not choose or rewrite another environment manager.");
|
|
391
|
+
if (!project.frameworks.includes("flask"))
|
|
392
|
+
throw new SetupApplicationActionRequired("unsupported-framework", "Automatic setup currently supports single-package Flask servers only. Add Hue to one existing request path, then rerun hue resume.");
|
|
393
|
+
pythonProject(await safeRead(project.root, "pyproject.toml"));
|
|
394
|
+
await flaskConfiguration(project.root);
|
|
395
|
+
// uv searches parents for workspaces, even when the current package has its own manifest.
|
|
396
|
+
for (let parent = dirname(project.root);; parent = dirname(parent)) {
|
|
397
|
+
try {
|
|
398
|
+
await safeRead(parent, "pyproject.toml");
|
|
399
|
+
throw new SetupApplicationActionRequired("ambiguous-project", "An ancestor Python project may own this environment or workspace. Select an independent uv application project.");
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
if (error.code !== "ENOENT")
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
if (dirname(parent) === parent)
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
const original = await safeRead(project.root, "app.py");
|
|
409
|
+
const plan = {
|
|
410
|
+
language: "python",
|
|
411
|
+
manager: "uv",
|
|
412
|
+
framework: "flask",
|
|
413
|
+
entrypoint: "app.py",
|
|
414
|
+
requestPath: "/",
|
|
415
|
+
entryDigest: setupManagedDigest(original),
|
|
416
|
+
};
|
|
417
|
+
const source = unmanagedApplicationSource(original, plan);
|
|
418
|
+
const encoding = source
|
|
419
|
+
.split("\n")
|
|
420
|
+
.slice(0, 2)
|
|
421
|
+
.join("\n")
|
|
422
|
+
.match(/coding\s*[:=]\s*([-\w.]+)/u)?.[1];
|
|
423
|
+
if (source.startsWith("\ufeff") || (encoding && !/^(?:utf-?8|ascii)$/iu.test(encoding)))
|
|
424
|
+
throw new SetupApplicationActionRequired("ambiguous-entrypoint", "Automatic Flask setup preserves UTF-8/ASCII prologues only; review this source encoding before changes.");
|
|
425
|
+
plan.requestPath = syntax(source, "python").requestPath;
|
|
426
|
+
return plan;
|
|
427
|
+
}
|
|
428
|
+
/** Default command runner: fixed argv, no shell, bounded output and deadline. */
|
|
429
|
+
export async function runSetupCommand(input) {
|
|
430
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
431
|
+
const child = spawn(input.command, input.args, {
|
|
432
|
+
cwd: input.cwd,
|
|
433
|
+
env: input.env ?? process.env,
|
|
434
|
+
shell: false,
|
|
435
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
436
|
+
});
|
|
437
|
+
let outputBytes = 0;
|
|
438
|
+
const count = (chunk) => {
|
|
439
|
+
outputBytes += chunk.byteLength;
|
|
440
|
+
if (outputBytes > 64 * 1024)
|
|
441
|
+
child.kill("SIGKILL");
|
|
442
|
+
};
|
|
443
|
+
child.stdout.on("data", count);
|
|
444
|
+
child.stderr.on("data", count);
|
|
445
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), input.timeoutMillis);
|
|
446
|
+
child.once("error", (error) => {
|
|
447
|
+
clearTimeout(timer);
|
|
448
|
+
rejectPromise(error);
|
|
449
|
+
});
|
|
450
|
+
child.once("close", (code) => {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
if (code === 0 && outputBytes <= 64 * 1024)
|
|
453
|
+
resolvePromise();
|
|
454
|
+
else
|
|
455
|
+
rejectPromise(new Error("The bounded setup command did not complete successfully"));
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/** Installs the exact runtime with the detected owner of the project's manifest and lockfile. */
|
|
460
|
+
export async function installSetupRuntime(project, plan, runner = runSetupCommand) {
|
|
461
|
+
if (plan.language === "typescript") {
|
|
462
|
+
const bunConfig = plan.manager === "bun" ? await bunConfiguration(project.root) : undefined;
|
|
463
|
+
const specs = Object.entries(EXPRESS_RUNTIME_DEPENDENCIES).map(([name, version]) => `${name}@${version}`);
|
|
464
|
+
const manifest = JSON.parse(await safeRead(project.root, "package.json"));
|
|
465
|
+
const pinned = typescriptRuntimeDeclared(manifest);
|
|
466
|
+
await rejectNodeWorkspaceAncestors(project.root);
|
|
467
|
+
const lockNames = plan.manager === "npm"
|
|
468
|
+
? ["package-lock.json", "npm-shrinkwrap.json"]
|
|
469
|
+
: ["bun.lock", "bun.lockb"];
|
|
470
|
+
let locked = false;
|
|
471
|
+
for (const name of lockNames) {
|
|
472
|
+
try {
|
|
473
|
+
await safeRead(project.root, name);
|
|
474
|
+
locked = true;
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
if (error.code !== "ENOENT")
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const args = plan.manager === "npm"
|
|
482
|
+
? pinned
|
|
483
|
+
? [locked ? "ci" : "install", "--ignore-scripts", "--no-audit", "--no-fund"]
|
|
484
|
+
: ["install", "--save-exact", "--ignore-scripts", "--no-audit", "--no-fund", ...specs]
|
|
485
|
+
: pinned
|
|
486
|
+
? ["install", ...(locked ? ["--frozen-lockfile"] : []), "--ignore-scripts"]
|
|
487
|
+
: ["add", "--exact", "--ignore-scripts", ...specs];
|
|
488
|
+
await runner({
|
|
489
|
+
command: plan.manager,
|
|
490
|
+
args: bunConfig ? ["--no-env-file", `--config=${bunConfig}`, ...args] : args,
|
|
491
|
+
cwd: project.root,
|
|
492
|
+
timeoutMillis: 120_000,
|
|
493
|
+
});
|
|
494
|
+
const updated = JSON.parse(await safeRead(project.root, "package.json"));
|
|
495
|
+
if (!typescriptRuntimeDeclared(updated))
|
|
496
|
+
throw new Error("The package manager did not record the exact Hue runtime version");
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
const before = await safeRead(project.root, "pyproject.toml");
|
|
500
|
+
const python = pythonProject(before);
|
|
501
|
+
if (!python.hasHue)
|
|
502
|
+
await runner({
|
|
503
|
+
command: "uv",
|
|
504
|
+
args: ["add", "--no-build", "--no-sync", `hue-run==${PYTHON_RUNTIME_VERSION}`],
|
|
505
|
+
cwd: project.root,
|
|
506
|
+
timeoutMillis: 120_000,
|
|
507
|
+
});
|
|
508
|
+
if (!pythonProject(await safeRead(project.root, "pyproject.toml")).hasHue)
|
|
509
|
+
throw new Error("uv did not record the exact Hue runtime version");
|
|
510
|
+
await runner({
|
|
511
|
+
command: "uv",
|
|
512
|
+
args: ["sync", "--locked", "--no-build", "--no-install-project", "--no-default-groups"],
|
|
513
|
+
cwd: project.root,
|
|
514
|
+
timeoutMillis: 120_000,
|
|
515
|
+
});
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
function relativeImport(from, to) {
|
|
519
|
+
let value = relative(dirname(from), to).replaceAll("\\", "/");
|
|
520
|
+
if (!value.startsWith("."))
|
|
521
|
+
value = `./${value}`;
|
|
522
|
+
return value;
|
|
523
|
+
}
|
|
524
|
+
function markerBlock(plan) {
|
|
525
|
+
if (plan.language === "typescript") {
|
|
526
|
+
const modulePath = relativeImport(plan.entrypoint, "hue.setup.mjs");
|
|
527
|
+
return `// ${START_MARKER}\nimport { installHueExpress } from ${JSON.stringify(modulePath)};\n// ${END_MARKER}\n`;
|
|
528
|
+
}
|
|
529
|
+
return `# ${START_MARKER}\nfrom hue_setup import install_hue_flask\n# ${END_MARKER}\n`;
|
|
530
|
+
}
|
|
531
|
+
function callBlock(plan) {
|
|
532
|
+
return plan.language === "typescript"
|
|
533
|
+
? `\n// ${START_MARKER}\ninstallHueExpress(app, ${JSON.stringify(plan.requestPath)});\n// ${END_MARKER}\n`
|
|
534
|
+
: `\n# ${START_MARKER}\ninstall_hue_flask(app, ${JSON.stringify(plan.requestPath)})\n# ${END_MARKER}\n`;
|
|
535
|
+
}
|
|
536
|
+
function instrumentedSource(source, plan) {
|
|
537
|
+
const positions = syntax(source, plan.language);
|
|
538
|
+
const first = markerBlock(plan);
|
|
539
|
+
const second = callBlock({ ...plan, requestPath: positions.requestPath });
|
|
540
|
+
return (source.slice(0, positions.importOffset) +
|
|
541
|
+
first +
|
|
542
|
+
source.slice(positions.importOffset, positions.constructorEnd) +
|
|
543
|
+
second +
|
|
544
|
+
source.slice(positions.constructorEnd));
|
|
545
|
+
}
|
|
546
|
+
function unmanagedApplicationSource(source, plan) {
|
|
547
|
+
const first = markerBlock(plan);
|
|
548
|
+
const starts = source.split(START_MARKER).length - 1;
|
|
549
|
+
const ends = source.split(END_MARKER).length - 1;
|
|
550
|
+
if (!starts && !ends) {
|
|
551
|
+
if (/\b(?:installHueExpress|install_hue_flask)\b/u.test(source))
|
|
552
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "Existing Hue application wiring requires explicit review.");
|
|
553
|
+
return source;
|
|
554
|
+
}
|
|
555
|
+
const callPattern = plan.language === "typescript"
|
|
556
|
+
? /\n\/\/ Hue setup instrumentation \(managed; do not edit\)\ninstallHueExpress\(app, "[^"\n]*"\);\n\/\/ End Hue setup instrumentation\n/u
|
|
557
|
+
: /\n# Hue setup instrumentation \(managed; do not edit\)\ninstall_hue_flask\(app, "[^"\n]*"\)\n# End Hue setup instrumentation\n/u;
|
|
558
|
+
const stripped = source.replace(first, "").replace(callPattern, "");
|
|
559
|
+
let restored;
|
|
560
|
+
try {
|
|
561
|
+
restored = instrumentedSource(stripped, plan);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
/* Edited syntax is a managed-block conflict. */
|
|
565
|
+
}
|
|
566
|
+
if (starts !== 2 ||
|
|
567
|
+
ends !== 2 ||
|
|
568
|
+
!source.includes(first) ||
|
|
569
|
+
stripped === source ||
|
|
570
|
+
restored !== source)
|
|
571
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "The existing Hue instrumentation markers were edited or moved. Review the application wiring before rerunning setup.");
|
|
572
|
+
return stripped;
|
|
573
|
+
}
|
|
574
|
+
async function atomicSourceWrite(root, path, expectedSource, source) {
|
|
575
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
576
|
+
let handle;
|
|
577
|
+
try {
|
|
578
|
+
await safeDirectory(dirname(path));
|
|
579
|
+
const directory = await lstat(dirname(path), { bigint: true });
|
|
580
|
+
const original = await lstat(path, { bigint: true });
|
|
581
|
+
if (!original.isFile() || original.isSymbolicLink() || (original.mode & 3584n) !== 0n)
|
|
582
|
+
throw new Error("Unsafe setup application entrypoint");
|
|
583
|
+
const originalMode = Number(original.mode & 511n);
|
|
584
|
+
if ((await safeRead(root, relative(root, path))) !== expectedSource)
|
|
585
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "The application entrypoint changed after planning; Hue refused the concurrent edit.");
|
|
586
|
+
handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, originalMode);
|
|
587
|
+
await handle.writeFile(source, "utf8");
|
|
588
|
+
await handle.chmod(originalMode);
|
|
589
|
+
await handle.sync();
|
|
590
|
+
await handle.close();
|
|
591
|
+
handle = undefined;
|
|
592
|
+
await safeDirectory(dirname(path));
|
|
593
|
+
const existingDirectory = await lstat(dirname(path), { bigint: true });
|
|
594
|
+
const existing = await lstat(path, { bigint: true });
|
|
595
|
+
if (!existing.isFile() ||
|
|
596
|
+
existing.isSymbolicLink() ||
|
|
597
|
+
existing.dev !== original.dev ||
|
|
598
|
+
existing.ino !== original.ino ||
|
|
599
|
+
existing.mode !== original.mode ||
|
|
600
|
+
existing.mtimeNs !== original.mtimeNs ||
|
|
601
|
+
existing.ctimeNs !== original.ctimeNs ||
|
|
602
|
+
existingDirectory.dev !== directory.dev ||
|
|
603
|
+
existingDirectory.ino !== directory.ino ||
|
|
604
|
+
(await safeRead(root, relative(root, path))) !== expectedSource)
|
|
605
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "The application entrypoint changed while Hue was preparing its edit; no replacement was made.");
|
|
606
|
+
await rename(temporary, path);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
await handle?.close();
|
|
610
|
+
await safeDirectory(dirname(path));
|
|
611
|
+
await unlink(temporary).catch((error) => {
|
|
612
|
+
if (error.code !== "ENOENT")
|
|
613
|
+
throw error;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/** Adds only two owned wiring blocks around an existing app object; business logic is untouched. */
|
|
618
|
+
export async function wireSetupApplication(store, record, plan) {
|
|
619
|
+
const path = resolve(store.projectRoot, plan.entrypoint);
|
|
620
|
+
if (!inside(store.projectRoot, path))
|
|
621
|
+
throw new Error("Unsafe setup application path");
|
|
622
|
+
const source = await safeRead(store.projectRoot, plan.entrypoint);
|
|
623
|
+
const first = markerBlock(plan);
|
|
624
|
+
const second = callBlock(plan);
|
|
625
|
+
if (unmanagedApplicationSource(source, plan) !== source)
|
|
626
|
+
return undefined;
|
|
627
|
+
if (setupManagedDigest(source) !== plan.entryDigest)
|
|
628
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "The application entrypoint changed after detection; Hue refused to merge a concurrent edit.");
|
|
629
|
+
if (syntax(source, plan.language).requestPath !== plan.requestPath)
|
|
630
|
+
throw new SetupApplicationActionRequired("ambiguous-entrypoint", "The application entrypoint changed after detection; Hue made no wiring change.");
|
|
631
|
+
const next = instrumentedSource(source, plan);
|
|
632
|
+
await atomicSourceWrite(store.projectRoot, path, source, next);
|
|
633
|
+
record.managedFiles[`application:${plan.entrypoint}`] = setupManagedDigest(first + second);
|
|
634
|
+
await store.save(record);
|
|
635
|
+
return { path: plan.entrypoint, change: "updated" };
|
|
636
|
+
}
|
|
637
|
+
function validEvidence(value, version) {
|
|
638
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
639
|
+
throw new Error("Application instrumentation did not produce evidence");
|
|
640
|
+
const item = value;
|
|
641
|
+
if (Object.keys(item).sort().join("\0") !==
|
|
642
|
+
["credentialVersion", "source", "spanId", "traceId"].sort().join("\0") ||
|
|
643
|
+
item.source !== "existing-application-request" ||
|
|
644
|
+
item.credentialVersion !== version ||
|
|
645
|
+
typeof item.traceId !== "string" ||
|
|
646
|
+
!/^[a-f0-9]{32}$/u.test(item.traceId) ||
|
|
647
|
+
/^0+$/u.test(item.traceId) ||
|
|
648
|
+
typeof item.spanId !== "string" ||
|
|
649
|
+
!/^[a-f0-9]{16}$/u.test(item.spanId) ||
|
|
650
|
+
/^0+$/u.test(item.spanId))
|
|
651
|
+
throw new Error("Application instrumentation produced invalid evidence");
|
|
652
|
+
return {
|
|
653
|
+
source: "existing-application-request",
|
|
654
|
+
traceId: item.traceId,
|
|
655
|
+
spanId: item.spanId,
|
|
656
|
+
credentialVersion: version,
|
|
657
|
+
verified: false,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/** Starts the existing entrypoint without a shell and exercises one existing HTTP GET route. */
|
|
661
|
+
export async function exerciseSetupApplication(store, record, plan, signal, deadlines = { readinessMillis: 10_000, requestMillis: 10_000, evidenceMillis: 10_000 }) {
|
|
662
|
+
if (!record.credential)
|
|
663
|
+
throw new Error("Setup credential is not available");
|
|
664
|
+
applicationRequestUrl(plan.requestPath, "http://127.0.0.1:1");
|
|
665
|
+
await safeRead(store.projectRoot, plan.entrypoint);
|
|
666
|
+
if (plan.language === "typescript" && (plan.runtime === "bun" || plan.manager === "bun"))
|
|
667
|
+
await bunConfiguration(store.projectRoot);
|
|
668
|
+
if (plan.language === "python")
|
|
669
|
+
await flaskConfiguration(store.projectRoot);
|
|
670
|
+
if (record.applicationAttempt)
|
|
671
|
+
throw new SetupApplicationActionRequired("custom-instrumentation", "Hue already attempted this application request. It will not replay business work after missing telemetry evidence or account claim.");
|
|
672
|
+
record.applicationAttempt = {
|
|
673
|
+
credentialVersion: record.credential.version,
|
|
674
|
+
startedAt: new Date().toISOString(),
|
|
675
|
+
};
|
|
676
|
+
await store.save(record);
|
|
677
|
+
await store.removeApplicationEvidence();
|
|
678
|
+
const port = deadlines.port ?? randomInt(20_000, 60_000);
|
|
679
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
680
|
+
throw new Error("Invalid application port");
|
|
681
|
+
const socketProof = randomBytes(32).toString("base64url");
|
|
682
|
+
const command = plan.language === "typescript"
|
|
683
|
+
? plan.runtime === "bun"
|
|
684
|
+
? "bun"
|
|
685
|
+
: process.versions.bun
|
|
686
|
+
? "node"
|
|
687
|
+
: process.execPath
|
|
688
|
+
: "uv";
|
|
689
|
+
const args = plan.language === "typescript"
|
|
690
|
+
? [
|
|
691
|
+
...(plan.runtime === "bun" ? ["--no-env-file", "--config=/dev/null"] : []),
|
|
692
|
+
...(plan.sourceMaps ? ["--enable-source-maps"] : []),
|
|
693
|
+
plan.entrypoint,
|
|
694
|
+
]
|
|
695
|
+
: ["run", "--frozen", "--no-build", "--no-sync", "python", plan.entrypoint];
|
|
696
|
+
const child = spawn(command, args, {
|
|
697
|
+
cwd: store.projectRoot,
|
|
698
|
+
env: {
|
|
699
|
+
...process.env,
|
|
700
|
+
PORT: String(port),
|
|
701
|
+
HUE_SETUP_EVIDENCE_FILE: store.applicationEvidencePath,
|
|
702
|
+
HUE_SETUP_SOCKET_PROOF: socketProof,
|
|
703
|
+
},
|
|
704
|
+
shell: false,
|
|
705
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
706
|
+
});
|
|
707
|
+
let outputBytes = 0;
|
|
708
|
+
const count = (chunk) => {
|
|
709
|
+
outputBytes += chunk.byteLength;
|
|
710
|
+
if (outputBytes > 64 * 1024)
|
|
711
|
+
child.kill("SIGKILL");
|
|
712
|
+
};
|
|
713
|
+
child.stdout.on("data", count);
|
|
714
|
+
child.stderr.on("data", count);
|
|
715
|
+
let launchFailed = false;
|
|
716
|
+
child.once("error", () => {
|
|
717
|
+
launchFailed = true;
|
|
718
|
+
});
|
|
719
|
+
try {
|
|
720
|
+
if (child.pid === undefined)
|
|
721
|
+
throw new Error("The application runtime is unavailable");
|
|
722
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
723
|
+
const requestUrl = applicationRequestUrl(plan.requestPath, origin);
|
|
724
|
+
const socket = await connectOwnedApplication(port, socketProof, () => !launchFailed && child.exitCode === null, deadlines.readinessMillis, signal);
|
|
725
|
+
await requestOwnedApplication(socket, requestUrl, deadlines.requestMillis, signal);
|
|
726
|
+
const deadline = Date.now() + deadlines.evidenceMillis;
|
|
727
|
+
for (;;) {
|
|
728
|
+
if (signal?.aborted)
|
|
729
|
+
throw new Error("Setup interrupted");
|
|
730
|
+
try {
|
|
731
|
+
const info = await lstat(store.applicationEvidencePath);
|
|
732
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 4096)
|
|
733
|
+
throw new Error("Unsafe application evidence file");
|
|
734
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0)
|
|
735
|
+
throw new Error("Application evidence must use mode 0600");
|
|
736
|
+
const evidence = validEvidence(JSON.parse(await safeRead(store.projectRoot, relative(store.projectRoot, store.applicationEvidencePath))), record.credential.version);
|
|
737
|
+
record.applicationEvidence = evidence;
|
|
738
|
+
await store.save(record);
|
|
739
|
+
await store.removeApplicationEvidence();
|
|
740
|
+
return evidence;
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
if (error.code !== "ENOENT")
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
if (Date.now() >= deadline)
|
|
747
|
+
throw new Error("The application request did not produce bounded Hue evidence");
|
|
748
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
child.kill("SIGTERM");
|
|
753
|
+
await new Promise((resolvePromise) => {
|
|
754
|
+
if (child.exitCode !== null || child.pid === undefined || launchFailed)
|
|
755
|
+
resolvePromise();
|
|
756
|
+
else {
|
|
757
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 1000);
|
|
758
|
+
child.once("close", () => {
|
|
759
|
+
clearTimeout(timer);
|
|
760
|
+
resolvePromise();
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
await store.removeApplicationEvidence().catch(() => undefined);
|
|
765
|
+
}
|
|
766
|
+
}
|