effect-start 0.9.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/package.json +57 -0
  4. package/src/Bundle.ts +167 -0
  5. package/src/BundleFiles.ts +174 -0
  6. package/src/BundleHttp.test.ts +160 -0
  7. package/src/BundleHttp.ts +259 -0
  8. package/src/Commander.test.ts +1378 -0
  9. package/src/Commander.ts +672 -0
  10. package/src/Datastar.test.ts +267 -0
  11. package/src/Datastar.ts +68 -0
  12. package/src/Effect_HttpRouter.test.ts +570 -0
  13. package/src/EncryptedCookies.test.ts +427 -0
  14. package/src/EncryptedCookies.ts +451 -0
  15. package/src/FileHttpRouter.test.ts +207 -0
  16. package/src/FileHttpRouter.ts +122 -0
  17. package/src/FileRouter.ts +405 -0
  18. package/src/FileRouterCodegen.test.ts +598 -0
  19. package/src/FileRouterCodegen.ts +251 -0
  20. package/src/FileRouter_files.test.ts +64 -0
  21. package/src/FileRouter_path.test.ts +132 -0
  22. package/src/FileRouter_tree.test.ts +126 -0
  23. package/src/FileSystemExtra.ts +102 -0
  24. package/src/HttpAppExtra.ts +127 -0
  25. package/src/Hyper.ts +194 -0
  26. package/src/HyperHtml.test.ts +90 -0
  27. package/src/HyperHtml.ts +139 -0
  28. package/src/HyperNode.ts +37 -0
  29. package/src/JsModule.test.ts +14 -0
  30. package/src/JsModule.ts +116 -0
  31. package/src/PublicDirectory.test.ts +280 -0
  32. package/src/PublicDirectory.ts +108 -0
  33. package/src/Route.test.ts +873 -0
  34. package/src/Route.ts +992 -0
  35. package/src/Router.ts +80 -0
  36. package/src/SseHttpResponse.ts +55 -0
  37. package/src/Start.ts +133 -0
  38. package/src/StartApp.ts +43 -0
  39. package/src/StartHttp.ts +42 -0
  40. package/src/StreamExtra.ts +146 -0
  41. package/src/TestHttpClient.test.ts +54 -0
  42. package/src/TestHttpClient.ts +100 -0
  43. package/src/bun/BunBundle.test.ts +277 -0
  44. package/src/bun/BunBundle.ts +309 -0
  45. package/src/bun/BunBundle_imports.test.ts +50 -0
  46. package/src/bun/BunFullstackServer.ts +45 -0
  47. package/src/bun/BunFullstackServer_httpServer.ts +541 -0
  48. package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
  49. package/src/bun/BunImportTrackerPlugin.ts +97 -0
  50. package/src/bun/BunTailwindPlugin.test.ts +335 -0
  51. package/src/bun/BunTailwindPlugin.ts +322 -0
  52. package/src/bun/BunVirtualFilesPlugin.ts +59 -0
  53. package/src/bun/index.ts +4 -0
  54. package/src/client/Overlay.ts +34 -0
  55. package/src/client/ScrollState.ts +120 -0
  56. package/src/client/index.ts +101 -0
  57. package/src/index.ts +24 -0
  58. package/src/jsx-datastar.d.ts +63 -0
  59. package/src/jsx-runtime.ts +23 -0
  60. package/src/jsx.d.ts +4402 -0
  61. package/src/testing.ts +55 -0
  62. package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
  63. package/src/x/cloudflare/index.ts +1 -0
  64. package/src/x/datastar/Datastar.test.ts +267 -0
  65. package/src/x/datastar/Datastar.ts +68 -0
  66. package/src/x/datastar/index.ts +4 -0
  67. package/src/x/datastar/jsx-datastar.d.ts +63 -0
