effect-start 0.10.0 → 0.11.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.
@@ -1,218 +1,218 @@
1
- import * as Bun from "bun"
2
1
  import * as t from "bun:test"
3
2
  import * as Effect from "effect/Effect"
4
3
  import * as Route from "../Route.ts"
5
- import type * as Router from "../Router.ts"
4
+ import * as Router from "../Router.ts"
5
+ import * as TestHttpClient from "../TestHttpClient.ts"
6
+ import * as BunHttpServer from "./BunHttpServer.ts"
6
7
  import * as BunRoute from "./BunRoute.ts"
7
8
 
8
9
  t.describe("BunRoute proxy with Bun.serve", () => {
9
10
  t.test("BunRoute proxy returns same content as direct bundle access", async () => {
10
- const bunRoute = BunRoute.loadBundle(() =>
11
- import("../../static/TestPage.html")
12
- )
11
+ const bunRoute = BunRoute.html(() => import("../../static/TestPage.html"))
13
12
 
14
- const router: Router.RouterContext = {
15
- routes: [
16
- {
17
- path: "/test",
18
- load: () => Promise.resolve({ default: bunRoute }),
19
- },
20
- ],
21
- }
13
+ const router = Router.mount("/test", bunRoute)
22
14
 
23
- const routes = await Effect.runPromise(BunRoute.routesFromRouter(router))
15
+ await Effect.runPromise(
16
+ Effect
17
+ .gen(function*() {
18
+ const bunServer = yield* BunHttpServer.BunServer
19
+ const routes = yield* BunRoute.routesFromRouter(router)
20
+ bunServer.addRoutes(routes)
24
21
 
25
- const internalPath = Object.keys(routes).find((k) =>
26
- k.includes("~BunRoute-")
27
- )
28
- t.expect(internalPath).toBeDefined()
22
+ const internalPath = Object.keys(routes).find((k) =>
23
+ k.includes(".BunRoute-")
24
+
25
+ )
26
+ t.expect(internalPath).toBeDefined()
29
27
 
30
- const proxyHandler = routes["/test"]
31
- t.expect(typeof proxyHandler).toBe("function")
28
+ const proxyHandler = routes["/test"]
29
+ t.expect(typeof proxyHandler).toBe("function")
32
30
 
33
- const internalBundle = routes[internalPath!]
34
- t.expect(internalBundle).toHaveProperty("index")
31
+ const internalBundle = routes[internalPath!]
32
+ t.expect(internalBundle).toHaveProperty("index")
35
33
 
36
- const server = Bun.serve({
37
- port: 0,
38
- routes,
39
- fetch: () => new Response("Not found", { status: 404 }),
40
- })
34
+ const baseUrl =
35
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
36
+ const client = TestHttpClient.make<never, never>(
37
+ (req) => fetch(req),
38
+ {
39
+ baseUrl,
40
+ },
41
+ )
41
42
 
42
- try {
43
- const directResponse = await fetch(
44
- `http://localhost:${server.port}${internalPath}`,
45
- )
46
- const proxyResponse = await fetch(`http://localhost:${server.port}/test`)
43
+ const directResponse = yield* client.get(internalPath!)
44
+ const proxyResponse = yield* client.get("/test")
47
45
 
48
- t.expect(proxyResponse.status).toBe(directResponse.status)
46
+ t.expect(proxyResponse.status).toBe(directResponse.status)
49
47
 
50
- const directText = await directResponse.text()
51
- const proxyText = await proxyResponse.text()
48
+ const directText = yield* directResponse.text
49
+ const proxyText = yield* proxyResponse.text
52
50
 
53
- t.expect(proxyText).toBe(directText)
54
- t.expect(proxyText).toContain("Test Page Content")
55
- } finally {
56
- server.stop()
57
- }
51
+ t.expect(proxyText).toBe(directText)
52
+ t.expect(proxyText).toContain("Test Page Content")
53
+ })
54
+ .pipe(
55
+ Effect.scoped,
56
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
57
+ ),
58
+ )
58
59
  })
59
60
 
60
61
  t.test("multiple BunRoutes each get unique internal paths", async () => {
61
- const bunRoute1 = BunRoute.loadBundle(() =>
62
- import("../../static/TestPage.html")
63
- )
64
- const bunRoute2 = BunRoute.loadBundle(() =>
62
+ const bunRoute1 = BunRoute.html(() => import("../../static/TestPage.html"))
63
+ const bunRoute2 = BunRoute.html(() =>
65
64
  import("../../static/AnotherPage.html")
66
65
  )
67
66
 
68
- const router: Router.RouterContext = {
69
- routes: [
70
- {
71
- path: "/page1",
72
- load: () => Promise.resolve({ default: bunRoute1 }),
73
- },
74
- {
75
- path: "/page2",
76
- load: () => Promise.resolve({ default: bunRoute2 }),
77
- },
78
- ],
79
- }
80
-
81
- const routes = await Effect.runPromise(BunRoute.routesFromRouter(router))
82
-
83
- const internalPaths = Object.keys(routes).filter((k) =>
84
- k.includes("~BunRoute-")
67
+ const router = Router
68
+ .mount("/page1", bunRoute1)
69
+ .mount("/page2", bunRoute2)
70
+
71
+ await Effect.runPromise(
72
+ Effect
73
+ .gen(function*() {
74
+ const bunServer = yield* BunHttpServer.BunServer
75
+ const routes = yield* BunRoute.routesFromRouter(router)
76
+ bunServer.addRoutes(routes)
77
+
78
+ const internalPaths = Object.keys(routes).filter((k) =>
79
+ k.includes(".BunRoute-")
80
+ )
81
+ t.expect(internalPaths).toHaveLength(2)
82
+
83
+ const nonces = internalPaths.map((p) => {
84
+ const match = p.match(/\.BunRoute-([a-z0-9]+)/)
85
+ return match?.[1]
86
+ })
87
+ t.expect(nonces[0]).not.toBe(nonces[1])
88
+
89
+ const baseUrl =
90
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
91
+ const client = TestHttpClient.make<never, never>(
92
+ (req) => fetch(req),
93
+ {
94
+ baseUrl,
95
+ },
96
+ )
97
+
98
+ const response1 = yield* client.get("/page1")
99
+ const response2 = yield* client.get("/page2")
100
+
101
+ const text1 = yield* response1.text
102
+ const text2 = yield* response2.text
103
+
104
+ t.expect(text1).toContain("Test Page Content")
105
+ t.expect(text2).toContain("Another Page Content")
106
+ })
107
+ .pipe(
108
+ Effect.scoped,
109
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
110
+ ),
85
111
  )
86
- t.expect(internalPaths).toHaveLength(2)
87
-
88
- const nonces = internalPaths.map((p) => {
89
- const match = p.match(/~BunRoute-([a-z0-9]+)$/)
90
- return match?.[1]
91
- })
92
- t.expect(nonces[0]).toBe(nonces[1])
93
-
94
- const server = Bun.serve({
95
- port: 0,
96
- routes,
97
- fetch: () => new Response("Not found", { status: 404 }),
98
- })
99
-
100
- try {
101
- const response1 = await fetch(`http://localhost:${server.port}/page1`)
102
- const response2 = await fetch(`http://localhost:${server.port}/page2`)
103
-
104
- const text1 = await response1.text()
105
- const text2 = await response2.text()
106
-
107
- t.expect(text1).toContain("Test Page Content")
108
- t.expect(text2).toContain("Another Page Content")
109
- } finally {
110
- server.stop()
111
- }
112
112
  })
113
113
 
114
114
  t.test("proxy preserves request headers", async () => {
115
- const bunRoute = BunRoute.loadBundle(() =>
116
- import("../../static/TestPage.html")
115
+ const bunRoute = BunRoute.html(() => import("../../static/TestPage.html"))
116
+
117
+ const router = Router.mount("/headers-test", bunRoute)
118
+
119
+ await Effect.runPromise(
120
+ Effect
121
+ .gen(function*() {
122
+ const bunServer = yield* BunHttpServer.BunServer
123
+ const routes = yield* BunRoute.routesFromRouter(router)
124
+ bunServer.addRoutes(routes)
125
+
126
+ const baseUrl =
127
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
128
+ const client = TestHttpClient.make<never, never>(
129
+ (req) => fetch(req),
130
+ {
131
+ baseUrl,
132
+ },
133
+ )
134
+
135
+ const response = yield* client.get("/headers-test", {
136
+ headers: {
137
+ "Accept": "text/html",
138
+ "X-Custom-Header": "test-value",
139
+ },
140
+ })
141
+
142
+ t.expect(response.status).toBe(200)
143
+ const text = yield* response.text
144
+ t.expect(text).toContain("Test Page Content")
145
+ })
146
+ .pipe(
147
+ Effect.scoped,
148
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
149
+ ),
117
150
  )
118
-
119
- const router: Router.RouterContext = {
120
- routes: [
121
- {
122
- path: "/headers-test",
123
- load: () => Promise.resolve({ default: bunRoute }),
124
- },
125
- ],
126
- }
127
-
128
- const routes = await Effect.runPromise(BunRoute.routesFromRouter(router))
129
-
130
- const server = Bun.serve({
131
- port: 0,
132
- routes,
133
- fetch: () => new Response("Not found", { status: 404 }),
134
- })
135
-
136
- try {
137
- const response = await fetch(
138
- `http://localhost:${server.port}/headers-test`,
139
- {
140
- headers: {
141
- "Accept": "text/html",
142
- "X-Custom-Header": "test-value",
143
- },
144
- },
145
- )
146
-
147
- t.expect(response.status).toBe(200)
148
- t.expect(await response.text()).toContain("Test Page Content")
149
- } finally {
150
- server.stop()
151
- }
152
151
  })
153
152
 
154
153
  t.test("mixed BunRoute and regular routes work together", async () => {
155
- const bunRoute = BunRoute.loadBundle(() =>
156
- import("../../static/TestPage.html")
154
+ const bunRoute = BunRoute.html(() => import("../../static/TestPage.html"))
155
+
156
+ const router = Router
157
+ .mount("/html", bunRoute)
158
+ .mount("/api", Route.text("Hello from text route"))
159
+
160
+ await Effect.runPromise(
161
+ Effect
162
+ .gen(function*() {
163
+ const bunServer = yield* BunHttpServer.BunServer
164
+ const routes = yield* BunRoute.routesFromRouter(router)
165
+ bunServer.addRoutes(routes)
166
+
167
+ const baseUrl =
168
+ `http://${bunServer.server.hostname}:${bunServer.server.port}`
169
+ const client = TestHttpClient.make<never, never>(
170
+ (req) => fetch(req),
171
+ {
172
+ baseUrl,
173
+ },
174
+ )
175
+
176
+ const htmlResponse = yield* client.get("/html")
177
+ const apiResponse = yield* client.get("/api")
178
+
179
+ const htmlText = yield* htmlResponse.text
180
+ const apiText = yield* apiResponse.text
181
+
182
+ t.expect(htmlText).toContain("Test Page Content")
183
+ t.expect(apiText).toBe("Hello from text route")
184
+ })
185
+ .pipe(
186
+ Effect.scoped,
187
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
188
+ ),
157
189
  )
158
- const textRoute = Route.text(Effect.succeed("Hello from text route"))
159
-
160
- const router: Router.RouterContext = {
161
- routes: [
162
- {
163
- path: "/html",
164
- load: () => Promise.resolve({ default: bunRoute }),
165
- },
166
- {
167
- path: "/api",
168
- load: () => Promise.resolve({ default: textRoute }),
169
- },
170
- ],
171
- }
172
-
173
- const routes = await Effect.runPromise(BunRoute.routesFromRouter(router))
174
-
175
- const server = Bun.serve({
176
- port: 0,
177
- routes,
178
- fetch: () => new Response("Not found", { status: 404 }),
179
- })
180
-
181
- try {
182
- const htmlResponse = await fetch(`http://localhost:${server.port}/html`)
183
- const apiResponse = await fetch(`http://localhost:${server.port}/api`)
184
-
185
- t.expect(await htmlResponse.text()).toContain("Test Page Content")
186
- t.expect(await apiResponse.text()).toBe("Hello from text route")
187
- } finally {
188
- server.stop()
189
- }
190
190
  })
191
191
 
192
- t.test("nonce is different across separate routesFromRouter calls", async () => {
193
- const bunRoute = BunRoute.loadBundle(() =>
194
- import("../../static/TestPage.html")
192
+ t.test("nonce is different across separate BunRoute instances", async () => {
193
+ const bunRoute1 = BunRoute.html(() => import("../../static/TestPage.html"))
194
+ const bunRoute2 = BunRoute.html(() => import("../../static/TestPage.html"))
195
+
196
+ const router = Router
197
+ .mount("/test1", bunRoute1)
198
+ .mount("/test2", bunRoute2)
199
+
200
+ await Effect.runPromise(
201
+ Effect
202
+ .gen(function*() {
203
+ const routes = yield* BunRoute.routesFromRouter(router)
204
+
205
+ const internalPaths = Object.keys(routes).filter((k) =>
206
+ k.includes(".BunRoute-")
207
+ )
208
+
209
+ t.expect(internalPaths).toHaveLength(2)
210
+ t.expect(internalPaths[0]).not.toBe(internalPaths[1])
211
+ })
212
+ .pipe(
213
+ Effect.scoped,
214
+ Effect.provide(BunHttpServer.layer({ port: 0 })),
215
+ ),
195
216
  )
196
-
197
- const router: Router.RouterContext = {
198
- routes: [
199
- {
200
- path: "/test",
201
- load: () => Promise.resolve({ default: bunRoute }),
202
- },
203
- ],
204
- }
205
-
206
- const routes1 = await Effect.runPromise(BunRoute.routesFromRouter(router))
207
- const routes2 = await Effect.runPromise(BunRoute.routesFromRouter(router))
208
-
209
- const internalPath1 = Object.keys(routes1).find((k) =>
210
- k.includes("~BunRoute-")
211
- )
212
- const internalPath2 = Object.keys(routes2).find((k) =>
213
- k.includes("~BunRoute-")
214
- )
215
-
216
- t.expect(internalPath1).not.toBe(internalPath2)
217
217
  })
218
218
  })
package/src/index.ts CHANGED
@@ -1,24 +1,24 @@
1
- export * as Bundle from "./Bundle"
2
- export * as BundleHttp from "./BundleHttp"
1
+ export * as Bundle from "./Bundle.ts"
2
+ export * as BundleHttp from "./BundleHttp.ts"
3
3
 
4
- export * as StartHttp from "./StartHttp"
4
+ export * as StartHttp from "./StartHttp.ts"
5
5
 
6
- export * as HttpAppExtra from "./HttpAppExtra"
6
+ export * as HttpAppExtra from "./HttpAppExtra.ts"
7
7
 
8
- export * as PublicDirectory from "./PublicDirectory"
8
+ export * as PublicDirectory from "./PublicDirectory.ts"
9
9
 
10
- export * as TestHttpClient from "./TestHttpClient"
10
+ export * as TestHttpClient from "./TestHttpClient.ts"
11
11
  export {
12
12
  effectFn,
13
- } from "./testing"
13
+ } from "./testing.ts"
14
14
 
15
- export * as FileHttpRouter from "./FileHttpRouter"
16
- export * as FileRouter from "./FileRouter"
15
+ export * as FileHttpRouter from "./FileHttpRouter.ts"
16
+ export * as FileRouter from "./FileRouter.ts"
17
17
 
18
- export * as Route from "./Route"
19
- export * as Router from "./Router"
18
+ export * as Route from "./Route.ts"
19
+ export * as Router from "./Router.ts"
20
20
 
21
- export * as Start from "./Start"
21
+ export * as Start from "./Start.ts"
22
22
 
23
- export * as Hyper from "./Hyper"
24
- export * as HyperHtml from "./HyperHtml"
23
+ export * as Hyper from "./Hyper.ts"
24
+ export * as HyperHtml from "./HyperHtml.ts"
@@ -0,0 +1,74 @@
1
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
3
+ import {
4
+ describe,
5
+ expect,
6
+ it,
7
+ } from "bun:test"
8
+ import * as Effect from "effect/Effect"
9
+ import * as BasicAuthMiddleware from "./BasicAuthMiddleware.js"
10
+
11
+ const mockApp = Effect.succeed(
12
+ HttpServerResponse.text("OK", { status: 200 }),
13
+ )
14
+
15
+ const config: BasicAuthMiddleware.BasicAuthConfig = {
16
+ username: "admin",
17
+ password: "secret",
18
+ }
19
+
20
+ const runWithAuth = (authHeader: string | undefined) => {
21
+ const middleware = BasicAuthMiddleware.make(config)
22
+ const wrappedApp = middleware(mockApp)
23
+
24
+ const headers: Record<string, string> = {}
25
+ if (authHeader !== undefined) {
26
+ headers.authorization = authHeader
27
+ }
28
+
29
+ const mockRequest = HttpServerRequest.fromWeb(
30
+ new Request("http://localhost/test", { headers }),
31
+ )
32
+
33
+ return wrappedApp.pipe(
34
+ Effect.provideService(HttpServerRequest.HttpServerRequest, mockRequest),
35
+ Effect.runPromise,
36
+ )
37
+ }
38
+
39
+ describe("BasicAuthMiddleware", () => {
40
+ it("returns 401 when no authorization header is present", async () => {
41
+ const response = await runWithAuth(undefined)
42
+ expect(response.status).toBe(401)
43
+ expect(response.headers["www-authenticate"]).toBe("Basic")
44
+ })
45
+
46
+ it("returns 401 when authorization header does not start with Basic", async () => {
47
+ const response = await runWithAuth("Bearer token")
48
+ expect(response.status).toBe(401)
49
+ })
50
+
51
+ it("returns 401 when credentials are invalid", async () => {
52
+ const invalidCredentials = btoa("wrong:credentials")
53
+ const response = await runWithAuth(`Basic ${invalidCredentials}`)
54
+ expect(response.status).toBe(401)
55
+ })
56
+
57
+ it("returns 401 when username is wrong", async () => {
58
+ const invalidCredentials = btoa("wronguser:secret")
59
+ const response = await runWithAuth(`Basic ${invalidCredentials}`)
60
+ expect(response.status).toBe(401)
61
+ })
62
+
63
+ it("returns 401 when password is wrong", async () => {
64
+ const invalidCredentials = btoa("admin:wrongpassword")
65
+ const response = await runWithAuth(`Basic ${invalidCredentials}`)
66
+ expect(response.status).toBe(401)
67
+ })
68
+
69
+ it("passes through to app when credentials are valid", async () => {
70
+ const validCredentials = btoa("admin:secret")
71
+ const response = await runWithAuth(`Basic ${validCredentials}`)
72
+ expect(response.status).toBe(200)
73
+ })
74
+ })
@@ -0,0 +1,36 @@
1
+ import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
2
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
3
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
4
+ import * as Effect from "effect/Effect"
5
+
6
+ export interface BasicAuthConfig {
7
+ readonly username: string
8
+ readonly password: string
9
+ }
10
+
11
+ const unauthorizedResponse = HttpServerResponse.empty({
12
+ status: 401,
13
+ headers: { "WWW-Authenticate": "Basic" },
14
+ })
15
+
16
+ export const make = (config: BasicAuthConfig) =>
17
+ HttpMiddleware.make((app) =>
18
+ Effect.gen(function*() {
19
+ const request = yield* HttpServerRequest.HttpServerRequest
20
+ const authHeader = request.headers.authorization
21
+
22
+ if (!authHeader || !authHeader.startsWith("Basic ")) {
23
+ return unauthorizedResponse
24
+ }
25
+
26
+ const base64Credentials = authHeader.slice(6)
27
+ const credentials = atob(base64Credentials)
28
+ const [username, password] = credentials.split(":")
29
+
30
+ if (username !== config.username || password !== config.password) {
31
+ return unauthorizedResponse
32
+ }
33
+
34
+ return yield* app
35
+ })
36
+ )