@c9up/rune 0.1.7 → 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.
Files changed (51) hide show
  1. package/dist/MessagesProvider.d.ts +5 -0
  2. package/dist/MessagesProvider.d.ts.map +1 -1
  3. package/dist/MessagesProvider.js +1 -1
  4. package/dist/MessagesProvider.js.map +1 -1
  5. package/dist/Schema.d.ts +831 -35
  6. package/dist/Schema.d.ts.map +1 -1
  7. package/dist/Schema.js +2342 -140
  8. package/dist/Schema.js.map +1 -1
  9. package/dist/date.d.ts +36 -0
  10. package/dist/date.d.ts.map +1 -0
  11. package/dist/date.js +275 -0
  12. package/dist/date.js.map +1 -0
  13. package/dist/errors.d.ts +10 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +10 -0
  16. package/dist/errors.js.map +1 -1
  17. package/dist/formats.d.ts +148 -0
  18. package/dist/formats.d.ts.map +1 -0
  19. package/dist/formats.js +671 -0
  20. package/dist/formats.js.map +1 -0
  21. package/dist/index.d.ts +150 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +180 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/magic.d.ts +30 -0
  26. package/dist/magic.d.ts.map +1 -0
  27. package/dist/magic.js +154 -0
  28. package/dist/magic.js.map +1 -0
  29. package/dist/native.d.ts +18 -6
  30. package/dist/native.d.ts.map +1 -1
  31. package/dist/native.js +34 -19
  32. package/dist/native.js.map +1 -1
  33. package/dist/types.d.ts +15 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/types.js +12 -0
  36. package/dist/types.js.map +1 -0
  37. package/index.darwin-arm64.node +0 -0
  38. package/index.darwin-x64.node +0 -0
  39. package/index.linux-arm64-gnu.node +0 -0
  40. package/index.linux-x64-gnu.node +0 -0
  41. package/index.win32-x64-msvc.node +0 -0
  42. package/package.json +9 -1
  43. package/src/MessagesProvider.ts +1 -1
  44. package/src/Schema.ts +3389 -177
  45. package/src/date.ts +320 -0
  46. package/src/errors.ts +11 -0
  47. package/src/formats.ts +776 -0
  48. package/src/index.ts +269 -0
  49. package/src/magic.ts +181 -0
  50. package/src/native.ts +36 -21
  51. package/src/types.ts +55 -0
package/src/index.ts CHANGED
@@ -4,15 +4,42 @@
4
4
  * @implements FR38, FR39, FR40, FR41, FR42
5
5
  */
6
6
 
7
+ export type { DateFormat } from "./date.js";
8
+ /**
9
+ * Namespace import, mirroring `import { errors } from '@vinejs/vine'` — Adonis
10
+ * code catches on `errors.E_VALIDATION_ERROR`, so the namespace has to exist at
11
+ * the root and not only on the subpath.
12
+ */
13
+ export * as errors from "./errors.js";
7
14
  export { RuneError, RuneValidationError } from "./errors.js";
15
+ export type {
16
+ AlphaOptions,
17
+ EmailOptions,
18
+ NormalizeEmailOptions,
19
+ NormalizeUrlOptions,
20
+ UrlOptions,
21
+ } from "./formats.js";
8
22
  export type { MessagesProviderContract } from "./MessagesProvider.js";
9
23
  export { SimpleMessagesProvider } from "./MessagesProvider.js";
