cleargate 0.11.4 → 0.11.5

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,6 +1,6 @@
1
1
  {
2
- "cleargate_version": "0.11.4",
3
- "generated_at": "2026-05-05T17:48:13.929Z",
2
+ "cleargate_version": "0.11.5",
3
+ "generated_at": "2026-05-11T22:02:23.454Z",
4
4
  "files": [
5
5
  {
6
6
  "path": ".claude/agents/architect.md",
@@ -86,6 +86,40 @@ function requireMcpUrl(cfg) {
86
86
  }
87
87
  return cfg.mcpUrl;
88
88
  }
89
+ function saveConfig(updates, opts = {}) {
90
+ const home = os.homedir();
91
+ if (!home) {
92
+ throw new Error("Cannot determine home directory.");
93
+ }
94
+ const configPath = opts.configPath ?? path.join(home, ".cleargate", "config.json");
95
+ const dir = path.dirname(configPath);
96
+ let existing = {};
97
+ try {
98
+ const raw = fs.readFileSync(configPath, "utf8");
99
+ const parsed = JSON.parse(raw);
100
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
101
+ existing = parsed;
102
+ }
103
+ } catch (err) {
104
+ if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
105
+ }
106
+ }
107
+ const merged = { ...existing };
108
+ for (const [k, v] of Object.entries(updates)) {
109
+ if (v !== void 0) merged[k] = v;
110
+ }
111
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
112
+ try {
113
+ fs.chmodSync(dir, 448);
114
+ } catch {
115
+ }
116
+ const tmpPath = path.join(dir, ".config.json.tmp");
117
+ const json = JSON.stringify(merged, null, 2) + "\n";
118
+ fs.writeFileSync(tmpPath, json, { mode: 384 });
119
+ fs.chmodSync(tmpPath, 384);
120
+ fs.renameSync(tmpPath, configPath);
121
+ fs.chmodSync(configPath, 384);
122
+ }
89
123
 
90
124
  // src/lib/membership.ts
91
125
  import * as fs2 from "fs";
@@ -152,7 +186,45 @@ function getMembershipState(opts) {
152
186
  }
153
187
 
154
188
  // src/auth/acquire.ts
189
+ import * as fs3 from "fs";
190
+ import * as os3 from "os";
191
+ import * as path3 from "path";
155
192
  var CACHE = /* @__PURE__ */ new Map();
