@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.
- package/CHANGELOG.md +17 -3
- 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/access.d.ts +2 -0
- package/dist/commands/access.js +58 -9
- 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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person's cached Cognito session, read from disk and the shared Keychain
|
|
3
|
+
* item without any network call. Split out of cognito-session.ts so offline
|
|
4
|
+
* callers (lane start and the handoff commits) can read the cached ID token
|
|
5
|
+
* without bundling the refresh and login clients. cognito-session.ts
|
|
6
|
+
* re-exports these readers.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { accessTokenFingerprint, loadCachedTokens as loadCachedTokensFromFile } from "@indigoai-us/hq-cloud";
|
|
12
|
+
import { pickNewerSession, readKeychainTokens } from "./token-keychain.js";
|
|
13
|
+
/**
|
|
14
|
+
* On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
|
|
15
|
+
* Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
|
|
16
|
+
* mint in this process, that helper returns in-memory machine tokens when the
|
|
17
|
+
* person file is absent, which would falsely block the fresh-box fallback.
|
|
18
|
+
*/
|
|
19
|
+
export function personTokenCacheFile(home = os.homedir(), env = process.env) {
|
|
20
|
+
const override = env.HQ_STATE_DIR?.trim();
|
|
21
|
+
const stateDir = override && override.length > 0 ? override : path.join(home, ".hq");
|
|
22
|
+
return path.join(stateDir, "cognito-tokens.json");
|
|
23
|
+
}
|
|
24
|
+
export function decodeJwtPayload(token) {
|
|
25
|
+
if (!token)
|
|
26
|
+
return null;
|
|
27
|
+
const parts = token.split(".");
|
|
28
|
+
if (parts.length < 2 || !parts[1])
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
32
|
+
const parsed = JSON.parse(json);
|
|
33
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
34
|
+
return null;
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function readTokenFile(file) {
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(file))
|
|
44
|
+
return null;
|
|
45
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
46
|
+
if (typeof raw.accessToken !== "string" || typeof raw.idToken !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
return raw;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The person session from the shared macOS Keychain item (see
|
|
56
|
+
* ./token-keychain.ts), or null. Not read when the token file is missing:
|
|
57
|
+
* no file means signed out. Machine-identity and invalidated tokens are
|
|
58
|
+
* refused, the same as the file loader.
|
|
59
|
+
*/
|
|
60
|
+
export function personKeychainTokens(file, home, env, keychain = {}) {
|
|
61
|
+
if (!fs.existsSync(file))
|
|
62
|
+
return null;
|
|
63
|
+
const tokens = readKeychainTokens({ env, home, ...keychain });
|
|
64
|
+
if (!tokens)
|
|
65
|
+
return null;
|
|
66
|
+
const claims = decodeJwtPayload(tokens.idToken);
|
|
67
|
+
const entityType = claims?.["custom:entityType"];
|
|
68
|
+
if (typeof entityType === "string" && entityType.trim().length > 0)
|
|
69
|
+
return null;
|
|
70
|
+
if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
|
|
71
|
+
return null;
|
|
72
|
+
return tokens;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Person session for `env`: the Keychain item and the token file, the newer
|
|
76
|
+
* by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
|
|
77
|
+
* in-memory fallback, which can surface a different session when the file
|
|
78
|
+
* is missing or unreadable.
|
|
79
|
+
*/
|
|
80
|
+
export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
|
|
81
|
+
const file = personTokenCacheFile(home, env);
|
|
82
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The person's cached session: the shared Keychain item and hq-cloud's file
|
|
86
|
+
* loader, the newer by `expiresAt`. Writes still go only to the file in this
|
|
87
|
+
* step.
|
|
88
|
+
*/
|
|
89
|
+
export function loadCachedTokens(env = process.env, keychain = {}) {
|
|
90
|
+
const home = os.homedir();
|
|
91
|
+
const file = personTokenCacheFile(home, env);
|
|
92
|
+
const fromFile = loadCachedTokensFromFile();
|
|
93
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=person-token-cache.js.map
|
|
@@ -109,7 +109,17 @@ export type CallerIdentity = {
|
|
|
109
109
|
* {@link resolveCallerPersonUid} does. Use this where the server accepts an
|
|
110
110
|
* agent principal; use `resolveCallerPersonUid` where only a person can act.
|
|
111
111
|
*/
|
|
112
|
-
export declare function resolveCallerUid(token: string, baseUrl?: string
|
|
112
|
+
export declare function resolveCallerUid(token: string, baseUrl?: string, opts?: {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}): Promise<CallerIdentity>;
|
|
115
|
+
/**
|
|
116
|
+
* Thrown when the person lookup succeeds but the caller has no person entity.
|
|
117
|
+
* This is a definitive answer, unlike a network or HTTP failure, so callers
|
|
118
|
+
* that cache lookups may remember it.
|
|
119
|
+
*/
|
|
120
|
+
export declare class NoPersonEntityError extends Error {
|
|
121
|
+
constructor();
|
|
122
|
+
}
|
|
113
123
|
/** Thrown when a person-only command runs under an agent machine identity. */
|
|
114
124
|
export declare function agentIdentityUnsupportedError(agentUid: string, command?: string): Error;
|
|
115
125
|
/**
|
|
@@ -120,6 +130,16 @@ export declare function agentIdentityUnsupportedError(agentUid: string, command?
|
|
|
120
130
|
export declare function resolveCallerPersonUid(token: string, baseUrl?: string, opts?: {
|
|
121
131
|
command?: string;
|
|
122
132
|
}): Promise<string>;
|
|
133
|
+
/**
|
|
134
|
+
* A signal that aborts when any input signal aborts, with that signal's
|
|
135
|
+
* reason. It stands in for `AbortSignal.any`, which Node only added in 20.3,
|
|
136
|
+
* so the CLI keeps working on Node 20.0. Call `dispose` once the guarded work
|
|
137
|
+
* settles to remove the listeners from the inputs.
|
|
138
|
+
*/
|
|
139
|
+
export declare function anySignal(signals: AbortSignal[]): {
|
|
140
|
+
signal: AbortSignal;
|
|
141
|
+
dispose: () => void;
|
|
142
|
+
};
|
|
123
143
|
export declare function getEntityUid(token: string, opts: {
|
|
124
144
|
personal?: boolean;
|
|
125
145
|
companySlug?: string;
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -735,11 +735,22 @@ export function agentUidFromToken(token) {
|
|
|
735
735
|
* {@link resolveCallerPersonUid} does. Use this where the server accepts an
|
|
736
736
|
* agent principal; use `resolveCallerPersonUid` where only a person can act.
|
|
737
737
|
*/
|
|
738
|
-
export async function resolveCallerUid(token, baseUrl) {
|
|
738
|
+
export async function resolveCallerUid(token, baseUrl, opts = {}) {
|
|
739
739
|
const agentUid = agentUidFromToken(token);
|
|
740
740
|
if (agentUid)
|
|
741
741
|
return { kind: 'agent', uid: agentUid };
|
|
742
|
-
return { kind: 'person', uid: await lookupCallerPersonUid(token, baseUrl) };
|
|
742
|
+
return { kind: 'person', uid: await lookupCallerPersonUid(token, baseUrl, opts.signal) };
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Thrown when the person lookup succeeds but the caller has no person entity.
|
|
746
|
+
* This is a definitive answer, unlike a network or HTTP failure, so callers
|
|
747
|
+
* that cache lookups may remember it.
|
|
748
|
+
*/
|
|
749
|
+
export class NoPersonEntityError extends Error {
|
|
750
|
+
constructor() {
|
|
751
|
+
super('No person entity found for the caller. Sign in to HQ once to provision one.');
|
|
752
|
+
this.name = 'NoPersonEntityError';
|
|
753
|
+
}
|
|
743
754
|
}
|
|
744
755
|
/** Thrown when a person-only command runs under an agent machine identity. */
|
|
745
756
|
export function agentIdentityUnsupportedError(agentUid, command) {
|
|
@@ -758,14 +769,52 @@ export async function resolveCallerPersonUid(token, baseUrl, opts = {}) {
|
|
|
758
769
|
throw agentIdentityUnsupportedError(agentUid, opts.command);
|
|
759
770
|
return lookupCallerPersonUid(token, baseUrl);
|
|
760
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* A signal that aborts when any input signal aborts, with that signal's
|
|
774
|
+
* reason. It stands in for `AbortSignal.any`, which Node only added in 20.3,
|
|
775
|
+
* so the CLI keeps working on Node 20.0. Call `dispose` once the guarded work
|
|
776
|
+
* settles to remove the listeners from the inputs.
|
|
777
|
+
*/
|
|
778
|
+
export function anySignal(signals) {
|
|
779
|
+
const controller = new AbortController();
|
|
780
|
+
const listeners = [];
|
|
781
|
+
const dispose = () => {
|
|
782
|
+
for (const [input, listener] of listeners.splice(0))
|
|
783
|
+
input.removeEventListener('abort', listener);
|
|
784
|
+
};
|
|
785
|
+
for (const input of signals) {
|
|
786
|
+
if (input.aborted) {
|
|
787
|
+
dispose();
|
|
788
|
+
controller.abort(input.reason);
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
const listener = () => {
|
|
792
|
+
dispose();
|
|
793
|
+
controller.abort(input.reason);
|
|
794
|
+
};
|
|
795
|
+
input.addEventListener('abort', listener, { once: true });
|
|
796
|
+
listeners.push([input, listener]);
|
|
797
|
+
}
|
|
798
|
+
return { signal: controller.signal, dispose };
|
|
799
|
+
}
|
|
761
800
|
// Same selection rule as the backend's `resolveCallerPersonUid`: ascending by
|
|
762
801
|
// createdAt, tie-break by uid ascending. Returns the `prs_*` UID.
|
|
763
|
-
async function lookupCallerPersonUid(token, baseUrl) {
|
|
802
|
+
async function lookupCallerPersonUid(token, baseUrl, signal) {
|
|
803
|
+
const timeout = AbortSignal.timeout(identityLookupTimeoutMs());
|
|
804
|
+
const combined = signal ? anySignal([timeout, signal]) : { signal: timeout, dispose: () => { } };
|
|
805
|
+
try {
|
|
806
|
+
return await lookupCallerPersonUidWith(token, baseUrl, combined.signal);
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
combined.dispose();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function lookupCallerPersonUidWith(token, baseUrl, signal) {
|
|
764
813
|
const res = await vaultApiFetch({
|
|
765
814
|
token,
|
|
766
815
|
path: '/entity/by-type/person',
|
|
767
816
|
baseUrl,
|
|
768
|
-
signal
|
|
817
|
+
signal,
|
|
769
818
|
});
|
|
770
819
|
if (!res.ok) {
|
|
771
820
|
if (isUnavailableSecretLoadHttpStatus(res.status)) {
|
|
@@ -776,7 +825,7 @@ async function lookupCallerPersonUid(token, baseUrl) {
|
|
|
776
825
|
const data = (await res.json());
|
|
777
826
|
const persons = (data.entities ?? []).filter((e) => e.type === 'person');
|
|
778
827
|
if (persons.length === 0) {
|
|
779
|
-
throw new
|
|
828
|
+
throw new NoPersonEntityError();
|
|
780
829
|
}
|
|
781
830
|
persons.sort((a, b) => {
|
|
782
831
|
const ac = a.createdAt ?? '';
|