@shirudo/ddd-kit 2.2.0 → 3.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.js","names":["CACHE_LIMIT"],"sources":["../src/money/errors.ts","../src/money/money.ts","../src/money/arithmetic.ts","../src/money/parse.ts","../src/money/factory.ts","../src/money/format.ts","../src/money/snapshot.ts","../src/money/try-parse.ts"],"sourcesContent":["import { DomainError } from \"../core/errors\";\n\n/**\n * Renders a value for a diagnostic message with a hard size ceiling:\n * error messages must never carry attacker-sized payloads into logs.\n * Total: never throws, whatever the value (JSON.stringify alone would\n * crash on bigint and circular input, replacing the documented kit\n * error with a raw TypeError). Module-internal, not part of the\n * package entry.\n */\nexport function describeValue(value: unknown): string {\n\tif (typeof value === \"bigint\") {\n\t\tconst digits = value.toString();\n\t\treturn digits.length > 48\n\t\t\t? `${digits.slice(0, 48)}... (truncated bigint)`\n\t\t\t: `${digits}n`;\n\t}\n\tif (typeof value === \"string\" && value.length > 48) {\n\t\treturn `${JSON.stringify(value.slice(0, 48))} (truncated, ${value.length} chars)`;\n\t}\n\tlet rendered: string | undefined;\n\ttry {\n\t\trendered = JSON.stringify(value);\n\t} catch {\n\t\trendered = undefined;\n\t}\n\tif (rendered === undefined) {\n\t\ttry {\n\t\t\trendered = String(value);\n\t\t} catch {\n\t\t\trendered = `[unserializable ${typeof value}]`;\n\t\t}\n\t}\n\tif (rendered.length > 64) {\n\t\treturn `${rendered.slice(0, 64)} (truncated)`;\n\t}\n\treturn rendered;\n}\n\nexport class InvalidMoneyError extends DomainError<\"INVALID_MONEY\"> {\n\tconstructor(message: string) {\n\t\tsuper({ code: \"INVALID_MONEY\", message });\n\t}\n}\n\nexport class MoneyCurrencyMismatchError extends DomainError<\"MONEY_CURRENCY_MISMATCH\"> {\n\tconstructor(left: string, right: string) {\n\t\tsuper({\n\t\t\tcode: \"MONEY_CURRENCY_MISMATCH\",\n\t\t\tmessage: `money operations require the same currency; got ${describeValue(left)} and ${describeValue(right)}`,\n\t\t});\n\t}\n}\n\nexport class MoneyScaleMismatchError extends DomainError<\"MONEY_SCALE_MISMATCH\"> {\n\tconstructor(left: number, right: number) {\n\t\tsuper({\n\t\t\tcode: \"MONEY_SCALE_MISMATCH\",\n\t\t\tmessage: `money operations require the same scale; got ${left} and ${right} (rescaleMoney is the explicit conversion)`,\n\t\t});\n\t}\n}\n\nexport class MoneyPrecisionLossError extends DomainError<\"MONEY_PRECISION_LOSS\"> {\n\tconstructor(message: string) {\n\t\tsuper({ code: \"MONEY_PRECISION_LOSS\", message });\n\t}\n}\n\nexport class UnknownCurrencyError extends DomainError<\"UNKNOWN_CURRENCY\"> {\n\tconstructor(currency: string) {\n\t\tsuper({\n\t\t\tcode: \"UNKNOWN_CURRENCY\",\n\t\t\tmessage: `The currency scale resolver has no entry for ${describeValue(currency)}`,\n\t\t});\n\t}\n}\n","import { describeValue, InvalidMoneyError } from \"./errors\";\n\n/**\n * Currency identifier for {@link Money}. The kit deliberately ships no\n * currency table: which codes exist and which scale they use is the\n * consumer's decision (see `createMoneyFactory` for wiring a resolver\n * once). ISO 4217 alpha-3 codes are the recommended convention.\n */\nexport type CurrencyCode = string;\n\n/**\n * The canonical money representation for domain state, domain events,\n * and snapshots: an exact integer amount in minor units plus the\n * explicit scale that maps minor to major units.\n *\n * Plain data by design: values created by `moneyOfMinor` are frozen,\n * carry no methods or library internals, and survive `structuredClone`,\n * deep-freeze, and the kit's state diffing. The kit ships exact\n * operations only (lossless `addMoney`/`subtractMoney`/`negateMoney`\n * and lossless `rescaleMoney`); everything that carries a rounding or\n * distribution policy (multiplication, ratios, fees, division, lossy\n * rescaling, allocation, FX) is domain policy executed by your\n * calculation library at the use-case boundary (see\n * `moneyFromSnapshot` / `moneyToSnapshot`).\n *\n * Never `number`, never a decimal string, never `amountCents`: `10.99`\n * EUR is `{ amountMinor: 1099n, currency: \"EUR\", scale: 2 }`, and JPY\n * has scale 0, not an assumed 2.\n */\nexport interface Money {\n\t/**\n\t * Exact integer amount in minor units AT THIS VALUE'S SCALE. The\n\t * scale may deliberately differ from the currency's default\n\t * exponent (intermediate precision; the snapshot interop honors\n\t * the same distinction), so the reference unit is always\n\t * `this.scale`, never an assumed per-currency constant.\n\t */\n\treadonly amountMinor: bigint;\n\t/** Required currency identifier; operations never mix currencies. */\n\treadonly currency: CurrencyCode;\n\t/** Number of minor-unit digits per major unit (EUR 2, JPY 0). */\n\treadonly scale: number;\n\t/**\n\t * Type-level brand: a NON-EXPORTED unique symbol, never present at\n\t * runtime. A string key (\"__brand\") could be spelled out by any\n\t * caller; the module-private symbol cannot, so only the module's\n\t * constructors mint the type. Convert foreign plain shapes with\n\t * `moneyFromUnknown` (validates, copies, freezes) or `moneyFromDto`.\n\t */\n\treadonly [MONEY_BRAND]: true;\n}\n\ndeclare const MONEY_BRAND: unique symbol;\n\n/**\n * Wire shape for {@link Money}: `amountMinor` travels as a string\n * because JSON numbers are floats (silent precision loss past 2^53) and\n * `JSON.stringify` throws on bigint. Validate with `moneyFromDto`\n * immediately after deserialization; emit with `moneyToDto` right\n * before serialization.\n *\n * A type alias, not an interface, on purpose: only type aliases get the\n * implicit index signature that makes the DTO assignable to `JsonValue`,\n * which `PublishedCommand` payloads require.\n */\nexport type MoneyDto = {\n\t/** Integer string matching `/^-?\\d+$/`. */\n\treadonly amountMinor: string;\n\treadonly currency: string;\n\treadonly scale: number;\n};\n\nconst INTEGER_STRING = /^-?\\d+$/;\n\n/**\n * Hard bounds, enforced at the single construction door so hostile\n * input cannot buy unbounded CPU (BigInt conversion is superlinear in\n * digit count) or memory (cache keys, padded strings, log echoes):\n *\n * - amounts: fewer than 97 digits (uint256 is 78 digits; headroom)\n * - scale: at most 64 (ETH wei is 18)\n * - currency: at most 32 characters (ISO 4217 needs 3)\n *\n * Everything past a bound is `INVALID_MONEY` by construction, and the\n * DTO/parse boundaries reject oversized strings before converting them.\n */\nconst MONEY_AMOUNT_LIMIT = 10n ** 96n;\nconst MAX_MONEY_SCALE = 64;\nconst MAX_CURRENCY_LENGTH = 32;\nconst MAX_DTO_AMOUNT_LENGTH = 97; // sign + 96 digits\n\nfunction isValidAmount(amountMinor: unknown): amountMinor is bigint {\n\treturn (\n\t\ttypeof amountMinor === \"bigint\" &&\n\t\tamountMinor < MONEY_AMOUNT_LIMIT &&\n\t\tamountMinor > -MONEY_AMOUNT_LIMIT\n\t);\n}\n\nfunction isValidCurrency(currency: unknown): currency is string {\n\treturn (\n\t\ttypeof currency === \"string\" &&\n\t\tcurrency.length > 0 &&\n\t\tcurrency.length <= MAX_CURRENCY_LENGTH &&\n\t\t!/\\s/.test(currency)\n\t);\n}\n\n/**\n * Scale guard for operations that must validate a TARGET scale before\n * computing with it (10^scale on an unchecked value is an attack\n * surface). Module-internal export, not part of the package entry.\n */\nexport function assertValidScale(scale: number): void {\n\tif (!isValidScale(scale)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`scale must be an integer between 0 and ${MAX_MONEY_SCALE}; got ${describeValue(scale)}`,\n\t\t);\n\t}\n}\n\n/**\n * Currency counterpart to {@link assertValidScale}, for call sites\n * that must reject a wiring-provided currency before any input work.\n * Module-internal export, not part of the package entry.\n */\nexport function assertValidCurrency(currency: unknown): void {\n\tif (!isValidCurrency(currency)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`currency must be a non-empty string without whitespace, at most ${MAX_CURRENCY_LENGTH} characters; got ${describeValue(currency)}`,\n\t\t);\n\t}\n}\n\n/**\n * Validates an unknown plain shape and mints a FRESH, frozen\n * {@link Money} from it: the result shares no reference with the\n * input, so later mutation of the input cannot reach domain state.\n * The door for re-hydrating foreign minor-units data (rows already\n * mapped by an ORM, caches, deserialized snapshots); wire strings go\n * through `moneyFromDto` instead.\n */\nexport function moneyFromUnknown(value: unknown): Money {\n\tassertMoney(value);\n\treturn moneyOfMinor(value.amountMinor, value.currency, value.scale);\n}\n\nfunction isValidScale(scale: unknown): scale is number {\n\treturn (\n\t\ttypeof scale === \"number\" &&\n\t\tNumber.isSafeInteger(scale) &&\n\t\tscale >= 0 &&\n\t\tscale <= MAX_MONEY_SCALE\n\t);\n}\n\n/**\n * Constructs a frozen {@link Money} from an exact minor-unit amount.\n * The only door into the shape: rejects `number` amounts (floats have\n * no place in stored money), invalid scales, and empty or\n * whitespace-carrying currency codes with `InvalidMoneyError`.\n */\nexport function moneyOfMinor(\n\tamountMinor: bigint,\n\tcurrency: CurrencyCode,\n\tscale: number,\n): Money {\n\tif (typeof amountMinor !== \"bigint\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`amountMinor must be a bigint in minor units; got ${typeof amountMinor}`,\n\t\t);\n\t}\n\tif (!isValidAmount(amountMinor)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"amountMinor must stay below 97 digits (uint256 fits with headroom)\",\n\t\t);\n\t}\n\tassertValidCurrency(currency);\n\tassertValidScale(scale);\n\treturn Object.freeze({ amountMinor, currency, scale }) as Money;\n}\n\n/**\n * Validates a {@link MoneyDto} fresh off the wire and converts it to\n * {@link Money}. Takes `unknown` on purpose: this IS the trust\n * boundary, so callers never cast before validating. `amountMinor`\n * must be a plain integer string (`/^-?\\d+$/`); anything `Number()`\n * would tolerate but bigint arithmetic cannot represent exactly\n * (\"1e5\", \"10.99\", \"0x10\") is rejected with `InvalidMoneyError`.\n */\nexport function moneyFromDto(dto: unknown): Money {\n\tif (dto === null || typeof dto !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`MoneyDto must be an object; got ${dto === null ? \"null\" : typeof dto}`,\n\t\t);\n\t}\n\tconst { amountMinor, currency, scale } = dto as Partial<\n\t\tRecord<keyof MoneyDto, unknown>\n\t>;\n\t// The length ceiling runs BEFORE BigInt(): converting a hostile\n\t// multi-megabyte digit string is superlinear CPU.\n\tif (\n\t\ttypeof amountMinor !== \"string\" ||\n\t\tamountMinor.length > MAX_DTO_AMOUNT_LENGTH ||\n\t\t!INTEGER_STRING.test(amountMinor)\n\t) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`MoneyDto.amountMinor must be an integer string matching /^-?\\\\d+$/ with at most 96 digits; got ${describeValue(amountMinor)}`,\n\t\t);\n\t}\n\t// moneyOfMinor validates currency and scale.\n\treturn moneyOfMinor(\n\t\tBigInt(amountMinor),\n\t\tcurrency as CurrencyCode,\n\t\tscale as number,\n\t);\n}\n\n/**\n * Converts {@link Money} to its JSON-safe wire shape. Guards the input\n * so untyped callers cannot leak a number-amount object onto the wire.\n */\nexport function moneyToDto(money: Money): MoneyDto {\n\tassertMoney(money);\n\treturn {\n\t\tamountMinor: money.amountMinor.toString(),\n\t\tcurrency: money.currency,\n\t\tscale: money.scale,\n\t};\n}\n\n/**\n * Narrows an unknown value to the canonical {@link Money} shape. A\n * CHECK, not a door: narrowing neither copies nor freezes, so an\n * external alias can still mutate the underlying object after the\n * check. For anything entering domain state, mint a fresh frozen\n * value with {@link moneyFromUnknown} instead.\n */\nexport function isMoney(value: unknown): value is Money {\n\tif (value === null || typeof value !== \"object\") return false;\n\tconst candidate = value as Partial<Money>;\n\treturn (\n\t\tisValidAmount(candidate.amountMinor) &&\n\t\tisValidCurrency(candidate.currency) &&\n\t\tisValidScale(candidate.scale)\n\t);\n}\n\n/**\n * Loud form of {@link isMoney} for boundary functions. Module-internal\n * export, not part of the package entry.\n */\nexport function assertMoney(value: unknown): asserts value is Money {\n\tif (!isMoney(value)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"expected a Money value ({ amountMinor: bigint, currency: string, scale: number })\",\n\t\t);\n\t}\n}\n\n/**\n * REPRESENTATION equality, deliberately: amount, currency, AND scale.\n * `10.0` EUR at scale 1 and `10.00` EUR at scale 2 denote the same\n * monetary value but are NOT equal here, because silently conflating\n * scales is how precision bugs hide. For monetary-value comparison,\n * align the scales explicitly first (lossless `rescaleMoney` upscales\n * the coarser side) and then compare.\n */\nexport function moneyEquals(a: Money, b: Money): boolean {\n\treturn (\n\t\ta.amountMinor === b.amountMinor &&\n\t\ta.currency === b.currency &&\n\t\ta.scale === b.scale\n\t);\n}\n\n/** True when the amount is exactly zero. */\nexport function isZeroMoney(money: Money): boolean {\n\treturn money.amountMinor === 0n;\n}\n\n/** True when the amount is strictly greater than zero. */\nexport function isPositiveMoney(money: Money): boolean {\n\treturn money.amountMinor > 0n;\n}\n\n/** True when the amount is strictly less than zero. */\nexport function isNegativeMoney(money: Money): boolean {\n\treturn money.amountMinor < 0n;\n}\n\n/**\n * Renders the exact decimal representation (\"10.99\", \"-0.05\", \"10\").\n * The inverse of `parseMoneyInput` and the precision-safe input for\n * display formatting; never feed the result back into arithmetic.\n * Guards its input like the wire emitters do: a non-Money value fails\n * loudly instead of rendering garbage.\n */\nexport function moneyToDecimalString(money: Money): string {\n\tassertMoney(money);\n\tconst negative = money.amountMinor < 0n;\n\tconst digits = (negative ? -money.amountMinor : money.amountMinor).toString();\n\tconst sign = negative ? \"-\" : \"\";\n\tif (money.scale === 0) return sign + digits;\n\tconst padded = digits.padStart(money.scale + 1, \"0\");\n\tconst whole = padded.slice(0, -money.scale);\n\tconst fraction = padded.slice(-money.scale);\n\treturn `${sign}${whole}.${fraction}`;\n}\n","import {\n\tMoneyCurrencyMismatchError,\n\tMoneyPrecisionLossError,\n\tMoneyScaleMismatchError,\n} from \"./errors\";\nimport { assertValidScale, type Money, moneyOfMinor } from \"./money\";\n\n/**\n * Exact operations only. Everything here is closed (Money in, Money\n * out) and cannot lose information, so no rounding decision exists to\n * make. Anything that WOULD need one (multiplication, ratios, fees,\n * division, lossy rescaling) plus everything distributive (allocation:\n * splitting 10.00 EUR three ways must hand out 3.34 + 3.33 + 3.33, not\n * round three times) and rate-based (FX) is deliberately NOT\n * implemented by the kit: rounding timing, order, and remainder policy\n * are domain policy, and a battle-tested calculation library should\n * execute them at the use-case boundary (see the snapshot bridge). The\n * kit NEVER rounds; even over-precise parse input is rejected instead\n * of rounded.\n */\nfunction assertSameUnit(a: Money, b: Money): void {\n\tif (a.currency !== b.currency) {\n\t\tthrow new MoneyCurrencyMismatchError(a.currency, b.currency);\n\t}\n\tif (a.scale !== b.scale) {\n\t\tthrow new MoneyScaleMismatchError(a.scale, b.scale);\n\t}\n}\n\n/**\n * Exact addition of same-currency, same-scale amounts. Mismatches\n * throw (`MONEY_CURRENCY_MISMATCH` / `MONEY_SCALE_MISMATCH`); there is\n * no implicit conversion of either. A result past the amount bound\n * fails as `INVALID_MONEY` instead of wrapping.\n */\nexport function addMoney(a: Money, b: Money): Money {\n\tassertSameUnit(a, b);\n\treturn moneyOfMinor(a.amountMinor + b.amountMinor, a.currency, a.scale);\n}\n\n/** Exact subtraction under the same guards as {@link addMoney}. */\nexport function subtractMoney(a: Money, b: Money): Money {\n\tassertSameUnit(a, b);\n\treturn moneyOfMinor(a.amountMinor - b.amountMinor, a.currency, a.scale);\n}\n\n/** Exact sign flip; useful for ledger reversals and refunds. */\nexport function negateMoney(money: Money): Money {\n\treturn moneyOfMinor(-money.amountMinor, money.currency, money.scale);\n}\n\n/**\n * Converts to another scale, LOSSLESSLY or not at all: upscaling and\n * exact downscaling succeed; a downscale that would drop non-zero\n * digits throws `MONEY_PRECISION_LOSS`. There is deliberately no\n * rounding parameter; lossy conversions carry a rounding policy and\n * belong to your calculation library. The intended use is aligning\n * mixed-scale amounts for `addMoney`/`subtractMoney` by upscaling the\n * coarser one.\n */\nexport function rescaleMoney(money: Money, scale: number): Money {\n\t// Validated BEFORE 10^(scale diff): an unchecked target scale is\n\t// an amplification vector.\n\tassertValidScale(scale);\n\tif (scale === money.scale) return money;\n\tif (scale > money.scale) {\n\t\treturn moneyOfMinor(\n\t\t\tmoney.amountMinor * 10n ** BigInt(scale - money.scale),\n\t\t\tmoney.currency,\n\t\t\tscale,\n\t\t);\n\t}\n\tconst factor = 10n ** BigInt(money.scale - scale);\n\tif (money.amountMinor % factor !== 0n) {\n\t\tthrow new MoneyPrecisionLossError(\n\t\t\t`rescaling from scale ${money.scale} to ${scale} loses precision; lossy conversions belong to your calculation library, where the rounding policy is explicit`,\n\t\t);\n\t}\n\treturn moneyOfMinor(money.amountMinor / factor, money.currency, scale);\n}\n","import {\n\tdescribeValue,\n\tInvalidMoneyError,\n\tMoneyPrecisionLossError,\n} from \"./errors\";\nimport {\n\tassertValidScale,\n\ttype CurrencyCode,\n\ttype Money,\n\tmoneyOfMinor,\n} from \"./money\";\n\n/** Options for {@link parseMoneyInput}. */\nexport interface ParseMoneyInputOptions {\n\treadonly currency: CurrencyCode;\n\t/** Target scale; the kit ships no currency table, so it is explicit. */\n\treadonly scale: number;\n}\n\nconst DECIMAL_INPUT = /^(-?)(\\d+)(?:\\.(\\d+))?$/;\n\n// Rejected before the regex and long before BigInt(): a hostile\n// multi-megabyte \"amount\" must cost O(1), not superlinear conversion\n// work. 96 amount digits + 64 fraction digits + sign + dot fit easily.\nconst MAX_INPUT_LENGTH = 256;\n\n/**\n * Parses a plain decimal string (\"10.99\") into exact minor units,\n * without ever touching floating point. This is the safe replacement\n * for the classic bugs `Number(input) * 100` and `parseFloat`: no\n * exponents, no `Infinity`, no locale separators, and NO rounding; the\n * kit never rounds.\n *\n * EXACT OR REJECTED: missing fraction digits pad losslessly (\"10.5\" at\n * scale 2 is 1050n) and all-zero excess digits are accepted (\"10.990\"\n * at scale 2 is 1099n), but input that cannot be represented exactly\n * at the target scale throws `MONEY_PRECISION_LOSS`. Whether \"10.999\"\n * should be rejected or become 11.00 is a BUSINESS decision, not a\n * parsing feature: put it in a domain-named policy function (a\n * `normalizeQuotedPrice`, a `calculateVat`) that rounds via your\n * calculation library and returns `Money`.\n *\n * The grammar is deliberately strict (`/^-?\\d+(\\.\\d+)?$/`). Locale\n * input (\"10,99\", grouping, currency signs) is a UI concern; normalize\n * it to this grammar before calling.\n *\n * Takes `unknown` on purpose: this is the trust boundary for raw\n * request values, so callers pass `req.body.amount` directly instead\n * of coercing (`String([...])` silently joins arrays) or casting.\n */\nexport function parseMoneyInput(\n\tinput: unknown,\n\toptions: ParseMoneyInputOptions,\n): Money {\n\tconst { currency, scale } = options;\n\t// Validated BEFORE padEnd/BigInt: an out-of-range scale (e.g. from\n\t// a buggy resolver) must fail fast as INVALID_MONEY, not buy\n\t// superlinear conversion work or a raw RangeError first.\n\tassertValidScale(scale);\n\tif (typeof input !== \"string\" || input.length > MAX_INPUT_LENGTH) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money input must be a decimal string of at most ${MAX_INPUT_LENGTH} characters; got ${describeValue(input)}`,\n\t\t);\n\t}\n\tconst match = DECIMAL_INPUT.exec(input);\n\tif (!match) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money input must be a plain decimal string matching /^-?\\\\d+(\\\\.\\\\d+)?$/; got ${describeValue(input)}`,\n\t\t);\n\t}\n\tconst sign = match[1] ?? \"\";\n\tconst whole = match[2] ?? \"\";\n\tconst fraction = match[3] ?? \"\";\n\tif (fraction.length <= scale) {\n\t\treturn moneyOfMinor(\n\t\t\tBigInt(sign + whole + fraction.padEnd(scale, \"0\")),\n\t\t\tcurrency,\n\t\t\tscale,\n\t\t);\n\t}\n\tconst excess = fraction.slice(scale);\n\tif (/^0+$/.test(excess)) {\n\t\treturn moneyOfMinor(\n\t\t\tBigInt(sign + whole + fraction.slice(0, scale)),\n\t\t\tcurrency,\n\t\t\tscale,\n\t\t);\n\t}\n\tthrow new MoneyPrecisionLossError(\n\t\t`parsing ${describeValue(input)} at scale ${scale} would lose precision; accepting over-precise input is a business decision, round it in a domain-named policy function via your calculation library`,\n\t);\n}\n","import { UnknownCurrencyError } from \"./errors\";\nimport { type CurrencyCode, type Money, moneyOfMinor } from \"./money\";\nimport { parseMoneyInput } from \"./parse\";\n\n/**\n * Resolves a currency to its scale, or `undefined` for currencies it\n * does not know. The kit ships no currency table; this is the seam\n * where the consumer provides one, once, at the composition root. Any\n * source works as a one-liner: a plain record, the runtime's own data\n * via {@link currencyScaleFromIntl}, or the currency package of a\n * calculation library (`(code) => currencies[code]?.exponent`).\n */\nexport type CurrencyScaleResolver = (\n\tcurrency: CurrencyCode,\n) => number | undefined;\n\n/**\n * Currency-aware construction helpers bound to one\n * {@link CurrencyScaleResolver}. Unknown currencies fail loudly with\n * `UNKNOWN_CURRENCY` instead of guessing a scale.\n */\nexport interface MoneyFactory {\n\t/** `moneyOfMinor` with the scale resolved from the currency. */\n\tofMinor(amountMinor: bigint, currency: CurrencyCode): Money;\n\t/** `parseMoneyInput` (exact-only) with the scale resolved from the currency. */\n\tparse(input: unknown, currency: CurrencyCode): Money;\n\t/** Zero in the given currency at its resolved scale. */\n\tzero(currency: CurrencyCode): Money;\n\t/** The resolved scale; throws `UNKNOWN_CURRENCY` when unresolved. */\n\tscaleOf(currency: CurrencyCode): number;\n}\n\n/** Options for {@link createMoneyFactory}. */\nexport interface CreateMoneyFactoryOptions {\n\treadonly scaleFor: CurrencyScaleResolver;\n}\n\n/**\n * Binds a {@link CurrencyScaleResolver} once and returns construction\n * helpers that no longer need an explicit scale per call.\n *\n * @example\n * ```ts\n * const money = createMoneyFactory({\n * scaleFor: currencyScaleFromRecord({ EUR: 2, JPY: 0 }),\n * });\n * money.parse(\"10.99\", \"EUR\"); // { amountMinor: 1099n, currency: \"EUR\", scale: 2 }\n * ```\n */\nexport function createMoneyFactory(\n\toptions: CreateMoneyFactoryOptions,\n): MoneyFactory {\n\tconst { scaleFor } = options;\n\tconst scaleOf = (currency: CurrencyCode): number => {\n\t\tconst scale = scaleFor(currency);\n\t\tif (scale === undefined) throw new UnknownCurrencyError(currency);\n\t\treturn scale;\n\t};\n\treturn Object.freeze({\n\t\tofMinor: (amountMinor: bigint, currency: CurrencyCode) =>\n\t\t\tmoneyOfMinor(amountMinor, currency, scaleOf(currency)),\n\t\tparse: (input: unknown, currency: CurrencyCode) =>\n\t\t\tparseMoneyInput(input, { currency, scale: scaleOf(currency) }),\n\t\tzero: (currency: CurrencyCode) =>\n\t\t\tmoneyOfMinor(0n, currency, scaleOf(currency)),\n\t\tscaleOf,\n\t});\n}\n\n/**\n * Resolver over a plain currency-to-scale record\n * (`{ EUR: 2, JPY: 0 }`). The record is copied into a `Map` at\n * creation, so later mutation of the input and hostile own keys have\n * no effect.\n */\nexport function currencyScaleFromRecord(\n\trecord: Readonly<Record<string, number>>,\n): CurrencyScaleResolver {\n\tconst scales = new Map(Object.entries(record));\n\treturn (currency) => scales.get(currency);\n}\n\nconst CANONICAL_ISO_CODE = /^[A-Z]{3}$/;\n\n/**\n * Resolver backed by the runtime's own currency data (ICU via\n * `Intl.NumberFormat`), so no currency table ships with the kit or the\n * consumer. Resolves ONLY canonical uppercase ISO 4217 codes: Intl\n * itself would accept \"eur\", but a silent alias would let \"eur\"-Money\n * and \"EUR\"-Money circulate side by side until an operation throws\n * `MONEY_CURRENCY_MISMATCH`; here \"eur\" resolves to `undefined` and\n * fails fast as `UNKNOWN_CURRENCY` at the factory.\n *\n * A CONVENIENCE, not an enterprise source of truth: ICU resolves\n * well-formed but UNASSIGNED codes to its default of 2 rather than\n * `undefined`, and the data shifts with the runtime's ICU version.\n * Production money paths should pin a closed, versioned currency map\n * (`currencyScaleFromRecord`) or a calculation library's versioned\n * currency package; use this resolver for demos, prototypes, and\n * internal tooling.\n */\nexport function currencyScaleFromIntl(): CurrencyScaleResolver {\n\tconst cache = new Map<string, number | undefined>();\n\treturn (currency) => {\n\t\tif (typeof currency !== \"string\" || !CANONICAL_ISO_CODE.test(currency)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (cache.has(currency)) return cache.get(currency);\n\t\tlet scale: number | undefined;\n\t\ttry {\n\t\t\tscale = new Intl.NumberFormat(\"en\", {\n\t\t\t\tstyle: \"currency\",\n\t\t\t\tcurrency,\n\t\t\t}).resolvedOptions().maximumFractionDigits;\n\t\t} catch {\n\t\t\tscale = undefined;\n\t\t}\n\t\t// Bounded: attacker-influenced currency strings must not turn\n\t\t// the cache into a leak. The legitimate key population is tiny;\n\t\t// past the cap, misses stay correct and simply pay Intl again.\n\t\tif (cache.size < CACHE_LIMIT) cache.set(currency, scale);\n\t\treturn scale;\n\t};\n}\n\nconst CACHE_LIMIT = 1_000;\n","import { assertMoney, type Money, moneyToDecimalString } from \"./money\";\n\n// Bounded like the resolver cache: attacker-influenced currency/scale\n// pairs (any well-formed code formats) must not leak memory. Past the\n// cap, formatting stays correct and pays construction again.\nconst CACHE_LIMIT = 1_000;\n\nfunction formatterFor(\n\tlocale: string,\n\tcurrency: string,\n\tscale: number,\n): Intl.NumberFormat {\n\treturn new Intl.NumberFormat(locale, {\n\t\tstyle: \"currency\",\n\t\tcurrency,\n\t\tminimumFractionDigits: scale,\n\t\tmaximumFractionDigits: scale,\n\t});\n}\n\nfunction formatDecimal(formatter: Intl.NumberFormat, money: Money): string {\n\t// Intl accepts decimal strings since NumberFormat v3; the cast only\n\t// bridges TS lib types that predate it. A number round-trip instead\n\t// would silently lose precision past 2^53.\n\treturn formatter.format(moneyToDecimalString(money) as unknown as number);\n}\n\n/**\n * Formats for display via `Intl.NumberFormat`, feeding the exact\n * decimal string (never a float), with the money's own scale as the\n * fraction-digit count. Presentation only: the output is\n * locale-dependent text and must never flow back into parsing,\n * storage, or arithmetic. Non-Money input fails loudly with\n * `INVALID_MONEY` before Intl is touched.\n *\n * The currency must be well-formed for `Intl` (ISO alpha-3); for\n * non-ISO codes, format `moneyToDecimalString(money)` yourself.\n */\nexport function formatMoney(money: Money, locale: string): string {\n\tassertMoney(money);\n\treturn formatDecimal(\n\t\tformatterFor(locale, money.currency, money.scale),\n\t\tmoney,\n\t);\n}\n\n/**\n * Binds the locale once and caches one `Intl.NumberFormat` per\n * currency/scale pair; constructing formatters is expensive, so use\n * this over `formatMoney` anywhere hot (lists, tables, exports).\n */\nexport function createMoneyFormatter(locale: string): (money: Money) => string {\n\tconst formatters = new Map<string, Intl.NumberFormat>();\n\treturn (money) => {\n\t\tassertMoney(money);\n\t\tconst key = `${money.currency}@${money.scale}`;\n\t\tlet formatter = formatters.get(key);\n\t\tif (!formatter) {\n\t\t\tformatter = formatterFor(locale, money.currency, money.scale);\n\t\t\tif (formatters.size < CACHE_LIMIT) formatters.set(key, formatter);\n\t\t}\n\t\treturn formatDecimal(formatter, money);\n\t};\n}\n","import { describeValue, InvalidMoneyError } from \"./errors\";\nimport {\n\tassertMoney,\n\ttype CurrencyCode,\n\ttype Money,\n\tmoneyOfMinor,\n} from \"./money\";\n\n/**\n * The currency part of {@link MoneySnapshotLike}: either a bare code or\n * a currency object as calculation libraries model it (code, numeric\n * base, exponent). Structural on purpose; the kit depends on no\n * calculation library.\n */\nexport type MoneySnapshotCurrencyLike =\n\t| string\n\t| {\n\t\t\treadonly code: string;\n\t\t\treadonly base?: number | bigint | ReadonlyArray<number | bigint>;\n\t\t\treadonly exponent?: number | bigint;\n\t };\n\n/**\n * The `{ amount, currency, scale }` shape that calculation libraries\n * expose when serializing their money objects (a `toJSON()` result,\n * typically). `scale` falls back to the currency's `exponent` when\n * absent; number and bigint calculators are both accepted.\n */\nexport interface MoneySnapshotLike {\n\treadonly amount: number | bigint;\n\treadonly currency: MoneySnapshotCurrencyLike;\n\treadonly scale?: number | bigint;\n}\n\n/**\n * The canonical snapshot {@link moneyToSnapshot} emits: number-based\n * with an explicit base-10 currency object, which is exactly what the\n * common calculation-library constructors accept.\n */\nexport interface MoneySnapshot {\n\treadonly amount: number;\n\treadonly currency: {\n\t\treadonly code: CurrencyCode;\n\t\treadonly base: 10;\n\t\treadonly exponent: number;\n\t};\n\treadonly scale: number;\n}\n\nfunction toScaleNumber(value: unknown, what: string): number {\n\tconst scale = typeof value === \"bigint\" ? Number(value) : value;\n\tif (typeof scale !== \"number\" || !Number.isSafeInteger(scale)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`snapshot ${what} must be an integer; got ${describeValue(value)}`,\n\t\t);\n\t}\n\treturn scale;\n}\n\n/**\n * Converts a calculation-library snapshot into canonical {@link Money}.\n * The anti-corruption checks live here so they run exactly once, at\n * the boundary:\n *\n * - non-decimal currencies are rejected (`base` other than 10; some\n * library currency packages model MGA/MRU with base 5, and pre-1971\n * GBP with a base array): their minor units do not map onto a\n * power-of-ten scale\n * - number amounts must be safe integers; fractional or beyond-2^53\n * amounts are rejected instead of silently corrupted\n * - bigint amounts pass through exactly\n *\n * Takes `unknown` on purpose: this is the trust boundary for foreign\n * library data, so callers never cast before validating (the\n * {@link MoneySnapshotLike} type documents the expected shape).\n */\nexport function moneyFromSnapshot(snapshot: unknown): Money {\n\tif (snapshot === null || typeof snapshot !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money snapshot must be an object; got ${snapshot === null ? \"null\" : typeof snapshot}`,\n\t\t);\n\t}\n\tconst { amount, currency, scale } = snapshot as Partial<\n\t\tRecord<keyof MoneySnapshotLike, unknown>\n\t>;\n\tconst currencyObject = (\n\t\ttypeof currency === \"string\" ? { code: currency } : currency\n\t) as\n\t\t| Partial<Record<\"code\" | \"base\" | \"exponent\", unknown>>\n\t\t| null\n\t\t| undefined;\n\tif (currencyObject === null || typeof currencyObject !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`snapshot currency must be a code or a currency object; got ${typeof currency}`,\n\t\t);\n\t}\n\tconst { base } = currencyObject;\n\tif (base !== undefined && base !== 10 && base !== 10n) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`only base-10 currencies map onto Money; got base ${describeValue(base)} for ${describeValue(currencyObject.code)}`,\n\t\t);\n\t}\n\tconst scaleSource = scale ?? currencyObject.exponent;\n\tif (scaleSource === undefined) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"money snapshot carries neither a scale nor a currency exponent\",\n\t\t);\n\t}\n\t// moneyOfMinor validates the currency code.\n\treturn moneyOfMinor(\n\t\ttoBigIntAmount(amount),\n\t\tcurrencyObject.code as CurrencyCode,\n\t\ttoScaleNumber(scaleSource, \"scale\"),\n\t);\n}\n\nfunction toBigIntAmount(amount: unknown): bigint {\n\tif (typeof amount === \"bigint\") return amount;\n\tif (typeof amount === \"number\" && Number.isSafeInteger(amount)) {\n\t\treturn BigInt(amount);\n\t}\n\tthrow new InvalidMoneyError(\n\t\t`snapshot amount must be a bigint or a safe integer; got ${describeValue(amount)}`,\n\t);\n}\n\n/**\n * Converts {@link Money} into the snapshot shape the common\n * calculation-library constructors accept. Number-based by design (the\n * libraries' default calculators are), so amounts past\n * `Number.MAX_SAFE_INTEGER` are rejected loudly; wire such amounts\n * into a bigint calculator directly from `money.amountMinor` instead.\n */\nexport function moneyToSnapshot(money: Money): MoneySnapshot {\n\tassertMoney(money);\n\tconst amount = Number(money.amountMinor);\n\tif (!Number.isSafeInteger(amount)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`amountMinor ${money.amountMinor} exceeds Number.MAX_SAFE_INTEGER; use your library's bigint calculator and pass amountMinor directly`,\n\t\t);\n\t}\n\treturn {\n\t\tamount,\n\t\tcurrency: { code: money.currency, base: 10, exponent: money.scale },\n\t\tscale: money.scale,\n\t};\n}\n","import { err, ok, type Result } from \"@shirudo/result\";\nimport { InvalidMoneyError, MoneyPrecisionLossError } from \"./errors\";\nimport {\n\tassertValidCurrency,\n\tassertValidScale,\n\ttype Money,\n\tmoneyFromDto,\n} from \"./money\";\nimport { type ParseMoneyInputOptions, parseMoneyInput } from \"./parse\";\nimport { moneyFromSnapshot } from \"./snapshot\";\n\n/**\n * Result-returning counterparts to the three money boundary parsers,\n * for call sites that process many candidate values in one pass: a\n * CSV import, a batch migration, a message replay. Per-row try/catch\n * reads badly and, hand-rolled, usually catches too much; these\n * wrappers apply the `result-vs-throw` guide's discipline instead.\n *\n * The contract, mirroring `voValidated`: only the parser's DOCUMENTED\n * rejections of the INPUT become `Err`. Anything else, an assertion\n * firing, a typo-ed helper, a genuine bug, keeps propagating as a\n * throw, because a bug wrapped in `Err` is a bug silently counted as\n * a bad input row. That includes the parse OPTIONS: a broken scale\n * resolver is a bug in the caller's wiring, so it is validated\n * eagerly and throws instead of marking every row bad. The `Err`\n * types are exact per parser: the wire parsers reject only with\n * `InvalidMoneyError`, while decimal-string parsing can additionally\n * refuse over-precise input with `MoneyPrecisionLossError`.\n *\n * `Result`, `ok`, and `err` come from `@shirudo/result` (already a\n * peer dependency); import them from there to work with the branches.\n */\n\n// Deliberately NOT @shirudo/result's `fromThrowable`: its error\n// mapper is typed as a total unknown -> E conversion, and rethrowing\n// from it would lean on an undocumented implementation detail. Here\n// the rethrow of unexpected errors is a first-class branch, and the\n// type-guard parameter yields the exact per-parser Err union.\nfunction tryCatching<T, E extends Error>(\n\tparse: () => T,\n\tisExpected: (error: unknown) => error is E,\n): Result<T, E> {\n\ttry {\n\t\treturn ok(parse());\n\t} catch (error) {\n\t\tif (isExpected(error)) return err(error);\n\t\tthrow error;\n\t}\n}\n\n/**\n * {@link parseMoneyInput} as a `Result`: `Err` for the documented\n * rejections (malformed input as `InvalidMoneyError`, over-precise\n * input as `MoneyPrecisionLossError`), a throw for everything else.\n */\nexport function tryParseMoneyInput(\n\tinput: unknown,\n\toptions: ParseMoneyInputOptions,\n): Result<Money, InvalidMoneyError | MoneyPrecisionLossError> {\n\t// Options are wiring, not data: validated OUTSIDE the wrapper so a\n\t// bad scale or currency throws instead of becoming a per-row Err.\n\tassertValidScale(options.scale);\n\tassertValidCurrency(options.currency);\n\treturn tryCatching(\n\t\t() => parseMoneyInput(input, options),\n\t\t(error): error is InvalidMoneyError | MoneyPrecisionLossError =>\n\t\t\terror instanceof InvalidMoneyError ||\n\t\t\terror instanceof MoneyPrecisionLossError,\n\t);\n}\n\n/**\n * {@link moneyFromDto} as a `Result`: `Err` for the documented\n * rejection (`InvalidMoneyError`), a throw for everything else.\n */\nexport function tryMoneyFromDto(\n\tdto: unknown,\n): Result<Money, InvalidMoneyError> {\n\treturn tryCatching(\n\t\t() => moneyFromDto(dto),\n\t\t(error): error is InvalidMoneyError => error instanceof InvalidMoneyError,\n\t);\n}\n\n/**\n * {@link moneyFromSnapshot} as a `Result`: `Err` for the documented\n * rejection (`InvalidMoneyError`), a throw for everything else.\n */\nexport function tryMoneyFromSnapshot(\n\tsnapshot: unknown,\n): Result<Money, InvalidMoneyError> {\n\treturn tryCatching(\n\t\t() => moneyFromSnapshot(snapshot),\n\t\t(error): error is InvalidMoneyError => error instanceof InvalidMoneyError,\n\t);\n}\n"],"mappings":"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"}
@@ -1,54 +1,103 @@
1
- import { PublicIssue } from '@shirudo/base-error';
2
- import { PublicErrorView } from '@shirudo/base-error/presentation';
1
+ import { PublicIssue } from "@shirudo/base-error";
2
+ import { LocalizedPublicError, PublicErrorCatalog } from "@shirudo/base-error/public-error";
3
3
 
