@everystack/auth 0.4.8 → 0.4.9

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
@@ -4,6 +4,22 @@ All notable changes to `@everystack/auth` are documented here.
4
4
 
5
5
  Format based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.4.9] - 2026-10-14
8
+
9
+ ### Fixed
10
+
11
+ - break the model and auth import cycles; gate new ones
12
+
13
+ ### Changed
14
+
15
+ - share refresh token extraction
16
+
17
+ ## [Unreleased]
18
+
19
+ ### Changed
20
+
21
+ - share refresh token extraction
22
+
7
23
  ## [0.4.8] - 2026-09-26
8
24
 
9
25
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/auth",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "JWT authentication handlers, password hashing, and React auth context",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -108,7 +108,7 @@
108
108
  },
109
109
  "dependencies": {
110
110
  "bcryptjs": "3.0.3",
111
- "@everystack/model": "0.4.20"
111
+ "@everystack/model": "0.4.21"
112
112
  },
113
113
  "peerDependencies": {
114
114
  "@everystack/api": ">=0.1.0",
@@ -137,8 +137,8 @@
137
137
  "postgres": "3.4.9",
138
138
  "ts-jest": "29.4.9",
139
139
  "typescript": "5.9.3",
140
- "@everystack/server": "0.4.33",
141
- "@everystack/api": "0.4.2"
140
+ "@everystack/server": "0.4.35",
141
+ "@everystack/api": "0.4.3"
142
142
  },
