@c9up/rune 0.1.8 → 0.1.9
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/dist/Schema.d.ts +59 -7
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +102 -25
- package/dist/Schema.js.map +1 -1
- package/dist/formats.d.ts +2 -3
- package/dist/formats.d.ts.map +1 -1
- package/dist/formats.js +66 -7
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +18 -6
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +34 -19
- package/dist/native.js.map +1 -1
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/Schema.ts +157 -33
- package/src/formats.ts +61 -6
- package/src/index.ts +7 -0
- package/src/native.ts +36 -21
package/src/Schema.ts
CHANGED
|
@@ -54,9 +54,9 @@ import {
|
|
|
54
54
|
readHead,
|
|
55
55
|
} from "./magic.js";
|
|
56
56
|
import {
|
|
57
|
+
assertNativeAvailable,
|
|
57
58
|
isNativeAvailable,
|
|
58
59
|
validateNative,
|
|
59
|
-
warnNativeUnavailableOnce,
|
|
60
60
|
} from "./native.js";
|
|
61
61
|
|
|
62
62
|
export type ValidationMessageParams = Record<string, string | number | boolean>;
|
|
@@ -816,6 +816,21 @@ export interface ConditionalGroup {
|
|
|
816
816
|
}>;
|
|
817
817
|
}
|
|
818
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Called when no union branch matched (VineJS `UnionNoMatchCallback`). Report
|
|
821
|
+
* through the field context; reporting nothing suppresses the generic error.
|
|
822
|
+
*/
|
|
823
|
+
export type UnionNoMatchCallback = (
|
|
824
|
+
value: unknown,
|
|
825
|
+
field: FieldContext,
|
|
826
|
+
) => void;
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Called with a record's keys (VineJS `RecordKeysCallback`). Report through the
|
|
830
|
+
* field context; reporting nothing accepts the key set.
|
|
831
|
+
*/
|
|
832
|
+
export type RecordKeysCallback = (keys: string[], field: FieldContext) => void;
|
|
833
|
+
|
|
819
834
|
/** Per-field introspection returned inside `toJSON().schema`. */
|
|
820
835
|
export type SchemaIntrospection = Record<
|
|
821
836
|
string,
|
|
@@ -1405,10 +1420,10 @@ export function schema(
|
|
|
1405
1420
|
reporterError = nativeReporter?.createError;
|
|
1406
1421
|
return native;
|
|
1407
1422
|
}
|
|
1408
|
-
// This schema
|
|
1409
|
-
//
|
|
1410
|
-
//
|
|
1411
|
-
|
|
1423
|
+
// This schema carries nothing the engine cannot run, so the engine is
|
|
1424
|
+
// what must run it. Falling back to the TypeScript validator here made
|
|
1425
|
+
// the verdict depend on whether a binary loaded.
|
|
1426
|
+
assertNativeAvailable();
|
|
1412
1427
|
}
|
|
1413
1428
|
|
|
1414
1429
|
const errors: ValidationError[] = [];
|
|
@@ -1739,7 +1754,12 @@ export interface RuleDef {
|
|
|
1739
1754
|
param?: number;
|
|
1740
1755
|
/** Interpolation args exposed to i18n/messages providers (e.g. `{ min: 3 }`). */
|
|
1741
1756
|
args?: Record<string, unknown>;
|
|
1742
|
-
|
|
1757
|
+
/**
|
|
1758
|
+
* `field` is the context VineJS hands a rule — a callback-valued rule
|
|
1759
|
+
* (`in`, `notIn`, `enum`) reads `meta`/`parent` off it to compute its list
|
|
1760
|
+
* per request. Rules that do not need it simply declare one parameter.
|
|
1761
|
+
*/
|
|
1762
|
+
validate: (value: unknown, field: FieldContext) => boolean;
|
|
1743
1763
|
message: string;
|
|
1744
1764
|
/** Set when `.message()` overrode this rule's default text. */
|
|
1745
1765
|
hasCustomMessage?: boolean;
|
|
@@ -1776,12 +1796,12 @@ const UUID_RE =
|
|
|
1776
1796
|
*/
|
|
1777
1797
|
export type AllowedValues =
|
|
1778
1798
|
| ReadonlyArray<string | number | boolean>
|
|
1779
|
-
| (() => ReadonlyArray<string | number | boolean>);
|
|
1799
|
+
| ((field: FieldContext) => ReadonlyArray<string | number | boolean>);
|
|
1780
1800
|
|
|
1781
1801
|
/** Normalise a static list or a callback into a getter. */
|
|
1782
1802
|
function allowedValuesResolver(
|
|
1783
1803
|
values: AllowedValues,
|
|
1784
|
-
): () => ReadonlyArray<string | number | boolean> {
|
|
1804
|
+
): (field: FieldContext) => ReadonlyArray<string | number | boolean> {
|
|
1785
1805
|
if (typeof values === "function") return values;
|
|
1786
1806
|
const snapshot = [...values];
|
|
1787
1807
|
return () => snapshot;
|
|
@@ -1829,8 +1849,14 @@ export class RuleChain<Output = unknown> {
|
|
|
1829
1849
|
#camelCaseKeys = false;
|
|
1830
1850
|
#groups: ConditionalGroup[] = [];
|
|
1831
1851
|
#recordValueChain: RuleChain | null = null;
|
|
1852
|
+
#enumChoices:
|
|
1853
|
+
| ReadonlyArray<string | number | boolean>
|
|
1854
|
+
| ((field: FieldContext) => ReadonlyArray<string | number | boolean>)
|
|
1855
|
+
| null = null;
|
|
1856
|
+
#recordKeysCheck: RecordKeysCallback | null = null;
|
|
1832
1857
|
#tupleChains: RuleChain[] | null = null;
|
|
1833
1858
|
#unionChains: ConditionalBranch[] | null = null;
|
|
1859
|
+
#unionNoMatch: UnionNoMatchCallback | null = null;
|
|
1834
1860
|
#useRules: CompiledRule[] = [];
|
|
1835
1861
|
#asyncRules: AsyncCompiledRule[] = [];
|
|
1836
1862
|
/** Last rule added, whichever register it landed in — the `message()` target. */
|
|
@@ -1950,8 +1976,11 @@ export class RuleChain<Output = unknown> {
|
|
|
1950
1976
|
next.#camelCaseKeys = this.#camelCaseKeys;
|
|
1951
1977
|
next.#groups = [...this.#groups];
|
|
1952
1978
|
next.#recordValueChain = this.#recordValueChain;
|
|
1979
|
+
next.#enumChoices = this.#enumChoices;
|
|
1980
|
+
next.#recordKeysCheck = this.#recordKeysCheck;
|
|
1953
1981
|
next.#tupleChains = this.#tupleChains;
|
|
1954
1982
|
next.#unionChains = this.#unionChains;
|
|
1983
|
+
next.#unionNoMatch = this.#unionNoMatch;
|
|
1955
1984
|
next.#ruleMessages = new Map(this.#ruleMessages);
|
|
1956
1985
|
next.#lastRule = this.#lastRule;
|
|
1957
1986
|
next.#preTransforms = [...this.#preTransforms];
|
|
@@ -2406,6 +2435,18 @@ export class RuleChain<Output = unknown> {
|
|
|
2406
2435
|
return this.#retype<Record<string, OutputOf<Item>>>();
|
|
2407
2436
|
}
|
|
2408
2437
|
|
|
2438
|
+
/**
|
|
2439
|
+
* Check the record's KEYS, not its values (VineJS `record().validateKeys()`).
|
|
2440
|
+
*
|
|
2441
|
+
* The callback receives every key at once and reports through the field
|
|
2442
|
+
* context — the set is what matters when keys must be exclusive, exhaustive,
|
|
2443
|
+
* or drawn from a list only known at runtime.
|
|
2444
|
+
*/
|
|
2445
|
+
validateKeys(callback: RecordKeysCallback): this {
|
|
2446
|
+
this.#recordKeysCheck = callback;
|
|
2447
|
+
return this;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2409
2450
|
/**
|
|
2410
2451
|
* Fixed-length array with a schema per position (VineJS `tuple`). Extra
|
|
2411
2452
|
* items are rejected — a tuple that silently ignores a trailing element is
|
|
@@ -2436,6 +2477,20 @@ export class RuleChain<Output = unknown> {
|
|
|
2436
2477
|
* - bare chains: tried in order, first match wins, and a total miss reports a
|
|
2437
2478
|
* single `union` error rather than every losing branch's noise.
|
|
2438
2479
|
*/
|
|
2480
|
+
/**
|
|
2481
|
+
* What to do when NO union branch matched (VineJS `union().otherwise()`).
|
|
2482
|
+
*
|
|
2483
|
+
* The callback receives the value and the field, and reports the error it
|
|
2484
|
+
* wants — the point being that "matches nothing" is a useless message when
|
|
2485
|
+
* the caller knows which shapes were on offer. Reporting nothing from the
|
|
2486
|
+
* callback suppresses the generic error entirely, which is how a union
|
|
2487
|
+
* folded into a larger check stays quiet.
|
|
2488
|
+
*/
|
|
2489
|
+
otherwise(callback: UnionNoMatchCallback): this {
|
|
2490
|
+
this.#unionNoMatch = callback;
|
|
2491
|
+
return this;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2439
2494
|
union(chains: readonly UnionBranch[]): this {
|
|
2440
2495
|
this.#unionChains = chains.map(toUnionBranch);
|
|
2441
2496
|
// Marker rule: its name is not in NATIVE_RULES, which is what keeps a
|
|
@@ -2669,20 +2724,45 @@ export class RuleChain<Output = unknown> {
|
|
|
2669
2724
|
return this;
|
|
2670
2725
|
}
|
|
2671
2726
|
|
|
2672
|
-
/**
|
|
2727
|
+
/**
|
|
2728
|
+
* Must equal one of `values` (enum). Narrows the output to the union.
|
|
2729
|
+
*
|
|
2730
|
+
* `values` may be a callback receiving the field, which is how a list that
|
|
2731
|
+
* depends on the request — the roles this tenant allows, the statuses this
|
|
2732
|
+
* user may set — is computed per validation instead of frozen at import.
|
|
2733
|
+
*/
|
|
2673
2734
|
enum<const V extends readonly (string | number | boolean)[]>(
|
|
2674
|
-
values: V,
|
|
2735
|
+
values: V | ((field: FieldContext) => V),
|
|
2675
2736
|
): RuleChain<V[number]> {
|
|
2676
|
-
const
|
|
2737
|
+
const lazy = typeof values === "function";
|
|
2738
|
+
const resolve = allowedValuesResolver(values);
|
|
2739
|
+
this.#enumChoices = lazy ? values : [...values];
|
|
2677
2740
|
this.#pushRule({
|
|
2678
2741
|
name: "enum",
|
|
2679
|
-
args: { values:
|
|
2680
|
-
|
|
2742
|
+
args: lazy ? {} : { values: [...values] },
|
|
2743
|
+
// A computed list is per-call; the native engine only ever sees a
|
|
2744
|
+
// static array, so it must not run this rule.
|
|
2745
|
+
tsOnly: lazy,
|
|
2746
|
+
validate: (v, field) => resolve(field).includes(asPrimitive(v)),
|
|
2681
2747
|
message: "Invalid value",
|
|
2682
2748
|
});
|
|
2683
2749
|
return this.#retype<V[number]>();
|
|
2684
2750
|
}
|
|
2685
2751
|
|
|
2752
|
+
/**
|
|
2753
|
+
* The choices this enum was declared with (VineJS `getChoices()`) — the list
|
|
2754
|
+
* itself, or the callback when it is computed per request.
|
|
2755
|
+
*
|
|
2756
|
+
* Reading them back is what lets a form render the same options the
|
|
2757
|
+
* validator will accept, from one declaration instead of two.
|
|
2758
|
+
*/
|
|
2759
|
+
getChoices():
|
|
2760
|
+
| ReadonlyArray<string | number | boolean>
|
|
2761
|
+
| ((field: FieldContext) => ReadonlyArray<string | number | boolean>)
|
|
2762
|
+
| undefined {
|
|
2763
|
+
return this.#enumChoices ?? undefined;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2686
2766
|
/** Must equal a literal value. */
|
|
2687
2767
|
literal<V extends string | number | boolean>(value: V): RuleChain<V> {
|
|
2688
2768
|
this.#pushRule({
|
|
@@ -2922,8 +3002,13 @@ export class RuleChain<Output = unknown> {
|
|
|
2922
3002
|
}
|
|
2923
3003
|
|
|
2924
3004
|
/**
|
|
2925
|
-
* Must be a mobile number
|
|
2926
|
-
*
|
|
3005
|
+
* Must be a mobile number (VineJS/Adonis `mobile()`).
|
|
3006
|
+
*
|
|
3007
|
+
* With no `locale`, the number must be in E.164 form. With one or more,
|
|
3008
|
+
* it must match one of their numbering plans, as in VineJS. rune carries
|
|
3009
|
+
* its own plans rather than validator.js', so it knows fewer locales — an
|
|
3010
|
+
* unknown one raises `UNSUPPORTED_LOCALE` at schema build, naming the ones
|
|
3011
|
+
* it does know, rather than silently accepting anything at request time.
|
|
2927
3012
|
*/
|
|
2928
3013
|
mobile(options?: { locale?: string | string[]; strictMode?: boolean }): this {
|
|
2929
3014
|
const locales = options?.locale ? [options.locale].flat() : null;
|
|
@@ -3308,7 +3393,7 @@ export class RuleChain<Output = unknown> {
|
|
|
3308
3393
|
// A callback list is computed per call — the native engine only ever
|
|
3309
3394
|
// sees a static array, so it must not run this rule.
|
|
3310
3395
|
tsOnly: typeof values === "function",
|
|
3311
|
-
validate: (v) => resolve().includes(asPrimitive(v)),
|
|
3396
|
+
validate: (v, field) => resolve(field).includes(asPrimitive(v)),
|
|
3312
3397
|
message: "Invalid value",
|
|
3313
3398
|
});
|
|
3314
3399
|
return this;
|
|
@@ -3321,7 +3406,7 @@ export class RuleChain<Output = unknown> {
|
|
|
3321
3406
|
name: "notIn",
|
|
3322
3407
|
args: typeof values === "function" ? {} : { values: [...values] },
|
|
3323
3408
|
tsOnly: typeof values === "function",
|
|
3324
|
-
validate: (v) => !resolve().includes(asPrimitive(v)),
|
|
3409
|
+
validate: (v, field) => !resolve(field).includes(asPrimitive(v)),
|
|
3325
3410
|
message: "Invalid value",
|
|
3326
3411
|
});
|
|
3327
3412
|
return this;
|
|
@@ -3904,6 +3989,14 @@ export class RuleChain<Output = unknown> {
|
|
|
3904
3989
|
}
|
|
3905
3990
|
|
|
3906
3991
|
// 4b. Record values — same shape as the nested-object walk, arbitrary keys.
|
|
3992
|
+
if (this.#recordKeysCheck && isPlainObject(transformed)) {
|
|
3993
|
+
// Keys first: a key-level rule that rejects the shape makes the
|
|
3994
|
+
// per-value errors that would follow noise.
|
|
3995
|
+
this.#recordKeysCheck(
|
|
3996
|
+
Object.keys(transformed),
|
|
3997
|
+
this.#makeFieldContext(field, transformed, ctx, errors, () => {}),
|
|
3998
|
+
);
|
|
3999
|
+
}
|
|
3907
4000
|
if (this.#recordValueChain && isPlainObject(transformed)) {
|
|
3908
4001
|
const obj: Record<string, unknown> = { ...transformed };
|
|
3909
4002
|
transformed = obj;
|
|
@@ -3985,19 +4078,30 @@ export class RuleChain<Output = unknown> {
|
|
|
3985
4078
|
}
|
|
3986
4079
|
}
|
|
3987
4080
|
if (!matched) {
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
4081
|
+
if (this.#unionNoMatch) {
|
|
4082
|
+
// The callback owns the reporting: whatever it pushes is the
|
|
4083
|
+
// error, and pushing nothing means it handled the case itself.
|
|
4084
|
+
const reported: ValidationError[] = [];
|
|
4085
|
+
this.#unionNoMatch(
|
|
4086
|
+
transformed,
|
|
4087
|
+
this.#makeFieldContext(field, transformed, ctx, reported, () => {}),
|
|
4088
|
+
);
|
|
4089
|
+
errors.push(...reported);
|
|
4090
|
+
} else {
|
|
4091
|
+
errors.push({
|
|
3992
4092
|
field,
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4093
|
+
rule: "union",
|
|
4094
|
+
message: resolveRuleMessage(
|
|
4095
|
+
field,
|
|
4096
|
+
{
|
|
4097
|
+
name: "union",
|
|
4098
|
+
validate: () => false,
|
|
4099
|
+
message: "Does not match any allowed shape",
|
|
4100
|
+
},
|
|
4101
|
+
ctx,
|
|
4102
|
+
),
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4001
4105
|
}
|
|
4002
4106
|
}
|
|
4003
4107
|
|
|
@@ -4126,8 +4230,14 @@ export class RuleChain<Output = unknown> {
|
|
|
4126
4230
|
value: unknown,
|
|
4127
4231
|
ctx: RunContext,
|
|
4128
4232
|
): ValidationError | null {
|
|
4233
|
+
let context: FieldContext | undefined;
|
|
4234
|
+
const fieldContext = (): FieldContext =>
|
|
4235
|
+
(context ??= this.#makeFieldContext(field, value, ctx, [], () => {}));
|
|
4129
4236
|
for (const rule of this.#rules) {
|
|
4130
|
-
if (
|
|
4237
|
+
if (
|
|
4238
|
+
TYPE_RULE_NAMES.has(rule.name) &&
|
|
4239
|
+
!rule.validate(value, fieldContext())
|
|
4240
|
+
) {
|
|
4131
4241
|
return {
|
|
4132
4242
|
field,
|
|
4133
4243
|
rule: rule.name,
|
|
@@ -4146,10 +4256,21 @@ export class RuleChain<Output = unknown> {
|
|
|
4146
4256
|
ctx: RunContext,
|
|
4147
4257
|
): ValidationError[] {
|
|
4148
4258
|
const errors: ValidationError[] = [];
|
|
4259
|
+
// Built once and only if a rule actually reads it: most rules take the
|
|
4260
|
+
// value alone, and a context per rule per field is pure waste.
|
|
4261
|
+
let context: FieldContext | undefined;
|
|
4262
|
+
const fieldContext = (): FieldContext =>
|
|
4263
|
+
(context ??= this.#makeFieldContext(
|
|
4264
|
+
field,
|
|
4265
|
+
transformed,
|
|
4266
|
+
ctx,
|
|
4267
|
+
[],
|
|
4268
|
+
() => {},
|
|
4269
|
+
));
|
|
4149
4270
|
for (const rule of this.#rules) {
|
|
4150
4271
|
if (TYPE_RULE_NAMES.has(rule.name)) continue;
|
|
4151
4272
|
if (this.#bail && errors.length > 0) break;
|
|
4152
|
-
if (!rule.validate(transformed)) {
|
|
4273
|
+
if (!rule.validate(transformed, fieldContext())) {
|
|
4153
4274
|
errors.push({
|
|
4154
4275
|
field,
|
|
4155
4276
|
rule: rule.name,
|
|
@@ -4400,7 +4521,10 @@ export const rules = {
|
|
|
4400
4521
|
const typeRule = chain.rules.find((rule) =>
|
|
4401
4522
|
TYPE_RULE_NAMES.has(rule.name),
|
|
4402
4523
|
);
|
|
4403
|
-
return unionIf(
|
|
4524
|
+
return unionIf(
|
|
4525
|
+
(value, field) => typeRule?.validate(value, field) === true,
|
|
4526
|
+
chain,
|
|
4527
|
+
);
|
|
4404
4528
|
}),
|
|
4405
4529
|
);
|
|
4406
4530
|
},
|
|
@@ -4410,7 +4534,7 @@ export const rules = {
|
|
|
4410
4534
|
array: <Item extends RuleChain>(item?: Item): RuleChain<OutputOf<Item>[]> =>
|
|
4411
4535
|
new RuleChain().array(item),
|
|
4412
4536
|
enum: <const V extends readonly (string | number | boolean)[]>(
|
|
4413
|
-
values: V,
|
|
4537
|
+
values: V | ((field: FieldContext) => V),
|
|
4414
4538
|
): RuleChain<V[number]> => new RuleChain().enum(values),
|
|
4415
4539
|
literal: <V extends string | number | boolean>(value: V): RuleChain<V> =>
|
|
4416
4540
|
new RuleChain().literal(value),
|
package/src/formats.ts
CHANGED
|
@@ -157,9 +157,8 @@ export function isPostalCode(v: string, countryCode: string): boolean | null {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* Mobile numbers in E.164 form
|
|
161
|
-
*
|
|
162
|
-
* needs one writes a `.regex()` or a custom rule.
|
|
160
|
+
* Mobile numbers in E.164 form — the locale-less check. Per-locale plans live
|
|
161
|
+
* in `MOBILE_LOCALES`; see {@link isMobileForLocale}.
|
|
163
162
|
*/
|
|
164
163
|
export const isMobile = (v: string): boolean =>
|
|
165
164
|
E164_RE.test(v.replace(/[ .-]/g, ""));
|
|
@@ -490,12 +489,62 @@ export interface EmailOptions {
|
|
|
490
489
|
domain_specific_validation?: boolean;
|
|
491
490
|
}
|
|
492
491
|
|
|
492
|
+
/** Hard bound on anything handed to the email parser (RFC 5322 line limit). */
|
|
493
|
+
const MAX_EMAIL_INPUT = 998;
|
|
494
|
+
|
|
493
495
|
/** Unquoted local part: dot-separated atoms of RFC 5322 atext. */
|
|
494
496
|
const ATEXT = "[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+";
|
|
495
497
|
const DOT_ATOM_RE = new RegExp(`^${ATEXT}(?:\\.${ATEXT})*$`);
|
|
496
498
|
/** Quoted local part: `"anything but bare quote/backslash, or escaped"`. */
|
|
497
499
|
const QUOTED_LOCAL_RE = /^"(?:[^"\\]|\\.)*"$/;
|
|
498
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Split `Display Name <address@host>` into its address.
|
|
502
|
+
*
|
|
503
|
+
* Parsed rather than matched: the obvious pattern —
|
|
504
|
+
* `^\s*(?:"..."|[^<>@]*?)\s*<(.+)>\s*$` — lets `\s*` and `[^<>@]*?` both
|
|
505
|
+
* claim a space, so an input of N spaces has N ways to be split and the engine
|
|
506
|
+
* tries them all. Measured at O(n³): 8 KB of spaces blocked the event loop for
|
|
507
|
+
* 67 seconds, which turns any route validating an email into a denial of
|
|
508
|
+
* service. This walk is linear and answers the same question.
|
|
509
|
+
*
|
|
510
|
+
* Returns the address, or null when the input is not in display-name form.
|
|
511
|
+
*/
|
|
512
|
+
function displayNameAddress(input: string): string | null {
|
|
513
|
+
const trimmed = input.trim();
|
|
514
|
+
if (!trimmed.endsWith(">")) return null;
|
|
515
|
+
|
|
516
|
+
let open: number;
|
|
517
|
+
if (trimmed.startsWith('"')) {
|
|
518
|
+
// A quoted display name may contain anything, `<` included, so the
|
|
519
|
+
// address opens at the first `<` AFTER the closing quote.
|
|
520
|
+
const closingQuote = closingQuoteIndex(trimmed);
|
|
521
|
+
if (closingQuote === -1) return null;
|
|
522
|
+
open = trimmed.indexOf("<", closingQuote + 1);
|
|
523
|
+
if (open === -1) return null;
|
|
524
|
+
if (trimmed.slice(closingQuote + 1, open).trim() !== "") return null;
|
|
525
|
+
} else {
|
|
526
|
+
open = trimmed.indexOf("<");
|
|
527
|
+
if (open === -1) return null;
|
|
528
|
+
// An unquoted display name carries none of `<`, `>` or `@` — the same
|
|
529
|
+
// restriction the pattern expressed.
|
|
530
|
+
if (/[<>@]/.test(trimmed.slice(0, open))) return null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const address = trimmed.slice(open + 1, -1);
|
|
534
|
+
return address.length > 0 ? address : null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Index of the quote closing the one at position 0, or -1. */
|
|
538
|
+
function closingQuoteIndex(input: string): number {
|
|
539
|
+
for (let i = 1; i < input.length; i++) {
|
|
540
|
+
if (input[i] === "\\") {
|
|
541
|
+
i++;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (input[i] === '"') return i;
|
|
545
|
+
}
|
|
546
|
+
return -1;
|
|
547
|
+
}
|
|
499
548
|
|
|
500
549
|
/** A single DNS label: alphanumerics and inner hyphens, 1..63 chars. */
|
|
501
550
|
function isDnsLabel(label: string): boolean {
|
|
@@ -525,9 +574,15 @@ function isIpDomainLiteral(domain: string): boolean {
|
|
|
525
574
|
export function isEmail(value: string, options: EmailOptions = {}): boolean {
|
|
526
575
|
let candidate = value;
|
|
527
576
|
|
|
577
|
+
// Bound the input BEFORE any parsing. A caller may opt out of the 254-char
|
|
578
|
+
// address cap, but never out of a bound: an unbounded string reaching the
|
|
579
|
+
// parser is how a validator becomes an outage. RFC 5322 caps a whole line
|
|
580
|
+
// at 998 octets, so a display-name form has no business being longer.
|
|
581
|
+
if (value.length > MAX_EMAIL_INPUT) return false;
|
|
582
|
+
|
|
528
583
|
if (options.allow_display_name) {
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
584
|
+
const address = displayNameAddress(candidate);
|
|
585
|
+
if (address !== null) candidate = address;
|
|
531
586
|
} else if (/[<>]/.test(candidate)) {
|
|
532
587
|
return false;
|
|
533
588
|
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,11 @@ export type {
|
|
|
21
21
|
} from "./formats.js";
|
|
22
22
|
export type { MessagesProviderContract } from "./MessagesProvider.js";
|
|
23
23
|
export { SimpleMessagesProvider } from "./MessagesProvider.js";
|
|
24
|
+
export {
|
|
25
|
+
assertNativeAvailable,
|
|
26
|
+
isNativeAvailable,
|
|
27
|
+
RuneNativeRequiredError,
|
|
28
|
+
} from "./native.js";
|
|
24
29
|
export type {
|
|
25
30
|
AsyncCompiledRule,
|
|
26
31
|
AsyncRuleValidator,
|
|
@@ -31,8 +36,10 @@ export type {
|
|
|
31
36
|
DatabaseRuleOptions,
|
|
32
37
|
FieldContext,
|
|
33
38
|
Infer,
|
|
39
|
+
RecordKeysCallback,
|
|
34
40
|
RuleChain,
|
|
35
41
|
RuleValidator,
|
|
42
|
+
UnionNoMatchCallback,
|
|
36
43
|
ValidateOptions,
|
|
37
44
|
ValidationError,
|
|
38
45
|
ValidationMessageParams,
|
package/src/native.ts
CHANGED
|
@@ -58,28 +58,43 @@ export function isNativeAvailable(): boolean {
|
|
|
58
58
|
return native !== undefined;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/** Why the engine could not be loaded, phrased for whoever has to fix it. */
|
|
62
|
+
function unavailableReason(): string {
|
|
63
|
+
const target = `${platform}-${arch}`;
|
|
64
|
+
if (loadError !== undefined) {
|
|
65
|
+
return `failed to load (${loadError instanceof Error ? loadError.message : String(loadError)})`;
|
|
66
|
+
}
|
|
67
|
+
return platformMap[target] !== undefined
|
|
68
|
+
? "binary not found"
|
|
69
|
+
: `no prebuilt binary for ${target}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Raised when a schema needs the Rust engine and it is not there. */
|
|
73
|
+
export class RuneNativeRequiredError extends Error {
|
|
74
|
+
readonly code = "RUNE_NAPI_REQUIRED" as const;
|
|
75
|
+
constructor() {
|
|
76
|
+
super(
|
|
77
|
+
`[RUNE_NAPI_REQUIRED] The Rust validation engine is required but not loaded — ${unavailableReason()}.\n` +
|
|
78
|
+
"Install the prebuilt binary for this platform, or build it with `pnpm build:napi`.",
|
|
79
|
+
);
|
|
80
|
+
this.name = "RuneNativeRequiredError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
62
83
|
|
|
63
84
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
85
|
+
* Refuse to validate without the native engine.
|
|
86
|
+
*
|
|
87
|
+
* There is a TypeScript validator, and it used to take over silently with a
|
|
88
|
+
* one-time warning. That made a schema's verdict depend on whether a prebuilt
|
|
89
|
+
* binary happened to load: two deployments of the same code could disagree on
|
|
90
|
+
* whether a payload is valid, and the one that fell back lost the reason to
|
|
91
|
+
* have Rust at all. A schema the engine can run is now run BY the engine or
|
|
92
|
+
* not at all.
|
|
93
|
+
*
|
|
94
|
+
* The TypeScript path stays for what the engine genuinely cannot do — custom
|
|
95
|
+
* rules, a translator, a messages provider — where it is the only
|
|
96
|
+
* implementation, not a second one.
|
|
69
97
|
*/
|
|
70
|
-
export function
|
|
71
|
-
if (
|
|
72
|
-
warnedFallback = true;
|
|
73
|
-
const target = `${platform}-${arch}`;
|
|
74
|
-
const reason =
|
|
75
|
-
loadError !== undefined
|
|
76
|
-
? `failed to load (${loadError instanceof Error ? loadError.message : String(loadError)})`
|
|
77
|
-
: platformMap[target] !== undefined
|
|
78
|
-
? "binary not found"
|
|
79
|
-
: `no prebuilt binary for ${target}`;
|
|
80
|
-
console.warn(
|
|
81
|
-
`[rune] native validation engine unavailable — ${reason}. Falling back to the ` +
|
|
82
|
-
"TypeScript validator, whose results can differ subtly from the Rust engine — " +
|
|
83
|
-
"validation is platform-dependent for this process.",
|
|
84
|
-
);
|
|
98
|
+
export function assertNativeAvailable(): void {
|
|
99
|
+
if (native === undefined) throw new RuneNativeRequiredError();
|
|
85
100
|
}
|