@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 +21 -0
- package/dist/.holo-build-fingerprint +1 -0
- package/dist/chunk-TFYWXPME.mjs +54 -0
- package/dist/{contracts-CJ9Aq8pS.d.ts → contracts-CN5pB9Tx.d.ts} +4 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +61 -81
- package/dist/internal/client.d.ts +5 -2
- package/dist/internal/client.mjs +37 -34
- package/package.json +16 -10
- package/dist/chunk-WJOAU4V2.mjs +0 -162
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 =
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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 {
|
|
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 {
|
|
106
|
+
export { FormClientState, collectFormDirtyPaths, createFormClient, markClientSubmitControlFlowError, runWithBrowserFormElement };
|
|
107
|
+
export type { ClientCsrfOptions, ClientSubmitContext, ClientSubmitResult, FormFieldState, FormFieldTree, FormRequestCredentials, InferFormFieldTree, UseFormOptions, UseFormResult, ValidateOnMode };
|
package/dist/internal/client.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
normalizeFailureInput,
|
|
5
5
|
normalizeStatus,
|
|
6
6
|
sanitizeFlashedInput
|
|
7
|
-
} from "../chunk-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
"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.
|
|
32
|
-
"@holo-js/validation": "^0.
|
|
37
|
+
"@holo-js/kernel": "^0.4.0",
|
|
38
|
+
"@holo-js/validation": "^0.4.0"
|
|
33
39
|
},
|
|
34
40
|
"peerDependencies": {
|
|
35
|
-
"@holo-js/security": "^0.
|
|
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.
|
|
44
|
-
"tsup": "^
|
|
45
|
-
"typescript": "^
|
|
46
|
-
"vitest": "^
|
|
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.
|
|
55
|
+
"node": "^22.18.0 || ^24.0.0 || ^26.0.0"
|
|
50
56
|
}
|
|
51
57
|
}
|
package/dist/chunk-WJOAU4V2.mjs
DELETED
|
@@ -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
|
-
};
|