@luckystack/login 0.8.3 → 0.8.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.4] - 2026-08-17
11
+
12
+ ### Changed
13
+
14
+ - Corrected password-reset/session documentation for hash-at-rest tokens, one-active-token-per-user invalidation, atomic shared-token consumption, current route paths, and pluggable session storage.
15
+
10
16
  ## [0.8.1] - 2026-07-27
11
17
 
12
18
  ### Fixed
package/CLAUDE.md CHANGED
@@ -11,13 +11,13 @@
11
11
 
12
12
  - **Credentials auth** (email + bcrypt-hashed password) with combined login/register dispatcher.
13
13
  - **OAuth 2.0** via a pluggable provider registry. Built-in helpers for Google, GitHub, Discord, Facebook, Microsoft. Custom providers register as raw `OAuthProvider` objects.
14
- - **Redis-backed sessions** with sliding expiration, CSRF token minting, single-session enforcement, and a swappable `SessionAdapter` (DynamoDB, Postgres, JWT-stateless, etc.).
14
+ - **Redis-backed default sessions** with sliding expiration, CSRF token minting, single-session enforcement, and a swappable `SessionAdapter` (DynamoDB, Postgres, JWT-stateless, etc.).
15
15
  - **Lifecycle hooks**: `preLogin` / `postLogin`, `preRegister` / `postRegister`, `preLogout` / `postLogout`, `preSessionCreate` / `postSessionCreate`, `preSessionDelete` / `postSessionDelete`, `passwordResetRequested`, `passwordResetCompleted`, `passwordChanged`. All `pre*` hooks support stop-signals (abort the side-effect with a reason key).
16
16
  - **Pluggable user store** via `UserAdapter` — decouples auth from a specific Prisma `User` schema.
17
17
  - **Password-reset primitives** (`createPasswordResetToken`, `consumePasswordResetToken`, `updatePasswordHash`, `verifyPassword`) plus the framework-mode `sendPasswordResetEmail` orchestrator (requires the optional `@luckystack/email` peer).
18
18
  - **Dynamic post-login redirect** resolver registry (per-user / per-tenant / per-provider OAuth landing pages). Relative resolver results inherit the trusted frontend `defaultUrl` origin, not the backend callback origin.
19
19
 
20
- OAuth state is stored in Redis under `${projectName}-oauth-state:<provider>:<state>` with TTL `auth.oauthStateTtlSeconds`. Sessions live at `${projectName}-session:<token>`. Active-tokens-per-user set lives at `${projectName}-activeUsers:<userId>`. Password-reset and email-change tokens are **hashed at rest** (0.2.0): only `sha256(token)` is stored as the Redis key (`${projectName}-pwreset:<sha256(token)>` / `${projectName}-email-change:<sha256(token)>`) via the `@luckystack/core` one-time-token primitive (`issueOneTimeToken` / `consumeOneTimeToken`). The raw token is returned to the caller exactly once for the emailed URL, so a leaked Redis keyspace can't be replayed to mint a reset or confirm an email change.
20
+ OAuth state is always stored in Redis under `${projectName}-oauth-state:<provider>:<state>` with TTL `auth.oauthStateTtlSeconds`. The default Redis session adapter stores sessions at `${projectName}-session:<token>` and active-tokens-per-user sets at `${projectName}-activeUsers:<userId>`; a custom `SessionAdapter` owns those storage details. Password-reset and email-change tokens are **hashed at rest** (0.2.0): only `sha256(token)` is stored as the Redis key (`${projectName}-pwreset:<sha256(token)>` / `${projectName}-email-change:<sha256(token)>`) via the `@luckystack/core` one-time-token primitive (`issueOneTimeToken` / `consumeOneTimeToken`). The raw token is returned to the caller exactly once for the emailed URL, so a leaked Redis keyspace can't be replayed to mint a reset or confirm an email change.
21
21
 
22
22
  ---
23
23
 
@@ -139,7 +139,7 @@ All config keys live on `ProjectConfig` (from `@luckystack/core`). Resolved at c
139
139
  | `FACEBOOK_CLIENT_ID` / `FACEBOOK_CLIENT_SECRET` | `facebookProvider` | OAuth app credentials. |
140
140
  | `MICROSOFT_CLIENT_ID` / `MICROSOFT_CLIENT_SECRET` | `microsoftProvider` | OAuth app credentials. |
