@nifrajs/schema 3.5.0 → 4.0.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/dist/t.js CHANGED
@@ -1,13 +1,32 @@
1
- import { Type, } from "@sinclair/typebox";
1
+ import { CloneType, Kind, Type, TypeRegistry, } from "@sinclair/typebox";
2
2
  import { fromTypeBox } from "./adapter.js";
3
- /** Pull each property's raw TypeBox schema out of its `NifraSchema` wrapper. */
4
- function unwrap(props) {
3
+ import { fileOps, refuseFile } from "./file-kind.js";
4
+ const DEFERRED_KIND = "NifraDeferred";
5
+ /** Where `@nifrajs/web`'s output guard finds the inner schema's validate. Registered for two copies. */
6
+ const DEFERRED_VALIDATE = Symbol.for("nifra.schema.deferredValidate");
7
+ const isDeferredValue = (_, value) => typeof value === "object" &&
8
+ value !== null &&
9
+ value.__nifra_deferred === true &&
10
+ typeof value.promise?.then === "function";
11
+ /**
12
+ * Pull each property's raw TypeBox schema out of its `NifraSchema` wrapper. `where` names the calling
13
+ * constructor: these objects are checked as JSON, so a file field is refused here (it belongs in
14
+ * `t.form` from `@nifrajs/schema/form`) instead of failing every request later.
15
+ */
16
+ function unwrap(where, props) {
5
17
  const out = {};
6
18
  // Object.entries → own enumerable string keys only (no prototype walk).
7
- for (const [key, schema] of Object.entries(props))
19
+ for (const [key, schema] of Object.entries(props)) {
20
+ refuseFile(where, schema.jsonSchema);
8
21
  out[key] = schema.jsonSchema;
22
+ }
9
23
  return out;
10
24
  }
25
+ /** An open object whose scalar fields are coerced from strings - the shape of every request input
26
+ * that arrives as name/value text (a query string, a `Cookie` header). */
27
+ const textFields = (where) => (props, options) => fromTypeBox(Type.Object(unwrap(where, props), { additionalProperties: true, ...options }), {
28
+ coerce: true,
29
+ });
11
30
  /**
12
31
  * The built-in schema builder. Each constructor returns a `NifraSchema` - a
13
32
  * Standard Schema whose validated output type flows into `c.body`/`c.query`, and
@@ -31,28 +50,46 @@ export const t = {
31
50
  // rule. A body with extra keys fails validation (a 422), so `c.body` never carries attacker-supplied
32
51
  // properties (no mass-assignment). Use `t.looseObject` (or pass `{ additionalProperties: true }`) to
33
52
  // opt into an open object; an explicit `options.additionalProperties` always wins over the default.
34
- object: (props, options) => fromTypeBox(Type.Object(unwrap(props), { additionalProperties: false, ...options })),
53
+ object: (props, options) => fromTypeBox(Type.Object(unwrap("t.object", props), { additionalProperties: false, ...options })),
35
54
  /** Like `t.object` but ACCEPTS (and passes through) unknown fields - the explicit opt-out of the
36
55
  * strict default. Prefer `t.object` unless you genuinely need an open object. */
37
- looseObject: (props, options) => fromTypeBox(Type.Object(unwrap(props), { additionalProperties: true, ...options })),
38
- array: (item, options) => fromTypeBox(Type.Array(item.jsonSchema, options)),
39
- /** Marks a property optional inside `t.object`; standalone it is `T | undefined`. */
40
- optional: (schema) => fromTypeBox(Type.Optional(schema.jsonSchema)),
56
+ looseObject: (props, options) => fromTypeBox(Type.Object(unwrap("t.looseObject", props), { additionalProperties: true, ...options })),
57
+ /**
58
+ * A list. A list of `t.file()` (from `@nifrajs/schema/form`) honors `minItems` / `maxItems` and is
59
+ * used inside `t.form`.
60
+ */
61
+ array: (item, options) => fileOps("t.array", item)?.array(item, options) ??
62
+ fromTypeBox(Type.Array(item.jsonSchema, options)),
63
+ /** Marks a property optional inside `t.object` / `t.form`; standalone it is `T | undefined`. */
64
+ optional: (schema) => {
65
+ const plain = () => fromTypeBox(Type.Optional(schema.jsonSchema));
66
+ return (fileOps("t.optional", schema)?.optional(schema) ??
67
+ plain());
68
+ },
41
69
  // `const S` captures the argument as a tuple; the explicit return type then maps
42
70
  // over that captured tuple (`S[K]["jsonSchema"]`) so the union's `Static` is
43
71
  // `A | B`, not `unknown` - the value-level `.map` can't preserve per-element
44
72
  // types, so the output type is derived from `S` and the result cast to match
45
73
  // (order/length are preserved by `map`, so the tuple shape is sound).
46
- union: (schemas) => fromTypeBox(Type.Union(schemas.map((schema) => schema.jsonSchema))),
47
- record: (value, options) => fromTypeBox(Type.Record(Type.String(), value.jsonSchema, options)),
74
+ union: (schemas) => fromTypeBox(Type.Union(schemas.map((schema) => {
75
+ refuseFile("t.union", schema.jsonSchema);
76
+ return schema.jsonSchema;
77
+ }))),
78
+ record: (value, options) => {
79
+ refuseFile("t.record", value.jsonSchema);
80
+ return fromTypeBox(Type.Record(Type.String(), value.jsonSchema, options));
81
+ },
48
82
  // Composed from TypeBox directly (not `t.object`/`t.array`) so the `t` literal doesn't reference
49
83
  // itself during inference. Cursor pagination - not OFFSET - is the production default: stable under
50
84
  // concurrent inserts and O(1) per page. Build pages with `paginate()` + `encodeCursor`/`decodeCursor`.
51
85
  /** A cursor-pagination response envelope: `{ items: T[]; nextCursor: string | null }` (`null` = last page). */
52
- paginated: (item, options) => fromTypeBox(Type.Object({
53
- items: Type.Array(item.jsonSchema),
54
- nextCursor: Type.Union([Type.String(), Type.Null()]),
55
- }, { additionalProperties: false, ...options })),
86
+ paginated: (item, options) => {
87
+ refuseFile("t.paginated", item.jsonSchema);
88
+ return fromTypeBox(Type.Object({
89
+ items: Type.Array(item.jsonSchema),
90
+ nextCursor: Type.Union([Type.String(), Type.Null()]),
91
+ }, { additionalProperties: false, ...options }));
92
+ },
56
93
  /** A request query schema for cursor pagination: `{ cursor?: string; limit?: number }`. `maxLimit`
57
94
  * caps `limit` - a larger value fails validation (a 422), so a client can't request an unbounded page.
58
95
  * `coerce` is on because query values arrive as strings (`?limit=20` → `"20"`); it's what makes `limit`
@@ -70,8 +107,39 @@ export const t = {
70
107
  * traffic appends params your schema never declares, causing 422s that never appear in dev/CI). Pass
71
108
  * `{ additionalProperties: false }` to enforce a strict allowlist. This is the query-slot constructor;
72
109
  * `t.object` stays the constructor for body slots (no coercion, a JSON body is already typed). */
73
- query: (props, options) => fromTypeBox(Type.Object(unwrap(props), { additionalProperties: true, ...options }), {
74
- coerce: true,
75
- }),
110
+ query: textFields("t.query"),
111
+ /** A request-cookie schema for the `cookies` route slot, with the same string->scalar COERCION as
112
+ * `t.query` (cookie values always arrive as strings). **Open by default**: a browser sends every
113
+ * cookie the site has set - analytics, consent, other routes' sessions - so a strict allowlist
114
+ * would 422 real traffic. Declare the cookies the route reads; the rest pass through. */
115
+ cookies: textFields("t.cookies"),
116
+ /**
117
+ * A loader value marked with `defer()`, streamed in after the shell. `inner` describes what the
118
+ * promise resolves to; `@nifrajs/web` projects and validates the resolved value by it before it
119
+ * streams, so a deferred value is held to the same output contract as the rest of the data.
120
+ */
121
+ deferred: (inner) => {
122
+ refuseFile("t.deferred", inner.jsonSchema);
123
+ if (!TypeRegistry.Has(DEFERRED_KIND))
124
+ TypeRegistry.Set(DEFERRED_KIND, isDeferredValue);
125
+ return fromTypeBox(Type.Unsafe({
126
+ [Kind]: DEFERRED_KIND,
127
+ "x-nifra-deferred": true,
128
+ inner: inner.jsonSchema,
129
+ [DEFERRED_VALIDATE]: inner["~standard"].validate,
130
+ }));
131
+ },
132
+ /**
133
+ * Allow a field with a sensitive name (`token`, `password`, `apiKey`, ...) in an output schema.
134
+ * An output schema that declares one without this fails at route load; `reason` records why this
135
+ * value may reach the browser, for the reviewer who finds it later.
136
+ */
137
+ declassified: (reason, schema) => {
138
+ if (typeof reason !== "string" || reason.trim() === "") {
139
+ throw new TypeError("t.declassified: give the reason this field may reach the browser");
140
+ }
141
+ refuseFile("t.declassified", schema.jsonSchema);
142
+ return fromTypeBox(CloneType(schema.jsonSchema, { "x-nifra-declassified": reason }));
143
+ },
76
144
  };
