effect-start 0.17.0 → 0.17.2
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/dist/Commander.d.ts +103 -0
- package/dist/Commander.js +333 -0
- package/dist/ContentNegotiation.d.ts +13 -0
- package/dist/ContentNegotiation.js +364 -0
- package/dist/Development.d.ts +34 -0
- package/dist/Development.js +52 -0
- package/dist/Entity.d.ts +47 -0
- package/dist/Entity.js +224 -0
- package/dist/FileRouter.d.ts +61 -0
- package/dist/FileRouter.js +203 -0
- package/dist/FileRouterCodegen.d.ts +19 -0
- package/dist/FileRouterCodegen.js +176 -0
- package/dist/FileRouterPattern.d.ts +9 -0
- package/dist/FileRouterPattern.js +35 -0
- package/dist/Http.d.ts +37 -0
- package/dist/Http.js +92 -0
- package/dist/HttpAppExtra.d.ts +7 -0
- package/dist/HttpAppExtra.js +320 -0
- package/dist/HttpUtils.d.ts +3 -0
- package/dist/HttpUtils.js +11 -0
- package/dist/PathPattern.d.ts +134 -0
- package/dist/PathPattern.js +415 -0
- package/dist/Random.d.ts +5 -0
- package/dist/Random.js +49 -0
- package/dist/Route.d.ts +98 -0
- package/dist/Route.js +81 -0
- package/dist/RouteBody.d.ts +53 -0
- package/dist/RouteBody.js +67 -0
- package/dist/RouteHook.d.ts +12 -0
- package/dist/RouteHook.js +45 -0
- package/dist/RouteHttp.d.ts +21 -0
- package/dist/RouteHttp.js +260 -0
- package/dist/RouteHttpTracer.d.ts +10 -0
- package/dist/RouteHttpTracer.js +62 -0
- package/dist/RouteMount.d.ts +119 -0
- package/dist/RouteMount.js +77 -0
- package/dist/RouteSchema.d.ts +65 -0
- package/dist/RouteSchema.js +155 -0
- package/dist/RouteSse.d.ts +21 -0
- package/dist/RouteSse.js +85 -0
- package/dist/RouteTree.d.ts +56 -0
- package/dist/RouteTree.js +91 -0
- package/dist/RouteTrie.d.ts +20 -0
- package/dist/RouteTrie.js +157 -0
- package/dist/RouterPattern.d.ts +118 -0
- package/dist/RouterPattern.js +269 -0
- package/dist/SchemaExtra.d.ts +7 -0
- package/dist/SchemaExtra.js +74 -0
- package/dist/Start.d.ts +19 -0
- package/dist/Start.js +23 -0
- package/dist/StartApp.d.ts +19 -0
- package/dist/StartApp.js +21 -0
- package/dist/StreamExtra.d.ts +28 -0
- package/dist/StreamExtra.js +100 -0
- package/dist/TuplePathPattern.d.ts +9 -0
- package/dist/TuplePathPattern.js +63 -0
- package/dist/Values.d.ts +26 -0
- package/dist/Values.js +30 -0
- package/dist/bun/BunBundle.d.ts +12 -0
- package/dist/bun/BunBundle.js +145 -0
- package/dist/bun/BunHttpServer.d.ts +44 -0
- package/dist/bun/BunHttpServer.js +187 -0
- package/dist/bun/BunHttpServer_web.d.ts +60 -0
- package/dist/bun/BunHttpServer_web.js +252 -0
- package/dist/bun/BunImportTrackerPlugin.d.ts +13 -0
- package/dist/bun/BunImportTrackerPlugin.js +71 -0
- package/dist/bun/BunRoute.d.ts +49 -0
- package/dist/bun/BunRoute.js +131 -0
- package/dist/bun/BunRuntime.d.ts +1 -0
- package/dist/bun/BunRuntime.js +26 -0
- package/dist/bun/BunVirtualFilesPlugin.d.ts +4 -0
- package/dist/bun/BunVirtualFilesPlugin.js +40 -0
- package/dist/bun/_BunEnhancedResolve.d.ts +45 -0
- package/dist/bun/_BunEnhancedResolve.js +104 -0
- package/dist/bun/index.d.ts +4 -0
- package/dist/bun/index.js +4 -0
- package/dist/bundler/Bundle.d.ts +60 -0
- package/dist/bundler/Bundle.js +48 -0
- package/dist/bundler/BundleFiles.d.ts +13 -0
- package/dist/bundler/BundleFiles.js +94 -0
- package/dist/bundler/BundleHttp.d.ts +45 -0
- package/dist/bundler/BundleHttp.js +176 -0
- package/dist/client/Overlay.d.ts +2 -0
- package/dist/client/Overlay.js +32 -0
- package/dist/client/ScrollState.d.ts +6 -0
- package/dist/client/ScrollState.js +98 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +81 -0
- package/dist/experimental/EncryptedCookies.d.ts +51 -0
- package/dist/experimental/EncryptedCookies.js +243 -0
- package/dist/experimental/SseHttpResponse.d.ts +7 -0
- package/dist/experimental/SseHttpResponse.js +28 -0
- package/dist/experimental/index.d.ts +2 -0
- package/dist/experimental/index.js +2 -0
- package/dist/hyper/Hyper.d.ts +32 -0
- package/dist/hyper/Hyper.js +34 -0
- package/dist/hyper/HyperHtml.d.ts +23 -0
- package/dist/hyper/HyperHtml.js +144 -0
- package/dist/hyper/HyperNode.d.ts +14 -0
- package/dist/hyper/HyperNode.js +11 -0
- package/dist/hyper/HyperRoute.d.ts +8 -0
- package/dist/hyper/HyperRoute.js +32 -0
- package/dist/hyper/HyperRoute.test.d.ts +1 -0
- package/dist/hyper/HyperRoute.test.js +72 -0
- package/dist/hyper/index.d.ts +4 -0
- package/dist/hyper/index.js +4 -0
- package/dist/hyper/jsx-runtime.d.ts +7 -0
- package/dist/hyper/jsx-runtime.js +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/inference_check.d.ts +1 -0
- package/dist/inference_check.js +15 -0
- package/dist/middlewares/BasicAuthMiddleware.d.ts +8 -0
- package/dist/middlewares/BasicAuthMiddleware.js +22 -0
- package/dist/middlewares/index.d.ts +1 -0
- package/dist/middlewares/index.js +1 -0
- package/dist/node/FileSystem.d.ts +9 -0
- package/dist/node/FileSystem.js +440 -0
- package/dist/node/Utils.d.ts +1 -0
- package/dist/node/Utils.js +19 -0
- package/dist/repro_fail.d.ts +1 -0
- package/dist/repro_fail.js +14 -0
- package/dist/testing/TestHttpClient.d.ts +13 -0
- package/dist/testing/TestHttpClient.js +68 -0
- package/dist/testing/TestLogger.d.ts +13 -0
- package/dist/testing/TestLogger.js +29 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/utils.d.ts +9 -0
- package/dist/testing/utils.js +39 -0
- package/dist/x/cloudflare/CloudflareTunnel.d.ts +13 -0
- package/dist/x/cloudflare/CloudflareTunnel.js +43 -0
- package/dist/x/cloudflare/index.d.ts +1 -0
- package/dist/x/cloudflare/index.js +1 -0
- package/dist/x/datastar/Datastar.d.ts +6 -0
- package/dist/x/datastar/Datastar.js +46 -0
- package/dist/x/datastar/index.d.ts +2 -0
- package/dist/x/datastar/index.js +2 -0
- package/dist/x/tailwind/TailwindPlugin.d.ts +23 -0
- package/dist/x/tailwind/TailwindPlugin.js +219 -0
- package/dist/x/tailwind/compile.d.ts +19 -0
- package/dist/x/tailwind/compile.js +156 -0
- package/dist/x/tailwind/plugin.d.ts +2 -0
- package/dist/x/tailwind/plugin.js +15 -0
- package/package.json +68 -16
- package/src/RouteBody.test.ts +18 -0
- package/src/RouteBody.ts +126 -2
- package/src/x/tailwind/compile.ts +8 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const ScrollKey = "_BUNDLER_SCROLL";
|
|
2
|
+
/**
|
|
3
|
+
* Persist current scroll state to session storage.
|
|
4
|
+
* Scroll state is saved relatively to visible elements.
|
|
5
|
+
*/
|
|
6
|
+
export function persist() {
|
|
7
|
+
const anchors = [];
|
|
8
|
+
const step = window.innerHeight / 4;
|
|
9
|
+
for (let i = 1; i <= 3; i++) {
|
|
10
|
+
const y = step * i;
|
|
11
|
+
const element = document.elementFromPoint(0, y);
|
|
12
|
+
if (!element)
|
|
13
|
+
continue;
|
|
14
|
+
const target = element.id
|
|
15
|
+
? element
|
|
16
|
+
: element.closest("[id]") ?? element;
|
|
17
|
+
anchors.push({
|
|
18
|
+
selector: selectorFromElement(target),
|
|
19
|
+
offset: target.getBoundingClientRect().top,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const state = {
|
|
23
|
+
anchors,
|
|
24
|
+
scrollY: window.scrollY,
|
|
25
|
+
};
|
|
26
|
+
sessionStorage.setItem(ScrollKey, JSON.stringify(state));
|
|
27
|
+
}
|
|
28
|
+
export function restore() {
|
|
29
|
+
const timeout = 3000;
|
|
30
|
+
const tick = 50;
|
|
31
|
+
const raw = sessionStorage.getItem(ScrollKey);
|
|
32
|
+
if (!raw)
|
|
33
|
+
return;
|
|
34
|
+
sessionStorage.removeItem(ScrollKey);
|
|
35
|
+
const state = JSON.parse(raw);
|
|
36
|
+
const apply = () => {
|
|
37
|
+
for (const anchor of state.anchors) {
|
|
38
|
+
const element = document.querySelector(anchor.selector);
|
|
39
|
+
if (element) {
|
|
40
|
+
const rect = element.getBoundingClientRect();
|
|
41
|
+
const top = window.scrollY + rect.top - anchor.offset;
|
|
42
|
+
window.scrollTo({
|
|
43
|
+
top,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
window.scrollTo({
|
|
49
|
+
top: state.scrollY,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
let observer;
|
|
53
|
+
let stableTimer;
|
|
54
|
+
const deadline = setTimeout(() => {
|
|
55
|
+
observer.disconnect();
|
|
56
|
+
if (stableTimer)
|
|
57
|
+
clearTimeout(stableTimer);
|
|
58
|
+
apply();
|
|
59
|
+
}, timeout);
|
|
60
|
+
observer = new MutationObserver(() => {
|
|
61
|
+
if (stableTimer)
|
|
62
|
+
clearTimeout(stableTimer);
|
|
63
|
+
stableTimer = setTimeout(() => {
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
clearTimeout(deadline);
|
|
66
|
+
apply();
|
|
67
|
+
}, tick);
|
|
68
|
+
});
|
|
69
|
+
observer.observe(document.body, {
|
|
70
|
+
subtree: true,
|
|
71
|
+
childList: true,
|
|
72
|
+
attributes: true,
|
|
73
|
+
characterData: true,
|
|
74
|
+
});
|
|
75
|
+
stableTimer = setTimeout(() => {
|
|
76
|
+
observer.disconnect();
|
|
77
|
+
clearTimeout(deadline);
|
|
78
|
+
apply();
|
|
79
|
+
}, tick);
|
|
80
|
+
}
|
|
81
|
+
function selectorFromElement(element) {
|
|
82
|
+
if (element.id) {
|
|
83
|
+
return `#${CSS.escape(element.id)}`;
|
|
84
|
+
}
|
|
85
|
+
const parts = [];
|
|
86
|
+
let current = element;
|
|
87
|
+
while (current && current !== document.body) {
|
|
88
|
+
const parent = current.parentElement;
|
|
89
|
+
if (!parent)
|
|
90
|
+
break;
|
|
91
|
+
const index = Array
|
|
92
|
+
.from(parent.children)
|
|
93
|
+
.indexOf(current) + 1;
|
|
94
|
+
parts.unshift(`${current.tagName.toLowerCase()}:nth-child(${index})`);
|
|
95
|
+
current = parent;
|
|
96
|
+
}
|
|
97
|
+
return parts.join(" > ");
|
|
98
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module is intended to be imported in a browser bundle in a development.
|
|
3
|
+
* It is responsible for live reloading the page when bundle changes.
|
|
4
|
+
* When NODE_ENV=production, it does nothing.
|
|
5
|
+
*/
|
|
6
|
+
import { showBuildError } from "./Overlay.js";
|
|
7
|
+
import * as ScrollState from "./ScrollState.js";
|
|
8
|
+
const BUNDLE_URL = globalThis._BUNDLE_URL ?? "/_bundle";
|
|
9
|
+
function reload() {
|
|
10
|
+
ScrollState.persist();
|
|
11
|
+
window.location.reload();
|
|
12
|
+
}
|
|
13
|
+
async function loadAllEntrypoints() {
|
|
14
|
+
const manifest = await fetch(`/${BUNDLE_URL}/manifest.json`)
|
|
15
|
+
.then(v => v.json());
|
|
16
|
+
manifest
|
|
17
|
+
.artifacts
|
|
18
|
+
.filter(v => v.path.endsWith(".js"))
|
|
19
|
+
.forEach((artifact) => {
|
|
20
|
+
console.log(artifact.path);
|
|
21
|
+
const script = document.createElement("script");
|
|
22
|
+
script.src = `${BUNDLE_URL}/${artifact.path}`;
|
|
23
|
+
script.type = "module";
|
|
24
|
+
script.onload = () => {
|
|
25
|
+
console.debug("Bundle reloaded");
|
|
26
|
+
};
|
|
27
|
+
document.body.appendChild(script);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function handleBundleEvent(event) {
|
|
31
|
+
switch (event._tag) {
|
|
32
|
+
case "Change":
|
|
33
|
+
console.debug("Bundle change detected...");
|
|
34
|
+
reload();
|
|
35
|
+
break;
|
|
36
|
+
case "BuildError":
|
|
37
|
+
showBuildError(event.error);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function listen() {
|
|
42
|
+
const eventSource = new EventSource(`${BUNDLE_URL}/events`);
|
|
43
|
+
eventSource.addEventListener("message", (event) => {
|
|
44
|
+
try {
|
|
45
|
+
reloadAllMetaLinks();
|
|
46
|
+
const data = JSON.parse(event.data);
|
|
47
|
+
handleBundleEvent(data);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error("Error parsing SSE event", {
|
|
51
|
+
error,
|
|
52
|
+
event,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
eventSource.addEventListener("error", error => {
|
|
57
|
+
console.error("SSE connection error:", error);
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
eventSource.close();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function reloadAllMetaLinks() {
|
|
64
|
+
for (const link of document.getElementsByTagName("link")) {
|
|
65
|
+
const url = new URL(link.href);
|
|
66
|
+
if (url.host === window.location.host) {
|
|
67
|
+
const next = link.cloneNode();
|
|
68
|
+
// TODO: this won't work when link already has query params
|
|
69
|
+
next.href = next.href + "?" + Math.random().toString(36).slice(2);
|
|
70
|
+
next.onload = () => link.remove();
|
|
71
|
+
link.parentNode.insertBefore(next, link.nextSibling);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (process.env.NODE_ENV !== "production") {
|
|
77
|
+
window.addEventListener("load", () => {
|
|
78
|
+
ScrollState.restore();
|
|
79
|
+
listen();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Cookies, HttpApp, HttpServerResponse } from "@effect/platform";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
type CookieValue = string | number | boolean | null | undefined | {
|
|
6
|
+
[key: string]: CookieValue | unknown;
|
|
7
|
+
} | CookieValue[];
|
|
8
|
+
declare const EncryptedCookiesError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
9
|
+
readonly _tag: "EncryptedCookiesError";
|
|
10
|
+
} & Readonly<A>;
|
|
11
|
+
export declare class EncryptedCookiesError extends EncryptedCookiesError_base<{
|
|
12
|
+
cause: unknown;
|
|
13
|
+
cookie?: Cookies.Cookie;
|
|
14
|
+
}> {
|
|
15
|
+
}
|
|
16
|
+
declare const EncryptedCookies_base: Context.TagClass<EncryptedCookies, "EncryptedCookies", {
|
|
17
|
+
encrypt: (value: CookieValue) => Effect.Effect<string, EncryptedCookiesError>;
|
|
18
|
+
decrypt: (encryptedValue: string) => Effect.Effect<CookieValue, EncryptedCookiesError>;
|
|
19
|
+
encryptCookie: (cookie: Cookies.Cookie) => Effect.Effect<Cookies.Cookie, EncryptedCookiesError>;
|
|
20
|
+
decryptCookie: (cookie: Cookies.Cookie) => Effect.Effect<Cookies.Cookie, EncryptedCookiesError>;
|
|
21
|
+
}>;
|
|
22
|
+
export declare class EncryptedCookies extends EncryptedCookies_base {
|
|
23
|
+
}
|
|
24
|
+
export declare function layer(options: {
|
|
25
|
+
secret: string;
|
|
26
|
+
}): Layer.Layer<EncryptedCookies, EncryptedCookiesError, never>;
|
|
27
|
+
export declare function layerConfig(name?: string): Layer.Layer<EncryptedCookies, EncryptedCookiesError, never>;
|
|
28
|
+
export declare function encrypt(value: CookieValue, options: {
|
|
29
|
+
key: CryptoKey;
|
|
30
|
+
} | {
|
|
31
|
+
secret: string;
|
|
32
|
+
}): Effect.Effect<string, EncryptedCookiesError>;
|
|
33
|
+
export declare function encryptCookie(cookie: Cookies.Cookie, options: {
|
|
34
|
+
key: CryptoKey;
|
|
35
|
+
} | {
|
|
36
|
+
secret: string;
|
|
37
|
+
}): Effect.Effect<Cookies.Cookie, EncryptedCookiesError>;
|
|
38
|
+
export declare function decryptCookie(cookie: Cookies.Cookie, options: {
|
|
39
|
+
key: CryptoKey;
|
|
40
|
+
} | {
|
|
41
|
+
secret: string;
|
|
42
|
+
}): Effect.Effect<Cookies.Cookie, EncryptedCookiesError>;
|
|
43
|
+
export declare function decrypt(encryptedValue: string, options: {
|
|
44
|
+
key: CryptoKey;
|
|
45
|
+
} | {
|
|
46
|
+
secret: string;
|
|
47
|
+
}): Effect.Effect<CookieValue, EncryptedCookiesError>;
|
|
48
|
+
export declare function handleError<E>(app: HttpApp.Default<E | EncryptedCookiesError>): Effect.Effect<HttpServerResponse.HttpServerResponse, Exclude<E, {
|
|
49
|
+
_tag: "EncryptedCookiesError";
|
|
50
|
+
}>, import("@effect/platform/HttpServerRequest").HttpServerRequest>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Cookies, HttpServerResponse, } from "@effect/platform";
|
|
2
|
+
import { Effect, pipe, } from "effect";
|
|
3
|
+
import * as Config from "effect/Config";
|
|
4
|
+
import * as Context from "effect/Context";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
import * as Layer from "effect/Layer";
|
|
7
|
+
export class EncryptedCookiesError extends Data.TaggedError("EncryptedCookiesError") {
|
|
8
|
+
}
|
|
9
|
+
export class EncryptedCookies extends Context.Tag("EncryptedCookies")() {
|
|
10
|
+
}
|
|
11
|
+
export function layer(options) {
|
|
12
|
+
return Layer.effect(EncryptedCookies, Effect.gen(function* () {
|
|
13
|
+
const keyMaterial = yield* deriveKeyMaterial(options.secret);
|
|
14
|
+
// Pre-derive both keys once
|
|
15
|
+
const encryptKey = yield* deriveKey(keyMaterial, ["encrypt"]);
|
|
16
|
+
const decryptKey = yield* deriveKey(keyMaterial, ["decrypt"]);
|
|
17
|
+
return EncryptedCookies.of({
|
|
18
|
+
encrypt: (value) => encryptWithDerivedKey(value, encryptKey),
|
|
19
|
+
decrypt: (encryptedValue) => decryptWithDerivedKey(encryptedValue, decryptKey),
|
|
20
|
+
encryptCookie: (cookie) => encryptCookieWithDerivedKey(cookie, encryptKey),
|
|
21
|
+
decryptCookie: (cookie) => decryptCookieWithDerivedKey(cookie, decryptKey),
|
|
22
|
+
});
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export function layerConfig(name = "SECRET_KEY_BASE") {
|
|
26
|
+
return Effect
|
|
27
|
+
.gen(function* () {
|
|
28
|
+
const secret = yield* pipe(Config.nonEmptyString(name), Effect.flatMap((value) => {
|
|
29
|
+
return (value.length < 40)
|
|
30
|
+
? Effect.fail(new Error("ba"))
|
|
31
|
+
: Effect.succeed(value);
|
|
32
|
+
}), Effect.catchAll((err) => {
|
|
33
|
+
return Effect.dieMessage("SECRET_KEY_BASE must be at least 40 characters");
|
|
34
|
+
}));
|
|
35
|
+
return layer({ secret });
|
|
36
|
+
})
|
|
37
|
+
.pipe(Layer.unwrapEffect);
|
|
38
|
+
}
|
|
39
|
+
function encodeToBase64Segments(ciphertext, iv, authTag) {
|
|
40
|
+
return [
|
|
41
|
+
base64urlEncode(ciphertext),
|
|
42
|
+
base64urlEncode(iv),
|
|
43
|
+
base64urlEncode(authTag),
|
|
44
|
+
]
|
|
45
|
+
.join(".");
|
|
46
|
+
}
|
|
47
|
+
function base64urlEncode(data) {
|
|
48
|
+
const base64 = btoa(String.fromCharCode(...data));
|
|
49
|
+
return base64
|
|
50
|
+
.replace(/\+/g, "-")
|
|
51
|
+
.replace(/\//g, "_")
|
|
52
|
+
.replace(/=/g, "");
|
|
53
|
+
}
|
|
54
|
+
function decodeFromBase64Segments(segments) {
|
|
55
|
+
return Effect.gen(function* () {
|
|
56
|
+
const [ciphertextB64, ivB64, authTagB64] = segments;
|
|
57
|
+
const ciphertext = yield* Effect.try({
|
|
58
|
+
try: () => base64urlDecode(ciphertextB64),
|
|
59
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
60
|
+
});
|
|
61
|
+
const iv = yield* Effect.try({
|
|
62
|
+
try: () => base64urlDecode(ivB64),
|
|
63
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
64
|
+
});
|
|
65
|
+
const authTag = yield* Effect.try({
|
|
66
|
+
try: () => base64urlDecode(authTagB64),
|
|
67
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
68
|
+
});
|
|
69
|
+
return { ciphertext, iv, authTag };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function base64urlDecode(data) {
|
|
73
|
+
// Convert base64url back to standard base64
|
|
74
|
+
let base64 = data
|
|
75
|
+
.replace(/-/g, "+")
|
|
76
|
+
.replace(/_/g, "/");
|
|
77
|
+
// Add padding if needed
|
|
78
|
+
while (base64.length % 4) {
|
|
79
|
+
base64 += "=";
|
|
80
|
+
}
|
|
81
|
+
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Encrypts cookie value using the SECRET_KEY_BASE.
|
|
85
|
+
*/
|
|
86
|
+
function encryptWithDerivedKey(value, derivedKey) {
|
|
87
|
+
return Effect.gen(function* () {
|
|
88
|
+
if (value === null || value === undefined) {
|
|
89
|
+
return yield* Effect.fail(new EncryptedCookiesError({
|
|
90
|
+
cause: "Cannot encrypt empty value",
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
94
|
+
const data = new TextEncoder().encode(JSON.stringify(value));
|
|
95
|
+
const encrypted = yield* Effect.tryPromise({
|
|
96
|
+
try: () => crypto.subtle.encrypt({ name: "AES-GCM", iv }, derivedKey, data),
|
|
97
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
98
|
+
});
|
|
99
|
+
const encryptedArray = new Uint8Array(encrypted);
|
|
100
|
+
const authTagLength = 16;
|
|
101
|
+
const ciphertext = encryptedArray.slice(0, -authTagLength);
|
|
102
|
+
const authTag = encryptedArray.slice(-authTagLength);
|
|
103
|
+
return encodeToBase64Segments(ciphertext, iv, authTag);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function encrypt(value, options) {
|
|
107
|
+
return Effect.gen(function* () {
|
|
108
|
+
if ("key" in options) {
|
|
109
|
+
return yield* encryptWithDerivedKey(value, options.key);
|
|
110
|
+
}
|
|
111
|
+
const keyMaterial = yield* deriveKeyMaterial(options.secret);
|
|
112
|
+
const derivedKey = yield* deriveKey(keyMaterial, ["encrypt"]);
|
|
113
|
+
return yield* encryptWithDerivedKey(value, derivedKey);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function decryptWithDerivedKey(encryptedValue, derivedKey) {
|
|
117
|
+
return Effect.gen(function* () {
|
|
118
|
+
if (!encryptedValue || encryptedValue === null || encryptedValue === undefined) {
|
|
119
|
+
return yield* Effect.fail(new EncryptedCookiesError({
|
|
120
|
+
cause: "Cannot decrypt null, undefined, or empty value",
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
const segments = encryptedValue.split(".");
|
|
124
|
+
if (segments.length !== 3) {
|
|
125
|
+
return yield* Effect.fail(new EncryptedCookiesError({
|
|
126
|
+
cause: "Invalid encrypted cookie format",
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
const { ciphertext, iv, authTag } = yield* decodeFromBase64Segments(segments);
|
|
130
|
+
const encryptedData = new Uint8Array(ciphertext.length + authTag.length);
|
|
131
|
+
encryptedData.set(ciphertext);
|
|
132
|
+
encryptedData.set(authTag, ciphertext.length);
|
|
133
|
+
const decrypted = yield* Effect.tryPromise({
|
|
134
|
+
try: () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.slice(0) }, derivedKey, encryptedData),
|
|
135
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
136
|
+
});
|
|
137
|
+
const jsonString = new TextDecoder().decode(decrypted);
|
|
138
|
+
return yield* Effect.try({
|
|
139
|
+
try: () => JSON.parse(jsonString),
|
|
140
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function encryptCookieWithDerivedKey(cookie, derivedKey) {
|
|
145
|
+
return Effect.gen(function* () {
|
|
146
|
+
const encryptedValue = yield* encryptWithDerivedKey(cookie.value, derivedKey)
|
|
147
|
+
.pipe(Effect.mapError(error => new EncryptedCookiesError({
|
|
148
|
+
cause: error.cause,
|
|
149
|
+
cookie,
|
|
150
|
+
})));
|
|
151
|
+
return Cookies.unsafeMakeCookie(cookie.name, encryptedValue, cookie.options);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function decryptCookieWithDerivedKey(cookie, derivedKey) {
|
|
155
|
+
return Effect.gen(function* () {
|
|
156
|
+
const decryptedValue = yield* decryptWithDerivedKey(cookie.value, derivedKey)
|
|
157
|
+
.pipe(Effect.mapError(error => new EncryptedCookiesError({
|
|
158
|
+
cause: error.cause,
|
|
159
|
+
cookie,
|
|
160
|
+
})));
|
|
161
|
+
return Cookies.unsafeMakeCookie(cookie.name, JSON.stringify(decryptedValue), cookie.options);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function encryptCookie(cookie, options) {
|
|
165
|
+
return Effect.gen(function* () {
|
|
166
|
+
if ("key" in options) {
|
|
167
|
+
return yield* encryptCookieWithDerivedKey(cookie, options.key);
|
|
168
|
+
}
|
|
169
|
+
const encryptedValue = yield* encrypt(cookie.value, {
|
|
170
|
+
secret: options.secret,
|
|
171
|
+
})
|
|
172
|
+
.pipe(Effect.mapError(error => new EncryptedCookiesError({
|
|
173
|
+
cause: error.cause,
|
|
174
|
+
cookie,
|
|
175
|
+
})));
|
|
176
|
+
return Cookies.unsafeMakeCookie(cookie.name, encryptedValue, cookie.options);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export function decryptCookie(cookie, options) {
|
|
180
|
+
return Effect.gen(function* () {
|
|
181
|
+
if ("key" in options) {
|
|
182
|
+
return yield* decryptCookieWithDerivedKey(cookie, options.key);
|
|
183
|
+
}
|
|
184
|
+
const decryptedValue = yield* decrypt(cookie.value, {
|
|
185
|
+
secret: options.secret,
|
|
186
|
+
})
|
|
187
|
+
.pipe(Effect.mapError(error => new EncryptedCookiesError({
|
|
188
|
+
cause: error.cause,
|
|
189
|
+
cookie,
|
|
190
|
+
})));
|
|
191
|
+
return Cookies.unsafeMakeCookie(cookie.name, JSON.stringify(decryptedValue), cookie.options);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
export function decrypt(encryptedValue, options) {
|
|
195
|
+
return Effect.gen(function* () {
|
|
196
|
+
if ("key" in options) {
|
|
197
|
+
return yield* decryptWithDerivedKey(encryptedValue, options.key);
|
|
198
|
+
}
|
|
199
|
+
const keyMaterial = yield* deriveKeyMaterial(options.secret);
|
|
200
|
+
const derivedKey = yield* deriveKey(keyMaterial, ["decrypt"]);
|
|
201
|
+
return yield* decryptWithDerivedKey(encryptedValue, derivedKey);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function deriveKeyMaterial(secret) {
|
|
205
|
+
return Effect.gen(function* () {
|
|
206
|
+
const encoder = new TextEncoder();
|
|
207
|
+
const keyMaterial = yield* Effect.tryPromise({
|
|
208
|
+
try: () => crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HKDF" }, false, ["deriveKey"]),
|
|
209
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
210
|
+
});
|
|
211
|
+
return keyMaterial;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function deriveKey(keyMaterial, usage) {
|
|
215
|
+
return Effect.gen(function* () {
|
|
216
|
+
const encoder = new TextEncoder();
|
|
217
|
+
const key = yield* Effect.tryPromise({
|
|
218
|
+
try: () => crypto.subtle.deriveKey({
|
|
219
|
+
name: "HKDF",
|
|
220
|
+
salt: encoder.encode("cookie-encryption"),
|
|
221
|
+
info: encoder.encode("aes-256-gcm"),
|
|
222
|
+
hash: "SHA-256",
|
|
223
|
+
}, keyMaterial, { name: "AES-GCM", length: 256 }, false, usage),
|
|
224
|
+
catch: (error) => new EncryptedCookiesError({ cause: error }),
|
|
225
|
+
});
|
|
226
|
+
return key;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// TODO something si wrong with return type
|
|
230
|
+
export function handleError(app) {
|
|
231
|
+
return Effect.gen(function* () {
|
|
232
|
+
const res = yield* app.pipe(Effect.catchTag("EncryptedCookiesError", (error) => {
|
|
233
|
+
return HttpServerResponse.empty();
|
|
234
|
+
}));
|
|
235
|
+
return res;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function generateFriendlyKey(bits = 128) {
|
|
239
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
240
|
+
const length = Math.ceil(bits / Math.log2(chars.length));
|
|
241
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length));
|
|
242
|
+
return Array.from(bytes, b => chars[b % chars.length]).join("");
|
|
243
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse";
|
|
2
|
+
import * as Duration from "effect/Duration";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
export declare const make: <T = any>(stream: Stream.Stream<T, any>, options?: {
|
|
6
|
+
heartbeatInterval?: Duration.DurationInput;
|
|
7
|
+
}) => Effect.Effect<HttpServerResponse.HttpServerResponse, never, never>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse";
|
|
2
|
+
import * as Duration from "effect/Duration";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Function from "effect/Function";
|
|
5
|
+
import * as Schedule from "effect/Schedule";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as StreamExtra from "../StreamExtra.js";
|
|
8
|
+
const DefaultHeartbeatInterval = Duration.seconds(5);
|
|
9
|
+
export const make = (stream, options) => Effect.gen(function* () {
|
|
10
|
+
const heartbeat = Stream.repeat(Stream.succeed(null), Schedule.spaced(options?.heartbeatInterval ?? DefaultHeartbeatInterval));
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
const events = Function.pipe(Stream.merge(heartbeat.pipe(Stream.map(() => ":\n\n")), stream.pipe(Stream.map(event => `data: ${JSON.stringify(event)}\n\n`))),
|
|
13
|
+
// without Stream.tap, only two events are sent
|
|
14
|
+
// Effect.fork(Stream.runDrain) doesn't seem to work.
|
|
15
|
+
// Asked for help here: [2025-04-09]
|
|
16
|
+
// https://discord.com/channels/795981131316985866/1359523331400929341
|
|
17
|
+
Stream.tap(v => Effect.gen(function* () { })), Stream.map(str => encoder.encode(str)));
|
|
18
|
+
const toStream = StreamExtra.toReadableStreamRuntimePatched(yield* Effect.runtime());
|
|
19
|
+
// see toStream to understand why we're not using
|
|
20
|
+
// HttpServerResponse.stream here.
|
|
21
|
+
return HttpServerResponse.raw(toStream(events), {
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "text/event-stream",
|
|
24
|
+
"Cache-Control": "no-cache",
|
|
25
|
+
"Connection": "keep-alive",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import { HyperHooks } from "../x/datastar/index.ts";
|
|
4
|
+
import type { JSX } from "./jsx.d.ts";
|
|
5
|
+
type Elements = JSX.IntrinsicElements;
|
|
6
|
+
type Children = JSX.Children;
|
|
7
|
+
export type { Children, Elements, JSX, };
|
|
8
|
+
declare const Hyper_base: Context.TagClass<Hyper, "Hyper", {
|
|
9
|
+
hooks: typeof HyperHooks | undefined;
|
|
10
|
+
}>;
|
|
11
|
+
export declare class Hyper extends Hyper_base {
|
|
12
|
+
}
|
|
13
|
+
export declare function layer(opts: {
|
|
14
|
+
hooks: typeof HyperHooks;
|
|
15
|
+
}): Layer.Layer<Hyper, never, never>;
|
|
16
|
+
type Primitive = string | number | boolean | null | undefined;
|
|
17
|
+
export type HyperType = string | HyperComponent;
|
|
18
|
+
export type HyperProps = {
|
|
19
|
+
[key: string]: Primitive | ReadonlyArray<Primitive> | HyperNode | HyperNode[] | null | undefined;
|
|
20
|
+
};
|
|
21
|
+
export type HyperComponent = (props: HyperProps) => HyperNode | Primitive;
|
|
22
|
+
export interface HyperNode {
|
|
23
|
+
type: HyperType;
|
|
24
|
+
props: HyperProps;
|
|
25
|
+
}
|
|
26
|
+
export declare function h(type: HyperType, props: HyperProps): HyperNode;
|
|
27
|
+
export declare function unsafeUse<Value>(tag: Context.Tag<any, Value>): Value;
|
|
28
|
+
export type GenericJsxObject = {
|
|
29
|
+
type: any;
|
|
30
|
+
props: any;
|
|
31
|
+
};
|
|
32
|
+
export declare function isGenericJsxObject(value: unknown): value is GenericJsxObject;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Fiber from "effect/Fiber";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
5
|
+
export class Hyper extends Context.Tag("Hyper")() {
|
|
6
|
+
}
|
|
7
|
+
export function layer(opts) {
|
|
8
|
+
return Layer.sync(Hyper, () => {
|
|
9
|
+
return {
|
|
10
|
+
hooks: opts.hooks,
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const NoChildren = Object.freeze([]);
|
|
15
|
+
export function h(type, props) {
|
|
16
|
+
return {
|
|
17
|
+
type,
|
|
18
|
+
props: {
|
|
19
|
+
...props,
|
|
20
|
+
children: props.children ?? NoChildren,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function unsafeUse(tag) {
|
|
25
|
+
const currentFiber = Option.getOrThrow(Fiber.getCurrentFiber());
|
|
26
|
+
const context = currentFiber.currentContext;
|
|
27
|
+
return Context.unsafeGet(context, tag);
|
|
28
|
+
}
|
|
29
|
+
export function isGenericJsxObject(value) {
|
|
30
|
+
return typeof value === "object"
|
|
31
|
+
&& value !== null
|
|
32
|
+
&& "type" in value
|
|
33
|
+
&& "props" in value;
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders Hyper JSX nodes to HTML.
|
|
3
|
+
*
|
|
4
|
+
* Effect Start comes with {@link Hyper} and {@link JsxRuntime} to enable
|
|
5
|
+
* JSX support. The advantage of using JSX over HTML strings or templates
|
|
6
|
+
* is type safety and better editor support.
|
|
7
|
+
*
|
|
8
|
+
* JSX nodes are compatible with React's and Solid's.
|
|
9
|
+
|
|
10
|
+
* You can enable JSX support by updating `tsconfig.json`:
|
|
11
|
+
*
|
|
12
|
+
* {
|
|
13
|
+
* compilerOptions: {
|
|
14
|
+
* jsx: "react-jsx",
|
|
15
|
+
* jsxImportSource: "effect-start" | "react" | "praect" // etc.
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
import * as HyperNode from "./HyperNode.ts";
|
|
20
|
+
import type { JSX } from "./jsx.d.ts";
|
|
21
|
+
export declare function renderToString(node: JSX.Children, hooks?: {
|
|
22
|
+
onNode?: (node: HyperNode.HyperNode) => void;
|
|
23
|
+
}): string;
|