@evolu/common 8.9.0 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/src/Bytes.d.ts +647 -0
  2. package/dist/src/Bytes.d.ts.map +1 -0
  3. package/dist/src/{Binary.js → Bytes.js} +266 -16
  4. package/dist/src/Config.d.ts +142 -0
  5. package/dist/src/Config.d.ts.map +1 -0
  6. package/dist/src/Config.js +181 -0
  7. package/dist/src/Fs.d.ts +302 -0
  8. package/dist/src/Fs.d.ts.map +1 -0
  9. package/dist/src/Fs.js +111 -0
  10. package/dist/src/Number.d.ts +50 -7
  11. package/dist/src/Number.d.ts.map +1 -1
  12. package/dist/src/Number.js +47 -8
  13. package/dist/src/Object.d.ts +32 -0
  14. package/dist/src/Object.d.ts.map +1 -1
  15. package/dist/src/Object.js +41 -0
  16. package/dist/src/Sqlite.js +1 -1
  17. package/dist/src/Time.d.ts +74 -12
  18. package/dist/src/Time.d.ts.map +1 -1
  19. package/dist/src/Time.js +40 -2
  20. package/dist/src/Type.d.ts +1451 -79
  21. package/dist/src/Type.d.ts.map +1 -1
  22. package/dist/src/Type.js +1303 -96
  23. package/dist/src/index.d.ts +3 -1
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +3 -1
  26. package/dist/src/intl/_en.d.ts +24 -1
  27. package/dist/src/intl/_en.d.ts.map +1 -1
  28. package/dist/src/intl/_en.js +20 -0
  29. package/dist/src/intl/ar.d.ts +24 -1
  30. package/dist/src/intl/ar.d.ts.map +1 -1
  31. package/dist/src/intl/ar.js +20 -0
  32. package/dist/src/intl/bn.d.ts +24 -1
  33. package/dist/src/intl/bn.d.ts.map +1 -1
  34. package/dist/src/intl/bn.js +20 -0
  35. package/dist/src/intl/ca.d.ts +24 -1
  36. package/dist/src/intl/ca.d.ts.map +1 -1
  37. package/dist/src/intl/ca.js +20 -0
  38. package/dist/src/intl/cs.d.ts +24 -1
  39. package/dist/src/intl/cs.d.ts.map +1 -1
  40. package/dist/src/intl/cs.js +20 -0
  41. package/dist/src/intl/da.d.ts +24 -1
  42. package/dist/src/intl/da.d.ts.map +1 -1
  43. package/dist/src/intl/da.js +20 -0
  44. package/dist/src/intl/de.d.ts +24 -1
  45. package/dist/src/intl/de.d.ts.map +1 -1
  46. package/dist/src/intl/de.js +20 -0
  47. package/dist/src/intl/el.d.ts +24 -1
  48. package/dist/src/intl/el.d.ts.map +1 -1
  49. package/dist/src/intl/el.js +20 -0
  50. package/dist/src/intl/es.d.ts +24 -1
  51. package/dist/src/intl/es.d.ts.map +1 -1
  52. package/dist/src/intl/es.js +20 -0
  53. package/dist/src/intl/fa.d.ts +24 -1
  54. package/dist/src/intl/fa.d.ts.map +1 -1
  55. package/dist/src/intl/fa.js +20 -0
  56. package/dist/src/intl/fi.d.ts +24 -1
  57. package/dist/src/intl/fi.d.ts.map +1 -1
  58. package/dist/src/intl/fi.js +20 -0
  59. package/dist/src/intl/fil.d.ts +24 -1
  60. package/dist/src/intl/fil.d.ts.map +1 -1
  61. package/dist/src/intl/fil.js +20 -0
  62. package/dist/src/intl/fr.d.ts +24 -1
  63. package/dist/src/intl/fr.d.ts.map +1 -1
  64. package/dist/src/intl/fr.js +20 -0
  65. package/dist/src/intl/he.d.ts +24 -1
  66. package/dist/src/intl/he.d.ts.map +1 -1
  67. package/dist/src/intl/he.js +20 -0
  68. package/dist/src/intl/hi.d.ts +24 -1
  69. package/dist/src/intl/hi.d.ts.map +1 -1
  70. package/dist/src/intl/hi.js +20 -0
  71. package/dist/src/intl/hr.d.ts +24 -1
  72. package/dist/src/intl/hr.d.ts.map +1 -1
  73. package/dist/src/intl/hr.js +20 -0
  74. package/dist/src/intl/hu.d.ts +22 -1
  75. package/dist/src/intl/hu.d.ts.map +1 -1
  76. package/dist/src/intl/hu.js +18 -0
  77. package/dist/src/intl/id.d.ts +24 -1
  78. package/dist/src/intl/id.d.ts.map +1 -1
  79. package/dist/src/intl/id.js +20 -0
  80. package/dist/src/intl/it.d.ts +24 -1
  81. package/dist/src/intl/it.d.ts.map +1 -1
  82. package/dist/src/intl/it.js +20 -0
  83. package/dist/src/intl/ja.d.ts +24 -1
  84. package/dist/src/intl/ja.d.ts.map +1 -1
  85. package/dist/src/intl/ja.js +20 -0
  86. package/dist/src/intl/ko.d.ts +24 -1
  87. package/dist/src/intl/ko.d.ts.map +1 -1
  88. package/dist/src/intl/ko.js +20 -0
  89. package/dist/src/intl/ml.d.ts +24 -1
  90. package/dist/src/intl/ml.d.ts.map +1 -1
  91. package/dist/src/intl/ml.js +20 -0
  92. package/dist/src/intl/mr.d.ts +24 -1
  93. package/dist/src/intl/mr.d.ts.map +1 -1
  94. package/dist/src/intl/mr.js +20 -0
  95. package/dist/src/intl/ms.d.ts +24 -1
  96. package/dist/src/intl/ms.d.ts.map +1 -1
  97. package/dist/src/intl/ms.js +20 -0
  98. package/dist/src/intl/nb.d.ts +22 -1
  99. package/dist/src/intl/nb.d.ts.map +1 -1
  100. package/dist/src/intl/nb.js +18 -0
  101. package/dist/src/intl/nl.d.ts +24 -1
  102. package/dist/src/intl/nl.d.ts.map +1 -1
  103. package/dist/src/intl/nl.js +20 -0
  104. package/dist/src/intl/pa.d.ts +24 -1
  105. package/dist/src/intl/pa.d.ts.map +1 -1
  106. package/dist/src/intl/pa.js +20 -0
  107. package/dist/src/intl/pl.d.ts +23 -0
  108. package/dist/src/intl/pl.d.ts.map +1 -1
  109. package/dist/src/intl/pl.js +20 -0
  110. package/dist/src/intl/pt-BR.d.ts +24 -1
  111. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  112. package/dist/src/intl/pt-BR.js +20 -0
  113. package/dist/src/intl/pt.d.ts +24 -1
  114. package/dist/src/intl/pt.d.ts.map +1 -1
  115. package/dist/src/intl/pt.js +20 -0
  116. package/dist/src/intl/ro.d.ts +24 -1
  117. package/dist/src/intl/ro.d.ts.map +1 -1
  118. package/dist/src/intl/ro.js +20 -0
  119. package/dist/src/intl/sk.d.ts +24 -1
  120. package/dist/src/intl/sk.d.ts.map +1 -1
  121. package/dist/src/intl/sk.js +20 -0
  122. package/dist/src/intl/sl.d.ts +24 -1
  123. package/dist/src/intl/sl.d.ts.map +1 -1
  124. package/dist/src/intl/sl.js +20 -0
  125. package/dist/src/intl/sv.d.ts +24 -1
  126. package/dist/src/intl/sv.d.ts.map +1 -1
  127. package/dist/src/intl/sv.js +20 -0
  128. package/dist/src/intl/sw.d.ts +21 -0
  129. package/dist/src/intl/sw.d.ts.map +1 -1
  130. package/dist/src/intl/sw.js +18 -0
  131. package/dist/src/intl/ta.d.ts +24 -1
  132. package/dist/src/intl/ta.d.ts.map +1 -1
  133. package/dist/src/intl/ta.js +20 -0
  134. package/dist/src/intl/te.d.ts +24 -1
  135. package/dist/src/intl/te.d.ts.map +1 -1
  136. package/dist/src/intl/te.js +20 -0
  137. package/dist/src/intl/th.d.ts +24 -1
  138. package/dist/src/intl/th.d.ts.map +1 -1
  139. package/dist/src/intl/th.js +20 -0
  140. package/dist/src/intl/tr.d.ts +24 -1
  141. package/dist/src/intl/tr.d.ts.map +1 -1
  142. package/dist/src/intl/tr.js +20 -0
  143. package/dist/src/intl/uk.d.ts +80 -57
  144. package/dist/src/intl/uk.d.ts.map +1 -1
  145. package/dist/src/intl/uk.js +174 -149
  146. package/dist/src/intl/ur.d.ts +24 -1
  147. package/dist/src/intl/ur.d.ts.map +1 -1
  148. package/dist/src/intl/ur.js +20 -0
  149. package/dist/src/intl/vi.d.ts +24 -1
  150. package/dist/src/intl/vi.d.ts.map +1 -1
  151. package/dist/src/intl/vi.js +20 -0
  152. package/dist/src/intl/zh-CN.d.ts +24 -1
  153. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  154. package/dist/src/intl/zh-CN.js +20 -0
  155. package/dist/src/intl/zh-TW.d.ts +24 -1
  156. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  157. package/dist/src/intl/zh-TW.js +20 -0
  158. package/dist/src/local-first/Protocol.d.ts +1 -1
  159. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  160. package/dist/src/local-first/Protocol.js +1 -1
  161. package/dist/src/local-first/Schema.d.ts +10 -0
  162. package/dist/src/local-first/Schema.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +1 -1
  164. package/dist/src/local-first/Timestamp.js +1 -1
  165. package/package.json +1 -1
  166. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  167. package/src/{Binary.ts → Bytes.ts} +652 -21
  168. package/src/Config.test.ts +672 -0
  169. package/src/Config.ts +410 -0
  170. package/src/Fs.test.ts +105 -0
  171. package/src/Fs.ts +414 -0
  172. package/src/Number.test.ts +82 -18
  173. package/src/Number.ts +76 -8
  174. package/src/Object.test.ts +115 -1
  175. package/src/Object.ts +44 -0
  176. package/src/Sqlite.ts +1 -1
  177. package/src/Time.test.ts +82 -11
  178. package/src/Time.ts +141 -16
  179. package/src/Type.test.ts +3114 -157
  180. package/src/Type.ts +2553 -196
  181. package/src/index.ts +3 -1
  182. package/src/intl/_en.ts +70 -0
  183. package/src/intl/ar.ts +71 -0
  184. package/src/intl/bn.ts +70 -0
  185. package/src/intl/ca.ts +70 -0
  186. package/src/intl/cs.ts +70 -0
  187. package/src/intl/da.ts +70 -0
  188. package/src/intl/de.ts +70 -0
  189. package/src/intl/el.ts +70 -0
  190. package/src/intl/es.ts +70 -0
  191. package/src/intl/fa.ts +70 -0
  192. package/src/intl/fi.ts +70 -0
  193. package/src/intl/fil.ts +70 -0
  194. package/src/intl/fr.ts +70 -0
  195. package/src/intl/he.ts +70 -0
  196. package/src/intl/hi.ts +70 -0
  197. package/src/intl/hr.ts +70 -0
  198. package/src/intl/hu.ts +69 -0
  199. package/src/intl/id.ts +70 -0
  200. package/src/intl/intl.test.ts +819 -1
  201. package/src/intl/it.ts +70 -0
  202. package/src/intl/ja.ts +70 -0
  203. package/src/intl/ko.ts +68 -0
  204. package/src/intl/ml.ts +70 -0
  205. package/src/intl/mr.ts +70 -0
  206. package/src/intl/ms.ts +71 -0
  207. package/src/intl/nb.ts +69 -0
  208. package/src/intl/nl.ts +70 -0
  209. package/src/intl/pa.ts +70 -0
  210. package/src/intl/pl.ts +63 -0
  211. package/src/intl/pt-BR.ts +70 -0
  212. package/src/intl/pt.ts +71 -0
  213. package/src/intl/ro.ts +70 -0
  214. package/src/intl/sk.ts +71 -0
  215. package/src/intl/sl.ts +70 -0
  216. package/src/intl/sv.ts +70 -0
  217. package/src/intl/sw.ts +62 -0
  218. package/src/intl/ta.ts +70 -0
  219. package/src/intl/te.ts +70 -0
  220. package/src/intl/th.ts +68 -0
  221. package/src/intl/tr.ts +70 -0
  222. package/src/intl/uk.ts +228 -155
  223. package/src/intl/ur.ts +70 -0
  224. package/src/intl/vi.ts +70 -0
  225. package/src/intl/zh-CN.ts +68 -0
  226. package/src/intl/zh-TW.ts +68 -0
  227. package/src/local-first/Protocol.test.ts +1 -1
  228. package/src/local-first/Protocol.ts +1 -1
  229. package/src/local-first/Schema.ts +11 -0
  230. package/src/local-first/Storage.ts +1 -1
  231. package/src/local-first/Timestamp.ts +1 -1
  232. package/dist/src/Binary.d.ts +0 -254
  233. package/dist/src/Binary.d.ts.map +0 -1
