@znight/http 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +2333 -2
- package/dist/adapter-CGSceVEN.d.mts +876 -0
- package/dist/compiler.d.mts +156 -0
- package/dist/compiler.mjs +2180 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/express.d.mts +39 -0
- package/dist/express.mjs +135 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +33 -0
- package/dist/fastify.mjs +191 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +49 -0
- package/dist/hono.mjs +319 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +1254 -0
- package/dist/index.mjs +5618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-BUnkdSsa.mjs +183 -0
- package/dist/node-BUnkdSsa.mjs.map +1 -0
- package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
- package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
- package/dist/node.d.mts +30 -0
- package/dist/node.mjs +1 -0
- package/dist/path-JI87C4k8.d.mts +109 -0
- package/dist/path.d.mts +2 -0
- package/dist/path.mjs +245 -0
- package/dist/path.mjs.map +1 -0
- package/dist/size-D8rsQcV0.mjs +184 -0
- package/dist/size-D8rsQcV0.mjs.map +1 -0
- package/dist/utils-DCbu3a4w.mjs +27 -0
- package/dist/utils-DCbu3a4w.mjs.map +1 -0
- package/package.json +142 -4
- package/src/adapter.ts +240 -0
- package/src/adapters/express.ts +201 -0
- package/src/adapters/fastify.ts +280 -0
- package/src/adapters/hono.ts +488 -0
- package/src/adapters/node-io.ts +368 -0
- package/src/adapters/node.ts +135 -0
- package/src/adapters/shared.ts +508 -0
- package/src/body.ts +135 -0
- package/src/coerce.ts +195 -0
- package/src/compiler/analyze.ts +1513 -0
- package/src/compiler/ast.ts +90 -0
- package/src/compiler/coercion.ts +267 -0
- package/src/compiler/generate.ts +374 -0
- package/src/compiler/index.ts +76 -0
- package/src/compiler/model.ts +260 -0
- package/src/compiler/route-decorators.ts +405 -0
- package/src/compiler/validate.ts +288 -0
- package/src/context.ts +533 -0
- package/src/cookie-settings.ts +68 -0
- package/src/cookie-signer.ts +106 -0
- package/src/cookies.ts +726 -0
- package/src/cors.ts +349 -0
- package/src/decisions.ts +331 -0
- package/src/decorators.ts +486 -0
- package/src/event-channel.ts +197 -0
- package/src/exceptions.ts +257 -0
- package/src/files.ts +429 -0
- package/src/headers.ts +39 -0
- package/src/index.ts +185 -0
- package/src/inject-sse.ts +309 -0
- package/src/interfaces.ts +101 -0
- package/src/metadata.ts +162 -0
- package/src/mime.ts +157 -0
- package/src/multipart.ts +597 -0
- package/src/option-error.ts +31 -0
- package/src/path.ts +288 -0
- package/src/pipeline.ts +918 -0
- package/src/plugin-routes.ts +188 -0
- package/src/plugin.ts +1432 -0
- package/src/proxy.ts +236 -0
- package/src/query.ts +30 -0
- package/src/response.ts +240 -0
- package/src/router.ts +119 -0
- package/src/security-headers.ts +161 -0
- package/src/serialize.ts +264 -0
- package/src/size.ts +66 -0
- package/src/sse-parse.ts +194 -0
- package/src/sse.ts +650 -0
- package/src/standard-schema.ts +81 -0
- package/src/status.ts +78 -0
- package/src/stream.ts +200 -0
- package/src/tokens.ts +21 -0
- package/src/uploads.ts +176 -0
- package/src/utils.ts +31 -0
- package/src/validate.ts +65 -0
package/src/coerce.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { BadRequestException } from './exceptions.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a raw request value (route parameter, query value, header) to `T`, or throws a
|
|
5
|
+
* `400 Bad Request` naming `label` (`param "id"`, `query "page"`, `header "x-limit"`).
|
|
6
|
+
* Called by the compiler-generated `invoke` functions.
|
|
7
|
+
*/
|
|
8
|
+
export type Coercer<T> = (value: unknown, label: string) => T
|
|
9
|
+
|
|
10
|
+
/** The coercion helpers called by generated code (see the "Compiler contract" of the README). */
|
|
11
|
+
export interface Coercers {
|
|
12
|
+
/** A string. Rejects missing values and repeated query values. */
|
|
13
|
+
readonly string: Coercer<string>
|
|
14
|
+
/** A finite decimal number (`12`, `-1.5`, `1e3`). Rejects `''`, hex, `NaN`, `Infinity`. */
|
|
15
|
+
readonly number: Coercer<number>
|
|
16
|
+
/** A safe integer (`42`, `-7`). */
|
|
17
|
+
readonly integer: Coercer<number>
|
|
18
|
+
/** `true`/`1` → `true`, `false`/`0` → `false` (case-insensitive). */
|
|
19
|
+
readonly boolean: Coercer<boolean>
|
|
20
|
+
/** A bigint (`123`, `-9007199254740993`). */
|
|
21
|
+
readonly bigint: Coercer<bigint>
|
|
22
|
+
/**
|
|
23
|
+
* A `Date` from an ISO 8601 date (`2024-01-02`, UTC midnight), an ISO 8601 date-time with an
|
|
24
|
+
* offset (`2024-01-02T03:04:05Z`, `2024-01-02T03:04+02:00`) or a millisecond timestamp.
|
|
25
|
+
*/
|
|
26
|
+
readonly date: Coercer<Date>
|
|
27
|
+
/** One of `values` (string or number literals, compared after coercion). */
|
|
28
|
+
enum<const T extends readonly (string | number | boolean)[]>(value: unknown, label: string, values: T): T[number]
|
|
29
|
+
/** An array: a single value becomes a one-element array, each item is coerced with `item`. */
|
|
30
|
+
array<T>(value: unknown, label: string, item: Coercer<T>): T[]
|
|
31
|
+
/** `undefined` when the value is missing, otherwise `item(value, label)`. */
|
|
32
|
+
optional<T>(value: unknown, label: string, item: Coercer<T>): T | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// unambiguous (`\d+` and the fraction never compete for the same digits): linear time on any input
|
|
36
|
+
const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
|
|
37
|
+
const INTEGER = /^[+-]?\d+$/
|
|
38
|
+
/** ISO 8601 calendar date, optionally with a time and an offset (`Z`, `+02:00`, `+0200`). */
|
|
39
|
+
const ISO_DATE =
|
|
40
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(?:([Zz])|([+-])(\d{2}):?(\d{2}))?)?$/
|
|
41
|
+
|
|
42
|
+
function invalid(label: string, expected: string, value: unknown): BadRequestException {
|
|
43
|
+
// cookies often carry tokens: their values are never echoed
|
|
44
|
+
if (label.startsWith('cookie ')) return new BadRequestException(`Invalid ${label}: expected ${expected}`)
|
|
45
|
+
return new BadRequestException(`Invalid ${label}: expected ${expected}, received ${describe(value)}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function describe(value: unknown): string {
|
|
49
|
+
if (typeof value === 'string') return JSON.stringify(value.length > 64 ? `${value.slice(0, 64)}…` : value)
|
|
50
|
+
if (Array.isArray(value)) return `${value.length} values`
|
|
51
|
+
return typeof value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The single raw string of a value, or a 400 (missing / repeated / not a string). */
|
|
55
|
+
function single(value: unknown, label: string): string {
|
|
56
|
+
if (typeof value === 'string') return value
|
|
57
|
+
if (value === undefined || value === null) throw new BadRequestException(`Missing ${label}`)
|
|
58
|
+
if (Array.isArray(value))
|
|
59
|
+
throw new BadRequestException(`Invalid ${label}: expected a single value, received ${value.length}`)
|
|
60
|
+
throw invalid(label, 'a string', value)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toNumber(value: unknown, label: string): number {
|
|
64
|
+
const raw = single(value, label)
|
|
65
|
+
const text = raw.trim()
|
|
66
|
+
if (NUMBER.test(text)) {
|
|
67
|
+
const result = Number(text)
|
|
68
|
+
if (Number.isFinite(result)) return result
|
|
69
|
+
}
|
|
70
|
+
throw invalid(label, 'a number', raw)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toInteger(value: unknown, label: string): number {
|
|
74
|
+
const raw = single(value, label)
|
|
75
|
+
const text = raw.trim()
|
|
76
|
+
if (INTEGER.test(text)) {
|
|
77
|
+
const result = Number(text)
|
|
78
|
+
if (Number.isSafeInteger(result)) return result
|
|
79
|
+
}
|
|
80
|
+
throw invalid(label, 'an integer', raw)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toBoolean(value: unknown, label: string): boolean {
|
|
84
|
+
const raw = single(value, label)
|
|
85
|
+
switch (raw.trim().toLowerCase()) {
|
|
86
|
+
case 'true':
|
|
87
|
+
case '1':
|
|
88
|
+
return true
|
|
89
|
+
case 'false':
|
|
90
|
+
case '0':
|
|
91
|
+
return false
|
|
92
|
+
default:
|
|
93
|
+
throw invalid(label, 'a boolean (true, false, 1 or 0)', raw)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toBigint(value: unknown, label: string): bigint {
|
|
98
|
+
const raw = single(value, label)
|
|
99
|
+
const text = raw.trim()
|
|
100
|
+
if (INTEGER.test(text)) return BigInt(text)
|
|
101
|
+
throw invalid(label, 'an integer', raw)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toDate(value: unknown, label: string): Date {
|
|
105
|
+
const raw = single(value, label)
|
|
106
|
+
const text = raw.trim()
|
|
107
|
+
const date = INTEGER.test(text) ? new Date(Number(text)) : parseIsoDate(text)
|
|
108
|
+
if (date === undefined || Number.isNaN(date.getTime())) {
|
|
109
|
+
throw invalid(label, 'a date (ISO 8601 such as 2024-01-02 or 2024-01-02T10:00:00Z, or a timestamp)', raw)
|
|
110
|
+
}
|
|
111
|
+
return date
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parses an ISO 8601 date or date-time (see `Coercers.date`), `undefined` otherwise. Nothing else is
|
|
118
|
+
* handed to the engine's lenient `Date` parser (`foo 2024`, `March 7`), impossible dates
|
|
119
|
+
* (`2024-02-30`) are rejected instead of rolled over, and a date-time without offset is rejected
|
|
120
|
+
* rather than read in the server's time zone.
|
|
121
|
+
*/
|
|
122
|
+
function parseIsoDate(text: string): Date | undefined {
|
|
123
|
+
const match = ISO_DATE.exec(text)
|
|
124
|
+
if (match === null) return undefined
|
|
125
|
+
const [, y, mo, d, h, mi, s, fraction, zulu, sign, oh, om] = match
|
|
126
|
+
const year = Number(y)
|
|
127
|
+
const month = Number(mo)
|
|
128
|
+
const day = Number(d)
|
|
129
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
|
130
|
+
const days = month === 2 && leap ? 29 : DAYS_IN_MONTH[month - 1]
|
|
131
|
+
if (days === undefined || day < 1 || day > days) return undefined
|
|
132
|
+
const date = new Date(0)
|
|
133
|
+
date.setUTCFullYear(year, month - 1, day) // years 0-99 are not shifted to 1900-1999 (Date.UTC)
|
|
134
|
+
if (h === undefined) return date
|
|
135
|
+
if (zulu === undefined && sign === undefined) return undefined
|
|
136
|
+
const hours = Number(h)
|
|
137
|
+
const minutes = Number(mi)
|
|
138
|
+
const seconds = s === undefined ? 0 : Number(s)
|
|
139
|
+
const offset = sign === undefined ? 0 : (sign === '-' ? -1 : 1) * (Number(oh) * 60 + Number(om))
|
|
140
|
+
if (hours > 23 || minutes > 59 || seconds > 59 || Math.abs(offset) >= 24 * 60 || Number(om ?? 0) > 59) {
|
|
141
|
+
return undefined
|
|
142
|
+
}
|
|
143
|
+
const milliseconds = fraction === undefined ? 0 : Number(fraction.slice(0, 3).padEnd(3, '0'))
|
|
144
|
+
date.setUTCHours(hours, minutes - offset, seconds, milliseconds)
|
|
145
|
+
return date
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toEnum<const T extends readonly (string | number | boolean)[]>(
|
|
149
|
+
value: unknown,
|
|
150
|
+
label: string,
|
|
151
|
+
values: T,
|
|
152
|
+
): T[number] {
|
|
153
|
+
const raw = single(value, label)
|
|
154
|
+
for (const candidate of values) {
|
|
155
|
+
if (typeof candidate === 'string' ? candidate === raw : String(candidate) === raw.trim()) return candidate
|
|
156
|
+
}
|
|
157
|
+
throw invalid(label, `one of ${values.map(candidate => JSON.stringify(candidate)).join(', ')}`, raw)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toArray<T>(value: unknown, label: string, item: Coercer<T>): T[] {
|
|
161
|
+
if (value === undefined || value === null) throw new BadRequestException(`Missing ${label}`)
|
|
162
|
+
if (!Array.isArray(value)) return [item(value, label)]
|
|
163
|
+
const result = new Array<T>(value.length)
|
|
164
|
+
for (let index = 0; index < value.length; index++) result[index] = item(value[index], `${label}[${index}]`)
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toOptional<T>(value: unknown, label: string, item: Coercer<T>): T | undefined {
|
|
169
|
+
return value === undefined || value === null ? undefined : item(value, label)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Coercion helpers used by generated `invoke` functions for typed parameters without schema:
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* invoke: (c, ctx) => c.list(
|
|
177
|
+
* coerce.number(ctx.params['id'], 'param "id"'),
|
|
178
|
+
* coerce.optional(ctx.query['page'], 'query "page"', coerce.integer),
|
|
179
|
+
* coerce.array(ctx.query['tag'], 'query "tag"', coerce.string),
|
|
180
|
+
* )
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* Every failure is a `BadRequestException` (400) whose message names the input.
|
|
184
|
+
*/
|
|
185
|
+
export const coerce: Coercers = Object.freeze({
|
|
186
|
+
string: single,
|
|
187
|
+
number: toNumber,
|
|
188
|
+
integer: toInteger,
|
|
189
|
+
boolean: toBoolean,
|
|
190
|
+
bigint: toBigint,
|
|
191
|
+
date: toDate,
|
|
192
|
+
enum: toEnum,
|
|
193
|
+
array: toArray,
|
|
194
|
+
optional: toOptional,
|
|
195
|
+
})
|