@rixl/sdk 0.9.0 → 0.9.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.d.ts +7 -2
- package/dist/index.js +826 -610
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { atom } from "nanostores";
|
|
2
2
|
import { decodeJwt } from "jose";
|
|
3
3
|
import * as v from "valibot";
|
|
4
|
-
import "ky";
|
|
5
4
|
|
|
6
5
|
//#region src/generated/core/bodySerializer.gen.ts
|
|
7
6
|
const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
@@ -352,9 +351,7 @@ const setAuthParams = async ({ security, ...options }) => {
|
|
|
352
351
|
case "cookie":
|
|
353
352
|
options.headers.append("Cookie", `${name}=${token}`);
|
|
354
353
|
break;
|
|
355
|
-
default:
|
|
356
|
-
options.headers.set(name, token);
|
|
357
|
-
break;
|
|
354
|
+
default: options.headers.set(name, token);
|
|
358
355
|
}
|
|
359
356
|
}
|
|
360
357
|
};
|
|
@@ -515,9 +512,7 @@ const createClient = (config = {}) => {
|
|
|
515
512
|
case "stream":
|
|
516
513
|
emptyData = response.body;
|
|
517
514
|
break;
|
|
518
|
-
default:
|
|
519
|
-
emptyData = {};
|
|
520
|
-
break;
|
|
515
|
+
default: emptyData = {};
|
|
521
516
|
}
|
|
522
517
|
return opts.responseStyle === "data" ? emptyData : {
|
|
523
518
|
data: emptyData,
|
|
@@ -2324,6 +2319,15 @@ function validateOAuthState(provider, state) {
|
|
|
2324
2319
|
return sessionStorage.getItem(storagePath(provider)) === state;
|
|
2325
2320
|
}
|
|
2326
2321
|
/**
|
|
2322
|
+
* Discards the stored state for a provider so the next login round-trip mints a
|
|
2323
|
+
* fresh one. The state doubles as the OAuth `nonce`, so reusing it across logins
|
|
2324
|
+
* would replay a nonce the provider has already issued a token for.
|
|
2325
|
+
* @param provider The provider identifier
|
|
2326
|
+
*/
|
|
2327
|
+
function clearOauthState(provider) {
|
|
2328
|
+
sessionStorage.removeItem(storagePath(provider));
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2327
2331
|
* Gets or Generates a complete state parameter for OAuth requests
|
|
2328
2332
|
* @param provider The identifier for the provider (e.g., 'google', 'apple')
|
|
2329
2333
|
* @returns A state string containing the provider identifier and random data
|
|
@@ -2347,9 +2351,10 @@ function generateOauthState(provider) {
|
|
|
2347
2351
|
* Builds an OAuth URL from configuration and metadata
|
|
2348
2352
|
*/
|
|
2349
2353
|
const buildOAuthUrl = (config, metadata, state) => {
|
|
2354
|
+
const redirectUri = config.redirectUri ?? window.location.origin;
|
|
2350
2355
|
const params = new URLSearchParams({
|
|
2351
2356
|
client_id: config.clientId,
|
|
2352
|
-
redirect_uri:
|
|
2357
|
+
redirect_uri: redirectUri,
|
|
2353
2358
|
response_type: metadata.responseType,
|
|
2354
2359
|
scope: [...metadata.defaultScopes, ...config.scope ? [config.scope] : []].join(" "),
|
|
2355
2360
|
state
|
|
@@ -2372,8 +2377,8 @@ const warnProviderNotConfigured = (providerName) => {
|
|
|
2372
2377
|
* This factory reduces code duplication across OAuth providers
|
|
2373
2378
|
*/
|
|
2374
2379
|
const createOAuthProvider = ({ provider, metadata }) => {
|
|
2375
|
-
const config = atom(null);
|
|
2376
|
-
const authUrl = atom(null);
|
|
2380
|
+
const config = shared(`provider.${provider}.config`, () => atom(null));
|
|
2381
|
+
const authUrl = shared(`provider.${provider}.authUrl`, () => atom(null));
|
|
2377
2382
|
const updateAuthUrl = () => {
|
|
2378
2383
|
const currentConfig = config.get();
|
|
2379
2384
|
if (!currentConfig) {
|
|
@@ -2449,8 +2454,8 @@ const updateAppleAuthUrl = appleProvider.updateAuthUrl;
|
|
|
2449
2454
|
|
|
2450
2455
|
//#endregion
|
|
2451
2456
|
//#region src/auth/providers/telegram.ts
|
|
2452
|
-
const telegramConfig = atom(null);
|
|
2453
|
-
const telegramAuthUrl = atom(null);
|
|
2457
|
+
const telegramConfig = shared("provider.telegram.config", () => atom(null));
|
|
2458
|
+
const telegramAuthUrl = shared("provider.telegram.authUrl", () => atom(null));
|
|
2454
2459
|
/**
|
|
2455
2460
|
* Updates the Telegram authentication URL using the configured settings
|
|
2456
2461
|
*/
|
|
@@ -2483,7 +2488,7 @@ function extractProviderFromState(state) {
|
|
|
2483
2488
|
const parts = state.split("_");
|
|
2484
2489
|
if (parts.length >= 1) return parts[0];
|
|
2485
2490
|
}
|
|
2486
|
-
const OAUTH_PROVIDERS = [
|
|
2491
|
+
const OAUTH_PROVIDERS$1 = [
|
|
2487
2492
|
"google",
|
|
2488
2493
|
"apple",
|
|
2489
2494
|
"microsoft"
|
|
@@ -2499,7 +2504,7 @@ function detectProvider() {
|
|
|
2499
2504
|
const state = urlParams.get("state");
|
|
2500
2505
|
if (id_token && state) {
|
|
2501
2506
|
const providerFromState = extractProviderFromState(state);
|
|
2502
|
-
return OAUTH_PROVIDERS.find((p) => providerFromState === p && validateOAuthState(p, state));
|
|
2507
|
+
return OAUTH_PROVIDERS$1.find((p) => providerFromState === p && validateOAuthState(p, state));
|
|
2503
2508
|
}
|
|
2504
2509
|
}
|
|
2505
2510
|
/**
|
|
@@ -2513,20 +2518,210 @@ function getProviderToken(provider) {
|
|
|
2513
2518
|
}
|
|
2514
2519
|
|
|
2515
2520
|
//#endregion
|
|
2516
|
-
//#region src/auth/
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2521
|
+
//#region src/auth/providers/callback.ts
|
|
2522
|
+
/** Params that carry the credential itself — their presence marks a callback response. */
|
|
2523
|
+
const CREDENTIAL_PARAMS = [
|
|
2524
|
+
"id_token",
|
|
2525
|
+
"code",
|
|
2526
|
+
"access_token",
|
|
2527
|
+
"tgAuthResult",
|
|
2528
|
+
"tgWebAppData"
|
|
2529
|
+
];
|
|
2530
|
+
/** Everything the providers append to the redirect URL, credential and bookkeeping alike. */
|
|
2531
|
+
const OAUTH_RESPONSE_PARAMS = [
|
|
2532
|
+
...CREDENTIAL_PARAMS,
|
|
2533
|
+
"state",
|
|
2534
|
+
"token_type",
|
|
2535
|
+
"expires_in",
|
|
2536
|
+
"scope",
|
|
2537
|
+
"session_state",
|
|
2538
|
+
"authuser",
|
|
2539
|
+
"prompt",
|
|
2540
|
+
"hd"
|
|
2541
|
+
];
|
|
2542
|
+
/**
|
|
2543
|
+
* Whether the URL this page loaded with carries a provider response at all —
|
|
2544
|
+
* unlike `detectProvider()`, this does not require the state to validate. The
|
|
2545
|
+
* gap between the two is the case where a login round-trip came back but cannot
|
|
2546
|
+
* be used, which otherwise ends the flow in silence.
|
|
2547
|
+
*/
|
|
2548
|
+
const hasProviderResponse = () => CREDENTIAL_PARAMS.some((key) => urlParams.has(key));
|
|
2549
|
+
const AUTH_URL_UPDATERS = {
|
|
2550
|
+
["google"]: updateGoogleAuthUrl,
|
|
2551
|
+
["apple"]: updateAppleAuthUrl,
|
|
2552
|
+
["microsoft"]: updateMicrosoftAuthUrl
|
|
2553
|
+
};
|
|
2554
|
+
/** Returns undefined when the half holds no credential, meaning it is the app's own and must not be touched. */
|
|
2555
|
+
const stripped = (query) => {
|
|
2556
|
+
const params = new URLSearchParams(query);
|
|
2557
|
+
if (!CREDENTIAL_PARAMS.some((key) => params.has(key))) return void 0;
|
|
2558
|
+
OAUTH_RESPONSE_PARAMS.forEach((key) => params.delete(key));
|
|
2559
|
+
return params.toString();
|
|
2560
|
+
};
|
|
2561
|
+
/**
|
|
2562
|
+
* Providers put their response in the query string or the fragment depending on
|
|
2563
|
+
* `response_mode`, and either half may also hold params the app itself put
|
|
2564
|
+
* there. Only the half actually carrying the credential is rewritten.
|
|
2565
|
+
*/
|
|
2566
|
+
const withoutOAuthResponse = (href) => {
|
|
2567
|
+
const url = new URL(href);
|
|
2568
|
+
url.search = stripped(url.search) ?? url.search;
|
|
2569
|
+
url.hash = stripped(url.hash.slice(1)) ?? url.hash;
|
|
2570
|
+
return url.toString();
|
|
2571
|
+
};
|
|
2572
|
+
/**
|
|
2573
|
+
* Retires the provider response after `initClient` is done with it.
|
|
2574
|
+
*
|
|
2575
|
+
* Left in place, the credential stays in the address bar and every reload — or
|
|
2576
|
+
* Vite's HMR full reload — replays an already-consumed `id_token`, which the
|
|
2577
|
+
* gateway rejects. Clearing the stored state on the way out also stops the next
|
|
2578
|
+
* login from reusing this round-trip's `nonce`.
|
|
2579
|
+
*
|
|
2580
|
+
* The `urlParams` snapshot is taken at import time and is intentionally left
|
|
2581
|
+
* untouched, so callers that already read the credential out of it keep working
|
|
2582
|
+
* for the rest of this page load.
|
|
2583
|
+
*/
|
|
2584
|
+
const completeOAuthCallback = () => {
|
|
2585
|
+
const provider = detectProvider();
|
|
2586
|
+
if (!provider) return;
|
|
2587
|
+
clearOauthState(provider);
|
|
2588
|
+
AUTH_URL_UPDATERS[provider]?.();
|
|
2589
|
+
window.history.replaceState(window.history.state, "", withoutOAuthResponse(window.location.href));
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
//#endregion
|
|
2593
|
+
//#region src/auth/api/types.ts
|
|
2594
|
+
/**
|
|
2595
|
+
* Generic API error class - wraps ky's HTTPError for consistency
|
|
2596
|
+
*/
|
|
2597
|
+
var ApiError = class extends Error {
|
|
2598
|
+
status;
|
|
2599
|
+
endpoint;
|
|
2600
|
+
data;
|
|
2601
|
+
constructor(message, ...rest) {
|
|
2602
|
+
super(message);
|
|
2603
|
+
this.name = "ApiError";
|
|
2604
|
+
const firstArg = rest[0];
|
|
2605
|
+
if (typeof firstArg === "number") {
|
|
2606
|
+
this.status = firstArg;
|
|
2607
|
+
this.endpoint = rest[1] ?? "";
|
|
2608
|
+
this.data = rest[2];
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
this.status = firstArg.status;
|
|
2612
|
+
this.endpoint = firstArg.endpoint;
|
|
2613
|
+
this.data = firstArg.data;
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
//#endregion
|
|
2618
|
+
//#region src/auth/providers/diagnostics.ts
|
|
2619
|
+
const claimsOf = (token) => {
|
|
2620
|
+
try {
|
|
2621
|
+
const { iss, aud, exp, nonce, email } = decodeJwt(token);
|
|
2622
|
+
return {
|
|
2623
|
+
iss: String(iss ?? ""),
|
|
2624
|
+
aud: String(aud ?? ""),
|
|
2625
|
+
exp: exp ? (/* @__PURE__ */ new Date(Number(exp) * 1e3)).toISOString() : void 0,
|
|
2626
|
+
expired: typeof exp === "number" && Date.now() >= exp * 1e3,
|
|
2627
|
+
nonce: String(nonce ?? ""),
|
|
2628
|
+
email: String(email ?? "")
|
|
2629
|
+
};
|
|
2630
|
+
} catch {
|
|
2631
|
+
return { note: "not a JWT — expected for Telegram, unexpected for Google/Apple/Microsoft" };
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
const verdictFor = (error) => {
|
|
2635
|
+
if ((typeof error.data === "object" && error.data !== null ? error.data : void 0)?.error === "invalid token type") return "CLIENT: the gateway does not accept this token_type. Expected one of Bearer, google, apple, microsoft, tgAuthResult, tgWebAppData.";
|
|
2636
|
+
if (error.status === HTTP_STATUS.UNAUTHORIZED) return "BACKEND or CONFIG: the gateway could not verify the credential. Compare `aud` above with the client ID the gateway is configured with, and check `expired`.";
|
|
2637
|
+
if (error.status === HTTP_STATUS.CONFLICT) return "EXPECTED: this email already belongs to an account created with a different provider.";
|
|
2638
|
+
if (error.status === HTTP_STATUS.BAD_REQUEST) return "BACKEND: the gateway rejected the request body. Read `response.body` for its reason.";
|
|
2639
|
+
return "UNKNOWN: see `response` below.";
|
|
2640
|
+
};
|
|
2641
|
+
const describe = (error) => {
|
|
2642
|
+
if (!(error instanceof ApiError)) return {
|
|
2643
|
+
response: {
|
|
2644
|
+
status: "none — the request never completed",
|
|
2645
|
+
detail: String(error)
|
|
2646
|
+
},
|
|
2647
|
+
verdict: "NETWORK or CORS: the browser never got a response. Check the gateway is reachable and its CORS headers allow this origin."
|
|
2648
|
+
};
|
|
2649
|
+
return {
|
|
2650
|
+
response: {
|
|
2651
|
+
status: error.status,
|
|
2652
|
+
body: error.data,
|
|
2653
|
+
message: error.message
|
|
2654
|
+
},
|
|
2655
|
+
verdict: verdictFor(error)
|
|
2656
|
+
};
|
|
2657
|
+
};
|
|
2658
|
+
const OAUTH_PROVIDERS = [
|
|
2659
|
+
"google",
|
|
2660
|
+
"apple",
|
|
2661
|
+
"microsoft"
|
|
2662
|
+
];
|
|
2663
|
+
const storedStates = () => Object.fromEntries(OAUTH_PROVIDERS.map((p) => [p, sessionStorage.getItem("__rixl_auth_state_" + p) ?? "(none)"]));
|
|
2664
|
+
/**
|
|
2665
|
+
* Reports a provider response that arrived but could not be used, which ends the
|
|
2666
|
+
* login without a request ever being sent — the case that otherwise looks like
|
|
2667
|
+
* "it just went back to the login page".
|
|
2668
|
+
*
|
|
2669
|
+
* `detectProvider()` requires the `state` in the URL to equal the one stored when
|
|
2670
|
+
* the login started, so the two states below are the thing to compare.
|
|
2671
|
+
*/
|
|
2672
|
+
const logUnusableProviderResponse = () => {
|
|
2673
|
+
const state = urlParams.get("state") ?? "(none)";
|
|
2674
|
+
console.error("[@rixl/sdk] a provider response is in the URL but no login was attempted", {
|
|
2675
|
+
urlState: state,
|
|
2676
|
+
storedStates: storedStates(),
|
|
2677
|
+
credentialParams: [
|
|
2678
|
+
"id_token",
|
|
2679
|
+
"code",
|
|
2680
|
+
"access_token"
|
|
2681
|
+
].filter((key) => urlParams.has(key)),
|
|
2682
|
+
verdict: state === "(none)" ? "CLIENT: the provider returned no `state`, so the response cannot be matched to a login attempt." : "CLIENT: `urlState` does not match the stored state for its provider. sessionStorage was cleared, the login started in a different tab, or the response was replayed after the state had already been retired."
|
|
2683
|
+
});
|
|
2684
|
+
};
|
|
2685
|
+
/**
|
|
2686
|
+
* Reports a failed provider token exchange. `provider` doubles as the `token_type`
|
|
2687
|
+
* sent on the wire, so it is printed as-is.
|
|
2688
|
+
*/
|
|
2689
|
+
const logProviderExchangeFailure = (provider, credential, error) => {
|
|
2690
|
+
console.error("[@rixl/sdk] provider login failed at POST /auth/v1/token", {
|
|
2691
|
+
request: {
|
|
2520
2692
|
token_type: provider,
|
|
2521
|
-
|
|
2522
|
-
country_code: options?.countryCode,
|
|
2523
|
-
origin: options?.origin
|
|
2693
|
+
credential: claimsOf(credential)
|
|
2524
2694
|
},
|
|
2525
|
-
|
|
2695
|
+
...describe(error)
|
|
2526
2696
|
});
|
|
2527
|
-
return data;
|
|
2528
2697
|
};
|
|
2529
2698
|
|
|
2699
|
+
//#endregion
|
|
2700
|
+
//#region src/auth/social/socialState.ts
|
|
2701
|
+
const socialStoragePath = (provider) => SOCIAL_CONNECT_KEY_PREFIX + provider;
|
|
2702
|
+
/**
|
|
2703
|
+
* Sets a flag indicating that a social provider connection is being attempted
|
|
2704
|
+
* @param provider The provider identifier
|
|
2705
|
+
*/
|
|
2706
|
+
function setSocialConnectAttempt(provider) {
|
|
2707
|
+
sessionStorage.setItem(socialStoragePath(provider), "true");
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Checks if there's a pending social provider connection attempt
|
|
2711
|
+
* @param provider The provider identifier
|
|
2712
|
+
* @returns True if there's a pending connection attempt, false otherwise
|
|
2713
|
+
*/
|
|
2714
|
+
function hasSocialConnectAttempt(provider) {
|
|
2715
|
+
return sessionStorage.getItem(socialStoragePath(provider)) === "true";
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Clears the social provider connection attempt flag
|
|
2719
|
+
* @param provider The provider identifier
|
|
2720
|
+
*/
|
|
2721
|
+
function clearSocialConnectAttempt(provider) {
|
|
2722
|
+
sessionStorage.removeItem(socialStoragePath(provider));
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2530
2725
|
//#endregion
|
|
2531
2726
|
//#region src/auth/initialization.ts
|
|
2532
2727
|
/**
|
|
@@ -2546,534 +2741,94 @@ function createDeferred() {
|
|
|
2546
2741
|
};
|
|
2547
2742
|
}
|
|
2548
2743
|
/**
|
|
2549
|
-
* Global deferred promise that tracks the initialization status of the auth library
|
|
2550
|
-
* This is resolved when initClient is called
|
|
2744
|
+
* Global deferred promise that tracks the initialization status of the auth library.
|
|
2745
|
+
* This is resolved when initClient is called.
|
|
2746
|
+
*
|
|
2747
|
+
* Shared across copies of this package. `connect()` runs in exactly one copy, so
|
|
2748
|
+
* a per-copy deferred leaves every other copy awaiting a promise nothing will
|
|
2749
|
+
* ever resolve — and because getToken() and the request interceptor both await
|
|
2750
|
+
* it, that is not a slow path but a permanent hang.
|
|
2551
2751
|
*/
|
|
2552
|
-
const initDeferred = createDeferred
|
|
2752
|
+
const initDeferred = shared("initDeferred", createDeferred);
|
|
2553
2753
|
|
|
2554
2754
|
//#endregion
|
|
2555
|
-
//#region src/auth/
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2755
|
+
//#region src/auth/api/error-handlers.ts
|
|
2756
|
+
/**
|
|
2757
|
+
* Helper to create error functions - reduces bundle size by reusing error creation logic
|
|
2758
|
+
*/
|
|
2759
|
+
const err = (message) => () => new Error(message);
|
|
2760
|
+
/** Reusable error handlers for common cases - reduces repetitive error messages */
|
|
2761
|
+
const commonErrors = {
|
|
2762
|
+
unauthorized: err("User is not authorized"),
|
|
2763
|
+
badRequest: err("Bad request"),
|
|
2764
|
+
notFound: err("Not found"),
|
|
2765
|
+
conflict: err("Resource already exists"),
|
|
2766
|
+
forbidden: err("Forbidden"),
|
|
2767
|
+
tooManyRequests: err("Too many requests")
|
|
2559
2768
|
};
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
} else cookieString += `; expires=${options.expires.toUTCString()}`;
|
|
2569
|
-
if (options.path) cookieString += `; path=${options.path}`;
|
|
2570
|
-
if (options.domain) cookieString += `; domain=${options.domain}`;
|
|
2571
|
-
if (options.secure) cookieString += `; secure`;
|
|
2572
|
-
if (options.sameSite) cookieString += `; samesite=${options.sameSite}`;
|
|
2769
|
+
/**
|
|
2770
|
+
* Standard error handler for API errors with custom status code handling
|
|
2771
|
+
*/
|
|
2772
|
+
const handleApiError = (error, statusHandlers) => {
|
|
2773
|
+
if (error instanceof ApiError) {
|
|
2774
|
+
const handler = statusHandlers[error.status];
|
|
2775
|
+
if (handler) throw new ApiError(handler().message, error.status, error.endpoint, error.data);
|
|
2776
|
+
throw error;
|
|
2573
2777
|
}
|
|
2574
|
-
|
|
2575
|
-
}
|
|
2576
|
-
function deleteCookie(name) {
|
|
2577
|
-
if (typeof document === "undefined") return;
|
|
2578
|
-
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
2579
|
-
}
|
|
2580
|
-
const encodeName = (name) => encodeURIComponent(name).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent);
|
|
2581
|
-
const encodeValue = (value) => encodeURIComponent(value).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent);
|
|
2778
|
+
throw error;
|
|
2779
|
+
};
|
|
2582
2780
|
|
|
2583
2781
|
//#endregion
|
|
2584
|
-
//#region src/auth/
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2782
|
+
//#region src/auth/api/utils.ts
|
|
2783
|
+
/**
|
|
2784
|
+
* Generic helper for API calls to handle initialization and error handling
|
|
2785
|
+
* @param fn The async function to execute
|
|
2786
|
+
* @param errorMap A map of status codes to error functions
|
|
2787
|
+
* @returns The result of the function execution
|
|
2788
|
+
*/
|
|
2789
|
+
const apiCall = async (fn, errorMap = {}) => {
|
|
2790
|
+
await initDeferred.promise;
|
|
2791
|
+
try {
|
|
2792
|
+
return await fn();
|
|
2793
|
+
} catch (error) {
|
|
2794
|
+
return handleApiError(error, errorMap);
|
|
2593
2795
|
}
|
|
2594
|
-
const stringValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
2595
|
-
setCookie(`${GLOBAL_PREFIX}_${key}`, stringValue, {
|
|
2596
|
-
expires,
|
|
2597
|
-
path: "/",
|
|
2598
|
-
sameSite: "Lax"
|
|
2599
|
-
});
|
|
2600
2796
|
};
|
|
2601
2797
|
|
|
2602
2798
|
//#endregion
|
|
2603
|
-
//#region src/auth/
|
|
2604
|
-
|
|
2605
|
-
const
|
|
2606
|
-
if (
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
return JSON.parse(value);
|
|
2610
|
-
} catch (err) {
|
|
2611
|
-
console.warn("Can't parse user data, error: ", err);
|
|
2612
|
-
return;
|
|
2799
|
+
//#region src/auth/api/tokens.ts
|
|
2800
|
+
function persistTokens(data) {
|
|
2801
|
+
const tokens = data ?? {};
|
|
2802
|
+
if (tokens.access_token && tokens.refresh_token && tokens.expires_in) {
|
|
2803
|
+
setTokens(tokens.access_token, tokens.refresh_token, Number(tokens.expires_in));
|
|
2804
|
+
return true;
|
|
2613
2805
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
const store = atom(parseUser());
|
|
2617
|
-
store.subscribe((value) => {
|
|
2618
|
-
if (typeof localStorage === "undefined" || !localStorage || typeof localStorage.setItem !== "function") return;
|
|
2619
|
-
if (value) localStorage.setItem(userPath, JSON.stringify(value));
|
|
2620
|
-
else localStorage.removeItem(userPath);
|
|
2621
|
-
});
|
|
2622
|
-
return store;
|
|
2623
|
-
});
|
|
2806
|
+
return false;
|
|
2807
|
+
}
|
|
2624
2808
|
|
|
2625
2809
|
//#endregion
|
|
2626
|
-
//#region src/auth/
|
|
2810
|
+
//#region src/auth/validation/base.ts
|
|
2627
2811
|
/**
|
|
2628
|
-
*
|
|
2629
|
-
*
|
|
2630
|
-
* @param token The JWT token to decode
|
|
2631
|
-
* @returns The decoded user data or undefined if decoding fails
|
|
2812
|
+
* 🛡️ INPUT VALIDATION UTILITY
|
|
2813
|
+
* Validates data before API calls and throws user-friendly errors
|
|
2632
2814
|
*/
|
|
2633
|
-
const
|
|
2815
|
+
const validateInput = (schema, data) => {
|
|
2634
2816
|
try {
|
|
2635
|
-
|
|
2636
|
-
return {
|
|
2637
|
-
id: decodedUser.id,
|
|
2638
|
-
email: decodedUser.email,
|
|
2639
|
-
first_name: decodedUser.first_name,
|
|
2640
|
-
last_name: decodedUser.last_name,
|
|
2641
|
-
username: decodedUser.username,
|
|
2642
|
-
image_url: decodedUser.image_url,
|
|
2643
|
-
language_code: decodedUser.language_code,
|
|
2644
|
-
org_id: decodedUser.org_id
|
|
2645
|
-
};
|
|
2817
|
+
return v.parse(schema, data);
|
|
2646
2818
|
} catch (error) {
|
|
2647
|
-
|
|
2648
|
-
|
|
2819
|
+
if (error instanceof v.ValiError) {
|
|
2820
|
+
const firstError = error.issues[0];
|
|
2821
|
+
throw new Error(firstError.message);
|
|
2822
|
+
}
|
|
2823
|
+
throw error;
|
|
2649
2824
|
}
|
|
2650
2825
|
};
|
|
2826
|
+
const EmailSchema = v.pipe(v.string("Email must be text"), v.email("Please enter a valid email address"), v.minLength(1, "Email is required"));
|
|
2827
|
+
const PasswordSchema = v.pipe(v.string("Password must be text"), v.minLength(8, "Password must be at least 8 characters long"), v.regex(/[A-Z]/, "Password must contain at least one uppercase letter"), v.regex(/[a-z]/, "Password must contain at least one lowercase letter"), v.regex(/[0-9]/, "Password must contain at least one number"));
|
|
2651
2828
|
/**
|
|
2652
|
-
*
|
|
2653
|
-
*
|
|
2654
|
-
*
|
|
2655
|
-
*/
|
|
2656
|
-
const decodeAndSetUser = (token) => {
|
|
2657
|
-
const userData = decodeToken(token);
|
|
2658
|
-
if (userData) {
|
|
2659
|
-
user.set(userData);
|
|
2660
|
-
return true;
|
|
2661
|
-
}
|
|
2662
|
-
return false;
|
|
2663
|
-
};
|
|
2664
|
-
/**
|
|
2665
|
-
* Checks if a token is expired based on the expiration timestamp
|
|
2666
|
-
* @param expireAt The expiration timestamp in milliseconds
|
|
2667
|
-
* @returns True if the token is expired, false otherwise
|
|
2668
|
-
*/
|
|
2669
|
-
const isTokenExpired = (expireAt) => {
|
|
2670
|
-
if (!expireAt) return true;
|
|
2671
|
-
return Date.now() >= expireAt;
|
|
2672
|
-
};
|
|
2673
|
-
|
|
2674
|
-
//#endregion
|
|
2675
|
-
//#region src/auth/authStore.ts
|
|
2676
|
-
const isLogged = shared("isLogged", () => atom(initVals["isLogged"] === "true" || detectProvider() !== void 0));
|
|
2677
|
-
const accessToken = shared("accessToken", () => atom(initVals["accessToken"]));
|
|
2678
|
-
const refreshToken = shared("refreshToken", () => atom(initVals["refreshToken"]));
|
|
2679
|
-
const expireAt = shared("expireAt", () => atom(Number(initVals["expireAt"])));
|
|
2680
|
-
const authError = shared("authError", () => atom(null));
|
|
2681
|
-
const requiresAction = shared("requiresAction", () => atom(initVals["requiresAction"] || null));
|
|
2682
|
-
const limitedAccessToken = shared("limitedAccessToken", () => atom(initVals["limitedAccessToken"] || null));
|
|
2683
|
-
let currentTokenPromise = null;
|
|
2684
|
-
const PROVIDER_URL_MAP = {
|
|
2685
|
-
google: googleAuthUrl,
|
|
2686
|
-
apple: appleAuthUrl,
|
|
2687
|
-
microsoft: microsoftAuthUrl,
|
|
2688
|
-
telegram: telegramAuthUrl
|
|
2689
|
-
};
|
|
2690
|
-
const login = async (provider) => {
|
|
2691
|
-
await initDeferred.promise;
|
|
2692
|
-
const authUrlAtom = PROVIDER_URL_MAP[provider];
|
|
2693
|
-
if (!authUrlAtom) throw new Error(`Unsupported provider: ${provider}`);
|
|
2694
|
-
const authUrl = authUrlAtom.get();
|
|
2695
|
-
if (authUrl) window.location.href = authUrl;
|
|
2696
|
-
else throw new Error(`${provider} provider is not configured. Please check your initClient configuration.`);
|
|
2697
|
-
};
|
|
2698
|
-
const refreshAccessToken = async (refresh) => {
|
|
2699
|
-
const result = await refreshTokens("Bearer", refresh);
|
|
2700
|
-
if (!("requires_action" in result)) setTokens(result.access_token, result.refresh_token, result.expires_in);
|
|
2701
|
-
};
|
|
2702
|
-
const ensureValidAccessToken = async (refresh) => {
|
|
2703
|
-
if (accessToken.get() && !isTokenExpired(expireAt.get())) return;
|
|
2704
|
-
try {
|
|
2705
|
-
await refreshAccessToken(refresh);
|
|
2706
|
-
} catch (refreshError) {
|
|
2707
|
-
console.error("Token refresh failed in getToken:", refreshError);
|
|
2708
|
-
removeTokens();
|
|
2709
|
-
throw refreshError;
|
|
2710
|
-
}
|
|
2711
|
-
};
|
|
2712
|
-
const getToken = async () => {
|
|
2713
|
-
if (currentTokenPromise) return currentTokenPromise;
|
|
2714
|
-
currentTokenPromise = (async () => {
|
|
2715
|
-
try {
|
|
2716
|
-
await initDeferred.promise;
|
|
2717
|
-
if (requiresAction.get()) return void 0;
|
|
2718
|
-
const currentRefreshToken = refreshToken.get();
|
|
2719
|
-
if (!currentRefreshToken) return void 0;
|
|
2720
|
-
await ensureValidAccessToken(currentRefreshToken);
|
|
2721
|
-
const token = accessToken.get();
|
|
2722
|
-
if (token) decodeAndSetUser(token);
|
|
2723
|
-
return token;
|
|
2724
|
-
} catch (error) {
|
|
2725
|
-
console.warn("Failed to getToken(). Error: ", error);
|
|
2726
|
-
throw error;
|
|
2727
|
-
} finally {
|
|
2728
|
-
currentTokenPromise = null;
|
|
2729
|
-
}
|
|
2730
|
-
})();
|
|
2731
|
-
return currentTokenPromise;
|
|
2732
|
-
};
|
|
2733
|
-
/**
|
|
2734
|
-
* Sets authentication tokens in the store
|
|
2735
|
-
* @param access The access token
|
|
2736
|
-
* @param refresh The refresh token
|
|
2737
|
-
* @param expiresIn The token expiration time in seconds
|
|
2738
|
-
*/
|
|
2739
|
-
const setTokens = (access, refresh, expiresIn) => {
|
|
2740
|
-
accessToken.set(access);
|
|
2741
|
-
refreshToken.set(refresh);
|
|
2742
|
-
expireAt.set(Date.now() + expiresIn * 1e3);
|
|
2743
|
-
isLogged.set(true);
|
|
2744
|
-
limitedAccessToken.set(null);
|
|
2745
|
-
requiresAction.set(null);
|
|
2746
|
-
authError.set(null);
|
|
2747
|
-
decodeAndSetUser(access);
|
|
2748
|
-
};
|
|
2749
|
-
/**
|
|
2750
|
-
* Removes authentication tokens from the store
|
|
2751
|
-
*/
|
|
2752
|
-
const removeTokens = () => {
|
|
2753
|
-
accessToken.set("");
|
|
2754
|
-
refreshToken.set("");
|
|
2755
|
-
expireAt.set(0);
|
|
2756
|
-
isLogged.set(false);
|
|
2757
|
-
user.set(void 0);
|
|
2758
|
-
limitedAccessToken.set(null);
|
|
2759
|
-
requiresAction.set(null);
|
|
2760
|
-
authError.set(null);
|
|
2761
|
-
};
|
|
2762
|
-
/**
|
|
2763
|
-
* Clears the auth error from the store
|
|
2764
|
-
*/
|
|
2765
|
-
const clearAuthError = () => {
|
|
2766
|
-
authError.set(null);
|
|
2767
|
-
};
|
|
2768
|
-
/**
|
|
2769
|
-
* Sets limited access state for users requiring additional action (e.g., Telegram users without email)
|
|
2770
|
-
* @param token The limited scope access token
|
|
2771
|
-
* @param action The required action (e.g., "add_email")
|
|
2772
|
-
*/
|
|
2773
|
-
const setLimitedAccessState = (token, action) => {
|
|
2774
|
-
limitedAccessToken.set(token);
|
|
2775
|
-
requiresAction.set(action);
|
|
2776
|
-
authError.set(null);
|
|
2777
|
-
isLogged.set(true);
|
|
2778
|
-
};
|
|
2779
|
-
/**
|
|
2780
|
-
* Clears the limited access state (after email verification completes or user logs out)
|
|
2781
|
-
*/
|
|
2782
|
-
const clearLimitedAccessState = () => {
|
|
2783
|
-
limitedAccessToken.set(null);
|
|
2784
|
-
requiresAction.set(null);
|
|
2785
|
-
if (!(!!accessToken.get() && !!refreshToken.get())) isLogged.set(false);
|
|
2786
|
-
};
|
|
2787
|
-
isLogged.subscribe((value) => setStoreCookie("isLogged", value));
|
|
2788
|
-
accessToken.subscribe((value) => setStoreCookie("accessToken", value));
|
|
2789
|
-
refreshToken.subscribe((value) => setStoreCookie("refreshToken", value));
|
|
2790
|
-
expireAt.subscribe((value) => setStoreCookie("expireAt", value));
|
|
2791
|
-
requiresAction.subscribe((value) => setStoreCookie("requiresAction", value));
|
|
2792
|
-
limitedAccessToken.subscribe((value) => setStoreCookie("limitedAccessToken", value));
|
|
2793
|
-
|
|
2794
|
-
//#endregion
|
|
2795
|
-
//#region src/auth/api/types.ts
|
|
2796
|
-
/**
|
|
2797
|
-
* Generic API error class - wraps ky's HTTPError for consistency
|
|
2798
|
-
*/
|
|
2799
|
-
var ApiError = class extends Error {
|
|
2800
|
-
status;
|
|
2801
|
-
endpoint;
|
|
2802
|
-
data;
|
|
2803
|
-
constructor(message, ...rest) {
|
|
2804
|
-
super(message);
|
|
2805
|
-
this.name = "ApiError";
|
|
2806
|
-
const firstArg = rest[0];
|
|
2807
|
-
if (typeof firstArg === "number") {
|
|
2808
|
-
this.status = firstArg;
|
|
2809
|
-
this.endpoint = rest[1] ?? "";
|
|
2810
|
-
this.data = rest[2];
|
|
2811
|
-
return;
|
|
2812
|
-
}
|
|
2813
|
-
this.status = firstArg.status;
|
|
2814
|
-
this.endpoint = firstArg.endpoint;
|
|
2815
|
-
this.data = firstArg.data;
|
|
2816
|
-
}
|
|
2817
|
-
};
|
|
2818
|
-
|
|
2819
|
-
//#endregion
|
|
2820
|
-
//#region src/auth/api-url.ts
|
|
2821
|
-
/**
|
|
2822
|
-
* Global API base URL store
|
|
2823
|
-
*/
|
|
2824
|
-
const apiURL = shared("apiURL", () => atom(""));
|
|
2825
|
-
|
|
2826
|
-
//#endregion
|
|
2827
|
-
//#region src/auth/api/sdk-client.ts
|
|
2828
|
-
function isWireErrorBody(error) {
|
|
2829
|
-
return typeof error === "object" && error !== null;
|
|
2830
|
-
}
|
|
2831
|
-
const state = shared("sdkClientState", () => ({
|
|
2832
|
-
configured: false,
|
|
2833
|
-
tokenResolver: getToken
|
|
2834
|
-
}));
|
|
2835
|
-
function setTokenResolver(resolver) {
|
|
2836
|
-
state.tokenResolver = resolver;
|
|
2837
|
-
}
|
|
2838
|
-
/**
|
|
2839
|
-
* Routes that do not require a Bearer token at the gateway edge. These
|
|
2840
|
-
* authenticate via credentials in the request body (or are webhooks verified by
|
|
2841
|
-
* signature), so attaching an Authorization header here would make the gateway
|
|
2842
|
-
* attempt to validate a stale/absent token and reject the request with 401.
|
|
2843
|
-
* Mirrors `publicRoutes` in backend/gateway/internal/routes/routes.go.
|
|
2844
|
-
*/
|
|
2845
|
-
const publicRoutes = [
|
|
2846
|
-
{
|
|
2847
|
-
method: "POST",
|
|
2848
|
-
path: "/auth/v1/token"
|
|
2849
|
-
},
|
|
2850
|
-
{
|
|
2851
|
-
method: "POST",
|
|
2852
|
-
path: "/auth/v1/register"
|
|
2853
|
-
},
|
|
2854
|
-
{
|
|
2855
|
-
method: "POST",
|
|
2856
|
-
path: "/auth/v1/login"
|
|
2857
|
-
},
|
|
2858
|
-
{
|
|
2859
|
-
method: "POST",
|
|
2860
|
-
path: "/auth/v1/email/verify"
|
|
2861
|
-
},
|
|
2862
|
-
{
|
|
2863
|
-
method: "POST",
|
|
2864
|
-
path: "/auth/v1/email/verify/resend"
|
|
2865
|
-
},
|
|
2866
|
-
{
|
|
2867
|
-
method: "POST",
|
|
2868
|
-
path: "/auth/v1/password/reset"
|
|
2869
|
-
},
|
|
2870
|
-
{
|
|
2871
|
-
method: "POST",
|
|
2872
|
-
path: "/auth/v1/password/reset/confirm"
|
|
2873
|
-
},
|
|
2874
|
-
{
|
|
2875
|
-
method: "POST",
|
|
2876
|
-
path: "/auth/v1/verify-totp"
|
|
2877
|
-
},
|
|
2878
|
-
{
|
|
2879
|
-
method: "POST",
|
|
2880
|
-
path: "/auth/v1/verify-passkey"
|
|
2881
|
-
},
|
|
2882
|
-
{
|
|
2883
|
-
method: "POST",
|
|
2884
|
-
path: "/auth/v1/invitations/",
|
|
2885
|
-
prefix: true
|
|
2886
|
-
},
|
|
2887
|
-
{
|
|
2888
|
-
method: "POST",
|
|
2889
|
-
path: "/auth/v1/passkey/login/begin"
|
|
2890
|
-
},
|
|
2891
|
-
{
|
|
2892
|
-
method: "POST",
|
|
2893
|
-
path: "/auth/v1/passkey/login/finish"
|
|
2894
|
-
},
|
|
2895
|
-
{
|
|
2896
|
-
method: "POST",
|
|
2897
|
-
path: "/auth/v1/logout"
|
|
2898
|
-
},
|
|
2899
|
-
{
|
|
2900
|
-
method: "POST",
|
|
2901
|
-
path: "/auth/v1/blog/unsubscribe/email"
|
|
2902
|
-
},
|
|
2903
|
-
{
|
|
2904
|
-
method: "POST",
|
|
2905
|
-
path: "/auth/v1/blog/broadcast"
|
|
2906
|
-
},
|
|
2907
|
-
{
|
|
2908
|
-
method: "GET",
|
|
2909
|
-
path: "/media/v1/videos/",
|
|
2910
|
-
prefix: true
|
|
2911
|
-
},
|
|
2912
|
-
{
|
|
2913
|
-
method: "GET",
|
|
2914
|
-
path: "/media/v1/images/",
|
|
2915
|
-
prefix: true
|
|
2916
|
-
},
|
|
2917
|
-
{
|
|
2918
|
-
method: "GET",
|
|
2919
|
-
path: "/media/v1/languages"
|
|
2920
|
-
},
|
|
2921
|
-
{
|
|
2922
|
-
method: "GET",
|
|
2923
|
-
path: "/posts/v1/feeds/",
|
|
2924
|
-
prefix: true
|
|
2925
|
-
},
|
|
2926
|
-
{
|
|
2927
|
-
method: "POST",
|
|
2928
|
-
path: "/billing/webhooks/stripe"
|
|
2929
|
-
},
|
|
2930
|
-
{
|
|
2931
|
-
method: "POST",
|
|
2932
|
-
path: "/webhooks/storage"
|
|
2933
|
-
},
|
|
2934
|
-
{
|
|
2935
|
-
method: "POST",
|
|
2936
|
-
path: "/platform/auth/v1/token"
|
|
2937
|
-
},
|
|
2938
|
-
{
|
|
2939
|
-
method: "POST",
|
|
2940
|
-
path: "/platform/auth/v1/refresh"
|
|
2941
|
-
}
|
|
2942
|
-
];
|
|
2943
|
-
function isPublicRoute(method, pathname) {
|
|
2944
|
-
const match = method.toUpperCase();
|
|
2945
|
-
return publicRoutes.some(({ method: m, path, prefix }) => match === m && (prefix ? pathname.startsWith(path) : pathname === path));
|
|
2946
|
-
}
|
|
2947
|
-
function configureSdkClient() {
|
|
2948
|
-
if (state.configured) return;
|
|
2949
|
-
state.configured = true;
|
|
2950
|
-
configureAllClients(apiURL.get());
|
|
2951
|
-
apiURL.subscribe((url) => {
|
|
2952
|
-
configureAllClients(url);
|
|
2953
|
-
});
|
|
2954
|
-
addClientInitializer((client) => {
|
|
2955
|
-
client.interceptors.request.use(async (request) => {
|
|
2956
|
-
if (request.headers.has("Authorization")) return request;
|
|
2957
|
-
const { pathname } = new URL(request.url);
|
|
2958
|
-
if (isPublicRoute(request.method, pathname)) return request;
|
|
2959
|
-
const token = await state.tokenResolver();
|
|
2960
|
-
if (!token) throw new ApiError("No access token available for an authenticated request", HTTP_STATUS.UNAUTHORIZED, pathname);
|
|
2961
|
-
request.headers.set("Authorization", `Bearer ${token}`);
|
|
2962
|
-
return request;
|
|
2963
|
-
});
|
|
2964
|
-
client.interceptors.error.use((error, response, request) => {
|
|
2965
|
-
if (error instanceof Error) return error;
|
|
2966
|
-
const body = isWireErrorBody(error) ? error : void 0;
|
|
2967
|
-
const status = response?.status ?? body?.code ?? 0;
|
|
2968
|
-
return new ApiError(body?.error || body?.details || (typeof error === "string" ? error : "Request failed"), status, request ? new URL(request.url).pathname : "", error);
|
|
2969
|
-
});
|
|
2970
|
-
});
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
|
-
//#endregion
|
|
2974
|
-
//#region src/auth/social/socialState.ts
|
|
2975
|
-
const socialStoragePath = (provider) => SOCIAL_CONNECT_KEY_PREFIX + provider;
|
|
2976
|
-
/**
|
|
2977
|
-
* Sets a flag indicating that a social provider connection is being attempted
|
|
2978
|
-
* @param provider The provider identifier
|
|
2979
|
-
*/
|
|
2980
|
-
function setSocialConnectAttempt(provider) {
|
|
2981
|
-
sessionStorage.setItem(socialStoragePath(provider), "true");
|
|
2982
|
-
}
|
|
2983
|
-
/**
|
|
2984
|
-
* Checks if there's a pending social provider connection attempt
|
|
2985
|
-
* @param provider The provider identifier
|
|
2986
|
-
* @returns True if there's a pending connection attempt, false otherwise
|
|
2987
|
-
*/
|
|
2988
|
-
function hasSocialConnectAttempt(provider) {
|
|
2989
|
-
return sessionStorage.getItem(socialStoragePath(provider)) === "true";
|
|
2990
|
-
}
|
|
2991
|
-
/**
|
|
2992
|
-
* Clears the social provider connection attempt flag
|
|
2993
|
-
* @param provider The provider identifier
|
|
2994
|
-
*/
|
|
2995
|
-
function clearSocialConnectAttempt(provider) {
|
|
2996
|
-
sessionStorage.removeItem(socialStoragePath(provider));
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
//#endregion
|
|
3000
|
-
//#region src/auth/api/error-handlers.ts
|
|
3001
|
-
/**
|
|
3002
|
-
* Helper to create error functions - reduces bundle size by reusing error creation logic
|
|
3003
|
-
*/
|
|
3004
|
-
const err = (message) => () => new Error(message);
|
|
3005
|
-
/** Reusable error handlers for common cases - reduces repetitive error messages */
|
|
3006
|
-
const commonErrors = {
|
|
3007
|
-
unauthorized: err("User is not authorized"),
|
|
3008
|
-
badRequest: err("Bad request"),
|
|
3009
|
-
notFound: err("Not found"),
|
|
3010
|
-
conflict: err("Resource already exists"),
|
|
3011
|
-
forbidden: err("Forbidden"),
|
|
3012
|
-
tooManyRequests: err("Too many requests")
|
|
3013
|
-
};
|
|
3014
|
-
/**
|
|
3015
|
-
* Standard error handler for API errors with custom status code handling
|
|
3016
|
-
*/
|
|
3017
|
-
const handleApiError = (error, statusHandlers) => {
|
|
3018
|
-
if (error instanceof ApiError) {
|
|
3019
|
-
const handler = statusHandlers[error.status];
|
|
3020
|
-
if (handler) throw new ApiError(handler().message, error.status, error.endpoint, error.data);
|
|
3021
|
-
throw error;
|
|
3022
|
-
}
|
|
3023
|
-
throw error;
|
|
3024
|
-
};
|
|
3025
|
-
|
|
3026
|
-
//#endregion
|
|
3027
|
-
//#region src/auth/api/utils.ts
|
|
3028
|
-
/**
|
|
3029
|
-
* Generic helper for API calls to handle initialization and error handling
|
|
3030
|
-
* @param fn The async function to execute
|
|
3031
|
-
* @param errorMap A map of status codes to error functions
|
|
3032
|
-
* @returns The result of the function execution
|
|
3033
|
-
*/
|
|
3034
|
-
const apiCall = async (fn, errorMap = {}) => {
|
|
3035
|
-
await initDeferred.promise;
|
|
3036
|
-
try {
|
|
3037
|
-
return await fn();
|
|
3038
|
-
} catch (error) {
|
|
3039
|
-
return handleApiError(error, errorMap);
|
|
3040
|
-
}
|
|
3041
|
-
};
|
|
3042
|
-
|
|
3043
|
-
//#endregion
|
|
3044
|
-
//#region src/auth/api/tokens.ts
|
|
3045
|
-
function persistTokens(data) {
|
|
3046
|
-
const tokens = data ?? {};
|
|
3047
|
-
if (tokens.access_token && tokens.refresh_token && tokens.expires_in) {
|
|
3048
|
-
setTokens(tokens.access_token, tokens.refresh_token, Number(tokens.expires_in));
|
|
3049
|
-
return true;
|
|
3050
|
-
}
|
|
3051
|
-
return false;
|
|
3052
|
-
}
|
|
3053
|
-
|
|
3054
|
-
//#endregion
|
|
3055
|
-
//#region src/auth/validation/base.ts
|
|
3056
|
-
/**
|
|
3057
|
-
* 🛡️ INPUT VALIDATION UTILITY
|
|
3058
|
-
* Validates data before API calls and throws user-friendly errors
|
|
3059
|
-
*/
|
|
3060
|
-
const validateInput = (schema, data) => {
|
|
3061
|
-
try {
|
|
3062
|
-
return v.parse(schema, data);
|
|
3063
|
-
} catch (error) {
|
|
3064
|
-
if (error instanceof v.ValiError) {
|
|
3065
|
-
const firstError = error.issues[0];
|
|
3066
|
-
throw new Error(firstError.message);
|
|
3067
|
-
}
|
|
3068
|
-
throw error;
|
|
3069
|
-
}
|
|
3070
|
-
};
|
|
3071
|
-
const EmailSchema = v.pipe(v.string("Email must be text"), v.email("Please enter a valid email address"), v.minLength(1, "Email is required"));
|
|
3072
|
-
const PasswordSchema = v.pipe(v.string("Password must be text"), v.minLength(8, "Password must be at least 8 characters long"), v.regex(/[A-Z]/, "Password must contain at least one uppercase letter"), v.regex(/[a-z]/, "Password must contain at least one lowercase letter"), v.regex(/[0-9]/, "Password must contain at least one number"));
|
|
3073
|
-
/**
|
|
3074
|
-
* Username validation schema
|
|
3075
|
-
* - Must be 4-24 characters
|
|
3076
|
-
* - Only letters, numbers, underscores, and periods
|
|
2829
|
+
* Username validation schema
|
|
2830
|
+
* - Must be 4-24 characters
|
|
2831
|
+
* - Only letters, numbers, underscores, and periods
|
|
3077
2832
|
*/
|
|
3078
2833
|
const UsernameSchema = v.pipe(v.string("Username must be text"), v.minLength(4, "Username must be 4-24 characters long"), v.maxLength(24, "Username must be 4-24 characters long"), v.regex(/^[a-z0-9_.]+$/, "Username can only contain lowercase letters, numbers, dots and underscores"));
|
|
3079
2834
|
/**
|
|
@@ -3169,55 +2924,491 @@ const listSocials = async () => {
|
|
|
3169
2924
|
});
|
|
3170
2925
|
}, { [HTTP_STATUS.UNAUTHORIZED]: () => /* @__PURE__ */ new Error("User is not authorized to list providers!") });
|
|
3171
2926
|
};
|
|
3172
|
-
const connectSocialInternal = async (provider, token) => {
|
|
3173
|
-
return apiCall(async () => {
|
|
3174
|
-
const requestBody = validateInput(ConnectProviderSchema, {
|
|
3175
|
-
provider,
|
|
3176
|
-
token
|
|
3177
|
-
});
|
|
3178
|
-
const
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
2927
|
+
const connectSocialInternal = async (provider, token) => {
|
|
2928
|
+
return apiCall(async () => {
|
|
2929
|
+
const requestBody = validateInput(ConnectProviderSchema, {
|
|
2930
|
+
provider,
|
|
2931
|
+
token
|
|
2932
|
+
});
|
|
2933
|
+
const sessionToken = await getToken();
|
|
2934
|
+
const headers = sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {};
|
|
2935
|
+
const { data } = await authV1ProvidersServiceConnectProvider({
|
|
2936
|
+
body: {
|
|
2937
|
+
provider: toProtoProvider(normalizeProviderType(requestBody.provider)),
|
|
2938
|
+
token: requestBody.token
|
|
2939
|
+
},
|
|
2940
|
+
headers,
|
|
2941
|
+
throwOnError: true
|
|
2942
|
+
});
|
|
2943
|
+
persistTokens(data);
|
|
2944
|
+
}, { [HTTP_STATUS.UNAUTHORIZED]: () => /* @__PURE__ */ new Error("User is not authorized to connect provider!") });
|
|
2945
|
+
};
|
|
2946
|
+
const disconnectSocial = async (provider) => {
|
|
2947
|
+
return apiCall(async () => {
|
|
2948
|
+
await authV1ProvidersServiceDisconnectProvider({
|
|
2949
|
+
path: { provider: toProtoProvider(provider) },
|
|
2950
|
+
throwOnError: true
|
|
2951
|
+
});
|
|
2952
|
+
}, {
|
|
2953
|
+
[HTTP_STATUS.UNAUTHORIZED]: () => /* @__PURE__ */ new Error("User is not authorized to disconnect provider!"),
|
|
2954
|
+
[HTTP_STATUS.NOT_FOUND]: () => /* @__PURE__ */ new Error("Provider not found!"),
|
|
2955
|
+
[HTTP_STATUS.BAD_REQUEST]: () => /* @__PURE__ */ new Error("Cannot disconnect the last social provider!")
|
|
2956
|
+
});
|
|
2957
|
+
};
|
|
2958
|
+
const connectSocial = (provider) => {
|
|
2959
|
+
setSocialConnectAttempt(provider);
|
|
2960
|
+
login(provider);
|
|
2961
|
+
};
|
|
2962
|
+
|
|
2963
|
+
//#endregion
|
|
2964
|
+
//#region src/auth/api/refresh-tokens.ts
|
|
2965
|
+
/**
|
|
2966
|
+
* Exchange a provider credential for a Rixl session, or refresh an existing
|
|
2967
|
+
* Rixl session with a Bearer refresh token.
|
|
2968
|
+
*
|
|
2969
|
+
* - For `AuthProvider.BEARER`, the call hits `/auth/v1/token` with the stored
|
|
2970
|
+
* Rixl refresh token.
|
|
2971
|
+
* - For OAuth/Telegram providers, the call hits `/auth/v1/providers/connect`
|
|
2972
|
+
* with the provider's `id_token` or Telegram payload. The wire `provider`
|
|
2973
|
+
* value is the `auth.v1.ExternalAccountProvider` enum (e.g.
|
|
2974
|
+
* `EXTERNAL_ACCOUNT_PROVIDER_GOOGLE`), not the short SDK name.
|
|
2975
|
+
*/
|
|
2976
|
+
const refreshTokens = async (provider, token, options) => {
|
|
2977
|
+
if (provider === "Bearer") {
|
|
2978
|
+
const { data } = await authV1TokenServiceRefreshToken({
|
|
2979
|
+
body: {
|
|
2980
|
+
token_type: "Bearer",
|
|
2981
|
+
refresh_token: token,
|
|
2982
|
+
country_code: options?.countryCode,
|
|
2983
|
+
origin: options?.origin
|
|
2984
|
+
},
|
|
2985
|
+
throwOnError: true
|
|
2986
|
+
});
|
|
2987
|
+
return data;
|
|
2988
|
+
}
|
|
2989
|
+
const requestProvider = normalizeProviderType(provider);
|
|
2990
|
+
const { data } = await authV1ProvidersServiceConnectProvider({
|
|
2991
|
+
body: {
|
|
2992
|
+
provider: toProtoProvider(requestProvider),
|
|
2993
|
+
token,
|
|
2994
|
+
country_code: options?.countryCode,
|
|
2995
|
+
origin: options?.origin
|
|
2996
|
+
},
|
|
2997
|
+
throwOnError: true
|
|
2998
|
+
});
|
|
2999
|
+
return data;
|
|
3000
|
+
};
|
|
3001
|
+
|
|
3002
|
+
//#endregion
|
|
3003
|
+
//#region src/auth/cookie/util.ts
|
|
3004
|
+
const splitOnFirstEquals = (pair) => {
|
|
3005
|
+
const trimmed = pair.trim();
|
|
3006
|
+
const separator = trimmed.indexOf("=");
|
|
3007
|
+
return separator === -1 ? [trimmed, ""] : [trimmed.slice(0, separator), trimmed.slice(separator + 1).trim()];
|
|
3008
|
+
};
|
|
3009
|
+
const getAllCookiesStartWith = (startWithKey) => {
|
|
3010
|
+
if (typeof document === "undefined") return {};
|
|
3011
|
+
return document.cookie.split(";").map(splitOnFirstEquals).filter(([key]) => key && key !== startWithKey && key.startsWith(startWithKey)).reduce((ac, [key, value]) => Object.assign(ac, { [key.slice(startWithKey.length + 1)]: value }), {});
|
|
3012
|
+
};
|
|
3013
|
+
function setCookie(name, value, options) {
|
|
3014
|
+
if (typeof document === "undefined") return;
|
|
3015
|
+
let cookieString = `${encodeName(name)}=${encodeValue(value)}`;
|
|
3016
|
+
if (options) {
|
|
3017
|
+
if (options.expires) if (typeof options.expires === "number") {
|
|
3018
|
+
const date = /* @__PURE__ */ new Date();
|
|
3019
|
+
date.setTime(date.getTime() + options.expires * 24 * 60 * 60 * 1e3);
|
|
3020
|
+
cookieString += `; expires=${date.toUTCString()}`;
|
|
3021
|
+
} else cookieString += `; expires=${options.expires.toUTCString()}`;
|
|
3022
|
+
if (options.path) cookieString += `; path=${options.path}`;
|
|
3023
|
+
if (options.domain) cookieString += `; domain=${options.domain}`;
|
|
3024
|
+
if (options.secure) cookieString += `; secure`;
|
|
3025
|
+
if (options.sameSite) cookieString += `; samesite=${options.sameSite}`;
|
|
3026
|
+
}
|
|
3027
|
+
document.cookie = cookieString;
|
|
3028
|
+
}
|
|
3029
|
+
function deleteCookie(name) {
|
|
3030
|
+
if (typeof document === "undefined") return;
|
|
3031
|
+
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
3032
|
+
}
|
|
3033
|
+
const encodeName = (name) => encodeURIComponent(name).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent);
|
|
3034
|
+
const encodeValue = (value) => encodeURIComponent(value).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent);
|
|
3035
|
+
|
|
3036
|
+
//#endregion
|
|
3037
|
+
//#region src/auth/cookie/index.ts
|
|
3038
|
+
const initVals = getAllCookiesStartWith(GLOBAL_PREFIX);
|
|
3039
|
+
const setStoreCookie = (key, value) => {
|
|
3040
|
+
const expires = /* @__PURE__ */ new Date();
|
|
3041
|
+
expires.setTime(expires.getTime() + 30 * 24 * 60 * 60 * 1e3);
|
|
3042
|
+
const cookieKey = `${GLOBAL_PREFIX}_${key}`;
|
|
3043
|
+
if (!value || value === "") {
|
|
3044
|
+
deleteCookie(cookieKey);
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
const stringValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
3048
|
+
setCookie(`${GLOBAL_PREFIX}_${key}`, stringValue, {
|
|
3049
|
+
expires,
|
|
3050
|
+
path: "/",
|
|
3051
|
+
sameSite: "Lax"
|
|
3052
|
+
});
|
|
3053
|
+
};
|
|
3054
|
+
|
|
3055
|
+
//#endregion
|
|
3056
|
+
//#region src/auth/userStore.ts
|
|
3057
|
+
const userPath = GLOBAL_PREFIX + "_user";
|
|
3058
|
+
const parseUser = () => {
|
|
3059
|
+
if (typeof localStorage === "undefined" || !localStorage || typeof localStorage.getItem !== "function") return;
|
|
3060
|
+
const value = localStorage.getItem(userPath);
|
|
3061
|
+
if (value && value != "undefined") try {
|
|
3062
|
+
return JSON.parse(value);
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
console.warn("Can't parse user data, error: ", err);
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
const user = shared("user", () => {
|
|
3069
|
+
const store = atom(parseUser());
|
|
3070
|
+
store.subscribe((value) => {
|
|
3071
|
+
if (typeof localStorage === "undefined" || !localStorage || typeof localStorage.setItem !== "function") return;
|
|
3072
|
+
if (value) localStorage.setItem(userPath, JSON.stringify(value));
|
|
3073
|
+
else localStorage.removeItem(userPath);
|
|
3074
|
+
});
|
|
3075
|
+
return store;
|
|
3076
|
+
});
|
|
3077
|
+
|
|
3078
|
+
//#endregion
|
|
3079
|
+
//#region src/auth/utils/jwt.ts
|
|
3080
|
+
/**
|
|
3081
|
+
* Decodes a JWT token and extracts user information
|
|
3082
|
+
* Uses jose library for modern, secure JWT handling
|
|
3083
|
+
* @param token The JWT token to decode
|
|
3084
|
+
* @returns The decoded user data or undefined if decoding fails
|
|
3085
|
+
*/
|
|
3086
|
+
const decodeToken = (token) => {
|
|
3087
|
+
try {
|
|
3088
|
+
const decodedUser = decodeJwt(token);
|
|
3089
|
+
return {
|
|
3090
|
+
id: decodedUser.id,
|
|
3091
|
+
email: decodedUser.email,
|
|
3092
|
+
first_name: decodedUser.first_name,
|
|
3093
|
+
last_name: decodedUser.last_name,
|
|
3094
|
+
username: decodedUser.username,
|
|
3095
|
+
image_url: decodedUser.image_url,
|
|
3096
|
+
language_code: decodedUser.language_code,
|
|
3097
|
+
org_id: decodedUser.org_id
|
|
3098
|
+
};
|
|
3099
|
+
} catch (error) {
|
|
3100
|
+
console.warn("Failed to decode JWT token. Error: ", error);
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
3104
|
+
/**
|
|
3105
|
+
* Decodes a JWT token and sets the user in the store
|
|
3106
|
+
* @param token The JWT token to decode
|
|
3107
|
+
* @returns True if user was successfully decoded and set, false otherwise
|
|
3108
|
+
*/
|
|
3109
|
+
const decodeAndSetUser = (token) => {
|
|
3110
|
+
const userData = decodeToken(token);
|
|
3111
|
+
if (userData) {
|
|
3112
|
+
user.set(userData);
|
|
3113
|
+
return true;
|
|
3114
|
+
}
|
|
3115
|
+
return false;
|
|
3116
|
+
};
|
|
3117
|
+
/**
|
|
3118
|
+
* Checks if a token is expired based on the expiration timestamp
|
|
3119
|
+
* @param expireAt The expiration timestamp in milliseconds
|
|
3120
|
+
* @returns True if the token is expired, false otherwise
|
|
3121
|
+
*/
|
|
3122
|
+
const isTokenExpired = (expireAt) => {
|
|
3123
|
+
if (!expireAt) return true;
|
|
3124
|
+
return Date.now() >= expireAt;
|
|
3125
|
+
};
|
|
3126
|
+
|
|
3127
|
+
//#endregion
|
|
3128
|
+
//#region src/auth/authStore.ts
|
|
3129
|
+
const isLogged = shared("isLogged", () => atom(initVals["isLogged"] === "true"));
|
|
3130
|
+
const accessToken = shared("accessToken", () => atom(initVals["accessToken"]));
|
|
3131
|
+
const refreshToken = shared("refreshToken", () => atom(initVals["refreshToken"]));
|
|
3132
|
+
const expireAt = shared("expireAt", () => atom(Number(initVals["expireAt"])));
|
|
3133
|
+
const authError = shared("authError", () => atom(null));
|
|
3134
|
+
const requiresAction = shared("requiresAction", () => atom(initVals["requiresAction"] || null));
|
|
3135
|
+
const limitedAccessToken = shared("limitedAccessToken", () => atom(initVals["limitedAccessToken"] || null));
|
|
3136
|
+
const inFlight = shared("getTokenPromise", () => ({ promise: null }));
|
|
3137
|
+
const PROVIDER_URL_MAP = {
|
|
3138
|
+
google: googleAuthUrl,
|
|
3139
|
+
apple: appleAuthUrl,
|
|
3140
|
+
microsoft: microsoftAuthUrl,
|
|
3141
|
+
telegram: telegramAuthUrl
|
|
3142
|
+
};
|
|
3143
|
+
const login = async (provider) => {
|
|
3144
|
+
await initDeferred.promise;
|
|
3145
|
+
const authUrlAtom = PROVIDER_URL_MAP[provider];
|
|
3146
|
+
if (!authUrlAtom) throw new Error(`Unsupported provider: ${provider}`);
|
|
3147
|
+
const authUrl = authUrlAtom.get();
|
|
3148
|
+
if (authUrl) window.location.href = authUrl;
|
|
3149
|
+
else throw new Error(`${provider} provider is not configured. Please check your initClient configuration.`);
|
|
3150
|
+
};
|
|
3151
|
+
const refreshAccessToken = async (refresh) => {
|
|
3152
|
+
const result = await refreshTokens("Bearer", refresh);
|
|
3153
|
+
if (!("requires_action" in result)) setTokens(result.access_token, result.refresh_token, result.expires_in);
|
|
3154
|
+
};
|
|
3155
|
+
const ensureValidAccessToken = async (refresh) => {
|
|
3156
|
+
if (accessToken.get() && !isTokenExpired(expireAt.get())) return;
|
|
3157
|
+
try {
|
|
3158
|
+
await refreshAccessToken(refresh);
|
|
3159
|
+
} catch (refreshError) {
|
|
3160
|
+
console.error("Token refresh failed in getToken:", refreshError);
|
|
3161
|
+
removeTokens();
|
|
3162
|
+
throw refreshError;
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
const getToken = async () => {
|
|
3166
|
+
if (inFlight.promise) return inFlight.promise;
|
|
3167
|
+
inFlight.promise = (async () => {
|
|
3168
|
+
try {
|
|
3169
|
+
await initDeferred.promise;
|
|
3170
|
+
if (requiresAction.get()) return void 0;
|
|
3171
|
+
const currentRefreshToken = refreshToken.get();
|
|
3172
|
+
if (!currentRefreshToken) return void 0;
|
|
3173
|
+
await ensureValidAccessToken(currentRefreshToken);
|
|
3174
|
+
const token = accessToken.get();
|
|
3175
|
+
if (token) decodeAndSetUser(token);
|
|
3176
|
+
return token;
|
|
3177
|
+
} catch (error) {
|
|
3178
|
+
console.warn("Failed to getToken(). Error: ", error);
|
|
3179
|
+
throw error;
|
|
3180
|
+
} finally {
|
|
3181
|
+
inFlight.promise = null;
|
|
3182
|
+
}
|
|
3183
|
+
})();
|
|
3184
|
+
return inFlight.promise;
|
|
3185
|
+
};
|
|
3186
|
+
/**
|
|
3187
|
+
* Sets authentication tokens in the store
|
|
3188
|
+
* @param access The access token
|
|
3189
|
+
* @param refresh The refresh token
|
|
3190
|
+
* @param expiresIn The token expiration time in seconds
|
|
3191
|
+
*/
|
|
3192
|
+
const setTokens = (access, refresh, expiresIn) => {
|
|
3193
|
+
accessToken.set(access);
|
|
3194
|
+
refreshToken.set(refresh);
|
|
3195
|
+
expireAt.set(Date.now() + expiresIn * 1e3);
|
|
3196
|
+
isLogged.set(true);
|
|
3197
|
+
limitedAccessToken.set(null);
|
|
3198
|
+
requiresAction.set(null);
|
|
3199
|
+
authError.set(null);
|
|
3200
|
+
decodeAndSetUser(access);
|
|
3201
|
+
};
|
|
3202
|
+
/**
|
|
3203
|
+
* Removes authentication tokens from the store
|
|
3204
|
+
*/
|
|
3205
|
+
const removeTokens = () => {
|
|
3206
|
+
accessToken.set("");
|
|
3207
|
+
refreshToken.set("");
|
|
3208
|
+
expireAt.set(0);
|
|
3209
|
+
isLogged.set(false);
|
|
3210
|
+
user.set(void 0);
|
|
3211
|
+
limitedAccessToken.set(null);
|
|
3212
|
+
requiresAction.set(null);
|
|
3213
|
+
authError.set(null);
|
|
3187
3214
|
};
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
});
|
|
3194
|
-
}, {
|
|
3195
|
-
[HTTP_STATUS.UNAUTHORIZED]: () => /* @__PURE__ */ new Error("User is not authorized to disconnect provider!"),
|
|
3196
|
-
[HTTP_STATUS.NOT_FOUND]: () => /* @__PURE__ */ new Error("Provider not found!"),
|
|
3197
|
-
[HTTP_STATUS.BAD_REQUEST]: () => /* @__PURE__ */ new Error("Cannot disconnect the last social provider!")
|
|
3198
|
-
});
|
|
3215
|
+
/**
|
|
3216
|
+
* Clears the auth error from the store
|
|
3217
|
+
*/
|
|
3218
|
+
const clearAuthError = () => {
|
|
3219
|
+
authError.set(null);
|
|
3199
3220
|
};
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3221
|
+
/**
|
|
3222
|
+
* Sets limited access state for users requiring additional action (e.g., Telegram users without email)
|
|
3223
|
+
* @param token The limited scope access token
|
|
3224
|
+
* @param action The required action (e.g., "add_email")
|
|
3225
|
+
*/
|
|
3226
|
+
const setLimitedAccessState = (token, action) => {
|
|
3227
|
+
limitedAccessToken.set(token);
|
|
3228
|
+
requiresAction.set(action);
|
|
3229
|
+
authError.set(null);
|
|
3230
|
+
isLogged.set(true);
|
|
3203
3231
|
};
|
|
3232
|
+
/**
|
|
3233
|
+
* Clears the limited access state (after email verification completes or user logs out)
|
|
3234
|
+
*/
|
|
3235
|
+
const clearLimitedAccessState = () => {
|
|
3236
|
+
limitedAccessToken.set(null);
|
|
3237
|
+
requiresAction.set(null);
|
|
3238
|
+
if (!(!!accessToken.get() && !!refreshToken.get())) isLogged.set(false);
|
|
3239
|
+
};
|
|
3240
|
+
isLogged.subscribe((value) => setStoreCookie("isLogged", value));
|
|
3241
|
+
accessToken.subscribe((value) => setStoreCookie("accessToken", value));
|
|
3242
|
+
refreshToken.subscribe((value) => setStoreCookie("refreshToken", value));
|
|
3243
|
+
expireAt.subscribe((value) => setStoreCookie("expireAt", value));
|
|
3244
|
+
requiresAction.subscribe((value) => setStoreCookie("requiresAction", value));
|
|
3245
|
+
limitedAccessToken.subscribe((value) => setStoreCookie("limitedAccessToken", value));
|
|
3204
3246
|
|
|
3205
3247
|
//#endregion
|
|
3206
|
-
//#region src/auth/api
|
|
3207
|
-
let tokenRefreshFunction = null;
|
|
3248
|
+
//#region src/auth/api-url.ts
|
|
3208
3249
|
/**
|
|
3209
|
-
*
|
|
3210
|
-
* This should be called during initialization
|
|
3250
|
+
* Global API base URL store
|
|
3211
3251
|
*/
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
|
|
3252
|
+
const apiURL = shared("apiURL", () => atom(""));
|
|
3253
|
+
|
|
3254
|
+
//#endregion
|
|
3255
|
+
//#region src/auth/api/sdk-client.ts
|
|
3256
|
+
function isWireErrorBody(error) {
|
|
3257
|
+
return typeof error === "object" && error !== null;
|
|
3258
|
+
}
|
|
3259
|
+
const state = shared("sdkClientState", () => ({
|
|
3260
|
+
configured: false,
|
|
3261
|
+
tokenResolver: getToken
|
|
3262
|
+
}));
|
|
3263
|
+
function setTokenResolver(resolver) {
|
|
3264
|
+
state.tokenResolver = resolver;
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Routes that do not require a Bearer token at the gateway edge. These
|
|
3268
|
+
* authenticate via credentials in the request body (or are webhooks verified by
|
|
3269
|
+
* signature), so attaching an Authorization header here would make the gateway
|
|
3270
|
+
* attempt to validate a stale/absent token and reject the request with 401.
|
|
3271
|
+
* Mirrors `publicRoutes` in backend/gateway/internal/routes/routes.go.
|
|
3272
|
+
*/
|
|
3273
|
+
const publicRoutes = [
|
|
3274
|
+
{
|
|
3275
|
+
method: "POST",
|
|
3276
|
+
path: "/auth/v1/token"
|
|
3277
|
+
},
|
|
3278
|
+
{
|
|
3279
|
+
method: "POST",
|
|
3280
|
+
path: "/auth/v1/register"
|
|
3281
|
+
},
|
|
3282
|
+
{
|
|
3283
|
+
method: "POST",
|
|
3284
|
+
path: "/auth/v1/login"
|
|
3285
|
+
},
|
|
3286
|
+
{
|
|
3287
|
+
method: "POST",
|
|
3288
|
+
path: "/auth/v1/email/verify"
|
|
3289
|
+
},
|
|
3290
|
+
{
|
|
3291
|
+
method: "POST",
|
|
3292
|
+
path: "/auth/v1/email/verify/resend"
|
|
3293
|
+
},
|
|
3294
|
+
{
|
|
3295
|
+
method: "POST",
|
|
3296
|
+
path: "/auth/v1/password/reset"
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
method: "POST",
|
|
3300
|
+
path: "/auth/v1/password/reset/confirm"
|
|
3301
|
+
},
|
|
3302
|
+
{
|
|
3303
|
+
method: "POST",
|
|
3304
|
+
path: "/auth/v1/verify-totp"
|
|
3305
|
+
},
|
|
3306
|
+
{
|
|
3307
|
+
method: "POST",
|
|
3308
|
+
path: "/auth/v1/verify-passkey"
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
method: "POST",
|
|
3312
|
+
path: "/auth/v1/invitations/",
|
|
3313
|
+
prefix: true
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
method: "POST",
|
|
3317
|
+
path: "/auth/v1/passkey/login/begin"
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
method: "POST",
|
|
3321
|
+
path: "/auth/v1/passkey/login/finish"
|
|
3322
|
+
},
|
|
3323
|
+
{
|
|
3324
|
+
method: "POST",
|
|
3325
|
+
path: "/auth/v1/logout"
|
|
3326
|
+
},
|
|
3327
|
+
{
|
|
3328
|
+
method: "POST",
|
|
3329
|
+
path: "/auth/v1/providers/connect"
|
|
3330
|
+
},
|
|
3331
|
+
{
|
|
3332
|
+
method: "POST",
|
|
3333
|
+
path: "/auth/v1/blog/unsubscribe/email"
|
|
3334
|
+
},
|
|
3335
|
+
{
|
|
3336
|
+
method: "POST",
|
|
3337
|
+
path: "/auth/v1/blog/broadcast"
|
|
3338
|
+
},
|
|
3339
|
+
{
|
|
3340
|
+
method: "GET",
|
|
3341
|
+
path: "/media/v1/videos/",
|
|
3342
|
+
prefix: true
|
|
3343
|
+
},
|
|
3344
|
+
{
|
|
3345
|
+
method: "GET",
|
|
3346
|
+
path: "/media/v1/images/",
|
|
3347
|
+
prefix: true
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
method: "GET",
|
|
3351
|
+
path: "/media/v1/languages"
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
method: "GET",
|
|
3355
|
+
path: "/posts/v1/feeds/",
|
|
3356
|
+
prefix: true
|
|
3357
|
+
},
|
|
3358
|
+
{
|
|
3359
|
+
method: "POST",
|
|
3360
|
+
path: "/billing/webhooks/stripe"
|
|
3361
|
+
},
|
|
3362
|
+
{
|
|
3363
|
+
method: "POST",
|
|
3364
|
+
path: "/webhooks/storage"
|
|
3365
|
+
},
|
|
3366
|
+
{
|
|
3367
|
+
method: "POST",
|
|
3368
|
+
path: "/platform/auth/v1/token"
|
|
3369
|
+
},
|
|
3370
|
+
{
|
|
3371
|
+
method: "POST",
|
|
3372
|
+
path: "/platform/auth/v1/refresh"
|
|
3373
|
+
}
|
|
3374
|
+
];
|
|
3375
|
+
function isPublicRoute(method, pathname) {
|
|
3376
|
+
const match = method.toUpperCase();
|
|
3377
|
+
return publicRoutes.some(({ method: m, path, prefix }) => match === m && (prefix ? pathname.startsWith(path) : pathname === path));
|
|
3378
|
+
}
|
|
3379
|
+
function configureSdkClient() {
|
|
3380
|
+
if (state.configured) return;
|
|
3381
|
+
state.configured = true;
|
|
3382
|
+
configureAllClients(apiURL.get());
|
|
3383
|
+
apiURL.subscribe((url) => {
|
|
3384
|
+
configureAllClients(url);
|
|
3385
|
+
});
|
|
3386
|
+
addClientInitializer((client) => {
|
|
3387
|
+
client.interceptors.request.use(async (request) => {
|
|
3388
|
+
if (request.headers.has("Authorization")) return request;
|
|
3389
|
+
const { pathname } = new URL(request.url);
|
|
3390
|
+
if (isPublicRoute(request.method, pathname)) return request;
|
|
3391
|
+
const token = await state.tokenResolver();
|
|
3392
|
+
if (!token) throw new ApiError("No access token available for an authenticated request", HTTP_STATUS.UNAUTHORIZED, pathname);
|
|
3393
|
+
request.headers.set("Authorization", `Bearer ${token}`);
|
|
3394
|
+
return request;
|
|
3395
|
+
});
|
|
3396
|
+
client.interceptors.error.use((error, response, request) => {
|
|
3397
|
+
if (error instanceof Error) return error;
|
|
3398
|
+
const body = isWireErrorBody(error) ? error : void 0;
|
|
3399
|
+
const status = response?.status ?? body?.code ?? 0;
|
|
3400
|
+
const message = body?.error || body?.details || (typeof error === "string" ? error : "Request failed");
|
|
3401
|
+
const endpoint = request ? new URL(request.url).pathname : "";
|
|
3402
|
+
return new ApiError(message, status, endpoint, error);
|
|
3403
|
+
});
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3215
3406
|
|
|
3216
3407
|
//#endregion
|
|
3217
3408
|
//#region src/auth/authConfig.ts
|
|
3218
|
-
|
|
3409
|
+
const config = shared("authConfig", () => ({ loginRedirectUrl: void 0 }));
|
|
3219
3410
|
const setLoginRedirectUrl = (url) => {
|
|
3220
|
-
loginRedirectUrl = url;
|
|
3411
|
+
config.loginRedirectUrl = url;
|
|
3221
3412
|
};
|
|
3222
3413
|
|
|
3223
3414
|
//#endregion
|
|
@@ -3228,24 +3419,26 @@ const setLoginRedirectUrl = (url) => {
|
|
|
3228
3419
|
* @returns A promise that resolves to the current access token (if available)
|
|
3229
3420
|
*/
|
|
3230
3421
|
const initClient = async (config) => {
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3422
|
+
try {
|
|
3423
|
+
await runInitSequence(config);
|
|
3424
|
+
} finally {
|
|
3425
|
+
completeOAuthCallback();
|
|
3426
|
+
}
|
|
3235
3427
|
return getToken();
|
|
3236
3428
|
};
|
|
3429
|
+
const runInitSequence = async (config) => {
|
|
3430
|
+
try {
|
|
3431
|
+
await initConfig(config);
|
|
3432
|
+
await initPage();
|
|
3433
|
+
} finally {
|
|
3434
|
+
initDeferred.resolve();
|
|
3435
|
+
}
|
|
3436
|
+
await initSocials();
|
|
3437
|
+
};
|
|
3237
3438
|
const initConfig = async (config) => {
|
|
3238
3439
|
apiURL.set(config.apiUrl);
|
|
3239
3440
|
configureSdkClient();
|
|
3240
3441
|
setLoginRedirectUrl(config.loginRedirectUrl);
|
|
3241
|
-
setTokenRefreshFunction(async () => {
|
|
3242
|
-
const currentRefreshToken = refreshToken.get();
|
|
3243
|
-
if (currentRefreshToken) {
|
|
3244
|
-
const result = await refreshTokens("Bearer", currentRefreshToken);
|
|
3245
|
-
if (result && !("requires_action" in result)) setTokens(result.access_token, result.refresh_token, result.expires_in);
|
|
3246
|
-
return getToken();
|
|
3247
|
-
}
|
|
3248
|
-
});
|
|
3249
3442
|
if (config.googleProvider) {
|
|
3250
3443
|
googleConfig.set(config.googleProvider);
|
|
3251
3444
|
updateGoogleAuthUrl();
|
|
@@ -3293,9 +3486,15 @@ const extractAuthErrorInfo = (error) => {
|
|
|
3293
3486
|
};
|
|
3294
3487
|
const handleInitPageApiError = (error) => {
|
|
3295
3488
|
const info = extractAuthErrorInfo(error);
|
|
3296
|
-
if (error.status === HTTP_STATUS.BAD_REQUEST && info.error === "invalid_grant")
|
|
3297
|
-
|
|
3298
|
-
|
|
3489
|
+
if (error.status === HTTP_STATUS.BAD_REQUEST && info.error === "invalid_grant") {
|
|
3490
|
+
setAuthErrorAndClear("email_not_verified", info.description, info.email);
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
if (error.status === HTTP_STATUS.CONFLICT) {
|
|
3494
|
+
setAuthErrorAndClear("provider_conflict", info.description, info.email);
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
setAuthErrorAndClear("provider_exchange_failed", info.error || info.description, info.email);
|
|
3299
3498
|
};
|
|
3300
3499
|
const exchangeProviderToken = async (provider, token) => {
|
|
3301
3500
|
const result = await refreshTokens(provider, token);
|
|
@@ -3303,14 +3502,18 @@ const exchangeProviderToken = async (provider, token) => {
|
|
|
3303
3502
|
};
|
|
3304
3503
|
const initPage = async () => {
|
|
3305
3504
|
const provider = detectProvider();
|
|
3306
|
-
if (!provider)
|
|
3505
|
+
if (!provider) {
|
|
3506
|
+
if (hasProviderResponse()) logUnusableProviderResponse();
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3307
3509
|
const token = getProviderToken(provider);
|
|
3308
3510
|
if (!token) return void 0;
|
|
3309
3511
|
try {
|
|
3310
3512
|
await exchangeProviderToken(provider, token);
|
|
3311
3513
|
} catch (error) {
|
|
3312
|
-
|
|
3313
|
-
throw error;
|
|
3514
|
+
logProviderExchangeFailure(provider, token, error);
|
|
3515
|
+
if (!(error instanceof ApiError)) throw error;
|
|
3516
|
+
handleInitPageApiError(error);
|
|
3314
3517
|
}
|
|
3315
3518
|
};
|
|
3316
3519
|
const initSocials = async () => {
|
|
@@ -3734,8 +3937,9 @@ const getUserInfo = async (userId) => {
|
|
|
3734
3937
|
};
|
|
3735
3938
|
const updateFullName = async (fullName) => {
|
|
3736
3939
|
return apiCall(async () => {
|
|
3940
|
+
const validatedInput = validateInput(UpdateNameSchema, { full_name: fullName });
|
|
3737
3941
|
await authV1UserServiceUpdateName({
|
|
3738
|
-
body:
|
|
3942
|
+
body: validatedInput,
|
|
3739
3943
|
throwOnError: true
|
|
3740
3944
|
});
|
|
3741
3945
|
}, {
|
|
@@ -3746,8 +3950,9 @@ const updateFullName = async (fullName) => {
|
|
|
3746
3950
|
};
|
|
3747
3951
|
const updateUsername = async (username) => {
|
|
3748
3952
|
return apiCall(async () => {
|
|
3953
|
+
const validatedInput = validateInput(UpdateUsernameSchema, { username });
|
|
3749
3954
|
await authV1UserServiceUpdateUsername({
|
|
3750
|
-
body:
|
|
3955
|
+
body: validatedInput,
|
|
3751
3956
|
throwOnError: true
|
|
3752
3957
|
});
|
|
3753
3958
|
}, {
|
|
@@ -3777,8 +3982,9 @@ const setupUserOTP = async () => {
|
|
|
3777
3982
|
};
|
|
3778
3983
|
const verifyUserOTP = async (code) => {
|
|
3779
3984
|
return apiCall(async () => {
|
|
3985
|
+
const validatedBody = validateInput(VerifyOTPCodeSchema, { code });
|
|
3780
3986
|
const { data } = await authV1OtpServiceVerifyOtp({
|
|
3781
|
-
body:
|
|
3987
|
+
body: validatedBody,
|
|
3782
3988
|
throwOnError: true
|
|
3783
3989
|
});
|
|
3784
3990
|
persistTokens(data);
|
|
@@ -3861,11 +4067,12 @@ const loginWithEmail = async (email, password) => {
|
|
|
3861
4067
|
};
|
|
3862
4068
|
const verifyTOTPForLogin = async (code, session_id) => {
|
|
3863
4069
|
return apiCall(async () => {
|
|
4070
|
+
const validatedInput = validateInput(LoginOTPVerifyRequestSchema, {
|
|
4071
|
+
code,
|
|
4072
|
+
session_id
|
|
4073
|
+
});
|
|
3864
4074
|
const { data } = await authV1OtpServiceVerifyTotpForLogin({
|
|
3865
|
-
body:
|
|
3866
|
-
code,
|
|
3867
|
-
session_id
|
|
3868
|
-
}),
|
|
4075
|
+
body: validatedInput,
|
|
3869
4076
|
throwOnError: true
|
|
3870
4077
|
});
|
|
3871
4078
|
persistTokens(data);
|
|
@@ -3916,13 +4123,14 @@ function isApiErrorBody(error) {
|
|
|
3916
4123
|
//#region src/auth/auth/register.ts
|
|
3917
4124
|
const registerWithEmail = async (email, password, subscribeToBlog, countryCode) => {
|
|
3918
4125
|
return apiCall(async () => {
|
|
4126
|
+
const validatedInput = validateInput(RegisterRequestSchema, {
|
|
4127
|
+
email,
|
|
4128
|
+
password,
|
|
4129
|
+
country_code: countryCode,
|
|
4130
|
+
subscribe_to_blog: subscribeToBlog
|
|
4131
|
+
});
|
|
3919
4132
|
const { data } = await authV1EmailServiceRegister({
|
|
3920
|
-
body:
|
|
3921
|
-
email,
|
|
3922
|
-
password,
|
|
3923
|
-
country_code: countryCode,
|
|
3924
|
-
subscribe_to_blog: subscribeToBlog
|
|
3925
|
-
}),
|
|
4133
|
+
body: validatedInput,
|
|
3926
4134
|
throwOnError: true
|
|
3927
4135
|
});
|
|
3928
4136
|
if (data.verification_id) return {
|
|
@@ -3935,8 +4143,9 @@ const registerWithEmail = async (email, password, subscribeToBlog, countryCode)
|
|
|
3935
4143
|
};
|
|
3936
4144
|
const resendEmailVerificationCode = async (email) => {
|
|
3937
4145
|
return apiCall(async () => {
|
|
4146
|
+
const validatedInput = validateInput(ResendEmailRequestSchema, { email });
|
|
3938
4147
|
const { data } = await authV1EmailServiceResendVerification({
|
|
3939
|
-
body:
|
|
4148
|
+
body: validatedInput,
|
|
3940
4149
|
throwOnError: true
|
|
3941
4150
|
});
|
|
3942
4151
|
if (data.verification_id) return {
|
|
@@ -3954,19 +4163,21 @@ const resendEmailVerificationCode = async (email) => {
|
|
|
3954
4163
|
//#region src/auth/auth/password.ts
|
|
3955
4164
|
const sendPasswordResetEmail = async (email) => {
|
|
3956
4165
|
return apiCall(async () => {
|
|
4166
|
+
const validatedInput = validateInput(ResendEmailRequestSchema, { email });
|
|
3957
4167
|
await authV1EmailServiceSendPasswordReset({
|
|
3958
|
-
body:
|
|
4168
|
+
body: validatedInput,
|
|
3959
4169
|
throwOnError: true
|
|
3960
4170
|
});
|
|
3961
4171
|
}, { [HTTP_STATUS.BAD_REQUEST]: () => /* @__PURE__ */ new Error("Bad request - invalid email or validation error") });
|
|
3962
4172
|
};
|
|
3963
4173
|
const confirmPasswordReset = async (token, password) => {
|
|
3964
4174
|
return apiCall(async () => {
|
|
4175
|
+
const validatedInput = validateInput(ResetPasswordRequestSchema, {
|
|
4176
|
+
token,
|
|
4177
|
+
new_password: password
|
|
4178
|
+
});
|
|
3965
4179
|
await authV1EmailServiceResetPassword({
|
|
3966
|
-
body:
|
|
3967
|
-
token,
|
|
3968
|
-
new_password: password
|
|
3969
|
-
}),
|
|
4180
|
+
body: validatedInput,
|
|
3970
4181
|
throwOnError: true
|
|
3971
4182
|
});
|
|
3972
4183
|
}, { [HTTP_STATUS.BAD_REQUEST]: () => /* @__PURE__ */ new Error("Bad request - invalid token or password") });
|
|
@@ -3976,8 +4187,9 @@ const confirmPasswordReset = async (token, password) => {
|
|
|
3976
4187
|
//#region src/auth/auth/email.ts
|
|
3977
4188
|
const initiateEmailChange = async (email) => {
|
|
3978
4189
|
return apiCall(async () => {
|
|
4190
|
+
const validatedInput = validateInput(ChangeEmailRequestSchema, { new_email: email });
|
|
3979
4191
|
const { data } = await authV1EmailServiceInitiateEmailChange({
|
|
3980
|
-
body:
|
|
4192
|
+
body: validatedInput,
|
|
3981
4193
|
throwOnError: true
|
|
3982
4194
|
});
|
|
3983
4195
|
if (data.verification_id) return {
|
|
@@ -3994,8 +4206,9 @@ const initiateEmailChange = async (email) => {
|
|
|
3994
4206
|
};
|
|
3995
4207
|
const addEmail = async (email) => {
|
|
3996
4208
|
return apiCall(async () => {
|
|
4209
|
+
const validatedInput = validateInput(ResendEmailRequestSchema, { email });
|
|
3997
4210
|
const { data } = await authV1EmailServiceAddEmail({
|
|
3998
|
-
body:
|
|
4211
|
+
body: validatedInput,
|
|
3999
4212
|
throwOnError: true
|
|
4000
4213
|
});
|
|
4001
4214
|
if (data.verification_id) return {
|
|
@@ -4289,10 +4502,11 @@ function serializeRegistrationCredential(cred) {
|
|
|
4289
4502
|
}
|
|
4290
4503
|
const finishPasskeyLogin = async (session_id, credential) => {
|
|
4291
4504
|
return apiCall(async () => {
|
|
4505
|
+
const serialized = serializeLoginCredential(credential);
|
|
4292
4506
|
const { data } = await authV1PasskeyServicePasskeyLoginFinish({
|
|
4293
4507
|
body: {
|
|
4294
4508
|
session_id,
|
|
4295
|
-
credential:
|
|
4509
|
+
credential: serialized
|
|
4296
4510
|
},
|
|
4297
4511
|
throwOnError: true
|
|
4298
4512
|
});
|
|
@@ -4332,11 +4546,12 @@ const beginPasskeyRegistration = async () => {
|
|
|
4332
4546
|
};
|
|
4333
4547
|
const finishPasskeyRegistration = async (session_id, name, credential) => {
|
|
4334
4548
|
return apiCall(async () => {
|
|
4549
|
+
const serialized = serializeRegistrationCredential(credential);
|
|
4335
4550
|
const { data } = await authV1PasskeyServicePasskeyRegisterFinish({
|
|
4336
4551
|
body: {
|
|
4337
4552
|
session_id,
|
|
4338
4553
|
name,
|
|
4339
|
-
credential:
|
|
4554
|
+
credential: serialized
|
|
4340
4555
|
},
|
|
4341
4556
|
throwOnError: true
|
|
4342
4557
|
});
|
|
@@ -4383,10 +4598,11 @@ const listPasskeys = async () => {
|
|
|
4383
4598
|
};
|
|
4384
4599
|
const verifyPasskeyForLogin = async (session_id, credential) => {
|
|
4385
4600
|
return apiCall(async () => {
|
|
4601
|
+
const serialized = serializeLoginCredential(credential);
|
|
4386
4602
|
const { data } = await authV1PasskeyServiceVerifyPasskeyForLogin({
|
|
4387
4603
|
body: {
|
|
4388
4604
|
session_id,
|
|
4389
|
-
credential:
|
|
4605
|
+
credential: serialized
|
|
4390
4606
|
},
|
|
4391
4607
|
throwOnError: true
|
|
4392
4608
|
});
|