package/dist/src/Type.js CHANGED
@@ -341,9 +341,9 @@
341
341
  * parameter type. If application code claims an accessor-backed object or an
342
342
  * object with excess properties is an Object Output, the assertion throws
343
343
  * because the application contract is broken. `orThrow` and `orNull` preserve
344
- * the assertion at their typed `Input` boundary, then apply {@link getOrThrow}
345
- * or {@link getOrNull} only to validation failures returned by the remaining
346
- * pipeline.
344
+ * the assertion at their typed `Input` boundary. After that boundary, `orThrow`
345
+ * throws returned validation errors with a formatted message and the original
346
+ * error as `cause`, while `orNull` maps them to `null`.
347
347
  *
348
348
  * Consequently, structural representation errors such as sparse Arrays,
349
349
  * accessors, and excess properties normally do not enter user-facing validation
@@ -454,12 +454,54 @@ import * as bip39 from "@scure/bip39";
454
454
  import { wordlist } from "@scure/bip39/wordlists/english.js";
455
455
  import { createMutableArray, } from "./Array.js";
456
456
  import { assert, assertNonNullable } from "./Assert.js";
457
+ import { eqData } from "./Eq.js";
457
458
  import { identity } from "./Function.js";
458
- import { createMutableRecord, getObjectKind, isPlainObject } from "./Object.js";
459
+ import { createMutableRecord, getObjectKind, isPlainObject, } from "./Object.js";
459
460
  import { hasNodeBuffer } from "./Platform.js";
460
- import { err, flatMapResult, getOk, getOrNull, getOrThrow, ok, trySync, } from "./Result.js";
461
+ import { err, flatMapResult, getOk, getOrNull, ok, trySync, } from "./Result.js";
461
462
  import { safelyStringifyUnknownValue } from "./String.js";
462
463
  import { instance, isInstance, } from "./Types.js";
464
+ /**
465
+ * Converts an error from a {@link Type} into formatted issues with paths.
466
+ *
467
+ * Pass the Type that produced the error. This uses its nested and localized
468
+ * formatters without validating the input again. To retain every issue, decode
469
+ * with `{ errors: "all" }`; this function cannot recover errors omitted during
470
+ * validation. A root issue has an empty path.
471
+ *
472
+ * ### Example
473
+ *
474
+ * ```ts
475
+ * import {
476
+ * assertEqual,
477
+ * assertErr,
478
+ * IntFromString,
479
+ * object,
480
+ * typeErrorToIssues,
481
+ * } from "@evolu/common";
482
+ *
483
+ * const Settings = object({ port: IntFromString });
484
+ *
485
+ * const result = Settings.fromUnknown({ port: "http" }, { errors: "all" });
486
+ * assertErr(result);
487
+ *
488
+ * assertEqual(typeErrorToIssues(Settings, result.error), [
489
+ * {
490
+ * path: ["port"],
491
+ * message: 'The value "http" is not a decimal integer.',
492
+ * },
493
+ * ]);
494
+ * ```
495
+ *
496
+ * @group Core
497
+ */
498
+ export const typeErrorToIssues = (type, error) => {
499
+ const runtimeType = type;
500
+ return runtimeType[getRuntimeTypeIssuesSymbol](error, "all").map((issue) => ({
501
+ path: issue.path,
502
+ message: formatRuntimeTypeIssue(issue),
503
+ }));
504
+ };
463
505
  const outputValidationSymbol =
464
506
  /*#__PURE__*/ globalThis.Symbol();
465
507
  const getRuntimeTypeIssuesSymbol =
@@ -492,6 +534,23 @@ const createCollectionRuntimeTypeIssues = (name, issuesKind, defaultFormatter, g
492
534
  });
493
535
  };
494
536
  const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
