@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.
Files changed (143) hide show
  1. package/package.json +2 -1
  2. package/src/__tests__/auth-client.test.ts +105 -0
  3. package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
  4. package/src/auth-client.ts +158 -0
  5. package/src/dashboard-client.ts +60 -0
  6. package/src/index.ts +88 -0
  7. package/src/kv-client.ts +316 -0
  8. package/src/lib/cross-origin-auth.ts +99 -0
  9. package/src/stubs/captcha.ts +24 -0
  10. package/src/stubs/hashes.ts +16 -0
  11. package/src/stubs/index.ts +17 -0
  12. package/src/stubs/passkey.ts +12 -0
  13. package/src/stubs/qr-code.ts +10 -0
  14. package/src/stubs/query.ts +16 -0
  15. package/src/stubs/realtime.ts +17 -0
  16. package/src/stubs/use-sync-external-store.ts +12 -0
  17. package/src/styles.css +141 -0
  18. package/src/types.ts +14 -0
  19. package/src/ui/components/auth/auth-callback.tsx +36 -0
  20. package/src/ui/components/auth/auth-form.tsx +310 -0
  21. package/src/ui/components/auth/auth-view.tsx +435 -0
  22. package/src/ui/components/auth/email-otp-button.tsx +53 -0
  23. package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
  24. package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
  25. package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
  26. package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
  27. package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
  28. package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
  29. package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
  30. package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
  31. package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
  32. package/src/ui/components/auth/magic-link-button.tsx +54 -0
  33. package/src/ui/components/auth/one-tap.tsx +53 -0
  34. package/src/ui/components/auth/otp-input-group.tsx +65 -0
  35. package/src/ui/components/auth/passkey-button.tsx +91 -0
  36. package/src/ui/components/auth/provider-button.tsx +155 -0
  37. package/src/ui/components/auth/sign-out.tsx +25 -0
  38. package/src/ui/components/auth/wallet-button.tsx +192 -0
  39. package/src/ui/components/auth-loading.tsx +21 -0
  40. package/src/ui/components/captcha/captcha.tsx +91 -0
  41. package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
  42. package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
  43. package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
  44. package/src/ui/components/email/email-template.tsx +216 -0
  45. package/src/ui/components/form-error.tsx +27 -0
  46. package/src/ui/components/password-input.tsx +56 -0
  47. package/src/ui/components/provider-icons.tsx +404 -0
  48. package/src/ui/components/redirect-to-sign-in.tsx +16 -0
  49. package/src/ui/components/redirect-to-sign-up.tsx +16 -0
  50. package/src/ui/components/signed-in.tsx +20 -0
  51. package/src/ui/components/signed-out.tsx +20 -0
  52. package/src/ui/components/ui/alert.tsx +66 -0
  53. package/src/ui/components/ui/button.tsx +70 -0
  54. package/src/ui/components/ui/card.tsx +92 -0
  55. package/src/ui/components/ui/checkbox.tsx +66 -0
  56. package/src/ui/components/ui/field.tsx +248 -0
  57. package/src/ui/components/ui/form.tsx +165 -0
  58. package/src/ui/components/ui/input-otp.tsx +77 -0
  59. package/src/ui/components/ui/input.tsx +21 -0
  60. package/src/ui/components/ui/label.tsx +23 -0
  61. package/src/ui/components/ui/separator.tsx +34 -0
  62. package/src/ui/components/ui/skeleton.tsx +13 -0
  63. package/src/ui/components/ui/textarea.tsx +18 -0
  64. package/src/ui/components/user-avatar.tsx +151 -0
  65. package/src/ui/hooks/use-auth-data.ts +193 -0
  66. package/src/ui/hooks/use-authenticate.ts +64 -0
  67. package/src/ui/hooks/use-captcha.tsx +151 -0
  68. package/src/ui/hooks/use-hydrated.ts +13 -0
  69. package/src/ui/hooks/use-lang.ts +32 -0
  70. package/src/ui/hooks/use-success-transition.ts +41 -0
  71. package/src/ui/hooks/use-theme.ts +39 -0
  72. package/src/ui/index.ts +46 -0
  73. package/src/ui/instantdb.ts +1 -0
  74. package/src/ui/lib/auth-data-cache.ts +90 -0
  75. package/src/ui/lib/auth-ui-provider.tsx +769 -0
  76. package/src/ui/lib/gravatar-utils.ts +58 -0
  77. package/src/ui/lib/image-utils.ts +55 -0
  78. package/src/ui/lib/instantdb/model-names.ts +24 -0
  79. package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
  80. package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
  81. package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
  82. package/src/ui/lib/instantdb/use-session.ts +55 -0
  83. package/src/ui/lib/social-providers.ts +150 -0
  84. package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  85. package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
  86. package/src/ui/lib/triplit/model-names.ts +24 -0
  87. package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
  88. package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
  89. package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
  90. package/src/ui/lib/triplit/use-session.ts +42 -0
  91. package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
  92. package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
  93. package/src/ui/lib/utils.ts +119 -0
  94. package/src/ui/lib/view-paths.ts +61 -0
  95. package/src/ui/lib/wallet.ts +129 -0
  96. package/src/ui/localization/admin-error-codes.ts +20 -0
  97. package/src/ui/localization/anonymous-error-codes.ts +6 -0
  98. package/src/ui/localization/api-key-error-codes.ts +32 -0
  99. package/src/ui/localization/auth-localization.ts +865 -0
  100. package/src/ui/localization/base-error-codes.ts +27 -0
  101. package/src/ui/localization/captcha-error-codes.ts +17 -0
  102. package/src/ui/localization/email-otp-error-codes.ts +7 -0
  103. package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
  104. package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
  105. package/src/ui/localization/multi-session-error-codes.ts +3 -0
  106. package/src/ui/localization/organization-error-codes.ts +57 -0
  107. package/src/ui/localization/passkey-error-codes.ts +10 -0
  108. package/src/ui/localization/phone-number-error-codes.ts +10 -0
  109. package/src/ui/localization/stripe-localization.ts +12 -0
  110. package/src/ui/localization/team-error-codes.ts +12 -0
  111. package/src/ui/localization/two-factor-error-codes.ts +12 -0
  112. package/src/ui/localization/username-error-codes.ts +9 -0
  113. package/src/ui/server.ts +4 -0
  114. package/src/ui/style.css +146 -0
  115. package/src/ui/tanstack.ts +1 -0
  116. package/src/ui/triplit.ts +1 -0
  117. package/src/ui/types/account-options.ts +35 -0
  118. package/src/ui/types/additional-fields.ts +21 -0
  119. package/src/ui/types/any-auth-client.ts +6 -0
  120. package/src/ui/types/api-key.ts +9 -0
  121. package/src/ui/types/auth-client.ts +41 -0
  122. package/src/ui/types/auth-hooks.ts +81 -0
  123. package/src/ui/types/auth-mutators.ts +21 -0
  124. package/src/ui/types/avatar-options.ts +29 -0
  125. package/src/ui/types/captcha-options.ts +32 -0
  126. package/src/ui/types/captcha-provider.ts +7 -0
  127. package/src/ui/types/credentials-options.ts +38 -0
  128. package/src/ui/types/delete-user-options.ts +7 -0
  129. package/src/ui/types/email-verification-options.ts +7 -0
  130. package/src/ui/types/fetch-error.ts +6 -0
  131. package/src/ui/types/generic-oauth-options.ts +16 -0
  132. package/src/ui/types/gravatar-options.ts +21 -0
  133. package/src/ui/types/image.ts +7 -0
  134. package/src/ui/types/invitation.ts +10 -0
  135. package/src/ui/types/link.ts +7 -0
  136. package/src/ui/types/organization-options.ts +106 -0
  137. package/src/ui/types/password-validation.ts +16 -0
  138. package/src/ui/types/profile.ts +15 -0
  139. package/src/ui/types/refetch.ts +1 -0
  140. package/src/ui/types/render-toast.ts +9 -0
  141. package/src/ui/types/sign-up-options.ts +7 -0
  142. package/src/ui/types/social-options.ts +16 -0
  143. 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.26",
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';