@holo-js/forms 0.3.27 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holo-JS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ 12e0c6925b4706daffb3ba511f35967af401f83673e6acb4448c06ca07a02a8a
@@ -0,0 +1,54 @@
1
+ import {
2
+ FormContractError
3
+ } from "./chunk-OFPH5LBY.mjs";
4
+
5
+ // src/failure.ts
6
+ function normalizeStatus(value, fallback) {
7
+ if (typeof value === "undefined") {
8
+ return fallback;
9
+ }
10
+ if (!Number.isInteger(value) || value < 100) {
11
+ throw new FormContractError("HTTP status codes must be integers greater than or equal to 100.");
12
+ }
13
+ return value;
14
+ }
15
+ function normalizeFailureInput(input, fallbackStatus) {
16
+ if (typeof input === "number") {
17
+ return {
18
+ status: normalizeStatus(input, fallbackStatus)
19
+ };
20
+ }
21
+ return {
22
+ status: normalizeStatus(input?.status, fallbackStatus),
23
+ errors: input?.errors
24
+ };
25
+ }
26
+ function normalizeFailureErrors(fallback, override) {
27
+ if (!override) {
28
+ return fallback;
29
+ }
30
+ const normalized = { ...fallback };
31
+ for (const [field, messages] of Object.entries(override)) {
32
+ if (typeof messages !== "undefined") {
33
+ normalized[field] = messages;
34
+ }
35
+ }
36
+ return normalized;
37
+ }
38
+
39
+ // src/sensitiveInput.ts
40
+ import { validationInternals } from "@holo-js/validation";
41
+ function sanitizeFlashedInput(values, schemaDefinition) {
42
+ return validationInternals.stripSensitiveInput(values, schemaDefinition?.fields, { files: true });
43
+ }
44
+ function clearSensitiveInputValues(values, schemaDefinition) {
45
+ return validationInternals.clearSensitiveInput(values, schemaDefinition?.fields, { files: true });
46
+ }
47
+
48
+ export {
49
+ normalizeStatus,
50
+ normalizeFailureInput,
51
+ normalizeFailureErrors,
52
+ sanitizeFlashedInput,
53
+ clearSensitiveInputValues
54
+ };
@@ -1,3 +1,4 @@
1
+ import { HoloRequestHeaders } from '@holo-js/kernel';
1
2
  import { ValidationSchema, SchemaInputShape, ValidationErrorBag, InferValidationSchemaData, InferSchemaData, FormLikeValidationInput } from '@holo-js/validation';
2
3
 
3
4
  type FormFailureErrors = Readonly<Partial<Record<string, readonly string[]>>>;
@@ -9,12 +10,7 @@ type FormFailureInput = number | FormFailureOptions | undefined;
9
10
  declare function normalizeStatus(value: number | undefined, fallback: number): number;
10
11
 
11
12
  type SensitiveSchema = Pick<ValidationSchema<SchemaInputShape>, 'fields'>;
12
- declare function collectSensitivePaths(schemaDefinition: SensitiveSchema | undefined): readonly string[];
13
13
  declare function sanitizeFlashedInput<TData>(values: Partial<TData> | TData, schemaDefinition?: SensitiveSchema): Partial<TData> | TData;
14
- declare const sensitiveInputInternals: {
15
- collectSensitivePaths: typeof collectSensitivePaths;
16
- DEFAULT_DONT_FLASH_FIELDS: readonly string[];
17
- };
18
14
 
19
15
  type InferFormData<TSchema extends ValidationSchema> = InferValidationSchemaData<TSchema>;
