hanzio 1.2.0 → 2.1.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 +786 -239
- package/dist/api-wrapper/builders.d.ts +4 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +39 -2
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +4 -5
- package/dist/api-wrapper/types.d.ts +103 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +19 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/index-hyt6c56h.js +290 -0
- package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-ktynw3vs.js +37 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-w882zry3.js +29 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/secrets/core.d.ts +7 -0
- package/dist/secrets/index.d.ts +3 -1
- package/dist/secrets/public-config.d.ts +7 -0
- package/dist/secrets/vite.d.ts +1 -0
- package/dist/secrets/worker.d.ts +1 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1130
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +739 -3145
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +50 -1428
- package/dist/src/secrets/vite.js +10 -39
- package/dist/src/secrets/worker.js +14 -273
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +340 -167
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +31 -24
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +78 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +32 -25
- package/src/api-wrapper/create-client.ts +171 -41
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +107 -2
- package/src/api-wrapper/headers.ts +11 -6
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +42 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +41 -43
- package/src/api-wrapper/types.ts +185 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +132 -147
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/secrets/core.ts +72 -16
- package/src/secrets/index.ts +16 -4
- package/src/secrets/public-config.ts +47 -0
- package/src/secrets/vite.ts +2 -0
- package/src/secrets/worker.ts +2 -0
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +58 -38
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/loaders.js +0 -1529
- package/dist/zod/index.js +0 -210
|
@@ -1,1360 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
HttpResponseError2,
|
|
3
|
+
createApiClient2
|
|
4
|
+
} from "../../index-j1ybktbr.js";
|
|
5
|
+
import {
|
|
6
|
+
getViteDefine2,
|
|
7
|
+
viteSecretSetPlugin2
|
|
8
|
+
} from "../../index-w882zry3.js";
|
|
9
|
+
import {
|
|
10
|
+
withSecretDeadline,
|
|
11
|
+
getProcessEnvironment,
|
|
12
|
+
resolveSecretEnvironment,
|
|
13
|
+
createSecretSet2,
|
|
14
|
+
cloudflareWorkerEnvLoader2
|
|
15
|
+
} from "../../index-hyt6c56h.js";
|
|
16
|
+
import {
|
|
17
|
+
SecretLoadError2,
|
|
18
|
+
readPublicConfig2
|
|
19
|
+
} from "../../index-ktynw3vs.js";
|
|
242
20
|
|
|
243
21
|
// src/secrets/infisical.ts
|
|
244
22
|
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
|
|
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;
|
|
441
|
-
|
|
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 {
|
|
489
|
-
zodError;
|
|
490
|
-
rawResponse;
|
|
491
|
-
issues;
|
|
492
|
-
validationIssues;
|
|
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 });
|
|
498
|
-
this.name = "ResponseValidationError";
|
|
499
|
-
this.zodError = zodError;
|
|
500
|
-
this.rawResponse = rawResponse;
|
|
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
|
-
};
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
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
|
-
};
|
|
539
|
-
}
|
|
540
|
-
}
|
|
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");
|
|
617
|
-
}
|
|
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) {
|
|
649
|
-
try {
|
|
650
|
-
parsedQuery = endpoint.reqQuerySchema.parse(query);
|
|
651
|
-
} catch (error) {
|
|
652
|
-
if (error instanceof ZodError2) {
|
|
653
|
-
throw new RequestValidationError(error, "query", query, ctx());
|
|
654
|
-
}
|
|
655
|
-
throw error;
|
|
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
23
|
|
|
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();
|
|
770
|
-
};
|
|
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;
|
|
853
|
-
}
|
|
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) {
|
|
889
|
-
try {
|
|
890
|
-
return await schema.parseAsync(data);
|
|
891
|
-
} catch (error) {
|
|
892
|
-
if (error instanceof ZodError3) {
|
|
893
|
-
throw new ResponseValidationError(error, data, ctx());
|
|
894
|
-
}
|
|
895
|
-
throw error;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
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);
|
|
902
|
-
try {
|
|
903
|
-
return await config.fetchFn(config.fullUrl, {
|
|
904
|
-
method: config.method,
|
|
905
|
-
headers: config.headers,
|
|
906
|
-
body: buildFetchBody(config.body, config.headers, (msg) => new ConfigError(msg, ctx())),
|
|
907
|
-
signal
|
|
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);
|
|
922
|
-
} finally {
|
|
923
|
-
if (timeout)
|
|
924
|
-
clearTimeout(timeout);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
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;
|
|
1001
|
-
let retryCount = 0;
|
|
1002
|
-
while (true) {
|
|
1003
|
-
let response;
|
|
1004
|
-
try {
|
|
1005
|
-
logger?.debug?.("API request", config);
|
|
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
|
-
});
|
|
1015
|
-
if (response.status >= 400) {
|
|
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
|
-
});
|
|
1036
|
-
retryCount++;
|
|
1037
|
-
await delay(delayMs);
|
|
1038
|
-
continue;
|
|
1039
|
-
}
|
|
1040
|
-
const { body, headers: responseHeaders } = await readErrorResponse(response);
|
|
1041
|
-
throw new HttpResponseError(response.status, body, responseHeaders, ctx({ attempt: retryCount }));
|
|
1042
|
-
}
|
|
1043
|
-
return {
|
|
1044
|
-
validatedData: await validateFn(response, retryCount),
|
|
1045
|
-
retryCount,
|
|
1046
|
-
httpStatus: response.status
|
|
1047
|
-
};
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
if (isNonRetryableApiError(error)) {
|
|
1050
|
-
throw error;
|
|
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)) {
|
|
1061
|
-
throw error;
|
|
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
|
-
});
|
|
1074
|
-
retryCount++;
|
|
1075
|
-
await delay(delayMs);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
async function validateAndTransformResponse(endpoint, response, ctx) {
|
|
1080
|
-
const responseHeaders = getHeadersAsObject(response.headers);
|
|
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
|
-
}
|
|
1093
|
-
const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
|
|
1094
|
-
const data = await validateResponse(endpoint.resSchema, transformedData, ctx);
|
|
1095
|
-
return { data, responseSizeBytes };
|
|
1096
|
-
}
|
|
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 ?? {} };
|
|
1117
|
-
}
|
|
1118
|
-
if (first === undefined || first === null) {
|
|
1119
|
-
return { input: undefined, options: second ?? {} };
|
|
1120
|
-
}
|
|
1121
|
-
if (second !== undefined) {
|
|
1122
|
-
return { input: undefined, options: second };
|
|
1123
|
-
}
|
|
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
|
-
});
|
|
1135
|
-
}
|
|
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
|
-
}
|
|
1304
|
-
}
|
|
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);
|
|
1309
|
-
}
|
|
1310
|
-
const request = async (key, input, options) => {
|
|
1311
|
-
if (key in actions) {
|
|
1312
|
-
return dispatchAction(key, input, options);
|
|
1313
|
-
}
|
|
1314
|
-
return invokeEndpoint(key, input ?? {});
|
|
1315
|
-
};
|
|
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;
|
|
1329
|
-
}
|
|
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;
|
|
1352
|
-
}
|
|
1353
|
-
var createApiClient = (apiConfig) => createApiClientImpl(apiConfig);
|
|
1354
24
|
// src/secrets/infisical-cli.ts
|
|
1355
25
|
async function readInfisicalCliToken(options) {
|
|
1356
26
|
const { execFile } = await import("node:child_process").catch(() => {
|
|
1357
|
-
throw new
|
|
27
|
+
throw new SecretLoadError2("CLI", "Infisical CLI fallback requires a runtime with subprocess support. Provide Infisical client credentials or a custom secret loader.");
|
|
1358
28
|
});
|
|
1359
29
|
options.signal.throwIfAborted();
|
|
1360
30
|
const env = {
|
|
@@ -1398,15 +68,15 @@ async function readInfisicalCliToken(options) {
|
|
|
1398
68
|
}
|
|
1399
69
|
const failure = error;
|
|
1400
70
|
if (failure?.code === "ENOENT") {
|
|
1401
|
-
throw new
|
|
71
|
+
throw new SecretLoadError2("CLI", "Infisical CLI not found. Install infisical and ensure it is on PATH, or provide Infisical client credentials.");
|
|
1402
72
|
}
|
|
1403
73
|
if (failure?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
1404
|
-
throw new
|
|
74
|
+
throw new SecretLoadError2("CLI", "Infisical CLI output exceeded the 10 MiB limit.");
|
|
1405
75
|
}
|
|
1406
76
|
if (failure?.killed) {
|
|
1407
|
-
throw new
|
|
77
|
+
throw new SecretLoadError2("TIMEOUT", `Infisical CLI timed out after ${(options.timeoutMs ?? 30000) / 1000} seconds or was terminated.`);
|
|
1408
78
|
}
|
|
1409
|
-
throw new
|
|
79
|
+
throw new SecretLoadError2("CLI", `Infisical CLI session token failed. Run infisical login --domain=${options.siteUrl} and verify access to the configured organization, project, and environment.`);
|
|
1410
80
|
}
|
|
1411
81
|
}
|
|
1412
82
|
|
|
@@ -1417,18 +87,18 @@ function validateInfisicalScope(options) {
|
|
|
1417
87
|
try {
|
|
1418
88
|
url = new URL(options.siteUrl);
|
|
1419
89
|
} catch {
|
|
1420
|
-
throw new
|
|
90
|
+
throw new SecretLoadError2("CONFIGURATION", "Invalid Infisical siteUrl.");
|
|
1421
91
|
}
|
|
1422
92
|
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
1423
|
-
throw new
|
|
93
|
+
throw new SecretLoadError2("CONFIGURATION", "Infisical siteUrl must be an HTTP(S) URL without credentials, query, or fragment.");
|
|
1424
94
|
}
|
|
1425
95
|
if (!options.projectId.trim() || !options.environment.trim() || !(options.secretPath ?? "/").startsWith("/")) {
|
|
1426
|
-
throw new
|
|
96
|
+
throw new SecretLoadError2("CONFIGURATION", "Infisical requires a projectId, environment, and absolute secretPath.");
|
|
1427
97
|
}
|
|
1428
98
|
}
|
|
1429
99
|
function validateInfisicalOrganizationId(organizationId) {
|
|
1430
100
|
if (typeof organizationId !== "string" || !organizationId.trim() || /\s/.test(organizationId)) {
|
|
1431
|
-
throw new
|
|
101
|
+
throw new SecretLoadError2("CONFIGURATION", "Infisical organizationId is required and must be a nonempty ID without whitespace.");
|
|
1432
102
|
}
|
|
1433
103
|
}
|
|
1434
104
|
|
|
@@ -1449,10 +119,10 @@ function parseInfisicalSessionToken(token) {
|
|
|
1449
119
|
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
1450
120
|
claims = sessionClaimsSchema.parse(JSON.parse(json));
|
|
1451
121
|
} catch {
|
|
1452
|
-
throw new
|
|
122
|
+
throw new SecretLoadError2("AUTHENTICATION", "Infisical returned an invalid user session token. Run infisical login for the configured domain.");
|
|
1453
123
|
}
|
|
1454
124
|
if (claims.exp !== undefined && claims.exp <= Date.now() / 1000) {
|
|
1455
|
-
throw new
|
|
125
|
+
throw new SecretLoadError2("AUTHENTICATION", "Infisical user session has expired. Run infisical login for the configured domain.");
|
|
1456
126
|
}
|
|
1457
127
|
return { organizationId: claims.subOrganizationId ?? claims.organizationId };
|
|
1458
128
|
}
|
|
@@ -1499,12 +169,12 @@ async function loadInfisicalSecrets(options) {
|
|
|
1499
169
|
const clientSecret = env?.[options.clientSecretEnvKey];
|
|
1500
170
|
const mode = options.auth ?? "auto";
|
|
1501
171
|
if (!["auto", "http", "cli"].includes(mode)) {
|
|
1502
|
-
throw new
|
|
172
|
+
throw new SecretLoadError2("CONFIGURATION", "Invalid Infisical auth mode.");
|
|
1503
173
|
}
|
|
1504
174
|
const useHttp = mode === "http" || mode === "auto" && Boolean(clientId && clientSecret);
|
|
1505
175
|
if (useHttp) {
|
|
1506
176
|
if (!clientId || !clientSecret) {
|
|
1507
|
-
throw new
|
|
177
|
+
throw new SecretLoadError2("AUTHENTICATION", "Infisical HTTP authentication requires both client credentials.");
|
|
1508
178
|
}
|
|
1509
179
|
options.reportSource?.("Infisical HTTP");
|
|
1510
180
|
return fetchInfisicalSecrets({ ...options, clientId, clientSecret });
|
|
@@ -1531,11 +201,11 @@ async function fetchInfisicalSessionSecrets(options) {
|
|
|
1531
201
|
});
|
|
1532
202
|
signal.throwIfAborted();
|
|
1533
203
|
if (data.isMfaEnabled) {
|
|
1534
|
-
throw new
|
|
204
|
+
throw new SecretLoadError2("AUTHENTICATION", "Infisical requires MFA for this organization. Complete infisical login for the target organization and configured domain before loading secrets.", { operation });
|
|
1535
205
|
}
|
|
1536
206
|
const scopedSession = parseInfisicalSessionToken(data.token);
|
|
1537
207
|
if (scopedSession.organizationId !== options.organizationId) {
|
|
1538
|
-
throw new
|
|
208
|
+
throw new SecretLoadError2("AUTHENTICATION", "Infisical returned a session for a different organization.", { operation });
|
|
1539
209
|
}
|
|
1540
210
|
token = data.token;
|
|
1541
211
|
}
|
|
@@ -1566,15 +236,15 @@ function throwInfisicalHttpError(error, operation, signal) {
|
|
|
1566
236
|
if (signal.aborted) {
|
|
1567
237
|
throw signal.reason;
|
|
1568
238
|
}
|
|
1569
|
-
if (error instanceof
|
|
239
|
+
if (error instanceof SecretLoadError2) {
|
|
1570
240
|
throw error;
|
|
1571
241
|
}
|
|
1572
|
-
const status = error instanceof
|
|
242
|
+
const status = error instanceof HttpResponseError2 ? error.status : undefined;
|
|
1573
243
|
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
|
|
244
|
+
throw new SecretLoadError2("HTTP", `Infisical HTTP ${operation} failed${status ? ` (status ${status})` : ""}.${guidance}`, { operation, status });
|
|
1575
245
|
}
|
|
1576
246
|
function createInfisicalApiClient(siteUrl) {
|
|
1577
|
-
return
|
|
247
|
+
return createApiClient2({
|
|
1578
248
|
name: "infisical",
|
|
1579
249
|
baseApiUrls: {
|
|
1580
250
|
infisical: siteUrl
|
|
@@ -1633,36 +303,6 @@ function getFetch() {
|
|
|
1633
303
|
return fetchFn(input, init);
|
|
1634
304
|
};
|
|
1635
305
|
}
|
|
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
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
return out;
|
|
1647
|
-
};
|
|
1648
|
-
return Object.assign(loader, { source: "Cloudflare Worker bindings" });
|
|
1649
|
-
}
|
|
1650
|
-
function workerBindingToString(value) {
|
|
1651
|
-
if (value === undefined || value === null) {
|
|
1652
|
-
return;
|
|
1653
|
-
}
|
|
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);
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
306
|
// src/secrets/loaders.ts
|
|
1667
307
|
function infisicalLoader(config) {
|
|
1668
308
|
validateInfisicalOrganizationId(config.organizationId);
|
|
@@ -1684,35 +324,12 @@ async function processEnvLoader(_ctx) {
|
|
|
1684
324
|
return {};
|
|
1685
325
|
}
|
|
1686
326
|
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.");
|
|
1697
|
-
}
|
|
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.");
|
|
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) })
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
327
|
|
|
1711
328
|
// src/secrets/index.ts
|
|
1712
329
|
function getSecretEnvironment() {
|
|
1713
330
|
return resolveSecretEnvironment(undefined, getProcessEnvironment());
|
|
1714
331
|
}
|
|
1715
|
-
|
|
332
|
+
function createSecretSet(keys, options) {
|
|
1716
333
|
let loader;
|
|
1717
334
|
if (!options.loader) {
|
|
1718
335
|
validateInfisicalOrganizationId(options.organizationId);
|
|
@@ -1720,7 +337,7 @@ async function defineSecretSet2(keys, options) {
|
|
|
1720
337
|
} else {
|
|
1721
338
|
loader = options.loader;
|
|
1722
339
|
}
|
|
1723
|
-
return
|
|
340
|
+
return createSecretSet2(keys, { ...options, loader }, getProcessEnvironment());
|
|
1724
341
|
}
|
|
1725
342
|
function createDefaultInfisicalLoader(options) {
|
|
1726
343
|
return (context) => loadInfisicalSecrets({
|
|
@@ -1734,16 +351,21 @@ function createDefaultInfisicalLoader(options) {
|
|
|
1734
351
|
clientSecretEnvKey: options.clientSecretEnvKey ?? "INFISICAL_CLIENT_SECRET"
|
|
1735
352
|
});
|
|
1736
353
|
}
|
|
354
|
+
async function defineSecretSet(keys, options) {
|
|
355
|
+
return createSecretSet(keys, options).load();
|
|
356
|
+
}
|
|
1737
357
|
export {
|
|
1738
358
|
DEFAULT_INFISICAL_SITE_URL,
|
|
1739
|
-
SecretLoadError,
|
|
1740
|
-
cloudflareWorkerEnvLoader,
|
|
1741
|
-
|
|
359
|
+
SecretLoadError2 as SecretLoadError,
|
|
360
|
+
cloudflareWorkerEnvLoader2 as cloudflareWorkerEnvLoader,
|
|
361
|
+
createSecretSet,
|
|
362
|
+
defineSecretSet,
|
|
1742
363
|
fetchInfisicalSecrets,
|
|
1743
364
|
getSecretEnvironment,
|
|
1744
|
-
getViteDefine,
|
|
365
|
+
getViteDefine2 as getViteDefine,
|
|
1745
366
|
infisicalLoader,
|
|
1746
367
|
loadInfisicalSecrets,
|
|
1747
368
|
processEnvLoader,
|
|
1748
|
-
|
|
369
|
+
readPublicConfig2 as readPublicConfig,
|
|
370
|
+
viteSecretSetPlugin2 as viteSecretSetPlugin
|
|
1749
371
|
};
|