@pikku/core 0.12.4 → 0.12.6

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 (235) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/dev/hot-reload.d.ts +10 -0
  3. package/dist/dev/hot-reload.js +158 -0
  4. package/dist/middleware-runner.d.ts +1 -0
  5. package/dist/middleware-runner.js +5 -0
  6. package/dist/permissions.d.ts +1 -0
  7. package/dist/permissions.js +5 -0
  8. package/dist/services/content-service.d.ts +2 -0
  9. package/dist/services/in-memory-workflow-service.d.ts +1 -0
  10. package/dist/services/in-memory-workflow-service.js +16 -11
  11. package/dist/services/workflow-service.d.ts +1 -0
  12. package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
  13. package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
  14. package/dist/wirings/channel/channel-middleware-runner.js +5 -0
  15. package/dist/wirings/http/http-runner.js +1 -1
  16. package/dist/wirings/workflow/pikku-workflow-service.d.ts +6 -0
  17. package/dist/wirings/workflow/pikku-workflow-service.js +51 -16
  18. package/dist/wirings/workflow/workflow.types.d.ts +2 -0
  19. package/package.json +5 -3
  20. package/src/crypto-utils.test.ts +214 -0
  21. package/src/crypto-utils.ts +213 -0
  22. package/src/dev/hot-reload.test.ts +484 -0
  23. package/src/dev/hot-reload.ts +212 -0
  24. package/src/errors/error-handler.ts +62 -0
  25. package/src/errors/error.test.ts +195 -0
  26. package/src/errors/errors.ts +374 -0
  27. package/src/errors/index.ts +2 -0
  28. package/src/factory-functions.test.ts +109 -0
  29. package/src/function/function-runner.test.ts +536 -0
  30. package/src/function/function-runner.ts +365 -0
  31. package/src/function/functions.types.ts +304 -0
  32. package/src/function/index.ts +5 -0
  33. package/src/handle-error.test.ts +424 -0
  34. package/src/handle-error.ts +69 -0
  35. package/src/index.ts +118 -0
  36. package/src/internal.ts +6 -0
  37. package/src/middleware/auth-apikey.test.ts +363 -0
  38. package/src/middleware/auth-apikey.ts +47 -0
  39. package/src/middleware/auth-bearer.test.ts +450 -0
  40. package/src/middleware/auth-bearer.ts +72 -0
  41. package/src/middleware/auth-cookie.test.ts +528 -0
  42. package/src/middleware/auth-cookie.ts +80 -0
  43. package/src/middleware/cors.test.ts +424 -0
  44. package/src/middleware/cors.ts +104 -0
  45. package/src/middleware/index.ts +5 -0
  46. package/src/middleware/remote-auth.test.ts +488 -0
  47. package/src/middleware/remote-auth.ts +68 -0
  48. package/src/middleware/timeout.ts +15 -0
  49. package/src/middleware-runner.test.ts +418 -0
  50. package/src/middleware-runner.ts +240 -0
  51. package/src/permissions.test.ts +434 -0
  52. package/src/permissions.ts +327 -0
  53. package/src/pikku-request.ts +23 -0
  54. package/src/pikku-response.ts +5 -0
  55. package/src/pikku-state.test.ts +224 -0
  56. package/src/pikku-state.ts +216 -0
  57. package/src/run-tests-script.test.ts +49 -0
  58. package/src/schema.test.ts +249 -0
  59. package/src/schema.ts +151 -0
  60. package/src/services/ai-agent-runner-service.ts +41 -0
  61. package/src/services/ai-run-state-service.ts +20 -0
  62. package/src/services/ai-storage-service.ts +39 -0
  63. package/src/services/content-service.ts +81 -0
  64. package/src/services/deployment-service.ts +22 -0
  65. package/src/services/gateway-service.ts +20 -0
  66. package/src/services/gopass-secrets.ts +98 -0
  67. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  68. package/src/services/in-memory-trigger-service.ts +64 -0
  69. package/src/services/in-memory-workflow-service.test.ts +351 -0
  70. package/src/services/in-memory-workflow-service.ts +512 -0
  71. package/src/services/index.ts +56 -0
  72. package/src/services/jwt-service.ts +30 -0
  73. package/src/services/local-content.ts +120 -0
  74. package/src/services/local-gateway-service.ts +62 -0
  75. package/src/services/local-secrets.test.ts +80 -0
  76. package/src/services/local-secrets.ts +94 -0
  77. package/src/services/local-variables.test.ts +61 -0
  78. package/src/services/local-variables.ts +39 -0
  79. package/src/services/logger-console.test.ts +118 -0
  80. package/src/services/logger-console.ts +98 -0
  81. package/src/services/logger.ts +57 -0
  82. package/src/services/scheduler-service.ts +86 -0
  83. package/src/services/schema-service.ts +33 -0
  84. package/src/services/scoped-secret-service.test.ts +74 -0
  85. package/src/services/scoped-secret-service.ts +41 -0
  86. package/src/services/secret-service.ts +38 -0
  87. package/src/services/trigger-service.ts +17 -0
  88. package/src/services/typed-secret-service.test.ts +93 -0
  89. package/src/services/typed-secret-service.ts +70 -0
  90. package/src/services/typed-variables-service.test.ts +73 -0
  91. package/src/services/typed-variables-service.ts +81 -0
  92. package/src/services/user-session-service.test.ts +113 -0
  93. package/src/services/user-session-service.ts +86 -0
  94. package/src/services/variables-service.ts +11 -0
  95. package/src/services/workflow-service.ts +96 -0
  96. package/src/testing/index.ts +2 -0
  97. package/src/testing/service-tests.ts +873 -0
  98. package/src/time-utils.test.ts +56 -0
  99. package/src/time-utils.ts +107 -0
  100. package/src/types/core.types.ts +478 -0
  101. package/src/types/state.types.ts +188 -0
  102. package/src/utils/hash.test.ts +68 -0
  103. package/src/utils/hash.ts +26 -0
  104. package/src/utils.test.ts +350 -0
  105. package/src/utils.ts +129 -0
  106. package/src/version.test.ts +80 -0
  107. package/src/version.ts +25 -0
  108. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  109. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  110. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  111. package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
  112. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  113. package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
  114. package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
  115. package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
  116. package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
  117. package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
  118. package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
  119. package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
  120. package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
  121. package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
  122. package/src/wirings/ai-agent/index.ts +37 -0
  123. package/src/wirings/channel/channel-common.ts +90 -0
  124. package/src/wirings/channel/channel-handler.ts +250 -0
  125. package/src/wirings/channel/channel-middleware-runner.ts +126 -0
  126. package/src/wirings/channel/channel-runner.ts +166 -0
  127. package/src/wirings/channel/channel-store.ts +29 -0
  128. package/src/wirings/channel/channel.types.ts +172 -0
  129. package/src/wirings/channel/define-channel-routes.ts +25 -0
  130. package/src/wirings/channel/eventhub-service.ts +34 -0
  131. package/src/wirings/channel/eventhub-store.ts +13 -0
  132. package/src/wirings/channel/index.ts +24 -0
  133. package/src/wirings/channel/local/index.ts +3 -0
  134. package/src/wirings/channel/local/local-channel-handler.ts +81 -0
  135. package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
  136. package/src/wirings/channel/local/local-channel-runner.ts +210 -0
  137. package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
  138. package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
  139. package/src/wirings/channel/log-channels.ts +20 -0
  140. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
  141. package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
  142. package/src/wirings/channel/serverless/index.ts +5 -0
  143. package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
  144. package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
  145. package/src/wirings/cli/channel/index.ts +1 -0
  146. package/src/wirings/cli/cli-runner.test.ts +403 -0
  147. package/src/wirings/cli/cli-runner.ts +529 -0
  148. package/src/wirings/cli/cli.types.ts +358 -0
  149. package/src/wirings/cli/command-parser.test.ts +445 -0
  150. package/src/wirings/cli/command-parser.ts +504 -0
  151. package/src/wirings/cli/define-cli-commands.ts +24 -0
  152. package/src/wirings/cli/index.ts +19 -0
  153. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  154. package/src/wirings/gateway/gateway-runner.ts +411 -0
  155. package/src/wirings/gateway/gateway.types.ts +149 -0
  156. package/src/wirings/gateway/index.ts +13 -0
  157. package/src/wirings/http/http-routes.test.ts +322 -0
  158. package/src/wirings/http/http-routes.ts +197 -0
  159. package/src/wirings/http/http-runner.test.ts +144 -0
  160. package/src/wirings/http/http-runner.ts +588 -0
  161. package/src/wirings/http/http.types.ts +369 -0
  162. package/src/wirings/http/index.ts +28 -0
  163. package/src/wirings/http/log-http-routes.ts +22 -0
  164. package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
  165. package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
  166. package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
  167. package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
  168. package/src/wirings/http/routers/http-router.ts +13 -0
  169. package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
  170. package/src/wirings/http/routers/path-to-regex.ts +126 -0
  171. package/src/wirings/http/web-request.test.ts +236 -0
  172. package/src/wirings/http/web-request.ts +104 -0
  173. package/src/wirings/mcp/index.ts +27 -0
  174. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
  175. package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
  176. package/src/wirings/mcp/mcp-runner.ts +323 -0
  177. package/src/wirings/mcp/mcp.types.ts +216 -0
  178. package/src/wirings/node/index.ts +2 -0
  179. package/src/wirings/node/node.types.ts +23 -0
  180. package/src/wirings/oauth2/index.ts +3 -0
  181. package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
  182. package/src/wirings/oauth2/oauth2-client.ts +335 -0
  183. package/src/wirings/oauth2/oauth2.types.ts +69 -0
  184. package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
  185. package/src/wirings/queue/index.ts +31 -0
  186. package/src/wirings/queue/queue-runner.test.ts +710 -0
  187. package/src/wirings/queue/queue-runner.ts +192 -0
  188. package/src/wirings/queue/queue.types.ts +189 -0
  189. package/src/wirings/queue/register-queue-helper.ts +60 -0
  190. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  191. package/src/wirings/queue/validate-worker-config.ts +116 -0
  192. package/src/wirings/rpc/index.ts +4 -0
  193. package/src/wirings/rpc/rpc-runner.ts +460 -0
  194. package/src/wirings/rpc/rpc-types.ts +56 -0
  195. package/src/wirings/rpc/wire-addon.ts +21 -0
  196. package/src/wirings/scheduler/index.ts +11 -0
  197. package/src/wirings/scheduler/log-schedulers.ts +20 -0
  198. package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
  199. package/src/wirings/scheduler/scheduler-runner.ts +133 -0
  200. package/src/wirings/scheduler/scheduler.types.ts +53 -0
  201. package/src/wirings/secret/index.ts +9 -0
  202. package/src/wirings/secret/secret.types.ts +32 -0
  203. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  204. package/src/wirings/secret/validate-secret-definitions.ts +82 -0
  205. package/src/wirings/trigger/index.ts +10 -0
  206. package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
  207. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  208. package/src/wirings/trigger/trigger-runner.ts +135 -0
  209. package/src/wirings/trigger/trigger.types.ts +178 -0
  210. package/src/wirings/variable/index.ts +8 -0
  211. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  212. package/src/wirings/variable/validate-variable-definitions.ts +69 -0
  213. package/src/wirings/variable/variable.types.ts +22 -0
  214. package/src/wirings/workflow/dsl/index.ts +31 -0
  215. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
  216. package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
  217. package/src/wirings/workflow/graph/graph-node.ts +222 -0
  218. package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
  219. package/src/wirings/workflow/graph/graph-runner.ts +816 -0
  220. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  221. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  222. package/src/wirings/workflow/graph/index.ts +19 -0
  223. package/src/wirings/workflow/graph/template.test.ts +49 -0
  224. package/src/wirings/workflow/graph/template.ts +42 -0
  225. package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
  226. package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
  227. package/src/wirings/workflow/index.ts +82 -0
  228. package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
  229. package/src/wirings/workflow/pikku-workflow-service.ts +1229 -0
  230. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  231. package/src/wirings/workflow/workflow-helpers.ts +79 -0
  232. package/src/wirings/workflow/workflow.types.ts +276 -0
  233. package/tsconfig.json +14 -0
  234. package/tsconfig.tsbuildinfo +1 -0
  235. package/lcov.info +0 -18891
