@motherbase/client 1.0.1 → 1.1.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/README.md CHANGED
@@ -1,121 +1,167 @@
1
- # @motherbase/client
2
-
3
- Typed, isomorphic client SDK for the Motherbase client. Works in the browser,
4
- Next.js (server & client), and Expo Go / React Native. Built on the global
5
- `fetch` with zero runtime dependencies (`socket.io-client` is an optional peer
6
- dependency, used only for realtime).
7
-
8
- ## Install
9
-
10
- ```bash
11
- npm i @motherbase/client
12
- # optional, only if you use realtime:
13
- npm i socket.io-client
14
- ```
15
-
16
- ## Quickstart
17
-
18
- ```ts
19
- import { createClient, localStorageStorage } from '@motherbase/client';
20
-
21
- const client = createClient('http://localhost:5050', {
22
- storage: localStorageStorage(), // omit for in-memory (SSR-safe) sessions
23
- });
24
-
25
- await client.hydrate(); // restore a persisted session (optional)
26
-
27
- // Auth
28
- await client.auth.register({ email: 'me@example.com', password: 'secret123' });
29
- const session = await client.auth.login({ email: 'me@example.com', password: 'secret123' });
30
- const user = await client.auth.getUser();
31
-
32
- // React to auth changes
33
- const unsubscribe = client.auth.onAuthStateChange((event, session) => {
34
- console.log(event, session?.user.email);
35
- });
36
-
37
- // Storage
38
- const file = await client.storage.upload(myBlob, { visibility: 'public' });
39
- const url = await client.storage.getUrl(file.id);
40
-
41
- // Devices (push)
42
- await client.devices.register({ token: pushToken, platform: 'ios' });
43
-
44
- // REST API (policy-checked /rest/v1)
45
- const products = await client.rest.from<{ id: string; name: string; price: number }>('products').list({
46
- filters: { price: { op: 'lt', value: 50 } },
47
- order: [{ field: 'name' }],
48
- limit: 20,
49
- });
50
- await client.rest.insert('products', { name: 'Tea', price: 12 });
51
-
52
- // Edge functions
53
- const result = await client.edge.invoke<{ ok: boolean }>('hello', { name: 'Ada' });
54
-
55
- // Realtime (requires socket.io-client)
56
- await client.realtime.connect();
57
- client.realtime.on('message', (msg) => console.log(msg));
58
- ```
59
-
60
- The SDK attaches the access token automatically and transparently refreshes it on
61
- a 401 (single-flight: concurrent requests share one refresh). When the refresh
62
- token is invalid it clears the session and emits a `signOut` event.
63
-
64
- ## Storage adapters
65
-
66
- | Runtime | Adapter |
67
- | -------- | ------- |
68
- | Browser | `localStorageStorage()` |
69
- | SSR/Node | `memoryStorage()` (default) |
70
- | Expo/RN | custom `{ get, set }` backed by `AsyncStorage` |
71
-
72
- ```ts
73
- import AsyncStorage from '@react-native-async-storage/async-storage';
74
- import { createClient } from '@motherbase/client';
75
-
76
- const client = createClient(API_URL, {
77
- storage: {
78
- async get() {
79
- const raw = await AsyncStorage.getItem('session');
80
- return raw ? JSON.parse(raw) : null;
81
- },
82
- async set(session) {
83
- if (session) await AsyncStorage.setItem('session', JSON.stringify(session));
84
- else await AsyncStorage.removeItem('session');
85
- },
86
- },
87
- });
88
- ```
89
-
90
- ## API surface
91
-
92
- - `client.auth` — register, login, loginWithEmail, validateEmail, refresh,
93
- logout, forgotPassword, resetPassword, getUser, getSession, onAuthStateChange
94
- - `client.users` — updateUsername, updateImage, deleteAccount
95
- - `client.storage` — upload, presign, getUrl, remove
96
- - `client.devices` — register, unregister
97
- - `client.rest` — typed CRUD over `/rest/v1/:model` with policy checks
98
- - `client.edge` — invoke public edge routes exposed at `/api/edge/:route`
99
- - `client.realtime` — connect, on, off, emit, disconnect
100
-
101
- All methods reject with a `clientError` (`message`, `httpStatus`, `apiStatus`)
102
- on failure.
103
-
104
- More examples live in [`docs/sdk.md`](../../docs/sdk.md). Studio also exposes a
105
- project-specific Connect page with the exact API URL, WebSocket URL and project
106
- ID to paste into `.env`.
107
-
108
- ### REST filters
109
-
110
- `client.rest` accepts the client query operators directly:
111
-
112
- ```ts
113
- await client.rest.from('products').list({
114
- select: ['id', 'name'],
115
- filters: {
116
- price: { op: 'lte', value: 100 },
117
- status: { op: 'in', value: ['active', 'draft'] },
118
- },
119
- order: [{ field: 'createdAt', direction: 'desc' }],
120
- });
121
- ```
1
+ # @motherbase/client
2
+
3
+ A **Supabase-js-compatible** client SDK for the Motherbase self-hosted backend.
4
+ Migrating from Supabase is mostly a package swap — replace
5
+ `@supabase/supabase-js` with `@motherbase/client` and keep the same
6
+ `createClient`, `.from()`, `.auth`, `.storage`, `.channel()` and `.functions`
7
+ shapes.
8
+
9
+ Typed, isomorphic (browser, Next.js server & client, Expo / React Native), built
10
+ on global `fetch` with zero runtime dependencies (`socket.io-client` is an
11
+ optional peer dependency, used only for realtime).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm i @motherbase/client
17
+ # optional, only if you use realtime:
18
+ npm i socket.io-client
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ ```ts
24
+ import { createClient, localStorageStorage } from '@motherbase/client';
25
+
26
+ const client = createClient(
27
+ 'http://localhost:5001',
28
+ 'your-anon-key', // optional; sent as an `apikey` header
29
+ { storage: localStorageStorage() } // omit for in-memory (SSR-safe) sessions
30
+ );
31
+
32
+ await client.hydrate(); // restore a persisted session (optional)
33
+
34
+ // Auth
35
+ await client.auth.signUp({ email: 'me@example.com', password: 'secret123' });
36
+ const { data, error } = await client.auth.signInWithPassword({
37
+ email: 'me@example.com',
38
+ password: 'secret123',
39
+ });
40
+
41
+ // Query builder (awaitable, PostgREST-style)
42
+ const { data: products } = await client
43
+ .from('products')
44
+ .select('id,name,price')
45
+ .lt('price', 50)
46
+ .order('name', { ascending: false })
47
+ .limit(20);
48
+
49
+ // Storage
50
+ const { data: uploaded } = await client.storage.from('avatars').upload('me.png', myBlob, {
51
+ visibility: 'public',
52
+ });
53
+
54
+ // Realtime — postgres_changes, broadcast, presence over one channel (Supabase-shaped)
55
+ const room = client
56
+ .channel('room-1', { config: { broadcast: { self: true }, presence: { key: userId } } })
57
+ .on('postgres_changes', { event: '*', schema: 'public', table: 'messages', filter: 'room_id=eq.42' }, p => {
58
+ console.log(p.eventType, p.new, p.old);
59
+ })
60
+ .on('broadcast', { event: 'cursor' }, ({ payload }) => console.log(payload))
61
+ .on('presence', { event: 'sync' }, () => console.log(room.presenceState()))
62
+ .subscribe();
63
+
64
+ await room.send({ type: 'broadcast', event: 'cursor', payload: { x: 1, y: 2 } });
65
+ await room.track({ online_at: Date.now() });
66
+
67
+ // Edge functions
68
+ const { data: result } = await client.functions.invoke('hello', { body: { name: 'Ada' } });
69
+ ```
70
+
71
+ Every data/auth/storage call resolves to `{ data, error }` and **never throws**
72
+ for expected API errors. `error` is `null` on success, otherwise
73
+ `{ message, status?, code? }`.
74
+
75
+ The SDK attaches the access token automatically and transparently refreshes it on
76
+ a 401 (single-flight: concurrent requests share one refresh). When the refresh
77
+ token is invalid it clears the session and emits `SIGNED_OUT`.
78
+
79
+ ## API surface (cheatsheet)
80
+
81
+ ```ts
82
+ // Data
83
+ client.from(table).select(cols).eq(col, v).in(col, [..]).order(col, { ascending }).limit(n).range(from, to)
84
+ client.from(table).select(cols).single() | .maybeSingle()
85
+ client.from(table).insert(rows) | .update(values).eq(..) | .upsert(rows) | .delete().eq(..)
86
+
87
+ // Auth → { data, error }
88
+ client.auth.signUp({ email, password })
89
+ client.auth.signInWithPassword({ email, password })
90
+ client.auth.signOut()
91
+ client.auth.getUser() // { data: { user } }
92
+ client.auth.getSession() // { data: { session } }
93
+ client.auth.refreshSession()
94
+ client.auth.onAuthStateChange((event, session) => {}) // SIGNED_IN | SIGNED_OUT | TOKEN_REFRESHED
95
+
96
+ // Storage → { data, error }
97
+ client.storage.from(bucket).upload(path, file, { visibility })
98
+ client.storage.from(bucket).download(path)
99
+ client.storage.from(bucket).remove([path])
100
+ client.storage.from(bucket).createSignedUrl(path, expiresIn)
101
+ client.storage.from(bucket).getPublicUrl(path) // sync
102
+
103
+ // Realtime
104
+ client.channel(name, { config: { broadcast: { self, ack }, presence: { key } } })
105
+ .on('postgres_changes', { event, schema, table, filter }, cb)
106
+ .on('broadcast', { event }, cb)
107
+ .on('presence', { event: 'sync' | 'join' | 'leave' }, cb)
108
+ .subscribe(status => {}) // SUBSCRIBED | TIMED_OUT | CLOSED | CHANNEL_ERROR
109
+ channel.send({ type: 'broadcast', event, payload })
110
+ channel.track(state); channel.untrack(); channel.presenceState()
111
+ client.removeChannel(channel)
112
+
113
+ // Edge functions → { data, error }
114
+ client.functions.invoke(name, { body })
115
+ ```
116
+
117
+ ## Storage adapters
118
+
119
+ | Runtime | Adapter |
120
+ | -------- | ------------------------------------------------------------- |
121
+ | Browser | `localStorageStorage()` or `cookieStorage()` |
122
+ | SSR/Node | `memoryStorage()` (default) or `serverCookieStorage(adapter)` |
123
+ | Expo/RN | custom `{ get, set }` backed by `AsyncStorage` |
124
+
125
+ ```ts
126
+ import AsyncStorage from '@react-native-async-storage/async-storage';
127
+ import { createClient } from '@motherbase/client';
128
+
129
+ const client = createClient(API_URL, ANON_KEY, {
130
+ storage: {
131
+ async get() {
132
+ const raw = await AsyncStorage.getItem('session');
133
+ return raw ? JSON.parse(raw) : null;
134
+ },
135
+ async set(session) {
136
+ if (session) await AsyncStorage.setItem('session', JSON.stringify(session));
137
+ else await AsyncStorage.removeItem('session');
138
+ },
139
+ },
140
+ });
141
+ ```
142
+
143
+ ## Compatibility matrix
144
+
145
+ | Area | Status | Notes |
146
+ | ------------------------------------------------------------- | ------------- | ------------------------------------------------------- |
147
+ | `createClient(url, key, options)` | Supported | `key` sent as `apikey` header. |
148
+ | `from().select/insert/update/delete` | Supported | Awaitable `{ data, error }`. |
149
+ | filters `eq/neq/gt/gte/lt/lte/like/ilike/in/is` | Supported | Mapped to the `/rest/v1` PostgREST dialect. |
150
+ | `order/limit/range/single/maybeSingle` | Supported | `range` → limit/offset. |
151
+ | `upsert()` | Partial | Emulated as `insert` (no `onConflict`). |
152
+ | relational embedding (`select('*, author(*)')`) | Not supported | Backend blocks joins. |
153
+ | `.rpc()` | Not supported | No exposed Postgres functions — use `functions.invoke`. |
154
+ | `.or/.not/.filter/.textSearch/.contains` | Not supported | Not in the backend dialect. |
155
+ | `auth.signUp/signInWithPassword/signOut` | Supported | |
156
+ | `auth.getUser/getSession/refreshSession/onAuthStateChange` | Supported | Events: `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`. |
157
+ | OAuth / magic link / OTP | Not supported | Email + password only. |
158
+ | `storage upload/download/remove/createSignedUrl/getPublicUrl` | Partial | `path` == server file id returned by `upload`. |
159
+ | `storage.list()` | Stub | Returns an unsupported error instead of throwing. |
160
+ | `channel().on('postgres_changes').subscribe()` | Supported | INSERT/UPDATE/DELETE/`*`, RLS-filtered, column `filter`. |
161
+ | `channel().on('broadcast')` + `.send()` | Supported | `config.broadcast.self` / `.ack`. |
162
+ | `channel().on('presence')` + `.track/.untrack/.presenceState` | Supported | `sync`/`join`/`leave`, keyed by `config.presence.key`. |
163
+ | `functions.invoke(name, { body })` | Supported | Maps to `/api/edge/:name`. |
164
+
165
+ More examples live in [`docs/sdk.md`](../../docs/sdk.md). Project Studio also
166
+ exposes a project-specific Connect page with the exact API URL, WebSocket URL and
167
+ project ID to paste into `.env`.
package/dist/auth.d.ts CHANGED
@@ -1,61 +1,66 @@
1
1
  import type { HttpClient } from './http.js';
