@hue-run/sdk 0.3.2 → 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.
Files changed (43) hide show
  1. package/CLI.md +270 -47
  2. package/ENVIRONMENTS.md +10 -0
  3. package/README.md +19 -3
  4. package/dist/client.d.ts +5 -5
  5. package/dist/client.js +13 -6
  6. package/dist/environment/tools.d.ts +6 -1
  7. package/dist/environment/tools.js +7 -1
  8. package/dist/environment/types.d.ts +6 -1
  9. package/dist/receipt.js +36 -8
  10. package/dist/setup/application.d.ts +74 -0
  11. package/dist/setup/application.js +766 -0
  12. package/dist/setup/backend.d.ts +229 -0
  13. package/dist/setup/backend.js +855 -0
  14. package/dist/setup/checkpoint.js +100 -30
  15. package/dist/setup/cli.js +20 -4
  16. package/dist/setup/configure.d.ts +13 -0
  17. package/dist/setup/configure.js +454 -0
  18. package/dist/setup/credential.d.ts +2 -0
  19. package/dist/setup/credential.js +9 -0
  20. package/dist/setup/detect.js +4 -1
  21. package/dist/setup/installation.d.ts +118 -0
  22. package/dist/setup/installation.js +605 -0
  23. package/dist/setup/lock.d.ts +2 -0
  24. package/dist/setup/lock.js +38 -0
  25. package/dist/setup/machine.d.ts +1 -10
  26. package/dist/setup/machine.js +8 -7
  27. package/dist/setup/render.d.ts +3 -1
  28. package/dist/setup/render.js +209 -6
  29. package/dist/setup/runner.d.ts +26 -76
  30. package/dist/setup/runner.js +320 -45
  31. package/dist/setup/socket.d.ts +7 -0
  32. package/dist/setup/socket.js +144 -0
  33. package/dist/setup/source.d.ts +9 -0
  34. package/dist/setup/source.js +269 -0
  35. package/dist/setup/types.d.ts +16 -9
  36. package/dist/setup/types.js +1 -1
  37. package/dist/setup.d.ts +6 -2
  38. package/dist/setup.js +3 -0
  39. package/dist/types.d.ts +24 -0
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +2 -1
  43. package/setup-events.schema.json +16 -9
@@ -1,6 +1,14 @@
1
+ import { SetupBackendError } from "./backend.js";
2
+ import { SetupApplicationActionRequired } from "./application.js";
3
+ import { acquireSetupCommandLock } from "./lock.js";
1
4
  import { createInitialSetupState, transitionSetup } from "./machine.js";
5
+ import { redactSetupTranscriptText } from "./render.js";
2
6
  import { SETUP_EVENT_CONTRACT_VERSION, } from "./types.js";
