@donotdev/supabase 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +10 -0
- package/README.md +125 -0
- package/dist/client/auth.d.ts +93 -0
- package/dist/client/auth.d.ts.map +1 -0
- package/dist/client/auth.js +1 -0
- package/dist/client/callableProvider.d.ts +62 -0
- package/dist/client/callableProvider.d.ts.map +1 -0
- package/dist/client/callableProvider.js +1 -0
- package/dist/client/crudAdapter.d.ts +94 -0
- package/dist/client/crudAdapter.d.ts.map +1 -0
- package/dist/client/crudAdapter.js +1 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/storageAdapter.d.ts +26 -0
- package/dist/client/storageAdapter.d.ts.map +1 -0
- package/dist/client/storageAdapter.js +1 -0
- package/dist/fieldMapper.d.ts +27 -0
- package/dist/fieldMapper.d.ts.map +1 -0
- package/dist/fieldMapper.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/server/authAdapter.d.ts +38 -0
- package/dist/server/authAdapter.d.ts.map +1 -0
- package/dist/server/authAdapter.js +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1 -0
- package/package.json +60 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# DoNotDev Framework License Agreement
|
|
2
|
+
|
|
3
|
+
**Version 1.0 – Effective Date: May 5, 2025**
|
|
4
|
+
|
|
5
|
+
Copyright © 2025 Ambroise Park Consulting. All rights reserved.
|
|
6
|
+
|
|
7
|
+
See the full license at [packages/providers/firebase/LICENSE.md](../../providers/firebase/LICENSE.md) or donotdev.com.
|
|
8
|
+
|
|
9
|
+
**DoNotDev Framework License**
|
|
10
|
+
Ambroise Park Consulting – 2025
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @donotdev/supabase
|
|
2
|
+
|
|
3
|
+
Supabase provider for DoNotDev: CRUD, auth, storage, and server auth adapters. Use this package when you want to run a DoNotDev app on Supabase instead of Firebase.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### Environment
|
|
8
|
+
|
|
9
|
+
- **Client:** `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLIC_KEY` (or `VITE_SUPABASE_ANON_KEY` for legacy) — public, safe to ship in client bundle
|
|
10
|
+
- **Server:** same URL, use `SUPABASE_SECRET_KEY` (or `SUPABASE_SERVICE_ROLE_KEY` for legacy) for admin auth (never expose this key to the client)
|
|
11
|
+
|
|
12
|
+
Run `dndev supabase:setup` to configure these interactively.
|
|
13
|
+
|
|
14
|
+
### Tables and storage
|
|
15
|
+
|
|
16
|
+
- **CRUD:** Entity `collection` name is used as the Supabase **table** name (e.g. `products` -> table `products`). Tables must exist and must have an `id` column (text or uuid).
|
|
17
|
+
- **Storage:** Default bucket is `uploads`. Create the bucket in the Supabase dashboard and set public policies as needed.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Client
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createClient } from '@supabase/supabase-js';
|
|
25
|
+
import { configureProviders } from '@donotdev/core';
|
|
26
|
+
import {
|
|
27
|
+
SupabaseCrudAdapter,
|
|
28
|
+
SupabaseStorageAdapter,
|
|
29
|
+
SupabaseAuth,
|
|
30
|
+
} from '@donotdev/supabase';
|
|
31
|
+
|
|
32
|
+
const supabase = createClient(
|
|
33
|
+
import.meta.env.VITE_SUPABASE_URL,
|
|
34
|
+
import.meta.env.VITE_SUPABASE_PUBLIC_KEY || import.meta.env.VITE_SUPABASE_ANON_KEY // New: sb_publishable_..., Legacy: anon key
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
configureProviders({
|
|
38
|
+
crud: new SupabaseCrudAdapter(supabase),
|
|
39
|
+
auth: new SupabaseAuth(supabase),
|
|
40
|
+
storage: new SupabaseStorageAdapter(supabase),
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Server
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import {
|
|
48
|
+
createServerClient,
|
|
49
|
+
SupabaseServerAuthAdapter,
|
|
50
|
+
} from '@donotdev/supabase/server';
|
|
51
|
+
import { SupabaseCrudAdapter } from '@donotdev/supabase';
|
|
52
|
+
|
|
53
|
+
const supabase = createServerClient(
|
|
54
|
+
process.env.SUPABASE_URL!,
|
|
55
|
+
process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY! // New: sb_secret_..., Legacy: service_role
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
configureProviders({
|
|
59
|
+
crud: new SupabaseCrudAdapter(supabase),
|
|
60
|
+
serverAuth: new SupabaseServerAuthAdapter(supabase),
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Exports
|
|
65
|
+
|
|
66
|
+
- **Client (`@donotdev/supabase`):** `SupabaseCrudAdapter`, `SupabaseStorageAdapter`, `SupabaseAuth`
|
|
67
|
+
- **Server (`@donotdev/supabase/server`):** `SupabaseServerAuthAdapter`, `createServerClient`
|
|
68
|
+
|
|
69
|
+
## What Works
|
|
70
|
+
|
|
71
|
+
All core framework features work when you register Supabase providers via `configureProviders()`:
|
|
72
|
+
|
|
73
|
+
| Feature | Status |
|
|
74
|
+
|---------|--------|
|
|
75
|
+
| CRUD (create, read, update, delete) | Full support, schema validation via valibot |
|
|
76
|
+
| Query with filters, ordering, pagination | Full support (all operators) |
|
|
77
|
+
| Real-time subscriptions (document + collection) | Full support via Supabase channels |
|
|
78
|
+
| Auth (email/password, OAuth, magic link) | Full support |
|
|
79
|
+
| OAuth partners (Google, GitHub, Facebook, Apple, etc.) | Full support (14+ providers) |
|
|
80
|
+
| Storage (upload, delete, get URL) | Full support |
|
|
81
|
+
| Server auth (token verification, custom claims) | Full support |
|
|
82
|
+
| Account linking | Supported via `linkIdentity()` |
|
|
83
|
+
|
|
84
|
+
## Known Limitations
|
|
85
|
+
|
|
86
|
+
### Auth Methods
|
|
87
|
+
|
|
88
|
+
| Method | Behavior | Workaround |
|
|
89
|
+
|--------|----------|------------|
|
|
90
|
+
| `deleteAccount()` | Throws `DELETE_ACCOUNT_REQUIRES_SERVER` | Implement via server endpoint using `auth.admin.deleteUser()` |
|
|
91
|
+
| `signInWithGoogleCredential()` | Returns `null` | Use `signInWithPartner('google')` for standard OAuth flow |
|
|
92
|
+
| `reauthenticateWithProvider()` | Triggers full OAuth redirect | Works but navigates away from page (no popup) |
|
|
93
|
+
|
|
94
|
+
### Framework Components (Firebase-Only)
|
|
95
|
+
|
|
96
|
+
These framework features import Firebase SDK directly and **will not work** with Supabase:
|
|
97
|
+
|
|
98
|
+
| Feature | Alternative |
|
|
99
|
+
|---------|------------|
|
|
100
|
+
| `EmailPasswordForm` component | Build custom form using `useAuth()` hook |
|
|
101
|
+
| `useGoogleOneTap` hook | Use `signInWithPartner('google')` |
|
|
102
|
+
| `FirebaseSmartRecovery` | Not needed for Supabase |
|
|
103
|
+
| `FirebaseAccountLinking` | Use `SupabaseAuth.linkWithPartner()` directly |
|
|
104
|
+
| `useStripeBilling` | Implement Stripe via Supabase Edge Functions or API routes |
|
|
105
|
+
|
|
106
|
+
### CLI Commands (Firebase-Only)
|
|
107
|
+
|
|
108
|
+
| Command | Alternative |
|
|
109
|
+
|---------|------------|
|
|
110
|
+
| `dndev emu` | Use `supabase start` (Supabase CLI) for local development |
|
|
111
|
+
| `dndev deploy` | Deploy via Vercel, Netlify, or other hosting |
|
|
112
|
+
| `dndev sync-secrets` (Firebase target) | Use `dndev sync-secrets --target github` for CI/CD secrets |
|
|
113
|
+
|
|
114
|
+
## Future Work
|
|
115
|
+
|
|
116
|
+
These gaps are tracked for future implementation:
|
|
117
|
+
|
|
118
|
+
- **Provider-agnostic EmailPasswordForm** — refactor to use `getProvider('auth')` instead of Firebase SDK
|
|
119
|
+
- **Provider-agnostic useStripeBilling** — abstract callable functions behind provider interface
|
|
120
|
+
- **Google One Tap for Supabase** — potentially implement via `signInWithIdToken()`
|
|
121
|
+
- **deleteAccount server template** — Supabase Edge Function template for user deletion
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
See [LICENSE.md](./LICENSE.md).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AuthProvider, AuthUser, AuthResult, AuthPartnerId, AuthMethod, UnsubscribeFn, UserRole, AuthState, AuthActions, SecurityContext } from '@donotdev/core';
|
|
2
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
/**
|
|
4
|
+
* Supabase Auth implementation of AuthProvider.
|
|
5
|
+
*
|
|
6
|
+
* @version 0.0.1
|
|
7
|
+
* @since 0.0.1
|
|
8
|
+
*/
|
|
9
|
+
export declare class SupabaseAuth implements AuthProvider {
|
|
10
|
+
private readonly client;
|
|
11
|
+
private store;
|
|
12
|
+
private initialized;
|
|
13
|
+
/** Optional security context — injected via setSecurity() */
|
|
14
|
+
private security;
|
|
15
|
+
/**
|
|
16
|
+
* Default lockout tracker — used when no SecurityContext is injected.
|
|
17
|
+
* When `setSecurity()` is called, delegation is to `security.authHardening` which
|
|
18
|
+
* respects the consumer's configured maxAttempts / lockoutMs.
|
|
19
|
+
* Using AuthHardening (not a raw Map) ensures a single lockout implementation.
|
|
20
|
+
*/
|
|
21
|
+
private readonly defaultHardening;
|
|
22
|
+
constructor(client: SupabaseClient);
|
|
23
|
+
/**
|
|
24
|
+
* Set store reference for state updates
|
|
25
|
+
* Called by useAuth on initialization.
|
|
26
|
+
*/
|
|
27
|
+
setStore(store: AuthState & AuthActions): void;
|
|
28
|
+
/**
|
|
29
|
+
* Inject a SecurityContext for audit logging + anomaly detection.
|
|
30
|
+
* Call after construction: `auth.setSecurity(security)`.
|
|
31
|
+
*
|
|
32
|
+
* @version 0.0.1
|
|
33
|
+
* @since 0.0.1
|
|
34
|
+
*/
|
|
35
|
+
setSecurity(security: SecurityContext): void;
|
|
36
|
+
/**
|
|
37
|
+
* Active hardening context — SecurityContext's when injected, otherwise the default instance.
|
|
38
|
+
* Typed as `AuthHardeningContext` (the interface) — do NOT cast to the concrete `AuthHardening`
|
|
39
|
+
* class, as a custom implementation may not return `LockoutResult` from `recordFailure`.
|
|
40
|
+
*/
|
|
41
|
+
private get _hardening();
|
|
42
|
+
/**
|
|
43
|
+
* Throws if email is currently locked out (SOC2 CC6.2).
|
|
44
|
+
* Uses SecurityContext.authHardening when injected (respects consumer config),
|
|
45
|
+
* otherwise the default AuthHardening instance with standard defaults.
|
|
46
|
+
*/
|
|
47
|
+
private _checkLockout;
|
|
48
|
+
private _recordFailure;
|
|
49
|
+
private _recordSuccess;
|
|
50
|
+
initialize(): Promise<void>;
|
|
51
|
+
getCurrentUser(): Promise<AuthUser | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Handle authenticated state change
|
|
54
|
+
* Updates store with user, subscription, and profile.
|
|
55
|
+
*/
|
|
56
|
+
private handleAuthStateChange;
|
|
57
|
+
signInWithEmailAndPassword(email: string, password: string): Promise<AuthUser>;
|
|
58
|
+
createUserWithEmailAndPassword(email: string, password: string): Promise<AuthUser>;
|
|
59
|
+
signOut(): Promise<void>;
|
|
60
|
+
sendPasswordResetEmail(email: string): Promise<void>;
|
|
61
|
+
updateUserProfile(updates: Partial<AuthUser>): Promise<void>;
|
|
62
|
+
getAccessToken(): Promise<string | null>;
|
|
63
|
+
refreshToken(): Promise<string | null>;
|
|
64
|
+
onAuthStateChanged(callback: (user: AuthUser | null) => void): UnsubscribeFn;
|
|
65
|
+
hasRole(role: UserRole): Promise<boolean>;
|
|
66
|
+
hasFeature(featureId: string): Promise<boolean>;
|
|
67
|
+
hasTier(requiredTier: string): Promise<boolean>;
|
|
68
|
+
signInWithEmail(email: string, password: string): Promise<AuthResult | null>;
|
|
69
|
+
createUserWithEmail(email: string, password: string): Promise<AuthResult | null>;
|
|
70
|
+
signInWithPartner(partnerId: AuthPartnerId, _method?: AuthMethod): Promise<AuthResult | null>;
|
|
71
|
+
linkWithPartner(partnerId: AuthPartnerId, _method?: AuthMethod): Promise<AuthResult | null>;
|
|
72
|
+
signInWithGoogleCredential(_credential: string): Promise<AuthResult | null>;
|
|
73
|
+
updatePassword(newPassword: string): Promise<void>;
|
|
74
|
+
sendEmailVerification(): Promise<void>;
|
|
75
|
+
sendSignInLinkToEmail(email: string, actionCodeSettings?: {
|
|
76
|
+
url: string;
|
|
77
|
+
handleCodeInApp: boolean;
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
signInWithEmailLink(_email: string, emailLink?: string): Promise<AuthResult | null>;
|
|
80
|
+
isSignInWithEmailLink(emailLink?: string): boolean;
|
|
81
|
+
getEmailVerificationStatus(): Promise<{
|
|
82
|
+
status: string;
|
|
83
|
+
}>;
|
|
84
|
+
isEmailVerificationEnabled(): Promise<boolean>;
|
|
85
|
+
reauthenticateWithPassword(password: string): Promise<void>;
|
|
86
|
+
reauthenticateWithProvider(partnerId: AuthPartnerId, _method?: AuthMethod): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Delete current user account. Delegates to callable provider (Edge Function) when configured,
|
|
89
|
+
* otherwise throws with an actionable error message.
|
|
90
|
+
*/
|
|
91
|
+
deleteAccount(_password?: string): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/client/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isClient as E,redirectToExternalUrl as P,hasProvider as U,getProvider as k,createDefaultUserProfile as W,SUBSCRIPTION_TIERS as I,hasRoleAccess as v,wrapAuthError as s,DoNotDevError as n}from"@donotdev/core";import{AuthHardening as R}from"@donotdev/core";function o(c){const t=c.user_metadata??{},e=c.app_metadata??{};return{id:c.id,email:c.email??void 0,displayName:t.full_name??t.name??void 0,photoURL:t.avatar_url??void 0,emailVerified:!!c.email_confirmed_at,role:e.role??"guest",customClaims:{...t,...e}}}const y={google:"google",github:"github",facebook:"facebook",apple:"apple",discord:"discord",slack:"slack",microsoft:"microsoft",spotify:"spotify",twitch:"twitch",twitter:"twitter",x:"twitter",linkedin:"linkedin",notion:"notion",medium:"medium",reddit:"reddit",yahoo:"yahoo",youtube:"youtube"};class N{client;store=null;initialized=!1;security=null;defaultHardening=new R;constructor(t){this.client=t}setStore(t){this.store=t}setSecurity(t){this.security=t}get _hardening(){return this.security?.authHardening??this.defaultHardening}_checkLockout(t){try{this._hardening.checkLockout(t)}catch(e){throw s(e instanceof Error?e:new Error(String(e)),"SupabaseAuth","signInWithEmailAndPassword",{email:t})}}_recordFailure(t){this._hardening.recordFailure(t);try{this._hardening.checkLockout(t)}catch{this.security?.audit({type:"auth.locked"})}this.security?.audit({type:"auth.login.failure"})}_recordSuccess(t){this._hardening.recordSuccess(t)}async initialize(){if(!this.initialized)try{const{data:t}=await this.client.auth.getSession();t.session?.user?await this.handleAuthStateChange(t.session.user):this.store?.setUnauthenticated(),this.initialized=!0}catch{this.store?.setUnauthenticated()}}async getCurrentUser(){const{data:{user:t}}=await this.client.auth.getUser();return t?o(t):null}async handleAuthStateChange(t){if(!this.store)return;const e=o(t),r={userId:e.id,tier:e.customClaims?.tier||I.FREE,status:"active",isActive:!0,features:[],subscriptionId:null,customerId:e.id,subscriptionEnd:null,cancelAtPeriodEnd:!1,updatedAt:new Date().toISOString()},a={...W(e.id),email:e.email??void 0,displayName:e.displayName??void 0,photoURL:e.photoURL??void 0,role:e.role};this.store.setAuthenticated(e,r,a)}async signInWithEmailAndPassword(t,e){this._checkLockout(t);try{const{data:{user:r},error:a}=await this.client.auth.signInWithPassword({email:t,password:e});if(a)throw this._recordFailure(t),s(a,"SupabaseAuth","signInWithEmailAndPassword",{email:t});if(!r)throw this._recordFailure(t),s(new Error("Sign in failed"),"SupabaseAuth","signInWithEmailAndPassword",{email:t});return this._recordSuccess(t),this.security?.audit({type:"auth.login.success",userId:r.id}),o(r)}catch(r){throw r instanceof n?r:s(r,"SupabaseAuth","signInWithEmailAndPassword",{email:t})}}async createUserWithEmailAndPassword(t,e){try{const{data:{user:r},error:a}=await this.client.auth.signUp({email:t,password:e});if(a)throw s(a,"SupabaseAuth","createUserWithEmailAndPassword",{email:t});if(!r)throw s(new Error("Sign up failed"),"SupabaseAuth","createUserWithEmailAndPassword",{email:t});if(Array.isArray(r.identities)&&r.identities.length===0)throw s(new Error("An account with this email already exists"),"SupabaseAuth","createUserWithEmailAndPassword",{email:t});return o(r)}catch(r){throw r instanceof n?r:s(r,"SupabaseAuth","createUserWithEmailAndPassword",{email:t})}}async signOut(){let t;try{const{data:{user:e}}=await this.client.auth.getUser();t=e?.id}catch{}await this.client.auth.signOut(),this.store?.setUnauthenticated(),this.security?.audit({type:"auth.logout",userId:t})}async sendPasswordResetEmail(t){this.security&&await this.security.checkRateLimit(`password_reset:${t}`,"write");try{const{error:e}=await this.client.auth.resetPasswordForEmail(t);if(e)throw s(e,"SupabaseAuth","sendPasswordResetEmail",{email:t});this.security?.audit({type:"auth.password.reset"})}catch(e){throw e instanceof n?e:s(e,"SupabaseAuth","sendPasswordResetEmail",{email:t})}}async updateUserProfile(t){try{const{data:{user:e}}=await this.client.auth.getUser();if(!e)throw s(new Error("No current user"),"SupabaseAuth","updateUserProfile");const r=e.user_metadata??{},a={full_name:t.displayName??r.full_name,name:t.displayName??r.name,avatar_url:t.photoURL??r.avatar_url},{error:i}=await this.client.auth.updateUser({data:a});if(i)throw s(i,"SupabaseAuth","updateUserProfile")}catch(e){throw e instanceof n?e:s(e,"SupabaseAuth","updateUserProfile")}}async getAccessToken(){const{data:{session:t}}=await this.client.auth.getSession();return t?.access_token??null}async refreshToken(){const{data:{session:t},error:e}=await this.client.auth.refreshSession();return e?null:t?.access_token??null}onAuthStateChanged(t){const{data:{subscription:e}}=this.client.auth.onAuthStateChange((r,a)=>{const i=a?.user||null;i?this.handleAuthStateChange(i).then(()=>{t(o(i))}).catch(()=>{t(o(i))}):(this.store?.setUnauthenticated(),t(null))});return()=>e.unsubscribe()}async hasRole(t){const e=await this.getCurrentUser();if(!e)return!1;const r=e.role??"guest";return v(r,t)}async hasFeature(t){const e=await this.getCurrentUser();if(!e?.customClaims)return!1;const r=e.customClaims.features;return Array.isArray(r)&&r.includes(t)}async hasTier(t){const e=await this.getCurrentUser();return e?.customClaims?e.customClaims.tier===t:!1}async signInWithEmail(t,e){return{user:await this.signInWithEmailAndPassword(t,e),success:!0}}async createUserWithEmail(t,e){return{user:await this.createUserWithEmailAndPassword(t,e),success:!0,isNewUser:!0}}async signInWithPartner(t,e){try{const r=y[t]??t;if(E()){const{data:u,error:h}=await this.client.auth.signInWithOAuth({provider:r,options:{skipBrowserRedirect:!0,redirectTo:window.location.origin}});if(h)throw s(h,"SupabaseAuth","signInWithPartner",{partnerId:t});if(u?.url){const f=window.screen.width/2-300,_=window.screen.height/2-700/2,p=window.open(u.url,"supabase-auth-popup",`width=600,height=700,left=${f},top=${_},resizable=yes,scrollbars=yes,status=yes`);if(!p)throw new Error("Popup blocked. Please allow popups for this site.");return new Promise(g=>{let m=null;const A=setInterval(async()=>{if(p.closed){clearInterval(A),m?.unsubscribe();const l=await this.getCurrentUser();g(l?{user:l,success:!0}:null)}},500),{data:{subscription:S}}=this.client.auth.onAuthStateChange((l,b)=>{l==="SIGNED_IN"&&b?.user&&(clearInterval(A),S.unsubscribe(),p.close(),g({user:o(b.user),success:!0}))});m=S})}}const{data:a,error:i}=await this.client.auth.signInWithOAuth({provider:r});if(i)throw s(i,"SupabaseAuth","signInWithPartner",{partnerId:t});return a?.url&&await P(a.url,{validateUrl:!0}),null}catch(r){throw r instanceof n?r:s(r,"SupabaseAuth","signInWithPartner",{partnerId:t})}}async linkWithPartner(t,e){try{const r=y[t]??t,{data:a,error:i}=await this.client.auth.linkIdentity({provider:r});if(i)throw s(i,"SupabaseAuth","linkWithPartner",{partnerId:t});const u=a?.user??(await this.client.auth.getUser()).data?.user;return u?{user:o(u),success:!0}:null}catch(r){throw r instanceof n?r:s(r,"SupabaseAuth","linkWithPartner",{partnerId:t})}}async signInWithGoogleCredential(t){return null}async updatePassword(t){try{const{error:e}=await this.client.auth.updateUser({password:t});if(e)throw s(e,"SupabaseAuth","updatePassword")}catch(e){throw e instanceof n?e:s(e,"SupabaseAuth","updatePassword")}}async sendEmailVerification(){try{const{data:{user:t}}=await this.client.auth.getUser();if(!t?.email)return;const{error:e}=await this.client.auth.resend({type:"signup",email:t.email});if(e)throw s(e,"SupabaseAuth","sendEmailVerification")}catch(t){throw t instanceof n?t:s(t,"SupabaseAuth","sendEmailVerification")}}async sendSignInLinkToEmail(t,e){try{const r=e?.url??(typeof window<"u"?window.location.origin:void 0),{error:a}=await this.client.auth.signInWithOtp({email:t,options:r?{emailRedirectTo:r}:void 0});if(a)throw s(a,"SupabaseAuth","sendSignInLinkToEmail",{email:t})}catch(r){throw r instanceof n?r:s(r,"SupabaseAuth","sendSignInLinkToEmail",{email:t})}}async signInWithEmailLink(t,e){if(!e)return null;try{const r=new URL(e),a=r.searchParams.get("token")||r.searchParams.get("token_hash")||"",i=r.searchParams.get("type")||"magiclink";if(!a){const d=r.hash?.slice(1)||"";if(!d)return null;const{data:{user:w},error:f}=await this.client.auth.verifyOtp({token_hash:d,type:"magiclink"});return f?null:w?{user:o(w),success:!0}:null}const{data:{user:u},error:h}=await this.client.auth.verifyOtp({token_hash:a,type:i});return h?null:u?{user:o(u),success:!0}:null}catch{return null}}isSignInWithEmailLink(t){if(!t)return!1;try{const e=new URL(t);return e.searchParams.has("token")||e.searchParams.has("token_hash")||e.searchParams.get("type")==="magiclink"||e.hash.includes("token_hash=")||e.hash.includes("type=magiclink")}catch{return t.includes("token=")||t.includes("token_hash=")||t.includes("type=magiclink")}}async getEmailVerificationStatus(){const{data:{user:t}}=await this.client.auth.getUser();return{status:t?.email_confirmed_at?"verified":"pending"}}async isEmailVerificationEnabled(){return!0}async reauthenticateWithPassword(t){try{const{data:{user:e}}=await this.client.auth.getUser();if(!e?.email)throw s(new Error("No email"),"SupabaseAuth","reauthenticateWithPassword");this._checkLockout(e.email);const{error:r}=await this.client.auth.signInWithPassword({email:e.email,password:t});if(r)throw this._recordFailure(e.email),s(r,"SupabaseAuth","reauthenticateWithPassword");this._recordSuccess(e.email)}catch(e){throw e instanceof n?e:s(e,"SupabaseAuth","reauthenticateWithPassword")}}async reauthenticateWithProvider(t,e){try{const r=y[t]??t,{data:a,error:i}=await this.client.auth.signInWithOAuth({provider:r});if(i)throw s(i,"SupabaseAuth","reauthenticateWithProvider",{partnerId:t});a?.url&&E()&&await P(a.url,{validateUrl:!0})}catch(r){throw r instanceof n?r:s(r,"SupabaseAuth","reauthenticateWithProvider",{partnerId:t})}}async deleteAccount(t){try{if(U("callable")){await k("callable").call("delete-account",{}),await this.client.auth.signOut();return}const e=new Error("Account deletion requires a server endpoint. Configure a callable provider or deploy a delete-account Edge Function.");throw e.code="DELETE_ACCOUNT_REQUIRES_SERVER",s(e,"SupabaseAuth","deleteAccount")}catch(e){throw e instanceof n?e:s(e,"SupabaseAuth","deleteAccount")}}}export{N as SupabaseAuth};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Supabase Callable Provider
|
|
3
|
+
* @description ICallableProvider implementation using Supabase Edge Functions.
|
|
4
|
+
* Auth token is automatically passed by the Supabase client SDK.
|
|
5
|
+
*
|
|
6
|
+
* @version 0.0.1
|
|
7
|
+
* @since 0.5.0
|
|
8
|
+
* @author AMBROISE PARK Consulting
|
|
9
|
+
*/
|
|
10
|
+
import type { ICallableProvider } from '@donotdev/core';
|
|
11
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
/**
|
|
13
|
+
* Invokes Supabase Edge Functions via the client SDK.
|
|
14
|
+
* The Supabase client automatically includes the user's auth token.
|
|
15
|
+
*
|
|
16
|
+
* CRUD operations (get_*, create_*, update_*, delete_*, list_*, listCard_*) are automatically
|
|
17
|
+
* routed to a single `crud` Edge Function with `_functionName` in the body for dispatch.
|
|
18
|
+
*
|
|
19
|
+
* **Important:** If you have custom Edge Functions whose names start with `get_`, `list_`,
|
|
20
|
+
* `create_`, `update_`, `delete_`, or `listCard_`, pass `crudCollections` to restrict
|
|
21
|
+
* auto-routing to only those explicit collection names. Without it, any function matching
|
|
22
|
+
* the prefix pattern is silently rerouted to the `crud` dispatcher.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { configureProviders } from '@donotdev/core';
|
|
27
|
+
* import { SupabaseCallableProvider } from '@donotdev/supabase';
|
|
28
|
+
*
|
|
29
|
+
* // Safe: only 'users' and 'products' collections route through the crud dispatcher
|
|
30
|
+
* configureProviders({
|
|
31
|
+
* callable: new SupabaseCallableProvider(supabaseClient, {
|
|
32
|
+
* crudCollections: ['users', 'products'],
|
|
33
|
+
* }),
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Without crudCollections: any get_*, list_*, etc. is auto-routed (may conflict with custom functions)
|
|
37
|
+
* configureProviders({
|
|
38
|
+
* callable: new SupabaseCallableProvider(supabaseClient),
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @version 0.0.1
|
|
43
|
+
* @since 0.5.0
|
|
44
|
+
*/
|
|
45
|
+
export declare class SupabaseCallableProvider implements ICallableProvider {
|
|
46
|
+
private readonly client;
|
|
47
|
+
private readonly crudFunctionName;
|
|
48
|
+
private readonly crudCollections;
|
|
49
|
+
private static readonly CRUD_OP_REGEX;
|
|
50
|
+
constructor(client: SupabaseClient, options?: {
|
|
51
|
+
crudFunctionName?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Explicit list of collection names that route through crudFunctionName.
|
|
54
|
+
* When provided, only operations for these exact collections are auto-routed.
|
|
55
|
+
* When omitted, any function matching the CRUD naming pattern is auto-routed.
|
|
56
|
+
* Provide this to prevent misrouting of custom functions like get_summary, list_report, etc.
|
|
57
|
+
*/
|
|
58
|
+
crudCollections?: string[];
|
|
59
|
+
});
|
|
60
|
+
call<TReq, TRes>(functionName: string, data: TReq): Promise<TRes>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=callableProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callableProvider.d.ts","sourceRoot":"","sources":["../../src/client/callableProvider.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAM5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,wBAAyB,YAAW,iBAAiB;IAO9D,OAAO,CAAC,QAAQ,CAAC,MAAM;IANzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IAErD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAqD;gBAGvE,MAAM,EAAE,cAAc,EACvC,OAAO,CAAC,EAAE;QACR,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B;;;;;WAKG;QACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5B;IAQG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAwCxE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{wrapCrudError as l,DoNotDevError as u}from"@donotdev/core";class n{client;crudFunctionName;crudCollections;static CRUD_OP_REGEX=/^(get|create|update|delete|list|listCard)_(.+)$/;constructor(t,c){this.client=t,this.crudFunctionName=c?.crudFunctionName??"crud",this.crudCollections=c?.crudCollections?new Set(c.crudCollections):null}async call(t,c){try{const r=n.CRUD_OP_REGEX.exec(t),e=r!==null&&(this.crudCollections===null||this.crudCollections.has(r[2])),i=e?this.crudFunctionName:t,s=e?{_functionName:t,...c}:c,{data:a,error:o}=await this.client.functions.invoke(i,{body:s});if(o)throw l(o instanceof Error?o:new Error(String(o)),"SupabaseCallableProvider","call",t);return a}catch(r){throw r instanceof u?r:l(r instanceof Error?r:new Error(String(r)),"SupabaseCallableProvider","call",t)}}}export{n as SupabaseCallableProvider};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Supabase CRUD Adapter
|
|
3
|
+
* @description ICrudAdapter implementation using Supabase PostgREST. Collection name = table name.
|
|
4
|
+
*
|
|
5
|
+
* **Caching contract:**
|
|
6
|
+
* This adapter is unaware of caching — CrudService owns TanStack Query entirely.
|
|
7
|
+
* The adapter only fetches/writes; CrudService updates GET + list caches after every mutation
|
|
8
|
+
* so the UI never refetches. Default staleTime is Infinity.
|
|
9
|
+
*
|
|
10
|
+
* **Audit fields (`createdById` / `updatedById`):**
|
|
11
|
+
* `add()` injects these from the active Supabase session (stored as created_by_id/updated_by_id in DB).
|
|
12
|
+
* The adapter's field mapper converts app ↔ backend names at the boundary only.
|
|
13
|
+
*
|
|
14
|
+
* **Field mapping (single boundary):**
|
|
15
|
+
* By default (`fieldMapping: 'camelToSnake'`), app field names (camelCase) are converted to/from
|
|
16
|
+
* backend column names (snake_case). Set `fieldMapping: 'identity'` if your entity uses snake_case
|
|
17
|
+
* and DB columns match. No normalization outside this adapter.
|
|
18
|
+
*
|
|
19
|
+
* **Cursor pagination:**
|
|
20
|
+
* `QueryOptions.startAfter` must be the value of the first `orderBy` field from the last row
|
|
21
|
+
* of the previous page — i.e. pass `lastVisible` returned by `query()` directly as `startAfter`.
|
|
22
|
+
*
|
|
23
|
+
* **Multi-tenant subscriptions:**
|
|
24
|
+
* `subscribeToCollection()` translates equality `where` clauses (e.g. scope filters) into
|
|
25
|
+
* Supabase channel-level filters so only relevant tenant rows trigger the callback.
|
|
26
|
+
* Each unique filter set gets its own named channel to prevent cross-tenant notifications.
|
|
27
|
+
* Rapid table changes are debounced (100 ms) so bursts of writes coalesce into one re-fetch.
|
|
28
|
+
*
|
|
29
|
+
* @version 0.0.3
|
|
30
|
+
* @since 0.0.1
|
|
31
|
+
* @author AMBROISE PARK Consulting
|
|
32
|
+
*/
|
|
33
|
+
import type { ICrudAdapter, QueryOptions, PaginatedQueryResult, DocumentSubscriptionCallback, CollectionSubscriptionCallback, dndevSchema } from '@donotdev/core';
|
|
34
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
35
|
+
import type { FieldMappingMode } from '../fieldMapper';
|
|
36
|
+
export declare class SupabaseCrudAdapter implements ICrudAdapter {
|
|
37
|
+
private readonly client;
|
|
38
|
+
private readonly idColumn;
|
|
39
|
+
/**
|
|
40
|
+
* Supabase enforces field-level and row-level security at the Postgres layer via RLS
|
|
41
|
+
* and column-level grants. The framework visibility gate is not needed here.
|
|
42
|
+
*
|
|
43
|
+
* **Required:** RLS must be enabled on every table this adapter accesses.
|
|
44
|
+
* Without RLS, authenticated users can read all rows and columns.
|
|
45
|
+
*
|
|
46
|
+
* ```sql
|
|
47
|
+
* ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
|
48
|
+
* -- Row-level: users see only their own rows
|
|
49
|
+
* CREATE POLICY "read_own" ON your_table FOR SELECT USING (auth.uid() = user_id);
|
|
50
|
+
* -- Field-level: restrict columns per role
|
|
51
|
+
* REVOKE SELECT ON your_table FROM anon;
|
|
52
|
+
* GRANT SELECT (id, public_col) ON your_table TO anon;
|
|
53
|
+
* GRANT SELECT ON your_table TO authenticated;
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
readonly dbLevelSecurity = true;
|
|
57
|
+
private readonly mapper;
|
|
58
|
+
private _roleCache;
|
|
59
|
+
private readonly _unsubscribeAuthState;
|
|
60
|
+
constructor(client: SupabaseClient, idColumn?: string, options?: {
|
|
61
|
+
normalizeColumns?: boolean;
|
|
62
|
+
fieldMapping?: FieldMappingMode;
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Invalidate the role cache immediately.
|
|
66
|
+
* Call this when you know the session has changed (e.g., after sign-out) and cannot
|
|
67
|
+
* wait for the auth state listener to fire.
|
|
68
|
+
*/
|
|
69
|
+
invalidateRoleCache(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Remove the auth state listener. Call when the adapter is no longer needed
|
|
72
|
+
* to prevent memory leaks in long-running server processes.
|
|
73
|
+
*/
|
|
74
|
+
dispose(): void;
|
|
75
|
+
private normalize;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the current user's role from the Supabase JWT app_metadata.
|
|
78
|
+
* Result is cached for 30 seconds to avoid a getSession() call on every read/query.
|
|
79
|
+
* Falls back to 'guest' when no session exists.
|
|
80
|
+
*/
|
|
81
|
+
private getUserRole;
|
|
82
|
+
get<T>(collection: string, id: string, schema?: dndevSchema<unknown>): Promise<T | null>;
|
|
83
|
+
add<T>(collection: string, data: T, schema?: dndevSchema<T>): Promise<{
|
|
84
|
+
id: string;
|
|
85
|
+
data: Record<string, unknown>;
|
|
86
|
+
}>;
|
|
87
|
+
set<T>(collection: string, id: string, data: T, schema?: dndevSchema<T>): Promise<void>;
|
|
88
|
+
update<T>(collection: string, id: string, data: Partial<T>): Promise<void>;
|
|
89
|
+
delete(collection: string, id: string): Promise<void>;
|
|
90
|
+
query<T>(collection: string, options: QueryOptions, schema?: dndevSchema<unknown>, _schemaType?: 'list' | 'listCard'): Promise<PaginatedQueryResult<T>>;
|
|
91
|
+
subscribe?<T>(collection: string, id: string, callback: DocumentSubscriptionCallback<T>, schema?: dndevSchema<unknown>): () => void;
|
|
92
|
+
subscribeToCollection?<T>(collection: string, options: QueryOptions, callback: CollectionSubscriptionCallback<T>, schema?: dndevSchema<unknown>): () => void;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=crudAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crudAdapter.d.ts","sourceRoot":"","sources":["../../src/client/crudAdapter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEZ,oBAAoB,EACpB,4BAA4B,EAC5B,8BAA8B,EAE9B,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAUxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA0HvD,qBAAa,mBAAoB,YAAW,YAAY;IAwBpD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAxB3B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,eAAe,QAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAa;gBAGhC,MAAM,EAAE,cAAc,EACtB,QAAQ,GAAE,MAA0B,EACrD,OAAO,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,gBAAgB,CAAA;KAAE;IAe3E;;;;OAIG;IACH,mBAAmB,IAAI,IAAI;IAI3B;;;OAGG;IACH,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,SAAS;IAIjB;;;;OAIG;YACW,WAAW;IAsBnB,GAAG,CAAC,CAAC,EACT,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,MAAM,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,GAC5B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IA2Bd,GAAG,CAAC,CAAC,EACT,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,CAAC,EACP,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IAgDnD,GAAG,CAAC,CAAC,EACT,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,CAAC,EACP,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,IAAI,CAAC;IAuCV,MAAM,CAAC,CAAC,EACZ,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GACf,OAAO,CAAC,IAAI,CAAC;IAqCV,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCrD,KAAK,CAAC,CAAC,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,EACrB,MAAM,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,EAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,GAChC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC;IAqFnC,SAAS,CAAC,CAAC,CAAC,EACV,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,4BAA4B,CAAC,CAAC,CAAC,EACzC,MAAM,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,GAC5B,MAAM,IAAI;IA6Db,qBAAqB,CAAC,CAAC,CAAC,EACtB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,8BAA8B,CAAC,CAAC,CAAC,EAC3C,MAAM,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,GAC5B,MAAM,IAAI;CAkFd"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{wrapCrudError as c,validateWithSchema as v,DoNotDevError as D,CRUD_OPERATORS as f,ERROR_CODES as U,filterVisibleFields as b}from"@donotdev/core";import{createFieldMapper as q}from"../fieldMapper";const L="id";function R(u){const{createdAt:t,updatedAt:r,created_at:s,updated_at:a,...e}=u;return e}function $(u){if(/[.(),]/.test(u))throw new Error("[crudAdapter] Invalid filter value: contains PostgREST operator characters");return u}function A(u,t){return typeof u=="string"?u:String(u)}function x(u,t,r){const{field:s,operator:a,value:e}=t,n=r(A(s,"where"));switch(a){case f.EQ:return u.eq(n,e);case f.NEQ:return u.neq(n,e);case f.LT:return u.lt(n,e);case f.LTE:return u.lte(n,e);case f.GT:return u.gt(n,e);case f.GTE:return u.gte(n,e);case f.IN:return u.in(n,Array.isArray(e)?e:[e]);case f.NOT_IN:return u.not(n,"in",Array.isArray(e)?e:[e]);case f.ARRAY_CONTAINS:return u.contains(n,e);case f.ARRAY_CONTAINS_ANY:if(!Array.isArray(e)||e.length===0)throw new D("SupabaseCrudAdapter: ARRAY_CONTAINS_ANY requires a non-empty array",U.INVALID_ARGUMENT,{context:{operator:a,field:s}});return u.overlaps(n,e);default:throw new D(`SupabaseCrudAdapter: unsupported operator "${a}"`,U.INVALID_ARGUMENT,{context:{operator:a,field:s}})}}class P{client;idColumn;dbLevelSecurity=!0;mapper;_roleCache=null;_unsubscribeAuthState;constructor(t,r=L,s){this.client=t,this.idColumn=r;const a=s?.fieldMapping??(s?.normalizeColumns===!1?"identity":"camelToSnake");this.mapper=q(a);const{data:{subscription:e}}=this.client.auth.onAuthStateChange(()=>{this._roleCache=null});this._unsubscribeAuthState=()=>e.unsubscribe()}invalidateRoleCache(){this._roleCache=null}dispose(){this._unsubscribeAuthState()}normalize(t){return this.mapper.fromBackendRow(t)}async getUserRole(){const t=Date.now();if(this._roleCache&&t<this._roleCache.expiresAt)return this._roleCache.role;try{const{data:r}=await this.client.auth.getSession(),s=r?.session?.user?.app_metadata;let a="guest";return s&&(s.role==="super"||s.isSuper===!0?a="super":s.role==="admin"||s.isAdmin===!0?a="admin":s.role==="user"?a="user":a=s.role||"guest"),this._roleCache={role:a,expiresAt:t+3e4},a}catch{return"guest"}}async get(t,r,s){const{data:a,error:e}=await this.client.from(t).select("*").eq(this.idColumn,r).maybeSingle();if(e){if(e.code==="PGRST116"||e.message?.includes("No rows"))return null;throw c(e,"SupabaseCrudAdapter","get",t,r)}if(!a)return null;const n=a,o=String(n[this.idColumn]??r),h={...n,id:o},d=this.normalize(h);if(s){const p=await this.getUserRole();return{...b(d,s,p),id:o}}return{...d,id:o}}async add(t,r,s){const a=s?v(s,r,"SupabaseCrudAdapter.add"):r,e=R(a);let n;try{const{data:i}=await this.client.auth.getSession();n=i?.session?.user?.id}catch(i){throw c(i instanceof Error?i:new Error(String(i)),"SupabaseCrudAdapter","add",t)}if(!n)throw c(new Error("Authentication required for this operation"),"SupabaseCrudAdapter","add",t);const o={...e,createdById:n,updatedById:n},h=this.mapper.toBackendKeys(o),{data:d,error:p}=await this.client.from(t).insert(h).select().single();if(p)throw c(p,"SupabaseCrudAdapter","add",t);if(!d)throw c(new Error("Insert returned no data"),"SupabaseCrudAdapter","add",t);const l=String(d[this.idColumn]??""),C=this.normalize({...d,id:l});return{id:l,data:C}}async set(t,r,s,a){const e=a?v(a,s,"SupabaseCrudAdapter.set"):s;let n;try{const{data:d}=await this.client.auth.getSession();n=d?.session?.user?.id}catch(d){throw c(d instanceof Error?d:new Error(String(d)),"SupabaseCrudAdapter","set",t,r)}if(!n)throw c(new Error("Authentication required for this operation"),"SupabaseCrudAdapter","set",t,r);const o=R({...e,[this.idColumn]:r,updatedById:n}),{error:h}=await this.client.from(t).upsert(this.mapper.toBackendKeys(o),{onConflict:this.idColumn});if(h)throw c(h,"SupabaseCrudAdapter","set",t,r)}async update(t,r,s){let a;try{const{data:o}=await this.client.auth.getSession();a=o?.session?.user?.id}catch(o){throw c(o instanceof Error?o:new Error(String(o)),"SupabaseCrudAdapter","update",t,r)}if(!a)throw c(new Error("Authentication required for this operation"),"SupabaseCrudAdapter","update",t,r);const e=R({...s,updatedById:a}),{error:n}=await this.client.from(t).update(this.mapper.toBackendKeys(e)).eq(this.idColumn,r);if(n)throw c(n,"SupabaseCrudAdapter","update",t,r)}async delete(t,r){let s;try{const{data:e}=await this.client.auth.getSession();s=e?.session?.user?.id}catch(e){throw c(e instanceof Error?e:new Error(String(e)),"SupabaseCrudAdapter","delete",t,r)}if(!s)throw c(new Error("Authentication required for this operation"),"SupabaseCrudAdapter","delete",t,r);const{error:a}=await this.client.from(t).delete().eq(this.idColumn,r);if(a)throw c(a,"SupabaseCrudAdapter","delete",t,r)}async query(t,r,s,a){let e=this.client.from(t).select("*",{count:"exact"});const n=m=>this.mapper.toBackendField(m);if(r.where)for(const m of r.where)e=x(e,m,n);const o=r.orderBy?.[0],h=A(o?.field??this.idColumn,"orderBy"),d=n(h),p=o?.direction??"asc";if(r.orderBy&&r.orderBy.length>0)for(const m of r.orderBy){const S=n(A(m.field,"orderBy"));e=e.order(S,{ascending:(m.direction??"asc")==="asc"})}else e=e.order(this.idColumn,{ascending:!0});const l=r.limit??50,C=r.startAfter??null;C&&(p==="asc"?e=e.gt(d,C):e=e.lt(d,C)),e=e.limit(l+1);const i=await e,{data:y,error:w,count:F}=i;if(w)throw c(w instanceof Error?w:new Error(w.message||String(w)),"SupabaseCrudAdapter","query",t);const E=y??[],I=E.length>l,_=I?E.slice(0,l):E,g=_[_.length-1],O=g!=null?String(g[d]??g[this.idColumn]??g.id??""):null,T=s?await this.getUserRole():null,N=[];for(const m of _){const S=String(m[this.idColumn]??m.id??""),z={...m,id:S},B=this.normalize(z),M=s&&T?b(B,s,T):B;N.push({...M,id:S})}return{items:N,total:F??void 0,hasMore:I,lastVisible:O}}subscribe(t,r,s,a){const e=this.client.channel(`doc:${encodeURIComponent(t)}:${encodeURIComponent(r)}`);return e.on("postgres_changes",{event:"*",schema:"public",table:t,filter:`${this.idColumn}=eq.${$(r)}`},n=>{const o=n.new;if(!o){s(null);return}const h={...o,id:o[this.idColumn]??r},d=this.normalize(h);a?this.getUserRole().then(p=>{const l=b(d,a,p);s({...l,id:r})},()=>{const p=b(d,a,"guest");s({...p,id:r})}):s({...d,id:r})}).subscribe(n=>{(n==="CHANNEL_ERROR"||n==="TIMED_OUT")&&s(null,c(new Error(`Subscription error: ${n}`),"SupabaseCrudAdapter","subscribe",t,r))}),()=>{this.client.removeChannel(e)}}subscribeToCollection(t,r,s,a){const e=(r.where??[]).filter(i=>i.operator===f.EQ&&i.value!=null),n=i=>this.mapper.toBackendField(i),o=e.length>0?e.map(i=>`${n(A(i.field,"subscribeToCollection.where"))}=eq.${$(String(i.value))}`).join("&"):void 0,h=e.length>0?e.map(i=>`${n(A(i.field,"subscribeToCollection.where"))}=eq.${encodeURIComponent(String(i.value))}`).join("&"):void 0,d=h?`col:${encodeURIComponent(t)}:${h}`:`col:${encodeURIComponent(t)}`,p=this.client.channel(d);let l=null;const C=()=>{l&&clearTimeout(l),l=setTimeout(()=>{this.query(t,r,a).then(i=>s(i.items),i=>{const y=i instanceof Error?i:new Error(String(i));s([],c(y,"SupabaseCrudAdapter","subscribeToCollection",t))})},100)};return p.on("postgres_changes",{event:"*",schema:"public",table:t,...o?{filter:o}:{}},C).subscribe(i=>{(i==="CHANNEL_ERROR"||i==="TIMED_OUT")&&s([],c(new Error(`Subscription error: ${i}`),"SupabaseCrudAdapter","subscribeToCollection",t))}),()=>{l&&clearTimeout(l),this.client.removeChannel(p)}}}export{P as SupabaseCrudAdapter};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Supabase client exports
|
|
3
|
+
* @description CRUD adapter, storage adapter, and auth provider for browser use.
|
|
4
|
+
*
|
|
5
|
+
* @version 0.0.1
|
|
6
|
+
* @since 0.0.1
|
|
7
|
+
* @author AMBROISE PARK Consulting
|
|
8
|
+
*/
|
|
9
|
+
export { SupabaseCallableProvider } from './callableProvider';
|
|
10
|
+
export { SupabaseCrudAdapter } from './crudAdapter';
|
|
11
|
+
export { SupabaseStorageAdapter } from './storageAdapter';
|
|
12
|
+
export { SupabaseAuth } from './auth';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SupabaseCallableProvider as e}from"./callableProvider";import{SupabaseCrudAdapter as p}from"./crudAdapter";import{SupabaseStorageAdapter as u}from"./storageAdapter";import{SupabaseAuth as S}from"./auth";export{S as SupabaseAuth,e as SupabaseCallableProvider,p as SupabaseCrudAdapter,u as SupabaseStorageAdapter};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Supabase Storage Adapter
|
|
3
|
+
* @description IStorageAdapter implementation using Supabase Storage.
|
|
4
|
+
*
|
|
5
|
+
* @version 0.0.1
|
|
6
|
+
* @since 0.0.1
|
|
7
|
+
* @author AMBROISE PARK Consulting
|
|
8
|
+
*/
|
|
9
|
+
import type { IStorageAdapter, UploadOptions, UploadResult } from '@donotdev/core';
|
|
10
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
11
|
+
/**
|
|
12
|
+
* Supabase Storage adapter implementing IStorageAdapter.
|
|
13
|
+
*
|
|
14
|
+
* @version 0.0.1
|
|
15
|
+
* @since 0.0.1
|
|
16
|
+
*/
|
|
17
|
+
export declare class SupabaseStorageAdapter implements IStorageAdapter {
|
|
18
|
+
private readonly client;
|
|
19
|
+
private readonly bucket;
|
|
20
|
+
constructor(client: SupabaseClient, bucket?: string);
|
|
21
|
+
upload(file: File | Blob, options?: UploadOptions): Promise<UploadResult>;
|
|
22
|
+
delete(urlOrPath: string): Promise<void>;
|
|
23
|
+
getUrl(path: string): Promise<string>;
|
|
24
|
+
private pathFromUrl;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=storageAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storageAdapter.d.ts","sourceRoot":"","sources":["../../src/client/storageAdapter.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACb,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D;;;;;GAKG;AACH,qBAAa,sBAAuB,YAAW,eAAe;IAE1D,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,cAAc,EACtB,MAAM,GAAE,MAAuB;IAG5C,MAAM,CACV,IAAI,EAAE,IAAI,GAAG,IAAI,EACjB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC;IAiClB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAO3C,OAAO,CAAC,WAAW;CA8BpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{wrapStorageError as r}from"@donotdev/core";const i="uploads";class d{client;bucket;constructor(e,t=i){this.client=e,this.bucket=t}async upload(e,t){try{t?.onProgress?.(0);const a=t?.storagePath??"",h=e instanceof File&&e.name?.includes(".")?e.name.slice(e.name.lastIndexOf(".")):"",s=t?.filename??`${Date.now()}-${Math.random().toString(36).slice(2,10)}${h}`,o=a?`${a}/${s}`:s,{data:n,error:c}=await this.client.storage.from(this.bucket).upload(o,e,{upsert:!0});if(c)throw r(c,"SupabaseStorageAdapter","upload",o);if(!n)throw r(new Error("Upload returned no data"),"SupabaseStorageAdapter","upload",o);t?.onProgress?.(100);const{data:{publicUrl:p}}=this.client.storage.from(this.bucket).getPublicUrl(n.path);return{url:p,path:n.path}}catch(a){throw r(a,"SupabaseStorageAdapter","upload",t?.storagePath||"")}}async delete(e){try{const t=e.startsWith("http")?this.pathFromUrl(e):e,{error:a}=await this.client.storage.from(this.bucket).remove([t]);if(a)throw r(a,"SupabaseStorageAdapter","delete",t)}catch(t){throw r(t,"SupabaseStorageAdapter","delete",e)}}async getUrl(e){const{data:{publicUrl:t}}=this.client.storage.from(this.bucket).getPublicUrl(e);return t}pathFromUrl(e){try{const t=new URL(e);let a=t.pathname.match(/\/storage\/v1\/object\/public\/[^/]+\/(.+)/);return a&&a[1]!=null||(a=t.pathname.match(/\/storage\/v1\/object\/sign\/[^/]+\/(.+)/),a&&a[1]!=null)?decodeURIComponent(a[1]):t.pathname&&t.pathname.length>1&&!t.pathname.startsWith("/api/")?decodeURIComponent(t.pathname.slice(1)):e}catch{return e}}}export{d as SupabaseStorageAdapter};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Supabase field name mapper — single boundary for app ↔ backend (Postgres) names.
|
|
3
|
+
* @description Entity field names (app) are translated to/from backend column names here only.
|
|
4
|
+
* Used by SupabaseCrudAdapter (client) and Supabase Edge Function CRUD handlers.
|
|
5
|
+
*
|
|
6
|
+
* Default: camelCase (app) ↔ snake_case (backend). Option: identity for entities using snake_case.
|
|
7
|
+
*
|
|
8
|
+
* @version 0.0.1
|
|
9
|
+
* @since 0.0.1
|
|
10
|
+
*/
|
|
11
|
+
export type FieldMappingMode = 'camelToSnake' | 'identity';
|
|
12
|
+
export interface SupabaseFieldMapper {
|
|
13
|
+
/** App field name → backend column name */
|
|
14
|
+
toBackendField(appFieldName: string): string;
|
|
15
|
+
/** App-shaped object → backend-shaped object (all keys mapped) */
|
|
16
|
+
toBackendKeys(obj: Record<string, unknown>): Record<string, unknown>;
|
|
17
|
+
/** Backend row → app-shaped object (keys mapped, timestamps to ISO) */
|
|
18
|
+
fromBackendRow(row: Record<string, unknown>): Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create the single boundary mapper for Supabase/Postgres.
|
|
22
|
+
* @param fieldMapping - 'camelToSnake' (default) or 'identity' when entity uses snake_case
|
|
23
|
+
*/
|
|
24
|
+
export declare function createFieldMapper(fieldMapping?: FieldMappingMode): SupabaseFieldMapper;
|
|
25
|
+
/** Default mapper (camelToSnake) for use when no adapter option is set */
|
|
26
|
+
export declare const defaultFieldMapper: SupabaseFieldMapper;
|
|
27
|
+
//# sourceMappingURL=fieldMapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldMapper.d.ts","sourceRoot":"","sources":["../src/fieldMapper.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,UAAU,CAAC;AA0B3D,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7C,kEAAkE;IAClE,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrE,uEAAuE;IACvE,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,GAAE,gBAAiC,GAC9C,mBAAmB,CA8BrB;AAED,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,qBAAoC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function s(e){return typeof e!="string"?String(e):e.replace(/[A-Z]/g,i=>`_${i.toLowerCase()}`)}function a(e){return typeof e!="string"?String(e):e.replace(/_([a-z])/g,(i,c)=>c.toUpperCase())}const f=new Set(["createdAt","updatedAt"]);function u(e){if(e instanceof Date)return e.toISOString();if(typeof e=="string"){if(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(e))return e;if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(e))return new Date(e.replace(" ","T")).toISOString()}return e}function y(e="camelToSnake"){return{toBackendField:t=>{if(typeof t!="string")throw new TypeError(`Supabase field mapper: field name must be a string (e.g. "createdById"), got ${typeof t}. Check where query options (orderBy/where) are built \u2014 ensure "field" is the field name string, not a schema or other value.`);return e==="identity"?t:s(t)},toBackendKeys:t=>{if(e==="identity")return t;const n={};for(const[r,o]of Object.entries(t))n[s(r)]=o;return n},fromBackendRow:t=>{const n={};for(const[r,o]of Object.entries(t)){const d=e==="identity"?r:a(r);n[d]=f.has(d)?u(o):o}return n}}}const S=y("camelToSnake");export{y as createFieldMapper,S as defaultFieldMapper};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @donotdev/supabase — Supabase provider for DoNotDev
|
|
3
|
+
* Client-only exports: adapters and client init.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
export * from './client';
|
|
8
|
+
export { createFieldMapper, defaultFieldMapper, type FieldMappingMode, type SupabaseFieldMapper, } from './fieldMapper';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,cAAc,UAAU,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export*from"./client";import{createFieldMapper as a,defaultFieldMapper as o}from"./fieldMapper";export{a as createFieldMapper,o as defaultFieldMapper};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Supabase Server Auth Adapter
|
|
3
|
+
* @description IServerAuthAdapter implementation using Supabase Auth Admin API.
|
|
4
|
+
* Requires a Supabase client created with the secret key (sb_secret_...) or service_role key (legacy).
|
|
5
|
+
*
|
|
6
|
+
* @version 0.0.1
|
|
7
|
+
* @since 0.0.1
|
|
8
|
+
* @author AMBROISE PARK Consulting
|
|
9
|
+
*/
|
|
10
|
+
import type { IServerAuthAdapter, VerifiedToken, ServerUserRecord } from '@donotdev/core';
|
|
11
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
/**
|
|
13
|
+
* Create a Supabase client with the secret key (sb_secret_...) or service_role key (legacy) for server-side auth admin.
|
|
14
|
+
* Use this only on the server (Node.js); never expose the secret key to the client.
|
|
15
|
+
*
|
|
16
|
+
* @param url - Supabase project URL
|
|
17
|
+
* @param secretKey - Secret key (sb_secret_...) or service_role key (legacy)
|
|
18
|
+
*
|
|
19
|
+
* @version 0.0.1
|
|
20
|
+
* @since 0.0.1
|
|
21
|
+
*/
|
|
22
|
+
export declare function createServerClient(url: string, secretKey: string): SupabaseClient;
|
|
23
|
+
/**
|
|
24
|
+
* Supabase server auth adapter implementing IServerAuthAdapter.
|
|
25
|
+
* Pass a Supabase client created with createServerClient(url, secretKey).
|
|
26
|
+
* Secret key can be SUPABASE_SECRET_KEY (sb_secret_...) or SUPABASE_SERVICE_ROLE_KEY (legacy).
|
|
27
|
+
*
|
|
28
|
+
* @version 0.0.1
|
|
29
|
+
* @since 0.0.1
|
|
30
|
+
*/
|
|
31
|
+
export declare class SupabaseServerAuthAdapter implements IServerAuthAdapter {
|
|
32
|
+
private readonly client;
|
|
33
|
+
constructor(client: SupabaseClient);
|
|
34
|
+
verifyToken(token: string): Promise<VerifiedToken>;
|
|
35
|
+
getUser(uid: string): Promise<ServerUserRecord | null>;
|
|
36
|
+
setCustomClaims(uid: string, claims: Record<string, unknown>): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=authAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authAdapter.d.ts","sourceRoot":"","sources":["../../src/server/authAdapter.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAO5D;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,cAAc,CAOhB;AAMD;;;;;;;GAOG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,cAAc;IAE7C,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAmClD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IA0BtD,eAAe,CACnB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC;CAwBjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{wrapAuthError as s,DoNotDevError as i}from"@donotdev/core";import{createClient as u}from"@supabase/supabase-js";function d(n,t){return u(n,t,{auth:{autoRefreshToken:!1,persistSession:!1}})}class p{client;constructor(t){this.client=t}async verifyToken(t){try{const{data:{user:r},error:e}=await this.client.auth.getUser(t);if(e){const a=e instanceof Error?e:new Error(e.message||e.error_description||String(e));throw s(a,"SupabaseServerAuthAdapter","verifyToken")}if(!r)throw s(new Error("Invalid token"),"SupabaseServerAuthAdapter","verifyToken");return{uid:r.id,email:r.email??void 0,claims:{...r.user_metadata,...r.app_metadata}}}catch(r){throw r instanceof i?r:s(r,"SupabaseServerAuthAdapter","verifyToken")}}async getUser(t){const{data:{user:r},error:e}=await this.client.auth.admin.getUserById(t);if(e||!r)return null;const a=r.user_metadata??{},o=typeof a.full_name=="string"?a.full_name:typeof a.name=="string"?a.name:void 0;return{uid:r.id,email:r.email??void 0,displayName:o,customClaims:{...r.user_metadata,...r.app_metadata}}}async setCustomClaims(t,r){try{const{error:e}=await this.client.auth.admin.updateUserById(t,{app_metadata:r});if(e){const a=e instanceof Error?e:new Error(e.message||e.error_description||String(e));throw s(a,"SupabaseServerAuthAdapter","setCustomClaims",{uid:t})}}catch(e){throw e instanceof i?e:s(e,"SupabaseServerAuthAdapter","setCustomClaims",{uid:t})}}}export{p as SupabaseServerAuthAdapter,d as createServerClient};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SupabaseServerAuthAdapter as t,createServerClient as a}from"./authAdapter";export{t as SupabaseServerAuthAdapter,a as createServerClient};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@donotdev/supabase",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
7
|
+
"description": "Supabase provider for DoNotDev — CRUD, auth, storage, server auth adapters",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./server": {
|
|
17
|
+
"types": "./dist/server/index.d.ts",
|
|
18
|
+
"import": "./dist/server/index.js",
|
|
19
|
+
"default": "./dist/server/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsc --noEmit --watch --listFiles false --listEmittedFiles false",
|
|
24
|
+
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
25
|
+
"type-check": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"valibot": "^1.2.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@donotdev/core": "^0.0.24",
|
|
34
|
+
"@supabase/supabase-js": "^2.45.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"package.json",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE.md"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/donotdev/dndev.git"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"donotdev",
|
|
49
|
+
"dndev",
|
|
50
|
+
"supabase",
|
|
51
|
+
"auth",
|
|
52
|
+
"storage",
|
|
53
|
+
"react",
|
|
54
|
+
"typescript"
|
|
55
|
+
],
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"registry": "https://registry.npmjs.org",
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|