@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 +126 -9
- package/dist/index.d.cts +75 -7
- package/dist/index.d.ts +75 -7
- package/dist/index.js +126 -9
- package/package.json +12 -13
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.
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
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
|
-
|
|
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.
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
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
|
-
|
|
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.
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|