141
141
  | `MICROSOFT_TENANT_ID` | `microsoftProvider` (optional) | Single-tenant Azure AD. Defaults to `'common'`. |
142
- | (callback origin) | All OAuth `callbackUrl` builders | The `callbackUrl` is the BACKEND origin + `/auth/callback/<provider>`. The scaffold derives it in `config.ts` (`oauthCallbackBase`: dev `http://localhost:80`, prod the public domain). No `DNS` env var — that was removed in 0.1.5. |
142
+ | (callback origin) | All OAuth `callbackUrl` builders | The `callbackUrl` is the BACKEND origin + `/auth/callback/<provider>`. The scaffold derives dev from `config.ports.ts` (or the explicit CLI port) and production from `PUBLIC_URL`; generic consumers still fall back to `app.publicUrl`. No `DNS` env var is required by the framework OAuth contract. |
143
143
  | `BCRYPT_ROUNDS` | `auth.bcryptRounds` | Surfaced through project config; salt rounds for credentials hashing. |
144
144
  | `TOTP_ENCRYPTION_KEY` | TOTP at-rest encryption | Current primary write key; new rows are `enc:v2:<key-id>:...`. |
145
145
  | `TOTP_ENCRYPTION_LEGACY_KEYS` | TOTP key rotation | JSON array of decrypt-only previous keys. Successful TOTP proofs lazily rewrite legacy/plaintext rows under the primary. |
@@ -180,7 +180,7 @@ All config keys live on `ProjectConfig` (from `@luckystack/core`). Resolved at c
180
180
  | `bcryptjs` | hard runtime dep | Password hashing + comparison. |
181
181
  | `validator` | hard runtime dep | Email validation + name escaping. |
182
182
  | `dotenv` | hard runtime dep | Env loading at boot. |
183
- | `@prisma/client` | **required peer** | Default `UserAdapter` talks to `prisma.user`. Register a custom `UserAdapter` if you cannot ship Prisma. |
183
+ | `@prisma/client` | **optional peer** | The default `UserAdapter` talks to `prisma.user`; register a custom `UserAdapter` when Prisma is not used. |
184
184
  | `socket.io` | **required peer** | `logout` and session enforcement broadcast through the live `io` instance from `getIoInstance()`. |
185
185
  | `@luckystack/email` | **optional peer** | Only required when `auth.forgotPassword === 'framework'`. Lazy-imported by `sendPasswordResetEmail` — every other API works without it. |
186
186
 
@@ -188,7 +188,7 @@ All config keys live on `ProjectConfig` (from `@luckystack/core`). Resolved at c
188
188
 
189
189
  ## Related links
190
190
 
191
- - Repo root contract: [`/.claude/CLAUDE.md`](../../.claude/CLAUDE.md)
191
+ - Repository-root contract: [`CLAUDE.md`](../../CLAUDE.md)
192
192
  - Package README: [`./README.md`](./README.md)
193
193
  - Architecture — auth lifecycle: [`/docs/ARCHITECTURE_AUTH.md`](../../docs/ARCHITECTURE_AUTH.md)
194
194
  - Architecture — sessions (Redis layout, sliding expiration, single-session enforcement): [`/docs/ARCHITECTURE_SESSION.md`](../../docs/ARCHITECTURE_SESSION.md)
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # @luckystack/login
2
2
 
3
- > Authentication for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). Credentials + OAuth (Google, GitHub, Facebook, Discord, Microsoft), Redis-backed sessions, single-session enforcement, and lifecycle hooks (`preLogin`, `preRegister`, `preLogout`, `preSessionCreate`, `preSessionDelete` and their `post*` counterparts).
3
+ > Authentication for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). Credentials + OAuth (Google, GitHub, Facebook, Discord, Microsoft), Redis-backed default sessions with a swappable session adapter, single-session enforcement, and lifecycle hooks (`preLogin`, `preRegister`, `preLogout`, `preSessionCreate`, `preSessionDelete` and their `post*` counterparts).
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @luckystack/login @luckystack/core @prisma/client socket.io
8
+ npm install @luckystack/login socket.io
9
9
  ```
10
10
 
11
+ The default user adapter uses Prisma; install optional peer `@prisma/client` when you use it. Framework-mode email flows additionally need optional peer `@luckystack/email`. Projects with custom `UserAdapter` and `SessionAdapter` implementations can use another data layer and session store.
12
+
11
13
  ## Quickstart
12
14
 
13
15
  ```ts