537
+ const formatRuntimeTypeIssue = (issue, formatIssue = formatDefaultRuntimeTypeIssue) => {
538
+ const summary = formatIssue(issue);
539
+ if (issue.alternatives === undefined)
540
+ return summary;
541
+ return [
542
+ summary,
543
+ ...issue.alternatives.flatMap(({ index, name, issues }) => issues.map((issue) => {
544
+ const path = issue.path
545
+ .map((key) => typeof key === "string"
546
+ ? `[${JSON.stringify(key)}]`
547
+ : `[${globalThis.String(key)}]`)
548
+ .join("");
549
+ const message = formatRuntimeTypeIssue(issue, formatIssue).replaceAll("\n", "\n ");
550
+ return `- ${index}: ${name}${path}: ${message}`;
551
+ })),
552
+ ].join("\n");
553
+ };
495
554
  export function assertType(type, value) {
496
555
  if (type === undefined)
497
556
  return;
@@ -530,6 +589,11 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
530
589
  * that produced them. Different localized Type sets can coexist in separate
531
590
  * application or dependency-injection scopes.
532
591
  *
592
+ * A Union formatter supplies the summary of the failure. Retained member
593
+ * failures are appended using their own localized formatters, with member
594
+ * indexes, Type names, and paths identifying each alternative. The Union still
595
+ * produces one issue at its enclosing path.
596
+ *
533
597
  * Localization is scoped to the selected Types instead of a package-wide
534
598
  * translation registry. Static imports give bundlers an explicit dependency
535
599
  * graph, so unrelated Types, locales, and formatters can be removed. Bundling
@@ -670,17 +734,26 @@ const withFormatError = (source, formatIssue, localizedTypeBySource) => {
670
734
  const parent = source.parent
671
735
  ? withFormatError(source.parent, formatIssue, localizedTypeBySource)
672
736
  : null;
673
- const getTypeIssues = (error, mode) => source[getRuntimeTypeIssuesSymbol](error, mode).map((issue) => ({
737
+ const localizeIssue = (issue) => ({
674
738
  ...issue,
675
739
  formatError: () => formatIssue(issue),
676
- }));
740
+ ...(issue.alternatives === undefined
741
+ ? {}
742
+ : {
743
+ alternatives: issue.alternatives.map((alternative) => ({
744
+ ...alternative,
745
+ issues: alternative.issues.map(localizeIssue),
746
+ })),
747
+ }),
748
+ });
749
+ const getTypeIssues = (error, mode) => source[getRuntimeTypeIssuesSymbol](error, mode).map(localizeIssue);
677
750
  const derived = createTypeNode(source.name, parent, source.fromUnknown, source.is, source[outputValidationSymbol], source[fromSymbol], source[encoderSymbol].parent ?? source[encoderSymbol], getTypeIssues, undefined, formatIssue);
678
751
  // Localization changes error rendering, not conversion semantics. Preserve
679
752
  // specialized public input operations such as json's single-parse Json
680
753
  // boundary. Private operation chains on derived still support composition.
681
754
  globalThis.Object.assign(derived, {
682
755
  from: source.from,
683
- orThrow: source.orThrow,
756
+ orThrow: createRuntimeOrThrow(getTerminalRuntimeNode(source.from), derived.formatError),
684
757
  orNull: source.orNull,
685
758
  });
686
759
  localizedTypeBySource.set(source, derived);
@@ -712,7 +785,12 @@ const localizeTypeReflection = (value, formatIssue, localizedTypeBySource) => {
712
785
  return value;
713
786
  const localized = globalThis.Object.create(globalThis.Object.getPrototypeOf(value));
714
787
  for (const key of Reflect.ownKeys(value)) {
715
- localized[key] = localizeTypeReflection(value[key], formatIssue, localizedTypeBySource);
788
+ const property = value[key];
789
+ // Configured defaults are opaque data whose identity must be preserved.
790
+ localized[key] =
791
+ key === "value" && globalThis.Object.hasOwn(value, defaultPropertySymbol)
792
+ ? property
793
+ : localizeTypeReflection(property, formatIssue, localizedTypeBySource);
716
794
  }
717
795
  return localized;
718
796
  };
@@ -728,6 +806,51 @@ const assertRefinementIdentity = (refinement) => (value) => {
728
806
  }
729
807
  return result;
730
808
  };
809
+ /**
810
+ * Creates a root {@link Type} with a custom error for an existing validator.
811
+ *
812
+ * The source must use identity encoding. The new Type accepts its Output as
813
+ * Input and hides its parent boundaries. Validation and error collection are
814
+ * delegated automatically; the mapper receives the failure and original value.
815
+ * The formatter presents the mapped error as one issue.
816
+ *
817
+ * ### Example
818
+ *
819
+ * ```ts
820
+ * import {
821
+ * assertEqual,
822
+ * assertErr,
823
+ * createTypeWithError,
824
+ * Number,
825
+ * String,
826
+ * union,
827
+ * type TypeError,
828
+ * type UnionError,
829
+ * } from "@evolu/common";
830
+ *
831
+ * interface ValueError extends TypeError<"Value"> {
832
+ * readonly cause: UnionError;
833
+ * }
834
+ *
835
+ * const Value = createTypeWithError(
836
+ * "Value",
837
+ * union(String, Number),
838
+ * (cause): ValueError => ({ type: "Value", cause }),
839
+ * () => "Enter text or a number.",
840
+ * );
841
+ *
842
+ * const result = Value.fromUnknown(false, { errors: "all" });
843
+ * assertErr(result);
844
+ * assertEqual(result.error.cause.errors.length, 2);
845
+ * assertEqual(Value.formatError(result.error), "Enter text or a number.");
846
+ * ```
847
+ *
848
+ * @group Construction
849
+ */
850
+ export const createTypeWithError = (name, type, mapError, formatError) => createRootType(name, (value, options) => {
851
+ const result = type.fromUnknown(value, options);
852
+ return result.ok ? result : err(mapError(result.error, value));
853
+ }, formatError);
731
854
  const createRootType = (name, fromUnknown, formatError, getTypeIssues) => {
732
855
  const runtimeFormatError = formatError;
733
856
  const runtimeGetTypeIssues = getTypeIssues ??
@@ -826,6 +949,11 @@ const fromSymbol =
826
949
  const templateLiteralSyntaxSymbol =
827
950
  /*#__PURE__*/ globalThis.Symbol();
828
951
  const mapRuntimeResult = (operation, map) => (value, options = firstValidationOptions) => map(operation(value, options), options);
952
+ const createRuntimeOrThrow = (fromInput, formatError) => mapRuntimeResult(fromInput, (result) => {
953
+ if (result.ok)
954
+ return result.value;
955
+ throw new Error(formatError(result.error), { cause: result.error });
956
+ });
829
957
  // `map` must return a fresh operation because this function can attach `.parent`.
830
958
  const mapRuntimeOperations = (operation, map) => {
831
959
  const mapped = map(operation);
@@ -863,7 +991,7 @@ const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, own
863
991
  : ownTo;
864
992
  const runtimeFormatError = runtimeParent?.[getRuntimeTypeIssuesSymbol] === getTypeIssues
865
993
  ? runtimeParent.formatError
866
- : (error) => formatIssue(getTypeIssues(error, "first")[0]);
994
+ : (error) => formatRuntimeTypeIssue(getTypeIssues(error, "first")[0], formatIssue);
867
995
  const typedFrom = addRuntimeAssertions(name, is, validateOutput, parent, from);
868
996
  const fromInput = getTerminalRuntimeNode(typedFrom);
869
997
  const typedTo = mapRuntimeOperations(to, (operation) => (value) => {
@@ -879,7 +1007,7 @@ const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, own
879
1007
  is,
880
1008
  from: typedFrom,
881
1009
  to: typedTo,
882
- orThrow: mapRuntimeResult(fromInput, getOrThrow),
1010
+ orThrow: createRuntimeOrThrow(fromInput, runtimeFormatError),
883
1011
  orNull: mapRuntimeResult(fromInput, getOrNull),
884
1012
  "~standard": createStandardSchemaProps(fromUnknown, getTypeIssues, formatIssue),
885
1013
  ...additionalProperties,
@@ -899,7 +1027,7 @@ const createStandardSchemaProps = (fromUnknown, getTypeIssues, formatIssue) => (
899
1027
  ? { value: result.value }
900
1028
  : {
901
1029
  issues: getTypeIssues(result.error, "all").map((issue) => ({
902
- message: formatIssue(issue),
1030
+ message: formatRuntimeTypeIssue(issue, formatIssue),
903
1031
  path: issue.path,
904
1032
  })),
905
1033
  };
@@ -1035,6 +1163,7 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
1035
1163
  * >();
1036
1164
  *
1037
1165
  * assertOk(Age.fromUnknown(122), 122);
1166
+ *
1038
1167
  * const invalid = Age.fromUnknown(200);
1039
1168
  * assertErr(invalid);
1040
1169
  * assertType(Data, invalid.error);
@@ -1060,6 +1189,46 @@ export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
1060
1189
  * @group Base
1061
1190
  */
1062
1191
  export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
1192
+ /**
1193
+ * Transforms a boolean spelled as text into a {@link Boolean}.
1194
+ *
1195
+ * This is useful for inputs that carry booleans as text, such as environment
1196
+ * variables, URL query parameters, and form fields. Exactly `true` and `false`
1197
+ * are accepted, the spellings JSON and JavaScript use, so a boolean has one
1198
+ * representation in every source.
1199
+ *
1200
+ * ### Example
1201
+ *
1202
+ * ```ts
1203
+ * import {
1204
+ * assertEqual,
1205
+ * assertErr,
1206
+ * assertOk,
1207
+ * BooleanFromString,
1208
+ * } from "@evolu/common";
1209
+ *
1210
+ * assertOk(BooleanFromString.fromUnknown("true"), true);
1211
+ * assertOk(BooleanFromString.fromUnknown("false"), false);
1212
+ * assertEqual(BooleanFromString.to(true), "true");
1213
+ *
1214
+ * const invalid = BooleanFromString.fromUnknown("yes");
1215
+ * assertErr(invalid, { type: "BooleanFromString", value: "yes" });
1216
+ * assertEqual(
1217
+ * BooleanFromString.formatError(invalid.error),
1218
+ * 'The value "yes" is not a boolean. Use true or false.',
1219
+ * );
1220
+ * ```
1221
+ *
1222
+ * @group Base
1223
+ */
1224
+ export const BooleanFromString = /*#__PURE__*/ transform("BooleanFromString", String, Boolean, {
1225
+ from: (value) => value === "true"
1226
+ ? ok(true)
1227
+ : value === "false"
1228
+ ? ok(false)
1229
+ : err({ type: "BooleanFromString", value }),
1230
+ to: (value) => (value ? "true" : "false"),
1231
+ }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a boolean. Use true or false.`);
1063
1232
  /**
1064
1233
  * A JavaScript symbol {@link Type}.
1065
1234
  *
@@ -1188,6 +1357,7 @@ export const instanceOf = (constructor) => {
1188
1357
  *
1189
1358
  * assertType<typeof Ready.Output, "ready">();
1190
1359
  * assertOk(Ready.fromUnknown("ready"), "ready");
1360
+ *
1191
1361
  * const invalid = Ready.fromUnknown("pending");
1192
1362
  * assertErr(invalid);
1193
1363
  * assertType(Data, invalid.error);
@@ -1241,9 +1411,8 @@ export function union(...typesOrLiterals) {
1241
1411
  const inputMembers = members.map(getTerminalRuntimeNode);
1242
1412
  const inputFrom = createUnionValidation(inputMembers, (member, value, options) => member.fromUnknown(value, options));
1243
1413
  const inputValidateOutput = createUnionValidation(inputMembers, (member, value, options) => member[outputValidationSymbol](value, options));
1244
- const defaultFormatter = (() => "A value does not match any allowed variant.");
1245
- const getTypeIssues = (error) => singleRuntimeTypeIssue("Union", error, defaultFormatter);
1246
- const input = createTypeNode("Union", null, inputFrom, (value) => inputMembers.some((member) => member.is(value)), inputValidateOutput, ok, identity, getTypeIssues);
1414
+ const getTypeIssues = createUnionRuntimeTypeIssues(members);
1415
+ const input = createTypeNode("Union", null, inputFrom, (value) => inputMembers.some((member) => member.is(value)), inputValidateOutput, ok, identity, createUnionRuntimeTypeIssues(inputMembers));
1247
1416
  const fromUnknown = createUnionValidation(members, (member, value, options) => member.fromUnknown(value, options));
1248
1417
  const validateOutput = createUnionValidation(members, (member, value, options) => member[outputValidationSymbol](value, options));
1249
1418
  const memberFromInputs = members.map((member) => getTerminalRuntimeNode(member[fromSymbol]));
@@ -1259,6 +1428,19 @@ export function union(...typesOrLiterals) {
1259
1428
  };
1260
1429
  return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members, [templateLiteralSyntaxSymbol]: true });
1261
1430
  }
1431
+ const createUnionRuntimeTypeIssues = (members) => (error) => [
1432
+ {
1433
+ name: "Union",
1434
+ error,
1435
+ path: [],
1436
+ formatError: () => "A value does not match any allowed variant.",
1437
+ alternatives: error.errors.map(({ index, error }) => ({
1438
+ index,
1439
+ name: members[index].name,
1440
+ issues: members[index][getRuntimeTypeIssuesSymbol](error, "all"),
1441
+ })),
1442
+ },
1443
+ ];
1262
1444
  const createUnionValidation = (members, validateMember) => (value, options = firstValidationOptions) => {
1263
1445
  let errors;
1264
1446
  for (let index = 0; index < members.length; index++) {
@@ -1398,6 +1580,7 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" && "members" in t
1398
1580
  * assertOk(result, ["cs", "CZ"]);
1399
1581
  * const locale = result.value;
1400
1582
  * assertType<typeof locale, SupportedLocale>();
1583
+ *
1401
1584
  * const invalid = SupportedLocale.fromUnknown("cs/CZ");
1402
1585
  * assertErr(invalid);
1403
1586
  * assertType(Data, invalid.error);
@@ -1775,115 +1958,673 @@ export function brand(name, parent, validate, formatError) {
1775
1958
  *
1776
1959
  * ```ts
1777
1960
  * import {
1778
- * assertEqual,
1961
+ * assertEqual,
1962
+ * assertErr,
1963
+ * assertOk,
1964
+ * assertType,
1965
+ * Data,
1966
+ * DateIso,
1967
+ * } from "@evolu/common";
1968
+ *
1969
+ * const value = "2023-01-01T12:00:00.000Z";
1970
+ * assertOk(DateIso.fromUnknown(value), value);
1971
+ *
1972
+ * const invalid = DateIso.fromUnknown("2023-01-01");
1973
+ * assertErr(invalid);
1974
+ * assertType(Data, invalid.error);
1975
+ * assertEqual(invalid.error, {
1976
+ * type: "DateIso",
1977
+ * value: "2023-01-01",
1978
+ * });
1979
+ * ```
1980
+ *
1981
+ * @group String
1982
+ */
1983
+ export const DateIso = /*#__PURE__*/ brand("DateIso", String, (value) => value.length === 24 && new globalThis.Date(value).toJSON() === value
1984
+ ? ok()
1985
+ : err({ type: "DateIso", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a canonical ISO date-time string.`);
1986
+ /**
1987
+ * Safely transforms a {@link Date} into a canonical {@link DateIso}.
1988
+ *
1989
+ * ### Example
1990
+ *
1991
+ * ```ts
1992
+ * import { assertEqual, assertOk, DateIsoFromDate } from "@evolu/common";
1993
+ *
1994
+ * const date = new Date("2025-01-01T12:00:00.000Z");
1995
+ * const result = DateIsoFromDate.fromUnknown(date);
1996
+ *
1997
+ * assertOk(result, "2025-01-01T12:00:00.000Z");
1998
+ * assertEqual(DateIsoFromDate.to(result.value), date);
1999
+ * ```
2000
+ *
2001
+ * @group String
2002
+ */
2003
+ export const DateIsoFromDate = /*#__PURE__*/ transform("DateIsoFromDate", Date, DateIso, {
2004
+ from: (value) => trySync(() => globalThis.Date.prototype.toISOString.call(value), () => ({ type: "DateIsoFromDate", value })),
2005
+ to: (value) => new globalThis.Date(value),
2006
+ }, () => "The Date cannot be represented as DateIso.");
2007
+ /**
2008
+ * Signed 64-bit {@link BigInt}.
2009
+ *
2010
+ * @group Number
2011
+ */
2012
+ export const Int64 = /*#__PURE__*/ brand("Int64", BigInt, (value) => globalThis.BigInt.asIntN(64, value) === value
2013
+ ? ok()
2014
+ : err({ type: "Int64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid signed 64-bit integer (Int64).`);
2015
+ /**
2016
+ * Unsigned 64-bit {@link BigInt}.
2017
+ *
2018
+ * @group Number
2019
+ */
2020
+ export const UInt64 = /*#__PURE__*/ brand("UInt64", BigInt, (value) => globalThis.BigInt.asUintN(64, value) === value
2021
+ ? ok()
2022
+ : err({ type: "UInt64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid unsigned 64-bit integer (UInt64).`);
2023
+ /**
2024
+ * Adds identifier validation in a naming convention to an existing string Type.
2025
+ *
2026
+ * An identifier is one or more ASCII words, each starting with a letter and
2027
+ * continuing with letters or digits. camelCase and PascalCase start every word
2028
+ * after the first with an uppercase letter, so `httpUrl` has two words and
2029
+ * `httpURL` has four. snake_case, kebab-case, and CONSTANT_CASE put exactly one
2030
+ * separator between words. Validation keeps the spelling and rejects empty
2031
+ * strings, whitespace, punctuation, and non-ASCII characters.
2032
+ *
2033
+ * Words never start with a digit. Identifier grammars in most languages forbid
2034
+ * a leading digit, and camelCase cannot mark a word boundary before one, so the
2035
+ * rule applies to every word and keeps every conversion exact. Join an
2036
+ * abbreviation such as `2FA` to the previous word, as in `MAX2FA_ATTEMPTS` and
2037
+ * `max2faAttempts`, or spell the number out, as in `TWO_FACTOR_SECRET`.
2038
+ *
2039
+ * Convert validated identifiers with functions such as
2040
+ * {@link camelCaseToSnakeCase}. Conversions preserve word boundaries, and
2041
+ * converting back restores the original spelling. They return only the
2042
+ * destination brand, dropping unrelated constraints such as input length.
2043
+ *
2044
+ * ### Example
2045
+ *
2046
+ * ```ts
2047
+ * import {
2048
+ * assertErr,
2049
+ * assertOk,
2050
+ * identifier,
2051
+ * maxLength,
2052
+ * String,
2053
+ * } from "@evolu/common";
2054
+ *
2055
+ * const EnvName = identifier("CONSTANT_CASE")(maxLength(16)(String));
2056
+ *
2057
+ * assertOk(EnvName.fromUnknown("HTTP2_PORT"), "HTTP2_PORT");
2058
+ * assertErr(EnvName.fromUnknown("HTTP_2_PORT"));
2059
+ * ```
2060
+ *
2061
+ * @group String
2062
+ */
2063
+ export const identifier = (casing) => (parent) => {
2064
+ const name = identifierBrandByCasing[casing];
2065
+ const pattern = identifierPatternByCasing[casing];
2066
+ return brand(name, parent, (value) => pattern.test(value)
2067
+ ? ok()
2068
+ : err({ type: name, value, casing }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a ${error.casing} identifier.`);
2069
+ };
2070
+ const identifierBrandByCasing = {
2071
+ camelCase: "CamelCaseIdentifier",
2072
+ PascalCase: "PascalCaseIdentifier",
2073
+ snake_case: "SnakeCaseIdentifier",
2074
+ "kebab-case": "KebabCaseIdentifier",
2075
+ CONSTANT_CASE: "ConstantCaseIdentifier",
2076
+ };
2077
+ const identifierPatternByCasing = {
2078
+ camelCase: /^[a-z][a-zA-Z0-9]*$/u,
2079
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/u,
2080
+ snake_case: /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/u,
2081
+ "kebab-case": /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/u,
2082
+ CONSTANT_CASE: /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)*$/u,
2083
+ };
2084
+ /**
2085
+ * A validated camelCase identifier, such as `http2Port`.
2086
+ *
2087
+ * See {@link identifier} for the grammar and the conversion functions.
2088
+ *
2089
+ * @group String
2090
+ */
2091
+ export const CamelCaseIdentifier =
2092
+ /*#__PURE__*/ identifier("camelCase")(String);
2093
+ /**
2094
+ * A validated PascalCase identifier, such as `Http2Port`.
2095
+ *
2096
+ * See {@link identifier} for the grammar and the conversion functions.
2097
+ *
2098
+ * @group String
2099
+ */
2100
+ export const PascalCaseIdentifier =
2101
+ /*#__PURE__*/ identifier("PascalCase")(String);
2102
+ /**
2103
+ * A validated snake_case identifier, such as `http2_port`.
2104
+ *
2105
+ * See {@link identifier} for the grammar and the conversion functions.
2106
+ *
2107
+ * @group String
2108
+ */
2109
+ export const SnakeCaseIdentifier =
2110
+ /*#__PURE__*/ identifier("snake_case")(String);
2111
+ /**
2112
+ * A validated kebab-case identifier, such as `http2-port`.
2113
+ *
2114
+ * See {@link identifier} for the grammar and the conversion functions.
2115
+ *
2116
+ * @group String
2117
+ */
2118
+ export const KebabCaseIdentifier =
2119
+ /*#__PURE__*/ identifier("kebab-case")(String);
2120
+ /**
2121
+ * A validated CONSTANT_CASE identifier, such as `HTTP2_PORT`.
2122
+ *
2123
+ * See {@link identifier} for the grammar and the conversion functions.
2124
+ *
2125
+ * @group String
2126
+ */
2127
+ export const ConstantCaseIdentifier =
2128
+ /*#__PURE__*/ identifier("CONSTANT_CASE")(String);
2129
+ /**
2130
+ * Converts a {@link CamelCaseIdentifier} to a {@link PascalCaseIdentifier}.
2131
+ *
2132
+ * Converts `http2Port` to `Http2Port`. Use {@link pascalCaseToCamelCase} to
2133
+ * recover the original spelling.
2134
+ *
2135
+ * @group String
2136
+ */
2137
+ export const camelCaseToPascalCase = (value) => (value.charAt(0).toUpperCase() + value.slice(1));
2138
+ /**
2139
+ * Converts a {@link CamelCaseIdentifier} to a {@link SnakeCaseIdentifier}.
2140
+ *
2141
+ * Converts `http2Port` to `http2_port`. Use {@link snakeCaseToCamelCase} to
2142
+ * recover the original spelling.
2143
+ *
2144
+ * @group String
2145
+ */
2146
+ export const camelCaseToSnakeCase = (value) => value.replaceAll(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
2147
+ /**
2148
+ * Converts a {@link CamelCaseIdentifier} to a {@link KebabCaseIdentifier}.
2149
+ *
2150
+ * Converts `http2Port` to `http2-port`. Use {@link kebabCaseToCamelCase} to
2151
+ * recover the original spelling.
2152
+ *
2153
+ * @group String
2154
+ */
2155
+ export const camelCaseToKebabCase = (value) => snakeCaseToKebabCase(camelCaseToSnakeCase(value));
2156
+ /**
2157
+ * Converts a {@link CamelCaseIdentifier} to a {@link ConstantCaseIdentifier}.
2158
+ *
2159
+ * Converts `http2Port` to `HTTP2_PORT`. Use {@link constantCaseToCamelCase} to
2160
+ * recover the original spelling.
2161
+ *
2162
+ * @group String
2163
+ */
2164
+ export const camelCaseToConstantCase = (value) => snakeCaseToConstantCase(camelCaseToSnakeCase(value));
2165
+ /**
2166
+ * Converts a {@link PascalCaseIdentifier} to a {@link CamelCaseIdentifier}.
2167
+ *
2168
+ * Converts `Http2Port` to `http2Port`. Use {@link camelCaseToPascalCase} to
2169
+ * recover the original spelling.
2170
+ *
2171
+ * @group String
2172
+ */
2173
+ export const pascalCaseToCamelCase = (value) => (value.charAt(0).toLowerCase() + value.slice(1));
2174
+ /**
2175
+ * Converts a {@link PascalCaseIdentifier} to a {@link SnakeCaseIdentifier}.
2176
+ *
2177
+ * Converts `Http2Port` to `http2_port`. Use {@link snakeCaseToPascalCase} to
2178
+ * recover the original spelling.
2179
+ *
2180
+ * @group String
2181
+ */
2182
+ export const pascalCaseToSnakeCase = (value) => camelCaseToSnakeCase(pascalCaseToCamelCase(value));
2183
+ /**
2184
+ * Converts a {@link PascalCaseIdentifier} to a {@link KebabCaseIdentifier}.
2185
+ *
2186
+ * Converts `Http2Port` to `http2-port`. Use {@link kebabCaseToPascalCase} to
2187
+ * recover the original spelling.
2188
+ *
2189
+ * @group String
2190
+ */
2191
+ export const pascalCaseToKebabCase = (value) => snakeCaseToKebabCase(pascalCaseToSnakeCase(value));
2192
+ /**
2193
+ * Converts a {@link PascalCaseIdentifier} to a {@link ConstantCaseIdentifier}.
2194
+ *
2195
+ * Converts `Http2Port` to `HTTP2_PORT`. Use {@link constantCaseToPascalCase} to
2196
+ * recover the original spelling.
2197
+ *
2198
+ * @group String
2199
+ */
2200
+ export const pascalCaseToConstantCase = (value) => snakeCaseToConstantCase(pascalCaseToSnakeCase(value));
2201
+ /**
2202
+ * Converts a {@link SnakeCaseIdentifier} to a {@link CamelCaseIdentifier}.
2203
+ *
2204
+ * Converts `http2_port` to `http2Port`. Use {@link camelCaseToSnakeCase} to
2205
+ * recover the original spelling.
2206
+ *
2207
+ * @group String
2208
+ */
2209
+ export const snakeCaseToCamelCase = (value) => value.replaceAll(/_[a-z]/gu, (word) => word.charAt(1).toUpperCase());
2210
+ /**
2211
+ * Converts a {@link SnakeCaseIdentifier} to a {@link PascalCaseIdentifier}.
2212
+ *
2213
+ * Converts `http2_port` to `Http2Port`. Use {@link pascalCaseToSnakeCase} to
2214
+ * recover the original spelling.
2215
+ *
2216
+ * @group String
2217
+ */
2218
+ export const snakeCaseToPascalCase = (value) => camelCaseToPascalCase(snakeCaseToCamelCase(value));
2219
+ /**
2220
+ * Converts a {@link SnakeCaseIdentifier} to a {@link KebabCaseIdentifier}.
2221
+ *
2222
+ * Converts `http2_port` to `http2-port`. Use {@link kebabCaseToSnakeCase} to
2223
+ * recover the original spelling.
2224
+ *
2225
+ * @group String
2226
+ */
2227
+ export const snakeCaseToKebabCase = (value) => value.replaceAll("_", "-");
2228
+ /**
2229
+ * Converts a {@link SnakeCaseIdentifier} to a {@link ConstantCaseIdentifier}.
2230
+ *
2231
+ * Converts `http2_port` to `HTTP2_PORT`. Use {@link constantCaseToSnakeCase} to
2232
+ * recover the original spelling.
2233
+ *
2234
+ * @group String
2235
+ */
2236
+ export const snakeCaseToConstantCase = (value) => value.toUpperCase();
2237
+ /**
2238
+ * Converts a {@link KebabCaseIdentifier} to a {@link CamelCaseIdentifier}.
2239
+ *
2240
+ * Converts `http2-port` to `http2Port`. Use {@link camelCaseToKebabCase} to
2241
+ * recover the original spelling.
2242
+ *
2243
+ * @group String
2244
+ */
2245
+ export const kebabCaseToCamelCase = (value) => snakeCaseToCamelCase(kebabCaseToSnakeCase(value));
2246
+ /**
2247
+ * Converts a {@link KebabCaseIdentifier} to a {@link PascalCaseIdentifier}.
2248
+ *
2249
+ * Converts `http2-port` to `Http2Port`. Use {@link pascalCaseToKebabCase} to
2250
+ * recover the original spelling.
2251
+ *
2252
+ * @group String
2253
+ */
2254
+ export const kebabCaseToPascalCase = (value) => camelCaseToPascalCase(kebabCaseToCamelCase(value));
2255
+ /**
2256
+ * Converts a {@link KebabCaseIdentifier} to a {@link SnakeCaseIdentifier}.
2257
+ *
2258
+ * Converts `http2-port` to `http2_port`. Use {@link snakeCaseToKebabCase} to
2259
+ * recover the original spelling.
2260
+ *
2261
+ * @group String
2262
+ */
2263
+ export const kebabCaseToSnakeCase = (value) => value.replaceAll("-", "_");
2264
+ /**
2265
+ * Converts a {@link KebabCaseIdentifier} to a {@link ConstantCaseIdentifier}.
2266
+ *
2267
+ * Converts `http2-port` to `HTTP2_PORT`. Use {@link constantCaseToKebabCase} to
2268
+ * recover the original spelling.
2269
+ *
2270
+ * @group String
2271
+ */
2272
+ export const kebabCaseToConstantCase = (value) => snakeCaseToConstantCase(kebabCaseToSnakeCase(value));
2273
+ /**
2274
+ * Converts a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier}.
2275
+ *
2276
+ * Converts `HTTP2_PORT` to `http2Port`. Use {@link camelCaseToConstantCase} to
2277
+ * recover the original spelling.
2278
+ *
2279
+ * @group String
2280
+ */
2281
+ export const constantCaseToCamelCase = (value) => snakeCaseToCamelCase(constantCaseToSnakeCase(value));
2282
+ /**
2283
+ * Decodes a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier} and
2284
+ * restores the original spelling when encoding.
2285
+ *
2286
+ * ### Example
2287
+ *
2288
+ * ```ts
2289
+ * import {
2290
+ * assertEqual,
2291
+ * assertOk,
2292
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
2293
+ * } from "@evolu/common";
2294
+ *
2295
+ * const Key = CamelCaseIdentifierFromConstantCaseIdentifier;
2296
+ * const result = Key.fromUnknown("HTTP2_PORT");
2297
+ * assertOk(result, "http2Port");
2298
+ * assertEqual(Key.to(result.value), "HTTP2_PORT");
2299
+ * ```
2300
+ *
2301
+ * @group String
2302
+ */
2303
+ export const CamelCaseIdentifierFromConstantCaseIdentifier =
2304
+ /*#__PURE__*/ transform("CamelCaseIdentifierFromConstantCaseIdentifier", ConstantCaseIdentifier, CamelCaseIdentifier, {
2305
+ from: (value) => ok(constantCaseToCamelCase(value)),
2306
+ to: camelCaseToConstantCase,
2307
+ });
2308
+ /**
2309
+ * Converts a {@link ConstantCaseIdentifier} to a {@link PascalCaseIdentifier}.
2310
+ *
2311
+ * Converts `HTTP2_PORT` to `Http2Port`. Use {@link pascalCaseToConstantCase} to
2312
+ * recover the original spelling.
2313
+ *
2314
+ * @group String
2315
+ */
2316
+ export const constantCaseToPascalCase = (value) => camelCaseToPascalCase(constantCaseToCamelCase(value));
2317
+ /**
2318
+ * Converts a {@link ConstantCaseIdentifier} to a {@link SnakeCaseIdentifier}.
2319
+ *
2320
+ * Converts `HTTP2_PORT` to `http2_port`. Use {@link snakeCaseToConstantCase} to
2321
+ * recover the original spelling.
2322
+ *
2323
+ * @group String
2324
+ */
2325
+ export const constantCaseToSnakeCase = (value) => value.toLowerCase();
2326
+ /**
2327
+ * Converts a {@link ConstantCaseIdentifier} to a {@link KebabCaseIdentifier}.
2328
+ *
2329
+ * Converts `HTTP2_PORT` to `http2-port`. Use {@link kebabCaseToConstantCase} to
2330
+ * recover the original spelling.
2331
+ *
2332
+ * @group String
2333
+ */
2334
+ export const constantCaseToKebabCase = (value) => snakeCaseToKebabCase(constantCaseToSnakeCase(value));
2335
+ /**
2336
+ * Adds capitalized text validation to an existing string Type.
2337
+ *
2338
+ * Narrows the output to TypeScript's `Capitalize<string>` while preserving the
2339
+ * parent Type's constraints. Validation leaves the text unchanged. Use
2340
+ * {@link capitalize} to change its casing.
2341
+ *
2342
+ * ### Example
2343
+ *
2344
+ * ```ts
2345
+ * import {
2346
+ * assertErr,
2347
+ * assertOk,
2348
+ * capitalized,
2349
+ * maxLength,
2350
+ * String,
2351
+ * } from "@evolu/common";
2352
+ *
2353
+ * const Label = capitalized(maxLength(50)(String));
2354
+ * assertOk(Label.fromUnknown("Hello world"), "Hello world");
2355
+ * assertErr(Label.fromUnknown("hello world"));
2356
+ * ```
2357
+ *
2358
+ * @group String
2359
+ */
2360
+ export const capitalized = (parent) => createType("Capitalized", parent, (value) => value === capitalize(value)
2361
+ ? ok(value)
2362
+ : err({ type: "Capitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`);
2363
+ /**
2364
+ * Validates capitalized text as TypeScript's `Capitalize<string>`.
2365
+ *
2366
+ * The rest of the text can use any casing. Empty strings and text starting with
2367
+ * an uncased character, such as a digit or emoji, are valid. Use
2368
+ * {@link capitalize} to produce a capitalized value from any string.
2369
+ *
2370
+ * Capitalization applies to general text, including spaces and punctuation.
2371
+ * Both `hello` and `Hello` become `Hello`, so the original initial casing
2372
+ * cannot be recovered. Capitalization does not identify words or turn text into
2373
+ * an identifier.
2374
+ *
2375
+ * ### Example
2376
+ *
2377
+ * ```ts
2378
+ * import { assertErr, assertOk, CapitalizedString } from "@evolu/common";
2379
+ *
2380
+ * const text: CapitalizedString = "Hello world";
2381
+ * assertOk(CapitalizedString.fromUnknown(text), text);
2382
+ * assertErr(CapitalizedString.fromUnknown("hello world"));
2383
+ * assertOk(CapitalizedString.fromUnknown(""), "");
2384
+ * ```
2385
+ *
2386
+ * @group String
2387
+ */
2388
+ export const CapitalizedString = /*#__PURE__*/ capitalized(String);
2389
+ /**
2390
+ * Uppercases the first Unicode code point and returns a
2391
+ * {@link CapitalizedString}.
2392
+ *
2393
+ * Preserves the remainder of the string and leaves an empty string unchanged.
2394
+ * Uses JavaScript's default Unicode casing without locale-specific rules.
2395
+ * Changing case can change the length, so input brands are not retained.
2396
+ *
2397
+ * ### Example
2398
+ *
2399
+ * ```ts
2400
+ * import { assertEqual, assertType, capitalize } from "@evolu/common";
2401
+ *
2402
+ * const text = capitalize("hello world");
2403
+ * assertEqual(text, "Hello world");
2404
+ * assertType<typeof text, "Hello world">();
2405
+ * assertEqual(capitalize(text), text);
2406
+ * ```
2407
+ *
2408
+ * @group String
2409
+ */
2410
+ export const capitalize = (value) => {
2411
+ const [first = ""] = value;
2412
+ return (first.toUpperCase() + value.slice(first.length));
2413
+ };
2414
+ /**
2415
+ * Adds uncapitalized text validation to an existing string Type.
2416
+ *
2417
+ * Narrows the output to TypeScript's `Uncapitalize<string>` while preserving
2418
+ * the parent Type's constraints. Validation leaves the text unchanged. Use
2419
+ * {@link uncapitalize} to change its casing.
2420
+ *
2421
+ * ### Example
2422
+ *
2423
+ * ```ts
2424
+ * import {
2425
+ * assertErr,
2426
+ * assertOk,
2427
+ * uncapitalized,
2428
+ * maxLength,
2429
+ * String,
2430
+ * } from "@evolu/common";
2431
+ *
2432
+ * const Label = uncapitalized(maxLength(50)(String));
2433
+ * assertOk(Label.fromUnknown("hello WORLD"), "hello WORLD");
2434
+ * assertErr(Label.fromUnknown("Hello WORLD"));
2435
+ * ```
2436
+ *
2437
+ * @group String
2438
+ */
2439
+ export const uncapitalized = (parent) => createType("Uncapitalized", parent, (value) => value === uncapitalize(value)
2440
+ ? ok(value)
2441
+ : err({ type: "Uncapitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must not start with an uppercase letter.`);
2442
+ /**
2443
+ * Validates uncapitalized text as TypeScript's `Uncapitalize<string>`.
2444
+ *
2445
+ * The rest of the text can use any casing. Empty strings and text starting with
2446
+ * an uncased character, such as a digit or emoji, are valid. Use
2447
+ * {@link uncapitalize} to produce an uncapitalized value from any string.
2448
+ *
2449
+ * ### Example
2450
+ *
2451
+ * ```ts
2452
+ * import { assertErr, assertOk, UncapitalizedString } from "@evolu/common";
2453
+ *
2454
+ * const text: UncapitalizedString = "hello WORLD";
2455
+ * assertOk(UncapitalizedString.fromUnknown(text), text);
2456
+ * assertErr(UncapitalizedString.fromUnknown("Hello WORLD"));
2457
+ * assertOk(UncapitalizedString.fromUnknown(""), "");
2458
+ * ```
2459
+ *
2460
+ * @group String
2461
+ */
2462
+ export const UncapitalizedString = /*#__PURE__*/ uncapitalized(String);
2463
+ /**
2464
+ * Lowercases the first Unicode code point and returns a
2465
+ * {@link UncapitalizedString}.
2466
+ *
2467
+ * Preserves the remainder of the string and leaves an empty string unchanged.
2468
+ * Uses JavaScript's default Unicode casing without locale-specific rules.
2469
+ * Changing case can change the length, so input brands are not retained.
2470
+ *
2471
+ * ### Example
2472
+ *
2473
+ * ```ts
2474
+ * import { assertEqual, assertType, uncapitalize } from "@evolu/common";
2475
+ *
2476
+ * const text = uncapitalize("Hello WORLD");
2477
+ * assertEqual(text, "hello WORLD");
2478
+ * assertType<typeof text, "hello WORLD">();
2479
+ * assertEqual(uncapitalize(text), text);
2480
+ * ```
2481
+ *
2482
+ * @group String
2483
+ */
2484
+ export const uncapitalize = (value) => {
2485
+ const [first = ""] = value;
2486
+ return (first.toLowerCase() + value.slice(first.length));
2487
+ };
2488
+ /**
2489
+ * Adds uppercased text validation to an existing string Type.
2490
+ *
2491
+ * Narrows the output to TypeScript's `Uppercase<string>` while preserving the
2492
+ * parent Type's constraints. Validation leaves the text unchanged. Use
2493
+ * {@link uppercase} to change its casing.
2494
+ *
2495
+ * ### Example
2496
+ *
2497
+ * ```ts
2498
+ * import {
1779
2499
  * assertErr,
1780
2500
  * assertOk,
1781
- * assertType,
1782
- * Data,
1783
- * DateIso,
2501
+ * uppercased,
2502
+ * maxLength,
2503
+ * String,
1784
2504
  * } from "@evolu/common";
1785
2505
  *
1786
- * const value = "2023-01-01T12:00:00.000Z";
1787
- * assertOk(DateIso.fromUnknown(value), value);
1788
- * const invalid = DateIso.fromUnknown("2023-01-01");
1789
- * assertErr(invalid);
1790
- * assertType(Data, invalid.error);
1791
- * assertEqual(invalid.error, {
1792
- * type: "DateIso",
1793
- * value: "2023-01-01",
1794
- * });
2506
+ * const Label = uppercased(maxLength(50)(String));
2507
+ * assertOk(Label.fromUnknown("HELLO WORLD"), "HELLO WORLD");
2508
+ * assertErr(Label.fromUnknown("Hello world"));
1795
2509
  * ```
1796
2510
  *
1797
2511
  * @group String
1798
2512
  */
1799
- export const DateIso = /*#__PURE__*/ brand("DateIso", String, (value) => value.length === 24 && new globalThis.Date(value).toJSON() === value
1800
- ? ok()
1801
- : err({ type: "DateIso", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a canonical ISO date-time string.`);
2513
+ export const uppercased = (parent) => createType("Uppercased", parent, (value) => value === uppercase(value)
2514
+ ? ok(value)
2515
+ : err({ type: "Uppercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be uppercased.`);
1802
2516
  /**
1803
- * Safely transforms a {@link Date} into a canonical {@link DateIso}.
2517
+ * Validates uppercased text as TypeScript's `Uppercase<string>`.
2518
+ *
2519
+ * Checks the whole string using JavaScript's Unicode uppercase mapping. Empty
2520
+ * strings and uncased characters, such as digits and emoji, are valid. Use
2521
+ * {@link uppercase} to produce an uppercased value from any string.
1804
2522
  *
1805
2523
  * ### Example
1806
2524
  *
1807
2525
  * ```ts
1808
- * import { assertEqual, assertOk, DateIsoFromDate } from "@evolu/common";
1809
- *
1810
- * const date = new Date("2025-01-01T12:00:00.000Z");
1811
- * const result = DateIsoFromDate.fromUnknown(date);
2526
+ * import { assertErr, assertOk, UppercasedString } from "@evolu/common";
1812
2527
  *
1813
- * assertOk(result, "2025-01-01T12:00:00.000Z");
1814
- * assertEqual(DateIsoFromDate.to(result.value), date);
2528
+ * const text: UppercasedString = "HELLO WORLD";
2529
+ * assertOk(UppercasedString.fromUnknown(text), text);
2530
+ * assertErr(UppercasedString.fromUnknown("Hello world"));
2531
+ * assertOk(UppercasedString.fromUnknown(""), "");
1815
2532
  * ```
1816
2533
  *
1817
2534
  * @group String
1818
2535
  */
1819
- export const DateIsoFromDate = /*#__PURE__*/ transform("DateIsoFromDate", Date, DateIso, {
1820
- from: (value) => trySync(() => globalThis.Date.prototype.toISOString.call(value), () => ({ type: "DateIsoFromDate", value })),
1821
- to: (value) => new globalThis.Date(value),
1822
- }, () => "The Date cannot be represented as DateIso.");
2536
+ export const UppercasedString = /*#__PURE__*/ uppercased(String);
1823
2537
  /**
1824
- * Signed 64-bit {@link BigInt}.
2538
+ * Uppercases the whole string and returns a {@link UppercasedString}.
1825
2539
  *
1826
- * @group Number
1827
- */
1828
- export const Int64 = /*#__PURE__*/ brand("Int64", BigInt, (value) => globalThis.BigInt.asIntN(64, value) === value
1829
- ? ok()
1830
- : err({ type: "Int64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid signed 64-bit integer (Int64).`);
1831
- /**
1832
- * Unsigned 64-bit {@link BigInt}.
2540
+ * Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
2541
+ * without locale-specific rules. Changing case can change the length, so input
2542
+ * brands are not retained.
1833
2543
  *
1834
- * @group Number
2544
+ * ### Example
2545
+ *
2546
+ * ```ts
2547
+ * import { assertEqual, assertType, uppercase } from "@evolu/common";
2548
+ *
2549
+ * const text = uppercase("Hello world");
2550
+ * assertEqual(text, "HELLO WORLD");
2551
+ * assertType<typeof text, "HELLO WORLD">();
2552
+ * assertEqual(uppercase(text), text);
2553
+ * ```
2554
+ *
2555
+ * @group String
1835
2556
  */
1836
- export const UInt64 = /*#__PURE__*/ brand("UInt64", BigInt, (value) => globalThis.BigInt.asUintN(64, value) === value
1837
- ? ok()
1838
- : err({ type: "UInt64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid unsigned 64-bit integer (UInt64).`);
2557
+ export const uppercase = (value) => value.toUpperCase();
1839
2558
  /**
1840
- * Capitalized {@link Brand}.
2559
+ * Adds lowercased text validation to an existing string Type.
1841
2560
  *
1842
- * Requires the first character of a string to be uppercase.
2561
+ * Narrows the output to TypeScript's `Lowercase<string>` while preserving the
2562
+ * parent Type's constraints. Validation leaves the text unchanged. Use
2563
+ * {@link lowercase} to change its casing.
1843
2564
  *
1844
2565
  * ### Example
1845
2566
  *
1846
2567
  * ```ts
1847
2568
  * import {
1848
- * assertEqual,
1849
2569
  * assertErr,
1850
2570
  * assertOk,
1851
- * assertType,
1852
- * Data,
2571
+ * lowercased,
2572
+ * maxLength,
1853
2573
  * String,
1854
- * capitalized,
1855
- * type Brand,
1856
2574
  * } from "@evolu/common";
1857
2575
  *
1858
- * const CapitalizedString = capitalized(String);
1859
- * type CapitalizedString = typeof CapitalizedString.Output;
2576
+ * const Label = lowercased(maxLength(50)(String));
2577
+ * assertOk(Label.fromUnknown("hello world"), "hello world");
2578
+ * assertErr(Label.fromUnknown("Hello WORLD"));
2579
+ * ```
1860
2580
  *
1861
- * assertType<CapitalizedString, string & Brand<"Capitalized">>();
2581
+ * @group String
2582
+ */
2583
+ export const lowercased = (parent) => createType("Lowercased", parent, (value) => value === lowercase(value)
2584
+ ? ok(value)
2585
+ : err({ type: "Lowercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be lowercased.`);
2586
+ /**
2587
+ * Validates lowercased text as TypeScript's `Lowercase<string>`.
1862
2588
  *
1863
- * assertOk(CapitalizedString.fromUnknown("Evolu"), "Evolu");
1864
- * const invalid = CapitalizedString.fromUnknown("evolu");
1865
- * assertErr(invalid);
1866
- * assertType(Data, invalid.error);
1867
- * assertEqual(invalid.error, {
1868
- * type: "Capitalized",
1869
- * value: "evolu",
1870
- * });
2589
+ * Checks the whole string using JavaScript's Unicode lowercase mapping. Empty
2590
+ * strings and uncased characters, such as digits and emoji, are valid. Use
2591
+ * {@link lowercase} to produce a lowercased value from any string.
2592
+ *
2593
+ * ### Example
2594
+ *
2595
+ * ```ts
2596
+ * import { assertErr, assertOk, LowercasedString } from "@evolu/common";
2597
+ *
2598
+ * const text: LowercasedString = "hello world";
2599
+ * assertOk(LowercasedString.fromUnknown(text), text);
2600
+ * assertErr(LowercasedString.fromUnknown("Hello WORLD"));
2601
+ * assertOk(LowercasedString.fromUnknown(""), "");
1871
2602
  * ```
1872
2603
  *
1873
2604
  * @group String
1874
2605
  */
1875
- export const capitalized = (parent) => brand("Capitalized", parent, (value) => {
1876
- const [first = ""] = value;
1877
- return value === first.toUpperCase() + value.slice(first.length)
1878
- ? ok()
1879
- : err({ type: "Capitalized", value });
1880
- }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`);
2606
+ export const LowercasedString = /*#__PURE__*/ lowercased(String);
1881
2607
  /**
1882
- * Capitalized {@link String}.
2608
+ * Lowercases the whole string and returns a {@link LowercasedString}.
2609
+ *
2610
+ * Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
2611
+ * without locale-specific rules. Changing case can change the length, so input
2612
+ * brands are not retained.
2613
+ *
2614
+ * ### Example
2615
+ *
2616
+ * ```ts
2617
+ * import { assertEqual, assertType, lowercase } from "@evolu/common";
2618
+ *
2619
+ * const text = lowercase("Hello WORLD");
2620
+ * assertEqual(text, "hello world");
2621
+ * assertType<typeof text, "hello world">();
2622
+ * assertEqual(lowercase(text), text);
2623
+ * ```
1883
2624
  *
1884
2625
  * @group String
1885
2626
  */
1886
- export const CapitalizedString = /*#__PURE__*/ capitalized(String);
2627
+ export const lowercase = (value) => value.toLowerCase();
1887
2628
  /**
1888
2629
  * String {@link Brand} without surrounding whitespace.
1889
2630
  *
@@ -1928,6 +2669,113 @@ export const TrimmedString = /*#__PURE__*/ trimmed(String);
1928
2669
  * @group String
1929
2670
  */
1930
2671
  export const trim = (value) => value.trim();
2672
+ /**
2673
+ * String {@link Brand} requiring an exact, case-sensitive prefix.
2674
+ *
2675
+ * Validation preserves the complete string, including the prefix. An empty
2676
+ * prefix accepts every string allowed by the parent Type. The prefix must be
2677
+ * one concrete string literal so different prefixes have distinct brands.
2678
+ *
2679
+ * ### Example
2680
+ *
2681
+ * ```ts
2682
+ * import {
2683
+ * assertEqual,
2684
+ * assertErr,
2685
+ * assertOk,
2686
+ * assertType,
2687
+ * maxLength,
2688
+ * startsWith,
2689
+ * String,
2690
+ * type Brand,
2691
+ * } from "@evolu/common";
2692
+ *
2693
+ * const EnvName = startsWith("APP_")(maxLength(64)(String));
2694
+ *
2695
+ * const name = EnvName.fromUnknown("APP_PORT");
2696
+ * assertOk(name, "APP_PORT");
2697
+ * assertType<
2698
+ * typeof name.value,
2699
+ * string & Brand<"MaxLength64"> & Brand<"StartsWithAPP_">
2700
+ * >();
2701
+ *
2702
+ * assertEqual(EnvName.to(name.value), "APP_PORT");
2703
+ *
2704
+ * assertErr(EnvName.fromUnknown("app_PORT"));
2705
+ * assertErr(EnvName.fromUnknown("APP_" + "X".repeat(61)));
2706
+ * ```
2707
+ *
2708
+ * @group String
2709
+ */
2710
+ export const startsWith = (prefix) => {
2711
+ const name = `StartsWith${prefix}`;
2712
+ return (parent) => brand(name, parent, (value) => value.startsWith(prefix)
2713
+ ? ok()
2714
+ : err({
2715
+ type: name,
2716
+ value,
2717
+ prefix,
2718
+ }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must start with ${safelyStringifyUnknownValue(error.prefix)}.`);
2719
+ };
2720
+ /**
2721
+ * Decodes a prefixed string with another {@link Type} and restores the prefix
2722
+ * when encoding.
2723
+ *
2724
+ * Uses {@link startsWith} to validate an exact, case-sensitive prefix before
2725
+ * removing one occurrence. The wrapped Type validates and decodes the suffix;
2726
+ * its constraints apply to the suffix, and its Output is preserved. Encoding
2727
+ * prepends the prefix to the wrapped Type's canonical string representation. An
2728
+ * empty prefix leaves that representation unchanged.
2729
+ *
2730
+ * The prefix must be one concrete string literal. The wrapped Type must accept
2731
+ * a string Input and encode to strings; its Output can have another type, as
2732
+ * with {@link PortFromString}.
2733
+ *
2734
+ * ### Example
2735
+ *
2736
+ * ```ts
2737
+ * import {
2738
+ * assertEqual,
2739
+ * assertErr,
2740
+ * assertOk,
2741
+ * assertType,
2742
+ * ConstantCaseIdentifier,
2743
+ * prefixed,
2744
+ * PortFromString,
2745
+ * type Port,
2746
+ * } from "@evolu/common";
2747
+ *
2748
+ * const EnvName = prefixed("APP_")(ConstantCaseIdentifier);
2749
+ *
2750
+ * const name = EnvName.fromUnknown("APP_PORT");
2751
+ * assertOk(name, "PORT");
2752
+ * assertType<typeof name.value, ConstantCaseIdentifier>();
2753
+ *
2754
+ * assertEqual(EnvName.to(name.value), "APP_PORT");
2755
+ *
2756
+ * assertErr(EnvName.fromUnknown("OTHER_PORT"));
2757
+ * assertErr(EnvName.fromUnknown("APP_port"));
2758
+ *
2759
+ * const PortSetting = prefixed("port:")(PortFromString);
2760
+ *
2761
+ * const port = PortSetting.fromUnknown("port:04000");
2762
+ * assertOk(port, 4000);
2763
+ * assertType<typeof port.value, Port>();
2764
+ *
2765
+ * assertEqual(PortSetting.to(port.value), "port:4000");
2766
+ * ```
2767
+ *
2768
+ * @group String
2769
+ */
2770
+ export const prefixed = (prefix) => (type) => {
2771
+ // A literal prefix makes the generated names concrete and distinct.
2772
+ const source = startsWith(prefix)(String);
2773
+ return transform(`Prefixed${prefix}`, source, type, {
2774
+ // The Input guard permits any string; concatenation establishes the prefix brand.
2775
+ from: (value) => ok(value.slice(prefix.length)),
2776
+ to: (value) => `${prefix}${value}`,
2777
+ });
2778
+ };
1931
2779
  /**
1932
2780
  * Minimum-length {@link Brand} for values whose `length` is at least `min`.
1933
2781
  *
@@ -2055,6 +2903,7 @@ export const length = (exact) => (parent) => {
2055
2903
  * assertType<UrlSafeString, string & Brand<"UrlSafeString">>();
2056
2904
  *
2057
2905
  * assertOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
2906
+ *
2058
2907
  * const invalid = UrlSafeString.fromUnknown("not safe");
2059
2908
  * assertErr(invalid);
2060
2909
  * assertType(Data, invalid.error);
@@ -2183,7 +3032,13 @@ export const uint8ArrayToBase64Url = (bytes) => uint8ArrayToBase64UrlString(byte
2183
3032
  */
2184
3033
  export const base64UrlToUint8Array = (value) => base64UrlStringToUint8Array(value);
2185
3034
  /**
2186
- * A non-empty URL-safe name containing at most 64 UTF-16 code units.
3035
+ * A non-empty file-system-safe and URL-safe token of at most 64 UTF-16 code
3036
+ * units.
3037
+ *
3038
+ * Evolu uses it for database file names, storage pool names, and log prefixes.
3039
+ * It accepts the {@link UrlSafeString} alphabet in any order, so it may start
3040
+ * with a digit, `-`, or `_`. It is not a language identifier; for
3041
+ * word-structured names, use {@link identifier}.
2187
3042
  *
2188
3043
  * @group String
2189
3044
  */
@@ -2643,6 +3498,7 @@ export const PositiveFiniteNumber = /*#__PURE__*/ positive(NonNegativeFiniteNumb
2643
3498
  * assertType<Int, number & Brand<"Int">>();
2644
3499
  *
2645
3500
  * assertOk(Int.fromUnknown(42), 42);
3501
+ *
2646
3502
  * const invalid = Int.fromUnknown(1.5);
2647
3503
  * assertErr(invalid);
2648
3504
  * assertType(Data, invalid.error);
@@ -2682,6 +3538,49 @@ export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
2682
3538
  * @group Number
2683
3539
  */
2684
3540
  export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
3541
+ /**
3542
+ * Transforms a decimal integer string into an {@link Int}.
3543
+ *
3544
+ * This is useful for inputs that carry numbers as text, such as environment
3545
+ * variables, URL query parameters, and form fields. The string must consist of
3546
+ * an optional minus sign and digits; the {@link Int} constraint then rejects
3547
+ * values outside the safe integer range.
3548
+ *
3549
+ * ### Example
3550
+ *
3551
+ * ```ts
3552
+ * import {
3553
+ * assertEqual,
3554
+ * assertErr,
3555
+ * assertOk,
3556
+ * assertSame,
3557
+ * IntFromString,
3558
+ * } from "@evolu/common";
3559
+ *
3560
+ * assertOk(IntFromString.fromUnknown("4000"), 4000);
3561
+ * assertOk(IntFromString.fromUnknown("-1"), -1);
3562
+ * assertEqual(IntFromString.to(IntFromString.orThrow("42")), "42");
3563
+ *
3564
+ * const negativeZero = IntFromString.orThrow("-0");
3565
+ * assertSame(negativeZero, -0);
3566
+ * assertEqual(IntFromString.to(negativeZero), "-0");
3567
+ *
3568
+ * const invalid = IntFromString.fromUnknown("4000.5");
3569
+ * assertErr(invalid, { type: "IntFromString", value: "4000.5" });
3570
+ * assertEqual(
3571
+ * IntFromString.formatError(invalid.error),
3572
+ * 'The value "4000.5" is not a decimal integer.',
3573
+ * );
3574
+ * ```
3575
+ *
3576
+ * @group Number
3577
+ */
3578
+ export const IntFromString = /*#__PURE__*/ transform("IntFromString", String, Int, {
3579
+ from: (value) => /^-?\d+$/u.test(value)
3580
+ ? ok(globalThis.Number(value))
3581
+ : err({ type: "IntFromString", value }),
3582
+ to: (value) => globalThis.Object.is(value, -0) ? "-0" : globalThis.String(value),
3583
+ }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a decimal integer.`);
2685
3584
  /**
2686
3585
  * Minimum {@link PositiveInt} value.
2687
3586
  *
@@ -2805,6 +3704,55 @@ export const lessThanOrEqualTo = (max) => (parent) => {
2805
3704
  ? ok()
2806
3705
  : err({ type: name, value, max }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be less than or equal to ${error.max}.`);
2807
3706
  };
3707
+ /**
3708
+ * A TCP or UDP port as an integer from zero through 65535, inclusive.
3709
+ *
3710
+ * When binding a server, zero requests an automatically assigned port. Use
3711
+ * {@link PortFromString} for configuration values supplied as text.
3712
+ *
3713
+ * ### Example
3714
+ *
3715
+ * ```ts
3716
+ * import { assertErr, assertOk, Port } from "@evolu/common";
3717
+ *
3718
+ * assertOk(Port.fromUnknown(0), 0);
3719
+ * assertOk(Port.fromUnknown(4000), 4000);
3720
+ * assertOk(Port.fromUnknown(65535), 65535);
3721
+ * assertErr(Port.fromUnknown(-1));
3722
+ * assertErr(Port.fromUnknown(65536));
3723
+ * assertErr(Port.fromUnknown(4000.5));
3724
+ * ```
3725
+ *
3726
+ * @group Number
3727
+ */
3728
+ export const Port = /*#__PURE__*/ brand("Port",
3729
+ /*#__PURE__*/ lessThanOrEqualTo(65535)(NonNegativeInt));
3730
+ /**
3731
+ * Parses a decimal integer string and validates it as a {@link Port}.
3732
+ *
3733
+ * Uses {@link IntFromString} for decimal parsing, including its rejection of
3734
+ * whitespace, plus signs, fractions, and exponent notation.
3735
+ *
3736
+ * ### Example
3737
+ *
3738
+ * ```ts
3739
+ * import {
3740
+ * assertEqual,
3741
+ * assertErr,
3742
+ * assertOk,
3743
+ * PortFromString,
3744
+ * } from "@evolu/common";
3745
+ *
3746
+ * assertOk(PortFromString.fromUnknown("0"), 0);
3747
+ * assertOk(PortFromString.fromUnknown("4000"), 4000);
3748
+ * assertErr(PortFromString.fromUnknown("65536"));
3749
+ * assertErr(PortFromString.fromUnknown("http"));
3750
+ * assertEqual(PortFromString.to(PortFromString.orThrow("04000")), "4000");
3751
+ * ```
3752
+ *
3753
+ * @group Number
3754
+ */
3755
+ export const PortFromString = /*#__PURE__*/ transform("PortFromString", IntFromString, Port, { from: ok, to: identity });
2808
3756
  /**
2809
3757
  * Finite {@link Number} from zero to one, inclusive.
2810
3758
  *
@@ -3033,6 +3981,7 @@ export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(NonPosi
3033
3981
  * >();
3034
3982
  *
3035
3983
  * assertOk(Tenths.fromUnknown(0.3), 0.3);
3984
+ *
3036
3985
  * const invalid = Tenths.fromUnknown(0.31);
3037
3986
  * assertErr(invalid);
3038
3987
  * assertType(Data, invalid.error);
@@ -3152,6 +4101,7 @@ export const between = (min, max) => (parent) => {
3152
4101
  *
3153
4102
  * const UserId = brand("UserId", String);
3154
4103
  * const UserIds = array(UserId);
4104
+ *
3155
4105
  * const result = UserIds.from.parent(["ada", "grace"]);
3156
4106
  *
3157
4107
  * assertType<
@@ -3160,6 +4110,7 @@ export const between = (min, max) => (parent) => {
3160
4110
  * >();
3161
4111
  * assertOk(result, ["ada", "grace"]);
3162
4112
  * assertOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
4113
+ *
3163
4114
  * const invalid = UserIds.fromUnknown("ada");
3164
4115
  * assertErr(invalid);
3165
4116
  * assertType(Data, invalid.error);
@@ -3731,25 +4682,25 @@ const createTupleType = (typeElements) => {
3731
4682
  };
3732
4683
  const validateTupleItems = (value, elements, validate, options, checkStructure) => validateIndexedArrayItems("Tuple", value, (value, elementOptions, index) => validate(elements[index], value, elementOptions, index), options, checkStructure);
3733
4684
  /**
3734
- * Decimal digit from `"0"` to `"9"`.
4685
+ * Decimal integer string from `"0"` to `"9"`.
3735
4686
  *
3736
4687
  * @group String
3737
4688
  */
3738
4689
  export const Digit = /*#__PURE__*/ union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
3739
4690
  /**
3740
- * Decimal digit from `"1"` to `"9"`.
4691
+ * Decimal integer string from `"1"` to `"9"`.
3741
4692
  *
3742
4693
  * @group String
3743
4694
  */
3744
4695
  export const Digit1To9 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6", "7", "8", "9");
3745
4696
  /**
3746
- * Decimal string from `"1"` to `"6"`.
4697
+ * Decimal integer string from `"1"` to `"6"`.
3747
4698
  *
3748
4699
  * @group String
3749
4700
  */
3750
4701
  export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
3751
4702
  /**
3752
- * Decimal string from `"1"` to `"23"`.
4703
+ * Decimal integer string from `"1"` to `"23"`, without a leading zero.
3753
4704
  *
3754
4705
  * @group String
3755
4706
  */
@@ -3757,7 +4708,7 @@ export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
3757
4708
  /*#__PURE__*/ templateLiteral("1", Digit),
3758
4709
  /*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")));
3759
4710
  /**
3760
- * Decimal string from `"1"` to `"51"`.
4711
+ * Decimal integer string from `"1"` to `"51"`, without a leading zero.
3761
4712
  *
3762
4713
  * @group String
3763
4714
  */
@@ -3765,14 +4716,14 @@ export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
3765
4716
  /*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
3766
4717
  /*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
3767
4718
  /**
3768
- * Decimal string from `"1"` to `"99"`.
4719
+ * Decimal integer string from `"1"` to `"99"`, without a leading zero.
3769
4720
  *
3770
4721
  * @group String
3771
4722
  */
3772
4723
  export const Digit1To99 = /*#__PURE__*/ union(Digit1To9,
3773
4724
  /*#__PURE__*/ templateLiteral(Digit1To9, Digit));
3774
4725
  /**
3775
- * Decimal string from `"1"` to `"59"`.
4726
+ * Decimal integer string from `"1"` to `"59"`, without a leading zero.
3776
4727
  *
3777
4728
  * @group String
3778
4729
  */
@@ -4026,6 +4977,7 @@ export { _Object as Object };
4026
4977
  * const valueType = typeof value;
4027
4978
  *
4028
4979
  * assertEqual(valueType, "function");
4980
+ *
4029
4981
  * const called = trySync(
4030
4982
  * () => {
4031
4983
  * if (value !== undefined) value.toFixed(0);
@@ -4061,6 +5013,7 @@ export { _Object as Object };
4061
5013
  * const valueType = typeof value;
4062
5014
  *
4063
5015
  * assertEqual(valueType, "function");
5016
+ *
4064
5017
  * const called = trySync(
4065
5018
  * () => {
4066
5019
  * if (value !== undefined) value.toFixed(0);
@@ -4279,16 +5232,116 @@ const validateRecordEntries = (input, validateKey, validateValue, options) => {
4279
5232
  * @group Objects
4280
5233
  */
4281
5234
  export const optional = (type) => createOptionalProperty(type);
4282
- const optionalPropertySymbol = /*#__PURE__*/ globalThis.Symbol();
5235
+ const optionalPropertySymbol =
5236
+ /*#__PURE__*/ globalThis.Symbol();
4283
5237
  const createOptionalProperty = (type) => ({
4284
5238
  type,
4285
5239
  [optionalPropertySymbol]: true,
4286
5240
  });
5241
+ export function withDefault(property, value, { strategy } = {}) {
5242
+ assert(strategy === undefined || strategy === "preserve", 'withDefault strategy must be omitted or "preserve".');
5243
+ const optionalInput = optionalPropertySymbol in property;
5244
+ const type = (optionalInput ? property.type : property);
5245
+ assert(type.is(value), `withDefault value must be an Output of ${type.name}.`);
5246
+ const originals = new Set([
5247
+ ...(optionalInput ? ["missing"] : []),
5248
+ ...(type.is(null) ? ["null"] : []),
5249
+ ...(type.is(undefined) ? ["undefined"] : []),
5250
+ ]);
5251
+ const supplied = createRootType("DefaultSupplied", (candidate) => (strategy === undefined && globalThis.Object.is(candidate, value)) ||
5252
+ (candidate !== null && candidate !== undefined && type.is(candidate))
5253
+ ? ok(candidate)
5254
+ : err({ type: "DefaultValue" }), () => "The value must be a non-nullish Output of its Type or, in replacement mode, the configured default.");
5255
+ const defaultValue = createRootType("DefaultValue", (candidate) => type.is(candidate) &&
5256
+ (globalThis.Object.is(candidate, value) ||
5257
+ (Data.is(value) && Data.is(candidate) && eqData(candidate, value)))
5258
+ ? ok(candidate)
5259
+ : err({ type: "DefaultValue" }), () => "A preserved default must equal the configured default value.");
5260
+ const output = (strategy === undefined
5261
+ ? supplied
5262
+ : discriminatedUnion("defaultUsed", object({ value: supplied, defaultUsed: literal(false) }), object({
5263
+ value: defaultValue,
5264
+ defaultUsed: literal(true),
5265
+ original: createRootType("DefaultOriginal", (candidate) => typeof candidate === "string" && originals.has(candidate)
5266
+ ? ok(candidate)
5267
+ : err({ type: "DefaultOriginal" }), () => "The original absence must be handled by this default declaration."),
5268
+ })));
5269
+ const ownGetTypeIssues = output[getRuntimeTypeIssuesSymbol];
5270
+ const outputWithIssues = {
5271
+ ...output,
5272
+ [outputValidationSymbol]: (value, options) => {
5273
+ const result = output[outputValidationSymbol](value, options);
5274
+ return result.ok
5275
+ ? result
5276
+ : err({ type: "WithDefault", outputError: result.error });
5277
+ },
5278
+ [getRuntimeTypeIssuesSymbol]: (error, mode) => error.type === "WithDefault" && "outputError" in error
5279
+ ? ownGetTypeIssues(error.outputError, mode)
5280
+ : type[getRuntimeTypeIssuesSymbol](error, mode),
5281
+ };
5282
+ const operations = {
5283
+ output: outputWithIssues,
5284
+ createObject: createDefaultObjectType,
5285
+ partial: (type) => createOptionalProperty(strategy === "preserve"
5286
+ ? withDefault(type, value, {
5287
+ strategy,
5288
+ })
5289
+ : withDefault(type, value)),
5290
+ decode: (candidate) => {
5291
+ const original = candidate === missingDefaultValue
5292
+ ? "missing"
5293
+ : candidate === null
5294
+ ? "null"
5295
+ : candidate === undefined
5296
+ ? "undefined"
5297
+ : undefined;
5298
+ const effective = original === undefined ? candidate : value;
5299
+ return strategy === undefined
5300
+ ? effective
5301
+ : original === undefined
5302
+ ? { value: effective, defaultUsed: false }
5303
+ : { value: effective, defaultUsed: true, original };
5304
+ },
5305
+ encode: (candidate) => {
5306
+ if (strategy === undefined)
5307
+ return candidate;
5308
+ const preserved = candidate;
5309
+ if (!preserved.defaultUsed)
5310
+ return preserved.value;
5311
+ switch (preserved.original) {
5312
+ case "missing":
5313
+ return missingDefaultValue;
5314
+ case "null":
5315
+ return null;
5316
+ case "undefined":
5317
+ return undefined;
5318
+ }
5319
+ },
5320
+ };
5321
+ if (optionalInput) {
5322
+ return {
5323
+ type,
5324
+ value,
5325
+ strategy: strategy ?? "replace",
5326
+ [defaultPropertySymbol]: operations,
5327
+ };
5328
+ }
5329
+ const fromOwn = (value) => ok(operations.decode(value));
5330
+ const fromParent = mapRuntimeOperations(type[fromSymbol], (operation) => (value, options) => flatMapResult(operation(value, options), fromOwn));
5331
+ return createTypeNode("WithDefault", type, (value, options) => flatMapResult(type.fromUnknown(value, options), fromOwn), operations.output.is, operations.output[outputValidationSymbol], createFromOperation(fromParent), operations.encode, operations.output[getRuntimeTypeIssuesSymbol]);
5332
+ }
5333
+ const defaultPropertySymbol =
5334
+ /*#__PURE__*/ globalThis.Symbol();
5335
+ const missingDefaultValue = /*#__PURE__*/ globalThis.Symbol();
4287
5336
  export function object(props, recordType) {
4288
5337
  return createObjectType(snapshotObjectProps(props), recordType);
4289
5338
  }
4290
5339
  const createObjectType = (props, recordType) => {
4291
5340
  const runtimeProps = props;
5341
+ const defaultProperty = globalThis.Object.values(runtimeProps).find(isDefaultProperty);
5342
+ if (defaultProperty) {
5343
+ return defaultProperty[defaultPropertySymbol].createObject(props, recordType);
5344
+ }
4292
5345
  const keys = globalThis.Object.keys(runtimeProps);
4293
5346
  const validate = (value, options, exactOutput) => {
4294
5347
  if (value === null || typeof value !== "object") {
@@ -4598,6 +5651,57 @@ const createObjectType = (props, recordType) => {
4598
5651
  }
4599
5652
  : { props: runtimeProps });
4600
5653
  };
5654
+ const createDefaultObjectType = (props, recordType) => {
5655
+ const inputProps = createMutableRecord();
5656
+ const outputProps = createMutableRecord();
5657
+ const defaults = new Map();
5658
+ for (const [key, property] of globalThis.Object.entries(props)) {
5659
+ if (isDefaultProperty(property)) {
5660
+ const defaultProperty = property;
5661
+ inputProps[key] = createOptionalProperty(defaultProperty.type);
5662
+ outputProps[key] = defaultProperty[defaultPropertySymbol].output;
5663
+ defaults.set(key, defaultProperty[defaultPropertySymbol]);
5664
+ }
5665
+ else {
5666
+ inputProps[key] = property;
5667
+ const type = objectPropertyToType(property);
5668
+ const output = createRootType(type.name, type[outputValidationSymbol], type.formatError, type[getRuntimeTypeIssuesSymbol]);
5669
+ outputProps[key] =
5670
+ optionalPropertySymbol in property
5671
+ ? createOptionalProperty(output)
5672
+ : output;
5673
+ }
5674
+ }
5675
+ const source = createObjectType(inputProps, recordType);
5676
+ // The object parent validates only its input structure and root value Types.
5677
+ const parent = getTerminalRuntimeNode(source);
5678
+ const output = createObjectType(outputProps, recordType);
5679
+ const finish = (value) => {
5680
+ const result = createMutableRecord(value);
5681
+ for (const [key, operations] of defaults) {
5682
+ result[key] = operations.decode(globalThis.Object.hasOwn(result, key)
5683
+ ? result[key]
5684
+ : missingDefaultValue);
5685
+ }
5686
+ return ok(result);
5687
+ };
5688
+ const fromInput = getTerminalRuntimeNode(source[fromSymbol]);
5689
+ const fromParent = (value, options) => flatMapResult(fromInput(value, options), finish);
5690
+ const to = (value) => {
5691
+ const result = createMutableRecord(value);
5692
+ for (const [key, operations] of defaults) {
5693
+ const original = operations.encode(result[key]);
5694
+ if (original === missingDefaultValue)
5695
+ delete result[key];
5696
+ else
5697
+ result[key] = original;
5698
+ }
5699
+ return source[encoderSymbol](result);
5700
+ };
5701
+ return createTypeNode("Object", parent, (value, options) => flatMapResult(source.fromUnknown(value, options), finish), output.is, output[outputValidationSymbol], createFromOperation(fromParent), to, createObjectRuntimeTypeIssues(source.formatError, props, recordType), recordType
5702
+ ? { props, record: recordType }
5703
+ : { props });
5704
+ };
4601
5705
  // Read descriptors instead of spreading so accessors are not invoked,
4602
5706
  // non-enumerable declarations are retained, and later mutations are isolated.
4603
5707
  const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
@@ -4613,7 +5717,12 @@ const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
4613
5717
  return runtimeProps;
4614
5718
  };
4615
5719
  const isOptionalProperty = (property) => optionalPropertySymbol in property;
4616
- const objectPropertyToType = (property) => (isOptionalProperty(property) ? property.type : property);
5720
+ const isDefaultProperty = (property) => defaultPropertySymbol in property;
5721
+ const objectPropertyToType = (property) => isDefaultProperty(property)
5722
+ ? property[defaultPropertySymbol].output
5723
+ : isOptionalProperty(property)
5724
+ ? property.type
5725
+ : property;
4617
5726
  const createRecordPropertyError = (issue) => ({
4618
5727
  type: "Record",
4619
5728
  reason: { kind: "Entries", issues: [issue] },
@@ -4622,7 +5731,8 @@ const createRecordPropertyError = (issue) => ({
4622
5731
  * Object {@link Type} with every property optional.
4623
5732
  *
4624
5733
  * No property is required, but every present property must still satisfy its
4625
- * Type.
5734
+ * Type. For {@link withDefault} properties, this disables the missing-property
5735
+ * default; defaults for present `null` or `undefined` values still apply.
4626
5736
  *
4627
5737
  * ### Example
4628
5738
  *
@@ -4650,9 +5760,11 @@ export const partial = (props, ..._validation) => {
4650
5760
  const partialProps = createMutableRecord();
4651
5761
  for (const key of globalThis.Object.keys(source)) {
4652
5762
  const property = source[key];
4653
- partialProps[key] = isOptionalProperty(property)
4654
- ? property
4655
- : createOptionalProperty(property);
5763
+ partialProps[key] = isDefaultProperty(property)
5764
+ ? property[defaultPropertySymbol].partial(property.type)
5765
+ : isOptionalProperty(property)
5766
+ ? property
5767
+ : createOptionalProperty(property);
4656
5768
  }
4657
5769
  return createObjectType(partialProps);
4658
5770
  };
@@ -4693,7 +5805,7 @@ export const nullableToOptional = (props, ..._validation) => {
4693
5805
  const optionalProps = createMutableRecord();
4694
5806
  for (const key of globalThis.Object.keys(source)) {
4695
5807
  const property = source[key];
4696
- if (isOptionalProperty(property)) {
5808
+ if (isOptionalProperty(property) || isDefaultProperty(property)) {
4697
5809
  optionalProps[key] = property;
4698
5810
  continue;
4699
5811
  }
@@ -4732,6 +5844,101 @@ export const omit = (objectType, ...keys) => {
4732
5844
  }
4733
5845
  return createObjectType(props, runtimeObjectType.record);
4734
5846
  };
5847
+ /**
5848
+ * Renames a strict {@link object} Type's encoded keys using a string codec.
5849
+ *
5850
+ * The object declaration uses semantic keys. Each key must be a valid Output of
5851
+ * the key Type. Its canonical encoding becomes the external property name.
5852
+ * Decoding accepts those exact names; aliases and unknown properties are
5853
+ * rejected. Values, optionality, and semantic Output stay with the object Type.
5854
+ * Only the outer keys change. Nested field codecs keep their own behavior.
5855
+ *
5856
+ * Construction rejects invalid schema keys, duplicate encodings, and key codecs
5857
+ * that do not decode their encodings back to the declared keys. These are
5858
+ * schema mistakes; invalid external values return normal Type errors. Error
5859
+ * paths use the external names, including missing required properties.
5860
+ *
5861
+ * ### Example
5862
+ *
5863
+ * ```ts
5864
+ * import {
5865
+ * assertEqual,
5866
+ * assertErr,
5867
+ * assertOk,
5868
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
5869
+ * object,
5870
+ * objectKeys,
5871
+ * optional,
5872
+ * PortFromString,
5873
+ * prefixed,
5874
+ * } from "@evolu/common";
5875
+ *
5876
+ * const Key = prefixed("APP_")(
5877
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
5878
+ * );
5879
+ * const Settings = objectKeys(Key)(
5880
+ * object({ port: optional(PortFromString) }),
5881
+ * );
5882
+ *
5883
+ * const result = Settings.fromUnknown({ APP_PORT: "04000" });
5884
+ * assertOk(result, { port: 4000 });
5885
+ *
5886
+ * assertEqual(Settings.to(result.value), { APP_PORT: "4000" });
5887
+ *
5888
+ * assertErr(Settings.fromUnknown({ APP_POTR: "4000" }));
5889
+ * ```
5890
+ *
5891
+ * @group Objects
5892
+ */
5893
+ export const objectKeys = (key) => (type) => {
5894
+ const runtimeKey = key;
5895
+ const runtimeOutput = type;
5896
+ const props = createMutableRecord();
5897
+ const outputKeyByInputKey = new Map();
5898
+ const inputKeyByOutputKey = new Map();
5899
+ for (const outputKey of globalThis.Object.keys(type.props)) {
5900
+ assert(runtimeKey.is(outputKey), `Invalid object schema key ${safelyStringifyUnknownValue(outputKey)} for ${key.name}.`);
5901
+ const inputKey = runtimeKey.to(outputKey);
5902
+ assert(!outputKeyByInputKey.has(inputKey), `Duplicate encoded object key ${safelyStringifyUnknownValue(inputKey)}.`);
5903
+ const decoded = runtimeKey.fromUnknown(inputKey, firstValidationOptions);
5904
+ assert(decoded.ok && decoded.value === outputKey, "An object key codec must decode its encoding to the declared key.");
5905
+ props[inputKey] = type.props[outputKey];
5906
+ outputKeyByInputKey.set(inputKey, outputKey);
5907
+ inputKeyByOutputKey.set(outputKey, inputKey);
5908
+ }
5909
+ const input = createObjectType(props);
5910
+ const fromUnknown = (value, options = firstValidationOptions) => {
5911
+ const result = input.fromUnknown(value, options);
5912
+ if (!result.ok)
5913
+ return err({ type: "ObjectKeys", error: result.error });
5914
+ const output = createMutableRecord();
5915
+ for (const [inputKey, value] of globalThis.Object.entries(result.value)) {
5916
+ output[outputKeyByInputKey.get(inputKey)] = value;
5917
+ }
5918
+ return ok(output);
5919
+ };
5920
+ const to = (value) => {
5921
+ const encoded = runtimeOutput[encoderSymbol](value);
5922
+ const input = createMutableRecord();
5923
+ for (const [key, value] of globalThis.Object.entries(encoded)) {
5924
+ input[inputKeyByOutputKey.get(key)] = value;
5925
+ }
5926
+ return input;
5927
+ };
5928
+ const validateOutput = (value, options) => {
5929
+ const result = runtimeOutput[outputValidationSymbol](value, options);
5930
+ return result.ok
5931
+ ? result
5932
+ : err({ type: "ObjectKeys", outputError: result.error });
5933
+ };
5934
+ const getTypeIssues = (error, mode) => {
5935
+ if ("outputError" in error) {
5936
+ return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
5937
+ }
5938
+ return input[getRuntimeTypeIssuesSymbol](error.error, mode);
5939
+ };
5940
+ return createTypeNode("ObjectKeys", Unknown, fromUnknown, runtimeOutput.is, validateOutput, createFromOperation(fromUnknown), to, getTypeIssues, { key, output: type });
5941
+ };
4735
5942
  export function result(okType, errorType) {
4736
5943
  return discriminatedUnion("ok", createObjectType({
4737
5944
  ok: literal(true),
@@ -5746,7 +6953,7 @@ export const json = (type, name, ..._validation) => {
5746
6953
  const BrandedJson = {
5747
6954
  ...typeNode,
5748
6955
  from,
5749
- orThrow: mapRuntimeResult(fromInput, getOrThrow),
6956
+ orThrow: createRuntimeOrThrow(fromInput, typeNode.formatError),
5750
6957
  orNull: mapRuntimeResult(fromInput, getOrNull),
5751
6958
  };
5752
6959
  return [