@@ -0,0 +1,170 @@
1
+ import { parse as parseQuery } from 'picoquery'
2
+ import { parse as parseCookie } from 'cookie'
3
+ import type { HTTPMethod, PikkuHTTPRequest, PikkuQuery } from './http.types.js'
4
+ import { UnprocessableContentError } from '../../errors/errors.js'
5
+
6
+ /**
7
+ * Abstract class representing a pikku request.
8
+ * @template In - The type of the request body.
9
+ * @group RequestResponse
10
+ */
11
+ export class PikkuFetchHTTPRequest<In = unknown>
12
+ implements PikkuHTTPRequest<In>
13
+ {
14
+ #cookies: Partial<Record<string, string>> | undefined
15
+ #params: Partial<Record<string, string | string[]>> = {}
16
+ #url: URL
17
+
18
+ constructor(private request: Request) {
19
+ this.#url = new URL(request.url)
20
+ }
21
+
22
+ public method(): HTTPMethod {
23
+ return this.request.method.toLowerCase() as HTTPMethod
24
+ }
25
+
26
+ public path(): string {
27
+ return this.#url.pathname
28
+ }
29
+
30
+ /**
31
+ * Retrieves the request body.
32
+ * @returns A promise that resolves to the request body.
33
+ */
34
+ public json(): Promise<In> {
35
+ return this.request.json()
36
+ }
37
+
38
+ /**
39
+ * Retrieves the raw request body as a Buffer.
40
+ * @returns A promise that resolves to the raw request body.
41
+ */
42
+ public arrayBuffer(): Promise<ArrayBuffer> {
43
+ return this.request.arrayBuffer()
44
+ }
45
+
46
+ public headers(): Record<string, string> {
47
+ return Object.fromEntries(this.request.headers.entries())
48
+ }
49
+
50
+ /**
51
+ * Retrieves the value of a specific header.
52
+ * @param headerName - The name of the header to retrieve.
53
+ * @returns The value of the header, or undefined if the header is not found.
54
+ */
55
+ public header(headerName: string): string | null {
56
+ return this.request.headers.get(headerName.toLowerCase())
57
+ }
58
+
59
+ /**
60
+ * Retrieves the cookies from the request.
61
+ * @returns An object containing the cookies.
62
+ */
63
+ public cookie(cookieName: string): string | null {
64
+ if (this.#cookies?.[cookieName]) {
65
+ return this.#cookies[cookieName]
66
+ }
67
+ const cookieHeader = this.header('cookie')
68
+ this.#cookies = cookieHeader ? parseCookie(cookieHeader) : {}
69
+ return this.#cookies[cookieName] || null
70
+ }
71
+
72
+ /**
73
+ * Retrieves the request parameters.
74
+ * @returns An object containing the request parameters.
75
+ */
76
+ public params(): Partial<Record<string, string | string[]>> {
77
+ return this.#params
78
+ }
79
+
80
+ /**
81
+ * Sets the request parameters.
82
+ * @param params - An object containing the request parameters to set.
83
+ */
84
+ public setParams(
85
+ params: Record<string, string | string[] | undefined>
86
+ ): void {
87
+ this.#params = params
88
+ }
89
+
90
+ /**
91
+ * Retrieves the query parameters from the request.
92
+ * @returns An object containing the query parameters.
93
+ */
94
+ public query(): PikkuQuery {
95
+ return parseQuery(this.#url.searchParams.toString()) as PikkuQuery
96
+ }
97
+
98
+ /**
99
+ * Retrieves the combined data from the request, including parameters, query, and body.
100
+ * @returns A promise that resolves to an object containing the combined data.
101
+ */
102
+ public async data(): Promise<In> {
103
+ const body = await this.body()
104
+ const parts = [this.params(), this.query(), body]
105
+ const merged: Record<string, unknown> = {}
106
+ for (const part of parts) {
107
+ for (const [key, value] of Object.entries(part)) {
108
+ if (key in merged && !valuesAreEquivalent(merged[key], value)) {
109
+ throw new UnprocessableContentError(
110
+ `Conflicting values for key "${key}": "${merged[key]}" vs "${value}"`
111
+ )
112
+ }
113
+ merged[key] ??= value
114
+ }
115
+ }
116
+ return merged as In
117
+ }
118
+
119
+ private async body(): Promise<any> {
120
+ const noBodyMethods: HTTPMethod[] = ['get', 'head', 'options', 'delete']
121
+ if (noBodyMethods.includes(this.method())) {
122
+ return {}
123
+ }
124
+
125
+ let body: any = {}
126
+ const contentType = this.header('content-type') || ''
127
+ try {
128
+ if (contentType.includes('application/json')) {
129
+ const parsed = await this.json()
130
+ body =
131
+ typeof parsed === 'object' &&
132
+ parsed !== null &&
133
+ !Array.isArray(parsed)
134
+ ? parsed
135
+ : { data: parsed }
136
+ } else if (contentType.includes('text/')) {
137
+ const text = await this.request.text()
138
+ body = { data: text }
139
+ } else if (contentType.includes('application/octet-stream')) {
140
+ const buffer = await this.request.arrayBuffer()
141
+ body = { data: buffer }
142
+ } else if (contentType === 'application/x-www-form-urlencoded') {
143
+ const text = await this.request.text()
144
+ body = Object.fromEntries(new URLSearchParams(text))
145
+ } else {
146
+ throw new UnprocessableContentError(
147
+ `Unsupported content type ${contentType}`
148
+ )
149
+ }
150
+ } catch (e) {
151
+ throw new UnprocessableContentError(`Error parsing body: ${e}`)
152
+ }
153
+ return body
154
+ }
155
+ }
156
+
157
+ function valuesAreEquivalent(a: unknown, b: unknown): boolean {
158
+ return coerce(a) === coerce(b)
159
+ }
160
+
161
+ function coerce(value: unknown): string | number | boolean {
162
+ if (typeof value === 'boolean' || typeof value === 'number') return value
163
+ if (typeof value === 'string') {
164
+ if (value === 'true') return true
165
+ if (value === 'false') return false
166
+ const num = Number(value)
167
+ return isNaN(num) ? value : num
168
+ }
169
+ return value as any
170
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'assert'
3
+ import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js' // Adjust path if needed
4
+
5
+ describe('PikkuFetchHTTPResponse', () => {
6
+ test('sets status code', () => {
7
+ const res = new PikkuFetchHTTPResponse().status(404).toResponse()
8
+ assert.strictEqual(res.status, 404)
9
+ })
10
+
11
+ test('sets headers', async () => {
12
+ const res = new PikkuFetchHTTPResponse()
13
+ .header('X-Test', 'value')
14
+ .header('X-Multi', ['a', 'b'])
15
+ .toResponse()
16
+
17
+ assert.strictEqual(res.headers.get('X-Test'), 'value')
18
+ const multi = res.headers.get('X-Multi')?.split(', ')
19
+ assert.deepEqual(multi, ['a', 'b'])
20
+ })
21
+
22
+ test('sets cookies', () => {
23
+ const res = new PikkuFetchHTTPResponse()
24
+ .cookie('token', 'abc123', { httpOnly: true, path: '/' })
25
+ .cookie('other', 'val', { maxAge: 3600 })
26
+ .toResponse()
27
+
28
+ const setCookieHeader = res.headers.get('Set-Cookie')
29
+ assert.ok(setCookieHeader)
30
+
31
+ const cookies = setCookieHeader!.split(', ')
32
+ assert.ok(cookies.some((c) => c.startsWith('token=abc123')))
33
+ assert.ok(cookies.some((c) => c.startsWith('other=val')))
34
+ })
35
+
36
+ test('json sets correct content type and body', async () => {
37
+ const res = new PikkuFetchHTTPResponse()
38
+ .json({ hello: 'world' })
39
+ .toResponse()
40
+
41
+ assert.strictEqual(res.headers.get('Content-Type'), 'application/json')
42
+ const body = await res.json()
43
+ assert.deepEqual(body, { hello: 'world' })
44
+ })
45
+
46
+ test('text sets correct content type and body', async () => {
47
+ const res = new PikkuFetchHTTPResponse().text('hello').toResponse()
48
+
49
+ assert.strictEqual(res.headers.get('Content-Type'), 'text/plain')
50
+ const text = await res.text()
51
+ assert.strictEqual(text, 'hello')
52
+ })
53
+
54
+ test('html sets correct content type and body', async () => {
55
+ const res = new PikkuFetchHTTPResponse().html('<p>hi</p>').toResponse()
56
+
57
+ assert.strictEqual(res.headers.get('Content-Type'), 'text/html')
58
+ const text = await res.text()
59
+ assert.strictEqual(text, '<p>hi</p>')
60
+ })
61
+
62
+ test('arrayBuffer sets correct content type', async () => {
63
+ const buffer = new Uint8Array([1, 2, 3]).buffer
64
+ const res = new PikkuFetchHTTPResponse().arrayBuffer(buffer).toResponse()
65
+
66
+ assert.strictEqual(
67
+ res.headers.get('Content-Type'),
68
+ 'application/octet-stream'
69
+ )
70
+ const body = await res.arrayBuffer()
71
+ assert.deepEqual(new Uint8Array(body), new Uint8Array([1, 2, 3]))
72
+ })
73
+
74
+ test('redirect sets Location header and status', () => {
75
+ const res = new PikkuFetchHTTPResponse()
76
+ .redirect('/login', 301)
77
+ .toResponse()
78
+
79
+ assert.strictEqual(res.status, 301)
80
+ assert.strictEqual(res.headers.get('Location'), '/login')
81
+ })
82
+ })
@@ -0,0 +1,147 @@
1
+ import type { PikkuHTTPResponse } from './http.types.js'
2
+ import type { SerializeOptions as CookieSerializeOptions } from 'cookie'
3
+ import { serialize as serializeCookie } from 'cookie'
4
+
5
+ export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
6
+ #statusCode: number = 200
7
+ #headers = new Headers()
8
+ #cookies = new Map<string, { value: string; flags: CookieSerializeOptions }>()
9
+
10
+ #body: BodyInit | null = null
11
+ #responseMode: 'stream' | null = null
12
+ #send: ((data: string) => void) | null = null
13
+ #close: (() => void) | null = null
14
+
15
+ public setMode(mode: 'stream') {
16
+ this.#responseMode = 'stream'
17
+ if (mode === 'stream') {
18
+ this.#body = this.createStream()
19
+ }
20
+ }
21
+
22
+ public status(code: number): this {
23
+ this.#statusCode = code
24
+ return this
25
+ }
26
+
27
+ public cookie(
28
+ name: string,
29
+ value: string,
30
+ flags: CookieSerializeOptions
31
+ ): this {
32
+ this.#cookies.set(name, { value, flags })
33
+ return this
34
+ }
35
+
36
+ public header(name: string, value: string | string[]): this {
37
+ this.#headers.delete(name)
38
+ if (Array.isArray(value)) {
39
+ value.forEach((v) => this.#headers.append(name, v))
40
+ } else {
41
+ this.#headers.set(name, value)
42
+ }
43
+ return this
44
+ }
45
+
46
+ public arrayBuffer(data: XMLHttpRequestBodyInit): this {
47
+ if (this.#responseMode === 'stream') {
48
+ this.#send!(data as string)
49
+ } else {
50
+ this.#body = data
51
+ this.header('Content-Type', 'application/octet-stream')
52
+ }
53
+ return this
54
+ }
55
+
56
+ public json(data: unknown): this {
57
+ if (this.#responseMode === 'stream') {
58
+ this.#send!(JSON.stringify(data))
59
+ } else {
60
+ this.#body = JSON.stringify(data)
61
+ this.header('Content-Type', 'application/json')
62
+ }
63
+ return this
64
+ }
65
+
66
+ public text(content: string): this {
67
+ if (this.#responseMode === 'stream') {
68
+ this.#send!(content)
69
+ } else {
70
+ this.#body = content
71
+ this.header('Content-Type', 'text/plain')
72
+ }
73
+ return this
74
+ }
75
+
76
+ public html(content: string): this {
77
+ if (this.#responseMode === 'stream') {
78
+ this.#send!(content)
79
+ } else {
80
+ this.#body = content
81
+ this.header('Content-Type', 'text/html')
82
+ }
83
+ return this
84
+ }
85
+
86
+ public send(data: any): this {
87
+ this.#body = data
88
+ return this
89
+ }
90
+
91
+ public redirect(location: string, status: number = 302): this {
92
+ if (
93
+ !location.startsWith('/') &&
94
+ !location.startsWith('https://') &&
95
+ !location.startsWith('http://')
96
+ ) {
97
+ throw new Error(
98
+ 'Invalid redirect location: must be a relative path or absolute URL'
99
+ )
100
+ }
101
+ this.#statusCode = status
102
+ this.header('Location', location)
103
+ return this
104
+ }
105
+
106
+ public close(): this {
107
+ if (this.#close) {
108
+ this.#close()
109
+ }
110
+ return this
111
+ }
112
+
113
+ public toResponse(args?: Record<string, any>): Response {
114
+ const cookieHeader = Array.from(this.#cookies.entries()).map(
115
+ ([name, { value, flags }]) => serializeCookie(name, value, flags)
116
+ )
117
+ // Multiple Set-Cookie headers must be appended separately, not joined with commas
118
+ // per HTTP specification (RFC 6265)
119
+ cookieHeader.forEach((cookie) => this.#headers.append('Set-Cookie', cookie))
120
+ return new Response(this.#body, {
121
+ ...args,
122
+ status: this.#statusCode,
123
+ headers: this.#headers,
124
+ })
125
+ }
126
+
127
+ private createStream(): ReadableStream<Uint8Array> {
128
+ const encoder = new TextEncoder()
129
+ return new ReadableStream({
130
+ start: (controller) => {
131
+ const send = (data: string) => {
132
+ controller.enqueue(encoder.encode(`data: ${data}\n\n`))
133
+ }
134
+
135
+ const close = () => {
136
+ controller.close()
137
+ }
138
+
139
+ this.#send = send
140
+ this.#close = close
141
+
142
+ // Force initial flush
143
+ controller.enqueue(encoder.encode(':\n\n'))
144
+ },
145
+ })
146
+ }
147
+ }
@@ -0,0 +1,13 @@
1
+ import type { HTTPMethod } from '../http.types.js'
2
+ import { PathToRegexRouter } from './path-to-regex.js'
3
+
4
+ export type MatchResult = {
5
+ route: string
6
+ params: Record<string, any>
7
+ } | null
8
+
9
+ export interface Router {
10
+ match(method: HTTPMethod, path: string): MatchResult
11
+ }
12
+
13
+ export const httpRouter = new PathToRegexRouter()