3
- /** Runs one installer setup-session command and emits one terminal event; it never launches a Hue Run. */
7
+ function rejectTerminal(status) {
8
+ if (status.state === "expired" || status.state === "purged")
9
+ throw new SetupBackendError("SETUP_EXPIRED", "This setup installation is terminal and will not be replaced automatically.", 410);
10
+ }
11
+ /** Runs one resumable setup command and emits exactly one terminal event. */
4
12
  export async function runSetup(options) {
5
13
  const now = options.now ?? (() => new Date());
6
14
  let sequence = 0;
@@ -23,7 +31,14 @@ export async function runSetup(options) {
23
31
  await options.emit(event);
24
32
  };
25
33
  let state;
34
+ let releaseLock;
26
35
  try {
36
+ if (options.backend) {
37
+ releaseLock = await acquireSetupCommandLock(options.projectRoot);
38
+ options.backend.resetLocalCache();
39
+ }
40
+ if (options.claimRestart && (options.command !== "claim" || options.mode !== "human"))
41
+ throw new Error("Refusing non-human browser handoff restart");
27
42
  state = await options.checkpoints.load(options.runId, options.projectRoot);
28
43
  await emit({
29
44
  event: "run.started",
@@ -34,84 +49,344 @@ export async function runSetup(options) {
34
49
  if (options.signal?.aborted)
35
50
  throw new Error("Setup interrupted");
36
51
  if (options.command === "status") {
52
+ if (!options.backend) {
53
+ await emit({
54
+ event: "diagnostic",
55
+ level: "info",
56
+ code: state ? `checkpoint.${state.phase}` : "checkpoint.absent",
57
+ message: state
58
+ ? `Checkpoint phase: ${state.phase}.`
59
+ : "No setup checkpoint exists for this project.",
60
+ });
61
+ await emit({ event: "run.completed", outcome: "unchanged", checkpointed: !!state });
62
+ return { outcome: "unchanged", state };
63
+ }
64
+ if (!state) {
65
+ const local = await options.backend.localInstallation();
66
+ if (!local) {
67
+ await emit({
68
+ event: "diagnostic",
69
+ level: "info",
70
+ code: "installation.absent",
71
+ message: "No Hue setup installation exists for this project and origin.",
72
+ });
73
+ await emit({ event: "run.completed", outcome: "unchanged", checkpointed: false });
74
+ return { outcome: "unchanged" };
75
+ }
76
+ }
77
+ }
78
+ if (!state) {
79
+ if (options.command === "resume" || options.command === "claim")
80
+ throw new Error("No setup checkpoint exists for this project");
81
+ state = createInitialSetupState(options.runId, options.projectRoot);
82
+ await options.checkpoints.save(state);
83
+ }
84
+ // A checkpoint is progress, never authority for current manifests or ownership.
85
+ {
86
+ state = createInitialSetupState(options.runId, options.projectRoot);
87
+ const first = transitionSetup(state, { type: "start" });
88
+ state = first.state;
89
+ await options.checkpoints.save(state);
90
+ for (const event of first.events)
91
+ await emit(event);
92
+ if (options.signal?.aborted)
93
+ throw new Error("Setup interrupted");
94
+ const project = await options.project.detect(first.effect.root, options.signal);
95
+ if (options.signal?.aborted)
96
+ throw new Error("Setup interrupted");
97
+ const second = transitionSetup(state, { type: "project.detected", project });
98
+ state = second.state;
99
+ await options.checkpoints.save(state);
100
+ for (const event of second.events)
101
+ await emit(event);
102
+ }
103
+ if (state.phase !== "local-ready")
104
+ throw new Error("Setup project detection did not complete");
105
+ if (!options.backend) {
106
+ await emit({
107
+ event: "action.required",
108
+ action: "configure",
109
+ message: "A SetupBackendAdapter is required to continue; no success was fabricated.",
110
+ });
111
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
112
+ return { outcome: "action_required", state };
113
+ }
114
+ const project = state.project;
115
+ let status;
116
+ const local = await options.backend.localInstallation();
117
+ if (options.command === "status" && !local) {
37
118
  await emit({
38
119
  event: "diagnostic",
39
120
  level: "info",
40
- code: state ? `checkpoint.${state.phase}` : "checkpoint.absent",
41
- message: state
42
- ? `Checkpoint phase: ${state.phase}.`
43
- : "No setup checkpoint exists for this project.",
121
+ code: "installation.absent",
122
+ message: "No Hue setup installation exists for this project and origin.",
123
+ });
124
+ await emit({ event: "run.completed", outcome: "unchanged", checkpointed: true });
125
+ return { outcome: "unchanged", state };
126
+ }
127
+ if (options.command === "claim" && !local)
128
+ throw new Error("No Hue setup installation exists for this project and origin");
129
+ try {
130
+ const availability = await options.backend.preflight(project, options.signal, options.command === "setup" || options.command === "resume");
131
+ if ((options.command === "setup" || options.command === "resume") &&
132
+ availability?.state === "inactive") {
133
+ await emit({
134
+ event: "diagnostic",
135
+ level: "warning",
136
+ code: "setup.inactive",
137
+ message: "Hue anonymous setup is currently inactive; no project or installation files were changed.",
138
+ });
139
+ await emit({
140
+ event: "action.required",
141
+ action: "configure",
142
+ message: "Rerun hue resume after Hue setup admissions are available.",
143
+ command: "hue resume",
144
+ });
145
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
146
+ return { outcome: "action_required", state };
147
+ }
148
+ if (availability) {
149
+ await emit({
150
+ event: "privacy.notice",
151
+ privacyUrl: availability.privacyNotice.url,
152
+ effectiveDate: availability.privacyNotice.effectiveDate,
153
+ securityUrl: availability.securityUrl,
154
+ });
155
+ }
156
+ }
157
+ catch (error) {
158
+ if (!(error instanceof SetupApplicationActionRequired))
159
+ throw error;
160
+ await emit({
161
+ event: "action.required",
162
+ action: error.code === "ambiguous-project" ? "select-project" : "integrate-application",
163
+ message: error.message,
164
+ command: "hue resume",
165
+ });
166
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
167
+ return { outcome: "action_required", state };
168
+ }
169
+ if (options.command === "setup" || options.command === "resume") {
170
+ await emit({ event: "step.started", step: "install-runtime" });
171
+ const installed = await options.backend.installRuntime(project);
172
+ await emit({
173
+ event: "step.completed",
174
+ step: "install-runtime",
175
+ outcome: installed ? "changed" : "unchanged",
44
176
  });
177
+ await options.backend.prepare();
178
+ // Once a credential exists, status is recovery and spends no provisioning admission.
179
+ status = local?.credential
180
+ ? await options.backend.status(options.signal)
181
+ : await options.backend.provision(options.signal);
182
+ rejectTerminal(status);
183
+ if (status.state === "active")
184
+ await emit({
185
+ event: "trial.created",
186
+ trialId: status.installationId,
187
+ expiresAt: status.expiresAt,
188
+ });
189
+ }
190
+ else {
191
+ status = await options.backend.status(options.signal);
192
+ rejectTerminal(status);
193
+ }
194
+ if (options.command === "status" && status.state === "active") {
45
195
  await emit({
46
- event: "run.completed",
47
- outcome: "unchanged",
48
- checkpointed: state !== undefined,
196
+ event: "diagnostic",
197
+ level: "info",
198
+ code: "installation.active",
199
+ message: "The metadata-only setup installation is active and awaiting account claim.",
49
200
  });
201
+ await emit({ event: "run.completed", outcome: "unchanged", checkpointed: true });
50
202
  return { outcome: "unchanged", state };
51
203
  }
52
- if (options.command === "claim") {
204
+ if (options.command === "claim" &&
205
+ status.state === "active" &&
206
+ local?.applicationEvidence?.verified &&
207
+ local.applicationEvidence.credentialVersion === 0) {
208
+ const handoff = await options.backend.prepareClaimHandoff(status, options.mode === "human", options.claimRestart === true, options.signal);
209
+ await emit({
210
+ event: "claim.required",
211
+ claimId: status.installationId,
212
+ });
53
213
  await emit({
54
214
  event: "action.required",
55
- action: "claim-project",
56
- message: "Project claim is not available in this build; no backend request was made. A future adapter must require a verified anonymous telemetry receipt first.",
215
+ action: handoff.restartRequired ? "restart-claim-handoff" : "open-claim-handoff",
216
+ message: handoff.restartRequired
217
+ ? "The one-time browser handoff is no longer usable. The project owner may explicitly replace it from an interactive local terminal."
218
+ : handoff.opened
219
+ ? "Finish account linkage in the browser opened from the owner-only local handoff, then rerun hue claim."
220
+ : "Ask the project owner to run hue claim in an interactive local terminal, finish account linkage in the browser, then rerun hue claim.",
221
+ command: handoff.restartRequired ? "hue claim --restart" : "hue claim",
57
222
  });
223
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
224
+ return { outcome: "action_required", state };
225
+ }
226
+ const before = await options.backend.prepare();
227
+ let oldCredential = status.state === "claimed" && before.credential?.version === 0
228
+ ? before.credential.apiKey
229
+ : status.state === "claimed"
230
+ ? before.revocationCredential?.apiKey
231
+ : undefined;
232
+ let refreshed = false;
233
+ for (;;) {
234
+ try {
235
+ await options.backend.credentials(status.credentialVersion, options.signal);
236
+ break;
237
+ }
238
+ catch (error) {
239
+ if (error instanceof SetupBackendError && error.code === "SETUP_CHANGED" && !refreshed) {
240
+ refreshed = true;
241
+ status = await options.backend.status(options.signal);
242
+ rejectTerminal(status);
243
+ if (status.state === "claimed" && before.credential?.version === 0)
244
+ oldCredential = before.credential.apiKey;
245
+ else if (status.state === "claimed" && before.revocationCredential)
246
+ oldCredential = before.revocationCredential.apiKey;
247
+ continue;
248
+ }
249
+ if (error instanceof SetupBackendError && error.code === "SETUP_REVOKED") {
250
+ await emit({
251
+ event: "action.required",
252
+ action: "configure",
253
+ message: "The managed setup key was revoked. An account owner must explicitly rotate an account-managed telemetry key; setup will not resurrect it.",
254
+ });
255
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
256
+ return { outcome: "action_required", state };
257
+ }
258
+ throw error;
259
+ }
260
+ }
261
+ await emit({ event: "step.started", step: "configure-telemetry" });
262
+ const changes = await options.backend.configure(project);
263
+ for (const change of changes)
264
+ await emit({ event: "file.changed", ...change });
265
+ await emit({
266
+ event: "step.completed",
267
+ step: "configure-telemetry",
268
+ outcome: changes.length ? "changed" : "unchanged",
269
+ });
270
+ await emit({ event: "step.started", step: "verify-application-receipt" });
271
+ const application = await options.backend.prepare();
272
+ if (!application.applicationEvidence && status.state === "claimed") {
58
273
  await emit({
59
- event: "run.completed",
60
- outcome: "action_required",
61
- checkpointed: state !== undefined,
274
+ event: "action.required",
275
+ action: "run-instrumented-request",
276
+ message: "The preserved initial application evidence is unavailable. Setup will not replay business work automatically; run one explicit instrumented request and rerun hue claim.",
277
+ command: "hue claim",
62
278
  });
279
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
63
280
  return { outcome: "action_required", state };
64
281
  }
65
- if (!state) {
66
- if (options.command === "resume")
67
- throw new Error("No setup checkpoint exists for this project");
68
- state = createInitialSetupState(options.runId, options.projectRoot);
69
- await options.checkpoints.save(state);
282
+ if (!application.applicationEvidence) {
283
+ try {
284
+ await options.backend.exerciseApplication(project, options.signal);
285
+ }
286
+ catch (error) {
287
+ if (!(error instanceof SetupApplicationActionRequired))
288
+ throw error;
289
+ await emit({
290
+ event: "action.required",
291
+ action: "run-instrumented-request",
292
+ message: error.message,
293
+ });
294
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
295
+ return { outcome: "action_required", state };
296
+ }
70
297
  }
71
- if (state.phase === "local-ready") {
72
- await emit({ event: "project.detected", project: state.project });
73
- await emit({ event: "plan.ready", plan: state.plan });
298
+ const evidence = await options.backend.verifyApplication(options.signal);
299
+ if (!evidence) {
300
+ await emit({
301
+ event: "diagnostic",
302
+ level: "warning",
303
+ code: "application.unverified",
304
+ message: "The existing application request was exported, but exact stored receipt evidence did not arrive within the bounded deadline.",
305
+ });
74
306
  await emit({
75
307
  event: "action.required",
76
- action: "configure",
77
- message: "Local inspection is complete. Telemetry configuration is not available in this build; no project files were changed.",
308
+ action: "run-instrumented-request",
309
+ message: "Run hue resume to retry only exact receipt verification; the business request will not be replayed.",
310
+ command: "hue resume",
78
311
  });
79
312
  await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
80
313
  return { outcome: "action_required", state };
81
314
  }
82
- const first = transitionSetup(state, { type: "start" });
83
- state = first.state;
84
- await options.checkpoints.save(state);
85
- for (const event of first.events)
86
- await emit(event);
87
- if (options.signal?.aborted)
88
- throw new Error("Setup interrupted");
89
- const project = await options.project.detect(first.effect.root, options.signal);
90
- if (options.signal?.aborted)
91
- throw new Error("Setup interrupted");
92
- const second = transitionSetup(state, { type: "project.detected", project });
93
- state = second.state;
94
- await options.checkpoints.save(state);
95
- for (const event of second.events)
96
- await emit(event);
97
- await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
98
- return { outcome: "action_required", state };
315
+ await emit({
316
+ event: "receipt.verified",
317
+ receiptId: evidence.traceId,
318
+ traceId: evidence.traceId,
319
+ source: "repository-http-boundary",
320
+ });
321
+ await emit({
322
+ event: "step.completed",
323
+ step: "verify-application-receipt",
324
+ outcome: "verified",
325
+ });
326
+ if (status.state === "active") {
327
+ const handoff = options.mode === "human"
328
+ ? await options.backend.prepareClaimHandoff(status, true, false, options.signal)
329
+ : { opened: false, state: "pending", restartRequired: false };
330
+ await emit({
331
+ event: "claim.required",
332
+ claimId: status.installationId,
333
+ });
334
+ await emit({
335
+ event: "action.required",
336
+ action: "open-claim-handoff",
337
+ message: handoff.opened
338
+ ? "An existing application request and its exact receipt are verified. Finish account linkage in the browser opened from the owner-only local handoff, then rerun hue claim."
339
+ : "An existing application request and its exact receipt are verified. Ask the project owner to run hue claim in an interactive local terminal and finish account linkage, then rerun hue claim.",
340
+ command: "hue claim",
341
+ });
342
+ await emit({ event: "run.completed", outcome: "action_required", checkpointed: true });
343
+ return { outcome: "action_required", state };
344
+ }
345
+ if (oldCredential)
346
+ await options.backend.verifyRevokedCredential(oldCredential, evidence, options.signal);
347
+ if (!(await options.backend.prepare()).anonymousKeyRevoked)
348
+ throw new SetupBackendError("unverified", "The original anonymous credential is unavailable for revocation verification; claim reconciliation is unverified.");
349
+ await emit({ event: "claim.completed", claimId: status.installationId });
350
+ await emit({
351
+ event: "diagnostic",
352
+ level: "info",
353
+ code: "claim.reconciled",
354
+ message: "The replacement credential can access the preserved application receipt, and the superseded anonymous key was refused.",
355
+ });
356
+ await emit({ event: "run.completed", outcome: "ready", checkpointed: true });
357
+ return { outcome: "ready", state };
99
358
  }
100
359
  catch (error) {
101
360
  if (!terminal) {
102
361
  const interrupted = options.signal?.aborted ||
103
362
  (error instanceof Error && error.message === "Setup interrupted");
363
+ const backendCode = error instanceof SetupBackendError ? error.code.toLowerCase() : undefined;
364
+ const missing = error instanceof Error &&
365
+ (error.message.startsWith("No setup checkpoint") ||
366
+ error.message.startsWith("No Hue setup installation"));
367
+ const conflict = error instanceof Error && error.message.startsWith("Refusing");
104
368
  await emit({
105
369
  event: "run.failed",
106
- code: interrupted ? "interrupted" : "setup_failed",
370
+ code: interrupted
371
+ ? "interrupted"
372
+ : conflict
373
+ ? "configuration_conflict"
374
+ : (backendCode ?? "setup_failed"),
107
375
  message: interrupted
108
376
  ? "Setup session was interrupted and can be resumed."
109
- : error instanceof Error && error.message.startsWith("No setup checkpoint")
377
+ : missing
110
378
  ? error.message
111
- : "Setup session could not complete. No credentials were stored.",
112
- resumable: state !== undefined,
379
+ : conflict
380
+ ? error.message
381
+ : error instanceof SetupBackendError
382
+ ? redactSetupTranscriptText(error.message)
383
+ : "Setup session could not complete. Local resumable state was preserved.",
384
+ resumable: state !== undefined && !missing,
113
385
  });
114
386
  }
115
387
  throw error;
116
388
  }
389
+ finally {
390
+ await releaseLock?.();
391
+ }
117
392
  }
@@ -0,0 +1,7 @@
1
+ import { type Socket } from "node:net";
2
+ /** Private attempt framing, never an HTTP header or public event. */
3
+ export declare function setupSocketPreface(nonce: string, serverPort: number, clientPort: number): Buffer;
4
+ /** Connect without sending anything; only the launched app knows this attempt's proof. */
5
+ export declare function connectOwnedApplication(port: number, nonce: string, stillRunning: () => boolean, timeoutMillis: number, signal?: AbortSignal): Promise<Socket>;
6
+ /** One HTTP request on exactly the authenticated connection. Never dial, redirect or retry. */
7
+ export declare function requestOwnedApplication(socket: Socket, url: URL, timeoutMillis: number, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,144 @@
1
+ import { Agent, request } from "node:http";
2
+ import { createConnection } from "node:net";
3
+ import { createHmac } from "node:crypto";
4
+ /** Private attempt framing, never an HTTP header or public event. */
5
+ export function setupSocketPreface(nonce, serverPort, clientPort) {
6
+ if (!/^[A-Za-z0-9_-]{43}$/u.test(nonce))
7
+ throw new Error("Invalid application ownership challenge");
8
+ const digest = createHmac("sha256", Buffer.from(nonce, "base64url"))
9
+ .update(`hue-setup-owned-v1\0${serverPort}\0${clientPort}`)
10
+ .digest("base64url");
11
+ return Buffer.from(`Hue-setup-owned:${digest}\n`, "ascii");
12
+ }
13
+ /** Connect without sending anything; only the launched app knows this attempt's proof. */
14
+ export async function connectOwnedApplication(port, nonce, stillRunning, timeoutMillis, signal) {
15
+ const deadline = Date.now() + timeoutMillis;
16
+ while (Date.now() < deadline) {
17
+ if (signal?.aborted)
18
+ throw new Error("Setup interrupted");
19
+ if (!stillRunning())
20
+ throw new Error("The application exited before verification");
21
+ const owned = await new Promise((resolve, reject) => {
22
+ const socket = createConnection({ host: "127.0.0.1", port });
23
+ let settled = false;
24
+ let received = Buffer.alloc(0);
25
+ let expected;
26
+ const finish = (success, refused = false) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ clearTimeout(timer);
31
+ signal?.removeEventListener("abort", abort);
32
+ socket.removeAllListeners();
33
+ if (success && stillRunning()) {
34
+ socket.pause();
35
+ // A peer reset between authentication and request creation must not be unhandled.
36
+ socket.on("error", () => undefined);
37
+ resolve(socket);
38
+ }
39
+ else {
40
+ socket.destroy();
41
+ if (refused)
42
+ resolve(undefined);
43
+ else
44
+ reject(new Error("The application socket did not prove ownership; no HTTP request was sent"));
45
+ }
46
+ };
47
+ const abort = () => finish(false);
48
+ const timer = setTimeout(abort, Math.max(1, Math.min(500, deadline - Date.now())));
49
+ signal?.addEventListener("abort", abort, { once: true });
50
+ socket.once("connect", () => {
51
+ expected = setupSocketPreface(nonce, socket.remotePort, socket.localPort);
52
+ });
53
+ socket.on("data", (data) => {
54
+ if (!expected || data.length > expected.length - received.length) {
55
+ finish(false);
56
+ return;
57
+ }
58
+ received = Buffer.concat([received, data]);
59
+ if (received.length > expected.length ||
60
+ !expected.subarray(0, received.length).equals(received))
61
+ finish(false);
62
+ else if (received.length === expected.length)
63
+ finish(true);
64
+ });
65
+ socket.once("error", (error) => finish(false, !expected && error.code === "ECONNREFUSED"));
66
+ socket.once("end", abort);
67
+ socket.once("close", abort);
68
+ });
69
+ if (owned)
70
+ return owned;
71
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))));
72
+ }
73
+ throw new Error("The application did not prove socket ownership within the setup deadline");
74
+ }
75
+ /** One HTTP request on exactly the authenticated connection. Never dial, redirect or retry. */
76
+ export async function requestOwnedApplication(socket, url, timeoutMillis, signal) {
77
+ if (socket.destroyed || socket.readableEnded || !socket.writable || signal?.aborted) {
78
+ socket.destroy();
79
+ throw new Error("The owned application connection closed before its request");
80
+ }
81
+ const agent = new Agent({ keepAlive: false, maxSockets: 1, maxTotalSockets: 1 });
82
+ let assigned = false;
83
+ agent.createConnection = () => {
84
+ if (assigned || socket.destroyed)
85
+ throw new Error("Refusing an application connection replacement");
86
+ assigned = true;
87
+ return socket;
88
+ };
89
+ try {
90
+ await new Promise((resolve, reject) => {
91
+ let finished = false;
92
+ const done = (error) => {
93
+ if (finished)
94
+ return;
95
+ finished = true;
96
+ clearTimeout(timer);
97
+ signal?.removeEventListener("abort", abort);
98
+ if (error)
99
+ reject(error);
100
+ else
101
+ resolve();
102
+ };
103
+ const fail = () => done(new Error("The single application request did not complete; it will not be replayed"));
104
+ const outgoing = request(url, { agent, method: "GET", maxHeaderSize: 16 * 1024 });
105
+ const abort = () => {
106
+ outgoing.destroy();
107
+ fail();
108
+ };
109
+ const timer = setTimeout(abort, timeoutMillis);
110
+ signal?.addEventListener("abort", abort, { once: true });
111
+ outgoing.once("socket", (actual) => {
112
+ if (actual !== socket) {
113
+ actual.destroy();
114
+ abort();
115
+ }
116
+ });
117
+ outgoing.once("error", fail);
118
+ outgoing.once("response", (response) => {
119
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
120
+ response.destroy();
121
+ done(new Error("The selected application handler did not return a successful response; it will not be replayed"));
122
+ return;
123
+ }
124
+ let bytes = 0;
125
+ response.on("data", (data) => {
126
+ bytes += data.length;
127
+ if (bytes > 1024 * 1024)
128
+ abort();
129
+ });
130
+ response.once("error", fail);
131
+ response.once("aborted", fail);
132
+ response.once("end", () => done());
133
+ });
134
+ outgoing.end();
135
+ socket.resume();
136
+ if (signal?.aborted)
137
+ abort();
138
+ });
139
+ }
140
+ finally {
141
+ agent.destroy();
142
+ socket.destroy();
143
+ }
144
+ }
@@ -0,0 +1,9 @@
1
+ export interface ApplicationSyntax {
2
+ constructorEnd: number;
3
+ importOffset: number;
4
+ requestPath: string;
5
+ }
6
+ /** Syntax inspection only: no application imports, evaluation, or package lifecycle. */
7
+ export declare function inspectExpressSource(source: string): ApplicationSyntax;
8
+ /** An isolated stdlib parser never imports/executes the customer's Python module. */
9
+ export declare function inspectFlaskSource(source: string): ApplicationSyntax;