77
145
  //# sourceMappingURL=t.js.map
package/dist/t.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"t.js","sourceRoot":"","sources":["../src/t.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"t.js","sourceRoot":"","sources":["../src/t.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/schema",
3
- "version": "3.5.0",
3
+ "version": "4.0.0",
4
4
  "description": "Built-in schema builder `t` - TypeBox-backed (free JSON Schema + compiled validators), implements Standard Schema so nifra routes validate it natively.",
5
5
  "keywords": [
6
6
  "bun",
@@ -16,6 +16,9 @@
16
16
  "standard-schema"
17
17
  ],
18
18
  "type": "module",
19
+ "nifra": {
20
+ "environment": "library"
21
+ },
19
22
  "main": "./dist/index.js",
20
23
  "module": "./dist/index.js",
21
24
  "types": "./dist/index.d.ts",
@@ -25,6 +28,11 @@
25
28
  "types": "./dist/index.d.ts",
26
29
  "default": "./dist/index.js"
27
30
  },
31
+ "./form": {
32
+ "bun": "./src/form.ts",
33
+ "types": "./dist/form.d.ts",
34
+ "default": "./dist/form.js"
35
+ },
28
36
  "./openapi": {
29
37
  "bun": "./src/openapi.ts",
30
38
  "types": "./dist/openapi.d.ts",
@@ -45,10 +53,11 @@
45
53
  "access": "public"
46
54
  },
