@primitive.ai/prim 0.1.0-alpha.44 → 0.1.0-alpha.46

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.
@@ -1,44 +1,211 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/daemon/server.ts
4
- import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
4
+ import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
5
5
  import { createServer } from "net";
6
6
  import { homedir as homedir5 } from "os";
7
- import { join as join6 } from "path";
7
+ import { join as join7 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
10
10
  // src/client.ts
11
- import { existsSync, readFileSync, writeFileSync } from "fs";
11
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
12
+ import {
13
+ chmodSync,
14
+ existsSync as existsSync2,
15
+ mkdirSync as mkdirSync2,
16
+ readFileSync as readFileSync2,
17
+ renameSync,
18
+ rmSync as rmSync2,
19
+ writeFileSync as writeFileSync2
20
+ } from "fs";
12
21
  import { homedir } from "os";
13
- import { join, resolve } from "path";
22
+ import { dirname as dirname2, join as join2, resolve } from "path";
23
+
24
+ // src/lib/file-lock.ts
25
+ import { randomBytes } from "crypto";
26
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
27
+ import { dirname, join } from "path";
28
+ var DEFAULT_TIMEOUT_MS = 3e4;
29
+ var DEFAULT_POLL_MS = 50;
30
+ var DEFAULT_INIT_GRACE_MS = 2e3;
31
+ var DIRECTORY_MODE = 448;
32
+ var FILE_MODE = 384;
33
+ function ownerPath(lockDir) {
34
+ return join(lockDir, "owner.json");
35
+ }
36
+ function readOwner(lockDir) {
37
+ try {
38
+ const owner = JSON.parse(readFileSync(ownerPath(lockDir), "utf8"));
39
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || typeof owner.createdAt !== "number") {
40
+ return null;
41
+ }
42
+ return owner;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function sameOwner(a, b) {
48
+ return a?.pid === b.pid && a.nonce === b.nonce && a.createdAt === b.createdAt;
49
+ }
50
+ function tryTakeLock(lockDir, now) {
51
+ try {
52
+ mkdirSync(lockDir, { mode: DIRECTORY_MODE });
53
+ } catch (error) {
54
+ if (error.code === "EEXIST") return null;
55
+ throw error;
56
+ }
57
+ const owner = {
58
+ pid: process.pid,
59
+ nonce: randomBytes(16).toString("hex"),
60
+ createdAt: now
61
+ };
62
+ try {
63
+ writeFileSync(ownerPath(lockDir), `${JSON.stringify(owner)}
64
+ `, {
65
+ encoding: "utf8",
66
+ mode: FILE_MODE,
67
+ flag: "wx"
68
+ });
69
+ return owner;
70
+ } catch (error) {
71
+ rmSync(lockDir, { recursive: true, force: true });
72
+ throw error;
73
+ }
74
+ }
75
+ function processIsAlive(pid) {
76
+ try {
77
+ process.kill(pid, 0);
78
+ return true;
79
+ } catch (error) {
80
+ return error.code === "EPERM";
81
+ }
82
+ }
83
+ function recoverStaleLock(lockDir, now, alive, initGraceMs) {
84
+ const owner = readOwner(lockDir);
85
+ if (owner) {
86
+ if (alive(owner.pid)) return false;
87
+ if (!sameOwner(readOwner(lockDir), owner)) return false;
88
+ rmSync(lockDir, { recursive: true, force: true });
89
+ return true;
90
+ }
91
+ try {
92
+ const before = statSync(lockDir).mtimeMs;
93
+ if (now - before < initGraceMs) return false;
94
+ if (readOwner(lockDir)) return false;
95
+ if (statSync(lockDir).mtimeMs !== before) return false;
96
+ rmSync(lockDir, { recursive: true, force: true });
97
+ return true;
98
+ } catch {
99
+ return true;
100
+ }
101
+ }
102
+ function abortError(signal) {
103
+ if (signal.reason instanceof Error) return signal.reason;
104
+ const error = new Error("The operation was aborted");
105
+ error.name = "AbortError";
106
+ return error;
107
+ }
108
+ function throwIfAborted(signal) {
109
+ if (signal?.aborted) throw abortError(signal);
110
+ }
111
+ async function defaultSleep(ms, signal) {
112
+ if (!signal) {
113
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
114
+ return;
115
+ }
116
+ throwIfAborted(signal);
117
+ await new Promise((resolve2, reject) => {
118
+ const timer = setTimeout(() => {
119
+ signal.removeEventListener("abort", onAbort);
120
+ resolve2();
121
+ }, ms);
122
+ const onAbort = () => {
123
+ clearTimeout(timer);
124
+ reject(abortError(signal));
125
+ };
126
+ signal.addEventListener("abort", onAbort, { once: true });
127
+ });
128
+ }
129
+ async function withFileLock(lockDir, operation, options = {}) {
130
+ mkdirSync(dirname(lockDir), { recursive: true, mode: DIRECTORY_MODE });
131
+ const nowMs = options.nowMs ?? Date.now;
132
+ const alive = options.processAlive ?? processIsAlive;
133
+ const deadline = nowMs() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
134
+ let owner = null;
135
+ while (!owner) {
136
+ throwIfAborted(options.signal);
137
+ const now = nowMs();
138
+ owner = tryTakeLock(lockDir, now);
139
+ if (owner) break;
140
+ recoverStaleLock(lockDir, now, alive, options.initGraceMs ?? DEFAULT_INIT_GRACE_MS);
141
+ if (now >= deadline) {
142
+ throw new Error(`timed out waiting for file lock ${lockDir}`);
143
+ }
144
+ if (options.sleep) {
145
+ await options.sleep(options.pollMs ?? DEFAULT_POLL_MS);
146
+ throwIfAborted(options.signal);
147
+ } else {
148
+ await defaultSleep(options.pollMs ?? DEFAULT_POLL_MS, options.signal);
149
+ }
150
+ }
151
+ try {
152
+ throwIfAborted(options.signal);
153
+ return await operation();
154
+ } finally {
155
+ if (sameOwner(readOwner(lockDir), owner)) {
156
+ rmSync(lockDir, { recursive: true, force: true });
157
+ }
158
+ }
159
+ }
160
+
161
+ // src/client.ts
162
+ var CONFIG_DIR_MODE = 448;
163
+ var CREDENTIAL_FILE_MODE = 384;
164
+ var REFRESH_THRESHOLD_MS = 6e4;
165
+ var DEFAULT_API_URL = "https://api.getprimitive.ai";
14
166
  function loadEnvFile() {
15
167
  const envVars = {};
16
- const candidates = [".env.local", ".env"];
17
- for (const file of candidates) {
168
+ for (const file of [".env.local", ".env"]) {
18
169
  const filePath = resolve(process.cwd(), file);
19
- if (existsSync(filePath)) {
20
- const content = readFileSync(filePath, "utf-8");
21
- for (const line of content.split("\n")) {
22
- const trimmed = line.trim();
23
- if (!trimmed || trimmed.startsWith("#")) continue;
24
- const eqIdx = trimmed.indexOf("=");
25
- if (eqIdx === -1) continue;
26
- const key = trimmed.slice(0, eqIdx).trim();
27
- const value = trimmed.slice(eqIdx + 1).trim();
28
- envVars[key] = value;
29
- }
170
+ if (!existsSync2(filePath)) continue;
171
+ const content = readFileSync2(filePath, "utf-8");
172
+ for (const line of content.split("\n")) {
173
+ const trimmed = line.trim();
174
+ if (!trimmed || trimmed.startsWith("#")) continue;
175
+ const eqIdx = trimmed.indexOf("=");
176
+ if (eqIdx === -1) continue;
177
+ envVars[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
30
178
  }
31
179
  }
32
180
  return envVars;
33
181
  }
34
- var TOKEN_FILE_PATH = join(homedir(), ".config", "prim", "token");
35
- var REFRESH_TOKEN_PATH = TOKEN_FILE_PATH.replace("/token", "/refresh_token");
36
- var TOKEN_EXPIRES_PATH = join(homedir(), ".config", "prim", "token_expires_at");
37
- var REFRESH_THRESHOLD_MS = 6e4;
38
- function isTokenExpiringSoon() {
39
- if (!existsSync(TOKEN_EXPIRES_PATH)) return false;
40
- const expiresAt = Number(readFileSync(TOKEN_EXPIRES_PATH, "utf-8").trim());
41
- return !Number.isNaN(expiresAt) && Date.now() >= expiresAt - REFRESH_THRESHOLD_MS;
182
+ var TOKEN_FILE_PATH = join2(homedir(), ".config", "prim", "token");
183
+ var REFRESH_TOKEN_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_token");
184
+ var TOKEN_EXPIRES_PATH = join2(dirname2(TOKEN_FILE_PATH), "token_expires_at");
185
+ var TERMINAL_REFRESH_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_terminal");
186
+ var CREDENTIAL_LOCK_PATH = join2(dirname2(TOKEN_FILE_PATH), "credentials.lock");
187
+ function readTrimmed(path) {
188
+ try {
189
+ const value = readFileSync2(path, "utf8").trim();
190
+ return value || void 0;
191
+ } catch {
192
+ return void 0;
193
+ }
194
+ }
195
+ function resolveAuthCredential() {
196
+ if (process.env.PRIM_TOKEN) {
197
+ return { token: process.env.PRIM_TOKEN, source: "environment" };
198
+ }
199
+ const stored = readTrimmed(TOKEN_FILE_PATH);
200
+ if (stored) {
201
+ return { token: stored, source: "token_file" };
202
+ }
203
+ const envToken = loadEnvFile().PRIM_TOKEN;
204
+ return envToken ? { token: envToken, source: "env_file" } : void 0;
205
+ }
206
+ function getSiteUrl() {
207
+ if (process.env.PRIM_API_URL) return process.env.PRIM_API_URL;
208
+ return loadEnvFile().PRIM_API_URL ?? DEFAULT_API_URL;
42
209
  }
43
210
  function getJwtExpiry(token) {
44
211
  const parts = token.split(".");
@@ -50,93 +217,169 @@ function getJwtExpiry(token) {
50
217
  return void 0;
51
218
  }
52
219
  }
53
- function saveTokenExpiry(token, expiresIn) {
54
- const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : getJwtExpiry(token);
55
- if (expiresAt) {
56
- writeFileSync(TOKEN_EXPIRES_PATH, String(expiresAt), { mode: 384 });
220
+ function expiresAtFor(token, expiresIn) {
221
+ return expiresIn === void 0 ? getJwtExpiry(token) : Date.now() + expiresIn * 1e3;
222
+ }
223
+ function ensureConfigDirectory() {
224
+ const directory = dirname2(TOKEN_FILE_PATH);
225
+ mkdirSync2(directory, { recursive: true, mode: CONFIG_DIR_MODE });
226
+ chmodSync(directory, CONFIG_DIR_MODE);
227
+ }
228
+ function atomicWrite(path, content) {
229
+ ensureConfigDirectory();
230
+ const temp = join2(
231
+ dirname2(path),
232
+ `.${path.slice(dirname2(path).length + 1)}.tmp-${process.pid}-${randomBytes2(8).toString("hex")}`
233
+ );
234
+ try {
235
+ writeFileSync2(temp, content, {
236
+ encoding: "utf8",
237
+ mode: CREDENTIAL_FILE_MODE,
238
+ flag: "wx"
239
+ });
240
+ renameSync(temp, path);
241
+ } catch (error) {
242
+ rmSync2(temp, { force: true });
243
+ throw error;
57
244
  }
58
245
  }
246
+ function removeCredentialFile(path) {
247
+ if (!existsSync2(path)) return false;
248
+ rmSync2(path, { force: true });
249
+ return true;
250
+ }
59
251
  function getTokenExpiresAt() {
60
- if (!existsSync(TOKEN_EXPIRES_PATH)) return void 0;
61
- const val = Number(readFileSync(TOKEN_EXPIRES_PATH, "utf-8").trim());
62
- return Number.isNaN(val) ? void 0 : val;
252
+ const stored = readTrimmed(TOKEN_EXPIRES_PATH);
253
+ if (stored === void 0) return void 0;
254
+ const value = Number(stored);
255
+ return Number.isNaN(value) ? void 0 : value;
63
256
  }
64
- function getAuthToken() {
65
- if (process.env.PRIM_TOKEN) {
66
- return process.env.PRIM_TOKEN;
67
- }
68
- if (existsSync(TOKEN_FILE_PATH)) {
69
- const token = readFileSync(TOKEN_FILE_PATH, "utf-8").trim();
70
- if (token) {
71
- return token;
72
- }
73
- }
74
- const envVars = loadEnvFile();
75
- if (envVars.PRIM_TOKEN) {
76
- return envVars.PRIM_TOKEN;
77
- }
78
- return void 0;
257
+ function isTokenExpiringSoon(credential) {
258
+ if (credential.source !== "token_file") return false;
259
+ const expiresAt = getTokenExpiresAt();
260
+ return expiresAt !== void 0 && Date.now() >= expiresAt - REFRESH_THRESHOLD_MS;
79
261
  }
80
- var DEFAULT_API_URL = "https://api.getprimitive.ai";
81
- function getSiteUrl() {
82
- if (process.env.PRIM_API_URL) {
83
- return process.env.PRIM_API_URL;
84
- }
85
- const envVars = loadEnvFile();
86
- if (envVars.PRIM_API_URL) {
87
- return envVars.PRIM_API_URL;
88
- }
89
- return DEFAULT_API_URL;
262
+ function refreshFingerprint(refreshToken2) {
263
+ return createHash("sha256").update(refreshToken2).digest("hex");
90
264
  }
91
- async function performTokenRefresh(options = {}) {
92
- if (!existsSync(REFRESH_TOKEN_PATH)) {
93
- return void 0;
265
+ function terminalFingerprint() {
266
+ return readTrimmed(TERMINAL_REFRESH_PATH);
267
+ }
268
+ function writeTerminalFingerprint(refreshToken2) {
269
+ atomicWrite(TERMINAL_REFRESH_PATH, `${refreshFingerprint(refreshToken2)}
270
+ `);
271
+ }
272
+ function clearTerminalFingerprint() {
273
+ removeCredentialFile(TERMINAL_REFRESH_PATH);
274
+ }
275
+ function isSessionEnded() {
276
+ if (resolveAuthCredential()?.source !== "token_file") return false;
277
+ const refreshToken2 = readTrimmed(REFRESH_TOKEN_PATH);
278
+ const ended = terminalFingerprint();
279
+ return Boolean(refreshToken2 && ended && refreshFingerprint(refreshToken2) === ended);
280
+ }
281
+ function withCredentialLock(operation, options = {}) {
282
+ return withFileLock(CREDENTIAL_LOCK_PATH, operation, options);
283
+ }
284
+ function commitCredentialsUnlocked(credentials) {
285
+ const accessToken = credentials.accessToken.trim();
286
+ const refreshToken2 = credentials.refreshToken.trim();
287
+ if (!accessToken || !refreshToken2) {
288
+ throw new Error("OAuth credentials require both access and refresh tokens");
94
289
  }
95
- const refreshTokenValue = readFileSync(REFRESH_TOKEN_PATH, "utf-8").trim();
96
- if (!refreshTokenValue) {
97
- return void 0;
290
+ atomicWrite(REFRESH_TOKEN_PATH, `${refreshToken2}
291
+ `);
292
+ const expiresAt = expiresAtFor(accessToken, credentials.expiresIn);
293
+ if (expiresAt === void 0) removeCredentialFile(TOKEN_EXPIRES_PATH);
294
+ else atomicWrite(TOKEN_EXPIRES_PATH, `${expiresAt}
295
+ `);
296
+ atomicWrite(TOKEN_FILE_PATH, `${accessToken}
297
+ `);
298
+ clearTerminalFingerprint();
299
+ _cachedCredential = { token: accessToken, source: "token_file" };
300
+ }
301
+ function isTerminalRefreshResponse(response, detail) {
302
+ if (detail.includes("invalid_grant") || detail.includes("Session has already ended")) return true;
303
+ try {
304
+ const error = JSON.parse(detail).error;
305
+ return error === "invalid_grant" || response.status === 401 && error === "Invalid or expired refresh token";
306
+ } catch {
307
+ return response.status === 401 && detail.includes("Invalid or expired refresh token");
98
308
  }
99
- const siteUrl = getSiteUrl();
100
- const response = await fetch(`${siteUrl}/mcp/broker/refresh`, {
101
- method: "POST",
102
- headers: { "Content-Type": "application/json" },
103
- body: JSON.stringify({ refresh_token: refreshTokenValue }),
104
- signal: options.signal
105
- });
106
- if (!response.ok) {
107
- if (options.quiet) return void 0;
108
- const detail = (await response.text().catch(() => "")).slice(0, 200);
109
- process.stderr.write(
110
- `[prim] token refresh rejected by broker: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}
309
+ }
310
+ function refreshDiagnostic(response, detail, quiet) {
311
+ if (quiet) return;
312
+ process.stderr.write(
313
+ `[prim] token refresh rejected by broker: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}
111
314
  `
112
- );
113
- return void 0;
114
- }
115
- const data = await response.json();
116
- if (!data.access_token) {
117
- return void 0;
118
- }
119
- writeFileSync(TOKEN_FILE_PATH, data.access_token, { mode: 384 });
120
- if (data.refresh_token) {
121
- writeFileSync(REFRESH_TOKEN_PATH, data.refresh_token, { mode: 384 });
122
- }
123
- saveTokenExpiry(data.access_token, data.expires_in);
124
- return data.access_token;
315
+ );
316
+ }
317
+ async function performTokenRefresh(options = {}) {
318
+ const selected = resolveAuthCredential();
319
+ if (selected?.source !== "token_file") return void 0;
320
+ const startingGeneration = readTrimmed(REFRESH_TOKEN_PATH);
321
+ if (!startingGeneration || isSessionEnded()) return void 0;
322
+ return withCredentialLock(
323
+ async () => {
324
+ const currentCredential = resolveAuthCredential();
325
+ if (currentCredential?.source !== "token_file") return void 0;
326
+ const currentGeneration = readTrimmed(REFRESH_TOKEN_PATH);
327
+ if (!currentGeneration) return void 0;
328
+ if (currentGeneration !== startingGeneration) {
329
+ return currentCredential.token;
330
+ }
331
+ if (isSessionEnded()) return void 0;
332
+ const response = await fetch(`${getSiteUrl()}/mcp/broker/refresh`, {
333
+ method: "POST",
334
+ headers: { "Content-Type": "application/json" },
335
+ body: JSON.stringify({ refresh_token: currentGeneration }),
336
+ signal: options.signal
337
+ });
338
+ if (!response.ok) {
339
+ const detail = (await response.text().catch(() => "")).slice(0, 200);
340
+ if (isTerminalRefreshResponse(response, detail) && readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
341
+ writeTerminalFingerprint(currentGeneration);
342
+ }
343
+ refreshDiagnostic(response, detail, options.quiet);
344
+ const winner = resolveAuthCredential();
345
+ return readTrimmed(REFRESH_TOKEN_PATH) !== currentGeneration && winner?.source === "token_file" ? winner.token : void 0;
346
+ }
347
+ let data;
348
+ try {
349
+ data = await response.json();
350
+ } catch {
351
+ if (readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
352
+ writeTerminalFingerprint(currentGeneration);
353
+ }
354
+ return void 0;
355
+ }
356
+ const record = typeof data === "object" && data !== null && !Array.isArray(data) ? data : void 0;
357
+ const accessToken = typeof record?.access_token === "string" ? record.access_token.trim() : "";
358
+ const replacementRefreshToken = typeof record?.refresh_token === "string" ? record.refresh_token.trim() : "";
359
+ if (!(accessToken && replacementRefreshToken) || replacementRefreshToken === currentGeneration) {
360
+ if (readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
361
+ writeTerminalFingerprint(currentGeneration);
362
+ }
363
+ return void 0;
364
+ }
365
+ commitCredentialsUnlocked({
366
+ accessToken,
367
+ refreshToken: replacementRefreshToken,
368
+ expiresIn: typeof record?.expires_in === "number" && Number.isFinite(record.expires_in) ? record.expires_in : void 0
369
+ });
370
+ return accessToken;
371
+ },
372
+ { signal: options.signal }
373
+ );
125
374
  }
126
375
  var _refreshInFlight;
127
376
  function refreshToken(options = {}) {
128
- if (_refreshInFlight) {
129
- return _refreshInFlight;
130
- }
377
+ if (_refreshInFlight) return _refreshInFlight;
131
378
  const attempt = performTokenRefresh(options).then((token) => {
132
- if (token) {
133
- _cachedToken = token;
134
- }
379
+ if (token) _cachedCredential = { token, source: "token_file" };
135
380
  return token;
136
381
  }).finally(() => {
137
- if (_refreshInFlight === attempt) {
138
- _refreshInFlight = void 0;
139
- }
382
+ if (_refreshInFlight === attempt) _refreshInFlight = void 0;
140
383
  });
141
384
  _refreshInFlight = attempt;
142
385
  return attempt;
@@ -149,29 +392,29 @@ var HttpError = class extends Error {
149
392
  this.status = status;
150
393
  }
151
394
  };
152
- var _cachedToken;
153
- async function request(method, path, body, options) {
154
- const siteUrl = getSiteUrl();
155
- const url = `${siteUrl}${path}`;
156
- if (!_cachedToken) {
157
- _cachedToken = getAuthToken();
395
+ var _cachedCredential;
396
+ function selectedCredential() {
397
+ const resolved = resolveAuthCredential();
398
+ if (!_cachedCredential || resolved?.token !== _cachedCredential.token || resolved?.source !== _cachedCredential.source) {
399
+ _cachedCredential = resolved;
158
400
  }
159
- if (_cachedToken && isTokenExpiringSoon()) {
160
- const newToken = await refreshToken({
401
+ return _cachedCredential;
402
+ }
403
+ async function request(method, path, body, options) {
404
+ const url = `${getSiteUrl()}${path}`;
405
+ let credential = selectedCredential();
406
+ let refreshAttempted = false;
407
+ if (credential && isTokenExpiringSoon(credential)) {
408
+ refreshAttempted = true;
409
+ const token = await refreshToken({
161
410
  signal: options?.signal,
162
411
  quiet: options?.quietRefresh
163
412
  });
164
- if (newToken) {
165
- _cachedToken = newToken;
166
- }
413
+ if (token) credential = { token, source: "token_file" };
167
414
  }
168
- const doFetch = async (token) => {
169
- const headers = {
170
- "Content-Type": "application/json"
171
- };
172
- if (token) {
173
- headers.Authorization = `Bearer ${token}`;
174
- }
415
+ const doFetch = (token) => {
416
+ const headers = { "Content-Type": "application/json" };
417
+ if (token) headers.Authorization = `Bearer ${token}`;
175
418
  return fetch(url, {
176
419
  method,
177
420
  headers,
@@ -179,26 +422,31 @@ async function request(method, path, body, options) {
179
422
  signal: options?.signal
180
423
  });
181
424
  };
182
- const tokenUsed = _cachedToken;
183
- let res = await doFetch(tokenUsed);
184
- if (res.status === 401) {
185
- const newToken = _cachedToken && _cachedToken !== tokenUsed ? _cachedToken : await refreshToken({
186
- signal: options?.signal,
187
- quiet: options?.quietRefresh
188
- });
189
- if (newToken) {
190
- _cachedToken = newToken;
191
- res = await doFetch(newToken);
425
+ const tokenUsed = credential?.token;
426
+ let response = await doFetch(tokenUsed);
427
+ if (response.status === 401) {
428
+ const latest = resolveAuthCredential();
429
+ let retryToken = latest?.token !== tokenUsed ? latest?.token : void 0;
430
+ if (!retryToken && latest?.source === "token_file" && !refreshAttempted) {
431
+ refreshAttempted = true;
432
+ retryToken = await refreshToken({ signal: options?.signal, quiet: options?.quietRefresh });
433
+ }
434
+ if (retryToken) {
435
+ _cachedCredential = {
436
+ token: retryToken,
437
+ source: latest?.source === "token_file" ? "token_file" : latest?.source ?? "token_file"
438
+ };
439
+ response = await doFetch(retryToken);
192
440
  }
193
441
  }
194
- if (!res.ok) {
195
- if (res.status === 401) {
442
+ if (!response.ok) {
443
+ if (response.status === 401) {
196
444
  throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
197
445
  }
198
- const errorBody = await res.json().catch(() => null);
199
- throw new HttpError(res.status, errorBody?.error ?? `HTTP ${res.status}`);
446
+ const errorBody = await response.json().catch(() => null);
447
+ throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
200
448
  }
201
- return res.json();
449
+ return response.json();
202
450
  }
203
451
  function getClient() {
204
452
  return {
@@ -208,7 +456,7 @@ function getClient() {
208
456
  }
209
457
 
210
458
  // src/flusher.ts
211
- import { createReadStream, renameSync, unlinkSync } from "fs";
459
+ import { createReadStream, renameSync as renameSync2, unlinkSync } from "fs";
212
460
  import { createInterface } from "readline";
213
461
 
214
462
  // src/ingest-response.ts
@@ -238,18 +486,18 @@ function requireDurableIngestAcknowledgement(value, expected) {
238
486
  import {
239
487
  appendFileSync,
240
488
  closeSync,
241
- existsSync as existsSync2,
489
+ existsSync as existsSync3,
242
490
  fstatSync,
243
- mkdirSync,
491
+ mkdirSync as mkdirSync3,
244
492
  openSync,
245
493
  opendirSync,
246
- readFileSync as readFileSync2,
494
+ readFileSync as readFileSync3,
247
495
  readSync,
248
496
  readdirSync
249
497
  } from "fs";
250
498
  import { homedir as homedir2 } from "os";
251
- import { dirname, join as join2 } from "path";
252
- var JOURNAL_DIR = join2(homedir2(), ".config", "prim", "moves");
499
+ import { dirname as dirname3, join as join3 } from "path";
500
+ var JOURNAL_DIR = join3(homedir2(), ".config", "prim", "moves");
253
501
  var JOURNAL_BASENAME = "journal.ndjson";
254
502
  var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
255
503
  var PENDING_STATS_MAX_FILES = 128;
@@ -261,7 +509,7 @@ function envSlug(apiUrl) {
261
509
  return slug;
262
510
  }
263
511
  function currentEnvDir() {
264
- return join2(JOURNAL_DIR, envSlug(getSiteUrl()));
512
+ return join3(JOURNAL_DIR, envSlug(getSiteUrl()));
265
513
  }
266
514
  function parseMoves(content) {
267
515
  const moves = [];
@@ -279,15 +527,15 @@ function parseMoves(content) {
279
527
  function listBuckets() {
280
528
  const out = [];
281
529
  const envDir = currentEnvDir();
282
- if (!existsSync2(envDir)) {
530
+ if (!existsSync3(envDir)) {
283
531
  return out;
284
532
  }
285
533
  for (const entry of readdirSync(envDir, { withFileTypes: true })) {
286
534
  if (!entry.isDirectory()) {
287
535
  continue;
288
536
  }
289
- const path = join2(envDir, entry.name, JOURNAL_BASENAME);
290
- if (existsSync2(path)) {
537
+ const path = join3(envDir, entry.name, JOURNAL_BASENAME);
538
+ if (existsSync3(path)) {
291
539
  out.push({ bucket: entry.name, path });
292
540
  }
293
541
  }
@@ -345,7 +593,7 @@ function parseFlushingPid(name) {
345
593
  return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
346
594
  }
347
595
  function listFlushingInDir(dir, bucket, options = {}) {
348
- if (!existsSync2(dir)) {
596
+ if (!existsSync3(dir)) {
349
597
  return [];
350
598
  }
351
599
  const out = [];
@@ -359,7 +607,7 @@ function listFlushingInDir(dir, bucket, options = {}) {
359
607
  entry = entries.readSync();
360
608
  continue;
361
609
  }
362
- const path = join2(dir, entry.name);
610
+ const path = join3(dir, entry.name);
363
611
  let sample;
364
612
  try {
365
613
  sample = sampleJournalFile(path, remainingBytes);
@@ -386,7 +634,7 @@ function listFlushingInDir(dir, bucket, options = {}) {
386
634
  }
387
635
  function listFlushing(options = {}) {
388
636
  const envDir = currentEnvDir();
389
- if (!existsSync2(envDir)) {
637
+ if (!existsSync3(envDir)) {
390
638
  return [];
391
639
  }
392
640
  const out = [];
@@ -397,7 +645,7 @@ function listFlushing(options = {}) {
397
645
  break;
398
646
  }
399
647
  if (entry.isDirectory()) {
400
- const found = listFlushingInDir(join2(envDir, entry.name), entry.name, {
648
+ const found = listFlushingInDir(join3(envDir, entry.name), entry.name, {
401
649
  sampleBytes: remainingBytes,
402
650
  maxFiles: maxFiles - out.length
403
651
  });
@@ -501,7 +749,7 @@ async function drainFlushingPath(flushingPath, client2 = getClient()) {
501
749
  async function drainPath(path) {
502
750
  const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
503
751
  try {
504
- renameSync(path, tmpPath);
752
+ renameSync2(path, tmpPath);
505
753
  } catch (err) {
506
754
  if (err.code === "ENOENT") {
507
755
  return 0;
@@ -510,7 +758,7 @@ async function drainPath(path) {
510
758
  }
511
759
  return drainFlushingPath(tmpPath);
512
760
  }
513
- function processIsAlive(pid) {
761
+ function processIsAlive2(pid) {
514
762
  try {
515
763
  process.kill(pid, 0);
516
764
  return true;
@@ -520,7 +768,7 @@ function processIsAlive(pid) {
520
768
  }
521
769
  function selectRecoverable(files, now, opts = {}) {
522
770
  const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
523
- const isAlive = opts.isAlive ?? processIsAlive;
771
+ const isAlive = opts.isAlive ?? processIsAlive2;
524
772
  return files.filter((f) => {
525
773
  if (f.pid === void 0) {
526
774
  return now - f.mtimeMs > quarantineMs;
@@ -592,14 +840,14 @@ function flush() {
592
840
  // src/daemon/client.ts
593
841
  import { createConnection } from "net";
594
842
  import { homedir as homedir3 } from "os";
595
- import { join as join3 } from "path";
596
- var SOCK_PATH = join3(homedir3(), ".config", "prim", "sock");
597
- var DEFAULT_TIMEOUT_MS = 250;
843
+ import { join as join4 } from "path";
844
+ var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
845
+ var DEFAULT_TIMEOUT_MS2 = 250;
598
846
  var nextRequestId = 1;
599
847
  function daemonRequest(method, params = {}, opts = {}) {
600
848
  const id = nextRequestId++;
601
849
  const request2 = { id, method, params };
602
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
850
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
603
851
  return new Promise((resolve2) => {
604
852
  let settled = false;
605
853
  let socket;
@@ -686,17 +934,17 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
686
934
  }
687
935
 
688
936
  // src/daemon/health.ts
689
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
937
+ import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
690
938
  import { homedir as homedir4 } from "os";
691
- import { dirname as dirname2, join as join4 } from "path";
692
- var CONFIG_DIR_MODE = 448;
939
+ import { dirname as dirname4, join as join5 } from "path";
940
+ var CONFIG_DIR_MODE2 = 448;
693
941
  var HEALTH_FILE_MODE = 384;
694
942
  var INGESTION_SLA_MS = 3e4;
695
943
  var HEARTBEAT_FRESH_MS = 9e4;
696
944
  var HEARTBEAT_RETRY_CAP_MS = 3e4;
697
945
  var INGESTION_RETRY_BASE_MS = 5e3;
698
946
  var INGESTION_RETRY_CAP_MS = 15e3;
699
- var DAEMON_HEALTH_PATH = join4(homedir4(), ".config", "prim", "daemon-health.json");
947
+ var DAEMON_HEALTH_PATH = join5(homedir4(), ".config", "prim", "daemon-health.json");
700
948
  function createDaemonHealthState(version, pid, startedAt2) {
701
949
  return {
702
950
  schemaVersion: 1,
@@ -719,7 +967,7 @@ function createDaemonHealthState(version, pid, startedAt2) {
719
967
  function refreshDaemonHealth(state, now) {
720
968
  state.heartbeat.healthy = state.heartbeat.consecutiveFailures === 0 && state.heartbeat.lastSuccessAt !== void 0 && now - state.heartbeat.lastSuccessAt < HEARTBEAT_FRESH_MS;
721
969
  state.ingestion.healthy = state.ingestion.consecutiveFailures === 0 && !state.ingestion.pendingSampled && (state.ingestion.pendingCount === 0 || state.ingestion.oldestPendingAt !== void 0 && now - state.ingestion.oldestPendingAt <= INGESTION_SLA_MS);
722
- state.healthy = state.heartbeat.healthy && state.ingestion.healthy;
970
+ state.healthy = state.heartbeat.healthy && state.ingestion.healthy && !state.needsReauth;
723
971
  }
724
972
  function retryDelayMs(consecutiveFailures, capMs, random) {
725
973
  const exponent = Math.max(0, consecutiveFailures - 1);
@@ -734,37 +982,37 @@ function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
734
982
  return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
735
983
  }
736
984
  function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
737
- const dir = dirname2(path);
738
- if (!existsSync3(dir)) {
739
- mkdirSync2(dir, { recursive: true, mode: CONFIG_DIR_MODE });
985
+ const dir = dirname4(path);
986
+ if (!existsSync4(dir)) {
987
+ mkdirSync4(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
740
988
  }
741
- const tmp = join4(dir, `.${process.pid}.daemon-health.tmp`);
742
- writeFileSync2(tmp, `${JSON.stringify(state, null, 2)}
989
+ const tmp = join5(dir, `.${process.pid}.daemon-health.tmp`);
990
+ writeFileSync3(tmp, `${JSON.stringify(state, null, 2)}
743
991
  `, { mode: HEALTH_FILE_MODE });
744
- chmodSync(tmp, HEALTH_FILE_MODE);
745
- renameSync2(tmp, path);
992
+ chmodSync2(tmp, HEALTH_FILE_MODE);
993
+ renameSync3(tmp, path);
746
994
  }
747
995
 
748
996
  // src/daemon/instance-lock.ts
749
997
  import { randomUUID } from "crypto";
750
998
  import {
751
- chmodSync as chmodSync2,
752
- existsSync as existsSync4,
753
- mkdirSync as mkdirSync3,
754
- readFileSync as readFileSync3,
755
- renameSync as renameSync3,
756
- rmSync,
757
- statSync,
999
+ chmodSync as chmodSync3,
1000
+ existsSync as existsSync5,
1001
+ mkdirSync as mkdirSync5,
1002
+ readFileSync as readFileSync4,
1003
+ renameSync as renameSync4,
1004
+ rmSync as rmSync3,
1005
+ statSync as statSync2,
758
1006
  unlinkSync as unlinkSync2,
759
- writeFileSync as writeFileSync3
1007
+ writeFileSync as writeFileSync4
760
1008
  } from "fs";
761
- import { join as join5 } from "path";
1009
+ import { join as join6 } from "path";
762
1010
  var DIR_MODE = 448;
763
- var FILE_MODE = 384;
1011
+ var FILE_MODE2 = 384;
764
1012
  var DAEMON_STARTUP_GRACE_MS = 5e3;
765
1013
  function readLockRecord(path) {
766
1014
  try {
767
- const value = JSON.parse(readFileSync3(path, "utf-8"));
1015
+ const value = JSON.parse(readFileSync4(path, "utf-8"));
768
1016
  if (Number.isInteger(value.pid) && typeof value.instanceId === "string" && typeof value.startedAt === "number") {
769
1017
  return value;
770
1018
  }
@@ -774,7 +1022,7 @@ function readLockRecord(path) {
774
1022
  }
775
1023
  function numericPid(path) {
776
1024
  try {
777
- const pid = Number(readFileSync3(path, "utf-8").trim());
1025
+ const pid = Number(readFileSync4(path, "utf-8").trim());
778
1026
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
779
1027
  } catch {
780
1028
  return;
@@ -789,16 +1037,16 @@ function daemonProcessIsAlive(pid) {
789
1037
  }
790
1038
  }
791
1039
  function readDaemonOwner(configDir) {
792
- return readLockRecord(join5(configDir, "daemon.lock", "owner.json"));
1040
+ return readLockRecord(join6(configDir, "daemon.lock", "owner.json"));
793
1041
  }
794
1042
  function readDaemonPid(configDir) {
795
- const path = join5(configDir, "daemon.pid");
1043
+ const path = join6(configDir, "daemon.pid");
796
1044
  const pid = numericPid(path);
797
1045
  if (pid === void 0) {
798
1046
  return;
799
1047
  }
800
1048
  try {
801
- return { pid, mtimeMs: statSync(path).mtimeMs };
1049
+ return { pid, mtimeMs: statSync2(path).mtimeMs };
802
1050
  } catch {
803
1051
  return;
804
1052
  }
@@ -807,29 +1055,29 @@ function acquireDaemonOwnership(configDir, opts = {}) {
807
1055
  const pid = opts.pid ?? process.pid;
808
1056
  const now = opts.now ?? Date.now();
809
1057
  const isAlive = opts.isAlive ?? daemonProcessIsAlive;
810
- const lockDir = join5(configDir, "daemon.lock");
811
- const ownerPath = join5(lockDir, "owner.json");
812
- const pidPath = join5(configDir, "daemon.pid");
1058
+ const lockDir = join6(configDir, "daemon.lock");
1059
+ const ownerPath2 = join6(lockDir, "owner.json");
1060
+ const pidPath = join6(configDir, "daemon.pid");
813
1061
  const instanceId = randomUUID();
814
- if (!existsSync4(configDir)) {
815
- mkdirSync3(configDir, { recursive: true, mode: DIR_MODE });
1062
+ if (!existsSync5(configDir)) {
1063
+ mkdirSync5(configDir, { recursive: true, mode: DIR_MODE });
816
1064
  }
817
1065
  for (; ; ) {
818
1066
  try {
819
- mkdirSync3(lockDir, { mode: DIR_MODE });
1067
+ mkdirSync5(lockDir, { mode: DIR_MODE });
820
1068
  break;
821
1069
  } catch (err) {
822
1070
  if (err.code !== "EEXIST") {
823
1071
  throw err;
824
1072
  }
825
- const existing = readLockRecord(ownerPath);
1073
+ const existing = readLockRecord(ownerPath2);
826
1074
  if (existing && isAlive(existing.pid)) {
827
1075
  throw new Error(`daemon already running (pid=${String(existing.pid)})`);
828
1076
  }
829
1077
  if (!existing) {
830
1078
  let mtimeMs;
831
1079
  try {
832
- mtimeMs = statSync(lockDir).mtimeMs;
1080
+ mtimeMs = statSync2(lockDir).mtimeMs;
833
1081
  } catch (statErr) {
834
1082
  if (statErr.code === "ENOENT") {
835
1083
  continue;
@@ -842,8 +1090,8 @@ function acquireDaemonOwnership(configDir, opts = {}) {
842
1090
  }
843
1091
  const stale = `${lockDir}.stale.${String(pid)}.${instanceId}`;
844
1092
  try {
845
- renameSync3(lockDir, stale);
846
- rmSync(stale, { recursive: true, force: true });
1093
+ renameSync4(lockDir, stale);
1094
+ rmSync3(stale, { recursive: true, force: true });
847
1095
  } catch (renameErr) {
848
1096
  if (renameErr.code !== "ENOENT") {
849
1097
  throw renameErr;
@@ -854,22 +1102,22 @@ function acquireDaemonOwnership(configDir, opts = {}) {
854
1102
  const ownership2 = {
855
1103
  configDir,
856
1104
  lockDir,
857
- ownerPath,
1105
+ ownerPath: ownerPath2,
858
1106
  pidPath,
859
1107
  pid,
860
1108
  instanceId
861
1109
  };
862
1110
  const record = { pid, instanceId, startedAt: now };
863
- writeFileSync3(ownerPath, `${JSON.stringify(record)}
864
- `, { mode: FILE_MODE });
865
- chmodSync2(ownerPath, FILE_MODE);
1111
+ writeFileSync4(ownerPath2, `${JSON.stringify(record)}
1112
+ `, { mode: FILE_MODE2 });
1113
+ chmodSync3(ownerPath2, FILE_MODE2);
866
1114
  const legacyPid = numericPid(pidPath);
867
1115
  if (legacyPid !== void 0 && legacyPid !== pid && isAlive(legacyPid)) {
868
- rmSync(lockDir, { recursive: true, force: true });
1116
+ rmSync3(lockDir, { recursive: true, force: true });
869
1117
  throw new Error(`daemon already running (pid=${String(legacyPid)})`);
870
1118
  }
871
- writeFileSync3(pidPath, String(pid), { mode: FILE_MODE });
872
- chmodSync2(pidPath, FILE_MODE);
1119
+ writeFileSync4(pidPath, String(pid), { mode: FILE_MODE2 });
1120
+ chmodSync3(pidPath, FILE_MODE2);
873
1121
  return ownership2;
874
1122
  }
875
1123
  function ownsDaemonInstance(ownership2) {
@@ -899,7 +1147,7 @@ function releaseDaemonOwnership(ownership2, socketPath) {
899
1147
  }
900
1148
  }
901
1149
  }
902
- rmSync(ownership2.lockDir, { recursive: true, force: true });
1150
+ rmSync3(ownership2.lockDir, { recursive: true, force: true });
903
1151
  if (cleanupError) {
904
1152
  throw cleanupError;
905
1153
  }
@@ -907,8 +1155,8 @@ function releaseDaemonOwnership(ownership2, socketPath) {
907
1155
  }
908
1156
 
909
1157
  // src/daemon/server.ts
910
- var CONFIG_DIR = join6(homedir5(), ".config", "prim");
911
- var SOCK_PATH2 = join6(CONFIG_DIR, "sock");
1158
+ var CONFIG_DIR = join7(homedir5(), ".config", "prim");
1159
+ var SOCK_PATH2 = join7(CONFIG_DIR, "sock");
912
1160
  var HEARTBEAT_INTERVAL_MS = 3e4;
913
1161
  var INGESTION_POLL_INTERVAL_MS = 15e3;
914
1162
  var TOKEN_CHECK_INTERVAL_MS = 6e4;
@@ -935,13 +1183,14 @@ var heartbeatRerunRequested = false;
935
1183
  var ownership;
936
1184
  var socketServer;
937
1185
  var shuttingDown = false;
1186
+ var reauthHold = false;
938
1187
  function resolveRuntimeVersion() {
939
1188
  if (process.env.PRIM_RUNTIME_VERSION) {
940
1189
  return process.env.PRIM_RUNTIME_VERSION;
941
1190
  }
942
1191
  try {
943
1192
  const here = fileURLToPath(new URL(".", import.meta.url));
944
- const pkg = JSON.parse(readFileSync4(join6(here, "..", "..", "package.json"), "utf-8"));
1193
+ const pkg = JSON.parse(readFileSync5(join7(here, "..", "..", "package.json"), "utf-8"));
945
1194
  return pkg.version ?? "unknown";
946
1195
  } catch {
947
1196
  return "unknown";
@@ -967,6 +1216,42 @@ function updatePendingHealth() {
967
1216
  daemonHealth.ingestion.oldestPendingAt = pending.oldestPendingAt;
968
1217
  daemonHealth.ingestion.strandedCount = pending.strandedCount;
969
1218
  }
1219
+ function enterReauthHold() {
1220
+ if (reauthHold) {
1221
+ return;
1222
+ }
1223
+ reauthHold = true;
1224
+ daemonHealth.needsReauth = true;
1225
+ if (heartbeatTimer) {
1226
+ clearTimeout(heartbeatTimer);
1227
+ heartbeatTimer = void 0;
1228
+ }
1229
+ if (ingestionTimer) {
1230
+ clearTimeout(ingestionTimer);
1231
+ ingestionTimer = void 0;
1232
+ }
1233
+ persistHealth();
1234
+ process.stderr.write(
1235
+ "[prim-daemon] authentication ended \u2014 halting heartbeat + ingestion until `prim auth login` (captures continue to the journal)\n"
1236
+ );
1237
+ }
1238
+ function exitReauthHold() {
1239
+ if (!reauthHold) {
1240
+ return;
1241
+ }
1242
+ reauthHold = false;
1243
+ daemonHealth.needsReauth = false;
1244
+ daemonHealth.heartbeat.consecutiveFailures = 0;
1245
+ daemonHealth.ingestion.consecutiveFailures = 0;
1246
+ daemonHealth.heartbeat.lastError = void 0;
1247
+ daemonHealth.ingestion.lastError = void 0;
1248
+ persistHealth();
1249
+ process.stderr.write(
1250
+ "[prim-daemon] re-authentication detected \u2014 resuming heartbeat + ingestion\n"
1251
+ );
1252
+ void sendHeartbeat();
1253
+ void runIngestionLoop();
1254
+ }
970
1255
  async function takeOwnership() {
971
1256
  const now = Date.now();
972
1257
  const recordedOwner = readDaemonOwner(CONFIG_DIR);
@@ -976,7 +1261,7 @@ async function takeOwnership() {
976
1261
  (pid) => pid !== void 0 && daemonProcessIsAlive(pid)
977
1262
  )
978
1263
  );
979
- if (existsSync5(SOCK_PATH2) || candidatePids.size > 0) {
1264
+ if (existsSync6(SOCK_PATH2) || candidatePids.size > 0) {
980
1265
  const snapshot = await daemonRequest(
981
1266
  "status_snapshot",
982
1267
  {},
@@ -1051,11 +1336,18 @@ async function performHeartbeat() {
1051
1336
  daemonHealth.heartbeat.lastError = errorMessage(err);
1052
1337
  daemonHealth.heartbeat.consecutiveFailures += 1;
1053
1338
  persistHealth();
1339
+ if (isSessionEnded()) {
1340
+ enterReauthHold();
1341
+ return;
1342
+ }
1054
1343
  process.stderr.write(`[prim-daemon] heartbeat error: ${errorMessage(err)}
1055
1344
  `);
1056
1345
  }
1057
1346
  }
1058
1347
  function sendHeartbeat() {
1348
+ if (shuttingDown || reauthHold) {
1349
+ return Promise.resolve();
1350
+ }
1059
1351
  if (heartbeatInFlight) {
1060
1352
  heartbeatRerunRequested = true;
1061
1353
  return heartbeatInFlight;
@@ -1068,7 +1360,7 @@ function sendHeartbeat() {
1068
1360
  do {
1069
1361
  heartbeatRerunRequested = false;
1070
1362
  await performHeartbeat();
1071
- } while (heartbeatRerunRequested && !shuttingDown);
1363
+ } while (heartbeatRerunRequested && !(shuttingDown || reauthHold));
1072
1364
  })().finally(() => {
1073
1365
  heartbeatInFlight = void 0;
1074
1366
  scheduleHeartbeat();
@@ -1076,7 +1368,7 @@ function sendHeartbeat() {
1076
1368
  return heartbeatInFlight;
1077
1369
  }
1078
1370
  function scheduleHeartbeat() {
1079
- if (shuttingDown) {
1371
+ if (shuttingDown || reauthHold) {
1080
1372
  return;
1081
1373
  }
1082
1374
  const failures = daemonHealth.heartbeat.consecutiveFailures;
@@ -1089,6 +1381,9 @@ function scheduleHeartbeat() {
1089
1381
  }, delay);
1090
1382
  }
1091
1383
  async function ensureTokenFresh() {
1384
+ if (resolveAuthCredential()?.source !== "token_file") {
1385
+ return;
1386
+ }
1092
1387
  const expiresAt = getTokenExpiresAt();
1093
1388
  if (!expiresAt) {
1094
1389
  return;
@@ -1101,7 +1396,7 @@ async function ensureTokenFresh() {
1101
1396
  }
1102
1397
  }
1103
1398
  function scheduleIngestion(delayMs) {
1104
- if (shuttingDown) {
1399
+ if (shuttingDown || reauthHold) {
1105
1400
  return;
1106
1401
  }
1107
1402
  ingestionTimer = setTimeout(() => {
@@ -1154,6 +1449,10 @@ async function runIngestionLoop() {
1154
1449
  } catch {
1155
1450
  }
1156
1451
  persistHealth();
1452
+ if (isSessionEnded()) {
1453
+ enterReauthHold();
1454
+ return;
1455
+ }
1157
1456
  process.stderr.write(`[prim-daemon] ingestion error: ${errorMessage(err)}
1158
1457
  `);
1159
1458
  scheduleIngestion(delay);
@@ -1198,6 +1497,7 @@ function handleStatusSnapshot(params) {
1198
1497
  lastHeartbeatAt,
1199
1498
  version: runtimeVersion,
1200
1499
  healthy: daemonHealth.healthy,
1500
+ needsReauth: daemonHealth.needsReauth === true,
1201
1501
  heartbeat: { ...daemonHealth.heartbeat },
1202
1502
  ingestion: { ...daemonHealth.ingestion }
1203
1503
  };
@@ -1332,13 +1632,26 @@ function startSocketServer() {
1332
1632
  });
1333
1633
  }
1334
1634
  function startTimers() {
1335
- persistHealth();
1336
- void sendHeartbeat();
1337
- void runIngestionLoop();
1635
+ if (isSessionEnded()) {
1636
+ enterReauthHold();
1637
+ } else {
1638
+ persistHealth();
1639
+ void sendHeartbeat();
1640
+ void runIngestionLoop();
1641
+ }
1338
1642
  void runTokenCheckLoop();
1339
1643
  }
1340
1644
  async function runTokenCheckLoop() {
1341
- await ensureTokenFresh();
1645
+ if (reauthHold) {
1646
+ if (!isSessionEnded() && resolveAuthCredential()) {
1647
+ exitReauthHold();
1648
+ }
1649
+ } else {
1650
+ await ensureTokenFresh();
1651
+ if (isSessionEnded()) {
1652
+ enterReauthHold();
1653
+ }
1654
+ }
1342
1655
  if (!shuttingDown) {
1343
1656
  tokenCheckTimer = setTimeout(() => {
1344
1657
  tokenCheckTimer = void 0;