@yedoma-labs/ysuly-problem 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @yedoma-labs/ysuly-problem
2
2
 
3
+ <picture>
4
+ <source media="(max-width: 640px)" srcset="https://raw.githubusercontent.com/yedoma-labs/assets/main/resized/banner-resized-mobile.png">
5
+ <img src="https://raw.githubusercontent.com/yedoma-labs/assets/main/resized/banner-resized.png" alt="Project Header">
6
+ </picture>
7
+
8
+ [![CI](https://github.com/yedoma-labs/ysuly-problem/actions/workflows/ci.yml/badge.svg)](https://github.com/yedoma-labs/ysuly-problem/actions/workflows/ci.yml)
9
+ [![npm version](https://img.shields.io/npm/v/@yedoma-labs/ysuly-problem.svg)](https://www.npmjs.com/package/@yedoma-labs/ysuly-problem)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-7-blue.svg)](https://www.typescriptlang.org/)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+
13
+ > **ысулы** (*ysuly*) — Yakutian (Sakha) for "(noch zu bestätigen)"
14
+
3
15
  Type-safe [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details for TypeScript, with a mapping from arbitrary thrown errors that never leaks internals. Zero runtime dependencies. Uses only Web-standard APIs (`Response`, `Headers`, `JSON`), so it runs on Node 20+, Bun, Deno and edge runtimes.
4
16
 
5
17
  ## The problem
@@ -102,7 +114,7 @@ Maps any value to a `ProblemDetails`. Options:
102
114
 
103
115
  ### `defineProblems(templates, { by? })`
104
116
 
105
- Defines named problem templates and validates them immediately. Each key is matched against `error.code` (default), against the error's constructor name (`by: "className"`), or against either (`by: "both"`). Matching never looks at `message`.
117
+ Defines named problem templates and validates them immediately. A template whose status cannot carry a body (any 1xx, 204, 205 and 304) is rejected here, so `toResponse` cannot fail later on a request. Each key is matched against `error.code` (default), against the error's constructor name (`by: "className"`), or against either (`by: "both"`). Matching never looks at `message`.
106
118
 
107
119
  ### `isProblemDetails(value)`
108
120
 
package/dist/index.cjs CHANGED
@@ -26,6 +26,13 @@ const DEFAULT_PROBLEM_TYPE = "about:blank";
26
26
  const TITLE_MAX_LENGTH = 200;
27
27
  const DETAIL_MAX_LENGTH = 1e3;
28
28
  const URI_MAX_LENGTH = 2048;
29
+ /**
30
+ * Whether a response with this status can carry a problem body: 200-599, except
31
+ * 204, 205 and 304, which must not have a body.
32
+ */
33
+ function canCarryProblemBody(status) {
34
+ return status >= 200 && status !== 204 && status !== 205 && status !== 304;
35
+ }
29
36
  /** Members defined by RFC 9457. They may not be used as extension keys. */
30
37
  const RESERVED_MEMBERS = /* @__PURE__ */ new Set([
31
38
  "type",
@@ -288,6 +295,7 @@ function defineProblems(templates, options = {}) {
288
295
  ...template,
289
296
  extensions: void 0
290
297
  });
298
+ if (!canCarryProblemBody(template.status)) issues.push(`${key}: status ${template.status} cannot carry a problem body`);
291
299
  } catch (error) {
292
300
  if (error instanceof ProblemError) issues.push(`${key}: ${error.issues.join("; ")}`);
293
301
  else throw error;
@@ -300,14 +308,6 @@ function defineProblems(templates, options = {}) {
300
308
  });
301
309
  }
302
310
  // src/response.ts
303
- /** Statuses that cannot carry a body, or that the Response constructor rejects. */
304
- const BODYLESS_OR_UNSUPPORTED_STATUSES = /* @__PURE__ */ new Set([
305
- 101,
306
- 103,
307
- 204,
308
- 205,
309
- 304
310
- ]);
311
311
  /**
312
312
  * Creates a Web-standard `Response` carrying the problem as
313
313
  * `application/problem+json; charset=utf-8`. The problem is validated and
@@ -315,7 +315,7 @@ const BODYLESS_OR_UNSUPPORTED_STATUSES = /* @__PURE__ */ new Set([
315
315
  */
316
316
  function toResponse(problem, options = {}) {
317
317
  const normalized = normalizeProblem(toProblemInput(problem));
318
- if (normalized.status < 200 || BODYLESS_OR_UNSUPPORTED_STATUSES.has(normalized.status)) throw new ProblemError([`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`]);
318
+ if (!canCarryProblemBody(normalized.status)) throw new ProblemError([`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`]);
319
319
  const headers = new Headers(options.headers);
320
320
  headers.set("content-type", PROBLEM_CONTENT_TYPE);
321
321
  return new Response(JSON.stringify(normalized), {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/sanitize.ts","../src/problem.ts","../src/client.ts","../src/from-error.ts","../src/registry.ts","../src/response.ts"],"sourcesContent":["const WHITESPACE_RUNS = /\\s+/g;\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/g;\n\n/**\n * Makes free text safe for a human-readable problem field: every whitespace run\n * (tabs, line breaks, Unicode line separators) becomes one space, other control\n * characters are removed, surrounding whitespace is trimmed and the result is\n * cut to `maxLength` code points.\n */\nexport function sanitizeText(value: string, maxLength: number): string {\n\tconst cleaned = value.replace(WHITESPACE_RUNS, \" \").replace(CONTROL_CHARS, \"\").trim();\n\treturn truncate(cleaned, maxLength);\n}\n\n/** Cuts a string to at most `maxLength` code points, ending with an ellipsis if cut. */\nexport function truncate(value: string, maxLength: number): string {\n\t// A string never has more code points than code units, so this is a cheap pre-check.\n\tif (value.length <= maxLength) {\n\t\treturn value;\n\t}\n\tconst chars = Array.from(value);\n\tif (chars.length <= maxLength) {\n\t\treturn value;\n\t}\n\treturn `${chars.slice(0, maxLength - 1).join(\"\")}…`;\n}\n","import { sanitizeText } from \"./sanitize.js\";\n\n/** Content-Type for RFC 9457 problem documents. */\nexport const PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\n\n/** Default problem `type` as defined by RFC 9457. */\nexport const DEFAULT_PROBLEM_TYPE = \"about:blank\";\n\nexport const TITLE_MAX_LENGTH = 200;\nexport const DETAIL_MAX_LENGTH = 1000;\nconst URI_MAX_LENGTH = 2048;\n\n/** Members defined by RFC 9457. They may not be used as extension keys. */\nexport const RESERVED_MEMBERS: ReadonlySet<string> = new Set([\n\t\"type\",\n\t\"title\",\n\t\"status\",\n\t\"detail\",\n\t\"instance\",\n]);\n\n/**\n * An RFC 9457 problem document. Extension members are flattened next to the\n * standard members, as the specification requires.\n */\nexport interface ProblemDetails {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\t[extension: string]: unknown;\n}\n\n/**\n * Input for `problem()`. Extensions are passed separately so that they can be\n * checked against the reserved members before a document is built.\n */\nexport interface ProblemInput {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\textensions?: Readonly<Record<string, unknown>>;\n}\n\nexport type ProblemErrorCode = \"INVALID_PROBLEM\";\n\n/** Thrown when problem details cannot be built or are invalid. Carries no input values. */\nexport class ProblemError extends Error {\n\treadonly code: ProblemErrorCode = \"INVALID_PROBLEM\";\n\treadonly issues: readonly string[];\n\n\tconstructor(issues: readonly string[]) {\n\t\tsuper(`Invalid problem details: ${issues.join(\"; \")}`);\n\t\tthis.name = \"ProblemError\";\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * Builds a validated problem document. Text fields are sanitized and length\n * limited; invalid input throws a `ProblemError` with code `INVALID_PROBLEM`.\n */\nexport function problem(input: ProblemInput): ProblemDetails {\n\treturn normalizeProblem(input);\n}\n\n/** @internal Validates and normalizes a problem input. Shared by all helpers. */\nexport function normalizeProblem(input: ProblemInput): ProblemDetails {\n\tconst issues: string[] = [];\n\n\tif (!isValidStatus(input.status)) {\n\t\tissues.push(\"status must be an integer between 100 and 599\");\n\t}\n\n\tconst title = typeof input.title === \"string\" ? sanitizeText(input.title, TITLE_MAX_LENGTH) : \"\";\n\tif (title === \"\") {\n\t\tissues.push(\"title must be a non-empty string\");\n\t}\n\n\tconst type =\n\t\tinput.type === undefined ? DEFAULT_PROBLEM_TYPE : uriReference(\"type\", input.type, issues);\n\tconst instance =\n\t\tinput.instance === undefined ? undefined : uriReference(\"instance\", input.instance, issues);\n\tconst detail = optionalText(\"detail\", input.detail, DETAIL_MAX_LENGTH, issues);\n\n\tconst extensions: Record<string, unknown> = {};\n\tif (input.extensions !== undefined) {\n\t\tif (!isRecord(input.extensions)) {\n\t\t\tissues.push(\"extensions must be a plain object\");\n\t\t} else {\n\t\t\tfor (const [key, value] of Object.entries(input.extensions)) {\n\t\t\t\tif (RESERVED_MEMBERS.has(key)) {\n\t\t\t\t\tissues.push(`extension key \"${key}\" collides with a reserved problem member`);\n\t\t\t\t} else if (key === \"__proto__\") {\n\t\t\t\t\tissues.push('extension key \"__proto__\" is not allowed');\n\t\t\t\t} else if (!isJsonSerializable(value)) {\n\t\t\t\t\tissues.push(`extension \"${key}\" must be JSON-serializable`);\n\t\t\t\t} else {\n\t\t\t\t\textensions[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\tconst result: ProblemDetails = { type, title, status: input.status };\n\tif (detail !== undefined) {\n\t\tresult.detail = detail;\n\t}\n\tif (instance !== undefined) {\n\t\tresult.instance = instance;\n\t}\n\tfor (const [key, value] of Object.entries(extensions)) {\n\t\tresult[key] = value;\n\t}\n\treturn result;\n}\n\n/** @internal */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** @internal */\nexport function isValidStatus(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 100 && value <= 599;\n}\n\nconst URI_REFERENCE_FORBIDDEN = /[\\s\\u0000-\\u001f\\u007f]/;\n\nfunction uriReference(name: string, value: unknown, issues: string[]): string {\n\tif (\n\t\ttypeof value !== \"string\" ||\n\t\tvalue.length === 0 ||\n\t\tvalue.length > URI_MAX_LENGTH ||\n\t\tURI_REFERENCE_FORBIDDEN.test(value)\n\t) {\n\t\tissues.push(\n\t\t\t`${name} must be a non-empty URI reference without whitespace or control characters (max ${URI_MAX_LENGTH} characters)`,\n\t\t);\n\t\treturn \"\";\n\t}\n\treturn value;\n}\n\nfunction optionalText(\n\tname: string,\n\tvalue: unknown,\n\tmaxLength: number,\n\tissues: string[],\n): string | undefined {\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\tif (typeof value !== \"string\") {\n\t\tissues.push(`${name} must be a string`);\n\t\treturn undefined;\n\t}\n\tconst text = sanitizeText(value, maxLength);\n\treturn text === \"\" ? undefined : text;\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n\ttry {\n\t\treturn JSON.stringify(value) !== undefined;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","import { isRecord, isValidStatus, type ProblemDetails } from \"./problem.js\";\n\n/** Largest problem body that `parseProblem` will read. */\nconst PARSE_MAX_BYTES = 64 * 1024;\n\n/**\n * Type guard for problem documents: `title` is a non-empty string, `status` is\n * an integer between 100 and 599, and `type`, `detail`, `instance` are strings\n * when present. Extension members are not inspected.\n */\nexport function isProblemDetails(value: unknown): value is ProblemDetails {\n\tif (!isRecord(value)) {\n\t\treturn false;\n\t}\n\tconst { type, title, status, detail, instance } = value;\n\treturn (\n\t\ttypeof title === \"string\" &&\n\t\ttitle.length > 0 &&\n\t\tisValidStatus(status) &&\n\t\t(type === undefined || typeof type === \"string\") &&\n\t\t(detail === undefined || typeof detail === \"string\") &&\n\t\t(instance === undefined || typeof instance === \"string\")\n\t);\n}\n\n/**\n * Reads a problem document from a `Response`. Returns `undefined` unless the\n * media type is `application/problem+json` or `application/json` and the body is\n * a valid problem. Bodies over 64 KiB, malformed JSON and read errors also\n * yield `undefined`; this function never throws.\n *\n * The response body is consumed. Call `response.clone()` first if it is needed later.\n */\nexport async function parseProblem(response: Response): Promise<ProblemDetails | undefined> {\n\ttry {\n\t\tconst mediaType = response.headers.get(\"content-type\")?.split(\";\")[0]?.trim().toLowerCase();\n\t\tif (mediaType !== \"application/problem+json\" && mediaType !== \"application/json\") {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst declared = Number(response.headers.get(\"content-length\"));\n\t\tif (Number.isFinite(declared) && declared > PARSE_MAX_BYTES) {\n\t\t\tawait response.body?.cancel();\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst bytes = await readLimited(response.body, PARSE_MAX_BYTES);\n\t\tif (bytes === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst parsed: unknown = JSON.parse(new TextDecoder().decode(bytes));\n\t\treturn isProblemDetails(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Reads a stream up to `limit` bytes. Resolves to `undefined` if the limit is exceeded. */\nasync function readLimited(\n\tbody: ReadableStream<Uint8Array> | null,\n\tlimit: number,\n): Promise<Uint8Array | undefined> {\n\tif (body === null) {\n\t\treturn undefined;\n\t}\n\tconst reader = body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) {\n\t\t\tbreak;\n\t\t}\n\t\ttotal += value.byteLength;\n\t\tif (total > limit) {\n\t\t\tawait reader.cancel();\n\t\t\treturn undefined;\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst bytes = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tbytes.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn bytes;\n}\n","import { normalizeProblem, type ProblemDetails } from \"./problem.js\";\nimport type { ProblemRegistry, ProblemTemplate } from \"./registry.js\";\nimport { truncate } from \"./sanitize.js\";\n\n/** A class used as a registry key in `fromError`. Matched with `instanceof`. */\nexport type ErrorClass = abstract new (...args: never[]) => unknown;\n\nexport interface FromErrorOptions {\n\t/**\n\t * Opt-in for unknown errors. Only when this returns `true` is the error\n\t * message copied into `detail` (sanitized and length limited).\n\t */\n\texpose?: (error: unknown) => boolean;\n\t/**\n\t * Known errors. A `ProblemRegistry` from `defineProblems`, or a `Map` whose\n\t * string keys match `error.code` and whose class keys match with `instanceof`.\n\t * The first matching entry wins. Matched errors never expose their message.\n\t */\n\tregistry?: ProblemRegistry | ReadonlyMap<ErrorClass | string, ProblemTemplate>;\n\t/** Template for unknown errors. Defaults to status 500 and a generic title. */\n\tfallback?: ProblemTemplate;\n\t/** Request identifier. When set, `instance` becomes `urn:request:<id>`. */\n\tinstanceFromRequestId?: string;\n}\n\nconst DEFAULT_FALLBACK: ProblemTemplate = { status: 500, title: \"Internal Server Error\" };\nconst REQUEST_ID_MAX_LENGTH = 128;\n\n/**\n * Maps any thrown value to a problem document without leaking internals.\n *\n * Unknown errors produce only the fallback (by default status 500 and a generic\n * title). Their `message`, `stack`, `cause` and `name` are never included unless\n * `expose(error)` returns `true`.\n */\nexport function fromError(error: unknown, options: FromErrorOptions = {}): ProblemDetails {\n\tconst instance = instanceFor(options.instanceFromRequestId);\n\n\tconst matched = findTemplate(error, options.registry);\n\tif (matched !== undefined) {\n\t\treturn normalizeProblem({\n\t\t\ttype: matched.type,\n\t\t\ttitle: matched.title,\n\t\t\tstatus: matched.status,\n\t\t\tdetail: matched.detail,\n\t\t\tinstance,\n\t\t});\n\t}\n\n\tconst fallback = options.fallback ?? DEFAULT_FALLBACK;\n\tconst exposed = options.expose !== undefined && isExposed(options.expose, error);\n\treturn normalizeProblem({\n\t\ttype: fallback.type,\n\t\ttitle: fallback.title,\n\t\tstatus: fallback.status,\n\t\tdetail: exposed ? messageOf(error) : fallback.detail,\n\t\tinstance,\n\t});\n}\n\nfunction instanceFor(requestId: string | undefined): string | undefined {\n\tif (requestId === undefined || requestId === \"\") {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn `urn:request:${encodeURIComponent(truncate(requestId, REQUEST_ID_MAX_LENGTH))}`;\n\t} catch {\n\t\t// Lone surrogates cannot be encoded; an unusable request id is dropped, not thrown.\n\t\treturn undefined;\n\t}\n}\n\nfunction findTemplate(\n\terror: unknown,\n\tregistry: FromErrorOptions[\"registry\"],\n): ProblemTemplate | undefined {\n\tif (registry === undefined) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\tif (isMap(registry)) {\n\t\t\tfor (const [matcher, template] of registry) {\n\t\t\t\tconst hit =\n\t\t\t\t\ttypeof matcher === \"string\"\n\t\t\t\t\t\t? readProperty(error, \"code\") === matcher\n\t\t\t\t\t\t: error instanceof matcher;\n\t\t\t\tif (hit) {\n\t\t\t\t\treturn template;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst { by, templates } = registry;\n\t\tfor (const [key, template] of Object.entries(templates)) {\n\t\t\tconst byCode = by !== \"className\" && readProperty(error, \"code\") === key;\n\t\t\tconst byClassName = by !== \"code\" && constructorName(error) === key;\n\t\t\tif (byCode || byClassName) {\n\t\t\t\treturn template;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\t// A throwing getter or hostile matcher must not change the response shape.\n\t\treturn undefined;\n\t}\n}\n\nfunction isMap(\n\tregistry: NonNullable<FromErrorOptions[\"registry\"]>,\n): registry is ReadonlyMap<ErrorClass | string, ProblemTemplate> {\n\treturn registry instanceof Map;\n}\n\nfunction isExposed(expose: (error: unknown) => boolean, error: unknown): boolean {\n\ttry {\n\t\treturn expose(error) === true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction messageOf(error: unknown): string | undefined {\n\ttry {\n\t\tconst message = readProperty(error, \"message\");\n\t\treturn typeof message === \"string\" ? message : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction constructorName(value: unknown): string | undefined {\n\tconst ctor = readProperty(value, \"constructor\");\n\treturn typeof ctor === \"function\" ? ctor.name : undefined;\n}\n\nfunction readProperty(value: unknown, key: string): unknown {\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\treturn undefined;\n\t}\n\treturn (value as Record<string, unknown>)[key];\n}\n","import { normalizeProblem, ProblemError } from \"./problem.js\";\n\n/** A problem definition that can be reused for errors of a known kind. */\nexport interface ProblemTemplate {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n}\n\nexport type ProblemTemplates = Readonly<Record<string, ProblemTemplate>>;\n\n/**\n * How registry keys are matched against errors:\n * - `code`: `error.code === key` (default, survives minification)\n * - `className`: `error.constructor.name === key`\n * - `both`: either of the two\n */\nexport type MatchBy = \"code\" | \"className\" | \"both\";\n\nexport interface DefineProblemsOptions {\n\tby?: MatchBy;\n}\n\nexport interface ProblemRegistry<T extends ProblemTemplates = ProblemTemplates> {\n\treadonly templates: T;\n\treadonly by: MatchBy;\n}\n\nconst MATCH_MODES: ReadonlySet<string> = new Set([\"code\", \"className\", \"both\"]);\n\n/**\n * Defines named problem templates for `fromError`. Every template is validated\n * immediately, so invalid definitions fail at startup instead of per request.\n *\n * @example\n * const problems = defineProblems({\n * NotFound: { status: 404, title: \"Not found\", type: \"https://example.com/problems/not-found\" },\n * });\n * fromError(error, { registry: problems });\n */\nexport function defineProblems<const T extends ProblemTemplates>(\n\ttemplates: T,\n\toptions: DefineProblemsOptions = {},\n): ProblemRegistry<T> {\n\tconst by = options.by ?? \"code\";\n\tconst issues: string[] = [];\n\n\tif (!MATCH_MODES.has(by)) {\n\t\tissues.push('by must be \"code\", \"className\" or \"both\"');\n\t}\n\n\tfor (const [key, template] of Object.entries(templates)) {\n\t\tif (key === \"\") {\n\t\t\tissues.push(\"template keys must be non-empty\");\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tnormalizeProblem({ ...template, extensions: undefined });\n\t\t} catch (error) {\n\t\t\tif (error instanceof ProblemError) {\n\t\t\t\tissues.push(`${key}: ${error.issues.join(\"; \")}`);\n\t\t\t} else {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\treturn Object.freeze({ templates: Object.freeze({ ...templates }), by });\n}\n","import {\n\tnormalizeProblem,\n\tPROBLEM_CONTENT_TYPE,\n\ttype ProblemDetails,\n\tProblemError,\n\ttype ProblemInput,\n} from \"./problem.js\";\n\n/** Statuses that cannot carry a body, or that the Response constructor rejects. */\nconst BODYLESS_OR_UNSUPPORTED_STATUSES: ReadonlySet<number> = new Set([101, 103, 204, 205, 304]);\n\nexport interface ToResponseOptions {\n\t/** Extra headers. The Content-Type is always set to the problem media type. */\n\theaders?: HeadersInit;\n}\n\n/**\n * Creates a Web-standard `Response` carrying the problem as\n * `application/problem+json; charset=utf-8`. The problem is validated and\n * sanitized again before it is serialized.\n */\nexport function toResponse(problem: ProblemDetails, options: ToResponseOptions = {}): Response {\n\tconst normalized = normalizeProblem(toProblemInput(problem));\n\n\tif (normalized.status < 200 || BODYLESS_OR_UNSUPPORTED_STATUSES.has(normalized.status)) {\n\t\tthrow new ProblemError([\n\t\t\t`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`,\n\t\t]);\n\t}\n\n\tconst headers = new Headers(options.headers);\n\theaders.set(\"content-type\", PROBLEM_CONTENT_TYPE);\n\n\treturn new Response(JSON.stringify(normalized), {\n\t\tstatus: normalized.status,\n\t\theaders,\n\t});\n}\n\n/** Converts the flattened wire shape into the input shape used for validation. */\nfunction toProblemInput(value: ProblemDetails): ProblemInput {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new ProblemError([\"problem must be an object\"]);\n\t}\n\tconst { type, title, status, detail, instance, ...extensions } = value;\n\treturn { type, title, status, detail, instance, extensions };\n}\n"],"mappings":";;AAAA,MAAM,kBAAkB;AACxB,MAAM,gBAAgB;;;;;;;AAQtB,SAAgB,aAAa,OAAe,WAA2B;CAEtE,OAAO,SADS,MAAM,QAAQ,iBAAiB,GAAG,CAAC,CAAC,QAAQ,eAAe,EAAE,CAAC,CAAC,KACzD,GAAG,SAAS;AACnC;;AAGA,SAAgB,SAAS,OAAe,WAA2B;CAElE,IAAI,MAAM,UAAU,WACnB,OAAO;CAER,MAAM,QAAQ,MAAM,KAAK,KAAK;CAC9B,IAAI,MAAM,UAAU,WACnB,OAAO;CAER,OAAO,GAAG,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;AAClD;;;ACtBA,MAAa,uBAAuB;;AAGpC,MAAa,uBAAuB;AAEpC,MAAa,mBAAmB;AAChC,MAAa,oBAAoB;AACjC,MAAM,iBAAiB;;AAGvB,MAAa,mCAAwC,IAAI,IAAI;CAC5D;CACA;CACA;CACA;CACA;AACD,CAAC;;AA+BD,IAAa,eAAb,cAAkC,MAAM;CACvC,OAAkC;CAClC;CAEA,YAAY,QAA2B;EACtC,MAAM,4BAA4B,OAAO,KAAK,IAAI,GAAG;EACrD,KAAK,OAAO;EACZ,KAAK,SAAS;CACf;AACD;;;;;AAMA,SAAgB,QAAQ,OAAqC;CAC5D,OAAO,iBAAiB,KAAK;AAC9B;;AAGA,SAAgB,iBAAiB,OAAqC;CACrE,MAAM,SAAmB,CAAC;CAE1B,IAAI,CAAC,cAAc,MAAM,MAAM,GAC9B,OAAO,KAAK,+CAA+C;CAG5D,MAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,aAAa,MAAM,OAAA,GAAuB,IAAI;CAC9F,IAAI,UAAU,IACb,OAAO,KAAK,kCAAkC;CAG/C,MAAM,OACL,MAAM,SAAS,KAAA,IAAY,uBAAuB,aAAa,QAAQ,MAAM,MAAM,MAAM;CAC1F,MAAM,WACL,MAAM,aAAa,KAAA,IAAY,KAAA,IAAY,aAAa,YAAY,MAAM,UAAU,MAAM;CAC3F,MAAM,SAAS,aAAa,UAAU,MAAM,QAAQ,mBAAmB,MAAM;CAE7E,MAAM,aAAsC,CAAC;CAC7C,IAAI,MAAM,eAAe,KAAA,GAAW;EACnC,IAAI,CAAC,SAAS,MAAM,UAAU,GAC7B,OAAO,KAAK,mCAAmC;OAE/C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,UAAU,GACzD,IAAI,iBAAiB,IAAI,GAAG,GAC3B,OAAO,KAAK,kBAAkB,IAAI,0CAA0C;OACtE,IAAI,QAAQ,aAClB,OAAO,KAAK,4CAA0C;OAChD,IAAI,CAAC,mBAAmB,KAAK,GACnC,OAAO,KAAK,cAAc,IAAI,4BAA4B;OAE1D,WAAW,OAAO;CAItB;CAEA,IAAI,OAAO,SAAS,GACnB,MAAM,IAAI,aAAa,MAAM;CAG9B,MAAM,SAAyB;EAAE;EAAM;EAAO,QAAQ,MAAM;CAAO;CACnE,IAAI,WAAW,KAAA,GACd,OAAO,SAAS;CAEjB,IAAI,aAAa,KAAA,GAChB,OAAO,WAAW;CAEnB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,GACnD,OAAO,OAAO;CAEf,OAAO;AACR;;AAGA,SAAgB,SAAS,OAAkD;CAC1E,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC3E;;AAGA,SAAgB,cAAc,OAAiC;CAC9D,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,OAAO,SAAS;AACzF;AAEA,MAAM,0BAA0B;AAEhC,SAAS,aAAa,MAAc,OAAgB,QAA0B;CAC7E,IACC,OAAO,UAAU,YACjB,MAAM,WAAW,KACjB,MAAM,SAAS,kBACf,wBAAwB,KAAK,KAAK,GACjC;EACD,OAAO,KACN,GAAG,KAAK,mFAAmF,eAAe,aAC3G;EACA,OAAO;CACR;CACA,OAAO;AACR;AAEA,SAAS,aACR,MACA,OACA,WACA,QACqB;CACrB,IAAI,UAAU,KAAA,GACb;CAED,IAAI,OAAO,UAAU,UAAU;EAC9B,OAAO,KAAK,GAAG,KAAK,kBAAkB;EACtC;CACD;CACA,MAAM,OAAO,aAAa,OAAO,SAAS;CAC1C,OAAO,SAAS,KAAK,KAAA,IAAY;AAClC;AAEA,SAAS,mBAAmB,OAAyB;CACpD,IAAI;EACH,OAAO,KAAK,UAAU,KAAK,MAAM,KAAA;CAClC,QAAQ;EACP,OAAO;CACR;AACD;;;AC3KA,MAAM,kBAAkB;;;;;;AAOxB,SAAgB,iBAAiB,OAAyC;CACzE,IAAI,CAAC,SAAS,KAAK,GAClB,OAAO;CAER,MAAM,EAAE,MAAM,OAAO,QAAQ,QAAQ,aAAa;CAClD,OACC,OAAO,UAAU,YACjB,MAAM,SAAS,KACf,cAAc,MAAM,MACnB,SAAS,KAAA,KAAa,OAAO,SAAS,cACtC,WAAW,KAAA,KAAa,OAAO,WAAW,cAC1C,aAAa,KAAA,KAAa,OAAO,aAAa;AAEjD;;;;;;;;;AAUA,eAAsB,aAAa,UAAyD;CAC3F,IAAI;EACH,MAAM,YAAY,SAAS,QAAQ,IAAI,cAAc,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY;EAC1F,IAAI,cAAc,8BAA8B,cAAc,oBAC7D;EAGD,MAAM,WAAW,OAAO,SAAS,QAAQ,IAAI,gBAAgB,CAAC;EAC9D,IAAI,OAAO,SAAS,QAAQ,KAAK,WAAW,iBAAiB;GAC5D,MAAM,SAAS,MAAM,OAAO;GAC5B;EACD;EAEA,MAAM,QAAQ,MAAM,YAAY,SAAS,MAAM,eAAe;EAC9D,IAAI,UAAU,KAAA,GACb;EAGD,MAAM,SAAkB,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAAC;EAClE,OAAO,iBAAiB,MAAM,IAAI,SAAS,KAAA;CAC5C,QAAQ;EACP;CACD;AACD;;AAGA,eAAe,YACd,MACA,OACkC;CAClC,IAAI,SAAS,MACZ;CAED,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,SAAuB,CAAC;CAC9B,IAAI,QAAQ;CACZ,SAAS;EACR,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;EAC1C,IAAI,MACH;EAED,SAAS,MAAM;EACf,IAAI,QAAQ,OAAO;GAClB,MAAM,OAAO,OAAO;GACpB;EACD;EACA,OAAO,KAAK,KAAK;CAClB;CACA,MAAM,QAAQ,IAAI,WAAW,KAAK;CAClC,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,IAAI,OAAO,MAAM;EACvB,UAAU,MAAM;CACjB;CACA,OAAO;AACR;;AC/DA,MAAM,mBAAoC;CAAE,QAAQ;CAAK,OAAO;AAAwB;AACxF,MAAM,wBAAwB;;;;;;;;AAS9B,SAAgB,UAAU,OAAgB,UAA4B,CAAC,GAAmB;CACzF,MAAM,WAAW,YAAY,QAAQ,qBAAqB;CAE1D,MAAM,UAAU,aAAa,OAAO,QAAQ,QAAQ;CACpD,IAAI,YAAY,KAAA,GACf,OAAO,iBAAiB;EACvB,MAAM,QAAQ;EACd,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAChB,QAAQ,QAAQ;EAChB;CACD,CAAC;CAGF,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,UAAU,QAAQ,WAAW,KAAA,KAAa,UAAU,QAAQ,QAAQ,KAAK;CAC/E,OAAO,iBAAiB;EACvB,MAAM,SAAS;EACf,OAAO,SAAS;EAChB,QAAQ,SAAS;EACjB,QAAQ,UAAU,UAAU,KAAK,IAAI,SAAS;EAC9C;CACD,CAAC;AACF;AAEA,SAAS,YAAY,WAAmD;CACvE,IAAI,cAAc,KAAA,KAAa,cAAc,IAC5C;CAED,IAAI;EACH,OAAO,eAAe,mBAAmB,SAAS,WAAW,qBAAqB,CAAC;CACpF,QAAQ;EAEP;CACD;AACD;AAEA,SAAS,aACR,OACA,UAC8B;CAC9B,IAAI,aAAa,KAAA,GAChB;CAED,IAAI;EACH,IAAI,MAAM,QAAQ,GAAG;GACpB,KAAK,MAAM,CAAC,SAAS,aAAa,UAKjC,IAHC,OAAO,YAAY,WAChB,aAAa,OAAO,MAAM,MAAM,UAChC,iBAAiB,SAEpB,OAAO;GAGT;EACD;EAEA,MAAM,EAAE,IAAI,cAAc;EAC1B,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,SAAS,GAAG;GACxD,MAAM,SAAS,OAAO,eAAe,aAAa,OAAO,MAAM,MAAM;GACrE,MAAM,cAAc,OAAO,UAAU,gBAAgB,KAAK,MAAM;GAChE,IAAI,UAAU,aACb,OAAO;EAET;EACA;CACD,QAAQ;EAEP;CACD;AACD;AAEA,SAAS,MACR,UACgE;CAChE,OAAO,oBAAoB;AAC5B;AAEA,SAAS,UAAU,QAAqC,OAAyB;CAChF,IAAI;EACH,OAAO,OAAO,KAAK,MAAM;CAC1B,QAAQ;EACP,OAAO;CACR;AACD;AAEA,SAAS,UAAU,OAAoC;CACtD,IAAI;EACH,MAAM,UAAU,aAAa,OAAO,SAAS;EAC7C,OAAO,OAAO,YAAY,WAAW,UAAU,KAAA;CAChD,QAAQ;EACP;CACD;AACD;AAEA,SAAS,gBAAgB,OAAoC;CAC5D,MAAM,OAAO,aAAa,OAAO,aAAa;CAC9C,OAAO,OAAO,SAAS,aAAa,KAAK,OAAO,KAAA;AACjD;AAEA,SAAS,aAAa,OAAgB,KAAsB;CAC3D,IAAK,OAAO,UAAU,YAAY,OAAO,UAAU,cAAe,UAAU,MAC3E;CAED,OAAQ,MAAkC;AAC3C;;AChHA,MAAM,8BAAmC,IAAI,IAAI;CAAC;CAAQ;CAAa;AAAM,CAAC;;;;;;;;;;;AAY9E,SAAgB,eACf,WACA,UAAiC,CAAC,GACb;CACrB,MAAM,KAAK,QAAQ,MAAM;CACzB,MAAM,SAAmB,CAAC;CAE1B,IAAI,CAAC,YAAY,IAAI,EAAE,GACtB,OAAO,KAAK,gDAA0C;CAGvD,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,SAAS,GAAG;EACxD,IAAI,QAAQ,IAAI;GACf,OAAO,KAAK,iCAAiC;GAC7C;EACD;EACA,IAAI;GACH,iBAAiB;IAAE,GAAG;IAAU,YAAY,KAAA;GAAU,CAAC;EACxD,SAAS,OAAO;GACf,IAAI,iBAAiB,cACpB,OAAO,KAAK,GAAG,IAAI,IAAI,MAAM,OAAO,KAAK,IAAI,GAAG;QAEhD,MAAM;EAER;CACD;CAEA,IAAI,OAAO,SAAS,GACnB,MAAM,IAAI,aAAa,MAAM;CAG9B,OAAO,OAAO,OAAO;EAAE,WAAW,OAAO,OAAO,EAAE,GAAG,UAAU,CAAC;EAAG;CAAG,CAAC;AACxE;;;AChEA,MAAM,mDAAwD,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAK;AAAG,CAAC;;;;;;AAY/F,SAAgB,WAAW,SAAyB,UAA6B,CAAC,GAAa;CAC9F,MAAM,aAAa,iBAAiB,eAAe,OAAO,CAAC;CAE3D,IAAI,WAAW,SAAS,OAAO,iCAAiC,IAAI,WAAW,MAAM,GACpF,MAAM,IAAI,aAAa,CACtB,UAAU,WAAW,OAAO,2EAC7B,CAAC;CAGF,MAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;CAC3C,QAAQ,IAAI,gBAAgB,oBAAoB;CAEhD,OAAO,IAAI,SAAS,KAAK,UAAU,UAAU,GAAG;EAC/C,QAAQ,WAAW;EACnB;CACD,CAAC;AACF;;AAGA,SAAS,eAAe,OAAqC;CAC5D,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACrE,MAAM,IAAI,aAAa,CAAC,2BAA2B,CAAC;CAErD,MAAM,EAAE,MAAM,OAAO,QAAQ,QAAQ,UAAU,GAAG,eAAe;CACjE,OAAO;EAAE;EAAM;EAAO;EAAQ;EAAQ;EAAU;CAAW;AAC5D"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/sanitize.ts","../src/problem.ts","../src/client.ts","../src/from-error.ts","../src/registry.ts","../src/response.ts"],"sourcesContent":["const WHITESPACE_RUNS = /\\s+/g;\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/g;\n\n/**\n * Makes free text safe for a human-readable problem field: every whitespace run\n * (tabs, line breaks, Unicode line separators) becomes one space, other control\n * characters are removed, surrounding whitespace is trimmed and the result is\n * cut to `maxLength` code points.\n */\nexport function sanitizeText(value: string, maxLength: number): string {\n\tconst cleaned = value.replace(WHITESPACE_RUNS, \" \").replace(CONTROL_CHARS, \"\").trim();\n\treturn truncate(cleaned, maxLength);\n}\n\n/** Cuts a string to at most `maxLength` code points, ending with an ellipsis if cut. */\nexport function truncate(value: string, maxLength: number): string {\n\t// A string never has more code points than code units, so this is a cheap pre-check.\n\tif (value.length <= maxLength) {\n\t\treturn value;\n\t}\n\tconst chars = Array.from(value);\n\tif (chars.length <= maxLength) {\n\t\treturn value;\n\t}\n\treturn `${chars.slice(0, maxLength - 1).join(\"\")}…`;\n}\n","import { sanitizeText } from \"./sanitize.js\";\n\n/** Content-Type for RFC 9457 problem documents. */\nexport const PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\n\n/** Default problem `type` as defined by RFC 9457. */\nexport const DEFAULT_PROBLEM_TYPE = \"about:blank\";\n\nexport const TITLE_MAX_LENGTH = 200;\nexport const DETAIL_MAX_LENGTH = 1000;\nconst URI_MAX_LENGTH = 2048;\n\n/**\n * Whether a response with this status can carry a problem body: 200-599, except\n * 204, 205 and 304, which must not have a body.\n */\nexport function canCarryProblemBody(status: number): boolean {\n\treturn status >= 200 && status !== 204 && status !== 205 && status !== 304;\n}\n\n/** Members defined by RFC 9457. They may not be used as extension keys. */\nexport const RESERVED_MEMBERS: ReadonlySet<string> = new Set([\n\t\"type\",\n\t\"title\",\n\t\"status\",\n\t\"detail\",\n\t\"instance\",\n]);\n\n/**\n * An RFC 9457 problem document. Extension members are flattened next to the\n * standard members, as the specification requires.\n */\nexport interface ProblemDetails {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\t[extension: string]: unknown;\n}\n\n/**\n * Input for `problem()`. Extensions are passed separately so that they can be\n * checked against the reserved members before a document is built.\n */\nexport interface ProblemInput {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\textensions?: Readonly<Record<string, unknown>>;\n}\n\nexport type ProblemErrorCode = \"INVALID_PROBLEM\";\n\n/** Thrown when problem details cannot be built or are invalid. Carries no input values. */\nexport class ProblemError extends Error {\n\treadonly code: ProblemErrorCode = \"INVALID_PROBLEM\";\n\treadonly issues: readonly string[];\n\n\tconstructor(issues: readonly string[]) {\n\t\tsuper(`Invalid problem details: ${issues.join(\"; \")}`);\n\t\tthis.name = \"ProblemError\";\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * Builds a validated problem document. Text fields are sanitized and length\n * limited; invalid input throws a `ProblemError` with code `INVALID_PROBLEM`.\n */\nexport function problem(input: ProblemInput): ProblemDetails {\n\treturn normalizeProblem(input);\n}\n\n/** @internal Validates and normalizes a problem input. Shared by all helpers. */\nexport function normalizeProblem(input: ProblemInput): ProblemDetails {\n\tconst issues: string[] = [];\n\n\tif (!isValidStatus(input.status)) {\n\t\tissues.push(\"status must be an integer between 100 and 599\");\n\t}\n\n\tconst title = typeof input.title === \"string\" ? sanitizeText(input.title, TITLE_MAX_LENGTH) : \"\";\n\tif (title === \"\") {\n\t\tissues.push(\"title must be a non-empty string\");\n\t}\n\n\tconst type =\n\t\tinput.type === undefined ? DEFAULT_PROBLEM_TYPE : uriReference(\"type\", input.type, issues);\n\tconst instance =\n\t\tinput.instance === undefined ? undefined : uriReference(\"instance\", input.instance, issues);\n\tconst detail = optionalText(\"detail\", input.detail, DETAIL_MAX_LENGTH, issues);\n\n\tconst extensions: Record<string, unknown> = {};\n\tif (input.extensions !== undefined) {\n\t\tif (!isRecord(input.extensions)) {\n\t\t\tissues.push(\"extensions must be a plain object\");\n\t\t} else {\n\t\t\tfor (const [key, value] of Object.entries(input.extensions)) {\n\t\t\t\tif (RESERVED_MEMBERS.has(key)) {\n\t\t\t\t\tissues.push(`extension key \"${key}\" collides with a reserved problem member`);\n\t\t\t\t} else if (key === \"__proto__\") {\n\t\t\t\t\tissues.push('extension key \"__proto__\" is not allowed');\n\t\t\t\t} else if (!isJsonSerializable(value)) {\n\t\t\t\t\tissues.push(`extension \"${key}\" must be JSON-serializable`);\n\t\t\t\t} else {\n\t\t\t\t\textensions[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\tconst result: ProblemDetails = { type, title, status: input.status };\n\tif (detail !== undefined) {\n\t\tresult.detail = detail;\n\t}\n\tif (instance !== undefined) {\n\t\tresult.instance = instance;\n\t}\n\tfor (const [key, value] of Object.entries(extensions)) {\n\t\tresult[key] = value;\n\t}\n\treturn result;\n}\n\n/** @internal */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** @internal */\nexport function isValidStatus(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 100 && value <= 599;\n}\n\nconst URI_REFERENCE_FORBIDDEN = /[\\s\\u0000-\\u001f\\u007f]/;\n\nfunction uriReference(name: string, value: unknown, issues: string[]): string {\n\tif (\n\t\ttypeof value !== \"string\" ||\n\t\tvalue.length === 0 ||\n\t\tvalue.length > URI_MAX_LENGTH ||\n\t\tURI_REFERENCE_FORBIDDEN.test(value)\n\t) {\n\t\tissues.push(\n\t\t\t`${name} must be a non-empty URI reference without whitespace or control characters (max ${URI_MAX_LENGTH} characters)`,\n\t\t);\n\t\treturn \"\";\n\t}\n\treturn value;\n}\n\nfunction optionalText(\n\tname: string,\n\tvalue: unknown,\n\tmaxLength: number,\n\tissues: string[],\n): string | undefined {\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\tif (typeof value !== \"string\") {\n\t\tissues.push(`${name} must be a string`);\n\t\treturn undefined;\n\t}\n\tconst text = sanitizeText(value, maxLength);\n\treturn text === \"\" ? undefined : text;\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n\ttry {\n\t\treturn JSON.stringify(value) !== undefined;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","import { isRecord, isValidStatus, type ProblemDetails } from \"./problem.js\";\n\n/** Largest problem body that `parseProblem` will read. */\nconst PARSE_MAX_BYTES = 64 * 1024;\n\n/**\n * Type guard for problem documents: `title` is a non-empty string, `status` is\n * an integer between 100 and 599, and `type`, `detail`, `instance` are strings\n * when present. Extension members are not inspected.\n */\nexport function isProblemDetails(value: unknown): value is ProblemDetails {\n\tif (!isRecord(value)) {\n\t\treturn false;\n\t}\n\tconst { type, title, status, detail, instance } = value;\n\treturn (\n\t\ttypeof title === \"string\" &&\n\t\ttitle.length > 0 &&\n\t\tisValidStatus(status) &&\n\t\t(type === undefined || typeof type === \"string\") &&\n\t\t(detail === undefined || typeof detail === \"string\") &&\n\t\t(instance === undefined || typeof instance === \"string\")\n\t);\n}\n\n/**\n * Reads a problem document from a `Response`. Returns `undefined` unless the\n * media type is `application/problem+json` or `application/json` and the body is\n * a valid problem. Bodies over 64 KiB, malformed JSON and read errors also\n * yield `undefined`; this function never throws.\n *\n * The response body is consumed. Call `response.clone()` first if it is needed later.\n */\nexport async function parseProblem(response: Response): Promise<ProblemDetails | undefined> {\n\ttry {\n\t\tconst mediaType = response.headers.get(\"content-type\")?.split(\";\")[0]?.trim().toLowerCase();\n\t\tif (mediaType !== \"application/problem+json\" && mediaType !== \"application/json\") {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst declared = Number(response.headers.get(\"content-length\"));\n\t\tif (Number.isFinite(declared) && declared > PARSE_MAX_BYTES) {\n\t\t\tawait response.body?.cancel();\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst bytes = await readLimited(response.body, PARSE_MAX_BYTES);\n\t\tif (bytes === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst parsed: unknown = JSON.parse(new TextDecoder().decode(bytes));\n\t\treturn isProblemDetails(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Reads a stream up to `limit` bytes. Resolves to `undefined` if the limit is exceeded. */\nasync function readLimited(\n\tbody: ReadableStream<Uint8Array> | null,\n\tlimit: number,\n): Promise<Uint8Array | undefined> {\n\tif (body === null) {\n\t\treturn undefined;\n\t}\n\tconst reader = body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) {\n\t\t\tbreak;\n\t\t}\n\t\ttotal += value.byteLength;\n\t\tif (total > limit) {\n\t\t\tawait reader.cancel();\n\t\t\treturn undefined;\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst bytes = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tbytes.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn bytes;\n}\n","import { normalizeProblem, type ProblemDetails } from \"./problem.js\";\nimport type { ProblemRegistry, ProblemTemplate } from \"./registry.js\";\nimport { truncate } from \"./sanitize.js\";\n\n/** A class used as a registry key in `fromError`. Matched with `instanceof`. */\nexport type ErrorClass = abstract new (...args: never[]) => unknown;\n\nexport interface FromErrorOptions {\n\t/**\n\t * Opt-in for unknown errors. Only when this returns `true` is the error\n\t * message copied into `detail` (sanitized and length limited).\n\t */\n\texpose?: (error: unknown) => boolean;\n\t/**\n\t * Known errors. A `ProblemRegistry` from `defineProblems`, or a `Map` whose\n\t * string keys match `error.code` and whose class keys match with `instanceof`.\n\t * The first matching entry wins. Matched errors never expose their message.\n\t */\n\tregistry?: ProblemRegistry | ReadonlyMap<ErrorClass | string, ProblemTemplate>;\n\t/** Template for unknown errors. Defaults to status 500 and a generic title. */\n\tfallback?: ProblemTemplate;\n\t/** Request identifier. When set, `instance` becomes `urn:request:<id>`. */\n\tinstanceFromRequestId?: string;\n}\n\nconst DEFAULT_FALLBACK: ProblemTemplate = { status: 500, title: \"Internal Server Error\" };\nconst REQUEST_ID_MAX_LENGTH = 128;\n\n/**\n * Maps any thrown value to a problem document without leaking internals.\n *\n * Unknown errors produce only the fallback (by default status 500 and a generic\n * title). Their `message`, `stack`, `cause` and `name` are never included unless\n * `expose(error)` returns `true`.\n */\nexport function fromError(error: unknown, options: FromErrorOptions = {}): ProblemDetails {\n\tconst instance = instanceFor(options.instanceFromRequestId);\n\n\tconst matched = findTemplate(error, options.registry);\n\tif (matched !== undefined) {\n\t\treturn normalizeProblem({\n\t\t\ttype: matched.type,\n\t\t\ttitle: matched.title,\n\t\t\tstatus: matched.status,\n\t\t\tdetail: matched.detail,\n\t\t\tinstance,\n\t\t});\n\t}\n\n\tconst fallback = options.fallback ?? DEFAULT_FALLBACK;\n\tconst exposed = options.expose !== undefined && isExposed(options.expose, error);\n\treturn normalizeProblem({\n\t\ttype: fallback.type,\n\t\ttitle: fallback.title,\n\t\tstatus: fallback.status,\n\t\tdetail: exposed ? messageOf(error) : fallback.detail,\n\t\tinstance,\n\t});\n}\n\nfunction instanceFor(requestId: string | undefined): string | undefined {\n\tif (requestId === undefined || requestId === \"\") {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn `urn:request:${encodeURIComponent(truncate(requestId, REQUEST_ID_MAX_LENGTH))}`;\n\t} catch {\n\t\t// Lone surrogates cannot be encoded; an unusable request id is dropped, not thrown.\n\t\treturn undefined;\n\t}\n}\n\nfunction findTemplate(\n\terror: unknown,\n\tregistry: FromErrorOptions[\"registry\"],\n): ProblemTemplate | undefined {\n\tif (registry === undefined) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\tif (isMap(registry)) {\n\t\t\tfor (const [matcher, template] of registry) {\n\t\t\t\tconst hit =\n\t\t\t\t\ttypeof matcher === \"string\"\n\t\t\t\t\t\t? readProperty(error, \"code\") === matcher\n\t\t\t\t\t\t: error instanceof matcher;\n\t\t\t\tif (hit) {\n\t\t\t\t\treturn template;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst { by, templates } = registry;\n\t\tfor (const [key, template] of Object.entries(templates)) {\n\t\t\tconst byCode = by !== \"className\" && readProperty(error, \"code\") === key;\n\t\t\tconst byClassName = by !== \"code\" && constructorName(error) === key;\n\t\t\tif (byCode || byClassName) {\n\t\t\t\treturn template;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\t// A throwing getter or hostile matcher must not change the response shape.\n\t\treturn undefined;\n\t}\n}\n\nfunction isMap(\n\tregistry: NonNullable<FromErrorOptions[\"registry\"]>,\n): registry is ReadonlyMap<ErrorClass | string, ProblemTemplate> {\n\treturn registry instanceof Map;\n}\n\nfunction isExposed(expose: (error: unknown) => boolean, error: unknown): boolean {\n\ttry {\n\t\treturn expose(error) === true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction messageOf(error: unknown): string | undefined {\n\ttry {\n\t\tconst message = readProperty(error, \"message\");\n\t\treturn typeof message === \"string\" ? message : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction constructorName(value: unknown): string | undefined {\n\tconst ctor = readProperty(value, \"constructor\");\n\treturn typeof ctor === \"function\" ? ctor.name : undefined;\n}\n\nfunction readProperty(value: unknown, key: string): unknown {\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\treturn undefined;\n\t}\n\treturn (value as Record<string, unknown>)[key];\n}\n","import { canCarryProblemBody, normalizeProblem, ProblemError } from \"./problem.js\";\n\n/** A problem definition that can be reused for errors of a known kind. */\nexport interface ProblemTemplate {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n}\n\nexport type ProblemTemplates = Readonly<Record<string, ProblemTemplate>>;\n\n/**\n * How registry keys are matched against errors:\n * - `code`: `error.code === key` (default, survives minification)\n * - `className`: `error.constructor.name === key`\n * - `both`: either of the two\n */\nexport type MatchBy = \"code\" | \"className\" | \"both\";\n\nexport interface DefineProblemsOptions {\n\tby?: MatchBy;\n}\n\nexport interface ProblemRegistry<T extends ProblemTemplates = ProblemTemplates> {\n\treadonly templates: T;\n\treadonly by: MatchBy;\n}\n\nconst MATCH_MODES: ReadonlySet<string> = new Set([\"code\", \"className\", \"both\"]);\n\n/**\n * Defines named problem templates for `fromError`. Every template is validated\n * immediately, so invalid definitions fail at startup instead of per request.\n *\n * @example\n * const problems = defineProblems({\n * NotFound: { status: 404, title: \"Not found\", type: \"https://example.com/problems/not-found\" },\n * });\n * fromError(error, { registry: problems });\n */\nexport function defineProblems<const T extends ProblemTemplates>(\n\ttemplates: T,\n\toptions: DefineProblemsOptions = {},\n): ProblemRegistry<T> {\n\tconst by = options.by ?? \"code\";\n\tconst issues: string[] = [];\n\n\tif (!MATCH_MODES.has(by)) {\n\t\tissues.push('by must be \"code\", \"className\" or \"both\"');\n\t}\n\n\tfor (const [key, template] of Object.entries(templates)) {\n\t\tif (key === \"\") {\n\t\t\tissues.push(\"template keys must be non-empty\");\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tnormalizeProblem({ ...template, extensions: undefined });\n\t\t\tif (!canCarryProblemBody(template.status)) {\n\t\t\t\tissues.push(`${key}: status ${template.status} cannot carry a problem body`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof ProblemError) {\n\t\t\t\tissues.push(`${key}: ${error.issues.join(\"; \")}`);\n\t\t\t} else {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\treturn Object.freeze({ templates: Object.freeze({ ...templates }), by });\n}\n","import {\n\tcanCarryProblemBody,\n\tnormalizeProblem,\n\tPROBLEM_CONTENT_TYPE,\n\ttype ProblemDetails,\n\tProblemError,\n\ttype ProblemInput,\n} from \"./problem.js\";\n\nexport interface ToResponseOptions {\n\t/** Extra headers. The Content-Type is always set to the problem media type. */\n\theaders?: HeadersInit;\n}\n\n/**\n * Creates a Web-standard `Response` carrying the problem as\n * `application/problem+json; charset=utf-8`. The problem is validated and\n * sanitized again before it is serialized.\n */\nexport function toResponse(problem: ProblemDetails, options: ToResponseOptions = {}): Response {\n\tconst normalized = normalizeProblem(toProblemInput(problem));\n\n\tif (!canCarryProblemBody(normalized.status)) {\n\t\tthrow new ProblemError([\n\t\t\t`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`,\n\t\t]);\n\t}\n\n\tconst headers = new Headers(options.headers);\n\theaders.set(\"content-type\", PROBLEM_CONTENT_TYPE);\n\n\treturn new Response(JSON.stringify(normalized), {\n\t\tstatus: normalized.status,\n\t\theaders,\n\t});\n}\n\n/** Converts the flattened wire shape into the input shape used for validation. */\nfunction toProblemInput(value: ProblemDetails): ProblemInput {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new ProblemError([\"problem must be an object\"]);\n\t}\n\tconst { type, title, status, detail, instance, ...extensions } = value;\n\treturn { type, title, status, detail, instance, extensions };\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -25,6 +25,13 @@ const DEFAULT_PROBLEM_TYPE = "about:blank";
25
25
  const TITLE_MAX_LENGTH = 200;
26
26
  const DETAIL_MAX_LENGTH = 1e3;
27
27
  const URI_MAX_LENGTH = 2048;
28
+ /**
29
+ * Whether a response with this status can carry a problem body: 200-599, except
30
+ * 204, 205 and 304, which must not have a body.
31
+ */
32
+ function canCarryProblemBody(status) {
33
+ return status >= 200 && status !== 204 && status !== 205 && status !== 304;
34
+ }
28
35
  /** Members defined by RFC 9457. They may not be used as extension keys. */
29
36
  const RESERVED_MEMBERS = /* @__PURE__ */ new Set([
30
37
  "type",
@@ -287,6 +294,7 @@ function defineProblems(templates, options = {}) {
287
294
  ...template,
288
295
  extensions: void 0
289
296
  });
297
+ if (!canCarryProblemBody(template.status)) issues.push(`${key}: status ${template.status} cannot carry a problem body`);
290
298
  } catch (error) {
291
299
  if (error instanceof ProblemError) issues.push(`${key}: ${error.issues.join("; ")}`);
292
300
  else throw error;
@@ -299,14 +307,6 @@ function defineProblems(templates, options = {}) {
299
307
  });
300
308
  }
301
309
  // src/response.ts
302
- /** Statuses that cannot carry a body, or that the Response constructor rejects. */
303
- const BODYLESS_OR_UNSUPPORTED_STATUSES = /* @__PURE__ */ new Set([
304
- 101,
305
- 103,
306
- 204,
307
- 205,
308
- 304
309
- ]);
310
310
  /**
311
311
  * Creates a Web-standard `Response` carrying the problem as
312
312
  * `application/problem+json; charset=utf-8`. The problem is validated and
@@ -314,7 +314,7 @@ const BODYLESS_OR_UNSUPPORTED_STATUSES = /* @__PURE__ */ new Set([
314
314
  */
315
315
  function toResponse(problem, options = {}) {
316
316
  const normalized = normalizeProblem(toProblemInput(problem));
317
- if (normalized.status < 200 || BODYLESS_OR_UNSUPPORTED_STATUSES.has(normalized.status)) throw new ProblemError([`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`]);
317
+ if (!canCarryProblemBody(normalized.status)) throw new ProblemError([`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`]);
318
318
  const headers = new Headers(options.headers);
319
319
  headers.set("content-type", PROBLEM_CONTENT_TYPE);
320
320
  return new Response(JSON.stringify(normalized), {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/sanitize.ts","../src/problem.ts","../src/client.ts","../src/from-error.ts","../src/registry.ts","../src/response.ts"],"sourcesContent":["const WHITESPACE_RUNS = /\\s+/g;\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/g;\n\n/**\n * Makes free text safe for a human-readable problem field: every whitespace run\n * (tabs, line breaks, Unicode line separators) becomes one space, other control\n * characters are removed, surrounding whitespace is trimmed and the result is\n * cut to `maxLength` code points.\n */\nexport function sanitizeText(value: string, maxLength: number): string {\n\tconst cleaned = value.replace(WHITESPACE_RUNS, \" \").replace(CONTROL_CHARS, \"\").trim();\n\treturn truncate(cleaned, maxLength);\n}\n\n/** Cuts a string to at most `maxLength` code points, ending with an ellipsis if cut. */\nexport function truncate(value: string, maxLength: number): string {\n\t// A string never has more code points than code units, so this is a cheap pre-check.\n\tif (value.length <= maxLength) {\n\t\treturn value;\n\t}\n\tconst chars = Array.from(value);\n\tif (chars.length <= maxLength) {\n\t\treturn value;\n\t}\n\treturn `${chars.slice(0, maxLength - 1).join(\"\")}…`;\n}\n","import { sanitizeText } from \"./sanitize.js\";\n\n/** Content-Type for RFC 9457 problem documents. */\nexport const PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\n\n/** Default problem `type` as defined by RFC 9457. */\nexport const DEFAULT_PROBLEM_TYPE = \"about:blank\";\n\nexport const TITLE_MAX_LENGTH = 200;\nexport const DETAIL_MAX_LENGTH = 1000;\nconst URI_MAX_LENGTH = 2048;\n\n/** Members defined by RFC 9457. They may not be used as extension keys. */\nexport const RESERVED_MEMBERS: ReadonlySet<string> = new Set([\n\t\"type\",\n\t\"title\",\n\t\"status\",\n\t\"detail\",\n\t\"instance\",\n]);\n\n/**\n * An RFC 9457 problem document. Extension members are flattened next to the\n * standard members, as the specification requires.\n */\nexport interface ProblemDetails {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\t[extension: string]: unknown;\n}\n\n/**\n * Input for `problem()`. Extensions are passed separately so that they can be\n * checked against the reserved members before a document is built.\n */\nexport interface ProblemInput {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\textensions?: Readonly<Record<string, unknown>>;\n}\n\nexport type ProblemErrorCode = \"INVALID_PROBLEM\";\n\n/** Thrown when problem details cannot be built or are invalid. Carries no input values. */\nexport class ProblemError extends Error {\n\treadonly code: ProblemErrorCode = \"INVALID_PROBLEM\";\n\treadonly issues: readonly string[];\n\n\tconstructor(issues: readonly string[]) {\n\t\tsuper(`Invalid problem details: ${issues.join(\"; \")}`);\n\t\tthis.name = \"ProblemError\";\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * Builds a validated problem document. Text fields are sanitized and length\n * limited; invalid input throws a `ProblemError` with code `INVALID_PROBLEM`.\n */\nexport function problem(input: ProblemInput): ProblemDetails {\n\treturn normalizeProblem(input);\n}\n\n/** @internal Validates and normalizes a problem input. Shared by all helpers. */\nexport function normalizeProblem(input: ProblemInput): ProblemDetails {\n\tconst issues: string[] = [];\n\n\tif (!isValidStatus(input.status)) {\n\t\tissues.push(\"status must be an integer between 100 and 599\");\n\t}\n\n\tconst title = typeof input.title === \"string\" ? sanitizeText(input.title, TITLE_MAX_LENGTH) : \"\";\n\tif (title === \"\") {\n\t\tissues.push(\"title must be a non-empty string\");\n\t}\n\n\tconst type =\n\t\tinput.type === undefined ? DEFAULT_PROBLEM_TYPE : uriReference(\"type\", input.type, issues);\n\tconst instance =\n\t\tinput.instance === undefined ? undefined : uriReference(\"instance\", input.instance, issues);\n\tconst detail = optionalText(\"detail\", input.detail, DETAIL_MAX_LENGTH, issues);\n\n\tconst extensions: Record<string, unknown> = {};\n\tif (input.extensions !== undefined) {\n\t\tif (!isRecord(input.extensions)) {\n\t\t\tissues.push(\"extensions must be a plain object\");\n\t\t} else {\n\t\t\tfor (const [key, value] of Object.entries(input.extensions)) {\n\t\t\t\tif (RESERVED_MEMBERS.has(key)) {\n\t\t\t\t\tissues.push(`extension key \"${key}\" collides with a reserved problem member`);\n\t\t\t\t} else if (key === \"__proto__\") {\n\t\t\t\t\tissues.push('extension key \"__proto__\" is not allowed');\n\t\t\t\t} else if (!isJsonSerializable(value)) {\n\t\t\t\t\tissues.push(`extension \"${key}\" must be JSON-serializable`);\n\t\t\t\t} else {\n\t\t\t\t\textensions[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\tconst result: ProblemDetails = { type, title, status: input.status };\n\tif (detail !== undefined) {\n\t\tresult.detail = detail;\n\t}\n\tif (instance !== undefined) {\n\t\tresult.instance = instance;\n\t}\n\tfor (const [key, value] of Object.entries(extensions)) {\n\t\tresult[key] = value;\n\t}\n\treturn result;\n}\n\n/** @internal */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** @internal */\nexport function isValidStatus(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 100 && value <= 599;\n}\n\nconst URI_REFERENCE_FORBIDDEN = /[\\s\\u0000-\\u001f\\u007f]/;\n\nfunction uriReference(name: string, value: unknown, issues: string[]): string {\n\tif (\n\t\ttypeof value !== \"string\" ||\n\t\tvalue.length === 0 ||\n\t\tvalue.length > URI_MAX_LENGTH ||\n\t\tURI_REFERENCE_FORBIDDEN.test(value)\n\t) {\n\t\tissues.push(\n\t\t\t`${name} must be a non-empty URI reference without whitespace or control characters (max ${URI_MAX_LENGTH} characters)`,\n\t\t);\n\t\treturn \"\";\n\t}\n\treturn value;\n}\n\nfunction optionalText(\n\tname: string,\n\tvalue: unknown,\n\tmaxLength: number,\n\tissues: string[],\n): string | undefined {\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\tif (typeof value !== \"string\") {\n\t\tissues.push(`${name} must be a string`);\n\t\treturn undefined;\n\t}\n\tconst text = sanitizeText(value, maxLength);\n\treturn text === \"\" ? undefined : text;\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n\ttry {\n\t\treturn JSON.stringify(value) !== undefined;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","import { isRecord, isValidStatus, type ProblemDetails } from \"./problem.js\";\n\n/** Largest problem body that `parseProblem` will read. */\nconst PARSE_MAX_BYTES = 64 * 1024;\n\n/**\n * Type guard for problem documents: `title` is a non-empty string, `status` is\n * an integer between 100 and 599, and `type`, `detail`, `instance` are strings\n * when present. Extension members are not inspected.\n */\nexport function isProblemDetails(value: unknown): value is ProblemDetails {\n\tif (!isRecord(value)) {\n\t\treturn false;\n\t}\n\tconst { type, title, status, detail, instance } = value;\n\treturn (\n\t\ttypeof title === \"string\" &&\n\t\ttitle.length > 0 &&\n\t\tisValidStatus(status) &&\n\t\t(type === undefined || typeof type === \"string\") &&\n\t\t(detail === undefined || typeof detail === \"string\") &&\n\t\t(instance === undefined || typeof instance === \"string\")\n\t);\n}\n\n/**\n * Reads a problem document from a `Response`. Returns `undefined` unless the\n * media type is `application/problem+json` or `application/json` and the body is\n * a valid problem. Bodies over 64 KiB, malformed JSON and read errors also\n * yield `undefined`; this function never throws.\n *\n * The response body is consumed. Call `response.clone()` first if it is needed later.\n */\nexport async function parseProblem(response: Response): Promise<ProblemDetails | undefined> {\n\ttry {\n\t\tconst mediaType = response.headers.get(\"content-type\")?.split(\";\")[0]?.trim().toLowerCase();\n\t\tif (mediaType !== \"application/problem+json\" && mediaType !== \"application/json\") {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst declared = Number(response.headers.get(\"content-length\"));\n\t\tif (Number.isFinite(declared) && declared > PARSE_MAX_BYTES) {\n\t\t\tawait response.body?.cancel();\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst bytes = await readLimited(response.body, PARSE_MAX_BYTES);\n\t\tif (bytes === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst parsed: unknown = JSON.parse(new TextDecoder().decode(bytes));\n\t\treturn isProblemDetails(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Reads a stream up to `limit` bytes. Resolves to `undefined` if the limit is exceeded. */\nasync function readLimited(\n\tbody: ReadableStream<Uint8Array> | null,\n\tlimit: number,\n): Promise<Uint8Array | undefined> {\n\tif (body === null) {\n\t\treturn undefined;\n\t}\n\tconst reader = body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) {\n\t\t\tbreak;\n\t\t}\n\t\ttotal += value.byteLength;\n\t\tif (total > limit) {\n\t\t\tawait reader.cancel();\n\t\t\treturn undefined;\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst bytes = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tbytes.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn bytes;\n}\n","import { normalizeProblem, type ProblemDetails } from \"./problem.js\";\nimport type { ProblemRegistry, ProblemTemplate } from \"./registry.js\";\nimport { truncate } from \"./sanitize.js\";\n\n/** A class used as a registry key in `fromError`. Matched with `instanceof`. */\nexport type ErrorClass = abstract new (...args: never[]) => unknown;\n\nexport interface FromErrorOptions {\n\t/**\n\t * Opt-in for unknown errors. Only when this returns `true` is the error\n\t * message copied into `detail` (sanitized and length limited).\n\t */\n\texpose?: (error: unknown) => boolean;\n\t/**\n\t * Known errors. A `ProblemRegistry` from `defineProblems`, or a `Map` whose\n\t * string keys match `error.code` and whose class keys match with `instanceof`.\n\t * The first matching entry wins. Matched errors never expose their message.\n\t */\n\tregistry?: ProblemRegistry | ReadonlyMap<ErrorClass | string, ProblemTemplate>;\n\t/** Template for unknown errors. Defaults to status 500 and a generic title. */\n\tfallback?: ProblemTemplate;\n\t/** Request identifier. When set, `instance` becomes `urn:request:<id>`. */\n\tinstanceFromRequestId?: string;\n}\n\nconst DEFAULT_FALLBACK: ProblemTemplate = { status: 500, title: \"Internal Server Error\" };\nconst REQUEST_ID_MAX_LENGTH = 128;\n\n/**\n * Maps any thrown value to a problem document without leaking internals.\n *\n * Unknown errors produce only the fallback (by default status 500 and a generic\n * title). Their `message`, `stack`, `cause` and `name` are never included unless\n * `expose(error)` returns `true`.\n */\nexport function fromError(error: unknown, options: FromErrorOptions = {}): ProblemDetails {\n\tconst instance = instanceFor(options.instanceFromRequestId);\n\n\tconst matched = findTemplate(error, options.registry);\n\tif (matched !== undefined) {\n\t\treturn normalizeProblem({\n\t\t\ttype: matched.type,\n\t\t\ttitle: matched.title,\n\t\t\tstatus: matched.status,\n\t\t\tdetail: matched.detail,\n\t\t\tinstance,\n\t\t});\n\t}\n\n\tconst fallback = options.fallback ?? DEFAULT_FALLBACK;\n\tconst exposed = options.expose !== undefined && isExposed(options.expose, error);\n\treturn normalizeProblem({\n\t\ttype: fallback.type,\n\t\ttitle: fallback.title,\n\t\tstatus: fallback.status,\n\t\tdetail: exposed ? messageOf(error) : fallback.detail,\n\t\tinstance,\n\t});\n}\n\nfunction instanceFor(requestId: string | undefined): string | undefined {\n\tif (requestId === undefined || requestId === \"\") {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn `urn:request:${encodeURIComponent(truncate(requestId, REQUEST_ID_MAX_LENGTH))}`;\n\t} catch {\n\t\t// Lone surrogates cannot be encoded; an unusable request id is dropped, not thrown.\n\t\treturn undefined;\n\t}\n}\n\nfunction findTemplate(\n\terror: unknown,\n\tregistry: FromErrorOptions[\"registry\"],\n): ProblemTemplate | undefined {\n\tif (registry === undefined) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\tif (isMap(registry)) {\n\t\t\tfor (const [matcher, template] of registry) {\n\t\t\t\tconst hit =\n\t\t\t\t\ttypeof matcher === \"string\"\n\t\t\t\t\t\t? readProperty(error, \"code\") === matcher\n\t\t\t\t\t\t: error instanceof matcher;\n\t\t\t\tif (hit) {\n\t\t\t\t\treturn template;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst { by, templates } = registry;\n\t\tfor (const [key, template] of Object.entries(templates)) {\n\t\t\tconst byCode = by !== \"className\" && readProperty(error, \"code\") === key;\n\t\t\tconst byClassName = by !== \"code\" && constructorName(error) === key;\n\t\t\tif (byCode || byClassName) {\n\t\t\t\treturn template;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\t// A throwing getter or hostile matcher must not change the response shape.\n\t\treturn undefined;\n\t}\n}\n\nfunction isMap(\n\tregistry: NonNullable<FromErrorOptions[\"registry\"]>,\n): registry is ReadonlyMap<ErrorClass | string, ProblemTemplate> {\n\treturn registry instanceof Map;\n}\n\nfunction isExposed(expose: (error: unknown) => boolean, error: unknown): boolean {\n\ttry {\n\t\treturn expose(error) === true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction messageOf(error: unknown): string | undefined {\n\ttry {\n\t\tconst message = readProperty(error, \"message\");\n\t\treturn typeof message === \"string\" ? message : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction constructorName(value: unknown): string | undefined {\n\tconst ctor = readProperty(value, \"constructor\");\n\treturn typeof ctor === \"function\" ? ctor.name : undefined;\n}\n\nfunction readProperty(value: unknown, key: string): unknown {\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\treturn undefined;\n\t}\n\treturn (value as Record<string, unknown>)[key];\n}\n","import { normalizeProblem, ProblemError } from \"./problem.js\";\n\n/** A problem definition that can be reused for errors of a known kind. */\nexport interface ProblemTemplate {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n}\n\nexport type ProblemTemplates = Readonly<Record<string, ProblemTemplate>>;\n\n/**\n * How registry keys are matched against errors:\n * - `code`: `error.code === key` (default, survives minification)\n * - `className`: `error.constructor.name === key`\n * - `both`: either of the two\n */\nexport type MatchBy = \"code\" | \"className\" | \"both\";\n\nexport interface DefineProblemsOptions {\n\tby?: MatchBy;\n}\n\nexport interface ProblemRegistry<T extends ProblemTemplates = ProblemTemplates> {\n\treadonly templates: T;\n\treadonly by: MatchBy;\n}\n\nconst MATCH_MODES: ReadonlySet<string> = new Set([\"code\", \"className\", \"both\"]);\n\n/**\n * Defines named problem templates for `fromError`. Every template is validated\n * immediately, so invalid definitions fail at startup instead of per request.\n *\n * @example\n * const problems = defineProblems({\n * NotFound: { status: 404, title: \"Not found\", type: \"https://example.com/problems/not-found\" },\n * });\n * fromError(error, { registry: problems });\n */\nexport function defineProblems<const T extends ProblemTemplates>(\n\ttemplates: T,\n\toptions: DefineProblemsOptions = {},\n): ProblemRegistry<T> {\n\tconst by = options.by ?? \"code\";\n\tconst issues: string[] = [];\n\n\tif (!MATCH_MODES.has(by)) {\n\t\tissues.push('by must be \"code\", \"className\" or \"both\"');\n\t}\n\n\tfor (const [key, template] of Object.entries(templates)) {\n\t\tif (key === \"\") {\n\t\t\tissues.push(\"template keys must be non-empty\");\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tnormalizeProblem({ ...template, extensions: undefined });\n\t\t} catch (error) {\n\t\t\tif (error instanceof ProblemError) {\n\t\t\t\tissues.push(`${key}: ${error.issues.join(\"; \")}`);\n\t\t\t} else {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\treturn Object.freeze({ templates: Object.freeze({ ...templates }), by });\n}\n","import {\n\tnormalizeProblem,\n\tPROBLEM_CONTENT_TYPE,\n\ttype ProblemDetails,\n\tProblemError,\n\ttype ProblemInput,\n} from \"./problem.js\";\n\n/** Statuses that cannot carry a body, or that the Response constructor rejects. */\nconst BODYLESS_OR_UNSUPPORTED_STATUSES: ReadonlySet<number> = new Set([101, 103, 204, 205, 304]);\n\nexport interface ToResponseOptions {\n\t/** Extra headers. The Content-Type is always set to the problem media type. */\n\theaders?: HeadersInit;\n}\n\n/**\n * Creates a Web-standard `Response` carrying the problem as\n * `application/problem+json; charset=utf-8`. The problem is validated and\n * sanitized again before it is serialized.\n */\nexport function toResponse(problem: ProblemDetails, options: ToResponseOptions = {}): Response {\n\tconst normalized = normalizeProblem(toProblemInput(problem));\n\n\tif (normalized.status < 200 || BODYLESS_OR_UNSUPPORTED_STATUSES.has(normalized.status)) {\n\t\tthrow new ProblemError([\n\t\t\t`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`,\n\t\t]);\n\t}\n\n\tconst headers = new Headers(options.headers);\n\theaders.set(\"content-type\", PROBLEM_CONTENT_TYPE);\n\n\treturn new Response(JSON.stringify(normalized), {\n\t\tstatus: normalized.status,\n\t\theaders,\n\t});\n}\n\n/** Converts the flattened wire shape into the input shape used for validation. */\nfunction toProblemInput(value: ProblemDetails): ProblemInput {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new ProblemError([\"problem must be an object\"]);\n\t}\n\tconst { type, title, status, detail, instance, ...extensions } = value;\n\treturn { type, title, status, detail, instance, extensions };\n}\n"],"mappings":";AAAA,MAAM,kBAAkB;AACxB,MAAM,gBAAgB;;;;;;;AAQtB,SAAgB,aAAa,OAAe,WAA2B;CAEtE,OAAO,SADS,MAAM,QAAQ,iBAAiB,GAAG,CAAC,CAAC,QAAQ,eAAe,EAAE,CAAC,CAAC,KACzD,GAAG,SAAS;AACnC;;AAGA,SAAgB,SAAS,OAAe,WAA2B;CAElE,IAAI,MAAM,UAAU,WACnB,OAAO;CAER,MAAM,QAAQ,MAAM,KAAK,KAAK;CAC9B,IAAI,MAAM,UAAU,WACnB,OAAO;CAER,OAAO,GAAG,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;AAClD;;;ACtBA,MAAa,uBAAuB;;AAGpC,MAAa,uBAAuB;AAEpC,MAAa,mBAAmB;AAChC,MAAa,oBAAoB;AACjC,MAAM,iBAAiB;;AAGvB,MAAa,mCAAwC,IAAI,IAAI;CAC5D;CACA;CACA;CACA;CACA;AACD,CAAC;;AA+BD,IAAa,eAAb,cAAkC,MAAM;CACvC,OAAkC;CAClC;CAEA,YAAY,QAA2B;EACtC,MAAM,4BAA4B,OAAO,KAAK,IAAI,GAAG;EACrD,KAAK,OAAO;EACZ,KAAK,SAAS;CACf;AACD;;;;;AAMA,SAAgB,QAAQ,OAAqC;CAC5D,OAAO,iBAAiB,KAAK;AAC9B;;AAGA,SAAgB,iBAAiB,OAAqC;CACrE,MAAM,SAAmB,CAAC;CAE1B,IAAI,CAAC,cAAc,MAAM,MAAM,GAC9B,OAAO,KAAK,+CAA+C;CAG5D,MAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,aAAa,MAAM,OAAA,GAAuB,IAAI;CAC9F,IAAI,UAAU,IACb,OAAO,KAAK,kCAAkC;CAG/C,MAAM,OACL,MAAM,SAAS,KAAA,IAAY,uBAAuB,aAAa,QAAQ,MAAM,MAAM,MAAM;CAC1F,MAAM,WACL,MAAM,aAAa,KAAA,IAAY,KAAA,IAAY,aAAa,YAAY,MAAM,UAAU,MAAM;CAC3F,MAAM,SAAS,aAAa,UAAU,MAAM,QAAQ,mBAAmB,MAAM;CAE7E,MAAM,aAAsC,CAAC;CAC7C,IAAI,MAAM,eAAe,KAAA,GAAW;EACnC,IAAI,CAAC,SAAS,MAAM,UAAU,GAC7B,OAAO,KAAK,mCAAmC;OAE/C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,UAAU,GACzD,IAAI,iBAAiB,IAAI,GAAG,GAC3B,OAAO,KAAK,kBAAkB,IAAI,0CAA0C;OACtE,IAAI,QAAQ,aAClB,OAAO,KAAK,4CAA0C;OAChD,IAAI,CAAC,mBAAmB,KAAK,GACnC,OAAO,KAAK,cAAc,IAAI,4BAA4B;OAE1D,WAAW,OAAO;CAItB;CAEA,IAAI,OAAO,SAAS,GACnB,MAAM,IAAI,aAAa,MAAM;CAG9B,MAAM,SAAyB;EAAE;EAAM;EAAO,QAAQ,MAAM;CAAO;CACnE,IAAI,WAAW,KAAA,GACd,OAAO,SAAS;CAEjB,IAAI,aAAa,KAAA,GAChB,OAAO,WAAW;CAEnB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,GACnD,OAAO,OAAO;CAEf,OAAO;AACR;;AAGA,SAAgB,SAAS,OAAkD;CAC1E,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC3E;;AAGA,SAAgB,cAAc,OAAiC;CAC9D,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,OAAO,SAAS;AACzF;AAEA,MAAM,0BAA0B;AAEhC,SAAS,aAAa,MAAc,OAAgB,QAA0B;CAC7E,IACC,OAAO,UAAU,YACjB,MAAM,WAAW,KACjB,MAAM,SAAS,kBACf,wBAAwB,KAAK,KAAK,GACjC;EACD,OAAO,KACN,GAAG,KAAK,mFAAmF,eAAe,aAC3G;EACA,OAAO;CACR;CACA,OAAO;AACR;AAEA,SAAS,aACR,MACA,OACA,WACA,QACqB;CACrB,IAAI,UAAU,KAAA,GACb;CAED,IAAI,OAAO,UAAU,UAAU;EAC9B,OAAO,KAAK,GAAG,KAAK,kBAAkB;EACtC;CACD;CACA,MAAM,OAAO,aAAa,OAAO,SAAS;CAC1C,OAAO,SAAS,KAAK,KAAA,IAAY;AAClC;AAEA,SAAS,mBAAmB,OAAyB;CACpD,IAAI;EACH,OAAO,KAAK,UAAU,KAAK,MAAM,KAAA;CAClC,QAAQ;EACP,OAAO;CACR;AACD;;;AC3KA,MAAM,kBAAkB;;;;;;AAOxB,SAAgB,iBAAiB,OAAyC;CACzE,IAAI,CAAC,SAAS,KAAK,GAClB,OAAO;CAER,MAAM,EAAE,MAAM,OAAO,QAAQ,QAAQ,aAAa;CAClD,OACC,OAAO,UAAU,YACjB,MAAM,SAAS,KACf,cAAc,MAAM,MACnB,SAAS,KAAA,KAAa,OAAO,SAAS,cACtC,WAAW,KAAA,KAAa,OAAO,WAAW,cAC1C,aAAa,KAAA,KAAa,OAAO,aAAa;AAEjD;;;;;;;;;AAUA,eAAsB,aAAa,UAAyD;CAC3F,IAAI;EACH,MAAM,YAAY,SAAS,QAAQ,IAAI,cAAc,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY;EAC1F,IAAI,cAAc,8BAA8B,cAAc,oBAC7D;EAGD,MAAM,WAAW,OAAO,SAAS,QAAQ,IAAI,gBAAgB,CAAC;EAC9D,IAAI,OAAO,SAAS,QAAQ,KAAK,WAAW,iBAAiB;GAC5D,MAAM,SAAS,MAAM,OAAO;GAC5B;EACD;EAEA,MAAM,QAAQ,MAAM,YAAY,SAAS,MAAM,eAAe;EAC9D,IAAI,UAAU,KAAA,GACb;EAGD,MAAM,SAAkB,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAAC;EAClE,OAAO,iBAAiB,MAAM,IAAI,SAAS,KAAA;CAC5C,QAAQ;EACP;CACD;AACD;;AAGA,eAAe,YACd,MACA,OACkC;CAClC,IAAI,SAAS,MACZ;CAED,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,SAAuB,CAAC;CAC9B,IAAI,QAAQ;CACZ,SAAS;EACR,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;EAC1C,IAAI,MACH;EAED,SAAS,MAAM;EACf,IAAI,QAAQ,OAAO;GAClB,MAAM,OAAO,OAAO;GACpB;EACD;EACA,OAAO,KAAK,KAAK;CAClB;CACA,MAAM,QAAQ,IAAI,WAAW,KAAK;CAClC,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,IAAI,OAAO,MAAM;EACvB,UAAU,MAAM;CACjB;CACA,OAAO;AACR;;AC/DA,MAAM,mBAAoC;CAAE,QAAQ;CAAK,OAAO;AAAwB;AACxF,MAAM,wBAAwB;;;;;;;;AAS9B,SAAgB,UAAU,OAAgB,UAA4B,CAAC,GAAmB;CACzF,MAAM,WAAW,YAAY,QAAQ,qBAAqB;CAE1D,MAAM,UAAU,aAAa,OAAO,QAAQ,QAAQ;CACpD,IAAI,YAAY,KAAA,GACf,OAAO,iBAAiB;EACvB,MAAM,QAAQ;EACd,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAChB,QAAQ,QAAQ;EAChB;CACD,CAAC;CAGF,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,UAAU,QAAQ,WAAW,KAAA,KAAa,UAAU,QAAQ,QAAQ,KAAK;CAC/E,OAAO,iBAAiB;EACvB,MAAM,SAAS;EACf,OAAO,SAAS;EAChB,QAAQ,SAAS;EACjB,QAAQ,UAAU,UAAU,KAAK,IAAI,SAAS;EAC9C;CACD,CAAC;AACF;AAEA,SAAS,YAAY,WAAmD;CACvE,IAAI,cAAc,KAAA,KAAa,cAAc,IAC5C;CAED,IAAI;EACH,OAAO,eAAe,mBAAmB,SAAS,WAAW,qBAAqB,CAAC;CACpF,QAAQ;EAEP;CACD;AACD;AAEA,SAAS,aACR,OACA,UAC8B;CAC9B,IAAI,aAAa,KAAA,GAChB;CAED,IAAI;EACH,IAAI,MAAM,QAAQ,GAAG;GACpB,KAAK,MAAM,CAAC,SAAS,aAAa,UAKjC,IAHC,OAAO,YAAY,WAChB,aAAa,OAAO,MAAM,MAAM,UAChC,iBAAiB,SAEpB,OAAO;GAGT;EACD;EAEA,MAAM,EAAE,IAAI,cAAc;EAC1B,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,SAAS,GAAG;GACxD,MAAM,SAAS,OAAO,eAAe,aAAa,OAAO,MAAM,MAAM;GACrE,MAAM,cAAc,OAAO,UAAU,gBAAgB,KAAK,MAAM;GAChE,IAAI,UAAU,aACb,OAAO;EAET;EACA;CACD,QAAQ;EAEP;CACD;AACD;AAEA,SAAS,MACR,UACgE;CAChE,OAAO,oBAAoB;AAC5B;AAEA,SAAS,UAAU,QAAqC,OAAyB;CAChF,IAAI;EACH,OAAO,OAAO,KAAK,MAAM;CAC1B,QAAQ;EACP,OAAO;CACR;AACD;AAEA,SAAS,UAAU,OAAoC;CACtD,IAAI;EACH,MAAM,UAAU,aAAa,OAAO,SAAS;EAC7C,OAAO,OAAO,YAAY,WAAW,UAAU,KAAA;CAChD,QAAQ;EACP;CACD;AACD;AAEA,SAAS,gBAAgB,OAAoC;CAC5D,MAAM,OAAO,aAAa,OAAO,aAAa;CAC9C,OAAO,OAAO,SAAS,aAAa,KAAK,OAAO,KAAA;AACjD;AAEA,SAAS,aAAa,OAAgB,KAAsB;CAC3D,IAAK,OAAO,UAAU,YAAY,OAAO,UAAU,cAAe,UAAU,MAC3E;CAED,OAAQ,MAAkC;AAC3C;;AChHA,MAAM,8BAAmC,IAAI,IAAI;CAAC;CAAQ;CAAa;AAAM,CAAC;;;;;;;;;;;AAY9E,SAAgB,eACf,WACA,UAAiC,CAAC,GACb;CACrB,MAAM,KAAK,QAAQ,MAAM;CACzB,MAAM,SAAmB,CAAC;CAE1B,IAAI,CAAC,YAAY,IAAI,EAAE,GACtB,OAAO,KAAK,gDAA0C;CAGvD,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,SAAS,GAAG;EACxD,IAAI,QAAQ,IAAI;GACf,OAAO,KAAK,iCAAiC;GAC7C;EACD;EACA,IAAI;GACH,iBAAiB;IAAE,GAAG;IAAU,YAAY,KAAA;GAAU,CAAC;EACxD,SAAS,OAAO;GACf,IAAI,iBAAiB,cACpB,OAAO,KAAK,GAAG,IAAI,IAAI,MAAM,OAAO,KAAK,IAAI,GAAG;QAEhD,MAAM;EAER;CACD;CAEA,IAAI,OAAO,SAAS,GACnB,MAAM,IAAI,aAAa,MAAM;CAG9B,OAAO,OAAO,OAAO;EAAE,WAAW,OAAO,OAAO,EAAE,GAAG,UAAU,CAAC;EAAG;CAAG,CAAC;AACxE;;;AChEA,MAAM,mDAAwD,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAK;AAAG,CAAC;;;;;;AAY/F,SAAgB,WAAW,SAAyB,UAA6B,CAAC,GAAa;CAC9F,MAAM,aAAa,iBAAiB,eAAe,OAAO,CAAC;CAE3D,IAAI,WAAW,SAAS,OAAO,iCAAiC,IAAI,WAAW,MAAM,GACpF,MAAM,IAAI,aAAa,CACtB,UAAU,WAAW,OAAO,2EAC7B,CAAC;CAGF,MAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;CAC3C,QAAQ,IAAI,gBAAgB,oBAAoB;CAEhD,OAAO,IAAI,SAAS,KAAK,UAAU,UAAU,GAAG;EAC/C,QAAQ,WAAW;EACnB;CACD,CAAC;AACF;;AAGA,SAAS,eAAe,OAAqC;CAC5D,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACrE,MAAM,IAAI,aAAa,CAAC,2BAA2B,CAAC;CAErD,MAAM,EAAE,MAAM,OAAO,QAAQ,QAAQ,UAAU,GAAG,eAAe;CACjE,OAAO;EAAE;EAAM;EAAO;EAAQ;EAAQ;EAAU;CAAW;AAC5D"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/sanitize.ts","../src/problem.ts","../src/client.ts","../src/from-error.ts","../src/registry.ts","../src/response.ts"],"sourcesContent":["const WHITESPACE_RUNS = /\\s+/g;\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/g;\n\n/**\n * Makes free text safe for a human-readable problem field: every whitespace run\n * (tabs, line breaks, Unicode line separators) becomes one space, other control\n * characters are removed, surrounding whitespace is trimmed and the result is\n * cut to `maxLength` code points.\n */\nexport function sanitizeText(value: string, maxLength: number): string {\n\tconst cleaned = value.replace(WHITESPACE_RUNS, \" \").replace(CONTROL_CHARS, \"\").trim();\n\treturn truncate(cleaned, maxLength);\n}\n\n/** Cuts a string to at most `maxLength` code points, ending with an ellipsis if cut. */\nexport function truncate(value: string, maxLength: number): string {\n\t// A string never has more code points than code units, so this is a cheap pre-check.\n\tif (value.length <= maxLength) {\n\t\treturn value;\n\t}\n\tconst chars = Array.from(value);\n\tif (chars.length <= maxLength) {\n\t\treturn value;\n\t}\n\treturn `${chars.slice(0, maxLength - 1).join(\"\")}…`;\n}\n","import { sanitizeText } from \"./sanitize.js\";\n\n/** Content-Type for RFC 9457 problem documents. */\nexport const PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\n\n/** Default problem `type` as defined by RFC 9457. */\nexport const DEFAULT_PROBLEM_TYPE = \"about:blank\";\n\nexport const TITLE_MAX_LENGTH = 200;\nexport const DETAIL_MAX_LENGTH = 1000;\nconst URI_MAX_LENGTH = 2048;\n\n/**\n * Whether a response with this status can carry a problem body: 200-599, except\n * 204, 205 and 304, which must not have a body.\n */\nexport function canCarryProblemBody(status: number): boolean {\n\treturn status >= 200 && status !== 204 && status !== 205 && status !== 304;\n}\n\n/** Members defined by RFC 9457. They may not be used as extension keys. */\nexport const RESERVED_MEMBERS: ReadonlySet<string> = new Set([\n\t\"type\",\n\t\"title\",\n\t\"status\",\n\t\"detail\",\n\t\"instance\",\n]);\n\n/**\n * An RFC 9457 problem document. Extension members are flattened next to the\n * standard members, as the specification requires.\n */\nexport interface ProblemDetails {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\t[extension: string]: unknown;\n}\n\n/**\n * Input for `problem()`. Extensions are passed separately so that they can be\n * checked against the reserved members before a document is built.\n */\nexport interface ProblemInput {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n\tinstance?: string;\n\textensions?: Readonly<Record<string, unknown>>;\n}\n\nexport type ProblemErrorCode = \"INVALID_PROBLEM\";\n\n/** Thrown when problem details cannot be built or are invalid. Carries no input values. */\nexport class ProblemError extends Error {\n\treadonly code: ProblemErrorCode = \"INVALID_PROBLEM\";\n\treadonly issues: readonly string[];\n\n\tconstructor(issues: readonly string[]) {\n\t\tsuper(`Invalid problem details: ${issues.join(\"; \")}`);\n\t\tthis.name = \"ProblemError\";\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * Builds a validated problem document. Text fields are sanitized and length\n * limited; invalid input throws a `ProblemError` with code `INVALID_PROBLEM`.\n */\nexport function problem(input: ProblemInput): ProblemDetails {\n\treturn normalizeProblem(input);\n}\n\n/** @internal Validates and normalizes a problem input. Shared by all helpers. */\nexport function normalizeProblem(input: ProblemInput): ProblemDetails {\n\tconst issues: string[] = [];\n\n\tif (!isValidStatus(input.status)) {\n\t\tissues.push(\"status must be an integer between 100 and 599\");\n\t}\n\n\tconst title = typeof input.title === \"string\" ? sanitizeText(input.title, TITLE_MAX_LENGTH) : \"\";\n\tif (title === \"\") {\n\t\tissues.push(\"title must be a non-empty string\");\n\t}\n\n\tconst type =\n\t\tinput.type === undefined ? DEFAULT_PROBLEM_TYPE : uriReference(\"type\", input.type, issues);\n\tconst instance =\n\t\tinput.instance === undefined ? undefined : uriReference(\"instance\", input.instance, issues);\n\tconst detail = optionalText(\"detail\", input.detail, DETAIL_MAX_LENGTH, issues);\n\n\tconst extensions: Record<string, unknown> = {};\n\tif (input.extensions !== undefined) {\n\t\tif (!isRecord(input.extensions)) {\n\t\t\tissues.push(\"extensions must be a plain object\");\n\t\t} else {\n\t\t\tfor (const [key, value] of Object.entries(input.extensions)) {\n\t\t\t\tif (RESERVED_MEMBERS.has(key)) {\n\t\t\t\t\tissues.push(`extension key \"${key}\" collides with a reserved problem member`);\n\t\t\t\t} else if (key === \"__proto__\") {\n\t\t\t\t\tissues.push('extension key \"__proto__\" is not allowed');\n\t\t\t\t} else if (!isJsonSerializable(value)) {\n\t\t\t\t\tissues.push(`extension \"${key}\" must be JSON-serializable`);\n\t\t\t\t} else {\n\t\t\t\t\textensions[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\tconst result: ProblemDetails = { type, title, status: input.status };\n\tif (detail !== undefined) {\n\t\tresult.detail = detail;\n\t}\n\tif (instance !== undefined) {\n\t\tresult.instance = instance;\n\t}\n\tfor (const [key, value] of Object.entries(extensions)) {\n\t\tresult[key] = value;\n\t}\n\treturn result;\n}\n\n/** @internal */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** @internal */\nexport function isValidStatus(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 100 && value <= 599;\n}\n\nconst URI_REFERENCE_FORBIDDEN = /[\\s\\u0000-\\u001f\\u007f]/;\n\nfunction uriReference(name: string, value: unknown, issues: string[]): string {\n\tif (\n\t\ttypeof value !== \"string\" ||\n\t\tvalue.length === 0 ||\n\t\tvalue.length > URI_MAX_LENGTH ||\n\t\tURI_REFERENCE_FORBIDDEN.test(value)\n\t) {\n\t\tissues.push(\n\t\t\t`${name} must be a non-empty URI reference without whitespace or control characters (max ${URI_MAX_LENGTH} characters)`,\n\t\t);\n\t\treturn \"\";\n\t}\n\treturn value;\n}\n\nfunction optionalText(\n\tname: string,\n\tvalue: unknown,\n\tmaxLength: number,\n\tissues: string[],\n): string | undefined {\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\tif (typeof value !== \"string\") {\n\t\tissues.push(`${name} must be a string`);\n\t\treturn undefined;\n\t}\n\tconst text = sanitizeText(value, maxLength);\n\treturn text === \"\" ? undefined : text;\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n\ttry {\n\t\treturn JSON.stringify(value) !== undefined;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","import { isRecord, isValidStatus, type ProblemDetails } from \"./problem.js\";\n\n/** Largest problem body that `parseProblem` will read. */\nconst PARSE_MAX_BYTES = 64 * 1024;\n\n/**\n * Type guard for problem documents: `title` is a non-empty string, `status` is\n * an integer between 100 and 599, and `type`, `detail`, `instance` are strings\n * when present. Extension members are not inspected.\n */\nexport function isProblemDetails(value: unknown): value is ProblemDetails {\n\tif (!isRecord(value)) {\n\t\treturn false;\n\t}\n\tconst { type, title, status, detail, instance } = value;\n\treturn (\n\t\ttypeof title === \"string\" &&\n\t\ttitle.length > 0 &&\n\t\tisValidStatus(status) &&\n\t\t(type === undefined || typeof type === \"string\") &&\n\t\t(detail === undefined || typeof detail === \"string\") &&\n\t\t(instance === undefined || typeof instance === \"string\")\n\t);\n}\n\n/**\n * Reads a problem document from a `Response`. Returns `undefined` unless the\n * media type is `application/problem+json` or `application/json` and the body is\n * a valid problem. Bodies over 64 KiB, malformed JSON and read errors also\n * yield `undefined`; this function never throws.\n *\n * The response body is consumed. Call `response.clone()` first if it is needed later.\n */\nexport async function parseProblem(response: Response): Promise<ProblemDetails | undefined> {\n\ttry {\n\t\tconst mediaType = response.headers.get(\"content-type\")?.split(\";\")[0]?.trim().toLowerCase();\n\t\tif (mediaType !== \"application/problem+json\" && mediaType !== \"application/json\") {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst declared = Number(response.headers.get(\"content-length\"));\n\t\tif (Number.isFinite(declared) && declared > PARSE_MAX_BYTES) {\n\t\t\tawait response.body?.cancel();\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst bytes = await readLimited(response.body, PARSE_MAX_BYTES);\n\t\tif (bytes === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst parsed: unknown = JSON.parse(new TextDecoder().decode(bytes));\n\t\treturn isProblemDetails(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Reads a stream up to `limit` bytes. Resolves to `undefined` if the limit is exceeded. */\nasync function readLimited(\n\tbody: ReadableStream<Uint8Array> | null,\n\tlimit: number,\n): Promise<Uint8Array | undefined> {\n\tif (body === null) {\n\t\treturn undefined;\n\t}\n\tconst reader = body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) {\n\t\t\tbreak;\n\t\t}\n\t\ttotal += value.byteLength;\n\t\tif (total > limit) {\n\t\t\tawait reader.cancel();\n\t\t\treturn undefined;\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst bytes = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tbytes.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn bytes;\n}\n","import { normalizeProblem, type ProblemDetails } from \"./problem.js\";\nimport type { ProblemRegistry, ProblemTemplate } from \"./registry.js\";\nimport { truncate } from \"./sanitize.js\";\n\n/** A class used as a registry key in `fromError`. Matched with `instanceof`. */\nexport type ErrorClass = abstract new (...args: never[]) => unknown;\n\nexport interface FromErrorOptions {\n\t/**\n\t * Opt-in for unknown errors. Only when this returns `true` is the error\n\t * message copied into `detail` (sanitized and length limited).\n\t */\n\texpose?: (error: unknown) => boolean;\n\t/**\n\t * Known errors. A `ProblemRegistry` from `defineProblems`, or a `Map` whose\n\t * string keys match `error.code` and whose class keys match with `instanceof`.\n\t * The first matching entry wins. Matched errors never expose their message.\n\t */\n\tregistry?: ProblemRegistry | ReadonlyMap<ErrorClass | string, ProblemTemplate>;\n\t/** Template for unknown errors. Defaults to status 500 and a generic title. */\n\tfallback?: ProblemTemplate;\n\t/** Request identifier. When set, `instance` becomes `urn:request:<id>`. */\n\tinstanceFromRequestId?: string;\n}\n\nconst DEFAULT_FALLBACK: ProblemTemplate = { status: 500, title: \"Internal Server Error\" };\nconst REQUEST_ID_MAX_LENGTH = 128;\n\n/**\n * Maps any thrown value to a problem document without leaking internals.\n *\n * Unknown errors produce only the fallback (by default status 500 and a generic\n * title). Their `message`, `stack`, `cause` and `name` are never included unless\n * `expose(error)` returns `true`.\n */\nexport function fromError(error: unknown, options: FromErrorOptions = {}): ProblemDetails {\n\tconst instance = instanceFor(options.instanceFromRequestId);\n\n\tconst matched = findTemplate(error, options.registry);\n\tif (matched !== undefined) {\n\t\treturn normalizeProblem({\n\t\t\ttype: matched.type,\n\t\t\ttitle: matched.title,\n\t\t\tstatus: matched.status,\n\t\t\tdetail: matched.detail,\n\t\t\tinstance,\n\t\t});\n\t}\n\n\tconst fallback = options.fallback ?? DEFAULT_FALLBACK;\n\tconst exposed = options.expose !== undefined && isExposed(options.expose, error);\n\treturn normalizeProblem({\n\t\ttype: fallback.type,\n\t\ttitle: fallback.title,\n\t\tstatus: fallback.status,\n\t\tdetail: exposed ? messageOf(error) : fallback.detail,\n\t\tinstance,\n\t});\n}\n\nfunction instanceFor(requestId: string | undefined): string | undefined {\n\tif (requestId === undefined || requestId === \"\") {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn `urn:request:${encodeURIComponent(truncate(requestId, REQUEST_ID_MAX_LENGTH))}`;\n\t} catch {\n\t\t// Lone surrogates cannot be encoded; an unusable request id is dropped, not thrown.\n\t\treturn undefined;\n\t}\n}\n\nfunction findTemplate(\n\terror: unknown,\n\tregistry: FromErrorOptions[\"registry\"],\n): ProblemTemplate | undefined {\n\tif (registry === undefined) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\tif (isMap(registry)) {\n\t\t\tfor (const [matcher, template] of registry) {\n\t\t\t\tconst hit =\n\t\t\t\t\ttypeof matcher === \"string\"\n\t\t\t\t\t\t? readProperty(error, \"code\") === matcher\n\t\t\t\t\t\t: error instanceof matcher;\n\t\t\t\tif (hit) {\n\t\t\t\t\treturn template;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst { by, templates } = registry;\n\t\tfor (const [key, template] of Object.entries(templates)) {\n\t\t\tconst byCode = by !== \"className\" && readProperty(error, \"code\") === key;\n\t\t\tconst byClassName = by !== \"code\" && constructorName(error) === key;\n\t\t\tif (byCode || byClassName) {\n\t\t\t\treturn template;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\t// A throwing getter or hostile matcher must not change the response shape.\n\t\treturn undefined;\n\t}\n}\n\nfunction isMap(\n\tregistry: NonNullable<FromErrorOptions[\"registry\"]>,\n): registry is ReadonlyMap<ErrorClass | string, ProblemTemplate> {\n\treturn registry instanceof Map;\n}\n\nfunction isExposed(expose: (error: unknown) => boolean, error: unknown): boolean {\n\ttry {\n\t\treturn expose(error) === true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction messageOf(error: unknown): string | undefined {\n\ttry {\n\t\tconst message = readProperty(error, \"message\");\n\t\treturn typeof message === \"string\" ? message : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction constructorName(value: unknown): string | undefined {\n\tconst ctor = readProperty(value, \"constructor\");\n\treturn typeof ctor === \"function\" ? ctor.name : undefined;\n}\n\nfunction readProperty(value: unknown, key: string): unknown {\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\treturn undefined;\n\t}\n\treturn (value as Record<string, unknown>)[key];\n}\n","import { canCarryProblemBody, normalizeProblem, ProblemError } from \"./problem.js\";\n\n/** A problem definition that can be reused for errors of a known kind. */\nexport interface ProblemTemplate {\n\ttype?: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail?: string;\n}\n\nexport type ProblemTemplates = Readonly<Record<string, ProblemTemplate>>;\n\n/**\n * How registry keys are matched against errors:\n * - `code`: `error.code === key` (default, survives minification)\n * - `className`: `error.constructor.name === key`\n * - `both`: either of the two\n */\nexport type MatchBy = \"code\" | \"className\" | \"both\";\n\nexport interface DefineProblemsOptions {\n\tby?: MatchBy;\n}\n\nexport interface ProblemRegistry<T extends ProblemTemplates = ProblemTemplates> {\n\treadonly templates: T;\n\treadonly by: MatchBy;\n}\n\nconst MATCH_MODES: ReadonlySet<string> = new Set([\"code\", \"className\", \"both\"]);\n\n/**\n * Defines named problem templates for `fromError`. Every template is validated\n * immediately, so invalid definitions fail at startup instead of per request.\n *\n * @example\n * const problems = defineProblems({\n * NotFound: { status: 404, title: \"Not found\", type: \"https://example.com/problems/not-found\" },\n * });\n * fromError(error, { registry: problems });\n */\nexport function defineProblems<const T extends ProblemTemplates>(\n\ttemplates: T,\n\toptions: DefineProblemsOptions = {},\n): ProblemRegistry<T> {\n\tconst by = options.by ?? \"code\";\n\tconst issues: string[] = [];\n\n\tif (!MATCH_MODES.has(by)) {\n\t\tissues.push('by must be \"code\", \"className\" or \"both\"');\n\t}\n\n\tfor (const [key, template] of Object.entries(templates)) {\n\t\tif (key === \"\") {\n\t\t\tissues.push(\"template keys must be non-empty\");\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tnormalizeProblem({ ...template, extensions: undefined });\n\t\t\tif (!canCarryProblemBody(template.status)) {\n\t\t\t\tissues.push(`${key}: status ${template.status} cannot carry a problem body`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof ProblemError) {\n\t\t\t\tissues.push(`${key}: ${error.issues.join(\"; \")}`);\n\t\t\t} else {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length > 0) {\n\t\tthrow new ProblemError(issues);\n\t}\n\n\treturn Object.freeze({ templates: Object.freeze({ ...templates }), by });\n}\n","import {\n\tcanCarryProblemBody,\n\tnormalizeProblem,\n\tPROBLEM_CONTENT_TYPE,\n\ttype ProblemDetails,\n\tProblemError,\n\ttype ProblemInput,\n} from \"./problem.js\";\n\nexport interface ToResponseOptions {\n\t/** Extra headers. The Content-Type is always set to the problem media type. */\n\theaders?: HeadersInit;\n}\n\n/**\n * Creates a Web-standard `Response` carrying the problem as\n * `application/problem+json; charset=utf-8`. The problem is validated and\n * sanitized again before it is serialized.\n */\nexport function toResponse(problem: ProblemDetails, options: ToResponseOptions = {}): Response {\n\tconst normalized = normalizeProblem(toProblemInput(problem));\n\n\tif (!canCarryProblemBody(normalized.status)) {\n\t\tthrow new ProblemError([\n\t\t\t`status ${normalized.status} cannot be sent with a problem body (use 200-599, except 204, 205 and 304)`,\n\t\t]);\n\t}\n\n\tconst headers = new Headers(options.headers);\n\theaders.set(\"content-type\", PROBLEM_CONTENT_TYPE);\n\n\treturn new Response(JSON.stringify(normalized), {\n\t\tstatus: normalized.status,\n\t\theaders,\n\t});\n}\n\n/** Converts the flattened wire shape into the input shape used for validation. */\nfunction toProblemInput(value: ProblemDetails): ProblemInput {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new ProblemError([\"problem must be an object\"]);\n\t}\n\tconst { type, title, status, detail, instance, ...extensions } = value;\n\treturn { type, title, status, detail, instance, extensions };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,86 +1,84 @@
1
1
  {
2
- "name": "@yedoma-labs/ysuly-problem",
3
- "version": "1.0.0",
4
- "description": "Type-safe RFC 9457 Problem Details with safe error mapping, zero dependencies",
5
- "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "import": {
12
- "types": "./dist/index.d.ts",
13
- "default": "./dist/index.js"
14
- },
15
- "require": {
16
- "types": "./dist/index.d.cts",
17
- "default": "./dist/index.cjs"
18
- }
19
- }
20
- },
21
- "files": [
22
- "dist"
23
- ],
24
- "engines": {
25
- "node": ">=20"
26
- },
27
- "packageManager": "pnpm@12.15.0",
28
- "scripts": {
29
- "build": "tsdown",
30
- "test": "vitest run",
31
- "test:watch": "vitest",
32
- "test:coverage": "vitest run --coverage",
33
- "lint": "biome check src tests",
34
- "lint:fix": "biome check --write src tests",
35
- "format": "biome format --write src tests",
36
- "typecheck": "tsc --noEmit",
37
- "prepublishOnly": "pnpm run build"
38
- },
39
- "keywords": [
40
- "problem-details",
41
- "rfc9457",
42
- "rfc7807",
43
- "problem+json",
44
- "error-handling",
45
- "api",
46
- "typescript",
47
- "zero-dependency"
48
- ],
49
- "repository": {
50
- "type": "git",
51
- "url": "https://github.com/yedoma-labs/ysuly-problem"
52
- },
53
- "author": "",
54
- "license": "MIT",
55
- "simple-git-hooks": {
56
- "pre-commit": "pnpm exec lint-staged",
57
- "pre-push": "pnpm run typecheck && pnpm run test",
58
- "commit-msg": "pnpm exec commitlint --edit $1"
59
- },
60
- "lint-staged": {
61
- "*.ts": [
62
- "biome check --write"
63
- ]
64
- },
65
- "devDependencies": {
66
- "@biomejs/biome": "2.6.3",
67
- "@commitlint/cli": "^21.2.4",
68
- "@commitlint/config-conventional": "^21.2.4",
69
- "@semantic-release/changelog": "^7.0.0",
70
- "@semantic-release/commit-analyzer": "^13.0.1",
71
- "@semantic-release/exec": "^7.1.0",
72
- "@semantic-release/git": "^11.0.1",
73
- "@semantic-release/github": "^12.0.10",
74
- "@semantic-release/npm": "^13.2.0",
75
- "@semantic-release/release-notes-generator": "^14.1.1",
76
- "@types/node": "26.6.5",
77
- "@typescript/typescript6": "^6.0.2",
78
- "@vitest/coverage-v8": "^5.0.5",
79
- "lint-staged": "^17.6.0",
80
- "semantic-release": "^25.0.9",
81
- "simple-git-hooks": "^2.14.0",
82
- "tsdown": "^0.23.2",
83
- "typescript": "7.0.2",
84
- "vitest": "5.0.5"
85
- }
86
- }
2
+ "name": "@yedoma-labs/ysuly-problem",
3
+ "version": "1.0.1",
4
+ "description": "Type-safe RFC 9457 Problem Details with safe error mapping, zero dependencies",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "problem-details",
29
+ "rfc9457",
30
+ "rfc7807",
31
+ "problem+json",
32
+ "error-handling",
33
+ "api",
34
+ "typescript",
35
+ "zero-dependency"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/yedoma-labs/ysuly-problem"
40
+ },
41
+ "author": "",
42
+ "license": "MIT",
43
+ "simple-git-hooks": {
44
+ "pre-commit": "pnpm exec lint-staged",
45
+ "pre-push": "pnpm run typecheck && pnpm run test",
46
+ "commit-msg": "pnpm exec commitlint --edit $1"
47
+ },
48
+ "lint-staged": {
49
+ "*.ts": [
50
+ "biome check --write"
51
+ ]
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "2.6.3",
55
+ "@commitlint/cli": "^21.2.4",
56
+ "@commitlint/config-conventional": "^21.2.4",
57
+ "@semantic-release/changelog": "^7.0.0",
58
+ "@semantic-release/commit-analyzer": "^13.0.1",
59
+ "@semantic-release/exec": "^7.1.0",
60
+ "@semantic-release/git": "^11.0.1",
61
+ "@semantic-release/github": "^12.0.10",
62
+ "@semantic-release/npm": "^13.2.0",
63
+ "@semantic-release/release-notes-generator": "^14.1.1",
64
+ "@types/node": "26.6.5",
65
+ "@typescript/typescript6": "^6.0.2",
66
+ "@vitest/coverage-v8": "^5.0.5",
67
+ "lint-staged": "^17.6.0",
68
+ "semantic-release": "^25.0.9",
69
+ "simple-git-hooks": "^2.14.0",
70
+ "tsdown": "^0.23.2",
71
+ "typescript": "7.0.2",
72
+ "vitest": "5.0.5"
73
+ },
74
+ "scripts": {
75
+ "build": "tsdown",
76
+ "test": "vitest run",
77
+ "test:watch": "vitest",
78
+ "test:coverage": "vitest run --coverage",
79
+ "lint": "biome check src tests",
80
+ "lint:fix": "biome check --write src tests",
81
+ "format": "biome format --write src tests",
82
+ "typecheck": "tsc --noEmit"
83
+ }
84
+ }