24
+ export {
25
+ assertNativeAvailable,
26
+ isNativeAvailable,
27
+ RuneNativeRequiredError,
28
+ } from "./native.js";
10
29
  export type {
30
+ AsyncCompiledRule,
31
+ AsyncRuleValidator,
11
32
  CompiledRule,
33
+ CreateRuleOptions,
34
+ DatabaseLookup,
35
+ DatabaseResolver,
36
+ DatabaseRuleOptions,
12
37
  FieldContext,
13
38
  Infer,
39
+ RecordKeysCallback,
14
40
  RuleChain,
15
41
  RuleValidator,
42
+ UnionNoMatchCallback,
16
43
  ValidateOptions,
17
44
  ValidationError,
18
45
  ValidationMessageParams,
@@ -21,9 +48,251 @@ export type {
21
48
  ValidationTranslator,
22
49
  } from "./Schema.js";
23
50
  export {
51
+ bindDatabase,
52
+ bindHostResolver,
53
+ bindRosetta,
54
+ compile,
55
+ create,
56
+ createAsyncRule,
57
+ createRule,
58
+ rules,
59
+ schema,
60
+ setConvertEmptyStringsToNull,
61
+ setDateTransform,
62
+ setGlobalErrorReporter,
63
+ setValidationTranslator,
64
+ } from "./Schema.js";
65
+
66
+ import type { MessagesProviderContract } from "./MessagesProvider.js";
67
+ import type { RuleChain, ValidateOptions } from "./Schema.js";
68
+ import {
69
+ bindDatabase,
70
+ bindHostResolver,
24
71
  bindRosetta,
72
+ compile,
73
+ create,
74
+ createAsyncRule,
25
75
  createRule,
76
+ getConvertEmptyStringsToNull,
77
+ getGlobalErrorReporter,
78
+ getGlobalMessagesProvider,
79
+ group,
80
+ groupElse,
81
+ groupIf,
26
82
  rules,
27
83
  schema,
84
+ setConvertEmptyStringsToNull,
85
+ setDateTransform,
86
+ setGlobalErrorReporter,
87
+ setGlobalMessagesProvider,
28
88
  setValidationTranslator,
29
89
  } from "./Schema.js";
90
+
91
+ /**
92
+ * A validator whose entry points REQUIRE `{ meta }`.
93
+ *
94
+ * VineJS refuses `validate(data)` once `withMetaData<T>()` declared metadata as
95
+ * required; keeping `meta` optional meant the guard existed at runtime but the
96
+ * compiler still waved the missing-metadata call through.
97
+ */
98
+ type WithRequiredMeta<V, M> = {
99
+ [K in keyof V]: V[K] extends (data: infer D, options?: infer O) => infer R
100
+ ? (data: D, options: Omit<O & object, "meta"> & { meta: M }) => R
101
+ : V[K];
102
+ };
103
+
104
+ /**
105
+ * Default export, mirroring `import vine from '@vinejs/vine'`.
106
+ *
107
+ * Named `rune`, not `vine` — the deviation is the package name, nothing else:
108
+ * the type factories (`rune.string()`, `rune.date()`, …) and the validator
109
+ * factories (`rune.create`, `rune.compile`) sit on one object exactly like
110
+ * VineJS, so an Adonis validator transcribes line for line.
111
+ */
112
+ const rune = {
113
+ ...rules,
114
+ schema,
115
+ create,
116
+ compile,
117
+ createRule,
118
+ createAsyncRule,
119
+ // `group` carries its branch factories, mirroring `vine.group.if/else`.
120
+ group: Object.assign(group, {
121
+ if: groupIf,
122
+ else: groupElse,
123
+ otherwise: groupElse,
124
+ }),
125
+ bindDatabase,
126
+ bindHostResolver,
127
+ bindRosetta,
128
+ setDateTransform,
129
+ setValidationTranslator,
130
+ /**
131
+ * Convert `""` to `null` before validating (VineJS
132
+ * `convertEmptyStringsToNull`). An HTML form posts empty inputs as `""`, and
133
+ * an optional field should read that as "absent", not as a present empty
134
+ * string that fails `minLength`.
135
+ */
136
+ set convertEmptyStringsToNull(enabled: boolean) {
137
+ setConvertEmptyStringsToNull(enabled);
138
+ },
139
+ get convertEmptyStringsToNull(): boolean {
140
+ return getConvertEmptyStringsToNull();
141
+ },
142
+ /**
143
+ * Predicate helpers VineJS exposes as `vine.helpers`, for writing custom
144
+ * rules without reimplementing the same three checks each time.
145
+ */
146
+ helpers: {
147
+ /** `true` for `true`, `1`, `"1"`, `"true"`, `"on"`, `"yes"`. */
148
+ isTrue: (value: unknown): boolean =>
149
+ value === true ||
150
+ value === 1 ||
151
+ (typeof value === "string" &&
152
+ ["1", "true", "on", "yes"].includes(value.toLowerCase())),
153
+ /** `true` for `false`, `0`, `"0"`, `"false"`, `"off"`, `"no"`. */
154
+ isFalse: (value: unknown): boolean =>
155
+ value === false ||
156
+ value === 0 ||
157
+ (typeof value === "string" &&
158
+ ["0", "false", "off", "no"].includes(value.toLowerCase())),
159
+ /** Neither `undefined` nor `null` (an empty string IS defined). */
160
+ exists: (value: unknown): boolean => value !== undefined && value !== null,
161
+ /** `undefined` or `null`. */
162
+ isMissing: (value: unknown): boolean =>
163
+ value === undefined || value === null,
164
+ /** A plain object, not an array and not `null`. */
165
+ isObject: (value: unknown): value is Record<string, unknown> =>
166
+ typeof value === "object" && value !== null && !Array.isArray(value),
167
+ /** An array. */
168
+ isArray: Array.isArray,
169
+ /** Every listed key is present on the object (VineJS `helpers.hasKeys`). */
170
+ hasKeys: (value: unknown, keys: readonly string[]): boolean =>
171
+ typeof value === "object" &&
172
+ value !== null &&
173
+ keys.every((key) => key in value),
174
+ /**
175
+ * No duplicate in the data set, optionally compared on one or more fields
176
+ * (VineJS `helpers.isDistinct`). `null` / `undefined` items are ignored,
177
+ * and items missing a compared key are SKIPPED, so two absent values are
178
+ * not a duplicate of each other.
179
+ */
180
+ isDistinct: (
181
+ dataSet: readonly unknown[],
182
+ fields?: string | string[],
183
+ ): boolean => {
184
+ const list = fields === undefined ? null : [fields].flat();
185
+ const keys: string[] = [];
186
+ for (const item of dataSet) {
187
+ if (item === null || item === undefined) continue;
188
+ if (list === null) {
189
+ keys.push(JSON.stringify(item));
190
+ continue;
191
+ }
192
+ if (typeof item !== "object" || item === null) continue;
193
+ const record: Record<string, unknown> = { ...item };
194
+ if (list.some((k) => record[k] === undefined || record[k] === null)) {
195
+ continue;
196
+ }
197
+ keys.push(JSON.stringify(list.map((k) => record[k])));
198
+ }
199
+ return new Set(keys).size === keys.length;
200
+ },
201
+ /** Read a dotted path off the validated data (VineJS `helpers.getNestedValue`). */
202
+ getNestedValue: (
203
+ key: string,
204
+ field: { data: Record<string, unknown> },
205
+ ): unknown => {
206
+ let cursor: unknown = field.data;
207
+ for (const segment of key.split(".")) {
208
+ if (typeof cursor !== "object" || cursor === null) return undefined;
209
+ cursor = (cursor as Record<string, unknown>)[segment];
210
+ }
211
+ return cursor;
212
+ },
213
+ /** Make every property of a shape optional (VineJS `helpers.optional`). */
214
+ optional: (props: Record<string, RuleChain>): Record<string, RuleChain> =>
215
+ Object.fromEntries(
216
+ Object.entries(props).map(([key, chain]) => [
217
+ key,
218
+ chain.clone().optional(),
219
+ ]),
220
+ ),
221
+ },
222
+ /** One-shot validation, VineJS `vine.validate({ schema, data })`. */
223
+ validate<T extends Record<string, RuleChain>>(
224
+ options: { schema: T | RuleChain; data: unknown } & ValidateOptions,
225
+ ) {
226
+ // Spread the whole ValidateOptions rather than re-listing its keys: the
227
+ // hand-listed version silently dropped `errorReporter` when it was added.
228
+ const { schema: fields, data, ...validateOptions } = options;
229
+ return create(fields as T).validate(data, validateOptions);
230
+ },
231
+ /** One-shot non-throwing validation, VineJS `vine.tryValidate`. */
232
+ tryValidate<T extends Record<string, RuleChain>>(
233
+ options: { schema: T | RuleChain; data: unknown } & ValidateOptions,
234
+ ) {
235
+ // Spread the whole ValidateOptions rather than re-listing its keys: the
236
+ // hand-listed version silently dropped `errorReporter` when it was added.
237
+ const { schema: fields, data, ...validateOptions } = options;
238
+ return create(fields as T).tryValidate(data, validateOptions);
239
+ },
240
+ /**
241
+ * Type the `meta` passed to `validate(data, { meta })` (VineJS
242
+ * `withMetaData`). Purely a typing seam — rune carries meta on the call.
243
+ */
244
+ withMetaData<M extends Record<string, unknown>>(
245
+ validateMeta?: (meta: M) => void,
246
+ ): {
247
+ create<T extends Record<string, RuleChain>>(
248
+ fields: T,
249
+ ): WithRequiredMeta<ReturnType<typeof create<T>>, M>;
250
+ } {
251
+ return {
252
+ create<T extends Record<string, RuleChain>>(fields: T) {
253
+ const validator = create(fields);
254
+ if (!validateMeta) {
255
+ return validator as WithRequiredMeta<ReturnType<typeof create<T>>, M>;
256
+ }
257
+ // The callback runs BEFORE the payload: meta that is wrong makes
258
+ // every rule reading it meaningless, so failing early is the only
259
+ // honest outcome.
260
+ const guard = <A extends unknown[], R>(
261
+ run: (...args: A) => R,
262
+ ): ((...args: A) => R) => {
263
+ return (...args: A): R => {
264
+ const options = args[1] as { meta?: M } | undefined;
265
+ validateMeta((options?.meta ?? {}) as M);
266
+ return run(...args);
267
+ };
268
+ };
269
+ return {
270
+ ...validator,
271
+ validate: guard(validator.validate),
272
+ validateResult: guard(validator.validateResult),
273
+ validateResultAsync: guard(validator.validateResultAsync),
274
+ validateOrThrow: guard(validator.validateOrThrow),
275
+ validateOrThrowAsync: guard(validator.validateOrThrowAsync),
276
+ tryValidate: guard(validator.tryValidate),
277
+ tryValidateSync: guard(validator.tryValidateSync),
278
+ } as WithRequiredMeta<ReturnType<typeof create<T>>, M>;
279
+ },
280
+ };
281
+ },
282
+ /** Process-wide error reporter (VineJS `vine.errorReporter`). */
283
+ set errorReporter(reporter: Parameters<typeof setGlobalErrorReporter>[0],) {
284
+ setGlobalErrorReporter(reporter);
285
+ },
286
+ get errorReporter(): ReturnType<typeof getGlobalErrorReporter> {
287
+ return getGlobalErrorReporter();
288
+ },
289
+ /** Bind the global messages provider (VineJS `vine.messagesProvider`). */
290
+ set messagesProvider(provider: MessagesProviderContract | null) {
291
+ setGlobalMessagesProvider(provider);
292
+ },
293
+ get messagesProvider(): MessagesProviderContract | null {
294
+ return getGlobalMessagesProvider();
295
+ },
296
+ };
297
+
298
+ export default rune;
package/src/magic.ts ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Content-based file type detection — magic numbers.
3
+ *
4
+ * Adonis rejects a `.exe` renamed to `.jpg` because its bodyparser sniffs the
5
+ * real bytes. rune validates a structural object, so on its own it can only
6
+ * trust what the upload DECLARES. This module closes that gap when a byte
7
+ * source is reachable: no external dependency, just the signature table and
8
+ * `node:fs` to read the first bytes.
9
+ *
10
+ * Signatures are matched at their documented offsets, longest-first, so a
11
+ * container that shares a prefix with another format is not mistaken for it.
12
+ */
13
+
14
+ import { open } from "node:fs/promises";
15
+
16
+ /** One magic-number signature. */
17
+ interface Signature {
18
+ /** File extension, lowercase, no dot. */
19
+ ext: string;
20
+ /** Canonical MIME type. */
21
+ mime: string;
22
+ /** Byte offset the pattern starts at. */
23
+ offset: number;
24
+ /** Bytes to match; `null` matches any byte at that position. */
25
+ bytes: ReadonlyArray<number | null>;
26
+ }
27
+
28
+ const ascii = (text: string): number[] =>
29
+ [...text].map((char) => char.charCodeAt(0));
30
+
31
+ /**
32
+ * Ordered longest-pattern-first within an offset group. `zip` sits LAST of the
33
+ * PK family because docx/xlsx/pptx are zip containers: matching `zip` first
34
+ * would label every Office document a zip.
35
+ */
36
+ const SIGNATURES: readonly Signature[] = [
37
+ {
38
+ ext: "png",
39
+ mime: "image/png",
40
+ offset: 0,
41
+ bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
42
+ },
43
+ { ext: "jpg", mime: "image/jpeg", offset: 0, bytes: [0xff, 0xd8, 0xff] },
44
+ { ext: "gif", mime: "image/gif", offset: 0, bytes: ascii("GIF8") },
45
+ { ext: "bmp", mime: "image/bmp", offset: 0, bytes: ascii("BM") },
46
+ { ext: "webp", mime: "image/webp", offset: 8, bytes: ascii("WEBP") },
47
+ { ext: "avif", mime: "image/avif", offset: 4, bytes: ascii("ftypavif") },
48
+ { ext: "heic", mime: "image/heic", offset: 4, bytes: ascii("ftypheic") },
49
+ {
50
+ ext: "tif",
51
+ mime: "image/tiff",
52
+ offset: 0,
53
+ bytes: [0x49, 0x49, 0x2a, 0x00],
54
+ },
55
+ {
56
+ ext: "tif",
57
+ mime: "image/tiff",
58
+ offset: 0,
59
+ bytes: [0x4d, 0x4d, 0x00, 0x2a],
60
+ },
61
+ {
62
+ ext: "ico",
63
+ mime: "image/x-icon",
64
+ offset: 0,
65
+ bytes: [0x00, 0x00, 0x01, 0x00],
66
+ },
67
+ { ext: "pdf", mime: "application/pdf", offset: 0, bytes: ascii("%PDF-") },
68
+ { ext: "mp4", mime: "video/mp4", offset: 4, bytes: ascii("ftyp") },
69
+ {
70
+ ext: "webm",
71
+ mime: "video/webm",
72
+ offset: 0,
73
+ bytes: [0x1a, 0x45, 0xdf, 0xa3],
74
+ },
75
+ { ext: "ogg", mime: "audio/ogg", offset: 0, bytes: ascii("OggS") },
76
+ { ext: "wav", mime: "audio/wav", offset: 8, bytes: ascii("WAVE") },
77
+ { ext: "mp3", mime: "audio/mpeg", offset: 0, bytes: ascii("ID3") },
78
+ { ext: "mp3", mime: "audio/mpeg", offset: 0, bytes: [0xff, 0xfb] },
79
+ { ext: "flac", mime: "audio/flac", offset: 0, bytes: ascii("fLaC") },
80
+ { ext: "gz", mime: "application/gzip", offset: 0, bytes: [0x1f, 0x8b] },
81
+ { ext: "bz2", mime: "application/x-bzip2", offset: 0, bytes: ascii("BZh") },
82
+ {
83
+ ext: "7z",
84
+ mime: "application/x-7z-compressed",
85
+ offset: 0,
86
+ bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c],
87
+ },
88
+ { ext: "rar", mime: "application/vnd.rar", offset: 0, bytes: ascii("Rar!") },
89
+ {
90
+ ext: "zip",
91
+ mime: "application/zip",
92
+ offset: 0,
93
+ bytes: [0x50, 0x4b, 0x03, 0x04],
94
+ },
95
+ // Executables — the whole point of sniffing: these must never pass as media.
96
+ {
97
+ ext: "exe",
98
+ mime: "application/vnd.microsoft.portable-executable",
99
+ offset: 0,
100
+ bytes: ascii("MZ"),
101
+ },
102
+ {
103
+ ext: "elf",
104
+ mime: "application/x-elf",
105
+ offset: 0,
106
+ bytes: [0x7f, 0x45, 0x4c, 0x46],
107
+ },
108
+ {
109
+ ext: "class",
110
+ mime: "application/java-vm",
111
+ offset: 0,
112
+ bytes: [0xca, 0xfe, 0xba, 0xbe],
113
+ },
114
+ {
115
+ ext: "wasm",
116
+ mime: "application/wasm",
117
+ offset: 0,
118
+ bytes: [0x00, 0x61, 0x73, 0x6d],
119
+ },
120
+ ];
121
+
122
+ /** How many leading bytes are enough for every signature above. */
123
+ export const MAGIC_HEAD_BYTES = 32;
124
+
125
+ /** The detected type of a byte head, or `null` when nothing matches. */
126
+ export interface DetectedType {
127
+ ext: string;
128
+ mime: string;
129
+ }
130
+
131
+ /** Does `head` carry `signature` at its documented offset? */
132
+ function matches(head: Uint8Array, signature: Signature): boolean {
133
+ if (head.length < signature.offset + signature.bytes.length) return false;
134
+ return signature.bytes.every((byte, index) => {
135
+ if (byte === null) return true;
136
+ return head[signature.offset + index] === byte;
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Detect the type of a byte head. Returns `null` when no signature matches —
142
+ * never a guess: an unrecognised head must be reported as unrecognised, not
143
+ * silently accepted as whatever was declared.
144
+ */
145
+ export function detectFileType(head: Uint8Array): DetectedType | null {
146
+ for (const signature of SIGNATURES) {
147
+ if (matches(head, signature))
148
+ return { ext: signature.ext, mime: signature.mime };
149
+ }
150
+ return null;
151
+ }
152
+
153
+ /** Read the leading bytes of a file. */
154
+ export async function readHead(
155
+ path: string,
156
+ length = MAGIC_HEAD_BYTES,
157
+ ): Promise<Uint8Array> {
158
+ const handle = await open(path, "r");
159
+ try {
160
+ const buffer = new Uint8Array(length);
161
+ const { bytesRead } = await handle.read(buffer, 0, length, 0);
162
+ return buffer.subarray(0, bytesRead);
163
+ } finally {
164
+ await handle.close();
165
+ }
166
+ }
167
+
168
+ /** Extensions that a given detected type legitimately covers. */
169
+ const ALIASES: Record<string, readonly string[]> = {
170
+ jpg: ["jpg", "jpeg"],
171
+ tif: ["tif", "tiff"],
172
+ // A zip container is what an Office document actually is on the wire.
173
+ zip: ["zip", "docx", "xlsx", "pptx", "odt", "ods", "odp", "epub", "jar"],
174
+ mp4: ["mp4", "m4v", "m4a", "mov"],
175
+ };
176
+
177
+ /** Is `declared` an acceptable spelling of `detected`? */
178
+ export function extensionMatches(detected: string, declared: string): boolean {
179
+ const normalized = declared.replace(/^\./, "").toLowerCase();
180
+ return (ALIASES[detected] ?? [detected]).includes(normalized);
181
+ }
package/src/native.ts CHANGED
@@ -58,28 +58,43 @@ export function isNativeAvailable(): boolean {
58
58
  return native !== undefined;
59
59
  }
60
60
 
61
- let warnedFallback = false;
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
- * 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).
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 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
- );
98
+ export function assertNativeAvailable(): void {
99
+ if (native === undefined) throw new RuneNativeRequiredError();
85
100
  }
package/src/types.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @module @c9up/rune/types
3
+ * @description
4
+ * Type-only surface, mirroring `@vinejs/vine/types`.
5
+ *
6
+ * A separate subpath so a consumer can `import type { FieldContext } from
7
+ * "@c9up/rune/types"` exactly as it would from Vine — importing types through
8
+ * the value entrypoint pulls the whole module graph into a file that only
9
+ * needed a signature.
10
+ */
11
+
12
+ export type { DateFormat } from "./date.js";
13
+ export type {
14
+ AlphaOptions,
15
+ EmailOptions,
16
+ NormalizeEmailOptions,
17
+ NormalizeUrlOptions,
18
+ UrlOptions,
19
+ } from "./formats.js";
20
+ export type {
21
+ MessageArgs,
22
+ MessagesProviderContract,
23
+ ValidationFields,
24
+ ValidationMessages,
25
+ } from "./MessagesProvider.js";
26
+ export type {
27
+ AsyncCompiledRule,
28
+ AsyncRuleValidator,
29
+ CompiledRule,
30
+ ConditionalBranch,
31
+ ConditionalGroup,
32
+ CreateRuleOptions,
33
+ DatabaseLookup,
34
+ DatabaseResolver,
35
+ DatabaseRuleOptions,
36
+ ErrorReporterContract,
37
+ ErrorReporterFactory,
38
+ FieldContext,
39
+ FileLike,
40
+ HostResolver,
41
+ Infer,
42
+ JsonSchemaModifier,
43
+ ParseContext,
44
+ RuleChain,
45
+ RuleDef,
46
+ RuleValidator,
47
+ SchemaIntrospection,
48
+ UnionBranch,
49
+ ValidateOptions,
50
+ ValidationError,
51
+ ValidationMessageParams,
52
+ ValidationResult,
53
+ ValidationSchema,
54
+ ValidationTranslator,
55
+ } from "./Schema.js";