@@ -28,7 +30,7 @@ const result = await loginWithCredentials({ email, password });
28
30
  // → { status: false, reason } on failure (including hook-stop with the hook's errorCode)
29
31
  ```
30
32
 
31
- Sessions are stored in Redis under `${PROJECT_NAME}-session:<token>` and are sliding (every authenticated read extends the TTL by `ProjectConfig.session.expiryDays`).
33
+ The default `redisSessionAdapter` stores sessions under `${PROJECT_NAME}-session:<token>` and applies sliding expiration (every authenticated read extends the TTL by `ProjectConfig.session.expiryDays`). Register a custom `SessionAdapter` when sessions should live in another store.
32
34
 
33
35
  ## Hooks
34
36
 
@@ -195,8 +197,8 @@ import {
195
197
  | `loginCallback(pathname, req, res)` | OAuth state-exchange handler — wired to `/auth/callback/<provider>` by `@luckystack/server`. |
196
198
  | `createOAuthState(providerName)` | Issue a CSRF state token (Redis, NX, TTL from project config). |
197
199
  | `logout({ token, socket, userId })` | End a single socket's session. |
198
- | `saveSession(token, user, newUser?)` | Write to Redis + broadcast to existing connections. |
199
- | `getSession(token)` | Read + slide expiration. Dispatches `preSessionRefresh` / `postSessionRefresh` around the Redis EXPIRE call. |
200
+ | `saveSession(token, user, newUser?)` | Write through the active `SessionAdapter` + broadcast to existing connections. |
201
+ | `getSession(token)` | Read + slide expiration through the active adapter. Dispatches `preSessionRefresh` / `postSessionRefresh` around the adapter TTL refresh. |
200
202
  | `deleteSession(token)` | Hard delete + clean up active-tokens set. |
201
203
  | `getAllSessions()` | Admin utility — scans all sessions. |
202
204
  | `revokeUserSessions(userId)` | Force-logout every active session for a user. |
@@ -204,6 +206,7 @@ import {
204
206
  | `registerOAuthProviders(list)` / `getOAuthProviders()` / `isFullOAuthProvider(p)` | OAuth registry. |
205
207
  | `googleProvider`, `githubProvider`, `discordProvider`, `facebookProvider`, `microsoftProvider`, `credentialsProvider` | Built-in provider factories. |
206
208
  | `registerUserAdapter(adapter)` / `getUserAdapter()` / `isUserAdapterRegistered()` / `defaultPrismaUserAdapter` | Pluggable user store. |
209
+ | `registerSessionAdapter(adapter)` / `getSessionAdapter()` / `redisSessionAdapter` | Pluggable session storage; Redis is the default. |
207
210
  | `registerPostLoginRedirect(resolver)` / `getPostLoginRedirect()` | Dynamic redirect resolution. |
208
211
  | `createPasswordResetToken`, `consumePasswordResetToken`, `updatePasswordHash`, `verifyPassword`, `sendPasswordResetEmail` | Password-reset primitives. |
209
212
 
@@ -221,18 +224,16 @@ If your project has no such surfaces this is informational. The framework intent
221
224
  ## Related architecture docs
222
225
 
223
226
  - [`docs/ARCHITECTURE_AUTH.md`](../../docs/ARCHITECTURE_AUTH.md) — OAuth + credentials lifecycle, allowed-origin checks, role guards.
224
- - [`docs/ARCHITECTURE_SESSION.md`](../../docs/ARCHITECTURE_SESSION.md) — Redis layout, sliding expiration, single-session enforcement.
227
+ - [`docs/ARCHITECTURE_SESSION.md`](../../docs/ARCHITECTURE_SESSION.md) — session adapters, default Redis layout, sliding expiration, and single-session enforcement.
225
228
  - [`docs/ARCHITECTURE_EMAIL.md`](../../docs/ARCHITECTURE_EMAIL.md) — forgot-password modes (`framework` / `custom` / `disabled`).
226
229
 
227
230
  ## Dependencies
228
231
 
229
232
  - Runtime: `@luckystack/core`, `bcryptjs`, `validator`, `dotenv`
230
- - Peer (canonical ranges, standardized 2026-05-07):
231
- - `@prisma/client@^6.19.0`
232
- - `socket.io@^4.8.0`
233
- - Optional peer: `@luckystack/email` — only required when `forgotPassword: 'framework'`. The package lazy-imports it; without it, the framework-mode flow is disabled but every other API works.
233
+ - Required peer: `socket.io@^4.8.0`
234
+ - Optional peers: `@prisma/client@^6.19.0` (default user adapter) and `@luckystack/email` (only when `forgotPassword: 'framework'`). The package lazy-imports optional peers; every other API works without them.
234
235
 
235
- Your Prisma schema must include a `User` model with at least: `id`, `email`, `provider` (enum `PROVIDERS`), `password` (nullable), `name`, `avatar`, `avatarFallback`, `admin`, `language`. See [`prisma/schema.prisma`](../../prisma/schema.prisma) for the canonical shape, or register a `UserAdapter` to talk to a different schema.
236
+ When using the default Prisma `UserAdapter`, your schema must include a `User` model with at least: `id`, `email`, `provider` (enum `PROVIDERS`), `password` (nullable), `name`, `avatar`, `avatarFallback`, `admin`, `language`. See [`prisma/schema.prisma`](../../prisma/schema.prisma) for the canonical shape. A custom `UserAdapter` and `SessionAdapter` can bind auth to another data layer and session store.
236
237
 
237
238
  ## License
238
239
 
package/docs/hooks.md CHANGED
@@ -166,7 +166,7 @@ registerHook('postLogout', async ({ userId, token }) => {
166
166
  { token: string; user: BaseSessionLayout; persistent: boolean }
167
167
  ```
