-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfind.ts
More file actions
135 lines (129 loc) · 5 KB
/
find.ts
File metadata and controls
135 lines (129 loc) · 5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* @file Resolve a secret from the canonical fleet precedence order: process env
* → OS keychain. Returns the value + the source it came from so the caller
* can log which slot won. Why a helper instead of inlining: every consumer of
* the fleet's Socket API token does the same dance — check
* `process.env.SOCKET_API_TOKEN`, fall back to a legacy env-var alias, fall
* back to `readSecret({...})` from the keychain. Tools drift on the exact
* order and on which aliases they consider. This helper centralizes the
* precedence so a token check in socket-cli, socket-mcp, depot, and ad-hoc
* scripts all behave the same way. Not included: `.env` file parsing. Hosts
* that want `.env` support should pre-populate `process.env` from `.env`
* themselves (e.g. with `dotenv.config()` at process start). Folding `.env`
* reads into this helper would couple the secrets API to a config-file parser
* that's better lived at the application boundary. Prompt minimization: every
* keychain read short-circuits through the process-scoped cache in
* `./_internal.ts`. So calling `resolve` multiple times in one process spawns
* at most one `security` (macOS) / `secret-tool` (Linux) / `powershell`
* (Windows) per `{service, account}` pair. Combined with macOS's `-A -T ''`
* ACL (set by `writeSecret`), this means: at most one Keychain auth prompt
* across a process's entire lifetime — and zero prompts when the env-var path
* covers the read.
*/
import { readSecret, readSecretSync } from './keychain'
export interface ResolveOptions {
/**
* Logical service identifier for the keychain lookup. Same value the caller
* would pass to `writeSecret` / `readSecret`. Ignored when the env-var path
* resolves the value first.
*/
service: string
/**
* Names to try, in order. Each name is checked as both an env-var
* (`process.env[name]`) and a keychain account name. Env-var matches always
* beat keychain matches (env-var is cheaper and doesn't risk a Keychain
* prompt).
*
* Order matters: list the canonical name first so a legacy alias is only
* consulted when the canonical entry is missing.
*/
accounts: readonly string[]
/**
* When `true`, skip the keychain fallback entirely. The resolver checks
* `process.env[account]` for each account and returns `undefined` immediately
* if none match. Use this in headless contexts (CI runners, bootstrap hooks)
* where a Keychain auth prompt is unacceptable.
*
* @default false
*/
allowEnvOnly?: boolean | undefined
}
export interface ResolveResult {
value: string
/**
* Where the value came from: 'env' — `process.env[<account>]` had a non-empty
* value. 'keychain' — env-var was empty/missing; the value was read from the
* OS credential store under the matching account.
*/
source: 'env' | 'keychain'
/**
* Which account in `accounts` was the actual hit.
*/
account: string
}
/**
* Pull a non-empty string from `process.env[name]`. Returns `undefined` for
* missing or empty values so the caller can fall through to the next source
* without distinguishing the two cases.
*/
export function readEnv(name: string): string | undefined {
const value = process.env[name]
if (typeof value !== 'string') {
return undefined
}
const trimmed = value.trim()
return trimmed ? trimmed : undefined
}
/**
* Resolve a secret following the canonical env → keychain order. Returns
* `undefined` when neither source has the value — the caller's signal to prompt
* the user or surface a setup hint.
*/
export async function resolve(
opts: ResolveOptions,
): Promise<ResolveResult | undefined> {
const { accounts, allowEnvOnly, service } = opts
for (let i = 0, { length } = accounts; i < length; i += 1) {
const account = accounts[i]!
const fromEnv = readEnv(account)
if (fromEnv) {
return { value: fromEnv, source: 'env', account }
}
}
if (allowEnvOnly) {
return undefined
}
for (let i = 0, { length } = accounts; i < length; i += 1) {
const account = accounts[i]!
const fromKeychain = await readSecret({ service, account })
if (fromKeychain) {
return { value: fromKeychain, source: 'keychain', account }
}
}
return undefined
}
/**
* Sync variant for non-async callers (hook initializers, schema validators that
* run before any `await` machinery exists).
*/
export function resolveSync(opts: ResolveOptions): ResolveResult | undefined {
const { accounts, allowEnvOnly, service } = opts
for (let i = 0, { length } = accounts; i < length; i += 1) {
const account = accounts[i]!
const fromEnv = readEnv(account)
if (fromEnv) {
return { value: fromEnv, source: 'env', account }
}
}
if (allowEnvOnly) {
return undefined
}
for (let i = 0, { length } = accounts; i < length; i += 1) {
const account = accounts[i]!
const fromKeychain = readSecretSync({ service, account })
if (fromKeychain) {
return { value: fromKeychain, source: 'keychain', account }
}
}
return undefined
}