@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,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credentialResolver.ts — documented credential precedence for the
|
|
3
|
+
* OverSky daemon and CLI.
|
|
4
|
+
*
|
|
5
|
+
* Roadmap: docs/daemon-auth-roadmap.md, Phase H.
|
|
6
|
+
*
|
|
7
|
+
* The resolver walks a fixed, ordered list of credential sources and
|
|
8
|
+
* returns the first non-null token along with its source name. Source
|
|
9
|
+
* names are part of the public surface — they show up in
|
|
10
|
+
* `oversky doctor`, `oversky status`, and `sky whoami`, and operators
|
|
11
|
+
* use them to triage "where did this token come from?" tickets.
|
|
12
|
+
*
|
|
13
|
+
* Precedence (high → low):
|
|
14
|
+
*
|
|
15
|
+
* 1. flag — `--token <jwt>` CLI flag (debugging)
|
|
16
|
+
* 2. env-token — `OVERSKY_TOKEN` env (CI / static; Phase I adds
|
|
17
|
+
* server-side scope=ci validation)
|
|
18
|
+
* 3. auth-helper — `OVERSKY_AUTH_HELPER` script output (rotating
|
|
19
|
+
* creds — Vault, AWS SM, internal SSO proxy)
|
|
20
|
+
* 4. fd-handoff — `OVERSKY_AUTH_FD` parent→child IPC (Electron
|
|
21
|
+
* supervisor → daemon child). Internal; never
|
|
22
|
+
* user-configured.
|
|
23
|
+
* 5. keychain — macOS Keychain (`ai.skrr.daemon`)
|
|
24
|
+
* 6. file — `~/.skrr/auth.json` (0600)
|
|
25
|
+
* 7. interactive — Browser/device-code login (TTY only, blocked
|
|
26
|
+
* under launchd via `isHeadless()`)
|
|
27
|
+
*
|
|
28
|
+
* Skip rules (Phase H.2 + H.4):
|
|
29
|
+
* - `bareMode: true` skips slots 4–7 (no platform storage, no IPC,
|
|
30
|
+
* no interactive). Bare mode demands an explicit token from env or
|
|
31
|
+
* flag — designed for reproducible CI runs and "is the daemon
|
|
32
|
+
* broken or is my config corrupted?" triage.
|
|
33
|
+
* - `storeMode: 'env-only'` skips slots 5 + 6 (and the daemon
|
|
34
|
+
* wrapper additionally disables the auth-generation.json sidecar).
|
|
35
|
+
* - `storeMode: 'keychain'` skips slot 6 (file backend).
|
|
36
|
+
* - `storeMode: 'file'` skips slot 5 (keychain backend).
|
|
37
|
+
*
|
|
38
|
+
* Backend access uses dependency-injected callbacks
|
|
39
|
+
* (`readKeychainToken`, `readFileToken`, `runInteractiveLogin`) so this
|
|
40
|
+
* module stays free of OS-specific imports and the Sky CLI can wire its
|
|
41
|
+
* own backends without dragging the daemon's `keychain.ts` into
|
|
42
|
+
* non-daemon binaries.
|
|
43
|
+
*/
|
|
44
|
+
import { type AuthHelperFailureReason } from './authHelper.js';
|
|
45
|
+
export type CredentialSource = 'flag' | 'env-token' | 'auth-helper' | 'fd-handoff' | 'keychain' | 'file' | 'interactive' | 'none';
|
|
46
|
+
/**
|
|
47
|
+
* Ordered slots tried by `CredentialResolver.resolve()`. Excludes
|
|
48
|
+
* `'none'` (only emitted when no slot produced a token). Treat this as
|
|
49
|
+
* the canonical published precedence — flipping the order is a public
|
|
50
|
+
* API change and breaks operator runbooks.
|
|
51
|
+
*/
|
|
52
|
+
export declare const CREDENTIAL_PRECEDENCE: readonly ["flag", "env-token", "auth-helper", "fd-handoff", "keychain", "file", "interactive"];
|
|
53
|
+
export type CredentialStoreMode = 'auto' | 'keychain' | 'file' | 'env-only';
|
|
54
|
+
export interface CredentialResolverOptions {
|
|
55
|
+
/** `--token <jwt>` flag value, when present on the CLI. */
|
|
56
|
+
flagToken?: string;
|
|
57
|
+
/** `--bare` mode (or `OVERSKY_BARE_MODE=1`): skips slots 4–7. */
|
|
58
|
+
bareMode?: boolean;
|
|
59
|
+
/** `config.credentials.store` value; defaults to 'auto'. */
|
|
60
|
+
storeMode?: CredentialStoreMode;
|
|
61
|
+
/**
|
|
62
|
+
* Test/explicit override for `OVERSKY_TOKEN`. Pass `undefined` to read
|
|
63
|
+
* from `process.env`; pass `null` to force-skip the slot; pass a
|
|
64
|
+
* string to use that value directly.
|
|
65
|
+
*/
|
|
66
|
+
envTokenOverride?: string | null;
|
|
67
|
+
/** Test/explicit override for `OVERSKY_AUTH_HELPER` env. Same semantics. */
|
|
68
|
+
authHelperPathOverride?: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Predicate consulted before invoking the interactive slot. The daemon
|
|
71
|
+
* wires `isHeadlessDaemon()`; the CLI may wire its own check.
|
|
72
|
+
*/
|
|
73
|
+
isHeadless?: () => boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Read the keychain backend. Returns the token string or null. The
|
|
76
|
+
* daemon wraps `keychain.readToken()` (and handles `resetAvailability`
|
|
77
|
+
* on misses) inside this callback.
|
|
78
|
+
*/
|
|
79
|
+
readKeychainToken?: () => string | null;
|
|
80
|
+
/** Read the file backend (`~/.skrr/auth.json`). */
|
|
81
|
+
readFileToken?: () => string | null;
|
|
82
|
+
/** Trigger interactive login; returns the new token or null. */
|
|
83
|
+
runInteractiveLogin?: () => Promise<string | null>;
|
|
84
|
+
/**
|
|
85
|
+
* L6.1 — workspace-trust gate for `OVERSKY_AUTH_HELPER`.
|
|
86
|
+
*
|
|
87
|
+
* Called BEFORE the helper is invoked. The daemon wires this to the
|
|
88
|
+
* `daemon/src/permissions/workspace-trust.ts` checker; tests can pass a
|
|
89
|
+
* stub directly. When this returns `{trusted: false, ...}` for a
|
|
90
|
+
* project- or local-scope helper, the resolver:
|
|
91
|
+
* 1. Logs an `auth_helper_blocked_untrusted` event via
|
|
92
|
+
* `onAuthHelperBlocked` if wired.
|
|
93
|
+
* 2. Fails CLOSED — returns a `{token: null, source: 'auth-helper'}`
|
|
94
|
+
* record with `failClosed: true` so the chain does NOT fall
|
|
95
|
+
* through to OAuth. (See L6.1 spec point 4.)
|
|
96
|
+
*
|
|
97
|
+
* Absent → no trust check is performed (backward-compat for the CLI
|
|
98
|
+
* and tests that don't care about workspace-trust). Production daemon
|
|
99
|
+
* MUST wire this.
|
|
100
|
+
*/
|
|
101
|
+
helperTrustGate?: (helperPath: string, fromEnvVar: boolean) => HelperTrustDecision;
|
|
102
|
+
/**
|
|
103
|
+
* Audit hook fired when `helperTrustGate` blocks the helper. Daemon
|
|
104
|
+
* wires this to the event-log so security-sensitive denials are
|
|
105
|
+
* persisted alongside other auth events.
|
|
106
|
+
*/
|
|
107
|
+
onAuthHelperBlocked?: (info: AuthHelperBlockedInfo) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Audit hook fired when L6.3 returns the cold-cache failure sentinel.
|
|
110
|
+
* Useful for surfacing "your helper is broken" without spamming the
|
|
111
|
+
* event-log on every read within the TTL.
|
|
112
|
+
*/
|
|
113
|
+
onAuthHelperSentinelHit?: (info: AuthHelperSentinelInfo) => void;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* L6.1 — return value of the workspace-trust callback.
|
|
117
|
+
*/
|
|
118
|
+
export interface HelperTrustDecision {
|
|
119
|
+
trusted: boolean;
|
|
120
|
+
origin: 'user' | 'project' | 'local';
|
|
121
|
+
reason?: 'no-record' | 'helper-changed';
|
|
122
|
+
}
|
|
123
|
+
export interface AuthHelperBlockedInfo {
|
|
124
|
+
helperPath: string;
|
|
125
|
+
origin: 'user' | 'project' | 'local';
|
|
126
|
+
reason: 'no-record' | 'helper-changed';
|
|
127
|
+
cwd: string;
|
|
128
|
+
}
|
|
129
|
+
export interface AuthHelperSentinelInfo {
|
|
130
|
+
helperPath: string;
|
|
131
|
+
reason: AuthHelperFailureReason;
|
|
132
|
+
cachedAt: number;
|
|
133
|
+
ttlMs: number;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Token kind discriminator (Phase I).
|
|
137
|
+
*
|
|
138
|
+
* - 'standard' — anything that should be treated as a JWT (the default
|
|
139
|
+
* daemon device-code-issued access token, refresh-derived
|
|
140
|
+
* rotations, the user's CLI session token, etc.).
|
|
141
|
+
* - 'ci' — long-lived CI token with the `osk_ci_` prefix. The
|
|
142
|
+
* daemon uses this for the bootstrap-to-daemon-JWT flow
|
|
143
|
+
* (POST /api/daemons/token), MUST NOT schedule refresh,
|
|
144
|
+
* and on auth failure exits 75 with a CI-specific error
|
|
145
|
+
* rather than writing needs-reauth.json.
|
|
146
|
+
* - 'install' — short-lived web-minted JWT with `scope=daemon-install`.
|
|
147
|
+
* The daemon exchanges it through the same /api/daemons/token
|
|
148
|
+
* bootstrap path, persists only the returned daemon
|
|
149
|
+
* credential, and never treats the install JWT as a normal
|
|
150
|
+
* session token.
|
|
151
|
+
*
|
|
152
|
+
* Detection is local-only; no server round-trip. The JWT payload read is
|
|
153
|
+
* intentionally signature-less and is only used to select the client
|
|
154
|
+
* bootstrap path. The server remains the authority when the token is
|
|
155
|
+
* exchanged at /api/daemons/token.
|
|
156
|
+
*
|
|
157
|
+
* The kill switch `OVERSKY_DISABLE_CI_TOKEN_RESOLVER=1` forces every token to
|
|
158
|
+
* `'standard'` for triage purposes.
|
|
159
|
+
*/
|
|
160
|
+
export type TokenKind = 'standard' | 'ci' | 'install';
|
|
161
|
+
/** CI-token plaintext prefix; mirrors api/server/services/Auth/daemonCiTokens.js. */
|
|
162
|
+
export declare const CI_TOKEN_PREFIX = "osk_ci_";
|
|
163
|
+
/**
|
|
164
|
+
* Classify a token by local shape. Pure function; safe to call on any string
|
|
165
|
+
* (including `null`-coerced empty). Returns `'standard'` for empty input
|
|
166
|
+
* so callers don't need to null-check before classification.
|
|
167
|
+
*
|
|
168
|
+
* Honors `OVERSKY_DISABLE_CI_TOKEN_RESOLVER=1` kill switch — when set,
|
|
169
|
+
* everything classifies as `'standard'` so the resolver can be used as
|
|
170
|
+
* a triage tool ("is the CI-token path the problem?").
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* SECURITY NOTE — this reads `scope` out of an UNVERIFIED JWT payload.
|
|
174
|
+
*
|
|
175
|
+
* That is safe here, and stays safe only under one condition: the return value
|
|
176
|
+
* selects a local code path and nothing else. `'install'` means "refresh this
|
|
177
|
+
* the install-token way"; it grants no capability, skips no check, and asserts
|
|
178
|
+
* no identity. The server still validates the token on every request, so a
|
|
179
|
+
* forged `scope: daemon-install` buys an attacker a different local branch and
|
|
180
|
+
* the same 401.
|
|
181
|
+
*
|
|
182
|
+
* The forgery is trivial — no signature is checked, so anyone who can write the
|
|
183
|
+
* credential file or set the env var can put any claim in it. That is fine
|
|
184
|
+
* while the only consequence is routing. It stops being fine the moment a
|
|
185
|
+
* caller treats `TokenKind` as a permission. Do not let this value gate a
|
|
186
|
+
* capability, and do not add a branch here that returns a kind meaning "more
|
|
187
|
+
* privileged".
|
|
188
|
+
*
|
|
189
|
+
* See `jwtUtils.ts` for the general rule.
|
|
190
|
+
*/
|
|
191
|
+
export declare function classifyTokenKind(token: string | null | undefined): TokenKind;
|
|
192
|
+
export interface ResolvedCredential {
|
|
193
|
+
token: string | null;
|
|
194
|
+
source: CredentialSource;
|
|
195
|
+
/**
|
|
196
|
+
* Phase I — discriminator the daemon uses to branch on refresh
|
|
197
|
+
* scheduling, needs-reauth handling, and the bootstrap-to-daemon-JWT
|
|
198
|
+
* flow. Always `'standard'` when `token` is null.
|
|
199
|
+
*/
|
|
200
|
+
kind: TokenKind;
|
|
201
|
+
}
|
|
202
|
+
/** @internal test-only — drop the helper cache between tests. */
|
|
203
|
+
export declare function __resetCredentialResolverForTest(): void;
|
|
204
|
+
/**
|
|
205
|
+
* L6.3 — typed error thrown when the cold-cache failure sentinel is in
|
|
206
|
+
* effect. Callers (daemon refresh path, CLI) can catch this specifically
|
|
207
|
+
* to render "auth helper unavailable: <reason>" without falling through
|
|
208
|
+
* to OAuth or device-code retry.
|
|
209
|
+
*/
|
|
210
|
+
export declare class AuthHelperUnavailableError extends Error {
|
|
211
|
+
readonly reason: AuthHelperFailureReason;
|
|
212
|
+
readonly cachedAt: number;
|
|
213
|
+
constructor(reason: AuthHelperFailureReason, cachedAt: number);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* L6.1 — typed error thrown when the workspace-trust gate blocks a
|
|
217
|
+
* project- or local-scope helper. Surfaces the actionable next step
|
|
218
|
+
* (`oversky trust accept`) to the user; the daemon catches and presents
|
|
219
|
+
* this without falling through to OAuth.
|
|
220
|
+
*/
|
|
221
|
+
export declare class AuthHelperUntrustedError extends Error {
|
|
222
|
+
readonly origin: 'user' | 'project' | 'local';
|
|
223
|
+
readonly reason: 'no-record' | 'helper-changed';
|
|
224
|
+
readonly helperPath: string;
|
|
225
|
+
constructor(helperPath: string, origin: 'user' | 'project' | 'local', reason: 'no-record' | 'helper-changed');
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* H3-6 — public invalidation seam for the `OVERSKY_AUTH_HELPER` result
|
|
229
|
+
* cache. 401 handlers call this right after receiving a 401 from the
|
|
230
|
+
* server so the NEXT credential resolve re-invokes the helper to mint
|
|
231
|
+
* a fresh token instead of serving the stale (cached) one for up to
|
|
232
|
+
* `OVERSKY_AUTH_HELPER_TTL_MS`.
|
|
233
|
+
*
|
|
234
|
+
* Without this seam, a rotated secret at the helper's source (Vault,
|
|
235
|
+
* AWS Secrets Manager, etc.) could take up to 5 minutes to propagate
|
|
236
|
+
* to OverSky even when the server is actively rejecting the old
|
|
237
|
+
* value. The helper is the only slot in the precedence chain that
|
|
238
|
+
* naturally caches; keychain/file reads are already one-shot per
|
|
239
|
+
* resolve.
|
|
240
|
+
*
|
|
241
|
+
* L6.3 — also clears the cold-cache failure sentinel. A 401 means the
|
|
242
|
+
* server is rejecting whatever we sent, which usually means the helper
|
|
243
|
+
* needs to retry from scratch — even when the LAST helper attempt
|
|
244
|
+
* failed and seeded the sentinel. Without this, a transient helper
|
|
245
|
+
* failure during the same TTL window as a server-issued 401 would lock
|
|
246
|
+
* the daemon out for the full TTL.
|
|
247
|
+
*
|
|
248
|
+
* Idempotent: calling this on an already-empty cache is a no-op.
|
|
249
|
+
*/
|
|
250
|
+
export declare function invalidateAuthHelperCache(): void;
|
|
251
|
+
export declare class CredentialResolver {
|
|
252
|
+
private readonly opts;
|
|
253
|
+
constructor(opts?: CredentialResolverOptions);
|
|
254
|
+
/**
|
|
255
|
+
* Walk the precedence chain and return the first non-null token.
|
|
256
|
+
* Returns `{ token: null, source: 'none' }` when nothing matches.
|
|
257
|
+
*
|
|
258
|
+
* Fail-closed exceptions from the auth-helper slot
|
|
259
|
+
* (`AuthHelperUntrustedError`, `AuthHelperUnavailableError`) propagate
|
|
260
|
+
* out — they intentionally PREVENT fall-through to OAuth. Callers
|
|
261
|
+
* that want soft-fall-through behavior must catch explicitly. See
|
|
262
|
+
* L6.1 spec point 4 and L6.3.
|
|
263
|
+
*/
|
|
264
|
+
resolve(): Promise<ResolvedCredential>;
|
|
265
|
+
/**
|
|
266
|
+
* Test seam: invoke a single slot in isolation. Production code should
|
|
267
|
+
* always go through `resolve()` so the chain ordering is enforced.
|
|
268
|
+
*/
|
|
269
|
+
tryResolveSlot(source: Exclude<CredentialSource, 'none'>): Promise<string | null>;
|
|
270
|
+
private shouldSkip;
|
|
271
|
+
private tryResolveFlag;
|
|
272
|
+
private tryResolveEnvToken;
|
|
273
|
+
private tryResolveAuthHelper;
|
|
274
|
+
private tryResolveFdHandoff;
|
|
275
|
+
private tryResolveKeychain;
|
|
276
|
+
private tryResolveFile;
|
|
277
|
+
private tryResolveInteractive;
|
|
278
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credentialResolver.ts — documented credential precedence for the
|
|
3
|
+
* OverSky daemon and CLI.
|
|
4
|
+
*
|
|
5
|
+
* Roadmap: docs/daemon-auth-roadmap.md, Phase H.
|
|
6
|
+
*
|
|
7
|
+
* The resolver walks a fixed, ordered list of credential sources and
|
|
8
|
+
* returns the first non-null token along with its source name. Source
|
|
9
|
+
* names are part of the public surface — they show up in
|
|
10
|
+
* `oversky doctor`, `oversky status`, and `sky whoami`, and operators
|
|
11
|
+
* use them to triage "where did this token come from?" tickets.
|
|
12
|
+
*
|
|
13
|
+
* Precedence (high → low):
|
|
14
|
+
*
|
|
15
|
+
* 1. flag — `--token <jwt>` CLI flag (debugging)
|
|
16
|
+
* 2. env-token — `OVERSKY_TOKEN` env (CI / static; Phase I adds
|
|
17
|
+
* server-side scope=ci validation)
|
|
18
|
+
* 3. auth-helper — `OVERSKY_AUTH_HELPER` script output (rotating
|
|
19
|
+
* creds — Vault, AWS SM, internal SSO proxy)
|
|
20
|
+
* 4. fd-handoff — `OVERSKY_AUTH_FD` parent→child IPC (Electron
|
|
21
|
+
* supervisor → daemon child). Internal; never
|
|
22
|
+
* user-configured.
|
|
23
|
+
* 5. keychain — macOS Keychain (`ai.skrr.daemon`)
|
|
24
|
+
* 6. file — `~/.skrr/auth.json` (0600)
|
|
25
|
+
* 7. interactive — Browser/device-code login (TTY only, blocked
|
|
26
|
+
* under launchd via `isHeadless()`)
|
|
27
|
+
*
|
|
28
|
+
* Skip rules (Phase H.2 + H.4):
|
|
29
|
+
* - `bareMode: true` skips slots 4–7 (no platform storage, no IPC,
|
|
30
|
+
* no interactive). Bare mode demands an explicit token from env or
|
|
31
|
+
* flag — designed for reproducible CI runs and "is the daemon
|
|
32
|
+
* broken or is my config corrupted?" triage.
|
|
33
|
+
* - `storeMode: 'env-only'` skips slots 5 + 6 (and the daemon
|
|
34
|
+
* wrapper additionally disables the auth-generation.json sidecar).
|
|
35
|
+
* - `storeMode: 'keychain'` skips slot 6 (file backend).
|
|
36
|
+
* - `storeMode: 'file'` skips slot 5 (keychain backend).
|
|
37
|
+
*
|
|
38
|
+
* Backend access uses dependency-injected callbacks
|
|
39
|
+
* (`readKeychainToken`, `readFileToken`, `runInteractiveLogin`) so this
|
|
40
|
+
* module stays free of OS-specific imports and the Sky CLI can wire its
|
|
41
|
+
* own backends without dragging the daemon's `keychain.ts` into
|
|
42
|
+
* non-daemon binaries.
|
|
43
|
+
*/
|
|
44
|
+
import { invokeAuthHelper, getDefaultHelperTtlMs, getLastAuthHelperFailureReason, SENTINEL_TRIGGERING_FAILURES, } from './authHelper.js';
|
|
45
|
+
import { readAuthFromFd } from './fdToken.js';
|
|
46
|
+
import { decodeJwtPayload } from './jwtUtils.js';
|
|
47
|
+
import { getAuthLogger } from './runtime.js';
|
|
48
|
+
/**
|
|
49
|
+
* Ordered slots tried by `CredentialResolver.resolve()`. Excludes
|
|
50
|
+
* `'none'` (only emitted when no slot produced a token). Treat this as
|
|
51
|
+
* the canonical published precedence — flipping the order is a public
|
|
52
|
+
* API change and breaks operator runbooks.
|
|
53
|
+
*/
|
|
54
|
+
export const CREDENTIAL_PRECEDENCE = [
|
|
55
|
+
'flag',
|
|
56
|
+
'env-token',
|
|
57
|
+
'auth-helper',
|
|
58
|
+
'fd-handoff',
|
|
59
|
+
'keychain',
|
|
60
|
+
'file',
|
|
61
|
+
'interactive',
|
|
62
|
+
];
|
|
63
|
+
/** CI-token plaintext prefix; mirrors api/server/services/Auth/daemonCiTokens.js. */
|
|
64
|
+
export const CI_TOKEN_PREFIX = 'osk_ci_';
|
|
65
|
+
/**
|
|
66
|
+
* Classify a token by local shape. Pure function; safe to call on any string
|
|
67
|
+
* (including `null`-coerced empty). Returns `'standard'` for empty input
|
|
68
|
+
* so callers don't need to null-check before classification.
|
|
69
|
+
*
|
|
70
|
+
* Honors `OVERSKY_DISABLE_CI_TOKEN_RESOLVER=1` kill switch — when set,
|
|
71
|
+
* everything classifies as `'standard'` so the resolver can be used as
|
|
72
|
+
* a triage tool ("is the CI-token path the problem?").
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* SECURITY NOTE — this reads `scope` out of an UNVERIFIED JWT payload.
|
|
76
|
+
*
|
|
77
|
+
* That is safe here, and stays safe only under one condition: the return value
|
|
78
|
+
* selects a local code path and nothing else. `'install'` means "refresh this
|
|
79
|
+
* the install-token way"; it grants no capability, skips no check, and asserts
|
|
80
|
+
* no identity. The server still validates the token on every request, so a
|
|
81
|
+
* forged `scope: daemon-install` buys an attacker a different local branch and
|
|
82
|
+
* the same 401.
|
|
83
|
+
*
|
|
84
|
+
* The forgery is trivial — no signature is checked, so anyone who can write the
|
|
85
|
+
* credential file or set the env var can put any claim in it. That is fine
|
|
86
|
+
* while the only consequence is routing. It stops being fine the moment a
|
|
87
|
+
* caller treats `TokenKind` as a permission. Do not let this value gate a
|
|
88
|
+
* capability, and do not add a branch here that returns a kind meaning "more
|
|
89
|
+
* privileged".
|
|
90
|
+
*
|
|
91
|
+
* See `jwtUtils.ts` for the general rule.
|
|
92
|
+
*/
|
|
93
|
+
export function classifyTokenKind(token) {
|
|
94
|
+
if (process.env.OVERSKY_DISABLE_CI_TOKEN_RESOLVER === '1')
|
|
95
|
+
return 'standard';
|
|
96
|
+
if (typeof token !== 'string' || token.length === 0)
|
|
97
|
+
return 'standard';
|
|
98
|
+
if (token.startsWith(CI_TOKEN_PREFIX))
|
|
99
|
+
return 'ci';
|
|
100
|
+
const payload = decodeJwtPayload(token);
|
|
101
|
+
if (payload &&
|
|
102
|
+
typeof payload === 'object' &&
|
|
103
|
+
!Array.isArray(payload) &&
|
|
104
|
+
payload.scope === 'daemon-install') {
|
|
105
|
+
return 'install';
|
|
106
|
+
}
|
|
107
|
+
return 'standard';
|
|
108
|
+
}
|
|
109
|
+
let _helperCache = null;
|
|
110
|
+
/** @internal test-only — drop the helper cache between tests. */
|
|
111
|
+
export function __resetCredentialResolverForTest() {
|
|
112
|
+
_helperCache = null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* L6.3 — typed error thrown when the cold-cache failure sentinel is in
|
|
116
|
+
* effect. Callers (daemon refresh path, CLI) can catch this specifically
|
|
117
|
+
* to render "auth helper unavailable: <reason>" without falling through
|
|
118
|
+
* to OAuth or device-code retry.
|
|
119
|
+
*/
|
|
120
|
+
export class AuthHelperUnavailableError extends Error {
|
|
121
|
+
reason;
|
|
122
|
+
cachedAt;
|
|
123
|
+
constructor(reason, cachedAt) {
|
|
124
|
+
super(`auth helper unavailable: ${reason}`);
|
|
125
|
+
this.name = 'AuthHelperUnavailableError';
|
|
126
|
+
this.reason = reason;
|
|
127
|
+
this.cachedAt = cachedAt;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* L6.1 — typed error thrown when the workspace-trust gate blocks a
|
|
132
|
+
* project- or local-scope helper. Surfaces the actionable next step
|
|
133
|
+
* (`oversky trust accept`) to the user; the daemon catches and presents
|
|
134
|
+
* this without falling through to OAuth.
|
|
135
|
+
*/
|
|
136
|
+
export class AuthHelperUntrustedError extends Error {
|
|
137
|
+
origin;
|
|
138
|
+
reason;
|
|
139
|
+
helperPath;
|
|
140
|
+
constructor(helperPath, origin, reason) {
|
|
141
|
+
super(`OVERSKY_AUTH_HELPER (${origin}-scope) is not trusted for this workspace ` +
|
|
142
|
+
`(reason: ${reason}). Run \`oversky trust accept\` to allow it.`);
|
|
143
|
+
this.name = 'AuthHelperUntrustedError';
|
|
144
|
+
this.origin = origin;
|
|
145
|
+
this.reason = reason;
|
|
146
|
+
this.helperPath = helperPath;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* H3-6 — public invalidation seam for the `OVERSKY_AUTH_HELPER` result
|
|
151
|
+
* cache. 401 handlers call this right after receiving a 401 from the
|
|
152
|
+
* server so the NEXT credential resolve re-invokes the helper to mint
|
|
153
|
+
* a fresh token instead of serving the stale (cached) one for up to
|
|
154
|
+
* `OVERSKY_AUTH_HELPER_TTL_MS`.
|
|
155
|
+
*
|
|
156
|
+
* Without this seam, a rotated secret at the helper's source (Vault,
|
|
157
|
+
* AWS Secrets Manager, etc.) could take up to 5 minutes to propagate
|
|
158
|
+
* to OverSky even when the server is actively rejecting the old
|
|
159
|
+
* value. The helper is the only slot in the precedence chain that
|
|
160
|
+
* naturally caches; keychain/file reads are already one-shot per
|
|
161
|
+
* resolve.
|
|
162
|
+
*
|
|
163
|
+
* L6.3 — also clears the cold-cache failure sentinel. A 401 means the
|
|
164
|
+
* server is rejecting whatever we sent, which usually means the helper
|
|
165
|
+
* needs to retry from scratch — even when the LAST helper attempt
|
|
166
|
+
* failed and seeded the sentinel. Without this, a transient helper
|
|
167
|
+
* failure during the same TTL window as a server-issued 401 would lock
|
|
168
|
+
* the daemon out for the full TTL.
|
|
169
|
+
*
|
|
170
|
+
* Idempotent: calling this on an already-empty cache is a no-op.
|
|
171
|
+
*/
|
|
172
|
+
export function invalidateAuthHelperCache() {
|
|
173
|
+
_helperCache = null;
|
|
174
|
+
}
|
|
175
|
+
export class CredentialResolver {
|
|
176
|
+
opts;
|
|
177
|
+
constructor(opts = {}) {
|
|
178
|
+
this.opts = opts;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Walk the precedence chain and return the first non-null token.
|
|
182
|
+
* Returns `{ token: null, source: 'none' }` when nothing matches.
|
|
183
|
+
*
|
|
184
|
+
* Fail-closed exceptions from the auth-helper slot
|
|
185
|
+
* (`AuthHelperUntrustedError`, `AuthHelperUnavailableError`) propagate
|
|
186
|
+
* out — they intentionally PREVENT fall-through to OAuth. Callers
|
|
187
|
+
* that want soft-fall-through behavior must catch explicitly. See
|
|
188
|
+
* L6.1 spec point 4 and L6.3.
|
|
189
|
+
*/
|
|
190
|
+
async resolve() {
|
|
191
|
+
for (const source of CREDENTIAL_PRECEDENCE) {
|
|
192
|
+
if (this.shouldSkip(source))
|
|
193
|
+
continue;
|
|
194
|
+
const token = await this.tryResolveSlot(source);
|
|
195
|
+
if (token) {
|
|
196
|
+
return { token, source, kind: classifyTokenKind(token) };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { token: null, source: 'none', kind: 'standard' };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Test seam: invoke a single slot in isolation. Production code should
|
|
203
|
+
* always go through `resolve()` so the chain ordering is enforced.
|
|
204
|
+
*/
|
|
205
|
+
async tryResolveSlot(source) {
|
|
206
|
+
switch (source) {
|
|
207
|
+
case 'flag':
|
|
208
|
+
return this.tryResolveFlag();
|
|
209
|
+
case 'env-token':
|
|
210
|
+
return this.tryResolveEnvToken();
|
|
211
|
+
case 'auth-helper':
|
|
212
|
+
return this.tryResolveAuthHelper();
|
|
213
|
+
case 'fd-handoff':
|
|
214
|
+
return this.tryResolveFdHandoff();
|
|
215
|
+
case 'keychain':
|
|
216
|
+
return this.tryResolveKeychain();
|
|
217
|
+
case 'file':
|
|
218
|
+
return this.tryResolveFile();
|
|
219
|
+
case 'interactive':
|
|
220
|
+
return this.tryResolveInteractive();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
shouldSkip(source) {
|
|
224
|
+
const { bareMode } = this.opts;
|
|
225
|
+
const storeMode = this.opts.storeMode ?? 'auto';
|
|
226
|
+
if (bareMode &&
|
|
227
|
+
(source === 'fd-handoff' ||
|
|
228
|
+
source === 'keychain' ||
|
|
229
|
+
source === 'file' ||
|
|
230
|
+
source === 'interactive')) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (storeMode === 'env-only' && (source === 'keychain' || source === 'file')) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (storeMode === 'keychain' && source === 'file')
|
|
237
|
+
return true;
|
|
238
|
+
if (storeMode === 'file' && source === 'keychain')
|
|
239
|
+
return true;
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
// ------------------------------------------------------------------
|
|
243
|
+
// Slot implementations
|
|
244
|
+
// ------------------------------------------------------------------
|
|
245
|
+
tryResolveFlag() {
|
|
246
|
+
const v = this.opts.flagToken;
|
|
247
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
248
|
+
}
|
|
249
|
+
tryResolveEnvToken() {
|
|
250
|
+
if (this.opts.envTokenOverride === null)
|
|
251
|
+
return null; // explicit force-skip
|
|
252
|
+
if (typeof this.opts.envTokenOverride === 'string') {
|
|
253
|
+
return this.opts.envTokenOverride.length > 0 ? this.opts.envTokenOverride : null;
|
|
254
|
+
}
|
|
255
|
+
const v = process.env.OVERSKY_TOKEN;
|
|
256
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
257
|
+
}
|
|
258
|
+
async tryResolveAuthHelper() {
|
|
259
|
+
let helperPath;
|
|
260
|
+
let fromEnvVar = false;
|
|
261
|
+
if (this.opts.authHelperPathOverride === null)
|
|
262
|
+
return null;
|
|
263
|
+
if (typeof this.opts.authHelperPathOverride === 'string') {
|
|
264
|
+
helperPath =
|
|
265
|
+
this.opts.authHelperPathOverride.length > 0 ? this.opts.authHelperPathOverride : null;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
helperPath = process.env.OVERSKY_AUTH_HELPER ?? null;
|
|
269
|
+
fromEnvVar = helperPath !== null;
|
|
270
|
+
}
|
|
271
|
+
if (!helperPath)
|
|
272
|
+
return null;
|
|
273
|
+
// ---------------------------------------------------------------
|
|
274
|
+
// L6.3 — sentinel served BEFORE re-spawning. Failure cache prevents
|
|
275
|
+
// silent fall-through to OAuth within the TTL window.
|
|
276
|
+
// ---------------------------------------------------------------
|
|
277
|
+
if (_helperCache && Date.now() < _helperCache.expiresAt) {
|
|
278
|
+
if (_helperCache.kind === 'success') {
|
|
279
|
+
return _helperCache.token;
|
|
280
|
+
}
|
|
281
|
+
// Failed sentinel still warm — fail closed. The slot returns
|
|
282
|
+
// a fail-closed signal via thrown error so the chain doesn't
|
|
283
|
+
// continue to keychain/file/OAuth.
|
|
284
|
+
this.opts.onAuthHelperSentinelHit?.({
|
|
285
|
+
helperPath,
|
|
286
|
+
reason: _helperCache.reason,
|
|
287
|
+
cachedAt: _helperCache.cachedAt,
|
|
288
|
+
ttlMs: _helperCache.expiresAt - _helperCache.cachedAt,
|
|
289
|
+
});
|
|
290
|
+
throw new AuthHelperUnavailableError(_helperCache.reason, _helperCache.cachedAt);
|
|
291
|
+
}
|
|
292
|
+
// ---------------------------------------------------------------
|
|
293
|
+
// L6.1 — workspace-trust gate. Project- and local-scope helpers
|
|
294
|
+
// must have an accepted trust record for the cwd. User-scope
|
|
295
|
+
// helpers (env var, ~/.skrr/) bypass the check.
|
|
296
|
+
//
|
|
297
|
+
// Trust check runs BEFORE invocation so we never even spawn an
|
|
298
|
+
// untrusted script. Failed trust = fail-closed throw, never
|
|
299
|
+
// falls through to OAuth.
|
|
300
|
+
// ---------------------------------------------------------------
|
|
301
|
+
if (this.opts.helperTrustGate) {
|
|
302
|
+
const decision = this.opts.helperTrustGate(helperPath, fromEnvVar);
|
|
303
|
+
if (!decision.trusted) {
|
|
304
|
+
const reason = decision.reason ?? 'no-record';
|
|
305
|
+
this.opts.onAuthHelperBlocked?.({
|
|
306
|
+
helperPath,
|
|
307
|
+
origin: decision.origin,
|
|
308
|
+
reason,
|
|
309
|
+
cwd: process.cwd(),
|
|
310
|
+
});
|
|
311
|
+
throw new AuthHelperUntrustedError(helperPath, decision.origin, reason);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const result = await invokeAuthHelper(helperPath);
|
|
315
|
+
if (result) {
|
|
316
|
+
const ttlMs = getDefaultHelperTtlMs();
|
|
317
|
+
const expiresAt = result.expiresAt ?? Date.now() + ttlMs;
|
|
318
|
+
_helperCache = { kind: 'success', token: result.token, expiresAt };
|
|
319
|
+
return result.token;
|
|
320
|
+
}
|
|
321
|
+
// ---------------------------------------------------------------
|
|
322
|
+
// L6.3 — invocation failed. Distinguish two failure classes:
|
|
323
|
+
//
|
|
324
|
+
// - Validation failures ('disabled', 'invalid-path',
|
|
325
|
+
// 'not-executable', null) — helper isn't really wired up.
|
|
326
|
+
// Fall through to next slot. NO sentinel.
|
|
327
|
+
//
|
|
328
|
+
// - Execution failures ('timeout', 'non-zero-exit', 'oversized',
|
|
329
|
+
// 'empty-output', 'parse-error', 'spawn-error') — the helper
|
|
330
|
+
// was wired up but couldn't deliver a token. Cache the
|
|
331
|
+
// sentinel and fail closed for the TTL window. This matches
|
|
332
|
+
// CC's `' '` literal sentinel at CC-A-34.
|
|
333
|
+
// ---------------------------------------------------------------
|
|
334
|
+
const reason = getLastAuthHelperFailureReason();
|
|
335
|
+
if (reason && SENTINEL_TRIGGERING_FAILURES.has(reason)) {
|
|
336
|
+
const ttlMs = getDefaultHelperTtlMs();
|
|
337
|
+
const cachedAt = Date.now();
|
|
338
|
+
_helperCache = {
|
|
339
|
+
kind: 'failed',
|
|
340
|
+
reason,
|
|
341
|
+
cachedAt,
|
|
342
|
+
expiresAt: cachedAt + ttlMs,
|
|
343
|
+
};
|
|
344
|
+
this.opts.onAuthHelperSentinelHit?.({
|
|
345
|
+
helperPath,
|
|
346
|
+
reason,
|
|
347
|
+
cachedAt,
|
|
348
|
+
ttlMs,
|
|
349
|
+
});
|
|
350
|
+
throw new AuthHelperUnavailableError(reason, cachedAt);
|
|
351
|
+
}
|
|
352
|
+
// Validation failure — no sentinel; fall through.
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
async tryResolveFdHandoff() {
|
|
356
|
+
// FD handoff is daemon-only; CLI never has OVERSKY_AUTH_FD set, so
|
|
357
|
+
// the env check below makes this a near-zero-cost slot for CLI
|
|
358
|
+
// processes. The fdToken module does its own kill-switch check
|
|
359
|
+
// (`fdTokenHandoff` killSwitch) and bounded read.
|
|
360
|
+
if (!process.env.OVERSKY_AUTH_FD)
|
|
361
|
+
return null;
|
|
362
|
+
try {
|
|
363
|
+
const payload = await readAuthFromFd();
|
|
364
|
+
if (!payload)
|
|
365
|
+
return null;
|
|
366
|
+
if (payload.accessExpiresAt && Date.now() > payload.accessExpiresAt) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return payload.accessToken || null;
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
getAuthLogger().warn(`[credentialResolver] FD handoff read failed: ${formatErr(err)}`);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
tryResolveKeychain() {
|
|
377
|
+
if (!this.opts.readKeychainToken)
|
|
378
|
+
return null;
|
|
379
|
+
try {
|
|
380
|
+
return this.opts.readKeychainToken();
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
getAuthLogger().warn(`[credentialResolver] keychain read failed: ${formatErr(err)}`);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
tryResolveFile() {
|
|
388
|
+
if (!this.opts.readFileToken)
|
|
389
|
+
return null;
|
|
390
|
+
try {
|
|
391
|
+
return this.opts.readFileToken();
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
getAuthLogger().warn(`[credentialResolver] file read failed: ${formatErr(err)}`);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async tryResolveInteractive() {
|
|
399
|
+
if (!this.opts.runInteractiveLogin)
|
|
400
|
+
return null;
|
|
401
|
+
if (this.opts.isHeadless?.())
|
|
402
|
+
return null;
|
|
403
|
+
try {
|
|
404
|
+
return await this.opts.runInteractiveLogin();
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
getAuthLogger().warn(`[credentialResolver] interactive login failed: ${formatErr(err)}`);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function formatErr(err) {
|
|
413
|
+
return err instanceof Error ? err.message : String(err);
|
|
414
|
+
}
|