attaform 0.0.1 → 0.14.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/dist/chunks/devtools.cjs +179 -0
  4. package/dist/chunks/devtools.cjs.map +1 -0
  5. package/dist/chunks/devtools.mjs +177 -0
  6. package/dist/chunks/devtools.mjs.map +1 -0
  7. package/dist/chunks/indexeddb.cjs +119 -0
  8. package/dist/chunks/indexeddb.cjs.map +1 -0
  9. package/dist/chunks/indexeddb.mjs +117 -0
  10. package/dist/chunks/indexeddb.mjs.map +1 -0
  11. package/dist/chunks/local-storage.cjs +58 -0
  12. package/dist/chunks/local-storage.cjs.map +1 -0
  13. package/dist/chunks/local-storage.mjs +56 -0
  14. package/dist/chunks/local-storage.mjs.map +1 -0
  15. package/dist/chunks/session-storage.cjs +58 -0
  16. package/dist/chunks/session-storage.cjs.map +1 -0
  17. package/dist/chunks/session-storage.mjs +56 -0
  18. package/dist/chunks/session-storage.mjs.map +1 -0
  19. package/dist/index.cjs +173 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +493 -0
  22. package/dist/index.d.mts +493 -0
  23. package/dist/index.d.ts +493 -0
  24. package/dist/index.mjs +141 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/nuxt.cjs +97 -0
  27. package/dist/nuxt.cjs.map +1 -0
  28. package/dist/nuxt.d.cts +38 -0
  29. package/dist/nuxt.d.mts +38 -0
  30. package/dist/nuxt.d.ts +38 -0
  31. package/dist/nuxt.mjs +94 -0
  32. package/dist/nuxt.mjs.map +1 -0
  33. package/dist/runtime/plugins/attaform.cjs +32 -0
  34. package/dist/runtime/plugins/attaform.cjs.map +1 -0
  35. package/dist/runtime/plugins/attaform.d.cts +5 -0
  36. package/dist/runtime/plugins/attaform.d.mts +5 -0
  37. package/dist/runtime/plugins/attaform.d.ts +5 -0
  38. package/dist/runtime/plugins/attaform.mjs +30 -0
  39. package/dist/runtime/plugins/attaform.mjs.map +1 -0
  40. package/dist/shared/attaform.B5GWYl76.cjs +386 -0
  41. package/dist/shared/attaform.B5GWYl76.cjs.map +1 -0
  42. package/dist/shared/attaform.BRTxpA3q.mjs +3283 -0
  43. package/dist/shared/attaform.BRTxpA3q.mjs.map +1 -0
  44. package/dist/shared/attaform.BYc9kugA.d.ts +124 -0
  45. package/dist/shared/attaform.Bubm_slq.cjs +622 -0
  46. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -0
  47. package/dist/shared/attaform.BwaYWtMs.d.cts +126 -0
  48. package/dist/shared/attaform.BwaYWtMs.d.mts +126 -0
  49. package/dist/shared/attaform.BwaYWtMs.d.ts +126 -0
  50. package/dist/shared/attaform.CNJO3mME.cjs +3295 -0
  51. package/dist/shared/attaform.CNJO3mME.cjs.map +1 -0
  52. package/dist/shared/attaform.CRgix6_n.cjs +796 -0
  53. package/dist/shared/attaform.CRgix6_n.cjs.map +1 -0
  54. package/dist/shared/attaform.CXZgUECn.d.cts +124 -0
  55. package/dist/shared/attaform.CXpzmj38.mjs +617 -0
  56. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -0
  57. package/dist/shared/attaform.Cc93zNzD.mjs +83 -0
  58. package/dist/shared/attaform.Cc93zNzD.mjs.map +1 -0
  59. package/dist/shared/attaform.DDXrY-1Q.d.cts +2568 -0
  60. package/dist/shared/attaform.DDXrY-1Q.d.mts +2568 -0
  61. package/dist/shared/attaform.DDXrY-1Q.d.ts +2568 -0
  62. package/dist/shared/attaform.DOKOyb3Y.d.mts +124 -0
  63. package/dist/shared/attaform.DlgKK10S.mjs +789 -0
  64. package/dist/shared/attaform.DlgKK10S.mjs.map +1 -0
  65. package/dist/shared/attaform.al_rpt7_.mjs +361 -0
  66. package/dist/shared/attaform.al_rpt7_.mjs.map +1 -0
  67. package/dist/shared/attaform.xKWYHMdq.cjs +89 -0
  68. package/dist/shared/attaform.xKWYHMdq.cjs.map +1 -0
  69. package/dist/transforms.cjs +11 -0
  70. package/dist/transforms.cjs.map +1 -0
  71. package/dist/transforms.d.cts +49 -0
  72. package/dist/transforms.d.mts +49 -0
  73. package/dist/transforms.d.ts +49 -0
  74. package/dist/transforms.mjs +2 -0
  75. package/dist/transforms.mjs.map +1 -0
  76. package/dist/vite.cjs +39 -0
  77. package/dist/vite.cjs.map +1 -0
  78. package/dist/vite.d.cts +53 -0
  79. package/dist/vite.d.mts +53 -0
  80. package/dist/vite.d.ts +53 -0
  81. package/dist/vite.mjs +37 -0
  82. package/dist/vite.mjs.map +1 -0
  83. package/dist/zod-v3.cjs +1511 -0
  84. package/dist/zod-v3.cjs.map +1 -0
  85. package/dist/zod-v3.d.cts +164 -0
  86. package/dist/zod-v3.d.mts +164 -0
  87. package/dist/zod-v3.d.ts +164 -0
  88. package/dist/zod-v3.mjs +1504 -0
  89. package/dist/zod-v3.mjs.map +1 -0
  90. package/dist/zod.cjs +1548 -0
  91. package/dist/zod.cjs.map +1 -0
  92. package/dist/zod.d.cts +67 -0
  93. package/dist/zod.d.mts +67 -0
  94. package/dist/zod.d.ts +67 -0
  95. package/dist/zod.mjs +1541 -0
  96. package/dist/zod.mjs.map +1 -0
  97. package/package.json +182 -6
