forked from nicobailon/pi-web-access
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcredential-source.ts
More file actions
191 lines (172 loc) · 6.22 KB
/
Copy pathcredential-source.ts
File metadata and controls
191 lines (172 loc) · 6.22 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(exec);
const COMMAND_TIMEOUT_MS = 5_000;
const MAX_CREDENTIAL_BYTES = 16_384;
const ENV_SOURCE = /^\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})$/;
const OP_SESSION_NAME = /^OP_SESSION_[A-Za-z0-9_]+$/;
const COMMAND_ENVIRONMENT_NAMES = [
"HOME",
"USER",
"LOGNAME",
"PATH",
"LANG",
"LC_ALL",
"LC_CTYPE",
"TERM",
"TMPDIR",
"XDG_CONFIG_HOME",
"XDG_RUNTIME_DIR",
"DBUS_SESSION_BUS_ADDRESS",
"SSH_AUTH_SOCK",
"WSL_DISTRO_NAME",
"WSL_INTEROP",
] as const;
export type CredentialFailureCategory =
| "invalid-source"
| "command-failed"
| "command-timeout"
| "command-aborted"
| "command-empty"
| "command-invalid-output"
| "command-output-too-large"
| "environment-empty"
| "oauth-credential-rejected";
export class CredentialResolutionError extends Error {
readonly provider: string;
readonly category: CredentialFailureCategory;
constructor(provider: string, category: CredentialFailureCategory) {
const suffix =
category === "command-aborted" ? "aborted" :
category === "oauth-credential-rejected" ? "OAuth token exchange rejected the credentials" :
category;
super(`${provider} credential resolution failed: ${suffix}`);
this.name = "CredentialResolutionError";
this.provider = provider;
this.category = category;
}
}
export interface CredentialCommandResult {
stdout: string | Buffer;
}
export interface CredentialCommandOptions {
signal?: AbortSignal;
timeoutMs: number;
maxOutputBytes: number;
environment: Record<string, string>;
}
export type CredentialCommandRunner = (
command: string,
options: CredentialCommandOptions,
) => Promise<CredentialCommandResult>;
export interface CredentialOptions {
provider: string;
configuredValue?: unknown;
environmentValue?: unknown;
environment?: Record<string, string | undefined>;
signal?: AbortSignal;
runCommand?: CredentialCommandRunner;
}
export function redactCredential(text: string, credential: string | null | undefined): string {
return credential ? text.split(credential).join("[redacted]") : text;
}
function normalize(value: unknown): string | null {
if (typeof value !== "string") return null;
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function commandEnvironment(source: Record<string, string | undefined>): Record<string, string> {
const environment: Record<string, string> = {};
for (const name of COMMAND_ENVIRONMENT_NAMES) {
const value = source[name];
if (value !== undefined) environment[name] = value;
}
for (const [name, value] of Object.entries(source)) {
if (value !== undefined && OP_SESSION_NAME.test(name)) environment[name] = value;
}
return environment;
}
function configuredSource(options: CredentialOptions): string | null {
return normalize(options.configuredValue);
}
function explicitEnvironmentName(source: string): string | null {
const match = source.match(ENV_SOURCE);
return match ? match[1] ?? match[2] : null;
}
function escapedSource(source: string): string | null {
if (source.startsWith("$$") || source.startsWith("$!")) return source.slice(1);
return null;
}
function isMalformedExplicitSource(source: string): boolean {
return source.startsWith("$") && escapedSource(source) === null && explicitEnvironmentName(source) === null;
}
async function defaultRunCommand(
command: string,
options: CredentialCommandOptions,
): Promise<CredentialCommandResult> {
const result = await execAsync(command, {
encoding: "utf8",
env: options.environment,
maxBuffer: options.maxOutputBytes + 1,
signal: options.signal,
timeout: options.timeoutMs,
windowsHide: true,
});
return { stdout: result.stdout };
}
function commandFailureCategory(error: unknown, signal?: AbortSignal): CredentialFailureCategory {
if (signal?.aborted) return "command-aborted";
if (error && typeof error === "object") {
const code = (error as { code?: string }).code;
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "command-output-too-large";
if ((error as { killed?: boolean }).killed || code === "ETIMEDOUT") return "command-timeout";
}
return "command-failed";
}
export function hasCredentialSource(options: CredentialOptions): boolean {
const source = configuredSource(options);
if (source?.startsWith("!")) return true;
if (source?.startsWith("$")) return true;
return normalize(options.environmentValue) !== null || source !== null;
}
export async function resolveCredential(options: CredentialOptions): Promise<string | null> {
const source = configuredSource(options);
const escaped = source ? escapedSource(source) : null;
if (escaped !== null) return escaped;
if (source?.startsWith("!")) {
const command = source.slice(1).trim();
if (!command) throw new CredentialResolutionError(options.provider, "invalid-source");
let result: CredentialCommandResult;
try {
result = await (options.runCommand ?? defaultRunCommand)(command, {
signal: options.signal,
timeoutMs: COMMAND_TIMEOUT_MS,
maxOutputBytes: MAX_CREDENTIAL_BYTES,
environment: commandEnvironment(options.environment ?? process.env),
});
} catch (error) {
throw new CredentialResolutionError(options.provider, commandFailureCategory(error, options.signal));
}
const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : result.stdout;
if (Buffer.byteLength(stdout, "utf8") > MAX_CREDENTIAL_BYTES) {
throw new CredentialResolutionError(options.provider, "command-output-too-large");
}
const value = stdout.trim();
if (!value) throw new CredentialResolutionError(options.provider, "command-empty");
if (/[\0-\x1f\x7f]/.test(value)) {
throw new CredentialResolutionError(options.provider, "command-invalid-output");
}
return value;
}
if (source && isMalformedExplicitSource(source)) {
throw new CredentialResolutionError(options.provider, "invalid-source");
}
if (source?.startsWith("$")) {
const name = explicitEnvironmentName(source);
if (!name) throw new CredentialResolutionError(options.provider, "invalid-source");
const value = normalize((options.environment ?? process.env)[name]);
if (!value) throw new CredentialResolutionError(options.provider, "environment-empty");
return value;
}
return normalize(options.environmentValue) ?? source;
}