20
16
  interface FormFailurePayload<TData> {
@@ -42,11 +38,7 @@ interface FormSecurityOptions {
42
38
  readonly throttle?: string;
43
39
  readonly bag?: string;
44
40
  }
45
- type RequestLikeHeaders = Headers | ReadonlyArray<readonly [string, string]> | Record<string, string | readonly string[] | undefined> | {
46
- readonly get?: (name: string) => string | null | undefined;
47
- readonly forEach?: (callback: (value: string, key: string) => void) => void;
48
- readonly entries?: () => Iterable<readonly [string, string]>;
49
- };
41
+ type RequestLikeHeaders = HoloRequestHeaders;
50
42
  interface StructuredRequestLikeObject {
51
43
  readonly method?: string;
52
44
  readonly path?: string;
@@ -129,4 +121,5 @@ declare const formsInternals: {
129
121
  serializeSubmissionState: typeof serializeSubmissionState;
130
122
  };
131
123
 
132
- export { type FormFailureErrors as F, type InferFormData as I, type SerializedFormSubmission as S, type FormFailureInput as a, type FormFailureOptions as b, type FormFailurePayload as c, type FormRequestLikeInput as d, type FormSecurityOptions as e, type FormSubmissionFailure as f, type FormSubmissionResult as g, type FormSubmissionSuccess as h, type FormSuccessPayload as i, createFailedSubmission as j, createSuccessfulSubmission as k, formsInternals as l, sanitizeFlashedInput as m, sensitiveInputInternals as n, safeParse as s, validate as v };
124
+ export { createFailedSubmission as j, createSuccessfulSubmission as k, formsInternals as l, sanitizeFlashedInput as m, safeParse as s, validate as v };
125
+ export type { FormFailureErrors as F, InferFormData as I, SerializedFormSubmission as S, FormFailureInput as a, FormFailureOptions as b, FormFailurePayload as c, FormRequestLikeInput as d, FormSecurityOptions as e, FormSubmissionFailure as f, FormSubmissionResult as g, FormSubmissionSuccess as h, FormSuccessPayload as i };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { F as FormFailureErrors, a as FormFailureInput, b as FormFailureOptions, c as FormFailurePayload, d as FormRequestLikeInput, e as FormSecurityOptions, f as FormSubmissionFailure, g as FormSubmissionResult, h as FormSubmissionSuccess, i as FormSuccessPayload, I as InferFormData, S as SerializedFormSubmission, j as createFailedSubmission, k as createSuccessfulSubmission, l as formsInternals, s as safeParse, m as sanitizeFlashedInput, n as sensitiveInputInternals, v as validate } from './contracts-CJ9Aq8pS.js';
1
+ export { F as FormFailureErrors, a as FormFailureInput, b as FormFailureOptions, c as FormFailurePayload, d as FormRequestLikeInput, e as FormSecurityOptions, f as FormSubmissionFailure, g as FormSubmissionResult, h as FormSubmissionSuccess, i as FormSuccessPayload, I as InferFormData, S as SerializedFormSubmission, j as createFailedSubmission, k as createSuccessfulSubmission, l as formsInternals, s as safeParse, m as sanitizeFlashedInput, v as validate } from './contracts-CN5pB9Tx.js';
2
+ import '@holo-js/kernel';
2
3
  import '@holo-js/validation';
3
4
 
4
5
  declare class FormContractError extends Error {
package/dist/index.mjs CHANGED
@@ -2,14 +2,14 @@ import {
2
2
  normalizeFailureErrors,
3
3
  normalizeFailureInput,
4
4
  normalizeStatus,
5
- sanitizeFlashedInput,
6
- sensitiveInputInternals
7
- } from "./chunk-WJOAU4V2.mjs";
5
+ sanitizeFlashedInput
6
+ } from "./chunk-TFYWXPME.mjs";
8
7
  import {
9
8
  FormContractError
10
9
  } from "./chunk-OFPH5LBY.mjs";
11
10
 
12
11
  // src/contracts.ts
12
+ import { HoloRequestBodyTooLargeError, readBoundedRequestBody, requestUrlInternals } from "@holo-js/kernel";
13
13
  import {
14
14
  ValidationException,
15
15
  createErrorBag,
@@ -188,18 +188,6 @@ function createFormDataRequestHeaders(requestHeaders) {
188
188
  });
189
189
  return formHeaders;
190
190
  }
191
- function resolveAmbientRequestUrl(headers) {
192
- const referer = headers.get("referer");
193
- if (referer) {
194
- try {
195
- return new URL(referer).href;
196
- } catch {
197
- }
198
- }
199
- const protocol = headers.get("x-forwarded-proto") ?? "http";
200
- const host = headers.get("x-forwarded-host") ?? headers.get("host") ?? "localhost";
201
- return `${protocol}://${host}/`;
202
- }
203
191
  function isNextHeadersModule(value) {
204
192
  return !!value && typeof value === "object" && typeof value.headers === "function";
205
193
  }
@@ -227,7 +215,7 @@ async function resolveAmbientFormDataRequest(input) {
227
215
  return void 0;
228
216
  }
229
217
  const requestHeaders = await nextHeadersModule.headers();
230
- return new Request(resolveAmbientRequestUrl(requestHeaders), {
218
+ return new Request(requestUrlInternals.resolveRoutedRequestUrl({ headers: requestHeaders }), {
231
219
  method: "POST",
232
220
  headers: createFormDataRequestHeaders(requestHeaders),
233
221
  body: input
@@ -237,15 +225,7 @@ function getStructuredWebRequest(input) {
237
225
  return input.web?.request instanceof Request ? void 0 : input.web?.request;
238
226
  }
239
227
  function extractRequestLikeUrl(input, headers) {
240
- const webRequest = getStructuredWebRequest(input);
241
- const url = webRequest?.url ?? (typeof input.url === "string" ? input.url : input.url?.toString()) ?? (typeof input.req === "object" && !(input.req instanceof Request) ? input.req.url : void 0) ?? input.node?.req?.url ?? input.path ?? "/";
242
- try {
243
- return new URL(url).toString();
244
- } catch {
245
- const protocol = headers.get("x-forwarded-proto") ?? "http";
246
- const host = headers.get("x-forwarded-host") ?? headers.get("host") ?? "localhost";
247
- return new URL(url, `${protocol}://${host}`).toString();
248
- }
228
+ return requestUrlInternals.resolveRoutedRequestUrl({ ...input, headers }).href;
249
229
  }
250
230
  function extractRequestLikeMethod(input) {
251
231
  const webRequest = getStructuredWebRequest(input);
@@ -376,37 +356,33 @@ function createSecurityFailureMetadata(error) {
376
356
  ...typeof retryAt === "string" ? { retryAt } : {}
377
357
  };
378
358
  }
379
- async function createSecurityFailureSubmission(input, schemaDefinition, error) {
380
- let values = {};
381
- let flattenedErrors = {};
382
- try {
383
- const inspection = await safeParseInput(input.clone(), schemaDefinition);
384
- if (inspection.valid) {
385
- values = inspection.data;
386
- } else {
387
- values = inspection.values;
388
- flattenedErrors = inspection.errors.flatten();
389
- }
390
- } catch {
391
- values = {};
392
- flattenedErrors = {};
393
- }
394
- return createFailedSubmission(
395
- schemaDefinition,
396
- values,
397
- {
398
- ...flattenedErrors,
399
- _root: [
400
- ...flattenedErrors._root ?? [],
401
- error.message
402
- ]
403
- },
404
- error.status,
405
- createSecurityFailureMetadata(error)
406
- );
359
+ function captureBodyTooLarge(error) {
360
+ if (error instanceof HoloRequestBodyTooLargeError) return error;
361
+ throw error;
362
+ }
363
+ function submittedRequestInit(request, body) {
364
+ return {
365
+ method: request.method,
366
+ headers: new Headers(request.headers),
367
+ body,
368
+ duplex: "half",
369
+ signal: request.signal,
370
+ credentials: request.credentials,
371
+ redirect: request.redirect,
372
+ referrer: request.referrer,
373
+ referrerPolicy: request.referrerPolicy,
374
+ integrity: request.integrity,
375
+ keepalive: request.keepalive,
376
+ cache: request.cache === "only-if-cached" ? void 0 : request.cache,
377
+ mode: request.mode === "navigate" ? void 0 : request.mode
378
+ };
379
+ }
380
+ async function bufferSubmittedRequest(request) {
381
+ if (request.method === "GET" || request.method === "HEAD") return request;
382
+ const body = await readBoundedRequestBody(request.clone());
383
+ return new Request(request.url, submittedRequestInit(request, body));
407
384
  }
408
385
  async function safeParse(input, schemaDefinition, options = {}) {
409
- let validatedSubmission;
410
386
  const throttle = typeof options.throttle === "string" ? options.throttle : void 0;
411
387
  const usesSecurityOptions = typeof throttle === "string";
412
388
  const normalizedRequestInput = await normalizeRequestLikeInputForValidation(input) ?? (usesSecurityOptions ? await resolveAmbientFormDataRequest(input) : void 0);
@@ -416,45 +392,49 @@ async function safeParse(input, schemaDefinition, options = {}) {
416
392
  "Security-aware safeParse() options require a Request or request-like event input."
417
393
  );
418
394
  }
395
+ const submitted = usesSecurityOptions ? await bufferSubmittedRequest(normalizedRequestInput).catch(captureBodyTooLarge) : validationInput;
396
+ if (submitted instanceof HoloRequestBodyTooLargeError) {
397
+ return createFailedSubmission(schemaDefinition, {}, {
398
+ _root: [submitted.message]
399
+ }, submitted.status);
400
+ }
401
+ const parsedInput = usesSecurityOptions ? submitted.clone() : submitted;
402
+ const result = await safeParseInput(parsedInput, schemaDefinition).catch(captureBodyTooLarge);
403
+ if (result instanceof HoloRequestBodyTooLargeError) {
404
+ return createFailedSubmission(schemaDefinition, {}, {
405
+ _root: [result.message]
406
+ }, result.status);
407
+ }
419
408
  if (usesSecurityOptions) {
420
- const request = normalizedRequestInput;
409
+ const request = submitted;
421
410
  try {
422
411
  const { loadSecurityModule } = await import("./security-YE5UGG47.mjs");
423
412
  const security = await loadSecurityModule();
424
- const inspection = await safeParseInput(request.clone(), schemaDefinition);
425
- const throttleValues = inspection.valid ? inspection.data : inspection.values;
426
- validatedSubmission = inspection.valid ? createSuccessfulSubmission(schemaDefinition, inspection.data) : createFailedSubmission(schemaDefinition, inspection.values, inspection.errors.flatten());
427
413
  await security.rateLimit(throttle, {
428
414
  request,
429
- values: throttleValues
415
+ values: result.valid ? result.data : void 0
430
416
  });
431
417
  } catch (error) {
432
418
  const { formsSecurityInternals } = await import("./security-YE5UGG47.mjs");
433
419
  if (formsSecurityInternals.isRootSecurityError(error)) {
434
- if (validatedSubmission) {
435
- return createFailedSubmission(
436
- schemaDefinition,
437
- validatedSubmission.valid ? validatedSubmission.data : validatedSubmission.values,
438
- {
439
- ...validatedSubmission.valid ? {} : validatedSubmission.errors.flatten(),
440
- _root: [
441
- ...!validatedSubmission.valid ? validatedSubmission.errors.flatten()._root ?? [] : [],
442
- error.message
443
- ]
444
- },
445
- error.status,
446
- createSecurityFailureMetadata(error)
447
- );
448
- }
449
- return await createSecurityFailureSubmission(request, schemaDefinition, error);
420
+ const fieldErrors = result.valid ? {} : result.errors.flatten();
421
+ return createFailedSubmission(
422
+ schemaDefinition,
423
+ result.valid ? result.data : result.values,
424
+ {
425
+ ...fieldErrors,
426
+ _root: [
427
+ ...fieldErrors._root ?? [],
428
+ error.message
429
+ ]
430
+ },
431
+ error.status,
432
+ createSecurityFailureMetadata(error)
433
+ );
450
434
  }
451
435
  throw error;
452
436
  }
453
437
  }
454
- if (validatedSubmission) {
455
- return validatedSubmission;
456
- }
457
- const result = await safeParseInput(validationInput, schemaDefinition);
458
438
  if (result.valid) {
459
439
  return createSuccessfulSubmission(schemaDefinition, result.data);
460
440
  }
@@ -470,7 +450,8 @@ async function validate(input, schemaDefinition, options = {}) {
470
450
  ValidationException.withMessages(result.errors.flatten(), {
471
451
  bag: options.bag
472
452
  }),
473
- result.values
453
+ result.values,
454
+ schemaDefinition
474
455
  );
475
456
  validationInternals.setValidationExceptionStatus(exception, failure.status);
476
457
  validationInternals.setValidationExceptionMetadata(exception, {
@@ -496,6 +477,5 @@ export {
496
477
  formsInternals,
497
478
  safeParse,
498
479
  sanitizeFlashedInput,
499
- sensitiveInputInternals,
500
480
  validate
501
481
  };
@@ -1,5 +1,7 @@
1
- import { S as SerializedFormSubmission, c as FormFailurePayload, i as FormSuccessPayload, g as FormSubmissionResult, I as InferFormData } from '../contracts-CJ9Aq8pS.js';
1
+ import { SerializedFormSubmission, FormFailurePayload, FormSuccessPayload, FormSubmissionResult, InferFormData } from '../index.js';
2
+ export { m as sanitizeFlashedInput } from '../contracts-CN5pB9Tx.js';
2
3
  import { ValidationErrorBag, WebFileLike, ValidationSchema, SchemaInputShape, FieldBuilderInput, InferFieldOutput } from '@holo-js/validation';
4
+ import '@holo-js/kernel';
3
5
 
4
6
  declare function collectFormDirtyPaths(current: unknown, initial: unknown, prefix?: string): readonly string[];
5
7
  declare class FormClientState<TValues, TSuccess = unknown> {
@@ -101,4 +103,5 @@ declare function runWithBrowserFormElement<TData, TSuccess>(formClient: Pick<Use
101
103
  */
102
104
  declare function createFormClient<TSchema extends ValidationSchema, TSuccess = unknown>(schemaDefinition: TSchema, options?: UseFormOptions<InferFormData<TSchema>, TSuccess>): UseFormResult<InferFormData<TSchema>, TSuccess, InferFormFieldTree<TSchema>>;
103
105
 
104
- export { type ClientCsrfOptions, type ClientSubmitContext, type ClientSubmitResult, FormClientState, type FormFieldState, type FormFieldTree, type FormRequestCredentials, type InferFormFieldTree, type UseFormOptions, type UseFormResult, type ValidateOnMode, collectFormDirtyPaths, createFormClient, markClientSubmitControlFlowError, runWithBrowserFormElement };
106
+ export { FormClientState, collectFormDirtyPaths, createFormClient, markClientSubmitControlFlowError, runWithBrowserFormElement };
107
+ export type { ClientCsrfOptions, ClientSubmitContext, ClientSubmitResult, FormFieldState, FormFieldTree, FormRequestCredentials, InferFormFieldTree, UseFormOptions, UseFormResult, ValidateOnMode };
@@ -4,7 +4,7 @@ import {
4
4
  normalizeFailureInput,
5
5
  normalizeStatus,
6
6
  sanitizeFlashedInput
7
- } from "../chunk-WJOAU4V2.mjs";
7
+ } from "../chunk-TFYWXPME.mjs";
8
8
  import {
9
9
  createMissingSecurityPackageError,
10
10
  isMissingOptionalPackageError,
@@ -32,20 +32,29 @@ async function loadSecurityClientModule() {
32
32
  });
33
33
  return await securityClientModulePromise;
34
34
  }
35
- async function getClientCsrfField() {
36
- const runtime = globalThis;
37
- if (!runtime.document || typeof runtime.document.cookie !== "string") {
38
- return void 0;
39
- }
40
- let security;
35
+ async function loadInstalledSecurityClientModule() {
41
36
  try {
42
- security = await loadSecurityClientModule();
37
+ return await loadSecurityClientModule();
43
38
  } catch (error) {
44
39
  if (error instanceof FormContractError) {
45
40
  return void 0;
46
41
  }
47
42
  throw error;
48
43
  }
44
+ }
45
+ async function getClientCsrfConfig() {
46
+ const config = (await loadInstalledSecurityClientModule())?.getSecurityClientConfig().csrf;
47
+ return { field: config?.field ?? "_token", header: config?.header ?? "X-CSRF-TOKEN" };
48
+ }
49
+ async function getClientCsrfField() {
50
+ const runtime = globalThis;
51
+ if (!runtime.document || typeof runtime.document.cookie !== "string") {
52
+ return void 0;
53
+ }
54
+ const security = await loadInstalledSecurityClientModule();
55
+ if (!security) {
56
+ return void 0;
57
+ }
49
58
  const config = security.getSecurityClientConfig().csrf;
50
59
  const value = parseCookieHeader(runtime.document.cookie)[config.cookie];
51
60
  if (!value) {
@@ -58,10 +67,7 @@ async function getClientCsrfField() {
58
67
  }
59
68
 
60
69
  // src/internal/validation-exception.ts
61
- import {
62
- isValidationException
63
- } from "@holo-js/validation";
64
- import { parseValidationExceptionDigest } from "@holo-js/validation/internal/compiled";
70
+ import { serializeValidationException } from "@holo-js/validation";
65
71
  function isPlainObject(value) {
66
72
  return !!value && typeof value === "object" && !Array.isArray(value);
67
73
  }
@@ -91,20 +97,7 @@ function formFailurePayloadFromError(error) {
91
97
  return candidates.find((candidate) => isFormFailurePayload(candidate));
92
98
  }
93
99
  function validationExceptionToFailure(error, fallbackValues = {}) {
94
- const payload = isValidationException(error) ? error.toJSON() : parseValidationExceptionDigest(error);
95
- if (payload) {
96
- return {
97
- ok: false,
98
- status: payload.status,
99
- submitted: true,
100
- valid: false,
101
- values: payload.values && Object.keys(payload.values).length > 0 ? payload.values : fallbackValues,
102
- errors: payload.errors,
103
- ...typeof payload.retryAfterSeconds === "number" ? { retryAfterSeconds: payload.retryAfterSeconds } : {},
104
- ...typeof payload.retryAt === "string" ? { retryAt: payload.retryAt } : {}
105
- };
106
- }
107
- const formFailurePayload = formFailurePayloadFromError(error);
100
+ const formFailurePayload = formFailurePayloadFromError(serializeValidationException(error) ?? error);
108
101
  if (formFailurePayload) {
109
102
  return {
110
103
  ok: false,
@@ -184,8 +177,10 @@ function getFormValueAtPath(root, path) {
184
177
  }
185
178
  return cursor;
186
179
  }
180
+ var UNSAFE_FORM_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
187
181
  function setFormValueAtPath(root, path, value) {
188
182
  const parts = splitFormPath(path);
183
+ if (parts.some((part) => UNSAFE_FORM_PATH_SEGMENTS.has(part))) return;
189
184
  let cursor = root;
190
185
  for (const [index, part] of parts.entries()) {
191
186
  const last = index === parts.length - 1;
@@ -702,7 +697,16 @@ async function defaultSubmitter(context, options) {
702
697
  if (!csrfPayload || typeof csrfPayload !== "object" || !("token" in csrfPayload) || typeof csrfPayload.token !== "string" || !csrfPayload.token) {
703
698
  return createTransportFailure(context.values);
704
699
  }
705
- headers.set("X-CSRF-TOKEN", csrfPayload.token);
700
+ if ("field" in csrfPayload && typeof csrfPayload.field === "string" && csrfPayload.field) {
701
+ context.formData.set(csrfPayload.field, csrfPayload.token);
702
+ } else {
703
+ const config = await getClientCsrfConfig();
704
+ if (config.header.toLowerCase() === "x-xsrf-token") {
705
+ context.formData.set(config.field, csrfPayload.token);
706
+ } else {
707
+ headers.set(config.header, csrfPayload.token);
708
+ }
709
+ }
706
710
  }
707
711
  const response = await fetch(context.action, {
708
712
  method,
@@ -758,16 +762,14 @@ function createFormClient(schemaDefinition, options = {}) {
758
762
  values: sanitizeFlashedInput(normalized.values, schemaDefinition)
759
763
  };
760
764
  if (applies) {
761
- const values = mergeFormValues(state.values, sanitized.values);
762
- clearSensitiveInputValues(values, schemaDefinition);
765
+ const values = clearSensitiveInputValues(mergeFormValues(state.values, sanitized.values), schemaDefinition);
763
766
  state.applyServer(values, sanitized.errors, sanitized);
764
767
  }
765
768
  return sanitized;
766
769
  }
767
770
  const normalizedSubmission = normalized;
768
771
  if (applies) {
769
- const values = mergeFormValues(state.values, normalizedSubmission.values);
770
- clearSensitiveInputValues(values, schemaDefinition);
772
+ const values = clearSensitiveInputValues(mergeFormValues(state.values, normalizedSubmission.values), schemaDefinition);
771
773
  state.applyServer(values, normalizedSubmission.errors.flatten(), normalizedSubmission.valid ? void 0 : normalizedSubmission.fail());
772
774
  }
773
775
  return normalizedSubmission;
@@ -793,9 +795,9 @@ function createFormClient(schemaDefinition, options = {}) {
793
795
  if (!localSubmission.valid) {
794
796
  return localSubmission;
795
797
  }
796
- if (!isSafeMethod(method)) {
798
+ if (!isSafeMethod(method) && !(options.csrf && !options.submitter)) {
797
799
  const csrfField = await getClientCsrfField();
798
- if (csrfField) {
800
+ if (csrfField && !formData.has(csrfField.name)) {
799
801
  formData.set(csrfField.name, csrfField.value);
800
802
  }
801
803
  }
@@ -883,5 +885,6 @@ export {
883
885
  collectFormDirtyPaths,
884
886
  createFormClient,
885
887
  markClientSubmitControlFlowError,
886
- runWithBrowserFormElement
888
+ runWithBrowserFormElement,
889
+ sanitizeFlashedInput
887
890
  };
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@holo-js/forms",
3
- "version": "0.3.27",
3
+ "version": "0.4.0",
4
4
  "description": "Holo-JS Framework - forms-first validation and submission contracts",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cobraprojects/holo-js.git",
10
+ "directory": "packages/forms"
11
+ },
7
12
  "exports": {
8
13
  ".": {
9
14
  "types": "./dist/index.d.ts",
@@ -19,7 +24,8 @@
19
24
  "main": "./dist/index.mjs",
20
25
  "types": "./dist/index.d.ts",
21
26
  "files": [
22
- "dist"
27
+ "dist",
28
+ "LICENSE"
23
29
  ],
24
30
  "scripts": {
25
31
  "build": "tsup",
@@ -28,11 +34,11 @@
28
34
  "test": "vitest --run && node --test --test-concurrency=3 ../../tests/forms-native-browser.mjs ../../tests/forms-native-packages.mjs"
29
35
  },
30
36
  "dependencies": {
31
- "@holo-js/kernel": "^0.3.27",
32
- "@holo-js/validation": "^0.3.27"
37
+ "@holo-js/kernel": "^0.4.0",
38
+ "@holo-js/validation": "^0.4.0"
33
39
  },
34
40
  "peerDependencies": {
35
- "@holo-js/security": "^0.3.27"
41
+ "@holo-js/security": "^0.4.0"
36
42
  },
37
43
  "peerDependenciesMeta": {
38
44
  "@holo-js/security": {
@@ -40,12 +46,12 @@
40
46
  }
41
47
  },
42
48
  "devDependencies": {
43
- "@types/node": "^22.10.2",
44
- "tsup": "^8.3.5",
45
- "typescript": "^5.7.2",
46
- "vitest": "^4.1.5"
49
+ "@types/node": "^22.20.5",
50
+ "tsup": "^9.0.0",
51
+ "typescript": "^6.0.3",
52
+ "vitest": "^5.0.5"
47
53
  },
48
54
  "engines": {
49
- "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
55
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
50
56
  }
51
57
  }
@@ -1,162 +0,0 @@
1
- import {
2
- FormContractError
3
- } from "./chunk-OFPH5LBY.mjs";
4
-
5
- // src/failure.ts
6
- function normalizeStatus(value, fallback) {
7
- if (typeof value === "undefined") {
8
- return fallback;
9
- }
10
- if (!Number.isInteger(value) || value < 100) {
11
- throw new FormContractError("HTTP status codes must be integers greater than or equal to 100.");
12
- }
13
- return value;
14
- }
15
- function normalizeFailureInput(input, fallbackStatus) {
16
- if (typeof input === "number") {
17
- return {
18
- status: normalizeStatus(input, fallbackStatus)
19
- };
20
- }
21
- return {
22
- status: normalizeStatus(input?.status, fallbackStatus),
23
- errors: input?.errors
24
- };
25
- }
26
- function normalizeFailureErrors(fallback, override) {
27
- if (!override) {
28
- return fallback;
29
- }
30
- const normalized = { ...fallback };
31
- for (const [field, messages] of Object.entries(override)) {
32
- if (typeof messages !== "undefined") {
33
- normalized[field] = messages;
34
- }
35
- }
36
- return normalized;
37
- }
38
-
39
- // src/sensitiveInput.ts
40
- var DEFAULT_DONT_FLASH_FIELDS = Object.freeze([
41
- "confirm_password",
42
- "confirmPassword",
43
- "current_password",
44
- "currentPassword",
45
- "new_password",
46
- "newPassword",
47
- "password",
48
- "password_confirmation",
49
- "passwordConfirmation"
50
- ]);
51
- var DEFAULT_DONT_FLASH_FIELD_SET = new Set(DEFAULT_DONT_FLASH_FIELDS);
52
- function isPlainObject(value) {
53
- return !!value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Blob);
54
- }
55
- function isFieldDefinition(value) {
56
- return isPlainObject(value) && typeof value.kind === "string" && Array.isArray(value.rules);
57
- }
58
- function isSchemaField(value) {
59
- return isPlainObject(value) && value.kind === "field" && isFieldDefinition(value.definition);
60
- }
61
- function collectSensitivePathsFromFields(fields, prefix = "", output = []) {
62
- for (const [key, value] of Object.entries(fields)) {
63
- const path = prefix ? `${prefix}.${key}` : key;
64
- if (DEFAULT_DONT_FLASH_FIELD_SET.has(key)) {
65
- output.push(path);
66
- }
67
- if (isSchemaField(value)) {
68
- if (value.definition.sensitive === true || value.definition.kind === "file") {
69
- output.push(path);
70
- }
71
- continue;
72
- }
73
- if (isPlainObject(value)) {
74
- collectSensitivePathsFromFields(value, path, output);
75
- }
76
- }
77
- return output;
78
- }
79
- function collectSensitivePaths(schemaDefinition) {
80
- if (!schemaDefinition) {
81
- return [];
82
- }
83
- return collectSensitivePathsFromFields(schemaDefinition.fields);
84
- }
85
- function deletePath(root, path) {
86
- const parts = path.split(".").filter(Boolean);
87
- const leaf = parts.at(-1);
88
- if (!leaf) {
89
- return;
90
- }
91
- let cursor = root;
92
- for (const part of parts.slice(0, -1)) {
93
- if (!isPlainObject(cursor)) {
94
- return;
95
- }
96
- cursor = cursor[part];
97
- }
98
- if (isPlainObject(cursor)) {
99
- delete cursor[leaf];
100
- }
101
- }
102
- function clearPath(root, path) {
103
- const parts = path.split(".").filter(Boolean);
104
- const leaf = parts.at(-1);
105
- if (!leaf) {
106
- return;
107
- }
108
- let cursor = root;
109
- for (const part of parts.slice(0, -1)) {
110
- if (!isPlainObject(cursor)) {
111
- return;
112
- }
113
- cursor = cursor[part];
114
- }
115
- if (isPlainObject(cursor) && leaf in cursor) {
116
- cursor[leaf] = "";
117
- }
118
- }
119
- function sanitizePlainObject(values, sensitivePaths) {
120
- if (!isPlainObject(values)) {
121
- return values;
122
- }
123
- const output = structuredClone(values);
124
- for (const field of DEFAULT_DONT_FLASH_FIELD_SET) {
125
- delete output[field];
126
- }
127
- for (const path of sensitivePaths) {
128
- deletePath(output, path);
129
- }
130
- return output;
131
- }
132
- function sanitizeFlashedInput(values, schemaDefinition) {
133
- return sanitizePlainObject(values, collectSensitivePaths(schemaDefinition));
134
- }
135
- function clearSensitiveInputValues(values, schemaDefinition) {
136
- if (!isPlainObject(values)) {
137
- return values;
138
- }
139
- const mutableValues = values;
140
- for (const field of DEFAULT_DONT_FLASH_FIELD_SET) {
141
- if (field in mutableValues) {
142
- mutableValues[field] = "";
143
- }
144
- }
145
- for (const path of collectSensitivePaths(schemaDefinition)) {
146
- clearPath(mutableValues, path);
147
- }
148
- return values;
149
- }
150
- var sensitiveInputInternals = {
151
- collectSensitivePaths,
152
- DEFAULT_DONT_FLASH_FIELDS
153
- };
154
-
155
- export {
156
- normalizeStatus,
157
- normalizeFailureInput,
158
- normalizeFailureErrors,
159
- sanitizeFlashedInput,
160
- clearSensitiveInputValues,
161
- sensitiveInputInternals
162
- };