@skrr-ai/auth-core 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +21 -0
  2. package/dist/cjs/aead.d.ts +24 -0
  3. package/dist/cjs/aead.js +65 -0
  4. package/dist/cjs/authExpiredDetector.d.ts +65 -0
  5. package/dist/cjs/authExpiredDetector.js +89 -0
  6. package/dist/cjs/authHelper.d.ts +51 -0
  7. package/dist/cjs/authHelper.js +321 -0
  8. package/dist/cjs/credentialEnvelope.d.ts +85 -0
  9. package/dist/cjs/credentialEnvelope.js +230 -0
  10. package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
  11. package/dist/cjs/credentialEnvelopeBridge.js +637 -0
  12. package/dist/cjs/credentialResolver.d.ts +278 -0
  13. package/dist/cjs/credentialResolver.js +423 -0
  14. package/dist/cjs/daemonPair.d.ts +172 -0
  15. package/dist/cjs/daemonPair.js +495 -0
  16. package/dist/cjs/daemonScopes.d.ts +74 -0
  17. package/dist/cjs/daemonScopes.js +84 -0
  18. package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
  19. package/dist/cjs/deviceIdentityBridge.js +591 -0
  20. package/dist/cjs/deviceKey.d.ts +167 -0
  21. package/dist/cjs/deviceKey.js +332 -0
  22. package/dist/cjs/fdToken.d.ts +17 -0
  23. package/dist/cjs/fdToken.js +238 -0
  24. package/dist/cjs/harnessTrust.d.ts +72 -0
  25. package/dist/cjs/harnessTrust.js +103 -0
  26. package/dist/cjs/headers-helper.d.ts +25 -0
  27. package/dist/cjs/headers-helper.js +323 -0
  28. package/dist/cjs/index.d.ts +37 -0
  29. package/dist/cjs/index.js +305 -0
  30. package/dist/cjs/jwtUtils.d.ts +35 -0
  31. package/dist/cjs/jwtUtils.js +61 -0
  32. package/dist/cjs/kek/index.d.ts +89 -0
  33. package/dist/cjs/kek/index.js +208 -0
  34. package/dist/cjs/kek/linux.d.ts +92 -0
  35. package/dist/cjs/kek/linux.js +596 -0
  36. package/dist/cjs/kek/macos.d.ts +112 -0
  37. package/dist/cjs/kek/macos.js +599 -0
  38. package/dist/cjs/kek/types.d.ts +111 -0
  39. package/dist/cjs/kek/types.js +43 -0
  40. package/dist/cjs/kek/windows.d.ts +71 -0
  41. package/dist/cjs/kek/windows.js +350 -0
  42. package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
  43. package/dist/cjs/kek/zeroize-registry.js +63 -0
  44. package/dist/cjs/legacyStatePreflight.d.ts +67 -0
  45. package/dist/cjs/legacyStatePreflight.js +78 -0
  46. package/dist/cjs/localIdentity.d.ts +54 -0
  47. package/dist/cjs/localIdentity.js +57 -0
  48. package/dist/cjs/loginLocalhost.d.ts +115 -0
  49. package/dist/cjs/loginLocalhost.js +368 -0
  50. package/dist/cjs/loginWithLocalhost.d.ts +131 -0
  51. package/dist/cjs/loginWithLocalhost.js +359 -0
  52. package/dist/cjs/machineId.d.ts +35 -0
  53. package/dist/cjs/machineId.js +239 -0
  54. package/dist/cjs/messages.d.ts +9 -0
  55. package/dist/cjs/messages.js +44 -0
  56. package/dist/cjs/package.json +3 -0
  57. package/dist/cjs/pkce.d.ts +62 -0
  58. package/dist/cjs/pkce.js +158 -0
  59. package/dist/cjs/recoveryCode.d.ts +61 -0
  60. package/dist/cjs/recoveryCode.js +213 -0
  61. package/dist/cjs/refresh.d.ts +215 -0
  62. package/dist/cjs/refresh.js +877 -0
  63. package/dist/cjs/refreshClassification.d.ts +53 -0
  64. package/dist/cjs/refreshClassification.js +105 -0
  65. package/dist/cjs/refreshScheduler.d.ts +53 -0
  66. package/dist/cjs/refreshScheduler.js +332 -0
  67. package/dist/cjs/runtime.d.ts +149 -0
  68. package/dist/cjs/runtime.js +136 -0
  69. package/dist/cjs/spawnEnv.d.ts +76 -0
  70. package/dist/cjs/spawnEnv.js +153 -0
  71. package/dist/cjs/ttlParser.d.ts +54 -0
  72. package/dist/cjs/ttlParser.js +87 -0
  73. package/dist/cjs/types.d.ts +91 -0
  74. package/dist/cjs/types.js +26 -0
  75. package/dist/esm/aead.d.ts +24 -0
  76. package/dist/esm/aead.js +57 -0
  77. package/dist/esm/authExpiredDetector.d.ts +65 -0
  78. package/dist/esm/authExpiredDetector.js +85 -0
  79. package/dist/esm/authHelper.d.ts +51 -0
  80. package/dist/esm/authHelper.js +311 -0
  81. package/dist/esm/credentialEnvelope.d.ts +85 -0
  82. package/dist/esm/credentialEnvelope.js +213 -0
  83. package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
  84. package/dist/esm/credentialEnvelopeBridge.js +620 -0
  85. package/dist/esm/credentialResolver.d.ts +278 -0
  86. package/dist/esm/credentialResolver.js +414 -0
  87. package/dist/esm/daemonPair.d.ts +172 -0
  88. package/dist/esm/daemonPair.js +476 -0
  89. package/dist/esm/daemonScopes.d.ts +74 -0
  90. package/dist/esm/daemonScopes.js +80 -0
  91. package/dist/esm/deviceIdentityBridge.d.ts +113 -0
  92. package/dist/esm/deviceIdentityBridge.js +572 -0
  93. package/dist/esm/deviceKey.d.ts +167 -0
  94. package/dist/esm/deviceKey.js +320 -0
  95. package/dist/esm/fdToken.d.ts +17 -0
  96. package/dist/esm/fdToken.js +231 -0
  97. package/dist/esm/harnessTrust.d.ts +72 -0
  98. package/dist/esm/harnessTrust.js +97 -0
  99. package/dist/esm/headers-helper.d.ts +25 -0
  100. package/dist/esm/headers-helper.js +316 -0
  101. package/dist/esm/index.d.ts +37 -0
  102. package/dist/esm/index.js +144 -0
  103. package/dist/esm/jwtUtils.d.ts +35 -0
  104. package/dist/esm/jwtUtils.js +57 -0
  105. package/dist/esm/kek/index.d.ts +89 -0
  106. package/dist/esm/kek/index.js +162 -0
  107. package/dist/esm/kek/linux.d.ts +92 -0
  108. package/dist/esm/kek/linux.js +585 -0
  109. package/dist/esm/kek/macos.d.ts +112 -0
  110. package/dist/esm/kek/macos.js +586 -0
  111. package/dist/esm/kek/types.d.ts +111 -0
  112. package/dist/esm/kek/types.js +39 -0
  113. package/dist/esm/kek/windows.d.ts +71 -0
  114. package/dist/esm/kek/windows.js +344 -0
  115. package/dist/esm/kek/zeroize-registry.d.ts +44 -0
  116. package/dist/esm/kek/zeroize-registry.js +58 -0
  117. package/dist/esm/legacyStatePreflight.d.ts +67 -0
  118. package/dist/esm/legacyStatePreflight.js +71 -0
  119. package/dist/esm/localIdentity.d.ts +54 -0
  120. package/dist/esm/localIdentity.js +54 -0
  121. package/dist/esm/loginLocalhost.d.ts +115 -0
  122. package/dist/esm/loginLocalhost.js +355 -0
  123. package/dist/esm/loginWithLocalhost.d.ts +131 -0
  124. package/dist/esm/loginWithLocalhost.js +353 -0
  125. package/dist/esm/machineId.d.ts +35 -0
  126. package/dist/esm/machineId.js +231 -0
  127. package/dist/esm/messages.d.ts +9 -0
  128. package/dist/esm/messages.js +40 -0
  129. package/dist/esm/pkce.d.ts +62 -0
  130. package/dist/esm/pkce.js +148 -0
  131. package/dist/esm/recoveryCode.d.ts +61 -0
  132. package/dist/esm/recoveryCode.js +207 -0
  133. package/dist/esm/refresh.d.ts +215 -0
  134. package/dist/esm/refresh.js +863 -0
  135. package/dist/esm/refreshClassification.d.ts +53 -0
  136. package/dist/esm/refreshClassification.js +106 -0
  137. package/dist/esm/refreshScheduler.d.ts +53 -0
  138. package/dist/esm/refreshScheduler.js +329 -0
  139. package/dist/esm/runtime.d.ts +149 -0
  140. package/dist/esm/runtime.js +121 -0
  141. package/dist/esm/spawnEnv.d.ts +76 -0
  142. package/dist/esm/spawnEnv.js +149 -0
  143. package/dist/esm/ttlParser.d.ts +54 -0
  144. package/dist/esm/ttlParser.js +83 -0
  145. package/dist/esm/types.d.ts +91 -0
  146. package/dist/esm/types.js +21 -0
  147. package/package.json +82 -0
