@edcalderon/auth 1.0.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 ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-03-01
4
+
5
+ ### Initial Release
6
+
7
+ - ✨ Provider-agnostic `AuthClient` interface
8
+ - πŸ”Œ Built-in Supabase adapter (`SupabaseClient`)
9
+ - πŸ”Œ Built-in Firebase adapter (`FirebaseClient`)
10
+ - πŸ”Œ Hybrid adapter for Firebaseβ†’Supabase federated flows (`HybridClient`)
11
+ - βš›οΈ React `AuthProvider` and `useAuth` hook
12
+ - πŸ›‘οΈ Unified `User` type across all providers
13
+ - πŸ“¦ Published as `@edcalderon/auth` on NPM
package/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # @edcalderon/auth
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@edcalderon/auth?style=flat-square&color=0ea5e9)](https://www.npmjs.com/package/@edcalderon/auth)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@edcalderon/auth?style=flat-square&color=10b981)](https://www.npmjs.com/package/@edcalderon/auth)
5
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-181717?style=flat-square&logo=github)](https://github.com/edcalderon/my-second-brain/tree/main/packages/auth)
6
+
7
+ A universal, **provider-agnostic** authentication orchestration package for React applications. Swap between Supabase, Firebase, Directus, Google OAuth, or any custom provider without changing a single line of component code.
8
+
9
+ ---
10
+
11
+ ## πŸ“‹ Latest Changes (v1.0.0)
12
+
13
+ ### Initial Release
14
+
15
+ - ✨ Provider-agnostic `AuthClient` interface
16
+ - πŸ”Œ Built-in adapters: **Supabase**, **Firebase**, **Hybrid** (Firebaseβ†’Supabase)
17
+ - βš›οΈ React `AuthProvider` context and `useAuth` hook
18
+ - πŸ›‘οΈ Unified `User` type across all providers
19
+ - πŸ”‘ Session token access via `getSessionToken()`
20
+
21
+ For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
22
+
23
+ ---
24
+
25
+ ## πŸ—οΈ Architecture
26
+
27
+ The package follows a **Single Source of Truth** model with a **Federated OAuth Strategy**:
28
+
29
+ - **Principal Database (Source of Truth)**: Supabase anchors user identities, metadata, roles, and RLS policies in PostgreSQL (`auth.users`, `auth.identities`).
30
+ - **OAuth / Identity Providers**: External services (Firebase, Directus, native Google OAuth, Auth0, etc.) handle frontend login bridges or federated SSO flows.
31
+ - **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.
32
+
33
+ ```mermaid
34
+ graph TD
35
+ UI[Frontend Applications] -->|useAuth| EdAuth["@edcalderon/auth"]
36
+ EdAuth -->|Direct Session| Supabase(Supabase)
37
+ EdAuth -->|Federated Bridge| Firebase(Firebase OAuth)
38
+ EdAuth -->|Custom Adapter| Directus(Directus SSO)
39
+ EdAuth -->|Custom Adapter| Custom(Auth0 / Custom)
40
+
41
+ Firebase -->|Sync Session| Supabase
42
+ Directus -->|Sync Session| Supabase
43
+
44
+ Supabase -->|Roles & Scopes| DB[(PostgreSQL)]
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Features
50
+
51
+ - 🎯 **Provider-Agnostic** β€” one interface, any backend
52
+ - βš›οΈ **React-First** β€” `AuthProvider` + `useAuth` hook with full TypeScript types
53
+ - πŸ”Œ **Extensible Adapter System** β€” implement `AuthClient` to add any provider
54
+ - πŸ”€ **Hybrid Flows** β€” Firebase popup β†’ Supabase session bridging out of the box
55
+ - πŸ›‘οΈ **Unified User Model** β€” `User` type normalizes identities across providers
56
+ - πŸ”‘ **Session Token Access** β€” `getSessionToken()` for API calls regardless of provider
57
+ - πŸ“¦ **Tree-Shakeable** β€” import only the adapters you need
58
+ - πŸ—οΈ **Zero Lock-In** β€” swap providers by changing one line of dependency injection
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ### From NPM (public)
65
+
66
+ ```bash
67
+ npm install @edcalderon/auth
68
+ # or
69
+ pnpm add @edcalderon/auth
70
+ # or
71
+ yarn add @edcalderon/auth
72
+ ```
73
+
74
+ ### From Monorepo (internal workspace)
75
+
76
+ ```bash
77
+ pnpm --filter <your-app> add @edcalderon/auth@workspace:*
78
+ ```
79
+
80
+ ### Peer Dependencies
81
+
82
+ Install the peer dependencies for your chosen provider(s):
83
+
84
+ ```bash
85
+ # For Supabase
86
+ pnpm add @supabase/supabase-js
87
+
88
+ # For Firebase
89
+ pnpm add firebase
90
+
91
+ # For Hybrid (Firebase + Supabase)
92
+ pnpm add @supabase/supabase-js firebase
93
+ ```
94
+
95
+ > **Note:** `react` and `react-dom` (v18+ or v19+) are required peer dependencies.
96
+
97
+ ---
98
+
99
+ ## Quick Start
100
+
101
+ ### 1. Choose Your Provider
102
+
103
+ | Provider | Class | Peer Dependency | Use Case |
104
+ |----------|-------|-----------------|----------|
105
+ | Supabase | `SupabaseClient` | `@supabase/supabase-js` | Direct Supabase Auth |
106
+ | Firebase | `FirebaseClient` | `firebase` | Firebase-only applications |
107
+ | Hybrid | `HybridClient` | Both | Firebase popup β†’ Supabase session |
108
+ | Custom | Implement `AuthClient` | Your choice | Directus, Auth0, Keycloak, etc. |
109
+
110
+ ### 2. Create the Provider Wrapper
111
+
112
+ #### Supabase (Direct)
113
+
114
+ ```tsx
115
+ // components/auth/AuthProvider.tsx
116
+ "use client";
117
+
118
+ import { AuthProvider as UniversalAuthProvider, SupabaseClient, useAuth as useUniversalAuth } from "@edcalderon/auth";
119
+ import { supabase } from "@/lib/supabase";
120
+ import { useMemo, type ReactNode } from "react";
121
+
122
+ export function AuthProvider({ children }: { children: ReactNode }) {
123
+ const client = useMemo(() => new SupabaseClient(supabase), []);
124
+ return <UniversalAuthProvider client={client}>{children}</UniversalAuthProvider>;
125
+ }
126
+
127
+ export const useAuth = useUniversalAuth;
128
+ ```
129
+
130
+ #### Hybrid (Firebase β†’ Supabase)
131
+
132
+ ```tsx
133
+ "use client";
134
+
135
+ import { AuthProvider as UniversalAuthProvider, HybridClient, useAuth as useUniversalAuth } from "@edcalderon/auth";
136
+ import { supabase } from "@/lib/supabase";
137
+ import { auth, googleProvider, signInWithPopup, signOut, GoogleAuthProvider } from "@/lib/firebase";
138
+ import { useMemo, type ReactNode } from "react";
139
+
140
+ export function AuthProvider({ children }: { children: ReactNode }) {
141
+ const client = useMemo(() => new HybridClient({
142
+ supabase,
143
+ firebaseAuth: auth,
144
+ firebaseMethods: {
145
+ signInWithPopup,
146
+ signOut,
147
+ credentialFromResult: GoogleAuthProvider.credentialFromResult,
148
+ },
149
+ googleProvider,
150
+ }), []);
151
+
152
+ return <UniversalAuthProvider client={client}>{children}</UniversalAuthProvider>;
153
+ }
154
+
155
+ export const useAuth = useUniversalAuth;
156
+ ```
157
+
158
+ ### 3. Use in Components
159
+
160
+ Every component consumes **identical signatures** regardless of which provider is active:
161
+
162
+ ```tsx
163
+ import { useAuth } from "@/components/auth/AuthProvider";
164
+
165
+ export default function Dashboard() {
166
+ const { user, loading, error, signInWithGoogle, signOutUser } = useAuth();
167
+
168
+ if (loading) return <Spinner />;
169
+ if (error) return <p>Error: {error}</p>;
170
+ if (!user) return <button onClick={() => signInWithGoogle()}>Sign In with Google</button>;
171
+
172
+ return (
173
+ <div>
174
+ <p>Welcome, {user.email} (via {user.provider})</p>
175
+ <button onClick={signOutUser}>Sign Out</button>
176
+ </div>
177
+ );
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## πŸ”Œ Extensibility β€” Custom Adapters
184
+
185
+ The core strength of `@edcalderon/auth` is that **any authentication provider** can be integrated by implementing the `AuthClient` interface. No changes to your React components are required.
186
+
187
+ ### The `AuthClient` Interface
188
+
189
+ ```typescript
190
+ export interface AuthClient {
191
+ getUser(): Promise<User | null>;
192
+ signInWithEmail(email: string, password: string): Promise<User>;
193
+ signInWithGoogle(redirectTo?: string): Promise<void>;
194
+ signOut(): Promise<void>;
195
+ onAuthStateChange(callback: (user: User | null) => void): () => void;
196
+ getSessionToken(): Promise<string | null>;
197
+ }
198
+ ```
199
+
200
+ ### The `User` Type
201
+
202
+ ```typescript
203
+ export interface User {
204
+ id: string;
205
+ email?: string;
206
+ avatarUrl?: string;
207
+ provider?: string;
208
+ metadata?: Record<string, any>;
209
+ }
210
+ ```
211
+
212
+ ### Example: Directus Adapter
213
+
214
+ A custom Directus adapter that uses Directus SSO (e.g., Google OAuth through Directus) and optionally syncs sessions back to Supabase:
215
+
216
+ ```typescript
217
+ import type { AuthClient, User } from "@edcalderon/auth";
218
+
219
+ interface DirectusClientOptions {
220
+ directusUrl: string;
221
+ supabase?: any; // Optional: sync to Supabase as source of truth
222
+ }
223
+
224
+ export class DirectusClient implements AuthClient {
225
+ private directusUrl: string;
226
+ private supabase: any;
227
+ private currentUser: User | null = null;
228
+ private listeners: Set<(user: User | null) => void> = new Set();
229
+
230
+ constructor(options: DirectusClientOptions) {
231
+ this.directusUrl = options.directusUrl;
232
+ this.supabase = options.supabase;
233
+ }
234
+
235
+ private mapUser(directusUser: any): User | null {
236
+ if (!directusUser) return null;
237
+ return {
238
+ id: directusUser.id,
239
+ email: directusUser.email,
240
+ avatarUrl: directusUser.avatar
241
+ ? `${this.directusUrl}/assets/${directusUser.avatar}`
242
+ : undefined,
243
+ provider: "directus",
244
+ metadata: {
245
+ firstName: directusUser.first_name,
246
+ lastName: directusUser.last_name,
247
+ role: directusUser.role,
248
+ },
249
+ };
250
+ }
251
+
252
+ async getUser(): Promise<User | null> {
253
+ try {
254
+ const res = await fetch(`${this.directusUrl}/users/me`, {
255
+ credentials: "include",
256
+ });
257
+ if (!res.ok) return null;
258
+ const { data } = await res.json();
259
+ this.currentUser = this.mapUser(data);
260
+ return this.currentUser;
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ async signInWithEmail(email: string, password: string): Promise<User> {
267
+ const res = await fetch(`${this.directusUrl}/auth/login`, {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/json" },
270
+ credentials: "include",
271
+ body: JSON.stringify({ email, password }),
272
+ });
273
+ if (!res.ok) throw new Error("Directus login failed");
274
+ const user = await this.getUser();
275
+ if (!user) throw new Error("No user after login");
276
+ this.notifyListeners(user);
277
+
278
+ // Optional: sync to Supabase
279
+ if (this.supabase) {
280
+ await this.syncToSupabase(user);
281
+ }
282
+ return user;
283
+ }
284
+
285
+ async signInWithGoogle(redirectTo?: string): Promise<void> {
286
+ // Directus SSO β€” redirect to Directus Google OAuth endpoint
287
+ const callback = redirectTo || window.location.origin + "/auth/callback";
288
+ window.location.href =
289
+ `${this.directusUrl}/auth/login/google?redirect=${encodeURIComponent(callback)}`;
290
+ }
291
+
292
+ async signOut(): Promise<void> {
293
+ await fetch(`${this.directusUrl}/auth/logout`, {
294
+ method: "POST",
295
+ credentials: "include",
296
+ });
297
+ this.currentUser = null;
298
+ this.notifyListeners(null);
299
+ }
300
+
301
+ onAuthStateChange(callback: (user: User | null) => void): () => void {
302
+ this.listeners.add(callback);
303
+ return () => { this.listeners.delete(callback); };
304
+ }
305
+
306
+ async getSessionToken(): Promise<string | null> {
307
+ try {
308
+ const res = await fetch(`${this.directusUrl}/auth/refresh`, {
309
+ method: "POST",
310
+ credentials: "include",
311
+ });
312
+ if (!res.ok) return null;
313
+ const { data } = await res.json();
314
+ return data?.access_token ?? null;
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+
320
+ private notifyListeners(user: User | null) {
321
+ this.listeners.forEach((cb) => cb(user));
322
+ }
323
+
324
+ private async syncToSupabase(user: User) {
325
+ // Sync user identity to Supabase as source of truth
326
+ // Implementation depends on your Supabase setup
327
+ }
328
+ }
329
+ ```
330
+
331
+ **Usage:**
332
+
333
+ ```tsx
334
+ import { AuthProvider as UniversalAuthProvider } from "@edcalderon/auth";
335
+ import { DirectusClient } from "./adapters/DirectusClient";
336
+
337
+ const client = new DirectusClient({
338
+ directusUrl: "https://directus.example.com",
339
+ supabase: supabaseInstance, // optional sync
340
+ });
341
+
342
+ <UniversalAuthProvider client={client}>
343
+ <App />
344
+ </UniversalAuthProvider>
345
+ ```
346
+
347
+ ### Example: Auth0 Adapter (Skeleton)
348
+
349
+ ```typescript
350
+ import type { AuthClient, User } from "@edcalderon/auth";
351
+
352
+ export class Auth0Client implements AuthClient {
353
+ constructor(private auth0: any) {}
354
+
355
+ async getUser(): Promise<User | null> { /* ... */ }
356
+ async signInWithEmail(email: string, password: string): Promise<User> { /* ... */ }
357
+ async signInWithGoogle(redirectTo?: string): Promise<void> { /* ... */ }
358
+ async signOut(): Promise<void> { /* ... */ }
359
+ onAuthStateChange(callback: (user: User | null) => void): () => void { /* ... */ }
360
+ async getSessionToken(): Promise<string | null> { /* ... */ }
361
+ }
362
+ ```
363
+
364
+ By implementing the `AuthClient` interface, any provider fits into the same `<AuthProvider>` and `useAuth()` workflow β€” **zero changes** to your component tree.
365
+
366
+ ---
367
+
368
+ ## Built-in Adapters
369
+
370
+ ### `SupabaseClient`
371
+
372
+ Direct Supabase Auth adapter. Uses `@supabase/supabase-js` for session management, OAuth, and email/password.
373
+
374
+ ```typescript
375
+ import { SupabaseClient } from "@edcalderon/auth";
376
+ import { createClient } from "@supabase/supabase-js";
377
+
378
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
379
+ const client = new SupabaseClient(supabase);
380
+ ```
381
+
382
+ **Features:**
383
+ - Email/password sign-in (`signInWithPassword`)
384
+ - Google OAuth (`signInWithOAuth`)
385
+ - Session token via `getSession().access_token`
386
+ - Real-time auth state changes via `onAuthStateChange`
387
+
388
+ ### `FirebaseClient`
389
+
390
+ Firebase-only adapter. Uses Firebase Auth methods via dependency injection (tree-shaking friendly).
391
+
392
+ ```typescript
393
+ import { FirebaseClient } from "@edcalderon/auth";
394
+ import { getAuth, GoogleAuthProvider, signInWithEmailAndPassword, signInWithPopup, signOut, onAuthStateChanged } from "firebase/auth";
395
+
396
+ const auth = getAuth(app);
397
+ const client = new FirebaseClient(auth, {
398
+ signInWithEmailAndPassword,
399
+ signInWithPopup,
400
+ signOut,
401
+ onAuthStateChanged,
402
+ }, new GoogleAuthProvider());
403
+ ```
404
+
405
+ **Features:**
406
+ - Email/password sign-in
407
+ - Google popup sign-in
408
+ - Firebase ID token via `getIdToken()`
409
+ - Real-time auth state changes
410
+
411
+ ### `HybridClient`
412
+
413
+ Bridges Firebase Google popup β†’ Supabase `signInWithIdToken`. Perfect for apps that need Firebase's popup UX but Supabase as the data backend.
414
+
415
+ ```typescript
416
+ import { HybridClient } from "@edcalderon/auth";
417
+
418
+ const client = new HybridClient({
419
+ supabase,
420
+ firebaseAuth: auth,
421
+ firebaseMethods: { signInWithPopup, signOut, credentialFromResult: GoogleAuthProvider.credentialFromResult },
422
+ googleProvider: new GoogleAuthProvider(),
423
+ });
424
+ ```
425
+
426
+ **Features:**
427
+ - Firebase popup β†’ extracts Google OIDC ID token β†’ passes to Supabase `signInWithIdToken`
428
+ - Graceful fallback to Supabase native OAuth when Firebase is not configured
429
+ - Dual sign-out (Firebase + Supabase)
430
+ - Auth state tracked via Supabase session
431
+
432
+ ---
433
+
434
+ ## API Reference
435
+
436
+ ### `<AuthProvider>`
437
+
438
+ React context provider that wraps your app with authentication state.
439
+
440
+ ```tsx
441
+ <AuthProvider client={authClient}>
442
+ {children}
443
+ </AuthProvider>
444
+ ```
445
+
446
+ | Prop | Type | Description |
447
+ |------|------|-------------|
448
+ | `client` | `AuthClient` | The authentication adapter instance |
449
+ | `children` | `ReactNode` | Child components |
450
+
451
+ ### `useAuth()`
452
+
453
+ React hook that returns the current authentication state and actions.
454
+
455
+ ```typescript
456
+ const {
457
+ user, // User | null
458
+ loading, // boolean
459
+ error, // string | null
460
+ client, // AuthClient (direct access)
461
+ signInWithEmail, // (email: string, password: string) => Promise<User>
462
+ signInWithGoogle, // (redirectTo?: string) => Promise<void>
463
+ signOutUser, // () => Promise<void>
464
+ } = useAuth();
465
+ ```
466
+
467
+ > **Note:** `useAuth()` must be called within an `<AuthProvider>`. It will throw if used outside the provider tree.
468
+
469
+ ---
470
+
471
+ ## Publishing & Releases
472
+
473
+ ### Automated NPM Publishing
474
+
475
+ This package uses GitHub Actions for automated publishing to NPM when version tags are created.
476
+
477
+ #### Release Process
478
+
479
+ 1. **Update Version**: Bump the version in `package.json`
480
+ ```bash
481
+ cd packages/auth
482
+ npm version patch # or minor, major
483
+ ```
484
+
485
+ 2. **Create Git Tag**: Create and push an `auth-v*` tag
486
+ ```bash
487
+ git add packages/auth/package.json
488
+ git commit -m "chore(auth): bump version to X.Y.Z"
489
+ git tag auth-vX.Y.Z
490
+ git push && git push --tags
491
+ ```
492
+
493
+ 3. **Automated Publishing**: GitHub Actions will automatically build and publish to NPM
494
+
495
+ #### NPM Token Setup
496
+
497
+ To enable automated publishing:
498
+
499
+ 1. Go to [NPM](https://www.npmjs.com/) β†’ Access Tokens β†’ Generate New Token
500
+ 2. Create a token with **Automation** scope
501
+ 3. Add to GitHub repository secrets as `NPM_TOKEN`
502
+
503
+ ---
504
+
505
+ ## Documentation
506
+
507
+ - **[CHANGELOG](CHANGELOG.md)** β€” Version history and changes
508
+ - **[GitHub Releases](https://github.com/edcalderon/my-second-brain/releases)** β€” Tagged releases
509
+
510
+ ---
511
+
512
+ ## License
513
+
514
+ MIT Β© Edward
@@ -0,0 +1,17 @@
1
+ import { ReactNode } from "react";
2
+ import { AuthClient, User } from "./types";
3
+ interface AuthContextValue {
4
+ user: User | null;
5
+ loading: boolean;
6
+ error: string | null;
7
+ client: AuthClient;
8
+ signInWithEmail: (email: string, password: string) => Promise<User>;
9
+ signInWithGoogle: (redirectTo?: string) => Promise<void>;
10
+ signOutUser: () => Promise<void>;
11
+ }
12
+ export declare function AuthProvider({ client, children }: {
13
+ client: AuthClient;
14
+ children: ReactNode;
15
+ }): import("react/jsx-runtime").JSX.Element;
16
+ export declare function useAuth(): AuthContextValue;
17
+ export {};
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useEffect, useState, useMemo } from "react";
4
+ const AuthContext = createContext(undefined);
5
+ export function AuthProvider({ client, children }) {
6
+ const [user, setUser] = useState(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState(null);
9
+ useEffect(() => {
10
+ let mounted = true;
11
+ async function initializeAuth() {
12
+ try {
13
+ const u = await client.getUser();
14
+ if (mounted) {
15
+ setUser(u);
16
+ setLoading(false);
17
+ }
18
+ }
19
+ catch (err) {
20
+ if (mounted) {
21
+ setError(err.message || "Failed to initialize auth");
22
+ setLoading(false);
23
+ }
24
+ }
25
+ }
26
+ initializeAuth();
27
+ const unsubscribe = client.onAuthStateChange((newUser) => {
28
+ if (mounted) {
29
+ setUser(newUser);
30
+ setLoading(false);
31
+ }
32
+ });
33
+ return () => {
34
+ mounted = false;
35
+ unsubscribe();
36
+ };
37
+ }, [client]);
38
+ const value = useMemo(() => ({
39
+ user,
40
+ loading,
41
+ error,
42
+ client,
43
+ signInWithEmail: async (email, password) => {
44
+ setError(null);
45
+ try {
46
+ return await client.signInWithEmail(email, password);
47
+ }
48
+ catch (err) {
49
+ setError(err.message);
50
+ throw err;
51
+ }
52
+ },
53
+ signInWithGoogle: async (redirectTo) => {
54
+ setError(null);
55
+ try {
56
+ await client.signInWithGoogle(redirectTo);
57
+ }
58
+ catch (err) {
59
+ setError(err.message);
60
+ throw err;
61
+ }
62
+ },
63
+ signOutUser: async () => {
64
+ setError(null);
65
+ try {
66
+ await client.signOut();
67
+ }
68
+ catch (err) {
69
+ setError(err.message);
70
+ throw err;
71
+ }
72
+ },
73
+ }), [user, loading, error, client]);
74
+ return _jsx(AuthContext.Provider, { value: value, children: children });
75
+ }
76
+ export function useAuth() {
77
+ const context = useContext(AuthContext);
78
+ if (!context) {
79
+ throw new Error("useAuth must be used within AuthProvider");
80
+ }
81
+ return context;
82
+ }
83
+ //# sourceMappingURL=AuthProvider.js.map
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./providers/SupabaseClient";
3
+ export * from "./providers/FirebaseClient";
4
+ export * from "./providers/HybridClient";
5
+ export * from "./AuthProvider";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./types";
2
+ export * from "./providers/SupabaseClient";
3
+ export * from "./providers/FirebaseClient";
4
+ export * from "./providers/HybridClient";
5
+ export * from "./AuthProvider";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ import type { Auth } from "firebase/auth";
2
+ import type { GoogleAuthProvider as GoogleAuthProviderType } from "firebase/auth";
3
+ import { AuthClient, User } from "../types";
4
+ export interface FirebaseMethods {
5
+ signInWithEmailAndPassword: any;
6
+ signInWithPopup: any;
7
+ signOut: any;
8
+ onAuthStateChanged: any;
9
+ }
10
+ export declare class FirebaseClient implements AuthClient {
11
+ private auth;
12
+ private methods;
13
+ private googleProvider;
14
+ constructor(auth: Auth, methods: FirebaseMethods, googleProvider: GoogleAuthProviderType);
15
+ private mapUser;
16
+ getUser(): Promise<User | null>;
17
+ signInWithEmail(email: string, password: string): Promise<User>;
18
+ signInWithGoogle(redirectTo?: string): Promise<void>;
19
+ signOut(): Promise<void>;
20
+ onAuthStateChange(callback: (user: User | null) => void): () => void;
21
+ getSessionToken(): Promise<string | null>;
22
+ }
@@ -0,0 +1,57 @@
1
+ export class FirebaseClient {
2
+ auth;
3
+ methods;
4
+ googleProvider;
5
+ constructor(auth, methods, googleProvider) {
6
+ this.auth = auth;
7
+ this.methods = methods;
8
+ this.googleProvider = googleProvider;
9
+ }
10
+ mapUser(user) {
11
+ if (!user)
12
+ return null;
13
+ return {
14
+ id: user.uid,
15
+ email: user.email || undefined,
16
+ avatarUrl: user.photoURL || undefined,
17
+ metadata: { displayName: user.displayName },
18
+ };
19
+ }
20
+ async getUser() {
21
+ if (this.auth.currentUser)
22
+ return this.mapUser(this.auth.currentUser);
23
+ return new Promise((resolve) => {
24
+ const unsubscribe = this.methods.onAuthStateChanged(this.auth, (user) => {
25
+ unsubscribe();
26
+ resolve(this.mapUser(user));
27
+ });
28
+ });
29
+ }
30
+ async signInWithEmail(email, password) {
31
+ const userCredential = await this.methods.signInWithEmailAndPassword(this.auth, email, password);
32
+ return this.mapUser(userCredential.user);
33
+ }
34
+ async signInWithGoogle(redirectTo) {
35
+ await this.methods.signInWithPopup(this.auth, this.googleProvider);
36
+ }
37
+ async signOut() {
38
+ await this.methods.signOut(this.auth);
39
+ }
40
+ onAuthStateChange(callback) {
41
+ const unsubscribe = this.methods.onAuthStateChanged(this.auth, (user) => {
42
+ callback(this.mapUser(user));
43
+ });
44
+ return () => unsubscribe();
45
+ }
46
+ async getSessionToken() {
47
+ if (!this.auth.currentUser)
48
+ return null;
49
+ try {
50
+ return await this.auth.currentUser.getIdToken();
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ }
57
+ //# sourceMappingURL=FirebaseClient.js.map
@@ -0,0 +1,29 @@
1
+ import type { SupabaseClient as SupabaseClientType } from "@supabase/supabase-js";
2
+ import type { Auth } from "firebase/auth";
3
+ import type { GoogleAuthProvider as GoogleAuthProviderType } from "firebase/auth";
4
+ import { AuthClient, User } from "../types";
5
+ export interface HybridFirebaseMethods {
6
+ signInWithPopup: any;
7
+ signOut: any;
8
+ credentialFromResult: any;
9
+ }
10
+ export interface HybridClientOptions {
11
+ supabase: SupabaseClientType;
12
+ firebaseAuth: Auth | null;
13
+ firebaseMethods: HybridFirebaseMethods | null;
14
+ googleProvider: GoogleAuthProviderType | null;
15
+ }
16
+ export declare class HybridClient implements AuthClient {
17
+ private supabase;
18
+ private firebaseAuth;
19
+ private methods;
20
+ private googleProvider;
21
+ constructor(options: HybridClientOptions);
22
+ private mapUser;
23
+ getUser(): Promise<User | null>;
24
+ signInWithEmail(email: string, password: string): Promise<User>;
25
+ signInWithGoogle(redirectTo?: string): Promise<void>;
26
+ signOut(): Promise<void>;
27
+ onAuthStateChange(callback: (user: User | null) => void): () => void;
28
+ getSessionToken(): Promise<string | null>;
29
+ }
@@ -0,0 +1,108 @@
1
+ export class HybridClient {
2
+ supabase;
3
+ firebaseAuth;
4
+ methods;
5
+ googleProvider;
6
+ constructor(options) {
7
+ this.supabase = options.supabase;
8
+ this.firebaseAuth = options.firebaseAuth;
9
+ this.methods = options.firebaseMethods;
10
+ this.googleProvider = options.googleProvider;
11
+ }
12
+ mapUser(user) {
13
+ if (!user)
14
+ return null;
15
+ return {
16
+ id: user.id,
17
+ email: user.email,
18
+ avatarUrl: user.user_metadata?.avatar_url || user.user_metadata?.picture,
19
+ provider: user.app_metadata?.provider || 'supabase',
20
+ metadata: user.user_metadata,
21
+ };
22
+ }
23
+ async getUser() {
24
+ const { data: { user }, error } = await this.supabase.auth.getUser();
25
+ if (error && error.message.includes("session"))
26
+ return null; // Safe fallback for unauthenticated
27
+ if (error)
28
+ throw error;
29
+ return this.mapUser(user);
30
+ }
31
+ async signInWithEmail(email, password) {
32
+ const { data, error } = await this.supabase.auth.signInWithPassword({
33
+ email,
34
+ password,
35
+ });
36
+ if (error)
37
+ throw error;
38
+ if (!data.user)
39
+ throw new Error("No user returned");
40
+ return this.mapUser(data.user);
41
+ }
42
+ async signInWithGoogle(redirectTo) {
43
+ if (!this.firebaseAuth || !this.methods || !this.googleProvider) {
44
+ console.warn("Firebase not configured, falling back to Supabase native OAuth");
45
+ const { error } = await this.supabase.auth.signInWithOAuth({
46
+ provider: "google",
47
+ options: {
48
+ redirectTo: redirectTo || (typeof window !== "undefined" ? window.location.origin : undefined),
49
+ },
50
+ });
51
+ if (error)
52
+ throw error;
53
+ return;
54
+ }
55
+ try {
56
+ // Firebase Google Popup
57
+ const userCredential = await this.methods.signInWithPopup(this.firebaseAuth, this.googleProvider);
58
+ // Generate Google OIDC ID token
59
+ const credential = this.methods.credentialFromResult(userCredential);
60
+ const idToken = credential?.idToken;
61
+ if (!idToken)
62
+ throw new Error("No Google ID Token found in credential");
63
+ // Pass parallel ID Token directly into Supabase
64
+ const { error: supaError } = await this.supabase.auth.signInWithIdToken({
65
+ provider: 'google',
66
+ token: idToken,
67
+ });
68
+ if (supaError)
69
+ throw supaError;
70
+ }
71
+ catch (error) {
72
+ // Ensure if anything fails, we clean up the floating firebase session
73
+ if (this.firebaseAuth && this.methods) {
74
+ await this.methods.signOut(this.firebaseAuth).catch(() => { });
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+ async signOut() {
80
+ // Break both sessions parallel safely
81
+ if (this.firebaseAuth && this.methods) {
82
+ try {
83
+ await this.methods.signOut(this.firebaseAuth);
84
+ }
85
+ catch (e) {
86
+ console.error("Firebase signout error:", e);
87
+ }
88
+ }
89
+ const { error } = await this.supabase.auth.signOut();
90
+ if (error)
91
+ throw error;
92
+ }
93
+ onAuthStateChange(callback) {
94
+ const { data: { subscription } } = this.supabase.auth.onAuthStateChange((_event, session) => {
95
+ callback(this.mapUser(session?.user));
96
+ });
97
+ return () => {
98
+ subscription.unsubscribe();
99
+ };
100
+ }
101
+ async getSessionToken() {
102
+ const { data: { session }, error } = await this.supabase.auth.getSession();
103
+ if (error || !session)
104
+ return null;
105
+ return session.access_token;
106
+ }
107
+ }
108
+ //# sourceMappingURL=HybridClient.js.map
@@ -0,0 +1,13 @@
1
+ import type { SupabaseClient as SupabaseClientType } from "@supabase/supabase-js";
2
+ import { AuthClient, User } from "../types";
3
+ export declare class SupabaseClient implements AuthClient {
4
+ private supabase;
5
+ constructor(supabase: SupabaseClientType);
6
+ private mapUser;
7
+ getUser(): Promise<User | null>;
8
+ signInWithEmail(email: string, password: string): Promise<User>;
9
+ signInWithGoogle(redirectTo?: string): Promise<void>;
10
+ signOut(): Promise<void>;
11
+ onAuthStateChange(callback: (user: User | null) => void): () => void;
12
+ getSessionToken(): Promise<string | null>;
13
+ }
@@ -0,0 +1,64 @@
1
+ export class SupabaseClient {
2
+ supabase;
3
+ constructor(supabase) {
4
+ this.supabase = supabase;
5
+ }
6
+ mapUser(user) {
7
+ if (!user)
8
+ return null;
9
+ return {
10
+ id: user.id,
11
+ email: user.email,
12
+ avatarUrl: user.user_metadata?.avatar_url,
13
+ provider: user.app_metadata?.provider,
14
+ metadata: user.user_metadata,
15
+ };
16
+ }
17
+ async getUser() {
18
+ const { data: { user }, error } = await this.supabase.auth.getUser();
19
+ if (error)
20
+ throw error;
21
+ return this.mapUser(user);
22
+ }
23
+ async signInWithEmail(email, password) {
24
+ const { data, error } = await this.supabase.auth.signInWithPassword({
25
+ email,
26
+ password,
27
+ });
28
+ if (error)
29
+ throw error;
30
+ if (!data.user)
31
+ throw new Error("No user returned");
32
+ return this.mapUser(data.user);
33
+ }
34
+ async signInWithGoogle(redirectTo) {
35
+ const { error } = await this.supabase.auth.signInWithOAuth({
36
+ provider: "google",
37
+ options: {
38
+ redirectTo: redirectTo || (typeof window !== "undefined" ? window.location.origin : undefined),
39
+ },
40
+ });
41
+ if (error)
42
+ throw error;
43
+ }
44
+ async signOut() {
45
+ const { error } = await this.supabase.auth.signOut();
46
+ if (error)
47
+ throw error;
48
+ }
49
+ onAuthStateChange(callback) {
50
+ const { data: { subscription } } = this.supabase.auth.onAuthStateChange((_event, session) => {
51
+ callback(this.mapUser(session?.user));
52
+ });
53
+ return () => {
54
+ subscription.unsubscribe();
55
+ };
56
+ }
57
+ async getSessionToken() {
58
+ const { data: { session }, error } = await this.supabase.auth.getSession();
59
+ if (error || !session)
60
+ return null;
61
+ return session.access_token;
62
+ }
63
+ }
64
+ //# sourceMappingURL=SupabaseClient.js.map
@@ -0,0 +1,15 @@
1
+ export interface User {
2
+ id: string;
3
+ email?: string;
4
+ avatarUrl?: string;
5
+ provider?: string;
6
+ metadata?: Record<string, any>;
7
+ }
8
+ export interface AuthClient {
9
+ getUser(): Promise<User | null>;
10
+ signInWithEmail(email: string, password: string): Promise<User>;
11
+ signInWithGoogle(redirectTo?: string): Promise<void>;
12
+ signOut(): Promise<void>;
13
+ onAuthStateChange(callback: (user: User | null) => void): () => void;
14
+ getSessionToken(): Promise<string | null>;
15
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@edcalderon/auth",
3
+ "version": "1.0.0",
4
+ "description": "A universal, provider-agnostic authentication orchestration package with extensible adapters for Supabase, Firebase, Directus, and custom OAuth providers",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "keywords": [
13
+ "auth",
14
+ "authentication",
15
+ "oauth",
16
+ "supabase",
17
+ "firebase",
18
+ "directus",
19
+ "provider-agnostic",
20
+ "react",
21
+ "sso"
22
+ ],
23
+ "author": "Edward",
24
+ "license": "MIT",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/edcalderon/my-second-brain.git",
31
+ "directory": "packages/auth"
32
+ },
33
+ "peerDependencies": {
34
+ "@supabase/supabase-js": "^2.0.0",
35
+ "firebase": "^10.0.0 || ^11.0.0 || ^12.0.0",
36
+ "react": "^18.0.0 || ^19.0.0",
37
+ "react-dom": "^18.0.0 || ^19.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@supabase/supabase-js": {
41
+ "optional": true
42
+ },
43
+ "firebase": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@types/react": "^19.2.7",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ }
54
+ }