@@ -0,0 +1,493 @@
1
+ import { Plugin, App, InjectionKey } from 'vue';
2
+ import { A as AttaformDefaults, C as CoercionRegistry, S as SlimPrimitiveKind, a as CoercionEntry, F as FormKey, G as GenericForm, U as UseFormConfiguration, b as AbstractSchema, D as DeepPartial, c as DefaultValuesShape, d as UseFormReturnType, R as RegisterValue, e as RegisterModelDynamicCustomDirective, V as ValidationError, f as ApiErrorEnvelope, g as ApiErrorDetails } from './shared/attaform.DDXrY-1Q.js';
3
+ export { h as ApiErrorEntry, i as CoercionResult, j as CustomDirectiveRegisterAssignerFn, k as DefaultValuesResponse, l as FieldState, m as FieldStateLeaf, n as FieldStateMap, o as FieldStateMapEntry, p as FlatPath, q as FormErrorRecord, r as FormErrorsSurface, s as FormMeta, t as FormStorage, u as FormStorageKind, H as HandleSubmit, v as HistoryConfig, I as IsTuple, M as MetaTrackerValue, N as NestedReadType, w as NestedType, O as OnError, x as OnInvalidSubmitPolicy, y as OnSubmit, P as Path, z as PathKey, B as PendingValidationStatus, E as PersistConfig, J as PersistConfigOptions, K as PersistIncludeMode, L as ROOT_PATH, Q as ROOT_PATH_KEY, T as ReactiveValidationStatus, W as RegisterDirective, X as RegisterFlatPath, Y as RegisterOptions, Z as RegisterSelectModifier, _ as RegisterTextModifier, $ as RegisterTransform, a0 as Segment, a1 as SetValueCallback, a2 as SetValuePayload, a3 as SettledValidationStatus, a4 as SlimRuntimeOf, a5 as SubmitHandler, a6 as Unset, a7 as ValidateOn, a8 as ValidateOnConfig, a9 as ValidationResponse, aa as ValidationResponseWithoutValue, ab as WriteMeta, ac as canonicalizePath, ad as isUnset, ae as parseDottedPath, af as unset } from './shared/attaform.DDXrY-1Q.js';
4
+ export { A as AttaformErrorCode, i as injectForm, u as useRegister } from './shared/attaform.BYc9kugA.js';
5
+ export { A as AnonPersistError, a as AttaformError, I as InvalidPathError, O as OutsideSetupError, R as RegistryNotInstalledError, b as ReservedFormKeyError, S as SensitivePersistFieldError, c as SubmitErrorHandlerError } from './shared/attaform.BwaYWtMs.js';
6
+
7
+ /**
8
+ * Portable SSR detection. The plugin captures this value at install time and
9
+ * exposes it via the registry so every runtime branch reads a single source
10
+ * of truth instead of sniffing `import.meta.*` (bundler-specific) at each
11
+ * call site.
12
+ *
13
+ * Consumers can override explicitly via `createAttaform({ ssr: true })`;
14
+ * the default heuristic handles the common Node-vs-browser split without
15
+ * relying on any bundler-injected flag.
16
+ */
17
+ interface SSRDetectOptions {
18
+ override?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Options for `createAttaform()`.
23
+ */
24
+ type AttaformPluginOptions = SSRDetectOptions & {
25
+ /**
26
+ * Whether to install the Vue DevTools integration. Default `true`.
27
+ * The DevTools peer dependency is loaded lazily — in production
28
+ * builds where it's absent, the import fails silently and no
29
+ * extra bundle is shipped. Pass `false` to skip even attempting
30
+ * the import.
31
+ */
32
+ devtools?: boolean;
33
+ /**
34
+ * App-level defaults applied to every `useForm` call in this app.
35
+ * Per-form options always win. See `AttaformDefaults` for
36
+ * the supported option set and the merge rules.
37
+ *
38
+ * ```ts
39
+ * app.use(
40
+ * createAttaform({
41
+ * defaults: { debounceMs: 100 },
42
+ * })
43
+ * )
44
+ * ```
45
+ */
46
+ defaults?: AttaformDefaults;
47
+ };
48
+ /**
49
+ * Create the Vue plugin that installs the form library on a Vue
50
+ * application. Call once per app, then `app.use(...)` the result.
51
+ *
52
+ * ```ts
53
+ * import { createApp } from 'vue'
54
+ * import { createAttaform } from 'attaform'
55
+ *
56
+ * createApp(App)
57
+ * .use(createAttaform())
58
+ * .mount('#app')
59
+ * ```
60
+ *
61
+ * Under SSR with bare Vue 3, pass `{ ssr: true }` from your server
62
+ * entry. Under Nuxt, install via `attaform/nuxt` instead —
63
+ * the Nuxt module wires both server and client automatically.
64
+ *
65
+ * Installing more than once on the same app is a no-op (the second
66
+ * call logs a dev-mode warning).
67
+ */
68
+ declare function createAttaform(options?: AttaformPluginOptions): Plugin;
69
+
70
+ /**
71
+ * Schema-driven coercion of user-typed DOM values at the v-register
72
+ * directive layer. When the slim schema declares a numeric or
73
+ * boolean type at a path, the directive coerces incoming string
74
+ * values (`'25'` → `25`, `'true'` → `true`) before the slim-primitive
75
+ * gate sees the write — making the schema authoritative for storage
76
+ * shape and freeing consumers from sprinkling `.number` modifiers
77
+ * across templates.
78
+ *
79
+ * Coercion is consumer-extensible: a `CoercionRegistry` is just an
80
+ * `Array<CoercionEntry>` keyed at config time by `(input, output)`
81
+ * `SlimPrimitiveKind` literals. The library ships
82
+ * `defaultCoercionRules` (string→number, string→boolean) and
83
+ * `defineCoercion` for type-narrowed authoring; consumers spread the
84
+ * defaults to extend or supply their own array to replace.
85
+ *
86
+ * Coercion applies ONLY to user-typed DOM values flowing through
87
+ * the directive's assigner. Programmatic writes (`form.setValue`,
88
+ * `setValueWithInternalPath`) bypass coercion — they're authoritative
89
+ * writes whose strict typing is on the caller. This mirrors the
90
+ * `transforms` pipeline's user-input-only contract.
91
+ */
92
+
93
+ /**
94
+ * Type-narrowing helper for authoring entries. At runtime it's
95
+ * identity; at compile time it preserves the `input` / `output`
96
+ * literal types so `transform`'s parameter is narrowed to the
97
+ * runtime type instead of widening to `SlimRuntimeOf<SlimPrimitiveKind>`.
98
+ *
99
+ * Without this helper, authoring `{ input: 'string', output:
100
+ * 'number', transform: (s) => ... }` against the broader
101
+ * `CoercionEntry` widens `s` to `string | number | boolean | ...`,
102
+ * forcing a cast in every transform body. `defineCoercion` is the
103
+ * opaque-free idiom.
104
+ */
105
+ declare function defineCoercion<I extends SlimPrimitiveKind, O extends SlimPrimitiveKind>(entry: CoercionEntry<I, O>): CoercionEntry<I, O>;
106
+ /**
107
+ * The library's built-in registry. Two cells: string→number and
108
+ * string→boolean. Re-exported so consumers can spread it when
109
+ * supplying a custom registry that extends defaults.
110
+ */
111
+ declare const defaultCoercionRules: CoercionRegistry;
112
+
113
+ /**
114
+ * Per-Vue-app container for all form state instances. Each
115
+ * `app.use(createAttaform())` call gets its own registry,
116
+ * so the library runs under bare Vue 3 + SSR (via
117
+ * `@vue/server-renderer`) and Nuxt with the same code path.
118
+ *
119
+ * Each form's state lives in `forms: Map<FormKey, FormStore<GenericForm>>`.
120
+ * The type relaxation at storage time is necessary because different
121
+ * forms in the same app have different `Form` generics; callers recover
122
+ * the specific form type via `useForm`'s overloads.
123
+ */
124
+ /**
125
+ * Serialised snapshot of one form's state, captured by
126
+ * `renderAttaformState` for SSR and replayed by
127
+ * `hydrateAttaformState` on the client. Round-trips through
128
+ * JSON-safe tuples; field references are intentionally omitted
129
+ * (DOM nodes don't survive serialisation).
130
+ */
131
+ type SerializedFormData = {
132
+ /** The form's value at snapshot time. */
133
+ readonly form: unknown;
134
+ /**
135
+ * Errors produced by the schema at snapshot time. Replayed into
136
+ * the client form's error state at hydration; cleared on
137
+ * successful re-validation client-side.
138
+ */
139
+ readonly schemaErrors: ReadonlyArray<readonly [string, unknown]>;
140
+ /**
141
+ * Errors set explicitly via `setFieldErrors` / `addFieldErrors`
142
+ * (typically from a server response parsed via `parseApiErrors`)
143
+ * at snapshot time. Replayed at hydration; persists across
144
+ * client-side re-validation.
145
+ */
146
+ readonly userErrors: ReadonlyArray<readonly [string, unknown]>;
147
+ /** Per-field metadata (timestamps, raw values, connection flags) captured at snapshot time. */
148
+ readonly fields: ReadonlyArray<readonly [string, unknown]>;
149
+ /**
150
+ * Path keys that were in the form's `blankPaths` set at
151
+ * snapshot time. Round-trips the "displayed empty" UI state across
152
+ * the SSR boundary — without it, the client briefly renders
153
+ * `String(slim-default)` (e.g. `'0'`) for fields the server
154
+ * rendered as blank. Optional in the wire format so older payload
155
+ * shapes deserialise cleanly.
156
+ */
157
+ readonly blankPaths?: ReadonlyArray<string>;
158
+ };
159
+ /**
160
+ * The library's per-Vue-app container. One `AttaformRegistry` is
161
+ * created per `app.use(createAttaform())` call.
162
+ *
163
+ * Most consumers never touch this directly — `useForm` and
164
+ * `injectForm` reach the registry on your behalf. Access it
165
+ * explicitly only when wiring SSR or a custom plugin integration.
166
+ */
167
+ type AttaformRegistry = {
168
+ /** `true` while running on the server during SSR; `false` on the client. */
169
+ readonly isSSR: boolean;
170
+ /** App-level defaults applied to every `useForm` call. */
171
+ readonly defaults: AttaformDefaults;
172
+ };
173
+ /**
174
+ * The Vue `InjectionKey` under which the registry is provided on the
175
+ * app. Most consumers never need this — `useForm` and
176
+ * `injectForm` resolve the registry automatically.
177
+ */
178
+ declare const kAttaformRegistry: InjectionKey<AttaformRegistry>;
179
+ declare module 'vue' {
180
+ interface App {
181
+ }
182
+ }
183
+ /** Options for `createRegistry`. */
184
+ type CreateRegistryOptions = SSRDetectOptions & {
185
+ /**
186
+ * App-level defaults applied to every `useForm` call. Per-form
187
+ * options always win. Omitted is equivalent to `{}`.
188
+ */
189
+ defaults?: AttaformDefaults;
190
+ };
191
+ /**
192
+ * Create a fresh `AttaformRegistry`. `createAttaform()` calls
193
+ * this internally — most consumers never need to call it directly.
194
+ * Use it when building a custom plugin that doesn't want the
195
+ * `createAttaform` plugin's auto-install behaviour (e.g. test
196
+ * harnesses, embedded apps).
197
+ */
198
+ declare function createRegistry(options?: CreateRegistryOptions): AttaformRegistry;
199
+ /**
200
+ * Look up the current app's registry from inside a component's
201
+ * `setup()` (or any synchronous code on the setup call stack).
202
+ *
203
+ * Most consumers don't need this — `useForm` and `injectForm`
204
+ * call it on your behalf. Reach for it directly when building
205
+ * custom integrations that need the raw registry.
206
+ *
207
+ * Throws:
208
+ * - `OutsideSetupError` when called outside a Vue setup context
209
+ * (e.g. from an event handler or async callback). Move the call
210
+ * into setup, or trigger it from a child component.
211
+ * - `RegistryNotInstalledError` when called inside setup but the
212
+ * plugin wasn't installed. Add
213
+ * `app.use(createAttaform())` to your app entry.
214
+ */
215
+ declare function useRegistry(): AttaformRegistry;
216
+ /**
217
+ * Look up a Vue app's registry by `App` reference. Used by
218
+ * SSR helpers (`renderAttaformState`, `hydrateAttaformState`) that
219
+ * run outside a component setup context.
220
+ *
221
+ * Throws `RegistryNotInstalledError` when the app hasn't been wired
222
+ * with `createAttaform()`.
223
+ */
224
+ declare function getRegistryFromApp(app: App): AttaformRegistry;
225
+
226
+ /**
227
+ * Serialised snapshot of every form in a Vue app, produced by
228
+ * `renderAttaformState` and consumed by `hydrateAttaformState`.
229
+ *
230
+ * JSON-safe — pass to `JSON.stringify`, `devalue`, or any other
231
+ * serialiser before embedding in your SSR payload.
232
+ */
233
+ type SerializedAttaformState = {
234
+ /** Tuples of `[formKey, snapshot]` for every form in the app. */
235
+ readonly forms: ReadonlyArray<readonly [FormKey, SerializedFormData]>;
236
+ };
237
+ /**
238
+ * Snapshot every form on a Vue app for SSR. Call from your server
239
+ * entry after rendering the app:
240
+ *
241
+ * ```ts
242
+ * import { renderToString } from '@vue/server-renderer'
243
+ * import { renderAttaformState, escapeForInlineScript } from 'attaform'
244
+ *
245
+ * const html = await renderToString(app)
246
+ * const state = renderAttaformState(app)
247
+ * const payload = escapeForInlineScript(JSON.stringify(state))
248
+ *
249
+ * return `
250
+ * ${html}
251
+ * <script>window.__ATTAFORM_STATE__ = ${payload}</script>
252
+ * `
253
+ * ```
254
+ *
255
+ * Pair with `hydrateAttaformState` on the client to restore the
256
+ * forms in their server-rendered state. Nuxt users don't need this —
257
+ * `attaform/nuxt` wires SSR automatically.
258
+ */
259
+ declare function renderAttaformState(app: App): SerializedAttaformState;
260
+ /**
261
+ * Restore forms from a server-rendered snapshot on the client. Call
262
+ * from your client entry before mounting:
263
+ *
264
+ * ```ts
265
+ * import { createApp } from 'vue'
266
+ * import { createAttaform, hydrateAttaformState } from 'attaform'
267
+ *
268
+ * const app = createApp(App).use(createAttaform())
269
+ * hydrateAttaformState(app, window.__ATTAFORM_STATE__)
270
+ * app.mount('#app')
271
+ * ```
272
+ *
273
+ * The next `useForm({ key })` call for each serialised form picks up
274
+ * the snapshot transparently — no further action is required.
275
+ */
276
+ declare function hydrateAttaformState(app: App, payload: SerializedAttaformState): void;
277
+
278
+ /**
279
+ * Escape a JSON string so it's safe to embed inside an inline
280
+ * `<script>` tag during SSR. Plain `JSON.stringify` is not safe — a
281
+ * form value containing the literal substring `</script>` would
282
+ * break out of the script tag.
283
+ *
284
+ * ```ts
285
+ * const payload = escapeForInlineScript(JSON.stringify(renderAttaformState(app)))
286
+ * // `<script>window.__ATTAFORM_STATE__ = ${payload}</script>` is safe.
287
+ * ```
288
+ *
289
+ * Output remains valid JSON — `JSON.parse` round-trips back to the
290
+ * original value on the client.
291
+ */
292
+ declare function escapeForInlineScript(json: string): string;
293
+
294
+ /**
295
+ * Schema-agnostic `useForm`. Accepts any object that implements
296
+ * `AbstractSchema` — useful when integrating a custom schema
297
+ * adapter or a third-party validation library.
298
+ *
299
+ * ```ts
300
+ * import { useForm } from 'attaform'
301
+ *
302
+ * const form = useForm({
303
+ * schema: myCustomAdapter,
304
+ * defaultValues: { name: '' },
305
+ * })
306
+ * ```
307
+ *
308
+ * Most consumers prefer a typed entry point — e.g.
309
+ * `attaform/zod` (v4) or `attaform/zod-v3` —
310
+ * which wrap the underlying library's schema with the matching
311
+ * adapter automatically.
312
+ *
313
+ * Returns the same form API as the typed entry points; see
314
+ * `UseFormReturnType` for the full surface.
315
+ */
316
+ declare function useAbstractForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(configuration: UseFormConfiguration<Form, GetValueFormType, AbstractSchema<Form, GetValueFormType>, DeepPartial<DefaultValuesShape<Form>>>): UseFormReturnType<Form, GetValueFormType>;
317
+
318
+ /**
319
+ * Symbol slot used by custom directive integrations to install an
320
+ * assigner on the bound element. Read by the v-register directive
321
+ * when a DOM event fires:
322
+ *
323
+ * ```ts
324
+ * import { assignKey } from 'attaform'
325
+ * el[assignKey] = (value) => myCustomWriter(value)
326
+ * ```
327
+ *
328
+ * Most consumers never need this — the built-in directives wire
329
+ * default assigners for text inputs, checkboxes, radios, and selects.
330
+ */
331
+ declare const assignKey: unique symbol;
332
+ /**
333
+ * Type guard for a `RegisterValue`. Returns `true` when `val` looks
334
+ * like the object returned from `form.register(path)`.
335
+ *
336
+ * ```ts
337
+ * if (isRegisterValue(slotValue)) {
338
+ * // slotValue.innerRef is now a Ref<unknown>
339
+ * }
340
+ * ```
341
+ *
342
+ * Useful when building wrapper components that accept either a
343
+ * `RegisterValue` or a plain ref via the same prop.
344
+ */
345
+ declare function isRegisterValue<Value = unknown>(val: unknown): val is RegisterValue<Value>;
346
+ /**
347
+ * The `v-register` directive. Bind a form field to a native input,
348
+ * select, textarea, checkbox, or radio:
349
+ *
350
+ * ```vue
351
+ * <input v-register="form.register('email')" />
352
+ * <select v-register="form.register('country')">
353
+ * <option value="us">US</option>
354
+ * <option value="uk">UK</option>
355
+ * </select>
356
+ * ```
357
+ *
358
+ * The directive picks the right binding strategy automatically based
359
+ * on the element's `tagName` and `type`. Registered globally by
360
+ * `createAttaform()` — most consumers never import it
361
+ * directly, but it's exposed for advanced integrations that wire
362
+ * directives manually.
363
+ */
364
+ declare const vRegister: RegisterModelDynamicCustomDirective;
365
+
366
+ /**
367
+ * Result of `parseApiErrors`. Branch on `ok` to handle the two cases:
368
+ *
369
+ * ```ts
370
+ * const result = parseApiErrors(payload, { formKey: form.key })
371
+ * if (result.ok) {
372
+ * form.setFieldErrors(result.errors)
373
+ * } else {
374
+ * console.warn('Bad error payload:', result.rejected)
375
+ * }
376
+ * ```
377
+ *
378
+ * `ok: true` means the payload was recognised — `errors` may still be
379
+ * empty if the payload was valid but had no actual errors.
380
+ * `ok: false` means the payload didn't match a known shape; `rejected`
381
+ * carries a one-line description of why.
382
+ */
383
+ type ParseApiErrorsResult = {
384
+ /** `true` when the payload was recognised; `false` when the shape was unfamiliar. */
385
+ readonly ok: boolean;
386
+ /** Errors extracted from the payload. May be empty even when `ok: true`. */
387
+ readonly errors: ValidationError[];
388
+ /** When `ok: false`, a one-line description of why the payload was rejected. */
389
+ readonly rejected?: string;
390
+ };
391
+ /**
392
+ * Options for `parseApiErrors`. The size caps protect against
393
+ * misbehaving or hostile servers — exceeding any cap causes the
394
+ * parser to reject the payload wholesale rather than partially apply.
395
+ */
396
+ type ParseApiErrorsOptions = {
397
+ /**
398
+ * The form's identifier — pass `form.key`. Stamped on every
399
+ * produced `ValidationError` so errors route to the right form.
400
+ */
401
+ readonly formKey: FormKey;
402
+ /**
403
+ * Code stamped on `ValidationError`s synthesized from bare-string
404
+ * entries (the Rails / DRF / Laravel `{ field: ["msg"] }` shape).
405
+ * Default `'api:unknown'`. Pick something more specific
406
+ * (`'api:server-validation'`, `'myapp:legacy'`, …) when you know
407
+ * the source.
408
+ *
409
+ * Structured `{ message, code }` entries forward their `code`
410
+ * verbatim and ignore this option.
411
+ */
412
+ readonly defaultCode?: string;
413
+ /**
414
+ * Maximum number of distinct keys to accept. Default `1000`.
415
+ * Raise for trusted backends that legitimately produce more.
416
+ */
417
+ readonly maxEntries?: number;
418
+ /**
419
+ * Maximum number of path segments per key. Default `32`. Keys
420
+ * deeper than this are dropped (the rest of the payload still
421
+ * applies if it stays under the other caps).
422
+ */
423
+ readonly maxPathDepth?: number;
424
+ /**
425
+ * Maximum total path segments summed across every accepted key.
426
+ * Default `10000`. Bounds the worst-case traversal cost.
427
+ */
428
+ readonly maxTotalSegments?: number;
429
+ };
430
+ /**
431
+ * Default size caps + default fallback code used by `parseApiErrors`.
432
+ * Conservative; pass larger values (or a more specific code) via the
433
+ * options bag for trusted-backend integrations.
434
+ */
435
+ declare const PARSE_API_ERRORS_DEFAULTS: {
436
+ readonly maxEntries: 1000;
437
+ readonly maxPathDepth: 32;
438
+ readonly maxTotalSegments: 10000;
439
+ readonly defaultCode: "api:unknown";
440
+ };
441
+ /**
442
+ * Normalise a server-side validation error payload into
443
+ * `ValidationError[]`. Pair with `form.setFieldErrors` /
444
+ * `form.addFieldErrors` to surface server errors on the form:
445
+ *
446
+ * ```ts
447
+ * const response = await fetch('/api/signup', { … })
448
+ * if (!response.ok) {
449
+ * const payload = await response.json()
450
+ * const result = parseApiErrors(payload, { formKey: form.key })
451
+ * if (result.ok) form.setFieldErrors(result.errors)
452
+ * }
453
+ * ```
454
+ *
455
+ * Recognised payload shapes:
456
+ *
457
+ * - Wrapped envelope:
458
+ * `{ error: { details: { email: { message: 'taken', code: 'api:duplicate-email' } } } }`
459
+ * - Unwrapped envelope:
460
+ * `{ details: { email: { message: 'taken', code: 'api:duplicate-email' } } }`
461
+ * - Raw details record:
462
+ * `{ email: { message: 'taken', code: 'api:duplicate-email' } }`
463
+ * - **Bare-string Rails / DRF / Laravel shape:**
464
+ * `{ email: ['Email already taken.'], username: 'too short' }`
465
+ * - `null` / `undefined` — returns `{ ok: true, errors: [] }`
466
+ *
467
+ * Two entry shapes are accepted:
468
+ *
469
+ * 1. **Structured** — `{ message: string, code: string }`. The `code`
470
+ * is forwarded verbatim onto the produced `ValidationError`.
471
+ * 2. **Bare-string** — a plain string. Synthesized into
472
+ * `{ message: <string>, code: <defaultCode> }` where `defaultCode`
473
+ * comes from `options.defaultCode` (default `'api:unknown'`).
474
+ * Useful for the Rails / Django REST Framework / FastAPI / Laravel
475
+ * JSON shape that doesn't carry a per-field code.
476
+ *
477
+ * Each detail key's value can be a single entry, an array, or a mix
478
+ * of structured and bare-string entries; arrays expand into one
479
+ * `ValidationError` per entry. Pick a prefix on the server (`api:`,
480
+ * `auth:`, etc.) and stay consistent so error renderers can branch
481
+ * on `code` — or rely on `defaultCode` when the wire shape is
482
+ * message-only.
483
+ *
484
+ * Dotted keys (`"address.line1"`) are split into structured paths
485
+ * automatically. Use a custom server response shape outside these
486
+ * patterns? Build the `ValidationError[]` array yourself and pass
487
+ * it to `setFieldErrors` directly — `parseApiErrors` is just a
488
+ * convenience for the common shapes.
489
+ */
490
+ declare function parseApiErrors(payload: ApiErrorEnvelope | ApiErrorDetails | null | undefined | unknown, options: ParseApiErrorsOptions): ParseApiErrorsResult;
491
+
492
+ export { AbstractSchema, ApiErrorDetails, ApiErrorEnvelope, AttaformDefaults, CoercionEntry, CoercionRegistry, DeepPartial, DefaultValuesShape, FormKey, GenericForm, PARSE_API_ERRORS_DEFAULTS, RegisterValue, SlimPrimitiveKind, UseFormConfiguration, UseFormReturnType, ValidationError, assignKey, createAttaform, createRegistry, defaultCoercionRules, defineCoercion, escapeForInlineScript, getRegistryFromApp, hydrateAttaformState, isRegisterValue, kAttaformRegistry, parseApiErrors, renderAttaformState, useAbstractForm as useForm, useRegistry, vRegister };
493
+ export type { AttaformPluginOptions, AttaformRegistry, ParseApiErrorsOptions, ParseApiErrorsResult, SerializedAttaformState, SerializedFormData };
package/dist/index.mjs ADDED
@@ -0,0 +1,141 @@
1
+ export { a as assignKey, c as createAttaform, h as hydrateAttaformState, i as isRegisterValue, r as renderAttaformState, v as vRegister } from './shared/attaform.DlgKK10S.mjs';
2
+ import { I as InvalidPathError } from './shared/attaform.al_rpt7_.mjs';
3
+ export { A as AnonPersistError, a as AttaformError, O as OutsideSetupError, R as RegistryNotInstalledError, b as ReservedFormKeyError, S as SensitivePersistFieldError, c as SubmitErrorHandlerError, d as createRegistry, g as getRegistryFromApp, k as kAttaformRegistry, u as useRegister, e as useRegistry } from './shared/attaform.al_rpt7_.mjs';
4
+ export { A as AttaformErrorCode, d as defaultCoercionRules, a as defineCoercion, i as injectForm, b as isUnset, u as unset, c as useForm } from './shared/attaform.BRTxpA3q.mjs';
5
+ import { c as canonicalizePath } from './shared/attaform.Cc93zNzD.mjs';
6
+ export { R as ROOT_PATH, a as ROOT_PATH_KEY, p as parseDottedPath } from './shared/attaform.Cc93zNzD.mjs';
7
+
8
+ function escapeForInlineScript(json) {
9
+ return json.replace(/[<>&\u2028\u2029]/g, (char) => {
10
+ switch (char) {
11
+ case "<":
12
+ return "\\u003c";
13
+ case ">":
14
+ return "\\u003e";
15
+ case "&":
16
+ return "\\u0026";
17
+ case "\u2028":
18
+ return "\\u2028";
19
+ case "\u2029":
20
+ return "\\u2029";
21
+ default:
22
+ return char;
23
+ }
24
+ });
25
+ }
26
+
27
+ const PARSE_API_ERRORS_DEFAULTS = {
28
+ maxEntries: 1e3,
29
+ maxPathDepth: 32,
30
+ maxTotalSegments: 1e4,
31
+ defaultCode: "api:unknown"
32
+ };
33
+ function parseApiErrors(payload, options) {
34
+ const maxEntries = options.maxEntries ?? PARSE_API_ERRORS_DEFAULTS.maxEntries;
35
+ const maxPathDepth = options.maxPathDepth ?? PARSE_API_ERRORS_DEFAULTS.maxPathDepth;
36
+ const maxTotalSegments = options.maxTotalSegments ?? PARSE_API_ERRORS_DEFAULTS.maxTotalSegments;
37
+ const defaultCode = options.defaultCode ?? PARSE_API_ERRORS_DEFAULTS.defaultCode;
38
+ if (payload === null || payload === void 0) {
39
+ return { ok: true, errors: [] };
40
+ }
41
+ if (typeof payload !== "object") {
42
+ return { ok: false, errors: [], rejected: `payload was ${typeof payload}, expected object` };
43
+ }
44
+ const extraction = extractDetails(payload);
45
+ if (!extraction.ok) {
46
+ return { ok: false, errors: [], rejected: extraction.reason };
47
+ }
48
+ const { details } = extraction;
49
+ const entryCount = Object.keys(details).length;
50
+ if (entryCount > maxEntries) {
51
+ return {
52
+ ok: false,
53
+ errors: [],
54
+ rejected: `payload has ${entryCount} entries, exceeds maxEntries=${maxEntries}`
55
+ };
56
+ }
57
+ const errors = [];
58
+ let totalSegments = 0;
59
+ for (const [key, value] of Object.entries(details)) {
60
+ const entryList = Array.isArray(value) ? value : [value];
61
+ let segments;
62
+ try {
63
+ segments = canonicalizePath(key).segments;
64
+ } catch (err) {
65
+ if (err instanceof InvalidPathError) continue;
66
+ throw err;
67
+ }
68
+ if (segments.length > maxPathDepth) continue;
69
+ totalSegments += segments.length;
70
+ if (totalSegments > maxTotalSegments) {
71
+ return {
72
+ ok: false,
73
+ errors: [],
74
+ rejected: `payload total path segments exceeds maxTotalSegments=${maxTotalSegments}`
75
+ };
76
+ }
77
+ for (const entry of entryList) {
78
+ const message = typeof entry === "string" ? entry : entry.message;
79
+ const code = typeof entry === "string" ? defaultCode : entry.code;
80
+ if (message.length === 0) continue;
81
+ errors.push({
82
+ message,
83
+ path: Array.from(segments),
84
+ formKey: options.formKey,
85
+ code
86
+ });
87
+ }
88
+ }
89
+ return { ok: true, errors };
90
+ }
91
+ function extractDetails(payload) {
92
+ const wrappedError = payload["error"];
93
+ if (wrappedError !== null && wrappedError !== void 0 && typeof wrappedError === "object") {
94
+ const inner = wrappedError.details;
95
+ if (inner === void 0) {
96
+ return { ok: true, details: {} };
97
+ }
98
+ if (isDetailsRecord(inner)) return { ok: true, details: inner };
99
+ return {
100
+ ok: false,
101
+ reason: "error.details entries must be strings or { message, code } objects"
102
+ };
103
+ }
104
+ if (wrappedError !== null && wrappedError !== void 0 && typeof wrappedError !== "object") {
105
+ return {
106
+ ok: false,
107
+ reason: `payload.error was ${typeof wrappedError}, expected an object with { details }`
108
+ };
109
+ }
110
+ if ("details" in payload) {
111
+ const inner = payload["details"];
112
+ if (inner === void 0) return { ok: true, details: {} };
113
+ if (isDetailsRecord(inner)) return { ok: true, details: inner };
114
+ return { ok: false, reason: "details entries must be strings or { message, code } objects" };
115
+ }
116
+ if (isDetailsRecord(payload)) return { ok: true, details: payload };
117
+ if (Object.keys(payload).length === 0) return { ok: true, details: {} };
118
+ return { ok: false, reason: "unrecognised payload shape" };
119
+ }
120
+ function isStructuredEntry(value) {
121
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
122
+ const obj = value;
123
+ return typeof obj.message === "string" && typeof obj.code === "string";
124
+ }
125
+ function isAcceptedEntry(value) {
126
+ return typeof value === "string" || isStructuredEntry(value);
127
+ }
128
+ function isDetailsRecord(value) {
129
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
130
+ const record = value;
131
+ for (const k of Object.keys(record)) {
132
+ const v = record[k];
133
+ if (isAcceptedEntry(v)) continue;
134
+ if (Array.isArray(v) && v.every((entry) => isAcceptedEntry(entry))) continue;
135
+ return false;
136
+ }
137
+ return true;
138
+ }
139
+
140
+ export { InvalidPathError, PARSE_API_ERRORS_DEFAULTS, canonicalizePath, escapeForInlineScript, parseApiErrors };
141
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/runtime/core/serialize-script.ts","../src/runtime/core/parse-api-errors.ts"],"sourcesContent":["/**\n * Escape a JSON string so it's safe to embed inside an inline\n * `<script>` tag during SSR. Plain `JSON.stringify` is not safe — a\n * form value containing the literal substring `</script>` would\n * break out of the script tag.\n *\n * ```ts\n * const payload = escapeForInlineScript(JSON.stringify(renderAttaformState(app)))\n * // `<script>window.__ATTAFORM_STATE__ = ${payload}</script>` is safe.\n * ```\n *\n * Output remains valid JSON — `JSON.parse` round-trips back to the\n * original value on the client.\n */\nexport function escapeForInlineScript(json: string): string {\n return json.replace(/[<>&\\u2028\\u2029]/g, (char) => {\n switch (char) {\n case '<':\n return '\\\\u003c'\n case '>':\n return '\\\\u003e'\n case '&':\n return '\\\\u0026'\n case '\\u2028':\n return '\\\\u2028'\n case '\\u2029':\n return '\\\\u2029'\n default:\n return char\n }\n })\n}\n","import type {\n ApiErrorDetails,\n ApiErrorEntry,\n ApiErrorEnvelope,\n FormKey,\n ValidationError,\n} from '../types/types-api'\nimport { InvalidPathError } from './errors'\nimport { canonicalizePath } from './paths'\n\n/**\n * Result of `parseApiErrors`. Branch on `ok` to handle the two cases:\n *\n * ```ts\n * const result = parseApiErrors(payload, { formKey: form.key })\n * if (result.ok) {\n * form.setFieldErrors(result.errors)\n * } else {\n * console.warn('Bad error payload:', result.rejected)\n * }\n * ```\n *\n * `ok: true` means the payload was recognised — `errors` may still be\n * empty if the payload was valid but had no actual errors.\n * `ok: false` means the payload didn't match a known shape; `rejected`\n * carries a one-line description of why.\n */\nexport type ParseApiErrorsResult = {\n /** `true` when the payload was recognised; `false` when the shape was unfamiliar. */\n readonly ok: boolean\n /** Errors extracted from the payload. May be empty even when `ok: true`. */\n readonly errors: ValidationError[]\n /** When `ok: false`, a one-line description of why the payload was rejected. */\n readonly rejected?: string\n}\n\n/**\n * Options for `parseApiErrors`. The size caps protect against\n * misbehaving or hostile servers — exceeding any cap causes the\n * parser to reject the payload wholesale rather than partially apply.\n */\nexport type ParseApiErrorsOptions = {\n /**\n * The form's identifier — pass `form.key`. Stamped on every\n * produced `ValidationError` so errors route to the right form.\n */\n readonly formKey: FormKey\n /**\n * Code stamped on `ValidationError`s synthesized from bare-string\n * entries (the Rails / DRF / Laravel `{ field: [\"msg\"] }` shape).\n * Default `'api:unknown'`. Pick something more specific\n * (`'api:server-validation'`, `'myapp:legacy'`, …) when you know\n * the source.\n *\n * Structured `{ message, code }` entries forward their `code`\n * verbatim and ignore this option.\n */\n readonly defaultCode?: string\n /**\n * Maximum number of distinct keys to accept. Default `1000`.\n * Raise for trusted backends that legitimately produce more.\n */\n readonly maxEntries?: number\n /**\n * Maximum number of path segments per key. Default `32`. Keys\n * deeper than this are dropped (the rest of the payload still\n * applies if it stays under the other caps).\n */\n readonly maxPathDepth?: number\n /**\n * Maximum total path segments summed across every accepted key.\n * Default `10000`. Bounds the worst-case traversal cost.\n */\n readonly maxTotalSegments?: number\n}\n\n/**\n * Default size caps + default fallback code used by `parseApiErrors`.\n * Conservative; pass larger values (or a more specific code) via the\n * options bag for trusted-backend integrations.\n */\nexport const PARSE_API_ERRORS_DEFAULTS = {\n maxEntries: 1000,\n maxPathDepth: 32,\n maxTotalSegments: 10000,\n defaultCode: 'api:unknown',\n} as const\n\n/**\n * Normalise a server-side validation error payload into\n * `ValidationError[]`. Pair with `form.setFieldErrors` /\n * `form.addFieldErrors` to surface server errors on the form:\n *\n * ```ts\n * const response = await fetch('/api/signup', { … })\n * if (!response.ok) {\n * const payload = await response.json()\n * const result = parseApiErrors(payload, { formKey: form.key })\n * if (result.ok) form.setFieldErrors(result.errors)\n * }\n * ```\n *\n * Recognised payload shapes:\n *\n * - Wrapped envelope:\n * `{ error: { details: { email: { message: 'taken', code: 'api:duplicate-email' } } } }`\n * - Unwrapped envelope:\n * `{ details: { email: { message: 'taken', code: 'api:duplicate-email' } } }`\n * - Raw details record:\n * `{ email: { message: 'taken', code: 'api:duplicate-email' } }`\n * - **Bare-string Rails / DRF / Laravel shape:**\n * `{ email: ['Email already taken.'], username: 'too short' }`\n * - `null` / `undefined` — returns `{ ok: true, errors: [] }`\n *\n * Two entry shapes are accepted:\n *\n * 1. **Structured** — `{ message: string, code: string }`. The `code`\n * is forwarded verbatim onto the produced `ValidationError`.\n * 2. **Bare-string** — a plain string. Synthesized into\n * `{ message: <string>, code: <defaultCode> }` where `defaultCode`\n * comes from `options.defaultCode` (default `'api:unknown'`).\n * Useful for the Rails / Django REST Framework / FastAPI / Laravel\n * JSON shape that doesn't carry a per-field code.\n *\n * Each detail key's value can be a single entry, an array, or a mix\n * of structured and bare-string entries; arrays expand into one\n * `ValidationError` per entry. Pick a prefix on the server (`api:`,\n * `auth:`, etc.) and stay consistent so error renderers can branch\n * on `code` — or rely on `defaultCode` when the wire shape is\n * message-only.\n *\n * Dotted keys (`\"address.line1\"`) are split into structured paths\n * automatically. Use a custom server response shape outside these\n * patterns? Build the `ValidationError[]` array yourself and pass\n * it to `setFieldErrors` directly — `parseApiErrors` is just a\n * convenience for the common shapes.\n */\nexport function parseApiErrors(\n payload: ApiErrorEnvelope | ApiErrorDetails | null | undefined | unknown,\n options: ParseApiErrorsOptions\n): ParseApiErrorsResult {\n const maxEntries = options.maxEntries ?? PARSE_API_ERRORS_DEFAULTS.maxEntries\n const maxPathDepth = options.maxPathDepth ?? PARSE_API_ERRORS_DEFAULTS.maxPathDepth\n const maxTotalSegments = options.maxTotalSegments ?? PARSE_API_ERRORS_DEFAULTS.maxTotalSegments\n const defaultCode = options.defaultCode ?? PARSE_API_ERRORS_DEFAULTS.defaultCode\n\n if (payload === null || payload === undefined) {\n return { ok: true, errors: [] }\n }\n if (typeof payload !== 'object') {\n return { ok: false, errors: [], rejected: `payload was ${typeof payload}, expected object` }\n }\n\n const extraction = extractDetails(payload as Record<string, unknown>)\n if (!extraction.ok) {\n return { ok: false, errors: [], rejected: extraction.reason }\n }\n\n const { details } = extraction\n const entryCount = Object.keys(details).length\n // Enforce the guardrails before we spend time walking the payload.\n // Rejecting wholesale (not partial-applying) keeps the failure visible\n // so consumers can tune the caps or investigate the server payload.\n if (entryCount > maxEntries) {\n return {\n ok: false,\n errors: [],\n rejected: `payload has ${entryCount} entries, exceeds maxEntries=${maxEntries}`,\n }\n }\n\n const errors: ValidationError[] = []\n let totalSegments = 0\n for (const [key, value] of Object.entries(details)) {\n const entryList: ReadonlyArray<string | ApiErrorEntry> = Array.isArray(value) ? value : [value]\n // `canonicalizePath` throws `InvalidPathError` for dotted strings with\n // empty segments (e.g. `'. '`, `'a..b'`). A misbehaving server can\n // genuinely emit such a key; the hydrator is a normaliser, not a\n // validator, so we drop offending keys rather than let the exception\n // escape. Well-formed keys continue as normal.\n let segments: readonly (string | number)[]\n try {\n segments = canonicalizePath(key).segments\n } catch (err) {\n if (err instanceof InvalidPathError) continue\n throw err\n }\n // Per-path depth cap. We drop the offending key (rather than\n // rejecting the whole payload) because a single stray deep path\n // in an otherwise legitimate error set is still worth surfacing\n // the rest. Consumers who want strict rejection can post-filter\n // on `result.errors.length < details entryCount`.\n if (segments.length > maxPathDepth) continue\n // Total-segment cap. Enforced wholesale (not per-key) so a payload\n // that passes the per-key gate but stacks into a pathological\n // total still fails visibly. Mirrors `maxEntries` strictness.\n totalSegments += segments.length\n if (totalSegments > maxTotalSegments) {\n return {\n ok: false,\n errors: [],\n rejected: `payload total path segments exceeds maxTotalSegments=${maxTotalSegments}`,\n }\n }\n for (const entry of entryList) {\n // Bare-string entries (Rails / DRF / Laravel shape) synthesize a\n // `code` from `options.defaultCode`; structured `{ message, code }`\n // entries forward `code` verbatim. Empty messages drop silently\n // (`{ message: '' }` or `''`) — same recoverable-malformed-server\n // policy as before.\n const message = typeof entry === 'string' ? entry : entry.message\n const code = typeof entry === 'string' ? defaultCode : entry.code\n if (message.length === 0) continue\n errors.push({\n message,\n path: Array.from(segments),\n formKey: options.formKey,\n code,\n })\n }\n }\n return { ok: true, errors }\n}\n\ntype ExtractResult = { ok: true; details: ApiErrorDetails } | { ok: false; reason: string }\n\nfunction extractDetails(payload: Record<string, unknown>): ExtractResult {\n const wrappedError = payload['error']\n if (wrappedError !== null && wrappedError !== undefined && typeof wrappedError === 'object') {\n const inner = (wrappedError as { details?: unknown }).details\n if (inner === undefined) {\n // A wrapped envelope without details is considered \"no errors\" — valid shape.\n return { ok: true, details: {} }\n }\n if (isDetailsRecord(inner)) return { ok: true, details: inner }\n return {\n ok: false,\n reason: 'error.details entries must be strings or { message, code } objects',\n }\n }\n\n // `{ error: 'oops' }` / `{ error: 42 }` is a malformed wrapped envelope —\n // the server meant an error object but sent a scalar. Without this guard\n // the payload would fall through to the raw-details branch below, where\n // `{ error: 'oops' }` satisfies `isDetailsRecord` and silently produces\n // a phantom `ValidationError` at path `['error']`.\n if (wrappedError !== null && wrappedError !== undefined && typeof wrappedError !== 'object') {\n return {\n ok: false,\n reason: `payload.error was ${typeof wrappedError}, expected an object with { details }`,\n }\n }\n\n if ('details' in payload) {\n const inner = payload['details']\n if (inner === undefined) return { ok: true, details: {} }\n if (isDetailsRecord(inner)) return { ok: true, details: inner }\n return { ok: false, reason: 'details entries must be strings or { message, code } objects' }\n }\n\n if (isDetailsRecord(payload)) return { ok: true, details: payload }\n\n // Heuristic: if the payload has keys but none of them look like details,\n // it's probably a completely different shape. Reject.\n if (Object.keys(payload).length === 0) return { ok: true, details: {} }\n return { ok: false, reason: 'unrecognised payload shape' }\n}\n\nfunction isStructuredEntry(value: unknown): value is ApiErrorEntry {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n const obj = value as { message?: unknown; code?: unknown }\n return typeof obj.message === 'string' && typeof obj.code === 'string'\n}\n\n/**\n * Accepts either a structured `{ message, code }` entry OR a bare\n * string. Bare strings synthesize a `code` at parse time\n * (`options.defaultCode`) and are useful for the Rails / Django REST\n * Framework / Laravel JSON shape that doesn't carry a per-field code.\n */\nfunction isAcceptedEntry(value: unknown): value is string | ApiErrorEntry {\n return typeof value === 'string' || isStructuredEntry(value)\n}\n\n/**\n * A record is a \"details\" record when every value is either an\n * accepted entry or an array of accepted entries (mixing structured +\n * bare-string in the same array is fine; the parser normalises per\n * entry). Half-structured objects (e.g. `{ message: 'x' }` missing\n * `code`) are still rejected so the bug surfaces — see the\n * `'rejects entries that are objects but missing required fields'`\n * test for the rationale.\n */\nfunction isDetailsRecord(value: unknown): value is ApiErrorDetails {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n // Reject prototype-polluted keys — we don't use them here, but downstream\n // spreads shouldn't have to worry about this input.\n const record = value as Record<string, unknown>\n for (const k of Object.keys(record)) {\n const v = record[k]\n if (isAcceptedEntry(v)) continue\n if (Array.isArray(v) && v.every((entry) => isAcceptedEntry(entry))) continue\n return false\n }\n return true\n}\n"],"names":[],"mappings":";;;;;;;AAcO,SAAS,sBAAsB,IAAA,EAAsB;AAC1D,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,oBAAA,EAAsB,CAAC,IAAA,KAAS;AAClD,IAAA,QAAQ,IAAA;AAAM,MACZ,KAAK,GAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,GAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,GAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,QAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,QAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT;AACE,QAAA,OAAO,IAAA;AAAA;AACX,EACF,CAAC,CAAA;AACH;;ACkDO,MAAM,yBAAA,GAA4B;AAAA,EACvC,UAAA,EAAY,GAAA;AAAA,EACZ,YAAA,EAAc,EAAA;AAAA,EACd,gBAAA,EAAkB,GAAA;AAAA,EAClB,WAAA,EAAa;AACf;AAmDO,SAAS,cAAA,CACd,SACA,OAAA,EACsB;AACtB,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,UAAA,IAAc,yBAAA,CAA0B,UAAA;AACnE,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,YAAA,IAAgB,yBAAA,CAA0B,YAAA;AACvE,EAAA,MAAM,gBAAA,GAAmB,OAAA,CAAQ,gBAAA,IAAoB,yBAAA,CAA0B,gBAAA;AAC/E,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,yBAAA,CAA0B,WAAA;AAErE,EAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAA,KAAY,MAAA,EAAW;AAC7C,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAQ,EAAC,EAAE;AAAA,EAChC;AACA,EAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAC/B,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,IAAI,QAAA,EAAU,CAAA,YAAA,EAAe,OAAO,OAAO,CAAA,iBAAA,CAAA,EAAoB;AAAA,EAC7F;AAEA,EAAA,MAAM,UAAA,GAAa,eAAe,OAAkC,CAAA;AACpE,EAAA,IAAI,CAAC,WAAW,EAAA,EAAI;AAClB,IAAA,OAAO,EAAE,IAAI,KAAA,EAAO,MAAA,EAAQ,EAAC,EAAG,QAAA,EAAU,WAAW,MAAA,EAAO;AAAA,EAC9D;AAEA,EAAA,MAAM,EAAE,SAAQ,GAAI,UAAA;AACpB,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,CAAE,MAAA;AAIxC,EAAA,IAAI,aAAa,UAAA,EAAY;AAC3B,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,QAAQ,EAAC;AAAA,MACT,QAAA,EAAU,CAAA,YAAA,EAAe,UAAU,CAAA,6BAAA,EAAgC,UAAU,CAAA;AAAA,KAC/E;AAAA,EACF;AAEA,EAAA,MAAM,SAA4B,EAAC;AACnC,EAAA,IAAI,aAAA,GAAgB,CAAA;AACpB,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,MAAM,YAAmD,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA,GAAQ,CAAC,KAAK,CAAA;AAM9F,IAAA,IAAI,QAAA;AACJ,IAAA,IAAI;AACF,MAAA,QAAA,GAAW,gBAAA,CAAiB,GAAG,CAAA,CAAE,QAAA;AAAA,IACnC,SAAS,GAAA,EAAK;AACZ,MAAA,IAAI,eAAe,gBAAA,EAAkB;AACrC,MAAA,MAAM,GAAA;AAAA,IACR;AAMA,IAAA,IAAI,QAAA,CAAS,SAAS,YAAA,EAAc;AAIpC,IAAA,aAAA,IAAiB,QAAA,CAAS,MAAA;AAC1B,IAAA,IAAI,gBAAgB,gBAAA,EAAkB;AACpC,MAAA,OAAO;AAAA,QACL,EAAA,EAAI,KAAA;AAAA,QACJ,QAAQ,EAAC;AAAA,QACT,QAAA,EAAU,wDAAwD,gBAAgB,CAAA;AAAA,OACpF;AAAA,IACF;AACA,IAAA,KAAA,MAAW,SAAS,SAAA,EAAW;AAM7B,MAAA,MAAM,OAAA,GAAU,OAAO,KAAA,KAAU,QAAA,GAAW,QAAQ,KAAA,CAAM,OAAA;AAC1D,MAAA,MAAM,IAAA,GAAO,OAAO,KAAA,KAAU,QAAA,GAAW,cAAc,KAAA,CAAM,IAAA;AAC7D,MAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAC1B,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,OAAA;AAAA,QACA,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AAAA,QACzB,SAAS,OAAA,CAAQ,OAAA;AAAA,QACjB;AAAA,OACD,CAAA;AAAA,IACH;AAAA,EACF;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAO;AAC5B;AAIA,SAAS,eAAe,OAAA,EAAiD;AACvE,EAAA,MAAM,YAAA,GAAe,QAAQ,OAAO,CAAA;AACpC,EAAA,IAAI,iBAAiB,IAAA,IAAQ,YAAA,KAAiB,MAAA,IAAa,OAAO,iBAAiB,QAAA,EAAU;AAC3F,IAAA,MAAM,QAAS,YAAA,CAAuC,OAAA;AACtD,IAAA,IAAI,UAAU,MAAA,EAAW;AAEvB,MAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,OAAA,EAAS,EAAC,EAAE;AAAA,IACjC;AACA,IAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,SAAS,KAAA,EAAM;AAC9D,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ;AAAA,KACV;AAAA,EACF;AAOA,EAAA,IAAI,iBAAiB,IAAA,IAAQ,YAAA,KAAiB,MAAA,IAAa,OAAO,iBAAiB,QAAA,EAAU;AAC3F,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,kBAAA,EAAqB,OAAO,YAAY,CAAA,qCAAA;AAAA,KAClD;AAAA,EACF;AAEA,EAAA,IAAI,aAAa,OAAA,EAAS;AACxB,IAAA,MAAM,KAAA,GAAQ,QAAQ,SAAS,CAAA;AAC/B,IAAA,IAAI,KAAA,KAAU,QAAW,OAAO,EAAE,IAAI,IAAA,EAAM,OAAA,EAAS,EAAC,EAAE;AACxD,IAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,SAAS,KAAA,EAAM;AAC9D,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,8DAAA,EAA+D;AAAA,EAC7F;AAEA,EAAA,IAAI,eAAA,CAAgB,OAAO,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,SAAS,OAAA,EAAQ;AAIlE,EAAA,IAAI,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,CAAE,MAAA,KAAW,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,OAAA,EAAS,EAAC,EAAE;AACtE,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,4BAAA,EAA6B;AAC3D;AAEA,SAAS,kBAAkB,KAAA,EAAwC;AACjE,EAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,YAAY,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,KAAA;AAChF,EAAA,MAAM,GAAA,GAAM,KAAA;AACZ,EAAA,OAAO,OAAO,GAAA,CAAI,OAAA,KAAY,QAAA,IAAY,OAAO,IAAI,IAAA,KAAS,QAAA;AAChE;AAQA,SAAS,gBAAgB,KAAA,EAAiD;AACxE,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,iBAAA,CAAkB,KAAK,CAAA;AAC7D;AAWA,SAAS,gBAAgB,KAAA,EAA0C;AACjE,EAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,YAAY,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,KAAA;AAGhF,EAAA,MAAM,MAAA,GAAS,KAAA;AACf,EAAA,KAAA,MAAW,CAAA,IAAK,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,EAAG;AACnC,IAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,IAAA,IAAI,eAAA,CAAgB,CAAC,CAAA,EAAG;AACxB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,IAAK,CAAA,CAAE,KAAA,CAAM,CAAC,KAAA,KAAU,eAAA,CAAgB,KAAK,CAAC,CAAA,EAAG;AACpE,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,IAAA;AACT;;;;"}