@erikey/react 0.4.26 → 0.4.27
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 +2 -1
- package/src/__tests__/auth-client.test.ts +105 -0
- package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
- package/src/auth-client.ts +158 -0
- package/src/dashboard-client.ts +60 -0
- package/src/index.ts +88 -0
- package/src/kv-client.ts +316 -0
- package/src/lib/cross-origin-auth.ts +99 -0
- package/src/stubs/captcha.ts +24 -0
- package/src/stubs/hashes.ts +16 -0
- package/src/stubs/index.ts +17 -0
- package/src/stubs/passkey.ts +12 -0
- package/src/stubs/qr-code.ts +10 -0
- package/src/stubs/query.ts +16 -0
- package/src/stubs/realtime.ts +17 -0
- package/src/stubs/use-sync-external-store.ts +12 -0
- package/src/styles.css +141 -0
- package/src/types.ts +14 -0
- package/src/ui/components/auth/auth-callback.tsx +36 -0
- package/src/ui/components/auth/auth-form.tsx +310 -0
- package/src/ui/components/auth/auth-view.tsx +435 -0
- package/src/ui/components/auth/email-otp-button.tsx +53 -0
- package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
- package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
- package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
- package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
- package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
- package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
- package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
- package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
- package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
- package/src/ui/components/auth/magic-link-button.tsx +54 -0
- package/src/ui/components/auth/one-tap.tsx +53 -0
- package/src/ui/components/auth/otp-input-group.tsx +65 -0
- package/src/ui/components/auth/passkey-button.tsx +91 -0
- package/src/ui/components/auth/provider-button.tsx +155 -0
- package/src/ui/components/auth/sign-out.tsx +25 -0
- package/src/ui/components/auth/wallet-button.tsx +192 -0
- package/src/ui/components/auth-loading.tsx +21 -0
- package/src/ui/components/captcha/captcha.tsx +91 -0
- package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
- package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
- package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
- package/src/ui/components/email/email-template.tsx +216 -0
- package/src/ui/components/form-error.tsx +27 -0
- package/src/ui/components/password-input.tsx +56 -0
- package/src/ui/components/provider-icons.tsx +404 -0
- package/src/ui/components/redirect-to-sign-in.tsx +16 -0
- package/src/ui/components/redirect-to-sign-up.tsx +16 -0
- package/src/ui/components/signed-in.tsx +20 -0
- package/src/ui/components/signed-out.tsx +20 -0
- package/src/ui/components/ui/alert.tsx +66 -0
- package/src/ui/components/ui/button.tsx +70 -0
- package/src/ui/components/ui/card.tsx +92 -0
- package/src/ui/components/ui/checkbox.tsx +66 -0
- package/src/ui/components/ui/field.tsx +248 -0
- package/src/ui/components/ui/form.tsx +165 -0
- package/src/ui/components/ui/input-otp.tsx +77 -0
- package/src/ui/components/ui/input.tsx +21 -0
- package/src/ui/components/ui/label.tsx +23 -0
- package/src/ui/components/ui/separator.tsx +34 -0
- package/src/ui/components/ui/skeleton.tsx +13 -0
- package/src/ui/components/ui/textarea.tsx +18 -0
- package/src/ui/components/user-avatar.tsx +151 -0
- package/src/ui/hooks/use-auth-data.ts +193 -0
- package/src/ui/hooks/use-authenticate.ts +64 -0
- package/src/ui/hooks/use-captcha.tsx +151 -0
- package/src/ui/hooks/use-hydrated.ts +13 -0
- package/src/ui/hooks/use-lang.ts +32 -0
- package/src/ui/hooks/use-success-transition.ts +41 -0
- package/src/ui/hooks/use-theme.ts +39 -0
- package/src/ui/index.ts +46 -0
- package/src/ui/instantdb.ts +1 -0
- package/src/ui/lib/auth-data-cache.ts +90 -0
- package/src/ui/lib/auth-ui-provider.tsx +769 -0
- package/src/ui/lib/gravatar-utils.ts +58 -0
- package/src/ui/lib/image-utils.ts +55 -0
- package/src/ui/lib/instantdb/model-names.ts +24 -0
- package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
- package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
- package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
- package/src/ui/lib/instantdb/use-session.ts +55 -0
- package/src/ui/lib/social-providers.ts +150 -0
- package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
- package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
- package/src/ui/lib/triplit/model-names.ts +24 -0
- package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
- package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
- package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
- package/src/ui/lib/triplit/use-session.ts +42 -0
- package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
- package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
- package/src/ui/lib/utils.ts +119 -0
- package/src/ui/lib/view-paths.ts +61 -0
- package/src/ui/lib/wallet.ts +129 -0
- package/src/ui/localization/admin-error-codes.ts +20 -0
- package/src/ui/localization/anonymous-error-codes.ts +6 -0
- package/src/ui/localization/api-key-error-codes.ts +32 -0
- package/src/ui/localization/auth-localization.ts +865 -0
- package/src/ui/localization/base-error-codes.ts +27 -0
- package/src/ui/localization/captcha-error-codes.ts +17 -0
- package/src/ui/localization/email-otp-error-codes.ts +7 -0
- package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
- package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
- package/src/ui/localization/multi-session-error-codes.ts +3 -0
- package/src/ui/localization/organization-error-codes.ts +57 -0
- package/src/ui/localization/passkey-error-codes.ts +10 -0
- package/src/ui/localization/phone-number-error-codes.ts +10 -0
- package/src/ui/localization/stripe-localization.ts +12 -0
- package/src/ui/localization/team-error-codes.ts +12 -0
- package/src/ui/localization/two-factor-error-codes.ts +12 -0
- package/src/ui/localization/username-error-codes.ts +9 -0
- package/src/ui/server.ts +4 -0
- package/src/ui/style.css +146 -0
- package/src/ui/tanstack.ts +1 -0
- package/src/ui/triplit.ts +1 -0
- package/src/ui/types/account-options.ts +35 -0
- package/src/ui/types/additional-fields.ts +21 -0
- package/src/ui/types/any-auth-client.ts +6 -0
- package/src/ui/types/api-key.ts +9 -0
- package/src/ui/types/auth-client.ts +41 -0
- package/src/ui/types/auth-hooks.ts +81 -0
- package/src/ui/types/auth-mutators.ts +21 -0
- package/src/ui/types/avatar-options.ts +29 -0
- package/src/ui/types/captcha-options.ts +32 -0
- package/src/ui/types/captcha-provider.ts +7 -0
- package/src/ui/types/credentials-options.ts +38 -0
- package/src/ui/types/delete-user-options.ts +7 -0
- package/src/ui/types/email-verification-options.ts +7 -0
- package/src/ui/types/fetch-error.ts +6 -0
- package/src/ui/types/generic-oauth-options.ts +16 -0
- package/src/ui/types/gravatar-options.ts +21 -0
- package/src/ui/types/image.ts +7 -0
- package/src/ui/types/invitation.ts +10 -0
- package/src/ui/types/link.ts +7 -0
- package/src/ui/types/organization-options.ts +106 -0
- package/src/ui/types/password-validation.ts +16 -0
- package/src/ui/types/profile.ts +15 -0
- package/src/ui/types/refetch.ts +1 -0
- package/src/ui/types/render-toast.ts +9 -0
- package/src/ui/types/sign-up-options.ts +7 -0
- package/src/ui/types/social-options.ts +16 -0
- package/src/ui/types/team-options.ts +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erikey/react",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.27",
|
|
4
4
|
"description": "React SDK for Erikey - B2B authentication and user management. UI components based on better-auth-ui.",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
26
|
+
"src",
|
|
26
27
|
"README.md",
|
|
27
28
|
"LICENSE"
|
|
28
29
|
],
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for React SDK auth-client
|
|
3
|
+
*
|
|
4
|
+
* These tests validate:
|
|
5
|
+
* - Client creation with correct configuration
|
|
6
|
+
* - Cross-origin detection
|
|
7
|
+
* - Token storage hooks work correctly
|
|
8
|
+
*
|
|
9
|
+
* Note: We're now using better-auth's native API, so we don't test
|
|
10
|
+
* the internal API methods - those are tested by better-auth itself.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
13
|
+
import { createAuthClient } from '../auth-client';
|
|
14
|
+
import * as crossOriginAuth from '../lib/cross-origin-auth';
|
|
15
|
+
|
|
16
|
+
// Mock the cross-origin-auth module
|
|
17
|
+
vi.mock('../lib/cross-origin-auth', () => ({
|
|
18
|
+
shouldUseBearerAuth: vi.fn(),
|
|
19
|
+
storeToken: vi.fn(),
|
|
20
|
+
getStoredToken: vi.fn(),
|
|
21
|
+
clearToken: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('createAuthClient', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('configuration', () => {
|
|
34
|
+
it('should create a client with default baseUrl', () => {
|
|
35
|
+
vi.mocked(crossOriginAuth.shouldUseBearerAuth).mockReturnValue(false);
|
|
36
|
+
|
|
37
|
+
const client = createAuthClient({ projectId: 'pk_test_123' });
|
|
38
|
+
|
|
39
|
+
expect(client).toBeDefined();
|
|
40
|
+
expect(client.signIn).toBeDefined();
|
|
41
|
+
expect(client.signUp).toBeDefined();
|
|
42
|
+
expect(client.signOut).toBeDefined();
|
|
43
|
+
expect(client.useSession).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should create a client with custom baseUrl', () => {
|
|
47
|
+
vi.mocked(crossOriginAuth.shouldUseBearerAuth).mockReturnValue(false);
|
|
48
|
+
|
|
49
|
+
const client = createAuthClient({
|
|
50
|
+
projectId: 'pk_test_123',
|
|
51
|
+
baseUrl: 'http://localhost:7203',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(client).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should detect cross-origin context', () => {
|
|
58
|
+
vi.mocked(crossOriginAuth.shouldUseBearerAuth).mockReturnValue(true);
|
|
59
|
+
vi.mocked(crossOriginAuth.getStoredToken).mockReturnValue('test-token');
|
|
60
|
+
|
|
61
|
+
const client = createAuthClient({ projectId: 'pk_test_123' });
|
|
62
|
+
|
|
63
|
+
expect(crossOriginAuth.shouldUseBearerAuth).toHaveBeenCalledWith(
|
|
64
|
+
'https://auth.erikey.com'
|
|
65
|
+
);
|
|
66
|
+
expect(client).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('cross-origin token handling', () => {
|
|
71
|
+
it('should use bearer token in cross-origin context', () => {
|
|
72
|
+
vi.mocked(crossOriginAuth.shouldUseBearerAuth).mockReturnValue(true);
|
|
73
|
+
vi.mocked(crossOriginAuth.getStoredToken).mockReturnValue('stored-token');
|
|
74
|
+
|
|
75
|
+
// Just verify the client is created - actual token usage is tested via integration tests
|
|
76
|
+
const client = createAuthClient({ projectId: 'pk_test_123' });
|
|
77
|
+
|
|
78
|
+
expect(client).toBeDefined();
|
|
79
|
+
expect(crossOriginAuth.getStoredToken).not.toHaveBeenCalled(); // Called lazily
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('API surface', () => {
|
|
84
|
+
it('should expose better-auth client methods', () => {
|
|
85
|
+
vi.mocked(crossOriginAuth.shouldUseBearerAuth).mockReturnValue(false);
|
|
86
|
+
|
|
87
|
+
const client = createAuthClient({ projectId: 'pk_test_123' });
|
|
88
|
+
|
|
89
|
+
// Core auth methods
|
|
90
|
+
expect(client.signIn).toBeDefined();
|
|
91
|
+
expect(client.signIn.email).toBeDefined();
|
|
92
|
+
expect(client.signIn.social).toBeDefined();
|
|
93
|
+
expect(client.signUp).toBeDefined();
|
|
94
|
+
expect(client.signUp.email).toBeDefined();
|
|
95
|
+
expect(client.signOut).toBeDefined();
|
|
96
|
+
|
|
97
|
+
// Session hook
|
|
98
|
+
expect(client.useSession).toBeDefined();
|
|
99
|
+
expect(typeof client.useSession).toBe('function');
|
|
100
|
+
|
|
101
|
+
// Other methods
|
|
102
|
+
expect(client.getSession).toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security tests for localStorage token encryption
|
|
3
|
+
*
|
|
4
|
+
* NOTE: These tests require the encryption functions to be exported from auth-client.ts
|
|
5
|
+
* Add these exports to make tests pass:
|
|
6
|
+
*
|
|
7
|
+
* export { storeToken, getStoredToken, clearToken };
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
+
|
|
11
|
+
// Mock localStorage for Node environment
|
|
12
|
+
const localStorageMock = (() => {
|
|
13
|
+
let store: Record<string, string> = {};
|
|
14
|
+
return {
|
|
15
|
+
getItem: (key: string) => store[key] || null,
|
|
16
|
+
setItem: (key: string, value: string) => {
|
|
17
|
+
store[key] = value;
|
|
18
|
+
},
|
|
19
|
+
removeItem: (key: string) => {
|
|
20
|
+
delete store[key];
|
|
21
|
+
},
|
|
22
|
+
clear: () => {
|
|
23
|
+
store = {};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
global.localStorage = localStorageMock;
|
|
30
|
+
|
|
31
|
+
// These imports will fail until encryption functions are implemented and exported
|
|
32
|
+
// This is intentional - the tests guide the implementation (TDD approach)
|
|
33
|
+
let storeToken: any;
|
|
34
|
+
let getStoredToken: any;
|
|
35
|
+
let clearToken: any;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Try to import the functions (will fail initially)
|
|
39
|
+
const authClient = await import('../../auth-client');
|
|
40
|
+
storeToken = (authClient as any).storeToken;
|
|
41
|
+
getStoredToken = (authClient as any).getStoredToken;
|
|
42
|
+
clearToken = (authClient as any).clearToken;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn('⚠️ Encryption functions not yet exported from auth-client.ts');
|
|
45
|
+
console.warn(' Add: export { storeToken, getStoredToken, clearToken };');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('localStorage Token Encryption', () => {
|
|
49
|
+
const projectId = 'test-project-123';
|
|
50
|
+
const mockSession = {
|
|
51
|
+
id: 'session-123',
|
|
52
|
+
token: 'super-secret-token-12345',
|
|
53
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
localStorage.clear();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should encrypt tokens before storing', async () => {
|
|
61
|
+
if (!storeToken) {
|
|
62
|
+
console.warn('⚠️ Skipping test: storeToken not implemented');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await storeToken(projectId, mockSession);
|
|
67
|
+
|
|
68
|
+
const rawStored = localStorage.getItem(`erikey.session.${projectId}`);
|
|
69
|
+
expect(rawStored).not.toBeNull();
|
|
70
|
+
|
|
71
|
+
const stored = JSON.parse(rawStored!);
|
|
72
|
+
|
|
73
|
+
// Token should NOT be stored in plain text
|
|
74
|
+
expect(stored.token).not.toBe(mockSession.token);
|
|
75
|
+
expect(stored.token).toMatch(/^[A-Za-z0-9+/=]+$/); // Base64 format
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should decrypt tokens when retrieving', async () => {
|
|
79
|
+
if (!storeToken || !getStoredToken) {
|
|
80
|
+
console.warn('⚠️ Skipping test: encryption functions not implemented');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await storeToken(projectId, mockSession);
|
|
85
|
+
const retrieved = await getStoredToken(projectId);
|
|
86
|
+
|
|
87
|
+
// Should decrypt back to original token
|
|
88
|
+
expect(retrieved).toBe(mockSession.token);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return null for expired tokens', async () => {
|
|
92
|
+
if (!storeToken || !getStoredToken) {
|
|
93
|
+
console.warn('⚠️ Skipping test: encryption functions not implemented');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const expiredSession = {
|
|
98
|
+
...mockSession,
|
|
99
|
+
expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await storeToken(projectId, expiredSession);
|
|
103
|
+
const retrieved = await getStoredToken(projectId);
|
|
104
|
+
|
|
105
|
+
expect(retrieved).toBeNull();
|
|
106
|
+
// Should also clean up localStorage
|
|
107
|
+
expect(localStorage.getItem(`erikey.session.${projectId}`)).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle decryption errors gracefully', async () => {
|
|
111
|
+
if (!getStoredToken) {
|
|
112
|
+
console.warn('⚠️ Skipping test: getStoredToken not implemented');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Manually corrupt the stored data
|
|
117
|
+
localStorage.setItem(`erikey.session.${projectId}`, JSON.stringify({
|
|
118
|
+
token: 'corrupted-data!!!',
|
|
119
|
+
expiresAt: mockSession.expiresAt,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
const retrieved = await getStoredToken(projectId);
|
|
123
|
+
|
|
124
|
+
expect(retrieved).toBeNull();
|
|
125
|
+
// Should clean up corrupted data
|
|
126
|
+
expect(localStorage.getItem(`erikey.session.${projectId}`)).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should clear tokens completely', async () => {
|
|
130
|
+
if (!storeToken || !getStoredToken || !clearToken) {
|
|
131
|
+
console.warn('⚠️ Skipping test: encryption functions not implemented');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await storeToken(projectId, mockSession);
|
|
136
|
+
clearToken(projectId);
|
|
137
|
+
|
|
138
|
+
expect(localStorage.getItem(`erikey.session.${projectId}`)).toBeNull();
|
|
139
|
+
const retrieved = await getStoredToken(projectId);
|
|
140
|
+
expect(retrieved).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should use different encryption per project', async () => {
|
|
144
|
+
if (!storeToken) {
|
|
145
|
+
console.warn('⚠️ Skipping test: storeToken not implemented');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projectId1 = 'project-1';
|
|
150
|
+
const projectId2 = 'project-2';
|
|
151
|
+
|
|
152
|
+
await storeToken(projectId1, mockSession);
|
|
153
|
+
await storeToken(projectId2, mockSession);
|
|
154
|
+
|
|
155
|
+
const stored1 = localStorage.getItem(`erikey.session.${projectId1}`);
|
|
156
|
+
const stored2 = localStorage.getItem(`erikey.session.${projectId2}`);
|
|
157
|
+
|
|
158
|
+
// Encrypted values should be different (different encryption keys)
|
|
159
|
+
expect(stored1).not.toBe(stored2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle non-existent tokens', async () => {
|
|
163
|
+
if (!getStoredToken) {
|
|
164
|
+
console.warn('⚠️ Skipping test: getStoredToken not implemented');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const retrieved = await getStoredToken('non-existent-project');
|
|
169
|
+
expect(retrieved).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @erikey/react - Auth Client
|
|
3
|
+
*
|
|
4
|
+
* Wraps better-auth/react with Erikey-specific enhancements:
|
|
5
|
+
* - X-Project-Id header injection for multi-tenant routing
|
|
6
|
+
* - Bearer token auth for cross-origin iframes (Sandpack)
|
|
7
|
+
* - localStorage token storage when cookies are blocked
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - Uses better-auth's native reactivity (nanostores + useSyncExternalStore)
|
|
11
|
+
* - Hooks into fetchOptions.onSuccess to store tokens for cross-origin
|
|
12
|
+
* - No Proxy wrapper - uses better-auth's useSession directly
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createAuthClient as createBetterAuthClient } from 'better-auth/react';
|
|
16
|
+
import { siweClient } from 'better-auth/client/plugins';
|
|
17
|
+
import {
|
|
18
|
+
shouldUseBearerAuth,
|
|
19
|
+
storeToken,
|
|
20
|
+
getStoredToken,
|
|
21
|
+
clearToken,
|
|
22
|
+
type StoredSession,
|
|
23
|
+
} from './lib/cross-origin-auth';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Types
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
export interface AuthClientConfig {
|
|
30
|
+
/**
|
|
31
|
+
* Your Erikey project ID (pk_live_xxx or pk_test_xxx)
|
|
32
|
+
*/
|
|
33
|
+
projectId: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Base URL for the auth API
|
|
37
|
+
* @default 'https://auth.erikey.com'
|
|
38
|
+
*/
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Re-export better-auth types
|
|
43
|
+
export type { Session, User } from 'better-auth';
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Auth Client
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create an auth client for end-user authentication
|
|
51
|
+
*
|
|
52
|
+
* Uses better-auth/react with Erikey-specific configuration.
|
|
53
|
+
* Automatically detects cross-origin contexts (Sandpack, deployed sites)
|
|
54
|
+
* and uses Bearer tokens instead of cookies.
|
|
55
|
+
*
|
|
56
|
+
* @example Basic usage
|
|
57
|
+
* ```tsx
|
|
58
|
+
* import { createAuthClient } from '@erikey/react';
|
|
59
|
+
*
|
|
60
|
+
* const auth = createAuthClient({
|
|
61
|
+
* projectId: 'pk_live_xxx',
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // Email auth
|
|
65
|
+
* await auth.signIn.email({ email, password });
|
|
66
|
+
* await auth.signUp.email({ email, password, name });
|
|
67
|
+
*
|
|
68
|
+
* // Social OAuth
|
|
69
|
+
* await auth.signIn.social({ provider: 'google' });
|
|
70
|
+
*
|
|
71
|
+
* // Reactive session hook (React) - uses better-auth's nanostores
|
|
72
|
+
* const { data: session, isPending } = auth.useSession();
|
|
73
|
+
*
|
|
74
|
+
* // Sign out
|
|
75
|
+
* await auth.signOut();
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function createAuthClient(config: AuthClientConfig) {
|
|
79
|
+
const { projectId, baseUrl = 'https://auth.erikey.com' } = config;
|
|
80
|
+
|
|
81
|
+
// Detect if we're in a cross-origin context (Sandpack iframe, deployed site)
|
|
82
|
+
const useBearerAuth = shouldUseBearerAuth(baseUrl);
|
|
83
|
+
|
|
84
|
+
// Build fetch options with Erikey-specific configuration
|
|
85
|
+
const fetchOptions: NonNullable<Parameters<typeof createBetterAuthClient>[0]>['fetchOptions'] = {
|
|
86
|
+
// Use onRequest hook to inject headers into EVERY request
|
|
87
|
+
// (better-auth doesn't pass fetchOptions.headers through correctly)
|
|
88
|
+
onRequest: (context) => {
|
|
89
|
+
// Always inject project ID for multi-tenant routing
|
|
90
|
+
context.headers.set('X-Project-Id', projectId);
|
|
91
|
+
|
|
92
|
+
// For cross-origin contexts, inject Bearer token
|
|
93
|
+
if (useBearerAuth) {
|
|
94
|
+
const token = getStoredToken(projectId);
|
|
95
|
+
if (token) {
|
|
96
|
+
context.headers.set('Authorization', `Bearer ${token}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return context;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Hook into successful responses to handle token storage
|
|
104
|
+
onSuccess: async (context) => {
|
|
105
|
+
if (!useBearerAuth) return;
|
|
106
|
+
|
|
107
|
+
// Get the request path from the URL
|
|
108
|
+
const url = context.response?.url || '';
|
|
109
|
+
const path = new URL(url, baseUrl).pathname;
|
|
110
|
+
|
|
111
|
+
// Store token after successful sign-in or sign-up
|
|
112
|
+
// The server returns { token, session, user } for auth endpoints
|
|
113
|
+
const token = (context.data as any)?.token;
|
|
114
|
+
if (token && (path.includes('/sign-in') || path.includes('/sign-up'))) {
|
|
115
|
+
const session: StoredSession = {
|
|
116
|
+
id: (context.data as any)?.session?.id || 'session',
|
|
117
|
+
token,
|
|
118
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
119
|
+
};
|
|
120
|
+
storeToken(projectId, session);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Clear token after sign-out
|
|
124
|
+
if (path.includes('/sign-out')) {
|
|
125
|
+
clearToken(projectId);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Handle errors - clear token on auth errors
|
|
130
|
+
onError: async (context) => {
|
|
131
|
+
if (!useBearerAuth) return;
|
|
132
|
+
|
|
133
|
+
// If we get a 401, token is invalid - clear it
|
|
134
|
+
if (context.response?.status === 401) {
|
|
135
|
+
clearToken(projectId);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Create and return the better-auth client
|
|
141
|
+
// No Proxy needed - better-auth's useSession uses nanostores which are reactive
|
|
142
|
+
// The atomListeners system will trigger session refetch after sign-in/sign-up/sign-out
|
|
143
|
+
const client = createBetterAuthClient({
|
|
144
|
+
baseURL: baseUrl,
|
|
145
|
+
fetchOptions,
|
|
146
|
+
// For same-origin, include cookies (cross-origin uses Bearer from fetchOptions.auth)
|
|
147
|
+
...(!useBearerAuth && { credentials: 'include' as const }),
|
|
148
|
+
// Enable SIWE (Sign-In with Ethereum) support
|
|
149
|
+
plugins: [siweClient()],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return client;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Type helper for inferring the auth client type
|
|
157
|
+
*/
|
|
158
|
+
export type AuthClient = ReturnType<typeof createAuthClient>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @erikey/react - Dashboard Client
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around better-auth/react for the Erikey dashboard.
|
|
5
|
+
*/
|
|
6
|
+
import { createAuthClient as createBetterAuthClient } from 'better-auth/react';
|
|
7
|
+
import { siweClient } from 'better-auth/client/plugins';
|
|
8
|
+
|
|
9
|
+
export interface DashboardClientConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Base URL for the auth API
|
|
12
|
+
* @default process.env.NEXT_PUBLIC_AUTH_URL || 'https://api.erikey.com'
|
|
13
|
+
*/
|
|
14
|
+
baseURL?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether to include credentials in requests
|
|
18
|
+
* @default 'include'
|
|
19
|
+
*/
|
|
20
|
+
credentials?: RequestCredentials;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create dashboard client with better-auth
|
|
25
|
+
*
|
|
26
|
+
* This is for the Erikey dashboard (erikey.com) - NOT for end-user auth.
|
|
27
|
+
* For end-user auth in customer apps, use `createAuthClient` instead.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: For dashboard user signup (which creates org + project), use the
|
|
30
|
+
* typed API client (signupApi.dashboard) from @/lib/api/typed-api instead.
|
|
31
|
+
* This client only handles Better Auth operations (signIn, session, etc.)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { createDashboardClient } from '@erikey/react';
|
|
36
|
+
*
|
|
37
|
+
* export const auth = createDashboardClient({
|
|
38
|
+
* baseURL: process.env.NEXT_PUBLIC_AUTH_URL
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Use Better Auth methods
|
|
42
|
+
* export const { useSession, signIn, signOut } = auth;
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function createDashboardClient(config?: DashboardClientConfig) {
|
|
46
|
+
const baseURL = config?.baseURL || 'https://api.erikey.com';
|
|
47
|
+
|
|
48
|
+
return createBetterAuthClient({
|
|
49
|
+
baseURL,
|
|
50
|
+
credentials: config?.credentials || 'include',
|
|
51
|
+
plugins: [
|
|
52
|
+
siweClient(), // Enable Sign-In with Ethereum
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Type helper for inferring the dashboard client type
|
|
59
|
+
*/
|
|
60
|
+
export type DashboardClient = ReturnType<typeof createDashboardClient>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @erikey/react - React SDK for Erikey
|
|
3
|
+
*
|
|
4
|
+
* Two auth clients for different use cases:
|
|
5
|
+
*
|
|
6
|
+
* 1. `createDashboardClient` - For the Erikey dashboard (Better Auth)
|
|
7
|
+
* 2. `createAuthClient` - For customer apps to authenticate end-users
|
|
8
|
+
*
|
|
9
|
+
* @example Dashboard (erikey.com)
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { createDashboardClient } from '@erikey/react';
|
|
12
|
+
*
|
|
13
|
+
* export const auth = createDashboardClient({
|
|
14
|
+
* baseUrl: process.env.NEXT_PUBLIC_AUTH_URL
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* export const { useSession, signIn, signOut, organization } = auth;
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example Customer App (end-user auth)
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { createAuthClient } from '@erikey/react';
|
|
23
|
+
*
|
|
24
|
+
* const auth = createAuthClient({
|
|
25
|
+
* projectId: process.env.NEXT_PUBLIC_ERIKEY_PROJECT_ID!, // pk_live_xxx
|
|
26
|
+
* baseUrl: process.env.NEXT_PUBLIC_ERIKEY_BASE_URL, // Optional
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Email auth
|
|
30
|
+
* await auth.signIn.email({ email, password });
|
|
31
|
+
* await auth.signUp.email({ email, password, name });
|
|
32
|
+
*
|
|
33
|
+
* // Social OAuth
|
|
34
|
+
* await auth.signIn.social({ provider: 'google' });
|
|
35
|
+
*
|
|
36
|
+
* // Reactive session hook
|
|
37
|
+
* const { data: session, isPending } = auth.useSession();
|
|
38
|
+
*
|
|
39
|
+
* // Sign out
|
|
40
|
+
* await auth.signOut();
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example KV Store (per-user storage)
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import { createKvClient } from '@erikey/react';
|
|
46
|
+
*
|
|
47
|
+
* const kv = createKvClient({
|
|
48
|
+
* projectId: process.env.NEXT_PUBLIC_ERIKEY_PROJECT_ID!,
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* await kv.setValue('preferences', { theme: 'dark' });
|
|
52
|
+
* const { data } = await kv.getValue('preferences');
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
// Dashboard auth (for Erikey dashboard - uses Better Auth)
|
|
57
|
+
export { createDashboardClient } from './dashboard-client';
|
|
58
|
+
export type {
|
|
59
|
+
DashboardClient,
|
|
60
|
+
DashboardClientConfig,
|
|
61
|
+
} from './dashboard-client';
|
|
62
|
+
|
|
63
|
+
// End-user auth (for customer apps - wraps Better Auth with Erikey enhancements)
|
|
64
|
+
export { createAuthClient } from './auth-client';
|
|
65
|
+
export type {
|
|
66
|
+
AuthClient,
|
|
67
|
+
AuthClientConfig,
|
|
68
|
+
Session,
|
|
69
|
+
User,
|
|
70
|
+
} from './auth-client';
|
|
71
|
+
|
|
72
|
+
// KV Store (Erikey-specific per-user storage)
|
|
73
|
+
export { createKvClient } from './kv-client';
|
|
74
|
+
export type {
|
|
75
|
+
KvClient,
|
|
76
|
+
KvClientConfig,
|
|
77
|
+
KvPair,
|
|
78
|
+
KvBulkSetInput,
|
|
79
|
+
SetValueResponse,
|
|
80
|
+
GetValueResponse,
|
|
81
|
+
GetValuesResponse,
|
|
82
|
+
DeleteValueResponse,
|
|
83
|
+
DeleteValuesResponse,
|
|
84
|
+
SetValuesResponse,
|
|
85
|
+
} from './kv-client';
|
|
86
|
+
|
|
87
|
+
// Export shared types from core (for dashboard client compatibility)
|
|
88
|
+
export type { SessionData, Organization, APIKey, Permission } from './types';
|