@motherbase/client 1.4.0 → 1.5.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.
Files changed (90) hide show
  1. package/README.md +379 -284
  2. package/package.json +1 -1
  3. package/dist/admin.d.ts +0 -81
  4. package/dist/admin.d.ts.map +0 -1
  5. package/dist/admin.js +0 -85
  6. package/dist/admin.js.map +0 -1
  7. package/dist/auth.d.ts +0 -362
  8. package/dist/auth.d.ts.map +0 -1
  9. package/dist/auth.js +0 -721
  10. package/dist/auth.js.map +0 -1
  11. package/dist/channel.d.ts +0 -122
  12. package/dist/channel.d.ts.map +0 -1
  13. package/dist/channel.js +0 -213
  14. package/dist/channel.js.map +0 -1
  15. package/dist/compat.d.ts +0 -40
  16. package/dist/compat.d.ts.map +0 -1
  17. package/dist/compat.js +0 -98
  18. package/dist/compat.js.map +0 -1
  19. package/dist/cookie.d.ts +0 -68
  20. package/dist/cookie.d.ts.map +0 -1
  21. package/dist/cookie.js +0 -152
  22. package/dist/cookie.js.map +0 -1
  23. package/dist/database.d.ts +0 -188
  24. package/dist/database.d.ts.map +0 -1
  25. package/dist/database.js +0 -2
  26. package/dist/database.js.map +0 -1
  27. package/dist/devices.d.ts +0 -13
  28. package/dist/devices.d.ts.map +0 -1
  29. package/dist/devices.js +0 -13
  30. package/dist/devices.js.map +0 -1
  31. package/dist/edge.d.ts +0 -7
  32. package/dist/edge.d.ts.map +0 -1
  33. package/dist/edge.js +0 -13
  34. package/dist/edge.js.map +0 -1
  35. package/dist/functions.d.ts +0 -60
  36. package/dist/functions.d.ts.map +0 -1
  37. package/dist/functions.js +0 -75
  38. package/dist/functions.js.map +0 -1
  39. package/dist/http.d.ts +0 -123
  40. package/dist/http.d.ts.map +0 -1
  41. package/dist/http.js +0 -205
  42. package/dist/http.js.map +0 -1
  43. package/dist/index.d.ts +0 -185
  44. package/dist/index.d.ts.map +0 -1
  45. package/dist/index.js +0 -331
  46. package/dist/index.js.map +0 -1
  47. package/dist/mfa.d.ts +0 -109
  48. package/dist/mfa.d.ts.map +0 -1
  49. package/dist/mfa.js +0 -134
  50. package/dist/mfa.js.map +0 -1
  51. package/dist/postgrest.d.ts +0 -326
  52. package/dist/postgrest.d.ts.map +0 -1
  53. package/dist/postgrest.js +0 -629
  54. package/dist/postgrest.js.map +0 -1
  55. package/dist/profile.d.ts +0 -16
  56. package/dist/profile.d.ts.map +0 -1
  57. package/dist/profile.js +0 -26
  58. package/dist/profile.js.map +0 -1
  59. package/dist/query.d.ts +0 -97
  60. package/dist/query.d.ts.map +0 -1
  61. package/dist/query.js +0 -94
  62. package/dist/query.js.map +0 -1
  63. package/dist/realtime.d.ts +0 -114
  64. package/dist/realtime.d.ts.map +0 -1
  65. package/dist/realtime.js +0 -372
  66. package/dist/realtime.js.map +0 -1
  67. package/dist/rest.d.ts +0 -50
  68. package/dist/rest.d.ts.map +0 -1
  69. package/dist/rest.js +0 -75
  70. package/dist/rest.js.map +0 -1
  71. package/dist/session.d.ts +0 -47
  72. package/dist/session.d.ts.map +0 -1
  73. package/dist/session.js +0 -136
  74. package/dist/session.js.map +0 -1
  75. package/dist/storage.d.ts +0 -239
  76. package/dist/storage.d.ts.map +0 -1
  77. package/dist/storage.js +0 -391
  78. package/dist/storage.js.map +0 -1
  79. package/dist/testing.d.ts +0 -38
  80. package/dist/testing.d.ts.map +0 -1
  81. package/dist/testing.js +0 -50
  82. package/dist/testing.js.map +0 -1
  83. package/dist/types.d.ts +0 -343
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js +0 -2
  86. package/dist/types.js.map +0 -1
  87. package/dist/warn.d.ts +0 -4
  88. package/dist/warn.d.ts.map +0 -1
  89. package/dist/warn.js +0 -27
  90. package/dist/warn.js.map +0 -1
