@mostajs/auth-lite 0.1.1 → 0.3.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/index.d.ts CHANGED
@@ -1,18 +1,3 @@
1
- /**
2
- * @mostajs/auth-lite — minimal, dependency-free email/password + session auth
3
- * for Next.js (App Router) on top of @mostajs/orm.
4
- *
5
- * Why "lite": it boots in WebContainers (Bolt.new / StackBlitz) and the edge —
6
- * no native addon (no bcrypt/argon2), and it sets the session cookie on the
7
- * NextResponse object inside Route Handlers, which sidesteps the AsyncLocalStorage
8
- * pitfalls of `cookies()` after a DB call in constrained runtimes.
9
- *
10
- * Password hashing is salted, iterated SHA-256 (works everywhere). For a
11
- * production server you can swap in argon2/scrypt — the API is unchanged.
12
- *
13
- * @author Dr Hamid MADANI <drmdh@msn.com>
14
- */
15
- import type { NextRequest } from 'next/server';
16
1
  import type { EntitySchema } from '@mostajs/orm';
17
2
  export declare function hashPassword(password: string): string;
18
3
  export declare function verifyPassword(password: string, stored: string): boolean;
@@ -42,10 +27,13 @@ export interface AuthLiteConfig {
42
27
  loginErrorPath?: string;
43
28
  /** Redirect on signup error (default "/signup?error=<kind>"). */
44
29
  signupErrorPath?: (kind: 'invalid' | 'exists') => string;
30
+ /**
31
+ * Embedding in a **cross-site iframe** (ex. CodeSandbox preview `*.csb.app`) :
32
+ * un cookie `sameSite:'lax'` n'est PAS renvoyé → session perdue. Mettre `true`
33
+ * pour utiliser `sameSite:'none'; secure:true` **quand la requête est en
34
+ * https** (détecté via `x-forwarded-proto`), et retomber sur `lax` en http
35
+ * (localhost dev). Défaut `false` (lax). ⚠️ `none` réduit la protection CSRF —
36
+ * à n'activer que pour des previews embarquées, pas pour une vraie prod hors-iframe.
37
+ */
38
+ crossSiteCookie?: boolean;
45
39
  }
46
- export declare function createAuthHandlers(config: AuthLiteConfig): {
47
- login: (req: NextRequest) => Promise<import("next/server").NextResponse<unknown>>;
48
- signup: (req: NextRequest) => Promise<import("next/server").NextResponse<unknown>>;
49
- logout: (req: NextRequest) => Promise<import("next/server").NextResponse<unknown>>;
50
- };
51
- export declare function createGetCurrentUser<TUser = unknown>(config: AuthLiteConfig): () => Promise<TUser | null>;
package/dist/index.js CHANGED
@@ -1,15 +1,22 @@
1
- import { createHash, randomBytes, timingSafeEqual } from 'crypto';
2
1
  /**
3
- * Réponse 303 avec un `Location` **relatif** (ex. `/dashboard`). Le navigateur
4
- * le résout contre l'URL PUBLIQUE de la requête courante → fonctionne en
5
- * WebContainer (StackBlitz/Bolt qui ne transmettent pas l'hôte public au
6
- * serveur), derrière un reverse proxy, et en localhost, **sans que le serveur
7
- * ait besoin de connaître son hôte**. Plus robuste qu'une URL absolue.
2
+ * @mostajs/auth-lite minimal email/password + session auth on @mostajs/orm.
3
+ *
4
+ * This entry (`@mostajs/auth-lite`) is the **framework-agnostic core** :
5
+ * password hashing + the `Session` schema + the shared types. It imports
6
+ * **no `next`**, so it loads in Node / edge / WebContainer and is unit-testable.
7
+ *
8
+ * The **Next.js adapter** (Route Handlers + `getCurrentUser`) lives in the
9
+ * `@mostajs/auth-lite/next` subpath — it statically imports `next/server` and
10
+ * `next/headers` so that `cookies()` is the first `await` (request scope intact
11
+ * in WebContainers).
12
+ *
13
+ * Password hashing is salted, iterated SHA-256 (no native addon — boots
14
+ * everywhere). For a production server you can swap in argon2/scrypt; the API
15
+ * is unchanged.
16
+ *
17
+ * @author Dr Hamid MADANI <drmdh@msn.com>
8
18
  */
