@linabase/js 0.4.4 → 0.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.
package/dist/index.cjs CHANGED
@@ -698,6 +698,12 @@ var AuthClient = class {
698
698
  _restorePromise = Promise.resolve();
699
699
  /** API key for auth requests that shouldn't use the (possibly expired) access token. */
700
700
  _apiKey = "";
701
+ /** Absolute base URL of the Linabase REST API, used to build browser-level
702
+ * redirect targets (signInWithOAuth) that must be resolved against the
703
+ * API origin, not the caller's page origin. Empty string means "resolve
704
+ * relative to the current page" which is only safe in server-side or
705
+ * test contexts. */
706
+ _baseUrl = "";
701
707
  currentSession = null;
702
708
  /**
703
709
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -708,17 +714,59 @@ var AuthClient = class {
708
714
  this.request = request;
709
715
  }
710
716
  /**
711
- * Set (or clear) the current session. Use this to restore a persisted
712
- * session on app launch (e.g., from AsyncStorage / SecureStore).
713
- * Automatically refreshes the token if it has expired.
714
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
715
- * Internal callers (signIn, signUp, etc.) pass _internal=true to skip the emit.
717
+ * Set (or clear) the current session.
718
+ *
719
+ * Three valid shapes:
720
+ * - `null` clear the session.
721
+ * - Full `AuthSession` restore a persisted session (e.g. from
722
+ * AsyncStorage). Auto-refreshes if the token is already expired.
723
+ * - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
724
+ * pattern. Used by OAuth / magic-link callbacks that only have
725
+ * tokens in the URL hash and no user object. The SDK installs the
726
+ * tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
727
+ * a promise that resolves once the user is populated (or rejects
728
+ * if the token is invalid).
729
+ *
730
+ * Internal callers (signIn, signUp, etc.) pass `_internal=true` to
731
+ * skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
732
+ * lifecycle events.
716
733
  */
717
734
  setSession(session, _internal) {
735
+ if (session === null) {
736
+ this.currentSession = null;
737
+ if (this.onSessionChange) this.onSessionChange(null);
738
+ if (!_internal) this.emit("INITIAL_SESSION", null);
739
+ return;
740
+ }
741
+ if (!("user" in session) || !session.user) {
742
+ const tokens = {
743
+ access_token: session.access_token,
744
+ refresh_token: session.refresh_token,
745
+ token_type: session.token_type ?? "bearer",
746
+ expires_in: session.expires_in ?? 3600,
747
+ expires_at: session.expires_at ?? Math.floor(Date.now() / 1e3) + (session.expires_in ?? 3600),
748
+ user: null
749
+ };
750
+ this.currentSession = tokens;
751
+ if (this.onSessionChange) this.onSessionChange(tokens);
752
+ return (async () => {
753
+ const { data, error } = await this.getUser();
754
+ if (error || !data.user) {
755
+ this.currentSession = null;
756
+ if (this.onSessionChange) this.onSessionChange(null);
757
+ return { data: null, error: error ?? { message: "User lookup failed" } };
758
+ }
759
+ const full = { ...tokens, user: data.user };
760
+ this.currentSession = full;
761
+ if (this.onSessionChange) this.onSessionChange(full);
762
+ if (!_internal) this.emit("SIGNED_IN", full);
763
+ return { data: full, error: null };
764
+ })();
765
+ }
718
766
  this.currentSession = session;
719
767
  if (this.onSessionChange) this.onSessionChange(session);
720
768
  if (!_internal) {
721
- if (session && session.expires_at <= Math.floor(Date.now() / 1e3)) {
769
+ if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
722
770
  this.refreshSession().then(({ error }) => {
723
771
  if (error) {
724
772
  this.currentSession = null;
@@ -794,8 +842,10 @@ var AuthClient = class {
794
842
  // ─── OAuth ─────────────────────────────────────────────────
795
843
  signInWithOAuth(params) {
796
844
  const queryParams = new URLSearchParams({ provider: params.provider });
845
+ if (this._apiKey) queryParams.set("apikey", this._apiKey);
797
846
  if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
798
- const url = `/auth/v1/authorize?${queryParams}`;
847
+ const base = this._baseUrl.replace(/\/$/, "");
848
+ const url = `${base}/auth/v1/authorize?${queryParams}`;
799
849
  if (typeof window !== "undefined") {
800
850
  window.location.href = url;
801
851
  }
@@ -824,9 +874,25 @@ var AuthClient = class {
824
874
  }
825
875
  return { data: { session: null }, error: null };
826
876
  }
827
- async getUser() {
877
+ /**
878
+ * Install an OAuth / magic-link session from the URL hash.
879
+ * Thin wrapper around `setSession(tokens)` kept for the auto-detect path
880
+ * in createClient. New consumers should just call `setSession()` directly.
881
+ */
882
+ async _ingestHashTokens(tokens) {
883
+ const result = this.setSession(tokens);
884
+ if (result instanceof Promise) await result;
885
+ }
886
+ /**
887
+ * Fetch the current user.
888
+ * @param jwt Optional access token to use instead of the stored session.
889
+ * Useful for validating a JWT without installing it as the active session
890
+ * (e.g. admin tooling, custom auth bridges).
891
+ */
892
+ async getUser(jwt) {
828
893
  try {
829
- const res = await this.request("/auth/v1/user");
894
+ const options = jwt ? { headers: { Authorization: `Bearer ${jwt}` } } : {};
895
+ const res = await this.request("/auth/v1/user", options);
830
896
  const data = await res.json();
831
897
  if (!res.ok) return { data: { user: null }, error: data };
832
898
  return { data: { user: data }, error: null };
@@ -1159,6 +1225,7 @@ function createClient(config) {
1159
1225
  }
1160
1226
  const authClient = new AuthClient(request);
1161
1227
  authClient._apiKey = apiKey;
1228
+ authClient._baseUrl = baseUrl;
1162
1229
  const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
1163
1230
  const sessionStorage = config.auth?.storage;
1164
1231
  const sessionKey = config.auth?.storageKey || "@linabase/session";
@@ -1183,10 +1250,41 @@ function createClient(config) {
1183
1250
  }
1184
1251
  });
1185
1252
  }
1253
+ const detectSessionInUrl = config.auth?.detectSessionInUrl !== false;
1254
+ if (detectSessionInUrl && typeof window !== "undefined" && window.location?.hash) {
1255
+ const pending = authClient._restorePromise;
1256
+ authClient._restorePromise = Promise.resolve(pending).then(async () => {
1257
+ const hash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
1258
+ const params = new URLSearchParams(hash);
1259
+ const access_token = params.get("access_token");
1260
+ const refresh_token = params.get("refresh_token");
1261
+ if (!access_token || !refresh_token) return;
1262
+ const expires_in = Number(params.get("expires_in") || 3600);
1263
+ const expires_at = Number(
1264
+ params.get("expires_at") || Math.floor(Date.now() / 1e3) + expires_in
1265
+ );
1266
+ const ingest = authClient._ingestHashTokens({
1267
+ access_token,
1268
+ refresh_token,
1269
+ token_type: params.get("token_type") || "bearer",
1270
+ expires_in,
1271
+ expires_at
1272
+ });
1273
+ try {
1274
+ const url = new URL(window.location.href);
1275
+ url.hash = "";
1276
+ window.history.replaceState(null, "", url.toString());
1277
+ } catch {
1278
+ }
1279
+ await ingest;
1280
+ });
1281
+ }
1186
1282
  function buildClient(reqFn, branchSlug) {
1187
1283
  const rc = new RpcClient(reqFn);
1188
1284
  const ac = branchSlug ? new AuthClient(reqFn) : authClient;
1189
1285
  if (branchSlug) {
1286
+ ac._apiKey = apiKey;
1287
+ ac._baseUrl = baseUrl;
1190
1288
  ac.onSessionChange = (session) => {
1191
1289
  accessToken = session?.access_token || null;
1192
1290
  };
@@ -1235,6 +1333,20 @@ function createClient(config) {
1235
1333
  }
1236
1334
  };
1237
1335
  }
1336
+ if (config.branch) {
1337
+ let pinnedRequest2 = function(path, options = {}) {
1338
+ return request(path, {
1339
+ ...options,
1340
+ headers: {
1341
+ ...options.headers,
1342
+ "X-Branch": pinned
1343
+ }
1344
+ });
1345
+ };
1346
+ var pinnedRequest = pinnedRequest2;
1347
+ const pinned = config.branch;
1348
+ return buildClient(pinnedRequest2, pinned);
1349
+ }
1238
1350
  return buildClient(request);
1239
1351
  }
1240
1352
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -251,6 +251,20 @@ interface AuthSession {
251
251
  expires_at: number;
252
252
  user: AuthUser;
253
253
  }
254
+ /**
255
+ * Token-only input accepted by `setSession` when a full AuthSession isn't
256
+ * available yet (OAuth / magic-link callbacks, custom auth bridges). The SDK
257
+ * fetches the user and fills in defaults for `token_type` / `expires_in` /
258
+ * `expires_at` when omitted.
259
+ */
260
+ interface SetSessionTokens {
261
+ access_token: string;
262
+ refresh_token: string;
263
+ token_type?: string;
264
+ expires_in?: number;
265
+ expires_at?: number;
266
+ user?: null;
267
+ }
254
268
  interface AuthUser {
255
269
  id: string;
256
270
  email: string | null;
@@ -270,6 +284,12 @@ declare class AuthClient {
270
284
  _restorePromise: Promise<void>;
271
285
  /** API key for auth requests that shouldn't use the (possibly expired) access token. */
272
286
  _apiKey: string;
287
+ /** Absolute base URL of the Linabase REST API, used to build browser-level
288
+ * redirect targets (signInWithOAuth) that must be resolved against the
289
+ * API origin, not the caller's page origin. Empty string means "resolve
290
+ * relative to the current page" which is only safe in server-side or
291
+ * test contexts. */
292
+ _baseUrl: string;
273
293
  private currentSession;
274
294
  /**
275
295
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -278,13 +298,27 @@ declare class AuthClient {
278
298
  onSessionChange: ((session: AuthSession | null) => void) | null;
279
299
  constructor(request: RequestFn$1);
280
300
  /**
281
- * Set (or clear) the current session. Use this to restore a persisted
282
- * session on app launch (e.g., from AsyncStorage / SecureStore).
283
- * Automatically refreshes the token if it has expired.
284
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
285
- * Internal callers (signIn, signUp, etc.) pass _internal=true to skip the emit.
301
+ * Set (or clear) the current session.
302
+ *
303
+ * Three valid shapes:
304
+ * - `null` clear the session.
305
+ * - Full `AuthSession` restore a persisted session (e.g. from
306
+ * AsyncStorage). Auto-refreshes if the token is already expired.
307
+ * - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
308
+ * pattern. Used by OAuth / magic-link callbacks that only have
309
+ * tokens in the URL hash and no user object. The SDK installs the
310
+ * tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
311
+ * a promise that resolves once the user is populated (or rejects
312
+ * if the token is invalid).
313
+ *
314
+ * Internal callers (signIn, signUp, etc.) pass `_internal=true` to
315
+ * skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
316
+ * lifecycle events.
286
317
  */
287
- setSession(session: AuthSession | null, _internal?: boolean): void;
318
+ setSession(session: AuthSession | SetSessionTokens | null, _internal?: boolean): void | Promise<{
319
+ data: AuthSession | null;
320
+ error: any;
321
+ }>;
288
322
  signUp(params: {
289
323
  email: string;
290
324
  password: string;
@@ -331,7 +365,19 @@ declare class AuthClient {
331
365
  };
332
366
  error: any;
333
367
  }>;
334
- getUser(): Promise<{
368
+ /**
369
+ * Install an OAuth / magic-link session from the URL hash.
370
+ * Thin wrapper around `setSession(tokens)` kept for the auto-detect path
371
+ * in createClient. New consumers should just call `setSession()` directly.
372
+ */
373
+ _ingestHashTokens(tokens: SetSessionTokens): Promise<void>;
374
+ /**
375
+ * Fetch the current user.
376
+ * @param jwt Optional access token to use instead of the stored session.
377
+ * Useful for validating a JWT without installing it as the active session
378
+ * (e.g. admin tooling, custom auth bridges).
379
+ */
380
+ getUser(jwt?: string): Promise<{
335
381
  data: {
336
382
  user: AuthUser | null;
337
383
  };
@@ -472,11 +518,31 @@ interface LinabaseConfig {
472
518
  url: string;
473
519
  anonKey?: string;
474
520
  serviceRoleKey?: string;
521
+ /**
522
+ * Optional: target a specific DB branch for the entire lifetime of the client.
523
+ * When set, every request carries an `X-Branch: {slug}` header so the REST API
524
+ * routes it to the branch schemas. Equivalent to calling `.branch(slug)` on
525
+ * the returned client, except it applies to the root client directly.
526
+ *
527
+ * Leave unset (the default) to use the main project schemas.
528
+ */
529
+ branch?: string;
475
530
  /** Persistent storage for auth sessions (e.g., AsyncStorage for React Native). */
476
531
  auth?: {
477
532
  storage?: SessionStorage;
478
533
  storageKey?: string;
479
534
  persistSession?: boolean;
535
+ /**
536
+ * Auto-consume OAuth / magic-link tokens from the URL hash on startup.
537
+ * When the server redirects the user back with
538
+ * `#access_token=…&refresh_token=…`, the SDK reads the fragment, calls
539
+ * setSession, and clears the hash from the URL so the tokens aren't
540
+ * visible in the address bar or bookmarked by mistake.
541
+ *
542
+ * Defaults to true in browsers (disable only if you want to handle the
543
+ * hash yourself). No-op in Node / SSR since there's no window.
544
+ */
545
+ detectSessionInUrl?: boolean;
480
546
  };
481
547
  }
482
548
  interface LinabaseClient {
package/dist/index.d.ts CHANGED
@@ -251,6 +251,20 @@ interface AuthSession {
251
251
  expires_at: number;
252
252
  user: AuthUser;
253
253
  }
254
+ /**
255
+ * Token-only input accepted by `setSession` when a full AuthSession isn't
256
+ * available yet (OAuth / magic-link callbacks, custom auth bridges). The SDK
257
+ * fetches the user and fills in defaults for `token_type` / `expires_in` /
258
+ * `expires_at` when omitted.
259
+ */
260
+ interface SetSessionTokens {
261
+ access_token: string;
262
+ refresh_token: string;
263
+ token_type?: string;
264
+ expires_in?: number;
265
+ expires_at?: number;
266
+ user?: null;
267
+ }
254
268
  interface AuthUser {
255
269
  id: string;
256
270
  email: string | null;
@@ -270,6 +284,12 @@ declare class AuthClient {
270
284
  _restorePromise: Promise<void>;
271
285
  /** API key for auth requests that shouldn't use the (possibly expired) access token. */
272
286
  _apiKey: string;
287
+ /** Absolute base URL of the Linabase REST API, used to build browser-level
288
+ * redirect targets (signInWithOAuth) that must be resolved against the
289
+ * API origin, not the caller's page origin. Empty string means "resolve
290
+ * relative to the current page" which is only safe in server-side or
291
+ * test contexts. */
292
+ _baseUrl: string;
273
293
  private currentSession;
274
294
  /**
275
295
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -278,13 +298,27 @@ declare class AuthClient {
278
298
  onSessionChange: ((session: AuthSession | null) => void) | null;
279
299
  constructor(request: RequestFn$1);
280
300
  /**
281
- * Set (or clear) the current session. Use this to restore a persisted
282
- * session on app launch (e.g., from AsyncStorage / SecureStore).
283
- * Automatically refreshes the token if it has expired.
284
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
285
- * Internal callers (signIn, signUp, etc.) pass _internal=true to skip the emit.
301
+ * Set (or clear) the current session.
302
+ *
303
+ * Three valid shapes:
304
+ * - `null` clear the session.
305
+ * - Full `AuthSession` restore a persisted session (e.g. from
306
+ * AsyncStorage). Auto-refreshes if the token is already expired.
307
+ * - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
308
+ * pattern. Used by OAuth / magic-link callbacks that only have
309
+ * tokens in the URL hash and no user object. The SDK installs the
310
+ * tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
311
+ * a promise that resolves once the user is populated (or rejects
312
+ * if the token is invalid).
313
+ *
314
+ * Internal callers (signIn, signUp, etc.) pass `_internal=true` to
315
+ * skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
316
+ * lifecycle events.
286
317
  */
287
- setSession(session: AuthSession | null, _internal?: boolean): void;
318
+ setSession(session: AuthSession | SetSessionTokens | null, _internal?: boolean): void | Promise<{
319
+ data: AuthSession | null;
320
+ error: any;
321
+ }>;
288
322
  signUp(params: {
289
323
  email: string;
290
324
  password: string;
@@ -331,7 +365,19 @@ declare class AuthClient {
331
365
  };
332
366
  error: any;
333
367
  }>;
334
- getUser(): Promise<{
368
+ /**
369
+ * Install an OAuth / magic-link session from the URL hash.
370
+ * Thin wrapper around `setSession(tokens)` kept for the auto-detect path
371
+ * in createClient. New consumers should just call `setSession()` directly.
372
+ */
373
+ _ingestHashTokens(tokens: SetSessionTokens): Promise<void>;
374
+ /**
375
+ * Fetch the current user.
376
+ * @param jwt Optional access token to use instead of the stored session.
377
+ * Useful for validating a JWT without installing it as the active session
378
+ * (e.g. admin tooling, custom auth bridges).
379
+ */
380
+ getUser(jwt?: string): Promise<{
335
381
  data: {
336
382
  user: AuthUser | null;
337
383
  };
@@ -472,11 +518,31 @@ interface LinabaseConfig {
472
518
  url: string;
473
519
  anonKey?: string;
474
520
  serviceRoleKey?: string;
521
+ /**
522
+ * Optional: target a specific DB branch for the entire lifetime of the client.
523
+ * When set, every request carries an `X-Branch: {slug}` header so the REST API
524
+ * routes it to the branch schemas. Equivalent to calling `.branch(slug)` on
525
+ * the returned client, except it applies to the root client directly.
526
+ *
527
+ * Leave unset (the default) to use the main project schemas.
528
+ */
529
+ branch?: string;
475
530
  /** Persistent storage for auth sessions (e.g., AsyncStorage for React Native). */
476
531
  auth?: {
477
532
  storage?: SessionStorage;
478
533
  storageKey?: string;
479
534
  persistSession?: boolean;
535
+ /**
536
+ * Auto-consume OAuth / magic-link tokens from the URL hash on startup.
537
+ * When the server redirects the user back with
538
+ * `#access_token=…&refresh_token=…`, the SDK reads the fragment, calls
539
+ * setSession, and clears the hash from the URL so the tokens aren't
540
+ * visible in the address bar or bookmarked by mistake.
541
+ *
542
+ * Defaults to true in browsers (disable only if you want to handle the
543
+ * hash yourself). No-op in Node / SSR since there's no window.
544
+ */
545
+ detectSessionInUrl?: boolean;
480
546
  };
481
547
  }
482
548
  interface LinabaseClient {
package/dist/index.js CHANGED
@@ -666,6 +666,12 @@ var AuthClient = class {
666
666
  _restorePromise = Promise.resolve();
667
667
  /** API key for auth requests that shouldn't use the (possibly expired) access token. */
668
668
  _apiKey = "";
669
+ /** Absolute base URL of the Linabase REST API, used to build browser-level
670
+ * redirect targets (signInWithOAuth) that must be resolved against the
671
+ * API origin, not the caller's page origin. Empty string means "resolve
672
+ * relative to the current page" which is only safe in server-side or
673
+ * test contexts. */
674
+ _baseUrl = "";
669
675
  currentSession = null;
670
676
  /**
671
677
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -676,17 +682,59 @@ var AuthClient = class {
676
682
  this.request = request;
677
683
  }
678
684
  /**
679
- * Set (or clear) the current session. Use this to restore a persisted
680
- * session on app launch (e.g., from AsyncStorage / SecureStore).
681
- * Automatically refreshes the token if it has expired.
682
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
683
- * Internal callers (signIn, signUp, etc.) pass _internal=true to skip the emit.
685
+ * Set (or clear) the current session.
686
+ *
687
+ * Three valid shapes:
688
+ * - `null` clear the session.
689
+ * - Full `AuthSession` restore a persisted session (e.g. from
690
+ * AsyncStorage). Auto-refreshes if the token is already expired.
691
+ * - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
692
+ * pattern. Used by OAuth / magic-link callbacks that only have
693
+ * tokens in the URL hash and no user object. The SDK installs the
694
+ * tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
695
+ * a promise that resolves once the user is populated (or rejects
696
+ * if the token is invalid).
697
+ *
698
+ * Internal callers (signIn, signUp, etc.) pass `_internal=true` to
699
+ * skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
700
+ * lifecycle events.
684
701
  */
685
702
  setSession(session, _internal) {
703
+ if (session === null) {
704
+ this.currentSession = null;
705
+ if (this.onSessionChange) this.onSessionChange(null);
706
+ if (!_internal) this.emit("INITIAL_SESSION", null);
707
+ return;
708
+ }
709
+ if (!("user" in session) || !session.user) {
710
+ const tokens = {
711
+ access_token: session.access_token,
712
+ refresh_token: session.refresh_token,
713
+ token_type: session.token_type ?? "bearer",
714
+ expires_in: session.expires_in ?? 3600,
715
+ expires_at: session.expires_at ?? Math.floor(Date.now() / 1e3) + (session.expires_in ?? 3600),
716
+ user: null
717
+ };
718
+ this.currentSession = tokens;
719
+ if (this.onSessionChange) this.onSessionChange(tokens);
720
+ return (async () => {
721
+ const { data, error } = await this.getUser();
722
+ if (error || !data.user) {
723
+ this.currentSession = null;
724
+ if (this.onSessionChange) this.onSessionChange(null);
725
+ return { data: null, error: error ?? { message: "User lookup failed" } };
726
+ }
727
+ const full = { ...tokens, user: data.user };
728
+ this.currentSession = full;
729
+ if (this.onSessionChange) this.onSessionChange(full);
730
+ if (!_internal) this.emit("SIGNED_IN", full);
731
+ return { data: full, error: null };
732
+ })();
733
+ }
686
734
  this.currentSession = session;
687
735
  if (this.onSessionChange) this.onSessionChange(session);
688
736
  if (!_internal) {
689
- if (session && session.expires_at <= Math.floor(Date.now() / 1e3)) {
737
+ if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
690
738
  this.refreshSession().then(({ error }) => {
691
739
  if (error) {
692
740
  this.currentSession = null;
@@ -762,8 +810,10 @@ var AuthClient = class {
762
810
  // ─── OAuth ─────────────────────────────────────────────────
763
811
  signInWithOAuth(params) {
764
812
  const queryParams = new URLSearchParams({ provider: params.provider });
813
+ if (this._apiKey) queryParams.set("apikey", this._apiKey);
765
814
  if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
766
- const url = `/auth/v1/authorize?${queryParams}`;
815
+ const base = this._baseUrl.replace(/\/$/, "");
816
+ const url = `${base}/auth/v1/authorize?${queryParams}`;
767
817
  if (typeof window !== "undefined") {
768
818
  window.location.href = url;
769
819
  }
@@ -792,9 +842,25 @@ var AuthClient = class {
792
842
  }
793
843
  return { data: { session: null }, error: null };
794
844
  }
795
- async getUser() {
845
+ /**
846
+ * Install an OAuth / magic-link session from the URL hash.
847
+ * Thin wrapper around `setSession(tokens)` kept for the auto-detect path
848
+ * in createClient. New consumers should just call `setSession()` directly.
849
+ */
850
+ async _ingestHashTokens(tokens) {
851
+ const result = this.setSession(tokens);
852
+ if (result instanceof Promise) await result;
853
+ }
854
+ /**
855
+ * Fetch the current user.
856
+ * @param jwt Optional access token to use instead of the stored session.
857
+ * Useful for validating a JWT without installing it as the active session
858
+ * (e.g. admin tooling, custom auth bridges).
859
+ */
860
+ async getUser(jwt) {
796
861
  try {
797
- const res = await this.request("/auth/v1/user");
862
+ const options = jwt ? { headers: { Authorization: `Bearer ${jwt}` } } : {};
863
+ const res = await this.request("/auth/v1/user", options);
798
864
  const data = await res.json();
799
865
  if (!res.ok) return { data: { user: null }, error: data };
800
866
  return { data: { user: data }, error: null };
@@ -1127,6 +1193,7 @@ function createClient(config) {
1127
1193
  }
1128
1194
  const authClient = new AuthClient(request);
1129
1195
  authClient._apiKey = apiKey;
1196
+ authClient._baseUrl = baseUrl;
1130
1197
  const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
1131
1198
  const sessionStorage = config.auth?.storage;
1132
1199
  const sessionKey = config.auth?.storageKey || "@linabase/session";
@@ -1151,10 +1218,41 @@ function createClient(config) {
1151
1218
  }
1152
1219
  });
1153
1220
  }
1221
+ const detectSessionInUrl = config.auth?.detectSessionInUrl !== false;
1222
+ if (detectSessionInUrl && typeof window !== "undefined" && window.location?.hash) {
1223
+ const pending = authClient._restorePromise;
1224
+ authClient._restorePromise = Promise.resolve(pending).then(async () => {
1225
+ const hash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
1226
+ const params = new URLSearchParams(hash);
1227
+ const access_token = params.get("access_token");
1228
+ const refresh_token = params.get("refresh_token");
1229
+ if (!access_token || !refresh_token) return;
1230
+ const expires_in = Number(params.get("expires_in") || 3600);
1231
+ const expires_at = Number(
1232
+ params.get("expires_at") || Math.floor(Date.now() / 1e3) + expires_in
1233
+ );
1234
+ const ingest = authClient._ingestHashTokens({
1235
+ access_token,
1236
+ refresh_token,
1237
+ token_type: params.get("token_type") || "bearer",
1238
+ expires_in,
1239
+ expires_at
1240
+ });
1241
+ try {
1242
+ const url = new URL(window.location.href);
1243
+ url.hash = "";
1244
+ window.history.replaceState(null, "", url.toString());
1245
+ } catch {
1246
+ }
1247
+ await ingest;
1248
+ });
1249
+ }
1154
1250
  function buildClient(reqFn, branchSlug) {
1155
1251
  const rc = new RpcClient(reqFn);
1156
1252
  const ac = branchSlug ? new AuthClient(reqFn) : authClient;
1157
1253
  if (branchSlug) {
1254
+ ac._apiKey = apiKey;
1255
+ ac._baseUrl = baseUrl;
1158
1256
  ac.onSessionChange = (session) => {
1159
1257
  accessToken = session?.access_token || null;
1160
1258
  };
@@ -1203,6 +1301,20 @@ function createClient(config) {
1203
1301
  }
1204
1302
  };
1205
1303
  }
1304
+ if (config.branch) {
1305
+ let pinnedRequest2 = function(path, options = {}) {
1306
+ return request(path, {
1307
+ ...options,
1308
+ headers: {
1309
+ ...options.headers,
1310
+ "X-Branch": pinned
1311
+ }
1312
+ });
1313
+ };
1314
+ var pinnedRequest = pinnedRequest2;
1315
+ const pinned = config.branch;
1316
+ return buildClient(pinnedRequest2, pinned);
1317
+ }
1206
1318
  return buildClient(request);
1207
1319
  }
1208
1320
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linabase/js",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "JavaScript/TypeScript client SDK for Linabase (database, storage, auth)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,24 +35,23 @@
35
35
  "directory": "packages/sdk-js"
36
36
  },
37
37
  "homepage": "https://linabase.com",
38
+ "devDependencies": {
39
+ "@types/pg": "^8.11.0",
40
+ "@vitest/coverage-v8": "^3.2.4",
41
+ "pg": "^8.13.0",
42
+ "tsup": "^8.3.0",
43
+ "typescript": "^5.7.0",
44
+ "vitest": "^3.2.1",
45
+ "@linabase/rest-api": "0.0.1",
46
+ "@linabase/db": "0.0.1"
47
+ },
38
48
  "scripts": {
39
49
  "build": "tsup",
40
50
  "dev": "tsup --watch",
41
- "prepublishOnly": "tsup",
42
51
  "test": "vitest run",
43
52
  "test:watch": "vitest",
44
53
  "test:coverage": "vitest run --coverage",
45
54
  "typecheck": "tsc --noEmit",
46
55
  "lint": "eslint src/"
47
- },
48
- "devDependencies": {
49
- "@linabase/db": "workspace:*",
50
- "@linabase/rest-api": "workspace:*",
51
- "@types/pg": "^8.11.0",
52
- "@vitest/coverage-v8": "^3.2.4",
53
- "pg": "^8.13.0",
54
- "tsup": "^8.3.0",
55
- "typescript": "^5.7.0",
56
- "vitest": "^3.2.1"
57
56
  }
58
- }
57
+ }