@c9up/rune 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Rune Validation Schema — fluent validation rules.
3
+ *
4
+ * @implements FR38, FR39, FR40, FR41
5
+ */
6
+ export type ValidationMessageParams = Record<string, string | number | boolean>;
7
+ export type ValidationTranslator = (key: string, params?: ValidationMessageParams) => string | undefined;
8
+ export interface ValidationError {
9
+ field: string;
10
+ rule: string;
11
+ message: string;
12
+ }
13
+ /**
14
+ * Validation result — discriminated union that narrows `data` to the schema's
15
+ * `T` when `valid` is `true`, removing the need for callers to cast or guard
16
+ * `data` separately.
17
+ */
18
+ export type ValidationResult<T = Record<string, unknown>> = {
19
+ valid: true;
20
+ errors: ValidationError[];
21
+ data: T;
22
+ } | {
23
+ valid: false;
24
+ errors: ValidationError[];
25
+ data?: undefined;
26
+ };
27
+ export interface ValidationSchema<T = Record<string, unknown>> {
28
+ fields: Record<string, RuleChain>;
29
+ validate(data: unknown): ValidationResult<T>;
30
+ }
31
+ /**
32
+ * Create a validation schema.
33
+ *
34
+ * Pass `T` explicitly when the caller wants `result.data` typed as a concrete
35
+ * shape after `result.valid === true` narrows the union — runtime validation
36
+ * is unchanged, the generic only types the success branch.
37
+ *
38
+ * const RegisterValidator = schema<{ email: string; password: string }>({
39
+ * email: rules.string().email(),
40
+ * password: rules.string().min(8),
41
+ * });
42
+ *
43
+ * The default `Record<string, unknown>` matches the historical untyped surface
44
+ * so existing call sites that read `result.data` field-by-field with their
45
+ * own narrowing continue to compile.
46
+ */
47
+ export declare function schema<T = Record<string, unknown>>(fields: Record<string, RuleChain>): ValidationSchema<T>;
48
+ export declare function setValidationTranslator(translator?: ValidationTranslator): void;
49
+ export declare function bindRosetta(rosetta: {
50
+ t(key: string, params?: ValidationMessageParams): string;
51
+ }): void;
52
+ /** Rule definition stored on a chain. */
53
+ export interface RuleDef {
54
+ name: string;
55
+ param?: number;
56
+ validate: (value: unknown) => boolean;
57
+ message: string;
58
+ }
59
+ /** Rule chain — fluent validation builder. */
60
+ export declare class RuleChain {
61
+ #private;
62
+ /** Public read access to rules (for OpenAPI generation, Rust bridge). */
63
+ get rules(): readonly RuleDef[];
64
+ get isOptionalField(): boolean;
65
+ get transforms(): ReadonlyArray<{
66
+ name: string;
67
+ fn: (value: unknown) => unknown;
68
+ }>;
69
+ /** Mark field as optional. */
70
+ optional(): this;
71
+ /** Must be an object matching a nested schema. */
72
+ object(shape: Record<string, RuleChain>): this;
73
+ /** Must be an array. Items validated by the provided chain. */
74
+ array(itemChain?: RuleChain): this;
75
+ /** Must be a string. */
76
+ string(): this;
77
+ /** Must be a number. */
78
+ number(): this;
79
+ /** Must be a boolean. */
80
+ boolean(): this;
81
+ /** Minimum length (string) or minimum value (number). */
82
+ min(n: number): this;
83
+ /** Maximum length (string) or maximum value (number). */
84
+ max(n: number): this;
85
+ /** Must be a valid email. */
86
+ email(): this;
87
+ /** Must be positive (> 0) and finite. */
88
+ positive(): this;
89
+ /** Trim whitespace (transform). */
90
+ trim(): this;
91
+ /** Custom validation rule. */
92
+ custom(name: string, validate: (value: unknown) => boolean, message?: string): this;
93
+ /** Set custom error message for the last rule. */
94
+ message(msg: string): this;
95
+ /** Internal: validate a field value and return errors + transformed value. */
96
+ _validateWithTransform(field: string, value: unknown): {
97
+ errors: ValidationError[];
98
+ transformed: unknown;
99
+ };
100
+ /** Internal: validate a field value against all rules. */
101
+ _validate(field: string, value: unknown): ValidationError[];
102
+ /** Internal: apply transforms. */
103
+ _transform(value: unknown): unknown;
104
+ }
105
+ /** Entry point for building rules. */
106
+ export declare const rules: {
107
+ string: () => RuleChain;
108
+ number: () => RuleChain;
109
+ boolean: () => RuleChain;
110
+ any: () => RuleChain;
111
+ };
112
+ //# sourceMappingURL=Schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Schema.d.ts","sourceRoot":"","sources":["../src/Schema.ts"],"names":[],"mappings":"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"}
package/dist/Schema.js ADDED
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Rune Validation Schema — fluent validation rules.
3
+ *
4
+ * @implements FR38, FR39, FR40, FR41
5
+ */
6
+ import { RuneError } from "./errors.js";
7
+ import { isNativeAvailable, validateNative, warnNativeUnavailableOnce, } from "./native.js";
8
+ /** Type guard: narrows `unknown` to a plain object (non-null, non-array, typeof 'object'). */
9
+ function isPlainObject(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ /** Rules the Rust validation engine can handle natively. */
13
+ const STANDARD_RULES = new Set([
14
+ "string",
15
+ "number",
16
+ "boolean",
17
+ "min",
18
+ "max",
19
+ "email",
20
+ "positive",
21
+ ]);
22
+ /** Default messages for standard rules — used to detect custom-message overrides. */
23
+ const STANDARD_MSGS = {
24
+ string: "Must be a string",
25
+ number: "Must be a number",
26
+ boolean: "Must be a boolean",
27
+ min: "Minimum",
28
+ max: "Maximum",
29
+ email: "Must be a valid email",
30
+ positive: "Must be positive",
31
+ };
32
+ const TYPE_RULE_NAMES = new Set([
33
+ "string",
34
+ "number",
35
+ "boolean",
36
+ "object",
37
+ "array",
38
+ ]);
39
+ let validationTranslator;
40
+ function hasDefaultMessage(rule) {
41
+ if (rule.name === "min" || rule.name === "max") {
42
+ if (typeof rule.param !== "number")
43
+ return false;
44
+ const expected = `${rule.name === "min" ? "Minimum" : "Maximum"} ${rule.param}`;
45
+ return rule.message === expected;
46
+ }
47
+ const defaultMsg = STANDARD_MSGS[rule.name];
48
+ return defaultMsg !== undefined && rule.message === defaultMsg;
49
+ }
50
+ function resolveValidationMessage(key, fallback, params) {
51
+ const translated = validationTranslator?.(key, params);
52
+ if (typeof translated === "string" && translated.length > 0) {
53
+ return translated;
54
+ }
55
+ return fallback;
56
+ }
57
+ function resolveRuleMessage(field, rule) {
58
+ const fallback = rule.message;
59
+ if (!STANDARD_RULES.has(rule.name)) {
60
+ return fallback;
61
+ }
62
+ if (!hasDefaultMessage(rule)) {
63
+ return fallback;
64
+ }
65
+ const params = { field };
66
+ if (rule.name === "min" && typeof rule.param === "number") {
67
+ params.min = rule.param;
68
+ }
69
+ if (rule.name === "max" && typeof rule.param === "number") {
70
+ params.max = rule.param;
71
+ }
72
+ return resolveValidationMessage(`validation.${rule.name}`, fallback, params);
73
+ }
74
+ /** Compute once: does any field rule prevent dispatching to Rust? */
75
+ function detectHasCustomRules(fields) {
76
+ return Object.values(fields).some((chain) => {
77
+ return chain.rules.some((r) => {
78
+ if (!STANDARD_RULES.has(r.name))
79
+ return true; // custom rule
80
+ if (!hasDefaultMessage(r))
81
+ return true; // custom message
82
+ return false;
83
+ });
84
+ });
85
+ }
86
+ /**
87
+ * Create a validation schema.
88
+ *
89
+ * Pass `T` explicitly when the caller wants `result.data` typed as a concrete
90
+ * shape after `result.valid === true` narrows the union — runtime validation
91
+ * is unchanged, the generic only types the success branch.
92
+ *
93
+ * const RegisterValidator = schema<{ email: string; password: string }>({
94
+ * email: rules.string().email(),
95
+ * password: rules.string().min(8),
96
+ * });
97
+ *
98
+ * The default `Record<string, unknown>` matches the historical untyped surface
99
+ * so existing call sites that read `result.data` field-by-field with their
100
+ * own narrowing continue to compile.
101
+ */
102
+ export function schema(fields) {
103
+ // Computed once at construction time, not per validate() call.
104
+ const hasCustomRules = detectHasCustomRules(fields);
105
+ return {
106
+ fields,
107
+ validate(data) {
108
+ if (!isPlainObject(data)) {
109
+ return {
110
+ valid: false,
111
+ errors: [
112
+ {
113
+ field: "_root",
114
+ rule: "type",
115
+ message: "Input must be an object",
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ if (!hasCustomRules && !validationTranslator) {
121
+ if (isNativeAvailable()) {
122
+ return validateWithRust(fields, data);
123
+ }
124
+ // This schema would have used the native engine, but it isn't
125
+ // loaded — surface the platform-dependent TS fallback once instead
126
+ // of diverging silently. (Schemas with custom rules / a translator
127
+ // always run on TS by design and don't warn.)
128
+ warnNativeUnavailableOnce();
129
+ }
130
+ const errors = [];
131
+ const validated = {};
132
+ for (const [field, chain] of Object.entries(fields)) {
133
+ const value = data[field];
134
+ const result = chain._validateWithTransform(field, value);
135
+ errors.push(...result.errors);
136
+ if (result.errors.length === 0 && value !== undefined) {
137
+ validated[field] = result.transformed;
138
+ }
139
+ }
140
+ if (errors.length === 0) {
141
+ return { valid: true, errors, data: validated };
142
+ }
143
+ return { valid: false, errors };
144
+ },
145
+ };
146
+ }
147
+ export function setValidationTranslator(translator) {
148
+ validationTranslator = translator;
149
+ }
150
+ export function bindRosetta(rosetta) {
151
+ setValidationTranslator((key, params) => rosetta.t(key, params));
152
+ }
153
+ /** Rule chain — fluent validation builder. */
154
+ export class RuleChain {
155
+ #rules = [];
156
+ #isOptional = false;
157
+ #transforms = [];
158
+ #nestedSchema = null;
159
+ #arrayItemChain = null;
160
+ /** Public read access to rules (for OpenAPI generation, Rust bridge). */
161
+ get rules() {
162
+ return this.#rules;
163
+ }
164
+ get isOptionalField() {
165
+ return this.#isOptional;
166
+ }
167
+ get transforms() {
168
+ return this.#transforms;
169
+ }
170
+ /** Mark field as optional. */
171
+ optional() {
172
+ this.#isOptional = true;
173
+ return this;
174
+ }
175
+ /** Must be an object matching a nested schema. */
176
+ object(shape) {
177
+ this.#rules.push({
178
+ name: "object",
179
+ validate: (v) => typeof v === "object" && v !== null && !Array.isArray(v),
180
+ message: "Must be an object",
181
+ });
182
+ this.#nestedSchema = shape;
183
+ return this;
184
+ }
185
+ /** Must be an array. Items validated by the provided chain. */
186
+ array(itemChain) {
187
+ this.#rules.push({
188
+ name: "array",
189
+ validate: (v) => Array.isArray(v),
190
+ message: "Must be an array",
191
+ });
192
+ this.#arrayItemChain = itemChain ?? null;
193
+ return this;
194
+ }
195
+ /** Must be a string. */
196
+ string() {
197
+ this.#rules.push({
198
+ name: "string",
199
+ validate: (v) => typeof v === "string",
200
+ message: "Must be a string",
201
+ });
202
+ return this;
203
+ }
204
+ /** Must be a number. */
205
+ number() {
206
+ this.#rules.push({
207
+ name: "number",
208
+ validate: (v) => typeof v === "number" && !Number.isNaN(v) && Number.isFinite(v),
209
+ message: "Must be a number",
210
+ });
211
+ return this;
212
+ }
213
+ /** Must be a boolean. */
214
+ boolean() {
215
+ this.#rules.push({
216
+ name: "boolean",
217
+ validate: (v) => typeof v === "boolean",
218
+ message: "Must be a boolean",
219
+ });
220
+ return this;
221
+ }
222
+ /** Minimum length (string) or minimum value (number). */
223
+ min(n) {
224
+ this.#rules.push({
225
+ name: "min",
226
+ param: n,
227
+ validate: (v) => typeof v === "string"
228
+ ? [...v].length >= n
229
+ : typeof v === "number"
230
+ ? v >= n
231
+ : false,
232
+ message: `Minimum ${n}`,
233
+ });
234
+ return this;
235
+ }
236
+ /** Maximum length (string) or maximum value (number). */
237
+ max(n) {
238
+ this.#rules.push({
239
+ name: "max",
240
+ param: n,
241
+ validate: (v) => typeof v === "string"
242
+ ? [...v].length <= n
243
+ : typeof v === "number"
244
+ ? v <= n
245
+ : false,
246
+ message: `Maximum ${n}`,
247
+ });
248
+ return this;
249
+ }
250
+ /** Must be a valid email. */
251
+ email() {
252
+ this.#rules.push({
253
+ name: "email",
254
+ // Mirror the Rust engine's regex exactly (crates/rune-engine/src/engine.rs)
255
+ // so the SAME schema validates identically whether or not the native
256
+ // binary loaded: no whitespace anywhere (the old TS rule rejected only
257
+ // \r\n, silently accepting interior spaces), a single @, dotted domain.
258
+ validate: (v) => typeof v === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
259
+ message: "Must be a valid email",
260
+ });
261
+ return this;
262
+ }
263
+ /** Must be positive (> 0) and finite. */
264
+ positive() {
265
+ this.#rules.push({
266
+ name: "positive",
267
+ validate: (v) => typeof v === "number" && Number.isFinite(v) && v > 0,
268
+ message: "Must be positive",
269
+ });
270
+ return this;
271
+ }
272
+ /** Trim whitespace (transform). */
273
+ trim() {
274
+ this.#transforms.push({
275
+ name: "trim",
276
+ fn: (v) => (typeof v === "string" ? v.trim() : v),
277
+ });
278
+ return this;
279
+ }
280
+ /** Custom validation rule. */
281
+ custom(name, validate, message) {
282
+ this.#rules.push({
283
+ name,
284
+ validate,
285
+ message: message ?? `Failed custom rule: ${name}`,
286
+ });
287
+ return this;
288
+ }
289
+ /** Set custom error message for the last rule. */
290
+ message(msg) {
291
+ if (this.#rules.length === 0) {
292
+ throw new RuneError("NO_RULE", "message() must be called after a rule");
293
+ }
294
+ this.#rules[this.#rules.length - 1].message = msg;
295
+ return this;
296
+ }
297
+ /** Internal: validate a field value and return errors + transformed value. */
298
+ _validateWithTransform(field, value) {
299
+ if (value === undefined || value === null) {
300
+ if (this.#isOptional)
301
+ return { errors: [], transformed: value };
302
+ return { errors: [this.#requiredError(field)], transformed: value };
303
+ }
304
+ // 1. Type rules first on the raw value — bail on type mismatch.
305
+ const typeError = this.#runTypeRules(field, value);
306
+ if (typeError)
307
+ return { errors: [typeError], transformed: value };
308
+ // 2. Apply transforms (trim, etc.), then run value rules on the result.
309
+ let transformed = this.#applyTransformsTo(value);
310
+ const errors = this.#runValueRules(field, transformed);
311
+ // 4. Nested object validation (only if type check passed — not arrays)
312
+ if (this.#nestedSchema &&
313
+ typeof transformed === "object" &&
314
+ transformed !== null &&
315
+ !Array.isArray(transformed)) {
316
+ transformed = { ...transformed };
317
+ for (const [nestedField, chain] of Object.entries(this.#nestedSchema)) {
318
+ const nestedValue = transformed[nestedField];
319
+ const nestedResult = chain._validateWithTransform(`${field}.${nestedField}`, nestedValue);
320
+ errors.push(...nestedResult.errors);
321
+ if (nestedResult.transformed !== undefined) {
322
+ transformed[nestedField] =
323
+ nestedResult.transformed;
324
+ }
325
+ }
326
+ }
327
+ // 5. Array item validation
328
+ if (this.#arrayItemChain && Array.isArray(transformed)) {
329
+ transformed = [...transformed];
330
+ for (let i = 0; i < transformed.length; i++) {
331
+ const itemResult = this.#arrayItemChain._validateWithTransform(`${field}.${i}`, transformed[i]);
332
+ errors.push(...itemResult.errors);
333
+ if (itemResult.transformed !== undefined) {
334
+ transformed[i] = itemResult.transformed;
335
+ }
336
+ }
337
+ }
338
+ return { errors, transformed };
339
+ }
340
+ #requiredError(field) {
341
+ return {
342
+ field,
343
+ rule: "required",
344
+ message: resolveValidationMessage("validation.required", `${field} is required`, { field }),
345
+ };
346
+ }
347
+ /** Run the type rules (string/number/…) on the raw value; first failure bails. */
348
+ #runTypeRules(field, value) {
349
+ for (const rule of this.#rules) {
350
+ if (TYPE_RULE_NAMES.has(rule.name) && !rule.validate(value)) {
351
+ return {
352
+ field,
353
+ rule: rule.name,
354
+ message: resolveRuleMessage(field, rule),
355
+ };
356
+ }
357
+ }
358
+ return null;
359
+ }
360
+ /** Run the non-type rules (min/max/email/…) on the transformed value. */
361
+ #runValueRules(field, transformed) {
362
+ const errors = [];
363
+ for (const rule of this.#rules) {
364
+ if (!TYPE_RULE_NAMES.has(rule.name) && !rule.validate(transformed)) {
365
+ errors.push({
366
+ field,
367
+ rule: rule.name,
368
+ message: resolveRuleMessage(field, rule),
369
+ });
370
+ }
371
+ }
372
+ return errors;
373
+ }
374
+ /** Internal: validate a field value against all rules. */
375
+ _validate(field, value) {
376
+ return this._validateWithTransform(field, value).errors;
377
+ }
378
+ /** Internal: apply transforms. */
379
+ _transform(value) {
380
+ return this.#applyTransformsTo(value);
381
+ }
382
+ #applyTransformsTo(value) {
383
+ let result = value;
384
+ for (const transform of this.#transforms) {
385
+ result = transform.fn(result);
386
+ }
387
+ return result;
388
+ }
389
+ }
390
+ /** Entry point for building rules. */
391
+ export const rules = {
392
+ string: () => new RuleChain().string(),
393
+ number: () => new RuleChain().number(),
394
+ boolean: () => new RuleChain().boolean(),
395
+ any: () => new RuleChain(),
396
+ };
397
+ /** Serialize schema + data and validate via Rust NAPI. */
398
+ function validateWithRust(fields, data) {
399
+ const schemaDesc = {};
400
+ for (const [field, chain] of Object.entries(fields)) {
401
+ const rules = chain.rules.map((r) => ({
402
+ name: r.name,
403
+ // Serialize THIS rule's own param. Previously a find-first lookup by
404
+ // rule name returned the first matching rule's param, so min(3).min(5)
405
+ // sent both Rust entries as min=3, dropping the 5 bound (audit 2026-06-13).
406
+ params: r.name === "min"
407
+ ? { min: r.param }
408
+ : r.name === "max"
409
+ ? { max: r.param }
410
+ : null,
411
+ }));
412
+ schemaDesc[field] = {
413
+ rules,
414
+ optional: chain.isOptionalField,
415
+ transforms: chain.transforms.map((t) => t.name),
416
+ };
417
+ }
418
+ const request = JSON.stringify({ schema: schemaDesc, data });
419
+ const native = validateNative(request);
420
+ if (native.valid && native.data !== undefined) {
421
+ return { valid: true, errors: native.errors, data: native.data };
422
+ }
423
+ return { valid: false, errors: native.errors };
424
+ }
425
+ //# sourceMappingURL=Schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Schema.js","sourceRoot":"","sources":["../src/Schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * RuneError — structured error for Rune validation.
3
+ */
4
+ export declare class RuneError extends Error {
5
+ readonly code: string;
6
+ readonly hint?: string;
7
+ constructor(code: string, message: string, options?: {
8
+ hint?: string;
9
+ });
10
+ }
11
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEX,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;CAMtE"}
package/dist/errors.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * RuneError — structured error for Rune validation.
3
+ */
4
+ export class RuneError extends Error {
5
+ code;
6
+ hint;
7
+ constructor(code, message, options) {
8
+ super(message);
9
+ this.name = "RuneError";
10
+ this.code = `RUNE_${code}`;
11
+ this.hint = options?.hint;
12
+ }
13
+ }
14
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAC1B,IAAI,CAAS;IACb,IAAI,CAAU;IAEvB,YAAY,IAAY,EAAE,OAAe,EAAE,OAA2B;QACrE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,QAAQ,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC;IAC3B,CAAC;CACD"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module @c9up/rune
3
+ * @description Rune — Validation engine for the Ream framework
4
+ * @implements FR38, FR39, FR40, FR41, FR42
5
+ */
6
+ export { RuneError } from "./errors.js";
7
+ export type { RuleChain, ValidationError, ValidationMessageParams, ValidationResult, ValidationSchema, ValidationTranslator, } from "./Schema.js";
8
+ export { bindRosetta, rules, schema, setValidationTranslator, } from "./Schema.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EACX,SAAS,EACT,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,WAAW,EACX,KAAK,EACL,MAAM,EACN,uBAAuB,GACvB,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @module @c9up/rune
3
+ * @description Rune — Validation engine for the Ream framework
4
+ * @implements FR38, FR39, FR40, FR41, FR42
5
+ */
6
+ export { RuneError } from "./errors.js";
7
+ export { bindRosetta, rules, schema, setValidationTranslator, } from "./Schema.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AASxC,OAAO,EACN,WAAW,EACX,KAAK,EACL,MAAM,EACN,uBAAuB,GACvB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Native validation loader — loads the Rust NAPI binary.
3
+ *
4
+ * @implements FR40
5
+ */
6
+ /**
7
+ * Validate data via the Rust NAPI engine.
8
+ */
9
+ export declare function validateNative(requestJson: string): {
10
+ valid: boolean;
11
+ errors: Array<{
12
+ field: string;
13
+ rule: string;
14
+ message: string;
15
+ }>;
16
+ data?: Record<string, unknown>;
17
+ };
18
+ export declare function isNativeAvailable(): boolean;
19
+ /**
20
+ * Surface — exactly once per process — that the native engine is unavailable so
21
+ * the schema is falling back to the TypeScript validator. The two paths can
22
+ * differ subtly, so a silent fallback makes validation results depend on whether
23
+ * the prebuilt binary loaded (platform-dependent). Callers invoke this only when
24
+ * they WOULD have used the native engine (no custom rules / translator).
25
+ */
26
+ export declare function warnNativeUnavailableOnce(): void;
27
+ //# sourceMappingURL=native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA8BH;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IACpD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAaA;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAID;;;;;;GAMG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAehD"}
package/dist/native.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Native validation loader — loads the Rust NAPI binary.
3
+ *
4
+ * @implements FR40
5
+ */
6
+ import { createRequire } from "node:module";
7
+ import { dirname, join } from "node:path";
8
+ import { arch, platform } from "node:process";
9
+ import { fileURLToPath } from "node:url";
10
+ const require2 = createRequire(import.meta.url);
11
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
12
+ const platformMap = {
13
+ "linux-x64": "linux-x64-gnu",
14
+ "linux-arm64": "linux-arm64-gnu",
15
+ "darwin-x64": "darwin-x64",
16
+ "darwin-arm64": "darwin-arm64",
17
+ "win32-x64": "win32-x64-msvc",
18
+ };
19
+ let native;
20
+ let loadError;
21
+ try {
22
+ const suffix = platformMap[`${platform}-${arch}`];
23
+ if (suffix) {
24
+ native = require2(join(__dirname2, `../index.${suffix}.node`));
25
+ }
26
+ }
27
+ catch (e) {
28
+ loadError = e;
29
+ }
30
+ /**
31
+ * Validate data via the Rust NAPI engine.
32
+ */
33
+ export function validateNative(requestJson) {
34
+ if (!native) {
35
+ throw new Error(`[RUNE_NAPI_NOT_FOUND] Rust validation engine not available: ${loadError ?? "binary not found"}`);
36
+ }
37
+ const result = JSON.parse(native.validate(requestJson));
38
+ if (typeof result.valid !== "boolean" || !Array.isArray(result.errors)) {
39
+ throw new Error(`[RUNE_NAPI_INVALID_RESPONSE] Rust engine returned unexpected shape: ${JSON.stringify(result)}`);
40
+ }
41
+ return result;
42
+ }
43
+ export function isNativeAvailable() {
44
+ return native !== undefined;
45
+ }
46
+ let warnedFallback = false;
47
+ /**
48
+ * Surface — exactly once per process — that the native engine is unavailable so
49
+ * the schema is falling back to the TypeScript validator. The two paths can
50
+ * differ subtly, so a silent fallback makes validation results depend on whether
51
+ * the prebuilt binary loaded (platform-dependent). Callers invoke this only when
52
+ * they WOULD have used the native engine (no custom rules / translator).
53
+ */
54
+ export function warnNativeUnavailableOnce() {
55
+ if (warnedFallback)
56
+ return;
57
+ warnedFallback = true;
58
+ const target = `${platform}-${arch}`;
59
+ const reason = loadError !== undefined
60
+ ? `failed to load (${loadError instanceof Error ? loadError.message : String(loadError)})`
61
+ : platformMap[target] !== undefined
62
+ ? "binary not found"
63
+ : `no prebuilt binary for ${target}`;
64
+ console.warn(`[rune] native validation engine unavailable — ${reason}. Falling back to the ` +
65
+ "TypeScript validator, whose results can differ subtly from the Rust engine — " +
66
+ "validation is platform-dependent for this process.");
67
+ }
68
+ //# sourceMappingURL=native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAChD,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE3D,MAAM,WAAW,GAA2B;IAC3C,WAAW,EAAE,eAAe;IAC5B,aAAa,EAAE,iBAAiB;IAChC,YAAY,EAAE,YAAY;IAC1B,cAAc,EAAE,cAAc;IAC9B,WAAW,EAAE,gBAAgB;CAC7B,CAAC;AAEF,IAAI,MAA0D,CAAC;AAC/D,IAAI,SAAkB,CAAC;AAEvB,IAAI,CAAC;IACJ,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC,CAAC;IAClD,IAAI,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,MAAM,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;AACF,CAAC;AAAC,OAAO,CAAC,EAAE,CAAC;IACZ,SAAS,GAAG,CAAC,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,WAAmB;IAKjD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACd,+DAA+D,SAAS,IAAI,kBAAkB,EAAE,CAChG,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;IACxD,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CACd,uEAAuE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAC/F,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC;AAED,MAAM,UAAU,iBAAiB;IAChC,OAAO,MAAM,KAAK,SAAS,CAAC;AAC7B,CAAC;AAED,IAAI,cAAc,GAAG,KAAK,CAAC;AAE3B;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB;IACxC,IAAI,cAAc;QAAE,OAAO;IAC3B,cAAc,GAAG,IAAI,CAAC;IACtB,MAAM,MAAM,GAAG,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC;IACrC,MAAM,MAAM,GACX,SAAS,KAAK,SAAS;QACtB,CAAC,CAAC,mBAAmB,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG;QAC1F,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,SAAS;YAClC,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,0BAA0B,MAAM,EAAE,CAAC;IACxC,OAAO,CAAC,IAAI,CACX,iDAAiD,MAAM,wBAAwB;QAC9E,+EAA+E;QAC/E,oDAAoD,CACrD,CAAC;AACH,CAAC"}
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/rune",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Rune — Validation engine for the Ream framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/Schema.ts CHANGED
@@ -5,7 +5,11 @@
5
5
  */
