@inshorts/glint-http 1.2.0 → 1.5.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,39 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/browser.ts
21
+ var browser_exports = {};
22
+ __export(browser_exports, {
23
+ readJson: () => readJson
24
+ });
25
+ module.exports = __toCommonJS(browser_exports);
26
+ async function readJson(response, options = {}) {
27
+ const json = await response.json().catch(() => null);
28
+ if (!response.ok) {
29
+ throw new Error(
30
+ typeof json?.message === "string" && json.message || typeof json?.error === "string" && json.error || options.fallbackMessage || "Request failed"
31
+ );
32
+ }
33
+ return json ?? {};
34
+ }
35
+ // Annotate the CommonJS export names for ESM import in node:
36
+ 0 && (module.exports = {
37
+ readJson
38
+ });
39
+ //# sourceMappingURL=browser.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["/**\n * Browser-side JSON helpers for panel `fetch` call sites.\n *\n * Server code should keep using `fetchUpstream` / `createUpstreamClient`.\n * This module only parses a `Response` the browser already received.\n */\n\nexport type BrowserJson = {\n data?: unknown;\n message?: string;\n error?: string;\n [key: string]: unknown;\n};\n\nexport type ReadJsonOptions = {\n /** Used when the body has neither `message` nor `error`. */\n fallbackMessage?: string;\n};\n\n/**\n * Parse JSON from a browser `fetch` response.\n *\n * Throws `Error` with the body's `message` (or CRM `error`) when `!ok`.\n * Never throws on empty/invalid JSON for OK responses — returns `{}`.\n */\nexport async function readJson(\n response: Response,\n options: ReadJsonOptions = {},\n): Promise<BrowserJson> {\n const json = (await response.json().catch(() => null)) as BrowserJson | null;\n if (!response.ok) {\n throw new Error(\n (typeof json?.message === \"string\" && json.message) ||\n (typeof json?.error === \"string\" && json.error) ||\n options.fallbackMessage ||\n \"Request failed\",\n );\n }\n return json ?? {};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,eAAsB,SACpB,UACA,UAA2B,CAAC,GACN;AACtB,QAAM,OAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,IAAI;AACpD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACP,OAAO,MAAM,YAAY,YAAY,KAAK,WACxC,OAAO,MAAM,UAAU,YAAY,KAAK,SACzC,QAAQ,mBACR;AAAA,IACJ;AAAA,EACF;AACA,SAAO,QAAQ,CAAC;AAClB;","names":[]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Browser-side JSON helpers for panel `fetch` call sites.
3
+ *
4
+ * Server code should keep using `fetchUpstream` / `createUpstreamClient`.
5
+ * This module only parses a `Response` the browser already received.
6
+ */
7
+ type BrowserJson = {
8
+ data?: unknown;
9
+ message?: string;
10
+ error?: string;
11
+ [key: string]: unknown;
12
+ };
13
+ type ReadJsonOptions = {
14
+ /** Used when the body has neither `message` nor `error`. */
15
+ fallbackMessage?: string;
16
+ };
17
+ /**
18
+ * Parse JSON from a browser `fetch` response.
19
+ *
20
+ * Throws `Error` with the body's `message` (or CRM `error`) when `!ok`.
21
+ * Never throws on empty/invalid JSON for OK responses — returns `{}`.
22
+ */
23
+ declare function readJson(response: Response, options?: ReadJsonOptions): Promise<BrowserJson>;
24
+
25
+ export { type BrowserJson, type ReadJsonOptions, readJson };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Browser-side JSON helpers for panel `fetch` call sites.
3
+ *
4
+ * Server code should keep using `fetchUpstream` / `createUpstreamClient`.
5
+ * This module only parses a `Response` the browser already received.
6
+ */
7
+ type BrowserJson = {
8
+ data?: unknown;
9
+ message?: string;
10
+ error?: string;
11
+ [key: string]: unknown;
12
+ };
13
+ type ReadJsonOptions = {
14
+ /** Used when the body has neither `message` nor `error`. */
15
+ fallbackMessage?: string;
16
+ };
17
+ /**
18
+ * Parse JSON from a browser `fetch` response.
19
+ *
20
+ * Throws `Error` with the body's `message` (or CRM `error`) when `!ok`.
21
+ * Never throws on empty/invalid JSON for OK responses — returns `{}`.
22
+ */
23
+ declare function readJson(response: Response, options?: ReadJsonOptions): Promise<BrowserJson>;
24
+
25
+ export { type BrowserJson, type ReadJsonOptions, readJson };
@@ -0,0 +1,7 @@
1
+ import {
2
+ readJson
3
+ } from "./chunk-5GQCQ73P.js";
4
+ export {
5
+ readJson
6
+ };
7
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,15 @@
1
+ // src/browser.ts
2
+ async function readJson(response, options = {}) {
3
+ const json = await response.json().catch(() => null);
4
+ if (!response.ok) {
5
+ throw new Error(
6
+ typeof json?.message === "string" && json.message || typeof json?.error === "string" && json.error || options.fallbackMessage || "Request failed"
7
+ );
8
+ }
9
+ return json ?? {};
10
+ }
11
+
12
+ export {
13
+ readJson
14
+ };
15
+ //# sourceMappingURL=chunk-5GQCQ73P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["/**\n * Browser-side JSON helpers for panel `fetch` call sites.\n *\n * Server code should keep using `fetchUpstream` / `createUpstreamClient`.\n * This module only parses a `Response` the browser already received.\n */\n\nexport type BrowserJson = {\n data?: unknown;\n message?: string;\n error?: string;\n [key: string]: unknown;\n};\n\nexport type ReadJsonOptions = {\n /** Used when the body has neither `message` nor `error`. */\n fallbackMessage?: string;\n};\n\n/**\n * Parse JSON from a browser `fetch` response.\n *\n * Throws `Error` with the body's `message` (or CRM `error`) when `!ok`.\n * Never throws on empty/invalid JSON for OK responses — returns `{}`.\n */\nexport async function readJson(\n response: Response,\n options: ReadJsonOptions = {},\n): Promise<BrowserJson> {\n const json = (await response.json().catch(() => null)) as BrowserJson | null;\n if (!response.ok) {\n throw new Error(\n (typeof json?.message === \"string\" && json.message) ||\n (typeof json?.error === \"string\" && json.error) ||\n options.fallbackMessage ||\n \"Request failed\",\n );\n }\n return json ?? {};\n}\n"],"mappings":";AAyBA,eAAsB,SACpB,UACA,UAA2B,CAAC,GACN;AACtB,QAAM,OAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,IAAI;AACpD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACP,OAAO,MAAM,YAAY,YAAY,KAAK,WACxC,OAAO,MAAM,UAAU,YAAY,KAAK,SACzC,QAAQ,mBACR;AAAA,IACJ;AAAA,EACF;AACA,SAAO,QAAQ,CAAC;AAClB;","names":[]}
package/dist/index.cjs CHANGED
@@ -24,6 +24,8 @@ __export(index_exports, {
24
24
  UpstreamConfigError: () => UpstreamConfigError,
25
25
  UpstreamError: () => UpstreamError,
26
26
  UpstreamTimeoutError: () => UpstreamTimeoutError,
27
+ authTokenOf: () => authTokenOf,
28
+ createApiResponder: () => createApiResponder,
27
29
  createUpstreamClient: () => createUpstreamClient,
28
30
  curlForRequest: () => curlForRequest,
29
31
  curlOf: () => curlOf,
@@ -35,6 +37,9 @@ __export(index_exports, {
35
37
  logApiError: () => logApiError,
36
38
  messageOf: () => messageOf,
37
39
  parseUpstream: () => parseUpstream,
40
+ putAuthorLogin: () => putAuthorLogin,
41
+ readAuthorLoginBody: () => readAuthorLoginBody,
42
+ readJson: () => readJson,
38
43
  requireUpstreamBaseUrl: () => requireUpstreamBaseUrl,
39
44
  resetUpstreamFailureLog: () => resetUpstreamFailureLog,
40
45
  statusOf: () => statusOf,
@@ -404,12 +409,80 @@ function logApiError(error, context) {
404
409
  })
405
410
  );
406
411
  }
412
+
413
+ // src/responder.ts
414
+ function errorBody(bodyKey, text) {
415
+ return { [bodyKey]: text };
416
+ }
417
+ function createApiResponder(options) {
418
+ const service = options.service.trim();
419
+ if (!service) {
420
+ throw new Error("createApiResponder: service is required");
421
+ }
422
+ const bodyKey = options.bodyKey ?? "message";
423
+ function errorResponse2(error, fallback = "Request failed") {
424
+ return Response.json(errorBody(bodyKey, messageOf(error, fallback)), {
425
+ status: statusOf(error)
426
+ });
427
+ }
428
+ function invalidResponse2(message) {
429
+ return Response.json(errorBody(bodyKey, message), { status: 400 });
430
+ }
431
+ function respondApiError(error, fallback, route, requestId) {
432
+ logApiError(error, { service, route, requestId });
433
+ return errorResponse2(error, fallback);
434
+ }
435
+ return { errorResponse: errorResponse2, invalidResponse: invalidResponse2, respondApiError };
436
+ }
437
+
438
+ // src/authorLogin.ts
439
+ async function putAuthorLogin(options) {
440
+ const base = options.baseUrl.replace(/\/$/, "");
441
+ const rawPath = options.path?.trim() || "/v1/author/login";
442
+ const path = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
443
+ const loginUrl = `${base}${path}`;
444
+ return fetchUpstream(
445
+ loginUrl,
446
+ {
447
+ method: "PUT",
448
+ headers: {
449
+ "X-AUTH-TOKEN": options.systemToken,
450
+ "Content-Type": "application/json"
451
+ },
452
+ body: JSON.stringify({ access_token: options.accessToken })
453
+ },
454
+ options.timeoutMs
455
+ );
456
+ }
457
+ async function readAuthorLoginBody(response) {
458
+ try {
459
+ return await response.json();
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+ function authTokenOf(body) {
465
+ return (body?.auth_token ?? body?.token ?? "").trim();
466
+ }
467
+
468
+ // src/browser.ts
469
+ async function readJson(response, options = {}) {
470
+ const json = await response.json().catch(() => null);
471
+ if (!response.ok) {
472
+ throw new Error(
473
+ typeof json?.message === "string" && json.message || typeof json?.error === "string" && json.error || options.fallbackMessage || "Request failed"
474
+ );
475
+ }
476
+ return json ?? {};
477
+ }
407
478
  // Annotate the CommonJS export names for ESM import in node:
408
479
  0 && (module.exports = {
409
480
  DEFAULT_UPSTREAM_TIMEOUT_MS,
410
481
  UpstreamConfigError,
411
482
  UpstreamError,
412
483
  UpstreamTimeoutError,
484
+ authTokenOf,
485
+ createApiResponder,
413
486
  createUpstreamClient,
414
487
  curlForRequest,
415
488
  curlOf,
@@ -421,6 +494,9 @@ function logApiError(error, context) {
421
494
  logApiError,
422
495
  messageOf,
423
496
  parseUpstream,
497
+ putAuthorLogin,
498
+ readAuthorLoginBody,
499
+ readJson,
424
500
  requireUpstreamBaseUrl,
425
501
  resetUpstreamFailureLog,
426
502
  statusOf,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/curl.ts","../src/errors.ts","../src/upstream.ts","../src/client.ts","../src/response.ts"],"sourcesContent":["export {\n createUpstreamClient,\n type UpstreamClient,\n type UpstreamClientConfig,\n type UpstreamRequest,\n} from \"./client\";\nexport { toCurl, type CurlOptions, type CurlRequest } from \"./curl\";\nexport {\n UpstreamConfigError,\n UpstreamError,\n UpstreamTimeoutError,\n curlOf,\n isUpstreamFailure,\n} from \"./errors\";\nexport {\n errorResponse,\n invalidResponse,\n logApiError,\n messageOf,\n statusOf,\n} from \"./response\";\nexport {\n DEFAULT_UPSTREAM_TIMEOUT_MS,\n curlForRequest,\n fetchUpstream,\n internalHeaders,\n parseUpstream,\n requireUpstreamBaseUrl,\n resetUpstreamFailureLog,\n} from \"./upstream\";\n","/**\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":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/curl.ts","../src/errors.ts","../src/upstream.ts","../src/client.ts","../src/response.ts","../src/responder.ts","../src/authorLogin.ts","../src/browser.ts"],"sourcesContent":["export {\n createUpstreamClient,\n type UpstreamClient,\n type UpstreamClientConfig,\n type UpstreamRequest,\n} from \"./client\";\nexport { toCurl, type CurlOptions, type CurlRequest } from \"./curl\";\nexport {\n UpstreamConfigError,\n UpstreamError,\n UpstreamTimeoutError,\n curlOf,\n isUpstreamFailure,\n} from \"./errors\";\nexport {\n errorResponse,\n invalidResponse,\n logApiError,\n messageOf,\n statusOf,\n} from \"./response\";\nexport {\n createApiResponder,\n type ApiErrorBodyKey,\n type ApiResponder,\n type ApiResponderOptions,\n} from \"./responder\";\nexport {\n DEFAULT_UPSTREAM_TIMEOUT_MS,\n curlForRequest,\n fetchUpstream,\n internalHeaders,\n parseUpstream,\n requireUpstreamBaseUrl,\n resetUpstreamFailureLog,\n} from \"./upstream\";\nexport {\n putAuthorLogin,\n readAuthorLoginBody,\n authTokenOf,\n type AuthorLoginRequest,\n type AuthorLoginBody,\n} from \"./authorLogin\";\nexport {\n readJson,\n type BrowserJson,\n type ReadJsonOptions,\n} from \"./browser\";\n\n","/**\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","/**\n * Factory for route-handler error responses.\n *\n * Panels differ only in the JSON key (`message` vs CRM's `error`) and the\n * service name used when logging. Everything else — status extraction, curl\n * redaction, fallback messaging — stays shared.\n */\n\nimport { logApiError, messageOf, statusOf } from \"./response\";\n\nexport type ApiErrorBodyKey = \"message\" | \"error\";\n\nexport type ApiResponderOptions = {\n /** Service name written into Cloud Logging (`logApiError`). */\n service: string;\n /**\n * JSON key for the human-readable failure string.\n * Default `\"message\"` (Glint / inpix / sales). CRM uses `\"error\"`.\n */\n bodyKey?: ApiErrorBodyKey;\n};\n\nexport type ApiResponder = {\n errorResponse: (error: unknown, fallback?: string) => Response;\n invalidResponse: (message: string) => Response;\n /**\n * Log (status + message + curl when present), then return the JSON body.\n * Prefer this in route `catch` blocks.\n */\n respondApiError: (\n error: unknown,\n fallback: string,\n route: string,\n requestId?: string,\n ) => Response;\n};\n\nfunction errorBody(\n bodyKey: ApiErrorBodyKey,\n text: string,\n): Record<ApiErrorBodyKey, string> {\n return { [bodyKey]: text } as Record<ApiErrorBodyKey, string>;\n}\n\n/**\n * Bind a service name (and optional body key) once; reuse across routes.\n *\n * ```ts\n * const { respondApiError, invalidResponse } = createApiResponder({\n * service: \"users-crm\",\n * bodyKey: \"error\",\n * });\n * ```\n */\nexport function createApiResponder(\n options: ApiResponderOptions,\n): ApiResponder {\n const service = options.service.trim();\n if (!service) {\n throw new Error(\"createApiResponder: service is required\");\n }\n const bodyKey: ApiErrorBodyKey = options.bodyKey ?? \"message\";\n\n function errorResponse(\n error: unknown,\n fallback = \"Request failed\",\n ): Response {\n return Response.json(errorBody(bodyKey, messageOf(error, fallback)), {\n status: statusOf(error),\n });\n }\n\n function invalidResponse(message: string): Response {\n return Response.json(errorBody(bodyKey, message), { status: 400 });\n }\n\n function respondApiError(\n error: unknown,\n fallback: string,\n route: string,\n requestId?: string,\n ): Response {\n logApiError(error, { service, route, requestId });\n return errorResponse(error, fallback);\n }\n\n return { errorResponse, invalidResponse, respondApiError };\n}\n","/**\n * Shared `PUT /v1/author/login` exchange used by every Next panel.\n *\n * Sends the Google access token to the article/sales author service with a\n * system token. Returns the Response so panels can map fields themselves.\n * Uses `fetchUpstream` for timeout + redacted curl on failure.\n */\n\nimport { fetchUpstream } from \"./upstream\";\n\nexport type AuthorLoginRequest = {\n /** Absolute base URL of the author/sales backend (no trailing slash). */\n baseUrl: string;\n /** System `X-AUTH-TOKEN` for the login call. */\n systemToken: string;\n /** Google OAuth access token from NextAuth. */\n accessToken: string;\n /** Override path (default `/v1/author/login`). */\n path?: string;\n /** Timeout ms (defaults via `fetchUpstream`). */\n timeoutMs?: number;\n};\n\n/** Flat fields commonly returned by author login. */\nexport type AuthorLoginBody = {\n auth_token?: string;\n token?: string;\n email_id?: string;\n user_id?: string;\n name?: string;\n username?: string;\n permission_list?: string[];\n vendor?: string;\n roles?: Record<string, unknown>;\n is_admin?: boolean;\n app_names?: Record<string, unknown>;\n regions?: Record<string, unknown>;\n author?: {\n id?: string;\n name?: string;\n email_id?: string;\n user_name?: string;\n role?: string;\n is_admin?: boolean;\n };\n [key: string]: unknown;\n};\n\n/**\n * Exchange a Google access token for a panel auth token.\n *\n * Throws on network/timeout. Non-OK HTTP statuses are returned (and logged\n * once by `fetchUpstream`) so callers can decline sign-in without treating\n * every 401 as a thrown error.\n */\nexport async function putAuthorLogin(\n options: AuthorLoginRequest,\n): Promise<Response> {\n const base = options.baseUrl.replace(/\\/$/, \"\");\n const rawPath = options.path?.trim() || \"/v1/author/login\";\n const path = rawPath.startsWith(\"/\") ? rawPath : `/${rawPath}`;\n const loginUrl = `${base}${path}`;\n\n return fetchUpstream(\n loginUrl,\n {\n method: \"PUT\",\n headers: {\n \"X-AUTH-TOKEN\": options.systemToken,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ access_token: options.accessToken }),\n },\n options.timeoutMs,\n );\n}\n\n/** Parse JSON from an author-login response. */\nexport async function readAuthorLoginBody(\n response: Response,\n): Promise<AuthorLoginBody | null> {\n try {\n return (await response.json()) as AuthorLoginBody;\n } catch {\n return null;\n }\n}\n\n/** Prefer `auth_token`, fall back to legacy `token`. */\nexport function authTokenOf(body: AuthorLoginBody | null | undefined): string {\n return (body?.auth_token ?? body?.token ?? \"\").trim();\n}\n","/**\n * Browser-side JSON helpers for panel `fetch` call sites.\n *\n * Server code should keep using `fetchUpstream` / `createUpstreamClient`.\n * This module only parses a `Response` the browser already received.\n */\n\nexport type BrowserJson = {\n data?: unknown;\n message?: string;\n error?: string;\n [key: string]: unknown;\n};\n\nexport type ReadJsonOptions = {\n /** Used when the body has neither `message` nor `error`. */\n fallbackMessage?: string;\n};\n\n/**\n * Parse JSON from a browser `fetch` response.\n *\n * Throws `Error` with the body's `message` (or CRM `error`) when `!ok`.\n * Never throws on empty/invalid JSON for OK responses — returns `{}`.\n */\nexport async function readJson(\n response: Response,\n options: ReadJsonOptions = {},\n): Promise<BrowserJson> {\n const json = (await response.json().catch(() => null)) as BrowserJson | null;\n if (!response.ok) {\n throw new Error(\n (typeof json?.message === \"string\" && json.message) ||\n (typeof json?.error === \"string\" && json.error) ||\n options.fallbackMessage ||\n \"Request failed\",\n );\n }\n return json ?? {};\n}\n"],"mappings":"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","names":["errorResponse","invalidResponse"]}
package/dist/index.d.cts CHANGED
@@ -1,3 +1,5 @@
1
+ export { BrowserJson, ReadJsonOptions, readJson } from './browser.cjs';
2
+
1
3
  type UpstreamClientConfig = {
2
4
  /** Read on every request, so tests and deployments can swap it lazily. */
3
5
  baseUrl: () => string;
@@ -154,6 +156,44 @@ declare function logApiError(error: unknown, context: {
154
156
  requestId?: string;
155
157
  }): void;
156
158
 
159
+ /**
160
+ * Factory for route-handler error responses.
161
+ *
162
+ * Panels differ only in the JSON key (`message` vs CRM's `error`) and the
163
+ * service name used when logging. Everything else — status extraction, curl
164
+ * redaction, fallback messaging — stays shared.
165
+ */
166
+ type ApiErrorBodyKey = "message" | "error";
167
+ type ApiResponderOptions = {
168
+ /** Service name written into Cloud Logging (`logApiError`). */
169
+ service: string;
170
+ /**
171
+ * JSON key for the human-readable failure string.
172
+ * Default `"message"` (Glint / inpix / sales). CRM uses `"error"`.
173
+ */
174
+ bodyKey?: ApiErrorBodyKey;
175
+ };
176
+ type ApiResponder = {
177
+ errorResponse: (error: unknown, fallback?: string) => Response;
178
+ invalidResponse: (message: string) => Response;
179
+ /**
180
+ * Log (status + message + curl when present), then return the JSON body.
181
+ * Prefer this in route `catch` blocks.
182
+ */
183
+ respondApiError: (error: unknown, fallback: string, route: string, requestId?: string) => Response;
184
+ };
185
+ /**
186
+ * Bind a service name (and optional body key) once; reuse across routes.
187
+ *
188
+ * ```ts
189
+ * const { respondApiError, invalidResponse } = createApiResponder({
190
+ * service: "users-crm",
191
+ * bodyKey: "error",
192
+ * });
193
+ * ```
194
+ */
195
+ declare function createApiResponder(options: ApiResponderOptions): ApiResponder;
196
+
157
197
  /**
158
198
  * Server-side `fetch` for calls to internal upstream services. These calls
159
199
  * usually attach system tokens, so never import this into browser code.
@@ -197,4 +237,60 @@ declare function fetchUpstream(input: RequestInfo | URL, init?: RequestInit, tim
197
237
  */
198
238
  declare function parseUpstream<T>(response: Response, makeError: (message: string, status: number) => Error, fallbackMessage?: string): Promise<T>;
199
239
 
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 };
240
+ /**
241
+ * Shared `PUT /v1/author/login` exchange used by every Next panel.
242
+ *
243
+ * Sends the Google access token to the article/sales author service with a
244
+ * system token. Returns the Response so panels can map fields themselves.
245
+ * Uses `fetchUpstream` for timeout + redacted curl on failure.
246
+ */
247
+ type AuthorLoginRequest = {
248
+ /** Absolute base URL of the author/sales backend (no trailing slash). */
249
+ baseUrl: string;
250
+ /** System `X-AUTH-TOKEN` for the login call. */
251
+ systemToken: string;
252
+ /** Google OAuth access token from NextAuth. */
253
+ accessToken: string;
254
+ /** Override path (default `/v1/author/login`). */
255
+ path?: string;
256
+ /** Timeout ms (defaults via `fetchUpstream`). */
257
+ timeoutMs?: number;
258
+ };
259
+ /** Flat fields commonly returned by author login. */
260
+ type AuthorLoginBody = {
261
+ auth_token?: string;
262
+ token?: string;
263
+ email_id?: string;
264
+ user_id?: string;
265
+ name?: string;
266
+ username?: string;
267
+ permission_list?: string[];
268
+ vendor?: string;
269
+ roles?: Record<string, unknown>;
270
+ is_admin?: boolean;
271
+ app_names?: Record<string, unknown>;
272
+ regions?: Record<string, unknown>;
273
+ author?: {
274
+ id?: string;
275
+ name?: string;
276
+ email_id?: string;
277
+ user_name?: string;
278
+ role?: string;
279
+ is_admin?: boolean;
280
+ };
281
+ [key: string]: unknown;
282
+ };
283
+ /**
284
+ * Exchange a Google access token for a panel auth token.
285
+ *
286
+ * Throws on network/timeout. Non-OK HTTP statuses are returned (and logged
287
+ * once by `fetchUpstream`) so callers can decline sign-in without treating
288
+ * every 401 as a thrown error.
289
+ */
290
+ declare function putAuthorLogin(options: AuthorLoginRequest): Promise<Response>;
291
+ /** Parse JSON from an author-login response. */
292
+ declare function readAuthorLoginBody(response: Response): Promise<AuthorLoginBody | null>;
293
+ /** Prefer `auth_token`, fall back to legacy `token`. */
294
+ declare function authTokenOf(body: AuthorLoginBody | null | undefined): string;
295
+
296
+ export { type ApiErrorBodyKey, type ApiResponder, type ApiResponderOptions, type AuthorLoginBody, type AuthorLoginRequest, type CurlOptions, type CurlRequest, DEFAULT_UPSTREAM_TIMEOUT_MS, type UpstreamClient, type UpstreamClientConfig, UpstreamConfigError, UpstreamError, type UpstreamRequest, UpstreamTimeoutError, authTokenOf, createApiResponder, createUpstreamClient, curlForRequest, curlOf, errorResponse, fetchUpstream, internalHeaders, invalidResponse, isUpstreamFailure, logApiError, messageOf, parseUpstream, putAuthorLogin, readAuthorLoginBody, requireUpstreamBaseUrl, resetUpstreamFailureLog, statusOf, toCurl };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { BrowserJson, ReadJsonOptions, readJson } from './browser.js';
2
+
1
3
  type UpstreamClientConfig = {
2
4
  /** Read on every request, so tests and deployments can swap it lazily. */
3
5
  baseUrl: () => string;
@@ -154,6 +156,44 @@ declare function logApiError(error: unknown, context: {
154
156
  requestId?: string;
155
157
  }): void;
156
158
 
159
+ /**
160
+ * Factory for route-handler error responses.
161
+ *
162
+ * Panels differ only in the JSON key (`message` vs CRM's `error`) and the
163
+ * service name used when logging. Everything else — status extraction, curl
164
+ * redaction, fallback messaging — stays shared.
165
+ */
166
+ type ApiErrorBodyKey = "message" | "error";
167
+ type ApiResponderOptions = {
168
+ /** Service name written into Cloud Logging (`logApiError`). */
169
+ service: string;
170
+ /**
171
+ * JSON key for the human-readable failure string.
172
+ * Default `"message"` (Glint / inpix / sales). CRM uses `"error"`.
173
+ */
174
+ bodyKey?: ApiErrorBodyKey;
175
+ };
176
+ type ApiResponder = {
177
+ errorResponse: (error: unknown, fallback?: string) => Response;
178
+ invalidResponse: (message: string) => Response;
179
+ /**
180
+ * Log (status + message + curl when present), then return the JSON body.
181
+ * Prefer this in route `catch` blocks.
182
+ */
183
+ respondApiError: (error: unknown, fallback: string, route: string, requestId?: string) => Response;
184
+ };
185
+ /**
186
+ * Bind a service name (and optional body key) once; reuse across routes.
187
+ *
188
+ * ```ts
189
+ * const { respondApiError, invalidResponse } = createApiResponder({
190
+ * service: "users-crm",
191
+ * bodyKey: "error",
192
+ * });
193
+ * ```
194
+ */
195
+ declare function createApiResponder(options: ApiResponderOptions): ApiResponder;
196
+
157
197
  /**
158
198
  * Server-side `fetch` for calls to internal upstream services. These calls
159
199
  * usually attach system tokens, so never import this into browser code.
@@ -197,4 +237,60 @@ declare function fetchUpstream(input: RequestInfo | URL, init?: RequestInit, tim
197
237
  */
198
238
  declare function parseUpstream<T>(response: Response, makeError: (message: string, status: number) => Error, fallbackMessage?: string): Promise<T>;
199
239
 
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 };
240
+ /**
241
+ * Shared `PUT /v1/author/login` exchange used by every Next panel.
242
+ *
243
+ * Sends the Google access token to the article/sales author service with a
244
+ * system token. Returns the Response so panels can map fields themselves.
245
+ * Uses `fetchUpstream` for timeout + redacted curl on failure.
246
+ */
247
+ type AuthorLoginRequest = {
248
+ /** Absolute base URL of the author/sales backend (no trailing slash). */
249
+ baseUrl: string;
250
+ /** System `X-AUTH-TOKEN` for the login call. */
251
+ systemToken: string;
252
+ /** Google OAuth access token from NextAuth. */
253
+ accessToken: string;
254
+ /** Override path (default `/v1/author/login`). */
255
+ path?: string;
256
+ /** Timeout ms (defaults via `fetchUpstream`). */
257
+ timeoutMs?: number;
258
+ };
259
+ /** Flat fields commonly returned by author login. */
260
+ type AuthorLoginBody = {
261
+ auth_token?: string;
262
+ token?: string;
263
+ email_id?: string;
264
+ user_id?: string;
265
+ name?: string;
266
+ username?: string;
267
+ permission_list?: string[];
268
+ vendor?: string;
269
+ roles?: Record<string, unknown>;
270
+ is_admin?: boolean;
271
+ app_names?: Record<string, unknown>;
272
+ regions?: Record<string, unknown>;
273
+ author?: {
274
+ id?: string;
275
+ name?: string;
276
+ email_id?: string;
277
+ user_name?: string;
278
+ role?: string;
279
+ is_admin?: boolean;
280
+ };
281
+ [key: string]: unknown;
282
+ };
283
+ /**
284
+ * Exchange a Google access token for a panel auth token.
285
+ *
286
+ * Throws on network/timeout. Non-OK HTTP statuses are returned (and logged
287
+ * once by `fetchUpstream`) so callers can decline sign-in without treating
288
+ * every 401 as a thrown error.
289
+ */
290
+ declare function putAuthorLogin(options: AuthorLoginRequest): Promise<Response>;
291
+ /** Parse JSON from an author-login response. */
292
+ declare function readAuthorLoginBody(response: Response): Promise<AuthorLoginBody | null>;
293
+ /** Prefer `auth_token`, fall back to legacy `token`. */
294
+ declare function authTokenOf(body: AuthorLoginBody | null | undefined): string;
295
+
296
+ export { type ApiErrorBodyKey, type ApiResponder, type ApiResponderOptions, type AuthorLoginBody, type AuthorLoginRequest, type CurlOptions, type CurlRequest, DEFAULT_UPSTREAM_TIMEOUT_MS, type UpstreamClient, type UpstreamClientConfig, UpstreamConfigError, UpstreamError, type UpstreamRequest, UpstreamTimeoutError, authTokenOf, createApiResponder, createUpstreamClient, curlForRequest, curlOf, errorResponse, fetchUpstream, internalHeaders, invalidResponse, isUpstreamFailure, logApiError, messageOf, parseUpstream, putAuthorLogin, readAuthorLoginBody, requireUpstreamBaseUrl, resetUpstreamFailureLog, statusOf, toCurl };
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ readJson
3
+ } from "./chunk-5GQCQ73P.js";
4
+
1
5
  // src/curl.ts
2
6
  var SECRET_HEADER = /^(authorization|cookie|set-cookie|proxy-authorization)$|token|secret|api[-_]?key|password|credential/i;
3
7
  var SECRET_FIELD = /^(access_token|refresh_token|id_token|password|secret|token|authorization|api_key|apikey|client_secret)$/i;
@@ -360,11 +364,68 @@ function logApiError(error, context) {
360
364
  })
361
365
  );
362
366
  }
