@mpen/routekit 0.1.0 → 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 (133) hide show
  1. package/dist/bin.d.mts +4 -0
  2. package/dist/client/react.d.mts +178 -0
  3. package/dist/client/react.mjs +142 -0
  4. package/dist/client.d.mts +433 -0
  5. package/dist/client.mjs +264 -0
  6. package/dist/content-BuDOmhH_.mjs +102 -0
  7. package/dist/core-CzUCxvGk.d.mts +140 -0
  8. package/dist/core-DbmQauwS.mjs +81 -0
  9. package/dist/handlers.d.mts +72 -0
  10. package/dist/handlers.mjs +153 -0
  11. package/dist/index.d.mts +3 -0
  12. package/dist/index.mjs +1152 -0
  13. package/dist/middleware.d.mts +388 -0
  14. package/dist/middleware.mjs +1222 -0
  15. package/dist/request-Dn0zc-xm.mjs +1025 -0
  16. package/dist/response/content.d.mts +79 -0
  17. package/dist/response/content.mjs +2 -0
  18. package/dist/response/json-rpc.d.mts +1 -0
  19. package/dist/response/json-rpc.mjs +1 -0
  20. package/dist/response/problem/valibot.d.mts +230 -0
  21. package/dist/response/problem/valibot.mjs +258 -0
  22. package/dist/response/problem.d.mts +415 -0
  23. package/dist/response/problem.mjs +183 -0
  24. package/dist/response/status.d.mts +45 -0
  25. package/dist/response/status.mjs +2 -0
  26. package/dist/responses-B379Ep9Y.d.mts +296 -0
  27. package/dist/responses-BpVrgeYi.mjs +101 -0
  28. package/dist/router-Cwb7ak0J.d.mts +1819 -0
  29. package/dist/routes.d.mts +282 -0
  30. package/dist/routes.mjs +311 -0
  31. package/dist/status-C-8mw-FB.mjs +59 -0
  32. package/dist/valibot-D7liFYyB.d.mts +290 -0
  33. package/dist/valibot-Du97X-TS.mjs +326 -0
  34. package/package.json +8 -2
  35. package/src/bin/gen-api-client.test.ts +0 -70
  36. package/src/bin/gen-api-client.ts +0 -986
  37. package/src/client/headers.ts +0 -31
  38. package/src/client/index.ts +0 -8
  39. package/src/client/promise.ts +0 -11
  40. package/src/client/react/index.test.tsx +0 -266
  41. package/src/client/react/index.ts +0 -431
  42. package/src/client/responses.test.ts +0 -151
  43. package/src/client/responses.ts +0 -278
  44. package/src/client/transport.ts +0 -74
  45. package/src/client/transports/body-codec.ts +0 -61
  46. package/src/client/transports/fetch.ts +0 -113
  47. package/src/client/tsconfig.json +0 -9
  48. package/src/client/types.ts +0 -15
  49. package/src/client/url.ts +0 -31
  50. package/src/index.ts +0 -63
  51. package/src/router/fetch-types.ts +0 -13
  52. package/src/router/handlers/index.ts +0 -2
  53. package/src/router/handlers/openapi/index.ts +0 -2
  54. package/src/router/handlers/openapi/openapi.ts +0 -293
  55. package/src/router/integration/zod-openapi.test.ts +0 -74
  56. package/src/router/lib/charset.test.ts +0 -22
  57. package/src/router/lib/charset.ts +0 -133
  58. package/src/router/lib/collections.ts +0 -3
  59. package/src/router/lib/format.test.ts +0 -67
  60. package/src/router/lib/format.ts +0 -35
  61. package/src/router/lib/host.ts +0 -4
  62. package/src/router/lib/json-schema.ts +0 -6
  63. package/src/router/lib/media-type.test.ts +0 -122
  64. package/src/router/lib/media-type.ts +0 -289
  65. package/src/router/lib/pathname.test.ts +0 -18
  66. package/src/router/lib/pathname.ts +0 -19
  67. package/src/router/lib/route-names.ts +0 -70
  68. package/src/router/lib/route-normalize.test.ts +0 -36
  69. package/src/router/lib/route-normalize.ts +0 -67
  70. package/src/router/lib/schema-merge.ts +0 -56
  71. package/src/router/middleware/accept-ctx.test.ts +0 -33
  72. package/src/router/middleware/accept-ctx.ts +0 -12
  73. package/src/router/middleware/body-limit.test.ts +0 -112
  74. package/src/router/middleware/body-limit.ts +0 -121
  75. package/src/router/middleware/content-type-context.ts +0 -0
  76. package/src/router/middleware/cors.test.ts +0 -269
  77. package/src/router/middleware/cors.ts +0 -490
  78. package/src/router/middleware/csrf.test.ts +0 -106
  79. package/src/router/middleware/csrf.ts +0 -192
  80. package/src/router/middleware/define.ts +0 -249
  81. package/src/router/middleware/index.ts +0 -34
  82. package/src/router/middleware/jsxhtml-response.ts +0 -0
  83. package/src/router/middleware/oas-swagger.ts +0 -0
  84. package/src/router/middleware/rate-limit.test.ts +0 -886
  85. package/src/router/middleware/rate-limit.ts +0 -920
  86. package/src/router/middleware/request-id-ctx.test.ts +0 -183
  87. package/src/router/middleware/request-id-ctx.ts +0 -135
  88. package/src/router/middleware/request-logger-format.test.ts +0 -16
  89. package/src/router/middleware/request-logger-format.ts +0 -269
  90. package/src/router/middleware/request-logger.test.ts +0 -267
  91. package/src/router/middleware/request-logger.ts +0 -131
  92. package/src/router/middleware/start-time-ctx.ts +0 -5
  93. package/src/router/request.ts +0 -611
  94. package/src/router/response/core.ts +0 -181
  95. package/src/router/response/directives.ts +0 -233
  96. package/src/router/response/formats/content/bodyless.ts +0 -54
  97. package/src/router/response/formats/content/content.ts +0 -79
  98. package/src/router/response/formats/content/index.ts +0 -2
  99. package/src/router/response/formats/json-rpc/index.ts +0 -2
  100. package/src/router/response/formats/problem/badRequest.ts +0 -90
  101. package/src/router/response/formats/problem/conflict.ts +0 -90
  102. package/src/router/response/formats/problem/created.ts +0 -40
  103. package/src/router/response/formats/problem/index.ts +0 -27
  104. package/src/router/response/formats/problem/notFound.ts +0 -90
  105. package/src/router/response/formats/problem/permissionDenied.ts +0 -90
  106. package/src/router/response/formats/problem/problem.test.ts +0 -888
  107. package/src/router/response/formats/problem/rateLimited.ts +0 -90
  108. package/src/router/response/formats/problem/responses.ts +0 -219
  109. package/src/router/response/formats/problem/root-errors.ts +0 -48
  110. package/src/router/response/formats/problem/sessionExpired.ts +0 -90
  111. package/src/router/response/formats/problem/types.ts +0 -170
  112. package/src/router/response/formats/problem/unauthenticated.ts +0 -90
  113. package/src/router/response/formats/problem/valibot.ts +0 -410
  114. package/src/router/response/formats/status/index.ts +0 -1
  115. package/src/router/response/formats/status/responses.ts +0 -59
  116. package/src/router/response/formats/status/status.test.ts +0 -21
  117. package/src/router/response/framers.ts +0 -85
  118. package/src/router/response/index.ts +0 -28
  119. package/src/router/response/openapi.test.ts +0 -96
  120. package/src/router/response/openapi.ts +0 -1
  121. package/src/router/response/serializers.ts +0 -66
  122. package/src/router/response/stream.ts +0 -35
  123. package/src/router/router.test.ts +0 -1571
  124. package/src/router/router.ts +0 -1965
  125. package/src/router/routes/index.ts +0 -46
  126. package/src/router/routes/valibot/index.ts +0 -18
  127. package/src/router/routes/valibot/valibot.ts +0 -1393
  128. package/src/router/routes/valibot.test.ts +0 -286
  129. package/src/router/routes/zod/index.ts +0 -18
  130. package/src/router/routes/zod/zod.ts +0 -1318
  131. package/src/router/routes/zod.test.ts +0 -280
  132. package/src/router/server-interface.ts +0 -31
  133. package/src/router/types.ts +0 -657
