@holo-js/forms 0.3.28 → 0.4.1
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-DzAvmYd0.d.ts → contracts-CN5pB9Tx.d.ts} +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +61 -62
- package/dist/internal/client.d.ts +4 -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
|
+
666f4015b9d1339491c25a709f28c6da08e4247a73c32767f75cd35519173814
|
|
@@ -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
|
+
};
|
|
@@ -10,12 +10,7 @@ type FormFailureInput = number | FormFailureOptions | undefined;
|
|
|
10
10
|
declare function normalizeStatus(value: number | undefined, fallback: number): number;
|
|
11
11
|
|
|
12
12
|
type SensitiveSchema = Pick<ValidationSchema<SchemaInputShape>, 'fields'>;
|
|
13
|
-
declare function collectSensitivePaths(schemaDefinition: SensitiveSchema | undefined): readonly string[];
|
|
14
13
|
declare function sanitizeFlashedInput<TData>(values: Partial<TData> | TData, schemaDefinition?: SensitiveSchema): Partial<TData> | TData;
|
|
15
|
-
declare const sensitiveInputInternals: {
|
|
16
|
-
collectSensitivePaths: typeof collectSensitivePaths;
|
|
17
|
-
DEFAULT_DONT_FLASH_FIELDS: readonly string[];
|
|
18
|
-
};
|
|
19
14
|
|
|
20
15
|
type InferFormData<TSchema extends ValidationSchema> = InferValidationSchemaData<TSchema>;
|
|
21
16
|
interface FormFailurePayload<TData> {
|
|
@@ -126,4 +121,5 @@ declare const formsInternals: {
|
|
|
126
121
|
serializeSubmissionState: typeof serializeSubmissionState;
|
|
127
122
|
};
|
|
128
123
|
|
|
129
|
-
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,4 @@
|
|
|
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
2
|
import '@holo-js/kernel';
|
|
3
3
|
import '@holo-js/validation';
|
|
4
4
|
|
package/dist/index.mjs
CHANGED
|
@@ -2,15 +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
|
|
13
|
-
import {
|
|
12
|
+
import { HoloRequestBodyTooLargeError, readBoundedRequestBody, requestUrlInternals } from "@holo-js/kernel";
|
|
14
13
|
import {
|
|
15
14
|
ValidationException,
|
|
16
15
|
createErrorBag,
|
|
@@ -216,7 +215,7 @@ async function resolveAmbientFormDataRequest(input) {
|
|
|
216
215
|
return void 0;
|
|
217
216
|
}
|
|
218
217
|
const requestHeaders = await nextHeadersModule.headers();
|
|
219
|
-
return new Request(
|
|
218
|
+
return new Request(requestUrlInternals.resolveRoutedRequestUrl({ headers: requestHeaders }), {
|
|
220
219
|
method: "POST",
|
|
221
220
|
headers: createFormDataRequestHeaders(requestHeaders),
|
|
222
221
|
body: input
|
|
@@ -226,7 +225,7 @@ function getStructuredWebRequest(input) {
|
|
|
226
225
|
return input.web?.request instanceof Request ? void 0 : input.web?.request;
|
|
227
226
|
}
|
|
228
227
|
function extractRequestLikeUrl(input, headers) {
|
|
229
|
-
return
|
|
228
|
+
return requestUrlInternals.resolveRoutedRequestUrl({ ...input, headers }).href;
|
|
230
229
|
}
|
|
231
230
|
function extractRequestLikeMethod(input) {
|
|
232
231
|
const webRequest = getStructuredWebRequest(input);
|
|
@@ -357,37 +356,33 @@ function createSecurityFailureMetadata(error) {
|
|
|
357
356
|
...typeof retryAt === "string" ? { retryAt } : {}
|
|
358
357
|
};
|
|
359
358
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
error.status,
|
|
386
|
-
createSecurityFailureMetadata(error)
|
|
387
|
-
);
|
|
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));
|
|
388
384
|
}
|
|
389
385
|
async function safeParse(input, schemaDefinition, options = {}) {
|
|
390
|
-
let validatedSubmission;
|
|
391
386
|
const throttle = typeof options.throttle === "string" ? options.throttle : void 0;
|
|
392
387
|
const usesSecurityOptions = typeof throttle === "string";
|
|
393
388
|
const normalizedRequestInput = await normalizeRequestLikeInputForValidation(input) ?? (usesSecurityOptions ? await resolveAmbientFormDataRequest(input) : void 0);
|
|
@@ -397,45 +392,49 @@ async function safeParse(input, schemaDefinition, options = {}) {
|
|
|
397
392
|
"Security-aware safeParse() options require a Request or request-like event input."
|
|
398
393
|
);
|
|
399
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
|
+
}
|
|
400
408
|
if (usesSecurityOptions) {
|
|
401
|
-
const request =
|
|
409
|
+
const request = submitted;
|
|
402
410
|
try {
|
|
403
411
|
const { loadSecurityModule } = await import("./security-YE5UGG47.mjs");
|
|
404
412
|
const security = await loadSecurityModule();
|
|
405
|
-
const inspection = await safeParseInput(request.clone(), schemaDefinition);
|
|
406
|
-
const throttleValues = inspection.valid ? inspection.data : inspection.values;
|
|
407
|
-
validatedSubmission = inspection.valid ? createSuccessfulSubmission(schemaDefinition, inspection.data) : createFailedSubmission(schemaDefinition, inspection.values, inspection.errors.flatten());
|
|
408
413
|
await security.rateLimit(throttle, {
|
|
409
414
|
request,
|
|
410
|
-
values:
|
|
415
|
+
values: result.valid ? result.data : void 0
|
|
411
416
|
});
|
|
412
417
|
} catch (error) {
|
|
413
418
|
const { formsSecurityInternals } = await import("./security-YE5UGG47.mjs");
|
|
414
419
|
if (formsSecurityInternals.isRootSecurityError(error)) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
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
|
+
);
|
|
431
434
|
}
|
|
432
435
|
throw error;
|
|
433
436
|
}
|
|
434
437
|
}
|
|
435
|
-
if (validatedSubmission) {
|
|
436
|
-
return validatedSubmission;
|
|
437
|
-
}
|
|
438
|
-
const result = await safeParseInput(validationInput, schemaDefinition);
|
|
439
438
|
if (result.valid) {
|
|
440
439
|
return createSuccessfulSubmission(schemaDefinition, result.data);
|
|
441
440
|
}
|
|
@@ -451,7 +450,8 @@ async function validate(input, schemaDefinition, options = {}) {
|
|
|
451
450
|
ValidationException.withMessages(result.errors.flatten(), {
|
|
452
451
|
bag: options.bag
|
|
453
452
|
}),
|
|
454
|
-
result.values
|
|
453
|
+
result.values,
|
|
454
|
+
schemaDefinition
|
|
455
455
|
);
|
|
456
456
|
validationInternals.setValidationExceptionStatus(exception, failure.status);
|
|
457
457
|
validationInternals.setValidationExceptionMetadata(exception, {
|
|
@@ -477,6 +477,5 @@ export {
|
|
|
477
477
|
formsInternals,
|
|
478
478
|
safeParse,
|
|
479
479
|
sanitizeFlashedInput,
|
|
480
|
-
sensitiveInputInternals,
|
|
481
480
|
validate
|
|
482
481
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
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';
|
|
3
4
|
import '@holo-js/kernel';
|
|
4
5
|
|
|
@@ -102,4 +103,5 @@ declare function runWithBrowserFormElement<TData, TSuccess>(formClient: Pick<Use
|
|
|
102
103
|
*/
|
|
103
104
|
declare function createFormClient<TSchema extends ValidationSchema, TSuccess = unknown>(schemaDefinition: TSchema, options?: UseFormOptions<InferFormData<TSchema>, TSuccess>): UseFormResult<InferFormData<TSchema>, TSuccess, InferFormFieldTree<TSchema>>;
|
|
104
105
|
|
|
105
|
-
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.1",
|
|
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.1",
|
|
38
|
+
"@holo-js/validation": "^0.4.1"
|
|
33
39
|
},
|
|
34
40
|
"peerDependencies": {
|
|
35
|
-
"@holo-js/security": "^0.
|
|
41
|
+
"@holo-js/security": "^0.4.1"
|
|
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
|
-
};
|