@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.
- package/README.md +21 -0
- package/dist/cjs/aead.d.ts +24 -0
- package/dist/cjs/aead.js +65 -0
- package/dist/cjs/authExpiredDetector.d.ts +65 -0
- package/dist/cjs/authExpiredDetector.js +89 -0
- package/dist/cjs/authHelper.d.ts +51 -0
- package/dist/cjs/authHelper.js +321 -0
- package/dist/cjs/credentialEnvelope.d.ts +85 -0
- package/dist/cjs/credentialEnvelope.js +230 -0
- package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/cjs/credentialEnvelopeBridge.js +637 -0
- package/dist/cjs/credentialResolver.d.ts +278 -0
- package/dist/cjs/credentialResolver.js +423 -0
- package/dist/cjs/daemonPair.d.ts +172 -0
- package/dist/cjs/daemonPair.js +495 -0
- package/dist/cjs/daemonScopes.d.ts +74 -0
- package/dist/cjs/daemonScopes.js +84 -0
- package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
- package/dist/cjs/deviceIdentityBridge.js +591 -0
- package/dist/cjs/deviceKey.d.ts +167 -0
- package/dist/cjs/deviceKey.js +332 -0
- package/dist/cjs/fdToken.d.ts +17 -0
- package/dist/cjs/fdToken.js +238 -0
- package/dist/cjs/harnessTrust.d.ts +72 -0
- package/dist/cjs/harnessTrust.js +103 -0
- package/dist/cjs/headers-helper.d.ts +25 -0
- package/dist/cjs/headers-helper.js +323 -0
- package/dist/cjs/index.d.ts +37 -0
- package/dist/cjs/index.js +305 -0
- package/dist/cjs/jwtUtils.d.ts +35 -0
- package/dist/cjs/jwtUtils.js +61 -0
- package/dist/cjs/kek/index.d.ts +89 -0
- package/dist/cjs/kek/index.js +208 -0
- package/dist/cjs/kek/linux.d.ts +92 -0
- package/dist/cjs/kek/linux.js +596 -0
- package/dist/cjs/kek/macos.d.ts +112 -0
- package/dist/cjs/kek/macos.js +599 -0
- package/dist/cjs/kek/types.d.ts +111 -0
- package/dist/cjs/kek/types.js +43 -0
- package/dist/cjs/kek/windows.d.ts +71 -0
- package/dist/cjs/kek/windows.js +350 -0
- package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
- package/dist/cjs/kek/zeroize-registry.js +63 -0
- package/dist/cjs/legacyStatePreflight.d.ts +67 -0
- package/dist/cjs/legacyStatePreflight.js +78 -0
- package/dist/cjs/localIdentity.d.ts +54 -0
- package/dist/cjs/localIdentity.js +57 -0
- package/dist/cjs/loginLocalhost.d.ts +115 -0
- package/dist/cjs/loginLocalhost.js +368 -0
- package/dist/cjs/loginWithLocalhost.d.ts +131 -0
- package/dist/cjs/loginWithLocalhost.js +359 -0
- package/dist/cjs/machineId.d.ts +35 -0
- package/dist/cjs/machineId.js +239 -0
- package/dist/cjs/messages.d.ts +9 -0
- package/dist/cjs/messages.js +44 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pkce.d.ts +62 -0
- package/dist/cjs/pkce.js +158 -0
- package/dist/cjs/recoveryCode.d.ts +61 -0
- package/dist/cjs/recoveryCode.js +213 -0
- package/dist/cjs/refresh.d.ts +215 -0
- package/dist/cjs/refresh.js +877 -0
- package/dist/cjs/refreshClassification.d.ts +53 -0
- package/dist/cjs/refreshClassification.js +105 -0
- package/dist/cjs/refreshScheduler.d.ts +53 -0
- package/dist/cjs/refreshScheduler.js +332 -0
- package/dist/cjs/runtime.d.ts +149 -0
- package/dist/cjs/runtime.js +136 -0
- package/dist/cjs/spawnEnv.d.ts +76 -0
- package/dist/cjs/spawnEnv.js +153 -0
- package/dist/cjs/ttlParser.d.ts +54 -0
- package/dist/cjs/ttlParser.js +87 -0
- package/dist/cjs/types.d.ts +91 -0
- package/dist/cjs/types.js +26 -0
- package/dist/esm/aead.d.ts +24 -0
- package/dist/esm/aead.js +57 -0
- package/dist/esm/authExpiredDetector.d.ts +65 -0
- package/dist/esm/authExpiredDetector.js +85 -0
- package/dist/esm/authHelper.d.ts +51 -0
- package/dist/esm/authHelper.js +311 -0
- package/dist/esm/credentialEnvelope.d.ts +85 -0
- package/dist/esm/credentialEnvelope.js +213 -0
- package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/esm/credentialEnvelopeBridge.js +620 -0
- package/dist/esm/credentialResolver.d.ts +278 -0
- package/dist/esm/credentialResolver.js +414 -0
- package/dist/esm/daemonPair.d.ts +172 -0
- package/dist/esm/daemonPair.js +476 -0
- package/dist/esm/daemonScopes.d.ts +74 -0
- package/dist/esm/daemonScopes.js +80 -0
- package/dist/esm/deviceIdentityBridge.d.ts +113 -0
- package/dist/esm/deviceIdentityBridge.js +572 -0
- package/dist/esm/deviceKey.d.ts +167 -0
- package/dist/esm/deviceKey.js +320 -0
- package/dist/esm/fdToken.d.ts +17 -0
- package/dist/esm/fdToken.js +231 -0
- package/dist/esm/harnessTrust.d.ts +72 -0
- package/dist/esm/harnessTrust.js +97 -0
- package/dist/esm/headers-helper.d.ts +25 -0
- package/dist/esm/headers-helper.js +316 -0
- package/dist/esm/index.d.ts +37 -0
- package/dist/esm/index.js +144 -0
- package/dist/esm/jwtUtils.d.ts +35 -0
- package/dist/esm/jwtUtils.js +57 -0
- package/dist/esm/kek/index.d.ts +89 -0
- package/dist/esm/kek/index.js +162 -0
- package/dist/esm/kek/linux.d.ts +92 -0
- package/dist/esm/kek/linux.js +585 -0
- package/dist/esm/kek/macos.d.ts +112 -0
- package/dist/esm/kek/macos.js +586 -0
- package/dist/esm/kek/types.d.ts +111 -0
- package/dist/esm/kek/types.js +39 -0
- package/dist/esm/kek/windows.d.ts +71 -0
- package/dist/esm/kek/windows.js +344 -0
- package/dist/esm/kek/zeroize-registry.d.ts +44 -0
- package/dist/esm/kek/zeroize-registry.js +58 -0
- package/dist/esm/legacyStatePreflight.d.ts +67 -0
- package/dist/esm/legacyStatePreflight.js +71 -0
- package/dist/esm/localIdentity.d.ts +54 -0
- package/dist/esm/localIdentity.js +54 -0
- package/dist/esm/loginLocalhost.d.ts +115 -0
- package/dist/esm/loginLocalhost.js +355 -0
- package/dist/esm/loginWithLocalhost.d.ts +131 -0
- package/dist/esm/loginWithLocalhost.js +353 -0
- package/dist/esm/machineId.d.ts +35 -0
- package/dist/esm/machineId.js +231 -0
- package/dist/esm/messages.d.ts +9 -0
- package/dist/esm/messages.js +40 -0
- package/dist/esm/pkce.d.ts +62 -0
- package/dist/esm/pkce.js +148 -0
- package/dist/esm/recoveryCode.d.ts +61 -0
- package/dist/esm/recoveryCode.js +207 -0
- package/dist/esm/refresh.d.ts +215 -0
- package/dist/esm/refresh.js +863 -0
- package/dist/esm/refreshClassification.d.ts +53 -0
- package/dist/esm/refreshClassification.js +106 -0
- package/dist/esm/refreshScheduler.d.ts +53 -0
- package/dist/esm/refreshScheduler.js +329 -0
- package/dist/esm/runtime.d.ts +149 -0
- package/dist/esm/runtime.js +121 -0
- package/dist/esm/spawnEnv.d.ts +76 -0
- package/dist/esm/spawnEnv.js +149 -0
- package/dist/esm/ttlParser.d.ts +54 -0
- package/dist/esm/ttlParser.js +83 -0
- package/dist/esm/types.d.ts +91 -0
- package/dist/esm/types.js +21 -0
- 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
|
+
}
|