@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.
@@ -0,0 +1,200 @@
1
+ type UpstreamClientConfig = {
2
+ /** Read on every request, so tests and deployments can swap it lazily. */
3
+ baseUrl: () => string;
4
+ /** Read on every request; a natural place to attach a system token. */
5
+ headers?: () => HeadersInit;
6
+ /**
7
+ * Builds the error thrown for a non-OK response.
8
+ * Defaults to `UpstreamError`. Third arg is a redacted curl (optional).
9
+ */
10
+ makeError?: (message: string, status: number, curl?: string) => Error;
11
+ /** Default timeout for this client; see `fetchUpstream`. */
12
+ timeoutMs?: number;
13
+ };
14
+ type UpstreamRequest = Omit<RequestInit, "body" | "headers"> & {
15
+ /** Serialised with `JSON.stringify`. */
16
+ body?: unknown;
17
+ /** Merged over the client's headers; same-named ones win. */
18
+ headers?: HeadersInit;
19
+ /** Overrides the client's timeout for this call. */
20
+ timeoutMs?: number;
21
+ };
22
+ interface UpstreamClient {
23
+ request<T>(path: string, init?: UpstreamRequest): Promise<T>;
24
+ }
25
+ /**
26
+ * Creates a small server-only JSON client. Configuration stays lazy so tests
27
+ * and deployments can substitute environment-backed URLs and credentials.
28
+ *
29
+ * On failure the thrown error has:
30
+ * - `message` — upstream / timeout reason (safe for the API JSON body)
31
+ * - `status` — HTTP status for `errorResponse`
32
+ * - `curl` — redacted replay line for **server logs only**
33
+ */
34
+ declare function createUpstreamClient(config: UpstreamClientConfig): UpstreamClient;
35
+
36
+ /**
37
+ * Turns a request into a `curl` line you can paste into a terminal.
38
+ *
39
+ * The point is the thing that is missing when an upstream call fails in
40
+ * production: the log says a request to some service returned 502, and
41
+ * reproducing it means reading the calling code to work out what was actually
42
+ * sent. This writes that down at the moment it fails.
43
+ *
44
+ * **Nothing secret goes in.** Every header whose name looks like a credential
45
+ * is emitted as `$ENV_VAR` where the value matches a known one, and
46
+ * `<redacted>` otherwise — so the line is still runnable after
47
+ * `export SOME_SYS_TOKEN=…`, without the token itself ever reaching a log
48
+ * aggregator. Body fields that look like credentials get the same treatment.
49
+ *
50
+ * The alternative — logging the real header — is how tokens end up in Cloud
51
+ * Logging, readable by anyone with project viewer, retained for a month, and
52
+ * impossible to rotate out of the archive.
53
+ */
54
+ interface CurlRequest {
55
+ method?: string;
56
+ url: string;
57
+ headers?: HeadersInit;
58
+ body?: unknown;
59
+ }
60
+ interface CurlOptions {
61
+ /** Longest body to include before truncating. */
62
+ maxBody?: number;
63
+ /**
64
+ * Environment variable names to match secret values against, so the line
65
+ * comes out as `$SOME_SYS_TOKEN` rather than `<redacted>` and can actually
66
+ * be replayed. Defaults to `process.env`; only values of 8+ characters are
67
+ * matched.
68
+ */
69
+ env?: Record<string, string | undefined>;
70
+ }
71
+ declare function toCurl(request: CurlRequest, options?: CurlOptions): string;
72
+
73
+ /**
74
+ * Errors this package throws. Each carries a numeric `status`, so
75
+ * `errorResponse` turns it into the right HTTP code without having to know
76
+ * the class.
77
+ *
78
+ * `curl` is for **server logs only** — never put it in the JSON body sent to
79
+ * the browser (`errorResponse` strips it).
80
+ */
81
+ /** A required upstream setting (base URL, token) is missing. */
82
+ declare class UpstreamConfigError extends Error {
83
+ status: number;
84
+ curl?: string;
85
+ constructor(message: string, options?: {
86
+ curl?: string;
87
+ });
88
+ }
89
+ /** The upstream did not answer before the deadline. */
90
+ declare class UpstreamTimeoutError extends Error {
91
+ status: number;
92
+ curl?: string;
93
+ constructor(message?: string, options?: {
94
+ curl?: string;
95
+ });
96
+ }
97
+ /**
98
+ * The default error `createUpstreamClient` throws for a non-OK response when
99
+ * the caller does not supply its own `makeError`.
100
+ */
101
+ declare class UpstreamError extends Error {
102
+ status: number;
103
+ curl?: string;
104
+ constructor(message: string, status: number, options?: {
105
+ curl?: string;
106
+ });
107
+ }
108
+ /** True when `error` looks like one of ours (or any `{ status, message }`). */
109
+ declare function isUpstreamFailure(error: unknown): error is Error & {
110
+ status: number;
111
+ curl?: string;
112
+ };
113
+ /** Read a redacted replay curl if the thrown value carries one. */
114
+ declare function curlOf(error: unknown): string | undefined;
115
+
116
+ /**
117
+ * Turning a thrown error into an HTTP response, for route handlers.
118
+ *
119
+ * This does not list error classes. Anything thrown with a numeric `status`
120
+ * (every error in this package, and most upstream ones) keeps that status, so
121
+ * an upstream 404 reaches the browser as a 404 rather than a 500.
122
+ *
123
+ * Returns a standard `Response`, which Next.js route handlers, Hono, Remix and
124
+ * any other fetch-style server accept as-is.
125
+ *
126
+ * **Body is always `{ message }` only.** Never include `curl` — that is for
127
+ * Cloud Logging via `logApiError`.
128
+ */
129
+ /** Only a real HTTP failure code. Anything else is our fault, so 500. */
130
+ declare function statusOf(error: unknown): number;
131
+ declare function messageOf(error: unknown, fallback?: string): string;
132
+ /**
133
+ * `fallback` is used only when the thrown value is not an `Error` (a rejected
134
+ * string, say), or is one with an empty message. A real error's own message is
135
+ * more useful than a generic one.
136
+ */
137
+ declare function errorResponse(error: unknown, fallback?: string): Response;
138
+ /** A rejected payload: the caller sent something we will not act on. */
139
+ declare function invalidResponse(message: string): Response;
140
+ /**
141
+ * Log an API / upstream failure for Cloud Logging, including redacted curl
142
+ * when present. Call this in a route `catch` **before** `errorResponse`.
143
+ *
144
+ * ```ts
145
+ * } catch (error) {
146
+ * logApiError(error, { service: "inpix-panel", route: "POST /api/x" });
147
+ * return errorResponse(error);
148
+ * }
149
+ * ```
150
+ */
151
+ declare function logApiError(error: unknown, context: {
152
+ service: string;
153
+ route?: string;
154
+ requestId?: string;
155
+ }): void;
156
+
157
+ /**
158
+ * Server-side `fetch` for calls to internal upstream services. These calls
159
+ * usually attach system tokens, so never import this into browser code.
160
+ */
161
+ /**
162
+ * Base headers for an internal service-to-service call. `deviceId` names the
163
+ * calling app (`"inpix-panel"`, `"users-crm"`) so upstream logs show who sent it.
164
+ */
165
+ declare function internalHeaders(deviceId: string): Record<string, string>;
166
+ /** Used when neither the call nor `UPSTREAM_TIMEOUT_MS` sets a timeout. */
167
+ declare const DEFAULT_UPSTREAM_TIMEOUT_MS = 15000;
168
+ /**
169
+ * Require an absolute upstream base URL. There is deliberately no default
170
+ * host: missing config fails closed instead of quietly calling a hard-coded one.
171
+ */
172
+ declare function requireUpstreamBaseUrl(value: string | null | undefined, envName: string): string;
173
+ /** Test seam. */
174
+ declare function resetUpstreamFailureLog(): void;
175
+ /**
176
+ * `fetch` with an AbortSignal timeout. Throws `UpstreamTimeoutError` (status
177
+ * 504) when the deadline is hit before the upstream responds.
178
+ *
179
+ * The timeout is `timeoutMs` if given, else the `UPSTREAM_TIMEOUT_MS` env var,
180
+ * else `DEFAULT_UPSTREAM_TIMEOUT_MS`.
181
+ *
182
+ * Every failure — a non-OK status, a timeout, a refused connection — is logged
183
+ * once here with a replayable curl, so no caller has to remember to do it and
184
+ * none of them can do it differently.
185
+ *
186
+ * No caller currently treats a non-OK status as normal flow, so there is no
187
+ * opt-out. If one appears — a lookup where 404 means "absent" — add a
188
+ * `silentStatuses` option here rather than logging around it at the call site.
189
+ */
190
+ /** Build the redacted replay curl for this attempt (also used on thrown errors). */
191
+ declare function curlForRequest(input: RequestInfo | URL, init?: RequestInit): string;
192
+ declare function fetchUpstream(input: RequestInfo | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
193
+ /**
194
+ * Reads an upstream response: returns parsed JSON on success (undefined for
195
+ * 204), otherwise extracts a `message`/`error` field and throws via the
196
+ * provided factory so callers keep their own typed error class + status.
197
+ */
198
+ declare function parseUpstream<T>(response: Response, makeError: (message: string, status: number) => Error, fallbackMessage?: string): Promise<T>;
199
+
200
+ export { type CurlOptions, type CurlRequest, DEFAULT_UPSTREAM_TIMEOUT_MS, type UpstreamClient, type UpstreamClientConfig, UpstreamConfigError, UpstreamError, type UpstreamRequest, UpstreamTimeoutError, createUpstreamClient, curlForRequest, curlOf, errorResponse, fetchUpstream, internalHeaders, invalidResponse, isUpstreamFailure, logApiError, messageOf, parseUpstream, requireUpstreamBaseUrl, resetUpstreamFailureLog, statusOf, toCurl };
@@ -0,0 +1,200 @@
1
+ type UpstreamClientConfig = {
2
+ /** Read on every request, so tests and deployments can swap it lazily. */
3
+ baseUrl: () => string;
4
+ /** Read on every request; a natural place to attach a system token. */
5
+ headers?: () => HeadersInit;
6
+ /**
7
+ * Builds the error thrown for a non-OK response.
8
+ * Defaults to `UpstreamError`. Third arg is a redacted curl (optional).
9
+ */
10
+ makeError?: (message: string, status: number, curl?: string) => Error;
11
+ /** Default timeout for this client; see `fetchUpstream`. */
12
+ timeoutMs?: number;
13
+ };
14
+ type UpstreamRequest = Omit<RequestInit, "body" | "headers"> & {
15
+ /** Serialised with `JSON.stringify`. */
16
+ body?: unknown;
17
+ /** Merged over the client's headers; same-named ones win. */
18
+ headers?: HeadersInit;
19
+ /** Overrides the client's timeout for this call. */
20
+ timeoutMs?: number;
21
+ };
22
+ interface UpstreamClient {
23
+ request<T>(path: string, init?: UpstreamRequest): Promise<T>;
24
+ }
25
+ /**
26
+ * Creates a small server-only JSON client. Configuration stays lazy so tests
27
+ * and deployments can substitute environment-backed URLs and credentials.
28
+ *
29
+ * On failure the thrown error has:
30
+ * - `message` — upstream / timeout reason (safe for the API JSON body)
31
+ * - `status` — HTTP status for `errorResponse`
32
+ * - `curl` — redacted replay line for **server logs only**
33
+ */
34
+ declare function createUpstreamClient(config: UpstreamClientConfig): UpstreamClient;
35
+
36
+ /**
37
+ * Turns a request into a `curl` line you can paste into a terminal.
38
+ *
39
+ * The point is the thing that is missing when an upstream call fails in
40
+ * production: the log says a request to some service returned 502, and
41
+ * reproducing it means reading the calling code to work out what was actually
42
+ * sent. This writes that down at the moment it fails.
43
+ *
44
+ * **Nothing secret goes in.** Every header whose name looks like a credential
45
+ * is emitted as `$ENV_VAR` where the value matches a known one, and
46
+ * `<redacted>` otherwise — so the line is still runnable after
47
+ * `export SOME_SYS_TOKEN=…`, without the token itself ever reaching a log
48
+ * aggregator. Body fields that look like credentials get the same treatment.
49
+ *
50
+ * The alternative — logging the real header — is how tokens end up in Cloud
51
+ * Logging, readable by anyone with project viewer, retained for a month, and
52
+ * impossible to rotate out of the archive.
53
+ */
54
+ interface CurlRequest {
55
+ method?: string;
56
+ url: string;
57
+ headers?: HeadersInit;
58
+ body?: unknown;
59
+ }
60
+ interface CurlOptions {
61
+ /** Longest body to include before truncating. */
62
+ maxBody?: number;
63
+ /**
64
+ * Environment variable names to match secret values against, so the line
65
+ * comes out as `$SOME_SYS_TOKEN` rather than `<redacted>` and can actually
66
+ * be replayed. Defaults to `process.env`; only values of 8+ characters are
67
+ * matched.
68
+ */
69
+ env?: Record<string, string | undefined>;
70
+ }
71
+ declare function toCurl(request: CurlRequest, options?: CurlOptions): string;
72
+
73
+ /**
74
+ * Errors this package throws. Each carries a numeric `status`, so
75
+ * `errorResponse` turns it into the right HTTP code without having to know
76
+ * the class.
77
+ *
78
+ * `curl` is for **server logs only** — never put it in the JSON body sent to
79
+ * the browser (`errorResponse` strips it).
80
+ */
81
+ /** A required upstream setting (base URL, token) is missing. */
82
+ declare class UpstreamConfigError extends Error {
83
+ status: number;
84
+ curl?: string;
85
+ constructor(message: string, options?: {
86
+ curl?: string;
87
+ });
88
+ }
89
+ /** The upstream did not answer before the deadline. */
90
+ declare class UpstreamTimeoutError extends Error {
91
+ status: number;
92
+ curl?: string;
93
+ constructor(message?: string, options?: {
94
+ curl?: string;
95
+ });
96
+ }
97
+ /**
98
+ * The default error `createUpstreamClient` throws for a non-OK response when
99
+ * the caller does not supply its own `makeError`.
100
+ */
101
+ declare class UpstreamError extends Error {
102
+ status: number;
103
+ curl?: string;
104
+ constructor(message: string, status: number, options?: {
105
+ curl?: string;
106
+ });
107
+ }
108
+ /** True when `error` looks like one of ours (or any `{ status, message }`). */
109
+ declare function isUpstreamFailure(error: unknown): error is Error & {
110
+ status: number;
111
+ curl?: string;
112
+ };
113
+ /** Read a redacted replay curl if the thrown value carries one. */
114
+ declare function curlOf(error: unknown): string | undefined;
115
+
116
+ /**
117
+ * Turning a thrown error into an HTTP response, for route handlers.
118
+ *
119
+ * This does not list error classes. Anything thrown with a numeric `status`
120
+ * (every error in this package, and most upstream ones) keeps that status, so
121
+ * an upstream 404 reaches the browser as a 404 rather than a 500.
122
+ *
123
+ * Returns a standard `Response`, which Next.js route handlers, Hono, Remix and
124
+ * any other fetch-style server accept as-is.
125
+ *
126
+ * **Body is always `{ message }` only.** Never include `curl` — that is for
127
+ * Cloud Logging via `logApiError`.
128
+ */
129
+ /** Only a real HTTP failure code. Anything else is our fault, so 500. */
130
+ declare function statusOf(error: unknown): number;
131
+ declare function messageOf(error: unknown, fallback?: string): string;
132
+ /**
133
+ * `fallback` is used only when the thrown value is not an `Error` (a rejected
134
+ * string, say), or is one with an empty message. A real error's own message is
135
+ * more useful than a generic one.
136
+ */
137
+ declare function errorResponse(error: unknown, fallback?: string): Response;
138
+ /** A rejected payload: the caller sent something we will not act on. */
139
+ declare function invalidResponse(message: string): Response;
140
+ /**
141
+ * Log an API / upstream failure for Cloud Logging, including redacted curl
142
+ * when present. Call this in a route `catch` **before** `errorResponse`.
143
+ *
144
+ * ```ts
145
+ * } catch (error) {
146
+ * logApiError(error, { service: "inpix-panel", route: "POST /api/x" });
147
+ * return errorResponse(error);
148
+ * }
149
+ * ```
150
+ */
151
+ declare function logApiError(error: unknown, context: {
152
+ service: string;
153
+ route?: string;
154
+ requestId?: string;
155
+ }): void;
156
+
157
+ /**
158
+ * Server-side `fetch` for calls to internal upstream services. These calls
159
+ * usually attach system tokens, so never import this into browser code.
160
+ */
161
+ /**
162
+ * Base headers for an internal service-to-service call. `deviceId` names the
163
+ * calling app (`"inpix-panel"`, `"users-crm"`) so upstream logs show who sent it.
164
+ */
165
+ declare function internalHeaders(deviceId: string): Record<string, string>;
166
+ /** Used when neither the call nor `UPSTREAM_TIMEOUT_MS` sets a timeout. */
167
+ declare const DEFAULT_UPSTREAM_TIMEOUT_MS = 15000;
168
+ /**
169
+ * Require an absolute upstream base URL. There is deliberately no default
170
+ * host: missing config fails closed instead of quietly calling a hard-coded one.
171
+ */
172
+ declare function requireUpstreamBaseUrl(value: string | null | undefined, envName: string): string;
173
+ /** Test seam. */
174
+ declare function resetUpstreamFailureLog(): void;
175
+ /**
176
+ * `fetch` with an AbortSignal timeout. Throws `UpstreamTimeoutError` (status
177
+ * 504) when the deadline is hit before the upstream responds.
178
+ *
179
+ * The timeout is `timeoutMs` if given, else the `UPSTREAM_TIMEOUT_MS` env var,
180
+ * else `DEFAULT_UPSTREAM_TIMEOUT_MS`.
181
+ *
182
+ * Every failure — a non-OK status, a timeout, a refused connection — is logged
183
+ * once here with a replayable curl, so no caller has to remember to do it and
184
+ * none of them can do it differently.
185
+ *
186
+ * No caller currently treats a non-OK status as normal flow, so there is no
187
+ * opt-out. If one appears — a lookup where 404 means "absent" — add a
188
+ * `silentStatuses` option here rather than logging around it at the call site.
189
+ */
190
+ /** Build the redacted replay curl for this attempt (also used on thrown errors). */
191
+ declare function curlForRequest(input: RequestInfo | URL, init?: RequestInit): string;
192
+ declare function fetchUpstream(input: RequestInfo | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
193
+ /**
194
+ * Reads an upstream response: returns parsed JSON on success (undefined for
195
+ * 204), otherwise extracts a `message`/`error` field and throws via the
196
+ * provided factory so callers keep their own typed error class + status.
197
+ */
198
+ declare function parseUpstream<T>(response: Response, makeError: (message: string, status: number) => Error, fallbackMessage?: string): Promise<T>;
199
+
200
+ export { type CurlOptions, type CurlRequest, DEFAULT_UPSTREAM_TIMEOUT_MS, type UpstreamClient, type UpstreamClientConfig, UpstreamConfigError, UpstreamError, type UpstreamRequest, UpstreamTimeoutError, createUpstreamClient, curlForRequest, curlOf, errorResponse, fetchUpstream, internalHeaders, invalidResponse, isUpstreamFailure, logApiError, messageOf, parseUpstream, requireUpstreamBaseUrl, resetUpstreamFailureLog, statusOf, toCurl };