@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
@@ -0,0 +1,454 @@
1
+ import { constants } from "node:fs";
2
+ import { link, lstat, open, readdir, realpath, rename, unlink } from "node:fs/promises";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { setupManagedDigest, } from "./installation.js";
6
+ async function cleanupTemporary(path) {
7
+ try {
8
+ const info = await lstat(path);
9
+ if (info.isFile())
10
+ await unlink(path);
11
+ }
12
+ catch (error) {
13
+ if (error.code !== "ENOENT")
14
+ throw error;
15
+ }
16
+ }
17
+ function inside(parent, child) {
18
+ const path = relative(parent, child);
19
+ return path === "" || (!path.startsWith("..") && !path.startsWith("/"));
20
+ }
21
+ async function safeDirectory(path) {
22
+ const paths = [];
23
+ let current = resolve(path);
24
+ for (;;) {
25
+ paths.unshift(current);
26
+ const parent = dirname(current);
27
+ if (parent === current)
28
+ break;
29
+ current = parent;
30
+ }
31
+ for (const entry of paths) {
32
+ const info = await lstat(entry);
33
+ if (info.isSymbolicLink() || !info.isDirectory())
34
+ throw new Error("Refusing a setup configuration path with a symlink ancestor");
35
+ }
36
+ if ((await realpath(path)) !== resolve(path))
37
+ throw new Error("Refusing a setup configuration path outside its actual directory");
38
+ }
39
+ async function readManaged(path) {
40
+ await safeDirectory(dirname(path));
41
+ let handle;
42
+ try {
43
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
44
+ }
45
+ catch (error) {
46
+ if (error.code === "ENOENT")
47
+ return undefined;
48
+ throw new Error("Refusing unsafe setup configuration");
49
+ }
50
+ try {
51
+ const info = await handle.stat();
52
+ if (!info.isFile() || info.size > 1024 * 1024 || (info.mode & 0o7000) !== 0)
53
+ throw new Error("Refusing unsafe setup configuration");
54
+ const contents = await handle.readFile("utf8");
55
+ const after = await handle.stat();
56
+ if (info.mtimeMs !== after.mtimeMs ||
57
+ info.ctimeMs !== after.ctimeMs ||
58
+ info.size !== after.size)
59
+ throw new Error("Refusing setup configuration changed while reading");
60
+ return { contents, info };
61
+ }
62
+ finally {
63
+ await handle.close();
64
+ }
65
+ }
66
+ function sameSnapshot(left, right) {
67
+ if (!left || !right)
68
+ return left === right;
69
+ return (left.contents === right.contents &&
70
+ left.info.dev === right.info.dev &&
71
+ left.info.ino === right.info.ino &&
72
+ left.info.mode === right.info.mode &&
73
+ left.info.mtimeMs === right.info.mtimeMs &&
74
+ left.info.ctimeMs === right.info.ctimeMs);
75
+ }
76
+ function serviceName(root) {
77
+ const normalized = basename(root).replace(/[^A-Za-z0-9_.-]+/gu, "-");
78
+ let start = 0;
79
+ let end = normalized.length;
80
+ while (normalized[start] === "-")
81
+ start += 1;
82
+ while (end > start && normalized[end - 1] === "-")
83
+ end -= 1;
84
+ const value = normalized.slice(start, end).slice(0, 220);
85
+ return value ? `hue-setup-${value}` : "hue-setup-project";
86
+ }
87
+ function typescriptConfig(store) {
88
+ return `// Managed by Hue setup. This file contains no credential.
89
+ import { closeSync, constants, fsyncSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
90
+ import { fileURLToPath } from "node:url";
91
+ import { createHmac } from "node:crypto";
92
+ import { createServer, createConnection } from "node:net";
93
+ import { createHue } from "@hue-run/sdk";
94
+ import { context, createContextKey, ROOT_CONTEXT, SpanKind } from "@opentelemetry/api";
95
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
96
+
97
+ // Generated application bootstrap owns this policy; the SDK core registers nothing.
98
+ const contextCheck = createContextKey("hue.setup.context-check");
99
+ async function hasWorkingContext() {
100
+ try {
101
+ return await context.with(ROOT_CONTEXT.setValue(contextCheck, true), async () => {
102
+ await Promise.resolve();
103
+ await new Promise((resolve) => setImmediate(resolve));
104
+ return context.active().getValue(contextCheck) === true;
105
+ });
106
+ } catch { return false; }
107
+ }
108
+ let ownedContext;
109
+ if (!(await hasWorkingContext())) {
110
+ const candidate = new AsyncLocalStorageContextManager().enable();
111
+ if (!context.setGlobalContextManager(candidate)) {
112
+ candidate.disable();
113
+ throw new Error("Existing OpenTelemetry context ownership is unsupported; review the application bootstrap");
114
+ }
115
+ ownedContext = candidate;
116
+ if (!(await hasWorkingContext())) {
117
+ ownedContext.disable();
118
+ throw new Error("This runtime does not support the setup context integration");
119
+ }
120
+ }
121
+ process.once("beforeExit", () => { ownedContext?.disable(); ownedContext = undefined; });
122
+
123
+ const installation = JSON.parse(
124
+ readFileSync(fileURLToPath(new URL("./.hue/${basename(store.path)}", import.meta.url)), "utf8"),
125
+ );
126
+ if (!installation.credential?.apiKey) throw new Error("Run hue resume to recover Hue credentials");
127
+
128
+ export const hue = createHue({
129
+ apiKey: installation.credential.apiKey,
130
+ baseUrl: installation.origin,
131
+ serviceName: ${JSON.stringify(serviceName(store.projectRoot))},
132
+ captureContent: false,
133
+ });
134
+
135
+ const evidencePath = fileURLToPath(new URL("./.hue/${basename(store.applicationEvidencePath)}", import.meta.url));
136
+ const expressInstalled = Symbol.for("hue.setup.express.installed");
137
+
138
+ // Setup-only transport. Normal app serving never sends an ownership frame.
139
+ function installOwnedListener(app) {
140
+ const proof = process.env.HUE_SETUP_SOCKET_PROOF;
141
+ delete process.env.HUE_SETUP_SOCKET_PROOF;
142
+ if (proof === undefined) return;
143
+ if (!/^[A-Za-z0-9_-]{43}$/u.test(proof)) throw new Error("Invalid setup invocation");
144
+ const original = app.listen;
145
+ app.listen = function (port, host) {
146
+ if ((host !== undefined && host !== "127.0.0.1") || arguments.length > 2)
147
+ throw new Error("Unsupported setup listener ownership");
148
+ const inner = original.call(app, 0, "127.0.0.1");
149
+ let accepted = false;
150
+ const sockets = new Set();
151
+ const outer = createServer((socket) => {
152
+ if (accepted || !inner.listening) { socket.destroy(); return; }
153
+ accepted = true;
154
+ sockets.add(socket);
155
+ const upstream = createConnection({ host: "127.0.0.1", port: inner.address().port });
156
+ sockets.add(upstream);
157
+ const close = () => { socket.destroy(); upstream.destroy(); sockets.delete(socket); sockets.delete(upstream); };
158
+ socket.on("error", close); upstream.on("error", close);
159
+ socket.on("close", close); upstream.on("close", close);
160
+ socket.setTimeout(15000, close); upstream.setTimeout(15000, close);
161
+ upstream.once("connect", () => {
162
+ if (!inner.listening || socket.destroyed) { close(); return; }
163
+ const digest = createHmac("sha256", Buffer.from(proof, "base64url"))
164
+ .update("hue-setup-owned-v1\\0" + socket.localPort + "\\0" + socket.remotePort).digest("base64url");
165
+ socket.write("Hue-setup-owned:" + digest + "\\n");
166
+ socket.pipe(upstream); upstream.pipe(socket);
167
+ });
168
+ });
169
+ const close = () => { for (const socket of sockets) socket.destroy(); inner.close(); };
170
+ outer.on("error", close); outer.on("close", close);
171
+ inner.on("error", () => outer.close());
172
+ inner.once("listening", () => outer.listen(Number(port), "127.0.0.1"));
173
+ return outer;
174
+ };
175
+ }
176
+
177
+ function saveEvidence(value) {
178
+ if (process.env.HUE_SETUP_EVIDENCE_FILE !== evidencePath) return;
179
+ const temporary = evidencePath + "." + process.pid + ".tmp";
180
+ let descriptor;
181
+ try {
182
+ descriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
183
+ writeFileSync(descriptor, JSON.stringify(value) + "\\n", "utf8");
184
+ fsyncSync(descriptor);
185
+ closeSync(descriptor);
186
+ descriptor = undefined;
187
+ renameSync(temporary, evidencePath);
188
+ } finally {
189
+ if (descriptor !== undefined) closeSync(descriptor);
190
+ try { unlinkSync(temporary); } catch (error) { if (error?.code !== "ENOENT") throw error; }
191
+ }
192
+ }
193
+
194
+ export function installHueExpress(app, requestPath) {
195
+ if (app[expressInstalled]) throw new Error("Duplicate Hue setup middleware requires explicit review");
196
+ app[expressInstalled] = true;
197
+ installOwnedListener(app);
198
+ app.use((request, response, next) => {
199
+ void (async () => {
200
+ const ids = await hue.withSpan("hue.metadata", async (span) => {
201
+ const finished = new Promise((resolve) => {
202
+ response.once("finish", resolve);
203
+ response.once("close", resolve);
204
+ });
205
+ next();
206
+ await finished;
207
+ return { traceId: span.traceId, spanId: span.spanId };
208
+ }, { kind: SpanKind.SERVER });
209
+ await hue.flush();
210
+ if (request.method === "GET" && request.route?.path === requestPath && request.path === requestPath && response.statusCode >= 200 && response.statusCode < 300 && response.writableFinished)
211
+ saveEvidence({ ...ids, credentialVersion: installation.credential.version, source: "existing-application-request" });
212
+ })().catch(() => undefined);
213
+ });
214
+ }
215
+ `;
216
+ }
217
+ function pythonConfig(store) {
218
+ return `# Managed by Hue setup. This file contains no credential.
219
+ import json
220
+ import os
221
+ import base64
222
+ import hashlib
223
+ import hmac
224
+ from pathlib import Path
225
+
226
+ from flask import g, request
227
+ from hue_sdk import Hue
228
+ from opentelemetry.trace import SpanKind, use_span
229
+ from werkzeug.serving import WSGIRequestHandler
230
+
231
+ _installation = json.loads(
232
+ (Path(__file__).parent / ".hue" / ${JSON.stringify(basename(store.path))}).read_text(encoding="utf-8")
233
+ )
234
+ if not _installation.get("credential", {}).get("apiKey"):
235
+ raise RuntimeError("Run hue resume to recover Hue credentials")
236
+
237
+ hue = Hue(
238
+ api_key=_installation["credential"]["apiKey"],
239
+ base_url=_installation["origin"],
240
+ service_name=${JSON.stringify(serviceName(store.projectRoot))},
241
+ capture_content=False,
242
+ )
243
+
244
+ _evidence_path = Path(__file__).parent / ".hue" / ${JSON.stringify(basename(store.applicationEvidencePath))}
245
+
246
+
247
+ def _save_evidence(value):
248
+ if os.environ.get("HUE_SETUP_EVIDENCE_FILE") != str(_evidence_path):
249
+ return
250
+ temporary = _evidence_path.with_name(f".{_evidence_path.name}.{os.getpid()}.tmp")
251
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
252
+ if hasattr(os, "O_NOFOLLOW"):
253
+ flags |= os.O_NOFOLLOW
254
+ descriptor = os.open(temporary, flags, 0o600)
255
+ try:
256
+ os.write(descriptor, (json.dumps(value) + "\\n").encode("utf-8"))
257
+ os.fsync(descriptor)
258
+ finally:
259
+ os.close(descriptor)
260
+ os.replace(temporary, _evidence_path)
261
+
262
+
263
+ def install_hue_flask(app, request_path):
264
+ if app.extensions.get("hue_setup_installed"):
265
+ raise RuntimeError("Duplicate Hue setup middleware requires explicit review")
266
+ app.extensions["hue_setup_installed"] = True
267
+ proof = os.environ.pop("HUE_SETUP_SOCKET_PROOF", None)
268
+ if proof is not None:
269
+ if len(proof) != 43 or any(char not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-" for char in proof):
270
+ raise RuntimeError("Invalid setup invocation")
271
+ accepted = False
272
+ class OwnedHandler(WSGIRequestHandler):
273
+ def handle(self):
274
+ nonlocal accepted
275
+ if accepted:
276
+ return
277
+ accepted = True
278
+ try:
279
+ self.connection.settimeout(15)
280
+ message = "hue-setup-owned-v1\\0" + str(self.connection.getsockname()[1]) + "\\0" + str(self.connection.getpeername()[1])
281
+ digest = base64.urlsafe_b64encode(hmac.new(base64.urlsafe_b64decode(proof + "="), message.encode("ascii"), hashlib.sha256).digest()).rstrip(b"=")
282
+ self.connection.sendall(b"Hue-setup-owned:" + digest + b"\\n")
283
+ super().handle()
284
+ except (OSError, ValueError):
285
+ pass
286
+ original_run = app.run
287
+ def owned_run(*args, **kwargs):
288
+ if args or set(kwargs) - {"host", "port"} or kwargs.get("host", "127.0.0.1") != "127.0.0.1":
289
+ raise RuntimeError("Unsupported setup listener ownership")
290
+ return original_run(**kwargs, request_handler=OwnedHandler, load_dotenv=False, use_reloader=False, threaded=False)
291
+ app.run = owned_run
292
+
293
+ @app.before_request
294
+ def _hue_setup_before_request():
295
+ try:
296
+ span = hue.tracer.start_span("hue.metadata", kind=SpanKind.SERVER)
297
+ context = use_span(span, end_on_exit=False, record_exception=False, set_status_on_exception=False)
298
+ context.__enter__()
299
+ g._hue_setup_context = context
300
+ g._hue_setup_span = span
301
+ except Exception:
302
+ pass
303
+
304
+ @app.after_request
305
+ def _hue_setup_after_request(response):
306
+ try:
307
+ context = getattr(g, "_hue_setup_context", None)
308
+ span = getattr(g, "_hue_setup_span", None)
309
+ g._hue_setup_context = None
310
+ g._hue_setup_span = None
311
+ if context is not None:
312
+ context.__exit__(None, None, None)
313
+ if span is None:
314
+ return response
315
+ matched = request.method == "GET" and request.url_rule is not None and request.url_rule.rule == request_path and 200 <= response.status_code < 300
316
+ ended = False
317
+ def finish(complete):
318
+ nonlocal ended
319
+ if ended:
320
+ return
321
+ ended = True
322
+ try:
323
+ ids = span.get_span_context()
324
+ span.end()
325
+ if complete and matched and os.environ.get("HUE_SETUP_EVIDENCE_FILE") == str(_evidence_path) and hue.force_flush():
326
+ _save_evidence({"traceId": format(ids.trace_id, "032x"), "spanId": format(ids.span_id, "016x"), "credentialVersion": _installation["credential"]["version"], "source": "existing-application-request"})
327
+ except Exception:
328
+ pass
329
+ if response.is_streamed:
330
+ original = response.response
331
+ def streamed():
332
+ complete = False
333
+ try:
334
+ with use_span(span, end_on_exit=False, record_exception=False, set_status_on_exception=False):
335
+ yield from original
336
+ complete = True
337
+ finally:
338
+ finish(complete)
339
+ response.response = streamed()
340
+ response.call_on_close(lambda: finish(False))
341
+ else:
342
+ finish(True)
343
+ except Exception:
344
+ pass
345
+ return response
346
+ `;
347
+ }
348
+ async function atomicManagedWrite(path, contents, previous) {
349
+ await safeDirectory(dirname(path));
350
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
351
+ let handle;
352
+ try {
353
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, previous ? previous.info.mode & 0o777 : 0o644);
354
+ await handle.chmod(previous ? previous.info.mode & 0o777 : 0o644);
355
+ await handle.writeFile(contents, "utf8");
356
+ await handle.sync();
357
+ await handle.close();
358
+ handle = undefined;
359
+ if (!sameSnapshot(previous, await readManaged(path)))
360
+ throw new Error("Refusing setup configuration changed before replacement");
361
+ if (previous)
362
+ await rename(temporary, path);
363
+ else {
364
+ // link is an atomic create-if-absent: never replace a file created after validation.
365
+ await link(temporary, path);
366
+ await unlink(temporary);
367
+ }
368
+ }
369
+ finally {
370
+ await handle?.close();
371
+ await cleanupTemporary(temporary);
372
+ }
373
+ }
374
+ async function writeManaged(store, record, relativePath, contents) {
375
+ const path = join(store.projectRoot, relativePath);
376
+ if (!inside(store.projectRoot, path))
377
+ throw new Error("Unsafe setup configuration path");
378
+ const expected = setupManagedDigest(contents);
379
+ const previous = await readManaged(path);
380
+ if (previous !== undefined && setupManagedDigest(previous.contents) === expected) {
381
+ if (record.managedFiles[relativePath] !== expected) {
382
+ record.managedFiles[relativePath] = expected;
383
+ await store.save(record);
384
+ }
385
+ return undefined;
386
+ }
387
+ const savedDigest = record.managedFiles[relativePath];
388
+ if (previous && (!savedDigest || setupManagedDigest(previous.contents) !== savedDigest))
389
+ throw new Error(`Refusing to overwrite custom or unexpectedly edited configuration at ${relativePath}`);
390
+ await atomicManagedWrite(path, contents, previous);
391
+ record.managedFiles[relativePath] = expected;
392
+ await store.save(record);
393
+ return { path: relativePath, change: previous ? "updated" : "created" };
394
+ }
395
+ async function rejectCustomEnvironment(projectRoot) {
396
+ await safeDirectory(projectRoot);
397
+ if (process.env.HUE_API_KEY)
398
+ throw new Error("Refusing to replace an existing custom Hue credential from HUE_API_KEY");
399
+ const entries = await readdir(projectRoot, { withFileTypes: true });
400
+ if (entries.length > 10_000)
401
+ throw new Error("Refusing to scan an oversized project directory");
402
+ for (const entry of entries) {
403
+ if (!(entry.name === ".env" || entry.name.startsWith(".env.")))
404
+ continue;
405
+ if (entry.isSymbolicLink())
406
+ throw new Error(`Refusing unsafe credential file at ${entry.name}`);
407
+ if (!entry.isFile())
408
+ continue;
409
+ const path = join(projectRoot, entry.name);
410
+ const source = (await readManaged(path))?.contents;
411
+ if (source === undefined)
412
+ throw new Error("Refusing credential file changed during inspection");
413
+ if (/^\s*(?:export\s+)?HUE_API_KEY\s*=/mu.test(source))
414
+ throw new Error(`Refusing to replace an existing custom Hue credential in ${entry.name}`);
415
+ }
416
+ }
417
+ /** Refuses credential/config conflicts before setup makes a provisioning request. */
418
+ export async function validateSetupConfiguration(store, record, project) {
419
+ if (project.root !== store.projectRoot)
420
+ throw new Error("Setup project identity changed");
421
+ if (project.languages.length === 0)
422
+ throw new Error("No supported TypeScript or Python project was detected");
423
+ await rejectCustomEnvironment(store.projectRoot);
424
+ const candidates = [];
425
+ if (project.languages.includes("typescript"))
426
+ candidates.push(["hue.setup.mjs", typescriptConfig(store)]);
427
+ if (project.languages.includes("python"))
428
+ candidates.push(["hue_setup.py", pythonConfig(store)]);
429
+ for (const [relativePath, expected] of candidates) {
430
+ const path = join(store.projectRoot, relativePath);
431
+ const previous = await readManaged(path);
432
+ if (previous) {
433
+ const digest = setupManagedDigest(previous.contents);
434
+ if (digest !== setupManagedDigest(expected) && digest !== record?.managedFiles[relativePath])
435
+ throw new Error(`Refusing to overwrite custom or unexpectedly edited configuration at ${relativePath}`);
436
+ }
437
+ }
438
+ }
439
+ /** Writes only secret-free, metadata-only integration modules and never executes project code. */
440
+ export async function configureSetupProject(store, record, project) {
441
+ await validateSetupConfiguration(store, record, project);
442
+ const changes = [];
443
+ if (project.languages.includes("typescript")) {
444
+ const change = await writeManaged(store, record, "hue.setup.mjs", typescriptConfig(store));
445
+ if (change)
446
+ changes.push(change);
447
+ }
448
+ if (project.languages.includes("python")) {
449
+ const change = await writeManaged(store, record, "hue_setup.py", pythonConfig(store));
450
+ if (change)
451
+ changes.push(change);
452
+ }
453
+ return changes;
454
+ }
@@ -0,0 +1,2 @@
1
+ /** Validates both the bearer namespace and its exact stored key identity. */
2
+ export declare function validSetupCredentialIdentity(apiKey: unknown, keyId: unknown): apiKey is string;
@@ -0,0 +1,9 @@
1
+ /** Closed pre-release setup credential grammar, distinct from ordinary project service keys. */
2
+ const SETUP_KEY = /^hue_setup_(live|test)_setup-([a-f0-9]{24})_([A-Za-z0-9_-]{43})$/u;
3
+ /** Validates both the bearer namespace and its exact stored key identity. */
4
+ export function validSetupCredentialIdentity(apiKey, keyId) {
5
+ if (typeof apiKey !== "string" || typeof keyId !== "string")
6
+ return false;
7
+ const match = SETUP_KEY.exec(apiKey);
8
+ return match !== null && keyId === `setup-${match[2]}`;
9
+ }
@@ -86,7 +86,10 @@ export async function detectSetupProject(projectRoot) {
86
86
  .filter((value) => value !== undefined)
87
87
  .join("\n");
88
88
  const languages = [];
89
- if (contents.has("tsconfig.json") || nodeDependencies.has("typescript"))
89
+ // The existing public language discriminator covers JavaScript as well as TypeScript.
90
+ if (contents.has("tsconfig.json") ||
91
+ nodeDependencies.has("typescript") ||
92
+ nodeDependencies.has("express"))
90
93
  languages.push("typescript");
91
94
  if (python || contents.has("pyproject.toml"))
92
95
  languages.push("python");
@@ -0,0 +1,118 @@
1
+ /** Telemetry credential saved only in an ignored owner-only installation file. */
2
+ export interface SetupStoredCredential {
3
+ /** Fixed setup-only key family; never an ordinary account-managed key. */
4
+ kind: "anonymous_trial";
5
+ /** Setup credentials have exactly one metadata-only permission in both generations. */
6
+ capabilities: ["setup_telemetry_write"];
7
+ /** Bearer credential used solely for telemetry export and receipt verification. */
8
+ apiKey: string;
9
+ /** Server identifier for the credential generation. */
10
+ keyId: string;
11
+ /** Anonymous or post-claim credential generation. */
12
+ version: 0 | 1;
13
+ }
14
+ /** Exact metadata probe identifiers retained for resumable receipt verification. */
15
+ export interface SetupStoredProbe {
16
+ /** Lowercase external trace identifier. */
17
+ traceId: string;
18
+ /** Lowercase external span identifier expected in the receipt. */
19
+ spanId: string;
20
+ /** Credential generation that exported the probe. */
21
+ credentialVersion: 0 | 1;
22
+ /** Whether an exact metadata-only receipt was observed. */
23
+ verified: boolean;
24
+ }
25
+ /** Exact identifiers emitted by a request through the repository's existing application. */
26
+ export interface SetupStoredApplicationEvidence extends SetupStoredProbe {
27
+ /** Closed source label that distinguishes app evidence from a synthetic setup probe. */
28
+ source: "existing-application-request";
29
+ }
30
+ /** Durable no-replay marker written before starting an existing application request. */
31
+ export interface SetupStoredApplicationAttempt {
32
+ /** Credential generation for which application work was attempted once. */
33
+ credentialVersion: 0 | 1;
34
+ /** Bounded ISO timestamp for diagnostics and explicit recovery. */
35
+ startedAt: string;
36
+ }
37
+ /** Durable client identity for an idempotent one-time browser handoff request. */
38
+ export interface SetupStoredClaimHandoff {
39
+ /** Lowercase UUIDv4 persisted before the handoff request is sent. */
40
+ id: string;
41
+ /** Compare-and-swap predecessor supplied when this handoff was created. */
42
+ previousHandoffId: string | null;
43
+ /** Last strictly validated server state, when a response was received. */
44
+ state?: "pending" | "consumed" | "expired" | "revoked";
45
+ /** Fixed server expiry, when a response was received. */
46
+ expiresAt?: string;
47
+ /** Fixed browser session expiry, when an exchange succeeded. */
48
+ sessionExpiresAt?: string | null;
49
+ }
50
+ /** Secret local installation state. It must never be emitted or copied into diagnostics. */
51
+ export interface SetupInstallationRecord {
52
+ /** Local file format version. */
53
+ format: 1;
54
+ /** Exact normalized Hue origin owning this installation. */
55
+ origin: string;
56
+ /** Lowercase UUIDv4 installation identity. */
57
+ installationId: string;
58
+ /** Unpadded base64url installation proof; never log or diagnose this value. */
59
+ installationSecret: string;
60
+ /** Latest locally managed telemetry credential. */
61
+ credential?: SetupStoredCredential;
62
+ /** Superseded anonymous credential retained only until its revocation is verified. */
63
+ revocationCredential?: SetupStoredCredential;
64
+ /** Durable positive verification of the superseded key on the dedicated receipt route. */
65
+ anonymousKeyRevoked?: true;
66
+ /** Latest probe awaiting or carrying exact receipt evidence. */
67
+ probe?: SetupStoredProbe;
68
+ /** Latest existing-application request awaiting or carrying exact receipt evidence. */
69
+ applicationEvidence?: SetupStoredApplicationEvidence;
70
+ /** Prevents automatic replay after startup, request, export or evidence loss. */
71
+ applicationAttempt?: SetupStoredApplicationAttempt;
72
+ /** Current proof-bound one-time browser handoff identity; never a bearer capability. */
73
+ claimHandoff?: SetupStoredClaimHandoff;
74
+ /** Provision request timestamps used to enforce the local hourly bound. */
75
+ provisionAttempts: string[];
76
+ /** Digests of files setup owns and may safely replace. */
77
+ managedFiles: Record<string, string>;
78
+ }
79
+ /** Owner-only, project/origin-scoped storage for installation proof and telemetry credentials. */
80
+ export declare class FileSetupInstallationStore {
81
+ private lastSnapshot?;
82
+ /** Resolved project directory containing the installation state. */
83
+ readonly projectRoot: string;
84
+ /** Exact normalized Hue origin scoped to this store. */
85
+ readonly origin: string;
86
+ /** Owner-only `.hue` directory. */
87
+ readonly directory: string;
88
+ /** Origin-scoped ignored installation record path. */
89
+ readonly path: string;
90
+ /** Origin-scoped owner-only browser handoff; its contents are never emitted. */
91
+ readonly claimHandoffPath: string;
92
+ /** Origin-scoped private application evidence transfer path. */
93
+ readonly applicationEvidencePath: string;
94
+ constructor(projectRoot: string, origin: string);
95
+ private rejectUnsafeProjectRoot;
96
+ private hasGitWorktree;
97
+ private privatePaths;
98
+ private assertPrivateGitProtection;
99
+ private snapshot;
100
+ private ensureIgnoreFile;
101
+ private ensureRootIgnored;
102
+ /** Revalidates owner-only storage and ignore rules before an existing proof is used for I/O. */
103
+ ensureIgnored(): Promise<void>;
104
+ /** Loads a valid owner-only record without creating one. */
105
+ load(): Promise<SetupInstallationRecord | undefined>;
106
+ /** Creates and durably saves the installation proof before any caller may perform network I/O. */
107
+ loadOrCreate(): Promise<SetupInstallationRecord>;
108
+ /** Atomically replaces this store's validated owner-only record. */
109
+ save(record: SetupInstallationRecord): Promise<void>;
110
+ /** Saves a private browser redirect without putting its capability in a process argument. */
111
+ saveClaimHandoff(claimUrl: string): Promise<string>;
112
+ /** Removes a consumed or terminal claim handoff without following symlinks. */
113
+ removeClaimHandoff(): Promise<void>;
114
+ /** Removes the private child-to-parent evidence transfer file without following symlinks. */
115
+ removeApplicationEvidence(): Promise<void>;
116
+ }
117
+ /** Computes the digest used to detect unexpected edits to managed files. */
118
+ export declare function setupManagedDigest(contents: string): string;