@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 +127 -10
- package/dist/index.d.cts +73 -7
- package/dist/index.d.ts +73 -7
- package/dist/index.js +127 -10
- package/package.json +1 -1
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
|
|
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.
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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
|
-
|
|
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.
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|