4
- /** Details shape carried by the view for a {@link toPublicErrorView} result. */
4
+ //#region src/presentation/kit-public-errors.d.ts
5
+ /** Details shape carried by the validation views this catalog projects. */
5
6
  interface PublicErrorViewDetails {
6
- /** Whitelisted field issues, present only for a `ValidationError`. */
7
- readonly issues: readonly PublicIssue[];
7
+ /** Whitelisted field issues, present only for a validation error. */
8
+ readonly issues: readonly PublicIssue[];
8
9
  }
10
+ /**
11
+ * Builds the kit's public-error catalog: one descriptor per public code
12
+ * the kit itself can emit, ready for base-error's `project` / `localize`
13
+ * / `toProblem` pipeline, and the single source of truth behind
14
+ * `toPublicErrorView`. Messages are client-safe and carry no occurrence
15
+ * data (no id, version, or technical detail).
16
+ *
17
+ * A FACTORY, deliberately not a shared instance: base-error's
18
+ * `registerByCode` / `register` widen the TYPE but register into the
19
+ * same underlying catalog, so a shared export would let one consumer's
20
+ * extension leak into every other (and a second registration of the
21
+ * same code throws). Each caller builds its own catalog at its
22
+ * composition root and extends that:
23
+ *
24
+ * ```ts
25
+ * import { createKitPublicErrors } from "@shirudo/ddd-kit/presentation";
26
+ *
27
+ * const catalog = createKitPublicErrors().registerByCode(
28
+ * "ORDER_ALREADY_SHIPPED",
29
+ * {
30
+ * publicCode: "ORDER_ALREADY_SHIPPED",
31
+ * status: 409,
32
+ * userMessages: new LocalizedMessageSet({
33
+ * baseLocale: "en",
34
+ * messages: { en: "This order has already been shipped." },
35
+ * }),
36
+ * },
37
+ * );
38
+ * ```
39
+ *
40
+ * Kit errors resolve by their stable `code` (since v3,
41
+ * `error.name === error.code`); the base-error validation family resolves
42
+ * by capability (see the matcher), with its whitelisted issues projected
43
+ * under `details.issues`. Everything else degrades to the
44
+ * `INTERNAL_ERROR` fallback.
45
+ */
46
+ declare function createKitPublicErrors(): import("@shirudo/base-error/public-error").PublicErrorCatalog<"INTERNAL_ERROR" | "AGGREGATE_NOT_FOUND" | "CONCURRENCY_CONFLICT" | "DUPLICATE_AGGREGATE" | "INVALID_MONEY" | "MONEY_CURRENCY_MISMATCH" | "MONEY_SCALE_MISMATCH" | "MONEY_PRECISION_LOSS" | "UNKNOWN_CURRENCY" | "VALIDATION_FAILED">;
47
+ //#endregion
48
+ //#region src/presentation/public-error-view.d.ts
9
49
  /** Options for {@link toPublicErrorView}. */