6
6
 
7
7
  import { RuneError } from "./errors.js";
8
- import { isNativeAvailable, validateNative } from "./native.js";
8
+ import {
9
+ isNativeAvailable,
10
+ validateNative,
11
+ warnNativeUnavailableOnce,
12
+ } from "./native.js";
9
13
 
10
14
  export type ValidationMessageParams = Record<string, string | number | boolean>;
11
15
  export type ValidationTranslator = (
@@ -161,8 +165,15 @@ export function schema<T = Record<string, unknown>>(
161
165
  };
162
166
  }
163
167
 
164
- if (isNativeAvailable() && !hasCustomRules && !validationTranslator) {
165
- return validateWithRust<T>(fields, data);
168
+ if (!hasCustomRules && !validationTranslator) {
169
+ if (isNativeAvailable()) {
170
+ return validateWithRust<T>(fields, data);
171
+ }
172
+ // This schema would have used the native engine, but it isn't
173
+ // loaded — surface the platform-dependent TS fallback once instead
174
+ // of diverging silently. (Schemas with custom rules / a translator
175
+ // always run on TS by design and don't warn.)
176
+ warnNativeUnavailableOnce();
166
177
  }
167
178
 
168
179
  const errors: ValidationError[] = [];
@@ -322,15 +333,12 @@ export class RuleChain {
322
333
  email(): this {
323
334
  this.#rules.push({
324
335
  name: "email",
325
- validate: (v) => {
326
- if (typeof v !== "string") return false;
327
- if (/[\r\n]/.test(v)) return false;
328
- const at = v.indexOf("@");
329
- if (at <= 0 || at !== v.lastIndexOf("@")) return false;
330
- const domain = v.slice(at + 1);
331
- const dot = domain.lastIndexOf(".");
332
- return dot > 0 && dot < domain.length - 1;
333
- },
336
+ // Mirror the Rust engine's regex exactly (crates/rune-engine/src/engine.rs)
337
+ // so the SAME schema validates identically whether or not the native
338
+ // binary loaded: no whitespace anywhere (the old TS rule rejected only
339
+ // \r\n, silently accepting interior spaces), a single @, dotted domain.
340
+ validate: (v) =>
341
+ typeof v === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
334
342
  message: "Must be a valid email",
335
343
  });
336
344
  return this;
@@ -523,11 +531,14 @@ function validateWithRust<T>(
523
531
  for (const [field, chain] of Object.entries(fields)) {
524
532
  const rules = chain.rules.map((r) => ({
525
533
  name: r.name,
534
+ // Serialize THIS rule's own param. Previously a find-first lookup by
535
+ // rule name returned the first matching rule's param, so min(3).min(5)
536
+ // sent both Rust entries as min=3, dropping the 5 bound (audit 2026-06-13).
526
537
  params:
527
538
  r.name === "min"
528
- ? { min: extractParam(chain, "min") }
539
+ ? { min: r.param }
529
540
  : r.name === "max"
530
- ? { max: extractParam(chain, "max") }
541
+ ? { max: r.param }
531
542
  : null,
532
543
  }));
533
544
  schemaDesc[field] = {
@@ -544,10 +555,3 @@ function validateWithRust<T>(
544
555
  }
545
556
  return { valid: false, errors: native.errors };
546
557
  }
547
-
548
- function extractParam(chain: RuleChain, ruleName: string): number | undefined {
549
- const rule = chain.rules.find((r) => r.name === ruleName);
550
- if (!rule) return undefined;
551
- // Use stored param directly (no longer parsed from message text)
552
- return rule.param;
553
- }
package/src/native.ts CHANGED
@@ -57,3 +57,29 @@ export function validateNative(requestJson: string): {
57
57
  export function isNativeAvailable(): boolean {
58
58
  return native !== undefined;
59
59
  }
60
+
61
+ let warnedFallback = false;
62
+
63
+ /**
64
+ * Surface — exactly once per process — that the native engine is unavailable so
65
+ * the schema is falling back to the TypeScript validator. The two paths can
66
+ * differ subtly, so a silent fallback makes validation results depend on whether
67
+ * the prebuilt binary loaded (platform-dependent). Callers invoke this only when
68
+ * they WOULD have used the native engine (no custom rules / translator).
69
+ */
70
+ export function warnNativeUnavailableOnce(): void {
71
+ if (warnedFallback) return;
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
+ );
85
+ }