143
143
  "scripts": {
144
144
  "test": "jest",
package/src/account.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Account-state and password-column helpers shared by index.ts and postgres.ts.
3
+ * A leaf module: index.ts imports postgres.ts, so these cannot live in index.ts.
4
+ */
5
+
6
+ /**
7
+ * Why a user row may not hold a session, or null when it may. Reads row fields, not table
8
+ * columns, so an app schema without `bannedUntil`/`deletedAt` simply never trips it.
9
+ * Mirrors the SQL side: auth.sign_in filters deleted_at, auth.validate_refresh_token
10
+ * rejects deleted_at (P0004) and an unexpired banned_until (P0007).
11
+ */
12
+ export function accountBlock(user: any): 'deleted' | 'banned' | null {
13
+ if (user.deletedAt) return 'deleted';
14
+ if (user.bannedUntil && new Date(user.bannedUntil).getTime() > Date.now()) return 'banned';
15
+ return null;
16
+ }
17
+
18
+ /** The refusal for a sign-in path that already proved identity (OAuth), or null to proceed.
19
+ * A missing row is treated as a deleted account. */
20
+ export function accountBlockedResponse(user: any): Response | null {
21
+ const block = user ? accountBlock(user) : 'deleted';
22
+ if (block === 'banned') return Response.json({ message: 'Account suspended' }, { status: 403 });
23
+ if (block === 'deleted') return Response.json({ message: 'Account disabled' }, { status: 401 });
24
+ return null;
25
+ }
26
+
27
+ /** Signout must always clear cookies. A failed server-side revoke is logged, never thrown:
28
+ * only the error's code or message is logged, never the token. */
29
+ export function warnRevokeFailed(err: unknown): void {
30
+ const e = err as { code?: string; message?: string } | undefined;
31
+ // eslint-disable-next-line no-console
32
+ console.warn('[everystack/auth] signout: refresh token revocation failed:', e?.code ?? e?.message ?? 'unknown error');
33
+ }
34
+
35
+ /**
36
+ * Build insert values with the correct password column name.
37
+ * Checks which column exists on the Drizzle table definition.
38
+ */
39
+ export function withPassword(users: any, values: Record<string, any>, hash: string | null): Record<string, any> {
40
+ if ('encryptedPassword' in users) {
41
+ return { ...values, encryptedPassword: hash };
42
+ }
43
+ return { ...values, passwordHash: hash };
44
+ }
package/src/index.ts CHANGED
@@ -38,7 +38,9 @@ import type { OAuthProvider, OAuthUserInfo } from './oauth';
38
38
  export type { OAuthProvider, OAuthUserInfo } from './oauth';
39
39
  export { createAppleProvider, createGoogleProvider } from './oauth';
40
40
  import { createPostgresAuthHandlers } from './postgres';
41
- import { getRefreshFromCookies, readPresentedRefreshToken } from './cookies';
41
+ import { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './account';
42
+ export { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './account';
43
+ import { readPresentedRefreshToken } from './cookies';
42
44
  import { authResponse, signoutResponse } from './response';
43
45
  import type { CookieConfig } from './response';
44
46
  import type { EnrichClaimsFn } from './plugin';
@@ -63,45 +65,6 @@ export function getPasswordHash(user: any): string | null {
63
65
  return user.encryptedPassword ?? user.passwordHash ?? null;
64
66
  }
65
67
 
66
- /**
67
- * Build insert values with the correct password column name.
68
- * Checks which column exists on the Drizzle table definition.
69
- */
70
- /**
71
- * Why a user row may not hold a session, or null when it may. Reads row fields, not table
72
- * columns, so an app schema without `bannedUntil`/`deletedAt` simply never trips it.
73
- * Mirrors the SQL side: auth.sign_in filters deleted_at, auth.validate_refresh_token
74
- * rejects deleted_at (P0004) and an unexpired banned_until (P0007).
75
- */
76
- export function accountBlock(user: any): 'deleted' | 'banned' | null {
77
- if (user.deletedAt) return 'deleted';
78
- if (user.bannedUntil && new Date(user.bannedUntil).getTime() > Date.now()) return 'banned';
79
- return null;
80
- }
81
-
82
- /** The refusal for a sign-in path that already proved identity (OAuth), or null to proceed.
83
- * A missing row is treated as a deleted account. */
84
- export function accountBlockedResponse(user: any): Response | null {
85
- const block = user ? accountBlock(user) : 'deleted';
86
- if (block === 'banned') return Response.json({ message: 'Account suspended' }, { status: 403 });
87
- if (block === 'deleted') return Response.json({ message: 'Account disabled' }, { status: 401 });
88
- return null;
89
- }
90
-
91
- /** Signout must always clear cookies. A failed server-side revoke is logged, never thrown:
92
- * only the error's code or message is logged, never the token. */
93
- export function warnRevokeFailed(err: unknown): void {
94
- const e = err as { code?: string; message?: string } | undefined;
95
- // eslint-disable-next-line no-console
96
- console.warn('[everystack/auth] signout: refresh token revocation failed:', e?.code ?? e?.message ?? 'unknown error');
97
- }
98
-
99
- export function withPassword(users: any, values: Record<string, any>, hash: string | null): Record<string, any> {
100
- if ('encryptedPassword' in users) {
101
- return { ...values, encryptedPassword: hash };
102
- }
103
- return { ...values, passwordHash: hash };
104
- }
105
68
 
106
69
  export interface TokenPayload {
107
70
  sub: string;
@@ -656,17 +619,7 @@ export function createAuthHandlers(
656
619
  const csrfError = csrfCheck(request, options?.cookies);
657
620
  if (csrfError) return csrfError;
658
621
 
659
- // Read refresh token from JSON body (mobile) or cookie (web SSR)
660
- let refreshToken: string | undefined;
661
- try {
662
- const body = await request.clone().json();
663
- refreshToken = body?.refreshToken;
664
- } catch {
665
- // Empty body is fine — fall through to cookie
666
- }
667
- if (!refreshToken) {
668
- refreshToken = getRefreshFromCookies(request.headers.get('cookie')) ?? undefined;
669
- }
622
+ const refreshToken = await readPresentedRefreshToken(request);
670
623
  if (!refreshToken) {
671
624
  return Response.json(
672
625
  { message: 'refreshToken is required' },
package/src/postgres.ts CHANGED
@@ -18,9 +18,9 @@ import { createJwt } from './jwt';
18
18
  import { hmacNode } from './jwt/hmac-node';
19
19
  import type { RefreshTokenOptions } from './refresh';
20
20
  import type { AuthHandlers, AuthOptions, TokenPayload } from './index';
21
- import { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './index';
21
+ import { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './account';
22
22
  import type { OAuthProvider } from './oauth';
23
- import { getRefreshFromCookies, readPresentedRefreshToken } from './cookies';
23
+ import { readPresentedRefreshToken } from './cookies';
24
24
  import { authResponse, signoutResponse } from './response';
25
25
  import type { CookieConfig } from './response';
26
26
  import { validateCsrfOrigin } from './csrf';
@@ -436,17 +436,7 @@ export function createPostgresAuthHandlers(
436
436
  const csrfError = csrfCheck(request, options?.cookies);
437
437
  if (csrfError) return csrfError;
438
438
 
439
- // Read refresh token from JSON body (mobile) or cookie (web SSR)
440
- let refreshToken: string | undefined;
441
- try {
442
- const body = await request.clone().json();
443
- refreshToken = body?.refreshToken;
444
- } catch {
445
- // Empty body is fine — fall through to cookie
446
- }
447
- if (!refreshToken) {
448
- refreshToken = getRefreshFromCookies(request.headers.get('cookie')) ?? undefined;
449
- }
439
+ const refreshToken = await readPresentedRefreshToken(request);
450
440
  if (!refreshToken) {
451
441
  return Response.json(
452
442
  { message: 'refreshToken is required' },