@@ -0,0 +1,427 @@
1
+ import * as Cookies from "@effect/platform/Cookies"
2
+ import * as t from "bun:test"
3
+ import * as ConfigProvider from "effect/ConfigProvider"
4
+ import * as Effect from "effect/Effect"
5
+ import * as EncryptedCookies from "./EncryptedCookies.ts"
6
+
7
+ t.describe("encrypt", () => {
8
+ t.test("return encrypted string in correct format", async () => {
9
+ const value = "hello world"
10
+
11
+ const result = await Effect.runPromise(
12
+ EncryptedCookies.encrypt(value, { secret: "test-secret" }),
13
+ )
14
+
15
+ // Check format: three base64url segments separated by .
16
+ const segments = result.split(".")
17
+ t.expect(segments).toHaveLength(3)
18
+
19
+ // Each segment should be base64url (no +, /, or = characters
20
+ // so cookie values are not escaped)
21
+ segments.forEach((segment: string) => {
22
+ t.expect(segment).not.toMatch(/[+/=]/)
23
+ // Should be valid base64url that can be decoded
24
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/")
25
+ const paddedBase64 = base64 + "=".repeat((4 - base64.length % 4) % 4)
26
+ t.expect(() => atob(paddedBase64)).not.toThrow()
27
+ })
28
+ })
29
+
30
+ t.test("produce different results for same input due to random IV", async () => {
31
+ const value = "same value"
32
+
33
+ const result1 = await Effect.runPromise(
34
+ EncryptedCookies.encrypt(value, { secret: "test-secret" }),
35
+ )
36
+ const result2 = await Effect.runPromise(
37
+ EncryptedCookies.encrypt(value, { secret: "test-secret" }),
38
+ )
39
+
40
+ t.expect(result1).not.toBe(result2)
41
+
42
+ // But both should have correct format
43
+ t.expect(result1.split(".")).toHaveLength(3)
44
+ t.expect(result2.split(".")).toHaveLength(3)
45
+ })
46
+
47
+ t.test("handle empty string", async () => {
48
+ const value = ""
49
+
50
+ const result = await Effect.runPromise(
51
+ EncryptedCookies.encrypt(value, { secret: "test-secret" }),
52
+ )
53
+
54
+ t.expect(result.split(".")).toHaveLength(3)
55
+ })
56
+
57
+ t.test("handle special characters", async () => {
58
+ const value = "hello 世界! @#$%^&*()"
59
+
60
+ const result = await Effect.runPromise(
61
+ EncryptedCookies.encrypt(value, { secret: "test-secret" }),
62
+ )
63
+
64
+ t.expect(result.split(".")).toHaveLength(3)
65
+ })
66
+
67
+ t.test("handle object with undefined properties", async () => {
68
+ const value = { id: "some", optional: undefined }
69
+
70
+ const encrypted = await Effect.runPromise(
71
+ EncryptedCookies.encrypt(value, { secret: "test-secret" }),
72
+ )
73
+ const decrypted = await Effect.runPromise(
74
+ EncryptedCookies.decrypt(encrypted, { secret: "test-secret" }),
75
+ )
76
+
77
+ // JSON.stringify removes undefined properties
78
+ t.expect(decrypted).toEqual({ id: "some" })
79
+ })
80
+ })
81
+
82
+ t.describe("decrypt", () => {
83
+ t.test("decrypt encrypted string successfully", async () => {
84
+ const originalValue = "hello world"
85
+
86
+ const encrypted = await Effect.runPromise(
87
+ EncryptedCookies.encrypt(originalValue, { secret: "test-secret" }),
88
+ )
89
+ const decrypted = await Effect.runPromise(
90
+ EncryptedCookies.decrypt(encrypted, { secret: "test-secret" }),
91
+ )
92
+
93
+ t.expect(decrypted).toBe(originalValue)
94
+ })
95
+
96
+ t.test("handle empty string round-trip", async () => {
97
+ const originalValue = ""
98
+
99
+ const encrypted = await Effect.runPromise(
100
+ EncryptedCookies.encrypt(originalValue, { secret: "test-secret" }),
101
+ )
102
+ const decrypted = await Effect.runPromise(
103
+ EncryptedCookies.decrypt(encrypted, { secret: "test-secret" }),
104
+ )
105
+
106
+ t.expect(decrypted).toBe(originalValue)
107
+ })
108
+
109
+ t.test("handle special characters round-trip", async () => {
110
+ const originalValue = "hello 世界! @#$%^&*()"
111
+
112
+ const encrypted = await Effect.runPromise(
113
+ EncryptedCookies.encrypt(originalValue, { secret: "test-secret" }),
114
+ )
115
+ const decrypted = await Effect.runPromise(
116
+ EncryptedCookies.decrypt(encrypted, { secret: "test-secret" }),
117
+ )
118
+
119
+ t.expect(decrypted).toBe(originalValue)
120
+ })
121
+
122
+ t.test("fail with invalid format", () => {
123
+ const invalidValue = "not-encrypted"
124
+
125
+ t
126
+ .expect(
127
+ Effect.runPromise(
128
+ EncryptedCookies.decrypt(invalidValue, { secret: "test-secret" }),
129
+ ),
130
+ )
131
+ .rejects
132
+ .toThrow()
133
+ })
134
+
135
+ t.test("fail with wrong number of segments", () => {
136
+ const invalidValue = "one.two"
137
+
138
+ t
139
+ .expect(
140
+ Effect.runPromise(
141
+ EncryptedCookies.decrypt(invalidValue, { secret: "test-secret" }),
142
+ ),
143
+ )
144
+ .rejects
145
+ .toThrow()
146
+ })
147
+
148
+ t.test("fail with invalid base64", () => {
149
+ const invalidValue = "invalid.base64.data"
150
+
151
+ t
152
+ .expect(
153
+ Effect.runPromise(
154
+ EncryptedCookies.decrypt(invalidValue, { secret: "test-secret" }),
155
+ ),
156
+ )
157
+ .rejects
158
+ .toThrow()
159
+ })
160
+
161
+ t.test("fail with null value", () => {
162
+ t
163
+ .expect(
164
+ Effect.runPromise(
165
+ EncryptedCookies.encrypt(null, { secret: "test-secret" }),
166
+ ),
167
+ )
168
+ .rejects
169
+ .toThrow()
170
+ })
171
+
172
+ t.test("fail with undefined value", () => {
173
+ t
174
+ .expect(
175
+ Effect.runPromise(
176
+ EncryptedCookies.encrypt(undefined, { secret: "test-secret" }),
177
+ ),
178
+ )
179
+ .rejects
180
+ .toThrow()
181
+ })
182
+
183
+ t.test("fail with empty encrypted value", () => {
184
+ t
185
+ .expect(
186
+ Effect.runPromise(
187
+ EncryptedCookies.decrypt("", { secret: "test-secret" }),
188
+ ),
189
+ )
190
+ .rejects
191
+ .toThrow()
192
+ })
193
+ })
194
+
195
+ t.describe("encryptCookie", () => {
196
+ t.test("preserve cookie properties and encrypt value", async () => {
197
+ const cookie = Cookies.unsafeMakeCookie("test", "hello world")
198
+
199
+ const result = await Effect.runPromise(
200
+ EncryptedCookies.encryptCookie(cookie, { secret: "test-secret" }),
201
+ )
202
+
203
+ // Cookie properties should be preserved
204
+ t.expect(result.name).toBe("test")
205
+
206
+ // Value should be encrypted (different from original)
207
+ t.expect(result.value).not.toBe("hello world")
208
+
209
+ // Should be in encrypted format
210
+ t.expect(result.value.split(".")).toHaveLength(3)
211
+ })
212
+ })
213
+
214
+ t.describe("decryptCookie", () => {
215
+ t.test("preserve cookie properties and decrypt value", async () => {
216
+ const originalCookie = Cookies.unsafeMakeCookie("test", "hello world")
217
+
218
+ const encrypted = await Effect.runPromise(
219
+ EncryptedCookies.encryptCookie(originalCookie, { secret: "test-secret" }),
220
+ )
221
+ const decrypted = await Effect.runPromise(
222
+ EncryptedCookies.decryptCookie(encrypted, { secret: "test-secret" }),
223
+ )
224
+
225
+ // Cookie properties should be preserved
226
+ t.expect(decrypted.name).toBe("test")
227
+
228
+ // Value should be JSON stringified (string values are now always serialized)
229
+ t.expect(decrypted.value).toBe("\"hello world\"")
230
+ })
231
+ })
232
+
233
+ t.describe("service", () => {
234
+ t.test("service uses pre-calculated key material", async () => {
235
+ const testSecret = "test-secret-key"
236
+ const testValue = "hello world"
237
+
238
+ const program = Effect.gen(function*() {
239
+ const service = yield* EncryptedCookies.EncryptedCookies
240
+
241
+ const encrypted = yield* service.encrypt(testValue)
242
+ const decrypted = yield* service.decrypt(encrypted)
243
+
244
+ return { encrypted, decrypted }
245
+ })
246
+
247
+ const result = await Effect.runPromise(
248
+ program.pipe(
249
+ Effect.provide(EncryptedCookies.layer({ secret: testSecret })),
250
+ ),
251
+ )
252
+
253
+ t.expect(result.decrypted).toBe(testValue)
254
+ t.expect(result.encrypted).not.toBe(testValue)
255
+ t.expect(result.encrypted.split(".")).toHaveLength(3)
256
+ })
257
+
258
+ t.test("service cookie functions work with pre-calculated key", async () => {
259
+ const testSecret = "test-secret-key"
260
+ const originalCookie = Cookies.unsafeMakeCookie("test", "hello world")
261
+
262
+ const program = Effect.gen(function*() {
263
+ const service = yield* EncryptedCookies.EncryptedCookies
264
+
265
+ const encrypted = yield* service.encryptCookie(originalCookie)
266
+ const decrypted = yield* service.decryptCookie(encrypted)
267
+
268
+ return { encrypted, decrypted }
269
+ })
270
+
271
+ const result = await Effect.runPromise(
272
+ program.pipe(
273
+ Effect.provide(EncryptedCookies.layer({ secret: testSecret })),
274
+ ),
275
+ )
276
+
277
+ t.expect(result.decrypted.name).toBe("test")
278
+ t.expect(result.decrypted.value).toBe("\"hello world\"")
279
+ t.expect(result.encrypted.value).not.toBe("hello world")
280
+ })
281
+
282
+ t.test("functions work with pre-derived keys passed as options", async () => {
283
+ const testSecret = "test-secret-key"
284
+ const testValue = "hello world"
285
+
286
+ // Pre-derive keys manually
287
+ const program = Effect.gen(function*() {
288
+ const keyMaterial = yield* Effect.tryPromise({
289
+ try: () =>
290
+ crypto.subtle.importKey(
291
+ "raw",
292
+ new TextEncoder().encode(testSecret),
293
+ { name: "HKDF" },
294
+ false,
295
+ ["deriveKey"],
296
+ ),
297
+ catch: (error) => error,
298
+ })
299
+
300
+ const encryptKey = yield* Effect.tryPromise({
301
+ try: () =>
302
+ crypto.subtle.deriveKey(
303
+ {
304
+ name: "HKDF",
305
+ salt: new TextEncoder().encode("cookie-encryption"),
306
+ info: new TextEncoder().encode("aes-256-gcm"),
307
+ hash: "SHA-256",
308
+ },
309
+ keyMaterial,
310
+ { name: "AES-GCM", length: 256 },
311
+ false,
312
+ ["encrypt"],
313
+ ),
314
+ catch: (error) => error,
315
+ })
316
+
317
+ const decryptKey = yield* Effect.tryPromise({
318
+ try: () =>
319
+ crypto.subtle.deriveKey(
320
+ {
321
+ name: "HKDF",
322
+ salt: new TextEncoder().encode("cookie-encryption"),
323
+ info: new TextEncoder().encode("aes-256-gcm"),
324
+ hash: "SHA-256",
325
+ },
326
+ keyMaterial,
327
+ { name: "AES-GCM", length: 256 },
328
+ false,
329
+ ["decrypt"],
330
+ ),
331
+ catch: (error) => error,
332
+ })
333
+
334
+ // Use functions with pre-derived keys
335
+ const encrypted = yield* EncryptedCookies.encrypt(testValue, {
336
+ key: encryptKey,
337
+ })
338
+ const decrypted = yield* EncryptedCookies.decrypt(encrypted, {
339
+ key: decryptKey,
340
+ })
341
+
342
+ return { encrypted, decrypted }
343
+ })
344
+
345
+ const result = await Effect.runPromise(program)
346
+
347
+ t.expect(result.decrypted).toBe(testValue)
348
+ t.expect(result.encrypted).not.toBe(testValue)
349
+ t.expect(result.encrypted.split(".")).toHaveLength(3)
350
+ })
351
+ })
352
+
353
+ t.describe("layerConfig", () => {
354
+ t.test("succeed with valid SECRET_KEY_BASE", async () => {
355
+ const validSecret = "a".repeat(40)
356
+
357
+ const program = Effect.gen(function*() {
358
+ const service = yield* EncryptedCookies.EncryptedCookies
359
+ const encrypted = yield* service.encrypt("test")
360
+ const decrypted = yield* service.decrypt(encrypted)
361
+ return decrypted
362
+ })
363
+
364
+ const result = await Effect.runPromise(
365
+ program.pipe(
366
+ Effect.provide(
367
+ EncryptedCookies.layerConfig("SECRET_KEY_BASE"),
368
+ ),
369
+ Effect.withConfigProvider(
370
+ ConfigProvider.fromMap(
371
+ new Map([["SECRET_KEY_BASE", validSecret]]),
372
+ ),
373
+ ),
374
+ ),
375
+ )
376
+
377
+ t.expect(result).toBe("test")
378
+ })
379
+
380
+ t.test("fail with short SECRET_KEY_BASE", async () => {
381
+ const shortSecret = "short"
382
+
383
+ const program = Effect.gen(function*() {
384
+ const service = yield* EncryptedCookies.EncryptedCookies
385
+ return yield* service.encrypt("test")
386
+ })
387
+
388
+ t
389
+ .expect(
390
+ Effect.runPromise(
391
+ program.pipe(
392
+ Effect.provide(
393
+ EncryptedCookies.layerConfig("SECRET_KEY_BASE"),
394
+ ),
395
+ Effect.withConfigProvider(
396
+ ConfigProvider.fromMap(
397
+ new Map([["SECRET_KEY_BASE", shortSecret]]),
398
+ ),
399
+ ),
400
+ ),
401
+ ),
402
+ )
403
+ .rejects
404
+ .toThrow("SECRET_KEY_BASE must be at least 40 characters")
405
+ })
406
+
407
+ t.test("fail with missing SECRET_KEY_BASE", async () => {
408
+ const program = Effect.gen(function*() {
409
+ const service = yield* EncryptedCookies.EncryptedCookies
410
+ return yield* service.encrypt("test")
411
+ })
412
+
413
+ t
414
+ .expect(
415
+ Effect.runPromise(
416
+ program.pipe(
417
+ Effect.provide(
418
+ EncryptedCookies.layerConfig("SECRET_KEY_BASE"),
419
+ ),
420
+ Effect.withConfigProvider(ConfigProvider.fromMap(new Map())),
421
+ ),
422
+ ),
423
+ )
424
+ .rejects
425
+ .toThrow("SECRET_KEY_BASE must be at least 40 characters")
426
+ })
427
+ })