@linabase/js 0.4.4 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -52,7 +52,12 @@ var DatabaseClient = class {
52
52
  // ─── Query Methods ──────────────────────────────────────
53
53
  /** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
54
54
  select(columns, options) {
55
- this.method = options?.head ? "HEAD" : "GET";
55
+ const isMutation = this.method === "POST" || this.method === "PATCH" || this.method === "DELETE";
56
+ if (!isMutation) {
57
+ this.method = options?.head ? "HEAD" : "GET";
58
+ } else if (!this.preferHeaders.some((h) => h.startsWith("return="))) {
59
+ this.preferHeaders.push("return=representation");
60
+ }
56
61
  if (columns && columns !== "*") this.params.set("select", columns);
57
62
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
58
63
  return this;
@@ -698,6 +703,12 @@ var AuthClient = class {
698
703
  _restorePromise = Promise.resolve();
699
704
  /** API key for auth requests that shouldn't use the (possibly expired) access token. */
700
705
  _apiKey = "";
706
+ /** Absolute base URL of the Linabase REST API, used to build browser-level
707
+ * redirect targets (signInWithOAuth) that must be resolved against the
708
+ * API origin, not the caller's page origin. Empty string means "resolve
709
+ * relative to the current page" which is only safe in server-side or
710
+ * test contexts. */
711
+ _baseUrl = "";
701
712
  currentSession = null;
702
713
  /**
703
714
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -708,17 +719,59 @@ var AuthClient = class {
708
719
  this.request = request;
709
720
  }
710
721
  /**
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.
722
+ * Set (or clear) the current session.
723
+ *
724
+ * Three valid shapes:
725
+ * - `null` clear the session.
726
+ * - Full `AuthSession` restore a persisted session (e.g. from
727
+ * AsyncStorage). Auto-refreshes if the token is already expired.
728
+ * - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
729
+ * pattern. Used by OAuth / magic-link callbacks that only have
730
+ * tokens in the URL hash and no user object. The SDK installs the
731
+ * tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
732
+ * a promise that resolves once the user is populated (or rejects
733
+ * if the token is invalid).
734
+ *
735
+ * Internal callers (signIn, signUp, etc.) pass `_internal=true` to
736
+ * skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
737
+ * lifecycle events.
716
738
  */
717
739
  setSession(session, _internal) {
740
+ if (session === null) {
741
+ this.currentSession = null;
742
+ if (this.onSessionChange) this.onSessionChange(null);
743
+ if (!_internal) this.emit("INITIAL_SESSION", null);
744
+ return;
745
+ }
746
+ if (!("user" in session) || !session.user) {
747
+ const tokens = {
748
+ access_token: session.access_token,
749
+ refresh_token: session.refresh_token,
750
+ token_type: session.token_type ?? "bearer",
751
+ expires_in: session.expires_in ?? 3600,
752
+ expires_at: session.expires_at ?? Math.floor(Date.now() / 1e3) + (session.expires_in ?? 3600),
753
+ user: null
754
+ };
755
+ this.currentSession = tokens;
756
+ if (this.onSessionChange) this.onSessionChange(tokens);
757
+ return (async () => {
758
+ const { data, error } = await this.getUser();
759
+ if (error || !data.user) {
760
+ this.currentSession = null;
761
+ if (this.onSessionChange) this.onSessionChange(null);
762
+ return { data: null, error: error ?? { message: "User lookup failed" } };
763
+ }
764
+ const full = { ...tokens, user: data.user };
765
+ this.currentSession = full;
766
+ if (this.onSessionChange) this.onSessionChange(full);
767
+ if (!_internal) this.emit("SIGNED_IN", full);
768
+ return { data: full, error: null };
769
+ })();
770
+ }
718
771
  this.currentSession = session;
719
772
  if (this.onSessionChange) this.onSessionChange(session);
720
773
  if (!_internal) {
721
- if (session && session.expires_at <= Math.floor(Date.now() / 1e3)) {
774
+ if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
722
775
  this.refreshSession().then(({ error }) => {
723
776
  if (error) {
724
777
  this.currentSession = null;
@@ -794,8 +847,10 @@ var AuthClient = class {
794
847
  // ─── OAuth ─────────────────────────────────────────────────
795
848
  signInWithOAuth(params) {
796
849
  const queryParams = new URLSearchParams({ provider: params.provider });
850
+ if (this._apiKey) queryParams.set("apikey", this._apiKey);
797
851
  if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
798
- const url = `/auth/v1/authorize?${queryParams}`;
852
+ const base = this._baseUrl.replace(/\/$/, "");
853
+ const url = `${base}/auth/v1/authorize?${queryParams}`;
799
854
  if (typeof window !== "undefined") {
800
855
  window.location.href = url;
801
856
  }
@@ -824,9 +879,25 @@ var AuthClient = class {
824
879
  }
825
880
  return { data: { session: null }, error: null };
826
881
  }
827
- async getUser() {
882
+ /**
883
+ * Install an OAuth / magic-link session from the URL hash.
884
+ * Thin wrapper around `setSession(tokens)` kept for the auto-detect path
885
+ * in createClient. New consumers should just call `setSession()` directly.
886
+ */
887
+ async _ingestHashTokens(tokens) {
888
+ const result = this.setSession(tokens);
889
+ if (result instanceof Promise) await result;
890
+ }
891
+ /**
892
+ * Fetch the current user.
893
+ * @param jwt Optional access token to use instead of the stored session.
894
+ * Useful for validating a JWT without installing it as the active session
895
+ * (e.g. admin tooling, custom auth bridges).
896
+ */
897
+ async getUser(jwt) {
828
898
  try {
829
- const res = await this.request("/auth/v1/user");
899
+ const options = jwt ? { headers: { Authorization: `Bearer ${jwt}` } } : {};
900
+ const res = await this.request("/auth/v1/user", options);
830
901
  const data = await res.json();
831
902
  if (!res.ok) return { data: { user: null }, error: data };
832
903
  return { data: { user: data }, error: null };
@@ -1159,6 +1230,7 @@ function createClient(config) {
1159
1230
  }
1160
1231
  const authClient = new AuthClient(request);
1161
1232
  authClient._apiKey = apiKey;
1233
+ authClient._baseUrl = baseUrl;
1162
1234
  const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
1163
1235
  const sessionStorage = config.auth?.storage;
1164
1236
  const sessionKey = config.auth?.storageKey || "@linabase/session";
@@ -1183,10 +1255,41 @@ function createClient(config) {
1183
1255
  }
1184
1256
  });
1185
1257
  }
1258
+ const detectSessionInUrl = config.auth?.detectSessionInUrl !== false;
1259
+ if (detectSessionInUrl && typeof window !== "undefined" && window.location?.hash) {
1260
+ const pending = authClient._restorePromise;
1261
+ authClient._restorePromise = Promise.resolve(pending).then(async () => {
1262
+ const hash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
1263
+ const params = new URLSearchParams(hash);
1264
+ const access_token = params.get("access_token");
1265
+ const refresh_token = params.get("refresh_token");
1266
+ if (!access_token || !refresh_token) return;
1267
+ const expires_in = Number(params.get("expires_in") || 3600);
1268
+ const expires_at = Number(
1269
+ params.get("expires_at") || Math.floor(Date.now() / 1e3) + expires_in
1270
+ );
1271
+ const ingest = authClient._ingestHashTokens({
1272
+ access_token,
1273
+ refresh_token,
1274
+ token_type: params.get("token_type") || "bearer",
1275
+ expires_in,
1276
+ expires_at
1277
+ });
1278
+ try {
1279
+ const url = new URL(window.location.href);
1280
+ url.hash = "";
1281
+ window.history.replaceState(null, "", url.toString());
1282
+ } catch {
1283
+ }
1284
+ await ingest;
1285
+ });
1286
+ }
1186
1287
  function buildClient(reqFn, branchSlug) {
1187
1288
  const rc = new RpcClient(reqFn);
1188
1289
  const ac = branchSlug ? new AuthClient(reqFn) : authClient;
1189
1290
  if (branchSlug) {
1291
+ ac._apiKey = apiKey;
1292
+ ac._baseUrl = baseUrl;
1190
1293
  ac.onSessionChange = (session) => {
1191
1294
  accessToken = session?.access_token || null;
1192
1295
  };
@@ -1235,6 +1338,20 @@ function createClient(config) {
1235
1338
  }
1236
1339
  };
1237
1340
  }
1341
+ if (config.branch) {
1342
+ let pinnedRequest2 = function(path, options = {}) {
1343
+ return request(path, {
1344
+ ...options,
1345
+ headers: {
1346
+ ...options.headers,
1347
+ "X-Branch": pinned
1348
+ }
1349
+ });
1350
+ };
1351
+ var pinnedRequest = pinnedRequest2;
1352
+ const pinned = config.branch;
1353
+ return buildClient(pinnedRequest2, pinned);
1354
+ }
1238
1355
  return buildClient(request);
1239
1356
  }
1240
1357
  // 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
@@ -20,7 +20,12 @@ var DatabaseClient = class {
20
20
  // ─── Query Methods ──────────────────────────────────────
21
21
  /** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
22
22
  select(columns, options) {
23
- this.method = options?.head ? "HEAD" : "GET";
23
+ const isMutation = this.method === "POST" || this.method === "PATCH" || this.method === "DELETE";
24
+ if (!isMutation) {
25
+ this.method = options?.head ? "HEAD" : "GET";
26
+ } else if (!this.preferHeaders.some((h) => h.startsWith("return="))) {
27
+ this.preferHeaders.push("return=representation");
28
+ }
24
29
  if (columns && columns !== "*") this.params.set("select", columns);
25
30
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
26
31
  return this;
@@ -666,6 +671,12 @@ var AuthClient = class {
666
671
  _restorePromise = Promise.resolve();
667
672
  /** API key for auth requests that shouldn't use the (possibly expired) access token. */
668
673
  _apiKey = "";
674
+ /** Absolute base URL of the Linabase REST API, used to build browser-level
675
+ * redirect targets (signInWithOAuth) that must be resolved against the
676
+ * API origin, not the caller's page origin. Empty string means "resolve
677
+ * relative to the current page" which is only safe in server-side or
678
+ * test contexts. */
679
+ _baseUrl = "";
669
680
  currentSession = null;
670
681
  /**
671
682
  * Callback that the parent LinabaseClient can set to update the Authorization
@@ -676,17 +687,59 @@ var AuthClient = class {
676
687
  this.request = request;
677
688
  }
678
689
  /**
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.
690
+ * Set (or clear) the current session.
691
+ *
692
+ * Three valid shapes:
693
+ * - `null` clear the session.
694
+ * - Full `AuthSession` restore a persisted session (e.g. from
695
+ * AsyncStorage). Auto-refreshes if the token is already expired.
696
+ * - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
697
+ * pattern. Used by OAuth / magic-link callbacks that only have
698
+ * tokens in the URL hash and no user object. The SDK installs the
699
+ * tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
700
+ * a promise that resolves once the user is populated (or rejects
701
+ * if the token is invalid).
702
+ *
703
+ * Internal callers (signIn, signUp, etc.) pass `_internal=true` to
704
+ * skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
705
+ * lifecycle events.
684
706
  */
685
707
  setSession(session, _internal) {
708
+ if (session === null) {
709
+ this.currentSession = null;
710
+ if (this.onSessionChange) this.onSessionChange(null);
711
+ if (!_internal) this.emit("INITIAL_SESSION", null);
712
+ return;
713
+ }
714
+ if (!("user" in session) || !session.user) {
715
+ const tokens = {
716
+ access_token: session.access_token,
717
+ refresh_token: session.refresh_token,
718
+ token_type: session.token_type ?? "bearer",
719
+ expires_in: session.expires_in ?? 3600,
720
+ expires_at: session.expires_at ?? Math.floor(Date.now() / 1e3) + (session.expires_in ?? 3600),
721
+ user: null
722
+ };
723
+ this.currentSession = tokens;
724
+ if (this.onSessionChange) this.onSessionChange(tokens);
725
+ return (async () => {
726
+ const { data, error } = await this.getUser();
727
+ if (error || !data.user) {
728
+ this.currentSession = null;
729
+ if (this.onSessionChange) this.onSessionChange(null);
730
+ return { data: null, error: error ?? { message: "User lookup failed" } };
731
+ }
732
+ const full = { ...tokens, user: data.user };
733
+ this.currentSession = full;
734
+ if (this.onSessionChange) this.onSessionChange(full);
735
+ if (!_internal) this.emit("SIGNED_IN", full);
736
+ return { data: full, error: null };
737
+ })();
738
+ }
686
739
  this.currentSession = session;
687
740
  if (this.onSessionChange) this.onSessionChange(session);
688
741
  if (!_internal) {
689
- if (session && session.expires_at <= Math.floor(Date.now() / 1e3)) {
742
+ if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
690
743
  this.refreshSession().then(({ error }) => {
691
744
  if (error) {
692
745
  this.currentSession = null;
@@ -762,8 +815,10 @@ var AuthClient = class {
762
815
  // ─── OAuth ─────────────────────────────────────────────────
763
816
  signInWithOAuth(params) {
764
817
  const queryParams = new URLSearchParams({ provider: params.provider });
818
+ if (this._apiKey) queryParams.set("apikey", this._apiKey);
765
819
  if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
766
- const url = `/auth/v1/authorize?${queryParams}`;
820
+ const base = this._baseUrl.replace(/\/$/, "");
821
+ const url = `${base}/auth/v1/authorize?${queryParams}`;
767
822
  if (typeof window !== "undefined") {
768
823
  window.location.href = url;
769
824
  }
@@ -792,9 +847,25 @@ var AuthClient = class {
792
847
  }
793
848
  return { data: { session: null }, error: null };
794
849
  }
795
- async getUser() {
850
+ /**
851
+ * Install an OAuth / magic-link session from the URL hash.
852
+ * Thin wrapper around `setSession(tokens)` kept for the auto-detect path
853
+ * in createClient. New consumers should just call `setSession()` directly.
854
+ */
855
+ async _ingestHashTokens(tokens) {
856
+ const result = this.setSession(tokens);
857
+ if (result instanceof Promise) await result;
858
+ }
859
+ /**
860
+ * Fetch the current user.
861
+ * @param jwt Optional access token to use instead of the stored session.
862
+ * Useful for validating a JWT without installing it as the active session
863
+ * (e.g. admin tooling, custom auth bridges).
864
+ */
865
+ async getUser(jwt) {
796
866
  try {
797
- const res = await this.request("/auth/v1/user");
867
+ const options = jwt ? { headers: { Authorization: `Bearer ${jwt}` } } : {};
868
+ const res = await this.request("/auth/v1/user", options);
798
869
  const data = await res.json();
799
870
  if (!res.ok) return { data: { user: null }, error: data };
800
871
  return { data: { user: data }, error: null };
@@ -1127,6 +1198,7 @@ function createClient(config) {
1127
1198
  }
1128
1199
  const authClient = new AuthClient(request);
1129
1200
  authClient._apiKey = apiKey;
1201
+ authClient._baseUrl = baseUrl;
1130
1202
  const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
1131
1203
  const sessionStorage = config.auth?.storage;
1132
1204
  const sessionKey = config.auth?.storageKey || "@linabase/session";
@@ -1151,10 +1223,41 @@ function createClient(config) {
1151
1223
  }
1152
1224
  });
1153
1225
  }
1226
+ const detectSessionInUrl = config.auth?.detectSessionInUrl !== false;
1227
+ if (detectSessionInUrl && typeof window !== "undefined" && window.location?.hash) {
1228
+ const pending = authClient._restorePromise;
1229
+ authClient._restorePromise = Promise.resolve(pending).then(async () => {
1230
+ const hash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
1231
+ const params = new URLSearchParams(hash);
1232
+ const access_token = params.get("access_token");
1233
+ const refresh_token = params.get("refresh_token");
1234
+ if (!access_token || !refresh_token) return;
1235
+ const expires_in = Number(params.get("expires_in") || 3600);
1236
+ const expires_at = Number(
1237
+ params.get("expires_at") || Math.floor(Date.now() / 1e3) + expires_in
1238
+ );
1239
+ const ingest = authClient._ingestHashTokens({
1240
+ access_token,
1241
+ refresh_token,
1242
+ token_type: params.get("token_type") || "bearer",
1243
+ expires_in,
1244
+ expires_at
1245
+ });
1246
+ try {
1247
+ const url = new URL(window.location.href);
1248
+ url.hash = "";
1249
+ window.history.replaceState(null, "", url.toString());
1250
+ } catch {
1251
+ }
1252
+ await ingest;
1253
+ });
1254
+ }
1154
1255
  function buildClient(reqFn, branchSlug) {
1155
1256
  const rc = new RpcClient(reqFn);
1156
1257
  const ac = branchSlug ? new AuthClient(reqFn) : authClient;
1157
1258
  if (branchSlug) {
1259
+ ac._apiKey = apiKey;
1260
+ ac._baseUrl = baseUrl;
1158
1261
  ac.onSessionChange = (session) => {
1159
1262
  accessToken = session?.access_token || null;
1160
1263
  };
@@ -1203,6 +1306,20 @@ function createClient(config) {
1203
1306
  }
1204
1307
  };
1205
1308
  }
1309
+ if (config.branch) {
1310
+ let pinnedRequest2 = function(path, options = {}) {
1311
+ return request(path, {
1312
+ ...options,
1313
+ headers: {
1314
+ ...options.headers,
1315
+ "X-Branch": pinned
1316
+ }
1317
+ });
1318
+ };
1319
+ var pinnedRequest = pinnedRequest2;
1320
+ const pinned = config.branch;
1321
+ return buildClient(pinnedRequest2, pinned);
1322
+ }
1206
1323
  return buildClient(request);
1207
1324
  }
1208
1325
  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.1",
4
4
  "description": "JavaScript/TypeScript client SDK for Linabase (database, storage, auth)",
5
5
  "license": "MIT",
6
6
  "type": "module",