2
2
  import type { SessionStore } from './session.js';
3
- import type { AuthListener, Session, User } from './types.js';
4
- interface RegisterParams {
3
+ import type { AuthChangeListener, MotherbaseResponse, Session, User } from './types.js';
4
+ export interface SignUpCredentials {
5
5
  email: string;
6
6
  password: string;
7
+ /** Optional extra profile field supported by the backend. */
7
8
  username?: string;
8
9
  }
9
- interface LoginParams {
10
+ export interface SignInWithPasswordCredentials {
10
11
  email: string;
11
12
  password: string;
12
13
  }
13
- interface ResetPasswordParams {
14
- email: string;
15
- secretCode: string;
16
- newPassword: string;
17
- }
18
- interface AppleAuthParams {
19
- identityToken: string;
20
- email?: string | null;
21
- fullName?: {
22
- givenName?: string | null;
23
- familyName?: string | null;
24
- } | null;
14
+ export interface Subscription {
15
+ unsubscribe(): void;
25
16
  }
26
- export declare class AuthModule {
17
+ /**
18
+ * Supabase-compatible auth client. Maps supabase-js method names onto the
19
+ * Motherbase auth endpoints and reuses the existing token/refresh/session-store
20
+ * machinery. Returns `{ data, error }` and never throws for expected failures.
21
+ */
22
+ export declare class AuthClient {
27
23
  private readonly http;
28
24
  private readonly store;
29
25
  constructor(http: HttpClient, store: SessionStore);
30
- register(params: RegisterParams): Promise<Session>;
31
- login(params: LoginParams): Promise<Session>;
32
- /** Passwordless email flow: requests a verification code. */
33
- loginWithEmail(params: {
34
- email: string;
35
- }): Promise<unknown>;
36
- validateEmail(params: {
37
- emailVerificationId: string;
38
- secretCode: string;
39
- }): Promise<Session>;
40
- forgotPassword(params: {
41
- email: string;
42
- }): Promise<unknown>;
43
- resetPassword(params: ResetPasswordParams): Promise<unknown>;
44
- loginWithGoogle(params: {
45
- idToken: string;
46
- }): Promise<Session>;
47
- google(idToken: string): Promise<Session>;
48
- loginWithApple(params: AppleAuthParams): Promise<Session>;
49
- apple(params: AppleAuthParams): Promise<Session>;
50
- /** Rotate tokens using the stored refresh token. Returns true on success. */
51
- refresh(): Promise<boolean>;
52
- logout(): Promise<void>;
53
- /** Fetch the current user from the API (requires a valid session). */
54
- getUser(): Promise<User>;
55
- /** The locally-held session, if any. */
56
- getSession(): Session | null;
57
- onAuthStateChange(listener: AuthListener): () => void;
26
+ signUp(credentials: SignUpCredentials): Promise<MotherbaseResponse<{
27
+ user: User | null;
28
+ session: Session | null;
29
+ }>>;
30
+ signInWithPassword(credentials: SignInWithPasswordCredentials): Promise<MotherbaseResponse<{
31
+ user: User | null;
32
+ session: Session | null;
33
+ }>>;
34
+ signOut(): Promise<{
35
+ error: MotherbaseResponse<null>['error'];
36
+ }>;
37
+ /** Fetch the current user from Better Auth (requires a valid session). */
38
+ getUser(): Promise<MotherbaseResponse<{
39
+ user: User | null;
40
+ }>>;
41
+ /** The locally-held session, if any (no network call). */
42
+ getSession(): Promise<MotherbaseResponse<{
43
+ session: Session | null;
44
+ }>>;
45
+ /**
46
+ * Revalidate the current session against the server. Better Auth bearer
47
+ * sessions have no refresh token — if the session is still valid the user is
48
+ * refreshed; otherwise the local session is cleared.
49
+ */
50
+ refreshSession(): Promise<MotherbaseResponse<{
51
+ user: User | null;
52
+ session: Session | null;
53
+ }>>;
54
+ onAuthStateChange(callback: AuthChangeListener): {
55
+ data: {
56
+ subscription: Subscription;
57
+ };
58
+ };
59
+ /**
60
+ * Internal: refresh used by the HttpClient's 401 path. Returns true on success.
61
+ * Not part of the public Supabase surface.
62
+ */
63
+ _refresh(): Promise<boolean>;
58
64
  private persist;
59
65
  }
60
- export {};
61
66
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAE9D,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AACD,UAAU,eAAe;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAC7E;AAeD,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAc,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAxC,IAAI,EAAE,UAAU,EAAmB,KAAK,EAAE,YAAY;IAE7E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IASlD,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IASlD,6DAA6D;IAC7D,cAAc,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrD,aAAa,CAAC,MAAM,EAAE;QAAE,mBAAmB,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IASlG,cAAc,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAI3D,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAItD,eAAe,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IASpE,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAInC,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAS/D,KAAK,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAIhD,6EAA6E;IACvE,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAgB3B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB7B,sEAAsE;IACtE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,wCAAwC;IACxC,UAAU,IAAI,OAAO,GAAG,IAAI;IAI5B,iBAAiB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;YAIvC,OAAO;CAKtB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAEV,kBAAkB,EAElB,kBAAkB,EAClB,OAAO,EACP,IAAI,EACL,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAyBD,MAAM,WAAW,YAAY;IAC3B,WAAW,IAAI,IAAI,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAc,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAxC,IAAI,EAAE,UAAU,EAAmB,KAAK,EAAE,YAAY;IAE7E,MAAM,CACV,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,kBAAkB,CAAC;QAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAexE,kBAAkB,CACtB,WAAW,EAAE,6BAA6B,GACzC,OAAO,CAAC,kBAAkB,CAAC;QAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAexE,OAAO,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IAUtE,0EAA0E;IACpE,OAAO,IAAI,OAAO,CAAC,kBAAkB,CAAC;QAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IASnE,0DAA0D;IACpD,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC;QAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAI5E;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,kBAAkB,CAAC;QAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAoBnG,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG;QAAE,IAAI,EAAE;YAAE,YAAY,EAAE,YAAY,CAAA;SAAE,CAAA;KAAE;IAOzF;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;YAKpB,OAAO;CAKtB"}
package/dist/auth.js CHANGED
@@ -1,110 +1,116 @@
1
- /** Split the flattened auth payload into a clean Session. */
1
+ import { toError } from './http.js';
2
+ import { HTTP_ROUTES } from '@motherbase/contracts';
3
+ /** Build a clean Session from the Better Auth auth payload. */
2
4
  function toSession(payload) {
3
- const { accessToken, refreshToken, userToken: _userToken, ...user } = payload;
4
- return { accessToken, refreshToken, user: user };
5
+ return { accessToken: payload.token, user: payload.user };
5
6
  }
6
- export class AuthModule {
7
+ const EVENT_MAP = {
8
+ signIn: 'SIGNED_IN',
9
+ signOut: 'SIGNED_OUT',
10
+ tokenRefreshed: 'TOKEN_REFRESHED',
11
+ };
12
+ /**
13
+ * Supabase-compatible auth client. Maps supabase-js method names onto the
14
+ * Motherbase auth endpoints and reuses the existing token/refresh/session-store
15
+ * machinery. Returns `{ data, error }` and never throws for expected failures.
16
+ */
17
+ export class AuthClient {
7
18
  constructor(http, store) {
8
19
  this.http = http;
9
20
  this.store = store;
10
21
  }
11
- async register(params) {
12
- const payload = await this.http.request('/api/auth/register', {
13
- method: 'POST',
14
- body: params,
15
- anonymous: true,
16
- });
17
- return this.persist(payload);
18
- }
19
- async login(params) {
20
- const payload = await this.http.request('/api/auth/login', {
21
- method: 'POST',
22
- body: params,
23
- anonymous: true,
24
- });
25
- return this.persist(payload);
26
- }
27
- /** Passwordless email flow: requests a verification code. */
28
- loginWithEmail(params) {
29
- return this.http.request('/api/auth/email', { method: 'PUT', body: params, anonymous: true });
30
- }
31
- async validateEmail(params) {
32
- const payload = await this.http.request('/api/auth/validate-email', {
33
- method: 'PUT',
34
- body: params,
35
- anonymous: true,
36
- });
37
- return this.persist(payload);
38
- }
39
- forgotPassword(params) {
40
- return this.http.request('/api/auth/forgot-password', { method: 'POST', body: params, anonymous: true });
41
- }
42
- resetPassword(params) {
43
- return this.http.request('/api/auth/reset-password', { method: 'POST', body: params, anonymous: true });
44
- }
45
- async loginWithGoogle(params) {
46
- const payload = await this.http.request('/api/auth/google', {
47
- method: 'PUT',
48
- body: params,
49
- anonymous: true,
50
- });
51
- return this.persist(payload);
52
- }
53
- google(idToken) {
54
- return this.loginWithGoogle({ idToken });
55
- }
56
- async loginWithApple(params) {
57
- const payload = await this.http.request('/api/auth/apple', {
58
- method: 'PUT',
59
- body: params,
60
- anonymous: true,
61
- });
62
- return this.persist(payload);
63
- }
64
- apple(params) {
65
- return this.loginWithApple(params);
22
+ async signUp(credentials) {
23
+ try {
24
+ const payload = await this.http.request(HTTP_ROUTES.authSignUp, {
25
+ method: 'POST',
26
+ body: { email: credentials.email, password: credentials.password, name: credentials.username ?? credentials.email },
27
+ anonymous: true,
28
+ raw: true,
29
+ });
30
+ const session = await this.persist(payload);
31
+ return { data: { user: session.user, session }, error: null };
32
+ }
33
+ catch (err) {
34
+ return { data: { user: null, session: null }, error: toError(err) };
35
+ }
66
36
  }
67
- /** Rotate tokens using the stored refresh token. Returns true on success. */
68
- async refresh() {
69
- const session = this.store.get();
70
- if (!session?.refreshToken)
71
- return false;
37
+ async signInWithPassword(credentials) {
72
38
  try {
73
- const tokens = await this.http.request('/api/auth/refresh', { method: 'POST', body: { refreshToken: session.refreshToken }, anonymous: true });
74
- await this.store.setTokens(tokens.accessToken, tokens.refreshToken);
75
- return true;
39
+ const payload = await this.http.request(HTTP_ROUTES.authSignIn, {
40
+ method: 'POST',
41
+ body: { email: credentials.email, password: credentials.password },
42
+ anonymous: true,
43
+ raw: true,
44
+ });
45
+ const session = await this.persist(payload);
46
+ return { data: { user: session.user, session }, error: null };
76
47
  }
77
- catch {
78
- await this.store.set(null, 'signOut');
79
- return false;
48
+ catch (err) {
49
+ return { data: { user: null, session: null }, error: toError(err) };
80
50
  }
81
51
  }
82
- async logout() {
83
- const session = this.store.get();
84
- if (session?.refreshToken) {
85
- try {
86
- await this.http.request('/api/auth/logout', {
87
- method: 'POST',
88
- body: { refreshToken: session.refreshToken },
89
- anonymous: true,
90
- });
91
- }
92
- catch {
93
- // best-effort; clear locally regardless
94
- }
52
+ async signOut() {
53
+ try {
54
+ await this.http.request(HTTP_ROUTES.authSignOut, { method: 'POST', raw: true });
55
+ }
56
+ catch {
57
+ // best-effort; clear locally regardless
95
58
  }
96
59
  await this.store.set(null, 'signOut');
60
+ return { error: null };
97
61
  }
98
- /** Fetch the current user from the API (requires a valid session). */
99
- getUser() {
100
- return this.http.request('/api/auth/');
62
+ /** Fetch the current user from Better Auth (requires a valid session). */
63
+ async getUser() {
64
+ try {
65
+ const payload = await this.http.request(HTTP_ROUTES.authSession, { raw: true });
66
+ return { data: { user: payload?.user ?? null }, error: null };
67
+ }
68
+ catch (err) {
69
+ return { data: { user: null }, error: toError(err) };
70
+ }
101
71
  }
102
- /** The locally-held session, if any. */
103
- getSession() {
104
- return this.store.get();
72
+ /** The locally-held session, if any (no network call). */
73
+ async getSession() {
74
+ return { data: { session: this.store.get() }, error: null };
75
+ }
76
+ /**
77
+ * Revalidate the current session against the server. Better Auth bearer
78
+ * sessions have no refresh token — if the session is still valid the user is
79
+ * refreshed; otherwise the local session is cleared.
80
+ */
81
+ async refreshSession() {
82
+ const current = this.store.get();
83
+ if (!current?.accessToken) {
84
+ return { data: { user: null, session: null }, error: { message: 'Auth session missing!' } };
85
+ }
86
+ try {
87
+ const payload = await this.http.request(HTTP_ROUTES.authSession, { raw: true });
88
+ if (!payload?.user) {
89
+ await this.store.set(null, 'signOut');
90
+ return { data: { user: null, session: null }, error: { message: 'Session expired' } };
91
+ }
92
+ const session = { accessToken: current.accessToken, user: payload.user };
93
+ await this.store.set(session, 'tokenRefreshed');
94
+ return { data: { user: session.user, session }, error: null };
95
+ }
96
+ catch (err) {
97
+ await this.store.set(null, 'signOut');
98
+ return { data: { user: null, session: null }, error: toError(err) };
99
+ }
105
100
  }
106
- onAuthStateChange(listener) {
107
- return this.store.subscribe(listener);
101
+ onAuthStateChange(callback) {
102
+ const unsubscribe = this.store.subscribe((event, session) => {
103
+ callback(EVENT_MAP[event], session);
104
+ });
105
+ return { data: { subscription: { unsubscribe } } };
106
+ }
107
+ /**
108
+ * Internal: refresh used by the HttpClient's 401 path. Returns true on success.
109
+ * Not part of the public Supabase surface.
110
+ */
111
+ async _refresh() {
112
+ const result = await this.refreshSession();
113
+ return result.error === null;
108
114
  }
109
115
  async persist(payload) {
110
116
  const session = toSession(payload);