47
55
  "dependencies": {
56
+ "@nifrajs/uploads": "4.0.0",
48
57
  "@sinclair/typebox": "^0.34.49"
49
58
  },
50
59
  "peerDependencies": {
51
- "@nifrajs/core": "^3.5.0"
60
+ "@nifrajs/core": "^4.0.0"
52
61
  },
53
62
  "devDependencies": {
54
63
  "@nifrajs/core": "workspace:*",
package/src/adapter.ts CHANGED
@@ -34,6 +34,21 @@ function toIssue(error: { readonly message: string; readonly path: string }): St
34
34
  }
35
35
  }
36
36
 
37
+ // The issues past this are never reported (a 422 lists 100), so walking them is wasted work on a
38
+ // large hostile body.
39
+ const MAX_ISSUES = 100
40
+
41
+ function firstIssues(
42
+ errors: Iterable<{ readonly message: string; readonly path: string }>,
43
+ ): StandardIssue[] {
44
+ const issues: StandardIssue[] = []
45
+ for (const error of errors) {
46
+ issues.push(toIssue(error))
47
+ if (issues.length === MAX_ISSUES) break
48
+ }
49
+ return issues
50
+ }
51
+
37
52
  // ---------------------------------------------------------------------------------------------
38
53
  // Precompiled coercion for flat scalar objects - the shape every real query schema has.
