@svadmin/supabase 0.11.0 → 0.11.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/supabase",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Supabase adapters — DataProvider, AuthProvider, LiveProvider, AuditHandler",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,10 +20,10 @@
20
20
  }
21
21
  },
22
22
  "peerDependencies": {
23
- "@svadmin/core": "^0.25.0",
23
+ "@svadmin/core": "^0.25.2",
24
24
  "@supabase/supabase-js": "^2.0.0",
25
25
  "@refinedev/supabase": "^5.0.0",
26
- "@supacloud/js": "^0.10.0"
26
+ "@supacloud/js": "^0.12.0"
27
27
  },
28
28
  "license": "MIT",
29
29
  "author": "zuohuadong",
@@ -3,6 +3,26 @@ import type { SupabaseClient } from '@supabase/supabase-js';
3
3
  import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
4
4
  import { audit } from '@svadmin/core';
5
5
 
6
+ const INVALID_REFRESH_TOKEN_MESSAGES = [
7
+ 'refresh token is not valid',
8
+ 'invalid refresh token',
9
+ 'refresh_token_not_found',
10
+ 'jwt expired',
11
+ ];
12
+
13
+ function isInvalidRefreshTokenError(error: unknown): error is Error {
14
+ if (!(error instanceof Error)) {
15
+ return false;
16
+ }
17
+
18
+ const message = error.message.toLowerCase();
19
+ return INVALID_REFRESH_TOKEN_MESSAGES.some((fragment) => message.includes(fragment));
20
+ }
21
+
22
+ async function clearInvalidSession(client: SupabaseClient): Promise<void> {
23
+ await client.auth.signOut({ scope: 'local' });
24
+ }
25
+
6
26
  export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider {
7
27
  return {
8
28
  async login({ email, password }: Record<string, unknown>): Promise<AuthActionResult> {
@@ -23,16 +43,42 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
23
43
  },
24
44
 
25
45
  async check(): Promise<CheckResult> {
26
- const { data: { session } } = await client.auth.getSession();
46
+ const { data: { session }, error } = await client.auth.getSession();
47
+ if (error) {
48
+ if (isInvalidRefreshTokenError(error)) {
49
+ await clearInvalidSession(client);
50
+ return {
51
+ authenticated: false,
52
+ redirectTo: '/login',
53
+ logout: true,
54
+ error: { message: 'Session expired, please sign in again.' },
55
+ };
56
+ }
57
+
58
+ return {
59
+ authenticated: false,
60
+ redirectTo: '/login',
61
+ logout: true,
62
+ error: { message: 'Session validation failed.' },
63
+ };
64
+ }
27
65
  if (session) return { authenticated: true };
28
66
  return { authenticated: false, redirectTo: '/login', logout: true };
29
67
  },
30
68
 
31
69
  async getIdentity(): Promise<Identity | null> {
32
- const { data: { user } } = await client.auth.getUser();
70
+ const { data: { user }, error } = await client.auth.getUser();
71
+ if (error && isInvalidRefreshTokenError(error)) {
72
+ await clearInvalidSession(client);
73
+ return null;
74
+ }
33
75
  if (!user) return null;
34
76
 
35
- const { data: { session } } = await client.auth.getSession();
77
+ const { data: { session }, error: sessionError } = await client.auth.getSession();
78
+ if (sessionError && isInvalidRefreshTokenError(sessionError)) {
79
+ await clearInvalidSession(client);
80
+ return null;
81
+ }
36
82
 
37
83
  return {
38
84
  id: user.id,
@@ -44,7 +90,11 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
44
90
  },
45
91
 
46
92
  async getPermissions(): Promise<unknown> {
47
- const { data: { user } } = await client.auth.getUser();
93
+ const { data: { user }, error } = await client.auth.getUser();
94
+ if (error && isInvalidRefreshTokenError(error)) {
95
+ await clearInvalidSession(client);
96
+ return null;
97
+ }
48
98
  return user?.user_metadata?.role ?? 'user';
49
99
  },
50
100
 
@@ -71,6 +121,11 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
71
121
  },
72
122
 
73
123
  async onError(error: unknown): Promise<{ redirectTo?: string; logout?: boolean }> {
124
+ if (isInvalidRefreshTokenError(error)) {
125
+ await clearInvalidSession(client);
126
+ return { redirectTo: '/login', logout: true };
127
+ }
128
+
74
129
  if (error instanceof Error && error.message.includes('401')) {
75
130
  // Pre-flight check: race-condition guard
76
131
  // Supabase might have just refreshed the token in the background right after our fetch failed
@@ -135,6 +135,24 @@ describe('Supabase AuthProvider', () => {
135
135
  expect(result.logout).toBe(true);
136
136
  });
137
137
 
138
+ test('check clears invalid refresh token sessions', async () => {
139
+ const { createSupabaseAuthProvider } = await import('./auth-provider');
140
+ const client = createMockSupabaseClient({
141
+ auth: {
142
+ getSession: mock(async () => ({
143
+ data: { session: null },
144
+ error: new Error('Refresh token is not valid'),
145
+ })),
146
+ }
147
+ });
148
+ const auth = createSupabaseAuthProvider(client);
149
+ const result = await auth.check();
150
+ expect(result.authenticated).toBe(false);
151
+ expect(result.logout).toBe(true);
152
+ expect(result.redirectTo).toBe('/login');
153
+ expect(client.auth.signOut).toHaveBeenCalled();
154
+ });
155
+
138
156
  test('getIdentity surfaces user metadata', async () => {
139
157
  const { createSupabaseAuthProvider } = await import('./auth-provider');
140
158
  const auth = createSupabaseAuthProvider(createMockSupabaseClient());
@@ -152,6 +170,22 @@ describe('Supabase AuthProvider', () => {
152
170
  expect(role).toBe('admin');
153
171
  });
154
172
 
173
+ test('getIdentity clears invalid refresh token sessions', async () => {
174
+ const { createSupabaseAuthProvider } = await import('./auth-provider');
175
+ const client = createMockSupabaseClient({
176
+ auth: {
177
+ getUser: mock(async () => ({
178
+ data: { user: null },
179
+ error: new Error('Invalid refresh token'),
180
+ })),
181
+ }
182
+ });
183
+ const auth = createSupabaseAuthProvider(client);
184
+ const identity = await auth.getIdentity();
185
+ expect(identity).toBeNull();
186
+ expect(client.auth.signOut).toHaveBeenCalled();
187
+ });
188
+
155
189
  test('onError returns logout true on 401 with no session', async () => {
156
190
  const { createSupabaseAuthProvider } = await import('./auth-provider');
157
191
  const client = createMockSupabaseClient({
@@ -172,6 +206,16 @@ describe('Supabase AuthProvider', () => {
172
206
  expect(result.logout).toBeUndefined();
173
207
  expect(client.auth.signOut).not.toHaveBeenCalled();
174
208
  });
209
+
210
+ test('onError logs out on invalid refresh token', async () => {
211
+ const { createSupabaseAuthProvider } = await import('./auth-provider');
212
+ const client = createMockSupabaseClient();
213
+ const auth = createSupabaseAuthProvider(client);
214
+ const result = await auth.onError!(new Error('Refresh token is not valid'));
215
+ expect(result.logout).toBe(true);
216
+ expect(result.redirectTo).toBe('/login');
217
+ expect(client.auth.signOut).toHaveBeenCalled();
218
+ });
175
219
  });
176
220
 
177
221