@@ -1,267 +0,0 @@
1
- #!/usr/bin/env -S bun test
2
- import { describe, expect, it } from 'bun:test'
3
- import { HttpMethod, HttpStatus } from '@mpen/http'
4
- import { JsonLogger, MemoryLogger, TerminalLogger } from '@mpen/logger'
5
- import { Router } from '../router'
6
- import { response as routekitResponse } from '../response'
7
- import { requestIdCtx } from './request-id-ctx'
8
- import { requestLogger } from './request-logger'
9
- import {
10
- formatRoutekitTerminalLogRecord,
11
- transformRoutekitJsonLogRecord,
12
- } from './request-logger-format'
13
-
14
- describe(requestLogger.name, () => {
15
- it('logs correlated start and completion activity records for matched requests', async () => {
16
- const logger = new MemoryLogger()
17
- const router = new Router()
18
- .setLogger(logger)
19
- .useRequest(requestIdCtx({ generate: () => 'req.1' }))
20
- .useRequest(requestLogger({ trustedClientAddressHeader: 'x-forwarded-for' }))
21
- .get('/users/:id', () => routekitResponse({ ok: true }))
22
-
23
- const response = await router.fetch(
24
- new Request('https://example.com/users/1', {
25
- method: HttpMethod.GET,
26
- headers: {
27
- 'user-agent': 'routekit-test/1',
28
- 'x-forwarded-for': '203.0.113.9, 10.0.0.1',
29
- },
30
- }),
31
- )
32
-
33
- expect(response.status).toBe(HttpStatus.OK)
34
- expect(logger.logs).toHaveLength(2)
35
- expect(logger.logs[0]).toEqual({
36
- level: 'info',
37
- data: ['request started'],
38
- context: {
39
- 'client.address': '203.0.113.9',
40
- 'http.request.method': HttpMethod.GET,
41
- 'http.route': '/users/:id',
42
- 'request.id': 'req.1',
43
- 'url.path': '/users/1',
44
- 'user_agent.original': 'routekit-test/1',
45
- },
46
- metadata: { activity: { name: 'request', phase: 'start' } },
47
- })
48
- expect(logger.logs[1]?.level).toBe('info')
49
- expect(logger.logs[1]?.data).toEqual(['request completed'])
50
- expect(logger.logs[1]?.context).toMatchObject({
51
- 'event.name': 'http.server.request',
52
- 'http.request.method': HttpMethod.GET,
53
- 'http.response.body.size': 11,
54
- 'http.response.status_code': HttpStatus.OK,
55
- 'http.route': '/users/:id',
56
- 'request.id': 'req.1',
57
- 'url.path': '/users/1',
58
- 'user_agent.original': 'routekit-test/1',
59
- 'client.address': '203.0.113.9',
60
- })
61
- expect(logger.logs[1]?.metadata).toEqual({ activity: { name: 'request', phase: 'end' } })
62
- expect(typeof logger.logs[1]?.context.duration_ms).toBe('number')
63
- })
64
-
65
- it('covers generated responses and reports server failures at error severity', async () => {
66
- const logger = new MemoryLogger()
67
- const router = new Router()
68
- .setLogger(logger)
69
- .useRequest(requestIdCtx({ generate: () => 'req.generated' }))
70
- .useRequest(requestLogger())
71
- .get('/boom', () => {
72
- throw new Error('boom')
73
- })
74
-
75
- const missing = await router.fetch(new Request('https://example.com/missing'))
76
- const failed = await router.fetch(new Request('https://example.com/boom'))
77
-
78
- expect(missing.status).toBe(HttpStatus.NOT_FOUND)
79
- expect(failed.status).toBe(HttpStatus.INTERNAL_SERVER_ERROR)
80
-
81
- const completions = logger.logs.filter((record) => record.data[0] === 'request completed')
82
- expect(completions).toHaveLength(2)
83
- expect(completions[0]?.context['http.response.status_code']).toBe(HttpStatus.NOT_FOUND)
84
- expect(completions[0]?.level).toBe('info')
85
- expect(completions[1]?.context['http.response.status_code']).toBe(
86
- HttpStatus.INTERNAL_SERVER_ERROR,
87
- )
88
- expect(completions[1]?.level).toBe('error')
89
- })
90
-
91
- it('does not record forwarded client addresses without an explicitly trusted header', async () => {
92
- const logger = new MemoryLogger()
93
- const router = new Router()
94
- .setLogger(logger)
95
- .useRequest(requestIdCtx({ generate: () => 'req.ignored' }))
96
- .useRequest(requestLogger())
97
- .get('/users', () => new Response(null))
98
-
99
- await router.fetch(
100
- new Request('https://example.com/users', {
101
- headers: {
102
- 'x-forwarded-for': '203.0.113.9',
103
- },
104
- }),
105
- )
106
-
107
- expect(logger.logs[0]?.context['client.address']).toBeUndefined()
108
- expect(logger.logs[1]?.context['client.address']).toBeUndefined()
109
- })
110
-
111
- it('writes one named JSON access event without activity lifecycle details', async () => {
112
- const lines: string[] = []
113
- const logger = new JsonLogger({
114
- transformRecord: transformRoutekitJsonLogRecord,
115
- writeLine: (line) => lines.push(line),
116
- })
117
- const router = new Router()
118
- .setLogger(logger)
119
- .useRequest(requestIdCtx({ generate: () => 'req.json' }))
120
- .useRequest(requestLogger())
121
- .get('/users', () => routekitResponse({ ok: true }))
122
-
123
- await router.fetch(new Request('https://example.com/users'))
124
-
125
- expect(lines).toHaveLength(1)
126
- const payload = JSON.parse(lines[0]!)
127
- expect(payload['event.name']).toBe('http.server.request')
128
- expect(payload.message).toBeUndefined()
129
- expect(payload['activity.name']).toBeUndefined()
130
- expect(payload['activity.phase']).toBeUndefined()
131
- expect(payload['request.id']).toBe('req.json')
132
- expect(payload['http.response.status_code']).toBe(HttpStatus.OK)
133
- })
134
- })
135
-
136
- describe(formatRoutekitTerminalLogRecord.name, () => {
137
- it('renders compact contextual activity start and completion output', () => {
138
- const lines: string[] = []
139
- const logger = new TerminalLogger({
140
- color: false,
141
- formatRecord: formatRoutekitTerminalLogRecord,
142
- maxWidth: 200,
143
- write: (line) => lines.push(line),
144
- }).withContext({ 'service.name': 'api' })
145
- const activity = logger.startActivity('request', {
146
- 'request.id': 'req.1',
147
- 'http.request.method': 'GET',
148
- 'url.path': '/users/1',
149
- hidden: 'not-rendered',
150
- })
151
-
152
- activity.end({
153
- context: { 'http.response.body.size': 1536, 'http.response.status_code': 200 },
154
- })
155
-
156
- const output = lines.join('')
157
-
158
- expect(output).toContain('[api] [req.1] \u2192 GET /users/1')
159
- expect(output).toContain('[api] [req.1] \u2190 GET /users/1 200')
160
- expect(output).toContain('ms 1.50 KiB')
161
- expect(output).not.toContain('request started')
162
- expect(output).not.toContain('request completed')
163
- expect(output).not.toContain('not-rendered')
164
- expect(
165
- output
166
- .trimEnd()
167
- .split('\n')
168
- .every((line) => !line.endsWith(' ')),
169
- ).toBe(true)
170
- })
171
-
172
- it('colors request correlations, completion status, and slow duration indicators', () => {
173
- const lines: string[] = []
174
- const logger = new TerminalLogger({
175
- color: true,
176
- formatRecord: formatRoutekitTerminalLogRecord,
177
- maxWidth: 200,
178
- write: (line) => lines.push(line),
179
- }).withContext({ 'service.name': 'api' })
180
- const fast = logger.startActivity('request', {
181
- 'request.id': '1',
182
- 'http.request.method': 'GET',
183
- 'url.path': '/fast',
184
- })
185
- const slow = logger.startActivity('request', {
186
- 'request.id': '2',
187
- 'http.request.method': 'POST',
188
- 'url.path': '/slow',
189
- })
190
-
191
- fast.end({ context: { 'http.response.status_code': 204, duration_ms: 1 } })
192
- slow.end({ context: { 'http.response.status_code': 503, duration_ms: 1200 } })
193
- logger
194
- .withContext({
195
- 'request.id': '3',
196
- 'http.request.method': 'GET',
197
- 'url.path': '/manual-slow',
198
- duration_ms: 1200,
199
- })
200
- .info('slow checkpoint')
201
- logger
202
- .withContext({
203
- 'request.id': '4',
204
- 'http.request.method': 'GET',
205
- 'url.path': '/manual-medium',
206
- duration_ms: 300,
207
- })
208
- .info('medium checkpoint')
209
-
210
- const output = lines.join('')
211
-
212
- expect(output).toContain('\x1B[1m\x1B[94m[api]\x1B[39m\x1B[22m')
213
- expect(output).toContain('\x1B[1m\x1B[97mGET\x1B[39m\x1B[22m')
214
- expect(output).toContain('\x1B[1m\x1B[92m204\x1B[39m\x1B[22m')
215
- expect(output).toContain('\x1B[1m\x1B[91m503\x1B[39m\x1B[22m')
216
- expect(output).toContain('\x1B[91m1200ms\x1B[39m')
217
- expect(output).toContain('\x1B[93m300ms\x1B[39m')
218
- expect(output.match(/\x1B\[(?:9[1-6]|3[5-6])m\[[1234]\]/gu)).toHaveLength(6)
219
- })
220
-
221
- it('formats durations with precision based on elapsed time', () => {
222
- const lines: string[] = []
223
- const logger = new TerminalLogger({
224
- color: false,
225
- formatRecord: formatRoutekitTerminalLogRecord,
226
- maxWidth: 200,
227
- write: (line) => lines.push(line),
228
- }).withContext({
229
- 'request.id': 'req.1',
230
- 'http.request.method': 'GET',
231
- 'url.path': '/timing',
232
- })
233
-
234
- logger.withContext({ duration_ms: 0.493 }).info('sub-ms')
235
- logger.withContext({ duration_ms: 1.088 }).info('single-ms')
236
- logger.withContext({ duration_ms: 16.726 }).info('double-ms')
237
- logger.withContext({ duration_ms: 100 }).info('hundred-ms')
238
- logger.withContext({ duration_ms: 1200.4 }).info('slow-ms')
239
-
240
- const output = lines.join('')
241
-
242
- expect(output).toContain('0.49ms')
243
- expect(output).toContain('1.1ms')
244
- expect(output).toContain('16.7ms')
245
- expect(output).toContain('100ms')
246
- expect(output).toContain('1200ms')
247
- })
248
-
249
- it('keeps ordinary request-context messages as messages', () => {
250
- const lines: string[] = []
251
- const logger = new TerminalLogger({
252
- color: false,
253
- formatRecord: formatRoutekitTerminalLogRecord,
254
- maxWidth: 200,
255
- write: (line) => lines.push(line),
256
- }).withContext({
257
- 'request.id': 'req.1',
258
- 'http.request.method': 'POST',
259
- 'url.path': '/todos',
260
- })
261
-
262
- logger.info('request started')
263
-
264
- expect(lines.join('')).toContain('[req.1] POST /todos request started')
265
- expect(lines.join('')).not.toContain('\u2192')
266
- })
267
- })
@@ -1,131 +0,0 @@
1
- import { LogLevel } from '@mpen/logger'
2
- import type { AnyContext, RequestMiddleware } from '../types'
3
- import { ROUTEKIT_HTTP_SERVER_REQUEST_EVENT_NAME } from './request-logger-format'
4
-
5
- /**
6
- * Options for [`requestLogger`]{@link requestLogger}.
7
- *
8
- * @example
9
- * ```ts
10
- * const options: RequestLoggerOptions = {
11
- * completionLevel: response => response.status >= 500 ? LogLevel.ERROR : LogLevel.INFO,
12
- * }
13
- * ```
14
- */
15
- export interface RequestLoggerOptions {
16
- /**
17
- * Activity label used for start and completion records.
18
- * @defaultValue `'request'`
19
- */
20
- activityName?: string
21
- /**
22
- * Completion severity or a function selecting severity from the final response.
23
- */
24
- completionLevel?: LogLevel | ((response: Response) => LogLevel)
25
- /**
26
- * Header containing a trusted proxy-provided original client address.
27
- *
28
- * Only configure this when direct clients cannot provide or alter this header. For
29
- * `x-forwarded-for`, the first comma-separated address is recorded as `client.address`.
30
- *
31
- * @example
32
- * ```ts
33
- * requestLogger({ trustedClientAddressHeader: 'x-forwarded-for' })
34
- * ```
35
- */
36
- trustedClientAddressHeader?: string
37
- }
38
-
39
- function defaultCompletionLevel(response: Response): LogLevel {
40
- return response.status >= 500 ? LogLevel.ERROR : LogLevel.INFO
41
- }
42
-
43
- const HTTP_SERVER_REQUEST_EVENT = {
44
- 'event.name': ROUTEKIT_HTTP_SERVER_REQUEST_EVENT_NAME,
45
- } as const
46
-
47
- function getResponseBodySize(response: Response): number | undefined {
48
- if (response.body == null) {
49
- return 0
50
- }
51
-
52
- const value = response.headers.get('content-length')
53
- if (value == null || !/^\d+$/u.test(value)) {
54
- return undefined
55
- }
56
-
57
- const size = Number(value)
58
- return Number.isSafeInteger(size) ? size : undefined
59
- }
60
-
61
- function getClientAddress(
62
- request: { headers: Headers },
63
- trustedHeader: string | undefined,
64
- ): string | undefined {
65
- if (trustedHeader == null) {
66
- return undefined
67
- }
68
-
69
- const value = request.headers.get(trustedHeader)?.trim()
70
- if (!value) {
71
- return undefined
72
- }
73
-
74
- return trustedHeader.toLowerCase() === 'x-forwarded-for'
75
- ? value.split(',', 1)[0]?.trim()
76
- : value
77
- }
78
-
79
- /**
80
- * Log a timed request activity through the request's contextual logger.
81
- *
82
- * @example
83
- * ```ts
84
- * router.useRequest(requestIdCtx())
85
- * router.useRequest(requestLogger())
86
- * ```
87
- *
88
- * @param options - Request activity naming and severity options.
89
- * @returns Request-boundary middleware that logs final status and duration.
90
- * @typeParam Ctx - Context containing the correlated request identifier.
91
- */
92
- export function requestLogger<
93
- Ctx extends { requestId: string } & object = AnyContext & { requestId: string },
94
- >(options: RequestLoggerOptions = {}): RequestMiddleware<{}, Ctx> {
95
- const activityName = options.activityName ?? 'request'
96
-
97
- return async (ctx, next) => {
98
- const clientAddress = getClientAddress(ctx.request, options.trustedClientAddressHeader)
99
- const userAgent = ctx.request.headers.get('user-agent') ?? undefined
100
- const activity = ctx.logger.startActivity(activityName, {
101
- ...(clientAddress == null ? {} : { 'client.address': clientAddress }),
102
- ...(userAgent == null ? {} : { 'user_agent.original': userAgent }),
103
- })
104
- ctx.logger = activity.logger
105
-
106
- try {
107
- const response = await next()
108
- const bodySize = getResponseBodySize(response)
109
- const level =
110
- typeof options.completionLevel === 'function'
111
- ? options.completionLevel(response)
112
- : (options.completionLevel ?? defaultCompletionLevel(response))
113
- activity.end({
114
- context: {
115
- ...HTTP_SERVER_REQUEST_EVENT,
116
- 'http.response.status_code': response.status,
117
- ...(bodySize == null ? {} : { 'http.response.body.size': bodySize }),
118
- },
119
- level,
120
- })
121
- return response
122
- } catch (error) {
123
- activity.end({
124
- context: HTTP_SERVER_REQUEST_EVENT,
125
- data: [error],
126
- level: LogLevel.ERROR,
127
- })
128
- throw error
129
- }
130
- }
131
- }
@@ -1,5 +0,0 @@
1
- import type { ContextMiddleware } from '../types'
2
-
3
- export const startTimeCtx = (): ContextMiddleware<{ startTime: number }> => (ctx) => {
4
- ctx.startTime = Date.now()
5
- }