367
+
368
+ // src/responder.ts
369
+ function errorBody(bodyKey, text) {
370
+ return { [bodyKey]: text };
371
+ }
372
+ function createApiResponder(options) {
373
+ const service = options.service.trim();
374
+ if (!service) {
375
+ throw new Error("createApiResponder: service is required");
376
+ }
377
+ const bodyKey = options.bodyKey ?? "message";
378
+ function errorResponse2(error, fallback = "Request failed") {
379
+ return Response.json(errorBody(bodyKey, messageOf(error, fallback)), {
380
+ status: statusOf(error)
381
+ });
382
+ }
383
+ function invalidResponse2(message) {
384
+ return Response.json(errorBody(bodyKey, message), { status: 400 });
385
+ }
386
+ function respondApiError(error, fallback, route, requestId) {
387
+ logApiError(error, { service, route, requestId });
388
+ return errorResponse2(error, fallback);
389
+ }
390
+ return { errorResponse: errorResponse2, invalidResponse: invalidResponse2, respondApiError };
391
+ }
392
+
393
+ // src/authorLogin.ts
394
+ async function putAuthorLogin(options) {
395
+ const base = options.baseUrl.replace(/\/$/, "");
396
+ const rawPath = options.path?.trim() || "/v1/author/login";
397
+ const path = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
398
+ const loginUrl = `${base}${path}`;
399
+ return fetchUpstream(
400
+ loginUrl,
401
+ {
402
+ method: "PUT",
403
+ headers: {
404
+ "X-AUTH-TOKEN": options.systemToken,
405
+ "Content-Type": "application/json"
406
+ },
407
+ body: JSON.stringify({ access_token: options.accessToken })
408
+ },
409
+ options.timeoutMs
410
+ );
411
+ }
412
+ async function readAuthorLoginBody(response) {
413
+ try {
414
+ return await response.json();
415
+ } catch {
416
+ return null;
417
+ }
418
+ }
419
+ function authTokenOf(body) {
420
+ return (body?.auth_token ?? body?.token ?? "").trim();
421
+ }
363
422
  export {
364
423
  DEFAULT_UPSTREAM_TIMEOUT_MS,
365
424
  UpstreamConfigError,
366
425
  UpstreamError,
367
426
  UpstreamTimeoutError,
427
+ authTokenOf,
428
+ createApiResponder,
368
429
  createUpstreamClient,
369
430
  curlForRequest,
370
431
  curlOf,
@@ -376,6 +437,9 @@ export {
376
437
  logApiError,
377
438
  messageOf,
378
439
  parseUpstream,
440
+ putAuthorLogin,
441
+ readAuthorLoginBody,
442
+ readJson,
379
443
  requireUpstreamBaseUrl,
380
444
  resetUpstreamFailureLog,
381
445
  statusOf,
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../src/curl.ts","../src/errors.ts","../src/upstream.ts","../src/client.ts","../src/response.ts","../src/responder.ts","../src/authorLogin.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","/**\n * Factory for route-handler error responses.\n *\n * Panels differ only in the JSON key (`message` vs CRM's `error`) and the\n * service name used when logging. Everything else — status extraction, curl\n * redaction, fallback messaging — stays shared.\n */\n\nimport { logApiError, messageOf, statusOf } from \"./response\";\n\nexport type ApiErrorBodyKey = \"message\" | \"error\";\n\nexport type ApiResponderOptions = {\n /** Service name written into Cloud Logging (`logApiError`). */\n service: string;\n /**\n * JSON key for the human-readable failure string.\n * Default `\"message\"` (Glint / inpix / sales). CRM uses `\"error\"`.\n */\n bodyKey?: ApiErrorBodyKey;\n};\n\nexport type ApiResponder = {\n errorResponse: (error: unknown, fallback?: string) => Response;\n invalidResponse: (message: string) => Response;\n /**\n * Log (status + message + curl when present), then return the JSON body.\n * Prefer this in route `catch` blocks.\n */\n respondApiError: (\n error: unknown,\n fallback: string,\n route: string,\n requestId?: string,\n ) => Response;\n};\n\nfunction errorBody(\n bodyKey: ApiErrorBodyKey,\n text: string,\n): Record<ApiErrorBodyKey, string> {\n return { [bodyKey]: text } as Record<ApiErrorBodyKey, string>;\n}\n\n/**\n * Bind a service name (and optional body key) once; reuse across routes.\n *\n * ```ts\n * const { respondApiError, invalidResponse } = createApiResponder({\n * service: \"users-crm\",\n * bodyKey: \"error\",\n * });\n * ```\n */\nexport function createApiResponder(\n options: ApiResponderOptions,\n): ApiResponder {\n const service = options.service.trim();\n if (!service) {\n throw new Error(\"createApiResponder: service is required\");\n }\n const bodyKey: ApiErrorBodyKey = options.bodyKey ?? \"message\";\n\n function errorResponse(\n error: unknown,\n fallback = \"Request failed\",\n ): Response {\n return Response.json(errorBody(bodyKey, messageOf(error, fallback)), {\n status: statusOf(error),\n });\n }\n\n function invalidResponse(message: string): Response {\n return Response.json(errorBody(bodyKey, message), { status: 400 });\n }\n\n function respondApiError(\n error: unknown,\n fallback: string,\n route: string,\n requestId?: string,\n ): Response {\n logApiError(error, { service, route, requestId });\n return errorResponse(error, fallback);\n }\n\n return { errorResponse, invalidResponse, respondApiError };\n}\n","/**\n * Shared `PUT /v1/author/login` exchange used by every Next panel.\n *\n * Sends the Google access token to the article/sales author service with a\n * system token. Returns the Response so panels can map fields themselves.\n * Uses `fetchUpstream` for timeout + redacted curl on failure.\n */\n\nimport { fetchUpstream } from \"./upstream\";\n\nexport type AuthorLoginRequest = {\n /** Absolute base URL of the author/sales backend (no trailing slash). */\n baseUrl: string;\n /** System `X-AUTH-TOKEN` for the login call. */\n systemToken: string;\n /** Google OAuth access token from NextAuth. */\n accessToken: string;\n /** Override path (default `/v1/author/login`). */\n path?: string;\n /** Timeout ms (defaults via `fetchUpstream`). */\n timeoutMs?: number;\n};\n\n/** Flat fields commonly returned by author login. */\nexport type AuthorLoginBody = {\n auth_token?: string;\n token?: string;\n email_id?: string;\n user_id?: string;\n name?: string;\n username?: string;\n permission_list?: string[];\n vendor?: string;\n roles?: Record<string, unknown>;\n is_admin?: boolean;\n app_names?: Record<string, unknown>;\n regions?: Record<string, unknown>;\n author?: {\n id?: string;\n name?: string;\n email_id?: string;\n user_name?: string;\n role?: string;\n is_admin?: boolean;\n };\n [key: string]: unknown;\n};\n\n/**\n * Exchange a Google access token for a panel auth token.\n *\n * Throws on network/timeout. Non-OK HTTP statuses are returned (and logged\n * once by `fetchUpstream`) so callers can decline sign-in without treating\n * every 401 as a thrown error.\n */\nexport async function putAuthorLogin(\n options: AuthorLoginRequest,\n): Promise<Response> {\n const base = options.baseUrl.replace(/\\/$/, \"\");\n const rawPath = options.path?.trim() || \"/v1/author/login\";\n const path = rawPath.startsWith(\"/\") ? rawPath : `/${rawPath}`;\n const loginUrl = `${base}${path}`;\n\n return fetchUpstream(\n loginUrl,\n {\n method: \"PUT\",\n headers: {\n \"X-AUTH-TOKEN\": options.systemToken,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ access_token: options.accessToken }),\n },\n options.timeoutMs,\n );\n}\n\n/** Parse JSON from an author-login response. */\nexport async function readAuthorLoginBody(\n response: Response,\n): Promise<AuthorLoginBody | null> {\n try {\n return (await response.json()) as AuthorLoginBody;\n } catch {\n return null;\n }\n}\n\n/** Prefer `auth_token`, fall back to legacy `token`. */\nexport function authTokenOf(body: AuthorLoginBody | null | undefined): string {\n return (body?.auth_token ?? body?.token ?? \"\").trim();\n}\n"],"mappings":"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","names":["errorResponse","invalidResponse"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshorts/glint-http",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "Glint server-side HTTP client: timeouts, typed errors, redacted curl on failure.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -18,6 +18,16 @@
18
18
  "default": "./dist/index.cjs"
19
19
  }
20
20
  },
21
+ "./browser": {
22
+ "import": {
23
+ "types": "./dist/browser.d.ts",
24
+ "default": "./dist/browser.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/browser.d.cts",
28
+ "default": "./dist/browser.cjs"
29
+ }
30
+ },
21
31
  "./package.json": "./package.json"
22
32
  },
23
33
  "files": [
@@ -44,5 +54,8 @@
44
54
  "tsup": "^8.3.5",
45
55
  "typescript": "^5.7.2",
46
56
  "vitest": "^2.1.8"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
47
60
  }
48
61
  }