168
168
 
169
- `persistent` is the inverse of `projectConfig.session.basedToken` — `true` means "this session survives a browser restart" (the default), `false` means "session-cookie only". Audit handlers can use this to differentiate a "remember me" login from a one-shot one.
169
+ `persistent` is the inverse of `projectConfig.session.basedToken` — `true` means cookie mode (the credential can survive a browser restart according to cookie expiry, and is shared across tabs); `false` means tab-scoped `sessionStorage` bearer mode. Audit handlers can use this to differentiate a "remember me" login from a one-shot one.
170
170
 
171
171
  `user` is the freshly-sanitized session layout — password already stripped, avatar already resolved. Hooks should treat it as read-only; mutating it does NOT affect the saved session (the framework writes the original reference, but other hooks expect immutability).
172
172
 
@@ -18,18 +18,23 @@ This is the password lifecycle from "user clicks Forgot password" through "new p
18
18
 
19
19
  The mode is read at call time via `getProjectConfig()`, so consumers can toggle it through `registerProjectConfig` without restarting. In practice, switching modes mid-process is uncommon — pick a mode at boot and stick with it.
20
20
 
21
- ## Token layout
21
+ ## Token layout and hash-at-rest contract
22
22
 
23
23
  ```ts
24
- const token = randomBytes(32).toString('hex'); // 64 char URL-safe string
25
- const key = `${getProjectName()}-pwreset:${token}`;
26
- await redis.set(key, userId, 'EX', auth.passwordResetTtlSeconds);
24
+ const handle = issueOneTimeToken('-pwreset', auth.passwordResetTtlSeconds, userId);
25
+ await handle.store();
26
+
27
+ // Redis stores only the hash-derived key:
28
+ // <projectName>-pwreset:<sha256(handle.token)> -> userId
29
+ return handle.token; // raw 64-character token, returned only for delivery
27
30
  ```
28
31
 
