@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
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Plugin routes (RFC 0002 H1): routes another plugin asks `HttpPlugin` to serve (documentation,
3
+ * health or metrics endpoints), registered on every adapter in the same pipeline as the compiled
4
+ * controllers.
5
+ */
6
+ import type { Container, Plugin } from '@znight/core'
7
+ import type { HttpContext } from './context.ts'
8
+ import type { RouteMetaEntry } from './decorators.ts'
9
+ import type { HttpMethod } from './metadata.ts'
10
+ import type { GuardRef } from './plugin.ts'
11
+
12
+ import { ErrorCode, ZnightError } from '@znight/core'
13
+
14
+ import { isGroupOf } from './decorators.ts'
15
+ import { joinPaths, normalizePath, parseRoutePath, type RoutePath, RoutePathError } from './path.ts'
16
+
17
+ /**
18
+ * A route served by `HttpPlugin` on behalf of another plugin. It runs through the same pipeline as
19
+ * controller routes: `http:request` hook, request scope, global guards, then its own `guards`, the
20
+ * handler, the exception mappers; global interceptors do not wrap it. The handler result is
21
+ * serialised like a controller result (`HttpResponse`, objects as JSON, strings, streams...).
22
+ */
23
+ export interface PluginRoute {
24
+ /** `GET` routes also answer `HEAD`; `ALL` registers every method without its own route. */
25
+ readonly method: HttpMethod
26
+ /** Path relative to the `HttpPlugin` prefix, with the controller grammar (`/openapi.json`, `/docs/_assets/*`). */
27
+ readonly path: string
28
+ /** Handler name, shown in `HttpPlugin.routes`, logs and errors (`document`). */
29
+ readonly name: string
30
+ readonly handler: (ctx: HttpContext) => unknown
31
+ /**
32
+ * Route metadata (`[Public.meta()]`): read by guards (`Public.get(ctx)`) and by the boot check of
33
+ * required route decorator groups. One entry per group.
34
+ */
35
+ readonly meta?: readonly RouteMetaEntry[]
36
+ /** Guards of the route (provider classes or instances), run after the global guards. */
37
+ readonly guards?: readonly GuardRef[]
38
+ }
39
+
40
+ /** What {@link PluginHttpRoutes.httpRoutes} receives. */
41
+ export interface PluginHttpRoutesContext {
42
+ /** The `HttpPlugin` prefix (`''` or `/api`), already applied to the paths of the routes. */
43
+ readonly prefix: string
44
+ /** The application container (initialised). */
45
+ readonly container: Container
46
+ }
47
+
48
+ /**
49
+ * Implemented by a plugin that serves HTTP routes. `HttpPlugin` calls `httpRoutes()` once in its
50
+ * `onBootstrap`, after `container.init()` and whatever the plugin order, then registers the routes
51
+ * on its adapter. A path that breaks the grammar, or that collides with another route, is ZNIGHT007
52
+ * naming the plugin.
53
+ *
54
+ * ```ts
55
+ * export class StatusPlugin implements Plugin, PluginHttpRoutes {
56
+ * readonly name = 'status'
57
+ * httpRoutes(): readonly PluginRoute[] {
58
+ * return [{ method: 'GET', path: '/status', name: 'status', handler: () => ({ up: true }), meta: [Public.meta()] }]
59
+ * }
60
+ * }
61
+ * ```
62
+ */
63
+ export interface PluginHttpRoutes {
64
+ httpRoutes(context: PluginHttpRoutesContext): readonly PluginRoute[]
65
+ }
66
+
67
+ /** A validated plugin route (internal). */
68
+ export interface CollectedPluginRoute {
69
+ readonly plugin: string
70
+ /** The plugin instance (the "controller" of the route). */
71
+ readonly instance: object
72
+ readonly method: HttpMethod
73
+ /** Full path, prefix included. */
74
+ readonly path: RoutePath
75
+ /** Normalised path without the prefix. */
76
+ readonly relativePath: string
77
+ readonly name: string
78
+ readonly handler: (ctx: HttpContext) => unknown
79
+ readonly meta: readonly NormalizedMetaEntry[]
80
+ readonly guards: readonly unknown[]
81
+ }
82
+
83
+ /** A plugin route metadata entry with its group (internal). */
84
+ export interface NormalizedMetaEntry {
85
+ readonly key: string
86
+ readonly value: unknown
87
+ readonly group: string
88
+ readonly required: boolean
89
+ }
90
+
91
+ const METHODS: ReadonlySet<string> = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'ALL'])
92
+
93
+ /** Calls `httpRoutes()` on every other plugin implementing it, in plugin order, and validates the routes. */
94
+ export function collectPluginRoutes(
95
+ plugins: readonly Plugin[],
96
+ self: Plugin,
97
+ context: PluginHttpRoutesContext,
98
+ ): CollectedPluginRoute[] {
99
+ const collected: CollectedPluginRoute[] = []
100
+ for (const plugin of plugins) {
101
+ if (plugin === self) continue
102
+ const provider = plugin as Plugin & Partial<PluginHttpRoutes>
103
+ if (typeof provider.httpRoutes !== 'function') continue
104
+ const name = String(plugin.name)
105
+ const routes: unknown = provider.httpRoutes(context)
106
+ if (!Array.isArray(routes)) {
107
+ throw invalid(name, `httpRoutes() must return an array of routes (received ${describe(routes)})`)
108
+ }
109
+ for (const route of routes as unknown[]) collected.push(validateRoute(plugin, name, route, context.prefix))
110
+ }
111
+ return collected
112
+ }
113
+
114
+ function validateRoute(instance: object, plugin: string, value: unknown, prefix: string): CollectedPluginRoute {
115
+ if (value === null || typeof value !== 'object') throw invalid(plugin, `invalid route ${describe(value)}`)
116
+ const route = value as Partial<PluginRoute>
117
+ const label = `${String(route.method)} ${String(route.path)}`
118
+ if (typeof route.name !== 'string' || route.name === '') {
119
+ throw invalid(plugin, `the route ${label} needs a name (handler name shown in HttpPlugin.routes)`)
120
+ }
121
+ const where = `${label} (${route.name})`
122
+ if (typeof route.method !== 'string' || !METHODS.has(route.method)) {
123
+ throw invalid(plugin, `unknown method ${describe(route.method)} of the route ${where}`)
124
+ }
125
+ if (typeof route.handler !== 'function') throw invalid(plugin, `the route ${where} has no handler function`)
126
+ if (typeof route.path !== 'string') throw invalid(plugin, `the route ${where} has no path`)
127
+ let path: RoutePath
128
+ try {
129
+ path = parseRoutePath(joinPaths(prefix, route.path))
130
+ } catch (error) {
131
+ if (!(error instanceof RoutePathError)) throw error
132
+ throw invalid(plugin, `invalid path of the route ${where}: ${error.issue.message}`, error)
133
+ }
134
+ const meta = route.meta ?? []
135
+ if (!Array.isArray(meta)) throw invalid(plugin, `the meta of the route ${where} must be an array of entries`)
136
+ const entries = (meta as unknown[]).map(entry => metaEntry(plugin, where, entry))
137
+ const groups = new Map<string, string>()
138
+ for (const entry of entries) {
139
+ const other = groups.get(entry.group)
140
+ if (other !== undefined) {
141
+ throw invalid(
142
+ plugin,
143
+ `the route ${where} has two entries of the "${entry.group}" group (${other}, ${entry.key}): keep one`,
144
+ )
145
+ }
146
+ groups.set(entry.group, entry.key)
147
+ }
148
+ const guards = route.guards ?? []
149
+ if (!Array.isArray(guards)) throw invalid(plugin, `the guards of the route ${where} must be an array`)
150
+ return {
151
+ plugin,
152
+ instance,
153
+ method: route.method as HttpMethod,
154
+ path,
155
+ relativePath: normalizePath(route.path),
156
+ name: route.name,
157
+ handler: route.handler,
158
+ meta: entries,
159
+ guards,
160
+ }
161
+ }
162
+
163
+ function metaEntry(plugin: string, where: string, value: unknown): NormalizedMetaEntry {
164
+ const entry = (value ?? {}) as Partial<RouteMetaEntry>
165
+ if (value === null || typeof value !== 'object' || typeof entry.key !== 'string' || entry.key === '') {
166
+ throw invalid(plugin, `invalid meta entry ${describe(value)} of the route ${where}: expected Decorator.meta(value)`)
167
+ }
168
+ const group = entry.group ?? entry.key
169
+ const required = entry.required ?? false
170
+ if (!isGroupOf(group, entry.key) || typeof required !== 'boolean') {
171
+ throw invalid(plugin, `invalid group of the meta entry "${entry.key}" of the route ${where}`)
172
+ }
173
+ return { key: entry.key, value: entry.value, group, required }
174
+ }
175
+
176
+ function invalid(plugin: string, reason: string, cause?: unknown): ZnightError {
177
+ return new ZnightError(ErrorCode.InvalidRoute, `HttpPlugin: plugin "${plugin}": ${reason}`, {
178
+ ...(cause === undefined ? {} : { cause }),
179
+ })
180
+ }
181
+
182
+ function describe(value: unknown): string {
183
+ try {
184
+ return typeof value === 'function' ? 'a function' : (JSON.stringify(value) ?? String(value))
185
+ } catch {
186
+ return typeof value
187
+ }
188
+ }