@inshorts/glint-http 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,384 @@
1
+ // src/curl.ts
2
+ var SECRET_HEADER = /^(authorization|cookie|set-cookie|proxy-authorization)$|token|secret|api[-_]?key|password|credential/i;
3
+ var SECRET_FIELD = /^(access_token|refresh_token|id_token|password|secret|token|authorization|api_key|apikey|client_secret)$/i;
4
+ var DEFAULT_MAX_BODY = 512;
5
+ function headerEntries(headers) {
6
+ if (!headers) return [];
7
+ if (headers instanceof Headers) return [...headers.entries()];
8
+ if (Array.isArray(headers)) return headers.map(([k, v]) => [k, String(v)]);
9
+ return Object.entries(headers).map(([k, v]) => [k, String(v)]);
10
+ }
11
+ function envNameFor(value, env) {
12
+ if (!value) return null;
13
+ for (const [name, candidate] of Object.entries(env)) {
14
+ if (candidate && candidate.length >= 8 && candidate === value) return name;
15
+ }
16
+ return null;
17
+ }
18
+ function redactHeaderValue(name, value, env) {
19
+ if (!SECRET_HEADER.test(name)) return value;
20
+ const envName = envNameFor(value, env);
21
+ return envName ? `$${envName}` : "<redacted>";
22
+ }
23
+ function redactBodyValue(value, depth = 0) {
24
+ if (depth > 6 || value == null) return value;
25
+ if (Array.isArray(value))
26
+ return value.map((v) => redactBodyValue(v, depth + 1));
27
+ if (typeof value === "object") {
28
+ return Object.fromEntries(
29
+ Object.entries(value).map(([k, v]) => [
30
+ k,
31
+ SECRET_FIELD.test(k) ? "<redacted>" : redactBodyValue(v, depth + 1)
32
+ ])
33
+ );
34
+ }
35
+ return value;
36
+ }
37
+ function formatBody(body, maxBody) {
38
+ if (body == null) return null;
39
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
40
+ const keys = [...body.keys()];
41
+ return `<form-data: ${keys.join(", ") || "empty"}>`;
42
+ }
43
+ if (typeof body !== "string") return "<non-text body>";
44
+ let text = body;
45
+ try {
46
+ text = JSON.stringify(redactBodyValue(JSON.parse(body)));
47
+ } catch {
48
+ }
49
+ return text.length > maxBody ? `${text.slice(0, maxBody)}\u2026<truncated>` : text;
50
+ }
51
+ function shellQuote(value) {
52
+ return `'${value.replace(/'/g, `'\\''`)}'`;
53
+ }
54
+ function toCurl(request, options = {}) {
55
+ const maxBody = options.maxBody ?? DEFAULT_MAX_BODY;
56
+ const env = options.env ?? (typeof process === "undefined" ? {} : process.env);
57
+ const method = (request.method || "GET").toUpperCase();
58
+ const parts = [`curl -X ${method} ${shellQuote(request.url)}`];
59
+ for (const [name, value] of headerEntries(request.headers)) {
60
+ parts.push(
61
+ `-H ${shellQuote(`${name}: ${redactHeaderValue(name, value, env)}`)}`
62
+ );
63
+ }
64
+ const body = formatBody(request.body, maxBody);
65
+ if (body != null) parts.push(`-d ${shellQuote(body)}`);
66
+ return parts.join(" ");
67
+ }
68
+
69
+ // src/errors.ts
70
+ var UpstreamConfigError = class extends Error {
71
+ status = 500;
72
+ curl;
73
+ constructor(message, options) {
74
+ super(message);
75
+ this.name = "UpstreamConfigError";
76
+ this.curl = options?.curl;
77
+ }
78
+ };
79
+ var UpstreamTimeoutError = class extends Error {
80
+ status = 504;
81
+ curl;
82
+ constructor(message = "Upstream request timed out", options) {
83
+ super(message);
84
+ this.name = "UpstreamTimeoutError";
85
+ this.curl = options?.curl;
86
+ }
87
+ };
88
+ var UpstreamError = class extends Error {
89
+ status;
90
+ curl;
91
+ constructor(message, status, options) {
92
+ super(message);
93
+ this.name = "UpstreamError";
94
+ this.status = status;
95
+ this.curl = options?.curl;
96
+ }
97
+ };
98
+ function isUpstreamFailure(error) {
99
+ if (!(error instanceof Error)) return false;
100
+ const status = error.status;
101
+ return typeof status === "number" && status >= 400 && status <= 599;
102
+ }
103
+ function curlOf(error) {
104
+ if (!error || typeof error !== "object") return void 0;
105
+ const curl = error.curl;
106
+ return typeof curl === "string" && curl.length > 0 ? curl : void 0;
107
+ }
108
+
109
+ // src/upstream.ts
110
+ function internalHeaders(deviceId) {
111
+ return {
112
+ "Content-Type": "application/json",
113
+ "x-os-type": "internal",
114
+ "x-device-id": deviceId
115
+ };
116
+ }
117
+ var DEFAULT_UPSTREAM_TIMEOUT_MS = 15e3;
118
+ function requireUpstreamBaseUrl(value, envName) {
119
+ const trimmed = value?.trim();
120
+ if (!trimmed) {
121
+ throw new UpstreamConfigError(
122
+ `${envName} is required for this upstream call (set it in .env)`
123
+ );
124
+ }
125
+ return trimmed.replace(/\/$/, "");
126
+ }
127
+ function resolveTimeoutMs(timeoutMs) {
128
+ if (typeof timeoutMs === "number" && timeoutMs > 0) return timeoutMs;
129
+ const fromEnv = Number(
130
+ typeof process === "undefined" ? void 0 : process.env.UPSTREAM_TIMEOUT_MS
131
+ );
132
+ if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
133
+ return DEFAULT_UPSTREAM_TIMEOUT_MS;
134
+ }
135
+ function urlOf(input) {
136
+ if (typeof input === "string") return input;
137
+ if (input instanceof URL) return input.toString();
138
+ return input.url;
139
+ }
140
+ var FAILURE_LOG_WINDOW_MS = 6e4;
141
+ var recentFailures = /* @__PURE__ */ new Map();
142
+ function resetUpstreamFailureLog() {
143
+ recentFailures.clear();
144
+ }
145
+ function shouldLog(key, now) {
146
+ const seen = recentFailures.get(key);
147
+ if (!seen || now - seen.loggedAt >= FAILURE_LOG_WINDOW_MS) {
148
+ recentFailures.set(key, { loggedAt: now, suppressed: 0 });
149
+ return { log: true, suppressed: seen?.suppressed ?? 0 };
150
+ }
151
+ seen.suppressed += 1;
152
+ return { log: false, suppressed: seen.suppressed };
153
+ }
154
+ function logUpstreamFailure(details) {
155
+ const { input, init, status, statusText, durationMs, error } = details;
156
+ const method = (init?.method || "GET").toUpperCase();
157
+ const url = urlOf(input);
158
+ const outcome = status != null ? `${status}${statusText ? ` ${statusText}` : ""}` : error instanceof Error ? `${error.name}: ${error.message}` : "network error";
159
+ const { log: shouldWrite, suppressed } = shouldLog(
160
+ `${method} ${url} ${outcome}`,
161
+ Date.now()
162
+ );
163
+ if (!shouldWrite) return;
164
+ const repro = toCurl({
165
+ method,
166
+ url,
167
+ headers: init?.headers,
168
+ body: init?.body
169
+ });
170
+ const alsoSuppressed = suppressed > 0 ? ` (+${suppressed} more in the last minute)` : "";
171
+ const severity = status != null && status < 500 ? "WARNING" : "ERROR";
172
+ const emit = severity === "WARNING" ? console.warn : console.error;
173
+ emit(
174
+ JSON.stringify({
175
+ severity,
176
+ message: "upstream-failure",
177
+ event: "upstream-failure",
178
+ method,
179
+ url,
180
+ status: status ?? null,
181
+ statusText: statusText || null,
182
+ outcome,
183
+ durationMs,
184
+ suppressed: suppressed > 0 ? suppressed : void 0,
185
+ note: alsoSuppressed || void 0,
186
+ curl: repro,
187
+ error: error instanceof Error ? { name: error.name, message: error.message } : error ? String(error) : void 0
188
+ })
189
+ );
190
+ }
191
+ function curlForRequest(input, init) {
192
+ return toCurl({
193
+ method: (init?.method || "GET").toUpperCase(),
194
+ url: urlOf(input),
195
+ headers: init?.headers,
196
+ body: init?.body
197
+ });
198
+ }
199
+ async function fetchUpstream(input, init, timeoutMs) {
200
+ const controller = new AbortController();
201
+ const external = init?.signal;
202
+ const onExternalAbort = () => controller.abort();
203
+ if (external) {
204
+ if (external.aborted) controller.abort();
205
+ else external.addEventListener("abort", onExternalAbort, { once: true });
206
+ }
207
+ const timer = setTimeout(
208
+ () => controller.abort(),
209
+ resolveTimeoutMs(timeoutMs)
210
+ );
211
+ const startedAt = Date.now();
212
+ const curl = curlForRequest(input, init);
213
+ try {
214
+ const response = await fetch(input, { ...init, signal: controller.signal });
215
+ if (!response.ok) {
216
+ logUpstreamFailure({
217
+ input,
218
+ init,
219
+ status: response.status,
220
+ statusText: response.statusText,
221
+ durationMs: Date.now() - startedAt
222
+ });
223
+ }
224
+ return response;
225
+ } catch (error) {
226
+ if (!external?.aborted) {
227
+ logUpstreamFailure({
228
+ input,
229
+ init,
230
+ durationMs: Date.now() - startedAt,
231
+ error: controller.signal.aborted && !external?.aborted ? new UpstreamTimeoutError("Upstream request timed out", { curl }) : error
232
+ });
233
+ }
234
+ if (controller.signal.aborted && !external?.aborted) {
235
+ throw new UpstreamTimeoutError("Upstream request timed out", { curl });
236
+ }
237
+ if (error instanceof Error && !error.curl) {
238
+ error.curl = curl;
239
+ }
240
+ throw error;
241
+ } finally {
242
+ clearTimeout(timer);
243
+ external?.removeEventListener("abort", onExternalAbort);
244
+ }
245
+ }
246
+ function readUpstreamError(value) {
247
+ if (typeof value === "string") {
248
+ const trimmed = value.trim();
249
+ return trimmed || null;
250
+ }
251
+ if (value && typeof value === "object" && !Array.isArray(value)) {
252
+ const record = value;
253
+ return readUpstreamError(record.message) ?? readUpstreamError(record.error);
254
+ }
255
+ return null;
256
+ }
257
+ async function parseUpstream(response, makeError, fallbackMessage = `Request failed with status ${response.status}`) {
258
+ const text = await response.text();
259
+ const trimmed = text.trim();
260
+ if (response.ok) {
261
+ if (response.status === 204 || !trimmed) return void 0;
262
+ try {
263
+ return JSON.parse(trimmed);
264
+ } catch {
265
+ throw makeError(`Invalid JSON from upstream (${response.status})`, 502);
266
+ }
267
+ }
268
+ let message = fallbackMessage;
269
+ if (trimmed) {
270
+ try {
271
+ message = readUpstreamError(JSON.parse(trimmed)) || message;
272
+ } catch {
273
+ if (trimmed.length <= 300 && !trimmed.startsWith("<")) message = trimmed;
274
+ }
275
+ }
276
+ throw makeError(message, response.status);
277
+ }
278
+
279
+ // src/client.ts
280
+ function mergeHeaders(base, extra) {
281
+ const headers = new Headers(base);
282
+ if (extra) {
283
+ new Headers(extra).forEach((value, key) => headers.set(key, value));
284
+ }
285
+ return headers;
286
+ }
287
+ var defaultMakeError = (message, status, curl) => new UpstreamError(message, status, { curl });
288
+ function attachCurl(error, curl) {
289
+ if (!error.curl) {
290
+ error.curl = curl;
291
+ }
292
+ return error;
293
+ }
294
+ function createUpstreamClient(config) {
295
+ const makeError = config.makeError ?? defaultMakeError;
296
+ return {
297
+ async request(path, init = {}) {
298
+ const { body, headers: requestHeaders, timeoutMs, ...requestInit } = init;
299
+ const url = `${config.baseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
300
+ const headers = mergeHeaders(config.headers?.(), requestHeaders);
301
+ const fetchInit = {
302
+ ...requestInit,
303
+ headers,
304
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
305
+ };
306
+ const curl = toCurl({
307
+ method: (fetchInit.method || "GET").toUpperCase(),
308
+ url,
309
+ headers,
310
+ body: fetchInit.body
311
+ });
312
+ const response = await fetchUpstream(
313
+ url,
314
+ fetchInit,
315
+ timeoutMs ?? config.timeoutMs
316
+ );
317
+ return parseUpstream(
318
+ response,
319
+ (message, status) => attachCurl(makeError(message, status, curl), curl)
320
+ );
321
+ }
322
+ };
323
+ }
324
+
325
+ // src/response.ts
326
+ function statusOf(error) {
327
+ const status = error?.status;
328
+ return typeof status === "number" && status >= 400 && status <= 599 ? status : 500;
329
+ }
330
+ function messageOf(error, fallback = "Request failed") {
331
+ return error instanceof Error && error.message ? error.message : fallback;
332
+ }
333
+ function errorResponse(error, fallback = "Request failed") {
334
+ return Response.json(
335
+ { message: messageOf(error, fallback) },
336
+ { status: statusOf(error) }
337
+ );
338
+ }
339
+ function invalidResponse(message) {
340
+ return Response.json({ message }, { status: 400 });
341
+ }
342
+ function logApiError(error, context) {
343
+ const status = statusOf(error);
344
+ const message = messageOf(error);
345
+ const curl = curlOf(error);
346
+ const severity = status < 500 ? "WARNING" : "ERROR";
347
+ const write = severity === "WARNING" ? console.warn : console.error;
348
+ write(
349
+ JSON.stringify({
350
+ severity,
351
+ message,
352
+ event: "api-error",
353
+ service: context.service,
354
+ route: context.route,
355
+ requestId: context.requestId,
356
+ status,
357
+ curl: curl ?? void 0,
358
+ name: error instanceof Error ? error.name : void 0,
359
+ upstream: isUpstreamFailure(error) || void 0
360
+ })
361
+ );
362
+ }
363
+ export {
364
+ DEFAULT_UPSTREAM_TIMEOUT_MS,
365
+ UpstreamConfigError,
366
+ UpstreamError,
367
+ UpstreamTimeoutError,
368
+ createUpstreamClient,
369
+ curlForRequest,
370
+ curlOf,
371
+ errorResponse,
372
+ fetchUpstream,
373
+ internalHeaders,
374
+ invalidResponse,
375
+ isUpstreamFailure,
376
+ logApiError,
377
+ messageOf,
378
+ parseUpstream,
379
+ requireUpstreamBaseUrl,
380
+ resetUpstreamFailureLog,
381
+ statusOf,
382
+ toCurl
383
+ };
384
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/curl.ts","../src/errors.ts","../src/upstream.ts","../src/client.ts","../src/response.ts"],"sourcesContent":["/**\n * Turns a request into a `curl` line you can paste into a terminal.\n *\n * The point is the thing that is missing when an upstream call fails in\n * production: the log says a request to some service returned 502, and\n * reproducing it means reading the calling code to work out what was actually\n * sent. This writes that down at the moment it fails.\n *\n * **Nothing secret goes in.** Every header whose name looks like a credential\n * is emitted as `$ENV_VAR` where the value matches a known one, and\n * `<redacted>` otherwise — so the line is still runnable after\n * `export SOME_SYS_TOKEN=…`, without the token itself ever reaching a log\n * aggregator. Body fields that look like credentials get the same treatment.\n *\n * The alternative — logging the real header — is how tokens end up in Cloud\n * Logging, readable by anyone with project viewer, retained for a month, and\n * impossible to rotate out of the archive.\n */\n\n/** Header names whose values must never be logged. */\nconst SECRET_HEADER =\n /^(authorization|cookie|set-cookie|proxy-authorization)$|token|secret|api[-_]?key|password|credential/i;\n\n/** Body fields whose values must never be logged. */\nconst SECRET_FIELD =\n /^(access_token|refresh_token|id_token|password|secret|token|authorization|api_key|apikey|client_secret)$/i;\n\nconst DEFAULT_MAX_BODY = 512;\n\nexport interface CurlRequest {\n method?: string;\n url: string;\n headers?: HeadersInit;\n body?: unknown;\n}\n\nexport interface CurlOptions {\n /** Longest body to include before truncating. */\n maxBody?: number;\n /**\n * Environment variable names to match secret values against, so the line\n * comes out as `$SOME_SYS_TOKEN` rather than `<redacted>` and can actually\n * be replayed. Defaults to `process.env`; only values of 8+ characters are\n * matched.\n */\n env?: Record<string, string | undefined>;\n}\n\nfunction headerEntries(headers?: HeadersInit): Array<[string, string]> {\n if (!headers) return [];\n if (headers instanceof Headers) return [...headers.entries()];\n if (Array.isArray(headers)) return headers.map(([k, v]) => [k, String(v)]);\n return Object.entries(headers).map(([k, v]) => [k, String(v)]);\n}\n\n/** The env var holding this exact value, if there is one. */\nfunction envNameFor(\n value: string,\n env: Record<string, string | undefined>,\n): string | null {\n if (!value) return null;\n for (const [name, candidate] of Object.entries(env)) {\n // Short values match too easily to be worth guessing at.\n if (candidate && candidate.length >= 8 && candidate === value) return name;\n }\n return null;\n}\n\nfunction redactHeaderValue(\n name: string,\n value: string,\n env: Record<string, string | undefined>,\n): string {\n if (!SECRET_HEADER.test(name)) return value;\n const envName = envNameFor(value, env);\n return envName ? `$${envName}` : \"<redacted>\";\n}\n\n/** Recursively blanks credential-looking fields, leaving the shape intact. */\nfunction redactBodyValue(value: unknown, depth = 0): unknown {\n if (depth > 6 || value == null) return value;\n if (Array.isArray(value))\n return value.map((v) => redactBodyValue(v, depth + 1));\n if (typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([k, v]) => [\n k,\n SECRET_FIELD.test(k) ? \"<redacted>\" : redactBodyValue(v, depth + 1),\n ]),\n );\n }\n return value;\n}\n\nfunction formatBody(body: unknown, maxBody: number): string | null {\n if (body == null) return null;\n\n // Streams and multipart cannot be replayed from a log line, and a file's\n // bytes have no business in one. Say what it was instead.\n if (typeof FormData !== \"undefined\" && body instanceof FormData) {\n const keys = [...body.keys()];\n return `<form-data: ${keys.join(\", \") || \"empty\"}>`;\n }\n if (typeof body !== \"string\") return \"<non-text body>\";\n\n let text = body;\n try {\n text = JSON.stringify(redactBodyValue(JSON.parse(body)));\n } catch {\n // Not JSON — pass it through, still truncated below.\n }\n return text.length > maxBody ? `${text.slice(0, maxBody)}…<truncated>` : text;\n}\n\n/** Single-quoted for a POSIX shell. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nexport function toCurl(\n request: CurlRequest,\n options: CurlOptions = {},\n): string {\n const maxBody = options.maxBody ?? DEFAULT_MAX_BODY;\n const env =\n options.env ?? (typeof process === \"undefined\" ? {} : process.env);\n const method = (request.method || \"GET\").toUpperCase();\n\n const parts = [`curl -X ${method} ${shellQuote(request.url)}`];\n\n for (const [name, value] of headerEntries(request.headers)) {\n parts.push(\n `-H ${shellQuote(`${name}: ${redactHeaderValue(name, value, env)}`)}`,\n );\n }\n\n const body = formatBody(request.body, maxBody);\n if (body != null) parts.push(`-d ${shellQuote(body)}`);\n\n return parts.join(\" \");\n}\n","/**\n * Errors this package throws. Each carries a numeric `status`, so\n * `errorResponse` turns it into the right HTTP code without having to know\n * the class.\n *\n * `curl` is for **server logs only** — never put it in the JSON body sent to\n * the browser (`errorResponse` strips it).\n */\n\n/** A required upstream setting (base URL, token) is missing. */\nexport class UpstreamConfigError extends Error {\n status = 500;\n curl?: string;\n\n constructor(message: string, options?: { curl?: string }) {\n super(message);\n this.name = \"UpstreamConfigError\";\n this.curl = options?.curl;\n }\n}\n\n/** The upstream did not answer before the deadline. */\nexport class UpstreamTimeoutError extends Error {\n status = 504;\n curl?: string;\n\n constructor(\n message = \"Upstream request timed out\",\n options?: { curl?: string },\n ) {\n super(message);\n this.name = \"UpstreamTimeoutError\";\n this.curl = options?.curl;\n }\n}\n\n/**\n * The default error `createUpstreamClient` throws for a non-OK response when\n * the caller does not supply its own `makeError`.\n */\nexport class UpstreamError extends Error {\n status: number;\n curl?: string;\n\n constructor(\n message: string,\n status: number,\n options?: { curl?: string },\n ) {\n super(message);\n this.name = \"UpstreamError\";\n this.status = status;\n this.curl = options?.curl;\n }\n}\n\n/** True when `error` looks like one of ours (or any `{ status, message }`). */\nexport function isUpstreamFailure(\n error: unknown,\n): error is Error & { status: number; curl?: string } {\n if (!(error instanceof Error)) return false;\n const status = (error as { status?: unknown }).status;\n return typeof status === \"number\" && status >= 400 && status <= 599;\n}\n\n/** Read a redacted replay curl if the thrown value carries one. */\nexport function curlOf(error: unknown): string | undefined {\n if (!error || typeof error !== \"object\") return undefined;\n const curl = (error as { curl?: unknown }).curl;\n return typeof curl === \"string\" && curl.length > 0 ? curl : undefined;\n}\n","/**\n * Server-side `fetch` for calls to internal upstream services. These calls\n * usually attach system tokens, so never import this into browser code.\n */\n\nimport { toCurl } from \"./curl\";\nimport { UpstreamConfigError, UpstreamTimeoutError } from \"./errors\";\n\n/**\n * Base headers for an internal service-to-service call. `deviceId` names the\n * calling app (`\"inpix-panel\"`, `\"users-crm\"`) so upstream logs show who sent it.\n */\nexport function internalHeaders(deviceId: string): Record<string, string> {\n return {\n \"Content-Type\": \"application/json\",\n \"x-os-type\": \"internal\",\n \"x-device-id\": deviceId,\n };\n}\n\n/** Used when neither the call nor `UPSTREAM_TIMEOUT_MS` sets a timeout. */\nexport const DEFAULT_UPSTREAM_TIMEOUT_MS = 15_000;\n\n/**\n * Require an absolute upstream base URL. There is deliberately no default\n * host: missing config fails closed instead of quietly calling a hard-coded one.\n */\nexport function requireUpstreamBaseUrl(\n value: string | null | undefined,\n envName: string,\n): string {\n const trimmed = value?.trim();\n if (!trimmed) {\n throw new UpstreamConfigError(\n `${envName} is required for this upstream call (set it in .env)`,\n );\n }\n return trimmed.replace(/\\/$/, \"\");\n}\n\nfunction resolveTimeoutMs(timeoutMs?: number): number {\n if (typeof timeoutMs === \"number\" && timeoutMs > 0) return timeoutMs;\n const fromEnv = Number(\n typeof process === \"undefined\"\n ? undefined\n : process.env.UPSTREAM_TIMEOUT_MS,\n );\n if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;\n return DEFAULT_UPSTREAM_TIMEOUT_MS;\n}\n\nfunction urlOf(input: RequestInfo | URL): string {\n if (typeof input === \"string\") return input;\n if (input instanceof URL) return input.toString();\n return input.url;\n}\n\n/**\n * How long one kind of failure stays quiet after it has been reported once.\n *\n * A failing upstream fails for every request, so without this a bad ten minutes\n * writes one curl per request per pod — the ingest is billed by volume, and the\n * thousandth identical line is worth nothing anyway. One a minute per distinct\n * failure keeps the diagnosis and drops the repetition.\n */\nconst FAILURE_LOG_WINDOW_MS = 60_000;\n\n/** key → when it was last logged, and how many were swallowed since. */\nconst recentFailures = new Map<\n string,\n { loggedAt: number; suppressed: number }\n>();\n\n/** Test seam. */\nexport function resetUpstreamFailureLog(): void {\n recentFailures.clear();\n}\n\n/**\n * Whether to write this failure out, and what to say about the ones we did not.\n *\n * Deliberately keyed on method + URL + outcome rather than the body: the same\n * endpoint failing the same way is one problem however many payloads hit it,\n * and keying on the body would defeat the throttle entirely.\n */\nfunction shouldLog(\n key: string,\n now: number,\n): { log: boolean; suppressed: number } {\n const seen = recentFailures.get(key);\n\n if (!seen || now - seen.loggedAt >= FAILURE_LOG_WINDOW_MS) {\n recentFailures.set(key, { loggedAt: now, suppressed: 0 });\n return { log: true, suppressed: seen?.suppressed ?? 0 };\n }\n\n seen.suppressed += 1;\n return { log: false, suppressed: seen.suppressed };\n}\n\n/**\n * One line per failed upstream call, carrying the curl that reproduces it.\n *\n * This is the thing that was missing: a 502 reached the browser as a message\n * and left nothing behind, so working out what had actually been sent meant\n * reading the calling code and guessing at the payload. Credentials are\n * redacted by `toCurl` — see there for why the real header is never logged.\n */\nfunction logUpstreamFailure(details: {\n input: RequestInfo | URL;\n init?: RequestInit;\n status?: number;\n statusText?: string;\n durationMs: number;\n error?: unknown;\n}): void {\n const { input, init, status, statusText, durationMs, error } = details;\n const method = (init?.method || \"GET\").toUpperCase();\n const url = urlOf(input);\n\n const outcome =\n status != null\n ? `${status}${statusText ? ` ${statusText}` : \"\"}`\n : error instanceof Error\n ? `${error.name}: ${error.message}`\n : \"network error\";\n\n const { log: shouldWrite, suppressed } = shouldLog(\n `${method} ${url} ${outcome}`,\n Date.now(),\n );\n if (!shouldWrite) return;\n\n const repro = toCurl({\n method,\n url,\n headers: init?.headers,\n body: init?.body,\n });\n // Carried on the next line that does get written, so a suppressed burst is\n // still visible as a number rather than vanishing.\n const alsoSuppressed =\n suppressed > 0 ? ` (+${suppressed} more in the last minute)` : \"\";\n\n // A 5xx or a dead socket is ours to chase; a 4xx is usually the caller's\n // payload and should not page anyone. Single-line JSON → Cloud Logging.\n const severity = status != null && status < 500 ? \"WARNING\" : \"ERROR\";\n const emit = severity === \"WARNING\" ? console.warn : console.error;\n emit(\n JSON.stringify({\n severity,\n message: \"upstream-failure\",\n event: \"upstream-failure\",\n method,\n url,\n status: status ?? null,\n statusText: statusText || null,\n outcome,\n durationMs,\n suppressed: suppressed > 0 ? suppressed : undefined,\n note: alsoSuppressed || undefined,\n curl: repro,\n error:\n error instanceof Error\n ? { name: error.name, message: error.message }\n : error\n ? String(error)\n : undefined,\n }),\n );\n}\n\n/**\n * `fetch` with an AbortSignal timeout. Throws `UpstreamTimeoutError` (status\n * 504) when the deadline is hit before the upstream responds.\n *\n * The timeout is `timeoutMs` if given, else the `UPSTREAM_TIMEOUT_MS` env var,\n * else `DEFAULT_UPSTREAM_TIMEOUT_MS`.\n *\n * Every failure — a non-OK status, a timeout, a refused connection — is logged\n * once here with a replayable curl, so no caller has to remember to do it and\n * none of them can do it differently.\n *\n * No caller currently treats a non-OK status as normal flow, so there is no\n * opt-out. If one appears — a lookup where 404 means \"absent\" — add a\n * `silentStatuses` option here rather than logging around it at the call site.\n */\n/** Build the redacted replay curl for this attempt (also used on thrown errors). */\nexport function curlForRequest(\n input: RequestInfo | URL,\n init?: RequestInit,\n): string {\n return toCurl({\n method: (init?.method || \"GET\").toUpperCase(),\n url: urlOf(input),\n headers: init?.headers,\n body: init?.body,\n });\n}\n\nexport async function fetchUpstream(\n input: RequestInfo | URL,\n init?: RequestInit,\n timeoutMs?: number,\n): Promise<Response> {\n const controller = new AbortController();\n const external = init?.signal;\n const onExternalAbort = () => controller.abort();\n\n if (external) {\n if (external.aborted) controller.abort();\n else external.addEventListener(\"abort\", onExternalAbort, { once: true });\n }\n\n const timer = setTimeout(\n () => controller.abort(),\n resolveTimeoutMs(timeoutMs),\n );\n const startedAt = Date.now();\n const curl = curlForRequest(input, init);\n\n try {\n const response = await fetch(input, { ...init, signal: controller.signal });\n if (!response.ok) {\n logUpstreamFailure({\n input,\n init,\n status: response.status,\n statusText: response.statusText,\n durationMs: Date.now() - startedAt,\n });\n }\n return response;\n } catch (error) {\n // A caller-driven abort is not a failure; it is someone navigating away.\n if (!external?.aborted) {\n logUpstreamFailure({\n input,\n init,\n durationMs: Date.now() - startedAt,\n error:\n controller.signal.aborted && !external?.aborted\n ? new UpstreamTimeoutError(\"Upstream request timed out\", { curl })\n : error,\n });\n }\n if (controller.signal.aborted && !external?.aborted) {\n throw new UpstreamTimeoutError(\"Upstream request timed out\", { curl });\n }\n if (error instanceof Error && !(error as { curl?: string }).curl) {\n (error as { curl?: string }).curl = curl;\n }\n throw error;\n } finally {\n clearTimeout(timer);\n external?.removeEventListener(\"abort\", onExternalAbort);\n }\n}\n\n/**\n * `message` is sometimes a nested object (`{\"message\":{\"error\":\"…\"}}`).\n * Treating that as a string throws `Error: [object Object]`.\n */\nfunction readUpstreamError(value: unknown): string | null {\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n return trimmed || null;\n }\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const record = value as Record<string, unknown>;\n return readUpstreamError(record.message) ?? readUpstreamError(record.error);\n }\n return null;\n}\n\n/**\n * Reads an upstream response: returns parsed JSON on success (undefined for\n * 204), otherwise extracts a `message`/`error` field and throws via the\n * provided factory so callers keep their own typed error class + status.\n */\nexport async function parseUpstream<T>(\n response: Response,\n makeError: (message: string, status: number) => Error,\n fallbackMessage = `Request failed with status ${response.status}`,\n): Promise<T> {\n const text = await response.text();\n const trimmed = text.trim();\n\n if (response.ok) {\n if (response.status === 204 || !trimmed) return undefined as T;\n try {\n return JSON.parse(trimmed) as T;\n } catch {\n throw makeError(`Invalid JSON from upstream (${response.status})`, 502);\n }\n }\n\n let message = fallbackMessage;\n if (trimmed) {\n try {\n message = readUpstreamError(JSON.parse(trimmed)) || message;\n } catch {\n // A plain-text failure (`Unable to read json value`) is still the reason.\n if (trimmed.length <= 300 && !trimmed.startsWith(\"<\")) message = trimmed;\n }\n }\n\n throw makeError(message, response.status);\n}\n","import { toCurl } from \"./curl\";\nimport { UpstreamError } from \"./errors\";\nimport { fetchUpstream, parseUpstream } from \"./upstream\";\n\nexport type UpstreamClientConfig = {\n /** Read on every request, so tests and deployments can swap it lazily. */\n baseUrl: () => string;\n /** Read on every request; a natural place to attach a system token. */\n headers?: () => HeadersInit;\n /**\n * Builds the error thrown for a non-OK response.\n * Defaults to `UpstreamError`. Third arg is a redacted curl (optional).\n */\n makeError?: (message: string, status: number, curl?: string) => Error;\n /** Default timeout for this client; see `fetchUpstream`. */\n timeoutMs?: number;\n};\n\nexport type UpstreamRequest = Omit<RequestInit, \"body\" | \"headers\"> & {\n /** Serialised with `JSON.stringify`. */\n body?: unknown;\n /** Merged over the client's headers; same-named ones win. */\n headers?: HeadersInit;\n /** Overrides the client's timeout for this call. */\n timeoutMs?: number;\n};\n\nexport interface UpstreamClient {\n request<T>(path: string, init?: UpstreamRequest): Promise<T>;\n}\n\nfunction mergeHeaders(base?: HeadersInit, extra?: HeadersInit): Headers {\n const headers = new Headers(base);\n if (extra) {\n new Headers(extra).forEach((value, key) => headers.set(key, value));\n }\n return headers;\n}\n\nconst defaultMakeError = (message: string, status: number, curl?: string) =>\n new UpstreamError(message, status, { curl });\n\nfunction attachCurl(error: Error, curl: string): Error {\n if (!(error as { curl?: string }).curl) {\n (error as { curl?: string }).curl = curl;\n }\n return error;\n}\n\n/**\n * Creates a small server-only JSON client. Configuration stays lazy so tests\n * and deployments can substitute environment-backed URLs and credentials.\n *\n * On failure the thrown error has:\n * - `message` — upstream / timeout reason (safe for the API JSON body)\n * - `status` — HTTP status for `errorResponse`\n * - `curl` — redacted replay line for **server logs only**\n */\nexport function createUpstreamClient(\n config: UpstreamClientConfig,\n): UpstreamClient {\n const makeError = config.makeError ?? defaultMakeError;\n\n return {\n async request<T>(path: string, init: UpstreamRequest = {}): Promise<T> {\n const { body, headers: requestHeaders, timeoutMs, ...requestInit } = init;\n const url = `${config.baseUrl()}${path.startsWith(\"/\") ? path : `/${path}`}`;\n const headers = mergeHeaders(config.headers?.(), requestHeaders);\n const fetchInit: RequestInit = {\n ...requestInit,\n headers,\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n };\n const curl = toCurl({\n method: (fetchInit.method || \"GET\").toUpperCase(),\n url,\n headers,\n body: fetchInit.body,\n });\n\n const response = await fetchUpstream(\n url,\n fetchInit,\n timeoutMs ?? config.timeoutMs,\n );\n\n return parseUpstream<T>(response, (message, status) =>\n attachCurl(makeError(message, status, curl), curl),\n );\n },\n };\n}\n","/**\n * Turning a thrown error into an HTTP response, for route handlers.\n *\n * This does not list error classes. Anything thrown with a numeric `status`\n * (every error in this package, and most upstream ones) keeps that status, so\n * an upstream 404 reaches the browser as a 404 rather than a 500.\n *\n * Returns a standard `Response`, which Next.js route handlers, Hono, Remix and\n * any other fetch-style server accept as-is.\n *\n * **Body is always `{ message }` only.** Never include `curl` — that is for\n * Cloud Logging via `logApiError`.\n */\n\nimport { curlOf, isUpstreamFailure } from \"./errors\";\n\n/** Only a real HTTP failure code. Anything else is our fault, so 500. */\nexport function statusOf(error: unknown): number {\n const status = (error as { status?: unknown } | null | undefined)?.status;\n return typeof status === \"number\" && status >= 400 && status <= 599\n ? status\n : 500;\n}\n\nexport function messageOf(\n error: unknown,\n fallback = \"Request failed\",\n): string {\n return error instanceof Error && error.message ? error.message : fallback;\n}\n\n/**\n * `fallback` is used only when the thrown value is not an `Error` (a rejected\n * string, say), or is one with an empty message. A real error's own message is\n * more useful than a generic one.\n */\nexport function errorResponse(\n error: unknown,\n fallback = \"Request failed\",\n): Response {\n return Response.json(\n { message: messageOf(error, fallback) },\n { status: statusOf(error) },\n );\n}\n\n/** A rejected payload: the caller sent something we will not act on. */\nexport function invalidResponse(message: string): Response {\n return Response.json({ message }, { status: 400 });\n}\n\n/**\n * Log an API / upstream failure for Cloud Logging, including redacted curl\n * when present. Call this in a route `catch` **before** `errorResponse`.\n *\n * ```ts\n * } catch (error) {\n * logApiError(error, { service: \"inpix-panel\", route: \"POST /api/x\" });\n * return errorResponse(error);\n * }\n * ```\n */\nexport function logApiError(\n error: unknown,\n context: {\n service: string;\n route?: string;\n requestId?: string;\n },\n): void {\n const status = statusOf(error);\n const message = messageOf(error);\n const curl = curlOf(error);\n const severity = status < 500 ? \"WARNING\" : \"ERROR\";\n const write = severity === \"WARNING\" ? console.warn : console.error;\n\n write(\n JSON.stringify({\n severity,\n message,\n event: \"api-error\",\n service: context.service,\n route: context.route,\n requestId: context.requestId,\n status,\n curl: curl ?? undefined,\n name: error instanceof Error ? error.name : undefined,\n upstream: isUpstreamFailure(error) || undefined,\n }),\n );\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@inshorts/glint-http",
3
+ "version": "1.2.0",
4
+ "description": "Glint server-side HTTP client: timeouts, typed errors, redacted curl on failure.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/NewsInShorts/inshorts-npm-modules.git",
34
+ "directory": "glint/http"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "test": "vitest run",
39
+ "typecheck": "tsc --noEmit",
40
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.10.0",
44
+ "tsup": "^8.3.5",
45
+ "typescript": "^5.7.2",
46
+ "vitest": "^2.1.8"
47
+ }
48
+ }