29
- - **Entropy** — 256 bits via `randomBytes(32)`. Hex-encoded so it can be passed in a URL query string without any escaping.
30
- - **Storage** — Redis. Single key holds the `userId`; one-time use means the redemption deletes the key.
31
- - **Namespace** — `${projectName}-pwreset:<token>`. Shared `getProjectName()` helper means the namespace matches sessions, activeUsers, OAuth state, and rate-limit (no drift between Redis keys).
32
- - **TTL** — `auth.passwordResetTtlSeconds`. Default 3600 (1 hour). Tighten for high-security deployments; loosen if your users frequently check email on a different device than they reset on.
32
+ - **Entropy** — 256 bits via `randomBytes(32)`, hex-encoded as a 64-character URL-safe token.
33
+ - **Hash at rest** — the raw token is never persisted. `issueOneTimeToken` derives a SHA-256 key suffix, so a Redis keyspace/backup leak does not yield a redeemable reset link.
34
+ - **Namespace** — `formatKey('-pwreset', sha256(token))`; the active project/tenant key formatter remains authoritative.
35
+ - **Single active token per user** — `${projectName}-pwreset-user:<userId>` points to the current hash key. Issuing a replacement deletes the previous outstanding token before storing the new one.
36
+ - **TTL** — both the hash record and per-user pointer use `auth.passwordResetTtlSeconds` (default 3600 seconds).
37
+ - **Storage boundary** — password-reset tokens intentionally use the core one-time-token Redis primitive; this is independent of the pluggable session-storage adapter.
33
38
 
34
39
  ## Primitives
35
40
 
@@ -39,9 +44,9 @@ await redis.set(key, userId, 'EX', auth.passwordResetTtlSeconds);
39
44
  createPasswordResetToken(userId: string): Promise<string>;
40
45
  ```
41
46
 
42
- Mint + store + return. Caller is responsible for getting the token to the user — typically embedded in a `${appPublicUrl}/reset-password?token=<token>` URL inside an email, but custom modes might send it via SMS, push notification, or an in-app banner.
47
+ Invalidate the user's previous outstanding reset token, mint/store a new hash-at-rest token, persist the per-user pointer, and return the raw token. The caller is responsible for delivering it—normally in an `${appPublicUrl}/reset-password?token=<token>` URL.
43
48
 
44
- Always succeeds (returns the token string). If Redis is down, the underlying `redis.set` throws and propagates — wrap in `tryCatch` at the call site if you want to surface "email service degraded" rather than 500.
49
+ The promise rejects when Redis cannot read/delete/store the token records. Framework-mode callers normalize that failure; custom callers should use the standard `tryCatch` helper when they need a typed degradation path.
45
50
 
46
51
  ### `consumePasswordResetToken(token)`
47
52
 
@@ -49,23 +54,9 @@ Always succeeds (returns the token string). If Redis is down, the underlying `re
49
54
  consumePasswordResetToken(token: string): Promise<string | null>;
50
55
  ```
51
56
 
52
- Atomic `GET` + `DEL` in a Redis transaction:
53
-
54
- ```ts
55
- const txResult = await redis.multi().get(key).del(key).exec();
56
- const [getErr, value] = txResult[0];
57
- if (getErr) return null;
58
- return typeof value === 'string' && value.length > 0 ? value : null;
59
- ```
60
-
61
- Returns the bound `userId` on success, `null` on:
62
-
63
- - Empty or non-string `token` argument.
64
- - Token expired (Redis returns null for the GET).
65
- - Token already consumed (the DEL on a prior call already removed it).
66
- - The transaction failed entirely (Redis network blip).
57
+ Delegates to `consumeOneTimeToken('-pwreset', token)`. The shared primitive hashes the supplied raw token to derive the Redis key and performs one `MULTI` containing `GET` + `DEL`. It returns the bound `userId` only when both commands succeed.
67
58
 
68
- One-time-use is enforced by the atomic DEL — a second call with the same token returns `null` because the key is already gone. Replay attacks defeated.
59
+ Returns `null` for an empty/malformed token, missing or expired hash record, an already-consumed token, or any transaction/DEL failure. A concurrent second redemption sees no value and fails closed, providing at-most-once consumption.
69
60
 
70
61
  ### `updatePasswordHash(userId, plaintext)`
71
62
 
@@ -80,10 +71,11 @@ const reason = validatePassword(plaintext);
80
71
  if (reason) throw new PasswordPolicyError(reason);
81
72
  const salt = await bcrypt.genSalt(getProjectConfig().auth.bcryptRounds);
82
73
  const hashedPassword = await bcrypt.hash(plaintext, salt);
83
- await getUserAdapter().update(userId, { password: hashedPassword } as never);
74
+ const patch: Record<string, unknown> = { password: hashedPassword };
75
+ await getUserAdapter().update(userId, patch);
84
76
  ```
85
77
 
