mintiljs 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.
Files changed (57) hide show
  1. package/README.md +192 -0
  2. package/auth/index.ts +1 -0
  3. package/i18n/index.ts +1 -0
  4. package/index.ts +1 -0
  5. package/package.json +68 -0
  6. package/src/auth/index.ts +5 -0
  7. package/src/auth/jwt.ts +101 -0
  8. package/src/auth/middleware.ts +150 -0
  9. package/src/auth/session.ts +46 -0
  10. package/src/auth/store.ts +39 -0
  11. package/src/auth/types.ts +55 -0
  12. package/src/cli/cli.ts +142 -0
  13. package/src/cli/generate.ts +142 -0
  14. package/src/core/client.ts +157 -0
  15. package/src/core/config.ts +16 -0
  16. package/src/core/css.ts +72 -0
  17. package/src/core/islands.ts +90 -0
  18. package/src/core/logger.ts +37 -0
  19. package/src/core/routes.ts +247 -0
  20. package/src/core/runtime.ts +131 -0
  21. package/src/core/watcher.ts +43 -0
  22. package/src/i18n/index.ts +57 -0
  23. package/src/i18n/loader.ts +125 -0
  24. package/src/i18n/translate.ts +89 -0
  25. package/src/i18n/types.ts +10 -0
  26. package/src/index.ts +10 -0
  27. package/src/render/island.tsx +78 -0
  28. package/src/render/ssr.tsx +198 -0
  29. package/src/render/stream.tsx +144 -0
  30. package/src/router/api.ts +111 -0
  31. package/src/router/index.ts +9 -0
  32. package/src/router/middleware.ts +16 -0
  33. package/src/router/pages.ts +97 -0
  34. package/src/router/shared.ts +3 -0
  35. package/src/types/api.ts +106 -0
  36. package/src/types/config.ts +35 -0
  37. package/src/types/index.ts +4 -0
  38. package/src/types/middleware.ts +21 -0
  39. package/src/types/page.ts +51 -0
  40. package/src/types/plugin.ts +29 -0
  41. package/src/utils/fs.ts +46 -0
  42. package/src/utils/logger.ts +21 -0
  43. package/src/utils/network.ts +14 -0
  44. package/templates/default/api/hello.ts +9 -0
  45. package/templates/default/components/card.tsx +10 -0
  46. package/templates/default/components/layouts/base.tsx +26 -0
  47. package/templates/default/lib/greeting.ts +3 -0
  48. package/templates/default/mintil.config.ts +10 -0
  49. package/templates/default/package.json +17 -0
  50. package/templates/default/pages/(*).tsx +11 -0
  51. package/templates/default/pages/about.tsx +13 -0
  52. package/templates/default/pages/blog/(:slug).tsx +12 -0
  53. package/templates/default/pages/counter.tsx +36 -0
  54. package/templates/default/pages/index.tsx +14 -0
  55. package/templates/default/public/readme.txt +1 -0
  56. package/templates/default/styles.css +1 -0
  57. package/templates/default/tsconfig.json +21 -0
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # MintilJS
2
+
3
+ > Minimal SSR web framework for Bun — React pages rendered on the server, zero client JavaScript unless you ask for it.
4
+
5
+ MintilJS is a full-stack framework built on **Bun**, **Hono**, and **React 19**. Drop files in `pages/` and they become SSR routes. Add `api/` files for JSON endpoints. Throw in `islands/` for partial hydration. All with zero configuration.
6
+
7
+ ```sh
8
+ bun create mintiljs my-app
9
+ cd my-app
10
+ bun run mintil dev
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **File-based routing** — `pages/` → SSR routes, `api/` → JSON endpoints
16
+ - **SSR by default** — zero JS in the browser unless you opt in
17
+ - **`useClient`** — per-page hydration bundles via `Bun.build`
18
+ - **Islands** — independent interactive components, no full-page re-render
19
+ - **`getServerSideProps`** — per-request data fetching
20
+ - **Hierarchical middleware** — global, API-wide, directory-scoped
21
+ - **Layouts** — root layout (`components/layouts/base.tsx`) + per-directory overrides
22
+ - **Tailwind v4** — automatic CSS processing via PostCSS
23
+ - **Streaming SSR** — `renderToReadableStream` for pages without client bundles
24
+ - **Auth module** — `mintiljs/auth` (JWT, sessions, middleware)
25
+ - **i18n module** — `mintiljs/i18n` (auto-detected messages, placeholders, CSR fetcher)
26
+ - **Plugin system** — extend the app at startup
27
+ - **Auto-reload** — file watcher in development
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ bun add mintiljs
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```
38
+ my-app/
39
+ pages/ → SSR routes
40
+ index.tsx /
41
+ blog/
42
+ (:slug).tsx /blog/:slug
43
+ layout.tsx layout scoped to /blog/*
44
+ api/ → JSON endpoints
45
+ hello.ts /api/hello
46
+ middleware.ts applies to all /api/*
47
+ islands/ → auto-registered hydratable components
48
+ Counter.tsx
49
+ components/
50
+ layouts/
51
+ base.tsx root layout
52
+ i18n/ → auto-detected messages
53
+ messages/
54
+ en/0.json
55
+ pt-BR.json
56
+ mintil.config.ts → project config (optional)
57
+ middleware.ts → root middleware (every request)
58
+ ```
59
+
60
+ ### Create a page
61
+
62
+ ```tsx
63
+ // pages/index.tsx → /
64
+ export default function Home() {
65
+ return <h1 className="text-3xl font-bold">Home</h1>;
66
+ }
67
+ ```
68
+
69
+ ### Add an API endpoint
70
+
71
+ ```ts
72
+ // api/hello.ts → GET /api/hello
73
+ import type { ApiHandler } from "mintiljs";
74
+
75
+ export default (c) => c.json({ message: "Hello" });
76
+ ```
77
+
78
+ ### Make a page interactive
79
+
80
+ ```tsx
81
+ // pages/counter.tsx
82
+ import React from "react";
83
+
84
+ export const useClient = true;
85
+
86
+ export default function Counter() {
87
+ const [count, setCount] = React.useState(0);
88
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
89
+ }
90
+ ```
91
+
92
+ ### Fetch data on every request
93
+
94
+ ```tsx
95
+ import type { GetServerSideProps } from "mintiljs";
96
+
97
+ export const getServerSideProps: GetServerSideProps = async ({ params, searchParams }) => {
98
+ const data = await db.find(params.id);
99
+ return { props: { data } };
100
+ };
101
+
102
+ export default function Page({ data }: { data: any }) {
103
+ return <div>{data.name}</div>;
104
+ }
105
+ ```
106
+
107
+ ## Configuration
108
+
109
+ Export a `MintilConfig` default from `mintil.config.ts`:
110
+
111
+ ```ts
112
+ import type { MintilConfig } from "mintiljs";
113
+
114
+ export default {
115
+ port: 3456,
116
+ host: false, // true = bind to 0.0.0.0
117
+ mode: "development", // "development" | "production"
118
+ plugins: [],
119
+ } satisfies MintilConfig;
120
+ ```
121
+
122
+ ## CLI
123
+
124
+ | Command | Description |
125
+ |---|---|
126
+ | `mintil init <name>` | Scaffold a new project |
127
+ | `mintil dev` | Dev mode with auto-reload |
128
+ | `mintil start` | Production mode |
129
+ | `mintil g page <n>` | Scaffold a page |
130
+ | `mintil g api <n>` | Scaffold an API route |
131
+ | `mintil g island <n>` | Scaffold an island |
132
+
133
+ ## Modules
134
+
135
+ ### Auth (`mintiljs/auth`)
136
+
137
+ ```ts
138
+ import { createAuthMiddleware, InMemorySessionStore } from "mintiljs/auth";
139
+
140
+ const auth = createAuthMiddleware({
141
+ jwt: { secret: process.env.JWT_SECRET!, expiresIn: "1h" },
142
+ session: { store: new InMemorySessionStore(), maxAge: 86400, cookieName: "session", cookiePath: "/" },
143
+ });
144
+
145
+ const token = await auth.signJWT({ sub: userId });
146
+ const payload = await auth.verifyJWT(token);
147
+ app.get("/api/admin", auth.requireAuth, handler);
148
+ ```
149
+
150
+ ### i18n (`mintiljs/i18n`)
151
+
152
+ ```
153
+ i18n/messages/en/0.json → { "greeting": "Hello" }
154
+ i18n/messages/en/1.json → { "welcome": "Welcome to {site}!" }
155
+ i18n/messages/pt-BR.json → { "greeting": "Olá" }
156
+ ```
157
+
158
+ ```ts
159
+ import { getMessages, t, format } from "mintiljs/i18n";
160
+
161
+ const msgs = await getMessages("en");
162
+ t(msgs, "greeting") // "Hello"
163
+ t(msgs, "welcome", { site: "MintilJS" }) // "Welcome to MintilJS!"
164
+ format("Hello {name}!", { name: "John" }) // "Hello John!"
165
+ ```
166
+
167
+ The framework auto-registers `/_mintil/i18n/:locale` for CSR usage.
168
+
169
+ ### Plugin System
170
+
171
+ ```ts
172
+ import type { MintilPlugin } from "mintiljs";
173
+
174
+ const health: MintilPlugin = {
175
+ name: "health",
176
+ setup(app) { app.get("/health", (c) => c.json({ ok: true })); },
177
+ };
178
+
179
+ export default { plugins: [health] } satisfies MintilConfig;
180
+ ```
181
+
182
+ ## API Reference
183
+
184
+ Full TypeDoc-generated documentation at `docs/api/`:
185
+
186
+ ```sh
187
+ bun run docs
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT
package/auth/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "../src/auth/index.ts";
package/i18n/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "../src/i18n/index.ts";
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index.ts";
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "mintiljs",
3
+ "version": "0.1.0",
4
+ "description": "Minimal SSR web framework for Bun — React pages rendered on the server, zero client JS unless you ask for it",
5
+ "type": "module",
6
+ "module": "index.ts",
7
+ "main": "index.ts",
8
+ "types": "index.ts",
9
+ "exports": {
10
+ ".": "./index.ts",
11
+ "./auth": "./src/auth/index.ts",
12
+ "./i18n": "./src/i18n/index.ts"
13
+ },
14
+ "bin": {
15
+ "mintil": "./src/cli/cli.ts"
16
+ },
17
+ "files": [
18
+ "index.ts",
19
+ "src",
20
+ "templates",
21
+ "i18n",
22
+ "auth"
23
+ ],
24
+ "scripts": {
25
+ "dev": "bun run src/cli/cli.ts dev",
26
+ "start": "bun run src/cli/cli.ts start",
27
+ "mintil": "bun run src/cli/cli.ts",
28
+ "docs": "bun x typedoc --options typedoc.json"
29
+ },
30
+ "dependencies": {
31
+ "hono": "^4.12.25",
32
+ "react": "^19.2.7",
33
+ "react-dom": "^19.2.7"
34
+ },
35
+ "optionalDependencies": {
36
+ "@tailwindcss/postcss": "^4.3.1",
37
+ "postcss": "^8.5.15"
38
+ },
39
+ "peerDependencies": {
40
+ "typescript": "^5"
41
+ },
42
+ "devDependencies": {
43
+ "@types/bun": "latest",
44
+ "@types/react": "^19.2.17",
45
+ "@types/react-dom": "^19.2.3"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/mozinova/mintiljs.git"
50
+ },
51
+ "homepage": "https://github.com/mozinova/mintiljs#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/mozinova/mintiljs/issues"
54
+ },
55
+ "license": "MIT",
56
+ "author": "Daniel Mucamba",
57
+ "keywords": [
58
+ "ssr",
59
+ "react",
60
+ "bun",
61
+ "hono",
62
+ "framework",
63
+ "server-rendering",
64
+ "islands",
65
+ "i18n",
66
+ "authentication"
67
+ ]
68
+ }
@@ -0,0 +1,5 @@
1
+ export { InMemorySessionStore } from "./store.ts";
2
+ export { signJWT, verifyJWT } from "./jwt.ts";
3
+ export { createSession, getSession, updateSession, destroySession } from "./session.ts";
4
+ export { createAuthMiddleware } from "./middleware.ts";
5
+ export type { Session, SessionStore, JWTConfig, SessionConfig, AuthConfig } from "./types.ts";
@@ -0,0 +1,101 @@
1
+ function base64urlEncode(buffer: ArrayBuffer): string {
2
+ const base64 = Buffer.from(buffer).toString("base64");
3
+ return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
4
+ }
5
+
6
+ function base64urlDecode(str: string): Uint8Array {
7
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
8
+ const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
9
+ return Buffer.from(padded, "base64");
10
+ }
11
+
12
+ function parseExpiresIn(value: string | number): number {
13
+ if (typeof value === "number") return value;
14
+ const match = value.match(/^(\d+)\s*(s|m|h|d|w)$/);
15
+ if (!match) return 3600;
16
+ const num = parseInt(match[1], 10);
17
+ switch (match[2]) {
18
+ case "s": return num;
19
+ case "m": return num * 60;
20
+ case "h": return num * 3600;
21
+ case "d": return num * 86400;
22
+ case "w": return num * 604800;
23
+ default: return 3600;
24
+ }
25
+ }
26
+
27
+ export async function signJWT(
28
+ payload: Record<string, any>,
29
+ secret: string,
30
+ options?: { expiresIn?: string | number; issuer?: string; audience?: string },
31
+ ): Promise<string> {
32
+ const header = { alg: "HS256", typ: "JWT" };
33
+ const now = Math.floor(Date.now() / 1000);
34
+ const tokenPayload: Record<string, any> = {
35
+ ...payload,
36
+ iat: now,
37
+ };
38
+
39
+ if (options?.expiresIn) {
40
+ tokenPayload.exp = now + parseExpiresIn(options.expiresIn);
41
+ }
42
+ if (options?.issuer) tokenPayload.iss = options.issuer;
43
+ if (options?.audience) tokenPayload.aud = options.audience;
44
+
45
+ const headerB64 = base64urlEncode(new TextEncoder().encode(JSON.stringify(header)));
46
+ const payloadB64 = base64urlEncode(new TextEncoder().encode(JSON.stringify(tokenPayload)));
47
+
48
+ const key = await crypto.subtle.importKey(
49
+ "raw",
50
+ new TextEncoder().encode(secret),
51
+ { name: "HMAC", hash: "SHA-256" },
52
+ false,
53
+ ["sign"],
54
+ );
55
+
56
+ const signature = await crypto.subtle.sign(
57
+ "HMAC",
58
+ key,
59
+ new TextEncoder().encode(`${headerB64}.${payloadB64}`),
60
+ );
61
+
62
+ return `${headerB64}.${payloadB64}.${base64urlEncode(signature)}`;
63
+ }
64
+
65
+ export async function verifyJWT(
66
+ token: string,
67
+ secret: string,
68
+ options?: { issuer?: string; audience?: string },
69
+ ): Promise<Record<string, any> | null> {
70
+ const parts = token.split(".");
71
+ if (parts.length !== 3) return null;
72
+
73
+ const [headerB64, payloadB64, signatureB64] = parts;
74
+
75
+ const key = await crypto.subtle.importKey(
76
+ "raw",
77
+ new TextEncoder().encode(secret),
78
+ { name: "HMAC", hash: "SHA-256" },
79
+ false,
80
+ ["verify"],
81
+ );
82
+
83
+ const valid = await crypto.subtle.verify(
84
+ "HMAC",
85
+ key,
86
+ base64urlDecode(signatureB64),
87
+ new TextEncoder().encode(`${headerB64}.${payloadB64}`),
88
+ );
89
+
90
+ if (!valid) return null;
91
+
92
+ const payload = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
93
+
94
+ const now = Math.floor(Date.now() / 1000);
95
+ if (payload.exp && payload.exp < now) return null;
96
+
97
+ if (options?.issuer && payload.iss !== options.issuer) return null;
98
+ if (options?.audience && payload.aud !== options.audience) return null;
99
+
100
+ return payload;
101
+ }
@@ -0,0 +1,150 @@
1
+ import type { Context } from "hono";
2
+ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
3
+ import { verifyJWT, signJWT } from "./jwt.ts";
4
+ import { createSession, getSession, destroySession } from "./session.ts";
5
+ import type { Session, SessionStore, JWTConfig } from "./types.ts";
6
+ import type { ApiResponse } from "../types/api.ts";
7
+
8
+ export function createAuthMiddleware(config: {
9
+ jwt: JWTConfig;
10
+ session: {
11
+ store: SessionStore;
12
+ maxAge: number;
13
+ cookieName: string;
14
+ cookiePath: string;
15
+ };
16
+ }) {
17
+ const { jwt: jwtConfig, session: sessionConfig } = config;
18
+
19
+ async function getSessionFromCookie(c: Context): Promise<Session | null> {
20
+ const raw = getCookie(c, sessionConfig.cookieName);
21
+ if (!raw) return null;
22
+
23
+ const payload = await verifyJWT(raw, jwtConfig.secret);
24
+ if (!payload || !payload.sid) return null;
25
+
26
+ return getSession(sessionConfig.store, payload.sid as string);
27
+ }
28
+
29
+ async function setSessionCookie(c: Context, sid: string): Promise<void> {
30
+ const token = await signJWT(
31
+ { sid },
32
+ jwtConfig.secret,
33
+ { expiresIn: sessionConfig.maxAge },
34
+ );
35
+
36
+ setCookie(c, sessionConfig.cookieName, token, {
37
+ httpOnly: true,
38
+ secure: true,
39
+ sameSite: "Lax",
40
+ path: sessionConfig.cookiePath,
41
+ maxAge: sessionConfig.maxAge,
42
+ });
43
+ }
44
+
45
+ function clearSessionCookie(c: Context): void {
46
+ deleteCookie(c, sessionConfig.cookieName, { path: sessionConfig.cookiePath });
47
+ }
48
+
49
+ return {
50
+ signJWT: (payload: Record<string, any>) =>
51
+ signJWT(payload, jwtConfig.secret, {
52
+ expiresIn: jwtConfig.expiresIn,
53
+ issuer: jwtConfig.issuer,
54
+ audience: jwtConfig.audience,
55
+ }),
56
+ verifyJWT: (token: string) =>
57
+ verifyJWT(token, jwtConfig.secret, {
58
+ issuer: jwtConfig.issuer,
59
+ audience: jwtConfig.audience,
60
+ }),
61
+
62
+ sessionMiddleware: async (c: ApiResponse, next: () => Promise<void>) => {
63
+ const ctx = c as unknown as Context;
64
+ const session = await getSessionFromCookie(ctx);
65
+ if (session) {
66
+ c.set("session", session);
67
+ c.set("userId", session.userId);
68
+ }
69
+ await next();
70
+ },
71
+
72
+ requireAuth: async (c: ApiResponse, next: () => Promise<void>) => {
73
+ const authHeader = c.req.header("Authorization");
74
+ if (authHeader?.startsWith("Bearer ")) {
75
+ const token = authHeader.slice(7);
76
+ const payload = await verifyJWT(token, jwtConfig.secret, {
77
+ issuer: jwtConfig.issuer,
78
+ audience: jwtConfig.audience,
79
+ });
80
+ if (payload) {
81
+ c.set("user", payload);
82
+ c.set("userId", payload.sub || payload.userId);
83
+ await next();
84
+ return;
85
+ }
86
+ }
87
+
88
+ const ctx = c as unknown as Context;
89
+ const session = await getSessionFromCookie(ctx);
90
+ if (session) {
91
+ c.set("session", session);
92
+ c.set("userId", session.userId);
93
+ await next();
94
+ return;
95
+ }
96
+
97
+ c.status(401);
98
+ return c.json({ error: "Unauthorized" });
99
+ },
100
+
101
+ optionalAuth: async (c: ApiResponse, next: () => Promise<void>) => {
102
+ const authHeader = c.req.header("Authorization");
103
+ if (authHeader?.startsWith("Bearer ")) {
104
+ const token = authHeader.slice(7);
105
+ const payload = await verifyJWT(token, jwtConfig.secret, {
106
+ issuer: jwtConfig.issuer,
107
+ audience: jwtConfig.audience,
108
+ });
109
+ if (payload) {
110
+ c.set("user", payload);
111
+ c.set("userId", payload.sub || payload.userId);
112
+ }
113
+ } else {
114
+ const ctx = c as unknown as Context;
115
+ const session = await getSessionFromCookie(ctx);
116
+ if (session) {
117
+ c.set("session", session);
118
+ c.set("userId", session.userId);
119
+ }
120
+ }
121
+ await next();
122
+ },
123
+
124
+ login: async (
125
+ c: ApiResponse,
126
+ userId: string,
127
+ sessionData?: Record<string, any>,
128
+ ) => {
129
+ const sess = await createSession(
130
+ sessionConfig.store,
131
+ userId,
132
+ sessionData,
133
+ sessionConfig.maxAge,
134
+ );
135
+ await setSessionCookie(c as unknown as Context, sess.id);
136
+ return sess;
137
+ },
138
+
139
+ logout: async (c: ApiResponse) => {
140
+ const session = c.get("session") as Session | undefined;
141
+ if (session) {
142
+ await destroySession(sessionConfig.store, session.id);
143
+ }
144
+ clearSessionCookie(c as unknown as Context);
145
+ },
146
+
147
+ setSessionCookie,
148
+ clearSessionCookie,
149
+ };
150
+ }
@@ -0,0 +1,46 @@
1
+ import crypto from "node:crypto";
2
+ import type { Session, SessionStore } from "./types.ts";
3
+
4
+ export function createSessionId(): string {
5
+ return crypto.randomUUID();
6
+ }
7
+
8
+ export async function createSession(
9
+ store: SessionStore,
10
+ userId: string,
11
+ data: Record<string, any> = {},
12
+ maxAge: number = 7 * 24 * 3600,
13
+ ): Promise<Session> {
14
+ const now = new Date();
15
+ const session: Session = {
16
+ id: createSessionId(),
17
+ userId,
18
+ data,
19
+ createdAt: now,
20
+ expiresAt: new Date(now.getTime() + maxAge * 1000),
21
+ };
22
+ await store.create(session);
23
+ return session;
24
+ }
25
+
26
+ export async function getSession(
27
+ store: SessionStore,
28
+ sessionId: string,
29
+ ): Promise<Session | null> {
30
+ return store.get(sessionId);
31
+ }
32
+
33
+ export async function updateSession(
34
+ store: SessionStore,
35
+ sessionId: string,
36
+ data: Record<string, any>,
37
+ ): Promise<void> {
38
+ await store.update(sessionId, data);
39
+ }
40
+
41
+ export async function destroySession(
42
+ store: SessionStore,
43
+ sessionId: string,
44
+ ): Promise<void> {
45
+ await store.destroy(sessionId);
46
+ }
@@ -0,0 +1,39 @@
1
+ import type { Session, SessionStore } from "./types.ts";
2
+
3
+ export class InMemorySessionStore implements SessionStore {
4
+ private sessions = new Map<string, Session>();
5
+
6
+ async create(session: Session): Promise<void> {
7
+ this.sessions.set(session.id, session);
8
+ }
9
+
10
+ async get(id: string): Promise<Session | null> {
11
+ const s = this.sessions.get(id);
12
+ if (!s) return null;
13
+ if (s.expiresAt < new Date()) {
14
+ this.sessions.delete(id);
15
+ return null;
16
+ }
17
+ return s;
18
+ }
19
+
20
+ async update(id: string, data: Record<string, any>): Promise<void> {
21
+ const session = this.sessions.get(id);
22
+ if (session) {
23
+ session.data = data;
24
+ }
25
+ }
26
+
27
+ async destroy(id: string): Promise<void> {
28
+ this.sessions.delete(id);
29
+ }
30
+
31
+ async cleanup(): Promise<void> {
32
+ const now = new Date();
33
+ for (const [id, session] of this.sessions) {
34
+ if (session.expiresAt < now) {
35
+ this.sessions.delete(id);
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,55 @@
1
+ /** Represents an authenticated user session. */
2
+ export interface Session {
3
+ /** Unique session identifier. */
4
+ id: string;
5
+ /** ID of the user this session belongs to. */
6
+ userId: string;
7
+ /** Arbitrary data stored with the session. */
8
+ data: Record<string, any>;
9
+ /** When the session was created. */
10
+ createdAt: Date;
11
+ /** When the session expires and should be considered invalid. */
12
+ expiresAt: Date;
13
+ }
14
+
15
+ /**
16
+ * Abstract interface for session persistence.
17
+ * Implement this to store sessions in Redis, SQLite, etc.
18
+ */
19
+ export interface SessionStore {
20
+ create(session: Session): Promise<void>;
21
+ get(id: string): Promise<Session | null>;
22
+ update(id: string, data: Record<string, any>): Promise<void>;
23
+ destroy(id: string): Promise<void>;
24
+ cleanup?(): Promise<void>;
25
+ }
26
+
27
+ /** Configuration options for JWT signing and verification. */
28
+ export interface JWTConfig {
29
+ /** HMAC-SHA256 secret key. Must be kept private. */
30
+ secret: string;
31
+ /** Token expiration duration (e.g. `"1h"`, `"7d"`, or seconds as number). */
32
+ expiresIn?: string | number;
33
+ /** Expected `iss` claim for verification. */
34
+ issuer?: string;
35
+ /** Expected `aud` claim for verification. */
36
+ audience?: string;
37
+ }
38
+
39
+ /** Configuration for cookie-based sessions. */
40
+ export interface SessionConfig {
41
+ /** The session store implementation. */
42
+ store: SessionStore;
43
+ /** Session max age in seconds (default: 7 days). */
44
+ maxAge?: number;
45
+ /** Cookie name (default: `"mintil_session"`). */
46
+ cookieName?: string;
47
+ /** Cookie path (default: `"/"`). */
48
+ cookiePath?: string;
49
+ }
50
+
51
+ /** Complete configuration for the auth module. */
52
+ export interface AuthConfig {
53
+ jwt: JWTConfig;
54
+ session?: SessionConfig;
55
+ }