@linabase/js 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +121 -9
- package/dist/index.d.cts +73 -7
- package/dist/index.d.ts +73 -7
- package/dist/index.js +121 -9
- package/package.json +12 -13
package/dist/index.cjs
CHANGED
|
@@ -698,6 +698,12 @@ var AuthClient = class {
|
|
|
698
698
|
_restorePromise = Promise.resolve();
|
|
699
699
|
/** API key for auth requests that shouldn't use the (possibly expired) access token. */
|
|
700
700
|
_apiKey = "";
|
|
701
|
+
/** Absolute base URL of the Linabase REST API, used to build browser-level
|
|
702
|
+
* redirect targets (signInWithOAuth) that must be resolved against the
|
|
703
|
+
* API origin, not the caller's page origin. Empty string means "resolve
|
|
704
|
+
* relative to the current page" which is only safe in server-side or
|
|
705
|
+
* test contexts. */
|
|
706
|
+
_baseUrl = "";
|
|
701
707
|
currentSession = null;
|
|
702
708
|
/**
|
|
703
709
|
* Callback that the parent LinabaseClient can set to update the Authorization
|
|
@@ -708,17 +714,59 @@ var AuthClient = class {
|
|
|
708
714
|
this.request = request;
|
|
709
715
|
}
|
|
710
716
|
/**
|
|
711
|
-
* Set (or clear) the current session.
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
717
|
+
* Set (or clear) the current session.
|
|
718
|
+
*
|
|
719
|
+
* Three valid shapes:
|
|
720
|
+
* - `null` — clear the session.
|
|
721
|
+
* - Full `AuthSession` — restore a persisted session (e.g. from
|
|
722
|
+
* AsyncStorage). Auto-refreshes if the token is already expired.
|
|
723
|
+
* - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
|
|
724
|
+
* pattern. Used by OAuth / magic-link callbacks that only have
|
|
725
|
+
* tokens in the URL hash and no user object. The SDK installs the
|
|
726
|
+
* tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
|
|
727
|
+
* a promise that resolves once the user is populated (or rejects
|
|
728
|
+
* if the token is invalid).
|
|
729
|
+
*
|
|
730
|
+
* Internal callers (signIn, signUp, etc.) pass `_internal=true` to
|
|
731
|
+
* skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
|
|
732
|
+
* lifecycle events.
|
|
716
733
|
*/
|
|
717
734
|
setSession(session, _internal) {
|
|
735
|
+
if (session === null) {
|
|
736
|
+
this.currentSession = null;
|
|
737
|
+
if (this.onSessionChange) this.onSessionChange(null);
|
|
738
|
+
if (!_internal) this.emit("INITIAL_SESSION", null);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (!("user" in session) || !session.user) {
|
|
742
|
+
const tokens = {
|
|
743
|
+
access_token: session.access_token,
|
|
744
|
+
refresh_token: session.refresh_token,
|
|
745
|
+
token_type: session.token_type ?? "bearer",
|
|
746
|
+
expires_in: session.expires_in ?? 3600,
|
|
747
|
+
expires_at: session.expires_at ?? Math.floor(Date.now() / 1e3) + (session.expires_in ?? 3600),
|
|
748
|
+
user: null
|
|
749
|
+
};
|
|
750
|
+
this.currentSession = tokens;
|
|
751
|
+
if (this.onSessionChange) this.onSessionChange(tokens);
|
|
752
|
+
return (async () => {
|
|
753
|
+
const { data, error } = await this.getUser();
|
|
754
|
+
if (error || !data.user) {
|
|
755
|
+
this.currentSession = null;
|
|
756
|
+
if (this.onSessionChange) this.onSessionChange(null);
|
|
757
|
+
return { data: null, error: error ?? { message: "User lookup failed" } };
|
|
758
|
+
}
|
|
759
|
+
const full = { ...tokens, user: data.user };
|
|
760
|
+
this.currentSession = full;
|
|
761
|
+
if (this.onSessionChange) this.onSessionChange(full);
|
|
762
|
+
if (!_internal) this.emit("SIGNED_IN", full);
|
|
763
|
+
return { data: full, error: null };
|
|
764
|
+
})();
|
|
765
|
+
}
|
|
718
766
|
this.currentSession = session;
|
|
719
767
|
if (this.onSessionChange) this.onSessionChange(session);
|
|
720
768
|
if (!_internal) {
|
|
721
|
-
if (session
|
|
769
|
+
if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
|
|
722
770
|
this.refreshSession().then(({ error }) => {
|
|
723
771
|
if (error) {
|
|
724
772
|
this.currentSession = null;
|
|
@@ -794,8 +842,10 @@ var AuthClient = class {
|
|
|
794
842
|
// ─── OAuth ─────────────────────────────────────────────────
|
|
795
843
|
signInWithOAuth(params) {
|
|
796
844
|
const queryParams = new URLSearchParams({ provider: params.provider });
|
|
845
|
+
if (this._apiKey) queryParams.set("apikey", this._apiKey);
|
|
797
846
|
if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
|
|
798
|
-
const
|
|
847
|
+
const base = this._baseUrl.replace(/\/$/, "");
|
|
848
|
+
const url = `${base}/auth/v1/authorize?${queryParams}`;
|
|
799
849
|
if (typeof window !== "undefined") {
|
|
800
850
|
window.location.href = url;
|
|
801
851
|
}
|
|
@@ -824,9 +874,25 @@ var AuthClient = class {
|
|
|
824
874
|
}
|
|
825
875
|
return { data: { session: null }, error: null };
|
|
826
876
|
}
|
|
827
|
-
|
|
877
|
+
/**
|
|
878
|
+
* Install an OAuth / magic-link session from the URL hash.
|
|
879
|
+
* Thin wrapper around `setSession(tokens)` kept for the auto-detect path
|
|
880
|
+
* in createClient. New consumers should just call `setSession()` directly.
|
|
881
|
+
*/
|
|
882
|
+
async _ingestHashTokens(tokens) {
|
|
883
|
+
const result = this.setSession(tokens);
|
|
884
|
+
if (result instanceof Promise) await result;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Fetch the current user.
|
|
888
|
+
* @param jwt Optional access token to use instead of the stored session.
|
|
889
|
+
* Useful for validating a JWT without installing it as the active session
|
|
890
|
+
* (e.g. admin tooling, custom auth bridges).
|
|
891
|
+
*/
|
|
892
|
+
async getUser(jwt) {
|
|
828
893
|
try {
|
|
829
|
-
const
|
|
894
|
+
const options = jwt ? { headers: { Authorization: `Bearer ${jwt}` } } : {};
|
|
895
|
+
const res = await this.request("/auth/v1/user", options);
|
|
830
896
|
const data = await res.json();
|
|
831
897
|
if (!res.ok) return { data: { user: null }, error: data };
|
|
832
898
|
return { data: { user: data }, error: null };
|
|
@@ -1159,6 +1225,7 @@ function createClient(config) {
|
|
|
1159
1225
|
}
|
|
1160
1226
|
const authClient = new AuthClient(request);
|
|
1161
1227
|
authClient._apiKey = apiKey;
|
|
1228
|
+
authClient._baseUrl = baseUrl;
|
|
1162
1229
|
const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
|
|
1163
1230
|
const sessionStorage = config.auth?.storage;
|
|
1164
1231
|
const sessionKey = config.auth?.storageKey || "@linabase/session";
|
|
@@ -1183,10 +1250,41 @@ function createClient(config) {
|
|
|
1183
1250
|
}
|
|
1184
1251
|
});
|
|
1185
1252
|
}
|
|
1253
|
+
const detectSessionInUrl = config.auth?.detectSessionInUrl !== false;
|
|
1254
|
+
if (detectSessionInUrl && typeof window !== "undefined" && window.location?.hash) {
|
|
1255
|
+
const pending = authClient._restorePromise;
|
|
1256
|
+
authClient._restorePromise = Promise.resolve(pending).then(async () => {
|
|
1257
|
+
const hash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
1258
|
+
const params = new URLSearchParams(hash);
|
|
1259
|
+
const access_token = params.get("access_token");
|
|
1260
|
+
const refresh_token = params.get("refresh_token");
|
|
1261
|
+
if (!access_token || !refresh_token) return;
|
|
1262
|
+
const expires_in = Number(params.get("expires_in") || 3600);
|
|
1263
|
+
const expires_at = Number(
|
|
1264
|
+
params.get("expires_at") || Math.floor(Date.now() / 1e3) + expires_in
|
|
1265
|
+
);
|
|
1266
|
+
const ingest = authClient._ingestHashTokens({
|
|
1267
|
+
access_token,
|
|
1268
|
+
refresh_token,
|
|
1269
|
+
token_type: params.get("token_type") || "bearer",
|
|
1270
|
+
expires_in,
|
|
1271
|
+
expires_at
|
|
1272
|
+
});
|
|
1273
|
+
try {
|
|
1274
|
+
const url = new URL(window.location.href);
|
|
1275
|
+
url.hash = "";
|
|
1276
|
+
window.history.replaceState(null, "", url.toString());
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
await ingest;
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1186
1282
|
function buildClient(reqFn, branchSlug) {
|
|
1187
1283
|
const rc = new RpcClient(reqFn);
|
|
1188
1284
|
const ac = branchSlug ? new AuthClient(reqFn) : authClient;
|
|
1189
1285
|
if (branchSlug) {
|
|
1286
|
+
ac._apiKey = apiKey;
|
|
1287
|
+
ac._baseUrl = baseUrl;
|
|
1190
1288
|
ac.onSessionChange = (session) => {
|
|
1191
1289
|
accessToken = session?.access_token || null;
|
|
1192
1290
|
};
|
|
@@ -1235,6 +1333,20 @@ function createClient(config) {
|
|
|
1235
1333
|
}
|
|
1236
1334
|
};
|
|
1237
1335
|
}
|
|
1336
|
+
if (config.branch) {
|
|
1337
|
+
let pinnedRequest2 = function(path, options = {}) {
|
|
1338
|
+
return request(path, {
|
|
1339
|
+
...options,
|
|
1340
|
+
headers: {
|
|
1341
|
+
...options.headers,
|
|
1342
|
+
"X-Branch": pinned
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
};
|
|
1346
|
+
var pinnedRequest = pinnedRequest2;
|
|
1347
|
+
const pinned = config.branch;
|
|
1348
|
+
return buildClient(pinnedRequest2, pinned);
|
|
1349
|
+
}
|
|
1238
1350
|
return buildClient(request);
|
|
1239
1351
|
}
|
|
1240
1352
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.d.cts
CHANGED
|
@@ -251,6 +251,20 @@ interface AuthSession {
|
|
|
251
251
|
expires_at: number;
|
|
252
252
|
user: AuthUser;
|
|
253
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Token-only input accepted by `setSession` when a full AuthSession isn't
|
|
256
|
+
* available yet (OAuth / magic-link callbacks, custom auth bridges). The SDK
|
|
257
|
+
* fetches the user and fills in defaults for `token_type` / `expires_in` /
|
|
258
|
+
* `expires_at` when omitted.
|
|
259
|
+
*/
|
|
260
|
+
interface SetSessionTokens {
|
|
261
|
+
access_token: string;
|
|
262
|
+
refresh_token: string;
|
|
263
|
+
token_type?: string;
|
|
264
|
+
expires_in?: number;
|
|
265
|
+
expires_at?: number;
|
|
266
|
+
user?: null;
|
|
267
|
+
}
|
|
254
268
|
interface AuthUser {
|
|
255
269
|
id: string;
|
|
256
270
|
email: string | null;
|
|
@@ -270,6 +284,12 @@ declare class AuthClient {
|
|
|
270
284
|
_restorePromise: Promise<void>;
|
|
271
285
|
/** API key for auth requests that shouldn't use the (possibly expired) access token. */
|
|
272
286
|
_apiKey: string;
|
|
287
|
+
/** Absolute base URL of the Linabase REST API, used to build browser-level
|
|
288
|
+
* redirect targets (signInWithOAuth) that must be resolved against the
|
|
289
|
+
* API origin, not the caller's page origin. Empty string means "resolve
|
|
290
|
+
* relative to the current page" which is only safe in server-side or
|
|
291
|
+
* test contexts. */
|
|
292
|
+
_baseUrl: string;
|
|
273
293
|
private currentSession;
|
|
274
294
|
/**
|
|
275
295
|
* Callback that the parent LinabaseClient can set to update the Authorization
|
|
@@ -278,13 +298,27 @@ declare class AuthClient {
|
|
|
278
298
|
onSessionChange: ((session: AuthSession | null) => void) | null;
|
|
279
299
|
constructor(request: RequestFn$1);
|
|
280
300
|
/**
|
|
281
|
-
* Set (or clear) the current session.
|
|
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
|
@@ -666,6 +666,12 @@ var AuthClient = class {
|
|
|
666
666
|
_restorePromise = Promise.resolve();
|
|
667
667
|
/** API key for auth requests that shouldn't use the (possibly expired) access token. */
|
|
668
668
|
_apiKey = "";
|
|
669
|
+
/** Absolute base URL of the Linabase REST API, used to build browser-level
|
|
670
|
+
* redirect targets (signInWithOAuth) that must be resolved against the
|
|
671
|
+
* API origin, not the caller's page origin. Empty string means "resolve
|
|
672
|
+
* relative to the current page" which is only safe in server-side or
|
|
673
|
+
* test contexts. */
|
|
674
|
+
_baseUrl = "";
|
|
669
675
|
currentSession = null;
|
|
670
676
|
/**
|
|
671
677
|
* Callback that the parent LinabaseClient can set to update the Authorization
|
|
@@ -676,17 +682,59 @@ var AuthClient = class {
|
|
|
676
682
|
this.request = request;
|
|
677
683
|
}
|
|
678
684
|
/**
|
|
679
|
-
* Set (or clear) the current session.
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
685
|
+
* Set (or clear) the current session.
|
|
686
|
+
*
|
|
687
|
+
* Three valid shapes:
|
|
688
|
+
* - `null` — clear the session.
|
|
689
|
+
* - Full `AuthSession` — restore a persisted session (e.g. from
|
|
690
|
+
* AsyncStorage). Auto-refreshes if the token is already expired.
|
|
691
|
+
* - `{ access_token, refresh_token, ... }` — token-only, Supabase v2
|
|
692
|
+
* pattern. Used by OAuth / magic-link callbacks that only have
|
|
693
|
+
* tokens in the URL hash and no user object. The SDK installs the
|
|
694
|
+
* tokens, fetches `/auth/v1/user`, and emits SIGNED_IN. Returns
|
|
695
|
+
* a promise that resolves once the user is populated (or rejects
|
|
696
|
+
* if the token is invalid).
|
|
697
|
+
*
|
|
698
|
+
* Internal callers (signIn, signUp, etc.) pass `_internal=true` to
|
|
699
|
+
* skip the INITIAL_SESSION / SIGNED_IN emit since they emit their own
|
|
700
|
+
* lifecycle events.
|
|
684
701
|
*/
|
|
685
702
|
setSession(session, _internal) {
|
|
703
|
+
if (session === null) {
|
|
704
|
+
this.currentSession = null;
|
|
705
|
+
if (this.onSessionChange) this.onSessionChange(null);
|
|
706
|
+
if (!_internal) this.emit("INITIAL_SESSION", null);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (!("user" in session) || !session.user) {
|
|
710
|
+
const tokens = {
|
|
711
|
+
access_token: session.access_token,
|
|
712
|
+
refresh_token: session.refresh_token,
|
|
713
|
+
token_type: session.token_type ?? "bearer",
|
|
714
|
+
expires_in: session.expires_in ?? 3600,
|
|
715
|
+
expires_at: session.expires_at ?? Math.floor(Date.now() / 1e3) + (session.expires_in ?? 3600),
|
|
716
|
+
user: null
|
|
717
|
+
};
|
|
718
|
+
this.currentSession = tokens;
|
|
719
|
+
if (this.onSessionChange) this.onSessionChange(tokens);
|
|
720
|
+
return (async () => {
|
|
721
|
+
const { data, error } = await this.getUser();
|
|
722
|
+
if (error || !data.user) {
|
|
723
|
+
this.currentSession = null;
|
|
724
|
+
if (this.onSessionChange) this.onSessionChange(null);
|
|
725
|
+
return { data: null, error: error ?? { message: "User lookup failed" } };
|
|
726
|
+
}
|
|
727
|
+
const full = { ...tokens, user: data.user };
|
|
728
|
+
this.currentSession = full;
|
|
729
|
+
if (this.onSessionChange) this.onSessionChange(full);
|
|
730
|
+
if (!_internal) this.emit("SIGNED_IN", full);
|
|
731
|
+
return { data: full, error: null };
|
|
732
|
+
})();
|
|
733
|
+
}
|
|
686
734
|
this.currentSession = session;
|
|
687
735
|
if (this.onSessionChange) this.onSessionChange(session);
|
|
688
736
|
if (!_internal) {
|
|
689
|
-
if (session
|
|
737
|
+
if (session.expires_at <= Math.floor(Date.now() / 1e3)) {
|
|
690
738
|
this.refreshSession().then(({ error }) => {
|
|
691
739
|
if (error) {
|
|
692
740
|
this.currentSession = null;
|
|
@@ -762,8 +810,10 @@ var AuthClient = class {
|
|
|
762
810
|
// ─── OAuth ─────────────────────────────────────────────────
|
|
763
811
|
signInWithOAuth(params) {
|
|
764
812
|
const queryParams = new URLSearchParams({ provider: params.provider });
|
|
813
|
+
if (this._apiKey) queryParams.set("apikey", this._apiKey);
|
|
765
814
|
if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
|
|
766
|
-
const
|
|
815
|
+
const base = this._baseUrl.replace(/\/$/, "");
|
|
816
|
+
const url = `${base}/auth/v1/authorize?${queryParams}`;
|
|
767
817
|
if (typeof window !== "undefined") {
|
|
768
818
|
window.location.href = url;
|
|
769
819
|
}
|
|
@@ -792,9 +842,25 @@ var AuthClient = class {
|
|
|
792
842
|
}
|
|
793
843
|
return { data: { session: null }, error: null };
|
|
794
844
|
}
|
|
795
|
-
|
|
845
|
+
/**
|
|
846
|
+
* Install an OAuth / magic-link session from the URL hash.
|
|
847
|
+
* Thin wrapper around `setSession(tokens)` kept for the auto-detect path
|
|
848
|
+
* in createClient. New consumers should just call `setSession()` directly.
|
|
849
|
+
*/
|
|
850
|
+
async _ingestHashTokens(tokens) {
|
|
851
|
+
const result = this.setSession(tokens);
|
|
852
|
+
if (result instanceof Promise) await result;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Fetch the current user.
|
|
856
|
+
* @param jwt Optional access token to use instead of the stored session.
|
|
857
|
+
* Useful for validating a JWT without installing it as the active session
|
|
858
|
+
* (e.g. admin tooling, custom auth bridges).
|
|
859
|
+
*/
|
|
860
|
+
async getUser(jwt) {
|
|
796
861
|
try {
|
|
797
|
-
const
|
|
862
|
+
const options = jwt ? { headers: { Authorization: `Bearer ${jwt}` } } : {};
|
|
863
|
+
const res = await this.request("/auth/v1/user", options);
|
|
798
864
|
const data = await res.json();
|
|
799
865
|
if (!res.ok) return { data: { user: null }, error: data };
|
|
800
866
|
return { data: { user: data }, error: null };
|
|
@@ -1127,6 +1193,7 @@ function createClient(config) {
|
|
|
1127
1193
|
}
|
|
1128
1194
|
const authClient = new AuthClient(request);
|
|
1129
1195
|
authClient._apiKey = apiKey;
|
|
1196
|
+
authClient._baseUrl = baseUrl;
|
|
1130
1197
|
const persistSession = config.auth?.persistSession !== false && !!config.auth?.storage;
|
|
1131
1198
|
const sessionStorage = config.auth?.storage;
|
|
1132
1199
|
const sessionKey = config.auth?.storageKey || "@linabase/session";
|
|
@@ -1151,10 +1218,41 @@ function createClient(config) {
|
|
|
1151
1218
|
}
|
|
1152
1219
|
});
|
|
1153
1220
|
}
|
|
1221
|
+
const detectSessionInUrl = config.auth?.detectSessionInUrl !== false;
|
|
1222
|
+
if (detectSessionInUrl && typeof window !== "undefined" && window.location?.hash) {
|
|
1223
|
+
const pending = authClient._restorePromise;
|
|
1224
|
+
authClient._restorePromise = Promise.resolve(pending).then(async () => {
|
|
1225
|
+
const hash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
1226
|
+
const params = new URLSearchParams(hash);
|
|
1227
|
+
const access_token = params.get("access_token");
|
|
1228
|
+
const refresh_token = params.get("refresh_token");
|
|
1229
|
+
if (!access_token || !refresh_token) return;
|
|
1230
|
+
const expires_in = Number(params.get("expires_in") || 3600);
|
|
1231
|
+
const expires_at = Number(
|
|
1232
|
+
params.get("expires_at") || Math.floor(Date.now() / 1e3) + expires_in
|
|
1233
|
+
);
|
|
1234
|
+
const ingest = authClient._ingestHashTokens({
|
|
1235
|
+
access_token,
|
|
1236
|
+
refresh_token,
|
|
1237
|
+
token_type: params.get("token_type") || "bearer",
|
|
1238
|
+
expires_in,
|
|
1239
|
+
expires_at
|
|
1240
|
+
});
|
|
1241
|
+
try {
|
|
1242
|
+
const url = new URL(window.location.href);
|
|
1243
|
+
url.hash = "";
|
|
1244
|
+
window.history.replaceState(null, "", url.toString());
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
await ingest;
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1154
1250
|
function buildClient(reqFn, branchSlug) {
|
|
1155
1251
|
const rc = new RpcClient(reqFn);
|
|
1156
1252
|
const ac = branchSlug ? new AuthClient(reqFn) : authClient;
|
|
1157
1253
|
if (branchSlug) {
|
|
1254
|
+
ac._apiKey = apiKey;
|
|
1255
|
+
ac._baseUrl = baseUrl;
|
|
1158
1256
|
ac.onSessionChange = (session) => {
|
|
1159
1257
|
accessToken = session?.access_token || null;
|
|
1160
1258
|
};
|
|
@@ -1203,6 +1301,20 @@ function createClient(config) {
|
|
|
1203
1301
|
}
|
|
1204
1302
|
};
|
|
1205
1303
|
}
|
|
1304
|
+
if (config.branch) {
|
|
1305
|
+
let pinnedRequest2 = function(path, options = {}) {
|
|
1306
|
+
return request(path, {
|
|
1307
|
+
...options,
|
|
1308
|
+
headers: {
|
|
1309
|
+
...options.headers,
|
|
1310
|
+
"X-Branch": pinned
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
};
|
|
1314
|
+
var pinnedRequest = pinnedRequest2;
|
|
1315
|
+
const pinned = config.branch;
|
|
1316
|
+
return buildClient(pinnedRequest2, pinned);
|
|
1317
|
+
}
|
|
1206
1318
|
return buildClient(request);
|
|
1207
1319
|
}
|
|
1208
1320
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linabase/js",
|
|
3
|
-
"version": "0.
|
|
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
|
+
}
|