@znight/http 0.0.0-stage → 0.1.1

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2333 -2
  3. package/dist/adapter-CGSceVEN.d.mts +876 -0
  4. package/dist/compiler.d.mts +156 -0
  5. package/dist/compiler.mjs +2180 -0
  6. package/dist/compiler.mjs.map +1 -0
  7. package/dist/express.d.mts +39 -0
  8. package/dist/express.mjs +135 -0
  9. package/dist/express.mjs.map +1 -0
  10. package/dist/fastify.d.mts +33 -0
  11. package/dist/fastify.mjs +191 -0
  12. package/dist/fastify.mjs.map +1 -0
  13. package/dist/hono.d.mts +49 -0
  14. package/dist/hono.mjs +319 -0
  15. package/dist/hono.mjs.map +1 -0
  16. package/dist/index.d.mts +1254 -0
  17. package/dist/index.mjs +5618 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/node-BUnkdSsa.mjs +183 -0
  20. package/dist/node-BUnkdSsa.mjs.map +1 -0
  21. package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
  22. package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
  23. package/dist/node.d.mts +30 -0
  24. package/dist/node.mjs +1 -0
  25. package/dist/path-JI87C4k8.d.mts +109 -0
  26. package/dist/path.d.mts +2 -0
  27. package/dist/path.mjs +245 -0
  28. package/dist/path.mjs.map +1 -0
  29. package/dist/size-D8rsQcV0.mjs +184 -0
  30. package/dist/size-D8rsQcV0.mjs.map +1 -0
  31. package/dist/utils-DCbu3a4w.mjs +27 -0
  32. package/dist/utils-DCbu3a4w.mjs.map +1 -0
  33. package/package.json +142 -4
  34. package/src/adapter.ts +240 -0
  35. package/src/adapters/express.ts +201 -0
  36. package/src/adapters/fastify.ts +280 -0
  37. package/src/adapters/hono.ts +488 -0
  38. package/src/adapters/node-io.ts +368 -0
  39. package/src/adapters/node.ts +135 -0
  40. package/src/adapters/shared.ts +508 -0
  41. package/src/body.ts +135 -0
  42. package/src/coerce.ts +195 -0
  43. package/src/compiler/analyze.ts +1513 -0
  44. package/src/compiler/ast.ts +90 -0
  45. package/src/compiler/coercion.ts +267 -0
  46. package/src/compiler/generate.ts +374 -0
  47. package/src/compiler/index.ts +76 -0
  48. package/src/compiler/model.ts +260 -0
  49. package/src/compiler/route-decorators.ts +405 -0
  50. package/src/compiler/validate.ts +288 -0
  51. package/src/context.ts +533 -0
  52. package/src/cookie-settings.ts +68 -0
  53. package/src/cookie-signer.ts +106 -0
  54. package/src/cookies.ts +726 -0
  55. package/src/cors.ts +349 -0
  56. package/src/decisions.ts +331 -0
  57. package/src/decorators.ts +486 -0
  58. package/src/event-channel.ts +197 -0
  59. package/src/exceptions.ts +257 -0
  60. package/src/files.ts +429 -0
  61. package/src/headers.ts +39 -0
  62. package/src/index.ts +185 -0
  63. package/src/inject-sse.ts +309 -0
  64. package/src/interfaces.ts +101 -0
  65. package/src/metadata.ts +162 -0
  66. package/src/mime.ts +157 -0
  67. package/src/multipart.ts +597 -0
  68. package/src/option-error.ts +31 -0
  69. package/src/path.ts +288 -0
  70. package/src/pipeline.ts +918 -0
  71. package/src/plugin-routes.ts +188 -0
  72. package/src/plugin.ts +1432 -0
  73. package/src/proxy.ts +236 -0
  74. package/src/query.ts +30 -0
  75. package/src/response.ts +240 -0
  76. package/src/router.ts +119 -0
  77. package/src/security-headers.ts +161 -0
  78. package/src/serialize.ts +264 -0
  79. package/src/size.ts +66 -0
  80. package/src/sse-parse.ts +194 -0
  81. package/src/sse.ts +650 -0
  82. package/src/standard-schema.ts +81 -0
  83. package/src/status.ts +78 -0
  84. package/src/stream.ts +200 -0
  85. package/src/tokens.ts +21 -0
  86. package/src/uploads.ts +176 -0
  87. package/src/utils.ts +31 -0
  88. 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
+ })