hanzio 1.0.7 → 1.0.9
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 +8 -16
- package/dist/index.d.ts +0 -1
- package/dist/jwt/index.d.ts +3 -3
- package/dist/secrets/index.d.ts +1 -0
- package/dist/secrets/infisical.d.ts +2 -0
- package/dist/src/index.js +190 -47
- package/dist/src/jwt/index.js +98 -7
- package/dist/src/secrets/index.js +338 -26
- package/dist/src/secrets/infisical.js +339 -0
- package/package.json +22 -13
- package/src/index.ts +0 -1
- package/src/jwt/index.ts +158 -12
- package/src/secrets/index.ts +2 -43
- package/src/secrets/infisical.ts +108 -0
- package/src/gzip/index.ts +0 -29
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// src/api-wrapper/index.ts
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
|
|
4
|
+
class ResponseValidationError extends Error {
|
|
5
|
+
zodError;
|
|
6
|
+
rawResponse;
|
|
7
|
+
validationIssues;
|
|
8
|
+
constructor(zodError, rawResponse) {
|
|
9
|
+
const issues = zodError.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
10
|
+
super(`Response validation failed: ${issues}`);
|
|
11
|
+
this.name = "ResponseValidationError";
|
|
12
|
+
this.zodError = zodError;
|
|
13
|
+
this.rawResponse = rawResponse;
|
|
14
|
+
this.validationIssues = issues;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class HttpResponseError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
body;
|
|
21
|
+
constructor(status, body) {
|
|
22
|
+
super(`HTTP error: ${status} - ${body}`);
|
|
23
|
+
this.name = "HttpResponseError";
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.body = body;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function createApiClient(apiConfig) {
|
|
29
|
+
const request = async (endpointKey, input = {}) => {
|
|
30
|
+
const endpoint = apiConfig.endpoints[endpointKey];
|
|
31
|
+
if (!endpoint) {
|
|
32
|
+
throw new Error(`Unknown API endpoint: ${String(endpointKey)}`);
|
|
33
|
+
}
|
|
34
|
+
const { finalUrl, fullUrl } = buildUrl(endpoint, input.reqParams, input.reqQuery, apiConfig.baseApiUrls, apiConfig.defaultBaseApiUrl, input.url);
|
|
35
|
+
const endpointName = apiConfig.name ? `${apiConfig.name}.${String(endpointKey)}` : String(endpointKey);
|
|
36
|
+
const startedAt = Date.now();
|
|
37
|
+
let retryCount = 0;
|
|
38
|
+
let httpStatus = 0;
|
|
39
|
+
apiConfig.onRequestStart?.({ endpoint: endpointName, url: fullUrl });
|
|
40
|
+
try {
|
|
41
|
+
const config = {
|
|
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
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
apiConfig.logger?.error?.(error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return { request };
|
|
84
|
+
}
|
|
85
|
+
var createApiWrapper = createApiClient;
|
|
86
|
+
async function validateResponse(schema, data) {
|
|
87
|
+
try {
|
|
88
|
+
return await schema.parseAsync(data);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof ZodError) {
|
|
91
|
+
throw new ResponseValidationError(error, data);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function httpRequest(config) {
|
|
97
|
+
const queryString = config.doNotEncodeQueryParams ? Object.entries(config.queryParams).map(([key, value]) => `${key}=${value}`).join("&") : new URLSearchParams(config.queryParams).toString();
|
|
98
|
+
const fullUrl = queryString ? `${config.url}?${queryString}` : config.url;
|
|
99
|
+
const abortController = new AbortController;
|
|
100
|
+
const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => abortController.abort(), config.timeoutMs);
|
|
101
|
+
try {
|
|
102
|
+
return await config.fetchFn(fullUrl, {
|
|
103
|
+
method: config.method,
|
|
104
|
+
headers: config.headers,
|
|
105
|
+
body: buildFetchBody(config.body, config.headers),
|
|
106
|
+
signal: abortController.signal
|
|
107
|
+
});
|
|
108
|
+
} finally {
|
|
109
|
+
if (timeout)
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function makeRequestWithRetry(config, maxRetries, retryDelayMs, validateFn, shouldRetry, logger) {
|
|
114
|
+
let retryCount = 0;
|
|
115
|
+
while (true) {
|
|
116
|
+
try {
|
|
117
|
+
logger?.debug?.("API request", config);
|
|
118
|
+
const response = await httpRequest(config);
|
|
119
|
+
if (response.status >= 400) {
|
|
120
|
+
if (retryCount < maxRetries && shouldRetry({ response, retryCount, maxRetries })) {
|
|
121
|
+
retryCount++;
|
|
122
|
+
await delay(retryDelayMs);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
throw new HttpResponseError(response.status, await response.text());
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
validatedData: await validateFn(response),
|
|
129
|
+
retryCount,
|
|
130
|
+
httpStatus: response.status
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof HttpResponseError)
|
|
134
|
+
throw error;
|
|
135
|
+
if (retryCount >= maxRetries || !shouldRetry({ error, retryCount, maxRetries })) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
retryCount++;
|
|
139
|
+
await delay(retryDelayMs);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function validateAndTransformResponse(endpoint, response) {
|
|
144
|
+
const responseHeaders = getHeadersAsObject(response.headers);
|
|
145
|
+
const contentType = responseHeaders["content-type"] ?? "";
|
|
146
|
+
const responseData = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
147
|
+
const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
|
|
148
|
+
return validateResponse(endpoint.resSchema, transformedData);
|
|
149
|
+
}
|
|
150
|
+
function buildUrl(endpoint, params, reqQuery, baseApiUrls, defaultBaseApiUrl, url) {
|
|
151
|
+
if (url) {
|
|
152
|
+
const finalUrl2 = replacePathParams(url, params);
|
|
153
|
+
const queryString2 = new URLSearchParams(buildQueryParams(endpoint, reqQuery)).toString();
|
|
154
|
+
return {
|
|
155
|
+
finalUrl: finalUrl2,
|
|
156
|
+
fullUrl: queryString2 ? `${finalUrl2}?${queryString2}` : finalUrl2
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0];
|
|
160
|
+
if (!defaultBaseUrl) {
|
|
161
|
+
throw new Error("At least one base API URL is required");
|
|
162
|
+
}
|
|
163
|
+
const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl;
|
|
164
|
+
const configBaseUrl = baseApiUrls[baseUrlKey];
|
|
165
|
+
if (!configBaseUrl) {
|
|
166
|
+
throw new Error(`Unknown base API URL: ${baseUrlKey}`);
|
|
167
|
+
}
|
|
168
|
+
const baseUrl = typeof configBaseUrl === "function" ? configBaseUrl() : configBaseUrl;
|
|
169
|
+
const path = typeof endpoint.path === "function" ? endpoint.path(baseUrl) : endpoint.path;
|
|
170
|
+
const finalUrl = replacePathParams(`${baseUrl}${path}`, params);
|
|
171
|
+
const queryString = new URLSearchParams(buildQueryParams(endpoint, reqQuery)).toString();
|
|
172
|
+
return {
|
|
173
|
+
finalUrl,
|
|
174
|
+
fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function buildQueryParams(endpoint, query) {
|
|
178
|
+
const defaultParams = endpoint.reqDefaultQueryParams ?? {};
|
|
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)]));
|
|
182
|
+
}
|
|
183
|
+
function buildRequestBody(endpoint, body) {
|
|
184
|
+
if (!endpoint.reqBodySchema)
|
|
185
|
+
return body;
|
|
186
|
+
const parsedBody = endpoint.reqBodySchema.parse(body);
|
|
187
|
+
if (endpoint.reqBodyFormat !== "form-data") {
|
|
188
|
+
return parsedBody;
|
|
189
|
+
}
|
|
190
|
+
const formData = new FormData;
|
|
191
|
+
for (const [key, value] of Object.entries(parsedBody)) {
|
|
192
|
+
if (value == null)
|
|
193
|
+
continue;
|
|
194
|
+
formData.set(key, value instanceof Blob ? value : String(value));
|
|
195
|
+
}
|
|
196
|
+
return formData;
|
|
197
|
+
}
|
|
198
|
+
function buildHeaders(endpoint, headers, defaultHeaders) {
|
|
199
|
+
const contentType = endpoint.reqBodyFormat === "json" ? "application/json" : undefined;
|
|
200
|
+
const parsedHeaders = endpoint.reqHeadersSchema ? endpoint.reqHeadersSchema.parse(headers) : headers ?? {};
|
|
201
|
+
return {
|
|
202
|
+
...defaultHeaders,
|
|
203
|
+
...contentType && { "Content-Type": contentType },
|
|
204
|
+
...endpoint.defaultHeaders,
|
|
205
|
+
...parsedHeaders
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildFetchBody(body, headers) {
|
|
209
|
+
if (body === undefined || body === null)
|
|
210
|
+
return;
|
|
211
|
+
if (body instanceof FormData || body instanceof Blob)
|
|
212
|
+
return body;
|
|
213
|
+
if (headers["Content-Type"] === "application/json")
|
|
214
|
+
return JSON.stringify(body);
|
|
215
|
+
return body;
|
|
216
|
+
}
|
|
217
|
+
function replacePathParams(path, params) {
|
|
218
|
+
return path.replace(/:(\w+)/g, (_, key) => {
|
|
219
|
+
const value = params?.[key];
|
|
220
|
+
if (value === undefined) {
|
|
221
|
+
throw new Error(`Missing required path parameter: ${key}`);
|
|
222
|
+
}
|
|
223
|
+
return encodeURIComponent(String(value));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function getHeadersAsObject(headers) {
|
|
227
|
+
const result = {};
|
|
228
|
+
headers.forEach((value, key) => {
|
|
229
|
+
result[key] = value;
|
|
230
|
+
});
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
function calculateSizeInMb(data) {
|
|
234
|
+
const sizeInBytes = new TextEncoder().encode(JSON.stringify(data)).length;
|
|
235
|
+
return sizeInBytes / (1024 * 1024);
|
|
236
|
+
}
|
|
237
|
+
function defaultShouldRetry({ error, response }) {
|
|
238
|
+
if (error)
|
|
239
|
+
return true;
|
|
240
|
+
return response ? response.status >= 500 : false;
|
|
241
|
+
}
|
|
242
|
+
function delay(ms) {
|
|
243
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/secrets/infisical.ts
|
|
247
|
+
import { z } from "zod";
|
|
248
|
+
var defaultFetch = globalThis.fetch;
|
|
249
|
+
var infisicalAuthResponseSchema = z.object({
|
|
250
|
+
accessToken: z.string()
|
|
251
|
+
});
|
|
252
|
+
var infisicalSecretSchema = z.object({
|
|
253
|
+
secretKey: z.string(),
|
|
254
|
+
secretValue: z.string()
|
|
255
|
+
});
|
|
256
|
+
var infisicalSecretsResponseSchema = z.object({
|
|
257
|
+
secrets: z.array(infisicalSecretSchema)
|
|
258
|
+
});
|
|
259
|
+
async function loadInfisicalSecrets({
|
|
260
|
+
keys,
|
|
261
|
+
projectId,
|
|
262
|
+
environment,
|
|
263
|
+
siteUrl,
|
|
264
|
+
clientIdEnvKey,
|
|
265
|
+
clientSecretEnvKey
|
|
266
|
+
}) {
|
|
267
|
+
const clientId = process.env[clientIdEnvKey];
|
|
268
|
+
const clientSecret = process.env[clientSecretEnvKey];
|
|
269
|
+
if (!clientId || !clientSecret) {
|
|
270
|
+
throw new Error("Missing Infisical credentials");
|
|
271
|
+
}
|
|
272
|
+
const infisical = createInfisicalApiClient(siteUrl);
|
|
273
|
+
const { data: auth } = await infisical.request("login", {
|
|
274
|
+
reqBody: {
|
|
275
|
+
clientId,
|
|
276
|
+
clientSecret
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const { data } = await infisical.request("listSecrets", {
|
|
280
|
+
reqHeaders: {
|
|
281
|
+
Authorization: `Bearer ${auth.accessToken}`
|
|
282
|
+
},
|
|
283
|
+
reqQuery: {
|
|
284
|
+
environment,
|
|
285
|
+
projectId,
|
|
286
|
+
secretPath: "/",
|
|
287
|
+
viewSecretValue: true,
|
|
288
|
+
expandSecretReferences: true,
|
|
289
|
+
includeImports: true
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const wantedKeys = new Set(keys);
|
|
293
|
+
return Object.fromEntries(data.secrets.filter((secret) => wantedKeys.has(secret.secretKey)).map((secret) => [secret.secretKey, secret.secretValue]));
|
|
294
|
+
}
|
|
295
|
+
function createInfisicalApiClient(siteUrl) {
|
|
296
|
+
return createApiClient({
|
|
297
|
+
name: "infisical",
|
|
298
|
+
baseApiUrls: {
|
|
299
|
+
infisical: siteUrl
|
|
300
|
+
},
|
|
301
|
+
defaultBaseApiUrl: "infisical",
|
|
302
|
+
fetch: getFetch(),
|
|
303
|
+
retries: 1,
|
|
304
|
+
endpoints: {
|
|
305
|
+
login: {
|
|
306
|
+
method: "POST",
|
|
307
|
+
path: "/api/v1/auth/universal-auth/login",
|
|
308
|
+
reqBodySchema: z.object({
|
|
309
|
+
clientId: z.string(),
|
|
310
|
+
clientSecret: z.string()
|
|
311
|
+
}),
|
|
312
|
+
resSchema: infisicalAuthResponseSchema,
|
|
313
|
+
reqBodyFormat: "json"
|
|
314
|
+
},
|
|
315
|
+
listSecrets: {
|
|
316
|
+
method: "GET",
|
|
317
|
+
path: "/api/v4/secrets",
|
|
318
|
+
reqHeadersSchema: z.object({
|
|
319
|
+
Authorization: z.string()
|
|
320
|
+
}),
|
|
321
|
+
reqQuerySchema: z.object({
|
|
322
|
+
projectId: z.string(),
|
|
323
|
+
environment: z.string(),
|
|
324
|
+
secretPath: z.string(),
|
|
325
|
+
viewSecretValue: z.boolean(),
|
|
326
|
+
expandSecretReferences: z.boolean(),
|
|
327
|
+
includeImports: z.boolean()
|
|
328
|
+
}),
|
|
329
|
+
resSchema: infisicalSecretsResponseSchema
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function getFetch() {
|
|
335
|
+
return globalThis.fetch ?? defaultFetch;
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
loadInfisicalSecrets
|
|
339
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanzio",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A set of utility functions for TypeScript",
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "A lightweight set of utility functions for TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"module": "./dist/index.js",
|
|
7
|
-
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/src/index.js",
|
|
7
|
+
"main": "./dist/src/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
@@ -32,13 +32,25 @@
|
|
|
32
32
|
".": {
|
|
33
33
|
"types": "./dist/index.d.ts",
|
|
34
34
|
"bun": "./src/index.ts",
|
|
35
|
-
"import": "./dist/index.js",
|
|
36
|
-
"default": "./dist/index.js"
|
|
35
|
+
"import": "./dist/src/index.js",
|
|
36
|
+
"default": "./dist/src/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./secrets": {
|
|
39
|
+
"types": "./dist/secrets/index.d.ts",
|
|
40
|
+
"bun": "./src/secrets/index.ts",
|
|
41
|
+
"import": "./dist/src/secrets/index.js",
|
|
42
|
+
"default": "./dist/src/secrets/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./secrets/vite": {
|
|
45
|
+
"types": "./dist/secrets/vite.d.ts",
|
|
46
|
+
"bun": "./src/secrets/vite.ts",
|
|
47
|
+
"import": "./dist/src/secrets/vite.js",
|
|
48
|
+
"default": "./dist/src/secrets/vite.js"
|
|
37
49
|
}
|
|
38
50
|
},
|
|
39
51
|
"scripts": {
|
|
40
52
|
"build": "bun run build:js && bun run build:types",
|
|
41
|
-
"build:js": "bun build ./src/index.ts ./src/zod/index.ts ./src/api-wrapper/index.ts ./src/jwt/index.ts ./src/
|
|
53
|
+
"build:js": "bun build ./src/index.ts ./src/zod/index.ts ./src/api-wrapper/index.ts ./src/jwt/index.ts ./src/sitemap/index.ts ./src/p-queue/index.ts ./src/secrets/index.ts ./src/secrets/infisical.ts ./src/secrets/vite.ts --outdir ./dist --target node --packages external",
|
|
42
54
|
"build:types": "tsgo -p tsconfig.build.json",
|
|
43
55
|
"prepublishOnly": "bun run build",
|
|
44
56
|
"publish:patch": "npm version patch && npm publish",
|
|
@@ -53,15 +65,12 @@
|
|
|
53
65
|
"devDependencies": {
|
|
54
66
|
"@biomejs/biome": "^2.4.6",
|
|
55
67
|
"@types/bun": "latest",
|
|
56
|
-
"@types/pako": "^2.0.4",
|
|
57
68
|
"@typescript/native-preview": "^7.0.0-dev.20260306.1",
|
|
58
69
|
"husky": "^9.1.7",
|
|
59
|
-
"typescript": "^5.7.0"
|
|
70
|
+
"typescript": "^5.7.0",
|
|
71
|
+
"zod": "^4.3.6"
|
|
60
72
|
},
|
|
61
|
-
"
|
|
62
|
-
"@infisical/sdk": "^5.0.2",
|
|
63
|
-
"jose": "^6.2.3",
|
|
64
|
-
"pako": "^2.1.0",
|
|
73
|
+
"peerDependencies": {
|
|
65
74
|
"zod": "^4.3.6"
|
|
66
75
|
}
|
|
67
76
|
}
|
package/src/index.ts
CHANGED
package/src/jwt/index.ts
CHANGED
|
@@ -1,25 +1,171 @@
|
|
|
1
|
-
|
|
1
|
+
export type JWTPayload = Record<string, unknown>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const BYTE_CHUNK_SIZE = 0x8000
|
|
4
|
+
const textEncoder = new TextEncoder()
|
|
5
|
+
const textDecoder = new TextDecoder()
|
|
6
|
+
|
|
7
|
+
export async function jwtSign<Payload extends JWTPayload>(
|
|
8
|
+
payload: Payload,
|
|
5
9
|
secret: string
|
|
6
|
-
) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const header = encodeJson({ alg: 'HS256' })
|
|
12
|
+
const body = encodeJson({
|
|
13
|
+
...payload,
|
|
14
|
+
iat: Math.floor(Date.now() / 1000)
|
|
15
|
+
})
|
|
16
|
+
const data = `${header}.${body}`
|
|
17
|
+
const signature = await sign(data, secret)
|
|
18
|
+
|
|
19
|
+
return `${data}.${encodeBase64Url(signature)}`
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
export async function jwtVerify<Payload extends
|
|
22
|
+
export async function jwtVerify<Payload extends JWTPayload>(
|
|
15
23
|
token: string,
|
|
16
24
|
secret: string
|
|
17
25
|
): Promise<Payload | null> {
|
|
18
26
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
27
|
+
const parts = token.split('.')
|
|
28
|
+
if (parts.length !== 3) return null
|
|
29
|
+
|
|
30
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts as [
|
|
31
|
+
string,
|
|
32
|
+
string,
|
|
33
|
+
string
|
|
34
|
+
]
|
|
35
|
+
const header = decodeJson(encodedHeader)
|
|
36
|
+
if (!isJwtHeader(header)) return null
|
|
37
|
+
|
|
38
|
+
const data = `${encodedHeader}.${encodedPayload}`
|
|
39
|
+
const signature = decodeBase64Url(encodedSignature)
|
|
40
|
+
const isValid = await verify(data, signature, secret)
|
|
41
|
+
if (!isValid) return null
|
|
42
|
+
|
|
43
|
+
const payload = decodeJson(encodedPayload)
|
|
44
|
+
if (!isJwtPayload(payload) || !isTimeValid(payload)) return null
|
|
45
|
+
|
|
21
46
|
return payload as Payload
|
|
22
47
|
} catch (_error) {
|
|
23
48
|
return null
|
|
24
49
|
}
|
|
25
50
|
}
|
|
51
|
+
|
|
52
|
+
async function sign(data: string, secret: string): Promise<ArrayBuffer> {
|
|
53
|
+
const key = await getKey(secret, ['sign'])
|
|
54
|
+
return await getSubtleCrypto().sign('HMAC', key, encodeText(data))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function verify(
|
|
58
|
+
data: string,
|
|
59
|
+
signature: ArrayBuffer,
|
|
60
|
+
secret: string
|
|
61
|
+
): Promise<boolean> {
|
|
62
|
+
const key = await getKey(secret, ['verify'])
|
|
63
|
+
return await getSubtleCrypto().verify(
|
|
64
|
+
'HMAC',
|
|
65
|
+
key,
|
|
66
|
+
signature,
|
|
67
|
+
encodeText(data)
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function getKey(
|
|
72
|
+
secret: string,
|
|
73
|
+
usages: Array<'sign' | 'verify'>
|
|
74
|
+
): Promise<CryptoKey> {
|
|
75
|
+
return await getSubtleCrypto().importKey(
|
|
76
|
+
'raw',
|
|
77
|
+
encodeText(secret),
|
|
78
|
+
{
|
|
79
|
+
name: 'HMAC',
|
|
80
|
+
hash: 'SHA-256'
|
|
81
|
+
},
|
|
82
|
+
false,
|
|
83
|
+
usages
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function encodeJson(value: JWTPayload): string {
|
|
88
|
+
return encodeBase64Url(encodeText(JSON.stringify(value)))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function decodeJson(value: string): unknown {
|
|
92
|
+
return JSON.parse(textDecoder.decode(decodeBase64Url(value)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function encodeBase64Url(value: Uint8Array | ArrayBuffer): string {
|
|
96
|
+
const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : value
|
|
97
|
+
return btoa(bytesToBinaryString(bytes))
|
|
98
|
+
.replaceAll('+', '-')
|
|
99
|
+
.replaceAll('/', '_')
|
|
100
|
+
.replace(/=+$/, '')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function decodeBase64Url(value: string): ArrayBuffer {
|
|
104
|
+
const base64 = value.replaceAll('-', '+').replaceAll('_', '/')
|
|
105
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')
|
|
106
|
+
return binaryStringToBytes(atob(padded)).buffer
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function encodeText(value: string): ArrayBuffer {
|
|
110
|
+
return toArrayBuffer(textEncoder.encode(value))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
114
|
+
return bytes.buffer.slice(
|
|
115
|
+
bytes.byteOffset,
|
|
116
|
+
bytes.byteOffset + bytes.byteLength
|
|
117
|
+
) as ArrayBuffer
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function bytesToBinaryString(bytes: Uint8Array): string {
|
|
121
|
+
let binary = ''
|
|
122
|
+
|
|
123
|
+
for (let offset = 0; offset < bytes.length; offset += BYTE_CHUNK_SIZE) {
|
|
124
|
+
binary += String.fromCharCode(
|
|
125
|
+
...bytes.subarray(offset, offset + BYTE_CHUNK_SIZE)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return binary
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function binaryStringToBytes(binary: string): Uint8Array<ArrayBuffer> {
|
|
133
|
+
const bytes = new Uint8Array(binary.length)
|
|
134
|
+
|
|
135
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
136
|
+
bytes[index] = binary.charCodeAt(index)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return bytes
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getSubtleCrypto(): SubtleCrypto {
|
|
143
|
+
const subtle = globalThis.crypto?.subtle
|
|
144
|
+
if (!subtle) {
|
|
145
|
+
throw new Error('Web Crypto is not available in this runtime')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return subtle
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isJwtHeader(value: unknown): value is { alg: 'HS256' } {
|
|
152
|
+
return (
|
|
153
|
+
typeof value === 'object' &&
|
|
154
|
+
value !== null &&
|
|
155
|
+
'alg' in value &&
|
|
156
|
+
value.alg === 'HS256'
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isJwtPayload(value: unknown): value is JWTPayload {
|
|
161
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isTimeValid(payload: JWTPayload): boolean {
|
|
165
|
+
const now = Math.floor(Date.now() / 1000)
|
|
166
|
+
|
|
167
|
+
if (typeof payload.exp === 'number' && now >= payload.exp) return false
|
|
168
|
+
if (typeof payload.nbf === 'number' && now < payload.nbf) return false
|
|
169
|
+
|
|
170
|
+
return true
|
|
171
|
+
}
|
package/src/secrets/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { InfisicalSDK } from '@infisical/sdk'
|
|
2
1
|
import { networkInterfaces } from 'node:os'
|
|
3
2
|
import { colorize, createCoolLogger } from '../cool-console-log'
|
|
3
|
+
import { loadInfisicalSecrets } from './infisical'
|
|
4
4
|
|
|
5
5
|
const DEFAULT_INFISICAL_SITE_URL = 'https://eu.infisical.com'
|
|
6
6
|
const LOCAL_IP_VALUE = 'LOCAL_IP'
|
|
@@ -38,11 +38,7 @@ export type SecretSet<SecretKey extends string> = {
|
|
|
38
38
|
reload: () => Promise<SecretSet<SecretKey>>
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
secretKey: string
|
|
43
|
-
secretValue: string
|
|
44
|
-
}
|
|
45
|
-
|
|
41
|
+
export * from './infisical'
|
|
46
42
|
export * from './vite'
|
|
47
43
|
|
|
48
44
|
export function getSecretEnvironment(): SecretEnvironment {
|
|
@@ -135,43 +131,6 @@ export async function defineSecretSet<
|
|
|
135
131
|
return secretSet
|
|
136
132
|
}
|
|
137
133
|
|
|
138
|
-
async function loadInfisicalSecrets<SecretKey extends string>({
|
|
139
|
-
keys,
|
|
140
|
-
projectId,
|
|
141
|
-
environment,
|
|
142
|
-
siteUrl,
|
|
143
|
-
clientIdEnvKey,
|
|
144
|
-
clientSecretEnvKey
|
|
145
|
-
}: SecretSetLoaderContext<SecretKey>): Promise<
|
|
146
|
-
Partial<Record<SecretKey, string>>
|
|
147
|
-
> {
|
|
148
|
-
const clientId = process.env[clientIdEnvKey]
|
|
149
|
-
const clientSecret = process.env[clientSecretEnvKey]
|
|
150
|
-
|
|
151
|
-
if (!clientId || !clientSecret) {
|
|
152
|
-
throw new Error('Missing Infisical credentials')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const client = new InfisicalSDK({ siteUrl })
|
|
156
|
-
|
|
157
|
-
await client.auth().universalAuth.login({
|
|
158
|
-
clientId,
|
|
159
|
-
clientSecret
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
const { secrets } = (await client.secrets().listSecrets({
|
|
163
|
-
environment,
|
|
164
|
-
projectId
|
|
165
|
-
})) as { secrets: InfisicalSecret[] }
|
|
166
|
-
|
|
167
|
-
const wantedKeys = new Set<string>(keys)
|
|
168
|
-
return Object.fromEntries(
|
|
169
|
-
secrets
|
|
170
|
-
.filter((secret) => wantedKeys.has(secret.secretKey))
|
|
171
|
-
.map((secret) => [secret.secretKey, secret.secretValue])
|
|
172
|
-
) as Partial<Record<SecretKey, string>>
|
|
173
|
-
}
|
|
174
|
-
|
|
175
134
|
function resolveEnvironment(
|
|
176
135
|
environment: DefineSecretSetOptions<string>['environment']
|
|
177
136
|
): SecretEnvironment {
|