@linabase/js 0.4.3 → 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
@@ -696,6 +696,14 @@ var AuthClient = class {
696
696
  listeners = [];
697
697
  /** Promise that resolves when session restore from storage is complete. */
698
698
  _restorePromise = Promise.resolve();
699
+ /** API key for auth requests that shouldn't use the (possibly expired) access token. */
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 = "";
699
707
  currentSession = null;
700
708
  /**
701
709
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -706,17 +714,59 @@ var AuthClient = class {
706
714
  this.request = request;
707
715
  }
708
716
  /**
709
- * Set (or clear) the current session. Use this to restore a persisted
710
- * session on app launch (e.g., from AsyncStorage / SecureStore).
711
- * Automatically refreshes the token if it has expired.
712
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
713
- * 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.
714
733
  */
715
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
+ }
716
766
  this.currentSession = session;
717
767
  if (this.onSessionChange) this.onSessionChange(session);
718
768
  if (!_internal) {
719
- if (session && session.expires_at <= Math.floor(Date.now() / 1e3)) {
769
+ if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
720
770
  this.refreshSession().then(({ error }) => {
721
771
  if (error) {
722
772
  this.currentSession = null;
@@ -792,8 +842,10 @@ var AuthClient = class {
792
842
  // ─── OAuth ─────────────────────────────────────────────────
793
843
  signInWithOAuth(params) {
794
844
  const queryParams = new URLSearchParams({ provider: params.provider });
845
+ if (this._apiKey) queryParams.set("apikey", this._apiKey);
795
846
  if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
796
- const url = `/auth/v1/authorize?${queryParams}`;
847
+ const base = this._baseUrl.replace(/\/$/, "");
848
+ const url = `${base}/auth/v1/authorize?${queryParams}`;
797
849
  if (typeof window !== "undefined") {
798
850
  window.location.href = url;
799
851
  }
@@ -822,9 +874,25 @@ var AuthClient = class {
822
874
  }
823
875
  return { data: { session: null }, error: null };
824
876
  }
825
- 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) {
826
893
  try {
827
- 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);
828
896
  const data = await res.json();
829
897
  if (!res.ok) return { data: { user: null }, error: data };
830
898
  return { data: { user: data }, error: null };
@@ -839,6 +907,8 @@ var AuthClient = class {
839
907
  try {
840
908
  const res = await this.request("/auth/v1/token?grant_type=refresh_token", {
841
909
  method: "POST",
910
+ // Force API key auth instead of (possibly expired) access token
911
+ headers: { "Authorization": `Bearer ${this._apiKey}` },
842
912
  body: JSON.stringify({ refresh_token: this.currentSession.refresh_token })
843
913
  });
844
914
  const data = await res.json();
@@ -1154,6 +1224,8 @@ function createClient(config) {
1154
1224
  });
1155
1225
  }
1156
1226
  const authClient = new AuthClient(request);
1227
+ authClient._apiKey = apiKey;
1228
+ authClient._baseUrl = baseUrl;
1157
1229
  const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
1158
1230
  const sessionStorage = config.auth?.storage;
1159
1231
  const sessionKey = config.auth?.storageKey || "@linabase/session";
@@ -1178,10 +1250,41 @@ function createClient(config) {
1178
1250
  }
1179
1251
  });
1180
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
+ }
1181
1282
  function buildClient(reqFn, branchSlug) {
1182
1283
  const rc = new RpcClient(reqFn);
1183
1284
  const ac = branchSlug ? new AuthClient(reqFn) : authClient;
1184
1285
  if (branchSlug) {
1286
+ ac._apiKey = apiKey;
1287
+ ac._baseUrl = baseUrl;
1185
1288
  ac.onSessionChange = (session) => {
1186
1289
  accessToken = session?.access_token || null;
1187
1290
  };
@@ -1230,6 +1333,20 @@ function createClient(config) {
1230
1333
  }
1231
1334
  };
1232
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
+ }
1233
1350
  return buildClient(request);
1234
1351
  }
1235
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;
@@ -268,6 +282,14 @@ declare class AuthClient {
268
282
  private listeners;
269
283
  /** Promise that resolves when session restore from storage is complete. */
270
284
  _restorePromise: Promise<void>;
285
+ /** API key for auth requests that shouldn't use the (possibly expired) access token. */
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;
271
293
  private currentSession;
272
294
  /**
273
295
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -276,13 +298,27 @@ declare class AuthClient {
276
298
  onSessionChange: ((session: AuthSession | null) => void) | null;
277
299
  constructor(request: RequestFn$1);
278
300
  /**
279
- * Set (or clear) the current session. Use this to restore a persisted
280
- * session on app launch (e.g., from AsyncStorage / SecureStore).
281
- * Automatically refreshes the token if it has expired.
282
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
283
- * 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.
284
317
  */
285
- 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
+ }>;
286
322
  signUp(params: {
287
323
  email: string;
288
324
  password: string;
@@ -329,7 +365,19 @@ declare class AuthClient {
329
365
  };
330
366
  error: any;
331
367
  }>;
332
- 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<{
333
381
  data: {
334
382
  user: AuthUser | null;
335
383
  };
@@ -470,11 +518,31 @@ interface LinabaseConfig {
470
518
  url: string;
471
519
  anonKey?: string;
472
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;
473
530
  /** Persistent storage for auth sessions (e.g., AsyncStorage for React Native). */
474
531
  auth?: {
475
532
  storage?: SessionStorage;
476
533
  storageKey?: string;
477
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;
478
546
  };
479
547
  }
480
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;
@@ -268,6 +282,14 @@ declare class AuthClient {
268
282
  private listeners;
269
283
  /** Promise that resolves when session restore from storage is complete. */
270
284
  _restorePromise: Promise<void>;
285
+ /** API key for auth requests that shouldn't use the (possibly expired) access token. */
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;
271
293
  private currentSession;
272
294
  /**
273
295
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -276,13 +298,27 @@ declare class AuthClient {
276
298
  onSessionChange: ((session: AuthSession | null) => void) | null;
277
299
  constructor(request: RequestFn$1);
278
300
  /**
279
- * Set (or clear) the current session. Use this to restore a persisted
280
- * session on app launch (e.g., from AsyncStorage / SecureStore).
281
- * Automatically refreshes the token if it has expired.
282
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
283
- * 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.
284
317
  */
285
- 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
+ }>;
286
322
  signUp(params: {
287
323
  email: string;
288
324
  password: string;
@@ -329,7 +365,19 @@ declare class AuthClient {
329
365
  };
330
366
  error: any;
331
367
  }>;
332
- 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<{
333
381
  data: {
334
382
  user: AuthUser | null;
335
383
  };
@@ -470,11 +518,31 @@ interface LinabaseConfig {
470
518
  url: string;
471
519
  anonKey?: string;
472
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;
473
530
  /** Persistent storage for auth sessions (e.g., AsyncStorage for React Native). */
474
531
  auth?: {
475
532
  storage?: SessionStorage;
476
533
  storageKey?: string;
477
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;
478
546
  };
479
547
  }
480
548
  interface LinabaseClient {
package/dist/index.js CHANGED
@@ -664,6 +664,14 @@ var AuthClient = class {
664
664
  listeners = [];
665
665
  /** Promise that resolves when session restore from storage is complete. */
666
666
  _restorePromise = Promise.resolve();
667
+ /** API key for auth requests that shouldn't use the (possibly expired) access token. */
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 = "";
667
675
  currentSession = null;
668
676
  /**
669
677
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -674,17 +682,59 @@ var AuthClient = class {
674
682
  this.request = request;
675
683
  }
676
684
  /**
677
- * Set (or clear) the current session. Use this to restore a persisted
678
- * session on app launch (e.g., from AsyncStorage / SecureStore).
679
- * Automatically refreshes the token if it has expired.
680
- * Emits INITIAL_SESSION to onAuthStateChange listeners when called externally.
681
- * 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.
682
701
  */
683
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
+ }
684
734
  this.currentSession = session;
685
735
  if (this.onSessionChange) this.onSessionChange(session);
686
736
  if (!_internal) {
687
- if (session && session.expires_at <= Math.floor(Date.now() / 1e3)) {
737
+ if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
688
738
  this.refreshSession().then(({ error }) => {
689
739
  if (error) {
690
740
  this.currentSession = null;
@@ -760,8 +810,10 @@ var AuthClient = class {
760
810
  // ─── OAuth ─────────────────────────────────────────────────
761
811
  signInWithOAuth(params) {
762
812
  const queryParams = new URLSearchParams({ provider: params.provider });
813
+ if (this._apiKey) queryParams.set("apikey", this._apiKey);
763
814
  if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
764
- const url = `/auth/v1/authorize?${queryParams}`;
815
+ const base = this._baseUrl.replace(/\/$/, "");
816
+ const url = `${base}/auth/v1/authorize?${queryParams}`;
765
817
  if (typeof window !== "undefined") {
766
818
  window.location.href = url;
767
819
  }
@@ -790,9 +842,25 @@ var AuthClient = class {
790
842
  }
791
843
  return { data: { session: null }, error: null };
792
844
  }
793
- 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) {
794
861
  try {
795
- 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);
796
864
  const data = await res.json();
797
865
  if (!res.ok) return { data: { user: null }, error: data };
798
866
  return { data: { user: data }, error: null };
@@ -807,6 +875,8 @@ var AuthClient = class {
807
875
  try {
808
876
  const res = await this.request("/auth/v1/token?grant_type=refresh_token", {
809
877
  method: "POST",
878
+ // Force API key auth instead of (possibly expired) access token
879
+ headers: { "Authorization": `Bearer ${this._apiKey}` },
810
880
  body: JSON.stringify({ refresh_token: this.currentSession.refresh_token })
811
881
  });
812
882
  const data = await res.json();
@@ -1122,6 +1192,8 @@ function createClient(config) {
1122
1192
  });
1123
1193
  }
1124
1194
  const authClient = new AuthClient(request);
1195
+ authClient._apiKey = apiKey;
1196
+ authClient._baseUrl = baseUrl;
1125
1197
  const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
1126
1198
  const sessionStorage = config.auth?.storage;
1127
1199
  const sessionKey = config.auth?.storageKey || "@linabase/session";
@@ -1146,10 +1218,41 @@ function createClient(config) {
1146
1218
  }
1147
1219
  });
1148
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
+ }
1149
1250
  function buildClient(reqFn, branchSlug) {
1150
1251
  const rc = new RpcClient(reqFn);
1151
1252
  const ac = branchSlug ? new AuthClient(reqFn) : authClient;
1152
1253
  if (branchSlug) {
1254
+ ac._apiKey = apiKey;
1255
+ ac._baseUrl = baseUrl;
1153
1256
  ac.onSessionChange = (session) => {
1154
1257
  accessToken = session?.access_token || null;
1155
1258
  };
@@ -1198,6 +1301,20 @@ function createClient(config) {
1198
1301
  }
1199
1302
  };
1200
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
+ }
1201
1318
  return buildClient(request);
1202
1319
  }
1203
1320
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linabase/js",
3
- "version": "0.4.3",
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
+ }