86
- Throws `PasswordPolicyError` on policy violation. The cast to `never` on the patch tells TypeScript "we know `password` may not be a typed field on `UserRecord`, trust me" — the framework's `UserAdapter` interface is intentionally loose for this kind of optional column.
78
+ Throws `PasswordPolicyError` on policy violation. `UserAdapter.update` accepts a loose patch because custom user stores may expose different optional credential fields; no transport-typing cast is needed.
87
79
 
88
80
  Note this primitive deliberately does NOT consume a reset token. The reset-password completion API is responsible for the sequence:
89
81
 
@@ -209,9 +201,9 @@ const { sendEmail, renderEmailLayout } = await (
209
201
  The framework-mode flow has two HTTP endpoints (wired by `@luckystack/server` via the file-based router):
210
202
 
211
203
  ```
212
- 1. POST /api/login/sendReset/v1 → sendPasswordResetEmail({ email })
213
- └ token now in Redis, email on its way
214
- 2. POST /api/login/completeReset/v1 → consumePasswordResetToken(token)
204
+ 1. POST /api/reset-password/sendReset/v1 → sendPasswordResetEmail({ email })
205
+ └ token hash now in Redis; raw token is emailed
206
+ 2. POST /api/reset-password/confirmReset/v1 → consumePasswordResetToken(token)
215
207
  └ updatePasswordHash(userId, newPassword)
216
208
  └ revokeUserSessions(userId, currentToken?)
217
209
  └ dispatchHook('passwordResetCompleted', { userId, revokedOtherSessions })
@@ -11,7 +11,8 @@ Deep-dive on `saveSession` / `getSession` / `deleteSession`, sliding expiration,
11
11
  | `${projectName}-session:<token>` | string | JSON-encoded `BaseSessionLayout` (id, email, name, csrfToken, custom fields). |
12
12
  | `${projectName}-activeUsers:<userId>` | set | Tokens currently associated with this user. Read by single-session enforcement. |
13
13
  | `${projectName}-oauth-state:<provider>:<state>` | string | `'1'` sentinel + TTL. Consumed atomically on OAuth callback. (See `login.ts`.) |
14
- | `${projectName}-pwreset:<token>` | string | `userId` bound to the reset token. (See [`./password-reset.md`](./password-reset.md).) |
14
+ | `${projectName}-pwreset:<sha256(token)>` | string | `userId` bound to the hash-at-rest one-time token; raw token is never persisted. |
15
+ | `${projectName}-pwreset-user:<userId>` | string | Hash-key pointer for the user's only active reset token; replacement issuance invalidates the prior key. |
15
16
 
16
17
  `projectName` comes from `getProjectName()` in `@luckystack/core` — a single source of truth so the namespace is identical across sessions, activeUsers, OAuth state, password reset, and rate-limit.
17
18
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@luckystack/login",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": true
8
8
  },
9
9
  "type": "module",
10
- "description": "Authentication for LuckyStack: credentials + OAuth (Google/GitHub/Facebook/Discord), Redis-backed sessions, single-session enforcement, lifecycle hooks. Pairs with @luckystack/core.",
10
+ "description": "Authentication for LuckyStack: credentials + OAuth (Google/GitHub/Facebook/Discord/Microsoft/custom), Redis-backed sessions, single-session enforcement, password reset, 2FA, lifecycle hooks, and pluggable user/session adapters. Pairs with @luckystack/core.",
11
11
  "license": "MIT",
12
12
  "author": "Mathijs van Melick <mathijsvanmelick3@gmail.com>",
13
13
  "repository": {
@@ -59,7 +59,7 @@
59
59
  "test": "vitest run"
60
60
  },
61
61
  "dependencies": {
62
- "@luckystack/core": "^0.8.3",
62
+ "@luckystack/core": "^0.8.4",
63
63
  "bcryptjs": "^3.0.0",
64
64
  "dotenv": "^17.0.0",
65
65
  "validator": "^13.15.35"
@@ -67,7 +67,7 @@
67
67
  "peerDependencies": {
68
68
  "@prisma/client": "^6.19.0",
69
69
  "socket.io": "^4.8.0",
70
- "@luckystack/email": "^0.8.3"
70
+ "@luckystack/email": "^0.8.4"
71
71
  },
72
72
  "peerDependenciesMeta": {
73
73
  "@prisma/client": {