9
- async function see(location) {
10
- const { NextResponse } = await import('next/server');
11
- return new NextResponse(null, { status: 303, headers: { Location: location } });
12
- }
19
+ import { createHash, randomBytes, timingSafeEqual } from 'crypto';
13
20
  // ---------------------------------------------------------------------------
14
21
  // Password hashing — salted, iterated SHA-256 (no native addon, boots anywhere)
15
22
  // ---------------------------------------------------------------------------
@@ -49,84 +56,3 @@ export const SessionSchema = {
49
56
  indexes: [{ fields: ['token'], unique: true }],
50
57
  timestamps: true,
51
58
  };
52
- // ---------------------------------------------------------------------------
53
- // Route Handlers — login / signup / logout (set the cookie on the response)
54
- // ---------------------------------------------------------------------------
55
- export function createAuthHandlers(config) {
56
- const cookie = config.cookieName ?? 'session';
57
- const ttlMs = (config.ttlDays ?? 7) * 86400000;
58
- const afterAuth = config.afterAuth ?? '/dashboard';
59
- const afterLogout = config.afterLogout ?? '/';
60
- const loginError = config.loginErrorPath ?? '/login?error=invalid';
61
- const signupError = config.signupErrorPath ?? ((k) => `/signup?error=${k}`);
62
- async function startSession(sessions, userId) {
63
- const token = randomBytes(32).toString('hex');
64
- const expiresAt = new Date(Date.now() + ttlMs);
65
- await sessions.create({ token, user: userId, expiresAt });
66
- const res = await see(afterAuth);
67
- res.cookies.set(cookie, token, { httpOnly: true, sameSite: 'lax', path: '/', expires: expiresAt });
68
- return res;
69
- }
70
- /** POST handler — verify credentials, start a session. */
71
- async function login(req) {
72
- const form = await req.formData();
73
- const email = String(form.get('email') ?? '').toLowerCase().trim();
74
- const password = String(form.get('password') ?? '');
75
- const { users, sessions } = await config.getRepos();
76
- const user = await users.findOne({ email });
77
- if (!user || !verifyPassword(password, user.passwordHash)) {
78
- return see(loginError);
79
- }
80
- return startSession(sessions, user.id);
81
- }
82
- /** POST handler — create the account, start a session. */
83
- async function signup(req) {
84
- const form = await req.formData();
85
- const email = String(form.get('email') ?? '').toLowerCase().trim();
86
- const name = String(form.get('name') ?? '').trim();
87
- const password = String(form.get('password') ?? '');
88
- if (!email || !name || password.length < 6) {
89
- return see(signupError('invalid'));
90
- }
91
- const { users, sessions } = await config.getRepos();
92
- if (await users.findOne({ email })) {
93
- return see(signupError('exists'));
94
- }
95
- const user = await users.create({ email, name, passwordHash: hashPassword(password) });
96
- return startSession(sessions, user.id);
97
- }
98
- /** POST handler — destroy the session (DB + cookie). */
99
- async function logout(req) {
100
- const token = req.cookies.get(cookie)?.value;
101
- if (token) {
102
- const { sessions } = await config.getRepos();
103
- const session = await sessions.findOne({ token });
104
- if (session)
105
- await sessions.delete(session.id);
106
- }
107
- const res = await see(afterLogout);
108
- res.cookies.delete(cookie);
109
- return res;
110
- }
111
- return { login, signup, logout };
112
- }
113
- // ---------------------------------------------------------------------------
114
- // getCurrentUser — read the session in Server Components (cookie read before DB)
115
- // ---------------------------------------------------------------------------
116
- export function createGetCurrentUser(config) {
117
- const cookie = config.cookieName ?? 'session';
118
- return async function getCurrentUser() {
119
- const { cookies } = await import('next/headers');
120
- const token = (await cookies()).get(cookie)?.value;
121
- if (!token)
122
- return null;
123
- const { sessions } = await config.getRepos();
124
- const session = await sessions.findOne({ token });
125
- if (!session)
126
- return null;
127
- if (new Date(session.expiresAt) < new Date())
128
- return null;
129
- const populated = (await sessions.findByIdWithRelations(session.id, ['user']));
130
- return populated?.user ?? null;
131
- };
132
- }
package/dist/next.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @mostajs/auth-lite/next — Next.js (App Router) adapter.
3
+ *
4
+ * Route Handlers (login/signup/logout) + `getCurrentUser`. Imports `next/server`
5
+ * and `next/headers` **statically** on purpose:
6
+ * - the cookie is set on the `NextResponse` object (no `cookies()` write),
7
+ * - redirects use a **relative** `Location` → resolved by the browser against
8
+ * the public request URL, so it works in WebContainers (StackBlitz/Bolt,
9
+ * which don't forward the public host) and behind a reverse proxy,
10
+ * - `getCurrentUser` calls `cookies()` as the **first `await`** (no dynamic
11
+ * `import` before it) → the request AsyncLocalStorage scope stays intact in
12
+ * constrained runtimes (a preceding `await import(...)` loses it).
13
+ *
14
+ * The framework-agnostic core (hashing, `SessionSchema`, types) is in the root
15
+ * entry `@mostajs/auth-lite`.
16
+ *
17
+ * @author Dr Hamid MADANI <drmdh@msn.com>
18
+ */
19
+ import { type NextRequest, NextResponse } from 'next/server';
20
+ import { type AuthRepo, type AuthLiteConfig } from './index.js';
21
+ export type { AuthRepo, AuthLiteConfig };
22
+ export declare function createAuthHandlers(config: AuthLiteConfig): {
23
+ login: (req: NextRequest) => Promise<NextResponse<unknown>>;
24
+ signup: (req: NextRequest) => Promise<NextResponse<unknown>>;
25
+ logout: (req: NextRequest) => Promise<NextResponse<unknown>>;
26
+ };
27
+ export declare function createGetCurrentUser<TUser = unknown>(config: AuthLiteConfig): () => Promise<TUser | null>;
package/dist/next.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @mostajs/auth-lite/next — Next.js (App Router) adapter.
3
+ *
4
+ * Route Handlers (login/signup/logout) + `getCurrentUser`. Imports `next/server`
5
+ * and `next/headers` **statically** on purpose:
6
+ * - the cookie is set on the `NextResponse` object (no `cookies()` write),
7
+ * - redirects use a **relative** `Location` → resolved by the browser against
8
+ * the public request URL, so it works in WebContainers (StackBlitz/Bolt,
9
+ * which don't forward the public host) and behind a reverse proxy,
10
+ * - `getCurrentUser` calls `cookies()` as the **first `await`** (no dynamic
11
+ * `import` before it) → the request AsyncLocalStorage scope stays intact in
12
+ * constrained runtimes (a preceding `await import(...)` loses it).
13
+ *
14
+ * The framework-agnostic core (hashing, `SessionSchema`, types) is in the root
15
+ * entry `@mostajs/auth-lite`.
16
+ *
17
+ * @author Dr Hamid MADANI <drmdh@msn.com>
18
+ */
19
+ import { NextResponse } from 'next/server';
20
+ import { cookies } from 'next/headers';
21
+ import { randomBytes } from 'crypto';
22
+ import { hashPassword, verifyPassword } from './index.js';
23
+ /**
24
+ * 303 response with a **relative** `Location` (e.g. `/dashboard`). The browser
25
+ * resolves it against the current request's public URL → works in WebContainer
26
+ * / reverse-proxy / localhost without the server knowing its own host.
27
+ */
28
+ function see(location) {
29
+ return new NextResponse(null, { status: 303, headers: { Location: location } });
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Route Handlers — login / signup / logout (cookie set on the response)
33
+ // ---------------------------------------------------------------------------
34
+ export function createAuthHandlers(config) {
35
+ const cookie = config.cookieName ?? 'session';
36
+ const ttlMs = (config.ttlDays ?? 7) * 86400000;
37
+ const afterAuth = config.afterAuth ?? '/dashboard';
38
+ const afterLogout = config.afterLogout ?? '/';
39
+ const loginError = config.loginErrorPath ?? '/login?error=invalid';
40
+ const signupError = config.signupErrorPath ?? ((k) => `/signup?error=${k}`);
41
+ const crossSite = config.crossSiteCookie ?? false;
42
+ /** La requête arrive-t-elle en https (proxy forwarde `x-forwarded-proto`, ou URL https) ? */
43
+ function isHttps(req) {
44
+ if (req.headers.get('x-forwarded-proto') === 'https')
45
+ return true;
46
+ try {
47
+ return new URL(req.url).protocol === 'https:';
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /**
54
+ * Attributs du cookie de session. `crossSiteCookie` + https → `sameSite:'none'`
55
+ * + `secure` (cookie renvoyé en iframe cross-site, ex. CodeSandbox). Sinon
56
+ * `sameSite:'lax'` (et pas de `secure`, pour que localhost http garde la session).
57
+ */
58
+ function cookieOpts(req, expires) {
59
+ if (crossSite && isHttps(req)) {
60
+ return { httpOnly: true, sameSite: 'none', secure: true, path: '/', expires };
61
+ }
62
+ return { httpOnly: true, sameSite: 'lax', path: '/', expires };
63
+ }
64
+ async function openSession(req, sessions, userId) {
65
+ const token = randomBytes(32).toString('hex');
66
+ const expiresAt = new Date(Date.now() + ttlMs);
67
+ await sessions.create({ token, user: userId, expiresAt });
68
+ const res = see(afterAuth);
69
+ res.cookies.set(cookie, token, cookieOpts(req, expiresAt));
70
+ return res;
71
+ }
72
+ /** POST handler — verify credentials, start a session. */
73
+ async function login(req) {
74
+ const form = await req.formData();
75
+ const email = String(form.get('email') ?? '').toLowerCase().trim();
76
+ const password = String(form.get('password') ?? '');
77
+ const { users, sessions } = await config.getRepos();
78
+ const user = await users.findOne({ email });
79
+ if (!user || !verifyPassword(password, user.passwordHash)) {
80
+ return see(loginError);
81
+ }
82
+ return openSession(req, sessions, user.id);
83
+ }
84
+ /** POST handler — create the account, start a session. */
85
+ async function signup(req) {
86
+ const form = await req.formData();
87
+ const email = String(form.get('email') ?? '').toLowerCase().trim();
88
+ const name = String(form.get('name') ?? '').trim();
89
+ const password = String(form.get('password') ?? '');
90
+ if (!email || !name || password.length < 6) {
91
+ return see(signupError('invalid'));
92
+ }
93
+ const { users, sessions } = await config.getRepos();
94
+ if (await users.findOne({ email })) {
95
+ return see(signupError('exists'));
96
+ }
97
+ const user = await users.create({ email, name, passwordHash: hashPassword(password) });
98
+ return openSession(req, sessions, user.id);
99
+ }
100
+ /** POST handler — destroy the session (DB + cookie). */
101
+ async function logout(req) {
102
+ const token = req.cookies.get(cookie)?.value;
103
+ if (token) {
104
+ const { sessions } = await config.getRepos();
105
+ const session = await sessions.findOne({ token });
106
+ if (session)
107
+ await sessions.delete(session.id);
108
+ }
109
+ const res = see(afterLogout);
110
+ res.cookies.delete(cookie);
111
+ return res;
112
+ }
113
+ return { login, signup, logout };
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // getCurrentUser — read the session in Server Components.
117
+ // `cookies()` MUST be the first `await` (no preceding await/import) so the
118
+ // request scope survives in WebContainers.
119
+ // ---------------------------------------------------------------------------
120
+ export function createGetCurrentUser(config) {
121
+ const cookie = config.cookieName ?? 'session';
122
+ return async function getCurrentUser() {
123
+ const token = (await cookies()).get(cookie)?.value;
124
+ if (!token)
125
+ return null;
126
+ const { sessions } = await config.getRepos();
127
+ const session = await sessions.findOne({ token });
128
+ if (!session)
129
+ return null;
130
+ if (new Date(session.expiresAt) < new Date())
131
+ return null;
132
+ const populated = (await sessions.findByIdWithRelations(session.id, ['user']));
133
+ return populated?.user ?? null;
134
+ };
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/auth-lite",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Minimal email/password + session auth for Next.js on @mostajs/orm. No native addon — boots in Bolt.new / StackBlitz / edge.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
@@ -10,6 +10,11 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./next": {
15
+ "types": "./dist/next.d.ts",
16
+ "import": "./dist/next.js",
17
+ "default": "./dist/next.js"
13
18
  }
14
19
  },
15
20
  "files": [