@edcalderon/auth 1.1.2 โ 1.2.0
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 +14 -0
- package/README.md +173 -390
- package/dist/providers/HybridNativeClient.js +14 -0
- package/dist/providers/HybridWebClient.js +14 -0
- package/dist/providers/SupabaseClient.js +16 -1
- package/dist/types.d.ts +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-03-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- ๐ Added agnostic Web3 support to the core `SignInOptions` (`options.provider === 'web3'`).
|
|
8
|
+
- ๐ Upgraded `SupabaseClient` and `HybridClient` adapters to natively call Supabase's `signInWithWeb3` standard.
|
|
9
|
+
- ๐ Documented Wagmi and `@solana/wallet-adapter` implementation examples in README.
|
|
10
|
+
|
|
11
|
+
## [1.1.3] - 2026-03-02
|
|
12
|
+
|
|
13
|
+
### Docs
|
|
14
|
+
|
|
15
|
+
- ๐ Fully rewrote README to document the new `v1.1.0` Universal Compatibility (Web + Next.js + React Native/Expo) APIs and export paths.
|
|
16
|
+
|
|
3
17
|
## [1.1.2] - 2026-03-02
|
|
4
18
|
|
|
5
19
|
### Changed
|
package/README.md
CHANGED
|
@@ -4,500 +4,283 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@edcalderon/auth)
|
|
5
5
|
[](https://github.com/edcalderon/my-second-brain/tree/main/packages/auth)
|
|
6
6
|
|
|
7
|
-
A universal, **provider-agnostic** authentication orchestration package for
|
|
7
|
+
A universal, **provider-agnostic** authentication orchestration package designed for absolute runtime portability. One abstraction that works flawlessly across React Web `(18.x/19.x)`, Next.js `(14/15)`, and React Native/Expo `(SDK 50+)`.
|
|
8
|
+
|
|
9
|
+
Swap between Supabase, Firebase, Hybrid, or any custom provider without changing a single line of your UX component code.
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
11
|
-
## ๐ Latest Changes (v1.
|
|
13
|
+
## ๐ Latest Changes (v1.2.0)
|
|
12
14
|
|
|
13
|
-
###
|
|
15
|
+
### Added
|
|
14
16
|
|
|
15
|
-
-
|
|
17
|
+
- ๐ Added agnostic Web3 support to the core `SignInOptions` (`options.provider === 'web3'`).
|
|
18
|
+
- ๐ Upgraded `SupabaseClient` and `HybridClient` adapters to natively call Supabase's `signInWithWeb3` standard.
|
|
19
|
+
- ๐ Documented Wagmi and `@solana/wallet-adapter` implementation examples in README.
|
|
16
20
|
|
|
17
21
|
For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
|
|
18
22
|
|
|
19
23
|
---
|
|
20
24
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
The package follows a **Single Source of Truth** model with a **Federated OAuth Strategy**:
|
|
24
|
-
|
|
25
|
-
- **Principal Database (Source of Truth)**: Supabase anchors user identities, metadata, roles, and RLS policies in PostgreSQL (`auth.users`, `auth.identities`).
|
|
26
|
-
- **OAuth / Identity Providers**: External services (Firebase, Directus, native Google OAuth, Auth0, etc.) handle frontend login bridges or federated SSO flows.
|
|
27
|
-
- **The Orchestrator (`@edcalderon/auth`)**: A thin bridge layer that exposes generic interfaces (`User`, `AuthClient`). Applications consume a unified context without coupling to any specific vendor.
|
|
25
|
+
## ๐ Runtime Support Matrix
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
| Target Runtime | Engine / Framework | Notes | Supported Flow Semantics |
|
|
28
|
+
|----------------|--------------------|-------|-----------------|
|
|
29
|
+
| **Web** | React, Vite, SPA | Standard web APIs available (`window`) | `popup`, `redirect` |
|
|
30
|
+
| **Server** | Next.js Client | Compatible with App Router Contexts | `redirect`, `popup` |
|
|
31
|
+
| **Native** | Expo/React Native | Clean native bundles, strictly no web assumptions | `native` |
|
|
30
32
|
|
|
31
|
-
1. **Frontend Applications** `=>` consume **`@edcalderon/auth`** via `useAuth()`
|
|
32
|
-
2. **`@edcalderon/auth`** orchestrates the adapters:
|
|
33
|
-
- `=>` **Supabase Adapter** (Direct Session)
|
|
34
|
-
- `=>` **Hybrid Bridge** (Firebase OAuth + Supabase Session)
|
|
35
|
-
- `=>` **Custom Adapters** (e.g. Directus SSO, Auth0)
|
|
36
|
-
3. **Identity Providers** (Firebase/Directus) `=>` Sync Session to **Supabase**
|
|
37
|
-
4. **Supabase** `=>` Manages Roles & Scopes in the **PostgreSQL** Database
|
|
38
33
|
---
|
|
39
34
|
|
|
40
|
-
##
|
|
35
|
+
## ๐๏ธ Architecture
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- ๐ **Session Token Access** โ `getSessionToken()` for API calls regardless of provider
|
|
48
|
-
- ๐ฆ **Tree-Shakeable** โ import only the adapters you need
|
|
49
|
-
- ๐๏ธ **Zero Lock-In** โ swap providers by changing one line of dependency injection
|
|
37
|
+
The package follows a **Single Source of Truth** model with a **Federated OAuth Strategy**:
|
|
38
|
+
- **Principal Database (Source of Truth)**: Supabase anchors user identities, metadata, roles, and RLS policies in PostgreSQL.
|
|
39
|
+
- **The Orchestrator (`@edcalderon/auth`)**: A thin bridge layer exposing a generic interface (`User`, `AuthClient`).
|
|
40
|
+
|
|
41
|
+
The UI consumes a **unified context** disconnected entirely from provider implementations.
|
|
50
42
|
|
|
51
43
|
---
|
|
52
44
|
|
|
53
45
|
## Installation
|
|
54
46
|
|
|
55
|
-
### From NPM (public)
|
|
56
|
-
|
|
57
47
|
```bash
|
|
58
48
|
npm install @edcalderon/auth
|
|
59
49
|
# or
|
|
60
50
|
pnpm add @edcalderon/auth
|
|
61
|
-
# or
|
|
62
|
-
yarn add @edcalderon/auth
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### From Monorepo (internal workspace)
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
pnpm --filter <your-app> add @edcalderon/auth@workspace:*
|
|
69
51
|
```
|
|
70
52
|
|
|
71
53
|
### Peer Dependencies
|
|
72
54
|
|
|
73
|
-
Install
|
|
55
|
+
Install peers depending on what adapters you use. (The NPM module avoids forcing packages you won't ship to Native vs Web via strict subpath exports).
|
|
74
56
|
|
|
75
57
|
```bash
|
|
76
|
-
#
|
|
58
|
+
# Core requirements
|
|
59
|
+
pnpm add react react-dom
|
|
60
|
+
|
|
61
|
+
# Supabase (Adapter peers)
|
|
77
62
|
pnpm add @supabase/supabase-js
|
|
78
63
|
|
|
79
|
-
#
|
|
64
|
+
# Firebase (Hybrid/Pure peers)
|
|
80
65
|
pnpm add firebase
|
|
81
66
|
|
|
82
|
-
#
|
|
83
|
-
pnpm add
|
|
67
|
+
# Expo/Native Only
|
|
68
|
+
pnpm add react-native
|
|
84
69
|
```
|
|
85
70
|
|
|
86
|
-
> **Note:** `react` and `react-dom` (v18+ or v19+) are required peer dependencies.
|
|
87
|
-
|
|
88
71
|
---
|
|
89
72
|
|
|
90
|
-
##
|
|
73
|
+
## Subpath Exports (Crucial for RN/Next.js compatibility)
|
|
91
74
|
|
|
92
|
-
|
|
75
|
+
The package avoids bleeding `window` or `document` objects into Expo bundles or bleeding heavy native dependencies into web implementations via strict environment exports:
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
### 2. Create the Provider Wrapper
|
|
102
|
-
|
|
103
|
-
#### Supabase (Direct)
|
|
104
|
-
|
|
105
|
-
```tsx
|
|
106
|
-
// components/auth/AuthProvider.tsx
|
|
107
|
-
"use client";
|
|
77
|
+
- `@edcalderon/auth` (Shared Core interfaces + Contexts)
|
|
78
|
+
- `@edcalderon/auth/supabase`
|
|
79
|
+
- `@edcalderon/auth/firebase-web`
|
|
80
|
+
- `@edcalderon/auth/firebase-native`
|
|
81
|
+
- `@edcalderon/auth/hybrid-web`
|
|
82
|
+
- `@edcalderon/auth/hybrid-native`
|
|
108
83
|
|
|
109
|
-
|
|
110
|
-
import { supabase } from "@/lib/supabase";
|
|
111
|
-
import { useMemo, type ReactNode } from "react";
|
|
84
|
+
---
|
|
112
85
|
|
|
113
|
-
|
|
114
|
-
const client = useMemo(() => new SupabaseClient(supabase), []);
|
|
115
|
-
return <UniversalAuthProvider client={client}>{children}</UniversalAuthProvider>;
|
|
116
|
-
}
|
|
86
|
+
## Quick Start (Web & Next.js)
|
|
117
87
|
|
|
118
|
-
|
|
119
|
-
```
|
|
88
|
+
### 1. Unified React Component UI (Usage)
|
|
120
89
|
|
|
121
|
-
|
|
90
|
+
Your component code is 100% blind to what provider or environment you are using. The `signIn` orchestration handles translating standard intent into provider actions seamlessly.
|
|
122
91
|
|
|
123
92
|
```tsx
|
|
124
93
|
"use client";
|
|
125
|
-
|
|
126
|
-
import { AuthProvider as UniversalAuthProvider, HybridClient, useAuth as useUniversalAuth } from "@edcalderon/auth";
|
|
127
|
-
import { supabase } from "@/lib/supabase";
|
|
128
|
-
import { auth, googleProvider, signInWithPopup, signOut, GoogleAuthProvider } from "@/lib/firebase";
|
|
129
|
-
import { useMemo, type ReactNode } from "react";
|
|
130
|
-
|
|
131
|
-
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
132
|
-
const client = useMemo(() => new HybridClient({
|
|
133
|
-
supabase,
|
|
134
|
-
firebaseAuth: auth,
|
|
135
|
-
firebaseMethods: {
|
|
136
|
-
signInWithPopup,
|
|
137
|
-
signOut,
|
|
138
|
-
credentialFromResult: GoogleAuthProvider.credentialFromResult,
|
|
139
|
-
},
|
|
140
|
-
googleProvider,
|
|
141
|
-
}), []);
|
|
142
|
-
|
|
143
|
-
return <UniversalAuthProvider client={client}>{children}</UniversalAuthProvider>;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export const useAuth = useUniversalAuth;
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### 3. Use in Components
|
|
150
|
-
|
|
151
|
-
Every component consumes **identical signatures** regardless of which provider is active:
|
|
152
|
-
|
|
153
|
-
```tsx
|
|
154
|
-
import { useAuth } from "@/components/auth/AuthProvider";
|
|
94
|
+
import { useAuth } from "@edcalderon/auth";
|
|
155
95
|
|
|
156
96
|
export default function Dashboard() {
|
|
157
|
-
const { user, loading, error,
|
|
97
|
+
const { user, loading, error, signIn, signOutUser } = useAuth();
|
|
158
98
|
|
|
159
99
|
if (loading) return <Spinner />;
|
|
160
100
|
if (error) return <p>Error: {error}</p>;
|
|
161
|
-
|
|
101
|
+
|
|
102
|
+
if (!user) {
|
|
103
|
+
return (
|
|
104
|
+
<button onClick={() => signIn({ provider: "google", flow: "popup" })}>
|
|
105
|
+
Sign In with Google
|
|
106
|
+
</button>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
162
109
|
|
|
163
110
|
return (
|
|
164
111
|
<div>
|
|
165
|
-
<p>Welcome, {user.email}
|
|
112
|
+
<p>Welcome, {user.email}</p>
|
|
166
113
|
<button onClick={signOutUser}>Sign Out</button>
|
|
167
114
|
</div>
|
|
168
115
|
);
|
|
169
116
|
}
|
|
170
117
|
```
|
|
171
118
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
## ๐ Extensibility โ Custom Adapters
|
|
119
|
+
### 2. Web3 Crypto Wallets (Wagmi / Solana)
|
|
175
120
|
|
|
176
|
-
|
|
121
|
+
Because the orchestration is provider-blind, you can easily pair it with libraries like `wagmi` or `@solana/wallet-adapter-react`.
|
|
177
122
|
|
|
178
|
-
|
|
123
|
+
```tsx
|
|
124
|
+
"use client";
|
|
125
|
+
import { useAuth } from "@edcalderon/auth";
|
|
126
|
+
import { useWallet } from "@solana/wallet-adapter-react";
|
|
127
|
+
|
|
128
|
+
export function SolanaLogin() {
|
|
129
|
+
const { signIn } = useAuth();
|
|
130
|
+
const wallet = useWallet();
|
|
131
|
+
|
|
132
|
+
const handleWeb3SignIn = () => {
|
|
133
|
+
if (!wallet.connected) return;
|
|
134
|
+
|
|
135
|
+
signIn({
|
|
136
|
+
provider: "web3",
|
|
137
|
+
web3: {
|
|
138
|
+
chain: "solana",
|
|
139
|
+
wallet: wallet.wallet?.adapter // Pass the raw wallet adapter
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
179
143
|
|
|
180
|
-
|
|
181
|
-
export interface AuthClient {
|
|
182
|
-
getUser(): Promise<User | null>;
|
|
183
|
-
signInWithEmail(email: string, password: string): Promise<User>;
|
|
184
|
-
signInWithGoogle(redirectTo?: string): Promise<void>;
|
|
185
|
-
signOut(): Promise<void>;
|
|
186
|
-
onAuthStateChange(callback: (user: User | null) => void): () => void;
|
|
187
|
-
getSessionToken(): Promise<string | null>;
|
|
144
|
+
return <button onClick={handleWeb3SignIn}>Sign In with Solana</button>;
|
|
188
145
|
}
|
|
189
146
|
```
|
|
190
147
|
|
|
191
|
-
###
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
export interface User {
|
|
195
|
-
id: string;
|
|
196
|
-
email?: string;
|
|
197
|
-
avatarUrl?: string;
|
|
198
|
-
provider?: string;
|
|
199
|
-
metadata?: Record<string, any>;
|
|
200
|
-
}
|
|
201
|
-
```
|
|
148
|
+
### 3. Provider Top-Level App Injectors
|
|
202
149
|
|
|
203
|
-
|
|
150
|
+
Wire the environment appropriate class up at your app root.
|
|
204
151
|
|
|
205
|
-
|
|
152
|
+
#### Supabase (Web/Native Universal)
|
|
206
153
|
|
|
207
|
-
```
|
|
208
|
-
|
|
154
|
+
```tsx
|
|
155
|
+
"use client";
|
|
156
|
+
import { AuthProvider } from "@edcalderon/auth";
|
|
157
|
+
import { SupabaseClient } from "@edcalderon/auth/supabase";
|
|
158
|
+
import { supabase } from "@/lib/supabase";
|
|
209
159
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
160
|
+
export function AppProviders({ children }) {
|
|
161
|
+
// Works perfectly in both web and Next.js out of the box
|
|
162
|
+
const client = new SupabaseClient({ supabase });
|
|
163
|
+
return <AuthProvider client={client}>{children}</AuthProvider>;
|
|
213
164
|
}
|
|
165
|
+
```
|
|
214
166
|
|
|
215
|
-
|
|
216
|
-
private directusUrl: string;
|
|
217
|
-
private supabase: any;
|
|
218
|
-
private currentUser: User | null = null;
|
|
219
|
-
private listeners: Set<(user: User | null) => void> = new Set();
|
|
220
|
-
|
|
221
|
-
constructor(options: DirectusClientOptions) {
|
|
222
|
-
this.directusUrl = options.directusUrl;
|
|
223
|
-
this.supabase = options.supabase;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private mapUser(directusUser: any): User | null {
|
|
227
|
-
if (!directusUser) return null;
|
|
228
|
-
return {
|
|
229
|
-
id: directusUser.id,
|
|
230
|
-
email: directusUser.email,
|
|
231
|
-
avatarUrl: directusUser.avatar
|
|
232
|
-
? `${this.directusUrl}/assets/${directusUser.avatar}`
|
|
233
|
-
: undefined,
|
|
234
|
-
provider: "directus",
|
|
235
|
-
metadata: {
|
|
236
|
-
firstName: directusUser.first_name,
|
|
237
|
-
lastName: directusUser.last_name,
|
|
238
|
-
role: directusUser.role,
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async getUser(): Promise<User | null> {
|
|
244
|
-
try {
|
|
245
|
-
const res = await fetch(`${this.directusUrl}/users/me`, {
|
|
246
|
-
credentials: "include",
|
|
247
|
-
});
|
|
248
|
-
if (!res.ok) return null;
|
|
249
|
-
const { data } = await res.json();
|
|
250
|
-
this.currentUser = this.mapUser(data);
|
|
251
|
-
return this.currentUser;
|
|
252
|
-
} catch {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async signInWithEmail(email: string, password: string): Promise<User> {
|
|
258
|
-
const res = await fetch(`${this.directusUrl}/auth/login`, {
|
|
259
|
-
method: "POST",
|
|
260
|
-
headers: { "Content-Type": "application/json" },
|
|
261
|
-
credentials: "include",
|
|
262
|
-
body: JSON.stringify({ email, password }),
|
|
263
|
-
});
|
|
264
|
-
if (!res.ok) throw new Error("Directus login failed");
|
|
265
|
-
const user = await this.getUser();
|
|
266
|
-
if (!user) throw new Error("No user after login");
|
|
267
|
-
this.notifyListeners(user);
|
|
268
|
-
|
|
269
|
-
// Optional: sync to Supabase
|
|
270
|
-
if (this.supabase) {
|
|
271
|
-
await this.syncToSupabase(user);
|
|
272
|
-
}
|
|
273
|
-
return user;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async signInWithGoogle(redirectTo?: string): Promise<void> {
|
|
277
|
-
// Directus SSO โ redirect to Directus Google OAuth endpoint
|
|
278
|
-
const callback = redirectTo || window.location.origin + "/auth/callback";
|
|
279
|
-
window.location.href =
|
|
280
|
-
`${this.directusUrl}/auth/login/google?redirect=${encodeURIComponent(callback)}`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async signOut(): Promise<void> {
|
|
284
|
-
await fetch(`${this.directusUrl}/auth/logout`, {
|
|
285
|
-
method: "POST",
|
|
286
|
-
credentials: "include",
|
|
287
|
-
});
|
|
288
|
-
this.currentUser = null;
|
|
289
|
-
this.notifyListeners(null);
|
|
290
|
-
}
|
|
167
|
+
#### Hybrid (Firebase UI โ Supabase Database Session Bridging for Web)
|
|
291
168
|
|
|
292
|
-
|
|
293
|
-
this.listeners.add(callback);
|
|
294
|
-
return () => { this.listeners.delete(callback); };
|
|
295
|
-
}
|
|
169
|
+
Perfect if you want Firebase to handle the Google popup, but want to automatically consume the ID Token into Supabase to maintain your DB as the source of truth!
|
|
296
170
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!res.ok) return null;
|
|
304
|
-
const { data } = await res.json();
|
|
305
|
-
return data?.access_token ?? null;
|
|
306
|
-
} catch {
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
171
|
+
```tsx
|
|
172
|
+
"use client";
|
|
173
|
+
import { AuthProvider } from "@edcalderon/auth";
|
|
174
|
+
import { HybridWebClient } from "@edcalderon/auth/hybrid-web";
|
|
175
|
+
import { supabase } from "@/lib/supabase";
|
|
176
|
+
import { auth, signInWithPopup, signOut, GoogleAuthProvider } from "@/lib/firebase";
|
|
310
177
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
178
|
+
export function AppProviders({ children }) {
|
|
179
|
+
const client = new HybridWebClient({
|
|
180
|
+
supabase,
|
|
181
|
+
firebaseAuth: auth,
|
|
182
|
+
firebaseMethods: { signInWithPopup, signOut, credentialFromResult: GoogleAuthProvider.credentialFromResult },
|
|
183
|
+
googleProvider: new GoogleAuthProvider(),
|
|
184
|
+
});
|
|
314
185
|
|
|
315
|
-
|
|
316
|
-
// Sync user identity to Supabase as source of truth
|
|
317
|
-
// Implementation depends on your Supabase setup
|
|
318
|
-
}
|
|
186
|
+
return <AuthProvider client={client}>{children}</AuthProvider>;
|
|
319
187
|
}
|
|
320
188
|
```
|
|
321
189
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
```tsx
|
|
325
|
-
import { AuthProvider as UniversalAuthProvider } from "@edcalderon/auth";
|
|
326
|
-
import { DirectusClient } from "./adapters/DirectusClient";
|
|
190
|
+
---
|
|
327
191
|
|
|
328
|
-
|
|
329
|
-
directusUrl: "https://directus.example.com",
|
|
330
|
-
supabase: supabaseInstance, // optional sync
|
|
331
|
-
});
|
|
192
|
+
## Quick Start (Expo & React Native)
|
|
332
193
|
|
|
333
|
-
|
|
334
|
-
<App />
|
|
335
|
-
</UniversalAuthProvider>
|
|
336
|
-
```
|
|
194
|
+
React Native apps cannot safely utilize Web's window or popup assumptions. Because of the unified typings, your components never have to change, you just wire up the specific native adapters.
|
|
337
195
|
|
|
338
|
-
###
|
|
196
|
+
### Hybrid Strategy Native (`expo-auth-session`)
|
|
339
197
|
|
|
340
|
-
|
|
341
|
-
import type { AuthClient, User } from "@edcalderon/auth";
|
|
198
|
+
Instead of trying to pop up Firebase Web via polyfills, explicitly hand over native execution capabilities down to the adapter utilizing React Native Expo equivalents.
|
|
342
199
|
|
|
343
|
-
|
|
344
|
-
|
|
200
|
+
```tsx
|
|
201
|
+
import { AuthProvider } from "@edcalderon/auth";
|
|
202
|
+
import { HybridNativeClient } from "@edcalderon/auth/hybrid-native";
|
|
203
|
+
import { supabase } from "@/lib/supabase";
|
|
204
|
+
import { auth, signInWithCredential } from "firebase/auth";
|
|
205
|
+
import * as Google from 'expo-auth-session/providers/google'; // Or react-native-google-signin
|
|
206
|
+
|
|
207
|
+
export function ExpoProviders({ children }) {
|
|
208
|
+
// 1. You provide strictly native capability functions out of your Expo ecosystem
|
|
209
|
+
const nativeGoogleHandler = async (options) => {
|
|
210
|
+
// e.g promptAsync()
|
|
211
|
+
// Exchange credential response for Firebase Native Credentials
|
|
212
|
+
// Return { credential, idToken }
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const client = new HybridNativeClient({
|
|
216
|
+
supabase,
|
|
217
|
+
firebaseAuth: auth,
|
|
218
|
+
firebaseMethods: { signInWithCredential, signOut },
|
|
219
|
+
oauthHandlers: {
|
|
220
|
+
"google": nativeGoogleHandler
|
|
221
|
+
}
|
|
222
|
+
});
|
|
345
223
|
|
|
346
|
-
|
|
347
|
-
async signInWithEmail(email: string, password: string): Promise<User> { /* ... */ }
|
|
348
|
-
async signInWithGoogle(redirectTo?: string): Promise<void> { /* ... */ }
|
|
349
|
-
async signOut(): Promise<void> { /* ... */ }
|
|
350
|
-
onAuthStateChange(callback: (user: User | null) => void): () => void { /* ... */ }
|
|
351
|
-
async getSessionToken(): Promise<string | null> { /* ... */ }
|
|
224
|
+
return <AuthProvider client={client}>{children}</AuthProvider>;
|
|
352
225
|
}
|
|
353
226
|
```
|
|
354
227
|
|
|
355
|
-
|
|
228
|
+
Now, clicking `signIn({ provider: "google", flow: "native" })` from anywhere inside your Expo app safely triggers `nativeGoogleHandler` and orchestrates Firebase translation down to Supabase seamlessly behind the scenes!
|
|
356
229
|
|
|
357
230
|
---
|
|
358
231
|
|
|
359
|
-
##
|
|
360
|
-
|
|
361
|
-
### `SupabaseClient`
|
|
362
|
-
|
|
363
|
-
Direct Supabase Auth adapter. Uses `@supabase/supabase-js` for session management, OAuth, and email/password.
|
|
364
|
-
|
|
365
|
-
```typescript
|
|
366
|
-
import { SupabaseClient } from "@edcalderon/auth";
|
|
367
|
-
import { createClient } from "@supabase/supabase-js";
|
|
368
|
-
|
|
369
|
-
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
370
|
-
const client = new SupabaseClient(supabase);
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
**Features:**
|
|
374
|
-
- Email/password sign-in (`signInWithPassword`)
|
|
375
|
-
- Google OAuth (`signInWithOAuth`)
|
|
376
|
-
- Session token via `getSession().access_token`
|
|
377
|
-
- Real-time auth state changes via `onAuthStateChange`
|
|
232
|
+
## ๐ API Reference - Extensibility
|
|
378
233
|
|
|
379
|
-
### `
|
|
380
|
-
|
|
381
|
-
Firebase-only adapter. Uses Firebase Auth methods via dependency injection (tree-shaking friendly).
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
import { FirebaseClient } from "@edcalderon/auth";
|
|
385
|
-
import { getAuth, GoogleAuthProvider, signInWithEmailAndPassword, signInWithPopup, signOut, onAuthStateChanged } from "firebase/auth";
|
|
386
|
-
|
|
387
|
-
const auth = getAuth(app);
|
|
388
|
-
const client = new FirebaseClient(auth, {
|
|
389
|
-
signInWithEmailAndPassword,
|
|
390
|
-
signInWithPopup,
|
|
391
|
-
signOut,
|
|
392
|
-
onAuthStateChanged,
|
|
393
|
-
}, new GoogleAuthProvider());
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
**Features:**
|
|
397
|
-
- Email/password sign-in
|
|
398
|
-
- Google popup sign-in
|
|
399
|
-
- Firebase ID token via `getIdToken()`
|
|
400
|
-
- Real-time auth state changes
|
|
401
|
-
|
|
402
|
-
### `HybridClient`
|
|
234
|
+
### The `AuthClient` Interface
|
|
403
235
|
|
|
404
|
-
|
|
236
|
+
The core strength of `@edcalderon/auth` is that **any authentication service** can be mapped directly onto the `AuthClient` type, exposing typed portability out-of-the-box.
|
|
405
237
|
|
|
406
238
|
```typescript
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
**Features:**
|
|
418
|
-
- Firebase popup โ extracts Google OIDC ID token โ passes to Supabase `signInWithIdToken`
|
|
419
|
-
- Graceful fallback to Supabase native OAuth when Firebase is not configured
|
|
420
|
-
- Dual sign-out (Firebase + Supabase)
|
|
421
|
-
- Auth state tracked via Supabase session
|
|
422
|
-
|
|
423
|
-
---
|
|
424
|
-
|
|
425
|
-
## API Reference
|
|
426
|
-
|
|
427
|
-
### `<AuthProvider>`
|
|
239
|
+
type AuthRuntime = "web" | "native" | "server";
|
|
240
|
+
type OAuthFlow = "popup" | "redirect" | "native";
|
|
241
|
+
|
|
242
|
+
export interface Web3SignInOptions {
|
|
243
|
+
chain: "ethereum" | "solana" | "bitcoin";
|
|
244
|
+
wallet?: any;
|
|
245
|
+
message?: string;
|
|
246
|
+
signature?: string;
|
|
247
|
+
}
|
|
428
248
|
|
|
429
|
-
|
|
249
|
+
export interface SignInOptions {
|
|
250
|
+
provider?: "google" | "apple" | "github" | "web3" | string;
|
|
251
|
+
flow?: OAuthFlow;
|
|
252
|
+
redirectUri?: string;
|
|
253
|
+
web3?: Web3SignInOptions;
|
|
254
|
+
}
|
|
430
255
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
{
|
|
434
|
-
|
|
256
|
+
export interface AuthClient {
|
|
257
|
+
runtime: AuthRuntime;
|
|
258
|
+
capabilities(): { runtime: AuthRuntime; supportedFlows: OAuthFlow[] };
|
|
259
|
+
|
|
260
|
+
getUser(): Promise<User | null>;
|
|
261
|
+
signInWithEmail(email: string, password: string): Promise<User>;
|
|
262
|
+
signIn(options: SignInOptions): Promise<void>;
|
|
263
|
+
signOut(): Promise<void>;
|
|
264
|
+
|
|
265
|
+
onAuthStateChange(callback: (user: User | null) => void): () => void;
|
|
266
|
+
getSessionToken(): Promise<string | null>;
|
|
267
|
+
}
|
|
435
268
|
```
|
|
436
269
|
|
|
437
|
-
|
|
438
|
-
|------|------|-------------|
|
|
439
|
-
| `client` | `AuthClient` | The authentication adapter instance |
|
|
440
|
-
| `children` | `ReactNode` | Child components |
|
|
441
|
-
|
|
442
|
-
### `useAuth()`
|
|
443
|
-
|
|
444
|
-
React hook that returns the current authentication state and actions.
|
|
270
|
+
### The `User` Type
|
|
445
271
|
|
|
446
272
|
```typescript
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
273
|
+
export interface User {
|
|
274
|
+
id: string;
|
|
275
|
+
email?: string;
|
|
276
|
+
avatarUrl?: string;
|
|
277
|
+
provider?: string;
|
|
278
|
+
providerUserId?: string;
|
|
279
|
+
roles?: string[];
|
|
280
|
+
metadata?: Record<string, any>;
|
|
281
|
+
}
|
|
456
282
|
```
|
|
457
283
|
|
|
458
|
-
> **Note:** `useAuth()` must be called within an `<AuthProvider>`. It will throw if used outside the provider tree.
|
|
459
|
-
|
|
460
|
-
---
|
|
461
|
-
|
|
462
|
-
## Publishing & Releases
|
|
463
|
-
|
|
464
|
-
### Automated NPM Publishing
|
|
465
|
-
|
|
466
|
-
This package uses GitHub Actions for automated publishing to NPM when version tags are created.
|
|
467
|
-
|
|
468
|
-
#### Release Process
|
|
469
|
-
|
|
470
|
-
1. **Update Version**: Bump the version in `package.json`
|
|
471
|
-
```bash
|
|
472
|
-
cd packages/auth
|
|
473
|
-
npm version patch # or minor, major
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
2. **Create Git Tag**: Create and push an `auth-v*` tag
|
|
477
|
-
```bash
|
|
478
|
-
git add packages/auth/package.json
|
|
479
|
-
git commit -m "chore(auth): bump version to X.Y.Z"
|
|
480
|
-
git tag auth-vX.Y.Z
|
|
481
|
-
git push && git push --tags
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
3. **Automated Publishing**: GitHub Actions will automatically build and publish to NPM
|
|
485
|
-
|
|
486
|
-
#### NPM Token Setup
|
|
487
|
-
|
|
488
|
-
To enable automated publishing:
|
|
489
|
-
|
|
490
|
-
1. Go to [NPM](https://www.npmjs.com/) โ Access Tokens โ Generate New Token
|
|
491
|
-
2. Create a token with **Automation** scope
|
|
492
|
-
3. Add to GitHub repository secrets as `NPM_TOKEN`
|
|
493
|
-
|
|
494
|
-
---
|
|
495
|
-
|
|
496
|
-
## Documentation
|
|
497
|
-
|
|
498
|
-
- **[CHANGELOG](CHANGELOG.md)** โ Version history and changes
|
|
499
|
-
- **[GitHub Releases](https://github.com/edcalderon/my-second-brain/releases)** โ Tagged releases
|
|
500
|
-
|
|
501
284
|
---
|
|
502
285
|
|
|
503
286
|
## License
|
|
@@ -48,6 +48,20 @@ export class HybridNativeClient {
|
|
|
48
48
|
}
|
|
49
49
|
async signIn(options) {
|
|
50
50
|
const provider = options.provider || "google";
|
|
51
|
+
if (provider === "web3") {
|
|
52
|
+
if (!options.web3)
|
|
53
|
+
throw new Error("CONFIG_ERROR: options.web3 is required when provider is 'web3'");
|
|
54
|
+
const { error } = await this.supabase.auth.signInWithWeb3({
|
|
55
|
+
// @ts-ignore - Supabase TS types might be strict (e.g. 0x${string}), bypass for agnostic adapter
|
|
56
|
+
chain: options.web3.chain,
|
|
57
|
+
message: options.web3.message,
|
|
58
|
+
signature: options.web3.signature,
|
|
59
|
+
wallet: options.web3.wallet,
|
|
60
|
+
});
|
|
61
|
+
if (error)
|
|
62
|
+
throw new Error(`PROVIDER_ERROR: ${error.message}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
51
65
|
// Pure Supabase route
|
|
52
66
|
if (!this.firebaseAuth || !this.methods || !this.oauthHandlers[provider]) {
|
|
53
67
|
console.warn(`Native OAuth via Hybrid fallback for '${provider}' targeting Supabase purely`);
|
|
@@ -48,6 +48,20 @@ export class HybridWebClient {
|
|
|
48
48
|
}
|
|
49
49
|
async signIn(options) {
|
|
50
50
|
const provider = options.provider || "google";
|
|
51
|
+
if (provider === "web3") {
|
|
52
|
+
if (!options.web3)
|
|
53
|
+
throw new Error("CONFIG_ERROR: options.web3 is required when provider is 'web3'");
|
|
54
|
+
const { error } = await this.supabase.auth.signInWithWeb3({
|
|
55
|
+
// @ts-ignore - Supabase TS types might be strict (e.g. 0x${string}), bypass for agnostic adapter
|
|
56
|
+
chain: options.web3.chain,
|
|
57
|
+
message: options.web3.message,
|
|
58
|
+
signature: options.web3.signature,
|
|
59
|
+
wallet: options.web3.wallet,
|
|
60
|
+
});
|
|
61
|
+
if (error)
|
|
62
|
+
throw new Error(`PROVIDER_ERROR: ${error.message}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
51
65
|
if (!this.firebaseAuth || !this.methods || !this.googleProvider) {
|
|
52
66
|
console.warn("Firebase not configured on Hybrid fallback, using Supabase pure auth");
|
|
53
67
|
const { error } = await this.supabase.auth.signInWithOAuth({
|
|
@@ -50,7 +50,22 @@ export class SupabaseClient {
|
|
|
50
50
|
}
|
|
51
51
|
async signIn(options) {
|
|
52
52
|
if (!options.provider) {
|
|
53
|
-
throw new Error("CONFIG_ERROR: options.provider is required for Supabase OAuth");
|
|
53
|
+
throw new Error("CONFIG_ERROR: options.provider is required for Supabase OAuth or Web3");
|
|
54
|
+
}
|
|
55
|
+
if (options.provider === "web3") {
|
|
56
|
+
if (!options.web3) {
|
|
57
|
+
throw new Error("CONFIG_ERROR: options.web3 is required when provider is 'web3'");
|
|
58
|
+
}
|
|
59
|
+
const { error } = await this.supabase.auth.signInWithWeb3({
|
|
60
|
+
// @ts-ignore - Supabase TS types might be strict (e.g. 0x${string}), bypass for agnostic adapter
|
|
61
|
+
chain: options.web3.chain,
|
|
62
|
+
message: options.web3.message,
|
|
63
|
+
signature: options.web3.signature,
|
|
64
|
+
wallet: options.web3.wallet,
|
|
65
|
+
});
|
|
66
|
+
if (error)
|
|
67
|
+
throw new Error(`PROVIDER_ERROR: ${error.message}`);
|
|
68
|
+
return;
|
|
54
69
|
}
|
|
55
70
|
let redirectTo = options.redirectUri;
|
|
56
71
|
// Apply web assumption ONLY if strictly web
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export type AuthRuntime = "web" | "native" | "server";
|
|
2
2
|
export type OAuthFlow = "popup" | "redirect" | "native";
|
|
3
|
+
export interface Web3SignInOptions {
|
|
4
|
+
chain: "ethereum" | "solana" | "bitcoin";
|
|
5
|
+
wallet?: any;
|
|
6
|
+
message?: string;
|
|
7
|
+
signature?: string;
|
|
8
|
+
}
|
|
3
9
|
export interface SignInOptions {
|
|
4
|
-
provider?: "google" | "apple" | "github" | string;
|
|
10
|
+
provider?: "google" | "apple" | "github" | "web3" | string;
|
|
5
11
|
flow?: OAuthFlow;
|
|
6
12
|
redirectUri?: string;
|
|
13
|
+
web3?: Web3SignInOptions;
|
|
7
14
|
}
|
|
8
15
|
export type AuthErrorCode = "CONFIG_ERROR" | "UNSUPPORTED_FLOW" | "NETWORK_ERROR" | "PROVIDER_ERROR" | "SESSION_ERROR";
|
|
9
16
|
export interface User {
|