@indigoai-us/hq-cli 5.345.133 → 5.345.135

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 (44) hide show
  1. package/CHANGELOG.md +17 -3
  2. package/dist/command-catalog.generated.d.ts +45 -0
  3. package/dist/command-catalog.generated.js +56 -0
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/access.d.ts +2 -0
  7. package/dist/commands/access.js +58 -9
  8. package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
  9. package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
  10. package/dist/commands/core-handoff.d.ts +2 -0
  11. package/dist/commands/core-handoff.js +3 -1
  12. package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
  13. package/dist/commands/fingerprint.d.ts +10 -0
  14. package/dist/commands/fingerprint.js +191 -0
  15. package/dist/commands/lanes.js +9 -1
  16. package/dist/lib/fingerprint/fixtures.d.ts +19 -0
  17. package/dist/lib/fingerprint/fixtures.js +61 -0
  18. package/dist/lib/fingerprint/format.d.ts +50 -0
  19. package/dist/lib/fingerprint/format.js +105 -0
  20. package/dist/lib/fingerprint/offline.d.ts +16 -0
  21. package/dist/lib/fingerprint/offline.js +44 -0
  22. package/dist/lib/fingerprint/registry-client.d.ts +74 -0
  23. package/dist/lib/fingerprint/registry-client.js +141 -0
  24. package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
  25. package/dist/lib/fingerprint/resolve-local.js +67 -0
  26. package/dist/lib/fingerprint/self-core.d.ts +139 -0
  27. package/dist/lib/fingerprint/self-core.js +371 -0
  28. package/dist/lib/fingerprint/self.d.ts +44 -0
  29. package/dist/lib/fingerprint/self.js +191 -0
  30. package/dist/lib/lanes/spawn.d.ts +19 -1
  31. package/dist/lib/lanes/spawn.js +50 -6
  32. package/dist/lib/lanes/story-ledger.d.ts +1 -0
  33. package/dist/lib/lanes/story-ledger.js +3 -1
  34. package/dist/lib/lanes/types.d.ts +12 -0
  35. package/dist/lib/lanes/types.js +12 -1
  36. package/dist/utils/cli-network-lifecycle.d.ts +2 -0
  37. package/dist/utils/cli-network-lifecycle.js +5 -1
  38. package/dist/utils/cognito-session.d.ts +2 -21
  39. package/dist/utils/cognito-session.js +3 -84
  40. package/dist/utils/person-token-cache.d.ts +32 -0
  41. package/dist/utils/person-token-cache.js +95 -0
  42. package/dist/utils/vault-api.d.ts +21 -1
  43. package/dist/utils/vault-api.js +54 -5
  44. package/package.json +1 -1
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared fingerprint test vectors. hq-pro (US-006) copies this list so the CLI
3
+ * and the server agree on what a valid fingerprint is.
4
+ */
5
+ import type { FingerprintParts } from "./format.js";
6
+ export interface ValidFingerprintVector {
7
+ name: string;
8
+ parts: FingerprintParts;
9
+ value: string;
10
+ }
11
+ export interface InvalidFingerprintVector {
12
+ name: string;
13
+ value: string;
14
+ }
15
+ export declare const FINGERPRINT_TEST_VECTORS: {
16
+ valid: ValidFingerprintVector[];
17
+ invalid: InvalidFingerprintVector[];
18
+ };
19
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1,61 @@
1
+ export const FINGERPRINT_TEST_VECTORS = {
2
+ valid: [
3
+ {
4
+ name: "person on a laptop",
5
+ parts: {
6
+ company: "indigo",
7
+ identityUid: "prs_01J8ZK4Q7M2N3P4R5S6T7V8W9X",
8
+ hostKind: "laptop",
9
+ machineId: "e3ef5a5a-f51d-435d-90b9-dd876caa249b",
10
+ sessionId: "fabb19c9-95a7-48a4-9a95-f77e28d0e3db",
11
+ },
12
+ value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9",
13
+ },
14
+ {
15
+ name: "agent on a fleet box",
16
+ parts: {
17
+ company: "acme",
18
+ identityUid: "agt_01JA0000000000000000000000",
19
+ hostKind: "fleet",
20
+ machineId: "0123456789abcdef",
21
+ sessionId: "sess-1",
22
+ },
23
+ value: "hqfp1:acme:agt_01JA0000:fleet.01234567:sess-1",
24
+ },
25
+ {
26
+ name: "outpost with no company",
27
+ parts: {
28
+ company: null,
29
+ identityUid: "prs_01J8ZK4Q7M2N3P4R5S6T7V8W9X",
30
+ hostKind: "outpost",
31
+ machineId: "ABCDEF12-0000-0000-0000-000000000000",
32
+ sessionId: null,
33
+ },
34
+ value: "hqfp1:-:prs_01J8ZK4Q:outpost.abcdef12:-",
35
+ },
36
+ {
37
+ name: "session only",
38
+ parts: {
39
+ company: null,
40
+ identityUid: null,
41
+ hostKind: null,
42
+ machineId: null,
43
+ sessionId: "abcdefgh12345",
44
+ },
45
+ value: "hqfp1:-:-:-:abcdefgh",
46
+ },
47
+ ],
48
+ invalid: [
49
+ { name: "empty", value: "" },
50
+ { name: "wrong prefix", value: "hqfp2:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
51
+ { name: "too few fields", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a" },
52
+ { name: "too many fields", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9:x" },
53
+ { name: "bad identity prefix", value: "hqfp1:indigo:usr_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
54
+ { name: "short identity", value: "hqfp1:indigo:prs_01J8:laptop.e3ef5a5a:fabb19c9" },
55
+ { name: "bad host kind", value: "hqfp1:indigo:prs_01J8ZK4Q:desktop.e3ef5a5a:fabb19c9" },
56
+ { name: "non-hex machine", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.zzzzzzzz:fabb19c9" },
57
+ { name: "long session", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9x" },
58
+ { name: "empty field", value: "hqfp1::prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
59
+ ],
60
+ };
61
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * HQ session fingerprint value format.
3
+ *
4
+ * A fingerprint names one HQ session without exposing a full identity, machine
5
+ * or session id:
6
+ *
7
+ * hqfp1:<company>:<identity>:<machine>:<session>
8
+ *
9
+ * - company: company slug, or "-"
10
+ * - identity: "prs_" or "agt_" plus the first 8 chars of the ULID, or "-"
11
+ * - machine: "<laptop|outpost|fleet>.<first 8 hex chars of the machine id>", or "-"
12
+ * - session: first 8 chars of the session id, or "-"
13
+ *
14
+ * No component may contain ":". hq-pro (US-006) copies the test vectors in
15
+ * ./fixtures.ts so both sides agree on the format.
16
+ */
17
+ export declare const FINGERPRINT_PREFIX = "hqfp1";
18
+ export declare const UNKNOWN = "-";
19
+ export type HostKind = "laptop" | "outpost" | "fleet";
20
+ export type IdentityKind = "person" | "agent";
21
+ export declare const HOST_KINDS: readonly HostKind[];
22
+ export interface FingerprintParts {
23
+ company: string | null;
24
+ /** Full `prs_*` or `agt_*` uid. Truncated by build(). */
25
+ identityUid: string | null;
26
+ hostKind: HostKind | null;
27
+ /** Full machine id (UUID or hex). Truncated by build(). */
28
+ machineId: string | null;
29
+ /** Full session id. Truncated by build(). */
30
+ sessionId: string | null;
31
+ }
32
+ export interface ParsedFingerprint {
33
+ value: string;
34
+ company: string | null;
35
+ /** Short identity, for example `prs_01J8ABCD`. */
36
+ identity: string | null;
37
+ identityKind: IdentityKind | null;
38
+ hostKind: HostKind | null;
39
+ /** Short machine id, 8 lowercase hex chars. */
40
+ machine: string | null;
41
+ /** Short session id, 8 chars. */
42
+ session: string | null;
43
+ }
44
+ export declare const COMPANY_RE: RegExp;
45
+ /** Build a fingerprint value. Throws when a component is malformed or contains ":". */
46
+ export declare function build(parts: FingerprintParts): string;
47
+ /** Parse a fingerprint value. Returns null for anything that is not a valid hqfp1 value. */
48
+ export declare function parse(value: string): ParsedFingerprint | null;
49
+ export declare function isFingerprint(value: unknown): value is string;
50
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * HQ session fingerprint value format.
3
+ *
4
+ * A fingerprint names one HQ session without exposing a full identity, machine
5
+ * or session id:
6
+ *
7
+ * hqfp1:<company>:<identity>:<machine>:<session>
8
+ *
9
+ * - company: company slug, or "-"
10
+ * - identity: "prs_" or "agt_" plus the first 8 chars of the ULID, or "-"
11
+ * - machine: "<laptop|outpost|fleet>.<first 8 hex chars of the machine id>", or "-"
12
+ * - session: first 8 chars of the session id, or "-"
13
+ *
14
+ * No component may contain ":". hq-pro (US-006) copies the test vectors in
15
+ * ./fixtures.ts so both sides agree on the format.
16
+ */
17
+ export const FINGERPRINT_PREFIX = "hqfp1";
18
+ export const UNKNOWN = "-";
19
+ export const HOST_KINDS = ["laptop", "outpost", "fleet"];
20
+ export const COMPANY_RE = /^[a-z0-9][a-z0-9_-]*$/i;
21
+ const IDENTITY_RE = /^(prs|agt)_[A-Za-z0-9]{8}$/;
22
+ const MACHINE_RE = /^(laptop|outpost|fleet)\.[0-9a-f]{8}$/;
23
+ const SESSION_RE = /^[A-Za-z0-9._-]{1,8}$/;
24
+ function assertNoColon(name, value) {
25
+ if (value.includes(":")) {
26
+ throw new Error(`fingerprint ${name} must not contain ":"`);
27
+ }
28
+ }
29
+ function shortIdentity(uid) {
30
+ const match = /^(prs|agt)_([A-Za-z0-9]+)$/.exec(uid);
31
+ if (!match || match[2].length < 8) {
32
+ throw new Error("fingerprint identity must be prs_ or agt_ followed by at least 8 ULID chars");
33
+ }
34
+ return `${match[1]}_${match[2].slice(0, 8)}`;
35
+ }
36
+ function shortMachine(hostKind, machineId) {
37
+ const hex = machineId.toLowerCase().replace(/-/g, "");
38
+ if (!/^[0-9a-f]{8,}$/.test(hex)) {
39
+ throw new Error("fingerprint machine id must start with at least 8 hex chars");
40
+ }
41
+ return `${hostKind}.${hex.slice(0, 8)}`;
42
+ }
43
+ /** Build a fingerprint value. Throws when a component is malformed or contains ":". */
44
+ export function build(parts) {
45
+ const company = parts.company ?? UNKNOWN;
46
+ assertNoColon("company", company);
47
+ if (company !== UNKNOWN && !COMPANY_RE.test(company)) {
48
+ throw new Error("fingerprint company must be a slug");
49
+ }
50
+ let identity = UNKNOWN;
51
+ if (parts.identityUid) {
52
+ assertNoColon("identity", parts.identityUid);
53
+ identity = shortIdentity(parts.identityUid);
54
+ }
55
+ let machine = UNKNOWN;
56
+ if (parts.machineId) {
57
+ assertNoColon("machine", parts.machineId);
58
+ machine = shortMachine(parts.hostKind ?? "laptop", parts.machineId);
59
+ }
60
+ let session = UNKNOWN;
61
+ if (parts.sessionId) {
62
+ assertNoColon("session", parts.sessionId);
63
+ session = parts.sessionId.slice(0, 8);
64
+ if (!SESSION_RE.test(session)) {
65
+ throw new Error("fingerprint session id has invalid characters");
66
+ }
67
+ }
68
+ return [FINGERPRINT_PREFIX, company, identity, machine, session].join(":");
69
+ }
70
+ /** Parse a fingerprint value. Returns null for anything that is not a valid hqfp1 value. */
71
+ export function parse(value) {
72
+ if (typeof value !== "string")
73
+ return null;
74
+ const fields = value.split(":");
75
+ if (fields.length !== 5)
76
+ return null;
77
+ const [prefix, company, identity, machine, session] = fields;
78
+ if (prefix !== FINGERPRINT_PREFIX)
79
+ return null;
80
+ if (company !== UNKNOWN && !COMPANY_RE.test(company))
81
+ return null;
82
+ if (identity !== UNKNOWN && !IDENTITY_RE.test(identity))
83
+ return null;
84
+ if (machine !== UNKNOWN && !MACHINE_RE.test(machine))
85
+ return null;
86
+ if (session !== UNKNOWN && !SESSION_RE.test(session))
87
+ return null;
88
+ const hostKind = machine === UNKNOWN ? null : machine.split(".")[0];
89
+ let identityKind = null;
90
+ if (identity !== UNKNOWN)
91
+ identityKind = identity.startsWith("agt_") ? "agent" : "person";
92
+ return {
93
+ value,
94
+ company: company === UNKNOWN ? null : company,
95
+ identity: identity === UNKNOWN ? null : identity,
96
+ identityKind,
97
+ hostKind,
98
+ machine: machine === UNKNOWN ? null : machine.split(".")[1],
99
+ session: session === UNKNOWN ? null : session,
100
+ };
101
+ }
102
+ export function isFingerprint(value) {
103
+ return typeof value === "string" && parse(value) !== null;
104
+ }
105
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,16 @@
1
+ export declare const FINGERPRINT_TRAILER = "HQ-Fingerprint";
2
+ /**
3
+ * The fingerprint for `env`, or null when it cannot be built (no identity and
4
+ * no session id, or an error). `label` prefixes the stderr warning. With
5
+ * `readOnly` it creates no file and is null when no machine id exists yet.
6
+ */
7
+ export declare function offlineFingerprint(hqRoot: string, env: NodeJS.ProcessEnv, label: string, options?: {
8
+ readOnly?: boolean;
9
+ }): string | null;
10
+ /**
11
+ * `git commit` args that add the HQ-Fingerprint trailer, or none without a
12
+ * value. The trailer is a final `-m` paragraph, which `git interpret-trailers`
13
+ * parses on any git; `--trailer` needs git 2.32. Spread after the subject `-m`.
14
+ */
15
+ export declare function fingerprintTrailerArgs(fingerprint: string | null | undefined): string[];
16
+ //# sourceMappingURL=offline.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Offline fingerprint for callers that cannot await: lane start and the
3
+ * handoff commits. Uses the cached ID token and the machine creds only; it
4
+ * never makes a network call.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import { loadMachineCreds, machineCredsFilePath } from "@indigoai-us/hq-cloud";
8
+ import { loadCachedTokens } from "../../utils/person-token-cache.js";
9
+ import { buildSelfFingerprintOfflineSync } from "./self-core.js";
10
+ export const FINGERPRINT_TRAILER = "HQ-Fingerprint";
11
+ /**
12
+ * The fingerprint for `env`, or null when it cannot be built (no identity and
13
+ * no session id, or an error). `label` prefixes the stderr warning. With
14
+ * `readOnly` it creates no file and is null when no machine id exists yet.
15
+ */
16
+ export function offlineFingerprint(hqRoot, env, label, options = {}) {
17
+ try {
18
+ return buildSelfFingerprintOfflineSync({
19
+ hqRoot,
20
+ env,
21
+ readOnly: options.readOnly,
22
+ loadIdToken: () => loadCachedTokens()?.idToken ?? null,
23
+ loadMachineCreds: () => {
24
+ if (!fs.existsSync(machineCredsFilePath()))
25
+ return null;
26
+ const creds = loadMachineCreds();
27
+ return creds ? { entityType: creds.entityType, entityUid: creds.entityUid } : null;
28
+ },
29
+ }).fingerprint;
30
+ }
31
+ catch (err) {
32
+ process.stderr.write(`${label}: could not build the fingerprint: ${err.message}\n`);
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * `git commit` args that add the HQ-Fingerprint trailer, or none without a
38
+ * value. The trailer is a final `-m` paragraph, which `git interpret-trailers`
39
+ * parses on any git; `--trailer` needs git 2.32. Spread after the subject `-m`.
40
+ */
41
+ export function fingerprintTrailerArgs(fingerprint) {
42
+ return fingerprint ? ["-m", `${FINGERPRINT_TRAILER}: ${fingerprint}`] : [];
43
+ }
44
+ //# sourceMappingURL=offline.js.map
@@ -0,0 +1,74 @@
1
+ import type { HostKind } from "./format.js";
2
+ import type { VaultApiOptions } from "../../utils/vault-api.js";
3
+ export declare const REGISTRY_TIMEOUT_MS = 3000;
4
+ export declare const FINGERPRINT_LOG_FILE = "fingerprint.log";
5
+ export type RegistryFetch = (opts: VaultApiOptions) => Promise<Response>;
6
+ export interface RegistryClientOptions {
7
+ /** HTTP client. Defaults to vaultApiFetch, loaded on first use. */
8
+ fetch?: RegistryFetch;
9
+ baseUrl?: string;
10
+ timeoutMs?: number;
11
+ }
12
+ export interface RegisterFingerprintBody {
13
+ fingerprint: string;
14
+ companySlug: string;
15
+ identityUid: string;
16
+ machineId: string;
17
+ hostKind: HostKind;
18
+ sessionId: string;
19
+ label?: string;
20
+ }
21
+ /** hq-pro's FingerprintRow, as returned by POST /v1/fingerprints. */
22
+ export interface FingerprintRow {
23
+ pk: string;
24
+ companyUid: string;
25
+ companySlug: string;
26
+ identityUid: string;
27
+ identityKind: string;
28
+ machineId: string;
29
+ hostKind: HostKind;
30
+ sessionId: string;
31
+ firstSeenAt: string;
32
+ lastSeenAt: string;
33
+ recordedBySub?: string;
34
+ }
35
+ /** GET /v1/fingerprints/{value}: the row without recordedBySub, plus display names. */
36
+ export interface ResolvedFingerprint extends Omit<FingerprintRow, "recordedBySub"> {
37
+ identityName: string | null;
38
+ machineLabel: string | null;
39
+ }
40
+ export type RegistryFailure = {
41
+ ok: false;
42
+ /** HTTP status, or null when no response arrived (timeout, transport error). */
43
+ status: number | null;
44
+ /** The server's `error` text, or the thrown error's class and message. */
45
+ error: string;
46
+ };
47
+ export type RegisterResult = {
48
+ ok: true;
49
+ status: number;
50
+ row: FingerprintRow;
51
+ } | RegistryFailure;
52
+ export type LookupResult = {
53
+ ok: true;
54
+ found: true;
55
+ record: ResolvedFingerprint;
56
+ } | {
57
+ ok: true;
58
+ found: false;
59
+ } | RegistryFailure;
60
+ export declare function registerFingerprint(body: RegisterFingerprintBody, token: string, opts?: RegistryClientOptions): Promise<RegisterResult>;
61
+ export declare function lookupFingerprint(value: string, token: string, opts?: RegistryClientOptions): Promise<LookupResult>;
62
+ export interface FingerprintLogOptions {
63
+ /** Directory that holds fingerprint.log. Defaults to ~/.hq. */
64
+ dir?: string;
65
+ now?: () => Date;
66
+ }
67
+ export declare function fingerprintLogPath(dir?: string): string;
68
+ /**
69
+ * Appends one line to ~/.hq/fingerprint.log. A log that cannot be written is
70
+ * dropped: registration is fire-and-forget and must not print or fail.
71
+ */
72
+ export declare function appendFingerprintLog(op: string, detail: string, opts?: FingerprintLogOptions): void;
73
+ export declare function describeFailure(result: RegistryFailure): string;
74
+ //# sourceMappingURL=registry-client.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Client for the hq-pro fingerprint registry (session-fingerprints US-006):
3
+ *
4
+ * POST /v1/fingerprints record this session's fingerprint
5
+ * GET /v1/fingerprints/{value} resolve a fingerprint to its record
6
+ *
7
+ * Every call is capped at REGISTRY_TIMEOUT_MS and never throws: failures come
8
+ * back as a typed result. vault-api is imported lazily, and self.ts and the
9
+ * command import this module lazily, so the offline builder (self-core.ts,
10
+ * offline.ts) never reaches it.
11
+ *
12
+ * Failures are written as one line to ~/.hq/fingerprint.log. A line names the
13
+ * operation, the HTTP status or the error class, and the server's error text,
14
+ * never a token or a request header.
15
+ */
16
+ import * as fs from "node:fs";
17
+ import * as os from "node:os";
18
+ import * as path from "node:path";
19
+ export const REGISTRY_TIMEOUT_MS = 3000;
20
+ export const FINGERPRINT_LOG_FILE = "fingerprint.log";
21
+ async function defaultFetch(opts) {
22
+ const { vaultApiFetch } = await import("../../utils/vault-api.js");
23
+ return vaultApiFetch(opts);
24
+ }
25
+ /** Runs the request with an AbortController that fires after the timeout. */
26
+ async function withDeadline(run, ms) {
27
+ const controller = new AbortController();
28
+ let timer;
29
+ const timeout = new Promise((_, reject) => {
30
+ timer = setTimeout(() => {
31
+ const err = new Error(`timed out after ${ms}ms`);
32
+ err.name = "TimeoutError";
33
+ controller.abort(err);
34
+ reject(err);
35
+ }, ms);
36
+ });
37
+ try {
38
+ return await Promise.race([run(controller.signal), timeout]);
39
+ }
40
+ finally {
41
+ clearTimeout(timer);
42
+ }
43
+ }
44
+ async function readJson(response) {
45
+ try {
46
+ return await response.json();
47
+ }
48
+ catch {
49
+ // A body that is not JSON (an HTML gateway page, an empty 502) has no
50
+ // fields to read; the caller reports the status instead.
51
+ return null;
52
+ }
53
+ }
54
+ function serverError(body, status) {
55
+ if (body && typeof body === "object") {
56
+ const record = body;
57
+ if (typeof record.error === "string")
58
+ return record.error;
59
+ if (typeof record.message === "string")
60
+ return record.message;
61
+ }
62
+ return `HTTP ${status}`;
63
+ }
64
+ function thrownError(err) {
65
+ const e = err instanceof Error ? err : new Error(String(err));
66
+ return { ok: false, status: null, error: `${e.name}: ${e.message}` };
67
+ }
68
+ export async function registerFingerprint(body, token, opts = {}) {
69
+ const fetchImpl = opts.fetch ?? defaultFetch;
70
+ try {
71
+ const response = await withDeadline((signal) => fetchImpl({
72
+ baseUrl: opts.baseUrl,
73
+ token,
74
+ path: "/v1/fingerprints",
75
+ method: "POST",
76
+ body: { ...body },
77
+ signal,
78
+ }), opts.timeoutMs ?? REGISTRY_TIMEOUT_MS);
79
+ const json = await readJson(response);
80
+ if (response.status === 200 || response.status === 201) {
81
+ return { ok: true, status: response.status, row: json };
82
+ }
83
+ return { ok: false, status: response.status, error: serverError(json, response.status) };
84
+ }
85
+ catch (err) {
86
+ return thrownError(err);
87
+ }
88
+ }
89
+ export async function lookupFingerprint(value, token, opts = {}) {
90
+ const fetchImpl = opts.fetch ?? defaultFetch;
91
+ try {
92
+ const response = await withDeadline((signal) => fetchImpl({
93
+ baseUrl: opts.baseUrl,
94
+ token,
95
+ path: `/v1/fingerprints/${encodeURIComponent(value)}`,
96
+ signal,
97
+ }), opts.timeoutMs ?? REGISTRY_TIMEOUT_MS);
98
+ const json = await readJson(response);
99
+ if (response.status === 200 && json && typeof json === "object") {
100
+ return { ok: true, found: true, record: json };
101
+ }
102
+ // Unknown values and fingerprints of other companies get the same 404.
103
+ if (response.status === 404)
104
+ return { ok: true, found: false };
105
+ return { ok: false, status: response.status, error: serverError(json, response.status) };
106
+ }
107
+ catch (err) {
108
+ return thrownError(err);
109
+ }
110
+ }
111
+ export function fingerprintLogPath(dir = path.join(os.homedir(), ".hq")) {
112
+ return path.join(dir, FINGERPRINT_LOG_FILE);
113
+ }
114
+ /** Strips newlines and anything shaped like a bearer token or a JWT. */
115
+ function sanitize(text) {
116
+ return text
117
+ .replace(/[\r\n]+/g, " ")
118
+ .replace(/Bearer\s+\S+/gi, "Bearer <redacted>")
119
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g, "<redacted>")
120
+ .replace(/\bhqk_[A-Za-z0-9_-]+/g, "<redacted>")
121
+ .slice(0, 300);
122
+ }
123
+ /**
124
+ * Appends one line to ~/.hq/fingerprint.log. A log that cannot be written is
125
+ * dropped: registration is fire-and-forget and must not print or fail.
126
+ */
127
+ export function appendFingerprintLog(op, detail, opts = {}) {
128
+ const file = fingerprintLogPath(opts.dir);
129
+ const at = (opts.now ?? (() => new Date()))().toISOString();
130
+ try {
131
+ fs.mkdirSync(path.dirname(file), { recursive: true });
132
+ fs.appendFileSync(file, `${at} ${op} ${sanitize(detail)}\n`, { mode: 0o600 });
133
+ }
134
+ catch {
135
+ // Nowhere left to report it without changing the command's output.
136
+ }
137
+ }
138
+ export function describeFailure(result) {
139
+ return result.status === null ? result.error : `status=${result.status} ${result.error}`;
140
+ }
141
+ //# sourceMappingURL=registry-client.js.map
@@ -0,0 +1,14 @@
1
+ import type { HostKind, IdentityKind } from "./format.js";
2
+ export interface LocalFingerprintRecord {
3
+ fingerprint: string;
4
+ identityUid: string | null;
5
+ identityKind: IdentityKind | null;
6
+ machineId: string | null;
7
+ hostKind: HostKind | null;
8
+ sessionId: string | null;
9
+ company: string | null;
10
+ builtAt: string | null;
11
+ source: "local";
12
+ }
13
+ export declare function resolveLocalFingerprint(hqRoot: string, value: string): LocalFingerprintRecord | null;
14
+ //# sourceMappingURL=resolve-local.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Resolve a fingerprint against this machine's session records
3
+ * (workspace/sessions/<sid>/fingerprint.yaml). Exact value match only.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import yaml from "js-yaml";
8
+ import { FINGERPRINT_FILE } from "./self-core.js";
9
+ function str(value) {
10
+ return typeof value === "string" && value ? value : null;
11
+ }
12
+ export function resolveLocalFingerprint(hqRoot, value) {
13
+ const sessionsDir = path.join(hqRoot, "workspace", "sessions");
14
+ let entries;
15
+ try {
16
+ entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
17
+ }
18
+ catch (err) {
19
+ if (err.code === "ENOENT")
20
+ return null;
21
+ throw err;
22
+ }
23
+ for (const entry of entries) {
24
+ if (!entry.isDirectory())
25
+ continue;
26
+ const file = path.join(sessionsDir, entry.name, FINGERPRINT_FILE);
27
+ let raw;
28
+ try {
29
+ raw = fs.readFileSync(file, "utf8");
30
+ }
31
+ catch (err) {
32
+ if (err.code !== "ENOENT") {
33
+ process.stderr.write(`hq fingerprint: skipping unreadable ${file}: ${err.message}\n`);
34
+ }
35
+ continue;
36
+ }
37
+ // A cheap substring check skips the YAML parse for the many sessions that do not match.
38
+ if (!raw.includes(value))
39
+ continue;
40
+ let record;
41
+ try {
42
+ record = yaml.load(raw);
43
+ }
44
+ catch (err) {
45
+ process.stderr.write(`hq fingerprint: skipping unreadable ${file}: ${err.message}\n`);
46
+ continue;
47
+ }
48
+ if (!record || typeof record !== "object")
49
+ continue;
50
+ const r = record;
51
+ if (r.fingerprint !== value)
52
+ continue;
53
+ return {
54
+ fingerprint: value,
55
+ identityUid: str(r.identity_uid),
56
+ identityKind: str(r.identity_kind) ?? null,
57
+ machineId: str(r.machine_id),
58
+ hostKind: str(r.host_kind) ?? null,
59
+ sessionId: str(r.session_id),
60
+ company: str(r.company),
61
+ builtAt: str(r.built_at),
62
+ source: "local",
63
+ };
64
+ }
65
+ return null;
66
+ }
67
+ //# sourceMappingURL=resolve-local.js.map