39
54
  //
@@ -230,10 +245,10 @@ export function fromTypeBox<T extends TSchema>(
230
245
  }
231
246
  if (compiled !== undefined) {
232
247
  if (compiled.Check(input)) return { value: input as Static<T> }
233
- return { issues: [...compiled.Errors(input)].map(toIssue) }
248
+ return { issues: firstIssues(compiled.Errors(input)) }
234
249
  }
235
250
  if (Value.Check(schema, input)) return { value: input as Static<T> }
236
- return { issues: [...Value.Errors(schema, input)].map(toIssue) }
251
+ return { issues: firstIssues(Value.Errors(schema, input)) }
237
252
  },
238
253
  // Phantom: `types` carries no runtime value; this cast supplies the
239
254
  // compile-time `Static<T>` that nifra's `InferOutput` reads to type `c.body`.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * How `t` tells a file field apart without carrying the code that validates one.
3
+ *
4
+ * File fields come from `@nifrajs/schema/form`. They mark their JSON Schema, and the schema object
5
+ * brings its own list and optional constructors along, so `t.array` and `t.optional` handle a file
6
+ * while an app that never takes an upload ships none of that code.
7
+ */
8
+ import { type ArrayOptions, Kind, type TSchema } from "@sinclair/typebox"
9
+ import type { NifraSchema } from "./adapter.ts"
10
+
11
+ /** Marks the JSON Schema of a file field. Registered, so two copies of this package agree on it. */
12
+ export const FILE: unique symbol = Symbol.for("nifra.schema.file")
13
+
14
+ /** Carries the file-aware constructors on a file schema. Registered for the same reason. */
15
+ export const FILE_OPS: unique symbol = Symbol.for("nifra.schema.fileOps")
16
+
17
+ export interface FileOps {
18
+ optional(schema: NifraSchema): NifraSchema
19
+ array(item: NifraSchema, options?: ArrayOptions): NifraSchema
20
+ }
21
+
22
+ type Marked = { readonly [FILE]?: true; readonly [Kind]?: string; readonly items?: TSchema }
23
+
24
+ /** Whether a JSON Schema is a file field, or a list of them. */
25
+ export function isFileSchema(schema: TSchema): boolean {
26
+ const node = schema as unknown as Marked
27
+ return (
28
+ node[FILE] === true ||
29
+ (node[Kind] === "Array" && node.items !== undefined && isFileSchema(node.items))
30
+ )
31
+ }
32
+
33
+ /** Throw when a constructor that validates JSON is handed a file field. */
34
+ export function refuseFile(where: string, schema: TSchema): void {
35
+ if (isFileSchema(schema)) {
36
+ throw new TypeError(`${where}: a file field belongs in t.form() from "@nifrajs/schema/form"`)
37
+ }
38
+ }
39
+
40
+ /**
41
+ * The file-aware constructors of a file schema, or `undefined` for any other schema. A schema whose
42
+ * JSON Schema says "file" but which lost its validator is refused: TypeBox cannot check a file.
43
+ */
44
+ export function fileOps(where: string, schema: NifraSchema): FileOps | undefined {
45
+ const ops = (schema as { readonly [FILE_OPS]?: FileOps })[FILE_OPS]
46
+ if (ops === undefined) refuseFile(where, schema.jsonSchema)
47
+ return ops
48
+ }