193
+ function defaultDiskCachePath(env = process.env) {
194
+ const override = env["CLEARGATE_DISK_CACHE_PATH"];
195
+ if (override === "off") return null;
196
+ if (typeof override === "string" && override.length > 0) return override;
197
+ const home = os3.homedir();
198
+ if (!home) return null;
199
+ return path3.join(home, ".cleargate", "access-token.json");
200
+ }
201
+ function readDiskCache(filePath) {
202
+ try {
203
+ const raw = fs3.readFileSync(filePath, "utf8");
204
+ const parsed = JSON.parse(raw);
205
+ if (parsed !== null && typeof parsed === "object" && parsed.version === 1 && typeof parsed.entries === "object" && parsed.entries !== null) {
206
+ return parsed;
207
+ }
208
+ } catch {
209
+ }
210
+ return { version: 1, entries: {} };
211
+ }
212
+ function writeDiskCache(filePath, data) {
213
+ const dir = path3.dirname(filePath);
214
+ try {
215
+ fs3.mkdirSync(dir, { recursive: true, mode: 448 });
216
+ try {
217
+ fs3.chmodSync(dir, 448);
218
+ } catch {
219
+ }
220
+ const tmpPath = path3.join(dir, ".access-token.json.tmp");
221
+ fs3.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
222
+ fs3.chmodSync(tmpPath, 384);
223
+ fs3.renameSync(tmpPath, filePath);
224
+ fs3.chmodSync(filePath, 384);
225
+ } catch {
226
+ }
227
+ }
156
228
  function decodeJwtPayload2(token) {
157
229
  try {
158
230
  const parts = token.split(".");
@@ -186,6 +258,15 @@ async function acquireAccessToken(opts) {
186
258
  return cached.accessToken;
187
259
  }
188
260
  }
261
+ const diskCachePath = opts.diskCachePath === void 0 ? defaultDiskCachePath() : opts.diskCachePath;
262
+ if (!opts.forceRefresh && diskCachePath) {
263
+ const file = readDiskCache(diskCachePath);
264
+ const entry = file.entries[cacheKey];
265
+ if (entry && nowFn() < entry.expiresAtMs) {
266
+ CACHE.set(cacheKey, entry);
267
+ return entry.accessToken;
268
+ }
269
+ }
189
270
  const store = await (opts.createStore ?? createTokenStore)();
190
271
  const stored = await store.load(opts.profile);
191
272
  if (!stored) {
@@ -234,7 +315,13 @@ async function acquireAccessToken(opts) {
234
315
  const exp = payload?.exp;
235
316
  if (typeof exp === "number" && Number.isFinite(exp)) {
236
317
  const expiresAtMs = (exp - 60) * 1e3;
237
- CACHE.set(cacheKey, { accessToken, expiresAtMs });
318
+ const entry = { accessToken, expiresAtMs };
319
+ CACHE.set(cacheKey, entry);
320
+ if (diskCachePath) {
321
+ const file = readDiskCache(diskCachePath);
322
+ file.entries[cacheKey] = entry;
323
+ writeDiskCache(diskCachePath, file);
324
+ }
238
325
  }
239
326
  return accessToken;
240
327
  }
@@ -242,9 +329,10 @@ async function acquireAccessToken(opts) {
242
329
  export {
243
330
  loadConfig,
244
331
  requireMcpUrl,
332
+ saveConfig,
245
333
  decodeJwtPayload,
246
334
  getMembershipState,
247
335
  AcquireError,
248
336
  acquireAccessToken
249
337
  };
250
- //# sourceMappingURL=chunk-Q3BTSXCK.js.map
338
+ //# sourceMappingURL=chunk-WFNLCTY5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/lib/membership.ts","../src/auth/acquire.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\nexport const ConfigSchema = z\n .object({\n mcpUrl: z.string().url().optional(),\n profile: z.string().min(1).default('default'),\n logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),\n })\n .strict();\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/** Partial raw config used for each layer before merge */\ntype RawConfig = Partial<{\n mcpUrl: string | undefined;\n profile: string | undefined;\n logLevel: string | undefined;\n}>;\n\nexport interface LoadConfigOptions {\n flags?: RawConfig;\n env?: NodeJS.ProcessEnv;\n configPath?: string;\n}\n\n/**\n * Synchronously loads and merges config from all layers:\n * flags > env > config file > zod defaults\n */\nexport function loadConfig(opts: LoadConfigOptions = {}): Config {\n const {\n flags = {},\n env = process.env,\n configPath,\n } = opts;\n\n // Resolve config file path\n const resolvedConfigPath =\n configPath ??\n (() => {\n const home = os.homedir();\n if (!home) return null;\n return path.join(home, '.cleargate', 'config.json');\n })();\n\n // Layer: file\n let fileLayer: RawConfig = {};\n if (resolvedConfigPath) {\n try {\n const raw = fs.readFileSync(resolvedConfigPath, 'utf8');\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\n `Failed to parse config file at ${resolvedConfigPath}: invalid JSON`,\n );\n }\n // Validate file contents strictly (unknown keys will throw here)\n const fileResult = ConfigSchema.safeParse(parsed);\n if (!fileResult.success) {\n throw new Error(\n `Invalid config file at ${resolvedConfigPath}: ${fileResult.error.message}`,\n );\n }\n fileLayer = fileResult.data;\n } catch (err) {\n // Re-throw parse/validation errors; silently skip only ENOENT\n if (\n err instanceof Error &&\n 'code' in err &&\n (err as NodeJS.ErrnoException).code === 'ENOENT'\n ) {\n // file doesn't exist — skip silently\n } else {\n throw err;\n }\n }\n }\n\n // Layer: env\n const envLayer: RawConfig = {};\n if (env['CLEARGATE_MCP_URL']) {\n envLayer.mcpUrl = env['CLEARGATE_MCP_URL'];\n }\n if (env['CLEARGATE_PROFILE']) {\n envLayer.profile = env['CLEARGATE_PROFILE'];\n }\n if (env['CLEARGATE_LOG_LEVEL']) {\n envLayer.logLevel = env['CLEARGATE_LOG_LEVEL'];\n }\n\n // Merge: flags > env > file (start from {} so zod defaults fill in missing fields)\n const merged: Record<string, unknown> = {\n ...fileLayer,\n ...envLayer,\n ...(flags.mcpUrl !== undefined ? { mcpUrl: flags.mcpUrl } : {}),\n ...(flags.profile !== undefined ? { profile: flags.profile } : {}),\n ...(flags.logLevel !== undefined ? { logLevel: flags.logLevel } : {}),\n };\n\n // Remove undefined values so zod defaults apply properly\n for (const key of Object.keys(merged)) {\n if (merged[key] === undefined) {\n delete merged[key];\n }\n }\n\n const result = ConfigSchema.safeParse(merged);\n if (!result.success) {\n throw new Error(`Config validation failed: ${result.error.message}`);\n }\n\n return result.data;\n}\n\n/**\n * Asserts mcpUrl is present, throws a user-friendly error if not.\n */\nexport function requireMcpUrl(cfg: Config): string {\n if (cfg.mcpUrl === undefined) {\n throw new Error(\n 'mcpUrl not configured. Run `cleargate join <invite-url>` first.',\n );\n }\n return cfg.mcpUrl;\n}\n\nexport interface SaveConfigOptions {\n configPath?: string;\n}\n\n/**\n * Persist a partial update into ~/.cleargate/config.json.\n *\n * Reads the existing raw JSON (if present), shallow-merges `updates` on top, and\n * writes atomically with mode 0600. Unknown keys already in the file (e.g.\n * project_id, written by other surfaces) are preserved — strict Zod validation\n * is intentionally skipped here because admin-url.ts and other readers store\n * fields outside the strict schema.\n */\nexport function saveConfig(\n updates: Partial<{ mcpUrl: string; profile: string; logLevel: string }>,\n opts: SaveConfigOptions = {},\n): void {\n const home = os.homedir();\n if (!home) {\n throw new Error('Cannot determine home directory.');\n }\n const configPath =\n opts.configPath ?? path.join(home, '.cleargate', 'config.json');\n const dir = path.dirname(configPath);\n\n let existing: Record<string, unknown> = {};\n try {\n const raw = fs.readFileSync(configPath, 'utf8');\n const parsed = JSON.parse(raw);\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\n existing = parsed as Record<string, unknown>;\n }\n } catch (err) {\n if (\n !(\n err instanceof Error &&\n 'code' in err &&\n (err as NodeJS.ErrnoException).code === 'ENOENT'\n )\n ) {\n // Treat parse errors as recoverable — overwrite rather than fail join.\n }\n }\n\n const merged: Record<string, unknown> = { ...existing };\n for (const [k, v] of Object.entries(updates)) {\n if (v !== undefined) merged[k] = v;\n }\n\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n try {\n fs.chmodSync(dir, 0o700);\n } catch {\n // existing dir with custom mode — leave alone\n }\n\n const tmpPath = path.join(dir, '.config.json.tmp');\n const json = JSON.stringify(merged, null, 2) + '\\n';\n fs.writeFileSync(tmpPath, json, { mode: 0o600 });\n fs.chmodSync(tmpPath, 0o600);\n fs.renameSync(tmpPath, configPath);\n fs.chmodSync(configPath, 0o600);\n}\n","/**\n * membership.ts — CR-011\n *\n * Single source of truth for ClearGate membership state detection.\n * Cheap-path: reads ~/.cleargate/auth.json, decodes the stored refresh JWT\n * (introspection only — no signature verification), checks expiry.\n *\n * Returns 'pre-member' on: file missing | malformed JSON | malformed JWT | exp <= now.\n * Returns 'member' with decoded claims otherwise.\n *\n * No network call. Used by whoami --json, preAction gating hook, doctor --session-start banner.\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { Buffer } from 'node:buffer';\n\n// ─── Public types ─────────────────────────────────────────────────────────────\n\nexport type MembershipState =\n | { state: 'member'; email: string; project_id: string; expires_at: string }\n | { state: 'pre-member' };\n\nexport interface GetMembershipStateOpts {\n /** Profile name (default: 'default'). */\n profile?: string;\n /** Test seam: override the ~/.cleargate home directory. */\n cleargateHome?: string;\n /** Test seam: clock for expiry comparison (ms epoch). */\n now?: () => number;\n}\n\n// ─── JWT decode helper (extracted from whoami.ts) ─────────────────────────────\n\n/**\n * Decode a JWT payload without verifying the signature (introspection only).\n * Returns null if the token is malformed.\n */\nexport function decodeJwtPayload(jwt: string): Record<string, unknown> | null {\n const parts = jwt.split('.');\n if (parts.length !== 3) return null;\n try {\n const json = Buffer.from(parts[1]!, 'base64url').toString('utf8');\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n// ─── Auth file schema (mirrors src/auth/file-store.ts without Zod) ────────────\n\ninterface AuthFile {\n version: number;\n profiles: Record<string, { refreshToken: string }>;\n}\n\nfunction readAuthFile(authFilePath: string): AuthFile | null {\n let raw: string;\n try {\n raw = fs.readFileSync(authFilePath, 'utf8');\n } catch {\n // ENOENT or permission error → pre-member\n return null;\n }\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n (parsed as Record<string, unknown>)['version'] !== 1 ||\n typeof (parsed as Record<string, unknown>)['profiles'] !== 'object'\n ) {\n return null;\n }\n return parsed as AuthFile;\n } catch {\n return null;\n }\n}\n\n// ─── Main export ─────────────────────────────────────────────────────────────\n\n/**\n * Detect membership state locally.\n *\n * Algorithm:\n * 1. Resolve auth file path: <cleargateHome>/auth.json (default: ~/.cleargate/auth.json)\n * 2. Read & parse the file as an AuthFile { version, profiles }.\n * 3. Look up the profile's refreshToken.\n * 4. Decode the refreshToken as a JWT (base64url, no sig verify).\n * 5. Extract exp, sub (= email proxy), project_id.\n * 6. If exp <= now (ms) → pre-member.\n * 7. Otherwise → member.\n */\nexport function getMembershipState(opts?: GetMembershipStateOpts): MembershipState {\n const profile = opts?.profile ?? 'default';\n const nowMs = (opts?.now ?? Date.now)();\n\n // Resolve the auth file path\n const home = opts?.cleargateHome ?? path.join(os.homedir(), '.cleargate');\n const authFilePath = path.join(home, 'auth.json');\n\n // Read the auth file\n const authFile = readAuthFile(authFilePath);\n if (authFile === null) {\n return { state: 'pre-member' };\n }\n\n // Look up the profile\n const profileEntry = authFile.profiles[profile];\n if (!profileEntry || typeof profileEntry.refreshToken !== 'string') {\n return { state: 'pre-member' };\n }\n\n // Decode the stored refresh token as a JWT\n const claims = decodeJwtPayload(profileEntry.refreshToken);\n if (claims === null) {\n return { state: 'pre-member' };\n }\n\n // Check expiry (exp is in seconds per JWT spec)\n const exp = claims['exp'];\n if (typeof exp !== 'number' || !Number.isFinite(exp)) {\n return { state: 'pre-member' };\n }\n const expMs = exp * 1000;\n if (expMs <= nowMs) {\n return { state: 'pre-member' };\n }\n\n // Extract claims for member state\n // sub is the member UUID; we use it as the email proxy since the JWT\n // doesn't carry a separate email field (flashcard: sub = member UUID, not email).\n const sub = claims['sub'];\n const email = typeof sub === 'string' ? sub : '';\n const projectId = typeof claims['project_id'] === 'string' ? claims['project_id'] : '';\n const expiresAt = new Date(expMs).toISOString();\n\n return { state: 'member', email, project_id: projectId, expires_at: expiresAt };\n}\n","/**\n * acquireAccessToken — resolve a short-lived MCP access-token JWT.\n *\n * Resolution order (first success wins):\n * 1. CLEARGATE_MCP_TOKEN env var — CI / dev short-circuit (assumed JWT, not verified locally).\n * 2. In-memory single-flight cache (keyed by `${profile}::${mcpUrl}`) — returns cached token\n * if still valid (expires 60s before access token's `exp` claim).\n * 3. Stored refresh token (keychain/file) + POST /auth/refresh → rotates refresh token, returns access token.\n *\n * Errors surface to caller with a clear message so command handlers can exit cleanly.\n *\n * Lives here (not in mcp-client.ts) because the refresh flow needs TokenStore + mcpUrl and\n * mcp-client.ts is kept thin (just: host, bearer, JSON-RPC).\n */\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { createTokenStore } from './factory.js';\nimport type { TokenStore } from './token-store.js';\n\n// ── In-memory + on-disk single-flight cache ──────────────────────────────────\n// In-memory: process-local; naturally cleared when the Node CLI exits.\n// On-disk: ~/.cleargate/access-token.json (mode 0600), survives across CLI\n// invocations. Critical because each `cleargate` call is a fresh\n// process — without a disk cache every call hits keychain to load\n// the refresh token, then rotates it via /auth/refresh, which\n// re-saves to keychain and resets the macOS ACL → re-prompt loop.\n// Key: `${profile}::${mcpUrl}` — two profiles in same process never collide.\n// Env-token path (CLEARGATE_MCP_TOKEN) bypasses both caches entirely.\n\nconst CACHE = new Map<string, { accessToken: string; expiresAtMs: number }>();\n\ninterface DiskCacheEntry {\n accessToken: string;\n expiresAtMs: number;\n}\ninterface DiskCacheFile {\n version: 1;\n entries: Record<string, DiskCacheEntry>;\n}\n\nfunction defaultDiskCachePath(env: NodeJS.ProcessEnv = process.env): string | null {\n // Test override: setting CLEARGATE_DISK_CACHE_PATH=off disables the disk\n // cache entirely; setting it to a path uses that file instead of the home dir.\n const override = env['CLEARGATE_DISK_CACHE_PATH'];\n if (override === 'off') return null;\n if (typeof override === 'string' && override.length > 0) return override;\n const home = os.homedir();\n if (!home) return null;\n return path.join(home, '.cleargate', 'access-token.json');\n}\n\nfunction readDiskCache(filePath: string): DiskCacheFile {\n try {\n const raw = fs.readFileSync(filePath, 'utf8');\n const parsed = JSON.parse(raw) as unknown;\n if (\n parsed !== null &&\n typeof parsed === 'object' &&\n (parsed as { version?: unknown }).version === 1 &&\n typeof (parsed as { entries?: unknown }).entries === 'object' &&\n (parsed as { entries?: unknown }).entries !== null\n ) {\n return parsed as DiskCacheFile;\n }\n } catch {\n // ENOENT, parse error, schema mismatch — treat as empty\n }\n return { version: 1, entries: {} };\n}\n\nfunction writeDiskCache(filePath: string, data: DiskCacheFile): void {\n const dir = path.dirname(filePath);\n try {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n try {\n fs.chmodSync(dir, 0o700);\n } catch {\n // existing dir with custom mode — leave alone\n }\n const tmpPath = path.join(dir, '.access-token.json.tmp');\n fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\\n', { mode: 0o600 });\n fs.chmodSync(tmpPath, 0o600);\n fs.renameSync(tmpPath, filePath);\n fs.chmodSync(filePath, 0o600);\n } catch {\n // Disk-cache failures are non-fatal — the next call just refreshes again.\n }\n}\n\n/** Test seam: clear the in-memory acquire cache between tests. */\nexport function __resetAcquireCache(): void {\n CACHE.clear();\n}\n\n/** Decode a JWT payload without verifying the signature (CLI-side only). */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split('.');\n if (parts.length !== 3) return null;\n const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const json = Buffer.from(padded, 'base64').toString('utf8');\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nexport interface AcquireOptions {\n mcpUrl: string;\n profile: string;\n /** Force a fresh /auth/refresh even if the cache has a valid entry. */\n forceRefresh?: boolean;\n /** Test seam: overrides globalThis.fetch */\n fetch?: typeof globalThis.fetch;\n /** Test seam: overrides createTokenStore */\n createStore?: () => Promise<TokenStore>;\n /** Test seam: overrides process.env lookup */\n env?: NodeJS.ProcessEnv;\n /** Test seam: overrides Date.now() for expiry calculations. */\n now?: () => number;\n /** Test seam: overrides ~/.cleargate/access-token.json path. */\n diskCachePath?: string | null;\n}\n\nexport class AcquireError extends Error {\n constructor(\n message: string,\n public readonly code:\n | 'env_token'\n | 'no_stored_token'\n | 'invalid_token'\n | 'token_revoked'\n | 'transport'\n | 'unexpected_status'\n | 'bad_response',\n ) {\n super(message);\n this.name = 'AcquireError';\n }\n}\n\n/**\n * Returns a bearer string suitable for Authorization headers against /mcp and\n * /admin-api. Rotates the stored refresh token on success.\n */\nexport async function acquireAccessToken(opts: AcquireOptions): Promise<string> {\n const env = opts.env ?? process.env;\n const nowFn = opts.now ?? Date.now;\n\n // 1. Env short-circuit — CI / dev / manual paste. Assumed to be a valid JWT.\n // Env tokens are NOT cached — they have no known exp without decoding + the\n // env is set per-invocation in CI anyway.\n const envToken = env['CLEARGATE_MCP_TOKEN'];\n if (envToken && envToken.length > 0) {\n return envToken;\n }\n\n // 2a. In-memory cache check (skip when forceRefresh is set).\n const cacheKey = `${opts.profile}::${opts.mcpUrl}`;\n if (!opts.forceRefresh) {\n const cached = CACHE.get(cacheKey);\n if (cached && nowFn() < cached.expiresAtMs) {\n return cached.accessToken;\n }\n }\n\n // 2b. On-disk cache check — survives across CLI invocations and avoids\n // the keychain re-prompt loop that comes from per-call refresh-token\n // rotation. Disabled by passing diskCachePath: null (tests) or\n // CLEARGATE_DISK_CACHE_PATH=off in the real process env.\n // Note: consults process.env (not opts.env) because tests deliberately\n // pass empty `env: {}` to suppress CLEARGATE_MCP_TOKEN, but still want\n // the disk-cache override picked up from the test runner's env.\n const diskCachePath =\n opts.diskCachePath === undefined ? defaultDiskCachePath() : opts.diskCachePath;\n if (!opts.forceRefresh && diskCachePath) {\n const file = readDiskCache(diskCachePath);\n const entry = file.entries[cacheKey];\n if (entry && nowFn() < entry.expiresAtMs) {\n // Promote into in-memory cache for the rest of this process's lifetime.\n CACHE.set(cacheKey, entry);\n return entry.accessToken;\n }\n }\n\n // 3. Stored refresh token → POST /auth/refresh.\n const store = await (opts.createStore ?? createTokenStore)();\n const stored = await store.load(opts.profile);\n if (!stored) {\n throw new AcquireError(\n `No stored credentials for profile '${opts.profile}'. Run \\`cleargate join <invite-url>\\` first, or export CLEARGATE_MCP_TOKEN.`,\n 'no_stored_token',\n );\n }\n\n const fetchFn = opts.fetch ?? globalThis.fetch;\n\n let response: Response;\n try {\n response = await fetchFn(`${opts.mcpUrl}/auth/refresh`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ refresh_token: stored }),\n });\n } catch (err) {\n throw new AcquireError(\n `cannot reach ${opts.mcpUrl} (${err instanceof Error ? err.message : String(err)})`,\n 'transport',\n );\n }\n\n if (response.status === 401) {\n const body = (await response.json().catch(() => ({}))) as { error?: string };\n if (body.error === 'token_revoked') {\n throw new AcquireError(\n 'refresh token was revoked. Run `cleargate join <invite-url>` to re-authenticate.',\n 'token_revoked',\n );\n }\n throw new AcquireError(\n 'refresh token is invalid or expired. Run `cleargate join <invite-url>` to re-authenticate.',\n 'invalid_token',\n );\n }\n\n if (!response.ok) {\n throw new AcquireError(`unexpected status ${response.status} from /auth/refresh`, 'unexpected_status');\n }\n\n const body = (await response.json().catch(() => null)) as\n | { access_token?: unknown; refresh_token?: unknown }\n | null;\n if (\n !body ||\n typeof body.access_token !== 'string' ||\n typeof body.refresh_token !== 'string' ||\n body.access_token.length === 0 ||\n body.refresh_token.length === 0\n ) {\n throw new AcquireError('server returned unexpected /auth/refresh response shape', 'bad_response');\n }\n\n // Rotate — store the new refresh token so the next call uses a fresh jti.\n await store.save(opts.profile, body.refresh_token);\n\n const accessToken = body.access_token;\n\n // 4. Cache the new access token (expire 60s before the JWT exp claim) in\n // BOTH the in-memory map and on disk. The disk cache is what stops the\n // keychain re-prompt loop on subsequent CLI invocations.\n const payload = decodeJwtPayload(accessToken);\n const exp = payload?.exp;\n if (typeof exp === 'number' && Number.isFinite(exp)) {\n const expiresAtMs = (exp - 60) * 1000;\n const entry: DiskCacheEntry = { accessToken, expiresAtMs };\n CACHE.set(cacheKey, entry);\n if (diskCachePath) {\n const file = readDiskCache(diskCachePath);\n file.entries[cacheKey] = entry;\n writeDiskCache(diskCachePath, file);\n }\n }\n // If exp is missing or non-numeric, do NOT cache — next call will re-refresh.\n\n return accessToken;\n}\n"],"mappings":"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","names":["fs","os","path","Buffer","fs","os","path","decodeJwtPayload","body"]}