@hexclave/js 1.0.125 → 1.0.126
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/{admin-app-impl-D9hrl67H.d.ts → admin-app-impl-Df-WtGg0.d.ts} +29 -13
- package/dist/admin-app-impl-Df-WtGg0.d.ts.map +1 -0
- package/dist/clickmap/clickmap-core.d.ts +1 -1
- package/dist/clickmap/index.d.ts +1 -1
- package/dist/{client-app-DLFxr4f7.d.ts → client-app-C7Tnf0n1.d.ts} +34 -6
- package/dist/client-app-C7Tnf0n1.d.ts.map +1 -0
- package/dist/{common-BDyEKSM6.d.ts → common-Dii3iHJR.d.ts} +15 -3
- package/dist/common-Dii3iHJR.d.ts.map +1 -0
- package/dist/{common-CRu2FsO9.d.ts → common-GgG4l8fG.d.ts} +2 -2
- package/dist/{common-CRu2FsO9.d.ts.map → common-GgG4l8fG.d.ts.map} +1 -1
- package/dist/dev-tool/dev-tool-core.d.ts +1 -1
- package/dist/dev-tool/index.d.ts +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +4 -4
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +18 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.external-token-store.test.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.external-token-store.test.js +382 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.external-token-store.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.init-analytics.test.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.init-analytics.test.js +66 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.init-analytics.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +66 -14
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.d.ts +31 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.d.ts.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.js +115 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +16 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/esm/lib/hexclave-app/common.d.ts +14 -2
- package/dist/esm/lib/hexclave-app/common.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/common.js +17 -1
- package/dist/esm/lib/hexclave-app/common.js.map +1 -1
- package/dist/esm/lib/hexclave-app/index.d.ts +2 -2
- package/dist/esm/lib/hexclave-app/index.js +2 -2
- package/dist/esm/lib/hexclave-app/users/index.d.ts +14 -1
- package/dist/esm/lib/hexclave-app/users/index.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/users/index.js.map +1 -1
- package/dist/{index-ZiEXZJBF.d.ts → index-BTa6Yu7N.d.ts} +2 -2
- package/dist/{index-ZiEXZJBF.d.ts.map → index-BTa6Yu7N.d.ts.map} +1 -1
- package/dist/{index-DLPccxVU.d.ts → index-DlOcbI9W.d.ts} +2 -2
- package/dist/{index-DLPccxVU.d.ts.map → index-DlOcbI9W.d.ts.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.external-token-store.test.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.external-token-store.test.js +381 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.external-token-store.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.init-analytics.test.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.init-analytics.test.js +65 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.init-analytics.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +65 -13
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.d.ts +31 -0
- package/dist/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.d.ts.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.js +117 -0
- package/dist/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
- package/dist/lib/hexclave-app/common.d.ts +2 -2
- package/dist/lib/hexclave-app/common.js +19 -0
- package/dist/lib/hexclave-app/common.js.map +1 -1
- package/dist/lib/hexclave-app/customers/index.d.ts +1 -1
- package/dist/lib/hexclave-app/data-vault/index.d.ts +1 -1
- package/dist/lib/hexclave-app/index.d.ts +4 -4
- package/dist/lib/hexclave-app/index.js +18 -0
- package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
- package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
- package/dist/lib/hexclave-app/url-targets.d.ts +1 -1
- package/dist/lib/hexclave-app/users/index.d.ts +1 -1
- package/dist/lib/hexclave-app/users/index.js.map +1 -1
- package/dist/pushed-config-error-overlay/index.d.ts +1 -1
- package/dist/{redirect-page-urls-C2CXwCcv.d.ts → redirect-page-urls-DCSFN6dg.d.ts} +2 -2
- package/dist/{redirect-page-urls-C2CXwCcv.d.ts.map → redirect-page-urls-DCSFN6dg.d.ts.map} +1 -1
- package/dist/{url-targets-BJgkwPGW.d.ts → url-targets-DYLcHumF.d.ts} +2 -2
- package/dist/{url-targets-BJgkwPGW.d.ts.map → url-targets-DYLcHumF.d.ts.map} +1 -1
- package/package.json +2 -2
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.external-token-store.test.ts +425 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.init-analytics.test.ts +79 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +124 -13
- package/src/lib/hexclave-app/apps/implementations/external-token-store-session-adapter.ts +190 -0
- package/src/lib/hexclave-app/apps/interfaces/client-app.ts +17 -0
- package/src/lib/hexclave-app/common.ts +33 -0
- package/src/lib/hexclave-app/index.ts +4 -0
- package/src/lib/hexclave-app/users/index.ts +13 -1
- package/dist/admin-app-impl-D9hrl67H.d.ts.map +0 -1
- package/dist/client-app-DLFxr4f7.d.ts.map +0 -1
- package/dist/common-BDyEKSM6.d.ts.map +0 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
|
|
2
|
+
//===========================================
|
|
3
|
+
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
|
+
//===========================================
|
|
5
|
+
import { KnownErrors } from "@hexclave/shared";
|
|
6
|
+
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { HexclaveClientApp, HexclaveClientAppConstructorOptions } from "../interfaces/client-app";
|
|
9
|
+
|
|
10
|
+
const baseOptions = {
|
|
11
|
+
baseUrl: "http://localhost:12345",
|
|
12
|
+
projectId: "00000000-0000-4000-8000-000000000000",
|
|
13
|
+
publishableClientKey: "stack-pk-test",
|
|
14
|
+
tokenStore: "memory",
|
|
15
|
+
redirectMethod: "none",
|
|
16
|
+
noAutomaticPrefetch: true,
|
|
17
|
+
} satisfies HexclaveClientAppConstructorOptions<true, string>;
|
|
18
|
+
|
|
19
|
+
const analyticsAppDisabledMessage = "initAnalytics() was called, but the Analytics app is not enabled on this Hexclave project";
|
|
20
|
+
|
|
21
|
+
// The backend's AnalyticsNotEnabled rejection only reaches the client through the event tracker's and session
|
|
22
|
+
// recorder's upload callbacks, which only exist in a browser; this simulates such a rejection arriving.
|
|
23
|
+
function simulateAnalyticsUploadResult(app: HexclaveClientApp, result: Result<Response, Error>) {
|
|
24
|
+
const handler: unknown = Reflect.get(app, "_handleAnalyticsUploadResult");
|
|
25
|
+
if (typeof handler !== "function") {
|
|
26
|
+
throw new Error("Expected HexclaveClientApp to have a _handleAnalyticsUploadResult method");
|
|
27
|
+
}
|
|
28
|
+
handler.call(app, result);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("HexclaveClientApp.initAnalytics", () => {
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not throw when analytics are configured", () => {
|
|
37
|
+
const app = new HexclaveClientApp(baseOptions);
|
|
38
|
+
expect(() => app.initAnalytics()).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("throws when analytics are disabled with the analytics option", () => {
|
|
42
|
+
const app = new HexclaveClientApp({ ...baseOptions, analytics: { enabled: false } });
|
|
43
|
+
expect(() => app.initAnalytics()).toThrow("`analytics.enabled` constructor option is set to false");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("does not throw when only session replays are disabled", () => {
|
|
47
|
+
const app = new HexclaveClientApp({ ...baseOptions, analytics: { replays: { enabled: false } } });
|
|
48
|
+
expect(() => app.initAnalytics()).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("reports an error when the backend rejects uploads after initAnalytics() was called", () => {
|
|
52
|
+
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
53
|
+
const app = new HexclaveClientApp(baseOptions);
|
|
54
|
+
app.initAnalytics();
|
|
55
|
+
|
|
56
|
+
simulateAnalyticsUploadResult(app, Result.error(new KnownErrors.AnalyticsNotEnabled()));
|
|
57
|
+
// Repeated rejections must not report the error again.
|
|
58
|
+
simulateAnalyticsUploadResult(app, Result.error(new KnownErrors.AnalyticsNotEnabled()));
|
|
59
|
+
|
|
60
|
+
const reports = consoleError.mock.calls.filter((args) => args.some((arg) => String(arg).includes(analyticsAppDisabledMessage)));
|
|
61
|
+
expect(reports).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws from initAnalytics() when the backend already rejected uploads before it was called", () => {
|
|
65
|
+
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
66
|
+
const app = new HexclaveClientApp(baseOptions);
|
|
67
|
+
|
|
68
|
+
simulateAnalyticsUploadResult(app, Result.error(new KnownErrors.AnalyticsNotEnabled()));
|
|
69
|
+
expect(consoleError.mock.calls.some((args) => args.some((arg) => String(arg).includes(analyticsAppDisabledMessage)))).toBe(false);
|
|
70
|
+
|
|
71
|
+
expect(() => app.initAnalytics()).toThrow(analyticsAppDisabledMessage);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("ignores upload failures unrelated to the Analytics app being disabled", () => {
|
|
75
|
+
const app = new HexclaveClientApp(baseOptions);
|
|
76
|
+
simulateAnalyticsUploadResult(app, Result.error(new Error("network error")));
|
|
77
|
+
expect(() => app.initAnalytics()).not.toThrow();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -64,12 +64,13 @@ import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson }
|
|
|
64
64
|
import { _HexclaveAdminAppImplIncomplete } from "./admin-app-impl";
|
|
65
65
|
import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getAnalyticsBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveApiUrls, resolveConstructorOptions } from "./common";
|
|
66
66
|
import { EventTracker } from "./event-tracker";
|
|
67
|
+
import { ExternalTokenStoreSessionAdapter, isExternalTokenStore } from "./external-token-store-session-adapter";
|
|
67
68
|
import { augmentUrlWithPersistedRedirectBackState, getRawAfterAuthReturnTo, saveRedirectBackStateFromUrl } from "./redirect-back-state";
|
|
68
69
|
import { recordRedirectAndThrowIfLoopDetected } from "./redirect-loop-breaker";
|
|
69
70
|
import type { CrossDomainHandoffParams } from "./redirect-page-urls";
|
|
70
71
|
import { crossDomainAuthQueryParams, getCrossDomainHandoffParamsFromCurrentUrl, planRedirectToHandler } from "./redirect-page-urls";
|
|
71
72
|
import { subscribeSessionRefresh } from "./session-refresh-subscription";
|
|
72
|
-
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson, getSessionReplayOptions } from "./session-replay";
|
|
73
|
+
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson, getSessionReplayOptions, isAnalyticsNotEnabledError } from "./session-replay";
|
|
73
74
|
|
|
74
75
|
export function stripBrowserActionQueryParam() {
|
|
75
76
|
let stripAttemptsRemaining = 20;
|
|
@@ -202,6 +203,10 @@ function throwSetupError(error: HexclaveSetupError): never {
|
|
|
202
203
|
throw error;
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
function createAnalyticsAppDisabledOnProjectError(): Error {
|
|
207
|
+
return new Error("initAnalytics() was called, but the Analytics app is not enabled on this Hexclave project, so the backend rejects all analytics uploads. Enable the Analytics app in the Hexclave dashboard (or in hexclave.config.ts), or remove the initAnalytics() call if this is intentional.");
|
|
208
|
+
}
|
|
209
|
+
|
|
205
210
|
function getRedirectHelperInstruction(handlerName: string): string {
|
|
206
211
|
if (handlerName === "handler") {
|
|
207
212
|
return "Use a page-specific redirect helper such as app.redirectToSignIn() instead.";
|
|
@@ -360,7 +365,10 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
360
365
|
private readonly _analyticsOptions: AnalyticsOptions | undefined;
|
|
361
366
|
private _sessionRecorder: SessionRecorder | null = null;
|
|
362
367
|
private _eventTracker: EventTracker | null = null;
|
|
368
|
+
private _initAnalyticsCalled = false;
|
|
369
|
+
private _analyticsAppDisabledOnProject = false;
|
|
363
370
|
private _pendingSignOut: Promise<void> | null = null;
|
|
371
|
+
private readonly _externalTokenStoreSessionAdapter: ExternalTokenStoreSessionAdapter;
|
|
364
372
|
|
|
365
373
|
private __DEMO_ENABLE_SLIGHT_FETCH_DELAY = false;
|
|
366
374
|
private readonly _ownedAdminApps = new DependenciesMap<[InternalSession, string], _HexclaveAdminAppImplIncomplete<false, string>>();
|
|
@@ -765,6 +773,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
765
773
|
}
|
|
766
774
|
});
|
|
767
775
|
}
|
|
776
|
+
this._externalTokenStoreSessionAdapter = new ExternalTokenStoreSessionAdapter(
|
|
777
|
+
(sessionOptions) => this._interface.createSession(sessionOptions),
|
|
778
|
+
async (providerId, token) => {
|
|
779
|
+
const exchange = await this._interface.exchangeExternalAuthToken(providerId, token);
|
|
780
|
+
return Result.orThrow(exchange).accessToken;
|
|
781
|
+
},
|
|
782
|
+
);
|
|
768
783
|
|
|
769
784
|
this._tokenStoreInit = resolvedOptions.tokenStore;
|
|
770
785
|
this._redirectMethod = resolvedOptions.redirectMethod || (isBrowserLike() ? "window" : "none");
|
|
@@ -806,24 +821,28 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
806
821
|
return anonUser._internalSession;
|
|
807
822
|
};
|
|
808
823
|
|
|
809
|
-
const
|
|
824
|
+
const recordsAnalytics = this._getAnalyticsDisabledReason() == null && isBrowserLike();
|
|
810
825
|
|
|
811
826
|
const sessionReplayOptions = getSessionReplayOptions(this._analyticsOptions);
|
|
812
|
-
if (
|
|
827
|
+
if (recordsAnalytics && sessionReplayOptions.enabled) {
|
|
813
828
|
this._sessionRecorder = new SessionRecorder({
|
|
814
829
|
projectId: this.projectId,
|
|
815
830
|
sendBatch: async (body, opts) => {
|
|
816
|
-
return
|
|
831
|
+
return this._handleAnalyticsUploadResult(
|
|
832
|
+
await this._interface.sendSessionReplayBatch(body, await getAnalyticsSession(), opts),
|
|
833
|
+
);
|
|
817
834
|
},
|
|
818
835
|
}, sessionReplayOptions);
|
|
819
836
|
this._sessionRecorder.start();
|
|
820
837
|
}
|
|
821
838
|
|
|
822
|
-
if (
|
|
839
|
+
if (recordsAnalytics) {
|
|
823
840
|
this._eventTracker = new EventTracker({
|
|
824
841
|
projectId: this.projectId,
|
|
825
842
|
sendBatch: async (body, opts) => {
|
|
826
|
-
return
|
|
843
|
+
return this._handleAnalyticsUploadResult(
|
|
844
|
+
await this._interface.sendAnalyticsEventBatch(body, await getAnalyticsSession(), opts),
|
|
845
|
+
);
|
|
827
846
|
},
|
|
828
847
|
});
|
|
829
848
|
this._eventTracker.start();
|
|
@@ -1684,6 +1703,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1684
1703
|
default: {
|
|
1685
1704
|
if (tokenStoreInit === null) {
|
|
1686
1705
|
return createEmptyTokenStore();
|
|
1706
|
+
} else if (typeof tokenStoreInit === "object" && isExternalTokenStore(tokenStoreInit)) {
|
|
1707
|
+
return this._externalTokenStoreSessionAdapter.getOrCreateTokenStore(tokenStoreInit, (store, previousSessionKey) => {
|
|
1708
|
+
this._sessionsByTokenStoreAndSessionKey
|
|
1709
|
+
.get(store)
|
|
1710
|
+
?.get(previousSessionKey)
|
|
1711
|
+
?.suggestAccessTokenExpired();
|
|
1712
|
+
});
|
|
1687
1713
|
} else if (typeof tokenStoreInit === "object" && "headers" in tokenStoreInit) {
|
|
1688
1714
|
if (this._requestTokenStores.has(tokenStoreInit)) return this._requestTokenStores.get(tokenStoreInit)!;
|
|
1689
1715
|
|
|
@@ -1746,28 +1772,29 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1746
1772
|
* - So different token stores are separated and don't leak information between each other, eg. if the same user sends two requests to the same server they should get a different session object
|
|
1747
1773
|
*/
|
|
1748
1774
|
private _sessionsByTokenStoreAndSessionKey = new WeakMap<Store<TokenObject>, Map<string, InternalSession>>();
|
|
1775
|
+
|
|
1776
|
+
private _getSessionKeyForTokenStore(tokenStore: Store<TokenObject>, tokenObj: TokenObject): string {
|
|
1777
|
+
return this._externalTokenStoreSessionAdapter.getSessionKey(tokenStore, tokenObj);
|
|
1778
|
+
}
|
|
1749
1779
|
/**
|
|
1750
1780
|
* @param overrideTokenObj The tokens to build the session for, if they haven't been written to the token store yet
|
|
1751
1781
|
* (so we can warm a sign-in's session before the token store publishes it).
|
|
1752
1782
|
*/
|
|
1753
1783
|
protected _getSessionFromTokenStore(tokenStore: Store<TokenObject>, overrideTokenObj?: TokenObject): InternalSession {
|
|
1754
1784
|
const tokenObj = overrideTokenObj ?? tokenStore.get();
|
|
1755
|
-
const sessionKey =
|
|
1785
|
+
const sessionKey = this._getSessionKeyForTokenStore(tokenStore, tokenObj);
|
|
1756
1786
|
const existing = sessionKey ? this._sessionsByTokenStoreAndSessionKey.get(tokenStore)?.get(sessionKey) : null;
|
|
1757
1787
|
if (existing) return existing;
|
|
1758
1788
|
|
|
1759
|
-
const session = this.
|
|
1760
|
-
refreshToken: tokenObj.refreshToken,
|
|
1761
|
-
accessToken: tokenObj.accessToken,
|
|
1762
|
-
});
|
|
1789
|
+
const session = this._externalTokenStoreSessionAdapter.createSessionForTokenStore(tokenStore, tokenObj, sessionKey);
|
|
1763
1790
|
session.onAccessTokenChange((newAccessToken) => {
|
|
1764
|
-
tokenStore.update((old) =>
|
|
1791
|
+
tokenStore.update((old) => this._getSessionKeyForTokenStore(tokenStore, old) === sessionKey ? {
|
|
1765
1792
|
...old,
|
|
1766
1793
|
accessToken: newAccessToken?.token ?? null
|
|
1767
1794
|
} : old);
|
|
1768
1795
|
});
|
|
1769
1796
|
session.onInvalidate(() => {
|
|
1770
|
-
tokenStore.update((old) =>
|
|
1797
|
+
tokenStore.update((old) => this._getSessionKeyForTokenStore(tokenStore, old) === sessionKey ? {
|
|
1771
1798
|
...old,
|
|
1772
1799
|
accessToken: null,
|
|
1773
1800
|
refreshToken: null,
|
|
@@ -1796,6 +1823,11 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1796
1823
|
if (!("accessToken" in tokens) || !("refreshToken" in tokens)) {
|
|
1797
1824
|
throw new HexclaveAssertionError("Invalid tokens object; can't sign in with this", { tokens });
|
|
1798
1825
|
}
|
|
1826
|
+
// An external token store keys its session by the provider's session, so Hexclave refresh tokens from a
|
|
1827
|
+
// first-party sign-in would be written into (and then silently replaced by) the provider's session.
|
|
1828
|
+
if (this.isExternalAuthApp()) {
|
|
1829
|
+
throw new Error("This app authenticates through an external provider and cannot sign in with Hexclave tokens. Sign in through the provider instead.");
|
|
1830
|
+
}
|
|
1799
1831
|
const attemptId = ++this._signInAttemptCounter;
|
|
1800
1832
|
const tokenStore = this._getOrCreateTokenStore(await this._createCookieHelper());
|
|
1801
1833
|
|
|
@@ -2251,6 +2283,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
2251
2283
|
emailAuthEnabled: crud.auth_with_email,
|
|
2252
2284
|
otpAuthEnabled: crud.otp_auth_enabled,
|
|
2253
2285
|
oauthProviders: crud.oauth_providers,
|
|
2286
|
+
externalAuthProviders: crud.external_auth_providers,
|
|
2254
2287
|
passkeyAuthEnabled: crud.passkey_auth_enabled,
|
|
2255
2288
|
isMultiFactorRequired: crud.requires_totp_mfa,
|
|
2256
2289
|
isAnonymous: crud.is_anonymous,
|
|
@@ -3254,6 +3287,69 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3254
3287
|
return crud && this._currentUserFromCrud(crud, session);
|
|
3255
3288
|
}
|
|
3256
3289
|
|
|
3290
|
+
isExternalAuthApp(): boolean {
|
|
3291
|
+
return typeof this._tokenStoreInit === "object"
|
|
3292
|
+
&& this._tokenStoreInit != null
|
|
3293
|
+
&& isExternalTokenStore(this._tokenStoreInit);
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
initAnalytics(): void {
|
|
3297
|
+
// This doesn't start anything: the constructor already starts analytics capture (see
|
|
3298
|
+
// _initializeAutomaticSideEffects). Pages that never import the module constructing the client app record no
|
|
3299
|
+
// analytics, which is easy to miss when the app is only imported where auth is used. Having a dedicated function
|
|
3300
|
+
// to call on every tracked page makes that import explicit, and gives setup docs and AI prompts a concrete step
|
|
3301
|
+
// to point at. Since the caller has thereby declared that they expect analytics, we fail loudly whenever we know
|
|
3302
|
+
// analytics won't be recorded.
|
|
3303
|
+
this._initAnalyticsCalled = true;
|
|
3304
|
+
|
|
3305
|
+
const disabledReason = this._getAnalyticsDisabledReason();
|
|
3306
|
+
if (disabledReason != null) {
|
|
3307
|
+
throw new Error(`initAnalytics() was called, but this Hexclave app will not record analytics because ${disabledReason}. Remove the initAnalytics() call if this is intentional.`);
|
|
3308
|
+
}
|
|
3309
|
+
if (this._analyticsAppDisabledOnProject) {
|
|
3310
|
+
throw createAnalyticsAppDisabledOnProjectError();
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
// Outside the browser (eg. during SSR of a client component), analytics are never recorded, but the same module
|
|
3314
|
+
// runs again in the browser where they are, so this isn't an error.
|
|
3315
|
+
if (isBrowserLike() && this._eventTracker == null) {
|
|
3316
|
+
throw new HexclaveAssertionError("initAnalytics() was called in the browser and _getAnalyticsDisabledReason() returned null, but the constructor did not start the event tracker. The two must agree on when analytics are recorded.");
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
/**
|
|
3321
|
+
* Returns why this app will never record analytics, or null if it records them whenever it runs in a browser. The
|
|
3322
|
+
* constructor and initAnalytics() both use this, so that the conditions under which analytics start and the
|
|
3323
|
+
* conditions under which initAnalytics() throws can't drift apart.
|
|
3324
|
+
*
|
|
3325
|
+
* Whether the Analytics app is enabled on the project is not part of this, as the client only learns that once the
|
|
3326
|
+
* backend rejects an upload; see _handleAnalyticsUploadResult.
|
|
3327
|
+
*/
|
|
3328
|
+
private _getAnalyticsDisabledReason(): string | null {
|
|
3329
|
+
if (this._analyticsOptions?.enabled === false) {
|
|
3330
|
+
return "the `analytics.enabled` constructor option is set to false";
|
|
3331
|
+
}
|
|
3332
|
+
if (this._options.automaticSideEffects === false) {
|
|
3333
|
+
return "the `automaticSideEffects` constructor option is set to false, which also disables analytics";
|
|
3334
|
+
}
|
|
3335
|
+
if (!this._hasPersistentTokenStore()) {
|
|
3336
|
+
return "the `tokenStore` constructor option is null; analytics require a persistent token store such as \"cookie\" (or \"nextjs-cookie\" in Next.js)";
|
|
3337
|
+
}
|
|
3338
|
+
return null;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
private _handleAnalyticsUploadResult(result: Result<Response, Error>): Result<Response, Error> {
|
|
3342
|
+
if (result.status === "error" && isAnalyticsNotEnabledError(result.error) && !this._analyticsAppDisabledOnProject) {
|
|
3343
|
+
this._analyticsAppDisabledOnProject = true;
|
|
3344
|
+
// initAnalytics() usually runs at module load, long before the first upload, so it can't throw this itself. If
|
|
3345
|
+
// it gets called after this point instead, it throws synchronously (see initAnalytics()).
|
|
3346
|
+
if (this._initAnalyticsCalled) {
|
|
3347
|
+
captureError("initAnalytics", createAnalyticsAppDisabledOnProjectError());
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return result;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3257
3353
|
|
|
3258
3354
|
_getTokenPartialUserFromSession(session: InternalSession, options: GetCurrentPartialUserOptions<HasTokenStore>): TokenPartialUser | null {
|
|
3259
3355
|
const accessToken = session.getAccessTokenIfNotExpiredYet(0, null);
|
|
@@ -3269,6 +3365,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3269
3365
|
primaryEmail: accessToken.payload.email,
|
|
3270
3366
|
displayName: accessToken.payload.name,
|
|
3271
3367
|
primaryEmailVerified: accessToken.payload.email_verified,
|
|
3368
|
+
selectedTeamId: accessToken.payload.selected_team_id,
|
|
3272
3369
|
isAnonymous,
|
|
3273
3370
|
isMultiFactorRequired: accessToken.payload.requires_totp_mfa,
|
|
3274
3371
|
isRestricted: accessToken.payload.is_restricted,
|
|
@@ -3281,11 +3378,16 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3281
3378
|
if (!auth) {
|
|
3282
3379
|
return null;
|
|
3283
3380
|
}
|
|
3381
|
+
const selectedTeamId = auth.selected_team_id ?? null;
|
|
3382
|
+
if (selectedTeamId !== null && typeof selectedTeamId !== "string") {
|
|
3383
|
+
throw new HexclaveAssertionError("Expected the Convex selected_team_id claim to be a string or null.");
|
|
3384
|
+
}
|
|
3284
3385
|
return {
|
|
3285
3386
|
id: auth.subject,
|
|
3286
3387
|
displayName: auth.name ?? null,
|
|
3287
3388
|
primaryEmail: auth.email ?? null,
|
|
3288
3389
|
primaryEmailVerified: auth.email_verified as boolean,
|
|
3390
|
+
selectedTeamId,
|
|
3289
3391
|
isAnonymous: auth.is_anonymous as boolean,
|
|
3290
3392
|
isMultiFactorRequired: auth.requires_totp_mfa as boolean,
|
|
3291
3393
|
isRestricted: auth.is_restricted as boolean,
|
|
@@ -3841,6 +3943,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3841
3943
|
const signOutOperation = (async () => {
|
|
3842
3944
|
// Preserve the old behavior of serializing concurrent sign-outs without coupling them to unrelated stores.
|
|
3843
3945
|
await previousSignOut;
|
|
3946
|
+
const externalTokenStore = this._externalTokenStoreSessionAdapter.getExternalTokenStoreForSession(session);
|
|
3947
|
+
if (externalTokenStore != null) {
|
|
3948
|
+
throw new HexclaveAssertionError(
|
|
3949
|
+
`Cannot sign out an externally authenticated session through Hexclave. Sign out through the ${externalTokenStore.providerId} provider instead.`,
|
|
3950
|
+
);
|
|
3951
|
+
}
|
|
3844
3952
|
await this._interface.signOut(session);
|
|
3845
3953
|
if (options?.redirectUrl) {
|
|
3846
3954
|
await this._redirectTo({ url: options.redirectUrl, replace: true });
|
|
@@ -4018,6 +4126,9 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
4018
4126
|
if (typeof this._redirectMethod !== "string") {
|
|
4019
4127
|
throw new HexclaveAssertionError("Cannot serialize to JSON from an application with a non-string redirect method");
|
|
4020
4128
|
}
|
|
4129
|
+
if (typeof this._tokenStoreInit === "object" && this._tokenStoreInit != null && isExternalTokenStore(this._tokenStoreInit)) {
|
|
4130
|
+
throw new HexclaveAssertionError("Cannot serialize an app with an external token store because its callbacks are not serializable. Construct this app in the client runtime instead.");
|
|
4131
|
+
}
|
|
4021
4132
|
|
|
4022
4133
|
const publishableClientKey = "publishableClientKey" in this._interface.options
|
|
4023
4134
|
? this._interface.options.publishableClientKey
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
|
|
2
|
+
//===========================================
|
|
3
|
+
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
|
+
//===========================================
|
|
5
|
+
import { KnownErrors } from "@hexclave/shared";
|
|
6
|
+
import type { ExternalAuthProviderId } from "@hexclave/shared/dist/interface/external-auth";
|
|
7
|
+
import { AccessToken, InternalSession } from "@hexclave/shared/dist/sessions";
|
|
8
|
+
import { throwErr } from "@hexclave/shared/dist/utils/errors";
|
|
9
|
+
import { Store } from "@hexclave/shared/dist/utils/stores";
|
|
10
|
+
import type { ExternalTokenStore } from "../../common";
|
|
11
|
+
import { createEmptyTokenStore, type TokenObject } from "./common";
|
|
12
|
+
|
|
13
|
+
type ExternalTokenStoreState = {
|
|
14
|
+
generation: number,
|
|
15
|
+
providerSessionId: string | null | undefined,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type SessionOptions = {
|
|
19
|
+
refreshToken: string | null,
|
|
20
|
+
accessToken?: string | null,
|
|
21
|
+
sessionKey?: string,
|
|
22
|
+
refreshAccessTokenWithoutRefreshTokenCallback?: () => Promise<AccessToken | null>,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function isExternalTokenStore(value: object): value is ExternalTokenStore {
|
|
26
|
+
return "type" in value && value.type === "external";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ExternalTokenStoreSessionAdapter {
|
|
30
|
+
private readonly storesByExternalTokenStore = new WeakMap<ExternalTokenStore, Store<TokenObject>>();
|
|
31
|
+
private readonly externalTokenStoresByStore = new WeakMap<Store<TokenObject>, ExternalTokenStore>();
|
|
32
|
+
private readonly externalTokenStoresBySession = new WeakMap<InternalSession, ExternalTokenStore>();
|
|
33
|
+
private readonly states = new WeakMap<ExternalTokenStore, ExternalTokenStoreState>();
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly createSession: (options: SessionOptions) => InternalSession,
|
|
37
|
+
private readonly exchangeToken: (providerId: ExternalAuthProviderId, token: string) => Promise<string>,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
getOrCreateTokenStore(
|
|
41
|
+
externalTokenStore: ExternalTokenStore,
|
|
42
|
+
onProviderChange: (store: Store<TokenObject>, previousSessionKey: string) => void,
|
|
43
|
+
): Store<TokenObject> {
|
|
44
|
+
const existing = this.storesByExternalTokenStore.get(externalTokenStore);
|
|
45
|
+
if (existing != null) {
|
|
46
|
+
return existing;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const store = createEmptyTokenStore();
|
|
50
|
+
this.storesByExternalTokenStore.set(externalTokenStore, store);
|
|
51
|
+
this.externalTokenStoresByStore.set(store, externalTokenStore);
|
|
52
|
+
this.states.set(externalTokenStore, {
|
|
53
|
+
generation: 0,
|
|
54
|
+
providerSessionId: externalTokenStore.getSessionId?.(),
|
|
55
|
+
});
|
|
56
|
+
externalTokenStore.subscribe?.(() => {
|
|
57
|
+
const state = this.states.get(externalTokenStore) ?? throwErr("External token store state was not initialized");
|
|
58
|
+
// Derive the previous session's key from the recorded state, not from a live getSessionId()
|
|
59
|
+
// read: by the time this notification fires, the provider has already switched sessions, so a
|
|
60
|
+
// live read would compute the *new* key and the expiry hint would target the wrong session.
|
|
61
|
+
const previousSessionKey = this.buildSessionKey(externalTokenStore, state);
|
|
62
|
+
const providerSessionId = externalTokenStore.getSessionId?.();
|
|
63
|
+
if (externalTokenStore.getSessionId == null || providerSessionId !== state.providerSessionId) {
|
|
64
|
+
state.generation += 1;
|
|
65
|
+
state.providerSessionId = providerSessionId;
|
|
66
|
+
}
|
|
67
|
+
onProviderChange(store, previousSessionKey);
|
|
68
|
+
store.set({ accessToken: null, refreshToken: null });
|
|
69
|
+
});
|
|
70
|
+
return store;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getSessionKey(tokenStore: Store<TokenObject>, tokenObject: TokenObject): string {
|
|
74
|
+
const externalTokenStore = this.externalTokenStoresByStore.get(tokenStore);
|
|
75
|
+
if (externalTokenStore == null) {
|
|
76
|
+
return InternalSession.calculateSessionKey(tokenObject);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const state = this.states.get(externalTokenStore) ?? throwErr("External token store state was not initialized");
|
|
80
|
+
// Read the provider session id live (instead of from state) so a session switch is reflected
|
|
81
|
+
// immediately, even before the store's subscribe notification has fired.
|
|
82
|
+
return this.buildSessionKey(externalTokenStore, {
|
|
83
|
+
generation: state.generation,
|
|
84
|
+
providerSessionId: externalTokenStore.getSessionId?.(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private buildSessionKey(externalTokenStore: ExternalTokenStore, state: ExternalTokenStoreState): string {
|
|
89
|
+
if (externalTokenStore.getSessionId == null) {
|
|
90
|
+
return `external-${externalTokenStore.providerId}-${state.generation}`;
|
|
91
|
+
}
|
|
92
|
+
return state.providerSessionId == null
|
|
93
|
+
? "not-logged-in"
|
|
94
|
+
: `external-${externalTokenStore.providerId}-${state.providerSessionId}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
createSessionForTokenStore(
|
|
98
|
+
tokenStore: Store<TokenObject>,
|
|
99
|
+
tokenObject: TokenObject,
|
|
100
|
+
sessionKey: string,
|
|
101
|
+
): InternalSession {
|
|
102
|
+
const externalTokenStore = this.externalTokenStoresByStore.get(tokenStore);
|
|
103
|
+
let session: InternalSession;
|
|
104
|
+
if (externalTokenStore == null) {
|
|
105
|
+
session = this.createSession({
|
|
106
|
+
refreshToken: tokenObject.refreshToken,
|
|
107
|
+
accessToken: tokenObject.accessToken,
|
|
108
|
+
});
|
|
109
|
+
} else if (sessionKey === "not-logged-in") {
|
|
110
|
+
// A provider can clear its session before its token store notification arrives; never let
|
|
111
|
+
// that transiently expose the previous provider session's cached Hexclave token.
|
|
112
|
+
session = this.createSession({
|
|
113
|
+
refreshToken: null,
|
|
114
|
+
accessToken: null,
|
|
115
|
+
sessionKey,
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
session = this.createSession({
|
|
119
|
+
refreshToken: null,
|
|
120
|
+
accessToken: tokenObject.accessToken,
|
|
121
|
+
sessionKey,
|
|
122
|
+
refreshAccessTokenWithoutRefreshTokenCallback: async () => {
|
|
123
|
+
const state = this.states.get(externalTokenStore) ?? throwErr("External token store state was not initialized");
|
|
124
|
+
const expectedGeneration = state.generation;
|
|
125
|
+
const assertSessionIdentityUnchanged = () => {
|
|
126
|
+
const liveSessionKey = this.buildSessionKey(externalTokenStore, {
|
|
127
|
+
generation: state.generation,
|
|
128
|
+
providerSessionId: externalTokenStore.getSessionId?.(),
|
|
129
|
+
});
|
|
130
|
+
if (state.generation !== expectedGeneration || liveSessionKey !== sessionKey) {
|
|
131
|
+
throw new Error("The external provider session changed while exchanging a token; retrying with the current provider session.");
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
// Returning null from this callback permanently invalidates the cached InternalSession, so
|
|
135
|
+
// we must only do it when the provider session is definitively gone — for transient states
|
|
136
|
+
// we throw instead, which surfaces the failure to the current caller but lets the next
|
|
137
|
+
// request retry the exchange.
|
|
138
|
+
const attemptExchange = async (isRetry: boolean): Promise<AccessToken | null> => {
|
|
139
|
+
assertSessionIdentityUnchanged();
|
|
140
|
+
const externalToken = await externalTokenStore.getToken();
|
|
141
|
+
assertSessionIdentityUnchanged();
|
|
142
|
+
if (externalToken == null) {
|
|
143
|
+
if (externalTokenStore.getSessionId?.() != null) {
|
|
144
|
+
// The provider reports an active session but handed out no token; this is usually a
|
|
145
|
+
// transient state (eg. its SDK is still initializing), so don't treat it as signed out.
|
|
146
|
+
throw new Error("The external token store returned no token even though it reports an active provider session. This is usually transient (eg. the provider SDK is still initializing) and the exchange will be retried on the next request.");
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const accessToken = await this.exchangeToken(externalTokenStore.providerId, externalToken);
|
|
152
|
+
const validatedAccessToken = AccessToken.createIfValid(accessToken)
|
|
153
|
+
?? throwErr("External authentication exchange returned an invalid Hexclave access token");
|
|
154
|
+
// An explicit session key bypasses InternalSession's identity check. Refuse to install
|
|
155
|
+
// a token if the provider switched accounts while the exchange was in flight instead
|
|
156
|
+
// of returning null, which would permanently invalidate the reusable session.
|
|
157
|
+
assertSessionIdentityUnchanged();
|
|
158
|
+
return validatedAccessToken;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!KnownErrors.InvalidExternalAuthToken.isInstance(error)) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
// The server rejects provider tokens that expired in flight with the same error as
|
|
164
|
+
// genuinely bad ones, so retry once with a freshly fetched provider token: the provider
|
|
165
|
+
// SDK replaces an expired token. If the fresh token is rejected too, the cause is not
|
|
166
|
+
// transient (eg. an issuer or client-ID misconfiguration), and it does not mean the
|
|
167
|
+
// provider session ended either: the backend re-establishes a Hexclave session for any
|
|
168
|
+
// valid provider token, so a rejection never signals revocation. Propagate the error
|
|
169
|
+
// instead of returning null, which would permanently invalidate this session and make
|
|
170
|
+
// a misconfiguration look like a silent sign-out.
|
|
171
|
+
if (isRetry) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
return await attemptExchange(true);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return await attemptExchange(false);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (externalTokenStore != null) {
|
|
182
|
+
this.externalTokenStoresBySession.set(session, externalTokenStore);
|
|
183
|
+
}
|
|
184
|
+
return session;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getExternalTokenStoreForSession(session: InternalSession): ExternalTokenStore | undefined {
|
|
188
|
+
return this.externalTokenStoresBySession.get(session);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -83,6 +83,23 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
|
|
83
83
|
* `redirectToSignIn()`, `redirectToSignUp()`, `redirectToSignOut()`, or `redirectToAccountSettings()`.
|
|
84
84
|
*/
|
|
85
85
|
readonly urls: Readonly<ResolvedHandlerUrls>,
|
|
86
|
+
/** Whether this app delegates authentication to an external provider token store. */
|
|
87
|
+
isExternalAuthApp(): boolean,
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Marks the current page as one that should record analytics (page views, clicks, session replays, and clickmaps).
|
|
91
|
+
*
|
|
92
|
+
* Analytics capture starts automatically when the client app is constructed in the browser, so this function doesn't
|
|
93
|
+
* start anything itself. Call it at the top level of a client-side module that is loaded on every page you want to
|
|
94
|
+
* track (for example, your app's root layout or entry file); the call guarantees that the module creating the client
|
|
95
|
+
* app is actually imported, and therefore constructed, on that page.
|
|
96
|
+
*
|
|
97
|
+
* Throws if this app will never record analytics because of its constructor options (`analytics.enabled: false`,
|
|
98
|
+
* `automaticSideEffects: false`, or a `null` token store). If the Analytics app turns out to be disabled on the
|
|
99
|
+
* project, an error is reported as soon as the backend rejects the first upload. Outside the browser (eg. during
|
|
100
|
+
* server-side rendering), it doesn't throw for that reason alone.
|
|
101
|
+
*/
|
|
102
|
+
initAnalytics(): void,
|
|
86
103
|
|
|
87
104
|
signInWithOAuth(provider: string, options?: { returnTo?: string }): Promise<void>,
|
|
88
105
|
signInWithCredential(options: { email: string, password: string, noRedirect?: boolean }): Promise<Result<undefined, KnownErrors["EmailPasswordMismatch"] | KnownErrors["InvalidTotpCode"]>>,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
4
|
//===========================================
|
|
5
5
|
import { ProviderType } from "@hexclave/shared/dist/utils/oauth";
|
|
6
|
+
import type { ExternalAuthProviderId } from "@hexclave/shared/dist/interface/external-auth";
|
|
6
7
|
import type { GenericQueryCtx, UserIdentity } from "convex/server";
|
|
7
8
|
export type {
|
|
8
9
|
DefaultHandlerUrlTarget,
|
|
@@ -87,6 +88,37 @@ export type RequestLike = {
|
|
|
87
88
|
| Record<string, string | null>,
|
|
88
89
|
};
|
|
89
90
|
|
|
91
|
+
export type ExternalTokenStore = {
|
|
92
|
+
readonly type: "external",
|
|
93
|
+
readonly providerId: ExternalAuthProviderId,
|
|
94
|
+
getSessionId?(): string | null,
|
|
95
|
+
getToken(): Promise<string | null>,
|
|
96
|
+
/** The adapter retains this subscription for its own lifetime and does not unsubscribe it. */
|
|
97
|
+
subscribe?(callback: () => void): () => void,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type ExternalTokenStoreOptions = Omit<ExternalTokenStore, "type" | "providerId">;
|
|
101
|
+
|
|
102
|
+
function createExternalTokenStore(providerId: ExternalAuthProviderId, options: ExternalTokenStoreOptions): ExternalTokenStore {
|
|
103
|
+
return {
|
|
104
|
+
...options,
|
|
105
|
+
type: "external",
|
|
106
|
+
providerId,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clerkTokenStore(options: ExternalTokenStoreOptions): ExternalTokenStore {
|
|
111
|
+
return createExternalTokenStore("clerk-integration", options);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function betterAuthTokenStore(options: ExternalTokenStoreOptions): ExternalTokenStore {
|
|
115
|
+
return createExternalTokenStore("better-auth-integration", options);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function workosTokenStore(options: ExternalTokenStoreOptions): ExternalTokenStore {
|
|
119
|
+
return createExternalTokenStore("workos-integration", options);
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
export type TokenStoreInit<HasTokenStore extends boolean = boolean> =
|
|
91
123
|
HasTokenStore extends true ? (
|
|
92
124
|
| "cookie"
|
|
@@ -94,6 +126,7 @@ export type TokenStoreInit<HasTokenStore extends boolean = boolean> =
|
|
|
94
126
|
| "memory"
|
|
95
127
|
| RequestLike
|
|
96
128
|
| { accessToken: string, refreshToken: string }
|
|
129
|
+
| ExternalTokenStore
|
|
97
130
|
)
|
|
98
131
|
: HasTokenStore extends false ? null
|
|
99
132
|
: TokenStoreInit<true> | TokenStoreInit<false>;
|
|
@@ -83,13 +83,17 @@ export type {
|
|
|
83
83
|
} from "./internal-api-keys";
|
|
84
84
|
|
|
85
85
|
export {
|
|
86
|
+
betterAuthTokenStore,
|
|
87
|
+
clerkTokenStore,
|
|
86
88
|
hexclaveAppInternalsSymbol,
|
|
89
|
+
workosTokenStore,
|
|
87
90
|
} from "./common";
|
|
88
91
|
export {
|
|
89
92
|
getPagePrompt,
|
|
90
93
|
} from "./url-targets";
|
|
91
94
|
export type {
|
|
92
95
|
GetCurrentUserOptions,
|
|
96
|
+
ExternalTokenStore,
|
|
93
97
|
/** @deprecated Use GetCurrentUserOptions instead */
|
|
94
98
|
GetCurrentUserOptions as GetUserOptions,
|
|
95
99
|
HandlerUrlOptions,
|