package/dist/auth.js DELETED
@@ -1,721 +0,0 @@
1
- import { BackendError, toError } from './http.js';
2
- import { normalizeUser } from './compat.js';
3
- import { MfaClient } from './mfa.js';
4
- import { AuthAdminClient } from './admin.js';
5
- import { HTTP_ROUTES } from '@motherbase/contracts';
6
- function asIsoString(value) {
7
- if (typeof value === 'string')
8
- return value;
9
- if (value instanceof Date)
10
- return value.toISOString();
11
- return null;
12
- }
13
- /**
14
- * Read a JWT's payload without verifying it. Base64url, so `-`/`_` are mapped
15
- * back and the padding is restored before decoding. Returns null for anything
16
- * that is not a three-part token with a JSON object payload.
17
- */
18
- function decodeJwtPayload(token) {
19
- const parts = token.split('.');
20
- if (parts.length !== 3)
21
- return null;
22
- try {
23
- const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
24
- const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
25
- // `atob` is global in every runtime this SDK supports (browsers, Node 16+,
26
- // Deno, workers). Its output is latin1, so the percent-escape round trip is
27
- // what turns a multi-byte claim value back into correct UTF-8.
28
- const json = decodeURIComponent(Array.from(atob(padded), c => `%${c.charCodeAt(0).toString(16).padStart(2, '0')}`).join(''));
29
- const parsed = JSON.parse(json);
30
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
31
- }
32
- catch {
33
- return null;
34
- }
35
- }
36
- /** Build a clean Session from the Better Auth auth payload. */
37
- function toSession(payload) {
38
- return { accessToken: payload.token, user: payload.user };
39
- }
40
- /**
41
- * Read the token an OAuth callback left in the URL fragment and return the URL
42
- * with it removed, so a reload or a shared link cannot replay it.
43
- *
44
- * The provider callback cannot answer the SDK with a JSON body: it is a browser
45
- * redirect. The backend therefore appends `#access_token=…` to the redirect it
46
- * issues, the same implicit flow supabase-js reads. Exported for testing.
47
- */
48
- export function readAccessTokenFromUrl(href) {
49
- const hash = href.indexOf('#');
50
- if (hash === -1)
51
- return null;
52
- const params = new URLSearchParams(href.slice(hash + 1));
53
- const accessToken = params.get('access_token');
54
- if (!accessToken)
55
- return null;
56
- params.delete('access_token');
57
- params.delete('token_type');
58
- const rest = params.toString();
59
- return { accessToken, cleanedHref: `${href.slice(0, hash)}${rest ? `#${rest}` : ''}` };
60
- }
61
- const EVENT_MAP = {
62
- signIn: 'SIGNED_IN',
63
- signOut: 'SIGNED_OUT',
64
- tokenRefreshed: 'TOKEN_REFRESHED',
65
- userUpdated: 'USER_UPDATED',
66
- mfaVerified: 'MFA_CHALLENGE_VERIFIED',
67
- };
68
- /**
69
- * Supabase-compatible auth client. Maps supabase-js method names onto the
70
- * Motherbase auth endpoints and reuses the existing token/refresh/session-store
71
- * machinery. Returns `{ data, error }` and never throws for expected failures.
72
- */
73
- export class AuthClient {
74
- constructor(http, store) {
75
- this.http = http;
76
- this.store = store;
77
- this.autoRefreshTimer = null;
78
- // A successful verify can return a stepped-up session, so mfa re-runs the
79
- // session read through the same path every other method uses.
80
- this.mfa = new MfaClient(http, async () => {
81
- await this.refreshSession();
82
- });
83
- this.admin = new AuthAdminClient(http);
84
- }
85
- async signUp(credentials) {
86
- try {
87
- const payload = await this.http.request(HTTP_ROUTES.authSignUp, {
88
- method: 'POST',
89
- body: { email: credentials.email, password: credentials.password, name: credentials.username ?? credentials.email },
90
- anonymous: true,
91
- raw: true,
92
- });
93
- const session = await this.persist(payload);
94
- return { data: { user: session.user, session }, error: null };
95
- }
96
- catch (err) {
97
- return { data: { user: null, session: null }, error: toError(err) };
98
- }
99
- }
100
- async signInWithPassword(credentials) {
101
- try {
102
- const payload = await this.http.request(HTTP_ROUTES.authSignIn, {
103
- method: 'POST',
104
- body: { email: credentials.email, password: credentials.password },
105
- anonymous: true,
106
- raw: true,
107
- });
108
- const session = await this.persist(payload);
109
- return { data: { user: session.user, session }, error: null };
110
- }
111
- catch (err) {
112
- return { data: { user: null, session: null }, error: toError(err) };
113
- }
114
- }
115
- /**
116
- * Request a password-reset OTP by email. Always resolves successfully
117
- * (whether or not the email is registered) to avoid leaking account
118
- * existence; check `error` only for transport/server failures.
119
- */
120
- async resetPasswordForEmail(email) {
121
- try {
122
- await this.http.request(HTTP_ROUTES.authRequestPasswordReset, {
123
- method: 'POST',
124
- body: { email },
125
- anonymous: true,
126
- raw: true,
127
- });
128
- return { error: null };
129
- }
130
- catch (err) {
131
- return { error: toError(err) };
132
- }
133
- }
134
- /** Complete a password reset using the OTP sent to the user's email. */
135
- async resetPassword(args) {
136
- try {
137
- await this.http.request(HTTP_ROUTES.authResetPassword, {
138
- method: 'POST',
139
- body: { email: args.email, otp: args.otp, password: args.password },
140
- anonymous: true,
141
- raw: true,
142
- });
143
- return { error: null };
144
- }
145
- catch (err) {
146
- return { error: toError(err) };
147
- }
148
- }
149
- /**
150
- * Send a one-time sign-in code, by email or by SMS. Pair with `verifyOtp` to
151
- * complete the sign-in.
152
- *
153
- * `{ phone }` routes to the phone-number plugin, which the project has to
154
- * have enabled with an SMS sender configured; without one the call resolves
155
- * with the backend's error rather than pretending a code was sent.
156
- */
157
- async signInWithOtp(credentials) {
158
- if (!credentials.email && !credentials.phone) {
159
- return { data: {}, error: { message: 'signInWithOtp needs either `email` or `phone`.' } };
160
- }
161
- try {
162
- if (credentials.phone) {
163
- await this.http.request(HTTP_ROUTES.authSendPhoneOtp, {
164
- method: 'POST',
165
- body: { phoneNumber: credentials.phone },
166
- anonymous: true,
167
- raw: true,
168
- });
169
- return { data: {}, error: null };
170
- }
171
- await this.http.request(HTTP_ROUTES.authSendOtp, {
172
- method: 'POST',
173
- body: { email: credentials.email, type: 'sign-in' },
174
- anonymous: true,
175
- raw: true,
176
- });
177
- return { data: {}, error: null };
178
- }
179
- catch (err) {
180
- return { data: {}, error: toError(err) };
181
- }
182
- }
183
- /**
184
- * Send the one-time code again, supabase-js style.
185
- *
186
- * supabase-js names the flows `signup` (confirm a new address), `email_change`
187
- * and `recovery`; Better Auth names them `email-verification` and
188
- * `forget-password`. The mapping lives here so ported code keeps working.
189
- * `sms` and `phone_change` re-send an SMS code through the phone-number flow.
190
- */
191
- async resend(params) {
192
- if (params.type === 'sms' || params.type === 'phone_change') {
193
- if (!params.phone)
194
- return { data: {}, error: { message: '`phone` is required.' } };
195
- return this.signInWithOtp({ phone: params.phone });
196
- }
197
- const OTP_TYPE = {
198
- signup: 'email-verification',
199
- email_change: 'email-verification',
200
- recovery: 'forget-password',
201
- };
202
- const type = OTP_TYPE[params.type];
203
- if (!type) {
204
- return { data: {}, error: { message: `resend({ type: '${params.type}' }) is not supported.` } };
205
- }
206
- if (!params.email) {
207
- return { data: {}, error: { message: '`email` is required.' } };
208
- }
209
- try {
210
- await this.http.request(HTTP_ROUTES.authSendOtp, {
211
- method: 'POST',
212
- body: { email: params.email, type },
213
- anonymous: true,
214
- raw: true,
215
- });
216
- return { data: {}, error: null };
217
- }
218
- catch (err) {
219
- return { data: {}, error: toError(err) };
220
- }
221
- }
222
- /**
223
- * Complete a `signInWithOtp` sign-in with the code that was sent.
224
- *
225
- * The channel follows the identifier, as in supabase-js: `{ phone }` verifies
226
- * an SMS code, `{ email }` an email one. `type` is accepted for signature
227
- * parity and only disambiguates when both are somehow present.
228
- */
229
- async verifyOtp(params) {
230
- if (!params.email && !params.phone) {
231
- return { data: { user: null, session: null }, error: { message: 'verifyOtp needs either `email` or `phone`.' } };
232
- }
233
- try {
234
- const bySms = Boolean(params.phone) && params.type !== 'email';
235
- const payload = await this.http.request(bySms ? HTTP_ROUTES.authVerifyPhoneOtp : HTTP_ROUTES.authSignInEmailOtp, {
236
- method: 'POST',
237
- body: bySms
238
- ? { phoneNumber: params.phone, code: params.token }
239
- : { email: params.email, otp: params.token },
240
- anonymous: true,
241
- raw: true,
242
- });
243
- if (!payload?.token) {
244
- return { data: { user: null, session: null }, error: { message: 'The code did not produce a session.' } };
245
- }
246
- const session = await this.persist(payload);
247
- return { data: { user: session.user, session }, error: null };
248
- }
249
- catch (err) {
250
- return { data: { user: null, session: null }, error: toError(err) };
251
- }
252
- }
253
- /**
254
- * Start an OAuth sign-in with a configured social provider. Resolves with the
255
- * provider's authorize URL and, in a browser, redirects to it immediately
256
- * unless `options.skipBrowserRedirect` is set.
257
- */
258
- async signInWithOAuth(credentials) {
259
- // In a browser, hand the whole handshake to a top-level navigation. Starting
260
- // it with a fetch makes the OAuth state cookie a cross-site write, which
261
- // Safari and Firefox drop outright and Chrome drops in incognito, and the
262
- // callback then fails with state_mismatch. Navigating to the backend's own
263
- // origin makes that cookie first-party, so a plain SameSite=Lax cookie
264
- // survives everywhere. The backend redirects on from there.
265
- if (typeof window !== 'undefined' && !credentials.options?.skipBrowserRedirect) {
266
- const params = new URLSearchParams();
267
- if (credentials.options?.redirectTo)
268
- params.set('callbackURL', credentials.options.redirectTo);
269
- const query = params.toString();
270
- const url = this.http.url(`${HTTP_ROUTES.authSignInSocial}/${encodeURIComponent(credentials.provider)}${query ? `?${query}` : ''}`);
271
- window.location.href = url;
272
- return { data: { provider: credentials.provider, url }, error: null };
273
- }
274
- try {
275
- const payload = await this.http.request(HTTP_ROUTES.authSignInSocial, {
276
- method: 'POST',
277
- body: {
278
- provider: credentials.provider,
279
- callbackURL: credentials.options?.redirectTo,
280
- scopes: credentials.options?.scopes ? credentials.options.scopes.split(' ') : undefined,
281
- disableRedirect: true,
282
- },
283
- anonymous: true,
284
- raw: true,
285
- // The server answers with a signed OAuth state cookie it checks again on
286
- // the provider callback. Without 'include' the browser drops that cookie
287
- // on a cross-origin call and every callback fails with state_mismatch.
288
- credentials: 'include',
289
- });
290
- const url = payload?.url ?? null;
291
- if (url && !credentials.options?.skipBrowserRedirect && typeof window !== 'undefined') {
292
- window.location.href = url;
293
- }
294
- return { data: { provider: credentials.provider, url }, error: null };
295
- }
296
- catch (err) {
297
- return { data: { provider: credentials.provider, url: null }, error: toError(err) };
298
- }
299
- }
300
- /**
301
- * Start an enterprise SSO sign-in. Identify the provider by the user's email
302
- * domain, by an explicit `domain`, or by `providerId` when you already know
303
- * which one to use.
304
- *
305
- * Resolves with the identity provider's URL and, in a browser, redirects to it
306
- * unless `options.skipBrowserRedirect` is set. The project must have the SSO
307
- * plugin enabled and at least one provider registered (Studio →
308
- * Authentication → Organizations & SSO).
309
- */
310
- async signInWithSSO(params) {
311
- if (!params.domain && !params.email && !params.providerId && !params.organizationSlug) {
312
- return { data: { url: null }, error: { message: 'signInWithSSO needs a domain, email, providerId or organizationSlug' } };
313
- }
314
- try {
315
- const payload = await this.http.request('/api/auth/sign-in/sso', {
316
- method: 'POST',
317
- body: {
318
- ...(params.domain ? { domain: params.domain } : {}),
319
- ...(params.email ? { email: params.email } : {}),
320
- ...(params.providerId ? { providerId: params.providerId } : {}),
321
- ...(params.organizationSlug ? { organizationSlug: params.organizationSlug } : {}),
322
- callbackURL: params.options?.redirectTo,
323
- },
324
- anonymous: true,
325
- raw: true,
326
- credentials: 'include', // Same OAuth state cookie as signInWithOAuth.
327
- });
328
- const url = payload?.url ?? null;
329
- if (url && !params.options?.skipBrowserRedirect && typeof window !== 'undefined') {
330
- window.location.href = url;
331
- }
332
- return { data: { url }, error: null };
333
- }
334
- catch (err) {
335
- return { data: { url: null }, error: toError(err) };
336
- }
337
- }
338
- /**
339
- * Update the signed-in user's profile fields and/or password. Password
340
- * changes require `currentPassword` (a Better Auth requirement. There is no
341
- * blind password overwrite from an authenticated session).
342
- */
343
- async updateUser(attributes) {
344
- try {
345
- if (attributes.data && Object.keys(attributes.data).length > 0) {
346
- await this.http.request(HTTP_ROUTES.authUpdateUser, { method: 'POST', body: attributes.data, raw: true });
347
- }
348
- if (attributes.password) {
349
- if (!attributes.currentPassword) {
350
- return { data: { user: null }, error: { message: '`currentPassword` is required to change the password.' } };
351
- }
352
- await this.http.request(HTTP_ROUTES.authChangePassword, {
353
- method: 'POST',
354
- body: { newPassword: attributes.password, currentPassword: attributes.currentPassword },
355
- raw: true,
356
- });
357
- }
358
- const result = await this.getUser();
359
- // supabase-js emits USER_UPDATED here, and apps re-read the profile off
360
- // that event rather than off the return value. Refresh the stored session
361
- // so the user the listener receives is the updated one.
362
- const current = this.store.get();
363
- if (result.data?.user && current) {
364
- await this.store.set({ ...current, user: result.data.user }, 'userUpdated');
365
- }
366
- return result;
367
- }
368
- catch (err) {
369
- return { data: { user: null }, error: toError(err) };
370
- }
371
- }
372
- /**
373
- * Permanently delete the signed-in user's account. Not part of the
374
- * supabase-js client surface (there it's admin-only). Kept here because
375
- * self-service account deletion is a real product need. Requires the
376
- * backend's `delete-user` flow to be enabled; clears the local session on
377
- * success.
378
- */
379
- async deleteUser(attributes = {}) {
380
- try {
381
- await this.http.request(HTTP_ROUTES.authDeleteUser, {
382
- method: 'POST',
383
- body: attributes,
384
- raw: true,
385
- });
386
- await this.store.set(null, 'signOut');
387
- return { error: null };
388
- }
389
- catch (err) {
390
- return { error: toError(err) };
391
- }
392
- }
393
- /**
394
- * Adopt an externally-obtained access token as the current session (e.g. one
395
- * captured from an OAuth redirect). Validates it against the server before
396
- * persisting. `refresh_token` is accepted for supabase-js signature parity
397
- * and unused: Better Auth bearer sessions have none.
398
- */
399
- async setSession(currentSession) {
400
- await this.store.set({ accessToken: currentSession.access_token, user: { id: '', email: '' } }, 'signIn');
401
- try {
402
- const payload = await this.http.request(HTTP_ROUTES.authSession, { raw: true });
403
- if (!payload?.user) {
404
- await this.store.set(null, 'signOut');
405
- return { data: { user: null, session: null }, error: { message: 'Invalid session' } };
406
- }
407
- const session = { accessToken: currentSession.access_token, user: payload.user };
408
- await this.store.set(session, 'signIn');
409
- return { data: { user: session.user, session }, error: null };
410
- }
411
- catch (err) {
412
- await this.store.set(null, 'signOut');
413
- return { data: { user: null, session: null }, error: toError(err) };
414
- }
415
- }
416
- async signOut() {
417
- try {
418
- await this.http.request(HTTP_ROUTES.authSignOut, { method: 'POST', raw: true });
419
- }
420
- catch {
421
- // best-effort; clear locally regardless
422
- }
423
- await this.store.set(null, 'signOut');
424
- return { error: null };
425
- }
426
- /**
427
- * Finish an OAuth / SSO sign-in after the provider redirected back.
428
- *
429
- * `signInWithOAuth` only starts the flow; the session is created during the
430
- * redirect the browser makes to the backend callback, so the SDK has to pick
431
- * it up from the URL the browser landed on. Call this once on the page you
432
- * pointed `redirectTo` at (or on app start, which is where supabase-js does
433
- * the equivalent). Without it the user is signed in on the backend and signed
434
- * out in your app.
435
- *
436
- * Resolves with a null session and no error when there is nothing to read, so
437
- * it is safe to call on every page load.
438
- */
439
- async getSessionFromUrl(href) {
440
- const url = href ?? (typeof window !== 'undefined' ? window.location.href : undefined);
441
- const found = url ? readAccessTokenFromUrl(url) : null;
442
- if (!found)
443
- return { data: { user: null, session: null }, error: null };
444
- // Strip the token before the network call: a failed lookup must not leave a
445
- // usable token sitting in the address bar or in the back/forward history.
446
- if (!href && typeof window !== 'undefined') {
447
- window.history?.replaceState?.(window.history.state, '', found.cleanedHref);
448
- }
449
- try {
450
- // Resolve the user with the token explicitly rather than storing it first:
451
- // a rejected token must not emit a SIGNED_IN event carrying a null user.
452
- const payload = await this.http.request(HTTP_ROUTES.authSession, {
453
- raw: true,
454
- anonymous: true,
455
- headers: { Authorization: `Bearer ${found.accessToken}` },
456
- });
457
- if (!payload?.user) {
458
- return { data: { user: null, session: null }, error: { message: 'OAuth session is no longer valid' } };
459
- }
460
- const session = await this.persist({ token: found.accessToken, user: payload.user });
461
- return { data: { user: session.user, session }, error: null };
462
- }
463
- catch (err) {
464
- return { data: { user: null, session: null }, error: toError(err) };
465
- }
466
- }
467
- /** Fetch the current user from Better Auth (requires a valid session). */
468
- async getUser() {
469
- try {
470
- const payload = await this.http.request(HTTP_ROUTES.authSession, { raw: true });
471
- return { data: { user: normalizeUser(payload?.user) }, error: null };
472
- }
473
- catch (err) {
474
- return { data: { user: null }, error: toError(err) };
475
- }
476
- }
477
- /**
478
- * NEVER USE FOR AN AUTHORIZATION DECISION. Use {@link getUser} instead.
479
- *
480
- * The locally-held session, if any (no network call). It is whatever the
481
- * caller's cookie or storage said, parsed: unsigned JSON the browser sent,
482
- * which anyone can set by hand. `session.user.id`, `.email` and `.role` are
483
- * therefore attacker-controlled, and an admin allowlist checked against
484
- * them is checked against nothing.
485
- *
486
- * Reads and writes through `from()` are unaffected: they carry the access
487
- * token to the backend, which verifies it under RLS. It is the decisions
488
- * your own app takes from `session.user` that are forgeable.
489
- *
490
- * `getUser()` costs one round trip and returns the user the backend
491
- * confirms, in the same `{ data, error }` shape. Prefer it everywhere but a
492
- * cosmetic "am I probably signed in" check.
493
- */
494
- async getSession() {
495
- await this.store.hydrate();
496
- return { data: { session: this.store.get() }, error: null };
497
- }
498
- /**
499
- * Revalidate the current session against the server. Better Auth bearer
500
- * sessions have no refresh token: if the session is still valid the user is
501
- * refreshed; otherwise the local session is cleared.
502
- */
503
- async refreshSession() {
504
- await this.store.hydrate();
505
- const current = this.store.get();
506
- if (!current?.accessToken) {
507
- return { data: { user: null, session: null }, error: { message: 'Auth session missing!' } };
508
- }
509
- try {
510
- const payload = await this.http.request(HTTP_ROUTES.authSession, { raw: true, _retry: true });
511
- if (!payload?.user) {
512
- await this.store.set(null, 'signOut');
513
- return { data: { user: null, session: null }, error: { message: 'Session expired' } };
514
- }
515
- const session = { accessToken: current.accessToken, user: payload.user };
516
- await this.store.set(session, 'tokenRefreshed');
517
- return { data: { user: session.user, session }, error: null };
518
- }
519
- catch (err) {
520
- // Only a rejected token clears the session. A network failure or a 5xx
521
- // must leave it intact: middleware runs this on every navigation, so
522
- // one backend hiccup would otherwise sign the user out.
523
- const status = err instanceof BackendError ? err.httpStatus : 0;
524
- if (status === 401 || status === 403)
525
- await this.store.set(null, 'signOut');
526
- return { data: { user: null, session: null }, error: toError(err) };
527
- }
528
- }
529
- /**
530
- * Subscribe to auth changes, supabase-js style.
531
- *
532
- * The callback fires once with `INITIAL_SESSION` as soon as the stored
533
- * session has been read, before any later event. That first call is not a
534
- * nicety: the standard Supabase bootstrap is
535
- * `if (event === 'INITIAL_SESSION') setLoading(false)`, and without it the
536
- * app renders its loading state forever for a signed-out visitor, who never
537
- * produces any other event.
538
- *
539
- * It is delivered asynchronously (after hydration), so unsubscribing
540
- * synchronously suppresses it, exactly as in supabase-js.
541
- */
542
- /**
543
- * The providers linked to the signed-in user, in the supabase-js identity
544
- * shape. Backed by Better Auth's `list-accounts`, which is why `email` shows
545
- * up as an identity for a password account: it is a real linked credential,
546
- * exactly as in Supabase.
547
- */
548
- async getUserIdentities() {
549
- try {
550
- const accounts = await this.http.request(HTTP_ROUTES.authListAccounts, { raw: true });
551
- const userId = this.store.get()?.user?.id ?? '';
552
- const identities = (accounts ?? []).map(account => ({
553
- id: account.accountId ?? account.id,
554
- identity_id: account.id,
555
- user_id: userId,
556
- provider: account.providerId,
557
- identity_data: { sub: account.accountId, ...(account.scopes ? { scopes: account.scopes } : {}) },
558
- created_at: asIsoString(account.createdAt),
559
- updated_at: asIsoString(account.updatedAt),
560
- last_sign_in_at: asIsoString(account.updatedAt),
561
- }));
562
- return { data: { identities }, error: null };
563
- }
564
- catch (err) {
565
- return { data: null, error: toError(err) };
566
- }
567
- }
568
- /**
569
- * Link another provider to the signed-in user. Like `signInWithOAuth`, this
570
- * only starts the flow: the browser is sent to the provider and comes back
571
- * through the callback.
572
- */
573
- async linkIdentity(credentials) {
574
- try {
575
- const payload = await this.http.request(HTTP_ROUTES.authLinkSocial, {
576
- method: 'POST',
577
- body: {
578
- provider: credentials.provider,
579
- callbackURL: credentials.options?.redirectTo,
580
- scopes: credentials.options?.scopes?.split(/\s+/).filter(Boolean),
581
- },
582
- credentials: 'include',
583
- raw: true,
584
- });
585
- const url = payload?.url ?? null;
586
- if (url && !credentials.options?.skipBrowserRedirect && typeof window !== 'undefined') {
587
- window.location.assign(url);
588
- }
589
- return { data: { provider: credentials.provider, url }, error: null };
590
- }
591
- catch (err) {
592
- return { data: { provider: credentials.provider, url: null }, error: toError(err) };
593
- }
594
- }
595
- /** Unlink a provider from the signed-in user. */
596
- async unlinkIdentity(identity) {
597
- try {
598
- await this.http.request(HTTP_ROUTES.authUnlinkAccount, {
599
- method: 'POST',
600
- body: { providerId: identity.provider, accountId: identity.id },
601
- raw: true,
602
- });
603
- return { data: {}, error: null };
604
- }
605
- catch (err) {
606
- return { data: null, error: toError(err) };
607
- }
608
- }
609
- /**
610
- * Native mobile sign-in with a provider id token (Sign in with Apple, Google
611
- * One Tap), routed to Better Auth's social sign-in with an `idToken`.
612
- * The provider must be configured on the backend.
613
- */
614
- async signInWithIdToken(params) {
615
- try {
616
- const payload = await this.http.request(HTTP_ROUTES.authSignInSocial, {
617
- method: 'POST',
618
- body: {
619
- provider: params.provider,
620
- idToken: {
621
- token: params.token,
622
- ...(params.access_token ? { accessToken: params.access_token } : {}),
623
- ...(params.nonce ? { nonce: params.nonce } : {}),
624
- },
625
- },
626
- anonymous: true,
627
- raw: true,
628
- });
629
- if (!payload?.token) {
630
- return { data: { user: null, session: null }, error: { message: 'The provider did not return a session.' } };
631
- }
632
- const session = await this.persist(payload);
633
- return { data: { user: session.user, session }, error: null };
634
- }
635
- catch (err) {
636
- return { data: { user: null, session: null }, error: toError(err) };
637
- }
638
- }
639
- /**
640
- * Claims for the current session, as a JWT payload.
641
- *
642
- * supabase-js reads them out of the access token, which is itself a JWT.
643
- * A Better Auth session token is an opaque bearer, so this asks the server's
644
- * jwt plugin to mint one and decodes it. The decode is NOT a verification:
645
- * the token came from the authenticated endpoint over TLS, and a client
646
- * verifying a token it just asked for proves nothing. Verify server side,
647
- * against `/api/auth/jwks`.
648
- */
649
- async getClaims() {
650
- try {
651
- const payload = await this.http.request(HTTP_ROUTES.authToken, { raw: true });
652
- const token = payload?.token;
653
- const claims = token ? decodeJwtPayload(token) : null;
654
- if (!token || !claims) {
655
- return { data: null, error: { message: 'No JWT is available for this session.' } };
656
- }
657
- return { data: { claims, token }, error: null };
658
- }
659
- catch (err) {
660
- return { data: null, error: toError(err) };
661
- }
662
- }
663
- /**
664
- * Revalidate the session on a timer, the way supabase-js does while the tab
665
- * is focused. Motherbase refreshes on a 401 already, so this is opt-in: it
666
- * exists so a ported app that calls it keeps working, and so a long-lived
667
- * screen notices a revoked session without waiting for its next request.
668
- */
669
- startAutoRefresh(intervalMs = 60000) {
670
- this.stopAutoRefresh();
671
- this.autoRefreshTimer = setInterval(() => {
672
- if (this.store.get())
673
- void this.refreshSession();
674
- }, intervalMs);
675
- // Node keeps the process alive for a pending timer; a background refresh
676
- // must never be the reason a script does not exit.
677
- this.autoRefreshTimer.unref?.();
678
- }
679
- /** Stop the timer started by `startAutoRefresh`. Safe to call when none runs. */
680
- stopAutoRefresh() {
681
- if (this.autoRefreshTimer !== null) {
682
- clearInterval(this.autoRefreshTimer);
683
- this.autoRefreshTimer = null;
684
- }
685
- }
686
- onAuthStateChange(callback) {
687
- let active = true;
688
- const unsubscribeStore = this.store.subscribe((event, session) => {
689
- if (active)
690
- callback(EVENT_MAP[event], session);
691
- });
692
- void this.store.hydrate().then(session => {
693
- if (active)
694
- callback('INITIAL_SESSION', session);
695
- });
696
- return {
697
- data: {
698
- subscription: {
699
- unsubscribe: () => {
700
- active = false;
701
- unsubscribeStore();
702
- },
703
- },
704
- },
705
- };
706
- }
707
- /**
708
- * Internal: refresh used by the HttpClient's 401 path. Returns true on success.
709
- * Not part of the public Supabase surface.
710
- */
711
- async _refresh() {
712
- const result = await this.refreshSession();
713
- return result.error === null;
714
- }
715
- async persist(payload) {
716
- const session = toSession(payload);
717
- await this.store.set(session, 'signIn');
718
- return session;
719
- }
720
- }
721
- //# sourceMappingURL=auth.js.map