hanzio 1.0.10 → 1.2.0
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 +70 -4
- package/dist/api-wrapper/actions-impl.d.ts +4 -0
- package/dist/api-wrapper/builders.d.ts +23 -0
- package/dist/api-wrapper/create-client.d.ts +6 -0
- package/dist/api-wrapper/error-reporting.d.ts +7 -0
- package/dist/api-wrapper/errors.d.ts +64 -0
- package/dist/api-wrapper/headers.d.ts +10 -0
- package/dist/api-wrapper/index.d.ts +5 -84
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -0
- package/dist/api-wrapper/size.d.ts +1 -0
- package/dist/api-wrapper/test-helpers.d.ts +6 -0
- package/dist/api-wrapper/transport.d.ts +55 -0
- package/dist/api-wrapper/types.d.ts +154 -0
- package/dist/api-wrapper/zod-issues.d.ts +17 -0
- package/dist/cache/index.d.ts +26 -0
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +14 -23
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +14 -2
- package/dist/secrets/loaders.d.ts +9 -0
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/src/api-wrapper/index.js +1059 -177
- package/dist/src/index.js +2807 -1397
- package/dist/src/jwt/index.js +2 -2
- package/dist/src/p-queue/index.js +3 -3
- package/dist/src/secrets/index.js +1610 -407
- package/dist/src/secrets/infisical.js +1352 -214
- package/dist/src/secrets/loaders.js +1529 -0
- package/dist/src/secrets/vite.js +33 -11
- package/dist/src/secrets/worker.js +275 -0
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/zod/index.js +7 -7
- package/dist/typedswitch/index.d.ts +9 -5
- package/package.json +12 -3
- package/src/api-wrapper/actions-impl.ts +100 -0
- package/src/api-wrapper/builders.ts +200 -0
- package/src/api-wrapper/create-client.ts +393 -0
- package/src/api-wrapper/error-reporting.ts +31 -0
- package/src/api-wrapper/errors.ts +231 -0
- package/src/api-wrapper/headers.ts +79 -0
- package/src/api-wrapper/index.ts +5 -492
- package/src/api-wrapper/redaction.ts +62 -0
- package/src/api-wrapper/shared.ts +25 -0
- package/src/api-wrapper/size.ts +19 -0
- package/src/api-wrapper/test-helpers.ts +25 -0
- package/src/api-wrapper/transport.ts +350 -0
- package/src/api-wrapper/types.ts +259 -0
- package/src/api-wrapper/zod-issues.ts +157 -0
- package/src/cache/index.ts +228 -61
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +81 -140
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +219 -35
- package/src/secrets/loaders.ts +43 -0
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- package/src/typedswitch/index.ts +80 -69
|
@@ -1,297 +1,1578 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/secrets/errors.ts
|
|
2
|
+
class SecretLoadError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
operation;
|
|
5
|
+
status;
|
|
6
|
+
constructor(code, message, details = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "SecretLoadError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.operation = details.operation;
|
|
11
|
+
this.status = details.status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/secrets/deadline.ts
|
|
16
|
+
async function withSecretDeadline(options, operation) {
|
|
17
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
18
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
19
|
+
throw new SecretLoadError("CONFIGURATION", "Secret timeoutMs must be a positive finite timer duration.");
|
|
20
|
+
}
|
|
21
|
+
const controller = new AbortController;
|
|
22
|
+
const cancel = () => controller.abort(new SecretLoadError("ABORTED", "Secret loading was cancelled."));
|
|
23
|
+
if (options.signal?.aborted) {
|
|
24
|
+
cancel();
|
|
25
|
+
} else {
|
|
26
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
27
|
+
}
|
|
28
|
+
const timer = setTimeout(() => controller.abort(new SecretLoadError("TIMEOUT", `Secret loading timed out after ${timeoutMs}ms.`)), timeoutMs);
|
|
29
|
+
let rejectOnAbort = () => {};
|
|
30
|
+
try {
|
|
31
|
+
const aborted = new Promise((_, reject) => {
|
|
32
|
+
rejectOnAbort = () => reject(controller.signal.reason);
|
|
33
|
+
controller.signal.addEventListener("abort", rejectOnAbort, { once: true });
|
|
34
|
+
});
|
|
35
|
+
if (controller.signal.aborted) {
|
|
36
|
+
throw controller.signal.reason;
|
|
37
|
+
}
|
|
38
|
+
return await Promise.race([operation(controller.signal), aborted]);
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
42
|
+
controller.signal.removeEventListener("abort", rejectOnAbort);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/secrets/environment.ts
|
|
47
|
+
function getProcessEnvironment() {
|
|
48
|
+
return typeof process === "undefined" ? undefined : process.env;
|
|
49
|
+
}
|
|
50
|
+
function resolveSecretEnvironment(option, env) {
|
|
51
|
+
const explicit = typeof option === "function" ? option() : option;
|
|
52
|
+
if (typeof explicit === "object" && explicit !== null) {
|
|
53
|
+
if (typeof explicit.slug !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(explicit.slug)) {
|
|
54
|
+
throw new SecretLoadError("CONFIGURATION", "Custom secret environment requires a valid slug.");
|
|
55
|
+
}
|
|
56
|
+
return explicit.slug;
|
|
57
|
+
}
|
|
58
|
+
const selected = explicit ?? env?.SECRETS_ENV;
|
|
59
|
+
if (selected !== undefined) {
|
|
60
|
+
if (selected === "production" || selected === "prod") {
|
|
61
|
+
return "prod";
|
|
62
|
+
}
|
|
63
|
+
if (selected === "development" || selected === "dev") {
|
|
64
|
+
return "dev";
|
|
65
|
+
}
|
|
66
|
+
if (selected === "staging") {
|
|
67
|
+
return "staging";
|
|
68
|
+
}
|
|
69
|
+
throw new SecretLoadError("CONFIGURATION", "Invalid secret environment. Use dev, staging, prod, or an explicit { slug } option.");
|
|
70
|
+
}
|
|
71
|
+
if (env?.NODE_ENV === "production" || env?.NODE_ENV === "prod") {
|
|
72
|
+
return "prod";
|
|
73
|
+
}
|
|
74
|
+
if (env?.NODE_ENV === "staging") {
|
|
75
|
+
return "staging";
|
|
76
|
+
}
|
|
77
|
+
return "dev";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/secrets/core.ts
|
|
81
|
+
var registryKey = Symbol.for("hanzio.secrets.environmentWrites.v1");
|
|
82
|
+
var shared = globalThis;
|
|
83
|
+
var environmentWrites = shared[registryKey] ?? new WeakMap;
|
|
84
|
+
shared[registryKey] = environmentWrites;
|
|
85
|
+
var defaultLogger = {
|
|
86
|
+
info: (message, attrs) => console.log(`${message} environment=${attrs.environment} count=${attrs.count} source=${attrs.source} durationMs=${attrs.durationMs}`, { sources: attrs.sources }),
|
|
87
|
+
error: (message, attrs) => console.error(message, attrs)
|
|
88
|
+
};
|
|
89
|
+
function readOverrides(keys, env) {
|
|
90
|
+
if (!env) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
const writes = environmentWrites.get(env);
|
|
94
|
+
return Object.fromEntries(keys.flatMap((key) => {
|
|
95
|
+
const value = Object.hasOwn(env, key) ? env[key] : undefined;
|
|
96
|
+
if (writes?.has(key)) {
|
|
97
|
+
if (writes.get(key) === value) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
writes.delete(key);
|
|
101
|
+
}
|
|
102
|
+
return value ? [[key, value]] : [];
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
async function defineSecretSet(keys, options, environmentVariables) {
|
|
106
|
+
const keySet = [...new Set(keys)];
|
|
107
|
+
const environment = resolveSecretEnvironment(options.environment, environmentVariables);
|
|
108
|
+
const logger = options.logger === false ? undefined : options.logger ?? defaultLogger;
|
|
109
|
+
let cached;
|
|
110
|
+
let parsed;
|
|
111
|
+
let sources;
|
|
112
|
+
let inFlight;
|
|
113
|
+
const log = (level, message, attributes) => {
|
|
114
|
+
try {
|
|
115
|
+
logger?.[level](message, attributes);
|
|
116
|
+
} catch {}
|
|
117
|
+
};
|
|
118
|
+
const loadSnapshot = async (request, signal) => {
|
|
119
|
+
const local = readOverrides(keySet, environmentVariables);
|
|
120
|
+
const missing = keySet.filter((key) => !Object.hasOwn(local, key));
|
|
121
|
+
let loaderSource = options.loader.source ?? "custom loader";
|
|
122
|
+
let loaded = {};
|
|
123
|
+
if (missing.length) {
|
|
124
|
+
try {
|
|
125
|
+
loaded = await options.loader({
|
|
126
|
+
keys: missing,
|
|
127
|
+
environment,
|
|
128
|
+
signal,
|
|
129
|
+
timeoutMs: request.timeoutMs ?? options.timeoutMs ?? 30000,
|
|
130
|
+
reportSource: (source) => {
|
|
131
|
+
loaderSource = source;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof SecretLoadError) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
throw new SecretLoadError("LOADER", "Secret loader failed.");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const overrides = readOverrides(keySet, environmentVariables);
|
|
142
|
+
const next = Object.fromEntries(keySet.map((key) => {
|
|
143
|
+
if (Object.hasOwn(overrides, key)) {
|
|
144
|
+
return [key, overrides[key]];
|
|
145
|
+
}
|
|
146
|
+
if (Object.hasOwn(loaded, key)) {
|
|
147
|
+
return [key, loaded[key]];
|
|
148
|
+
}
|
|
149
|
+
return [key, undefined];
|
|
150
|
+
}));
|
|
151
|
+
const missingKeys = keySet.filter((key) => typeof next[key] !== "string" || !next[key]);
|
|
152
|
+
if (missingKeys.length) {
|
|
153
|
+
throw new SecretLoadError("MISSING_SECRETS", `Missing secrets: ${missingKeys.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
let nextParsed;
|
|
156
|
+
try {
|
|
157
|
+
nextParsed = options.schema ? await options.schema.parseAsync({ ...next }) : { ...next };
|
|
158
|
+
nextParsed = structuredClone(nextParsed);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new SecretLoadError("VALIDATION", "Secret schema validation failed. Check the configured schema; values are omitted.");
|
|
161
|
+
}
|
|
162
|
+
signal.throwIfAborted();
|
|
163
|
+
const nextSources = Object.fromEntries(keySet.map((key) => [
|
|
164
|
+
key,
|
|
165
|
+
Object.hasOwn(overrides, key) ? "process.env" : loaderSource
|
|
166
|
+
]));
|
|
167
|
+
if (options.writeToProcessEnv !== false && environmentVariables) {
|
|
168
|
+
let writes = environmentWrites.get(environmentVariables);
|
|
169
|
+
if (!writes) {
|
|
170
|
+
writes = new Map;
|
|
171
|
+
environmentWrites.set(environmentVariables, writes);
|
|
172
|
+
}
|
|
173
|
+
for (const key of keySet) {
|
|
174
|
+
if (Object.hasOwn(overrides, key)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
environmentVariables[key] = next[key];
|
|
178
|
+
writes.set(key, next[key]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
cached = next;
|
|
182
|
+
parsed = nextParsed;
|
|
183
|
+
sources = nextSources;
|
|
184
|
+
};
|
|
185
|
+
const load = (request = {}) => {
|
|
186
|
+
if (inFlight) {
|
|
187
|
+
return inFlight;
|
|
188
|
+
}
|
|
189
|
+
inFlight = (async () => {
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
try {
|
|
192
|
+
await withSecretDeadline({
|
|
193
|
+
timeoutMs: request.timeoutMs ?? options.timeoutMs,
|
|
194
|
+
signal: request.signal ?? options.signal
|
|
195
|
+
}, (signal) => loadSnapshot(request, signal));
|
|
196
|
+
const sourceCounts = new Map;
|
|
197
|
+
for (const key of keySet) {
|
|
198
|
+
const source = sources[key];
|
|
199
|
+
sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
const counts = Object.fromEntries(sourceCounts);
|
|
202
|
+
log("info", "Loaded secrets", {
|
|
203
|
+
environment,
|
|
204
|
+
count: keySet.length,
|
|
205
|
+
durationMs: Date.now() - start,
|
|
206
|
+
source: Object.keys(counts).join(" + ") || "none",
|
|
207
|
+
sources: counts
|
|
208
|
+
});
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const safe = error instanceof SecretLoadError ? error : new SecretLoadError("LOADER", "Secret loading failed.");
|
|
211
|
+
log("error", "Secret loading failed", {
|
|
212
|
+
environment,
|
|
213
|
+
count: keySet.length,
|
|
214
|
+
durationMs: Date.now() - start,
|
|
215
|
+
code: safe.code
|
|
216
|
+
});
|
|
217
|
+
throw safe;
|
|
218
|
+
}
|
|
219
|
+
})().finally(() => {
|
|
220
|
+
inFlight = undefined;
|
|
221
|
+
});
|
|
222
|
+
return inFlight;
|
|
223
|
+
};
|
|
224
|
+
const secretSet = {
|
|
225
|
+
secret(key) {
|
|
226
|
+
if (!Object.hasOwn(cached, key)) {
|
|
227
|
+
throw new SecretLoadError("MISSING_SECRETS", `Secret ${key} not found`);
|
|
228
|
+
}
|
|
229
|
+
return cached[key];
|
|
230
|
+
},
|
|
231
|
+
secrets: () => ({ ...cached }),
|
|
232
|
+
parsed: () => structuredClone(parsed),
|
|
233
|
+
sources: () => ({ ...sources }),
|
|
234
|
+
async reload(request) {
|
|
235
|
+
await load(request);
|
|
236
|
+
return secretSet;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
await load();
|
|
240
|
+
return secretSet;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/secrets/infisical.ts
|
|
244
|
+
import { z as z2 } from "zod";
|
|
245
|
+
// src/api-wrapper/headers.ts
|
|
246
|
+
var CANONICAL_HEADER_NAMES = {
|
|
247
|
+
"content-type": "Content-Type",
|
|
248
|
+
authorization: "Authorization",
|
|
249
|
+
accept: "Accept",
|
|
250
|
+
"user-agent": "User-Agent",
|
|
251
|
+
"x-request-id": "X-Request-Id",
|
|
252
|
+
"x-correlation-id": "X-Correlation-Id"
|
|
253
|
+
};
|
|
254
|
+
function normalizeHeaders(headers) {
|
|
255
|
+
const result = {};
|
|
256
|
+
const lowerToCanonical = new Map;
|
|
257
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
258
|
+
const lower = key.toLowerCase();
|
|
259
|
+
const canonical = CANONICAL_HEADER_NAMES[lower] ?? key;
|
|
260
|
+
const existing = lowerToCanonical.get(lower);
|
|
261
|
+
if (existing && existing !== canonical)
|
|
262
|
+
delete result[existing];
|
|
263
|
+
lowerToCanonical.set(lower, canonical);
|
|
264
|
+
result[canonical] = value;
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function getHeader(headers, name) {
|
|
269
|
+
const lower = name.toLowerCase();
|
|
270
|
+
const canonical = CANONICAL_HEADER_NAMES[lower];
|
|
271
|
+
if (canonical !== undefined) {
|
|
272
|
+
if (headers[canonical] !== undefined)
|
|
273
|
+
return headers[canonical];
|
|
274
|
+
}
|
|
275
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
276
|
+
if (k.toLowerCase() === lower)
|
|
277
|
+
return v;
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
function hasJsonContentType(headers) {
|
|
282
|
+
const ct = getHeader(headers, "content-type") ?? "";
|
|
283
|
+
return ct.includes("application/json");
|
|
284
|
+
}
|
|
285
|
+
function buildFetchBody(body, headers, configError) {
|
|
286
|
+
if (body === undefined || body === null)
|
|
287
|
+
return;
|
|
288
|
+
if (body instanceof FormData || body instanceof Blob)
|
|
289
|
+
return body;
|
|
290
|
+
if (hasJsonContentType(headers))
|
|
291
|
+
return JSON.stringify(body);
|
|
292
|
+
if (typeof body === "object" && (Object.getPrototypeOf(body) === Object.prototype || Object.getPrototypeOf(body) === null) && configError) {
|
|
293
|
+
throw configError("Request body is a plain object but Content-Type is not application/json");
|
|
294
|
+
}
|
|
295
|
+
return body;
|
|
296
|
+
}
|
|
297
|
+
function getHeadersAsObject(headers) {
|
|
298
|
+
const result = {};
|
|
299
|
+
headers.forEach((value, key) => {
|
|
300
|
+
result[key] = value;
|
|
301
|
+
});
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/api-wrapper/zod-issues.ts
|
|
2
306
|
import { ZodError } from "zod";
|
|
307
|
+
function formatZodIssues(error, rawData) {
|
|
308
|
+
return error.issues.map((issue) => {
|
|
309
|
+
const pathArray = [...issue.path];
|
|
310
|
+
const path = formatIssuePath(pathArray);
|
|
311
|
+
const value = getValueAtPath(rawData, pathArray);
|
|
312
|
+
const valuePreview = value === undefined ? undefined : previewValue(value);
|
|
313
|
+
const raw = issue;
|
|
314
|
+
const expected = typeof raw.expected === "string" ? raw.expected : undefined;
|
|
315
|
+
const received = typeof raw.received === "string" ? raw.received : value !== undefined ? typeofVerbose(value) : undefined;
|
|
316
|
+
const known = new Set([
|
|
317
|
+
"code",
|
|
318
|
+
"path",
|
|
319
|
+
"message",
|
|
320
|
+
"expected",
|
|
321
|
+
"received",
|
|
322
|
+
"fatal"
|
|
323
|
+
]);
|
|
324
|
+
const extra = {};
|
|
325
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
326
|
+
if (!known.has(k) && v !== undefined)
|
|
327
|
+
extra[k] = v;
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
path,
|
|
331
|
+
pathArray,
|
|
332
|
+
code: issue.code,
|
|
333
|
+
message: issue.message,
|
|
334
|
+
expected,
|
|
335
|
+
received,
|
|
336
|
+
value,
|
|
337
|
+
valuePreview,
|
|
338
|
+
extra: Object.keys(extra).length > 0 ? extra : undefined
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function renderIssueSummary(issues) {
|
|
343
|
+
if (issues.length === 0)
|
|
344
|
+
return "(no issues reported)";
|
|
345
|
+
return issues.map((issue, idx) => {
|
|
346
|
+
const parts = [];
|
|
347
|
+
parts.push(` [${idx + 1}] ${issue.path}: ${issue.message}`);
|
|
348
|
+
if (issue.expected || issue.received) {
|
|
349
|
+
const exp = issue.expected ? `expected ${issue.expected}` : "";
|
|
350
|
+
const rec = issue.received ? `received ${issue.received}` : "";
|
|
351
|
+
parts.push(` (${[exp, rec].filter(Boolean).join(", ")})`);
|
|
352
|
+
}
|
|
353
|
+
if (issue.valuePreview !== undefined) {
|
|
354
|
+
parts.push(` value: ${issue.valuePreview}`);
|
|
355
|
+
}
|
|
356
|
+
if (issue.extra) {
|
|
357
|
+
for (const [k, v] of Object.entries(issue.extra)) {
|
|
358
|
+
parts.push(` ${k}: ${previewValue(v, 120)}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return parts.join(`
|
|
362
|
+
`);
|
|
363
|
+
}).join(`
|
|
364
|
+
`);
|
|
365
|
+
}
|
|
366
|
+
function formatIssuePath(pathArray) {
|
|
367
|
+
if (pathArray.length === 0)
|
|
368
|
+
return "<root>";
|
|
369
|
+
let out = "";
|
|
370
|
+
for (const segment of pathArray) {
|
|
371
|
+
if (typeof segment === "number") {
|
|
372
|
+
out += `[${segment}]`;
|
|
373
|
+
} else if (/^[A-Za-z_$][\w$]*$/.test(segment)) {
|
|
374
|
+
out += out === "" ? segment : `.${segment}`;
|
|
375
|
+
} else {
|
|
376
|
+
out += `[${JSON.stringify(segment)}]`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
function getValueAtPath(data, pathArray) {
|
|
382
|
+
let current = data;
|
|
383
|
+
for (const segment of pathArray) {
|
|
384
|
+
if (current === null || current === undefined)
|
|
385
|
+
return;
|
|
386
|
+
if (typeof current !== "object")
|
|
387
|
+
return;
|
|
388
|
+
current = current[segment];
|
|
389
|
+
}
|
|
390
|
+
return current;
|
|
391
|
+
}
|
|
392
|
+
function typeofVerbose(value) {
|
|
393
|
+
if (value === null)
|
|
394
|
+
return "null";
|
|
395
|
+
if (Array.isArray(value))
|
|
396
|
+
return "array";
|
|
397
|
+
return typeof value;
|
|
398
|
+
}
|
|
399
|
+
function previewValue(value, maxLen = 200) {
|
|
400
|
+
let str;
|
|
401
|
+
try {
|
|
402
|
+
if (value === undefined)
|
|
403
|
+
str = "undefined";
|
|
404
|
+
else if (typeof value === "string")
|
|
405
|
+
str = JSON.stringify(value);
|
|
406
|
+
else
|
|
407
|
+
str = JSON.stringify(value);
|
|
408
|
+
} catch {
|
|
409
|
+
str = String(value);
|
|
410
|
+
}
|
|
411
|
+
if (str === undefined)
|
|
412
|
+
str = String(value);
|
|
413
|
+
if (str.length > maxLen)
|
|
414
|
+
str = `${str.slice(0, maxLen)}…`;
|
|
415
|
+
return str;
|
|
416
|
+
}
|
|
417
|
+
function makeJsonParseZodError(error, text) {
|
|
418
|
+
const message = error instanceof Error ? `Response body was not valid JSON: ${error.message}` : "Response body was not valid JSON";
|
|
419
|
+
const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;
|
|
420
|
+
return new ZodError([
|
|
421
|
+
{
|
|
422
|
+
code: "custom",
|
|
423
|
+
path: [],
|
|
424
|
+
message: `${message} (body preview: ${JSON.stringify(preview)})`,
|
|
425
|
+
input: text
|
|
426
|
+
}
|
|
427
|
+
]);
|
|
428
|
+
}
|
|
429
|
+
function tryParseJson(body) {
|
|
430
|
+
if (!body)
|
|
431
|
+
return;
|
|
432
|
+
try {
|
|
433
|
+
return JSON.parse(body);
|
|
434
|
+
} catch {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/api-wrapper/errors.ts
|
|
440
|
+
var MAX_BODY_PREVIEW = 500;
|
|
3
441
|
|
|
4
|
-
class
|
|
442
|
+
class ApiError extends Error {
|
|
443
|
+
context;
|
|
444
|
+
cause;
|
|
445
|
+
constructor(message, context, options) {
|
|
446
|
+
super(message);
|
|
447
|
+
this.name = "ApiError";
|
|
448
|
+
this.context = context;
|
|
449
|
+
this.cause = options?.cause;
|
|
450
|
+
}
|
|
451
|
+
toJSON() {
|
|
452
|
+
return {
|
|
453
|
+
name: this.name,
|
|
454
|
+
message: this.message,
|
|
455
|
+
context: this.context
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
class HttpResponseError extends ApiError {
|
|
461
|
+
status;
|
|
462
|
+
body;
|
|
463
|
+
bodyJson;
|
|
464
|
+
responseHeaders;
|
|
465
|
+
requestId;
|
|
466
|
+
constructor(status, body, responseHeaders, context) {
|
|
467
|
+
const preview = body.length > MAX_BODY_PREVIEW ? `${body.slice(0, MAX_BODY_PREVIEW)}…` : body;
|
|
468
|
+
super(`HTTP ${status} ${context.method} ${context.url} - ${preview}`, context);
|
|
469
|
+
this.name = "HttpResponseError";
|
|
470
|
+
this.status = status;
|
|
471
|
+
this.body = body;
|
|
472
|
+
this.responseHeaders = responseHeaders;
|
|
473
|
+
this.requestId = getHeader(responseHeaders, "x-request-id") ?? getHeader(responseHeaders, "x-correlation-id");
|
|
474
|
+
this.bodyJson = tryParseJson(body);
|
|
475
|
+
}
|
|
476
|
+
toJSON() {
|
|
477
|
+
return {
|
|
478
|
+
...super.toJSON(),
|
|
479
|
+
status: this.status,
|
|
480
|
+
body: this.body,
|
|
481
|
+
bodyJson: this.bodyJson,
|
|
482
|
+
requestId: this.requestId,
|
|
483
|
+
responseHeaders: this.responseHeaders
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
class ResponseValidationError extends ApiError {
|
|
5
489
|
zodError;
|
|
6
490
|
rawResponse;
|
|
491
|
+
issues;
|
|
7
492
|
validationIssues;
|
|
8
|
-
constructor(zodError, rawResponse) {
|
|
9
|
-
const issues = zodError
|
|
10
|
-
|
|
493
|
+
constructor(zodError, rawResponse, context) {
|
|
494
|
+
const issues = formatZodIssues(zodError, rawResponse);
|
|
495
|
+
const summary = renderIssueSummary(issues);
|
|
496
|
+
super(`Response validation failed (${context.method} ${context.url}):
|
|
497
|
+
${summary}`, context, { cause: zodError });
|
|
11
498
|
this.name = "ResponseValidationError";
|
|
12
499
|
this.zodError = zodError;
|
|
13
500
|
this.rawResponse = rawResponse;
|
|
14
|
-
this.
|
|
501
|
+
this.issues = issues;
|
|
502
|
+
this.validationIssues = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
503
|
+
}
|
|
504
|
+
toJSON() {
|
|
505
|
+
return {
|
|
506
|
+
...super.toJSON(),
|
|
507
|
+
issues: this.issues,
|
|
508
|
+
validationIssues: this.validationIssues,
|
|
509
|
+
rawResponsePreview: previewValue(this.rawResponse, 1000)
|
|
510
|
+
};
|
|
15
511
|
}
|
|
16
512
|
}
|
|
17
513
|
|
|
18
|
-
class
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
514
|
+
class RequestValidationError extends ApiError {
|
|
515
|
+
zodError;
|
|
516
|
+
target;
|
|
517
|
+
rawInput;
|
|
518
|
+
issues;
|
|
519
|
+
validationIssues;
|
|
520
|
+
constructor(zodError, target, rawInput, context) {
|
|
521
|
+
const issues = formatZodIssues(zodError, rawInput);
|
|
522
|
+
const summary = renderIssueSummary(issues);
|
|
523
|
+
super(`Request ${target} validation failed (${context.method} ${context.url}):
|
|
524
|
+
${summary}`, context, { cause: zodError });
|
|
525
|
+
this.name = "RequestValidationError";
|
|
526
|
+
this.zodError = zodError;
|
|
527
|
+
this.target = target;
|
|
528
|
+
this.rawInput = rawInput;
|
|
529
|
+
this.issues = issues;
|
|
530
|
+
this.validationIssues = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
531
|
+
}
|
|
532
|
+
toJSON() {
|
|
533
|
+
return {
|
|
534
|
+
...super.toJSON(),
|
|
535
|
+
target: this.target,
|
|
536
|
+
issues: this.issues,
|
|
537
|
+
validationIssues: this.validationIssues
|
|
538
|
+
};
|
|
26
539
|
}
|
|
27
540
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
541
|
+
|
|
542
|
+
class RequestTimeoutError extends ApiError {
|
|
543
|
+
timeoutMs;
|
|
544
|
+
constructor(timeoutMs, context) {
|
|
545
|
+
super(`Request timed out after ${timeoutMs}ms: ${context.method} ${context.url}`, context);
|
|
546
|
+
this.name = "RequestTimeoutError";
|
|
547
|
+
this.timeoutMs = timeoutMs;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
class NetworkError extends ApiError {
|
|
552
|
+
constructor(message, context, cause) {
|
|
553
|
+
super(message, context, { cause });
|
|
554
|
+
this.name = "NetworkError";
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
class RequestAbortedError extends ApiError {
|
|
559
|
+
constructor(context, cause) {
|
|
560
|
+
super(`Request aborted: ${context.method} ${context.url}`, context, {
|
|
561
|
+
cause
|
|
562
|
+
});
|
|
563
|
+
this.name = "RequestAbortedError";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
class ConfigError extends ApiError {
|
|
568
|
+
constructor(message, context) {
|
|
569
|
+
super(message, context);
|
|
570
|
+
this.name = "ConfigError";
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
class ActionError extends ApiError {
|
|
575
|
+
constructor(message, context, cause) {
|
|
576
|
+
super(message, context, { cause });
|
|
577
|
+
this.name = "ActionError";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
var isApiError = (e) => e instanceof ApiError;
|
|
581
|
+
var isHttpResponseError = (e) => e instanceof HttpResponseError;
|
|
582
|
+
var isResponseValidationError = (e) => e instanceof ResponseValidationError;
|
|
583
|
+
var isRequestValidationError = (e) => e instanceof RequestValidationError;
|
|
584
|
+
var isRequestTimeoutError = (e) => e instanceof RequestTimeoutError;
|
|
585
|
+
var isNetworkError = (e) => e instanceof NetworkError;
|
|
586
|
+
var isRequestAbortedError = (e) => e instanceof RequestAbortedError;
|
|
587
|
+
var isConfigError = (e) => e instanceof ConfigError;
|
|
588
|
+
var isActionError = (e) => e instanceof ActionError;
|
|
589
|
+
function isNonRetryableApiError(error) {
|
|
590
|
+
return error instanceof HttpResponseError || error instanceof ResponseValidationError || error instanceof RequestValidationError || error instanceof ConfigError || error instanceof RequestAbortedError;
|
|
591
|
+
}
|
|
592
|
+
// src/api-wrapper/builders.ts
|
|
593
|
+
import { ZodError as ZodError2 } from "zod";
|
|
594
|
+
function formatQueryString(queryParams, doNotEncode) {
|
|
595
|
+
if (Object.keys(queryParams).length === 0)
|
|
596
|
+
return "";
|
|
597
|
+
return doNotEncode ? Object.entries(queryParams).map(([key, value]) => `${key}=${value}`).join("&") : new URLSearchParams(queryParams).toString();
|
|
598
|
+
}
|
|
599
|
+
function buildUrl(options) {
|
|
600
|
+
const {
|
|
601
|
+
endpoint,
|
|
602
|
+
params,
|
|
603
|
+
queryParams,
|
|
604
|
+
baseApiUrls,
|
|
605
|
+
defaultBaseApiUrl,
|
|
606
|
+
url,
|
|
607
|
+
doNotEncodeQueryParams,
|
|
608
|
+
configError
|
|
609
|
+
} = options;
|
|
610
|
+
let finalUrl;
|
|
611
|
+
if (url) {
|
|
612
|
+
finalUrl = replacePathParams(url, params, configError);
|
|
613
|
+
} else {
|
|
614
|
+
const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0];
|
|
615
|
+
if (!defaultBaseUrl) {
|
|
616
|
+
throw configError("At least one base API URL is required");
|
|
33
617
|
}
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
618
|
+
const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl;
|
|
619
|
+
const configBaseUrl = baseApiUrls[baseUrlKey];
|
|
620
|
+
if (!configBaseUrl) {
|
|
621
|
+
throw configError(`Unknown base API URL: ${baseUrlKey}`);
|
|
622
|
+
}
|
|
623
|
+
const baseUrl = typeof configBaseUrl === "function" ? configBaseUrl() : configBaseUrl;
|
|
624
|
+
const path = typeof endpoint.path === "function" ? endpoint.path(baseUrl) : endpoint.path;
|
|
625
|
+
finalUrl = `${baseUrl}${replacePathParams(path, params, configError)}`;
|
|
626
|
+
}
|
|
627
|
+
const queryString = formatQueryString(queryParams, doNotEncodeQueryParams);
|
|
628
|
+
return {
|
|
629
|
+
finalUrl,
|
|
630
|
+
fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function buildPathParams(endpoint, params, ctx) {
|
|
634
|
+
if (!endpoint.reqParamsSchema)
|
|
635
|
+
return params;
|
|
636
|
+
try {
|
|
637
|
+
return endpoint.reqParamsSchema.parse(params);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (error instanceof ZodError2) {
|
|
640
|
+
throw new RequestValidationError(error, "params", params, ctx());
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function buildQueryParams(endpoint, query, ctx) {
|
|
646
|
+
const defaultParams = endpoint.reqDefaultQueryParams ?? {};
|
|
647
|
+
let parsedQuery;
|
|
648
|
+
if (endpoint.reqQuerySchema) {
|
|
40
649
|
try {
|
|
41
|
-
|
|
42
|
-
method: endpoint.method,
|
|
43
|
-
url: finalUrl,
|
|
44
|
-
queryParams: buildQueryParams(endpoint, input.reqQuery),
|
|
45
|
-
body: buildRequestBody(endpoint, input.reqBody),
|
|
46
|
-
headers: buildHeaders(endpoint, input.reqHeaders, apiConfig.defaultHeaders ?? {}),
|
|
47
|
-
doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
|
|
48
|
-
timeoutMs: apiConfig.timeoutMs,
|
|
49
|
-
fetchFn: apiConfig.fetch ?? fetch
|
|
50
|
-
};
|
|
51
|
-
const result = await makeRequestWithRetry(config, apiConfig.retries ?? 3, apiConfig.retryDelayMs ?? 300, (response) => validateAndTransformResponse(endpoint, response), apiConfig.shouldRetry ?? defaultShouldRetry, apiConfig.logger);
|
|
52
|
-
retryCount = result.retryCount;
|
|
53
|
-
httpStatus = result.httpStatus;
|
|
54
|
-
const responseTimeMs = Date.now() - startedAt;
|
|
55
|
-
const responseSizeMb = calculateSizeInMb(result.validatedData);
|
|
56
|
-
const requestSizeMb = calculateSizeInMb({
|
|
57
|
-
method: config.method,
|
|
58
|
-
url: fullUrl,
|
|
59
|
-
body: config.body,
|
|
60
|
-
headers: config.headers
|
|
61
|
-
});
|
|
62
|
-
apiConfig.onRequestEnd?.({
|
|
63
|
-
endpoint: endpointName,
|
|
64
|
-
url: fullUrl,
|
|
65
|
-
status: httpStatus,
|
|
66
|
-
retryCount,
|
|
67
|
-
responseTimeMs
|
|
68
|
-
});
|
|
69
|
-
return {
|
|
70
|
-
data: result.validatedData,
|
|
71
|
-
requestSizeMb,
|
|
72
|
-
responseSizeMb,
|
|
73
|
-
requestSizeTotalMb: requestSizeMb + responseSizeMb,
|
|
74
|
-
responseTimeMs,
|
|
75
|
-
httpStatus,
|
|
76
|
-
retryCount
|
|
77
|
-
};
|
|
650
|
+
parsedQuery = endpoint.reqQuerySchema.parse(query);
|
|
78
651
|
} catch (error) {
|
|
79
|
-
|
|
652
|
+
if (error instanceof ZodError2) {
|
|
653
|
+
throw new RequestValidationError(error, "query", query, ctx());
|
|
654
|
+
}
|
|
80
655
|
throw error;
|
|
81
656
|
}
|
|
657
|
+
} else {
|
|
658
|
+
parsedQuery = query ?? {};
|
|
659
|
+
}
|
|
660
|
+
const merged = { ...defaultParams, ...parsedQuery };
|
|
661
|
+
return Object.fromEntries(Object.entries(merged).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
|
|
662
|
+
}
|
|
663
|
+
function buildRequestBody(endpoint, body, ctx) {
|
|
664
|
+
if (!endpoint.reqBodySchema)
|
|
665
|
+
return body;
|
|
666
|
+
let parsedBody;
|
|
667
|
+
try {
|
|
668
|
+
parsedBody = endpoint.reqBodySchema.parse(body);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (error instanceof ZodError2) {
|
|
671
|
+
throw new RequestValidationError(error, "body", body, ctx());
|
|
672
|
+
}
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
if (endpoint.reqBodyFormat !== "form-data") {
|
|
676
|
+
return parsedBody;
|
|
677
|
+
}
|
|
678
|
+
const formData = new FormData;
|
|
679
|
+
for (const [key, value] of Object.entries(parsedBody)) {
|
|
680
|
+
if (value == null)
|
|
681
|
+
continue;
|
|
682
|
+
formData.set(key, value instanceof Blob ? value : String(value));
|
|
683
|
+
}
|
|
684
|
+
return formData;
|
|
685
|
+
}
|
|
686
|
+
function buildHeaders(endpoint, headers, defaultHeaders, ctx) {
|
|
687
|
+
const contentType = endpoint.reqBodyFormat === "json" ? "application/json" : undefined;
|
|
688
|
+
let parsedHeaders;
|
|
689
|
+
if (endpoint.reqHeadersSchema) {
|
|
690
|
+
try {
|
|
691
|
+
parsedHeaders = endpoint.reqHeadersSchema.parse(headers);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (error instanceof ZodError2) {
|
|
694
|
+
throw new RequestValidationError(error, "headers", headers, ctx());
|
|
695
|
+
}
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
parsedHeaders = headers ?? {};
|
|
700
|
+
}
|
|
701
|
+
return normalizeHeaders({
|
|
702
|
+
...defaultHeaders,
|
|
703
|
+
...contentType && { "Content-Type": contentType },
|
|
704
|
+
...endpoint.defaultHeaders,
|
|
705
|
+
...parsedHeaders
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function replacePathParams(path, params, configError) {
|
|
709
|
+
return path.replace(/:(\w+)/g, (_, key) => {
|
|
710
|
+
const value = params?.[key];
|
|
711
|
+
if (value === undefined) {
|
|
712
|
+
throw configError(`Missing required path parameter: ${key}`);
|
|
713
|
+
}
|
|
714
|
+
return encodeURIComponent(String(value));
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/api-wrapper/actions-impl.ts
|
|
719
|
+
function createActionCache() {
|
|
720
|
+
const store = new Map;
|
|
721
|
+
const isExpired = (entry) => entry.expiresAt !== undefined && entry.expiresAt <= Date.now();
|
|
722
|
+
const cache = async (key, fn, options) => {
|
|
723
|
+
const existing = store.get(key);
|
|
724
|
+
if (existing && !isExpired(existing)) {
|
|
725
|
+
if (existing.inflight)
|
|
726
|
+
return existing.inflight;
|
|
727
|
+
if (existing.hasValue)
|
|
728
|
+
return existing.value;
|
|
729
|
+
}
|
|
730
|
+
const promise = (async () => fn())();
|
|
731
|
+
const entry = {
|
|
732
|
+
hasValue: false,
|
|
733
|
+
inflight: promise,
|
|
734
|
+
expiresAt: options?.ttlMs !== undefined ? Date.now() + options.ttlMs : undefined
|
|
735
|
+
};
|
|
736
|
+
store.set(key, entry);
|
|
737
|
+
try {
|
|
738
|
+
const value = await promise;
|
|
739
|
+
entry.value = value;
|
|
740
|
+
entry.hasValue = true;
|
|
741
|
+
entry.inflight = undefined;
|
|
742
|
+
return value;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
store.delete(key);
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
cache.get = (key) => {
|
|
749
|
+
const entry = store.get(key);
|
|
750
|
+
if (!entry || !entry.hasValue)
|
|
751
|
+
return;
|
|
752
|
+
if (isExpired(entry)) {
|
|
753
|
+
store.delete(key);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
return entry.value;
|
|
757
|
+
};
|
|
758
|
+
cache.set = (key, value, options) => {
|
|
759
|
+
store.set(key, {
|
|
760
|
+
value,
|
|
761
|
+
hasValue: true,
|
|
762
|
+
expiresAt: options?.ttlMs !== undefined ? Date.now() + options.ttlMs : undefined
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
cache.invalidate = (key) => {
|
|
766
|
+
store.delete(key);
|
|
767
|
+
};
|
|
768
|
+
cache.clear = () => {
|
|
769
|
+
store.clear();
|
|
82
770
|
};
|
|
83
|
-
return
|
|
771
|
+
return cache;
|
|
772
|
+
}
|
|
773
|
+
function makeDefineAction() {
|
|
774
|
+
function defineAction(...args) {
|
|
775
|
+
if (args.length === 0) {
|
|
776
|
+
return (def) => ({
|
|
777
|
+
...def,
|
|
778
|
+
noRuntimeInput: false
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
const def = args[0];
|
|
782
|
+
return {
|
|
783
|
+
...def,
|
|
784
|
+
noRuntimeInput: true
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return defineAction;
|
|
788
|
+
}
|
|
789
|
+
var defineAction = makeDefineAction();
|
|
790
|
+
|
|
791
|
+
// src/api-wrapper/redaction.ts
|
|
792
|
+
var SENSITIVE_HEADER_KEYS = new Set([
|
|
793
|
+
"authorization",
|
|
794
|
+
"cookie",
|
|
795
|
+
"set-cookie",
|
|
796
|
+
"x-api-key",
|
|
797
|
+
"x-auth-token"
|
|
798
|
+
]);
|
|
799
|
+
var SENSITIVE_BODY_KEYS = new Set([
|
|
800
|
+
"password",
|
|
801
|
+
"token",
|
|
802
|
+
"accesstoken",
|
|
803
|
+
"access_token",
|
|
804
|
+
"refreshtoken",
|
|
805
|
+
"refresh_token",
|
|
806
|
+
"secret",
|
|
807
|
+
"client_secret",
|
|
808
|
+
"clientsecret",
|
|
809
|
+
"apikey",
|
|
810
|
+
"api_key"
|
|
811
|
+
]);
|
|
812
|
+
function redactHeaders(headers) {
|
|
813
|
+
if (!headers)
|
|
814
|
+
return;
|
|
815
|
+
const out = {};
|
|
816
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
817
|
+
if (SENSITIVE_HEADER_KEYS.has(k.toLowerCase())) {
|
|
818
|
+
out[k] = "[REDACTED]";
|
|
819
|
+
} else {
|
|
820
|
+
out[k] = v;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return out;
|
|
824
|
+
}
|
|
825
|
+
function redactBody(body) {
|
|
826
|
+
if (body === null || body === undefined)
|
|
827
|
+
return body;
|
|
828
|
+
if (typeof body !== "object" || Array.isArray(body)) {
|
|
829
|
+
return body;
|
|
830
|
+
}
|
|
831
|
+
const rec = body;
|
|
832
|
+
const out = { ...rec };
|
|
833
|
+
for (const key of Object.keys(out)) {
|
|
834
|
+
if (SENSITIVE_BODY_KEYS.has(key.toLowerCase())) {
|
|
835
|
+
out[key] = "[REDACTED]";
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
840
|
+
function defaultRedactContext(ctx) {
|
|
841
|
+
return {
|
|
842
|
+
...ctx,
|
|
843
|
+
requestHeaders: redactHeaders(ctx.requestHeaders),
|
|
844
|
+
requestBody: redactBody(ctx.requestBody),
|
|
845
|
+
meta: ctx.meta
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/api-wrapper/error-reporting.ts
|
|
850
|
+
function applyErrorContextRedaction(ctx, config) {
|
|
851
|
+
const base = defaultRedactContext(ctx);
|
|
852
|
+
return config.redact ? config.redact(base) : base;
|
|
84
853
|
}
|
|
85
|
-
|
|
86
|
-
|
|
854
|
+
async function reportApiError(err, config) {
|
|
855
|
+
err.context = applyErrorContextRedaction(err.context, config);
|
|
856
|
+
try {
|
|
857
|
+
await config.onError?.(err);
|
|
858
|
+
} catch {}
|
|
859
|
+
config.logger?.error?.(err);
|
|
860
|
+
return err;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/api-wrapper/size.ts
|
|
864
|
+
function calculateSizeInMb(data) {
|
|
865
|
+
let bytes;
|
|
866
|
+
if (data === undefined || data === null) {
|
|
867
|
+
bytes = 0;
|
|
868
|
+
} else if (data instanceof Blob) {
|
|
869
|
+
bytes = data.size;
|
|
870
|
+
} else if (typeof FormData !== "undefined" && data instanceof FormData) {
|
|
871
|
+
bytes = 0;
|
|
872
|
+
data.forEach((value) => {
|
|
873
|
+
if (value instanceof Blob)
|
|
874
|
+
bytes += value.size;
|
|
875
|
+
else
|
|
876
|
+
bytes += new TextEncoder().encode(String(value)).length;
|
|
877
|
+
});
|
|
878
|
+
} else if (typeof data === "string") {
|
|
879
|
+
bytes = new TextEncoder().encode(data).length;
|
|
880
|
+
} else {
|
|
881
|
+
bytes = new TextEncoder().encode(JSON.stringify(data) ?? "").length;
|
|
882
|
+
}
|
|
883
|
+
return bytes / (1024 * 1024);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/api-wrapper/transport.ts
|
|
887
|
+
import { ZodError as ZodError3 } from "zod";
|
|
888
|
+
async function validateResponse(schema, data, ctx) {
|
|
87
889
|
try {
|
|
88
890
|
return await schema.parseAsync(data);
|
|
89
891
|
} catch (error) {
|
|
90
|
-
if (error instanceof
|
|
91
|
-
throw new ResponseValidationError(error, data);
|
|
892
|
+
if (error instanceof ZodError3) {
|
|
893
|
+
throw new ResponseValidationError(error, data, ctx());
|
|
92
894
|
}
|
|
93
895
|
throw error;
|
|
94
896
|
}
|
|
95
897
|
}
|
|
96
|
-
async function httpRequest(config) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => abortController.abort(), config.timeoutMs);
|
|
898
|
+
async function httpRequest(config, ctx) {
|
|
899
|
+
const timeoutController = new AbortController;
|
|
900
|
+
const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => timeoutController.abort(), config.timeoutMs);
|
|
901
|
+
const signal = mergeSignals(timeoutController.signal, config.userSignal);
|
|
101
902
|
try {
|
|
102
|
-
return await config.fetchFn(fullUrl, {
|
|
903
|
+
return await config.fetchFn(config.fullUrl, {
|
|
103
904
|
method: config.method,
|
|
104
905
|
headers: config.headers,
|
|
105
|
-
body: buildFetchBody(config.body, config.headers),
|
|
106
|
-
signal
|
|
906
|
+
body: buildFetchBody(config.body, config.headers, (msg) => new ConfigError(msg, ctx())),
|
|
907
|
+
signal
|
|
107
908
|
});
|
|
909
|
+
} catch (error) {
|
|
910
|
+
if (isDomAbortError(error)) {
|
|
911
|
+
if (config.userSignal?.aborted) {
|
|
912
|
+
throw new RequestAbortedError(ctx(), error);
|
|
913
|
+
}
|
|
914
|
+
if (timeoutController.signal.aborted && config.timeoutMs !== undefined) {
|
|
915
|
+
throw new RequestTimeoutError(config.timeoutMs, ctx());
|
|
916
|
+
}
|
|
917
|
+
throw new RequestAbortedError(ctx(), error);
|
|
918
|
+
}
|
|
919
|
+
if (error instanceof ConfigError)
|
|
920
|
+
throw error;
|
|
921
|
+
throw new NetworkError(error instanceof Error ? error.message : String(error), ctx(), error);
|
|
108
922
|
} finally {
|
|
109
923
|
if (timeout)
|
|
110
924
|
clearTimeout(timeout);
|
|
111
925
|
}
|
|
112
926
|
}
|
|
113
|
-
|
|
927
|
+
function isDomAbortError(error) {
|
|
928
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
929
|
+
}
|
|
930
|
+
function mergeSignals(a, b) {
|
|
931
|
+
if (!b)
|
|
932
|
+
return a;
|
|
933
|
+
const anyFn = AbortSignal.any;
|
|
934
|
+
if (typeof anyFn === "function")
|
|
935
|
+
return anyFn([a, b]);
|
|
936
|
+
const controller = new AbortController;
|
|
937
|
+
const onAbort = () => controller.abort();
|
|
938
|
+
if (a.aborted)
|
|
939
|
+
controller.abort();
|
|
940
|
+
else
|
|
941
|
+
a.addEventListener("abort", onAbort, { once: true });
|
|
942
|
+
if (b.aborted)
|
|
943
|
+
controller.abort();
|
|
944
|
+
else
|
|
945
|
+
b.addEventListener("abort", onAbort, { once: true });
|
|
946
|
+
return controller.signal;
|
|
947
|
+
}
|
|
948
|
+
async function readErrorResponse(response) {
|
|
949
|
+
const body = await response.text();
|
|
950
|
+
return {
|
|
951
|
+
body,
|
|
952
|
+
headers: getHeadersAsObject(response.headers),
|
|
953
|
+
status: response.status
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function getRetryDelay(params) {
|
|
957
|
+
const maxDelay = params.maxRetryDelayMs ?? 30000;
|
|
958
|
+
const { retryDelay, attempt, response } = params;
|
|
959
|
+
if (response && (response.status === 429 || response.status === 503)) {
|
|
960
|
+
const retryAfter = response.headers.get("retry-after");
|
|
961
|
+
if (retryAfter) {
|
|
962
|
+
const seconds = Number(retryAfter);
|
|
963
|
+
if (Number.isFinite(seconds)) {
|
|
964
|
+
return Math.min(Math.max(0, seconds * 1000), maxDelay);
|
|
965
|
+
}
|
|
966
|
+
const dateMs = Date.parse(retryAfter);
|
|
967
|
+
if (!Number.isNaN(dateMs)) {
|
|
968
|
+
return Math.min(Math.max(0, dateMs - Date.now()), maxDelay);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const delayMs = typeof retryDelay === "function" ? retryDelay(attempt) : retryDelay;
|
|
973
|
+
return Math.min(delayMs, maxDelay);
|
|
974
|
+
}
|
|
975
|
+
function shouldAttemptRetryOnError(error) {
|
|
976
|
+
return !isNonRetryableApiError(error);
|
|
977
|
+
}
|
|
978
|
+
function shouldAttemptRetryOnHttp(retryCtx) {
|
|
979
|
+
const { error, response } = retryCtx;
|
|
980
|
+
if (error)
|
|
981
|
+
return shouldAttemptRetryOnError(error);
|
|
982
|
+
return response ? response.status >= 500 || response.status === 429 : false;
|
|
983
|
+
}
|
|
984
|
+
async function makeRequestWithRetry(args) {
|
|
985
|
+
const {
|
|
986
|
+
config,
|
|
987
|
+
maxRetries,
|
|
988
|
+
retryDelay,
|
|
989
|
+
maxRetryDelayMs,
|
|
990
|
+
validateFn,
|
|
991
|
+
shouldRetry,
|
|
992
|
+
logger,
|
|
993
|
+
ctx,
|
|
994
|
+
endpointName,
|
|
995
|
+
method,
|
|
996
|
+
fullUrl,
|
|
997
|
+
meta,
|
|
998
|
+
onResponse,
|
|
999
|
+
onRetry
|
|
1000
|
+
} = args;
|
|
114
1001
|
let retryCount = 0;
|
|
115
1002
|
while (true) {
|
|
1003
|
+
let response;
|
|
116
1004
|
try {
|
|
117
1005
|
logger?.debug?.("API request", config);
|
|
118
|
-
|
|
1006
|
+
response = await httpRequest(config, (over) => ctx({ attempt: retryCount, ...over }));
|
|
1007
|
+
await onResponse?.({
|
|
1008
|
+
endpoint: endpointName,
|
|
1009
|
+
method,
|
|
1010
|
+
url: fullUrl,
|
|
1011
|
+
response: response.clone(),
|
|
1012
|
+
attempt: retryCount,
|
|
1013
|
+
meta
|
|
1014
|
+
});
|
|
119
1015
|
if (response.status >= 400) {
|
|
120
|
-
|
|
1016
|
+
const retryCtx = {
|
|
1017
|
+
response,
|
|
1018
|
+
retryCount,
|
|
1019
|
+
maxRetries,
|
|
1020
|
+
endpoint: endpointName,
|
|
1021
|
+
method,
|
|
1022
|
+
url: fullUrl
|
|
1023
|
+
};
|
|
1024
|
+
if (retryCount < maxRetries && shouldRetry(retryCtx)) {
|
|
1025
|
+
const delayMs = getRetryDelay({
|
|
1026
|
+
retryDelay,
|
|
1027
|
+
attempt: retryCount,
|
|
1028
|
+
response,
|
|
1029
|
+
maxRetryDelayMs
|
|
1030
|
+
});
|
|
1031
|
+
await onRetry?.({
|
|
1032
|
+
...retryCtx,
|
|
1033
|
+
delayMs,
|
|
1034
|
+
nextAttempt: retryCount + 1
|
|
1035
|
+
});
|
|
121
1036
|
retryCount++;
|
|
122
|
-
await delay(
|
|
1037
|
+
await delay(delayMs);
|
|
123
1038
|
continue;
|
|
124
1039
|
}
|
|
125
|
-
|
|
1040
|
+
const { body, headers: responseHeaders } = await readErrorResponse(response);
|
|
1041
|
+
throw new HttpResponseError(response.status, body, responseHeaders, ctx({ attempt: retryCount }));
|
|
126
1042
|
}
|
|
127
1043
|
return {
|
|
128
|
-
validatedData: await validateFn(response),
|
|
1044
|
+
validatedData: await validateFn(response, retryCount),
|
|
129
1045
|
retryCount,
|
|
130
1046
|
httpStatus: response.status
|
|
131
1047
|
};
|
|
132
1048
|
} catch (error) {
|
|
133
|
-
if (error
|
|
1049
|
+
if (isNonRetryableApiError(error)) {
|
|
134
1050
|
throw error;
|
|
135
|
-
|
|
1051
|
+
}
|
|
1052
|
+
const retryCtx = {
|
|
1053
|
+
error,
|
|
1054
|
+
retryCount,
|
|
1055
|
+
maxRetries,
|
|
1056
|
+
endpoint: endpointName,
|
|
1057
|
+
method,
|
|
1058
|
+
url: fullUrl
|
|
1059
|
+
};
|
|
1060
|
+
if (retryCount >= maxRetries || !shouldRetry(retryCtx)) {
|
|
136
1061
|
throw error;
|
|
137
1062
|
}
|
|
1063
|
+
const delayMs = getRetryDelay({
|
|
1064
|
+
retryDelay,
|
|
1065
|
+
attempt: retryCount,
|
|
1066
|
+
response: undefined,
|
|
1067
|
+
maxRetryDelayMs
|
|
1068
|
+
});
|
|
1069
|
+
await onRetry?.({
|
|
1070
|
+
...retryCtx,
|
|
1071
|
+
delayMs,
|
|
1072
|
+
nextAttempt: retryCount + 1
|
|
1073
|
+
});
|
|
138
1074
|
retryCount++;
|
|
139
|
-
await delay(
|
|
1075
|
+
await delay(delayMs);
|
|
140
1076
|
}
|
|
141
1077
|
}
|
|
142
1078
|
}
|
|
143
|
-
async function validateAndTransformResponse(endpoint, response) {
|
|
1079
|
+
async function validateAndTransformResponse(endpoint, response, ctx) {
|
|
144
1080
|
const responseHeaders = getHeadersAsObject(response.headers);
|
|
145
|
-
const
|
|
146
|
-
const
|
|
1081
|
+
const text = await response.text();
|
|
1082
|
+
const responseSizeBytes = new TextEncoder().encode(text).length;
|
|
1083
|
+
let responseData;
|
|
1084
|
+
if (hasJsonContentType(responseHeaders)) {
|
|
1085
|
+
try {
|
|
1086
|
+
responseData = text === "" ? undefined : JSON.parse(text);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
throw new ResponseValidationError(makeJsonParseZodError(error, text), text, ctx());
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
responseData = text;
|
|
1092
|
+
}
|
|
147
1093
|
const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
|
|
148
|
-
|
|
1094
|
+
const data = await validateResponse(endpoint.resSchema, transformedData, ctx);
|
|
1095
|
+
return { data, responseSizeBytes };
|
|
149
1096
|
}
|
|
150
|
-
function
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
1097
|
+
function defaultShouldRetry(ctx) {
|
|
1098
|
+
if (ctx.error)
|
|
1099
|
+
return shouldAttemptRetryOnError(ctx.error);
|
|
1100
|
+
return shouldAttemptRetryOnHttp(ctx);
|
|
1101
|
+
}
|
|
1102
|
+
function delay(ms) {
|
|
1103
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/api-wrapper/create-client.ts
|
|
1107
|
+
function isActionInvokeOptionsOnly(value) {
|
|
1108
|
+
if (value === null || value === undefined)
|
|
1109
|
+
return false;
|
|
1110
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
1111
|
+
return false;
|
|
1112
|
+
return Object.keys(value).every((k) => k === "signal" || k === "meta");
|
|
1113
|
+
}
|
|
1114
|
+
function resolveActionInvocation(action, actionName, first, second) {
|
|
1115
|
+
if (!action.noRuntimeInput) {
|
|
1116
|
+
return { input: first, options: second ?? {} };
|
|
158
1117
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new Error("At least one base API URL is required");
|
|
1118
|
+
if (first === undefined || first === null) {
|
|
1119
|
+
return { input: undefined, options: second ?? {} };
|
|
162
1120
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!configBaseUrl) {
|
|
166
|
-
throw new Error(`Unknown base API URL: ${baseUrlKey}`);
|
|
1121
|
+
if (second !== undefined) {
|
|
1122
|
+
return { input: undefined, options: second };
|
|
167
1123
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const parsedQuery = endpoint.reqQuerySchema ? endpoint.reqQuerySchema.parse(query) : query ?? {};
|
|
180
|
-
const merged = { ...defaultParams, ...parsedQuery };
|
|
181
|
-
return Object.fromEntries(Object.entries(merged).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
|
|
1124
|
+
if (isActionInvokeOptionsOnly(first)) {
|
|
1125
|
+
return { input: undefined, options: first };
|
|
1126
|
+
}
|
|
1127
|
+
throw new ConfigError(`Action "${actionName}" does not accept input; pass only optional { signal, meta }`, {
|
|
1128
|
+
endpoint: actionName,
|
|
1129
|
+
method: "GET",
|
|
1130
|
+
url: "",
|
|
1131
|
+
attempt: 0,
|
|
1132
|
+
maxRetries: 0,
|
|
1133
|
+
elapsedMs: 0
|
|
1134
|
+
});
|
|
182
1135
|
}
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
1136
|
+
function createApiClientImpl(apiConfig) {
|
|
1137
|
+
let actions = {};
|
|
1138
|
+
const cache = createActionCache();
|
|
1139
|
+
const invokeEndpoint = async (endpointKey, input = {}) => {
|
|
1140
|
+
const endpoint = apiConfig.endpoints[endpointKey];
|
|
1141
|
+
const rawInput = input;
|
|
1142
|
+
const endpointName = apiConfig.name ? `${apiConfig.name}.${String(endpointKey)}` : String(endpointKey);
|
|
1143
|
+
const startedAt = Date.now();
|
|
1144
|
+
const maxRetries = input.retries ?? apiConfig.retries ?? 3;
|
|
1145
|
+
const baseContext = {
|
|
1146
|
+
endpoint: endpointName,
|
|
1147
|
+
method: endpoint?.method ?? "GET",
|
|
1148
|
+
url: "",
|
|
1149
|
+
attempt: 0,
|
|
1150
|
+
maxRetries,
|
|
1151
|
+
elapsedMs: 0,
|
|
1152
|
+
meta: input.meta
|
|
1153
|
+
};
|
|
1154
|
+
const elapsed = () => Date.now() - startedAt;
|
|
1155
|
+
const ctx = (over = {}) => ({
|
|
1156
|
+
...baseContext,
|
|
1157
|
+
elapsedMs: elapsed(),
|
|
1158
|
+
...over
|
|
1159
|
+
});
|
|
1160
|
+
if (!endpoint) {
|
|
1161
|
+
throw await reportApiError(new ConfigError(`Unknown API endpoint: ${String(endpointKey)}`, ctx()), apiConfig);
|
|
1162
|
+
}
|
|
1163
|
+
baseContext.method = endpoint.method;
|
|
1164
|
+
let pathParams;
|
|
1165
|
+
let queryParams;
|
|
1166
|
+
let body;
|
|
1167
|
+
let headers;
|
|
1168
|
+
try {
|
|
1169
|
+
pathParams = buildPathParams(endpoint, rawInput.reqParams, ctx);
|
|
1170
|
+
queryParams = buildQueryParams(endpoint, rawInput.reqQuery, ctx);
|
|
1171
|
+
body = buildRequestBody(endpoint, rawInput.reqBody, ctx);
|
|
1172
|
+
headers = buildHeaders(endpoint, rawInput.reqHeaders, apiConfig.defaultHeaders ?? {}, ctx);
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
if (e instanceof ApiError)
|
|
1175
|
+
throw await reportApiError(e, apiConfig);
|
|
1176
|
+
throw e;
|
|
1177
|
+
}
|
|
1178
|
+
let fullUrl;
|
|
1179
|
+
try {
|
|
1180
|
+
const built = buildUrl({
|
|
1181
|
+
endpoint,
|
|
1182
|
+
params: pathParams,
|
|
1183
|
+
queryParams,
|
|
1184
|
+
baseApiUrls: apiConfig.baseApiUrls,
|
|
1185
|
+
defaultBaseApiUrl: apiConfig.defaultBaseApiUrl,
|
|
1186
|
+
url: input.url,
|
|
1187
|
+
doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
|
|
1188
|
+
configError: (msg) => new ConfigError(msg, ctx())
|
|
1189
|
+
});
|
|
1190
|
+
fullUrl = built.fullUrl;
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
if (e instanceof ApiError)
|
|
1193
|
+
throw await reportApiError(e, apiConfig);
|
|
1194
|
+
throw e;
|
|
1195
|
+
}
|
|
1196
|
+
baseContext.url = fullUrl;
|
|
1197
|
+
baseContext.requestHeaders = headers;
|
|
1198
|
+
baseContext.requestBody = body;
|
|
1199
|
+
if (apiConfig.onRequest) {
|
|
1200
|
+
try {
|
|
1201
|
+
const result = await apiConfig.onRequest({
|
|
1202
|
+
endpoint: endpointName,
|
|
1203
|
+
method: endpoint.method,
|
|
1204
|
+
url: fullUrl,
|
|
1205
|
+
headers,
|
|
1206
|
+
body,
|
|
1207
|
+
meta: input.meta
|
|
1208
|
+
});
|
|
1209
|
+
if (result?.headers)
|
|
1210
|
+
headers = result.headers;
|
|
1211
|
+
if (result && "body" in result)
|
|
1212
|
+
body = result.body;
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
if (e instanceof ApiError)
|
|
1215
|
+
throw await reportApiError(e, apiConfig);
|
|
1216
|
+
throw e;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const config = {
|
|
1220
|
+
method: endpoint.method,
|
|
1221
|
+
fullUrl,
|
|
1222
|
+
body,
|
|
1223
|
+
headers,
|
|
1224
|
+
timeoutMs: input.timeoutMs ?? apiConfig.timeoutMs,
|
|
1225
|
+
fetchFn: input.fetch ?? apiConfig.fetch ?? fetch,
|
|
1226
|
+
userSignal: input.signal
|
|
1227
|
+
};
|
|
1228
|
+
try {
|
|
1229
|
+
const result = await makeRequestWithRetry({
|
|
1230
|
+
config,
|
|
1231
|
+
maxRetries,
|
|
1232
|
+
retryDelay: apiConfig.retryDelayMs ?? 300,
|
|
1233
|
+
maxRetryDelayMs: apiConfig.maxRetryDelayMs,
|
|
1234
|
+
validateFn: (response, attempt) => validateAndTransformResponse(endpoint, response, (over) => ctx({ attempt, ...over })),
|
|
1235
|
+
shouldRetry: apiConfig.shouldRetry ?? defaultShouldRetry,
|
|
1236
|
+
logger: apiConfig.logger,
|
|
1237
|
+
ctx,
|
|
1238
|
+
endpointName,
|
|
1239
|
+
method: endpoint.method,
|
|
1240
|
+
fullUrl,
|
|
1241
|
+
meta: input.meta,
|
|
1242
|
+
onResponse: apiConfig.onResponse,
|
|
1243
|
+
onRetry: apiConfig.onRetry
|
|
1244
|
+
});
|
|
1245
|
+
const responseTimeMs = elapsed();
|
|
1246
|
+
const responseSizeMb = result.validatedData.responseSizeBytes / (1024 * 1024);
|
|
1247
|
+
const requestBodySizeMb = calculateSizeInMb(config.body);
|
|
1248
|
+
return {
|
|
1249
|
+
data: result.validatedData.data,
|
|
1250
|
+
requestBodySizeMb,
|
|
1251
|
+
responseSizeMb,
|
|
1252
|
+
responseTimeMs,
|
|
1253
|
+
httpStatus: result.httpStatus,
|
|
1254
|
+
retryCount: result.retryCount
|
|
1255
|
+
};
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
if (error instanceof ApiError)
|
|
1258
|
+
throw await reportApiError(error, apiConfig);
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
async function invokeAction(name, rawInput, options = {}) {
|
|
1263
|
+
const action = actions[name];
|
|
1264
|
+
const inputForHandler = action.noRuntimeInput ? undefined : rawInput;
|
|
1265
|
+
const actionName = apiConfig.name ? `${apiConfig.name}.${name}` : name;
|
|
1266
|
+
const startedAt = Date.now();
|
|
1267
|
+
const baseContext = {
|
|
1268
|
+
endpoint: actionName,
|
|
1269
|
+
method: "GET",
|
|
1270
|
+
url: "",
|
|
1271
|
+
attempt: 0,
|
|
1272
|
+
maxRetries: 0,
|
|
1273
|
+
elapsedMs: 0,
|
|
1274
|
+
meta: options.meta,
|
|
1275
|
+
requestBody: action.noRuntimeInput ? undefined : rawInput
|
|
1276
|
+
};
|
|
1277
|
+
const ctx = (over = {}) => ({
|
|
1278
|
+
...baseContext,
|
|
1279
|
+
elapsedMs: Date.now() - startedAt,
|
|
1280
|
+
...over
|
|
1281
|
+
});
|
|
1282
|
+
apiConfig.logger?.debug?.("Action start", { action: actionName });
|
|
1283
|
+
try {
|
|
1284
|
+
const result = await action.handler({
|
|
1285
|
+
input: inputForHandler,
|
|
1286
|
+
api: proxy,
|
|
1287
|
+
signal: options.signal,
|
|
1288
|
+
meta: options.meta,
|
|
1289
|
+
logger: apiConfig.logger,
|
|
1290
|
+
cache
|
|
1291
|
+
});
|
|
1292
|
+
apiConfig.logger?.debug?.("Action end", {
|
|
1293
|
+
action: actionName,
|
|
1294
|
+
elapsedMs: Date.now() - startedAt
|
|
1295
|
+
});
|
|
1296
|
+
return result;
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
if (error instanceof ApiError) {
|
|
1299
|
+
throw await reportApiError(error, apiConfig);
|
|
1300
|
+
}
|
|
1301
|
+
const wrapped = new ActionError(`Action ${actionName} failed: ${error instanceof Error ? error.message : String(error)}`, ctx(), error);
|
|
1302
|
+
throw await reportApiError(wrapped, apiConfig);
|
|
1303
|
+
}
|
|
189
1304
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
formData.set(key, value instanceof Blob ? value : String(value));
|
|
1305
|
+
async function dispatchAction(name, first, second) {
|
|
1306
|
+
const action = actions[name];
|
|
1307
|
+
const { input, options } = resolveActionInvocation(action, name, first, second);
|
|
1308
|
+
return invokeAction(name, input, options);
|
|
195
1309
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
...defaultHeaders,
|
|
203
|
-
...contentType && { "Content-Type": contentType },
|
|
204
|
-
...endpoint.defaultHeaders,
|
|
205
|
-
...parsedHeaders
|
|
1310
|
+
const request = async (key, input, options) => {
|
|
1311
|
+
if (key in actions) {
|
|
1312
|
+
return dispatchAction(key, input, options);
|
|
1313
|
+
}
|
|
1314
|
+
return invokeEndpoint(key, input ?? {});
|
|
206
1315
|
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (value === undefined) {
|
|
221
|
-
throw new Error(`Missing required path parameter: ${key}`);
|
|
1316
|
+
const client = { request, cache };
|
|
1317
|
+
const proxy = new Proxy(client, {
|
|
1318
|
+
get(target, prop, receiver) {
|
|
1319
|
+
if (prop in target || typeof prop !== "string") {
|
|
1320
|
+
return Reflect.get(target, prop, receiver);
|
|
1321
|
+
}
|
|
1322
|
+
if (prop in apiConfig.endpoints) {
|
|
1323
|
+
return (input) => invokeEndpoint(prop, input ?? {});
|
|
1324
|
+
}
|
|
1325
|
+
if (prop in actions) {
|
|
1326
|
+
return (first, second) => dispatchAction(prop, first, second);
|
|
1327
|
+
}
|
|
1328
|
+
return;
|
|
222
1329
|
}
|
|
223
|
-
return encodeURIComponent(String(value));
|
|
224
1330
|
});
|
|
1331
|
+
const rawActions = apiConfig.actions;
|
|
1332
|
+
if (typeof rawActions === "function") {
|
|
1333
|
+
actions = rawActions({
|
|
1334
|
+
api: proxy,
|
|
1335
|
+
defineAction: makeDefineAction()
|
|
1336
|
+
});
|
|
1337
|
+
} else if (rawActions) {
|
|
1338
|
+
actions = rawActions;
|
|
1339
|
+
}
|
|
1340
|
+
const collisions = Object.keys(actions).filter((key) => (key in apiConfig.endpoints));
|
|
1341
|
+
if (collisions.length > 0) {
|
|
1342
|
+
throw new ConfigError(`Action name(s) collide with endpoint name(s): ${collisions.join(", ")}`, {
|
|
1343
|
+
endpoint: collisions[0],
|
|
1344
|
+
method: "GET",
|
|
1345
|
+
url: "",
|
|
1346
|
+
attempt: 0,
|
|
1347
|
+
maxRetries: 0,
|
|
1348
|
+
elapsedMs: 0
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
return proxy;
|
|
225
1352
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
1353
|
+
var createApiClient = (apiConfig) => createApiClientImpl(apiConfig);
|
|
1354
|
+
// src/secrets/infisical-cli.ts
|
|
1355
|
+
async function readInfisicalCliToken(options) {
|
|
1356
|
+
const { execFile } = await import("node:child_process").catch(() => {
|
|
1357
|
+
throw new SecretLoadError("CLI", "Infisical CLI fallback requires a runtime with subprocess support. Provide Infisical client credentials or a custom secret loader.");
|
|
230
1358
|
});
|
|
231
|
-
|
|
1359
|
+
options.signal.throwIfAborted();
|
|
1360
|
+
const env = {
|
|
1361
|
+
...getProcessEnvironment(),
|
|
1362
|
+
INFISICAL_DISABLE_UPDATE_CHECK: "true"
|
|
1363
|
+
};
|
|
1364
|
+
delete env.INFISICAL_TOKEN;
|
|
1365
|
+
delete env.INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN;
|
|
1366
|
+
delete env.TOKEN;
|
|
1367
|
+
try {
|
|
1368
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
1369
|
+
const child = execFile("infisical", [
|
|
1370
|
+
"user",
|
|
1371
|
+
"get",
|
|
1372
|
+
"token",
|
|
1373
|
+
"--plain",
|
|
1374
|
+
`--domain=${options.siteUrl}`,
|
|
1375
|
+
"--silent"
|
|
1376
|
+
], {
|
|
1377
|
+
env,
|
|
1378
|
+
encoding: "utf8",
|
|
1379
|
+
shell: false,
|
|
1380
|
+
timeout: options.timeoutMs ?? 30000,
|
|
1381
|
+
signal: options.signal,
|
|
1382
|
+
killSignal: "SIGKILL",
|
|
1383
|
+
maxBuffer: 10485760,
|
|
1384
|
+
windowsHide: true
|
|
1385
|
+
}, (error, output) => {
|
|
1386
|
+
if (error) {
|
|
1387
|
+
reject(error);
|
|
1388
|
+
} else {
|
|
1389
|
+
resolve(output);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
child.stdin?.end();
|
|
1393
|
+
});
|
|
1394
|
+
return stdout.trim();
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
if (options.signal.aborted) {
|
|
1397
|
+
throw options.signal.reason;
|
|
1398
|
+
}
|
|
1399
|
+
const failure = error;
|
|
1400
|
+
if (failure?.code === "ENOENT") {
|
|
1401
|
+
throw new SecretLoadError("CLI", "Infisical CLI not found. Install infisical and ensure it is on PATH, or provide Infisical client credentials.");
|
|
1402
|
+
}
|
|
1403
|
+
if (failure?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
1404
|
+
throw new SecretLoadError("CLI", "Infisical CLI output exceeded the 10 MiB limit.");
|
|
1405
|
+
}
|
|
1406
|
+
if (failure?.killed) {
|
|
1407
|
+
throw new SecretLoadError("TIMEOUT", `Infisical CLI timed out after ${(options.timeoutMs ?? 30000) / 1000} seconds or was terminated.`);
|
|
1408
|
+
}
|
|
1409
|
+
throw new SecretLoadError("CLI", `Infisical CLI session token failed. Run infisical login --domain=${options.siteUrl} and verify access to the configured organization, project, and environment.`);
|
|
1410
|
+
}
|
|
232
1411
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
1412
|
+
|
|
1413
|
+
// src/secrets/infisical-config.ts
|
|
1414
|
+
function validateInfisicalScope(options) {
|
|
1415
|
+
validateInfisicalOrganizationId(options.organizationId);
|
|
1416
|
+
let url;
|
|
1417
|
+
try {
|
|
1418
|
+
url = new URL(options.siteUrl);
|
|
1419
|
+
} catch {
|
|
1420
|
+
throw new SecretLoadError("CONFIGURATION", "Invalid Infisical siteUrl.");
|
|
1421
|
+
}
|
|
1422
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
1423
|
+
throw new SecretLoadError("CONFIGURATION", "Infisical siteUrl must be an HTTP(S) URL without credentials, query, or fragment.");
|
|
1424
|
+
}
|
|
1425
|
+
if (!options.projectId.trim() || !options.environment.trim() || !(options.secretPath ?? "/").startsWith("/")) {
|
|
1426
|
+
throw new SecretLoadError("CONFIGURATION", "Infisical requires a projectId, environment, and absolute secretPath.");
|
|
1427
|
+
}
|
|
236
1428
|
}
|
|
237
|
-
function
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
1429
|
+
function validateInfisicalOrganizationId(organizationId) {
|
|
1430
|
+
if (typeof organizationId !== "string" || !organizationId.trim() || /\s/.test(organizationId)) {
|
|
1431
|
+
throw new SecretLoadError("CONFIGURATION", "Infisical organizationId is required and must be a nonempty ID without whitespace.");
|
|
1432
|
+
}
|
|
241
1433
|
}
|
|
242
|
-
|
|
243
|
-
|
|
1434
|
+
|
|
1435
|
+
// src/secrets/infisical-session.ts
|
|
1436
|
+
import { z } from "zod";
|
|
1437
|
+
var sessionClaimsSchema = z.object({
|
|
1438
|
+
organizationId: z.string().optional(),
|
|
1439
|
+
subOrganizationId: z.string().optional(),
|
|
1440
|
+
exp: z.number().finite().optional()
|
|
1441
|
+
});
|
|
1442
|
+
function parseInfisicalSessionToken(token) {
|
|
1443
|
+
let claims;
|
|
1444
|
+
try {
|
|
1445
|
+
if (!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) {
|
|
1446
|
+
throw new Error("Invalid token format");
|
|
1447
|
+
}
|
|
1448
|
+
const payload = token.split(".")[1];
|
|
1449
|
+
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
1450
|
+
claims = sessionClaimsSchema.parse(JSON.parse(json));
|
|
1451
|
+
} catch {
|
|
1452
|
+
throw new SecretLoadError("AUTHENTICATION", "Infisical returned an invalid user session token. Run infisical login for the configured domain.");
|
|
1453
|
+
}
|
|
1454
|
+
if (claims.exp !== undefined && claims.exp <= Date.now() / 1000) {
|
|
1455
|
+
throw new SecretLoadError("AUTHENTICATION", "Infisical user session has expired. Run infisical login for the configured domain.");
|
|
1456
|
+
}
|
|
1457
|
+
return { organizationId: claims.subOrganizationId ?? claims.organizationId };
|
|
244
1458
|
}
|
|
245
1459
|
|
|
246
1460
|
// src/secrets/infisical.ts
|
|
247
|
-
|
|
1461
|
+
var DEFAULT_INFISICAL_SITE_URL = "https://eu.infisical.com";
|
|
248
1462
|
var defaultFetch = globalThis.fetch;
|
|
249
|
-
var infisicalAuthResponseSchema =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
secretKey: z.string(),
|
|
254
|
-
secretValue: z.string()
|
|
1463
|
+
var infisicalAuthResponseSchema = z2.object({ accessToken: z2.string() });
|
|
1464
|
+
var infisicalSecretSchema = z2.object({
|
|
1465
|
+
secretKey: z2.string(),
|
|
1466
|
+
secretValue: z2.string()
|
|
255
1467
|
});
|
|
256
|
-
var infisicalSecretsResponseSchema =
|
|
257
|
-
secrets:
|
|
1468
|
+
var infisicalSecretsResponseSchema = z2.object({
|
|
1469
|
+
secrets: z2.array(infisicalSecretSchema)
|
|
258
1470
|
});
|
|
259
|
-
async function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
1471
|
+
async function fetchInfisicalSecrets(options) {
|
|
1472
|
+
validateInfisicalScope(options);
|
|
1473
|
+
return withSecretDeadline(options, async (signal) => {
|
|
1474
|
+
let operation = "login";
|
|
1475
|
+
try {
|
|
1476
|
+
const infisical = createInfisicalApiClient(options.siteUrl);
|
|
1477
|
+
const { data: auth } = await infisical.request("login", {
|
|
1478
|
+
signal,
|
|
1479
|
+
reqBody: {
|
|
1480
|
+
clientId: options.clientId,
|
|
1481
|
+
clientSecret: options.clientSecret
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
signal.throwIfAborted();
|
|
1485
|
+
operation = "listSecrets";
|
|
1486
|
+
return await listInfisicalSecrets(infisical, auth.accessToken, {
|
|
1487
|
+
...options,
|
|
1488
|
+
signal
|
|
1489
|
+
});
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
throwInfisicalHttpError(error, operation, signal);
|
|
277
1492
|
}
|
|
278
1493
|
});
|
|
1494
|
+
}
|
|
1495
|
+
async function loadInfisicalSecrets(options) {
|
|
1496
|
+
validateInfisicalScope(options);
|
|
1497
|
+
const env = getProcessEnvironment();
|
|
1498
|
+
const clientId = env?.[options.clientIdEnvKey];
|
|
1499
|
+
const clientSecret = env?.[options.clientSecretEnvKey];
|
|
1500
|
+
const mode = options.auth ?? "auto";
|
|
1501
|
+
if (!["auto", "http", "cli"].includes(mode)) {
|
|
1502
|
+
throw new SecretLoadError("CONFIGURATION", "Invalid Infisical auth mode.");
|
|
1503
|
+
}
|
|
1504
|
+
const useHttp = mode === "http" || mode === "auto" && Boolean(clientId && clientSecret);
|
|
1505
|
+
if (useHttp) {
|
|
1506
|
+
if (!clientId || !clientSecret) {
|
|
1507
|
+
throw new SecretLoadError("AUTHENTICATION", "Infisical HTTP authentication requires both client credentials.");
|
|
1508
|
+
}
|
|
1509
|
+
options.reportSource?.("Infisical HTTP");
|
|
1510
|
+
return fetchInfisicalSecrets({ ...options, clientId, clientSecret });
|
|
1511
|
+
}
|
|
1512
|
+
options.reportSource?.(`Infisical CLI session (organization=${options.organizationId} project=${options.projectId} environment=${options.environment})`);
|
|
1513
|
+
return withSecretDeadline(options, (signal) => fetchInfisicalSessionSecrets({ ...options, signal }));
|
|
1514
|
+
}
|
|
1515
|
+
async function fetchInfisicalSessionSecrets(options) {
|
|
1516
|
+
const { signal } = options;
|
|
1517
|
+
let token = await readInfisicalCliToken(options);
|
|
1518
|
+
signal.throwIfAborted();
|
|
1519
|
+
const session = parseInfisicalSessionToken(token);
|
|
1520
|
+
const infisical = createInfisicalApiClient(options.siteUrl);
|
|
1521
|
+
let operation = "selectOrganization";
|
|
1522
|
+
try {
|
|
1523
|
+
if (session.organizationId !== options.organizationId) {
|
|
1524
|
+
const { data } = await infisical.request("selectOrganization", {
|
|
1525
|
+
signal,
|
|
1526
|
+
reqHeaders: {
|
|
1527
|
+
Authorization: `Bearer ${token}`,
|
|
1528
|
+
"User-Agent": "hanzio/secrets"
|
|
1529
|
+
},
|
|
1530
|
+
reqBody: { organizationId: options.organizationId }
|
|
1531
|
+
});
|
|
1532
|
+
signal.throwIfAborted();
|
|
1533
|
+
if (data.isMfaEnabled) {
|
|
1534
|
+
throw new SecretLoadError("AUTHENTICATION", "Infisical requires MFA for this organization. Complete infisical login for the target organization and configured domain before loading secrets.", { operation });
|
|
1535
|
+
}
|
|
1536
|
+
const scopedSession = parseInfisicalSessionToken(data.token);
|
|
1537
|
+
if (scopedSession.organizationId !== options.organizationId) {
|
|
1538
|
+
throw new SecretLoadError("AUTHENTICATION", "Infisical returned a session for a different organization.", { operation });
|
|
1539
|
+
}
|
|
1540
|
+
token = data.token;
|
|
1541
|
+
}
|
|
1542
|
+
signal.throwIfAborted();
|
|
1543
|
+
operation = "listSecrets";
|
|
1544
|
+
return await listInfisicalSecrets(infisical, token, options);
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
throwInfisicalHttpError(error, operation, signal);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
async function listInfisicalSecrets(infisical, token, options) {
|
|
279
1550
|
const { data } = await infisical.request("listSecrets", {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
},
|
|
1551
|
+
signal: options.signal,
|
|
1552
|
+
reqHeaders: { Authorization: `Bearer ${token}` },
|
|
283
1553
|
reqQuery: {
|
|
284
|
-
environment,
|
|
285
|
-
projectId,
|
|
286
|
-
secretPath: "/",
|
|
1554
|
+
environment: options.environment,
|
|
1555
|
+
projectId: options.projectId,
|
|
1556
|
+
secretPath: options.secretPath ?? "/",
|
|
287
1557
|
viewSecretValue: true,
|
|
288
1558
|
expandSecretReferences: true,
|
|
289
1559
|
includeImports: true
|
|
290
1560
|
}
|
|
291
1561
|
});
|
|
292
|
-
const wantedKeys = new Set(keys);
|
|
1562
|
+
const wantedKeys = new Set(options.keys);
|
|
293
1563
|
return Object.fromEntries(data.secrets.filter((secret) => wantedKeys.has(secret.secretKey)).map((secret) => [secret.secretKey, secret.secretValue]));
|
|
294
1564
|
}
|
|
1565
|
+
function throwInfisicalHttpError(error, operation, signal) {
|
|
1566
|
+
if (signal.aborted) {
|
|
1567
|
+
throw signal.reason;
|
|
1568
|
+
}
|
|
1569
|
+
if (error instanceof SecretLoadError) {
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
const status = error instanceof HttpResponseError ? error.status : undefined;
|
|
1573
|
+
const guidance = operation === "selectOrganization" && (status === 401 || status === 403) ? " Run infisical login for the target organization and configured domain, then verify your access." : "";
|
|
1574
|
+
throw new SecretLoadError("HTTP", `Infisical HTTP ${operation} failed${status ? ` (status ${status})` : ""}.${guidance}`, { operation, status });
|
|
1575
|
+
}
|
|
295
1576
|
function createInfisicalApiClient(siteUrl) {
|
|
296
1577
|
return createApiClient({
|
|
297
1578
|
name: "infisical",
|
|
@@ -302,12 +1583,26 @@ function createInfisicalApiClient(siteUrl) {
|
|
|
302
1583
|
fetch: getFetch(),
|
|
303
1584
|
retries: 1,
|
|
304
1585
|
endpoints: {
|
|
1586
|
+
selectOrganization: {
|
|
1587
|
+
method: "POST",
|
|
1588
|
+
path: "/api/v3/auth/select-organization",
|
|
1589
|
+
reqHeadersSchema: z2.object({
|
|
1590
|
+
Authorization: z2.string(),
|
|
1591
|
+
"User-Agent": z2.string()
|
|
1592
|
+
}),
|
|
1593
|
+
reqBodySchema: z2.object({ organizationId: z2.string() }),
|
|
1594
|
+
resSchema: z2.object({
|
|
1595
|
+
token: z2.string().min(1),
|
|
1596
|
+
isMfaEnabled: z2.boolean().optional()
|
|
1597
|
+
}),
|
|
1598
|
+
reqBodyFormat: "json"
|
|
1599
|
+
},
|
|
305
1600
|
login: {
|
|
306
1601
|
method: "POST",
|
|
307
1602
|
path: "/api/v1/auth/universal-auth/login",
|
|
308
|
-
reqBodySchema:
|
|
309
|
-
clientId:
|
|
310
|
-
clientSecret:
|
|
1603
|
+
reqBodySchema: z2.object({
|
|
1604
|
+
clientId: z2.string(),
|
|
1605
|
+
clientSecret: z2.string()
|
|
311
1606
|
}),
|
|
312
1607
|
resSchema: infisicalAuthResponseSchema,
|
|
313
1608
|
reqBodyFormat: "json"
|
|
@@ -315,16 +1610,16 @@ function createInfisicalApiClient(siteUrl) {
|
|
|
315
1610
|
listSecrets: {
|
|
316
1611
|
method: "GET",
|
|
317
1612
|
path: "/api/v4/secrets",
|
|
318
|
-
reqHeadersSchema:
|
|
319
|
-
Authorization:
|
|
1613
|
+
reqHeadersSchema: z2.object({
|
|
1614
|
+
Authorization: z2.string()
|
|
320
1615
|
}),
|
|
321
|
-
reqQuerySchema:
|
|
322
|
-
projectId:
|
|
323
|
-
environment:
|
|
324
|
-
secretPath:
|
|
325
|
-
viewSecretValue:
|
|
326
|
-
expandSecretReferences:
|
|
327
|
-
includeImports:
|
|
1616
|
+
reqQuerySchema: z2.object({
|
|
1617
|
+
projectId: z2.string(),
|
|
1618
|
+
environment: z2.string(),
|
|
1619
|
+
secretPath: z2.string(),
|
|
1620
|
+
viewSecretValue: z2.boolean(),
|
|
1621
|
+
expandSecretReferences: z2.boolean(),
|
|
1622
|
+
includeImports: z2.boolean()
|
|
328
1623
|
}),
|
|
329
1624
|
resSchema: infisicalSecretsResponseSchema
|
|
330
1625
|
}
|
|
@@ -332,215 +1627,123 @@ function createInfisicalApiClient(siteUrl) {
|
|
|
332
1627
|
});
|
|
333
1628
|
}
|
|
334
1629
|
function getFetch() {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
function getViteDefine(secretSet) {
|
|
340
|
-
return Object.fromEntries(Object.entries(secretSet.secrets()).map(([key, value]) => [
|
|
341
|
-
`import.meta.env.${key}`,
|
|
342
|
-
JSON.stringify(value)
|
|
343
|
-
]));
|
|
344
|
-
}
|
|
345
|
-
function viteSecretSetPlugin(secretSet) {
|
|
346
|
-
return {
|
|
347
|
-
name: "hanzio-secret-set",
|
|
348
|
-
config: () => ({
|
|
349
|
-
define: getViteDefine(secretSet)
|
|
350
|
-
})
|
|
1630
|
+
const fetchFn = globalThis.fetch ?? defaultFetch;
|
|
1631
|
+
return (input, init) => {
|
|
1632
|
+
init?.signal?.throwIfAborted();
|
|
1633
|
+
return fetchFn(input, init);
|
|
351
1634
|
};
|
|
352
1635
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
red: "\x1B[31m",
|
|
363
|
-
green: "\x1B[32m",
|
|
364
|
-
yellow: "\x1B[33m",
|
|
365
|
-
blue: "\x1B[34m",
|
|
366
|
-
magenta: "\x1B[35m",
|
|
367
|
-
cyan: "\x1B[36m",
|
|
368
|
-
white: "\x1B[37m",
|
|
369
|
-
gray: "\x1B[90m",
|
|
370
|
-
brightRed: "\x1B[91m",
|
|
371
|
-
brightGreen: "\x1B[92m",
|
|
372
|
-
brightYellow: "\x1B[93m",
|
|
373
|
-
brightBlue: "\x1B[94m",
|
|
374
|
-
brightMagenta: "\x1B[95m",
|
|
375
|
-
brightCyan: "\x1B[96m",
|
|
376
|
-
brightWhite: "\x1B[97m",
|
|
377
|
-
bgBlack: "\x1B[40m",
|
|
378
|
-
bgRed: "\x1B[41m",
|
|
379
|
-
bgGreen: "\x1B[42m",
|
|
380
|
-
bgYellow: "\x1B[43m",
|
|
381
|
-
bgBlue: "\x1B[44m",
|
|
382
|
-
bgMagenta: "\x1B[45m",
|
|
383
|
-
bgCyan: "\x1B[46m",
|
|
384
|
-
bgWhite: "\x1B[47m"
|
|
385
|
-
};
|
|
386
|
-
var levelColors = {
|
|
387
|
-
debug: { badge: terminalColors.gray, text: terminalColors.dim },
|
|
388
|
-
info: { badge: terminalColors.cyan, text: terminalColors.reset },
|
|
389
|
-
warn: { badge: terminalColors.yellow, text: terminalColors.yellow },
|
|
390
|
-
error: { badge: terminalColors.red, text: terminalColors.brightRed }
|
|
391
|
-
};
|
|
392
|
-
function formatAttributes(attrs) {
|
|
393
|
-
if (!attrs || Object.keys(attrs).length === 0)
|
|
394
|
-
return "";
|
|
395
|
-
const formatted = Object.entries(attrs).map(([k, v]) => `${terminalColors.dim}${k}=${terminalColors.reset}${v}`).join(" ");
|
|
396
|
-
return ` ${formatted}`;
|
|
397
|
-
}
|
|
398
|
-
function createCoolLogger(options = {}) {
|
|
399
|
-
const { colorsInDevOnly = true, onLog } = options;
|
|
400
|
-
const isDev = true;
|
|
401
|
-
const useColors = colorsInDevOnly ? isDev : true;
|
|
402
|
-
const createLogMethod = (level) => (message, attributes) => {
|
|
403
|
-
onLog?.(level, message, attributes);
|
|
404
|
-
if (level === "debug" && !isDev)
|
|
405
|
-
return;
|
|
406
|
-
if (useColors) {
|
|
407
|
-
const { badge, text } = levelColors[level];
|
|
408
|
-
const label = level.toUpperCase().padEnd(5);
|
|
409
|
-
const attrStr = formatAttributes(attributes);
|
|
410
|
-
console.log(`${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`);
|
|
411
|
-
} else {
|
|
412
|
-
console[level](`[${level.toUpperCase()}]`, message, attributes ?? "");
|
|
1636
|
+
// src/secrets/worker-loader.ts
|
|
1637
|
+
function cloudflareWorkerEnvLoader(workerEnv) {
|
|
1638
|
+
const loader = async (ctx) => {
|
|
1639
|
+
const out = Object.create(null);
|
|
1640
|
+
for (const key of ctx.keys) {
|
|
1641
|
+
const str = workerBindingToString(Object.hasOwn(workerEnv, key) ? workerEnv[key] : undefined);
|
|
1642
|
+
if (str !== undefined) {
|
|
1643
|
+
out[key] = str;
|
|
1644
|
+
}
|
|
413
1645
|
}
|
|
1646
|
+
return out;
|
|
414
1647
|
};
|
|
415
|
-
return {
|
|
416
|
-
debug: createLogMethod("debug"),
|
|
417
|
-
info: createLogMethod("info"),
|
|
418
|
-
warn: createLogMethod("warn"),
|
|
419
|
-
error: createLogMethod("error")
|
|
420
|
-
};
|
|
1648
|
+
return Object.assign(loader, { source: "Cloudflare Worker bindings" });
|
|
421
1649
|
}
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
const isDev = true;
|
|
425
|
-
if (devOnly && !isDev)
|
|
426
|
-
return;
|
|
427
|
-
const c = terminalColors;
|
|
428
|
-
const statusIcon = success ? `${c.brightGreen}✓${c.reset}` : `${c.brightRed}✗${c.reset}`;
|
|
429
|
-
const statusColor = success ? c.green : c.red;
|
|
430
|
-
const durationStr = `${c.dim}${durationMs.toFixed(1)}ms${c.reset}`;
|
|
431
|
-
const errorMessage = error ? ` ${c.dim}·${c.reset} ${c.red}${error instanceof Error ? error.message : String(error)}${c.reset}` : "";
|
|
432
|
-
console.log(`${statusColor}${c.bold}${operationName}${c.reset} ${statusIcon} ${c.dim}·${c.reset} ${durationStr}${errorMessage}`);
|
|
433
|
-
}
|
|
434
|
-
function colorize(text, color, ...modifiers) {
|
|
435
|
-
const isDev = true;
|
|
436
|
-
if (!isDev)
|
|
437
|
-
return text;
|
|
438
|
-
const colorCodes = [
|
|
439
|
-
terminalColors[color],
|
|
440
|
-
...modifiers.map((m) => terminalColors[m])
|
|
441
|
-
].join("");
|
|
442
|
-
return `${colorCodes}${text}${terminalColors.reset}`;
|
|
443
|
-
}
|
|
444
|
-
function logBanner(text, color = "cyan", options = {}) {
|
|
445
|
-
const { devOnly = true } = options;
|
|
446
|
-
const isDev = true;
|
|
447
|
-
if (devOnly && !isDev) {
|
|
448
|
-
console.log(`=== ${text} ===`);
|
|
1650
|
+
function workerBindingToString(value) {
|
|
1651
|
+
if (value === undefined || value === null) {
|
|
449
1652
|
return;
|
|
450
1653
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
1654
|
+
if (typeof value === "string") {
|
|
1655
|
+
return value === "" ? undefined : value;
|
|
1656
|
+
}
|
|
1657
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
1658
|
+
return String(value);
|
|
1659
|
+
}
|
|
1660
|
+
if (typeof value === "object") {
|
|
1661
|
+
return JSON.stringify(value);
|
|
1662
|
+
}
|
|
1663
|
+
return String(value);
|
|
457
1664
|
}
|
|
458
1665
|
|
|
459
|
-
// src/secrets/
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1666
|
+
// src/secrets/loaders.ts
|
|
1667
|
+
function infisicalLoader(config) {
|
|
1668
|
+
validateInfisicalOrganizationId(config.organizationId);
|
|
1669
|
+
const loader = async (ctx) => fetchInfisicalSecrets({
|
|
1670
|
+
keys: ctx.keys,
|
|
1671
|
+
projectId: config.projectId,
|
|
1672
|
+
organizationId: config.organizationId,
|
|
1673
|
+
environment: ctx.environment,
|
|
1674
|
+
siteUrl: config.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
|
|
1675
|
+
clientId: config.clientId,
|
|
1676
|
+
clientSecret: config.clientSecret,
|
|
1677
|
+
secretPath: config.secretPath,
|
|
1678
|
+
timeoutMs: config.timeoutMs ?? ctx.timeoutMs,
|
|
1679
|
+
signal: ctx.signal
|
|
1680
|
+
});
|
|
1681
|
+
return Object.assign(loader, { source: "Infisical HTTP" });
|
|
469
1682
|
}
|
|
470
|
-
async function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
clientIdEnvKey: options.clientIdEnvKey ?? "INFISICAL_CLIENT_ID",
|
|
485
|
-
clientSecretEnvKey: options.clientSecretEnvKey ?? "INFISICAL_CLIENT_SECRET"
|
|
486
|
-
});
|
|
487
|
-
const missingKeys = keySet.filter((key) => !(envSecrets[key] ?? loadedSecrets[key]));
|
|
488
|
-
if (missingKeys.length > 0) {
|
|
489
|
-
throw new Error(`Missing secrets: ${missingKeys.join(", ")}`);
|
|
490
|
-
}
|
|
491
|
-
const nextSecrets = Object.fromEntries(keySet.map((key) => [
|
|
492
|
-
key,
|
|
493
|
-
envSecrets[key] ?? loadedSecrets[key]
|
|
494
|
-
]));
|
|
495
|
-
for (const [key, value] of Object.entries(nextSecrets)) {
|
|
496
|
-
process.env[key] = value;
|
|
1683
|
+
async function processEnvLoader(_ctx) {
|
|
1684
|
+
return {};
|
|
1685
|
+
}
|
|
1686
|
+
Object.assign(processEnvLoader, { source: "process.env" });
|
|
1687
|
+
// src/secrets/vite.ts
|
|
1688
|
+
function getViteDefine(secretSet, options = {}) {
|
|
1689
|
+
const values = secretSet.secrets();
|
|
1690
|
+
const keys = options.publicKeys ?? Object.keys(values);
|
|
1691
|
+
if (!options.publicKeys && keys.some((key) => !key.startsWith("VITE_"))) {
|
|
1692
|
+
throw new SecretLoadError("CONFIGURATION", "Vite secret sets must contain only VITE_ keys, or provide an explicit publicKeys list.");
|
|
1693
|
+
}
|
|
1694
|
+
for (const key of keys) {
|
|
1695
|
+
if (!Object.hasOwn(values, key) || !/^[A-Za-z_$][\w$]*$/.test(key)) {
|
|
1696
|
+
throw new SecretLoadError("CONFIGURATION", "Vite publicKeys must reference configured keys with valid JavaScript identifiers.");
|
|
497
1697
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
};
|
|
501
|
-
const secretSet = {
|
|
502
|
-
secret(key) {
|
|
503
|
-
const value = cachedSecrets?.[key] ?? process.env[key];
|
|
504
|
-
if (!value) {
|
|
505
|
-
throw new Error(`Secret ${key} not found`);
|
|
506
|
-
}
|
|
507
|
-
return value;
|
|
508
|
-
},
|
|
509
|
-
secrets() {
|
|
510
|
-
if (!cachedSecrets) {
|
|
511
|
-
throw new Error("Secrets not initialized.");
|
|
512
|
-
}
|
|
513
|
-
return cachedSecrets;
|
|
514
|
-
},
|
|
515
|
-
async reload() {
|
|
516
|
-
await loadSecrets(true);
|
|
517
|
-
return secretSet;
|
|
1698
|
+
if (["MODE", "BASE_URL", "PROD", "DEV", "SSR"].includes(key)) {
|
|
1699
|
+
throw new SecretLoadError("CONFIGURATION", "Vite built-in environment constants cannot be overridden by secrets.");
|
|
518
1700
|
}
|
|
1701
|
+
}
|
|
1702
|
+
return Object.fromEntries(keys.map((key) => [`import.meta.env.${key}`, JSON.stringify(values[key])]));
|
|
1703
|
+
}
|
|
1704
|
+
function viteSecretSetPlugin(secretSet, options = {}) {
|
|
1705
|
+
return {
|
|
1706
|
+
name: "hanzio-secret-set",
|
|
1707
|
+
config: () => ({ define: getViteDefine(secretSet, options) })
|
|
519
1708
|
};
|
|
520
|
-
await loadSecrets();
|
|
521
|
-
return secretSet;
|
|
522
1709
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return
|
|
1710
|
+
|
|
1711
|
+
// src/secrets/index.ts
|
|
1712
|
+
function getSecretEnvironment() {
|
|
1713
|
+
return resolveSecretEnvironment(undefined, getProcessEnvironment());
|
|
1714
|
+
}
|
|
1715
|
+
async function defineSecretSet2(keys, options) {
|
|
1716
|
+
let loader;
|
|
1717
|
+
if (!options.loader) {
|
|
1718
|
+
validateInfisicalOrganizationId(options.organizationId);
|
|
1719
|
+
loader = createDefaultInfisicalLoader(options);
|
|
1720
|
+
} else {
|
|
1721
|
+
loader = options.loader;
|
|
1722
|
+
}
|
|
1723
|
+
return defineSecretSet(keys, { ...options, loader }, getProcessEnvironment());
|
|
527
1724
|
}
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1725
|
+
function createDefaultInfisicalLoader(options) {
|
|
1726
|
+
return (context) => loadInfisicalSecrets({
|
|
1727
|
+
...context,
|
|
1728
|
+
projectId: options.projectId,
|
|
1729
|
+
organizationId: options.organizationId,
|
|
1730
|
+
siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
|
|
1731
|
+
secretPath: options.secretPath,
|
|
1732
|
+
auth: options.auth,
|
|
1733
|
+
clientIdEnvKey: options.clientIdEnvKey ?? "INFISICAL_CLIENT_ID",
|
|
1734
|
+
clientSecretEnvKey: options.clientSecretEnvKey ?? "INFISICAL_CLIENT_SECRET"
|
|
532
1735
|
});
|
|
533
1736
|
}
|
|
534
|
-
function readEnvSecrets(keys, previousSecrets = null) {
|
|
535
|
-
return Object.fromEntries(keys.filter((key) => {
|
|
536
|
-
const value = process.env[key];
|
|
537
|
-
return value && value !== previousSecrets?.[key];
|
|
538
|
-
}).map((key) => [key, process.env[key]]));
|
|
539
|
-
}
|
|
540
1737
|
export {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1738
|
+
DEFAULT_INFISICAL_SITE_URL,
|
|
1739
|
+
SecretLoadError,
|
|
1740
|
+
cloudflareWorkerEnvLoader,
|
|
1741
|
+
defineSecretSet2 as defineSecretSet,
|
|
1742
|
+
fetchInfisicalSecrets,
|
|
544
1743
|
getSecretEnvironment,
|
|
545
|
-
|
|
1744
|
+
getViteDefine,
|
|
1745
|
+
infisicalLoader,
|
|
1746
|
+
loadInfisicalSecrets,
|
|
1747
|
+
processEnvLoader,
|
|
1748
|
+
viteSecretSetPlugin
|
|
546
1749
|
};
|