@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,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): Promise<CallerIdentity>;
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;
@@ -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: AbortSignal.timeout(identityLookupTimeoutMs()),
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 Error('No person entity found for the caller. Sign in to HQ once to provision one.');
828
+ throw new NoPersonEntityError();
780
829
  }
781
830
  persons.sort((a, b) => {
782
831
  const ac = a.createdAt ?? '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.133",
3
+ "version": "5.345.135",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {