@holo-js/forms 0.1.4 → 0.1.6

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,13 @@
1
+ import { SchemaInputShape, ValidationSchema, InferSchemaData } from '@holo-js/validation';
2
+ export { DEFAULT_VALIDATION_BAG, ErrorTree, ErrorTreeNode, FieldDefinition, FieldRule, InferSchemaData, SerializedValidationException, StandardSchemaV1, StandardSchemaV1Issue, StandardSchemaV1Props, StandardSchemaV1Result, ValidationErrorBag, ValidationException, ValidationExceptionOptions, ValidationFailure, ValidationResult, ValidationSchema, ValidationSuccess, WebFileLike, createErrorBag, defineSchema, field, isValidationException, parse, safeParse, validationInternals } from '@holo-js/validation';
3
+
4
+ interface FormSchema<TShape extends SchemaInputShape = SchemaInputShape> extends ValidationSchema<TShape> {
5
+ readonly mode: 'form';
6
+ readonly fields: ValidationSchema<TShape>['fields'];
7
+ readonly $values?: Partial<InferSchemaData<TShape>>;
8
+ }
9
+ type InferFormData<TSchema extends FormSchema> = TSchema extends FormSchema<infer TShape> ? InferSchemaData<TShape> : never;
10
+ declare function schema<TShape extends SchemaInputShape>(shapeOrSchema: TShape | ValidationSchema<TShape>): FormSchema<TShape>;
11
+ declare function isFormSchema(value: unknown): value is FormSchema;
12
+
13
+ export { type FormSchema, type InferFormData, isFormSchema, schema };
@@ -0,0 +1,26 @@
1
+ import {
2
+ DEFAULT_VALIDATION_BAG,
3
+ ValidationException,
4
+ createErrorBag,
5
+ defineSchema,
6
+ field,
7
+ isFormSchema,
8
+ isValidationException,
9
+ parse,
10
+ safeParse,
11
+ schema,
12
+ validationInternals
13
+ } from "./chunk-GPWXGHZE.mjs";
14
+ export {
15
+ DEFAULT_VALIDATION_BAG,
16
+ ValidationException,
17
+ createErrorBag,
18
+ defineSchema,
19
+ field,
20
+ isFormSchema,
21
+ isValidationException,
22
+ parse,
23
+ safeParse,
24
+ schema,
25
+ validationInternals
26
+ };
@@ -0,0 +1,48 @@
1
+ import {
2
+ createMissingSecurityPackageError,
3
+ isMissingOptionalPackageError,
4
+ isRootSecurityError,
5
+ parseCookieHeader
6
+ } from "./chunk-GH4IN2OV.mjs";
7
+ import "./chunk-OFPH5LBY.mjs";
8
+
9
+ // src/security.ts
10
+ var securityModulePromise;
11
+ async function importSecurityModule() {
12
+ if (typeof process !== "undefined" && process.env && process.env.VITEST) {
13
+ return await import(
14
+ /* @vite-ignore */
15
+ "@holo-js/security"
16
+ );
17
+ }
18
+ return await import("@holo-js/security");
19
+ }
20
+ async function loadSecurityModule() {
21
+ const runtime = globalThis;
22
+ if (runtime.__holoFormsSecurityModule__) {
23
+ return runtime.__holoFormsSecurityModule__;
24
+ }
25
+ securityModulePromise ??= (runtime.__holoFormsSecurityImport__ ? runtime.__holoFormsSecurityImport__() : importSecurityModule()).then((module) => module).catch(async (error) => {
26
+ securityModulePromise = void 0;
27
+ if (isMissingOptionalPackageError(error)) {
28
+ throw createMissingSecurityPackageError();
29
+ }
30
+ throw error;
31
+ });
32
+ return await securityModulePromise;
33
+ }
34
+ function resetSecurityModuleCache() {
35
+ securityModulePromise = void 0;
36
+ }
37
+ var formsSecurityInternals = {
38
+ createMissingSecurityPackageError,
39
+ isMissingOptionalPackageError,
40
+ isRootSecurityError,
41
+ parseCookieHeader,
42
+ resetSecurityModuleCache
43
+ };
44
+ export {
45
+ formsSecurityInternals,
46
+ loadSecurityModule,
47
+ resetSecurityModuleCache
48
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/forms",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Holo-JS Framework - forms-first validation and submission contracts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,10 +10,15 @@
10
10
  "import": "./dist/index.mjs",
11
11
  "default": "./dist/index.mjs"
12
12
  },
