effect-start 0.20.1 → 0.22.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.
- package/README.md +1 -4
- package/dist/Cookies.js +392 -0
- package/dist/FileSystem.js +131 -0
- package/dist/Socket.js +37 -0
- package/package.json +39 -40
- package/src/Commander.ts +73 -130
- package/src/ContentNegotiation.ts +68 -100
- package/src/Cookies.ts +408 -0
- package/src/Development.ts +48 -63
- package/src/Effectify.ts +222 -206
- package/src/Entity.ts +59 -86
- package/src/FilePathPattern.ts +5 -5
- package/src/FileRouter.ts +38 -63
- package/src/FileRouterCodegen.ts +64 -56
- package/src/FileSystem.ts +390 -0
- package/src/Http.ts +17 -50
- package/src/PathPattern.ts +33 -41
- package/src/PlatformError.ts +29 -50
- package/src/PlatformRuntime.ts +39 -47
- package/src/Route.ts +68 -187
- package/src/RouteBody.ts +45 -161
- package/src/RouteHook.ts +22 -45
- package/src/RouteHttp.ts +88 -142
- package/src/RouteHttpTracer.ts +25 -26
- package/src/RouteMount.ts +100 -238
- package/src/RouteSchema.ts +67 -201
- package/src/RouteSse.ts +28 -82
- package/src/RouteTree.ts +31 -79
- package/src/RouteTrie.ts +13 -32
- package/src/SchemaExtra.ts +3 -5
- package/src/Socket.ts +51 -0
- package/src/Start.ts +20 -21
- package/src/StreamExtra.ts +93 -96
- package/src/TuplePathPattern.ts +54 -43
- package/src/Unique.ts +9 -15
- package/src/Values.ts +26 -30
- package/src/bun/BunBundle.ts +27 -73
- package/src/bun/BunImportTrackerPlugin.ts +67 -65
- package/src/bun/BunRoute.ts +12 -31
- package/src/bun/BunRuntime.ts +3 -10
- package/src/bun/BunServer.ts +50 -60
- package/src/bun/BunVirtualFilesPlugin.ts +1 -4
- package/src/bun/_BunEnhancedResolve.ts +17 -42
- package/src/bun/_empty.html +0 -1
- package/src/bundler/Bundle.ts +20 -36
- package/src/bundler/BundleFiles.ts +36 -56
- package/src/client/Overlay.ts +1 -2
- package/src/client/ScrollState.ts +5 -9
- package/src/client/index.ts +10 -13
- package/src/datastar/actions/fetch.ts +29 -48
- package/src/datastar/actions/peek.ts +1 -5
- package/src/datastar/actions/setAll.ts +2 -2
- package/src/datastar/actions/toggleAll.ts +2 -2
- package/src/datastar/attributes/attr.ts +17 -18
- package/src/datastar/attributes/bind.ts +41 -61
- package/src/datastar/attributes/class.ts +2 -5
- package/src/datastar/attributes/computed.ts +2 -10
- package/src/datastar/attributes/effect.ts +1 -2
- package/src/datastar/attributes/indicator.ts +2 -8
- package/src/datastar/attributes/init.ts +2 -10
- package/src/datastar/attributes/jsonSignals.ts +1 -6
- package/src/datastar/attributes/on.ts +4 -13
- package/src/datastar/attributes/onIntersect.ts +10 -22
- package/src/datastar/attributes/onInterval.ts +2 -10
- package/src/datastar/attributes/onSignalPatch.ts +18 -28
- package/src/datastar/attributes/ref.ts +1 -2
- package/src/datastar/attributes/show.ts +1 -2
- package/src/datastar/attributes/signals.ts +1 -5
- package/src/datastar/attributes/style.ts +6 -12
- package/src/datastar/attributes/text.ts +1 -2
- package/src/datastar/engine.ts +102 -158
- package/src/datastar/index.ts +2 -2
- package/src/datastar/utils.ts +16 -51
- package/src/datastar/watchers/patchElements.ts +35 -93
- package/src/datastar/watchers/patchSignals.ts +1 -2
- package/src/experimental/EncryptedCookies.ts +81 -175
- package/src/experimental/index.ts +0 -1
- package/src/hyper/Hyper.ts +14 -33
- package/src/hyper/HyperHtml.ts +13 -10
- package/src/hyper/HyperNode.ts +2 -7
- package/src/hyper/HyperRoute.ts +2 -5
- package/src/hyper/jsx-runtime.ts +2 -10
- package/src/hyper/jsx.d.ts +171 -440
- package/src/lint/plugin.js +276 -0
- package/src/node/NodeFileSystem.ts +140 -202
- package/src/node/NodeUtils.ts +1 -3
- package/src/testing/TestLogger.ts +9 -22
- package/src/testing/index.ts +0 -1
- package/src/testing/utils.ts +30 -31
- package/src/x/cloudflare/CloudflareTunnel.ts +53 -65
- package/src/x/datastar/Datastar.ts +3 -10
- package/src/x/datastar/index.ts +1 -3
- package/src/x/datastar/jsx-datastar.d.ts +1 -4
- package/src/x/tailwind/TailwindPlugin.ts +119 -112
- package/src/x/tailwind/compile.ts +10 -33
- package/src/x/tailwind/plugin.ts +2 -2
- package/src/HttpAppExtra.ts +0 -478
- package/src/HttpUtils.ts +0 -17
- package/src/bun/BunPlatformHttpServer.ts +0 -88
- package/src/bun/BunServerRequest.ts +0 -396
- package/src/bundler/BundleHttp.ts +0 -259
- package/src/experimental/SseHttpResponse.ts +0 -55
- package/src/middlewares/BasicAuthMiddleware.ts +0 -36
- package/src/middlewares/index.ts +0 -1
- package/src/testing/TestHttpClient.ts +0 -148
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { watcher } from "../engine.ts"
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
supportsViewTransitions,
|
|
7
|
-
} from "../utils.ts"
|
|
8
|
-
|
|
9
|
-
const isValidType = <T extends readonly string[]>(
|
|
10
|
-
arr: T,
|
|
11
|
-
value: string,
|
|
12
|
-
): value is T[number] => (arr as readonly string[]).includes(value)
|
|
1
|
+
import { watcher, type HTMLOrSVG, type WatcherContext } from "../engine.ts"
|
|
2
|
+
import { aliasify, isHTMLOrSVG, supportsViewTransitions } from "../utils.ts"
|
|
3
|
+
|
|
4
|
+
const isValidType = <T extends readonly string[]>(arr: T, value: string): value is T[number] =>
|
|
5
|
+
(arr as ReadonlyArray<string>).includes(value)
|
|
13
6
|
|
|
14
7
|
const PATCH_MODES = [
|
|
15
8
|
"remove",
|
|
@@ -38,13 +31,7 @@ watcher({
|
|
|
38
31
|
name: "datastar-patch-elements",
|
|
39
32
|
apply(
|
|
40
33
|
ctx,
|
|
41
|
-
{
|
|
42
|
-
selector = "",
|
|
43
|
-
mode = "outer",
|
|
44
|
-
namespace = "html",
|
|
45
|
-
useViewTransition = "",
|
|
46
|
-
elements = "",
|
|
47
|
-
},
|
|
34
|
+
{ selector = "", mode = "outer", namespace = "html", useViewTransition = "", elements = "" },
|
|
48
35
|
) {
|
|
49
36
|
if (!isValidType(PATCH_MODES, mode)) {
|
|
50
37
|
throw ctx.error("PatchElementsInvalidMode", { mode })
|
|
@@ -78,27 +65,16 @@ const onPatchElements = (
|
|
|
78
65
|
{ error }: WatcherContext,
|
|
79
66
|
{ selector, mode, namespace, elements }: PatchElementsArgs,
|
|
80
67
|
) => {
|
|
81
|
-
const elementsWithSvgsRemoved = elements.replace(
|
|
82
|
-
/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,
|
|
83
|
-
"",
|
|
84
|
-
)
|
|
68
|
+
const elementsWithSvgsRemoved = elements.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "")
|
|
85
69
|
const hasHtml = /<\/html>/.test(elementsWithSvgsRemoved)
|
|
86
70
|
const hasHead = /<\/head>/.test(elementsWithSvgsRemoved)
|
|
87
71
|
const hasBody = /<\/body>/.test(elementsWithSvgsRemoved)
|
|
88
72
|
|
|
89
|
-
const wrapperTag = namespace === "svg"
|
|
90
|
-
|
|
91
|
-
: namespace === "mathml"
|
|
92
|
-
? "math"
|
|
93
|
-
: ""
|
|
94
|
-
const wrappedEls = wrapperTag
|
|
95
|
-
? `<${wrapperTag}>${elements}</${wrapperTag}>`
|
|
96
|
-
: elements
|
|
73
|
+
const wrapperTag = namespace === "svg" ? "svg" : namespace === "mathml" ? "math" : ""
|
|
74
|
+
const wrappedEls = wrapperTag ? `<${wrapperTag}>${elements}</${wrapperTag}>` : elements
|
|
97
75
|
|
|
98
76
|
const newDocument = new DOMParser().parseFromString(
|
|
99
|
-
hasHtml || hasHead || hasBody
|
|
100
|
-
? elements
|
|
101
|
-
: `<body><template>${wrappedEls}</template></body>`,
|
|
77
|
+
hasHtml || hasHead || hasBody ? elements : `<body><template>${wrappedEls}</template></body>`,
|
|
102
78
|
"text/html",
|
|
103
79
|
)
|
|
104
80
|
|
|
@@ -113,10 +89,7 @@ const onPatchElements = (
|
|
|
113
89
|
} else if (hasBody) {
|
|
114
90
|
newContent.appendChild(newDocument.body)
|
|
115
91
|
} else if (wrapperTag) {
|
|
116
|
-
const wrapperEl = newDocument
|
|
117
|
-
.querySelector("template")!
|
|
118
|
-
.content
|
|
119
|
-
.querySelector(wrapperTag)!
|
|
92
|
+
const wrapperEl = newDocument.querySelector("template")!.content.querySelector(wrapperTag)!
|
|
120
93
|
for (const child of wrapperEl.childNodes) {
|
|
121
94
|
newContent.appendChild(child)
|
|
122
95
|
}
|
|
@@ -162,9 +135,8 @@ for (const script of document.querySelectorAll("script")) {
|
|
|
162
135
|
}
|
|
163
136
|
|
|
164
137
|
const execute = (target: Element): void => {
|
|
165
|
-
const elScripts =
|
|
166
|
-
? [target]
|
|
167
|
-
: target.querySelectorAll("script")
|
|
138
|
+
const elScripts =
|
|
139
|
+
target instanceof HTMLScriptElement ? [target] : target.querySelectorAll("script")
|
|
168
140
|
for (const old of elScripts) {
|
|
169
141
|
if (!scripts.has(old)) {
|
|
170
142
|
const script = document.createElement("script")
|
|
@@ -235,11 +207,11 @@ const morph = (
|
|
|
235
207
|
mode: "outer" | "inner" = "outer",
|
|
236
208
|
): void => {
|
|
237
209
|
if (
|
|
238
|
-
(isHTMLOrSVG(oldElt)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
210
|
+
(isHTMLOrSVG(oldElt) &&
|
|
211
|
+
isHTMLOrSVG(newContent) &&
|
|
212
|
+
(oldElt as HTMLOrSVG).hasAttribute(aliasedIgnoreMorph) &&
|
|
213
|
+
(newContent as HTMLOrSVG).hasAttribute(aliasedIgnoreMorph)) ||
|
|
214
|
+
oldElt.parentElement?.closest(aliasedIgnoreMorphAttr)
|
|
243
215
|
) {
|
|
244
216
|
return
|
|
245
217
|
}
|
|
@@ -286,12 +258,7 @@ const morph = (
|
|
|
286
258
|
populateIdMapWithTree(parent, oldIdElements)
|
|
287
259
|
populateIdMapWithTree(normalizedElt, newIdElements)
|
|
288
260
|
|
|
289
|
-
morphChildren(
|
|
290
|
-
parent,
|
|
291
|
-
normalizedElt,
|
|
292
|
-
mode === "outer" ? oldElt : null,
|
|
293
|
-
oldElt.nextSibling,
|
|
294
|
-
)
|
|
261
|
+
morphChildren(parent, normalizedElt, mode === "outer" ? oldElt : null, oldElt.nextSibling)
|
|
295
262
|
|
|
296
263
|
ctxPantry.remove()
|
|
297
264
|
}
|
|
@@ -302,10 +269,7 @@ const morphChildren = (
|
|
|
302
269
|
insertionPoint: Node | null = null,
|
|
303
270
|
endPoint: Node | null = null,
|
|
304
271
|
): void => {
|
|
305
|
-
if (
|
|
306
|
-
oldParent instanceof HTMLTemplateElement
|
|
307
|
-
&& newParent instanceof HTMLTemplateElement
|
|
308
|
-
) {
|
|
272
|
+
if (oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement) {
|
|
309
273
|
oldParent = oldParent.content as unknown as Element
|
|
310
274
|
newParent = newParent.content as unknown as Element
|
|
311
275
|
}
|
|
@@ -373,11 +337,7 @@ const morphChildren = (
|
|
|
373
337
|
}
|
|
374
338
|
}
|
|
375
339
|
|
|
376
|
-
const findBestMatch = (
|
|
377
|
-
node: Node,
|
|
378
|
-
startPoint: Node | null,
|
|
379
|
-
endPoint: Node | null,
|
|
380
|
-
): Node | null => {
|
|
340
|
+
const findBestMatch = (node: Node, startPoint: Node | null, endPoint: Node | null): Node | null => {
|
|
381
341
|
let bestMatch: Node | null | undefined = null
|
|
382
342
|
let nextSibling = node.nextSibling
|
|
383
343
|
let siblingSoftMatchCount = 0
|
|
@@ -434,15 +394,12 @@ const findBestMatch = (
|
|
|
434
394
|
}
|
|
435
395
|
|
|
436
396
|
const isSoftMatch = (oldNode: Node, newNode: Node): boolean =>
|
|
437
|
-
oldNode.nodeType === newNode.nodeType
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|| (oldNode as Element).id === (newNode as Element).id)
|
|
397
|
+
oldNode.nodeType === newNode.nodeType &&
|
|
398
|
+
(oldNode as Element).tagName === (newNode as Element).tagName &&
|
|
399
|
+
(!(oldNode as Element).id || (oldNode as Element).id === (newNode as Element).id)
|
|
441
400
|
|
|
442
401
|
const removeNode = (node: Node): void => {
|
|
443
|
-
ctxIdMap.has(node)
|
|
444
|
-
? moveBefore(ctxPantry, node, null)
|
|
445
|
-
: node.parentNode?.removeChild(node)
|
|
402
|
+
ctxIdMap.has(node) ? moveBefore(ctxPantry, node, null) : node.parentNode?.removeChild(node)
|
|
446
403
|
}
|
|
447
404
|
|
|
448
405
|
const moveBefore: (parentNode: Node, node: Node, after: Node | null) => void =
|
|
@@ -451,35 +408,26 @@ const moveBefore: (parentNode: Node, node: Node, after: Node | null) => void =
|
|
|
451
408
|
|
|
452
409
|
const aliasedPreserveAttr = aliasify("preserve-attr")
|
|
453
410
|
|
|
454
|
-
const morphNode = (
|
|
455
|
-
oldNode: Node,
|
|
456
|
-
newNode: Node,
|
|
457
|
-
): Node => {
|
|
411
|
+
const morphNode = (oldNode: Node, newNode: Node): Node => {
|
|
458
412
|
const type = newNode.nodeType
|
|
459
413
|
|
|
460
414
|
if (type === 1) {
|
|
461
415
|
const oldElt = oldNode as Element
|
|
462
416
|
const newElt = newNode as Element
|
|
463
417
|
const shouldScopeChildren = oldElt.hasAttribute("data-scope-children")
|
|
464
|
-
if (
|
|
465
|
-
oldElt.hasAttribute(aliasedIgnoreMorph)
|
|
466
|
-
&& newElt.hasAttribute(aliasedIgnoreMorph)
|
|
467
|
-
) {
|
|
418
|
+
if (oldElt.hasAttribute(aliasedIgnoreMorph) && newElt.hasAttribute(aliasedIgnoreMorph)) {
|
|
468
419
|
return oldNode
|
|
469
420
|
}
|
|
470
421
|
|
|
471
422
|
if (
|
|
472
|
-
oldElt instanceof HTMLInputElement
|
|
473
|
-
|
|
474
|
-
|
|
423
|
+
oldElt instanceof HTMLInputElement &&
|
|
424
|
+
newElt instanceof HTMLInputElement &&
|
|
425
|
+
newElt.type !== "file"
|
|
475
426
|
) {
|
|
476
427
|
if (newElt.getAttribute("value") !== oldElt.getAttribute("value")) {
|
|
477
428
|
oldElt.value = newElt.getAttribute("value") ?? ""
|
|
478
429
|
}
|
|
479
|
-
} else if (
|
|
480
|
-
oldElt instanceof HTMLTextAreaElement
|
|
481
|
-
&& newElt instanceof HTMLTextAreaElement
|
|
482
|
-
) {
|
|
430
|
+
} else if (oldElt instanceof HTMLTextAreaElement && newElt instanceof HTMLTextAreaElement) {
|
|
483
431
|
if (newElt.value !== oldElt.value) {
|
|
484
432
|
oldElt.value = newElt.value
|
|
485
433
|
}
|
|
@@ -488,16 +436,12 @@ const morphNode = (
|
|
|
488
436
|
}
|
|
489
437
|
}
|
|
490
438
|
|
|
491
|
-
const preserveAttrs = (
|
|
492
|
-
|
|
439
|
+
const preserveAttrs = ((newNode as HTMLElement).getAttribute(aliasedPreserveAttr) ?? "").split(
|
|
440
|
+
" ",
|
|
493
441
|
)
|
|
494
|
-
.split(" ")
|
|
495
442
|
|
|
496
443
|
for (const { name, value } of newElt.attributes) {
|
|
497
|
-
if (
|
|
498
|
-
oldElt.getAttribute(name) !== value
|
|
499
|
-
&& !preserveAttrs.includes(name)
|
|
500
|
-
) {
|
|
444
|
+
if (oldElt.getAttribute(name) !== value && !preserveAttrs.includes(name)) {
|
|
501
445
|
oldElt.setAttribute(name, value)
|
|
502
446
|
}
|
|
503
447
|
}
|
|
@@ -518,9 +462,7 @@ const morphNode = (
|
|
|
518
462
|
}
|
|
519
463
|
|
|
520
464
|
if (shouldScopeChildren) {
|
|
521
|
-
oldElt.dispatchEvent(
|
|
522
|
-
new CustomEvent("datastar:scope-children", { bubbles: false }),
|
|
523
|
-
)
|
|
465
|
+
oldElt.dispatchEvent(new CustomEvent("datastar:scope-children", { bubbles: false }))
|
|
524
466
|
}
|
|
525
467
|
}
|
|
526
468
|
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
Cookies,
|
|
3
|
-
HttpApp,
|
|
4
|
-
HttpServerResponse,
|
|
5
|
-
} from "@effect/platform"
|
|
6
|
-
import {
|
|
7
|
-
Effect,
|
|
8
|
-
pipe,
|
|
9
|
-
} from "effect"
|
|
1
|
+
import * as Cookies from "../Cookies.ts"
|
|
10
2
|
import * as Config from "effect/Config"
|
|
11
3
|
import * as Context from "effect/Context"
|
|
12
4
|
import * as Data from "effect/Data"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
13
6
|
import * as Layer from "effect/Layer"
|
|
14
7
|
|
|
15
8
|
type CookieValue =
|
|
@@ -19,43 +12,33 @@ type CookieValue =
|
|
|
19
12
|
| null
|
|
20
13
|
| undefined
|
|
21
14
|
| {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
[key: string]:
|
|
16
|
+
| CookieValue
|
|
17
|
+
// some libraries, like XState, contain unknown in type
|
|
18
|
+
// that is serializable
|
|
19
|
+
| unknown
|
|
20
|
+
}
|
|
28
21
|
| CookieValue[]
|
|
29
22
|
|
|
30
|
-
export class EncryptedCookiesError
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}>
|
|
35
|
-
{}
|
|
23
|
+
export class EncryptedCookiesError extends Data.TaggedError("EncryptedCookiesError")<{
|
|
24
|
+
cause: unknown
|
|
25
|
+
cookie?: Cookies.Cookie
|
|
26
|
+
}> {}
|
|
36
27
|
|
|
37
28
|
export class EncryptedCookies extends Context.Tag("EncryptedCookies")<
|
|
38
29
|
EncryptedCookies,
|
|
39
30
|
{
|
|
40
|
-
encrypt: (
|
|
41
|
-
|
|
42
|
-
) => Effect.Effect<
|
|
43
|
-
|
|
44
|
-
encryptedValue: string,
|
|
45
|
-
) => Effect.Effect<CookieValue, EncryptedCookiesError>
|
|
46
|
-
encryptCookie: (
|
|
47
|
-
cookie: Cookies.Cookie,
|
|
48
|
-
) => Effect.Effect<Cookies.Cookie, EncryptedCookiesError>
|
|
49
|
-
decryptCookie: (
|
|
50
|
-
cookie: Cookies.Cookie,
|
|
51
|
-
) => Effect.Effect<Cookies.Cookie, EncryptedCookiesError>
|
|
31
|
+
encrypt: (value: CookieValue) => Effect.Effect<string, EncryptedCookiesError>
|
|
32
|
+
decrypt: (encryptedValue: string) => Effect.Effect<CookieValue, EncryptedCookiesError>
|
|
33
|
+
encryptCookie: (cookie: Cookies.Cookie) => Effect.Effect<Cookies.Cookie, EncryptedCookiesError>
|
|
34
|
+
decryptCookie: (cookie: Cookies.Cookie) => Effect.Effect<Cookies.Cookie, EncryptedCookiesError>
|
|
52
35
|
}
|
|
53
36
|
>() {}
|
|
54
37
|
|
|
55
38
|
export function layer(options: { secret: string }) {
|
|
56
39
|
return Layer.effect(
|
|
57
40
|
EncryptedCookies,
|
|
58
|
-
Effect.gen(function*() {
|
|
41
|
+
Effect.gen(function* () {
|
|
59
42
|
const keyMaterial = yield* deriveKeyMaterial(options.secret)
|
|
60
43
|
|
|
61
44
|
// Pre-derive both keys once
|
|
@@ -63,39 +46,28 @@ export function layer(options: { secret: string }) {
|
|
|
63
46
|
const decryptKey = yield* deriveKey(keyMaterial, ["decrypt"])
|
|
64
47
|
|
|
65
48
|
return EncryptedCookies.of({
|
|
66
|
-
encrypt: (value: CookieValue) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
encryptCookie: (cookie: Cookies.Cookie) =>
|
|
71
|
-
encryptCookieWithDerivedKey(cookie, encryptKey),
|
|
72
|
-
decryptCookie: (cookie: Cookies.Cookie) =>
|
|
73
|
-
decryptCookieWithDerivedKey(cookie, decryptKey),
|
|
49
|
+
encrypt: (value: CookieValue) => encryptWithDerivedKey(value, encryptKey),
|
|
50
|
+
decrypt: (encryptedValue: string) => decryptWithDerivedKey(encryptedValue, decryptKey),
|
|
51
|
+
encryptCookie: (cookie: Cookies.Cookie) => encryptCookieWithDerivedKey(cookie, encryptKey),
|
|
52
|
+
decryptCookie: (cookie: Cookies.Cookie) => decryptCookieWithDerivedKey(cookie, decryptKey),
|
|
74
53
|
})
|
|
75
54
|
}),
|
|
76
55
|
)
|
|
77
56
|
}
|
|
78
57
|
|
|
79
58
|
export function layerConfig(name = "SECRET_KEY_BASE") {
|
|
80
|
-
return Effect
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Effect.catchAll((err) => {
|
|
90
|
-
return Effect.dieMessage(
|
|
91
|
-
"SECRET_KEY_BASE must be at least 40 characters",
|
|
92
|
-
)
|
|
93
|
-
}),
|
|
94
|
-
)
|
|
59
|
+
return Effect.gen(function* () {
|
|
60
|
+
const secret = yield* Config.nonEmptyString(name).pipe(
|
|
61
|
+
Effect.flatMap((value) => {
|
|
62
|
+
return value.length < 40 ? Effect.fail(new Error("ba")) : Effect.succeed(value)
|
|
63
|
+
}),
|
|
64
|
+
Effect.catchAll((err) => {
|
|
65
|
+
return Effect.dieMessage("SECRET_KEY_BASE must be at least 40 characters")
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
95
68
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.pipe(Layer.unwrapEffect)
|
|
69
|
+
return layer({ secret })
|
|
70
|
+
}).pipe(Layer.unwrapEffect)
|
|
99
71
|
}
|
|
100
72
|
|
|
101
73
|
function encodeToBase64Segments(
|
|
@@ -103,29 +75,21 @@ function encodeToBase64Segments(
|
|
|
103
75
|
iv: Uint8Array,
|
|
104
76
|
authTag: Uint8Array,
|
|
105
77
|
): string {
|
|
106
|
-
return [
|
|
107
|
-
base64urlEncode(ciphertext),
|
|
108
|
-
base64urlEncode(iv),
|
|
109
|
-
base64urlEncode(authTag),
|
|
110
|
-
]
|
|
111
|
-
.join(".")
|
|
78
|
+
return [base64urlEncode(ciphertext), base64urlEncode(iv), base64urlEncode(authTag)].join(".")
|
|
112
79
|
}
|
|
113
80
|
|
|
114
81
|
function base64urlEncode(data: Uint8Array): string {
|
|
115
82
|
const base64 = btoa(String.fromCharCode(...data))
|
|
116
|
-
return base64
|
|
117
|
-
.replace(/\+/g, "-")
|
|
118
|
-
.replace(/\//g, "_")
|
|
119
|
-
.replace(/=/g, "")
|
|
83
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
|
120
84
|
}
|
|
121
85
|
|
|
122
86
|
function decodeFromBase64Segments(
|
|
123
|
-
segments: string
|
|
87
|
+
segments: Array<string>,
|
|
124
88
|
): Effect.Effect<
|
|
125
89
|
{ ciphertext: Uint8Array; iv: Uint8Array; authTag: Uint8Array },
|
|
126
90
|
EncryptedCookiesError
|
|
127
91
|
> {
|
|
128
|
-
return Effect.gen(function*() {
|
|
92
|
+
return Effect.gen(function* () {
|
|
129
93
|
const [ciphertextB64, ivB64, authTagB64] = segments
|
|
130
94
|
|
|
131
95
|
const ciphertext = yield* Effect.try({
|
|
@@ -149,16 +113,14 @@ function decodeFromBase64Segments(
|
|
|
149
113
|
|
|
150
114
|
function base64urlDecode(data: string): Uint8Array {
|
|
151
115
|
// Convert base64url back to standard base64
|
|
152
|
-
let base64 = data
|
|
153
|
-
.replace(/-/g, "+")
|
|
154
|
-
.replace(/_/g, "/")
|
|
116
|
+
let base64 = data.replace(/-/g, "+").replace(/_/g, "/")
|
|
155
117
|
|
|
156
118
|
// Add padding if needed
|
|
157
119
|
while (base64.length % 4) {
|
|
158
120
|
base64 += "="
|
|
159
121
|
}
|
|
160
122
|
|
|
161
|
-
return Uint8Array.from(atob(base64), c => c.charCodeAt(0))
|
|
123
|
+
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
|
|
162
124
|
}
|
|
163
125
|
|
|
164
126
|
/**
|
|
@@ -168,7 +130,7 @@ function encryptWithDerivedKey(
|
|
|
168
130
|
value: CookieValue,
|
|
169
131
|
derivedKey: CryptoKey,
|
|
170
132
|
): Effect.Effect<string, EncryptedCookiesError> {
|
|
171
|
-
return Effect.gen(function*() {
|
|
133
|
+
return Effect.gen(function* () {
|
|
172
134
|
if (value === null || value === undefined) {
|
|
173
135
|
return yield* Effect.fail(
|
|
174
136
|
new EncryptedCookiesError({
|
|
@@ -181,12 +143,7 @@ function encryptWithDerivedKey(
|
|
|
181
143
|
const data = new TextEncoder().encode(JSON.stringify(value))
|
|
182
144
|
|
|
183
145
|
const encrypted = yield* Effect.tryPromise({
|
|
184
|
-
try: () =>
|
|
185
|
-
crypto.subtle.encrypt(
|
|
186
|
-
{ name: "AES-GCM", iv },
|
|
187
|
-
derivedKey,
|
|
188
|
-
data,
|
|
189
|
-
),
|
|
146
|
+
try: () => crypto.subtle.encrypt({ name: "AES-GCM", iv }, derivedKey, data),
|
|
190
147
|
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
191
148
|
})
|
|
192
149
|
|
|
@@ -203,7 +160,7 @@ export function encrypt(
|
|
|
203
160
|
value: CookieValue,
|
|
204
161
|
options: { key: CryptoKey } | { secret: string },
|
|
205
162
|
): Effect.Effect<string, EncryptedCookiesError> {
|
|
206
|
-
return Effect.gen(function*() {
|
|
163
|
+
return Effect.gen(function* () {
|
|
207
164
|
if ("key" in options) {
|
|
208
165
|
return yield* encryptWithDerivedKey(value, options.key)
|
|
209
166
|
}
|
|
@@ -218,10 +175,8 @@ function decryptWithDerivedKey(
|
|
|
218
175
|
encryptedValue: string,
|
|
219
176
|
derivedKey: CryptoKey,
|
|
220
177
|
): Effect.Effect<CookieValue, EncryptedCookiesError> {
|
|
221
|
-
return Effect.gen(function*() {
|
|
222
|
-
if (
|
|
223
|
-
!encryptedValue || encryptedValue === null || encryptedValue === undefined
|
|
224
|
-
) {
|
|
178
|
+
return Effect.gen(function* () {
|
|
179
|
+
if (!encryptedValue || encryptedValue === null || encryptedValue === undefined) {
|
|
225
180
|
return yield* Effect.fail(
|
|
226
181
|
new EncryptedCookiesError({
|
|
227
182
|
cause: "Cannot decrypt null, undefined, or empty value",
|
|
@@ -238,9 +193,7 @@ function decryptWithDerivedKey(
|
|
|
238
193
|
)
|
|
239
194
|
}
|
|
240
195
|
|
|
241
|
-
const { ciphertext, iv, authTag } = yield* decodeFromBase64Segments(
|
|
242
|
-
segments,
|
|
243
|
-
)
|
|
196
|
+
const { ciphertext, iv, authTag } = yield* decodeFromBase64Segments(segments)
|
|
244
197
|
|
|
245
198
|
const encryptedData = new Uint8Array(ciphertext.length + authTag.length)
|
|
246
199
|
encryptedData.set(ciphertext)
|
|
@@ -248,11 +201,7 @@ function decryptWithDerivedKey(
|
|
|
248
201
|
|
|
249
202
|
const decrypted = yield* Effect.tryPromise({
|
|
250
203
|
try: () =>
|
|
251
|
-
crypto.subtle.decrypt(
|
|
252
|
-
{ name: "AES-GCM", iv: iv.slice(0) },
|
|
253
|
-
derivedKey,
|
|
254
|
-
encryptedData,
|
|
255
|
-
),
|
|
204
|
+
crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.slice(0) }, derivedKey, encryptedData),
|
|
256
205
|
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
257
206
|
})
|
|
258
207
|
|
|
@@ -269,19 +218,16 @@ function encryptCookieWithDerivedKey(
|
|
|
269
218
|
cookie: Cookies.Cookie,
|
|
270
219
|
derivedKey: CryptoKey,
|
|
271
220
|
): Effect.Effect<Cookies.Cookie, EncryptedCookiesError> {
|
|
272
|
-
return Effect.gen(function*() {
|
|
273
|
-
const encryptedValue = yield* encryptWithDerivedKey(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
)
|
|
277
|
-
.pipe(
|
|
278
|
-
Effect.mapError(error =>
|
|
221
|
+
return Effect.gen(function* () {
|
|
222
|
+
const encryptedValue = yield* encryptWithDerivedKey(cookie.value, derivedKey).pipe(
|
|
223
|
+
Effect.mapError(
|
|
224
|
+
(error) =>
|
|
279
225
|
new EncryptedCookiesError({
|
|
280
226
|
cause: error.cause,
|
|
281
227
|
cookie,
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
)
|
|
285
231
|
return Cookies.unsafeMakeCookie(cookie.name, encryptedValue, cookie.options)
|
|
286
232
|
})
|
|
287
233
|
}
|
|
@@ -289,24 +235,17 @@ function decryptCookieWithDerivedKey(
|
|
|
289
235
|
cookie: Cookies.Cookie,
|
|
290
236
|
derivedKey: CryptoKey,
|
|
291
237
|
): Effect.Effect<Cookies.Cookie, EncryptedCookiesError> {
|
|
292
|
-
return Effect.gen(function*() {
|
|
293
|
-
const decryptedValue = yield* decryptWithDerivedKey(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
297
|
-
.pipe(
|
|
298
|
-
Effect.mapError(error =>
|
|
238
|
+
return Effect.gen(function* () {
|
|
239
|
+
const decryptedValue = yield* decryptWithDerivedKey(cookie.value, derivedKey).pipe(
|
|
240
|
+
Effect.mapError(
|
|
241
|
+
(error) =>
|
|
299
242
|
new EncryptedCookiesError({
|
|
300
243
|
cause: error.cause,
|
|
301
244
|
cookie,
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
)
|
|
305
|
-
return Cookies.unsafeMakeCookie(
|
|
306
|
-
cookie.name,
|
|
307
|
-
JSON.stringify(decryptedValue),
|
|
308
|
-
cookie.options,
|
|
245
|
+
}),
|
|
246
|
+
),
|
|
309
247
|
)
|
|
248
|
+
return Cookies.unsafeMakeCookie(cookie.name, JSON.stringify(decryptedValue), cookie.options)
|
|
310
249
|
})
|
|
311
250
|
}
|
|
312
251
|
|
|
@@ -314,22 +253,22 @@ export function encryptCookie(
|
|
|
314
253
|
cookie: Cookies.Cookie,
|
|
315
254
|
options: { key: CryptoKey } | { secret: string },
|
|
316
255
|
): Effect.Effect<Cookies.Cookie, EncryptedCookiesError> {
|
|
317
|
-
return Effect.gen(function*() {
|
|
256
|
+
return Effect.gen(function* () {
|
|
318
257
|
if ("key" in options) {
|
|
319
258
|
return yield* encryptCookieWithDerivedKey(cookie, options.key)
|
|
320
259
|
}
|
|
321
260
|
|
|
322
261
|
const encryptedValue = yield* encrypt(cookie.value, {
|
|
323
262
|
secret: options.secret,
|
|
324
|
-
})
|
|
325
|
-
.
|
|
326
|
-
|
|
263
|
+
}).pipe(
|
|
264
|
+
Effect.mapError(
|
|
265
|
+
(error) =>
|
|
327
266
|
new EncryptedCookiesError({
|
|
328
267
|
cause: error.cause,
|
|
329
268
|
cookie,
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
|
|
269
|
+
}),
|
|
270
|
+
),
|
|
271
|
+
)
|
|
333
272
|
return Cookies.unsafeMakeCookie(cookie.name, encryptedValue, cookie.options)
|
|
334
273
|
})
|
|
335
274
|
}
|
|
@@ -338,27 +277,23 @@ export function decryptCookie(
|
|
|
338
277
|
cookie: Cookies.Cookie,
|
|
339
278
|
options: { key: CryptoKey } | { secret: string },
|
|
340
279
|
): Effect.Effect<Cookies.Cookie, EncryptedCookiesError> {
|
|
341
|
-
return Effect.gen(function*() {
|
|
280
|
+
return Effect.gen(function* () {
|
|
342
281
|
if ("key" in options) {
|
|
343
282
|
return yield* decryptCookieWithDerivedKey(cookie, options.key)
|
|
344
283
|
}
|
|
345
284
|
|
|
346
285
|
const decryptedValue = yield* decrypt(cookie.value, {
|
|
347
286
|
secret: options.secret,
|
|
348
|
-
})
|
|
349
|
-
.
|
|
350
|
-
|
|
287
|
+
}).pipe(
|
|
288
|
+
Effect.mapError(
|
|
289
|
+
(error) =>
|
|
351
290
|
new EncryptedCookiesError({
|
|
352
291
|
cause: error.cause,
|
|
353
292
|
cookie,
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
)
|
|
357
|
-
return Cookies.unsafeMakeCookie(
|
|
358
|
-
cookie.name,
|
|
359
|
-
JSON.stringify(decryptedValue),
|
|
360
|
-
cookie.options,
|
|
293
|
+
}),
|
|
294
|
+
),
|
|
361
295
|
)
|
|
296
|
+
return Cookies.unsafeMakeCookie(cookie.name, JSON.stringify(decryptedValue), cookie.options)
|
|
362
297
|
})
|
|
363
298
|
}
|
|
364
299
|
|
|
@@ -366,7 +301,7 @@ export function decrypt(
|
|
|
366
301
|
encryptedValue: string,
|
|
367
302
|
options: { key: CryptoKey } | { secret: string },
|
|
368
303
|
): Effect.Effect<CookieValue, EncryptedCookiesError> {
|
|
369
|
-
return Effect.gen(function*() {
|
|
304
|
+
return Effect.gen(function* () {
|
|
370
305
|
if ("key" in options) {
|
|
371
306
|
return yield* decryptWithDerivedKey(encryptedValue, options.key)
|
|
372
307
|
}
|
|
@@ -377,21 +312,15 @@ export function decrypt(
|
|
|
377
312
|
})
|
|
378
313
|
}
|
|
379
314
|
|
|
380
|
-
function deriveKeyMaterial(
|
|
381
|
-
|
|
382
|
-
): Effect.Effect<CryptoKey, EncryptedCookiesError> {
|
|
383
|
-
return Effect.gen(function*() {
|
|
315
|
+
function deriveKeyMaterial(secret: string): Effect.Effect<CryptoKey, EncryptedCookiesError> {
|
|
316
|
+
return Effect.gen(function* () {
|
|
384
317
|
const encoder = new TextEncoder()
|
|
385
318
|
|
|
386
319
|
const keyMaterial = yield* Effect.tryPromise({
|
|
387
320
|
try: () =>
|
|
388
|
-
crypto.subtle.importKey(
|
|
389
|
-
"
|
|
390
|
-
|
|
391
|
-
{ name: "HKDF" },
|
|
392
|
-
false,
|
|
393
|
-
["deriveKey"],
|
|
394
|
-
),
|
|
321
|
+
crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HKDF" }, false, [
|
|
322
|
+
"deriveKey",
|
|
323
|
+
]),
|
|
395
324
|
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
396
325
|
})
|
|
397
326
|
|
|
@@ -401,9 +330,9 @@ function deriveKeyMaterial(
|
|
|
401
330
|
|
|
402
331
|
function deriveKey(
|
|
403
332
|
keyMaterial: CryptoKey,
|
|
404
|
-
usage: KeyUsage
|
|
333
|
+
usage: Array<KeyUsage>,
|
|
405
334
|
): Effect.Effect<CryptoKey, EncryptedCookiesError> {
|
|
406
|
-
return Effect.gen(function*() {
|
|
335
|
+
return Effect.gen(function* () {
|
|
407
336
|
const encoder = new TextEncoder()
|
|
408
337
|
|
|
409
338
|
const key = yield* Effect.tryPromise({
|
|
@@ -426,26 +355,3 @@ function deriveKey(
|
|
|
426
355
|
return key
|
|
427
356
|
})
|
|
428
357
|
}
|
|
429
|
-
|
|
430
|
-
// TODO something si wrong with return type
|
|
431
|
-
export function handleError<E>(
|
|
432
|
-
app: HttpApp.Default<E | EncryptedCookiesError>,
|
|
433
|
-
) {
|
|
434
|
-
return Effect.gen(function*() {
|
|
435
|
-
const res = yield* app.pipe(
|
|
436
|
-
Effect.catchTag("EncryptedCookiesError", (error) => {
|
|
437
|
-
return HttpServerResponse.empty()
|
|
438
|
-
}),
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
return res
|
|
442
|
-
})
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function generateFriendlyKey(bits = 128) {
|
|
446
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
447
|
-
const length = Math.ceil(bits / Math.log2(chars.length))
|
|
448
|
-
const bytes = crypto.getRandomValues(new Uint8Array(length))
|
|
449
|
-
|
|
450
|
-
return Array.from(bytes, b => chars[b % chars.length]).join("")
|
|
451
|
-
}
|