@holo-js/forms 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,421 @@
1
+ // src/contracts.ts
2
+ import {
3
+ createErrorBag,
4
+ defineSchema,
5
+ isValidationSchema,
6
+ validate as validateInput
7
+ } from "@holo-js/validation";
8
+
9
+ // src/security.ts
10
+ var securityModulePromise;
11
+ function isMissingOptionalPackageError(error) {
12
+ if (!(error instanceof Error)) {
13
+ return false;
14
+ }
15
+ return [
16
+ /Cannot find package ['"]?@holo-js\/security['"]?/,
17
+ /Cannot find module ['"]?@holo-js\/security['"]?/,
18
+ /Failed to resolve module specifier ['"]?@holo-js\/security['"]?/,
19
+ /Failed to load url ['"]?@holo-js\/security['"]?/,
20
+ /Could not resolve ['"]?@holo-js\/security['"]?/
21
+ ].some((pattern) => pattern.test(error.message));
22
+ }
23
+ function createMissingSecurityPackageError() {
24
+ return new FormContractError(
25
+ "[@holo-js/forms] Security-aware form options require the optional @holo-js/security package to be installed."
26
+ );
27
+ }
28
+ async function importSecurityModule() {
29
+ const specifier = ["@holo-js", "security"].join("/");
30
+ if (typeof process !== "undefined" && process.env && process.env.VITEST) {
31
+ return await import(
32
+ /* @vite-ignore */
33
+ specifier
34
+ );
35
+ }
36
+ return await import(specifier);
37
+ }
38
+ async function loadSecurityModule() {
39
+ const runtime = globalThis;
40
+ if (runtime.__holoFormsSecurityModule__) {
41
+ return runtime.__holoFormsSecurityModule__;
42
+ }
43
+ securityModulePromise ??= (runtime.__holoFormsSecurityImport__ ? runtime.__holoFormsSecurityImport__() : importSecurityModule()).then((module) => module).catch(async (error) => {
44
+ securityModulePromise = void 0;
45
+ if (isMissingOptionalPackageError(error)) {
46
+ throw createMissingSecurityPackageError();
47
+ }
48
+ throw error;
49
+ });
50
+ return await securityModulePromise;
51
+ }
52
+ function resetSecurityModuleCache() {
53
+ securityModulePromise = void 0;
54
+ }
55
+ function parseCookieHeader(header) {
56
+ const decodeCookiePart = (value) => {
57
+ try {
58
+ return decodeURIComponent(value);
59
+ } catch {
60
+ return void 0;
61
+ }
62
+ };
63
+ return Object.freeze(Object.fromEntries(
64
+ header.split(";").map((segment) => segment.trim()).filter(Boolean).map((segment) => {
65
+ const separator = segment.indexOf("=");
66
+ if (separator <= 0) {
67
+ return void 0;
68
+ }
69
+ const key = decodeCookiePart(segment.slice(0, separator));
70
+ const value = decodeCookiePart(segment.slice(separator + 1));
71
+ if (!key || typeof value === "undefined") {
72
+ return void 0;
73
+ }
74
+ return [
75
+ key,
76
+ value
77
+ ];
78
+ }).filter((entry) => !!entry)
79
+ ));
80
+ }
81
+ function isRootSecurityError(error) {
82
+ const candidate = error;
83
+ return error instanceof Error && typeof candidate?.status === "number" && (candidate.status === 419 || candidate.status === 429);
84
+ }
85
+ var formsSecurityInternals = {
86
+ createMissingSecurityPackageError,
87
+ isMissingOptionalPackageError,
88
+ isRootSecurityError,
89
+ parseCookieHeader,
90
+ resetSecurityModuleCache
91
+ };
92
+
93
+ // src/contracts.ts
94
+ var FormContractError = class extends Error {
95
+ constructor(message) {
96
+ super(message);
97
+ this.name = "FormContractError";
98
+ }
99
+ };
100
+ function schema(shapeOrSchema) {
101
+ const base = isValidationSchema(shapeOrSchema) ? shapeOrSchema : defineSchema(shapeOrSchema);
102
+ return Object.freeze({
103
+ ...base,
104
+ mode: "form"
105
+ });
106
+ }
107
+ function isFormSchema(value) {
108
+ return isValidationSchema(value) && value.mode === "form";
109
+ }
110
+ function normalizeStatus(value, fallback) {
111
+ if (typeof value === "undefined") {
112
+ return fallback;
113
+ }
114
+ if (!Number.isInteger(value) || value < 100) {
115
+ throw new FormContractError("HTTP status codes must be integers greater than or equal to 100.");
116
+ }
117
+ return value;
118
+ }
119
+ function serializeSubmissionState(valid, values, errors) {
120
+ return Object.freeze({
121
+ valid,
122
+ submitted: true,
123
+ values,
124
+ errors: errors.flatten()
125
+ });
126
+ }
127
+ function createSubmission(valid, values, errors, failureStatus = 422) {
128
+ const normalizedFailureStatus = normalizeStatus(failureStatus, 422);
129
+ const serialize = () => serializeSubmissionState(valid, values, errors);
130
+ const failure = () => ({
131
+ ok: false,
132
+ status: normalizedFailureStatus,
133
+ valid: false,
134
+ values,
135
+ errors: errors.flatten()
136
+ });
137
+ const success = (payload, status) => ({
138
+ ok: true,
139
+ status: normalizeStatus(status, 200),
140
+ data: payload
141
+ });
142
+ if (valid) {
143
+ const data = values;
144
+ return Object.freeze({
145
+ valid: true,
146
+ submitted: true,
147
+ data,
148
+ values: data,
149
+ errors,
150
+ serialize,
151
+ success,
152
+ fail(status) {
153
+ const payload = failure();
154
+ return {
155
+ ...payload,
156
+ status: normalizeStatus(status, payload.status)
157
+ };
158
+ }
159
+ });
160
+ }
161
+ return Object.freeze({
162
+ valid: false,
163
+ submitted: true,
164
+ data: void 0,
165
+ values,
166
+ errors,
167
+ serialize,
168
+ success,
169
+ fail(status) {
170
+ const payload = failure();
171
+ return {
172
+ ...payload,
173
+ status: normalizeStatus(status, payload.status)
174
+ };
175
+ }
176
+ });
177
+ }
178
+ function createSuccessfulSubmission(schemaDefinition, data) {
179
+ void schemaDefinition;
180
+ return createSubmission(true, data, createErrorBag());
181
+ }
182
+ function createFailedSubmission(schemaDefinition, values, flattenedErrors, status = 422) {
183
+ void schemaDefinition;
184
+ const normalizedStatus = normalizeStatus(status, 422);
185
+ return createSubmission(
186
+ false,
187
+ values,
188
+ createErrorBag(flattenedErrors),
189
+ normalizedStatus
190
+ );
191
+ }
192
+ function isRequestInput(input) {
193
+ return typeof Request !== "undefined" && input instanceof Request;
194
+ }
195
+ function isRequestLikeBody(value) {
196
+ if (typeof value === "string" || value instanceof URLSearchParams) {
197
+ return true;
198
+ }
199
+ if (typeof FormData !== "undefined" && value instanceof FormData) {
200
+ return true;
201
+ }
202
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
203
+ return true;
204
+ }
205
+ if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) {
206
+ return true;
207
+ }
208
+ if (ArrayBuffer.isView(value)) {
209
+ return true;
210
+ }
211
+ return !!value && typeof value === "object" && (Symbol.asyncIterator in value || "pipe" in value || "getReader" in value);
212
+ }
213
+ function isHeadersTupleArray(value) {
214
+ return Array.isArray(value) && value.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string");
215
+ }
216
+ function isRequestLikeHeaders(value) {
217
+ return value instanceof Headers || isHeadersTupleArray(value) || !!value && typeof value === "object";
218
+ }
219
+ function normalizeRequestHeaders(input) {
220
+ if (input instanceof Headers) {
221
+ return new Headers(input);
222
+ }
223
+ const headers = new Headers();
224
+ if (isHeadersTupleArray(input)) {
225
+ for (const [name, value] of input) {
226
+ headers.append(name, value);
227
+ }
228
+ return headers;
229
+ }
230
+ if (input && typeof input === "object") {
231
+ for (const [name, value] of Object.entries(input)) {
232
+ if (typeof value === "string") {
233
+ headers.append(name, value);
234
+ continue;
235
+ }
236
+ if (Array.isArray(value)) {
237
+ const separator = name.toLowerCase() === "cookie" ? "; " : ",";
238
+ const joined = value.filter((entry) => typeof entry === "string").join(separator);
239
+ if (joined) {
240
+ headers.append(name, joined);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ return headers;
246
+ }
247
+ function extractRequestLikeUrl(input, headers) {
248
+ const url = input.web?.request?.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 ?? "/";
249
+ try {
250
+ return new URL(url).toString();
251
+ } catch {
252
+ const protocol = headers.get("x-forwarded-proto") ?? "http";
253
+ const host = headers.get("x-forwarded-host") ?? headers.get("host") ?? "localhost";
254
+ return new URL(url, `${protocol}://${host}`).toString();
255
+ }
256
+ }
257
+ function extractRequestLikeMethod(input) {
258
+ return input.web?.request?.method ?? input.method ?? (typeof input.req === "object" && !(input.req instanceof Request) ? input.req.method : void 0) ?? input.node?.req?.method ?? "GET";
259
+ }
260
+ function extractRequestLikeBody(input, headers, method) {
261
+ if (method === "GET" || method === "HEAD") {
262
+ return void 0;
263
+ }
264
+ const rawBody = input.body ?? (typeof input.req === "object" && !(input.req instanceof Request) ? input.req.body : void 0) ?? input.node?.req?.body ?? input.node?.req;
265
+ if (typeof rawBody === "undefined" || rawBody === null) {
266
+ return void 0;
267
+ }
268
+ if (isRequestLikeBody(rawBody)) {
269
+ return rawBody;
270
+ }
271
+ if (typeof rawBody === "object") {
272
+ if (!headers.has("content-type")) {
273
+ headers.set("content-type", "application/json");
274
+ }
275
+ return JSON.stringify(rawBody);
276
+ }
277
+ return String(rawBody);
278
+ }
279
+ function isRequestLikeInput(input) {
280
+ if (!input || typeof input !== "object") {
281
+ return false;
282
+ }
283
+ const candidate = input;
284
+ return candidate.web?.request instanceof Request || candidate.req instanceof Request || typeof candidate.method === "string" || typeof candidate.path === "string" || typeof candidate.url === "string" || candidate.url instanceof URL || isRequestLikeHeaders(candidate.headers) || !!candidate.req && typeof candidate.req === "object" || !!candidate.node?.req && typeof candidate.node.req === "object";
285
+ }
286
+ function normalizeRequestLikeInput(input) {
287
+ if (isRequestInput(input)) {
288
+ return input;
289
+ }
290
+ if (!isRequestLikeInput(input)) {
291
+ return void 0;
292
+ }
293
+ if (input.web?.request instanceof Request) {
294
+ return input.web.request;
295
+ }
296
+ if (input.req instanceof Request) {
297
+ return input.req;
298
+ }
299
+ const headers = normalizeRequestHeaders(
300
+ input.headers ?? (typeof input.req === "object" && !(input.req instanceof Request) ? input.req.headers : void 0) ?? input.node?.req?.headers
301
+ );
302
+ const method = extractRequestLikeMethod(input);
303
+ const body = extractRequestLikeBody(input, headers, method);
304
+ return new Request(extractRequestLikeUrl(input, headers), {
305
+ method,
306
+ headers,
307
+ body,
308
+ // Undici requires duplex when constructing a Request from a stream/async iterable in Node.
309
+ ...body && typeof body === "object" && (Symbol.asyncIterator in body || "pipe" in body || "getReader" in body) ? { duplex: "half" } : {}
310
+ });
311
+ }
312
+ async function createSecurityFailureSubmission(input, schemaDefinition, error) {
313
+ let values = {};
314
+ let flattenedErrors = {};
315
+ try {
316
+ const inspection = await validateInput(input.clone(), schemaDefinition);
317
+ if (inspection.valid) {
318
+ values = inspection.data;
319
+ } else {
320
+ values = inspection.values;
321
+ flattenedErrors = inspection.errors.flatten();
322
+ }
323
+ } catch {
324
+ values = {};
325
+ flattenedErrors = {};
326
+ }
327
+ return createFailedSubmission(
328
+ schemaDefinition,
329
+ values,
330
+ {
331
+ ...flattenedErrors,
332
+ _root: [
333
+ ...flattenedErrors._root ?? [],
334
+ error.message
335
+ ]
336
+ },
337
+ error.status
338
+ );
339
+ }
340
+ async function validate(input, schemaDefinition, options = {}) {
341
+ let validatedSubmission;
342
+ const usesSecurityOptions = options.csrf === true || typeof options.throttle === "string";
343
+ const normalizedRequestInput = usesSecurityOptions ? normalizeRequestLikeInput(input) : void 0;
344
+ const validationInput = normalizedRequestInput ?? input;
345
+ if (usesSecurityOptions && !normalizedRequestInput) {
346
+ throw new FormContractError(
347
+ "Security-aware validate() options require a Request or request-like event input."
348
+ );
349
+ }
350
+ if (usesSecurityOptions) {
351
+ const request = normalizedRequestInput;
352
+ try {
353
+ const security = await loadSecurityModule();
354
+ const verificationRequest = (() => {
355
+ try {
356
+ return request.clone();
357
+ } catch {
358
+ return request;
359
+ }
360
+ })();
361
+ if (options.csrf === true) {
362
+ await security.csrf.verify(verificationRequest);
363
+ }
364
+ if (typeof options.throttle === "string") {
365
+ const inspection = await validateInput(request.clone(), schemaDefinition);
366
+ const throttleValues = inspection.valid ? inspection.data : inspection.values;
367
+ validatedSubmission = inspection.valid ? createSuccessfulSubmission(schemaDefinition, inspection.data) : createFailedSubmission(schemaDefinition, inspection.values, inspection.errors.flatten());
368
+ await security.rateLimit(options.throttle, {
369
+ request,
370
+ values: throttleValues
371
+ });
372
+ }
373
+ } catch (error) {
374
+ if (formsSecurityInternals.isRootSecurityError(error)) {
375
+ if (validatedSubmission) {
376
+ return createFailedSubmission(
377
+ schemaDefinition,
378
+ validatedSubmission.valid ? validatedSubmission.data : validatedSubmission.values,
379
+ {
380
+ ...validatedSubmission.valid ? {} : validatedSubmission.errors.flatten(),
381
+ _root: [
382
+ ...!validatedSubmission.valid ? validatedSubmission.errors.flatten()._root ?? [] : [],
383
+ error.message
384
+ ]
385
+ },
386
+ error.status
387
+ );
388
+ }
389
+ return await createSecurityFailureSubmission(request, schemaDefinition, error);
390
+ }
391
+ throw error;
392
+ }
393
+ }
394
+ if (validatedSubmission) {
395
+ return validatedSubmission;
396
+ }
397
+ const result = await validateInput(validationInput, schemaDefinition);
398
+ if (result.valid) {
399
+ return createSuccessfulSubmission(schemaDefinition, result.data);
400
+ }
401
+ return createFailedSubmission(schemaDefinition, result.values, result.errors.flatten());
402
+ }
403
+ var formsInternals = {
404
+ createSubmission,
405
+ isRequestLikeHeaders,
406
+ normalizeRequestLikeInput,
407
+ normalizeStatus,
408
+ normalizeRequestHeaders,
409
+ serializeSubmissionState
410
+ };
411
+
412
+ export {
413
+ formsSecurityInternals,
414
+ FormContractError,
415
+ schema,
416
+ isFormSchema,
417
+ createSuccessfulSubmission,
418
+ createFailedSubmission,
419
+ validate,
420
+ formsInternals
421
+ };
package/dist/client.d.ts CHANGED
@@ -13,6 +13,7 @@ type ClientSubmitResult<TData> = FormSubmissionResult<TData> | SerializedFormSub
13
13
  interface UseFormOptions<TData> {
14
14
  readonly action?: string;
15
15
  readonly method?: string;
16
+ readonly csrf?: boolean;
16
17
  readonly validateOn?: ValidateOnMode;
17
18
  readonly initialValues?: Partial<TData>;
18
19
  readonly initialState?: SerializedFormSubmission<TData>;
@@ -28,7 +29,7 @@ interface FormFieldState<TValue> {
28
29
  onBlur(): Promise<void>;
29
30
  validate(): Promise<readonly string[]>;
30
31
  }
31
- type FormFieldTree<TData> = TData extends readonly unknown[] ? FormFieldState<TData> : TData extends PrimitiveLike ? FormFieldState<TData> : TData extends Record<string, unknown> ? {
32
+ type FormFieldTree<TData> = [TData] extends [readonly unknown[]] ? FormFieldState<TData> : [TData] extends [PrimitiveLike] ? FormFieldState<TData> : [TData] extends [Record<string, unknown>] ? {
32
33
  readonly [K in keyof TData]: FormFieldTree<TData[K]>;
33
34
  } : FormFieldState<TData>;
34
35
  interface UseFormResult<TData> {
package/dist/client.mjs CHANGED
@@ -1,11 +1,61 @@
1
1
  import {
2
+ FormContractError,
2
3
  createFailedSubmission,
3
4
  createSuccessfulSubmission,
5
+ formsSecurityInternals,
4
6
  validate
5
- } from "./chunk-Z6TEFT7S.mjs";
7
+ } from "./chunk-663LOLKH.mjs";
6
8
 
7
9
  // src/client.ts
8
10
  import { createErrorBag } from "@holo-js/validation";
11
+
12
+ // src/client-security.ts
13
+ var securityClientModulePromise;
14
+ async function importSecurityClientModule() {
15
+ if (process.env.VITEST) {
16
+ return await import(
17
+ /* @vite-ignore */
18
+ "@holo-js/security/client"
19
+ );
20
+ }
21
+ return await import("@holo-js/security/client");
22
+ }
23
+ async function loadSecurityClientModule() {
24
+ const runtime = globalThis;
25
+ if (runtime.__holoFormsSecurityClientModule__) {
26
+ return runtime.__holoFormsSecurityClientModule__;
27
+ }
28
+ securityClientModulePromise ??= (runtime.__holoFormsSecurityClientImport__ ? runtime.__holoFormsSecurityClientImport__() : importSecurityClientModule()).then((module) => module).catch(async (error) => {
29
+ securityClientModulePromise = void 0;
30
+ if (formsSecurityInternals.isMissingOptionalPackageError(error)) {
31
+ throw formsSecurityInternals.createMissingSecurityPackageError();
32
+ }
33
+ throw error;
34
+ });
35
+ return await securityClientModulePromise;
36
+ }
37
+ async function getClientCsrfField() {
38
+ const runtime = globalThis;
39
+ if (!runtime.document || typeof runtime.document.cookie !== "string") {
40
+ throw new FormContractError(
41
+ "[@holo-js/forms] useForm({ csrf: true }) requires a browser-like document.cookie environment."
42
+ );
43
+ }
44
+ const security = await loadSecurityClientModule();
45
+ const config = security.getSecurityClientConfig().csrf;
46
+ const value = formsSecurityInternals.parseCookieHeader(runtime.document.cookie)[config.cookie];
47
+ if (!value) {
48
+ throw new FormContractError(
49
+ `[@holo-js/forms] Missing CSRF cookie "${config.cookie}" required by useForm({ csrf: true }).`
50
+ );
51
+ }
52
+ return Object.freeze({
53
+ name: config.field,
54
+ value
55
+ });
56
+ }
57
+
58
+ // src/client.ts
9
59
  function isPlainObject(value) {
10
60
  return !!value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Blob);
11
61
  }
@@ -323,6 +373,10 @@ async function defaultSubmitter(context) {
323
373
  });
324
374
  return await normalizeFetchResponse(response, context.values);
325
375
  }
376
+ function isSafeMethod(method) {
377
+ const normalized = method.trim().toUpperCase();
378
+ return normalized === "GET" || normalized === "HEAD" || normalized === "OPTIONS" || normalized === "TRACE";
379
+ }
326
380
  function useForm(schemaDefinition, options = {}) {
327
381
  const initialValues = mergeValues(
328
382
  normalizeObject(options.initialState?.values),
@@ -392,11 +446,17 @@ function useForm(schemaDefinition, options = {}) {
392
446
  return local;
393
447
  }
394
448
  const submitter = options.submitter ?? defaultSubmitter;
449
+ const method = options.method ?? "POST";
450
+ const formData = buildFormData(state.values);
451
+ if (options.csrf === true && !isSafeMethod(method)) {
452
+ const csrfField = await getClientCsrfField();
453
+ formData.set(csrfField.name, csrfField.value);
454
+ }
395
455
  const response = await submitter({
396
456
  action: options.action,
397
- method: options.method ?? "POST",
457
+ method,
398
458
  values: state.values,
399
- formData: buildFormData(state.values)
459
+ formData
400
460
  });
401
461
  return this.applyServerState(response);
402
462
  } finally {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { ErrorTree, ErrorTreeNode, FieldDefinition, FieldRule, InferSchemaData,
3
3
 
4
4
  interface FormSchema<TShape extends SchemaInputShape = SchemaInputShape> extends ValidationSchema<TShape> {
5
5
  readonly mode: 'form';
6
+ readonly fields: ValidationSchema<TShape>['fields'];
6
7
  readonly $values?: Partial<InferSchemaData<TShape>>;
7
8
  }
8
9
  interface FormFailurePayload<TData> {
@@ -23,6 +24,35 @@ interface SerializedFormSubmission<TData> {
23
24
  readonly values: Partial<TData> | TData;
24
25
  readonly errors: Record<string, readonly string[]>;
25
26
  }
27
+ interface FormSecurityOptions {
28
+ readonly csrf?: boolean;
29
+ readonly throttle?: string;
30
+ }
31
+ type RequestLikeHeaders = Headers | ReadonlyArray<readonly [string, string]> | Record<string, string | readonly string[] | undefined>;
32
+ interface FormRequestLikeInput {
33
+ readonly method?: string;
34
+ readonly path?: string;
35
+ readonly url?: string | URL;
36
+ readonly headers?: RequestLikeHeaders;
37
+ readonly body?: unknown;
38
+ readonly req?: Request | {
39
+ readonly method?: string;
40
+ readonly url?: string;
41
+ readonly headers?: RequestLikeHeaders;
42
+ readonly body?: unknown;
43
+ };
44
+ readonly node?: {
45
+ readonly req?: {
46
+ readonly method?: string;
47
+ readonly url?: string;
48
+ readonly headers?: RequestLikeHeaders;
49
+ readonly body?: unknown;
50
+ };
51
+ };
52
+ readonly web?: {
53
+ readonly request?: Request;
54
+ };
55
+ }
26
56
  interface FormSubmissionSuccess<TData> {
27
57
  readonly valid: true;
28
58
  readonly submitted: true;
@@ -53,14 +83,20 @@ declare function schema<TShape extends SchemaInputShape>(shapeOrSchema: TShape |
53
83
  declare function isFormSchema(value: unknown): value is FormSchema;
54
84
  declare function normalizeStatus(value: number | undefined, fallback: number): number;
55
85
  declare function serializeSubmissionState<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>): SerializedFormSubmission<TData>;
56
- declare function createSubmission<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>): FormSubmissionResult<TData>;
57
- declare function createSuccessfulSubmission<TShape extends SchemaInputShape, TSchema extends FormSchema<TShape>>(schemaDefinition: TSchema, data: InferSchemaData<TShape>): FormSubmissionSuccess<InferSchemaData<TShape>>;
58
- declare function createFailedSubmission<TShape extends SchemaInputShape, TSchema extends FormSchema<TShape>>(schemaDefinition: TSchema, values: Partial<InferSchemaData<TShape>>, flattenedErrors: Record<string, readonly string[]>): FormSubmissionFailure<InferSchemaData<TShape>>;
59
- declare function validate<TShape extends SchemaInputShape, TSchema extends FormSchema<TShape>>(input: FormLikeValidationInput, schemaDefinition: TSchema): Promise<FormSubmissionResult<InferSchemaData<TShape>>>;
86
+ declare function createSubmission<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>, failureStatus?: number): FormSubmissionResult<TData>;
87
+ declare function createSuccessfulSubmission<TShape extends SchemaInputShape>(schemaDefinition: FormSchema<TShape>, data: InferSchemaData<TShape>): FormSubmissionSuccess<InferSchemaData<TShape>>;
88
+ declare function createFailedSubmission<TShape extends SchemaInputShape>(schemaDefinition: FormSchema<TShape>, values: Partial<InferSchemaData<TShape>>, flattenedErrors: Record<string, readonly string[]>, status?: number): FormSubmissionFailure<InferSchemaData<TShape>>;
89
+ declare function isRequestLikeHeaders(value: unknown): value is RequestLikeHeaders;
90
+ declare function normalizeRequestHeaders(input: unknown): Headers;
91
+ declare function normalizeRequestLikeInput(input: FormLikeValidationInput | FormRequestLikeInput | null | undefined): Request | undefined;
92
+ declare function validate<TShape extends SchemaInputShape>(input: FormLikeValidationInput | FormRequestLikeInput, schemaDefinition: FormSchema<TShape>, options?: FormSecurityOptions): Promise<FormSubmissionResult<InferSchemaData<TShape>>>;
60
93
  declare const formsInternals: {
61
94
  createSubmission: typeof createSubmission;
95
+ isRequestLikeHeaders: typeof isRequestLikeHeaders;
96
+ normalizeRequestLikeInput: typeof normalizeRequestLikeInput;
62
97
  normalizeStatus: typeof normalizeStatus;
98
+ normalizeRequestHeaders: typeof normalizeRequestHeaders;
63
99
  serializeSubmissionState: typeof serializeSubmissionState;
64
100
  };
65
101
 
66
- export { FormContractError, type FormFailurePayload, type FormSchema, type FormSubmissionFailure, type FormSubmissionResult, type FormSubmissionSuccess, type FormSuccessPayload, type SerializedFormSubmission, createFailedSubmission, createSuccessfulSubmission, formsInternals, isFormSchema, schema, validate };
102
+ export { FormContractError, type FormFailurePayload, type FormRequestLikeInput, type FormSchema, type FormSecurityOptions, type FormSubmissionFailure, type FormSubmissionResult, type FormSubmissionSuccess, type FormSuccessPayload, type SerializedFormSubmission, createFailedSubmission, createSuccessfulSubmission, formsInternals, isFormSchema, schema, validate };
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  isFormSchema,
7
7
  schema,
8
8
  validate
9
- } from "./chunk-Z6TEFT7S.mjs";
9
+ } from "./chunk-663LOLKH.mjs";
10
10
 
11
11
  // src/index.ts
12
12
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/forms",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Holo-JS Framework - forms-first validation and submission contracts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,15 @@
28
28
  "test": "vitest --run"
29
29
  },
30
30
  "dependencies": {
31
- "@holo-js/validation": "^0.1.1"
31
+ "@holo-js/validation": "^0.1.3"
32
+ },
33
+ "peerDependencies": {
34
+ "@holo-js/security": "^0.1.3"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "@holo-js/security": {
38
+ "optional": true
39
+ }
32
40
  },
33
41
  "devDependencies": {
34
42
  "@types/node": "^22.10.2",
@@ -1,124 +0,0 @@
1
- // src/contracts.ts
2
- import {
3
- createErrorBag,
4
- defineSchema,
5
- isValidationSchema,
6
- validate as validateInput
7
- } from "@holo-js/validation";
8
- var FormContractError = class extends Error {
9
- constructor(message) {
10
- super(message);
11
- this.name = "FormContractError";
12
- }
13
- };
14
- function schema(shapeOrSchema) {
15
- const base = isValidationSchema(shapeOrSchema) ? shapeOrSchema : defineSchema(shapeOrSchema);
16
- return Object.freeze({
17
- ...base,
18
- mode: "form"
19
- });
20
- }
21
- function isFormSchema(value) {
22
- return isValidationSchema(value) && value.mode === "form";
23
- }
24
- function normalizeStatus(value, fallback) {
25
- if (typeof value === "undefined") {
26
- return fallback;
27
- }
28
- if (!Number.isInteger(value) || value < 100) {
29
- throw new FormContractError("HTTP status codes must be integers greater than or equal to 100.");
30
- }
31
- return value;
32
- }
33
- function serializeSubmissionState(valid, values, errors) {
34
- return Object.freeze({
35
- valid,
36
- submitted: true,
37
- values,
38
- errors: errors.flatten()
39
- });
40
- }
41
- function createSubmission(valid, values, errors) {
42
- const serialize = () => serializeSubmissionState(valid, values, errors);
43
- const failure = () => ({
44
- ok: false,
45
- status: 422,
46
- valid: false,
47
- values,
48
- errors: errors.flatten()
49
- });
50
- const success = (payload, status) => ({
51
- ok: true,
52
- status: normalizeStatus(status, 200),
53
- data: payload
54
- });
55
- if (valid) {
56
- const data = values;
57
- return Object.freeze({
58
- valid: true,
59
- submitted: true,
60
- data,
61
- values: data,
62
- errors,
63
- serialize,
64
- success,
65
- fail(status) {
66
- const payload = failure();
67
- return {
68
- ...payload,
69
- status: normalizeStatus(status, payload.status)
70
- };
71
- }
72
- });
73
- }
74
- return Object.freeze({
75
- valid: false,
76
- submitted: true,
77
- data: void 0,
78
- values,
79
- errors,
80
- serialize,
81
- success,
82
- fail(status) {
83
- const payload = failure();
84
- return {
85
- ...payload,
86
- status: normalizeStatus(status, payload.status)
87
- };
88
- }
89
- });
90
- }
91
- function createSuccessfulSubmission(schemaDefinition, data) {
92
- void schemaDefinition;
93
- return createSubmission(true, data, createErrorBag());
94
- }
95
- function createFailedSubmission(schemaDefinition, values, flattenedErrors) {
96
- void schemaDefinition;
97
- return createSubmission(
98
- false,
99
- values,
100
- createErrorBag(flattenedErrors)
101
- );
102
- }
103
- async function validate(input, schemaDefinition) {
104
- const result = await validateInput(input, schemaDefinition);
105
- if (result.valid) {
106
- return createSuccessfulSubmission(schemaDefinition, result.data);
107
- }
108
- return createFailedSubmission(schemaDefinition, result.values, result.errors.flatten());
109
- }
110
- var formsInternals = {
111
- createSubmission,
112
- normalizeStatus,
113
- serializeSubmissionState
114
- };
115
-
116
- export {
117
- FormContractError,
118
- schema,
119
- isFormSchema,
120
- createSuccessfulSubmission,
121
- createFailedSubmission,
122
- validate,
123
- formsInternals
124
- };