gorsee 0.2.0 → 0.2.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/README.md +132 -4
- package/package.json +4 -2
- package/src/auth/index.ts +48 -17
- package/src/auth/redis-session-store.ts +46 -0
- package/src/auth/sqlite-session-store.ts +98 -0
- package/src/auth/store-utils.ts +21 -0
- package/src/build/client.ts +25 -7
- package/src/build/manifest.ts +34 -0
- package/src/build/route-metadata.ts +12 -0
- package/src/build/ssg.ts +19 -49
- package/src/cli/bun-plugin.ts +23 -2
- package/src/cli/cmd-build.ts +42 -71
- package/src/cli/cmd-check.ts +40 -26
- package/src/cli/cmd-create.ts +20 -5
- package/src/cli/cmd-deploy.ts +10 -2
- package/src/cli/cmd-dev.ts +9 -9
- package/src/cli/cmd-docs.ts +152 -0
- package/src/cli/cmd-generate.ts +15 -7
- package/src/cli/cmd-migrate.ts +15 -7
- package/src/cli/cmd-routes.ts +12 -5
- package/src/cli/cmd-start.ts +14 -5
- package/src/cli/cmd-test.ts +129 -0
- package/src/cli/cmd-typegen.ts +13 -3
- package/src/cli/cmd-upgrade.ts +143 -0
- package/src/cli/context.ts +12 -0
- package/src/cli/framework-md.ts +43 -16
- package/src/cli/index.ts +18 -0
- package/src/client.ts +26 -0
- package/src/dev/partial-handler.ts +17 -74
- package/src/dev/request-handler.ts +36 -67
- package/src/dev.ts +92 -157
- package/src/index-client.ts +4 -0
- package/src/index.ts +17 -2
- package/src/prod.ts +195 -253
- package/src/runtime/project.ts +73 -0
- package/src/server/cache-utils.ts +23 -0
- package/src/server/cache.ts +37 -14
- package/src/server/html-shell.ts +69 -0
- package/src/server/index.ts +40 -2
- package/src/server/manifest.ts +36 -0
- package/src/server/middleware.ts +18 -2
- package/src/server/not-found.ts +35 -0
- package/src/server/page-render.ts +123 -0
- package/src/server/redis-cache-store.ts +87 -0
- package/src/server/redis-client.ts +71 -0
- package/src/server/request-preflight.ts +45 -0
- package/src/server/route-request.ts +72 -0
- package/src/server/rpc-utils.ts +27 -0
- package/src/server/rpc.ts +70 -18
- package/src/server/sqlite-cache-store.ts +109 -0
- package/src/server/static-file.ts +63 -0
- package/src/server-entry.ts +36 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createContext, runMiddlewareChain, RedirectError, type Context, type MiddlewareFn } from "./middleware.ts"
|
|
2
|
+
import { resolvePageRoute, type ResolvedPageRoute } from "./page-render.ts"
|
|
3
|
+
import type { MatchResult } from "../router/matcher.ts"
|
|
4
|
+
|
|
5
|
+
type RouteModule = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
interface RouteRequestContext {
|
|
8
|
+
match: MatchResult
|
|
9
|
+
request: Request
|
|
10
|
+
mod: RouteModule
|
|
11
|
+
ctx: Context
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ResolvedRouteRequestContext extends RouteRequestContext {
|
|
15
|
+
resolved: ResolvedPageRoute
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RouteRequestOptions {
|
|
19
|
+
match: MatchResult
|
|
20
|
+
request: Request
|
|
21
|
+
extraMiddlewares?: MiddlewareFn[]
|
|
22
|
+
onPartialRequest: (ctx: ResolvedRouteRequestContext) => Promise<Response>
|
|
23
|
+
onPageRequest: (ctx: ResolvedRouteRequestContext) => Promise<Response>
|
|
24
|
+
onRouteError?: (error: unknown, ctx: ResolvedRouteRequestContext) => Promise<Response>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleRouteRequest(options: RouteRequestOptions): Promise<Response> {
|
|
28
|
+
const { match, request, extraMiddlewares = [], onPartialRequest, onPageRequest, onRouteError } = options
|
|
29
|
+
const mod = await import(match.route.filePath)
|
|
30
|
+
const ctx = createContext(request, match.params)
|
|
31
|
+
let routeCtx: ResolvedRouteRequestContext | undefined
|
|
32
|
+
|
|
33
|
+
if (typeof mod.GET === "function" && request.method === "GET") return (mod.GET as Function)(ctx)
|
|
34
|
+
if (typeof mod.POST === "function" && request.method === "POST") return (mod.POST as Function)(ctx)
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const resolved = await resolvePageRoute(mod, match, ctx)
|
|
38
|
+
if (!resolved) return new Response("Route has no default export", { status: 500 })
|
|
39
|
+
|
|
40
|
+
routeCtx = { match, request, mod, ctx, resolved }
|
|
41
|
+
const currentRouteCtx = routeCtx
|
|
42
|
+
const middlewares = await loadRouteMiddlewares(match, extraMiddlewares)
|
|
43
|
+
|
|
44
|
+
return await runMiddlewareChain(middlewares, ctx, async () => {
|
|
45
|
+
if (request.headers.get("X-Gorsee-Navigate") === "partial") {
|
|
46
|
+
return onPartialRequest(currentRouteCtx)
|
|
47
|
+
}
|
|
48
|
+
return onPageRequest(currentRouteCtx)
|
|
49
|
+
})
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof RedirectError) {
|
|
52
|
+
return new Response(null, {
|
|
53
|
+
status: error.status,
|
|
54
|
+
headers: { Location: error.url },
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
if (onRouteError && routeCtx) return onRouteError(error, routeCtx)
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function loadRouteMiddlewares(
|
|
63
|
+
match: MatchResult,
|
|
64
|
+
extraMiddlewares: MiddlewareFn[],
|
|
65
|
+
): Promise<MiddlewareFn[]> {
|
|
66
|
+
const middlewares = [...extraMiddlewares]
|
|
67
|
+
for (const mwPath of match.route.middlewarePaths) {
|
|
68
|
+
const mwMod = await import(mwPath)
|
|
69
|
+
if (typeof mwMod.default === "function") middlewares.push(mwMod.default)
|
|
70
|
+
}
|
|
71
|
+
return middlewares
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RPCRegistry } from "./rpc.ts"
|
|
2
|
+
|
|
3
|
+
export function createScopedRPCRegistry(registry: RPCRegistry, scope: string): RPCRegistry {
|
|
4
|
+
const prefix = `${scope}:`
|
|
5
|
+
const fileCounters = new Map<string, number>()
|
|
6
|
+
const scopedIds = new Set<string>()
|
|
7
|
+
return {
|
|
8
|
+
getHandler: (id) => registry.getHandler(prefix + id),
|
|
9
|
+
setHandler: (id, fn) => {
|
|
10
|
+
const scopedId = prefix + id
|
|
11
|
+
scopedIds.add(scopedId)
|
|
12
|
+
registry.setHandler(scopedId, fn)
|
|
13
|
+
},
|
|
14
|
+
deleteHandler: (id) => {
|
|
15
|
+
const scopedId = prefix + id
|
|
16
|
+
scopedIds.delete(scopedId)
|
|
17
|
+
registry.deleteHandler(scopedId)
|
|
18
|
+
},
|
|
19
|
+
clear: () => {
|
|
20
|
+
for (const scopedId of scopedIds) registry.deleteHandler(scopedId)
|
|
21
|
+
scopedIds.clear()
|
|
22
|
+
fileCounters.clear()
|
|
23
|
+
},
|
|
24
|
+
getFileCallCount: (file) => fileCounters.get(file) ?? 0,
|
|
25
|
+
setFileCallCount: (file, count) => { fileCounters.set(file, count) },
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/server/rpc.ts
CHANGED
|
@@ -9,23 +9,45 @@ export interface ServerOptions {
|
|
|
9
9
|
middleware?: unknown[]
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export type RPCHandler = (...args: unknown[]) => Promise<unknown>
|
|
13
|
+
|
|
14
|
+
export interface RPCRegistry {
|
|
15
|
+
getHandler(id: string): RPCHandler | undefined
|
|
16
|
+
setHandler(id: string, fn: RPCHandler): void
|
|
17
|
+
deleteHandler(id: string): void
|
|
18
|
+
clear(): void
|
|
19
|
+
getFileCallCount(file: string): number
|
|
20
|
+
setFileCallCount(file: string, count: number): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createMemoryRPCRegistry(): RPCRegistry {
|
|
24
|
+
const rpcHandlers = new Map<string, RPCHandler>()
|
|
25
|
+
const fileCallCounters = new Map<string, number>()
|
|
26
|
+
return {
|
|
27
|
+
getHandler: (id) => rpcHandlers.get(id),
|
|
28
|
+
setHandler: (id, fn) => { rpcHandlers.set(id, fn) },
|
|
29
|
+
deleteHandler: (id) => { rpcHandlers.delete(id) },
|
|
30
|
+
clear: () => {
|
|
31
|
+
rpcHandlers.clear()
|
|
32
|
+
fileCallCounters.clear()
|
|
33
|
+
},
|
|
34
|
+
getFileCallCount: (file) => fileCallCounters.get(file) ?? 0,
|
|
35
|
+
setFileCallCount: (file, count) => { fileCallCounters.set(file, count) },
|
|
36
|
+
}
|
|
37
|
+
}
|
|
14
38
|
|
|
15
|
-
|
|
16
|
-
const fileCallCounters = new Map<string, number>()
|
|
39
|
+
const defaultRPCRegistry = createMemoryRPCRegistry()
|
|
17
40
|
|
|
18
|
-
export function __registerRPC(id: string, fn:
|
|
19
|
-
|
|
41
|
+
export function __registerRPC(id: string, fn: RPCHandler): void {
|
|
42
|
+
defaultRPCRegistry.setHandler(id, fn)
|
|
20
43
|
}
|
|
21
44
|
|
|
22
45
|
export function __resetRPCState(): void {
|
|
23
|
-
|
|
24
|
-
rpcHandlers.clear()
|
|
46
|
+
defaultRPCRegistry.clear()
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
export function getRPCHandler(id: string):
|
|
28
|
-
return
|
|
49
|
+
export function getRPCHandler(id: string): RPCHandler | undefined {
|
|
50
|
+
return defaultRPCRegistry.getHandler(id)
|
|
29
51
|
}
|
|
30
52
|
|
|
31
53
|
function getCallerFile(): string | null {
|
|
@@ -52,22 +74,24 @@ export function server<TArgs extends unknown[], TReturn>(
|
|
|
52
74
|
): (...args: TArgs) => Promise<TReturn> {
|
|
53
75
|
const callerFile = getCallerFile()
|
|
54
76
|
if (callerFile) {
|
|
55
|
-
const counter =
|
|
56
|
-
|
|
77
|
+
const counter = defaultRPCRegistry.getFileCallCount(callerFile)
|
|
78
|
+
defaultRPCRegistry.setFileCallCount(callerFile, counter + 1)
|
|
57
79
|
const id = hashRPC(callerFile, counter)
|
|
58
|
-
|
|
80
|
+
defaultRPCRegistry.setHandler(id, fn as (...args: unknown[]) => Promise<unknown>)
|
|
59
81
|
}
|
|
60
82
|
return fn
|
|
61
83
|
}
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
export async function handleRPCRequestWithRegistry(
|
|
86
|
+
request: Request,
|
|
87
|
+
registry: Pick<RPCRegistry, "getHandler">,
|
|
88
|
+
): Promise<Response | null> {
|
|
65
89
|
const url = new URL(request.url)
|
|
66
90
|
const match = url.pathname.match(/^\/api\/_rpc\/([a-zA-Z0-9]+)$/)
|
|
67
91
|
if (!match) return null
|
|
68
92
|
|
|
69
93
|
const id = match[1]!
|
|
70
|
-
const handler =
|
|
94
|
+
const handler = registry.getHandler(id)
|
|
71
95
|
|
|
72
96
|
if (!handler) {
|
|
73
97
|
return new Response(JSON.stringify({ error: `RPC handler not found: ${id}` }), {
|
|
@@ -87,7 +111,28 @@ export async function handleRPCRequest(request: Request): Promise<Response | nul
|
|
|
87
111
|
headers: { "Content-Type": "application/json" },
|
|
88
112
|
})
|
|
89
113
|
}
|
|
90
|
-
|
|
114
|
+
// Read body with actual size limit (Content-Length can be spoofed)
|
|
115
|
+
const reader = request.body?.getReader()
|
|
116
|
+
let bodyBytes = 0
|
|
117
|
+
const chunks: Uint8Array[] = []
|
|
118
|
+
if (reader) {
|
|
119
|
+
while (true) {
|
|
120
|
+
const { done, value } = await reader.read()
|
|
121
|
+
if (done) break
|
|
122
|
+
bodyBytes += value.byteLength
|
|
123
|
+
if (bodyBytes > MAX_RPC_BODY) {
|
|
124
|
+
reader.cancel()
|
|
125
|
+
return new Response(JSON.stringify({ error: "Request body too large" }), {
|
|
126
|
+
status: 413,
|
|
127
|
+
headers: { "Content-Type": "application/json" },
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
chunks.push(value)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const body = new TextDecoder().decode(
|
|
134
|
+
chunks.length === 1 ? chunks[0] : Buffer.concat(chunks),
|
|
135
|
+
)
|
|
91
136
|
if (body) {
|
|
92
137
|
let parsed: unknown
|
|
93
138
|
try {
|
|
@@ -116,10 +161,17 @@ export async function handleRPCRequest(request: Request): Promise<Response | nul
|
|
|
116
161
|
headers: { "Content-Type": "application/json" },
|
|
117
162
|
})
|
|
118
163
|
} catch (err) {
|
|
119
|
-
|
|
164
|
+
// Don't leak internal error details to the client
|
|
165
|
+
const isDev = process.env.NODE_ENV !== "production"
|
|
166
|
+
const message = isDev && err instanceof Error ? err.message : "Internal server error"
|
|
120
167
|
return new Response(JSON.stringify({ error: message }), {
|
|
121
168
|
status: 500,
|
|
122
169
|
headers: { "Content-Type": "application/json" },
|
|
123
170
|
})
|
|
124
171
|
}
|
|
125
172
|
}
|
|
173
|
+
|
|
174
|
+
// RPC HTTP handler
|
|
175
|
+
export async function handleRPCRequest(request: Request): Promise<Response | null> {
|
|
176
|
+
return handleRPCRequestWithRegistry(request, defaultRPCRegistry)
|
|
177
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import type { CacheEntry, CacheStore } from "./cache.ts"
|
|
3
|
+
|
|
4
|
+
interface SQLiteCacheStoreOptions {
|
|
5
|
+
maxEntryAgeMs?: number
|
|
6
|
+
pruneExpiredOnInit?: boolean
|
|
7
|
+
pruneExpiredOnGet?: boolean
|
|
8
|
+
pruneExpiredOnSet?: boolean
|
|
9
|
+
pruneExpiredOnKeys?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ensureCacheTable(sqlite: Database): void {
|
|
13
|
+
sqlite.run(`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS gorsee_route_cache (
|
|
15
|
+
cache_key TEXT PRIMARY KEY,
|
|
16
|
+
body TEXT NOT NULL,
|
|
17
|
+
headers TEXT NOT NULL,
|
|
18
|
+
status INTEGER NOT NULL,
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
revalidating INTEGER NOT NULL DEFAULT 0
|
|
21
|
+
)
|
|
22
|
+
`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pruneExpiredRows(sqlite: Database, maxEntryAgeMs: number, now: number = Date.now()): number {
|
|
26
|
+
const cutoff = now - maxEntryAgeMs
|
|
27
|
+
const result = sqlite.prepare("DELETE FROM gorsee_route_cache WHERE created_at <= ?1").run(cutoff)
|
|
28
|
+
return (result as { changes?: number }).changes ?? 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createSQLiteCacheStore(
|
|
32
|
+
path = ":memory:",
|
|
33
|
+
options: SQLiteCacheStoreOptions = {},
|
|
34
|
+
): CacheStore & { close(): void; deleteExpired(now?: number): number } {
|
|
35
|
+
const sqlite = new Database(path)
|
|
36
|
+
sqlite.run("PRAGMA journal_mode=WAL;")
|
|
37
|
+
ensureCacheTable(sqlite)
|
|
38
|
+
const {
|
|
39
|
+
maxEntryAgeMs = 24 * 60 * 60 * 1000,
|
|
40
|
+
pruneExpiredOnInit = true,
|
|
41
|
+
pruneExpiredOnGet = true,
|
|
42
|
+
pruneExpiredOnSet = true,
|
|
43
|
+
pruneExpiredOnKeys = true,
|
|
44
|
+
} = options
|
|
45
|
+
|
|
46
|
+
if (pruneExpiredOnInit) pruneExpiredRows(sqlite, maxEntryAgeMs)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
async get(key) {
|
|
50
|
+
if (pruneExpiredOnGet) pruneExpiredRows(sqlite, maxEntryAgeMs)
|
|
51
|
+
const row = sqlite.prepare(`
|
|
52
|
+
SELECT body, headers, status, created_at, revalidating
|
|
53
|
+
FROM gorsee_route_cache
|
|
54
|
+
WHERE cache_key = ?1
|
|
55
|
+
`).get(key) as {
|
|
56
|
+
body: string
|
|
57
|
+
headers: string
|
|
58
|
+
status: number
|
|
59
|
+
created_at: number
|
|
60
|
+
revalidating: number
|
|
61
|
+
} | null
|
|
62
|
+
if (!row) return undefined
|
|
63
|
+
return {
|
|
64
|
+
body: row.body,
|
|
65
|
+
headers: JSON.parse(row.headers) as Record<string, string>,
|
|
66
|
+
status: row.status,
|
|
67
|
+
createdAt: row.created_at,
|
|
68
|
+
revalidating: row.revalidating === 1,
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async set(key, entry) {
|
|
72
|
+
if (pruneExpiredOnSet) pruneExpiredRows(sqlite, maxEntryAgeMs)
|
|
73
|
+
sqlite.prepare(`
|
|
74
|
+
INSERT INTO gorsee_route_cache (cache_key, body, headers, status, created_at, revalidating)
|
|
75
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
|
76
|
+
ON CONFLICT(cache_key) DO UPDATE SET
|
|
77
|
+
body = excluded.body,
|
|
78
|
+
headers = excluded.headers,
|
|
79
|
+
status = excluded.status,
|
|
80
|
+
created_at = excluded.created_at,
|
|
81
|
+
revalidating = excluded.revalidating
|
|
82
|
+
`).run(
|
|
83
|
+
key,
|
|
84
|
+
entry.body,
|
|
85
|
+
JSON.stringify(entry.headers),
|
|
86
|
+
entry.status,
|
|
87
|
+
entry.createdAt,
|
|
88
|
+
entry.revalidating ? 1 : 0,
|
|
89
|
+
)
|
|
90
|
+
},
|
|
91
|
+
async delete(key) {
|
|
92
|
+
sqlite.prepare("DELETE FROM gorsee_route_cache WHERE cache_key = ?1").run(key)
|
|
93
|
+
},
|
|
94
|
+
async clear() {
|
|
95
|
+
sqlite.prepare("DELETE FROM gorsee_route_cache").run()
|
|
96
|
+
},
|
|
97
|
+
async keys() {
|
|
98
|
+
if (pruneExpiredOnKeys) pruneExpiredRows(sqlite, maxEntryAgeMs)
|
|
99
|
+
const rows = sqlite.prepare("SELECT cache_key FROM gorsee_route_cache").all() as Array<{ cache_key: string }>
|
|
100
|
+
return rows.map((row) => row.cache_key)
|
|
101
|
+
},
|
|
102
|
+
deleteExpired(now = Date.now()) {
|
|
103
|
+
return pruneExpiredRows(sqlite, maxEntryAgeMs, now)
|
|
104
|
+
},
|
|
105
|
+
close() {
|
|
106
|
+
sqlite.close()
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getMimeType } from "./mime.ts"
|
|
2
|
+
import { fileETag, isNotModified } from "./etag.ts"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
|
|
5
|
+
export interface StaticFileOptions {
|
|
6
|
+
contentType?: string
|
|
7
|
+
cacheControl?: string
|
|
8
|
+
request?: Request
|
|
9
|
+
etag?: boolean
|
|
10
|
+
extraHeaders?: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function serveStaticFile(
|
|
14
|
+
rootDir: string,
|
|
15
|
+
relativePath: string,
|
|
16
|
+
options: StaticFileOptions = {},
|
|
17
|
+
): Promise<Response | null> {
|
|
18
|
+
const {
|
|
19
|
+
contentType,
|
|
20
|
+
cacheControl,
|
|
21
|
+
request,
|
|
22
|
+
etag = false,
|
|
23
|
+
extraHeaders = {},
|
|
24
|
+
} = options
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const filePath = resolve(rootDir, relativePath)
|
|
28
|
+
if (!filePath.startsWith(rootDir)) return null
|
|
29
|
+
|
|
30
|
+
const file = Bun.file(filePath)
|
|
31
|
+
if (!await file.exists()) return null
|
|
32
|
+
|
|
33
|
+
const headers: Record<string, string> = {
|
|
34
|
+
"Content-Type": contentType ?? getMimeType(filePath),
|
|
35
|
+
...extraHeaders,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (cacheControl) headers["Cache-Control"] = cacheControl
|
|
39
|
+
|
|
40
|
+
if (etag) {
|
|
41
|
+
const tag = await fileETag(filePath)
|
|
42
|
+
if (tag && request && isNotModified(request, tag)) {
|
|
43
|
+
return new Response(null, { status: 304, headers: extraHeaders })
|
|
44
|
+
}
|
|
45
|
+
if (tag) headers["ETag"] = tag
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Response(file, { headers })
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function servePrefixedStaticFile(
|
|
55
|
+
pathname: string,
|
|
56
|
+
prefix: string,
|
|
57
|
+
rootDir: string,
|
|
58
|
+
options: StaticFileOptions = {},
|
|
59
|
+
): Promise<Response | null> {
|
|
60
|
+
if (!pathname.startsWith(prefix)) return null
|
|
61
|
+
const relativePath = pathname.slice(prefix.length)
|
|
62
|
+
return serveStaticFile(rootDir, relativePath, options)
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Explicit server-facing public entrypoint.
|
|
2
|
+
// Use this in loaders, middleware, API routes, and server services.
|
|
3
|
+
|
|
4
|
+
export * from "./server/index.ts"
|
|
5
|
+
export {
|
|
6
|
+
createAuth,
|
|
7
|
+
createMemorySessionStore,
|
|
8
|
+
createNamespacedSessionStore,
|
|
9
|
+
createRedisSessionStore,
|
|
10
|
+
createSQLiteSessionStore,
|
|
11
|
+
type AuthConfig,
|
|
12
|
+
type Session,
|
|
13
|
+
type SessionStore,
|
|
14
|
+
} from "./auth/index.ts"
|
|
15
|
+
export { createDB, type DB, runMigrations, createMigration, type MigrationResult } from "./db/index.ts"
|
|
16
|
+
export {
|
|
17
|
+
securityHeaders,
|
|
18
|
+
type SecurityConfig,
|
|
19
|
+
csrfProtection,
|
|
20
|
+
generateCSRFToken,
|
|
21
|
+
validateCSRFToken,
|
|
22
|
+
createRateLimiter,
|
|
23
|
+
type RateLimiter,
|
|
24
|
+
cors,
|
|
25
|
+
type CORSOptions,
|
|
26
|
+
} from "./security/index.ts"
|
|
27
|
+
export { env, getPublicEnv, loadEnv } from "./env/index.ts"
|
|
28
|
+
export { log, setLogLevel } from "./log/index.ts"
|
|
29
|
+
export {
|
|
30
|
+
createNodeRedisLikeClient,
|
|
31
|
+
createIORedisLikeClient,
|
|
32
|
+
deleteExpiredRedisKeys,
|
|
33
|
+
type RedisLikeClient,
|
|
34
|
+
type NodeRedisClientLike,
|
|
35
|
+
type IORedisClientLike,
|
|
36
|
+
} from "./server/redis-client.ts"
|