@@ -0,0 +1,316 @@
1
+ /**
2
+ * headers-helper.ts — invocation contract for `OVERSKY_HEADERS_HELPER`.
3
+ *
4
+ * The helper is a user-supplied executable that prints a JSON object of
5
+ * HTTP headers to stdout. It exists so operators can supply dynamic,
6
+ * rotating credentials for outbound MCP server connections (e.g., a
7
+ * single `~/.skrr/mcp-headers.sh` that switches on `$MCP_SERVER_NAME`).
8
+ *
9
+ * This is a SIBLING to `OVERSKY_AUTH_HELPER` (daemon→OverSky auth).
10
+ * Headers helper authenticates OverSky to OUTBOUND MCP servers.
11
+ *
12
+ * Wire protocol (git credential-helper style — see deshaw/anthropic-issues#28):
13
+ * - `OVERSKY_HEADERS_HELPER` env points at an absolute, executable path.
14
+ * - We `spawn(path, [], { shell: false, detached: true })` — never via
15
+ * shell (no argument injection), detached so the group can be
16
+ * SIGKILL-ed on timeout.
17
+ * - `MCP_SERVER_NAME` and `MCP_SERVER_URL` are passed via env (NOT argv)
18
+ * so the values never appear in `ps` output.
19
+ * - The helper prints a JSON object of string-valued headers to stdout.
20
+ * - Stderr is captured for diagnostic logging only.
21
+ * - Stdout cap: 64 KiB. Real header payloads are tiny.
22
+ * - Default timeout: 10 s (same constant as auth-helper).
23
+ *
24
+ * Output validation (fail closed on violation):
25
+ * - Stdout MUST be valid JSON parseable as `Record<string, string>`.
26
+ * - Every key MUST match RFC 7230 §3.2.6 token chars:
27
+ * `^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$`
28
+ * - Every value MUST be a string.
29
+ * - `Authorization` values MUST start with a recognised scheme word
30
+ * followed by a space (`Bearer `, `Basic `, `Token `, `Digest `,
31
+ * `NTLM `, `Negotiate `, `AWS4-HMAC-SHA256 `, `MAC `). This guards
32
+ * against the common bug where a helper returns a raw secret without
33
+ * the scheme prefix, which causes a malformed Authorization header.
34
+ *
35
+ * Caching: per-serverName, 5-minute TTL (same as auth-helper DEFAULT_TTL_MS).
36
+ * Invalidation seam mirrors `invalidateAuthHelperCache`.
37
+ *
38
+ * Failure semantics: cold-cache helper failure throws (fail closed) rather
39
+ * than silently sending the request without auth headers. Warm-cache
40
+ * failure logs a warning and returns the stale cached value.
41
+ *
42
+ * Kill switch: `OVERSKY_DISABLE_HEADERS_HELPER=1` short-circuits to {}.
43
+ */
44
+ import { spawn } from 'node:child_process';
45
+ import fs from 'node:fs';
46
+ import path from 'node:path';
47
+ import { getAuthLogger } from './runtime.js';
48
+ // -------------------------------------------------------------------------
49
+ // Constants — deliberately not re-imported from authHelper.ts to keep the
50
+ // two modules independently deployable. Values are identical by spec.
51
+ // -------------------------------------------------------------------------
52
+ const MAX_OUTPUT_BYTES = 64 * 1024;
53
+ const HELPER_TIMEOUT_MS = 10_000;
54
+ const CACHE_TTL_MS = 5 * 60_000;
55
+ /**
56
+ * RFC 7230 §3.2.6 token character class.
57
+ * Valid HTTP header field name chars: printable US-ASCII minus separators.
58
+ */
59
+ const HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/;
60
+ /**
61
+ * Recognised Authorization scheme words. Requiring the scheme prefix
62
+ * prevents helpers from accidentally returning a raw secret as the
63
+ * header value, which produces a malformed Authorization header that
64
+ * either leaks or silently fails at the MCP server.
65
+ */
66
+ const AUTH_SCHEME_RE = /^(?:Bearer |Basic |Token |Digest |NTLM |Negotiate |AWS4-HMAC-SHA256 |MAC )/i;
67
+ /** Module-level cache: serverName → entry. */
68
+ const cache = new Map();
69
+ /**
70
+ * Invalidate the headers-helper cache.
71
+ * - `invalidateHeadersHelperCache('foo')` clears only the `foo` server.
72
+ * - `invalidateHeadersHelperCache()` clears all servers.
73
+ */
74
+ export function invalidateHeadersHelperCache(serverName) {
75
+ if (serverName !== undefined) {
76
+ cache.delete(serverName);
77
+ }
78
+ else {
79
+ cache.clear();
80
+ }
81
+ }
82
+ /**
83
+ * Invoke the headers helper for the named MCP server and return the
84
+ * resulting headers. Results are cached per `serverName` for 5 minutes.
85
+ *
86
+ * On cold-cache failure the function throws (fail closed — better to
87
+ * surface the misconfiguration than to silently send unauthenticated
88
+ * requests to an MCP server expecting credentials).
89
+ *
90
+ * On warm-cache helper failure the stale cached value is returned with
91
+ * a warning so ongoing requests are not disrupted during a transient
92
+ * helper outage.
93
+ *
94
+ * Returns `{}` (empty object) when no helper is configured.
95
+ */
96
+ export async function runHeadersHelper(opts) {
97
+ const log = getAuthLogger();
98
+ if (process.env.OVERSKY_DISABLE_HEADERS_HELPER === '1') {
99
+ log.debug?.('[headersHelper] OVERSKY_DISABLE_HEADERS_HELPER=1 — skipping');
100
+ return {};
101
+ }
102
+ const scriptPath = process.env.OVERSKY_HEADERS_HELPER?.trim();
103
+ if (!scriptPath) {
104
+ return {};
105
+ }
106
+ // Validate path before any subprocess spawn.
107
+ if (!path.isAbsolute(scriptPath)) {
108
+ throw new Error(`[headersHelper] OVERSKY_HEADERS_HELPER must be an absolute path; got "${scriptPath}"`);
109
+ }
110
+ let stat;
111
+ try {
112
+ stat = fs.statSync(scriptPath);
113
+ }
114
+ catch (err) {
115
+ throw new Error(`[headersHelper] OVERSKY_HEADERS_HELPER not found at "${scriptPath}": ${formatErr(err)}`);
116
+ }
117
+ if (!stat.isFile()) {
118
+ throw new Error(`[headersHelper] OVERSKY_HEADERS_HELPER "${scriptPath}" is not a regular file`);
119
+ }
120
+ if ((stat.mode & 0o111) === 0) {
121
+ throw new Error(`[headersHelper] OVERSKY_HEADERS_HELPER "${scriptPath}" is not executable`);
122
+ }
123
+ // Cache hit?
124
+ const cached = cache.get(opts.serverName);
125
+ if (cached && Date.now() < cached.expiresAt) {
126
+ log.debug?.(`[headersHelper] cache hit for server "${opts.serverName}"`);
127
+ return cached.headers;
128
+ }
129
+ const isWarm = cached !== undefined;
130
+ let headers;
131
+ try {
132
+ headers = await invokeScript(scriptPath, opts.serverName, opts.serverUrl, log);
133
+ }
134
+ catch (err) {
135
+ if (isWarm) {
136
+ log.warn?.(`[headersHelper] helper failed for "${opts.serverName}" (using stale cache): ${formatErr(err)}`);
137
+ return cached.headers;
138
+ }
139
+ // Cold cache: fail closed.
140
+ throw err;
141
+ }
142
+ cache.set(opts.serverName, {
143
+ headers,
144
+ expiresAt: Date.now() + CACHE_TTL_MS,
145
+ });
146
+ return headers;
147
+ }
148
+ // -------------------------------------------------------------------------
149
+ // Internal — script invocation
150
+ // -------------------------------------------------------------------------
151
+ async function invokeScript(scriptPath, serverName, serverUrl, log) {
152
+ return new Promise((resolve, reject) => {
153
+ const stdoutChunks = [];
154
+ const stderrChunks = [];
155
+ let stdoutBytes = 0;
156
+ let stderrBytes = 0;
157
+ let truncated = false;
158
+ let killedForTimeout = false;
159
+ let settled = false;
160
+ const settle = (result) => {
161
+ if (settled)
162
+ return;
163
+ settled = true;
164
+ clearTimeout(timer);
165
+ if (result instanceof Error) {
166
+ reject(result);
167
+ }
168
+ else {
169
+ resolve(result);
170
+ }
171
+ };
172
+ let child;
173
+ try {
174
+ child = spawn(scriptPath, [], {
175
+ shell: false,
176
+ // detached=true → child is its own process-group leader so we can
177
+ // SIGKILL the whole tree (vault, curl, etc.) on timeout, not just
178
+ // the script shell. Mirrors the same pattern in authHelper.ts.
179
+ detached: true,
180
+ stdio: ['ignore', 'pipe', 'pipe'],
181
+ env: {
182
+ ...process.env,
183
+ // Pass context via env, NOT argv — keeps the URL out of `ps` listings.
184
+ MCP_SERVER_NAME: serverName,
185
+ MCP_SERVER_URL: serverUrl,
186
+ },
187
+ });
188
+ }
189
+ catch (err) {
190
+ reject(new Error(`[headersHelper] spawn failed for "${serverName}": ${formatErr(err)}`));
191
+ return;
192
+ }
193
+ const timer = setTimeout(() => {
194
+ killedForTimeout = true;
195
+ log.warn?.(`[headersHelper] helper timed out after ${HELPER_TIMEOUT_MS}ms for "${serverName}" — killing`);
196
+ try {
197
+ if (typeof child.pid === 'number') {
198
+ process.kill(-child.pid, 'SIGKILL');
199
+ }
200
+ else {
201
+ child.kill('SIGKILL');
202
+ }
203
+ }
204
+ catch {
205
+ try {
206
+ child.kill('SIGKILL');
207
+ }
208
+ catch {
209
+ /* ignore */
210
+ }
211
+ }
212
+ try {
213
+ child.stdout?.destroy();
214
+ }
215
+ catch {
216
+ /* ignore */
217
+ }
218
+ try {
219
+ child.stderr?.destroy();
220
+ }
221
+ catch {
222
+ /* ignore */
223
+ }
224
+ settle(new Error(`[headersHelper] helper timed out after ${HELPER_TIMEOUT_MS}ms for server "${serverName}"`));
225
+ }, HELPER_TIMEOUT_MS);
226
+ if (typeof timer.unref === 'function')
227
+ timer.unref();
228
+ child.stdout?.on('data', (chunk) => {
229
+ stdoutBytes += chunk.length;
230
+ if (stdoutBytes > MAX_OUTPUT_BYTES) {
231
+ truncated = true;
232
+ try {
233
+ child.kill('SIGKILL');
234
+ }
235
+ catch {
236
+ /* ignore */
237
+ }
238
+ return;
239
+ }
240
+ stdoutChunks.push(chunk);
241
+ });
242
+ child.stderr?.on('data', (chunk) => {
243
+ if (stderrBytes < MAX_OUTPUT_BYTES) {
244
+ stderrChunks.push(chunk);
245
+ stderrBytes += chunk.length;
246
+ }
247
+ });
248
+ child.on('error', (err) => {
249
+ settle(new Error(`[headersHelper] runtime error for "${serverName}": ${formatErr(err)}`));
250
+ });
251
+ child.on('close', (code) => {
252
+ if (killedForTimeout) {
253
+ // timer already settled
254
+ return;
255
+ }
256
+ if (truncated) {
257
+ settle(new Error(`[headersHelper] stdout exceeded ${MAX_OUTPUT_BYTES} bytes for "${serverName}" — refusing to parse`));
258
+ return;
259
+ }
260
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8').trim();
261
+ if (stderr) {
262
+ log.debug?.(`[headersHelper] stderr for "${serverName}": ${stderr}`);
263
+ }
264
+ if (code !== 0) {
265
+ settle(new Error(`[headersHelper] helper exited with code ${code} for "${serverName}"`));
266
+ return;
267
+ }
268
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8').trim();
269
+ if (!stdout) {
270
+ settle(new Error(`[headersHelper] helper produced empty stdout for "${serverName}"`));
271
+ return;
272
+ }
273
+ let parsed;
274
+ try {
275
+ parsed = JSON.parse(stdout);
276
+ }
277
+ catch (err) {
278
+ settle(new Error(`[headersHelper] JSON parse failed for "${serverName}": ${formatErr(err)}`));
279
+ return;
280
+ }
281
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
282
+ settle(new Error(`[headersHelper] output for "${serverName}" must be a JSON object, got: ${JSON.stringify(parsed)}`));
283
+ return;
284
+ }
285
+ const result = {};
286
+ for (const [key, value] of Object.entries(parsed)) {
287
+ // Validate header name (RFC 7230 §3.2.6 token chars).
288
+ if (!HEADER_NAME_RE.test(key)) {
289
+ settle(new Error(`[headersHelper] invalid header name "${key}" for server "${serverName}" — must match RFC 7230 token chars`));
290
+ return;
291
+ }
292
+ // Values must be strings.
293
+ if (typeof value !== 'string') {
294
+ settle(new Error(`[headersHelper] non-string value for header "${key}" in server "${serverName}": ${typeof value}`));
295
+ return;
296
+ }
297
+ // Authorization values must include a scheme word.
298
+ if (key.toLowerCase() === 'authorization' && !AUTH_SCHEME_RE.test(value)) {
299
+ settle(new Error(`[headersHelper] Authorization header for "${serverName}" must start with a recognised scheme (Bearer, Basic, Token, …). ` +
300
+ `Got: "${value.slice(0, 20)}…". ` +
301
+ `This guards against raw secrets without a scheme prefix.`));
302
+ return;
303
+ }
304
+ result[key] = value;
305
+ }
306
+ log.debug?.(`[headersHelper] got ${Object.keys(result).length} header(s) for server "${serverName}"`);
307
+ settle(result);
308
+ });
309
+ });
310
+ }
311
+ // -------------------------------------------------------------------------
312
+ // Utilities
313
+ // -------------------------------------------------------------------------
314
+ function formatErr(err) {
315
+ return err instanceof Error ? err.message : String(err);
316
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @skrr-ai/auth-core — shared auth substrate.
3
+ *
4
+ * Public surface for the OverSky daemon, Sky CLI, and Electron main
5
+ * process. Each consumer calls `configureAuthCore()` once at startup to
6
+ * wire up its logger, config dir, kill switches, and binary name — then
7
+ * uses the rest of the API uniformly.
8
+ */
9
+ export { SCOPES, ALL_DAEMON_SCOPES, VALID_DAEMON_SCOPE_SET, isDaemonScope, OAUTH_CLI_SCOPE, type DaemonScope, type OAuthCliScope, type OAuthScope, } from './daemonScopes.js';
10
+ export { configureAuthCore, getAuthLogger, getAuthConfigDir, getAuthKillSwitches, getAuthBinaryName, emitAuthTelemetry, type AuthCoreConfig, type AuthLogger, type AuthKillSwitches, type AuthTelemetryEvent, } from './runtime.js';
11
+ export { PermanentAuthFailure, TransientAuthFailure, type PermanentAuthReason, type ReauthState, type TokenPair, } from './types.js';
12
+ export { decodeJwtExpiry, decodeJwtPayload } from './jwtUtils.js';
13
+ export { isAuthExpiredSseEvent, handleMidStreamAuthExpiry, type ParsedSseLike, type AuthExpiredCallbacks, type HandleMidStreamAuthExpiryResult, } from './authExpiredDetector.js';
14
+ export { formatReauthMessage, EXIT_NEEDS_REAUTH, type AuthFailureReason } from './messages.js';
15
+ export { classifyRefreshResponse, type RefreshResponseShape, type ClassifiedRefreshResult, type ClassifiedRefreshSuccess, type ClassifiedRefreshFailure, } from './refreshClassification.js';
16
+ export { refreshDaemonToken, revokeDaemonRefreshSession, withAuthLock, persistReauthState, needsReauthState, clearReauthState, installLockExitHandlers, __refreshDiagnostics, __resetUnknown401StreaksForTest, UNKNOWN_401_BUDGET, } from './refresh.js';
17
+ export { createRefreshScheduler, type RefreshScheduler, type RefreshSchedulerOptions, } from './refreshScheduler.js';
18
+ export { readAuthFromFd, __setFdReaderForTest, type FdAuthPayload } from './fdToken.js';
19
+ export { CredentialResolver, CREDENTIAL_PRECEDENCE, __resetCredentialResolverForTest, invalidateAuthHelperCache, classifyTokenKind, CI_TOKEN_PREFIX, AuthHelperUntrustedError, AuthHelperUnavailableError, type CredentialSource, type CredentialStoreMode, type CredentialResolverOptions, type ResolvedCredential, type TokenKind, type HelperTrustDecision, type AuthHelperBlockedInfo, type AuthHelperSentinelInfo, } from './credentialResolver.js';
20
+ export { invokeAuthHelper, getDefaultHelperTtlMs, getLastAuthHelperFailureReason, __resetAuthHelperFailureReasonForTest, SENTINEL_TRIGGERING_FAILURES, type HelperResult, type InvokeAuthHelperOptions, type AuthHelperFailureReason, } from './authHelper.js';
21
+ export { runHeadersHelper, invalidateHeadersHelperCache, type RunHeadersHelperOptions, } from './headers-helper.js';
22
+ export { generateVerifier, challenge, generateState, safeEqual, verifyChallenge, type PkceChallengeMethod, } from './pkce.js';
23
+ export { startLocalCallback, DEFAULT_LOGIN_TIMEOUT_MS, LocalCallbackError, LocalCallbackBindError, LocalCallbackTimeoutError, LocalCallbackStateMismatchError, LocalCallbackDeniedError, LocalCallbackMalformedError, type StartLocalCallbackOptions, type LocalCallbackResult, type LocalCallbackHandle, } from './loginLocalhost.js';
24
+ export { loginWithLocalhost, LoginInitError, LoginExchangeError, type BrowserOpener, type LoginWithLocalhostOptions, type LoginWithLocalhostResult, } from './loginWithLocalhost.js';
25
+ export { parseTtlDays, MIN_TTL_DAYS, MAX_TTL_DAYS, type ParseTtlResult, type ParseTtlSuccess, type ParseTtlError, } from './ttlParser.js';
26
+ export { formatRecoveryCode, parseRecoveryCode, normalizeRecoveryCode, isValidRecoveryCode, } from './recoveryCode.js';
27
+ export { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, ENVELOPE_MAGIC as CRED_ENVELOPE_MAGIC, generateDek, isEnvelopeString as isCredEnvelopeString, KEY_LEN as CRED_KEY_LEN, openBuffer as credOpenBuffer, sealBuffer as credSealBuffer, serialize as credEnvelopeSerialize, unwrap as credEnvelopeUnwrap, wrap as credEnvelopeWrap, type SealedBlob as CredSealedBlob, } from './credentialEnvelope.js';
28
+ export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, type KekStrategy, } from './kek/index.js';
29
+ export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
30
+ export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
31
+ export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
32
+ export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
33
+ export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
34
+ export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
35
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
36
+ export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
37
+ export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @skrr-ai/auth-core — shared auth substrate.
3
+ *
4
+ * Public surface for the OverSky daemon, Sky CLI, and Electron main
5
+ * process. Each consumer calls `configureAuthCore()` once at startup to
6
+ * wire up its logger, config dir, kill switches, and binary name — then
7
+ * uses the rest of the API uniformly.
8
+ */
9
+ // Daemon scope vocabulary — single source of truth for non-Mongoose tiers
10
+ // (daemon, desktop, cli). Schema-bound canonical lives in
11
+ // `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
12
+ // by the api-side parity test. See daemonScopes.ts header for details.
13
+ export { SCOPES, ALL_DAEMON_SCOPES, VALID_DAEMON_SCOPE_SET, isDaemonScope, OAUTH_CLI_SCOPE, } from './daemonScopes.js';
14
+ // Runtime / DI
15
+ export { configureAuthCore, getAuthLogger, getAuthConfigDir, getAuthKillSwitches, getAuthBinaryName, emitAuthTelemetry, } from './runtime.js';
16
+ // Types
17
+ export { PermanentAuthFailure, TransientAuthFailure, } from './types.js';
18
+ // JWT helpers
19
+ export { decodeJwtExpiry, decodeJwtPayload } from './jwtUtils.js';
20
+ // Mid-stream auth-expired SSE detector — recognises terminal "auth_expired"
21
+ // events from long-lived SSE / Socket.IO streams and orchestrates a
22
+ // refresher-driven recovery. Mobile RN keeps a byte-equal mirror; see file
23
+ // header.
24
+ export { isAuthExpiredSseEvent, handleMidStreamAuthExpiry, } from './authExpiredDetector.js';
25
+ // Reauth messaging
26
+ export { formatReauthMessage, EXIT_NEEDS_REAUTH } from './messages.js';
27
+ // Pure HTTP→{Permanent, Transient, Success} response classification —
28
+ // exposed so RN-side clients can apply the same mapping daemon uses.
29
+ export { classifyRefreshResponse, } from './refreshClassification.js';
30
+ // Refresh
31
+ export { refreshDaemonToken, revokeDaemonRefreshSession, withAuthLock, persistReauthState, needsReauthState, clearReauthState, installLockExitHandlers,
32
+ // H1 — Test-only diagnostic counters that prove every outgoing
33
+ // refresh request mints a fresh proof. Production code never reads
34
+ // this; tests use it to assert the per-attempt invariant.
35
+ __refreshDiagnostics,
36
+ // Finding 4 — Test-only reset for the per-daemonId unknown-shape-401
37
+ // retry budget. Lets tests start each case with a clean streak.
38
+ __resetUnknown401StreaksForTest,
39
+ // Finding 4 follow-up (BOOT-BUDGET COUPLING) — consumed by the daemon's
40
+ // startup-recovery loop to statically assert
41
+ // `STARTUP_RECOVERY_MAX_ATTEMPTS >= UNKNOWN_401_BUDGET` so the in-process
42
+ // unknown-401 latch always fires before the boot loop yields to the
43
+ // supervisor. See daemon/src/auth.ts.
44
+ UNKNOWN_401_BUDGET, } from './refresh.js';
45
+ // Refresh scheduler
46
+ export { createRefreshScheduler, } from './refreshScheduler.js';
47
+ // Fd handoff
48
+ export { readAuthFromFd, __setFdReaderForTest } from './fdToken.js';
49
+ // Credential resolver — Phase H precedence chain + Phase I tokenKind
50
+ export { CredentialResolver, CREDENTIAL_PRECEDENCE, __resetCredentialResolverForTest,
51
+ // H3-6 — public seam 401 handlers call to bust the helper cache so
52
+ // the next resolve re-invokes OVERSKY_AUTH_HELPER immediately
53
+ // instead of serving the stale cached token for up to its TTL.
54
+ invalidateAuthHelperCache, classifyTokenKind, CI_TOKEN_PREFIX,
55
+ // L6.1 / L6.3 — fail-closed errors thrown by the auth-helper slot.
56
+ AuthHelperUntrustedError, AuthHelperUnavailableError, } from './credentialResolver.js';
57
+ // Auth helper — invocation contract for OVERSKY_AUTH_HELPER
58
+ export { invokeAuthHelper, getDefaultHelperTtlMs, getLastAuthHelperFailureReason, __resetAuthHelperFailureReasonForTest, SENTINEL_TRIGGERING_FAILURES, } from './authHelper.js';
59
+ // Headers helper — invocation contract for OVERSKY_HEADERS_HELPER
60
+ // (authenticates OverSky to outbound MCP servers)
61
+ export { runHeadersHelper, invalidateHeadersHelperCache, } from './headers-helper.js';
62
+ // PKCE — RFC 7636 primitives for the Phase H.0 localhost-callback login.
63
+ // Keep `safeEqual` and `verifyChallenge` exported even though they are
64
+ // server-side concerns: server code consumes auth-core via the same
65
+ // public surface and the alternative (a separate server-only module)
66
+ // fragments the spec implementation across two locations.
67
+ export { generateVerifier, challenge, generateState, safeEqual, verifyChallenge, } from './pkce.js';
68
+ // Local 127.0.0.1 callback server — Phase H.0.2.
69
+ // Keep error classes exported; callers branch on them to decide
70
+ // fallback behavior (BindError → retry on different port; TimeoutError
71
+ // → fall back to device-code; StateMismatchError → fail loudly).
72
+ export { startLocalCallback, DEFAULT_LOGIN_TIMEOUT_MS, LocalCallbackError, LocalCallbackBindError, LocalCallbackTimeoutError, LocalCallbackStateMismatchError, LocalCallbackDeniedError, LocalCallbackMalformedError, } from './loginLocalhost.js';
73
+ // Phase H.0.2.b — single-call orchestrator that ties PKCE + local
74
+ // callback + server endpoints into the daemon-style token bundle.
75
+ // Daemon/CLI consume this; auth-core is platform-agnostic via the
76
+ // caller-injected `openBrowser` and `fetchImpl`.
77
+ export { loginWithLocalhost, LoginInitError, LoginExchangeError, } from './loginWithLocalhost.js';
78
+ // ADD-CLI1 — shared `--ttl` parser for `create-token`. Both the
79
+ // daemon CLI (`oversky create-token`) and the oclif CLI (`sky
80
+ // create-token`) import from here to prevent grammar drift.
81
+ export { parseTtlDays, MIN_TTL_DAYS, MAX_TTL_DAYS, } from './ttlParser.js';
82
+ // Tier 1 #3 — printable account-recovery code format (1Password-style
83
+ // 11-group base32). Server, CLI, and daemon-side helper all import from here.
84
+ export { formatRecoveryCode, parseRecoveryCode, normalizeRecoveryCode, isValidRecoveryCode, } from './recoveryCode.js';
85
+ // L12 Layer 0 — at-rest credential envelope encryption primitives.
86
+ // Pure crypto, no platform code. Daemon and CLI both consume.
87
+ export { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, ENVELOPE_MAGIC as CRED_ENVELOPE_MAGIC, generateDek, isEnvelopeString as isCredEnvelopeString, KEY_LEN as CRED_KEY_LEN, openBuffer as credOpenBuffer, sealBuffer as credSealBuffer, serialize as credEnvelopeSerialize, unwrap as credEnvelopeUnwrap, wrap as credEnvelopeWrap, } from './credentialEnvelope.js';
88
+ // L12 Layer 1 — KEK platform-strategy interface and implementations.
89
+ //
90
+ // This block used to claim "today every strategy reports available=false so
91
+ // the daemon falls through to legacy plaintext". That has not been true since
92
+ // the macOS `security(1)` backend (C5) and the Linux tier-3 machine-id HKDF
93
+ // fallback shipped: on Linux `available()` is now essentially always true, and
94
+ // on macOS it is true whenever a login keychain resolves. The stale note is
95
+ // worth calling out rather than deleting, because four daemon suites were
96
+ // written against it and only failed once CI first ran them on Linux.
97
+ //
98
+ // Tests that need the unavailable branch must inject `UnavailableKek`;
99
+ // `__setKekStrategyForTest(null)` restores the PLATFORM default, which is not
100
+ // the same thing and differs per OS.
101
+ export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, } from './kek/index.js';
102
+ // L11 Phase 1 — Device identity (Ed25519) + DPoP-shaped proof JWS.
103
+ // The crypto half of the two-axis identity story; daemon-side
104
+ // persistence + signing lives in `daemon/src/deviceIdentity.ts`.
105
+ // Server-side verification middleware is a later PR.
106
+ export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, } from './deviceKey.js';
107
+ // L11/L12 — composed runtime: init, transforms, sign, enroll, locks.
108
+ // Daemon and CLI consume these; the underlying primitives live above.
109
+ export {
110
+ // credEnvelope bridge
111
+ initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead,
112
+ // Test seams — `__`-prefixed so consumers don't import them in
113
+ // production code paths. Re-exported here so CLI/daemon spec files
114
+ // can resolve the wrapped-DEK path against the same module instance.
115
+ __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, } from './credentialEnvelopeBridge.js';
116
+ export {
117
+ // deviceIdentity bridge
118
+ initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState,
119
+ // Test seams — file path resolvers + state setters for spec files.
120
+ __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, } from './deviceIdentityBridge.js';
121
+ // Stable per-machine UUIDv7 identity persisted at <configDir>/daemon.id.
122
+ export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
123
+ // Tier 1 #2 — daemon QR pairing primitives. Pure crypto + AAD framing,
124
+ // shared between the daemon CLI orchestrator (`daemon/src/pair.ts`) and
125
+ // any future native-app callers. The mobile RN client mirrors the same
126
+ // shapes locally because it can't pull a Node-shaped package.
127
+ export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, } from './daemonPair.js';
128
+ // OSK-3897 — harness trust tiers, shared by the daemon (the authority) and the
129
+ // CLI (which publishes the fact in `sky code doctor`). The PROVENANCE half stays
130
+ // in the daemon on purpose: a tier is a published fact, provenance is evidence
131
+ // about bytes on disk that only the spawning process can gather.
132
+ export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, } from './harnessTrust.js';
133
+ // OSK-3892 — spawn-env sanitization, shared by the daemon (which has stripped
134
+ // its own credentials before every harness spawn all along) and the CLI (which
135
+ // spawns the SAME engine and did not). `daemon/src/env-sanitize.ts` re-exports
136
+ // these so its own importers are unchanged, and keeps `filterCustomEnv` — that
137
+ // governs SERVER-PROVIDED env, which no other process is handed.
138
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
139
+ /* Local-machine identity — the names this product leaves on a disk. Exported so
140
+ the CLI, daemon and Desktop share one definition instead of 47 literals. */
141
+ export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
142
+ /* Pre-rename state detection. Callers REFUSE on a positive result; nothing here
143
+ reads, copies or removes what it finds (§7.2, §8.4). */
144
+ export { findLegacyLocalState, describeLegacyState, } from './legacyStatePreflight.js';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * jwtUtils.ts — signature-less JWT payload inspection.
3
+ *
4
+ * Used by the proactive refresh scheduler to read the `exp` claim off an
5
+ * access token without round-tripping to the server. We do NOT verify the
6
+ * signature here — the token is one the server just issued to us, and any
7
+ * tampering would surface as a 401 on the next API call anyway.
8
+ *
9
+ * THEREFORE: NOTHING THESE FUNCTIONS RETURN MAY DECIDE WHAT THE HOLDER IS
10
+ * ALLOWED TO DO.
11
+ *
12
+ * The reasoning above is only sound because the decoded values are used to
13
+ * schedule work (when to refresh) and to pick a local code path — never to
14
+ * grant anything. A claim read here is attacker-controlled input in the exact
15
+ * case that matters: anyone who can write the credential file, set the env var,
16
+ * or hand us a token over an fd can put any `scope`, `role`, `exp` or `sub` in
17
+ * it, because forging a payload requires no key when nobody checks the
18
+ * signature. "It would 401 later" is not a control if the decision was already
19
+ * made locally before any call went out.
20
+ *
21
+ * Safe: refresh timing, telemetry, diagnostics, choosing which resolver branch
22
+ * to take. Unsafe: gating a capability, trusting an identity, skipping a check,
23
+ * widening a scope. If you need a claim you can trust, verify the signature or
24
+ * ask the server.
25
+ *
26
+ * `credentialResolver.classifyTokenKind` is the one non-`exp` reader today; see
27
+ * the note there for why routing on `scope` is not an authorization decision.
28
+ */
29
+ /** Base64url-decode the middle segment of a JWT and JSON-parse it. */
30
+ export declare function decodeJwtPayload(token: string): unknown | null;
31
+ /**
32
+ * Decode the `exp` (expiry) claim from a JWT without verifying the signature.
33
+ * @returns The `exp` value in Unix seconds, or `null` if unparseable / missing.
34
+ */
35
+ export declare function decodeJwtExpiry(token: string): number | null;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * jwtUtils.ts — signature-less JWT payload inspection.
3
+ *
4
+ * Used by the proactive refresh scheduler to read the `exp` claim off an
5
+ * access token without round-tripping to the server. We do NOT verify the
6
+ * signature here — the token is one the server just issued to us, and any
7
+ * tampering would surface as a 401 on the next API call anyway.
8
+ *
9
+ * THEREFORE: NOTHING THESE FUNCTIONS RETURN MAY DECIDE WHAT THE HOLDER IS
10
+ * ALLOWED TO DO.
11
+ *
12
+ * The reasoning above is only sound because the decoded values are used to
13
+ * schedule work (when to refresh) and to pick a local code path — never to
14
+ * grant anything. A claim read here is attacker-controlled input in the exact
15
+ * case that matters: anyone who can write the credential file, set the env var,
16
+ * or hand us a token over an fd can put any `scope`, `role`, `exp` or `sub` in
17
+ * it, because forging a payload requires no key when nobody checks the
18
+ * signature. "It would 401 later" is not a control if the decision was already
19
+ * made locally before any call went out.
20
+ *
21
+ * Safe: refresh timing, telemetry, diagnostics, choosing which resolver branch
22
+ * to take. Unsafe: gating a capability, trusting an identity, skipping a check,
23
+ * widening a scope. If you need a claim you can trust, verify the signature or
24
+ * ask the server.
25
+ *
26
+ * `credentialResolver.classifyTokenKind` is the one non-`exp` reader today; see
27
+ * the note there for why routing on `scope` is not an authorization decision.
28
+ */
29
+ /** Base64url-decode the middle segment of a JWT and JSON-parse it. */
30
+ export function decodeJwtPayload(token) {
31
+ if (typeof token !== 'string' || token.length === 0)
32
+ return null;
33
+ const parts = token.split('.');
34
+ if (parts.length !== 3 || !parts[1])
35
+ return null;
36
+ try {
37
+ const json = Buffer.from(parts[1], 'base64url').toString('utf8');
38
+ return JSON.parse(json);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /**
45
+ * Decode the `exp` (expiry) claim from a JWT without verifying the signature.
46
+ * @returns The `exp` value in Unix seconds, or `null` if unparseable / missing.
47
+ */
48
+ export function decodeJwtExpiry(token) {
49
+ const payload = decodeJwtPayload(token);
50
+ if (payload !== null &&
51
+ typeof payload === 'object' &&
52
+ 'exp' in payload &&
53
+ typeof payload.exp === 'number') {
54
+ return payload.exp;
55
+ }
56
+ return null;
57
+ }