13
- "./client": {
14
- "types": "./dist/client.d.ts",
15
- "import": "./dist/client.mjs",
16
- "default": "./dist/client.mjs"
13
+ "./schema": {
14
+ "types": "./dist/schema.d.ts",
15
+ "import": "./dist/schema.mjs",
16
+ "default": "./dist/schema.mjs"
17
+ },
18
+ "./internal/client": {
19
+ "types": "./dist/internal/client.d.ts",
20
+ "import": "./dist/internal/client.mjs",
21
+ "default": "./dist/internal/client.mjs"
17
22
  }
18
23
  },
19
24
  "main": "./dist/index.mjs",
@@ -28,10 +33,10 @@
28
33
  "test": "vitest --run"
29
34
  },
30
35
  "dependencies": {
31
- "@holo-js/validation": "^0.1.4"
36
+ "@holo-js/validation": "^0.1.6"
32
37
  },
33
38
  "peerDependencies": {
34
- "@holo-js/security": "^0.1.4"
39
+ "@holo-js/security": "^0.1.6"
35
40
  },
36
41
  "peerDependenciesMeta": {
37
42
  "@holo-js/security": {
@@ -42,6 +47,6 @@
42
47
  "@types/node": "^22.10.2",
43
48
  "tsup": "^8.3.5",
44
49
  "typescript": "^5.7.2",
45
- "vitest": "^2.1.8"
50
+ "vitest": "^4.1.5"
46
51
  }
47
52
  }
@@ -1,421 +0,0 @@
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 DELETED
@@ -1,52 +0,0 @@
1
- import { FormSubmissionResult, SerializedFormSubmission, FormFailurePayload, FormSuccessPayload, FormSchema } from './index.js';
2
- import { WebFileLike, ValidationErrorBag, SchemaInputShape, InferSchemaData } from '@holo-js/validation';
3
-
4
- type PrimitiveLike = string | number | boolean | bigint | symbol | null | undefined | Date | Blob | WebFileLike;
5
- type ValidateOnMode = 'submit' | 'blur' | 'change';
6
- interface ClientSubmitContext<TData> {
7
- readonly action?: string;
8
- readonly method: string;
9
- readonly values: TData;
10
- readonly formData: FormData;
11
- }
12
- type ClientSubmitResult<TData> = FormSubmissionResult<TData> | SerializedFormSubmission<TData> | FormFailurePayload<TData> | FormSuccessPayload<unknown>;
13
- interface UseFormOptions<TData> {
14
- readonly action?: string;
15
- readonly method?: string;
16
- readonly csrf?: boolean;
17
- readonly validateOn?: ValidateOnMode;
18
- readonly initialValues?: Partial<TData>;
19
- readonly initialState?: SerializedFormSubmission<TData>;
20
- readonly submitter?: (context: ClientSubmitContext<TData>) => Promise<ClientSubmitResult<TData>> | ClientSubmitResult<TData>;
21
- }
22
- interface FormFieldState<TValue> {
23
- value: TValue;
24
- readonly errors: readonly string[];
25
- readonly touched: boolean;
26
- readonly dirty: boolean;
27
- set(value: TValue): Promise<void>;
28
- onInput(value: TValue): Promise<void>;
29
- onBlur(): Promise<void>;
30
- validate(): Promise<readonly string[]>;
31
- }
32
- type FormFieldTree<TData> = [TData] extends [readonly unknown[]] ? FormFieldState<TData> : [TData] extends [PrimitiveLike] ? FormFieldState<TData> : [TData] extends [Record<string, unknown>] ? {
33
- readonly [K in keyof TData]: FormFieldTree<TData[K]>;
34
- } : FormFieldState<TData>;
35
- interface UseFormResult<TData> {
36
- readonly fields: FormFieldTree<TData>;
37
- readonly values: TData;
38
- readonly errors: ValidationErrorBag<TData>;
39
- readonly submitting: boolean;
40
- readonly valid: boolean;
41
- readonly lastSubmission?: SerializedFormSubmission<TData> | FormSuccessPayload<unknown>;
42
- subscribe(listener: () => void): () => void;
43
- validate(): Promise<FormSubmissionResult<TData>>;
44
- validateField(path: string): Promise<readonly string[]>;
45
- submit(): Promise<ClientSubmitResult<TData>>;
46
- reset(values?: Partial<TData>): void;
47
- setValue(path: string, value: unknown): Promise<void>;
48
- applyServerState(result: ClientSubmitResult<TData>): ClientSubmitResult<TData>;
49
- }
50
- declare function useForm<TSchema extends FormSchema<SchemaInputShape>>(schemaDefinition: TSchema, options?: UseFormOptions<InferSchemaData<TSchema['fields']>>): UseFormResult<InferSchemaData<TSchema['fields']>>;
51
-
52
- export { type ClientSubmitContext, type ClientSubmitResult, type FormFieldState, type FormFieldTree, type UseFormOptions, type UseFormResult, type ValidateOnMode, useForm };