10
50
  interface PublicErrorViewOptions {
11
- /**
12
- * BCP 47 locale tag stamped on the view. The built-in messages are English;
13
- * pass a locale only when you supply your own message resolution upstream.
14
- * Default `"en"`.
15
- */
16
- locale?: string;
51
+ /**
52
+ * PREFERRED BCP 47 locale tag. Resolution follows base-error's
53
+ * `localize` (RFC 4647 lookup with base-locale fallback), and the view
54
+ * carries the locale that actually RESOLVED, never a claimed one: with
55
+ * the kit's built-in English messages a `"de-DE"` preference still
56
+ * yields `locale: "en"` unless the catalog carries German. Default
57
+ * `"en"`.
58
+ */
59
+ locale?: string;
60
+ /**
61
+ * The public-error catalog to resolve against. Defaults to a private
62
+ * {@link createKitPublicErrors} instance; pass your own extended
63
+ * catalog (`createKitPublicErrors().registerByCode(...)`) so your own
64
+ * codes and locales resolve through the same pipeline.
65
+ */
66
+ catalog?: PublicErrorCatalog<string>;
17
67
  }
18
68
  /**
19
69
  * Maps a kit error (or any caught value) to a base-error
20
- * {@link PublicErrorView}: a **transport-neutral**, client-safe representation
21
- * (`code`, `message`, `locale`, optional `details`). It is deliberately *not* a
22
- * transport adapter: it carries no HTTP status, header, or exit code, because
23
- * those are the consumer's concern. Feed the view into base-error's
24
- * `defineProblemDetailsAdapter` (HTTP / RFC 9457), a gRPC status mapper, or a
25
- * CLI exit-code table, whichever boundary you are at.
26
- *
27
- * Total over `unknown`: an unmapped or non-kit value degrades to a generic
28
- * `INTERNAL_ERROR` view rather than leaking the technical message or throwing.
29
- * The kit's class-based errors match by their pinned `error.name`, so it
30
- * survives minification and duplicate installs (no `instanceof`). A
31
- * `ValidationError` is detected by its `publicIssues()` whitelist (base-error
32
- * names it after its code), and those issues ride along in `details.issues`.
70
+ * {@link LocalizedPublicError} by delegating to the public-error pipeline:
71
+ * `project` against a catalog (default {@link createKitPublicErrors}), then
72
+ * `localize` with the catalog's messages. A **transport-neutral**,
73
+ * client-safe representation (`code`, `message`, `locale`, optional
74
+ * `details`); feed it into base-error's `toProblem` (HTTP / RFC 9457), a
75
+ * gRPC status mapper, or a CLI exit-code table, whichever boundary you
76
+ * are at.
33
77
  *
34
- * For richer, multi-locale messages, register the kit's errors in a base-error
35
- * `PublicErrorRegistry` and use `PublicErrorPresenter` instead; this helper is
36
- * the lean, single-locale default.
78
+ * Total over `unknown`: an unmatched or hostile value (throwing
79
+ * accessors, a throwing or lying `publicIssues()`) degrades to the
80
+ * catalog's fallback view rather than leaking the technical message or
81
+ * crashing the 500 path. Kit errors resolve by their stable `code`
82
+ * (`error.name === error.code`, minification- and duplicate-install-
83
+ * stable); the base-error validation family resolves by capability with
84
+ * its whitelisted issues sanitized under `details.issues` (see the
85
+ * catalog). No base-error adoption is required to consume the view: it
86
+ * is a plain object read with plain property access.
37
87
  *
38
88
  * @example
39
89
  * ```ts
40
90
  * import { toPublicErrorView } from "@shirudo/ddd-kit/presentation";
41
- * import { defineProblemDetailsAdapter } from "@shirudo/base-error/problem-details";
91
+ * import { toProblem } from "@shirudo/base-error/public-error";
42
92
  *
43
- * const adapter = defineProblemDetailsAdapter({
44
- * definitions: { AggregateNotFoundError: { type: "about:blank", status: 404 } },
45
- * fallback: { type: "about:blank", status: 500 },
46
- * });
47
- *
48
- * const { body, status } = adapter.map(toPublicErrorView(error));
93
+ * const { body, status } = toProblem(
94
+ * { status: 500 }, // or the catalog, for per-code type/status
95
+ * toPublicErrorView(error),
96
+ * );
49
97
  * return Response.json(body, { status });
50
98
  * ```
51
99
  */
