alabjs 0.1.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/dist/adapters/cloudflare.d.ts +31 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +30 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +22 -0
- package/dist/adapters/deno.d.ts.map +1 -0
- package/dist/adapters/deno.js +21 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/web.d.ts +47 -0
- package/dist/adapters/web.d.ts.map +1 -0
- package/dist/adapters/web.js +212 -0
- package/dist/adapters/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/hooks.d.ts +119 -0
- package/dist/client/hooks.d.ts.map +1 -0
- package/dist/client/hooks.js +220 -0
- package/dist/client/hooks.js.map +1 -0
- package/dist/client/hooks.test.d.ts +2 -0
- package/dist/client/hooks.test.d.ts.map +1 -0
- package/dist/client/hooks.test.js +45 -0
- package/dist/client/hooks.test.js.map +1 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/offline.d.ts +52 -0
- package/dist/client/offline.d.ts.map +1 -0
- package/dist/client/offline.js +90 -0
- package/dist/client/offline.js.map +1 -0
- package/dist/client/provider.d.ts +12 -0
- package/dist/client/provider.d.ts.map +1 -0
- package/dist/client/provider.js +10 -0
- package/dist/client/provider.js.map +1 -0
- package/dist/commands/build.d.ts +18 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +173 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +8 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +447 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/info.d.ts +6 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/info.js +92 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/ssg.d.ts +8 -0
- package/dist/commands/ssg.d.ts.map +1 -0
- package/dist/commands/ssg.js +124 -0
- package/dist/commands/ssg.js.map +1 -0
- package/dist/commands/start.d.ts +7 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +26 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/test.d.ts +24 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +87 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +38 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +46 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/Font.d.ts +57 -0
- package/dist/components/Font.d.ts.map +1 -0
- package/dist/components/Font.js +33 -0
- package/dist/components/Font.js.map +1 -0
- package/dist/components/Image.d.ts +74 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +85 -0
- package/dist/components/Image.js.map +1 -0
- package/dist/components/Link.d.ts +23 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Link.js +48 -0
- package/dist/components/Link.js.map +1 -0
- package/dist/components/Script.d.ts +37 -0
- package/dist/components/Script.d.ts.map +1 -0
- package/dist/components/Script.js +70 -0
- package/dist/components/Script.js.map +1 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +6 -0
- package/dist/components/index.js.map +1 -0
- package/dist/i18n/i18n.test.d.ts +2 -0
- package/dist/i18n/i18n.test.d.ts.map +1 -0
- package/dist/i18n/i18n.test.js +132 -0
- package/dist/i18n/i18n.test.js.map +1 -0
- package/dist/i18n/index.d.ts +135 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +189 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/router/code-router.d.ts +204 -0
- package/dist/router/code-router.d.ts.map +1 -0
- package/dist/router/code-router.js +258 -0
- package/dist/router/code-router.js.map +1 -0
- package/dist/router/code-router.test.d.ts +2 -0
- package/dist/router/code-router.test.d.ts.map +1 -0
- package/dist/router/code-router.test.js +128 -0
- package/dist/router/code-router.test.js.map +1 -0
- package/dist/router/index.d.ts +4 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +2 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/manifest.d.ts +12 -0
- package/dist/router/manifest.d.ts.map +1 -0
- package/dist/router/manifest.js +2 -0
- package/dist/router/manifest.js.map +1 -0
- package/dist/server/app.d.ts +13 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +407 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/cache.d.ts +99 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +161 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/cache.test.d.ts +2 -0
- package/dist/server/cache.test.d.ts.map +1 -0
- package/dist/server/cache.test.js +150 -0
- package/dist/server/cache.test.js.map +1 -0
- package/dist/server/csrf.d.ts +28 -0
- package/dist/server/csrf.d.ts.map +1 -0
- package/dist/server/csrf.js +66 -0
- package/dist/server/csrf.js.map +1 -0
- package/dist/server/csrf.test.d.ts +2 -0
- package/dist/server/csrf.test.d.ts.map +1 -0
- package/dist/server/csrf.test.js +154 -0
- package/dist/server/csrf.test.js.map +1 -0
- package/dist/server/image.d.ts +18 -0
- package/dist/server/image.d.ts.map +1 -0
- package/dist/server/image.js +97 -0
- package/dist/server/image.js.map +1 -0
- package/dist/server/index.d.ts +57 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +58 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware.d.ts +53 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +80 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/middleware.test.d.ts +2 -0
- package/dist/server/middleware.test.d.ts.map +1 -0
- package/dist/server/middleware.test.js +125 -0
- package/dist/server/middleware.test.js.map +1 -0
- package/dist/server/revalidate.d.ts +49 -0
- package/dist/server/revalidate.d.ts.map +1 -0
- package/dist/server/revalidate.js +62 -0
- package/dist/server/revalidate.js.map +1 -0
- package/dist/server/revalidate.test.d.ts +2 -0
- package/dist/server/revalidate.test.d.ts.map +1 -0
- package/dist/server/revalidate.test.js +93 -0
- package/dist/server/revalidate.test.js.map +1 -0
- package/dist/server/server-fn.test.d.ts +2 -0
- package/dist/server/server-fn.test.d.ts.map +1 -0
- package/dist/server/server-fn.test.js +105 -0
- package/dist/server/server-fn.test.js.map +1 -0
- package/dist/server/sitemap.d.ts +9 -0
- package/dist/server/sitemap.d.ts.map +1 -0
- package/dist/server/sitemap.js +26 -0
- package/dist/server/sitemap.js.map +1 -0
- package/dist/server/sitemap.test.d.ts +2 -0
- package/dist/server/sitemap.test.d.ts.map +1 -0
- package/dist/server/sitemap.test.js +61 -0
- package/dist/server/sitemap.test.js.map +1 -0
- package/dist/server/sse.d.ts +59 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +91 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/sse.test.d.ts +2 -0
- package/dist/server/sse.test.d.ts.map +1 -0
- package/dist/server/sse.test.js +68 -0
- package/dist/server/sse.test.js.map +1 -0
- package/dist/signals/index.d.ts +101 -0
- package/dist/signals/index.d.ts.map +1 -0
- package/dist/signals/index.js +149 -0
- package/dist/signals/index.js.map +1 -0
- package/dist/signals/signals.test.d.ts +2 -0
- package/dist/signals/signals.test.d.ts.map +1 -0
- package/dist/signals/signals.test.js +146 -0
- package/dist/signals/signals.test.js.map +1 -0
- package/dist/ssr/html.d.ts +27 -0
- package/dist/ssr/html.d.ts.map +1 -0
- package/dist/ssr/html.js +107 -0
- package/dist/ssr/html.js.map +1 -0
- package/dist/ssr/html.test.d.ts +2 -0
- package/dist/ssr/html.test.d.ts.map +1 -0
- package/dist/ssr/html.test.js +178 -0
- package/dist/ssr/html.test.js.map +1 -0
- package/dist/ssr/render.d.ts +46 -0
- package/dist/ssr/render.d.ts.map +1 -0
- package/dist/ssr/render.js +87 -0
- package/dist/ssr/render.js.map +1 -0
- package/dist/ssr/router-dev.d.ts +60 -0
- package/dist/ssr/router-dev.d.ts.map +1 -0
- package/dist/ssr/router-dev.js +205 -0
- package/dist/ssr/router-dev.js.map +1 -0
- package/dist/ssr/router-dev.test.d.ts +2 -0
- package/dist/ssr/router-dev.test.d.ts.map +1 -0
- package/dist/ssr/router-dev.test.js +189 -0
- package/dist/ssr/router-dev.test.js.map +1 -0
- package/dist/test/index.d.ts +93 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +146 -0
- package/dist/test/index.js.map +1 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/napi.d.ts +15 -0
- package/dist/types/napi.d.ts.map +1 -0
- package/dist/types/napi.js +2 -0
- package/dist/types/napi.js.map +1 -0
- package/package.json +107 -0
- package/src/adapters/cloudflare.ts +30 -0
- package/src/adapters/deno.ts +21 -0
- package/src/adapters/web.ts +259 -0
- package/src/cli.ts +68 -0
- package/src/client/hooks.test.ts +54 -0
- package/src/client/hooks.ts +329 -0
- package/src/client/index.ts +5 -0
- package/src/client/offline-sw.ts +191 -0
- package/src/client/offline.ts +114 -0
- package/src/client/provider.tsx +14 -0
- package/src/commands/build.ts +201 -0
- package/src/commands/dev.ts +509 -0
- package/src/commands/info.ts +111 -0
- package/src/commands/ssg.ts +177 -0
- package/src/commands/start.ts +32 -0
- package/src/commands/test.ts +102 -0
- package/src/components/ErrorBoundary.tsx +73 -0
- package/src/components/Font.tsx +100 -0
- package/src/components/Image.tsx +141 -0
- package/src/components/Link.tsx +64 -0
- package/src/components/Script.tsx +97 -0
- package/src/components/index.ts +9 -0
- package/src/i18n/i18n.test.tsx +169 -0
- package/src/i18n/index.tsx +256 -0
- package/src/index.ts +10 -0
- package/src/router/code-router.test.ts +146 -0
- package/src/router/code-router.tsx +459 -0
- package/src/router/index.ts +18 -0
- package/src/router/manifest.ts +13 -0
- package/src/server/app.ts +466 -0
- package/src/server/cache.test.ts +192 -0
- package/src/server/cache.ts +195 -0
- package/src/server/csrf.test.ts +199 -0
- package/src/server/csrf.ts +80 -0
- package/src/server/image.ts +112 -0
- package/src/server/index.ts +144 -0
- package/src/server/middleware.test.ts +151 -0
- package/src/server/middleware.ts +95 -0
- package/src/server/revalidate.test.ts +106 -0
- package/src/server/revalidate.ts +75 -0
- package/src/server/server-fn.test.ts +127 -0
- package/src/server/sitemap.test.ts +68 -0
- package/src/server/sitemap.ts +30 -0
- package/src/server/sse.test.ts +81 -0
- package/src/server/sse.ts +110 -0
- package/src/signals/index.ts +177 -0
- package/src/signals/signals.test.ts +164 -0
- package/src/ssr/html.test.ts +200 -0
- package/src/ssr/html.ts +140 -0
- package/src/ssr/render.ts +144 -0
- package/src/ssr/router-dev.test.ts +230 -0
- package/src/ssr/router-dev.ts +229 -0
- package/src/test/index.ts +206 -0
- package/src/types/compiler.d.ts +25 -0
- package/src/types/index.ts +147 -0
- package/src/types/napi.ts +20 -0
- package/src/types/plugins.d.ts +3 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +32 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alab in-process cache for server functions.
|
|
3
|
+
*
|
|
4
|
+
* Nothing is cached unless you explicitly opt in — no implicit layers.
|
|
5
|
+
* You own the key, the TTL, and the invalidation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // app/posts/[id]/page.server.ts
|
|
10
|
+
* export const getPost = defineServerFn(
|
|
11
|
+
* async ({ params }) => db.posts.findById(params.id),
|
|
12
|
+
* { cache: { ttl: 60, tags: ["posts", `post:${params.id}`] } },
|
|
13
|
+
* );
|
|
14
|
+
*
|
|
15
|
+
* // Invalidate from another server function or API route
|
|
16
|
+
* import { invalidateCache } from "alabjs/cache";
|
|
17
|
+
* await invalidateCache({ tags: ["posts"] });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/** Sentinel value returned when a cache key has no valid entry. */
|
|
21
|
+
const CACHE_MISS = Symbol("alab:cache_miss");
|
|
22
|
+
/** Global in-process LRU-style cache. Shared across all server function calls. */
|
|
23
|
+
const _store = new Map();
|
|
24
|
+
export { CACHE_MISS };
|
|
25
|
+
/** Retrieve a cached value. Returns `CACHE_MISS` if absent or expired. */
|
|
26
|
+
export function getCached(key) {
|
|
27
|
+
const entry = _store.get(key);
|
|
28
|
+
if (!entry)
|
|
29
|
+
return CACHE_MISS;
|
|
30
|
+
if (Date.now() > entry.expires) {
|
|
31
|
+
_store.delete(key);
|
|
32
|
+
return CACHE_MISS;
|
|
33
|
+
}
|
|
34
|
+
return entry.data;
|
|
35
|
+
}
|
|
36
|
+
/** Store a value in the cache with a TTL (seconds) and optional tags. */
|
|
37
|
+
export function setCache(key, data, opts) {
|
|
38
|
+
_store.set(key, {
|
|
39
|
+
data,
|
|
40
|
+
expires: Date.now() + opts.ttl * 1_000,
|
|
41
|
+
tags: opts.tags ?? [],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Invalidate all cache entries that carry at least one of the given tags.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { invalidateCache } from "alabjs/cache";
|
|
50
|
+
* await invalidateCache({ tags: ["posts"] });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function invalidateCache(opts) {
|
|
54
|
+
for (const [key, entry] of _store) {
|
|
55
|
+
if (opts.tags.some((t) => entry.tags.includes(t))) {
|
|
56
|
+
_store.delete(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Invalidate a specific cache entry by its exact key.
|
|
62
|
+
* Prefer `invalidateCache({ tags })` for logical invalidation.
|
|
63
|
+
*/
|
|
64
|
+
export function invalidateCacheKey(key) {
|
|
65
|
+
_store.delete(key);
|
|
66
|
+
}
|
|
67
|
+
const _pageStore = new Map();
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve a cached HTML page. Returns `null` if absent or fully expired
|
|
70
|
+
* (more than 2× TTL past — serves stale-while-revalidate window).
|
|
71
|
+
*/
|
|
72
|
+
export function getCachedPage(pathname) {
|
|
73
|
+
const entry = _pageStore.get(pathname);
|
|
74
|
+
if (!entry)
|
|
75
|
+
return null;
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
// Still fresh
|
|
78
|
+
if (now <= entry.expires)
|
|
79
|
+
return { html: entry.html, stale: false };
|
|
80
|
+
// Stale-while-revalidate: serve stale for up to 2× TTL, trigger background regen
|
|
81
|
+
const ttl = entry.expires - (entry.expires - now); // rough TTL from stored data
|
|
82
|
+
if (now <= entry.expires + Math.max(ttl * 2, 60_000)) {
|
|
83
|
+
return { html: entry.html, stale: true };
|
|
84
|
+
}
|
|
85
|
+
_pageStore.delete(pathname);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** Store a rendered HTML page with a TTL (seconds). */
|
|
89
|
+
export function setCachedPage(pathname, html, ttl, tags = []) {
|
|
90
|
+
_pageStore.set(pathname, { html, expires: Date.now() + ttl * 1_000, revalidating: false, tags });
|
|
91
|
+
}
|
|
92
|
+
/** Mark a page as currently being revalidated to prevent concurrent regen. */
|
|
93
|
+
export function markPageRevalidating(pathname) {
|
|
94
|
+
const entry = _pageStore.get(pathname);
|
|
95
|
+
if (entry)
|
|
96
|
+
entry.revalidating = true;
|
|
97
|
+
}
|
|
98
|
+
/** Check if a page is currently being revalidated in the background. */
|
|
99
|
+
export function isPageRevalidating(pathname) {
|
|
100
|
+
return _pageStore.get(pathname)?.revalidating ?? false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Purge a specific page's cached HTML, forcing a fresh render on the next request.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { revalidatePath } from "alabjs/cache";
|
|
108
|
+
* await revalidatePath("/posts/1"); // next request regenerates the page
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function revalidatePath(pathname) {
|
|
112
|
+
_pageStore.delete(pathname);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Purge all cached pages whose pathname starts with the given prefix.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* await revalidatePath("/posts"); // clears /posts, /posts/1, /posts/2, ...
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export function revalidatePathPrefix(prefix) {
|
|
123
|
+
for (const key of _pageStore.keys()) {
|
|
124
|
+
if (key.startsWith(prefix))
|
|
125
|
+
_pageStore.delete(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Purge all server-function cache entries AND page HTML cache entries
|
|
130
|
+
* that carry at least one of the given tags.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* import { revalidateTag } from "alabjs/cache";
|
|
135
|
+
* revalidateTag({ tags: ["posts"] }); // clears both data and page caches
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function revalidateTag(opts) {
|
|
139
|
+
// Server-function data cache
|
|
140
|
+
invalidateCache(opts);
|
|
141
|
+
// Page HTML cache (ISR)
|
|
142
|
+
for (const [path, entry] of _pageStore) {
|
|
143
|
+
if (opts.tags.some((t) => entry.tags.includes(t))) {
|
|
144
|
+
_pageStore.delete(path);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Return a snapshot of all live cache entries (for the dev Cache Inspector). */
|
|
149
|
+
export function inspectCache() {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const result = [];
|
|
152
|
+
for (const [key, entry] of _store) {
|
|
153
|
+
const expiresIn = entry.expires - now;
|
|
154
|
+
if (expiresIn > 0)
|
|
155
|
+
result.push({ key, tags: entry.tags, expiresIn: Math.ceil(expiresIn / 1000) });
|
|
156
|
+
else
|
|
157
|
+
_store.delete(key);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/server/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,mEAAmE;AACnE,MAAM,UAAU,GAAkB,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAE5D,kFAAkF;AAClF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAC;AAE7C,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,0EAA0E;AAC1E,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,OAAO,UAAU,CAAC;IAC9B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnB,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,QAAQ,CACtB,GAAW,EACX,IAAa,EACb,IAAsC;IAEtC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE;QACd,IAAI;QACJ,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,GAAG,KAAK;QACtC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE;KACtB,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,IAAwB;IACtD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAaD,MAAM,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;AAErD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,cAAc;IACd,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACpE,iFAAiF;IACjF,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,6BAA6B;IAChF,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;QACrD,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC3C,CAAC;IACD,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC5B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,IAAY,EAAE,GAAW,EAAE,OAAiB,EAAE;IAC5F,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACnG,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,KAAK;QAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;AACvC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,OAAO,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,YAAY,IAAI,KAAK,CAAC;AACzD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;QACpC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrD,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,IAAwB;IACpD,6BAA6B;IAC7B,eAAe,CAAC,IAAI,CAAC,CAAC;IACtB,wBAAwB;IACxB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,UAAU,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClD,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,YAAY;IAK1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAA8D,EAAE,CAAC;IAC7E,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC;QACtC,IAAI,SAAS,GAAG,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;;YAC7F,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../src/server/cache.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { getCached, setCache, invalidateCache, invalidateCacheKey, CACHE_MISS, getCachedPage, setCachedPage, markPageRevalidating, isPageRevalidating, revalidatePath, revalidatePathPrefix, revalidateTag, inspectCache, } from "./cache.js";
|
|
3
|
+
// ─── Server-function cache ────────────────────────────────────────────────────
|
|
4
|
+
describe("server-function cache", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Clear cache between tests by invalidating everything
|
|
7
|
+
invalidateCacheKey("test-key");
|
|
8
|
+
invalidateCacheKey("a");
|
|
9
|
+
invalidateCacheKey("b");
|
|
10
|
+
invalidateCacheKey("c");
|
|
11
|
+
invalidateCacheKey("tagged");
|
|
12
|
+
invalidateCacheKey("other");
|
|
13
|
+
invalidateCacheKey("expired");
|
|
14
|
+
});
|
|
15
|
+
it("returns CACHE_MISS for unknown keys", () => {
|
|
16
|
+
expect(getCached("nonexistent")).toBe(CACHE_MISS);
|
|
17
|
+
});
|
|
18
|
+
it("stores and retrieves values", () => {
|
|
19
|
+
setCache("test-key", { name: "Ada" }, { ttl: 60 });
|
|
20
|
+
expect(getCached("test-key")).toEqual({ name: "Ada" });
|
|
21
|
+
});
|
|
22
|
+
it("stores primitive values", () => {
|
|
23
|
+
setCache("a", 42, { ttl: 60 });
|
|
24
|
+
setCache("b", "hello", { ttl: 60 });
|
|
25
|
+
setCache("c", null, { ttl: 60 });
|
|
26
|
+
expect(getCached("a")).toBe(42);
|
|
27
|
+
expect(getCached("b")).toBe("hello");
|
|
28
|
+
expect(getCached("c")).toBe(null);
|
|
29
|
+
});
|
|
30
|
+
it("returns CACHE_MISS for expired entries", () => {
|
|
31
|
+
// Set with 0 second TTL (expired immediately)
|
|
32
|
+
setCache("expired", "old", { ttl: 0 });
|
|
33
|
+
// Advance time slightly
|
|
34
|
+
vi.useFakeTimers();
|
|
35
|
+
vi.advanceTimersByTime(1);
|
|
36
|
+
expect(getCached("expired")).toBe(CACHE_MISS);
|
|
37
|
+
vi.useRealTimers();
|
|
38
|
+
});
|
|
39
|
+
it("invalidates by tag", () => {
|
|
40
|
+
setCache("tagged", { id: 1 }, { ttl: 60, tags: ["posts", "post:1"] });
|
|
41
|
+
setCache("other", { id: 2 }, { ttl: 60, tags: ["users"] });
|
|
42
|
+
invalidateCache({ tags: ["posts"] });
|
|
43
|
+
expect(getCached("tagged")).toBe(CACHE_MISS);
|
|
44
|
+
expect(getCached("other")).not.toBe(CACHE_MISS);
|
|
45
|
+
});
|
|
46
|
+
it("invalidates by exact key", () => {
|
|
47
|
+
setCache("a", 1, { ttl: 60 });
|
|
48
|
+
invalidateCacheKey("a");
|
|
49
|
+
expect(getCached("a")).toBe(CACHE_MISS);
|
|
50
|
+
});
|
|
51
|
+
it("revalidateTag is an alias for invalidateCache", () => {
|
|
52
|
+
setCache("tagged", "data", { ttl: 60, tags: ["t1"] });
|
|
53
|
+
revalidateTag({ tags: ["t1"] });
|
|
54
|
+
expect(getCached("tagged")).toBe(CACHE_MISS);
|
|
55
|
+
});
|
|
56
|
+
it("inspectCache returns live entries", () => {
|
|
57
|
+
setCache("a", 1, { ttl: 60, tags: ["x"] });
|
|
58
|
+
setCache("b", 2, { ttl: 120 });
|
|
59
|
+
const snapshot = inspectCache();
|
|
60
|
+
expect(snapshot.length).toBeGreaterThanOrEqual(2);
|
|
61
|
+
const aEntry = snapshot.find((e) => e.key === "a");
|
|
62
|
+
expect(aEntry).toBeDefined();
|
|
63
|
+
expect(aEntry.tags).toContain("x");
|
|
64
|
+
expect(aEntry.expiresIn).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
it("inspectCache filters out expired entries", () => {
|
|
67
|
+
setCache("expired", "data", { ttl: 0 });
|
|
68
|
+
vi.useFakeTimers();
|
|
69
|
+
vi.advanceTimersByTime(1);
|
|
70
|
+
const snapshot = inspectCache();
|
|
71
|
+
expect(snapshot.find((e) => e.key === "expired")).toBeUndefined();
|
|
72
|
+
vi.useRealTimers();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// ─── Page cache (ISR) ─────────────────────────────────────────────────────────
|
|
76
|
+
describe("page cache (ISR)", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
revalidatePath("/test");
|
|
79
|
+
revalidatePath("/a");
|
|
80
|
+
revalidatePath("/a/1");
|
|
81
|
+
revalidatePath("/a/2");
|
|
82
|
+
revalidatePath("/b");
|
|
83
|
+
});
|
|
84
|
+
it("returns null for uncached pages", () => {
|
|
85
|
+
expect(getCachedPage("/test")).toBe(null);
|
|
86
|
+
});
|
|
87
|
+
it("stores and retrieves page HTML", () => {
|
|
88
|
+
setCachedPage("/test", "<html>test</html>", 60);
|
|
89
|
+
const result = getCachedPage("/test");
|
|
90
|
+
expect(result).not.toBe(null);
|
|
91
|
+
expect(result.html).toBe("<html>test</html>");
|
|
92
|
+
expect(result.stale).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it("returns stale entry after TTL expires (stale-while-revalidate)", () => {
|
|
95
|
+
vi.useFakeTimers();
|
|
96
|
+
setCachedPage("/test", "<html>stale</html>", 10);
|
|
97
|
+
// Advance past the TTL
|
|
98
|
+
vi.advanceTimersByTime(11_000);
|
|
99
|
+
const result = getCachedPage("/test");
|
|
100
|
+
expect(result).not.toBe(null);
|
|
101
|
+
expect(result.stale).toBe(true);
|
|
102
|
+
vi.useRealTimers();
|
|
103
|
+
});
|
|
104
|
+
it("markPageRevalidating and isPageRevalidating", () => {
|
|
105
|
+
setCachedPage("/test", "<html>test</html>", 60);
|
|
106
|
+
expect(isPageRevalidating("/test")).toBe(false);
|
|
107
|
+
markPageRevalidating("/test");
|
|
108
|
+
expect(isPageRevalidating("/test")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it("isPageRevalidating returns false for uncached pages", () => {
|
|
111
|
+
expect(isPageRevalidating("/nonexistent")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
it("revalidatePath removes the cached page", () => {
|
|
114
|
+
setCachedPage("/test", "<html>test</html>", 60);
|
|
115
|
+
revalidatePath("/test");
|
|
116
|
+
expect(getCachedPage("/test")).toBe(null);
|
|
117
|
+
});
|
|
118
|
+
it("revalidatePathPrefix removes matching pages", () => {
|
|
119
|
+
setCachedPage("/a", "<html>a</html>", 60);
|
|
120
|
+
setCachedPage("/a/1", "<html>a1</html>", 60);
|
|
121
|
+
setCachedPage("/a/2", "<html>a2</html>", 60);
|
|
122
|
+
setCachedPage("/b", "<html>b</html>", 60);
|
|
123
|
+
revalidatePathPrefix("/a");
|
|
124
|
+
expect(getCachedPage("/a")).toBe(null);
|
|
125
|
+
expect(getCachedPage("/a/1")).toBe(null);
|
|
126
|
+
expect(getCachedPage("/a/2")).toBe(null);
|
|
127
|
+
expect(getCachedPage("/b")).not.toBe(null);
|
|
128
|
+
});
|
|
129
|
+
it("revalidateTag purges page HTML entries carrying a matching tag", () => {
|
|
130
|
+
setCachedPage("/posts", "<html>posts</html>", 60, ["posts"]);
|
|
131
|
+
setCachedPage("/posts/1", "<html>post 1</html>", 60, ["posts", "post:1"]);
|
|
132
|
+
setCachedPage("/about", "<html>about</html>", 60, ["static"]);
|
|
133
|
+
revalidateTag({ tags: ["posts"] });
|
|
134
|
+
expect(getCachedPage("/posts")).toBe(null);
|
|
135
|
+
expect(getCachedPage("/posts/1")).toBe(null);
|
|
136
|
+
expect(getCachedPage("/about")).not.toBe(null);
|
|
137
|
+
});
|
|
138
|
+
it("revalidateTag leaves page HTML entries with no matching tag intact", () => {
|
|
139
|
+
setCachedPage("/home", "<html>home</html>", 60, ["home"]);
|
|
140
|
+
revalidateTag({ tags: ["posts"] });
|
|
141
|
+
expect(getCachedPage("/home")).not.toBe(null);
|
|
142
|
+
});
|
|
143
|
+
it("setCachedPage stores tags and they survive a fresh read", () => {
|
|
144
|
+
setCachedPage("/tagged", "<html>tagged</html>", 60, ["t1", "t2"]);
|
|
145
|
+
expect(getCachedPage("/tagged")).not.toBe(null);
|
|
146
|
+
revalidateTag({ tags: ["t2"] });
|
|
147
|
+
expect(getCachedPage("/tagged")).toBe(null);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
//# sourceMappingURL=cache.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.test.js","sourceRoot":"","sources":["../../src/server/cache.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type H3Event } from "h3";
|
|
2
|
+
export declare const CSRF_COOKIE = "alab-csrf";
|
|
3
|
+
export declare const CSRF_HEADER = "x-csrf-token";
|
|
4
|
+
/**
|
|
5
|
+
* CSRF protection middleware using the Double Submit Cookie pattern.
|
|
6
|
+
*
|
|
7
|
+
* - Safe methods (GET, HEAD, OPTIONS) are always allowed.
|
|
8
|
+
* - Mutating requests must include an `x-csrf-token` header whose value
|
|
9
|
+
* matches the `alab-csrf` cookie set by `setCsrfCookie()`.
|
|
10
|
+
* - The cookie is `SameSite=Strict` (first line of defence). The header
|
|
11
|
+
* check is a second layer that prevents attacks from subdomains.
|
|
12
|
+
* - Disabled in development (NODE_ENV !== "production") for DX.
|
|
13
|
+
*/
|
|
14
|
+
export declare function csrfMiddleware(): import("h3").EventHandler<import("h3").EventHandlerRequest, void>;
|
|
15
|
+
/**
|
|
16
|
+
* Set a CSRF cookie on the response and return the generated token.
|
|
17
|
+
* Call this on every GET page response so the client always has a fresh token.
|
|
18
|
+
*
|
|
19
|
+
* The cookie is intentionally NOT HttpOnly so JavaScript can read and send
|
|
20
|
+
* it as the `x-csrf-token` request header.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setCsrfCookie(event: H3Event): string;
|
|
23
|
+
/**
|
|
24
|
+
* Inject the CSRF token as a `<meta name="csrf-token">` tag into
|
|
25
|
+
* the HTML shell so client code can read it without a separate request.
|
|
26
|
+
*/
|
|
27
|
+
export declare function csrfMetaTag(token: string): string;
|
|
28
|
+
//# sourceMappingURL=csrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/server/csrf.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,OAAO,EACb,MAAM,IAAI,CAAC;AAEZ,eAAO,MAAM,WAAW,cAAc,CAAC;AACvC,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAI1C;;;;;;;;;GASG;AACH,wBAAgB,cAAc,sEAwB7B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAapD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { defineEventHandler, getCookie, getHeader, setCookie, createError, } from "h3";
|
|
3
|
+
export const CSRF_COOKIE = "alab-csrf";
|
|
4
|
+
export const CSRF_HEADER = "x-csrf-token";
|
|
5
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
6
|
+
/**
|
|
7
|
+
* CSRF protection middleware using the Double Submit Cookie pattern.
|
|
8
|
+
*
|
|
9
|
+
* - Safe methods (GET, HEAD, OPTIONS) are always allowed.
|
|
10
|
+
* - Mutating requests must include an `x-csrf-token` header whose value
|
|
11
|
+
* matches the `alab-csrf` cookie set by `setCsrfCookie()`.
|
|
12
|
+
* - The cookie is `SameSite=Strict` (first line of defence). The header
|
|
13
|
+
* check is a second layer that prevents attacks from subdomains.
|
|
14
|
+
* - Disabled in development (NODE_ENV !== "production") for DX.
|
|
15
|
+
*/
|
|
16
|
+
export function csrfMiddleware() {
|
|
17
|
+
return defineEventHandler((event) => {
|
|
18
|
+
// Skip in development — avoid friction during local iteration.
|
|
19
|
+
if (process.env["NODE_ENV"] !== "production")
|
|
20
|
+
return;
|
|
21
|
+
const method = event.method.toUpperCase();
|
|
22
|
+
if (SAFE_METHODS.has(method))
|
|
23
|
+
return;
|
|
24
|
+
const cookieToken = getCookie(event, CSRF_COOKIE);
|
|
25
|
+
const headerToken = getHeader(event, CSRF_HEADER);
|
|
26
|
+
const tokensMatch = !!cookieToken &&
|
|
27
|
+
!!headerToken &&
|
|
28
|
+
cookieToken.length === headerToken.length &&
|
|
29
|
+
timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken));
|
|
30
|
+
if (!tokensMatch) {
|
|
31
|
+
throw createError({
|
|
32
|
+
statusCode: 403,
|
|
33
|
+
message: "CSRF token mismatch. Include the x-csrf-token header.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Set a CSRF cookie on the response and return the generated token.
|
|
40
|
+
* Call this on every GET page response so the client always has a fresh token.
|
|
41
|
+
*
|
|
42
|
+
* The cookie is intentionally NOT HttpOnly so JavaScript can read and send
|
|
43
|
+
* it as the `x-csrf-token` request header.
|
|
44
|
+
*/
|
|
45
|
+
export function setCsrfCookie(event) {
|
|
46
|
+
const existing = getCookie(event, CSRF_COOKIE);
|
|
47
|
+
if (existing)
|
|
48
|
+
return existing;
|
|
49
|
+
const token = crypto.randomUUID();
|
|
50
|
+
setCookie(event, CSRF_COOKIE, token, {
|
|
51
|
+
httpOnly: false,
|
|
52
|
+
sameSite: "strict",
|
|
53
|
+
secure: process.env["NODE_ENV"] === "production",
|
|
54
|
+
path: "/",
|
|
55
|
+
maxAge: 60 * 60 * 24, // 24 hours
|
|
56
|
+
});
|
|
57
|
+
return token;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Inject the CSRF token as a `<meta name="csrf-token">` tag into
|
|
61
|
+
* the HTML shell so client code can read it without a separate request.
|
|
62
|
+
*/
|
|
63
|
+
export function csrfMetaTag(token) {
|
|
64
|
+
return `<meta name="csrf-token" content="${token.replace(/"/g, """)}" />`;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=csrf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../../src/server/csrf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,GAEZ,MAAM,IAAI,CAAC;AAEZ,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AACvC,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAE1C,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAEzD;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,kBAAkB,CAAC,CAAC,KAAK,EAAE,EAAE;QAClC,+DAA+D;QAC/D,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY;YAAE,OAAO;QAErD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAC1C,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO;QAErC,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAClD,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAElD,MAAM,WAAW,GACf,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,WAAW;YACb,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM;YACzC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QAEtE,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,WAAW,CAAC;gBAChB,UAAU,EAAE,GAAG;gBACf,OAAO,EAAE,uDAAuD;aACjE,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAC/C,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAClC,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE;QACnC,QAAQ,EAAE,KAAK;QACf,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY;QAChD,IAAI,EAAE,GAAG;QACT,MAAM,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,WAAW;KAClC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,oCAAoC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.test.d.ts","sourceRoot":"","sources":["../../src/server/csrf.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { createApp, defineEventHandler, toWebHandler } from "h3";
|
|
3
|
+
import { csrfMiddleware, setCsrfCookie, csrfMetaTag, CSRF_COOKIE, CSRF_HEADER } from "./csrf.js";
|
|
4
|
+
// ─── csrfMetaTag ─────────────────────────────────────────────────────────────
|
|
5
|
+
describe("csrfMetaTag", () => {
|
|
6
|
+
it("generates a meta tag with the token", () => {
|
|
7
|
+
const html = csrfMetaTag("abc-123");
|
|
8
|
+
expect(html).toBe('<meta name="csrf-token" content="abc-123" />');
|
|
9
|
+
});
|
|
10
|
+
it("escapes double quotes in the token", () => {
|
|
11
|
+
const html = csrfMetaTag('token"with"quotes');
|
|
12
|
+
expect(html).toContain(""");
|
|
13
|
+
expect(html).not.toContain('content="token"');
|
|
14
|
+
});
|
|
15
|
+
it("handles empty token", () => {
|
|
16
|
+
const html = csrfMetaTag("");
|
|
17
|
+
expect(html).toBe('<meta name="csrf-token" content="" />');
|
|
18
|
+
});
|
|
19
|
+
it("handles UUID-style tokens", () => {
|
|
20
|
+
const html = csrfMetaTag("550e8400-e29b-41d4-a716-446655440000");
|
|
21
|
+
expect(html).toContain("550e8400-e29b-41d4-a716-446655440000");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
// ─── csrfMiddleware ───────────────────────────────────────────────────────────
|
|
25
|
+
describe("csrfMiddleware", () => {
|
|
26
|
+
const savedEnv = process.env["NODE_ENV"];
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
process.env["NODE_ENV"] = savedEnv;
|
|
29
|
+
});
|
|
30
|
+
function makeHandler() {
|
|
31
|
+
const app = createApp();
|
|
32
|
+
app.use(csrfMiddleware());
|
|
33
|
+
app.use(defineEventHandler(() => "ok"));
|
|
34
|
+
return toWebHandler(app);
|
|
35
|
+
}
|
|
36
|
+
const TOKEN = "550e8400-e29b-41d4-a716-446655440000";
|
|
37
|
+
// Safe methods pass without any token
|
|
38
|
+
it("allows GET without token in production", async () => {
|
|
39
|
+
process.env["NODE_ENV"] = "production";
|
|
40
|
+
const res = await makeHandler()(new Request("http://localhost/"));
|
|
41
|
+
expect(res.status).toBe(200);
|
|
42
|
+
});
|
|
43
|
+
it("allows HEAD without token in production", async () => {
|
|
44
|
+
process.env["NODE_ENV"] = "production";
|
|
45
|
+
const res = await makeHandler()(new Request("http://localhost/", { method: "HEAD" }));
|
|
46
|
+
expect(res.status).toBe(200);
|
|
47
|
+
});
|
|
48
|
+
it("allows OPTIONS without token in production", async () => {
|
|
49
|
+
process.env["NODE_ENV"] = "production";
|
|
50
|
+
const res = await makeHandler()(new Request("http://localhost/", { method: "OPTIONS" }));
|
|
51
|
+
expect(res.status).toBe(200);
|
|
52
|
+
});
|
|
53
|
+
// Valid POST
|
|
54
|
+
it("allows POST with matching cookie and header in production", async () => {
|
|
55
|
+
process.env["NODE_ENV"] = "production";
|
|
56
|
+
const res = await makeHandler()(new Request("http://localhost/", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
cookie: `${CSRF_COOKIE}=${TOKEN}`,
|
|
60
|
+
[CSRF_HEADER]: TOKEN,
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
expect(res.status).toBe(200);
|
|
64
|
+
});
|
|
65
|
+
// Missing token cases → 403
|
|
66
|
+
it("rejects POST with no cookie and no header in production", async () => {
|
|
67
|
+
process.env["NODE_ENV"] = "production";
|
|
68
|
+
const res = await makeHandler()(new Request("http://localhost/", { method: "POST" }));
|
|
69
|
+
expect(res.status).toBe(403);
|
|
70
|
+
});
|
|
71
|
+
it("rejects POST with cookie but no header in production", async () => {
|
|
72
|
+
process.env["NODE_ENV"] = "production";
|
|
73
|
+
const res = await makeHandler()(new Request("http://localhost/", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { cookie: `${CSRF_COOKIE}=${TOKEN}` },
|
|
76
|
+
}));
|
|
77
|
+
expect(res.status).toBe(403);
|
|
78
|
+
});
|
|
79
|
+
it("rejects POST with header but no cookie in production", async () => {
|
|
80
|
+
process.env["NODE_ENV"] = "production";
|
|
81
|
+
const res = await makeHandler()(new Request("http://localhost/", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { [CSRF_HEADER]: TOKEN },
|
|
84
|
+
}));
|
|
85
|
+
expect(res.status).toBe(403);
|
|
86
|
+
});
|
|
87
|
+
it("rejects POST with mismatched cookie and header in production", async () => {
|
|
88
|
+
process.env["NODE_ENV"] = "production";
|
|
89
|
+
const res = await makeHandler()(new Request("http://localhost/", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
cookie: `${CSRF_COOKIE}=token-a`,
|
|
93
|
+
[CSRF_HEADER]: "token-b",
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
expect(res.status).toBe(403);
|
|
97
|
+
});
|
|
98
|
+
// Non-production bypasses CSRF check
|
|
99
|
+
it("allows POST without token in development mode", async () => {
|
|
100
|
+
process.env["NODE_ENV"] = "development";
|
|
101
|
+
const res = await makeHandler()(new Request("http://localhost/", { method: "POST" }));
|
|
102
|
+
expect(res.status).toBe(200);
|
|
103
|
+
});
|
|
104
|
+
it("allows POST without token when NODE_ENV is unset", async () => {
|
|
105
|
+
delete process.env["NODE_ENV"];
|
|
106
|
+
const res = await makeHandler()(new Request("http://localhost/", { method: "POST" }));
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ─── setCsrfCookie ────────────────────────────────────────────────────────────
|
|
111
|
+
describe("setCsrfCookie", () => {
|
|
112
|
+
function makeSetCookieHandler() {
|
|
113
|
+
const app = createApp();
|
|
114
|
+
app.use(defineEventHandler((event) => {
|
|
115
|
+
const token = setCsrfCookie(event);
|
|
116
|
+
return { token };
|
|
117
|
+
}));
|
|
118
|
+
return toWebHandler(app);
|
|
119
|
+
}
|
|
120
|
+
it("sets a UUID token in the Set-Cookie header", async () => {
|
|
121
|
+
const res = await makeSetCookieHandler()(new Request("http://localhost/"));
|
|
122
|
+
const setCookieHeader = res.headers.get("set-cookie") ?? "";
|
|
123
|
+
expect(setCookieHeader).toMatch(/alab-csrf=[0-9a-f-]{36}/);
|
|
124
|
+
});
|
|
125
|
+
it("returns the generated token in the response", async () => {
|
|
126
|
+
const res = await makeSetCookieHandler()(new Request("http://localhost/"));
|
|
127
|
+
const body = (await res.json());
|
|
128
|
+
expect(body.token).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
129
|
+
});
|
|
130
|
+
it("reuses an existing cookie token instead of generating a new one", async () => {
|
|
131
|
+
const existingToken = "existing-csrf-token-value";
|
|
132
|
+
const res = await makeSetCookieHandler()(new Request("http://localhost/", {
|
|
133
|
+
headers: { cookie: `${CSRF_COOKIE}=${existingToken}` },
|
|
134
|
+
}));
|
|
135
|
+
const body = (await res.json());
|
|
136
|
+
expect(body.token).toBe(existingToken);
|
|
137
|
+
});
|
|
138
|
+
it("does not set HttpOnly on the cookie (must be readable by JS)", async () => {
|
|
139
|
+
const res = await makeSetCookieHandler()(new Request("http://localhost/"));
|
|
140
|
+
const setCookieHeader = (res.headers.get("set-cookie") ?? "").toLowerCase();
|
|
141
|
+
expect(setCookieHeader).not.toContain("httponly");
|
|
142
|
+
});
|
|
143
|
+
it("sets SameSite=Strict on the cookie", async () => {
|
|
144
|
+
const res = await makeSetCookieHandler()(new Request("http://localhost/"));
|
|
145
|
+
const setCookieHeader = (res.headers.get("set-cookie") ?? "").toLowerCase();
|
|
146
|
+
expect(setCookieHeader).toContain("samesite=strict");
|
|
147
|
+
});
|
|
148
|
+
it("sets path=/ on the cookie", async () => {
|
|
149
|
+
const res = await makeSetCookieHandler()(new Request("http://localhost/"));
|
|
150
|
+
const setCookieHeader = (res.headers.get("set-cookie") ?? "").toLowerCase();
|
|
151
|
+
expect(setCookieHeader).toContain("path=/");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
//# sourceMappingURL=csrf.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.test.js","sourceRoot":"","sources":["../../src/server/csrf.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Handle `/_alabjs/image` requests using the Rust image optimiser (alab-napi).
|
|
4
|
+
*
|
|
5
|
+
* Node.js reads the source file from `public/` then passes the raw bytes to
|
|
6
|
+
* Rust — same pattern as snapbolt-cli. Rust decodes, resizes, and encodes to
|
|
7
|
+
* WebP (libwebp-sys with `native` feature, or pure-Rust fallback).
|
|
8
|
+
*
|
|
9
|
+
* Query params:
|
|
10
|
+
* src — path relative to the project's `public/` directory (required)
|
|
11
|
+
* w — target width in pixels (required)
|
|
12
|
+
* q — quality 1–100 (default: 80)
|
|
13
|
+
* fmt — "webp" (default) | "jpeg" | "png"
|
|
14
|
+
*
|
|
15
|
+
* Cache-Control is set to 1 year / immutable for optimised responses.
|
|
16
|
+
*/
|
|
17
|
+
export declare function handleImageRequest(req: IncomingMessage, res: ServerResponse, publicDir: string): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/server/image.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAuFf"}
|