bank20baht-validator 0.4.2 → 0.5.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/README.md +9 -5
- package/dist/index.d.ts +12 -3
- package/dist/index.js +127 -58
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ POST /api/submissions {"id":"todo-form","data":{"title":"ab"}}
|
|
|
46
46
|
|
|
47
47
|
## Compatibility
|
|
48
48
|
|
|
49
|
-
Peers on `bank20baht-ui` **>=0.
|
|
49
|
+
Peers on `bank20baht-ui` **>=0.13 <0.14** (the engine and the `FieldType` union come from there; see `peerDependencies`). In a workspace that also builds `bank20baht-ui`, keep the peer range in
|
|
50
50
|
step with the core version or the validator's `tsc` build resolves a stale copy from npm.
|
|
51
51
|
|
|
52
52
|
## What it enforces
|
|
@@ -54,16 +54,20 @@ step with the core version or the validator's `tsc` build resolves a stale copy
|
|
|
54
54
|
- Every declarative validator of the field descriptors: `required`, `min_length`, `max_length`, `min`, `max`, `pattern`, `email`, and `custom` (via a `Registry`).
|
|
55
55
|
- Value **shape** per field type before any validator runs — a `password`/`text` must be a string, a `number` a finite number, a `dropdown` one of its option keys — so `{"$gt":""}` from a raw API call is rejected, not treated as "non-empty".
|
|
56
56
|
- **Workflow rules first** (`evaluate` from `bank20baht-ui/engine`): fields hidden by rules are excluded and smuggled values are cleared; `required` derived from rules is enforced; `setValue` results are computed.
|
|
57
|
-
- Approval steps: a `decision` must be present, and the comment policy (`required` / `requireOnReject`) is honored.
|
|
57
|
+
- Approval steps: a `decision` must be present, the comment must be a string, and the comment policy (`required` / `requireOnReject`) is honored (a blank or zero-width comment does not count).
|
|
58
|
+
- **Canonical data holds only what the UI could emit:** repeater rows keep their declared columns only (a row with its own `__proto__` key is rejected), file values must be uploaded refs `{ name, size, url, type? }` with an `http(s)` or relative `url` and keep only those keys, `-0` is stored as `0`.
|
|
59
|
+
- **Strict by default (0.5.0):** a `custom` validator or `submit` script with no own entry in the registry, and a script that throws, are issues instead of being skipped. Pass `{ strict: false }` as the fifth argument to get the client's warn-and-skip behaviour (previews, docs tooling).
|
|
58
60
|
- **Client `submit` scripts** (spec 01 §14): re-run headlessly with the same `registry.scripts` — `g.setValue()` lands in canonical data, `g.addErrorMessage()` becomes an issue on `$` (`form_name.$` in a workflow), returning `false` blocks. `load` / `change` scripts are UI-only and never run here, so a field a client script hides is still validated: keep submit-relevant logic in rules or validators. `parseSchema` rejects a `scripts[]` entry with an unknown `on`, no `handler`, or a `change` hook on a field that does not exist.
|
|
59
61
|
|
|
60
62
|
## API
|
|
61
63
|
|
|
62
64
|
| export | what it does |
|
|
63
65
|
| --- | --- |
|
|
64
|
-
| `validateSubmission(schema, data, registry?)` | dispatches on schema kind — the single server entry point |
|
|
65
|
-
| `validateFormData(form, data, registry?)` | mirrors `<baht-form>.validate()` for a standalone form |
|
|
66
|
-
| `validateWorkflowData(workflow, data, registry?)` | evaluates rules, then validates every visible form |
|
|
66
|
+
| `validateSubmission(schema, data, registry?, locale?, options?)` | dispatches on schema kind — the single server entry point |
|
|
67
|
+
| `validateFormData(form, data, registry?, locale?, options?)` | mirrors `<baht-form>.validate()` for a standalone form |
|
|
68
|
+
| `validateWorkflowData(workflow, data, registry?, locale?, options?)` | evaluates rules, then validates every visible form |
|
|
69
|
+
|
|
70
|
+
`options` is `{ strict?: boolean }` (default `true`, see above).
|
|
67
71
|
| `parseSchema(json)` | structural gate for storing builder output (throws on bad input) |
|
|
68
72
|
| `isFormDefinition` / `isWorkflowDefinition` | schema kind guards |
|
|
69
73
|
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ export interface ValidationIssue {
|
|
|
5
5
|
ref: string;
|
|
6
6
|
message: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* `strict` (default true) fails closed on anything the server cannot check:
|
|
10
|
+
* a `custom` validator or a `submit` script with no registry entry, and a
|
|
11
|
+
* script that throws, each become an issue. `strict: false` restores the
|
|
12
|
+
* client's warn-and-skip behaviour for callers that only preview.
|
|
13
|
+
*/
|
|
14
|
+
export interface ValidateOptions {
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
}
|
|
8
17
|
export interface ValidationResult {
|
|
9
18
|
valid: boolean;
|
|
10
19
|
issues: ValidationIssue[];
|
|
@@ -24,7 +33,7 @@ export declare function isFormDefinition(schema: unknown): schema is FormDefinit
|
|
|
24
33
|
* views are client-side display only, the server always validates the full
|
|
25
34
|
* form (a view can never weaken a validation rule).
|
|
26
35
|
*/
|
|
27
|
-
export declare function validateFormData(form: FormDefinition, input: unknown, registry?: Registry, locale?: Locale): ValidationResult;
|
|
36
|
+
export declare function validateFormData(form: FormDefinition, input: unknown, registry?: Registry, locale?: Locale, options?: ValidateOptions): ValidationResult;
|
|
28
37
|
/**
|
|
29
38
|
* Validate a whole workflow submission — mirrors what `<baht-workflow>`
|
|
30
39
|
* enforces across its steps: rules are evaluated first (`evaluate`), invisible
|
|
@@ -34,7 +43,7 @@ export declare function validateFormData(form: FormDefinition, input: unknown, r
|
|
|
34
43
|
* client fires `baht-complete { status: 'rejected' }` immediately, so later
|
|
35
44
|
* forms are never visited.
|
|
36
45
|
*/
|
|
37
|
-
export declare function validateWorkflowData(workflow: WorkflowDefinition, input: unknown, registry?: Registry, locale?: Locale): ValidationResult;
|
|
46
|
+
export declare function validateWorkflowData(workflow: WorkflowDefinition, input: unknown, registry?: Registry, locale?: Locale, options?: ValidateOptions): ValidationResult;
|
|
38
47
|
/**
|
|
39
48
|
* Validate a submission body's `data` against whichever schema kind `id`
|
|
40
49
|
* resolved to. This is the single server entry point:
|
|
@@ -42,7 +51,7 @@ export declare function validateWorkflowData(workflow: WorkflowDefinition, input
|
|
|
42
51
|
* const schema = store.get(body.id);
|
|
43
52
|
* const result = validateSubmission(schema, body.data);
|
|
44
53
|
*/
|
|
45
|
-
export declare function validateSubmission(schema: SchemaDefinition, data: unknown, registry?: Registry, locale?: Locale): ValidationResult;
|
|
54
|
+
export declare function validateSubmission(schema: SchemaDefinition, data: unknown, registry?: Registry, locale?: Locale, options?: ValidateOptions): ValidationResult;
|
|
46
55
|
/**
|
|
47
56
|
* Structural gate for storing builder output — the same checks the builder's
|
|
48
57
|
* Import applies, so the server only ever stores schemas the client could
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// every value must have the shape the UI control could actually emit before
|
|
14
14
|
// the authored validators run.
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
|
-
import { applyFlags, approvalFields, conditionRowRefs, createScriptContext, defaultValue, evaluate, hasScripts, runScripts, getCatalog, normalizeTagsValue, parseDisabledEntry, parseIsoDate, parseIsoDateTime, parseRowRef, resolveViewFields, setCatalog, setLocale, t, validateField, walkPath, normalizeLegacySchema, } from 'bank20baht-ui/engine';
|
|
16
|
+
import { applyFlags, approvalFields, conditionRowRefs, createScriptContext, defaultValue, evaluate, hasScripts, runScripts, getCatalog, normalizeTagsValue, parseDisabledEntry, parseIsoDate, parseIsoDateTime, parseRowRef, resolveViewFields, setCatalog, setLocale, t, isBlank, isSafeUrl, isUnsafePattern, validateField, walkPath, normalizeLegacySchema, } from 'bank20baht-ui/engine';
|
|
17
17
|
/**
|
|
18
18
|
* Run `fn` with the catalog switched to `locale`, restoring the previous
|
|
19
19
|
* catalog after. The engine catalog is module-global; validation is fully
|
|
@@ -127,10 +127,24 @@ function tagsShapeIssue(field, value, optionKeys) {
|
|
|
127
127
|
}
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
130
|
+
/** The serializable ref an uploader returns (spec 01 §13), with a link-safe url. */
|
|
131
|
+
function isUploadedFile(v) {
|
|
132
|
+
return (isRecord(v) &&
|
|
133
|
+
typeof v.name === 'string' &&
|
|
134
|
+
typeof v.size === 'number' &&
|
|
135
|
+
Number.isFinite(v.size) &&
|
|
136
|
+
v.size >= 0 &&
|
|
137
|
+
typeof v.url === 'string' &&
|
|
138
|
+
isSafeUrl(v.url) &&
|
|
139
|
+
(v.type === undefined || typeof v.type === 'string'));
|
|
140
|
+
}
|
|
141
|
+
function canonicalRef({ name, size, url, type }) {
|
|
142
|
+
return type === undefined ? { name, size, url } : { name, size, url, type };
|
|
143
|
+
}
|
|
130
144
|
function fileIssue(field, value) {
|
|
131
145
|
if (field.multiple)
|
|
132
|
-
return shapeMessage(Array.isArray(value) && value.every(
|
|
133
|
-
return shapeMessage(
|
|
146
|
+
return shapeMessage(Array.isArray(value) && value.every(isUploadedFile));
|
|
147
|
+
return shapeMessage(isUploadedFile(value));
|
|
134
148
|
}
|
|
135
149
|
function typeIssue(field, value) {
|
|
136
150
|
if (value === null || value === undefined)
|
|
@@ -179,6 +193,10 @@ function shapeIssues(field, value, ref) {
|
|
|
179
193
|
issues.push({ ref: `${ref}[${i}]`, message: t('pattern') });
|
|
180
194
|
return;
|
|
181
195
|
}
|
|
196
|
+
if (Object.hasOwn(row, '__proto__')) {
|
|
197
|
+
issues.push({ ref: `${ref}[${i}]`, message: t('pattern') });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
182
200
|
for (const sub of field.fields ?? []) {
|
|
183
201
|
if (sub.type === 'label' || UNSAFE_KEYS.has(sub.key))
|
|
184
202
|
continue;
|
|
@@ -199,8 +217,66 @@ function shapeIssues(field, value, ref) {
|
|
|
199
217
|
* views are client-side display only, the server always validates the full
|
|
200
218
|
* form (a view can never weaken a validation rule).
|
|
201
219
|
*/
|
|
202
|
-
export function validateFormData(form, input, registry, locale) {
|
|
203
|
-
|
|
220
|
+
export function validateFormData(form, input, registry, locale, options) {
|
|
221
|
+
const reg = serverRegistry([form], registry, options);
|
|
222
|
+
return withLocale(locale, () => formDataResult(form, input, reg));
|
|
223
|
+
}
|
|
224
|
+
function ownEntry(map, name) {
|
|
225
|
+
return map && Object.hasOwn(map, name) ? map[name] : undefined;
|
|
226
|
+
}
|
|
227
|
+
function guardedScript(name, handler) {
|
|
228
|
+
return (g, detail) => {
|
|
229
|
+
try {
|
|
230
|
+
return handler(g, detail);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.error(`[bank20baht-validator] submit script "${name}" threw`, err);
|
|
234
|
+
g.addErrorMessage(`submit script "${name}" failed`);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The registry the engine sees. In strict mode every `custom` validator and
|
|
241
|
+
* `submit` script the schema names resolves to something that fails closed:
|
|
242
|
+
* the caller's own entry (scripts wrapped so a throw is an issue), else a stub
|
|
243
|
+
* that reports the missing name. Own entries only, so `toString` never
|
|
244
|
+
* resolves through Object.prototype.
|
|
245
|
+
*/
|
|
246
|
+
function serverRegistry(forms, registry, options) {
|
|
247
|
+
if (options?.strict === false)
|
|
248
|
+
return registry;
|
|
249
|
+
const validators = new Map();
|
|
250
|
+
const scripts = new Map();
|
|
251
|
+
for (const form of forms) {
|
|
252
|
+
const fields = form.fields.flatMap((f) => [
|
|
253
|
+
f,
|
|
254
|
+
...(f.type === 'repeater' ? (f.fields ?? []) : []),
|
|
255
|
+
]);
|
|
256
|
+
for (const spec of fields.flatMap((f) => f.validators ?? [])) {
|
|
257
|
+
if (spec.type !== 'custom')
|
|
258
|
+
continue;
|
|
259
|
+
const name = spec.fn ?? '';
|
|
260
|
+
validators.set(name, ownEntry(registry?.validators, name) ?? (() => `unknown custom validator "${name}"`));
|
|
261
|
+
}
|
|
262
|
+
for (const spec of form.scripts ?? []) {
|
|
263
|
+
if (spec.on !== 'submit')
|
|
264
|
+
continue;
|
|
265
|
+
const name = spec.handler;
|
|
266
|
+
const handler = ownEntry(registry?.scripts, name);
|
|
267
|
+
scripts.set(name, handler
|
|
268
|
+
? guardedScript(name, handler)
|
|
269
|
+
: (g) => {
|
|
270
|
+
g.addErrorMessage(`submit script "${name}" is not registered`);
|
|
271
|
+
return false;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
...registry,
|
|
277
|
+
validators: Object.fromEntries(validators),
|
|
278
|
+
scripts: Object.fromEntries(scripts),
|
|
279
|
+
};
|
|
204
280
|
}
|
|
205
281
|
/**
|
|
206
282
|
* A tags value as the client emits it: trimmed, case-insensitive deduped
|
|
@@ -219,30 +295,39 @@ function canonicalTags(field, v) {
|
|
|
219
295
|
return normalizeTagsValue(v, field.options_from ? undefined : field.options);
|
|
220
296
|
}
|
|
221
297
|
/**
|
|
222
|
-
* Canonical value for one field: `tags` via
|
|
223
|
-
* transform `validateField` applies internally
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* repeater never nests, spec 01 §10). Every other
|
|
227
|
-
*
|
|
228
|
-
*
|
|
298
|
+
* Canonical value for one field, as the client would emit it: `tags` via
|
|
299
|
+
* `canonicalTags` (the transform `validateField` applies internally, so the
|
|
300
|
+
* persisted `data` matches byte-for-byte); a file ref keeps only its ref
|
|
301
|
+
* fields; `-0` becomes `0`; a repeater row is rebuilt from its declared
|
|
302
|
+
* columns only (a repeater never nests, spec 01 §10). Every other value
|
|
303
|
+
* passes through. Runs after `shapeIssues`, or on rule input where a
|
|
304
|
+
* wrong-shaped value is left for `shapeIssues` to reject.
|
|
229
305
|
*/
|
|
230
306
|
function normalizeCanonicalValue(field, raw) {
|
|
231
307
|
if (field.type === 'tags')
|
|
232
308
|
return canonicalTags(field, raw);
|
|
233
|
-
if (field.type
|
|
234
|
-
return raw;
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
309
|
+
if (field.type === 'number' || field.type === 'currency')
|
|
310
|
+
return Object.is(raw, -0) ? 0 : raw;
|
|
311
|
+
if (field.type === 'file') {
|
|
312
|
+
if (isUploadedFile(raw))
|
|
313
|
+
return canonicalRef(raw);
|
|
314
|
+
return Array.isArray(raw) && raw.every(isUploadedFile) ? raw.map(canonicalRef) : raw;
|
|
315
|
+
}
|
|
316
|
+
if (field.type === 'repeater' && Array.isArray(raw)) {
|
|
317
|
+
return raw.map((row) => canonicalRow(field, row));
|
|
318
|
+
}
|
|
319
|
+
return raw;
|
|
320
|
+
}
|
|
321
|
+
function canonicalRow(field, row) {
|
|
322
|
+
if (!isRecord(row))
|
|
323
|
+
return row;
|
|
324
|
+
const next = {};
|
|
325
|
+
for (const sub of field.fields ?? []) {
|
|
326
|
+
if (sub.type === 'label' || UNSAFE_KEYS.has(sub.key) || !Object.hasOwn(row, sub.key))
|
|
327
|
+
continue;
|
|
328
|
+
next[sub.key] = normalizeCanonicalValue(sub, row[sub.key]);
|
|
329
|
+
}
|
|
330
|
+
return next;
|
|
246
331
|
}
|
|
247
332
|
/**
|
|
248
333
|
* Copy of the workflow input with every tags value (and tags column)
|
|
@@ -362,8 +447,9 @@ function submitScriptIssues(form, data, registry, refPrefix = '') {
|
|
|
362
447
|
* client fires `baht-complete { status: 'rejected' }` immediately, so later
|
|
363
448
|
* forms are never visited.
|
|
364
449
|
*/
|
|
365
|
-
export function validateWorkflowData(workflow, input, registry, locale) {
|
|
366
|
-
|
|
450
|
+
export function validateWorkflowData(workflow, input, registry, locale, options) {
|
|
451
|
+
const reg = serverRegistry(workflow.forms, registry, options);
|
|
452
|
+
return withLocale(locale, () => workflowDataResult(workflow, input, reg));
|
|
367
453
|
}
|
|
368
454
|
/** Early-out for input that is not `{ formName: {…} }`, else null. */
|
|
369
455
|
function workflowInputRejection(input) {
|
|
@@ -439,7 +525,7 @@ function rowStatesOf(form, field, derived) {
|
|
|
439
525
|
}
|
|
440
526
|
function formFieldIssues(form, derived, registry) {
|
|
441
527
|
const issues = [];
|
|
442
|
-
for (const field of sorted(form.fields)) {
|
|
528
|
+
for (const field of sorted([...form.fields, ...approvalFields(form)])) {
|
|
443
529
|
if (field.type === 'label')
|
|
444
530
|
continue;
|
|
445
531
|
issues.push(...workflowFieldIssues(form, field, derived, registry));
|
|
@@ -479,11 +565,11 @@ function workflowDataResult(workflow, input, registry) {
|
|
|
479
565
|
.map((e) => ({ ref: e.ref, message: e.message }));
|
|
480
566
|
const pathList = pruneOffPath(workflow, derived);
|
|
481
567
|
for (const form of visitedForms(workflow, derived, pathList)) {
|
|
568
|
+
issues.push(...formFieldIssues(form, derived, registry));
|
|
482
569
|
if (form.kind === 'approval') {
|
|
483
570
|
issues.push(...approvalIssues(form, derived.data));
|
|
484
571
|
continue;
|
|
485
572
|
}
|
|
486
|
-
issues.push(...formFieldIssues(form, derived, registry));
|
|
487
573
|
if (hasScripts(form, 'submit')) {
|
|
488
574
|
const formData = (derived.data[form.form_name] ??= {});
|
|
489
575
|
issues.push(...submitScriptIssues(form, formData, registry, `${form.form_name}.`));
|
|
@@ -501,8 +587,9 @@ function approvalIssues(form, data) {
|
|
|
501
587
|
}
|
|
502
588
|
const mode = form.approval?.comment ?? 'optional';
|
|
503
589
|
const commentRequired = mode === 'required' || (mode === 'requireOnReject' && decision === 'rejected');
|
|
504
|
-
//
|
|
505
|
-
|
|
590
|
+
// the comment's shape was already checked; a wrong shape was reset to null
|
|
591
|
+
const comment = typeof values.comment === 'string' ? values.comment : '';
|
|
592
|
+
if (commentRequired && isBlank(comment)) {
|
|
506
593
|
issues.push({ ref: `${form.form_name}.comment`, message: t('commentRequired') });
|
|
507
594
|
}
|
|
508
595
|
return issues;
|
|
@@ -514,11 +601,12 @@ function approvalIssues(form, data) {
|
|
|
514
601
|
* const schema = store.get(body.id);
|
|
515
602
|
* const result = validateSubmission(schema, body.data);
|
|
516
603
|
*/
|
|
517
|
-
export function validateSubmission(schema, data, registry, locale) {
|
|
518
|
-
if (isWorkflowDefinition(schema))
|
|
519
|
-
return validateWorkflowData(schema, data, registry, locale);
|
|
604
|
+
export function validateSubmission(schema, data, registry, locale, options) {
|
|
605
|
+
if (isWorkflowDefinition(schema)) {
|
|
606
|
+
return validateWorkflowData(schema, data, registry, locale, options);
|
|
607
|
+
}
|
|
520
608
|
if (isFormDefinition(schema))
|
|
521
|
-
return validateFormData(schema, data, registry, locale);
|
|
609
|
+
return validateFormData(schema, data, registry, locale, options);
|
|
522
610
|
return {
|
|
523
611
|
valid: false,
|
|
524
612
|
issues: [{ ref: '$', message: 'unrecognized schema: neither a form nor a workflow' }],
|
|
@@ -684,25 +772,6 @@ function assertValidFields(form) {
|
|
|
684
772
|
assertSubFields(field, form.form_name);
|
|
685
773
|
}
|
|
686
774
|
}
|
|
687
|
-
/**
|
|
688
|
-
* True for the textbook ReDoS shape: an unbounded quantifier (`* + {n,}`)
|
|
689
|
-
* applied to a group whose ENTIRE body is one unboundedly-quantified atom —
|
|
690
|
-
* `(a+)+`, `(\d*)*`, `(?:\w+){2,}`. That exact nesting is always either
|
|
691
|
-
* redundant (matches the same language as a single quantifier) or exponential
|
|
692
|
-
* to backtrack, so rejecting it has no legitimate false positives. It is
|
|
693
|
-
* deliberately NARROW: longer bodies like `(abc+)+` are safe (the literal
|
|
694
|
-
* prefix removes the cross-iteration ambiguity) and other catastrophic shapes
|
|
695
|
-
* (e.g. overlapping alternation `(a|a)*`) are out of scope — patterns are
|
|
696
|
-
* builder-authored, so the goal is catching the classic authoring mistake at
|
|
697
|
-
* save time, not sandboxing hostile regexes.
|
|
698
|
-
*/
|
|
699
|
-
function hasNestedUnboundedQuantifier(pattern) {
|
|
700
|
-
// Strip escaped chars and character classes first so `\+`, `[+]` etc.
|
|
701
|
-
// aren't mistaken for quantifiers or group delimiters.
|
|
702
|
-
const stripped = pattern.replace(/\\./g, 'x').replace(/\[[^\]]*\]/g, 'C');
|
|
703
|
-
const unbounded = '(?:[*+]|\\{\\d+,\\})';
|
|
704
|
-
return new RegExp(`\\((?:\\?:)?[^()|]${unbounded}\\??\\)${unbounded}`).test(stripped);
|
|
705
|
-
}
|
|
706
775
|
function assertFieldKey(field, form_name) {
|
|
707
776
|
// labels are static blocks and may omit the key; everything else needs one
|
|
708
777
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard: value is untrusted JSON
|
|
@@ -725,10 +794,10 @@ function assertPatternValidator(v, where) {
|
|
|
725
794
|
catch {
|
|
726
795
|
throw new Error(`invalid pattern ${JSON.stringify(v.value)} on ${where}`);
|
|
727
796
|
}
|
|
728
|
-
if (
|
|
729
|
-
throw new Error(`pattern on ${where} nests unbounded quantifiers ` +
|
|
730
|
-
`(e.g. "(a+)+") — this backtracks catastrophically on hostile input; ` +
|
|
731
|
-
`
|
|
797
|
+
if (isUnsafePattern(pattern)) {
|
|
798
|
+
throw new Error(`pattern on ${where} nests or overlaps unbounded quantifiers ` +
|
|
799
|
+
`(e.g. "(a+)+", "(\\w|\\d)+") — this backtracks catastrophically on hostile input; ` +
|
|
800
|
+
`rewrite it so each character can match only one way`);
|
|
732
801
|
}
|
|
733
802
|
}
|
|
734
803
|
function assertFieldValidators(field, form_name) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bank20baht-validator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime validation for bahtui form/workflow JSON \u2014 the exact engine rules the client runs, on the server.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"bank20baht-ui": "^0.
|
|
44
|
+
"bank20baht-ui": "^0.13.0",
|
|
45
45
|
"typescript": "^5.8.3",
|
|
46
46
|
"vitest": "^3.2.4"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"bank20baht-ui": ">=0.
|
|
49
|
+
"bank20baht-ui": ">=0.13.0 <0.14.0"
|
|
50
50
|
},
|
|
51
51
|
"main": "./dist/index.js",
|
|
52
52
|
"types": "./dist/index.d.ts",
|