52
- declare function toPublicErrorView(error: unknown, options?: PublicErrorViewOptions): PublicErrorView<PublicErrorViewDetails>;
53
-
54
- export { type PublicErrorViewDetails, type PublicErrorViewOptions, toPublicErrorView };
100
+ declare function toPublicErrorView(error: unknown, options?: PublicErrorViewOptions): LocalizedPublicError<PublicErrorViewDetails>;
101
+ //#endregion
102
+ export { type PublicErrorViewDetails, type PublicErrorViewOptions, createKitPublicErrors, toPublicErrorView };
103
+ //# sourceMappingURL=presentation.d.ts.map
@@ -1,45 +1,214 @@
1
- var __defProp = Object.defineProperty;
2
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
1
+ import { LocalizedMessageSet, definePublicErrors, localize, project } from "@shirudo/base-error/public-error";
3
2
 
4
- // src/presentation/public-error-view.ts
5
- var KIT_PUBLIC_MESSAGES = {
6
- AggregateNotFoundError: "The requested resource could not be found.",
7
- ConcurrencyConflictError: "The resource was modified by another request. Please reload and try again.",
8
- DuplicateAggregateError: "The resource already exists."
9
- };
10
- var VALIDATION_MESSAGE = "The submitted data is invalid.";
11
- var FALLBACK_CODE = "INTERNAL_ERROR";
12
- var FALLBACK_MESSAGE = "An unexpected error occurred.";
13
- var DEFAULT_LOCALE = "en";
14
- function toPublicErrorView(error, options = {}) {
15
- const locale = options.locale ?? DEFAULT_LOCALE;
16
- const name = errorName(error);
17
- if (hasPublicIssues(error)) {
18
- return {
19
- code: name ?? "VALIDATION_FAILED",
20
- message: VALIDATION_MESSAGE,
21
- locale,
22
- details: { issues: error.publicIssues() }
23
- };
24
- }
25
- const message = name ? KIT_PUBLIC_MESSAGES[name] : void 0;
26
- if (message === void 0) {
27
- return { code: FALLBACK_CODE, message: FALLBACK_MESSAGE, locale };
28
- }
29
- return { code: name, message, locale };
3
+ //#region src/presentation/kit-public-errors.ts
4
+ /** Single-locale message set for the kit's built-in English texts. */
5
+ function english(message) {
6
+ return new LocalizedMessageSet({
7
+ baseLocale: "en",
8
+ messages: { en: message }
9
+ });
10
+ }
11
+ /**
12
+ * Duck-types the base-error validation family by capability, not
13
+ * `instanceof` (duplicate installs), and not by a name whitelist (custom
14
+ * codes): a SCREAMING_SNAKE `name` (base-error names a `ValidationError`
15
+ * after its code), `category === "VALIDATION"` (kit and
16
+ * convention-following consumer errors carry DOMAIN / INFRASTRUCTURE /
17
+ * WIRING, so a structured infrastructure error exposing a
18
+ * `publicIssues()` method is NOT mistaken for one), and a `publicIssues()`
19
+ * that actually returns an array. Reads and the probe call may throw on
20
+ * hostile inputs; `project` contains a throwing matcher as a miss, which
21
+ * keeps the projection total.
22
+ */
23
+ function isValidationErrorLike(error) {
24
+ if (typeof error !== "object" || error === null) return false;
25
+ const { name, category, publicIssues } = error;
26
+ return typeof name === "string" && /^[A-Z][A-Z0-9_]*$/.test(name) && category === "VALIDATION" && typeof publicIssues === "function" && Array.isArray(error.publicIssues());
27
+ }
28
+ /**
29
+ * Re-emits a `publicIssues()` result through the {@link PublicIssue}
30
+ * whitelist so only the documented wire fields (`message`, `path`, `code`,
31
+ * `pointer`) can reach a client: a duck-typed implementation must not be
32
+ * able to smuggle arbitrary payloads into the view. Drops entries without
33
+ * a string `message`.
34
+ */
35
+ function sanitizePublicIssues(result) {
36
+ const issues = [];
37
+ for (const entry of result) {
38
+ if (typeof entry !== "object" || entry === null) continue;
39
+ const { message, path, code, pointer } = entry;
40
+ if (typeof message !== "string") continue;
41
+ const issue = { message };
42
+ const safePath = sanitizeIssuePath(path);
43
+ if (safePath !== void 0) issue.path = safePath;
44
+ if (typeof code === "string") issue.code = code;
45
+ if (typeof pointer === "string") issue.pointer = pointer;
46
+ issues.push(issue);
47
+ }
48
+ return issues;
30
49
  }
31
- __name(toPublicErrorView, "toPublicErrorView");
32
- function errorName(error) {
33
- if (typeof error !== "object" || error === null) return void 0;
34
- const { name } = error;
35
- return typeof name === "string" ? name : void 0;
50
+ /** Keeps only the documented path segments: property keys or `{ key }`. */
51
+ function sanitizeIssuePath(path) {
52
+ if (!Array.isArray(path)) return void 0;
53
+ const segments = [];
54
+ for (const segment of path) {
55
+ if (typeof segment === "string" || typeof segment === "number" || typeof segment === "symbol") {
56
+ segments.push(segment);
57
+ continue;
58
+ }
59
+ if (typeof segment === "object" && segment !== null) {
60
+ const { key } = segment;
61
+ if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") segments.push({ key });
62
+ }
63
+ }
64
+ return segments;
36
65
  }
37
- __name(errorName, "errorName");
38
- function hasPublicIssues(error) {
39
- return typeof error === "object" && error !== null && typeof error.publicIssues === "function";
66
+ /**
67
+ * Builds the kit's public-error catalog: one descriptor per public code
68
+ * the kit itself can emit, ready for base-error's `project` / `localize`
69
+ * / `toProblem` pipeline, and the single source of truth behind
70
+ * `toPublicErrorView`. Messages are client-safe and carry no occurrence
71
+ * data (no id, version, or technical detail).
72
+ *
73
+ * A FACTORY, deliberately not a shared instance: base-error's
74
+ * `registerByCode` / `register` widen the TYPE but register into the
75
+ * same underlying catalog, so a shared export would let one consumer's
76
+ * extension leak into every other (and a second registration of the
77
+ * same code throws). Each caller builds its own catalog at its
78
+ * composition root and extends that:
79
+ *
80
+ * ```ts
81
+ * import { createKitPublicErrors } from "@shirudo/ddd-kit/presentation";
82
+ *
83
+ * const catalog = createKitPublicErrors().registerByCode(
84
+ * "ORDER_ALREADY_SHIPPED",
85
+ * {
86
+ * publicCode: "ORDER_ALREADY_SHIPPED",
87
+ * status: 409,
88
+ * userMessages: new LocalizedMessageSet({
89
+ * baseLocale: "en",
90
+ * messages: { en: "This order has already been shipped." },
91
+ * }),
92
+ * },
93
+ * );
94
+ * ```
95
+ *
96
+ * Kit errors resolve by their stable `code` (since v3,
97
+ * `error.name === error.code`); the base-error validation family resolves
98
+ * by capability (see the matcher), with its whitelisted issues projected
99
+ * under `details.issues`. Everything else degrades to the
100
+ * `INTERNAL_ERROR` fallback.
101
+ */
102
+ function createKitPublicErrors() {
103
+ return definePublicErrors({ fallback: {
104
+ publicCode: "INTERNAL_ERROR",
105
+ status: 500,
106
+ userMessages: english("An unexpected error occurred.")
107
+ } }).registerByCode("AGGREGATE_NOT_FOUND", {
108
+ publicCode: "AGGREGATE_NOT_FOUND",
109
+ status: 404,
110
+ userMessages: english("The requested resource could not be found.")
111
+ }).registerByCode("CONCURRENCY_CONFLICT", {
112
+ publicCode: "CONCURRENCY_CONFLICT",
113
+ status: 409,
114
+ retryable: true,
115
+ userMessages: english("The resource was modified by another request. Please reload and try again.")
116
+ }).registerByCode("DUPLICATE_AGGREGATE", {
117
+ publicCode: "DUPLICATE_AGGREGATE",
118
+ status: 409,
119
+ userMessages: english("The resource already exists.")
120
+ }).registerByCode("INVALID_MONEY", {
121
+ publicCode: "INVALID_MONEY",
122
+ status: 422,
123
+ userMessages: english("The submitted amount is not a valid monetary value.")
124
+ }).registerByCode("MONEY_CURRENCY_MISMATCH", {
125
+ publicCode: "MONEY_CURRENCY_MISMATCH",
126
+ status: 422,
127
+ userMessages: english("The amounts involved use different currencies.")
128
+ }).registerByCode("MONEY_SCALE_MISMATCH", {
129
+ publicCode: "MONEY_SCALE_MISMATCH",
130
+ status: 422,
131
+ userMessages: english("The amounts involved use different decimal precisions.")
132
+ }).registerByCode("MONEY_PRECISION_LOSS", {
133
+ publicCode: "MONEY_PRECISION_LOSS",
134
+ status: 422,
135
+ userMessages: english("The submitted amount has more decimal places than the currency allows.")
136
+ }).registerByCode("UNKNOWN_CURRENCY", {
137
+ publicCode: "UNKNOWN_CURRENCY",
138
+ status: 422,
139
+ userMessages: english("The currency is not supported.")
140
+ }).register({
141
+ match: isValidationErrorLike,
142
+ descriptor: {
143
+ publicCode: "VALIDATION_FAILED",
144
+ status: 422,
145
+ userMessages: english("The submitted data is invalid."),
146
+ projectDetails: (error) => ({ issues: sanitizePublicIssues(error.publicIssues()) })
147
+ }
148
+ });
149
+ }
150
+
151
+ //#endregion
152
+ //#region src/presentation/public-error-view.ts
153
+ /** Public code and message used for any unmapped or non-kit error. */
154
+ const FALLBACK_CODE = "INTERNAL_ERROR";
155
+ const FALLBACK_MESSAGE = "An unexpected error occurred.";
156
+ /** BCP 47 locale the built-in messages are written in. */
157
+ const DEFAULT_LOCALE = "en";
158
+ const defaultCatalog = createKitPublicErrors();
159
+ /**
160
+ * Maps a kit error (or any caught value) to a base-error
161
+ * {@link LocalizedPublicError} by delegating to the public-error pipeline:
162
+ * `project` against a catalog (default {@link createKitPublicErrors}), then
163
+ * `localize` with the catalog's messages. A **transport-neutral**,
164
+ * client-safe representation (`code`, `message`, `locale`, optional
165
+ * `details`); feed it into base-error's `toProblem` (HTTP / RFC 9457), a
166
+ * gRPC status mapper, or a CLI exit-code table, whichever boundary you
167
+ * are at.
168
+ *
169
+ * Total over `unknown`: an unmatched or hostile value (throwing
170
+ * accessors, a throwing or lying `publicIssues()`) degrades to the
171
+ * catalog's fallback view rather than leaking the technical message or
172
+ * crashing the 500 path. Kit errors resolve by their stable `code`
173
+ * (`error.name === error.code`, minification- and duplicate-install-
174
+ * stable); the base-error validation family resolves by capability with
175
+ * its whitelisted issues sanitized under `details.issues` (see the
176
+ * catalog). No base-error adoption is required to consume the view: it
177
+ * is a plain object read with plain property access.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * import { toPublicErrorView } from "@shirudo/ddd-kit/presentation";
182
+ * import { toProblem } from "@shirudo/base-error/public-error";
183
+ *
184
+ * const { body, status } = toProblem(
185
+ * { status: 500 }, // or the catalog, for per-code type/status
186
+ * toPublicErrorView(error),
187
+ * );
188
+ * return Response.json(body, { status });
189
+ * ```
190
+ */
191
+ function toPublicErrorView(error, options = {}) {
192
+ const locale = options.locale ?? DEFAULT_LOCALE;
193
+ const catalog = options.catalog ?? defaultCatalog;
194
+ try {
195
+ const view = project(catalog, error);
196
+ const messages = catalog.messagesFor(view.code);
197
+ if (messages !== void 0) return localize(view, messages, { locales: [locale] });
198
+ return {
199
+ ...view,
200
+ message: FALLBACK_MESSAGE,
201
+ locale: DEFAULT_LOCALE
202
+ };
203
+ } catch {
204
+ return {
205
+ code: FALLBACK_CODE,
206
+ message: FALLBACK_MESSAGE,
207
+ locale: DEFAULT_LOCALE
208
+ };
209
+ }
40
210
  }
41
- __name(hasPublicIssues, "hasPublicIssues");
42
211
 
43
- export { toPublicErrorView };
44
- //# sourceMappingURL=presentation.js.map
212
+ //#endregion
213
+ export { createKitPublicErrors, toPublicErrorView };
45
214
  //# sourceMappingURL=presentation.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/presentation/public-error-view.ts"],"names":[],"mappings":";;;;AAaA,IAAM,mBAAA,GAAwD;AAAA,EAC7D,sBAAA,EAAwB,4CAAA;AAAA,EACxB,wBAAA,EACC,4EAAA;AAAA,EACD,uBAAA,EAAyB;AAC1B,CAAA;AAGA,IAAM,kBAAA,GAAqB,gCAAA;AAE3B,IAAM,aAAA,GAAgB,gBAAA;AACtB,IAAM,gBAAA,GAAmB,+BAAA;AAEzB,IAAM,cAAA,GAAiB,IAAA;AAoDhB,SAAS,iBAAA,CACf,KAAA,EACA,OAAA,GAAkC,EAAC,EACO;AAC1C,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,cAAA;AACjC,EAAA,MAAM,IAAA,GAAO,UAAU,KAAK,CAAA;AAK5B,EAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG;AAC3B,IAAA,OAAO;AAAA,MACN,MAAM,IAAA,IAAQ,mBAAA;AAAA,MACd,OAAA,EAAS,kBAAA;AAAA,MACT,MAAA;AAAA,MACA,OAAA,EAAS,EAAE,MAAA,EAAQ,KAAA,CAAM,cAAa;AAAE,KACzC;AAAA,EACD;AAEA,EAAA,MAAM,OAAA,GAAU,IAAA,GAAO,mBAAA,CAAoB,IAAI,CAAA,GAAI,MAAA;AACnD,EAAA,IAAI,YAAY,MAAA,EAAW;AAC1B,IAAA,OAAO,EAAE,IAAA,EAAM,aAAA,EAAe,OAAA,EAAS,kBAAkB,MAAA,EAAO;AAAA,EACjE;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAgB,OAAA,EAAS,MAAA,EAAO;AAChD;AAzBgB,MAAA,CAAA,iBAAA,EAAA,mBAAA,CAAA;AA4BhB,SAAS,UAAU,KAAA,EAAoC;AACtD,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,MAAM,OAAO,MAAA;AACxD,EAAA,MAAM,EAAE,MAAK,GAAI,KAAA;AACjB,EAAA,OAAO,OAAO,IAAA,KAAS,QAAA,GAAW,IAAA,GAAO,MAAA;AAC1C;AAJS,MAAA,CAAA,SAAA,EAAA,WAAA,CAAA;AAWT,SAAS,gBACR,KAAA,EAC6C;AAC7C,EAAA,OACC,OAAO,KAAA,KAAU,QAAA,IACjB,UAAU,IAAA,IACV,OAAQ,MAAqC,YAAA,KAAiB,UAAA;AAEhE;AARS,MAAA,CAAA,eAAA,EAAA,iBAAA,CAAA","file":"presentation.js","sourcesContent":["import type { PublicIssue } from \"@shirudo/base-error\";\nimport type { PublicErrorView } from \"@shirudo/base-error/presentation\";\n\n/**\n * Safe, client-facing English messages for the kit's known errors. They carry\n * no occurrence data (no id, version, or technical detail), so they never leak\n * across the boundary. This is the transport-neutral *presentation* layer: the\n * technical error classes stay free of these strings (removed from the core in\n * 2.0); here is their opt-in home.\n *\n * Keyed by the kit's pinned `error.name` (stable across minification and\n * duplicate installs), so matching does not depend on `instanceof`.\n */\nconst KIT_PUBLIC_MESSAGES: Readonly<Record<string, string>> = {\n\tAggregateNotFoundError: \"The requested resource could not be found.\",\n\tConcurrencyConflictError:\n\t\t\"The resource was modified by another request. Please reload and try again.\",\n\tDuplicateAggregateError: \"The resource already exists.\",\n};\n\n/** Message for a `ValidationError`, which is detected by capability, not name. */\nconst VALIDATION_MESSAGE = \"The submitted data is invalid.\";\n/** Public code and message used for any unmapped or non-kit error. */\nconst FALLBACK_CODE = \"INTERNAL_ERROR\";\nconst FALLBACK_MESSAGE = \"An unexpected error occurred.\";\n/** BCP 47 locale the built-in messages are written in. */\nconst DEFAULT_LOCALE = \"en\";\n\n/** Details shape carried by the view for a {@link toPublicErrorView} result. */\nexport interface PublicErrorViewDetails {\n\t/** Whitelisted field issues, present only for a `ValidationError`. */\n\treadonly issues: readonly PublicIssue[];\n}\n\n/** Options for {@link toPublicErrorView}. */\nexport interface PublicErrorViewOptions {\n\t/**\n\t * BCP 47 locale tag stamped on the view. The built-in messages are English;\n\t * pass a locale only when you supply your own message resolution upstream.\n\t * Default `\"en\"`.\n\t */\n\tlocale?: string;\n}\n\n/**\n * Maps a kit error (or any caught value) to a base-error\n * {@link PublicErrorView}: a **transport-neutral**, client-safe representation\n * (`code`, `message`, `locale`, optional `details`). It is deliberately *not* a\n * transport adapter: it carries no HTTP status, header, or exit code, because\n * those are the consumer's concern. Feed the view into base-error's\n * `defineProblemDetailsAdapter` (HTTP / RFC 9457), a gRPC status mapper, or a\n * CLI exit-code table, whichever boundary you are at.\n *\n * Total over `unknown`: an unmapped or non-kit value degrades to a generic\n * `INTERNAL_ERROR` view rather than leaking the technical message or throwing.\n * The kit's class-based errors match by their pinned `error.name`, so it\n * survives minification and duplicate installs (no `instanceof`). A\n * `ValidationError` is detected by its `publicIssues()` whitelist (base-error\n * names it after its code), and those issues ride along in `details.issues`.\n *\n * For richer, multi-locale messages, register the kit's errors in a base-error\n * `PublicErrorRegistry` and use `PublicErrorPresenter` instead; this helper is\n * the lean, single-locale default.\n *\n * @example\n * ```ts\n * import { toPublicErrorView } from \"@shirudo/ddd-kit/presentation\";\n * import { defineProblemDetailsAdapter } from \"@shirudo/base-error/problem-details\";\n *\n * const adapter = defineProblemDetailsAdapter({\n * definitions: { AggregateNotFoundError: { type: \"about:blank\", status: 404 } },\n * fallback: { type: \"about:blank\", status: 500 },\n * });\n *\n * const { body, status } = adapter.map(toPublicErrorView(error));\n * return Response.json(body, { status });\n * ```\n */\nexport function toPublicErrorView(\n\terror: unknown,\n\toptions: PublicErrorViewOptions = {},\n): PublicErrorView<PublicErrorViewDetails> {\n\tconst locale = options.locale ?? DEFAULT_LOCALE;\n\tconst name = errorName(error);\n\n\t// ValidationError (and subclasses) are detected by the publicIssues()\n\t// whitelist, not by name: base-error names a ValidationError after its code\n\t// (\"VALIDATION_FAILED\"), so a name match would miss it.\n\tif (hasPublicIssues(error)) {\n\t\treturn {\n\t\t\tcode: name ?? \"VALIDATION_FAILED\",\n\t\t\tmessage: VALIDATION_MESSAGE,\n\t\t\tlocale,\n\t\t\tdetails: { issues: error.publicIssues() },\n\t\t};\n\t}\n\n\tconst message = name ? KIT_PUBLIC_MESSAGES[name] : undefined;\n\tif (message === undefined) {\n\t\treturn { code: FALLBACK_CODE, message: FALLBACK_MESSAGE, locale };\n\t}\n\n\treturn { code: name as string, message, locale };\n}\n\n/** Reads a string `name` off a caught value without assuming it is an Error. */\nfunction errorName(error: unknown): string | undefined {\n\tif (typeof error !== \"object\" || error === null) return undefined;\n\tconst { name } = error as { name?: unknown };\n\treturn typeof name === \"string\" ? name : undefined;\n}\n\n/**\n * Duck-types base-error's `ValidationError.publicIssues()` accessor. The\n * null/object guard keeps `toPublicErrorView` total: a thrown `null` or\n * `undefined` must degrade to the fallback view, not crash the presenter.\n */\nfunction hasPublicIssues(\n\terror: unknown,\n): error is { publicIssues(): PublicIssue[] } {\n\treturn (\n\t\ttypeof error === \"object\" &&\n\t\terror !== null &&\n\t\ttypeof (error as { publicIssues?: unknown }).publicIssues === \"function\"\n\t);\n}\n"]}
1
+ {"version":3,"file":"presentation.js","names":[],"sources":["../src/presentation/kit-public-errors.ts","../src/presentation/public-error-view.ts"],"sourcesContent":["import type { PublicIssue } from \"@shirudo/base-error\";\nimport {\n\tdefinePublicErrors,\n\tLocalizedMessageSet,\n} from \"@shirudo/base-error/public-error\";\n\n/** Details shape carried by the validation views this catalog projects. */\nexport interface PublicErrorViewDetails {\n\t/** Whitelisted field issues, present only for a validation error. */\n\treadonly issues: readonly PublicIssue[];\n}\n\n/** Single-locale message set for the kit's built-in English texts. */\nfunction english(message: string): LocalizedMessageSet {\n\treturn new LocalizedMessageSet({\n\t\tbaseLocale: \"en\",\n\t\tmessages: { en: message },\n\t});\n}\n\n/**\n * Duck-types the base-error validation family by capability, not\n * `instanceof` (duplicate installs), and not by a name whitelist (custom\n * codes): a SCREAMING_SNAKE `name` (base-error names a `ValidationError`\n * after its code), `category === \"VALIDATION\"` (kit and\n * convention-following consumer errors carry DOMAIN / INFRASTRUCTURE /\n * WIRING, so a structured infrastructure error exposing a\n * `publicIssues()` method is NOT mistaken for one), and a `publicIssues()`\n * that actually returns an array. Reads and the probe call may throw on\n * hostile inputs; `project` contains a throwing matcher as a miss, which\n * keeps the projection total.\n */\nfunction isValidationErrorLike(\n\terror: unknown,\n): error is { publicIssues(): unknown[] } {\n\tif (typeof error !== \"object\" || error === null) return false;\n\tconst { name, category, publicIssues } = error as {\n\t\tname?: unknown;\n\t\tcategory?: unknown;\n\t\tpublicIssues?: unknown;\n\t};\n\treturn (\n\t\ttypeof name === \"string\" &&\n\t\t/^[A-Z][A-Z0-9_]*$/.test(name) &&\n\t\tcategory === \"VALIDATION\" &&\n\t\ttypeof publicIssues === \"function\" &&\n\t\tArray.isArray((error as { publicIssues(): unknown }).publicIssues())\n\t);\n}\n\n/**\n * Re-emits a `publicIssues()` result through the {@link PublicIssue}\n * whitelist so only the documented wire fields (`message`, `path`, `code`,\n * `pointer`) can reach a client: a duck-typed implementation must not be\n * able to smuggle arbitrary payloads into the view. Drops entries without\n * a string `message`.\n */\nfunction sanitizePublicIssues(result: unknown[]): readonly PublicIssue[] {\n\tconst issues: PublicIssue[] = [];\n\tfor (const entry of result) {\n\t\tif (typeof entry !== \"object\" || entry === null) continue;\n\t\tconst { message, path, code, pointer } = entry as Record<string, unknown>;\n\t\tif (typeof message !== \"string\") continue;\n\t\tconst issue: PublicIssue = { message };\n\t\tconst safePath = sanitizeIssuePath(path);\n\t\tif (safePath !== undefined) issue.path = safePath;\n\t\tif (typeof code === \"string\") issue.code = code;\n\t\tif (typeof pointer === \"string\") issue.pointer = pointer;\n\t\tissues.push(issue);\n\t}\n\treturn issues;\n}\n\n/** Keeps only the documented path segments: property keys or `{ key }`. */\nfunction sanitizeIssuePath(path: unknown): PublicIssue[\"path\"] | undefined {\n\tif (!Array.isArray(path)) return undefined;\n\tconst segments: Array<PropertyKey | { readonly key: PropertyKey }> = [];\n\tfor (const segment of path) {\n\t\tif (\n\t\t\ttypeof segment === \"string\" ||\n\t\t\ttypeof segment === \"number\" ||\n\t\t\ttypeof segment === \"symbol\"\n\t\t) {\n\t\t\tsegments.push(segment);\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof segment === \"object\" && segment !== null) {\n\t\t\tconst { key } = segment as { key?: unknown };\n\t\t\tif (\n\t\t\t\ttypeof key === \"string\" ||\n\t\t\t\ttypeof key === \"number\" ||\n\t\t\t\ttypeof key === \"symbol\"\n\t\t\t) {\n\t\t\t\tsegments.push({ key });\n\t\t\t}\n\t\t}\n\t}\n\treturn segments;\n}\n\n/**\n * Builds the kit's public-error catalog: one descriptor per public code\n * the kit itself can emit, ready for base-error's `project` / `localize`\n * / `toProblem` pipeline, and the single source of truth behind\n * `toPublicErrorView`. Messages are client-safe and carry no occurrence\n * data (no id, version, or technical detail).\n *\n * A FACTORY, deliberately not a shared instance: base-error's\n * `registerByCode` / `register` widen the TYPE but register into the\n * same underlying catalog, so a shared export would let one consumer's\n * extension leak into every other (and a second registration of the\n * same code throws). Each caller builds its own catalog at its\n * composition root and extends that:\n *\n * ```ts\n * import { createKitPublicErrors } from \"@shirudo/ddd-kit/presentation\";\n *\n * const catalog = createKitPublicErrors().registerByCode(\n * \"ORDER_ALREADY_SHIPPED\",\n * {\n * publicCode: \"ORDER_ALREADY_SHIPPED\",\n * status: 409,\n * userMessages: new LocalizedMessageSet({\n * baseLocale: \"en\",\n * messages: { en: \"This order has already been shipped.\" },\n * }),\n * },\n * );\n * ```\n *\n * Kit errors resolve by their stable `code` (since v3,\n * `error.name === error.code`); the base-error validation family resolves\n * by capability (see the matcher), with its whitelisted issues projected\n * under `details.issues`. Everything else degrades to the\n * `INTERNAL_ERROR` fallback.\n */\nexport function createKitPublicErrors() {\n\treturn definePublicErrors({\n\t\tfallback: {\n\t\t\tpublicCode: \"INTERNAL_ERROR\",\n\t\t\tstatus: 500,\n\t\t\tuserMessages: english(\"An unexpected error occurred.\"),\n\t\t},\n\t})\n\t\t.registerByCode(\"AGGREGATE_NOT_FOUND\", {\n\t\t\tpublicCode: \"AGGREGATE_NOT_FOUND\",\n\t\t\tstatus: 404,\n\t\t\tuserMessages: english(\"The requested resource could not be found.\"),\n\t\t})\n\t\t.registerByCode(\"CONCURRENCY_CONFLICT\", {\n\t\t\tpublicCode: \"CONCURRENCY_CONFLICT\",\n\t\t\tstatus: 409,\n\t\t\tretryable: true,\n\t\t\tuserMessages: english(\n\t\t\t\t\"The resource was modified by another request. Please reload and try again.\",\n\t\t\t),\n\t\t})\n\t\t.registerByCode(\"DUPLICATE_AGGREGATE\", {\n\t\t\tpublicCode: \"DUPLICATE_AGGREGATE\",\n\t\t\tstatus: 409,\n\t\t\tuserMessages: english(\"The resource already exists.\"),\n\t\t})\n\t\t.registerByCode(\"INVALID_MONEY\", {\n\t\t\tpublicCode: \"INVALID_MONEY\",\n\t\t\tstatus: 422,\n\t\t\tuserMessages: english(\n\t\t\t\t\"The submitted amount is not a valid monetary value.\",\n\t\t\t),\n\t\t})\n\t\t.registerByCode(\"MONEY_CURRENCY_MISMATCH\", {\n\t\t\tpublicCode: \"MONEY_CURRENCY_MISMATCH\",\n\t\t\tstatus: 422,\n\t\t\tuserMessages: english(\"The amounts involved use different currencies.\"),\n\t\t})\n\t\t.registerByCode(\"MONEY_SCALE_MISMATCH\", {\n\t\t\tpublicCode: \"MONEY_SCALE_MISMATCH\",\n\t\t\tstatus: 422,\n\t\t\tuserMessages: english(\n\t\t\t\t\"The amounts involved use different decimal precisions.\",\n\t\t\t),\n\t\t})\n\t\t.registerByCode(\"MONEY_PRECISION_LOSS\", {\n\t\t\tpublicCode: \"MONEY_PRECISION_LOSS\",\n\t\t\tstatus: 422,\n\t\t\tuserMessages: english(\n\t\t\t\t\"The submitted amount has more decimal places than the currency allows.\",\n\t\t\t),\n\t\t})\n\t\t.registerByCode(\"UNKNOWN_CURRENCY\", {\n\t\t\tpublicCode: \"UNKNOWN_CURRENCY\",\n\t\t\tstatus: 422,\n\t\t\tuserMessages: english(\"The currency is not supported.\"),\n\t\t})\n\t\t.register({\n\t\t\tmatch: isValidationErrorLike,\n\t\t\tdescriptor: {\n\t\t\t\tpublicCode: \"VALIDATION_FAILED\",\n\t\t\t\tstatus: 422,\n\t\t\t\tuserMessages: english(\"The submitted data is invalid.\"),\n\t\t\t\tprojectDetails: (error): PublicErrorViewDetails => ({\n\t\t\t\t\tissues: sanitizePublicIssues(error.publicIssues()),\n\t\t\t\t}),\n\t\t\t},\n\t\t});\n}\n","import {\n\tlocalize,\n\ttype LocalizedPublicError,\n\tproject,\n\ttype PublicError,\n\ttype PublicErrorCatalog,\n} from \"@shirudo/base-error/public-error\";\nimport {\n\tcreateKitPublicErrors,\n\ttype PublicErrorViewDetails,\n} from \"./kit-public-errors\";\n\nexport type { PublicErrorViewDetails } from \"./kit-public-errors\";\n\n/** Public code and message used for any unmapped or non-kit error. */\nconst FALLBACK_CODE = \"INTERNAL_ERROR\";\nconst FALLBACK_MESSAGE = \"An unexpected error occurred.\";\n/** BCP 47 locale the built-in messages are written in. */\nconst DEFAULT_LOCALE = \"en\";\n\n// Private default instance: never exported and never handed out, so no\n// consumer can register into it (extensions go through options.catalog\n// on a consumer-built createKitPublicErrors() instance).\nconst defaultCatalog = createKitPublicErrors();\n\n/** Options for {@link toPublicErrorView}. */\nexport interface PublicErrorViewOptions {\n\t/**\n\t * PREFERRED BCP 47 locale tag. Resolution follows base-error's\n\t * `localize` (RFC 4647 lookup with base-locale fallback), and the view\n\t * carries the locale that actually RESOLVED, never a claimed one: with\n\t * the kit's built-in English messages a `\"de-DE\"` preference still\n\t * yields `locale: \"en\"` unless the catalog carries German. Default\n\t * `\"en\"`.\n\t */\n\tlocale?: string;\n\t/**\n\t * The public-error catalog to resolve against. Defaults to a private\n\t * {@link createKitPublicErrors} instance; pass your own extended\n\t * catalog (`createKitPublicErrors().registerByCode(...)`) so your own\n\t * codes and locales resolve through the same pipeline.\n\t */\n\tcatalog?: PublicErrorCatalog<string>;\n}\n\n/**\n * Maps a kit error (or any caught value) to a base-error\n * {@link LocalizedPublicError} by delegating to the public-error pipeline:\n * `project` against a catalog (default {@link createKitPublicErrors}), then\n * `localize` with the catalog's messages. A **transport-neutral**,\n * client-safe representation (`code`, `message`, `locale`, optional\n * `details`); feed it into base-error's `toProblem` (HTTP / RFC 9457), a\n * gRPC status mapper, or a CLI exit-code table, whichever boundary you\n * are at.\n *\n * Total over `unknown`: an unmatched or hostile value (throwing\n * accessors, a throwing or lying `publicIssues()`) degrades to the\n * catalog's fallback view rather than leaking the technical message or\n * crashing the 500 path. Kit errors resolve by their stable `code`\n * (`error.name === error.code`, minification- and duplicate-install-\n * stable); the base-error validation family resolves by capability with\n * its whitelisted issues sanitized under `details.issues` (see the\n * catalog). No base-error adoption is required to consume the view: it\n * is a plain object read with plain property access.\n *\n * @example\n * ```ts\n * import { toPublicErrorView } from \"@shirudo/ddd-kit/presentation\";\n * import { toProblem } from \"@shirudo/base-error/public-error\";\n *\n * const { body, status } = toProblem(\n * { status: 500 }, // or the catalog, for per-code type/status\n * toPublicErrorView(error),\n * );\n * return Response.json(body, { status });\n * ```\n */\nexport function toPublicErrorView(\n\terror: unknown,\n\toptions: PublicErrorViewOptions = {},\n): LocalizedPublicError<PublicErrorViewDetails> {\n\tconst locale = options.locale ?? DEFAULT_LOCALE;\n\tconst catalog = options.catalog ?? defaultCatalog;\n\t// The catch is the totality guarantee's last line: `project` is total\n\t// by contract, but a hostile catalog or message set handed in via\n\t// options must still degrade to the fallback view, never crash the\n\t// 500 path.\n\ttry {\n\t\tconst view = project(catalog, error) as PublicError<\n\t\t\tPublicErrorViewDetails,\n\t\t\tstring\n\t\t>;\n\t\tconst messages = catalog.messagesFor(view.code);\n\t\tif (messages !== undefined) {\n\t\t\treturn localize(view, messages, { locales: [locale] });\n\t\t}\n\t\t// A consumer descriptor without userMessages: keep the view, attach\n\t\t// the generic fallback text so the type stays LocalizedPublicError.\n\t\treturn { ...view, message: FALLBACK_MESSAGE, locale: DEFAULT_LOCALE };\n\t} catch {\n\t\treturn {\n\t\t\tcode: FALLBACK_CODE,\n\t\t\tmessage: FALLBACK_MESSAGE,\n\t\t\tlocale: DEFAULT_LOCALE,\n\t\t};\n\t}\n}\n"],"mappings":"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"}