@oomfware/forms 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -3,16 +3,167 @@ import { StandardSchemaV1 } from "@standard-schema/spec";
3
3
 
4
4
  //#region src/lib/types.d.ts
5
5
  type MaybePromise<T> = T | Promise<T>;
6
- //#endregion
7
- //#region src/lib/form.d.ts
6
+ type MaybeArray<T> = T | T[];
7
+ /**
8
+ * valid structure for form input data
9
+ */
8
10
  interface FormInput {
9
11
  [key: string]: MaybeArray<string | number | boolean | File | FormInput>;
10
12
  }
11
- type MaybeArray<T> = T | T[];
13
+ /**
14
+ * a validation issue with path information
15
+ */
12
16
  interface FormIssue {
13
17
  message: string;
14
18
  path: (string | number)[];
15
19
  }
20
+ /**
21
+ * maps input types to their corresponding value types
22
+ */
23
+ type InputTypeMap = {
24
+ text: string;
25
+ email: string;
26
+ password: string;
27
+ url: string;
28
+ tel: string;
29
+ search: string;
30
+ number: number;
31
+ range: number;
32
+ date: string;
33
+ 'datetime-local': string;
34
+ time: string;
35
+ month: string;
36
+ week: string;
37
+ color: string;
38
+ checkbox: boolean | string[];
39
+ radio: string;
40
+ file: File;
41
+ 'file multiple': File[];
42
+ hidden: string;
43
+ submit: string;
44
+ button: string;
45
+ reset: string;
46
+ image: string;
47
+ select: string;
48
+ 'select multiple': string[];
49
+ };
50
+ /**
51
+ * valid input types for a given value type
52
+ */
53
+ type FieldInputType<T> = { [K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never }[keyof InputTypeMap];
54
+ /**
55
+ * value argument for the `as()` method based on input type.
56
+ * returns `[value]` tuple for types that require a value, or `[]` otherwise.
57
+ *
58
+ * note: separating `type` from `value` in the `.as()` signature ensures TypeScript
59
+ * can properly infer the type parameter before resolving the value constraint.
60
+ * using `...args: [type, value?]` with a union of tuple types causes inference issues.
61
+ */
62
+ type AsValueArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox' ? Value extends string[] ? [value: Value[number] | (string & {})] : [] : Type extends 'radio' | 'submit' | 'hidden' ? [value: Value | (string & {})] : [];
63
+ interface CheckboxRadioProps<T extends 'checkbox' | 'radio'> {
64
+ name: string;
65
+ type: T;
66
+ value: string;
67
+ 'aria-invalid'?: 'true';
68
+ readonly checked: boolean;
69
+ }
70
+ interface FileProps {
71
+ name: string;
72
+ type: 'file';
73
+ 'aria-invalid'?: 'true';
74
+ }
75
+ interface FileMultipleProps {
76
+ name: string;
77
+ type: 'file';
78
+ multiple: true;
79
+ 'aria-invalid'?: 'true';
80
+ }
81
+ interface SelectProps {
82
+ name: string;
83
+ multiple: false;
84
+ 'aria-invalid'?: 'true';
85
+ readonly value: string;
86
+ }
87
+ interface SelectMultipleProps {
88
+ name: string;
89
+ multiple: true;
90
+ 'aria-invalid'?: 'true';
91
+ readonly value: string[];
92
+ }
93
+ interface TextProps {
94
+ name: string;
95
+ 'aria-invalid'?: 'true';
96
+ readonly value: string;
97
+ }
98
+ interface TypedInputProps<T extends string> {
99
+ name: string;
100
+ type: T;
101
+ 'aria-invalid'?: 'true';
102
+ readonly value: string;
103
+ }
104
+ /**
105
+ * input element properties based on input type
106
+ */
107
+ type InputElementProps<T extends keyof InputTypeMap> = T extends 'checkbox' | 'radio' ? CheckboxRadioProps<T> : T extends 'file' ? FileProps : T extends 'file multiple' ? FileMultipleProps : T extends 'select' ? SelectProps : T extends 'select multiple' ? SelectMultipleProps : T extends 'text' ? TextProps : TypedInputProps<T>;
108
+ /** valid leaf value types for form fields */
109
+ type FormFieldValue = string | string[] | number | boolean | File | File[];
110
+ /** guard to prevent infinite recursion when T is unknown or has an index signature */
111
+ type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
112
+ /** base methods available on all form fields */
113
+ interface FieldMethods<T> {
114
+ /** get the current value */
115
+ value(): T | undefined;
116
+ /** set the value */
117
+ set(value: T): T;
118
+ /** get validation issues for this field */
119
+ issues(): FormIssue[] | undefined;
120
+ }
121
+ /**
122
+ * leaf field type for primitive values with `.as()` method
123
+ */
124
+ type FormFieldLeaf<T extends FormFieldValue> = FieldMethods<T> & {
125
+ /**
126
+ * get props for binding to an input element.
127
+ * returns an object with `name`, `aria-invalid`, and type-specific props.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * <input {...fields.name.as('text')} />
132
+ * <input {...fields.age.as('number')} />
133
+ * <input {...fields.agreed.as('checkbox')} />
134
+ * <input {...fields.color.as('radio', 'red')} />
135
+ * ```
136
+ */
137
+ as<K$1 extends FieldInputType<T>>(type: K$1, ...value: AsValueArgs<K$1, T>): InputElementProps<K$1>;
138
+ };
139
+ /**
140
+ * container field type for objects/arrays with `.allIssues()` method
141
+ */
142
+ type FormFieldContainer<T> = FieldMethods<T> & {
143
+ /** get all issues for this field and descendants */
144
+ allIssues(): FormIssue[] | undefined;
145
+ };
146
+ /**
147
+ * fallback field type when recursion would be infinite
148
+ */
149
+ type UnknownField<T> = FieldMethods<T> & {
150
+ /** get all issues for this field and descendants */
151
+ allIssues(): FormIssue[] | undefined;
152
+ /** get props for an input element */
153
+ as<K$1 extends FieldInputType<FormFieldValue>>(type: K$1, ...value: AsValueArgs<K$1, FormFieldValue>): InputElementProps<K$1>;
154
+ } & {
155
+ [key: string | number]: UnknownField<any>;
156
+ };
157
+ type RecursiveFormFields = FormFieldContainer<any> & {
158
+ [key: string | number]: UnknownField<any>;
159
+ };
160
+ /**
161
+ * recursive type to build form fields structure with proxy access.
162
+ * preserves type information through the object hierarchy.
163
+ */
164
+ type FormFields<T> = WillRecurseIndefinitely<T> extends true ? RecursiveFormFields : NonNullable<T> extends string | number | boolean | File ? FormFieldLeaf<NonNullable<T>> : T extends string[] | File[] ? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> } : T extends Array<infer U> ? FormFieldContainer<T> & { [K in number]: FormFields<U> } : FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
165
+ //#endregion
166
+ //#region src/lib/form.d.ts
16
167
  /**
17
168
  * the issue creator proxy passed to form callbacks.
18
169
  * allows creating field-specific validation issues via property access.
@@ -56,43 +207,6 @@ interface FormButtonProps {
56
207
  type: 'submit';
57
208
  readonly formaction: string;
58
209
  }
59
- /** valid leaf value types for form fields */
60
- type FormFieldValue = string | string[] | number | boolean | File | File[];
61
- /** guard to prevent infinite recursion when T is unknown or has an index signature */
62
- type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
63
- /** base methods available on all form fields */
64
- interface FormFieldMethods<T> {
65
- /** get the current value */
66
- value(): T | undefined;
67
- /** set the value */
68
- set(value: T): T;
69
- /** get validation issues for this field */
70
- issues(): FormIssue[] | undefined;
71
- }
72
- /** leaf field (primitives, files) with .as() method */
73
- type FormFieldLeaf<T extends FormFieldValue> = FormFieldMethods<T> & {
74
- /** get props for an input element */
75
- as(type: string, value?: string): Record<string, unknown>;
76
- };
77
- /** container field (objects, arrays) with allIssues() method */
78
- type FormFieldContainer<T> = FormFieldMethods<T> & {
79
- /** get all issues for this field and descendants */
80
- allIssues(): FormIssue[] | undefined;
81
- };
82
- /** fallback field type when recursion would be infinite */
83
- type FormFieldUnknown<T> = FormFieldMethods<T> & {
84
- /** get all issues for this field and descendants */
85
- allIssues(): FormIssue[] | undefined;
86
- /** get props for an input element */
87
- as(type: string, value?: string): Record<string, unknown>;
88
- } & {
89
- [key: string | number]: FormFieldUnknown<unknown>;
90
- };
91
- /**
92
- * recursive type to build form fields structure with proxy access.
93
- * preserves type information through the object hierarchy.
94
- */
95
- type FormFields<T> = T extends void ? Record<string, never> : WillRecurseIndefinitely<T> extends true ? FormFieldUnknown<T> : NonNullable<T> extends string | number | boolean | File ? FormFieldLeaf<NonNullable<T>> : T extends string[] | File[] ? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> } : T extends Array<infer U> ? FormFieldContainer<T> & { [K in number]: FormFields<U> } : FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
96
210
  /**
97
211
  * creates a form without validation.
98
212
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/types.ts","../src/lib/form.ts","../src/lib/middleware.ts","../src/lib/errors.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/types.ts","../src/lib/form.ts","../src/lib/middleware.ts","../src/lib/errors.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -446,6 +446,14 @@ function isForm(value) {
446
446
  return value !== null && typeof value === "object" && kForm in value;
447
447
  }
448
448
  /**
449
+ * checks if the request is a cross-origin request based on Sec-Fetch-Site header.
450
+ * used for CSRF protection.
451
+ */
452
+ function isCrossOrigin(request) {
453
+ const secFetchSite = request.headers.get("sec-fetch-site");
454
+ return secFetchSite !== null && secFetchSite !== "same-origin" && secFetchSite !== "none";
455
+ }
456
+ /**
449
457
  * creates a forms middleware that registers forms and handles form submissions.
450
458
  *
451
459
  * @example
@@ -490,7 +498,10 @@ function forms(definitions) {
490
498
  const action = url.searchParams.get("__action");
491
499
  if (action && request.method === "POST") {
492
500
  const formInstance = formsById.get(action);
493
- if (formInstance) setFormState(formInstance, await processForm(formInstance, convertFormData(await request.formData())));
501
+ if (formInstance) {
502
+ if (isCrossOrigin(request)) return new Response(null, { status: 403 });
503
+ setFormState(formInstance, await processForm(formInstance, convertFormData(await request.formData())));
504
+ }
494
505
  }
495
506
  return next();
496
507
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["result: Record<string, unknown>","values: unknown[]","current: Record<string, unknown>","current: unknown","normalized: InternalFormIssue","result: Record<string, InternalFormIssue[]>","target","baseProps: InputProps","form","schema: StandardSchemaV1 | null","info: FormInfo","result: Record<string, unknown>","issue","formStore: FormStore"],"sources":["../src/lib/errors.ts","../src/lib/form-utils.ts","../src/lib/form.ts","../src/lib/middleware.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\n\n/**\n * error thrown when form validation fails imperatively\n */\nexport class ValidationError extends Error {\n\tissues: StandardSchemaV1.Issue[];\n\n\tconstructor(issues: StandardSchemaV1.Issue[]) {\n\t\tsuper('Validation failed');\n\t\tthis.name = 'ValidationError';\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * use this to throw a validation error to imperatively fail form validation.\n * can be used in combination with `issue` passed to form actions to create field-specific issues.\n *\n * @example\n * ```ts\n * import { invalid, form } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * export const login = form(\n * v.object({ name: v.string(), _password: v.string() }),\n * async ({ name, _password }, issue) => {\n * const success = tryLogin(name, _password);\n * if (!success) {\n * invalid('Incorrect username or password');\n * }\n *\n * // ...\n * }\n * );\n * ```\n */\nexport function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never {\n\tthrow new ValidationError(issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue)));\n}\n\n/**\n * checks whether this is a validation error thrown by {@link invalid}.\n */\nexport function isValidationError(e: unknown): e is ValidationError {\n\treturn e instanceof ValidationError;\n}\n","import type { StandardSchemaV1 } from '@standard-schema/spec';\n\n/**\n * internal representation of a form validation issue with computed path info\n */\nexport interface InternalFormIssue {\n\t/** dot/bracket notation path string (e.g., \"user.emails[0]\") */\n\tname: string;\n\t/** path segments as array */\n\tpath: (string | number)[];\n\t/** error message */\n\tmessage: string;\n\t/** whether this issue came from server validation */\n\tserver: boolean;\n}\n\n/**\n * sets a value in a nested object using a path string, mutating the original object\n */\nexport function setNestedValue(object: Record<string, unknown>, pathString: string, value: unknown): void {\n\tif (pathString.startsWith('n:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === '' ? undefined : parseFloat(value as string);\n\t} else if (pathString.startsWith('b:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === 'on';\n\t}\n\n\tdeepSet(object, splitPath(pathString), value);\n}\n\n/**\n * convert `FormData` into a POJO\n */\nexport function convertFormData(data: FormData): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (let key of data.keys()) {\n\t\tconst isArray = key.endsWith('[]');\n\t\tlet values: unknown[] = data.getAll(key);\n\n\t\tif (isArray) {\n\t\t\tkey = key.slice(0, -2);\n\t\t}\n\n\t\tif (values.length > 1 && !isArray) {\n\t\t\tthrow new Error(`Form cannot contain duplicated keys — \"${key}\" has ${values.length} values`);\n\t\t}\n\n\t\t// an empty `<input type=\"file\">` will submit a non-existent file, bizarrely\n\t\tvalues = values.filter(\n\t\t\t(entry) => typeof entry === 'string' || (entry as File).name !== '' || (entry as File).size > 0,\n\t\t);\n\n\t\tif (key.startsWith('n:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => (v === '' ? undefined : parseFloat(v as string)));\n\t\t} else if (key.startsWith('b:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => v === 'on');\n\t\t}\n\n\t\tsetNestedValue(result, key, isArray ? values : values[0]);\n\t}\n\n\treturn result;\n}\n\nconst PATH_REGEX = /^[a-zA-Z_$]\\w*(\\.[a-zA-Z_$]\\w*|\\[\\d+\\])*$/;\n\n/**\n * splits a path string like \"user.emails[0].address\" into [\"user\", \"emails\", \"0\", \"address\"]\n */\nexport function splitPath(path: string): string[] {\n\tif (!PATH_REGEX.test(path)) {\n\t\tthrow new Error(`Invalid path ${path}`);\n\t}\n\n\treturn path.split(/\\.|\\[|\\]/).filter(Boolean);\n}\n\n/**\n * check if a property key is dangerous and could lead to prototype pollution\n */\nfunction checkPrototypePollution(key: string): void {\n\tif (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n\t\tthrow new Error(`Invalid key \"${key}\": This key is not allowed to prevent prototype pollution.`);\n\t}\n}\n\n/**\n * sets a value in a nested object using an array of keys, mutating the original object.\n */\nexport function deepSet(object: Record<string, unknown>, keys: string[], value: unknown): void {\n\tlet current: Record<string, unknown> = object;\n\n\tfor (let i = 0; i < keys.length - 1; i += 1) {\n\t\tconst key = keys[i]!;\n\n\t\tcheckPrototypePollution(key);\n\n\t\tconst isArray = /^\\d+$/.test(keys[i + 1]!);\n\t\tconst exists = key in current;\n\t\tconst inner = current[key];\n\n\t\tif (exists && isArray !== Array.isArray(inner)) {\n\t\t\tthrow new Error(`Invalid array key ${keys[i + 1]}`);\n\t\t}\n\n\t\tif (!exists) {\n\t\t\tcurrent[key] = isArray ? [] : {};\n\t\t}\n\n\t\tcurrent = current[key] as Record<string, unknown>;\n\t}\n\n\tconst finalKey = keys[keys.length - 1]!;\n\tcheckPrototypePollution(finalKey);\n\tcurrent[finalKey] = value;\n}\n\n/**\n * gets a nested value from an object using a path array\n */\nexport function deepGet(object: Record<string, unknown>, path: (string | number)[]): unknown {\n\tlet current: unknown = object;\n\tfor (const key of path) {\n\t\tif (current == null || typeof current !== 'object') {\n\t\t\treturn current;\n\t\t}\n\t\tcurrent = (current as Record<string | number, unknown>)[key];\n\t}\n\treturn current;\n}\n\n/**\n * normalizes a Standard Schema issue into our internal format\n */\nexport function normalizeIssue(issue: StandardSchemaV1.Issue, server = false): InternalFormIssue {\n\tconst normalized: InternalFormIssue = { name: '', path: [], message: issue.message, server };\n\n\tif (issue.path !== undefined) {\n\t\tlet name = '';\n\n\t\tfor (const segment of issue.path) {\n\t\t\tconst key = typeof segment === 'object' ? (segment.key as string | number) : segment;\n\n\t\t\tnormalized.path.push(key as string | number);\n\n\t\t\tif (typeof key === 'number') {\n\t\t\t\tname += `[${key}]`;\n\t\t\t} else if (typeof key === 'string') {\n\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t}\n\t\t}\n\n\t\tnormalized.name = name;\n\t}\n\n\treturn normalized;\n}\n\n/**\n * flattens issues into a lookup object keyed by path\n * includes a special '$' key containing all issues\n */\nexport function flattenIssues(issues: InternalFormIssue[]): Record<string, InternalFormIssue[]> {\n\tconst result: Record<string, InternalFormIssue[]> = {};\n\n\tfor (const issue of issues) {\n\t\t(result.$ ??= []).push(issue);\n\n\t\tlet name = '';\n\n\t\tif (issue.path !== undefined) {\n\t\t\tfor (const key of issue.path) {\n\t\t\t\tif (typeof key === 'number') {\n\t\t\t\t\tname += `[${key}]`;\n\t\t\t\t} else if (typeof key === 'string') {\n\t\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t\t}\n\n\t\t\t\t(result[name] ??= []).push(issue);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * builds a path string from an array of path segments\n */\nexport function buildPathString(path: (string | number)[]): string {\n\tlet result = '';\n\n\tfor (const segment of path) {\n\t\tif (typeof segment === 'number') {\n\t\t\tresult += `[${segment}]`;\n\t\t} else {\n\t\t\tresult += result === '' ? segment : '.' + segment;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// #region field proxy\n\nexport interface FieldIssue {\n\tpath: (string | number)[];\n\tmessage: string;\n}\n\nexport interface FieldProxyMethods<T> {\n\t/** get the current value of this field */\n\tvalue(): T | undefined;\n\t/** set the value of this field */\n\tset(value: T): T;\n\t/** get validation issues for this exact field */\n\tissues(): FieldIssue[] | undefined;\n\t/** get all validation issues for this field and its descendants */\n\tallIssues(): FieldIssue[] | undefined;\n\t/**\n\t * get props for binding to an input element.\n\t * returns an object with `name`, `aria-invalid`, and type-specific props.\n\t */\n\tas(type: InputType, value?: string): InputProps;\n}\n\nexport type InputType =\n\t| 'text'\n\t| 'number'\n\t| 'range'\n\t| 'checkbox'\n\t| 'radio'\n\t| 'file'\n\t| 'file multiple'\n\t| 'select'\n\t| 'select multiple'\n\t| 'hidden'\n\t| 'submit'\n\t| 'email'\n\t| 'password'\n\t| 'tel'\n\t| 'url'\n\t| 'date'\n\t| 'time'\n\t| 'datetime-local'\n\t| 'month'\n\t| 'week'\n\t| 'color'\n\t| 'search';\n\nexport interface InputProps {\n\tname: string;\n\t'aria-invalid'?: 'true';\n\ttype?: string;\n\tvalue?: string;\n\tchecked?: boolean;\n\tmultiple?: boolean;\n}\n\n/**\n * creates a proxy-based field accessor for form data.\n * allows type-safe nested field access like `fields.user.emails[0].address.value()`.\n */\nexport function createFieldProxy<T>(\n\ttarget: unknown,\n\tgetInput: () => Record<string, unknown>,\n\tsetInput: (path: (string | number)[], value: unknown) => void,\n\tgetIssues: () => Record<string, InternalFormIssue[]>,\n\tpath: (string | number)[] = [],\n): T {\n\tconst getValue = () => {\n\t\treturn deepGet(getInput(), path);\n\t};\n\n\treturn new Proxy(target as object, {\n\t\tget(target, prop) {\n\t\t\tif (typeof prop === 'symbol') return (target as Record<symbol, unknown>)[prop];\n\n\t\t\t// Handle array access like jobs[0]\n\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, parseInt(prop, 10)]);\n\t\t\t}\n\n\t\t\tconst key = buildPathString(path);\n\n\t\t\tif (prop === 'set') {\n\t\t\t\tconst setFunc = function (newValue: unknown) {\n\t\t\t\t\tsetInput(path, newValue);\n\t\t\t\t\treturn newValue;\n\t\t\t\t};\n\t\t\t\treturn createFieldProxy(setFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'value') {\n\t\t\t\treturn createFieldProxy(getValue, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'issues' || prop === 'allIssues') {\n\t\t\t\tconst issuesFunc = (): FieldIssue[] | undefined => {\n\t\t\t\t\tconst allIssues = getIssues()[key === '' ? '$' : key];\n\n\t\t\t\t\tif (prop === 'allIssues') {\n\t\t\t\t\t\treturn allIssues?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\n\t\t\t\t\treturn allIssues\n\t\t\t\t\t\t?.filter((issue) => issue.name === key)\n\t\t\t\t\t\t?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(issuesFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'as') {\n\t\t\t\tconst asFunc = (type: InputType, inputValue?: string): InputProps => {\n\t\t\t\t\tconst isArray =\n\t\t\t\t\t\ttype === 'file multiple' ||\n\t\t\t\t\t\ttype === 'select multiple' ||\n\t\t\t\t\t\t(type === 'checkbox' && typeof inputValue === 'string');\n\n\t\t\t\t\tconst prefix =\n\t\t\t\t\t\ttype === 'number' || type === 'range' ? 'n:' : type === 'checkbox' && !isArray ? 'b:' : '';\n\n\t\t\t\t\t// Base properties for all input types\n\t\t\t\t\tconst baseProps: InputProps = {\n\t\t\t\t\t\tname: prefix + key + (isArray ? '[]' : ''),\n\t\t\t\t\t\tget 'aria-invalid'() {\n\t\t\t\t\t\t\tconst issues = getIssues();\n\t\t\t\t\t\t\treturn key in issues ? 'true' : undefined;\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\n\t\t\t\t\t// Add type attribute only for non-text inputs and non-select elements\n\t\t\t\t\tif (type !== 'text' && type !== 'select' && type !== 'select multiple') {\n\t\t\t\t\t\tbaseProps.type = type === 'file multiple' ? 'file' : type;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle submit and hidden inputs\n\t\t\t\t\tif (type === 'submit' || type === 'hidden') {\n\t\t\t\t\t\tif (!inputValue) {\n\t\t\t\t\t\t\tthrow new Error(`\\`${type}\\` inputs must have a value`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle select inputs\n\t\t\t\t\tif (type === 'select' || type === 'select multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\treturn getValue();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle checkbox inputs\n\t\t\t\t\tif (type === 'checkbox' || type === 'radio') {\n\t\t\t\t\t\tif (type === 'radio' && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Radio inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (type === 'checkbox' && isArray && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Checkbox array inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue ?? 'on', enumerable: true },\n\t\t\t\t\t\t\tchecked: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\tconst value = getValue();\n\n\t\t\t\t\t\t\t\t\tif (type === 'radio') {\n\t\t\t\t\t\t\t\t\t\treturn value === inputValue;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif (isArray) {\n\t\t\t\t\t\t\t\t\t\treturn ((value as string[] | undefined) ?? []).includes(inputValue!);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle file inputs (can't persist value, just return name/type/multiple)\n\t\t\t\t\tif (type === 'file' || type === 'file multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle all other input types (text, number, etc.)\n\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\tconst value = getValue();\n\t\t\t\t\t\t\t\treturn value != null ? String(value) : '';\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(asFunc, getInput, setInput, getIssues, [...path, 'as']);\n\t\t\t}\n\n\t\t\t// Handle property access (nested fields)\n\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, prop]);\n\t\t},\n\t}) as T;\n}\n\n// #endregion\n","import { createInjectionKey } from '@oomfware/fetch-router';\nimport { getContext } from '@oomfware/fetch-router/middlewares/async-context';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\n\nimport { ValidationError } from './errors.ts';\nimport {\n\tcreateFieldProxy,\n\tdeepSet,\n\tflattenIssues,\n\tnormalizeIssue,\n\ttype InternalFormIssue,\n} from './form-utils.ts';\nimport type { MaybePromise } from './types.ts';\n\n// #region types\n\nexport interface FormInput {\n\t[key: string]: MaybeArray<string | number | boolean | File | FormInput>;\n}\n\ntype MaybeArray<T> = T | T[];\n\nexport interface FormIssue {\n\tmessage: string;\n\tpath: (string | number)[];\n}\n\n/**\n * the issue creator proxy passed to form callbacks.\n * allows creating field-specific validation issues via property access.\n *\n * @example\n * ```ts\n * form(schema, async (data, issue) => {\n * if (emailTaken(data.email)) {\n * invalid(issue.email('Email already in use'));\n * }\n * // nested fields: issue.user.profile.name('Invalid name')\n * // array fields: issue.items[0].name('Invalid item name')\n * });\n * ```\n */\nexport type InvalidField<T> = ((message: string) => StandardSchemaV1.Issue) & {\n\t[K in keyof T]-?: T[K] extends (infer U)[]\n\t\t? InvalidFieldArray<U>\n\t\t: T[K] extends object\n\t\t\t? InvalidField<T[K]>\n\t\t\t: (message: string) => StandardSchemaV1.Issue;\n};\n\ntype InvalidFieldArray<T> = {\n\t[index: number]: T extends object ? InvalidField<T> : (message: string) => StandardSchemaV1.Issue;\n} & ((message: string) => StandardSchemaV1.Issue);\n\n/**\n * symbol used to identify form instances.\n */\nexport const kForm = Symbol.for('@oomfware/forms');\n\n/**\n * internal info attached to a form instance.\n * used by the forms() middleware to identify and process forms.\n */\nexport interface FormInfo {\n\t/** the schema, if any */\n\tschema: StandardSchemaV1 | null;\n\t/** the handler function */\n\tfn: (data: any, issue: any) => MaybePromise<any>;\n}\n\n/**\n * form config stored by the forms() middleware.\n */\nexport interface FormConfig {\n\t/** the form id, derived from registration name */\n\tid: string;\n}\n\n/**\n * form state stored by the forms() middleware.\n */\nexport interface FormState<Input = unknown, Output = unknown> {\n\t/** the submitted input data (for repopulating form on error) */\n\tinput?: Input;\n\t/** validation issues, flattened by path */\n\tissues?: Record<string, InternalFormIssue[]>;\n\t/** the handler result (if successful) */\n\tresult?: Output;\n}\n\n/**\n * the form store holds registered forms, their configs, and state.\n */\nexport interface FormStore {\n\t/** map of form instance to config */\n\tconfigs: WeakMap<InternalForm<any, any>, FormConfig>;\n\t/** state for each form instance */\n\tstate: WeakMap<InternalForm<any, any>, FormState>;\n}\n\n/**\n * injection key for the form store.\n */\nexport const FORM_STORE_KEY = createInjectionKey<FormStore>();\n\n/**\n * the return value of a form() function.\n * can be spread onto a <form> element.\n */\nexport interface Form<Input extends FormInput | void, Output> {\n\t/** HTTP method */\n\treadonly method: 'POST';\n\t/** the form action URL */\n\treadonly action: string;\n\t/** the handler result, if submission was successful */\n\treadonly result: Output | undefined;\n\t/** access form fields using object notation */\n\treadonly fields: FormFields<Input>;\n\t/** spread this onto a <button> or <input type=\"submit\"> */\n\treadonly buttonProps: FormButtonProps;\n}\n\n/**\n * internal form type with metadata.\n * used internally by middleware; cast Form to this when accessing `__`.\n */\nexport interface InternalForm<Input extends FormInput | void, Output> extends Form<Input, Output> {\n\t/** internal form info, used by forms() middleware */\n\treadonly __: FormInfo;\n}\n\nexport interface FormButtonProps {\n\ttype: 'submit';\n\treadonly formaction: string;\n}\n\n// #region field types\n\n/** valid leaf value types for form fields */\nexport type FormFieldValue = string | string[] | number | boolean | File | File[];\n\n/** guard to prevent infinite recursion when T is unknown or has an index signature */\ntype WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;\n\n/** base methods available on all form fields */\nexport interface FormFieldMethods<T> {\n\t/** get the current value */\n\tvalue(): T | undefined;\n\t/** set the value */\n\tset(value: T): T;\n\t/** get validation issues for this field */\n\tissues(): FormIssue[] | undefined;\n}\n\n/** leaf field (primitives, files) with .as() method */\nexport type FormFieldLeaf<T extends FormFieldValue> = FormFieldMethods<T> & {\n\t/** get props for an input element */\n\tas(type: string, value?: string): Record<string, unknown>;\n};\n\n/** container field (objects, arrays) with allIssues() method */\ntype FormFieldContainer<T> = FormFieldMethods<T> & {\n\t/** get all issues for this field and descendants */\n\tallIssues(): FormIssue[] | undefined;\n};\n\n/** fallback field type when recursion would be infinite */\ntype FormFieldUnknown<T> = FormFieldMethods<T> & {\n\t/** get all issues for this field and descendants */\n\tallIssues(): FormIssue[] | undefined;\n\t/** get props for an input element */\n\tas(type: string, value?: string): Record<string, unknown>;\n} & {\n\t[key: string | number]: FormFieldUnknown<unknown>;\n};\n\n/**\n * recursive type to build form fields structure with proxy access.\n * preserves type information through the object hierarchy.\n */\nexport type FormFields<T> = T extends void\n\t? Record<string, never>\n\t: WillRecurseIndefinitely<T> extends true\n\t\t? FormFieldUnknown<T>\n\t\t: NonNullable<T> extends string | number | boolean | File\n\t\t\t? FormFieldLeaf<NonNullable<T>>\n\t\t\t: T extends string[] | File[]\n\t\t\t\t? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> }\n\t\t\t\t: T extends Array<infer U>\n\t\t\t\t\t? FormFieldContainer<T> & { [K in number]: FormFields<U> }\n\t\t\t\t\t: FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };\n\n// #endregion\n\n// #region issue creator\n\n/**\n * creates an issue creator proxy that builds up paths for field-specific issues.\n */\nfunction createIssueCreator<T>(): InvalidField<T> {\n\treturn new Proxy((message: string) => createIssue(message), {\n\t\tget(_target, prop) {\n\t\t\tif (typeof prop === 'symbol') return undefined;\n\t\t\treturn createIssueProxy(prop, []);\n\t\t},\n\t}) as InvalidField<T>;\n\n\tfunction createIssue(message: string, path: (string | number)[] = []): StandardSchemaV1.Issue {\n\t\treturn { message, path };\n\t}\n\n\tfunction createIssueProxy(\n\t\tkey: string | number,\n\t\tpath: (string | number)[],\n\t): (message: string) => StandardSchemaV1.Issue {\n\t\tconst newPath = [...path, key];\n\n\t\tconst issueFunc = (message: string) => createIssue(message, newPath);\n\n\t\treturn new Proxy(issueFunc, {\n\t\t\tget(_target, prop) {\n\t\t\t\tif (typeof prop === 'symbol') return undefined;\n\n\t\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\t\treturn createIssueProxy(parseInt(prop, 10), newPath);\n\t\t\t\t}\n\n\t\t\t\treturn createIssueProxy(prop, newPath);\n\t\t\t},\n\t\t});\n\t}\n}\n\n// #endregion\n\n// #region form state access\n\n/**\n * get the form store from the current request context.\n * @throws if called outside of a request context\n */\nexport function getFormStore(): FormStore {\n\tconst context = getContext();\n\tconst store = context.store.inject(FORM_STORE_KEY);\n\n\tif (!store) {\n\t\tthrow new Error('form store not found. make sure the forms() middleware is installed.');\n\t}\n\n\treturn store;\n}\n\n/**\n * get config for a specific form instance.\n * @throws if form is not registered with forms() middleware\n */\nfunction getFormConfig(form: InternalForm<any, any>): FormConfig {\n\tconst store = getFormStore();\n\tconst config = store.configs.get(form);\n\n\tif (!config) {\n\t\tthrow new Error('form not registered. make sure to pass it to the forms() middleware.');\n\t}\n\n\treturn config;\n}\n\n/**\n * get state for a specific form instance.\n */\nexport function getFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n): FormState<Input, Output> | undefined {\n\tconst store = getFormStore();\n\treturn store.state.get(form) as FormState<Input, Output> | undefined;\n}\n\n/**\n * set state for a specific form instance.\n */\nexport function setFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n\tstate: FormState<Input, Output>,\n): void {\n\tconst store = getFormStore();\n\tstore.state.set(form, state);\n}\n\n// #endregion\n\n// #region form function\n\n/**\n * creates a form without validation.\n */\nexport function form<Output>(fn: () => MaybePromise<Output>): Form<void, Output>;\n\n/**\n * creates a form with unchecked input (no validation).\n */\nexport function form<Input extends FormInput, Output>(\n\tvalidate: 'unchecked',\n\tfn: (data: Input, issue: InvalidField<Input>) => MaybePromise<Output>,\n): Form<Input, Output>;\n\n/**\n * creates a form with Standard Schema validation.\n */\nexport function form<Schema extends StandardSchemaV1<FormInput, Record<string, unknown>>, Output>(\n\tvalidate: Schema,\n\tfn: (\n\t\tdata: StandardSchemaV1.InferOutput<Schema>,\n\t\tissue: InvalidField<StandardSchemaV1.InferInput<Schema>>,\n\t) => MaybePromise<Output>,\n): Form<StandardSchemaV1.InferInput<Schema>, Output>;\n\nexport function form(\n\tvalidateOrFn: StandardSchemaV1 | 'unchecked' | (() => MaybePromise<unknown>),\n\tmaybeFn?: (data: any, issue: any) => MaybePromise<unknown>,\n): Form<any, any> {\n\tconst fn = (maybeFn ?? validateOrFn) as (data: any, issue: any) => MaybePromise<unknown>;\n\n\tconst schema: StandardSchemaV1 | null =\n\t\t!maybeFn || validateOrFn === 'unchecked' ? null : (validateOrFn as StandardSchemaV1);\n\n\tconst instance = {} as InternalForm<any, any>;\n\n\tconst info: FormInfo = {\n\t\tschema,\n\t\tfn,\n\t};\n\n\t// method\n\tObject.defineProperty(instance, 'method', {\n\t\tvalue: 'POST',\n\t\tenumerable: true,\n\t});\n\n\t// action - computed from form store\n\tObject.defineProperty(instance, 'action', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn `?__action=${config.id}`;\n\t\t},\n\t\tenumerable: true,\n\t});\n\n\t// result - from state store\n\tObject.defineProperty(instance, 'result', {\n\t\tget() {\n\t\t\treturn getFormState(instance)?.result;\n\t\t},\n\t});\n\n\t// fields - proxy for field access\n\tObject.defineProperty(instance, 'fields', {\n\t\tget() {\n\t\t\treturn createFieldProxy(\n\t\t\t\t{},\n\t\t\t\t() => (getFormState(instance)?.input as Record<string, unknown>) ?? {},\n\t\t\t\t(path, value) => {\n\t\t\t\t\tconst currentState = getFormState(instance) ?? { input: {} };\n\t\t\t\t\tif (path.length === 0) {\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input: value });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst input = (currentState.input as Record<string, unknown>) ?? {};\n\t\t\t\t\t\tdeepSet(input, path.map(String), value);\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => getFormState(instance)?.issues ?? {},\n\t\t\t);\n\t\t},\n\t});\n\n\t// buttonProps\n\tObject.defineProperty(instance, 'buttonProps', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn {\n\t\t\t\ttype: 'submit' as const,\n\t\t\t\tformaction: `?__action=${config.id}`,\n\t\t\t};\n\t\t},\n\t});\n\n\t// internal info\n\tObject.defineProperty(instance, '__', {\n\t\tvalue: info,\n\t});\n\n\t// brand symbol for identification\n\tObject.defineProperty(instance, kForm, {\n\t\tvalue: true,\n\t\tenumerable: false,\n\t});\n\n\treturn instance;\n}\n\n// #endregion\n\n// #region form processing\n\n/**\n * redacts sensitive fields (those starting with `_`) from form input.\n * this prevents passwords and other sensitive data from being returned in form state.\n */\nfunction redactSensitiveFields(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (const key of Object.keys(obj)) {\n\t\tif (key.startsWith('_')) continue;\n\n\t\tconst value = obj[key];\n\n\t\tif (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n\t\t\tresult[key] = redactSensitiveFields(value as Record<string, unknown>);\n\t\t} else if (Array.isArray(value)) {\n\t\t\tresult[key] = value.map((item) =>\n\t\t\t\titem !== null && typeof item === 'object' && !(item instanceof File)\n\t\t\t\t\t? redactSensitiveFields(item as Record<string, unknown>)\n\t\t\t\t\t: item,\n\t\t\t);\n\t\t} else {\n\t\t\tresult[key] = value;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * process a form submission.\n * called by forms() middleware when a matching action is received.\n */\nexport async function processForm(formInstance: InternalForm<any, any>, data: FormInput): Promise<FormState> {\n\tconst { schema, fn } = formInstance.__;\n\n\tlet validatedData = data;\n\n\t// validate with schema if present\n\tif (schema) {\n\t\tconst result = await schema['~standard'].validate(data);\n\n\t\tif (result.issues) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(result.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\t\tvalidatedData = result.value as FormInput;\n\t}\n\n\t// run handler\n\tconst issue = createIssueCreator();\n\n\ttry {\n\t\treturn {\n\t\t\tresult: await fn(validatedData, issue),\n\t\t\tissues: undefined,\n\t\t\tinput: undefined,\n\t\t};\n\t} catch (e) {\n\t\tif (e instanceof ValidationError) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(e.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\n\t\tthrow e;\n\t}\n}\n\n// #endregion\n","import type { Middleware } from '@oomfware/fetch-router';\n\nimport { convertFormData } from './form-utils.ts';\nimport {\n\tFORM_STORE_KEY,\n\tkForm,\n\tprocessForm,\n\tsetFormState,\n\ttype Form,\n\ttype FormConfig,\n\ttype FormStore,\n\ttype InternalForm,\n} from './form.ts';\n\n// #region types\n\n/**\n * a record of form instances to register with the middleware.\n */\nexport type FormDefinitions = Record<string, Form<any, any>>;\n\n// #endregion\n\n// #region helpers\n\n/**\n * checks if a value is a form instance created by form().\n */\nfunction isForm(value: unknown): value is Form<any, any> {\n\treturn value !== null && typeof value === 'object' && kForm in value;\n}\n\n// #endregion\n\n// #region middleware\n\n/**\n * creates a forms middleware that registers forms and handles form submissions.\n *\n * @example\n * ```ts\n * import { form, forms } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * const createUserForm = form(\n * v.object({ name: v.string(), password: v.string() }),\n * async (input, issue) => {\n * // handle form submission\n * },\n * );\n *\n * router.map(routes.admin, {\n * middleware: [forms({ createUserForm })],\n * action() {\n * return render(\n * <form {...createUserForm}>\n * <input {...createUserForm.fields.name.as('text')} required />\n * </form>\n * );\n * },\n * });\n * ```\n */\nexport function forms(definitions: FormDefinitions): Middleware {\n\tconst formConfig = new WeakMap<InternalForm<any, any>, FormConfig>();\n\tconst formsById = new Map<string, InternalForm<any, any>>();\n\n\tfor (const [name, formInstance] of Object.entries(definitions)) {\n\t\tif (!isForm(formInstance)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst f = formInstance as InternalForm<any, any>;\n\n\t\tformConfig.set(f, { id: name });\n\t\tformsById.set(name, f);\n\t}\n\n\treturn async ({ request, url, store }, next) => {\n\t\t// create form store for this request\n\t\tconst formStore: FormStore = {\n\t\t\tconfigs: formConfig,\n\t\t\tstate: new WeakMap(),\n\t\t};\n\n\t\t// inject form store into context\n\t\tstore.provide(FORM_STORE_KEY, formStore);\n\n\t\t// check if this is a form submission\n\t\tconst action = url.searchParams.get('__action');\n\n\t\tif (action && request.method === 'POST') {\n\t\t\t// find the form\n\t\t\tconst formInstance = formsById.get(action);\n\n\t\t\tif (formInstance) {\n\t\t\t\t// parse form data\n\t\t\t\tconst formData = await request.formData();\n\t\t\t\tconst data = convertFormData(formData as unknown as FormData);\n\n\t\t\t\t// process the form\n\t\t\t\tconst state = await processForm(formInstance, data as any);\n\n\t\t\t\t// store the state\n\t\t\t\tsetFormState(formInstance, state);\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// #endregion\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["result: Record<string, unknown>","values: unknown[]","current: Record<string, unknown>","current: unknown","normalized: InternalFormIssue","result: Record<string, InternalFormIssue[]>","target","baseProps: Record<string, unknown>","form","schema: StandardSchemaV1 | null","info: FormInfo","result: Record<string, unknown>","issue","formStore: FormStore"],"sources":["../src/lib/errors.ts","../src/lib/form-utils.ts","../src/lib/form.ts","../src/lib/middleware.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\n\n/**\n * error thrown when form validation fails imperatively\n */\nexport class ValidationError extends Error {\n\tissues: StandardSchemaV1.Issue[];\n\n\tconstructor(issues: StandardSchemaV1.Issue[]) {\n\t\tsuper('Validation failed');\n\t\tthis.name = 'ValidationError';\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * use this to throw a validation error to imperatively fail form validation.\n * can be used in combination with `issue` passed to form actions to create field-specific issues.\n *\n * @example\n * ```ts\n * import { invalid, form } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * export const login = form(\n * v.object({ name: v.string(), _password: v.string() }),\n * async ({ name, _password }, issue) => {\n * const success = tryLogin(name, _password);\n * if (!success) {\n * invalid('Incorrect username or password');\n * }\n *\n * // ...\n * }\n * );\n * ```\n */\nexport function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never {\n\tthrow new ValidationError(issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue)));\n}\n\n/**\n * checks whether this is a validation error thrown by {@link invalid}.\n */\nexport function isValidationError(e: unknown): e is ValidationError {\n\treturn e instanceof ValidationError;\n}\n","import type { StandardSchemaV1 } from '@standard-schema/spec';\n\nimport type { FormIssue, InputType, InternalFormIssue } from './types.ts';\n\n/**\n * sets a value in a nested object using a path string, mutating the original object\n */\nexport function setNestedValue(object: Record<string, unknown>, pathString: string, value: unknown): void {\n\tif (pathString.startsWith('n:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === '' ? undefined : parseFloat(value as string);\n\t} else if (pathString.startsWith('b:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === 'on';\n\t}\n\n\tdeepSet(object, splitPath(pathString), value);\n}\n\n/**\n * convert `FormData` into a POJO\n */\nexport function convertFormData(data: FormData): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (let key of data.keys()) {\n\t\tconst isArray = key.endsWith('[]');\n\t\tlet values: unknown[] = data.getAll(key);\n\n\t\tif (isArray) {\n\t\t\tkey = key.slice(0, -2);\n\t\t}\n\n\t\tif (values.length > 1 && !isArray) {\n\t\t\tthrow new Error(`Form cannot contain duplicated keys — \"${key}\" has ${values.length} values`);\n\t\t}\n\n\t\t// an empty `<input type=\"file\">` will submit a non-existent file, bizarrely\n\t\tvalues = values.filter(\n\t\t\t(entry) => typeof entry === 'string' || (entry as File).name !== '' || (entry as File).size > 0,\n\t\t);\n\n\t\tif (key.startsWith('n:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => (v === '' ? undefined : parseFloat(v as string)));\n\t\t} else if (key.startsWith('b:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => v === 'on');\n\t\t}\n\n\t\tsetNestedValue(result, key, isArray ? values : values[0]);\n\t}\n\n\treturn result;\n}\n\nconst PATH_REGEX = /^[a-zA-Z_$]\\w*(\\.[a-zA-Z_$]\\w*|\\[\\d+\\])*$/;\n\n/**\n * splits a path string like \"user.emails[0].address\" into [\"user\", \"emails\", \"0\", \"address\"]\n */\nexport function splitPath(path: string): string[] {\n\tif (!PATH_REGEX.test(path)) {\n\t\tthrow new Error(`Invalid path ${path}`);\n\t}\n\n\treturn path.split(/\\.|\\[|\\]/).filter(Boolean);\n}\n\n/**\n * check if a property key is dangerous and could lead to prototype pollution\n */\nfunction checkPrototypePollution(key: string): void {\n\tif (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n\t\tthrow new Error(`Invalid key \"${key}\": This key is not allowed to prevent prototype pollution.`);\n\t}\n}\n\n/**\n * sets a value in a nested object using an array of keys, mutating the original object.\n */\nexport function deepSet(object: Record<string, unknown>, keys: string[], value: unknown): void {\n\tlet current: Record<string, unknown> = object;\n\n\tfor (let i = 0; i < keys.length - 1; i += 1) {\n\t\tconst key = keys[i]!;\n\n\t\tcheckPrototypePollution(key);\n\n\t\tconst isArray = /^\\d+$/.test(keys[i + 1]!);\n\t\tconst exists = key in current;\n\t\tconst inner = current[key];\n\n\t\tif (exists && isArray !== Array.isArray(inner)) {\n\t\t\tthrow new Error(`Invalid array key ${keys[i + 1]}`);\n\t\t}\n\n\t\tif (!exists) {\n\t\t\tcurrent[key] = isArray ? [] : {};\n\t\t}\n\n\t\tcurrent = current[key] as Record<string, unknown>;\n\t}\n\n\tconst finalKey = keys[keys.length - 1]!;\n\tcheckPrototypePollution(finalKey);\n\tcurrent[finalKey] = value;\n}\n\n/**\n * gets a nested value from an object using a path array\n */\nexport function deepGet(object: Record<string, unknown>, path: (string | number)[]): unknown {\n\tlet current: unknown = object;\n\tfor (const key of path) {\n\t\tif (current == null || typeof current !== 'object') {\n\t\t\treturn current;\n\t\t}\n\t\tcurrent = (current as Record<string | number, unknown>)[key];\n\t}\n\treturn current;\n}\n\n/**\n * normalizes a Standard Schema issue into our internal format\n */\nexport function normalizeIssue(issue: StandardSchemaV1.Issue, server = false): InternalFormIssue {\n\tconst normalized: InternalFormIssue = { name: '', path: [], message: issue.message, server };\n\n\tif (issue.path !== undefined) {\n\t\tlet name = '';\n\n\t\tfor (const segment of issue.path) {\n\t\t\tconst key = typeof segment === 'object' ? (segment.key as string | number) : segment;\n\n\t\t\tnormalized.path.push(key as string | number);\n\n\t\t\tif (typeof key === 'number') {\n\t\t\t\tname += `[${key}]`;\n\t\t\t} else if (typeof key === 'string') {\n\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t}\n\t\t}\n\n\t\tnormalized.name = name;\n\t}\n\n\treturn normalized;\n}\n\n/**\n * flattens issues into a lookup object keyed by path\n * includes a special '$' key containing all issues\n */\nexport function flattenIssues(issues: InternalFormIssue[]): Record<string, InternalFormIssue[]> {\n\tconst result: Record<string, InternalFormIssue[]> = {};\n\n\tfor (const issue of issues) {\n\t\t(result.$ ??= []).push(issue);\n\n\t\tlet name = '';\n\n\t\tif (issue.path !== undefined) {\n\t\t\tfor (const key of issue.path) {\n\t\t\t\tif (typeof key === 'number') {\n\t\t\t\t\tname += `[${key}]`;\n\t\t\t\t} else if (typeof key === 'string') {\n\t\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t\t}\n\n\t\t\t\t(result[name] ??= []).push(issue);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * builds a path string from an array of path segments\n */\nexport function buildPathString(path: (string | number)[]): string {\n\tlet result = '';\n\n\tfor (const segment of path) {\n\t\tif (typeof segment === 'number') {\n\t\t\tresult += `[${segment}]`;\n\t\t} else {\n\t\t\tresult += result === '' ? segment : '.' + segment;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// #region field proxy\n\n/**\n * creates a proxy-based field accessor for form data.\n * allows type-safe nested field access like `fields.user.emails[0].address.value()`.\n */\nexport function createFieldProxy<T>(\n\ttarget: unknown,\n\tgetInput: () => Record<string, unknown>,\n\tsetInput: (path: (string | number)[], value: unknown) => void,\n\tgetIssues: () => Record<string, InternalFormIssue[]>,\n\tpath: (string | number)[] = [],\n): T {\n\tconst getValue = () => {\n\t\treturn deepGet(getInput(), path);\n\t};\n\n\treturn new Proxy(target as object, {\n\t\tget(target, prop) {\n\t\t\tif (typeof prop === 'symbol') return (target as Record<symbol, unknown>)[prop];\n\n\t\t\t// handle array access like jobs[0]\n\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, parseInt(prop, 10)]);\n\t\t\t}\n\n\t\t\tconst key = buildPathString(path);\n\n\t\t\tif (prop === 'set') {\n\t\t\t\tconst setFunc = function (newValue: unknown) {\n\t\t\t\t\tsetInput(path, newValue);\n\t\t\t\t\treturn newValue;\n\t\t\t\t};\n\t\t\t\treturn createFieldProxy(setFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'value') {\n\t\t\t\treturn createFieldProxy(getValue, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'issues' || prop === 'allIssues') {\n\t\t\t\tconst issuesFunc = (): FormIssue[] | undefined => {\n\t\t\t\t\tconst allIssues = getIssues()[key === '' ? '$' : key];\n\n\t\t\t\t\tif (prop === 'allIssues') {\n\t\t\t\t\t\treturn allIssues?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\n\t\t\t\t\treturn allIssues\n\t\t\t\t\t\t?.filter((issue) => issue.name === key)\n\t\t\t\t\t\t?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(issuesFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'as') {\n\t\t\t\tconst asFunc = (type: InputType, inputValue?: string): Record<string, unknown> => {\n\t\t\t\t\tconst isArray =\n\t\t\t\t\t\ttype === 'file multiple' ||\n\t\t\t\t\t\ttype === 'select multiple' ||\n\t\t\t\t\t\t(type === 'checkbox' && typeof inputValue === 'string');\n\n\t\t\t\t\tconst prefix =\n\t\t\t\t\t\ttype === 'number' || type === 'range' ? 'n:' : type === 'checkbox' && !isArray ? 'b:' : '';\n\n\t\t\t\t\t// base properties for all input types\n\t\t\t\t\tconst baseProps: Record<string, unknown> = {\n\t\t\t\t\t\tname: prefix + key + (isArray ? '[]' : ''),\n\t\t\t\t\t\tget 'aria-invalid'() {\n\t\t\t\t\t\t\tconst issues = getIssues();\n\t\t\t\t\t\t\treturn key in issues ? 'true' : undefined;\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\n\t\t\t\t\t// add type attribute only for non-text inputs and non-select elements\n\t\t\t\t\tif (type !== 'text' && type !== 'select' && type !== 'select multiple') {\n\t\t\t\t\t\tbaseProps.type = type === 'file multiple' ? 'file' : type;\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle submit and hidden inputs\n\t\t\t\t\tif (type === 'submit' || type === 'hidden') {\n\t\t\t\t\t\tif (!inputValue) {\n\t\t\t\t\t\t\tthrow new Error(`\\`${type}\\` inputs must have a value`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle select inputs\n\t\t\t\t\tif (type === 'select' || type === 'select multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\treturn getValue();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle checkbox inputs\n\t\t\t\t\tif (type === 'checkbox' || type === 'radio') {\n\t\t\t\t\t\tif (type === 'radio' && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Radio inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (type === 'checkbox' && isArray && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Checkbox array inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue ?? 'on', enumerable: true },\n\t\t\t\t\t\t\tchecked: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\tconst value = getValue();\n\n\t\t\t\t\t\t\t\t\tif (type === 'radio') {\n\t\t\t\t\t\t\t\t\t\treturn value === inputValue;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif (isArray) {\n\t\t\t\t\t\t\t\t\t\treturn ((value as string[] | undefined) ?? []).includes(inputValue!);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle file inputs (can't persist value, just return name/type/multiple)\n\t\t\t\t\tif (type === 'file' || type === 'file multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle all other input types (text, number, etc.)\n\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\tconst value = getValue();\n\t\t\t\t\t\t\t\treturn value != null ? String(value) : '';\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(asFunc, getInput, setInput, getIssues, [...path, 'as']);\n\t\t\t}\n\n\t\t\t// handle property access (nested fields)\n\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, prop]);\n\t\t},\n\t}) as T;\n}\n\n// #endregion\n","import { createInjectionKey } from '@oomfware/fetch-router';\nimport { getContext } from '@oomfware/fetch-router/middlewares/async-context';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\n\nimport { ValidationError } from './errors.ts';\nimport { createFieldProxy, deepSet, flattenIssues, normalizeIssue } from './form-utils.ts';\nimport type { FormFields, FormInput, InternalFormIssue, MaybePromise } from './types.ts';\n\n// #region types\n\n/**\n * the issue creator proxy passed to form callbacks.\n * allows creating field-specific validation issues via property access.\n *\n * @example\n * ```ts\n * form(schema, async (data, issue) => {\n * if (emailTaken(data.email)) {\n * invalid(issue.email('Email already in use'));\n * }\n * // nested fields: issue.user.profile.name('Invalid name')\n * // array fields: issue.items[0].name('Invalid item name')\n * });\n * ```\n */\nexport type InvalidField<T> = ((message: string) => StandardSchemaV1.Issue) & {\n\t[K in keyof T]-?: T[K] extends (infer U)[]\n\t\t? InvalidFieldArray<U>\n\t\t: T[K] extends object\n\t\t\t? InvalidField<T[K]>\n\t\t\t: (message: string) => StandardSchemaV1.Issue;\n};\n\ntype InvalidFieldArray<T> = {\n\t[index: number]: T extends object ? InvalidField<T> : (message: string) => StandardSchemaV1.Issue;\n} & ((message: string) => StandardSchemaV1.Issue);\n\n/**\n * symbol used to identify form instances.\n */\nexport const kForm = Symbol.for('@oomfware/forms');\n\n/**\n * internal info attached to a form instance.\n * used by the forms() middleware to identify and process forms.\n */\nexport interface FormInfo {\n\t/** the schema, if any */\n\tschema: StandardSchemaV1 | null;\n\t/** the handler function */\n\tfn: (data: any, issue: any) => MaybePromise<any>;\n}\n\n/**\n * form config stored by the forms() middleware.\n */\nexport interface FormConfig {\n\t/** the form id, derived from registration name */\n\tid: string;\n}\n\n/**\n * form state stored by the forms() middleware.\n */\nexport interface FormState<Input = unknown, Output = unknown> {\n\t/** the submitted input data (for repopulating form on error) */\n\tinput?: Input;\n\t/** validation issues, flattened by path */\n\tissues?: Record<string, InternalFormIssue[]>;\n\t/** the handler result (if successful) */\n\tresult?: Output;\n}\n\n/**\n * the form store holds registered forms, their configs, and state.\n */\nexport interface FormStore {\n\t/** map of form instance to config */\n\tconfigs: WeakMap<InternalForm<any, any>, FormConfig>;\n\t/** state for each form instance */\n\tstate: WeakMap<InternalForm<any, any>, FormState>;\n}\n\n/**\n * injection key for the form store.\n */\nexport const FORM_STORE_KEY = createInjectionKey<FormStore>();\n\n/**\n * the return value of a form() function.\n * can be spread onto a <form> element.\n */\nexport interface Form<Input extends FormInput | void, Output> {\n\t/** HTTP method */\n\treadonly method: 'POST';\n\t/** the form action URL */\n\treadonly action: string;\n\t/** the handler result, if submission was successful */\n\treadonly result: Output | undefined;\n\t/** access form fields using object notation */\n\treadonly fields: FormFields<Input>;\n\t/** spread this onto a <button> or <input type=\"submit\"> */\n\treadonly buttonProps: FormButtonProps;\n}\n\n/**\n * internal form type with metadata.\n * used internally by middleware; cast Form to this when accessing `__`.\n */\nexport interface InternalForm<Input extends FormInput | void, Output> extends Form<Input, Output> {\n\t/** internal form info, used by forms() middleware */\n\treadonly __: FormInfo;\n}\n\nexport interface FormButtonProps {\n\ttype: 'submit';\n\treadonly formaction: string;\n}\n\n// #region issue creator\n\n/**\n * creates an issue creator proxy that builds up paths for field-specific issues.\n */\nfunction createIssueCreator<T>(): InvalidField<T> {\n\treturn new Proxy((message: string) => createIssue(message), {\n\t\tget(_target, prop) {\n\t\t\tif (typeof prop === 'symbol') return undefined;\n\t\t\treturn createIssueProxy(prop, []);\n\t\t},\n\t}) as InvalidField<T>;\n\n\tfunction createIssue(message: string, path: (string | number)[] = []): StandardSchemaV1.Issue {\n\t\treturn { message, path };\n\t}\n\n\tfunction createIssueProxy(\n\t\tkey: string | number,\n\t\tpath: (string | number)[],\n\t): (message: string) => StandardSchemaV1.Issue {\n\t\tconst newPath = [...path, key];\n\n\t\tconst issueFunc = (message: string) => createIssue(message, newPath);\n\n\t\treturn new Proxy(issueFunc, {\n\t\t\tget(_target, prop) {\n\t\t\t\tif (typeof prop === 'symbol') return undefined;\n\n\t\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\t\treturn createIssueProxy(parseInt(prop, 10), newPath);\n\t\t\t\t}\n\n\t\t\t\treturn createIssueProxy(prop, newPath);\n\t\t\t},\n\t\t});\n\t}\n}\n\n// #endregion\n\n// #region form state access\n\n/**\n * get the form store from the current request context.\n * @throws if called outside of a request context\n */\nexport function getFormStore(): FormStore {\n\tconst context = getContext();\n\tconst store = context.store.inject(FORM_STORE_KEY);\n\n\tif (!store) {\n\t\tthrow new Error('form store not found. make sure the forms() middleware is installed.');\n\t}\n\n\treturn store;\n}\n\n/**\n * get config for a specific form instance.\n * @throws if form is not registered with forms() middleware\n */\nfunction getFormConfig(form: InternalForm<any, any>): FormConfig {\n\tconst store = getFormStore();\n\tconst config = store.configs.get(form);\n\n\tif (!config) {\n\t\tthrow new Error('form not registered. make sure to pass it to the forms() middleware.');\n\t}\n\n\treturn config;\n}\n\n/**\n * get state for a specific form instance.\n */\nexport function getFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n): FormState<Input, Output> | undefined {\n\tconst store = getFormStore();\n\treturn store.state.get(form) as FormState<Input, Output> | undefined;\n}\n\n/**\n * set state for a specific form instance.\n */\nexport function setFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n\tstate: FormState<Input, Output>,\n): void {\n\tconst store = getFormStore();\n\tstore.state.set(form, state);\n}\n\n// #endregion\n\n// #region form function\n\n/**\n * creates a form without validation.\n */\nexport function form<Output>(fn: () => MaybePromise<Output>): Form<void, Output>;\n\n/**\n * creates a form with unchecked input (no validation).\n */\nexport function form<Input extends FormInput, Output>(\n\tvalidate: 'unchecked',\n\tfn: (data: Input, issue: InvalidField<Input>) => MaybePromise<Output>,\n): Form<Input, Output>;\n\n/**\n * creates a form with Standard Schema validation.\n */\nexport function form<Schema extends StandardSchemaV1<FormInput, Record<string, unknown>>, Output>(\n\tvalidate: Schema,\n\tfn: (\n\t\tdata: StandardSchemaV1.InferOutput<Schema>,\n\t\tissue: InvalidField<StandardSchemaV1.InferInput<Schema>>,\n\t) => MaybePromise<Output>,\n): Form<StandardSchemaV1.InferInput<Schema>, Output>;\n\nexport function form(\n\tvalidateOrFn: StandardSchemaV1 | 'unchecked' | (() => MaybePromise<unknown>),\n\tmaybeFn?: (data: any, issue: any) => MaybePromise<unknown>,\n): Form<any, any> {\n\tconst fn = (maybeFn ?? validateOrFn) as (data: any, issue: any) => MaybePromise<unknown>;\n\n\tconst schema: StandardSchemaV1 | null =\n\t\t!maybeFn || validateOrFn === 'unchecked' ? null : (validateOrFn as StandardSchemaV1);\n\n\tconst instance = {} as InternalForm<any, any>;\n\n\tconst info: FormInfo = {\n\t\tschema,\n\t\tfn,\n\t};\n\n\t// method\n\tObject.defineProperty(instance, 'method', {\n\t\tvalue: 'POST',\n\t\tenumerable: true,\n\t});\n\n\t// action - computed from form store\n\tObject.defineProperty(instance, 'action', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn `?__action=${config.id}`;\n\t\t},\n\t\tenumerable: true,\n\t});\n\n\t// result - from state store\n\tObject.defineProperty(instance, 'result', {\n\t\tget() {\n\t\t\treturn getFormState(instance)?.result;\n\t\t},\n\t});\n\n\t// fields - proxy for field access\n\tObject.defineProperty(instance, 'fields', {\n\t\tget() {\n\t\t\treturn createFieldProxy(\n\t\t\t\t{},\n\t\t\t\t() => (getFormState(instance)?.input as Record<string, unknown>) ?? {},\n\t\t\t\t(path, value) => {\n\t\t\t\t\tconst currentState = getFormState(instance) ?? { input: {} };\n\t\t\t\t\tif (path.length === 0) {\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input: value });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst input = (currentState.input as Record<string, unknown>) ?? {};\n\t\t\t\t\t\tdeepSet(input, path.map(String), value);\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => getFormState(instance)?.issues ?? {},\n\t\t\t);\n\t\t},\n\t});\n\n\t// buttonProps\n\tObject.defineProperty(instance, 'buttonProps', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn {\n\t\t\t\ttype: 'submit' as const,\n\t\t\t\tformaction: `?__action=${config.id}`,\n\t\t\t};\n\t\t},\n\t});\n\n\t// internal info\n\tObject.defineProperty(instance, '__', {\n\t\tvalue: info,\n\t});\n\n\t// brand symbol for identification\n\tObject.defineProperty(instance, kForm, {\n\t\tvalue: true,\n\t\tenumerable: false,\n\t});\n\n\treturn instance;\n}\n\n// #endregion\n\n// #region form processing\n\n/**\n * redacts sensitive fields (those starting with `_`) from form input.\n * this prevents passwords and other sensitive data from being returned in form state.\n */\nfunction redactSensitiveFields(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (const key of Object.keys(obj)) {\n\t\tif (key.startsWith('_')) continue;\n\n\t\tconst value = obj[key];\n\n\t\tif (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n\t\t\tresult[key] = redactSensitiveFields(value as Record<string, unknown>);\n\t\t} else if (Array.isArray(value)) {\n\t\t\tresult[key] = value.map((item) =>\n\t\t\t\titem !== null && typeof item === 'object' && !(item instanceof File)\n\t\t\t\t\t? redactSensitiveFields(item as Record<string, unknown>)\n\t\t\t\t\t: item,\n\t\t\t);\n\t\t} else {\n\t\t\tresult[key] = value;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * process a form submission.\n * called by forms() middleware when a matching action is received.\n */\nexport async function processForm(formInstance: InternalForm<any, any>, data: FormInput): Promise<FormState> {\n\tconst { schema, fn } = formInstance.__;\n\n\tlet validatedData = data;\n\n\t// validate with schema if present\n\tif (schema) {\n\t\tconst result = await schema['~standard'].validate(data);\n\n\t\tif (result.issues) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(result.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\t\tvalidatedData = result.value as FormInput;\n\t}\n\n\t// run handler\n\tconst issue = createIssueCreator();\n\n\ttry {\n\t\treturn {\n\t\t\tresult: await fn(validatedData, issue),\n\t\t\tissues: undefined,\n\t\t\tinput: undefined,\n\t\t};\n\t} catch (e) {\n\t\tif (e instanceof ValidationError) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(e.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\n\t\tthrow e;\n\t}\n}\n\n// #endregion\n","import type { Middleware } from '@oomfware/fetch-router';\n\nimport { convertFormData } from './form-utils.ts';\nimport {\n\tFORM_STORE_KEY,\n\tkForm,\n\tprocessForm,\n\tsetFormState,\n\ttype Form,\n\ttype FormConfig,\n\ttype FormStore,\n\ttype InternalForm,\n} from './form.ts';\n\n// #region types\n\n/**\n * a record of form instances to register with the middleware.\n */\nexport type FormDefinitions = Record<string, Form<any, any>>;\n\n// #endregion\n\n// #region helpers\n\n/**\n * checks if a value is a form instance created by form().\n */\nfunction isForm(value: unknown): value is Form<any, any> {\n\treturn value !== null && typeof value === 'object' && kForm in value;\n}\n\n/**\n * checks if the request is a cross-origin request based on Sec-Fetch-Site header.\n * used for CSRF protection.\n */\nfunction isCrossOrigin(request: Request): boolean {\n\tconst secFetchSite = request.headers.get('sec-fetch-site');\n\treturn secFetchSite !== null && secFetchSite !== 'same-origin' && secFetchSite !== 'none';\n}\n\n// #endregion\n\n// #region middleware\n\n/**\n * creates a forms middleware that registers forms and handles form submissions.\n *\n * @example\n * ```ts\n * import { form, forms } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * const createUserForm = form(\n * v.object({ name: v.string(), password: v.string() }),\n * async (input, issue) => {\n * // handle form submission\n * },\n * );\n *\n * router.map(routes.admin, {\n * middleware: [forms({ createUserForm })],\n * action() {\n * return render(\n * <form {...createUserForm}>\n * <input {...createUserForm.fields.name.as('text')} required />\n * </form>\n * );\n * },\n * });\n * ```\n */\nexport function forms(definitions: FormDefinitions): Middleware {\n\tconst formConfig = new WeakMap<InternalForm<any, any>, FormConfig>();\n\tconst formsById = new Map<string, InternalForm<any, any>>();\n\n\tfor (const [name, formInstance] of Object.entries(definitions)) {\n\t\tif (!isForm(formInstance)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst f = formInstance as InternalForm<any, any>;\n\n\t\tformConfig.set(f, { id: name });\n\t\tformsById.set(name, f);\n\t}\n\n\treturn async ({ request, url, store }, next) => {\n\t\t// create form store for this request\n\t\tconst formStore: FormStore = {\n\t\t\tconfigs: formConfig,\n\t\t\tstate: new WeakMap(),\n\t\t};\n\n\t\t// inject form store into context\n\t\tstore.provide(FORM_STORE_KEY, formStore);\n\n\t\t// check if this is a form submission\n\t\tconst action = url.searchParams.get('__action');\n\n\t\tif (action && request.method === 'POST') {\n\t\t\t// find the form\n\t\t\tconst formInstance = formsById.get(action);\n\n\t\t\tif (formInstance) {\n\t\t\t\t// reject cross-origin form submissions\n\t\t\t\tif (isCrossOrigin(request)) {\n\t\t\t\t\treturn new Response(null, { status: 403 });\n\t\t\t\t}\n\t\t\t\t// parse form data\n\t\t\t\tconst formData = await request.formData();\n\t\t\t\tconst data = convertFormData(formData as unknown as FormData);\n\n\t\t\t\t// process the form\n\t\t\t\tconst state = await processForm(formInstance, data as any);\n\n\t\t\t\t// store the state\n\t\t\t\tsetFormState(formInstance, state);\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// #endregion\n"],"mappings":";;;;;;;AAKA,IAAa,kBAAb,cAAqC,MAAM;CAC1C;CAEA,YAAY,QAAkC;AAC7C,QAAM,oBAAoB;AAC1B,OAAK,OAAO;AACZ,OAAK,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;AA0BhB,SAAgB,QAAQ,GAAG,QAAoD;AAC9E,OAAM,IAAI,gBAAgB,OAAO,KAAK,UAAW,OAAO,UAAU,WAAW,EAAE,SAAS,OAAO,GAAG,MAAO,CAAC;;;;;AAM3G,SAAgB,kBAAkB,GAAkC;AACnE,QAAO,aAAa;;;;;;;;ACtCrB,SAAgB,eAAe,QAAiC,YAAoB,OAAsB;AACzG,KAAI,WAAW,WAAW,KAAK,EAAE;AAChC,eAAa,WAAW,MAAM,EAAE;AAChC,UAAQ,UAAU,KAAK,SAAY,WAAW,MAAgB;YACpD,WAAW,WAAW,KAAK,EAAE;AACvC,eAAa,WAAW,MAAM,EAAE;AAChC,UAAQ,UAAU;;AAGnB,SAAQ,QAAQ,UAAU,WAAW,EAAE,MAAM;;;;;AAM9C,SAAgB,gBAAgB,MAAyC;CACxE,MAAMA,SAAkC,EAAE;AAE1C,MAAK,IAAI,OAAO,KAAK,MAAM,EAAE;EAC5B,MAAM,UAAU,IAAI,SAAS,KAAK;EAClC,IAAIC,SAAoB,KAAK,OAAO,IAAI;AAExC,MAAI,QACH,OAAM,IAAI,MAAM,GAAG,GAAG;AAGvB,MAAI,OAAO,SAAS,KAAK,CAAC,QACzB,OAAM,IAAI,MAAM,0CAA0C,IAAI,QAAQ,OAAO,OAAO,SAAS;AAI9F,WAAS,OAAO,QACd,UAAU,OAAO,UAAU,YAAa,MAAe,SAAS,MAAO,MAAe,OAAO,EAC9F;AAED,MAAI,IAAI,WAAW,KAAK,EAAE;AACzB,SAAM,IAAI,MAAM,EAAE;AAClB,YAAS,OAAO,KAAK,MAAO,MAAM,KAAK,SAAY,WAAW,EAAY,CAAE;aAClE,IAAI,WAAW,KAAK,EAAE;AAChC,SAAM,IAAI,MAAM,EAAE;AAClB,YAAS,OAAO,KAAK,MAAM,MAAM,KAAK;;AAGvC,iBAAe,QAAQ,KAAK,UAAU,SAAS,OAAO,GAAG;;AAG1D,QAAO;;AAGR,MAAM,aAAa;;;;AAKnB,SAAgB,UAAU,MAAwB;AACjD,KAAI,CAAC,WAAW,KAAK,KAAK,CACzB,OAAM,IAAI,MAAM,gBAAgB,OAAO;AAGxC,QAAO,KAAK,MAAM,WAAW,CAAC,OAAO,QAAQ;;;;;AAM9C,SAAS,wBAAwB,KAAmB;AACnD,KAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,YAC3D,OAAM,IAAI,MAAM,gBAAgB,IAAI,4DAA4D;;;;;AAOlG,SAAgB,QAAQ,QAAiC,MAAgB,OAAsB;CAC9F,IAAIC,UAAmC;AAEvC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG;EAC5C,MAAM,MAAM,KAAK;AAEjB,0BAAwB,IAAI;EAE5B,MAAM,UAAU,QAAQ,KAAK,KAAK,IAAI,GAAI;EAC1C,MAAM,SAAS,OAAO;EACtB,MAAM,QAAQ,QAAQ;AAEtB,MAAI,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7C,OAAM,IAAI,MAAM,qBAAqB,KAAK,IAAI,KAAK;AAGpD,MAAI,CAAC,OACJ,SAAQ,OAAO,UAAU,EAAE,GAAG,EAAE;AAGjC,YAAU,QAAQ;;CAGnB,MAAM,WAAW,KAAK,KAAK,SAAS;AACpC,yBAAwB,SAAS;AACjC,SAAQ,YAAY;;;;;AAMrB,SAAgB,QAAQ,QAAiC,MAAoC;CAC5F,IAAIC,UAAmB;AACvB,MAAK,MAAM,OAAO,MAAM;AACvB,MAAI,WAAW,QAAQ,OAAO,YAAY,SACzC,QAAO;AAER,YAAW,QAA6C;;AAEzD,QAAO;;;;;AAMR,SAAgB,eAAe,OAA+B,SAAS,OAA0B;CAChG,MAAMC,aAAgC;EAAE,MAAM;EAAI,MAAM,EAAE;EAAE,SAAS,MAAM;EAAS;EAAQ;AAE5F,KAAI,MAAM,SAAS,QAAW;EAC7B,IAAI,OAAO;AAEX,OAAK,MAAM,WAAW,MAAM,MAAM;GACjC,MAAM,MAAM,OAAO,YAAY,WAAY,QAAQ,MAA0B;AAE7E,cAAW,KAAK,KAAK,IAAuB;AAE5C,OAAI,OAAO,QAAQ,SAClB,SAAQ,IAAI,IAAI;YACN,OAAO,QAAQ,SACzB,SAAQ,SAAS,KAAK,MAAM,MAAM;;AAIpC,aAAW,OAAO;;AAGnB,QAAO;;;;;;AAOR,SAAgB,cAAc,QAAkE;CAC/F,MAAMC,SAA8C,EAAE;AAEtD,MAAK,MAAM,SAAS,QAAQ;AAC3B,GAAC,OAAO,MAAM,EAAE,EAAE,KAAK,MAAM;EAE7B,IAAI,OAAO;AAEX,MAAI,MAAM,SAAS,OAClB,MAAK,MAAM,OAAO,MAAM,MAAM;AAC7B,OAAI,OAAO,QAAQ,SAClB,SAAQ,IAAI,IAAI;YACN,OAAO,QAAQ,SACzB,SAAQ,SAAS,KAAK,MAAM,MAAM;AAGnC,IAAC,OAAO,UAAU,EAAE,EAAE,KAAK,MAAM;;;AAKpC,QAAO;;;;;AAMR,SAAgB,gBAAgB,MAAmC;CAClE,IAAI,SAAS;AAEb,MAAK,MAAM,WAAW,KACrB,KAAI,OAAO,YAAY,SACtB,WAAU,IAAI,QAAQ;KAEtB,WAAU,WAAW,KAAK,UAAU,MAAM;AAI5C,QAAO;;;;;;AASR,SAAgB,iBACf,QACA,UACA,UACA,WACA,OAA4B,EAAE,EAC1B;CACJ,MAAM,iBAAiB;AACtB,SAAO,QAAQ,UAAU,EAAE,KAAK;;AAGjC,QAAO,IAAI,MAAM,QAAkB,EAClC,IAAI,UAAQ,MAAM;AACjB,MAAI,OAAO,SAAS,SAAU,QAAQC,SAAmC;AAGzE,MAAI,QAAQ,KAAK,KAAK,CACrB,QAAO,iBAAiB,EAAE,EAAE,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,SAAS,MAAM,GAAG,CAAC,CAAC;EAG1F,MAAM,MAAM,gBAAgB,KAAK;AAEjC,MAAI,SAAS,OAAO;GACnB,MAAM,UAAU,SAAU,UAAmB;AAC5C,aAAS,MAAM,SAAS;AACxB,WAAO;;AAER,UAAO,iBAAiB,SAAS,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;;AAGjF,MAAI,SAAS,QACZ,QAAO,iBAAiB,UAAU,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;AAGlF,MAAI,SAAS,YAAY,SAAS,aAAa;GAC9C,MAAM,mBAA4C;IACjD,MAAM,YAAY,WAAW,CAAC,QAAQ,KAAK,MAAM;AAEjD,QAAI,SAAS,YACZ,QAAO,WAAW,KAAK,WAAW;KACjC,MAAM,MAAM;KACZ,SAAS,MAAM;KACf,EAAE;AAGJ,WAAO,WACJ,QAAQ,UAAU,MAAM,SAAS,IAAI,EACrC,KAAK,WAAW;KACjB,MAAM,MAAM;KACZ,SAAS,MAAM;KACf,EAAE;;AAGL,UAAO,iBAAiB,YAAY,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;;AAGpF,MAAI,SAAS,MAAM;GAClB,MAAM,UAAU,MAAiB,eAAiD;IACjF,MAAM,UACL,SAAS,mBACT,SAAS,qBACR,SAAS,cAAc,OAAO,eAAe;IAM/C,MAAMC,YAAqC;KAC1C,OAJA,SAAS,YAAY,SAAS,UAAU,OAAO,SAAS,cAAc,CAAC,UAAU,OAAO,MAIzE,OAAO,UAAU,OAAO;KACvC,IAAI,iBAAiB;AAEpB,aAAO,OADQ,WAAW,GACH,SAAS;;KAEjC;AAGD,QAAI,SAAS,UAAU,SAAS,YAAY,SAAS,kBACpD,WAAU,OAAO,SAAS,kBAAkB,SAAS;AAItD,QAAI,SAAS,YAAY,SAAS,UAAU;AAC3C,SAAI,CAAC,WACJ,OAAM,IAAI,MAAM,KAAK,KAAK,6BAA6B;AAGxD,YAAO,OAAO,iBAAiB,WAAW,EACzC,OAAO;MAAE,OAAO;MAAY,YAAY;MAAM,EAC9C,CAAC;;AAIH,QAAI,SAAS,YAAY,SAAS,kBACjC,QAAO,OAAO,iBAAiB,WAAW;KACzC,UAAU;MAAE,OAAO;MAAS,YAAY;MAAM;KAC9C,OAAO;MACN,YAAY;MACZ,MAAM;AACL,cAAO,UAAU;;MAElB;KACD,CAAC;AAIH,QAAI,SAAS,cAAc,SAAS,SAAS;AAC5C,SAAI,SAAS,WAAW,CAAC,WACxB,OAAM,IAAI,MAAM,iCAAiC;AAGlD,SAAI,SAAS,cAAc,WAAW,CAAC,WACtC,OAAM,IAAI,MAAM,0CAA0C;AAG3D,YAAO,OAAO,iBAAiB,WAAW;MACzC,OAAO;OAAE,OAAO,cAAc;OAAM,YAAY;OAAM;MACtD,SAAS;OACR,YAAY;OACZ,MAAM;QACL,MAAM,QAAQ,UAAU;AAExB,YAAI,SAAS,QACZ,QAAO,UAAU;AAGlB,YAAI,QACH,SAAS,SAAkC,EAAE,EAAE,SAAS,WAAY;AAGrE,eAAO;;OAER;MACD,CAAC;;AAIH,QAAI,SAAS,UAAU,SAAS,gBAC/B,QAAO,OAAO,iBAAiB,WAAW,EACzC,UAAU;KAAE,OAAO;KAAS,YAAY;KAAM,EAC9C,CAAC;AAIH,WAAO,OAAO,iBAAiB,WAAW,EACzC,OAAO;KACN,YAAY;KACZ,MAAM;MACL,MAAM,QAAQ,UAAU;AACxB,aAAO,SAAS,OAAO,OAAO,MAAM,GAAG;;KAExC,EACD,CAAC;;AAGH,UAAO,iBAAiB,QAAQ,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;;AAIhF,SAAO,iBAAiB,EAAE,EAAE,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;IAE5E,CAAC;;;;;;;;ACjUH,MAAa,QAAQ,OAAO,IAAI,kBAAkB;;;;AA8ClD,MAAa,iBAAiB,oBAA+B;;;;AAsC7D,SAAS,qBAAyC;AACjD,QAAO,IAAI,OAAO,YAAoB,YAAY,QAAQ,EAAE,EAC3D,IAAI,SAAS,MAAM;AAClB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,SAAO,iBAAiB,MAAM,EAAE,CAAC;IAElC,CAAC;CAEF,SAAS,YAAY,SAAiB,OAA4B,EAAE,EAA0B;AAC7F,SAAO;GAAE;GAAS;GAAM;;CAGzB,SAAS,iBACR,KACA,MAC8C;EAC9C,MAAM,UAAU,CAAC,GAAG,MAAM,IAAI;EAE9B,MAAM,aAAa,YAAoB,YAAY,SAAS,QAAQ;AAEpE,SAAO,IAAI,MAAM,WAAW,EAC3B,IAAI,SAAS,MAAM;AAClB,OAAI,OAAO,SAAS,SAAU,QAAO;AAErC,OAAI,QAAQ,KAAK,KAAK,CACrB,QAAO,iBAAiB,SAAS,MAAM,GAAG,EAAE,QAAQ;AAGrD,UAAO,iBAAiB,MAAM,QAAQ;KAEvC,CAAC;;;;;;;AAYJ,SAAgB,eAA0B;CAEzC,MAAM,QADU,YAAY,CACN,MAAM,OAAO,eAAe;AAElD,KAAI,CAAC,MACJ,OAAM,IAAI,MAAM,uEAAuE;AAGxF,QAAO;;;;;;AAOR,SAAS,cAAc,QAA0C;CAEhE,MAAM,SADQ,cAAc,CACP,QAAQ,IAAIC,OAAK;AAEtC,KAAI,CAAC,OACJ,OAAM,IAAI,MAAM,uEAAuE;AAGxF,QAAO;;;;;AAMR,SAAgB,aACf,QACuC;AAEvC,QADc,cAAc,CACf,MAAM,IAAIA,OAAK;;;;;AAM7B,SAAgB,aACf,QACA,OACO;AAEP,CADc,cAAc,CACtB,MAAM,IAAIA,QAAM,MAAM;;AA+B7B,SAAgB,KACf,cACA,SACiB;CACjB,MAAM,KAAM,WAAW;CAEvB,MAAMC,SACL,CAAC,WAAW,iBAAiB,cAAc,OAAQ;CAEpD,MAAM,WAAW,EAAE;CAEnB,MAAMC,OAAiB;EACtB;EACA;EACA;AAGD,QAAO,eAAe,UAAU,UAAU;EACzC,OAAO;EACP,YAAY;EACZ,CAAC;AAGF,QAAO,eAAe,UAAU,UAAU;EACzC,MAAM;AAEL,UAAO,aADQ,cAAc,SAAS,CACX;;EAE5B,YAAY;EACZ,CAAC;AAGF,QAAO,eAAe,UAAU,UAAU,EACzC,MAAM;AACL,SAAO,aAAa,SAAS,EAAE;IAEhC,CAAC;AAGF,QAAO,eAAe,UAAU,UAAU,EACzC,MAAM;AACL,SAAO,iBACN,EAAE,QACK,aAAa,SAAS,EAAE,SAAqC,EAAE,GACrE,MAAM,UAAU;GAChB,MAAM,eAAe,aAAa,SAAS,IAAI,EAAE,OAAO,EAAE,EAAE;AAC5D,OAAI,KAAK,WAAW,EACnB,cAAa,UAAU;IAAE,GAAG;IAAc,OAAO;IAAO,CAAC;QACnD;IACN,MAAM,QAAS,aAAa,SAAqC,EAAE;AACnE,YAAQ,OAAO,KAAK,IAAI,OAAO,EAAE,MAAM;AACvC,iBAAa,UAAU;KAAE,GAAG;KAAc;KAAO,CAAC;;WAG9C,aAAa,SAAS,EAAE,UAAU,EAAE,CAC1C;IAEF,CAAC;AAGF,QAAO,eAAe,UAAU,eAAe,EAC9C,MAAM;AAEL,SAAO;GACN,MAAM;GACN,YAAY,aAHE,cAAc,SAAS,CAGL;GAChC;IAEF,CAAC;AAGF,QAAO,eAAe,UAAU,MAAM,EACrC,OAAO,MACP,CAAC;AAGF,QAAO,eAAe,UAAU,OAAO;EACtC,OAAO;EACP,YAAY;EACZ,CAAC;AAEF,QAAO;;;;;;AAWR,SAAS,sBAAsB,KAAuD;CACrF,MAAMC,SAAkC,EAAE;AAE1C,MAAK,MAAM,OAAO,OAAO,KAAK,IAAI,EAAE;AACnC,MAAI,IAAI,WAAW,IAAI,CAAE;EAEzB,MAAM,QAAQ,IAAI;AAElB,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,IAAI,EAAE,iBAAiB,MAC9F,QAAO,OAAO,sBAAsB,MAAiC;WAC3D,MAAM,QAAQ,MAAM,CAC9B,QAAO,OAAO,MAAM,KAAK,SACxB,SAAS,QAAQ,OAAO,SAAS,YAAY,EAAE,gBAAgB,QAC5D,sBAAsB,KAAgC,GACtD,KACH;MAED,QAAO,OAAO;;AAIhB,QAAO;;;;;;AAOR,eAAsB,YAAY,cAAsC,MAAqC;CAC5G,MAAM,EAAE,QAAQ,OAAO,aAAa;CAEpC,IAAI,gBAAgB;AAGpB,KAAI,QAAQ;EACX,MAAM,SAAS,MAAM,OAAO,aAAa,SAAS,KAAK;AAEvD,MAAI,OAAO,OACV,QAAO;GACN,QAAQ;GACR,QAAQ,cAAc,OAAO,OAAO,KAAK,YAAU,eAAeC,SAAO,KAAK,CAAC,CAAC;GAChF,OAAO,sBAAsB,KAAK;GAClC;AAEF,kBAAgB,OAAO;;CAIxB,MAAM,QAAQ,oBAAoB;AAElC,KAAI;AACH,SAAO;GACN,QAAQ,MAAM,GAAG,eAAe,MAAM;GACtC,QAAQ;GACR,OAAO;GACP;UACO,GAAG;AACX,MAAI,aAAa,gBAChB,QAAO;GACN,QAAQ;GACR,QAAQ,cAAc,EAAE,OAAO,KAAK,YAAU,eAAeA,SAAO,KAAK,CAAC,CAAC;GAC3E,OAAO,sBAAsB,KAAK;GAClC;AAGF,QAAM;;;;;;;;;AClXR,SAAS,OAAO,OAAyC;AACxD,QAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,SAAS;;;;;;AAOhE,SAAS,cAAc,SAA2B;CACjD,MAAM,eAAe,QAAQ,QAAQ,IAAI,iBAAiB;AAC1D,QAAO,iBAAiB,QAAQ,iBAAiB,iBAAiB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCpF,SAAgB,MAAM,aAA0C;CAC/D,MAAM,6BAAa,IAAI,SAA6C;CACpE,MAAM,4BAAY,IAAI,KAAqC;AAE3D,MAAK,MAAM,CAAC,MAAM,iBAAiB,OAAO,QAAQ,YAAY,EAAE;AAC/D,MAAI,CAAC,OAAO,aAAa,CACxB;EAGD,MAAM,IAAI;AAEV,aAAW,IAAI,GAAG,EAAE,IAAI,MAAM,CAAC;AAC/B,YAAU,IAAI,MAAM,EAAE;;AAGvB,QAAO,OAAO,EAAE,SAAS,KAAK,SAAS,SAAS;EAE/C,MAAMC,YAAuB;GAC5B,SAAS;GACT,uBAAO,IAAI,SAAS;GACpB;AAGD,QAAM,QAAQ,gBAAgB,UAAU;EAGxC,MAAM,SAAS,IAAI,aAAa,IAAI,WAAW;AAE/C,MAAI,UAAU,QAAQ,WAAW,QAAQ;GAExC,MAAM,eAAe,UAAU,IAAI,OAAO;AAE1C,OAAI,cAAc;AAEjB,QAAI,cAAc,QAAQ,CACzB,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,CAAC;AAU3C,iBAAa,cAHC,MAAM,YAAY,cAHnB,gBADI,MAAM,QAAQ,UAAU,CACoB,CAGH,CAGzB;;;AAInC,SAAO,MAAM"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oomfware/forms",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "description": "form validation middleware",
6
6
  "license": "0BSD",
7
7
  "repository": {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { form } from './lib/form.ts';
2
2
  export { forms } from './lib/middleware.ts';
3
3
  export { invalid, ValidationError, isValidationError } from './lib/errors.ts';
4
- export type { Form, FormInput, FormIssue } from './lib/form.ts';
4
+ export type { Form } from './lib/form.ts';
5
+ export type { FormInput, FormIssue } from './lib/types.ts';
@@ -1,18 +1,6 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
 
3
- /**
4
- * internal representation of a form validation issue with computed path info
5
- */
6
- export interface InternalFormIssue {
7
- /** dot/bracket notation path string (e.g., "user.emails[0]") */
8
- name: string;
9
- /** path segments as array */
10
- path: (string | number)[];
11
- /** error message */
12
- message: string;
13
- /** whether this issue came from server validation */
14
- server: boolean;
15
- }
3
+ import type { FormIssue, InputType, InternalFormIssue } from './types.ts';
16
4
 
17
5
  /**
18
6
  * sets a value in a nested object using a path string, mutating the original object
@@ -207,60 +195,6 @@ export function buildPathString(path: (string | number)[]): string {
207
195
 
208
196
  // #region field proxy
209
197
 
210
- export interface FieldIssue {
211
- path: (string | number)[];
212
- message: string;
213
- }
214
-
215
- export interface FieldProxyMethods<T> {
216
- /** get the current value of this field */
217
- value(): T | undefined;
218
- /** set the value of this field */
219
- set(value: T): T;
220
- /** get validation issues for this exact field */
221
- issues(): FieldIssue[] | undefined;
222
- /** get all validation issues for this field and its descendants */
223
- allIssues(): FieldIssue[] | undefined;
224
- /**
225
- * get props for binding to an input element.
226
- * returns an object with `name`, `aria-invalid`, and type-specific props.
227
- */
228
- as(type: InputType, value?: string): InputProps;
229
- }
230
-
231
- export type InputType =
232
- | 'text'
233
- | 'number'
234
- | 'range'
235
- | 'checkbox'
236
- | 'radio'
237
- | 'file'
238
- | 'file multiple'
239
- | 'select'
240
- | 'select multiple'
241
- | 'hidden'
242
- | 'submit'
243
- | 'email'
244
- | 'password'
245
- | 'tel'
246
- | 'url'
247
- | 'date'
248
- | 'time'
249
- | 'datetime-local'
250
- | 'month'
251
- | 'week'
252
- | 'color'
253
- | 'search';
254
-
255
- export interface InputProps {
256
- name: string;
257
- 'aria-invalid'?: 'true';
258
- type?: string;
259
- value?: string;
260
- checked?: boolean;
261
- multiple?: boolean;
262
- }
263
-
264
198
  /**
265
199
  * creates a proxy-based field accessor for form data.
266
200
  * allows type-safe nested field access like `fields.user.emails[0].address.value()`.
@@ -280,7 +214,7 @@ export function createFieldProxy<T>(
280
214
  get(target, prop) {
281
215
  if (typeof prop === 'symbol') return (target as Record<symbol, unknown>)[prop];
282
216
 
283
- // Handle array access like jobs[0]
217
+ // handle array access like jobs[0]
284
218
  if (/^\d+$/.test(prop)) {
285
219
  return createFieldProxy({}, getInput, setInput, getIssues, [...path, parseInt(prop, 10)]);
286
220
  }
@@ -300,7 +234,7 @@ export function createFieldProxy<T>(
300
234
  }
301
235
 
302
236
  if (prop === 'issues' || prop === 'allIssues') {
303
- const issuesFunc = (): FieldIssue[] | undefined => {
237
+ const issuesFunc = (): FormIssue[] | undefined => {
304
238
  const allIssues = getIssues()[key === '' ? '$' : key];
305
239
 
306
240
  if (prop === 'allIssues') {
@@ -322,7 +256,7 @@ export function createFieldProxy<T>(
322
256
  }
323
257
 
324
258
  if (prop === 'as') {
325
- const asFunc = (type: InputType, inputValue?: string): InputProps => {
259
+ const asFunc = (type: InputType, inputValue?: string): Record<string, unknown> => {
326
260
  const isArray =
327
261
  type === 'file multiple' ||
328
262
  type === 'select multiple' ||
@@ -331,8 +265,8 @@ export function createFieldProxy<T>(
331
265
  const prefix =
332
266
  type === 'number' || type === 'range' ? 'n:' : type === 'checkbox' && !isArray ? 'b:' : '';
333
267
 
334
- // Base properties for all input types
335
- const baseProps: InputProps = {
268
+ // base properties for all input types
269
+ const baseProps: Record<string, unknown> = {
336
270
  name: prefix + key + (isArray ? '[]' : ''),
337
271
  get 'aria-invalid'() {
338
272
  const issues = getIssues();
@@ -340,12 +274,12 @@ export function createFieldProxy<T>(
340
274
  },
341
275
  };
342
276
 
343
- // Add type attribute only for non-text inputs and non-select elements
277
+ // add type attribute only for non-text inputs and non-select elements
344
278
  if (type !== 'text' && type !== 'select' && type !== 'select multiple') {
345
279
  baseProps.type = type === 'file multiple' ? 'file' : type;
346
280
  }
347
281
 
348
- // Handle submit and hidden inputs
282
+ // handle submit and hidden inputs
349
283
  if (type === 'submit' || type === 'hidden') {
350
284
  if (!inputValue) {
351
285
  throw new Error(`\`${type}\` inputs must have a value`);
@@ -356,7 +290,7 @@ export function createFieldProxy<T>(
356
290
  });
357
291
  }
358
292
 
359
- // Handle select inputs
293
+ // handle select inputs
360
294
  if (type === 'select' || type === 'select multiple') {
361
295
  return Object.defineProperties(baseProps, {
362
296
  multiple: { value: isArray, enumerable: true },
@@ -369,7 +303,7 @@ export function createFieldProxy<T>(
369
303
  });
370
304
  }
371
305
 
372
- // Handle checkbox inputs
306
+ // handle checkbox inputs
373
307
  if (type === 'checkbox' || type === 'radio') {
374
308
  if (type === 'radio' && !inputValue) {
375
309
  throw new Error('Radio inputs must have a value');
@@ -400,14 +334,14 @@ export function createFieldProxy<T>(
400
334
  });
401
335
  }
402
336
 
403
- // Handle file inputs (can't persist value, just return name/type/multiple)
337
+ // handle file inputs (can't persist value, just return name/type/multiple)
404
338
  if (type === 'file' || type === 'file multiple') {
405
339
  return Object.defineProperties(baseProps, {
406
340
  multiple: { value: isArray, enumerable: true },
407
341
  });
408
342
  }
409
343
 
410
- // Handle all other input types (text, number, etc.)
344
+ // handle all other input types (text, number, etc.)
411
345
  return Object.defineProperties(baseProps, {
412
346
  value: {
413
347
  enumerable: true,
@@ -422,7 +356,7 @@ export function createFieldProxy<T>(
422
356
  return createFieldProxy(asFunc, getInput, setInput, getIssues, [...path, 'as']);
423
357
  }
424
358
 
425
- // Handle property access (nested fields)
359
+ // handle property access (nested fields)
426
360
  return createFieldProxy({}, getInput, setInput, getIssues, [...path, prop]);
427
361
  },
428
362
  }) as T;
package/src/lib/form.ts CHANGED
@@ -3,28 +3,11 @@ import { getContext } from '@oomfware/fetch-router/middlewares/async-context';
3
3
  import type { StandardSchemaV1 } from '@standard-schema/spec';
4
4
 
5
5
  import { ValidationError } from './errors.ts';
6
- import {
7
- createFieldProxy,
8
- deepSet,
9
- flattenIssues,
10
- normalizeIssue,
11
- type InternalFormIssue,
12
- } from './form-utils.ts';
13
- import type { MaybePromise } from './types.ts';
6
+ import { createFieldProxy, deepSet, flattenIssues, normalizeIssue } from './form-utils.ts';
7
+ import type { FormFields, FormInput, InternalFormIssue, MaybePromise } from './types.ts';
14
8
 
15
9
  // #region types
16
10
 
17
- export interface FormInput {
18
- [key: string]: MaybeArray<string | number | boolean | File | FormInput>;
19
- }
20
-
21
- type MaybeArray<T> = T | T[];
22
-
23
- export interface FormIssue {
24
- message: string;
25
- path: (string | number)[];
26
- }
27
-
28
11
  /**
29
12
  * the issue creator proxy passed to form callbacks.
30
13
  * allows creating field-specific validation issues via property access.
@@ -134,64 +117,6 @@ export interface FormButtonProps {
134
117
  readonly formaction: string;
135
118
  }
136
119
 
137
- // #region field types
138
-
139
- /** valid leaf value types for form fields */
140
- export type FormFieldValue = string | string[] | number | boolean | File | File[];
141
-
142
- /** guard to prevent infinite recursion when T is unknown or has an index signature */
143
- type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
144
-
145
- /** base methods available on all form fields */
146
- export interface FormFieldMethods<T> {
147
- /** get the current value */
148
- value(): T | undefined;
149
- /** set the value */
150
- set(value: T): T;
151
- /** get validation issues for this field */
152
- issues(): FormIssue[] | undefined;
153
- }
154
-
155
- /** leaf field (primitives, files) with .as() method */
156
- export type FormFieldLeaf<T extends FormFieldValue> = FormFieldMethods<T> & {
157
- /** get props for an input element */
158
- as(type: string, value?: string): Record<string, unknown>;
159
- };
160
-
161
- /** container field (objects, arrays) with allIssues() method */
162
- type FormFieldContainer<T> = FormFieldMethods<T> & {
163
- /** get all issues for this field and descendants */
164
- allIssues(): FormIssue[] | undefined;
165
- };
166
-
167
- /** fallback field type when recursion would be infinite */
168
- type FormFieldUnknown<T> = FormFieldMethods<T> & {
169
- /** get all issues for this field and descendants */
170
- allIssues(): FormIssue[] | undefined;
171
- /** get props for an input element */
172
- as(type: string, value?: string): Record<string, unknown>;
173
- } & {
174
- [key: string | number]: FormFieldUnknown<unknown>;
175
- };
176
-
177
- /**
178
- * recursive type to build form fields structure with proxy access.
179
- * preserves type information through the object hierarchy.
180
- */
181
- export type FormFields<T> = T extends void
182
- ? Record<string, never>
183
- : WillRecurseIndefinitely<T> extends true
184
- ? FormFieldUnknown<T>
185
- : NonNullable<T> extends string | number | boolean | File
186
- ? FormFieldLeaf<NonNullable<T>>
187
- : T extends string[] | File[]
188
- ? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> }
189
- : T extends Array<infer U>
190
- ? FormFieldContainer<T> & { [K in number]: FormFields<U> }
191
- : FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
192
-
193
- // #endregion
194
-
195
120
  // #region issue creator
196
121
 
197
122
  /**
@@ -30,6 +30,15 @@ function isForm(value: unknown): value is Form<any, any> {
30
30
  return value !== null && typeof value === 'object' && kForm in value;
31
31
  }
32
32
 
33
+ /**
34
+ * checks if the request is a cross-origin request based on Sec-Fetch-Site header.
35
+ * used for CSRF protection.
36
+ */
37
+ function isCrossOrigin(request: Request): boolean {
38
+ const secFetchSite = request.headers.get('sec-fetch-site');
39
+ return secFetchSite !== null && secFetchSite !== 'same-origin' && secFetchSite !== 'none';
40
+ }
41
+
33
42
  // #endregion
34
43
 
35
44
  // #region middleware
@@ -94,6 +103,10 @@ export function forms(definitions: FormDefinitions): Middleware {
94
103
  const formInstance = formsById.get(action);
95
104
 
96
105
  if (formInstance) {
106
+ // reject cross-origin form submissions
107
+ if (isCrossOrigin(request)) {
108
+ return new Response(null, { status: 403 });
109
+ }
97
110
  // parse form data
98
111
  const formData = await request.formData();
99
112
  const data = convertFormData(formData as unknown as FormData);
package/src/lib/types.ts CHANGED
@@ -1 +1,254 @@
1
+ // #region utility types
2
+
1
3
  export type MaybePromise<T> = T | Promise<T>;
4
+
5
+ type MaybeArray<T> = T | T[];
6
+
7
+ // #endregion
8
+
9
+ // #region form input types
10
+
11
+ /**
12
+ * valid structure for form input data
13
+ */
14
+ export interface FormInput {
15
+ [key: string]: MaybeArray<string | number | boolean | File | FormInput>;
16
+ }
17
+
18
+ /**
19
+ * a validation issue with path information
20
+ */
21
+ export interface FormIssue {
22
+ message: string;
23
+ path: (string | number)[];
24
+ }
25
+
26
+ /**
27
+ * internal representation of a form validation issue with computed path info
28
+ */
29
+ export interface InternalFormIssue {
30
+ /** dot/bracket notation path string (e.g., "user.emails[0]") */
31
+ name: string;
32
+ /** path segments as array */
33
+ path: (string | number)[];
34
+ /** error message */
35
+ message: string;
36
+ /** whether this issue came from server validation */
37
+ server: boolean;
38
+ }
39
+
40
+ // #endregion
41
+
42
+ // #region input type mapping
43
+
44
+ /**
45
+ * maps input types to their corresponding value types
46
+ */
47
+ export type InputTypeMap = {
48
+ text: string;
49
+ email: string;
50
+ password: string;
51
+ url: string;
52
+ tel: string;
53
+ search: string;
54
+ number: number;
55
+ range: number;
56
+ date: string;
57
+ 'datetime-local': string;
58
+ time: string;
59
+ month: string;
60
+ week: string;
61
+ color: string;
62
+ checkbox: boolean | string[];
63
+ radio: string;
64
+ file: File;
65
+ 'file multiple': File[];
66
+ hidden: string;
67
+ submit: string;
68
+ button: string;
69
+ reset: string;
70
+ image: string;
71
+ select: string;
72
+ 'select multiple': string[];
73
+ };
74
+
75
+ /**
76
+ * all valid input type strings
77
+ */
78
+ export type InputType = keyof InputTypeMap;
79
+
80
+ /**
81
+ * valid input types for a given value type
82
+ */
83
+ export type FieldInputType<T> = {
84
+ [K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never;
85
+ }[keyof InputTypeMap];
86
+
87
+ /**
88
+ * value argument for the `as()` method based on input type.
89
+ * returns `[value]` tuple for types that require a value, or `[]` otherwise.
90
+ *
91
+ * note: separating `type` from `value` in the `.as()` signature ensures TypeScript
92
+ * can properly infer the type parameter before resolving the value constraint.
93
+ * using `...args: [type, value?]` with a union of tuple types causes inference issues.
94
+ */
95
+ export type AsValueArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
96
+ ? Value extends string[]
97
+ ? [value: Value[number] | (string & {})]
98
+ : []
99
+ : Type extends 'radio' | 'submit' | 'hidden'
100
+ ? [value: Value | (string & {})]
101
+ : [];
102
+
103
+ // #endregion
104
+
105
+ // #region input element props
106
+
107
+ interface CheckboxRadioProps<T extends 'checkbox' | 'radio'> {
108
+ name: string;
109
+ type: T;
110
+ value: string;
111
+ 'aria-invalid'?: 'true';
112
+ readonly checked: boolean;
113
+ }
114
+
115
+ interface FileProps {
116
+ name: string;
117
+ type: 'file';
118
+ 'aria-invalid'?: 'true';
119
+ }
120
+
121
+ interface FileMultipleProps {
122
+ name: string;
123
+ type: 'file';
124
+ multiple: true;
125
+ 'aria-invalid'?: 'true';
126
+ }
127
+
128
+ interface SelectProps {
129
+ name: string;
130
+ multiple: false;
131
+ 'aria-invalid'?: 'true';
132
+ readonly value: string;
133
+ }
134
+
135
+ interface SelectMultipleProps {
136
+ name: string;
137
+ multiple: true;
138
+ 'aria-invalid'?: 'true';
139
+ readonly value: string[];
140
+ }
141
+
142
+ interface TextProps {
143
+ name: string;
144
+ 'aria-invalid'?: 'true';
145
+ readonly value: string;
146
+ }
147
+
148
+ interface TypedInputProps<T extends string> {
149
+ name: string;
150
+ type: T;
151
+ 'aria-invalid'?: 'true';
152
+ readonly value: string;
153
+ }
154
+
155
+ /**
156
+ * input element properties based on input type
157
+ */
158
+ export type InputElementProps<T extends keyof InputTypeMap> = T extends 'checkbox' | 'radio'
159
+ ? CheckboxRadioProps<T>
160
+ : T extends 'file'
161
+ ? FileProps
162
+ : T extends 'file multiple'
163
+ ? FileMultipleProps
164
+ : T extends 'select'
165
+ ? SelectProps
166
+ : T extends 'select multiple'
167
+ ? SelectMultipleProps
168
+ : T extends 'text'
169
+ ? TextProps
170
+ : TypedInputProps<T>;
171
+
172
+ // #endregion
173
+
174
+ // #region field types
175
+
176
+ /** valid leaf value types for form fields */
177
+ export type FormFieldValue = string | string[] | number | boolean | File | File[];
178
+
179
+ /** guard to prevent infinite recursion when T is unknown or has an index signature */
180
+ type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
181
+
182
+ /** base methods available on all form fields */
183
+ interface FieldMethods<T> {
184
+ /** get the current value */
185
+ value(): T | undefined;
186
+ /** set the value */
187
+ set(value: T): T;
188
+ /** get validation issues for this field */
189
+ issues(): FormIssue[] | undefined;
190
+ }
191
+
192
+ /**
193
+ * leaf field type for primitive values with `.as()` method
194
+ */
195
+ export type FormFieldLeaf<T extends FormFieldValue> = FieldMethods<T> & {
196
+ /**
197
+ * get props for binding to an input element.
198
+ * returns an object with `name`, `aria-invalid`, and type-specific props.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * <input {...fields.name.as('text')} />
203
+ * <input {...fields.age.as('number')} />
204
+ * <input {...fields.agreed.as('checkbox')} />
205
+ * <input {...fields.color.as('radio', 'red')} />
206
+ * ```
207
+ */
208
+ as<K extends FieldInputType<T>>(type: K, ...value: AsValueArgs<K, T>): InputElementProps<K>;
209
+ };
210
+
211
+ /**
212
+ * container field type for objects/arrays with `.allIssues()` method
213
+ */
214
+ export type FormFieldContainer<T> = FieldMethods<T> & {
215
+ /** get all issues for this field and descendants */
216
+ allIssues(): FormIssue[] | undefined;
217
+ };
218
+
219
+ /**
220
+ * fallback field type when recursion would be infinite
221
+ */
222
+ type UnknownField<T> = FieldMethods<T> & {
223
+ /** get all issues for this field and descendants */
224
+ allIssues(): FormIssue[] | undefined;
225
+ /** get props for an input element */
226
+ as<K extends FieldInputType<FormFieldValue>>(
227
+ type: K,
228
+ ...value: AsValueArgs<K, FormFieldValue>
229
+ ): InputElementProps<K>;
230
+ } & {
231
+ [key: string | number]: UnknownField<any>;
232
+ };
233
+
234
+ // by breaking this out into its own type, we avoid the TS recursion depth limit
235
+ type RecursiveFormFields = FormFieldContainer<any> & {
236
+ [key: string | number]: UnknownField<any>;
237
+ };
238
+
239
+ /**
240
+ * recursive type to build form fields structure with proxy access.
241
+ * preserves type information through the object hierarchy.
242
+ */
243
+ export type FormFields<T> =
244
+ WillRecurseIndefinitely<T> extends true
245
+ ? RecursiveFormFields
246
+ : NonNullable<T> extends string | number | boolean | File
247
+ ? FormFieldLeaf<NonNullable<T>>
248
+ : T extends string[] | File[]
249
+ ? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> }
250
+ : T extends Array<infer U>
251
+ ? FormFieldContainer<T> & { [K in number]: FormFields<U> }
252
+ : FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
253
+
254
+ // #endregion