@indigoai-us/hq-cli 5.345.134 → 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.
- package/CHANGELOG.md +7 -0
- package/dist/command-catalog.generated.d.ts +45 -0
- package/dist/command-catalog.generated.js +56 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
- package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
- package/dist/commands/core-handoff.d.ts +2 -0
- package/dist/commands/core-handoff.js +3 -1
- package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
- package/dist/commands/fingerprint.d.ts +10 -0
- package/dist/commands/fingerprint.js +191 -0
- package/dist/commands/lanes.js +9 -1
- package/dist/lib/fingerprint/fixtures.d.ts +19 -0
- package/dist/lib/fingerprint/fixtures.js +61 -0
- package/dist/lib/fingerprint/format.d.ts +50 -0
- package/dist/lib/fingerprint/format.js +105 -0
- package/dist/lib/fingerprint/offline.d.ts +16 -0
- package/dist/lib/fingerprint/offline.js +44 -0
- package/dist/lib/fingerprint/registry-client.d.ts +74 -0
- package/dist/lib/fingerprint/registry-client.js +141 -0
- package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
- package/dist/lib/fingerprint/resolve-local.js +67 -0
- package/dist/lib/fingerprint/self-core.d.ts +139 -0
- package/dist/lib/fingerprint/self-core.js +371 -0
- package/dist/lib/fingerprint/self.d.ts +44 -0
- package/dist/lib/fingerprint/self.js +191 -0
- package/dist/lib/lanes/spawn.d.ts +19 -1
- package/dist/lib/lanes/spawn.js +50 -6
- package/dist/lib/lanes/story-ledger.d.ts +1 -0
- package/dist/lib/lanes/story-ledger.js +3 -1
- package/dist/lib/lanes/types.d.ts +12 -0
- package/dist/lib/lanes/types.js +12 -1
- package/dist/utils/cli-network-lifecycle.d.ts +2 -0
- package/dist/utils/cli-network-lifecycle.js +5 -1
- package/dist/utils/cognito-session.d.ts +2 -21
- package/dist/utils/cognito-session.js +3 -84
- package/dist/utils/person-token-cache.d.ts +32 -0
- package/dist/utils/person-token-cache.js +95 -0
- package/dist/utils/vault-api.d.ts +21 -1
- package/dist/utils/vault-api.js +54 -5
- package/package.json +1 -1
|
@@ -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
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type HostKind, type IdentityKind } from "./format.js";
|
|
2
|
+
export declare const SESSION_ID_ENV_VARS: readonly ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID"];
|
|
3
|
+
export declare const IDENTITY_LOOKUP_TIMEOUT_MS = 3000;
|
|
4
|
+
export declare const FINGERPRINT_FILE = "fingerprint.yaml";
|
|
5
|
+
export declare const IDENTITY_UID_RE: RegExp;
|
|
6
|
+
export type IdentitySource = "token" | "machine-creds" | "cache" | "lookup" | "none";
|
|
7
|
+
export interface SelfFingerprint {
|
|
8
|
+
fingerprint: string | null;
|
|
9
|
+
company: string | null;
|
|
10
|
+
identityUid: string | null;
|
|
11
|
+
identityKind: IdentityKind | null;
|
|
12
|
+
machineId: string | null;
|
|
13
|
+
hostKind: HostKind;
|
|
14
|
+
sessionId: string | null;
|
|
15
|
+
source: IdentitySource;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface MachineCredsSummary {
|
|
19
|
+
entityType?: string;
|
|
20
|
+
entityUid?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SelfFingerprintDeps {
|
|
23
|
+
hqRoot: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
offline?: boolean;
|
|
26
|
+
/** Returns the cached ID token, or null when signed out. */
|
|
27
|
+
loadIdToken?: () => string | null | Promise<string | null>;
|
|
28
|
+
/** Returns the machine creds, or null when this host has none or they are unreadable. */
|
|
29
|
+
loadMachineCreds?: () => MachineCredsSummary | null | Promise<MachineCredsSummary | null>;
|
|
30
|
+
/** Resolves the caller's uid over the network. The signal aborts on timeout. */
|
|
31
|
+
resolveIdentity?: (idToken: string, signal: AbortSignal) => Promise<{
|
|
32
|
+
kind: IdentityKind;
|
|
33
|
+
uid: string;
|
|
34
|
+
}>;
|
|
35
|
+
readMachineId?: (hqRoot: string) => string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Never create or write a file: read an existing machine id only, return a
|
|
38
|
+
* null fingerprint without one, and skip the session cache write. For the
|
|
39
|
+
* handoff commits, which must leave the tree as the shell script does.
|
|
40
|
+
*/
|
|
41
|
+
readOnly?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The caller found no HQ root, so `hqRoot` names no HQ tree. The machine is
|
|
44
|
+
* `-`, nothing under `hqRoot` is read or written, and the result carries
|
|
45
|
+
* `reason: "no HQ root"`.
|
|
46
|
+
*/
|
|
47
|
+
noRoot?: boolean;
|
|
48
|
+
lookupTimeoutMs?: number;
|
|
49
|
+
now?: () => Date;
|
|
50
|
+
warn?: (message: string) => void;
|
|
51
|
+
}
|
|
52
|
+
interface FingerprintCacheFile {
|
|
53
|
+
fingerprint?: string | null;
|
|
54
|
+
company?: string | null;
|
|
55
|
+
identity_uid?: string | null;
|
|
56
|
+
identity_kind?: IdentityKind | null;
|
|
57
|
+
identity_lookup_done?: boolean;
|
|
58
|
+
/** sha256 of the ID token `sub` the identity was resolved for, or null without one. */
|
|
59
|
+
token_owner?: string | null;
|
|
60
|
+
machine_id?: string | null;
|
|
61
|
+
host_kind?: HostKind;
|
|
62
|
+
session_id?: string;
|
|
63
|
+
built_at?: string;
|
|
64
|
+
/** When hq-pro recorded this fingerprint (US-007). Set by the registry step in self.ts. */
|
|
65
|
+
registered_at?: string;
|
|
66
|
+
/** Why registration will not be tried again for this fingerprint, such as "outpost". */
|
|
67
|
+
registration_skipped?: string;
|
|
68
|
+
}
|
|
69
|
+
/** Registration keys carried across rewrites while the fingerprint stays the same. */
|
|
70
|
+
type RegistrationKeys = Pick<FingerprintCacheFile, "registered_at" | "registration_skipped">;
|
|
71
|
+
export declare function resolveSessionId(env: NodeJS.ProcessEnv): string | null;
|
|
72
|
+
export declare function sessionDir(hqRoot: string, sessionId: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Read `<hqRoot>/.hq/machine-id`, creating it when missing. Mirrors
|
|
75
|
+
* getOrCreateMachineId in hq-cloud, which the package does not export (its
|
|
76
|
+
* exports map has no ./lib/machine-id entry). Falls back to the menubar id in
|
|
77
|
+
* ~/.hq/menubar.json before generating a new UUID, as hq-cloud does.
|
|
78
|
+
*/
|
|
79
|
+
export declare function readOrCreateMachineId(hqRoot: string): string | null;
|
|
80
|
+
/** Read `<hqRoot>/.hq/machine-id` without creating it. Null when it is missing or empty. */
|
|
81
|
+
export declare function readMachineIdIfPresent(hqRoot: string): string | null;
|
|
82
|
+
/** Machine ids are usually UUIDs. A free-form id is hashed so the fingerprint keeps 8 hex chars. */
|
|
83
|
+
export declare function machineIdForFingerprint(machineId: string): string;
|
|
84
|
+
export interface IdentityContext {
|
|
85
|
+
env: NodeJS.ProcessEnv;
|
|
86
|
+
hqRoot: string;
|
|
87
|
+
warn: (message: string) => void;
|
|
88
|
+
sessionId: string | null;
|
|
89
|
+
idToken: string | null;
|
|
90
|
+
owner: string | null;
|
|
91
|
+
hostKind: HostKind;
|
|
92
|
+
cacheFile: string | null;
|
|
93
|
+
identityUid: string | null;
|
|
94
|
+
source: IdentitySource;
|
|
95
|
+
lookupDone: boolean;
|
|
96
|
+
/** True when no local source named the identity and a person lookup may run. */
|
|
97
|
+
needsLookup: boolean;
|
|
98
|
+
/** The cached fingerprint and its registration keys, kept when the value is unchanged. */
|
|
99
|
+
cachedRegistration?: {
|
|
100
|
+
fingerprint: string | null;
|
|
101
|
+
} & RegistrationKeys;
|
|
102
|
+
}
|
|
103
|
+
export declare function identityContext(deps: SelfFingerprintDeps, idToken: string | null, creds: MachineCredsSummary | null): IdentityContext;
|
|
104
|
+
export interface SelfFingerprintSyncDeps extends Omit<SelfFingerprintDeps, "offline" | "loadIdToken" | "loadMachineCreds" | "resolveIdentity" | "lookupTimeoutMs"> {
|
|
105
|
+
/** Returns the cached ID token, or null when signed out. */
|
|
106
|
+
loadIdToken: () => string | null;
|
|
107
|
+
/** Returns the machine creds, or null when this host has none or they are unreadable. */
|
|
108
|
+
loadMachineCreds: () => MachineCredsSummary | null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The offline build as a synchronous call, for callers that cannot await
|
|
112
|
+
* (lane start). It never does the person lookup, so the loaders are required:
|
|
113
|
+
* the default ones live in self.ts, which loads them lazily and only
|
|
114
|
+
* asynchronously.
|
|
115
|
+
*/
|
|
116
|
+
export declare function buildSelfFingerprintOfflineSync(deps: SelfFingerprintSyncDeps): SelfFingerprint;
|
|
117
|
+
export declare function finishFingerprint(deps: SelfFingerprintDeps, ctx: IdentityContext): SelfFingerprint;
|
|
118
|
+
export interface FingerprintRegistrationState {
|
|
119
|
+
registeredAt: string | null;
|
|
120
|
+
skipped: string | null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The registration keys of this session's fingerprint.yaml, or null when the
|
|
124
|
+
* file is missing or holds another fingerprint (the registry step then has no
|
|
125
|
+
* record to mark).
|
|
126
|
+
*/
|
|
127
|
+
export declare function readRegistrationState(hqRoot: string, sessionId: string, fingerprint: string, warn: (message: string) => void): FingerprintRegistrationState | null;
|
|
128
|
+
/**
|
|
129
|
+
* Writes registered_at or registration_skipped into fingerprint.yaml. Re-reads
|
|
130
|
+
* the file first and writes nothing when it now holds another fingerprint.
|
|
131
|
+
* Returns true when the file was rewritten.
|
|
132
|
+
*/
|
|
133
|
+
export declare function recordRegistrationState(hqRoot: string, sessionId: string, fingerprint: string, update: {
|
|
134
|
+
registeredAt: string;
|
|
135
|
+
} | {
|
|
136
|
+
skipped: string;
|
|
137
|
+
}, warn: (message: string) => void): boolean;
|
|
138
|
+
export {};
|
|
139
|
+
//# sourceMappingURL=self-core.d.ts.map
|