@indigoai-us/hq-cloud 6.14.48 → 6.14.49
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/.github/workflows/publish.yml +23 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +25 -6
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +86 -1
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +45 -0
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +157 -36
- package/dist/cognito-auth.js.map +1 -1
- package/dist/read-only-state-dir.test.d.ts +27 -0
- package/dist/read-only-state-dir.test.d.ts.map +1 -0
- package/dist/read-only-state-dir.test.js +167 -0
- package/dist/read-only-state-dir.test.js.map +1 -0
- package/package.json +2 -2
- package/src/cli/reindex.test.ts +109 -1
- package/src/cli/reindex.ts +24 -6
- package/src/cognito-auth.ts +164 -37
- package/src/read-only-state-dir.test.ts +188 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-only-state-dir.test.js","sourceRoot":"","sources":["../src/read-only-state-dir.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cloud",
|
|
3
|
-
"version": "6.14.
|
|
4
|
-
"description": "HQ by Indigo cloud sync engine
|
|
3
|
+
"version": "6.14.49",
|
|
4
|
+
"description": "HQ by Indigo cloud sync engine — bidirectional S3 sync for mobile access",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
package/src/cli/reindex.test.ts
CHANGED
|
@@ -695,7 +695,8 @@ describe("reindex", () => {
|
|
|
695
695
|
const claudeSlug = (p: string): string => p.replace(/[^a-zA-Z0-9]/g, "-");
|
|
696
696
|
const grokKey = (p: string): string => encodeURIComponent(p);
|
|
697
697
|
|
|
698
|
-
const dest = (): string =>
|
|
698
|
+
const dest = (reindexRoot = root): string =>
|
|
699
|
+
path.join(reindexRoot, "workspace", ".session-logs");
|
|
699
700
|
|
|
700
701
|
beforeEach(() => {
|
|
701
702
|
claudeDir = fs.mkdtempSync(path.join(os.tmpdir(), "ms-claude-"));
|
|
@@ -746,6 +747,30 @@ describe("reindex", () => {
|
|
|
746
747
|
return abs;
|
|
747
748
|
}
|
|
748
749
|
|
|
750
|
+
// Build the real root under a canonical tmp path so this fixture remains
|
|
751
|
+
// stable even when the platform itself exposes its temp directory via a
|
|
752
|
+
// symlink (for example, /tmp -> /private/tmp on macOS).
|
|
753
|
+
function makeSymlinkRoot(): { realRoot: string; linkRoot: string; cleanup: () => void } | null {
|
|
754
|
+
const fixtureDir = fs.mkdtempSync(
|
|
755
|
+
path.join(fs.realpathSync.native(os.tmpdir()), "ms-session-log-alias-"),
|
|
756
|
+
);
|
|
757
|
+
const realRoot = path.join(fixtureDir, "real-hq");
|
|
758
|
+
const linkRoot = path.join(fixtureDir, "hq-alias");
|
|
759
|
+
fs.mkdirSync(realRoot);
|
|
760
|
+
try {
|
|
761
|
+
fs.symlinkSync(realRoot, linkRoot, "dir");
|
|
762
|
+
} catch (err) {
|
|
763
|
+
fs.rmSync(fixtureDir, { recursive: true, force: true });
|
|
764
|
+
if ((err as NodeJS.ErrnoException).code === "EPERM") return null;
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
realRoot,
|
|
769
|
+
linkRoot,
|
|
770
|
+
cleanup: () => fs.rmSync(fixtureDir, { recursive: true, force: true }),
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
749
774
|
// ── Claude ──────────────────────────────────────────────────────────────
|
|
750
775
|
it("claude: captures the HQ-root project AND descendant projects under claude/", () => {
|
|
751
776
|
const rootName = seedClaude(root);
|
|
@@ -776,6 +801,23 @@ describe("reindex", () => {
|
|
|
776
801
|
expect(fs.readFileSync(path.join(dest(), "orphan.txt"), "utf8")).toBe("keep\n");
|
|
777
802
|
});
|
|
778
803
|
|
|
804
|
+
it("claude: captures a realpath-keyed project when reindex uses a symlink alias", () => {
|
|
805
|
+
const fixture = makeSymlinkRoot();
|
|
806
|
+
if (!fixture) return; // Symlink-restricted host (for example, Windows without Developer Mode).
|
|
807
|
+
try {
|
|
808
|
+
const realName = seedClaude(fixture.realRoot);
|
|
809
|
+
const aliasName = claudeSlug(fixture.linkRoot);
|
|
810
|
+
|
|
811
|
+
expect(reindex({ repoRoot: fixture.linkRoot }).status).toBe(0);
|
|
812
|
+
|
|
813
|
+
const c = path.join(dest(fixture.realRoot), "claude");
|
|
814
|
+
expect(fs.readFileSync(path.join(c, realName, "session.jsonl"), "utf8")).toBe("claude\n");
|
|
815
|
+
expect(fs.existsSync(path.join(c, aliasName))).toBe(false);
|
|
816
|
+
} finally {
|
|
817
|
+
fixture.cleanup();
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
779
821
|
// ── Grok ────────────────────────────────────────────────────────────────
|
|
780
822
|
it("grok: captures the HQ-root and descendant cwd folders under grok/, excludes others", () => {
|
|
781
823
|
const rootName = seedGrok(root);
|
|
@@ -790,6 +832,25 @@ describe("reindex", () => {
|
|
|
790
832
|
expect(fs.existsSync(path.join(g, grokKey("/tmp/benchmark")))).toBe(false);
|
|
791
833
|
});
|
|
792
834
|
|
|
835
|
+
it("grok: captures a realpath-keyed session when reindex uses a symlink alias", () => {
|
|
836
|
+
const fixture = makeSymlinkRoot();
|
|
837
|
+
if (!fixture) return; // Symlink-restricted host (for example, Windows without Developer Mode).
|
|
838
|
+
try {
|
|
839
|
+
const realName = seedGrok(fixture.realRoot);
|
|
840
|
+
|
|
841
|
+
expect(reindex({ repoRoot: fixture.linkRoot }).status).toBe(0);
|
|
842
|
+
|
|
843
|
+
expect(
|
|
844
|
+
fs.readFileSync(
|
|
845
|
+
path.join(dest(fixture.realRoot), "grok", realName, "uuid-1", "chat_history.jsonl"),
|
|
846
|
+
"utf8",
|
|
847
|
+
),
|
|
848
|
+
).toBe("grok\n");
|
|
849
|
+
} finally {
|
|
850
|
+
fixture.cleanup();
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
793
854
|
// ── Codex ───────────────────────────────────────────────────────────────
|
|
794
855
|
it("codex: captures rollouts whose recorded cwd is the HQ tree, mirroring the date path", () => {
|
|
795
856
|
seedCodex(root, ["2026", "07", "13"], "rollout-root.jsonl");
|
|
@@ -805,6 +866,24 @@ describe("reindex", () => {
|
|
|
805
866
|
expect(fs.existsSync(path.join(cx, "rollout-other.jsonl"))).toBe(false);
|
|
806
867
|
});
|
|
807
868
|
|
|
869
|
+
it("codex: captures a realpath-keyed rollout when reindex uses a symlink alias", () => {
|
|
870
|
+
const fixture = makeSymlinkRoot();
|
|
871
|
+
if (!fixture) return; // Symlink-restricted host (for example, Windows without Developer Mode).
|
|
872
|
+
try {
|
|
873
|
+
seedCodex(path.join(fixture.realRoot, "workspace", "worktrees", "x"), ["2026", "07", "13"], "rollout-real.jsonl");
|
|
874
|
+
|
|
875
|
+
expect(reindex({ repoRoot: fixture.linkRoot }).status).toBe(0);
|
|
876
|
+
|
|
877
|
+
expect(
|
|
878
|
+
fs.existsSync(
|
|
879
|
+
path.join(dest(fixture.realRoot), "codex", "2026", "07", "13", "rollout-real.jsonl"),
|
|
880
|
+
),
|
|
881
|
+
).toBe(true);
|
|
882
|
+
} finally {
|
|
883
|
+
fixture.cleanup();
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
808
887
|
it("codex: skips a rollout already captured and unchanged (mtime guard)", () => {
|
|
809
888
|
const src = seedCodex(root, ["2026", "07", "13"], "rollout-a.jsonl");
|
|
810
889
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
@@ -827,6 +906,35 @@ describe("reindex", () => {
|
|
|
827
906
|
expect(fs.readFileSync(destFile, "utf8")).toContain("NEW");
|
|
828
907
|
});
|
|
829
908
|
|
|
909
|
+
it("captures exact-root and descendant entries once when the root is already canonical", () => {
|
|
910
|
+
const canonicalRoot = fs.realpathSync.native(root);
|
|
911
|
+
const claudeNames = [
|
|
912
|
+
seedClaude(canonicalRoot),
|
|
913
|
+
seedClaude(path.join(canonicalRoot, "workspace", "worktrees", "x")),
|
|
914
|
+
];
|
|
915
|
+
const grokNames = [
|
|
916
|
+
seedGrok(canonicalRoot),
|
|
917
|
+
seedGrok(path.join(canonicalRoot, "repos", "private", "x")),
|
|
918
|
+
];
|
|
919
|
+
seedCodex(canonicalRoot, ["2026", "07", "15"], "rollout-root.jsonl");
|
|
920
|
+
seedCodex(
|
|
921
|
+
path.join(canonicalRoot, "workspace", "worktrees", "x"),
|
|
922
|
+
["2026", "07", "15"],
|
|
923
|
+
"rollout-sub.jsonl",
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
expect(reindex({ repoRoot: canonicalRoot }).status).toBe(0);
|
|
927
|
+
|
|
928
|
+
expect(fs.readdirSync(path.join(dest(canonicalRoot), "claude")).sort()).toEqual(
|
|
929
|
+
claudeNames.sort(),
|
|
930
|
+
);
|
|
931
|
+
expect(fs.readdirSync(path.join(dest(canonicalRoot), "grok")).sort()).toEqual(grokNames.sort());
|
|
932
|
+
expect(fs.readdirSync(path.join(dest(canonicalRoot), "codex", "2026", "07", "15")).sort()).toEqual([
|
|
933
|
+
"rollout-root.jsonl",
|
|
934
|
+
"rollout-sub.jsonl",
|
|
935
|
+
]);
|
|
936
|
+
});
|
|
937
|
+
|
|
830
938
|
// ── Cross-harness ─────────────────────────────────────────────────────────
|
|
831
939
|
it("all three harnesses land side-by-side under their own subfolders", () => {
|
|
832
940
|
seedClaude(root);
|
package/src/cli/reindex.ts
CHANGED
|
@@ -266,6 +266,23 @@ function copySessionLogs(root: string): void {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// Candidate spellings of the HQ root to match harness-recorded cwds against:
|
|
270
|
+
// the lexical root AND its realpath. Harnesses key logs by the cwd the user
|
|
271
|
+
// actually ran in, which may reach the same tree through a symlink or a
|
|
272
|
+
// case-different spelling (macOS volumes are case-insensitive); realpath
|
|
273
|
+
// canonicalizes both. Best-effort: realpath may throw if root is gone.
|
|
274
|
+
// De-duplicated, lexical root first (stable, preferred dest naming).
|
|
275
|
+
function hqRootCandidates(root: string): string[] {
|
|
276
|
+
const out = [root];
|
|
277
|
+
try {
|
|
278
|
+
const real = fs.realpathSync.native(root);
|
|
279
|
+
if (real && real !== root) out.push(real);
|
|
280
|
+
} catch {
|
|
281
|
+
// best-effort — a missing/unreadable root just means no realpath candidate
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
269
286
|
/** `fs.statSync` without throwing (follows symlinks; null when missing). */
|
|
270
287
|
function statOrNull(p: string): fs.Stats | null {
|
|
271
288
|
try {
|
|
@@ -300,9 +317,9 @@ function copyClaudeSessionLogs(root: string, destBase: string): void {
|
|
|
300
317
|
const projectsDir = path.join(configDir, "projects");
|
|
301
318
|
if (!isDir(projectsDir)) return;
|
|
302
319
|
|
|
303
|
-
const
|
|
320
|
+
const slugs = [...new Set(hqRootCandidates(root).map((r) => r.replace(/[^a-zA-Z0-9]/g, "-")))];
|
|
304
321
|
for (const entry of globEntries(projectsDir)) {
|
|
305
|
-
if (entry
|
|
322
|
+
if (!slugs.some((slug) => entry === slug || entry.startsWith(`${slug}-`))) continue;
|
|
306
323
|
const src = path.join(projectsDir, entry);
|
|
307
324
|
if (!isDir(src)) continue;
|
|
308
325
|
copyTreeOverlay(src, path.join(destBase, "claude", entry), "claude session logs");
|
|
@@ -318,9 +335,9 @@ function copyGrokSessionLogs(root: string, destBase: string): void {
|
|
|
318
335
|
const sessionsDir = path.join(grokHome, "sessions");
|
|
319
336
|
if (!isDir(sessionsDir)) return;
|
|
320
337
|
|
|
321
|
-
const
|
|
338
|
+
const keys = [...new Set(hqRootCandidates(root).map((r) => encodeURIComponent(r)))];
|
|
322
339
|
for (const entry of globEntries(sessionsDir)) {
|
|
323
|
-
if (entry
|
|
340
|
+
if (!keys.some((key) => entry === key || entry.startsWith(`${key}%2F`))) continue;
|
|
324
341
|
const src = path.join(sessionsDir, entry);
|
|
325
342
|
if (!isDir(src)) continue;
|
|
326
343
|
copyTreeOverlay(src, path.join(destBase, "grok", entry), "grok session logs");
|
|
@@ -340,7 +357,8 @@ function copyCodexSessionLogs(root: string, destBase: string): void {
|
|
|
340
357
|
if (!isDir(sessionsDir)) return;
|
|
341
358
|
|
|
342
359
|
const destDir = path.join(destBase, "codex");
|
|
343
|
-
const
|
|
360
|
+
const candidates = hqRootCandidates(root);
|
|
361
|
+
const prefixes = candidates.map((r) => (r.endsWith(path.sep) ? r : r + path.sep));
|
|
344
362
|
|
|
345
363
|
for (const abs of walkFiles(sessionsDir)) {
|
|
346
364
|
if (!abs.endsWith(".jsonl")) continue;
|
|
@@ -355,7 +373,7 @@ function copyCodexSessionLogs(root: string, destBase: string): void {
|
|
|
355
373
|
|
|
356
374
|
const cwd = readCodexRolloutCwd(abs);
|
|
357
375
|
if (cwd === null) continue;
|
|
358
|
-
if (cwd
|
|
376
|
+
if (!candidates.includes(cwd) && !prefixes.some((prefix) => cwd.startsWith(prefix))) continue;
|
|
359
377
|
|
|
360
378
|
if (!safeMkdir(path.dirname(dest), "codex session-log dir")) continue;
|
|
361
379
|
try {
|
package/src/cognito-auth.ts
CHANGED
|
@@ -97,9 +97,39 @@ export class CognitoRefreshError extends CognitoAuthError {
|
|
|
97
97
|
// Token cache (~/.hq/cognito-tokens.json)
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Root for hq CLI state: the token cache and its refresh lock.
|
|
102
|
+
*
|
|
103
|
+
* `HQ_STATE_DIR` overrides it, mirroring the existing `HQ_MACHINE_CREDS_FILE`
|
|
104
|
+
* override for the creds path. Before this existed the location was
|
|
105
|
+
* `os.homedir()/.hq` with NO escape hatch, which made every auth path — machine
|
|
106
|
+
* identity included — require a writable HOME. A child job running with a
|
|
107
|
+
* read-only HQ state directory therefore died on a *cache write* before it ever
|
|
108
|
+
* reached the network:
|
|
109
|
+
*
|
|
110
|
+
* EACCES: permission denied, open '…/.hq/cognito-tokens.json.lock.candidate.…'
|
|
111
|
+
*
|
|
112
|
+
* Pointing HQ_STATE_DIR at a writable tmpfs lets such a job authenticate while
|
|
113
|
+
* leaving the credentials themselves wherever they are.
|
|
114
|
+
*
|
|
115
|
+
* Resolved per call (not once at import) so a caller can set it during startup
|
|
116
|
+
* and so tests can exercise both branches.
|
|
117
|
+
*/
|
|
118
|
+
export function hqStateDir(): string {
|
|
119
|
+
const override = process.env.HQ_STATE_DIR?.trim();
|
|
120
|
+
return override !== undefined && override.length > 0
|
|
121
|
+
? override
|
|
122
|
+
: path.join(os.homedir(), ".hq");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The Cognito token cache file inside {@link hqStateDir}. */
|
|
126
|
+
export function tokenCacheFile(): string {
|
|
127
|
+
return path.join(hqStateDir(), "cognito-tokens.json");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function tokenInvalidationPrefix(): string {
|
|
131
|
+
return `${tokenCacheFile()}.invalid.`;
|
|
132
|
+
}
|
|
103
133
|
|
|
104
134
|
/** Stable, non-secret identity for one access-token generation. */
|
|
105
135
|
export function accessTokenFingerprint(accessToken: string): string {
|
|
@@ -108,7 +138,7 @@ export function accessTokenFingerprint(accessToken: string): string {
|
|
|
108
138
|
|
|
109
139
|
function invalidationPathForFingerprint(fingerprint: string): string | null {
|
|
110
140
|
return /^[a-f0-9]{64}$/i.test(fingerprint)
|
|
111
|
-
? `${
|
|
141
|
+
? `${tokenInvalidationPrefix()}${fingerprint.toLowerCase()}`
|
|
112
142
|
: null;
|
|
113
143
|
}
|
|
114
144
|
|
|
@@ -123,8 +153,8 @@ export function invalidateCachedTokensByFingerprint(
|
|
|
123
153
|
if (!fingerprint) return;
|
|
124
154
|
const invalidationPath = invalidationPathForFingerprint(fingerprint);
|
|
125
155
|
if (!invalidationPath) return;
|
|
126
|
-
if (!fs.existsSync(
|
|
127
|
-
fs.mkdirSync(
|
|
156
|
+
if (!fs.existsSync(hqStateDir())) {
|
|
157
|
+
fs.mkdirSync(hqStateDir(), { recursive: true, mode: 0o700 });
|
|
128
158
|
}
|
|
129
159
|
try {
|
|
130
160
|
fs.writeFileSync(invalidationPath, "", { mode: 0o600, flag: "wx" });
|
|
@@ -144,46 +174,81 @@ function tokenIsInvalidated(accessToken: string): boolean {
|
|
|
144
174
|
return marker !== null && fs.existsSync(marker);
|
|
145
175
|
}
|
|
146
176
|
|
|
177
|
+
/**
|
|
178
|
+
* True for the errno family a read-only / not-writable state directory raises.
|
|
179
|
+
*
|
|
180
|
+
* A job whose HQ state dir is mounted read-only (container, restricted
|
|
181
|
+
* sandbox, root-owned dir entered as another user) must still be able to
|
|
182
|
+
* AUTHENTICATE — persisting the session is an optimisation, not a requirement.
|
|
183
|
+
*/
|
|
184
|
+
function isNotWritableError(err: unknown): boolean {
|
|
185
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
186
|
+
return code === "EROFS" || code === "EACCES" || code === "EPERM";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Process-local fallback used when the state directory cannot be written.
|
|
191
|
+
*
|
|
192
|
+
* Keeps the cache semantics intact for the life of the process instead of
|
|
193
|
+
* failing the call outright. It is deliberately NOT a substitute for the shared
|
|
194
|
+
* on-disk cache: a fresh subprocess starts empty and re-mints.
|
|
195
|
+
*/
|
|
196
|
+
let inMemoryCachedTokens: CognitoTokens | null = null;
|
|
197
|
+
|
|
147
198
|
export function loadCachedTokens(): CognitoTokens | null {
|
|
148
|
-
if (!fs.existsSync(
|
|
199
|
+
if (!fs.existsSync(tokenCacheFile())) return inMemoryCachedTokens;
|
|
149
200
|
try {
|
|
150
|
-
const raw = fs.readFileSync(
|
|
201
|
+
const raw = fs.readFileSync(tokenCacheFile(), "utf-8");
|
|
151
202
|
const tokens = JSON.parse(raw) as CognitoTokens;
|
|
152
203
|
if (!tokens.accessToken || tokenIsInvalidated(tokens.accessToken)) return null;
|
|
153
204
|
return tokens;
|
|
154
205
|
} catch {
|
|
155
|
-
return
|
|
206
|
+
return inMemoryCachedTokens;
|
|
156
207
|
}
|
|
157
208
|
}
|
|
158
209
|
|
|
159
210
|
export function saveCachedTokens(tokens: CognitoTokens): void {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
211
|
+
// Remember in-process FIRST: whatever happens to the disk write below, this
|
|
212
|
+
// process must not lose a session it already successfully minted.
|
|
213
|
+
inMemoryCachedTokens = tokens;
|
|
214
|
+
try {
|
|
215
|
+
if (!fs.existsSync(hqStateDir())) {
|
|
216
|
+
fs.mkdirSync(hqStateDir(), { recursive: true, mode: 0o700 });
|
|
217
|
+
}
|
|
218
|
+
const invalidationPath = invalidationPathForFingerprint(
|
|
219
|
+
accessTokenFingerprint(tokens.accessToken),
|
|
220
|
+
);
|
|
221
|
+
if (invalidationPath && fs.existsSync(invalidationPath)) {
|
|
222
|
+
fs.unlinkSync(invalidationPath);
|
|
223
|
+
}
|
|
224
|
+
const tmpPath = path.join(
|
|
225
|
+
hqStateDir(),
|
|
226
|
+
`.cognito-tokens.json.tmp.${process.pid}`,
|
|
227
|
+
);
|
|
228
|
+
fs.writeFileSync(tmpPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
229
|
+
fs.renameSync(tmpPath, tokenCacheFile());
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// A read-only state dir degrades to the in-memory cache. Any other error
|
|
232
|
+
// (disk full, corrupt path) is still a real fault and propagates.
|
|
233
|
+
if (!isNotWritableError(err)) throw err;
|
|
168
234
|
}
|
|
169
|
-
const tmpPath = path.join(HQ_DIR, `.cognito-tokens.json.tmp.${process.pid}`);
|
|
170
|
-
fs.writeFileSync(tmpPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
171
|
-
fs.renameSync(tmpPath, TOKEN_FILE);
|
|
172
235
|
}
|
|
173
236
|
|
|
174
237
|
export function clearCachedTokens(): void {
|
|
175
|
-
if (fs.existsSync(
|
|
176
|
-
if (!fs.existsSync(
|
|
177
|
-
const markerPrefix = `${path.basename(
|
|
178
|
-
for (const entry of fs.readdirSync(
|
|
238
|
+
if (fs.existsSync(tokenCacheFile())) fs.unlinkSync(tokenCacheFile());
|
|
239
|
+
if (!fs.existsSync(hqStateDir())) return;
|
|
240
|
+
const markerPrefix = `${path.basename(tokenCacheFile())}.invalid.`;
|
|
241
|
+
for (const entry of fs.readdirSync(hqStateDir())) {
|
|
179
242
|
if (entry.startsWith(markerPrefix)) {
|
|
180
|
-
fs.unlinkSync(path.join(
|
|
243
|
+
fs.unlinkSync(path.join(hqStateDir(), entry));
|
|
181
244
|
}
|
|
182
245
|
}
|
|
183
246
|
}
|
|
184
247
|
|
|
185
248
|
/** Advisory lockfile guarding the read→refresh→save cycle (see withRefreshLock). */
|
|
186
|
-
|
|
249
|
+
function refreshLockFile(): string {
|
|
250
|
+
return `${tokenCacheFile()}.lock`;
|
|
251
|
+
}
|
|
187
252
|
|
|
188
253
|
/** Thrown when the refresh lock can't be acquired within the deadline. */
|
|
189
254
|
export class RefreshLockTimeoutError extends Error {
|
|
@@ -196,7 +261,7 @@ export class RefreshLockTimeoutError extends Error {
|
|
|
196
261
|
/** The pid recorded in the lockfile, or null if absent/empty/malformed. */
|
|
197
262
|
function readLockOwnerPid(): number | null {
|
|
198
263
|
try {
|
|
199
|
-
const pid = Number.parseInt(fs.readFileSync(
|
|
264
|
+
const pid = Number.parseInt(fs.readFileSync(refreshLockFile(), "utf-8").trim(), 10);
|
|
200
265
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
201
266
|
} catch {
|
|
202
267
|
return null;
|
|
@@ -244,23 +309,38 @@ function processAlive(pid: number): boolean {
|
|
|
244
309
|
async function withRefreshLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
245
310
|
const timeoutMs = 20_000;
|
|
246
311
|
const pollMs = 50;
|
|
247
|
-
|
|
248
|
-
fs.
|
|
312
|
+
try {
|
|
313
|
+
if (!fs.existsSync(hqStateDir())) {
|
|
314
|
+
fs.mkdirSync(hqStateDir(), { recursive: true, mode: 0o700 });
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (!isNotWritableError(err)) throw err;
|
|
318
|
+
return await fn();
|
|
249
319
|
}
|
|
250
320
|
// Publish a fully-written inode with an atomic hard link. Creating the lock
|
|
251
321
|
// itself and then writing the pid leaves an observable empty-file window in
|
|
252
322
|
// which a contender can misclassify and unlink a live holder's lock.
|
|
253
|
-
const candidate = `${
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
323
|
+
const candidate = `${refreshLockFile()}.candidate.${process.pid}.${crypto.randomBytes(8).toString("hex")}`;
|
|
324
|
+
try {
|
|
325
|
+
fs.writeFileSync(candidate, String(process.pid), {
|
|
326
|
+
flag: "wx",
|
|
327
|
+
mode: 0o600,
|
|
328
|
+
});
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if (!isNotWritableError(err)) throw err;
|
|
331
|
+
// The state dir is not writable, so NO process can be persisting to this
|
|
332
|
+
// cache — there is nothing to serialize against and the lock's only job is
|
|
333
|
+
// moot. Run unlocked rather than failing the caller's authentication on a
|
|
334
|
+
// lockfile we neither need nor can create. (Before this, a read-only state
|
|
335
|
+
// dir raised EACCES here, killing machine auth before any network call.)
|
|
336
|
+
return await fn();
|
|
337
|
+
}
|
|
258
338
|
const deadline = Date.now() + timeoutMs;
|
|
259
339
|
let held = false;
|
|
260
340
|
try {
|
|
261
341
|
for (;;) {
|
|
262
342
|
try {
|
|
263
|
-
fs.linkSync(candidate,
|
|
343
|
+
fs.linkSync(candidate, refreshLockFile());
|
|
264
344
|
held = true;
|
|
265
345
|
break;
|
|
266
346
|
} catch (err) {
|
|
@@ -268,7 +348,7 @@ async function withRefreshLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
268
348
|
const owner = readLockOwnerPid();
|
|
269
349
|
if (owner === null || (owner !== process.pid && !processAlive(owner))) {
|
|
270
350
|
try {
|
|
271
|
-
fs.unlinkSync(
|
|
351
|
+
fs.unlinkSync(refreshLockFile()); // crashed/malformed holder — reclaim
|
|
272
352
|
} catch {
|
|
273
353
|
/* raced with another reclaimer */
|
|
274
354
|
}
|
|
@@ -283,7 +363,7 @@ async function withRefreshLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
283
363
|
// Release ONLY our own lock — never clobber a successor's.
|
|
284
364
|
if (held && readLockOwnerPid() === process.pid) {
|
|
285
365
|
try {
|
|
286
|
-
fs.unlinkSync(
|
|
366
|
+
fs.unlinkSync(refreshLockFile());
|
|
287
367
|
} catch {
|
|
288
368
|
/* already released */
|
|
289
369
|
}
|
|
@@ -544,6 +624,48 @@ export function isMachineIdentity(): boolean {
|
|
|
544
624
|
return loadMachineCreds() !== null;
|
|
545
625
|
}
|
|
546
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Whether the caller declared this process MUST run as a machine identity
|
|
629
|
+
* (`HQ_REQUIRE_MACHINE_IDENTITY=1`).
|
|
630
|
+
*
|
|
631
|
+
* Set it on unattended runners — child jobs, workers, skill subprocesses. It
|
|
632
|
+
* converts the worst failure mode in this module into a clear one.
|
|
633
|
+
*
|
|
634
|
+
* Without it, a machine context that cannot find its credentials does not
|
|
635
|
+
* error: `isMachineIdentity()` returns false and the caller quietly proceeds to
|
|
636
|
+
* the HUMAN Cognito path. On a headless box that means "launching browser
|
|
637
|
+
* sign-in" and a hang until something times out — observed in production, and
|
|
638
|
+
* reported by a client as child jobs "falling back to refreshing a human HQ
|
|
639
|
+
* session". The cause (a different HOME, so a different creds path) is nowhere
|
|
640
|
+
* in that message.
|
|
641
|
+
*/
|
|
642
|
+
export function machineIdentityRequired(): boolean {
|
|
643
|
+
const raw = process.env.HQ_REQUIRE_MACHINE_IDENTITY?.trim().toLowerCase();
|
|
644
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Throw a typed, diagnosable error when machine identity is REQUIRED but the
|
|
649
|
+
* creds file is not readable. Names the exact path checked and how it was
|
|
650
|
+
* derived, so the fix (set `HQ_MACHINE_CREDS_FILE`, or restore HOME) is
|
|
651
|
+
* readable straight off the error. No-op when not required or already a
|
|
652
|
+
* machine identity.
|
|
653
|
+
*/
|
|
654
|
+
export function assertMachineIdentityWhenRequired(): void {
|
|
655
|
+
if (!machineIdentityRequired() || isMachineIdentity()) return;
|
|
656
|
+
const explicit = process.env.HQ_MACHINE_CREDS_FILE?.trim();
|
|
657
|
+
const source =
|
|
658
|
+
explicit !== undefined && explicit.length > 0
|
|
659
|
+
? "HQ_MACHINE_CREDS_FILE"
|
|
660
|
+
: `os.homedir()=${os.homedir()}`;
|
|
661
|
+
throw new CognitoAuthError(
|
|
662
|
+
`HQ_REQUIRE_MACHINE_IDENTITY is set but no usable machine credentials were ` +
|
|
663
|
+
`found at ${machineCredsFilePath()} (path from ${source}). Refusing to ` +
|
|
664
|
+
`fall back to an interactive human session. Set HQ_MACHINE_CREDS_FILE to ` +
|
|
665
|
+
`the credentials file, or run with the HOME that owns it.`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
547
669
|
interface InitiateAuthResponse {
|
|
548
670
|
AuthenticationResult?: {
|
|
549
671
|
AccessToken?: string;
|
|
@@ -1231,6 +1353,11 @@ export async function getValidAccessToken(
|
|
|
1231
1353
|
return machine.idToken;
|
|
1232
1354
|
}
|
|
1233
1355
|
|
|
1356
|
+
// Past this point we are on the HUMAN path (cache -> refresh -> browser).
|
|
1357
|
+
// An unattended runner that declared itself a machine must fail here with a
|
|
1358
|
+
// diagnosable error rather than silently trying to sign a person in.
|
|
1359
|
+
assertMachineIdentityWhenRequired();
|
|
1360
|
+
|
|
1234
1361
|
let cached = loadCachedTokens();
|
|
1235
1362
|
|
|
1236
1363
|
// Stale-pool detection: if the cached access token was issued by a
|