@hexclave/tanstack-start 1.0.19 → 1.0.21
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/clickmap/clickmap-core.js +1 -1
- package/dist/clickmap/index.js +2 -2
- package/dist/components/api-key-dialogs.js +2 -2
- package/dist/components/credential-sign-in.js +6 -2
- package/dist/components/credential-sign-in.js.map +1 -1
- package/dist/components/credential-sign-up.js +1 -1
- package/dist/components/magic-link-sign-in.js +1 -1
- package/dist/components/team-switcher.js +4 -6
- package/dist/components/team-switcher.js.map +1 -1
- package/dist/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/emails-section.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/password-section.js +1 -1
- package/dist/components-page/account-settings/teams/team-creation-page.js +1 -1
- package/dist/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
- package/dist/components-page/auth-page.js +3 -3
- package/dist/components-page/auth-page.js.map +1 -1
- package/dist/components-page/cli-auth-confirm.js +1 -1
- package/dist/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/components-page/forgot-password.js +6 -2
- package/dist/components-page/forgot-password.js.map +1 -1
- package/dist/components-page/oauth-callback.js +7 -2
- package/dist/components-page/oauth-callback.js.map +1 -1
- package/dist/components-page/onboarding.js +1 -1
- package/dist/components-page/password-reset.js +1 -1
- package/dist/components-page/team-creation.js +3 -4
- package/dist/components-page/team-creation.js.map +1 -1
- package/dist/dev-tool/dev-tool-core.js +1 -1
- package/dist/dev-tool/index.js +1 -1
- package/dist/esm/clickmap/clickmap-core.js +1 -1
- package/dist/esm/clickmap/index.js +2 -2
- package/dist/esm/components/api-key-dialogs.js +2 -2
- package/dist/esm/components/credential-sign-in.js +6 -2
- package/dist/esm/components/credential-sign-in.js.map +1 -1
- package/dist/esm/components/credential-sign-up.js +1 -1
- package/dist/esm/components/magic-link-sign-in.js +1 -1
- package/dist/esm/components/team-switcher.js +4 -6
- package/dist/esm/components/team-switcher.js.map +1 -1
- package/dist/esm/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
- package/dist/esm/components-page/account-settings/email-and-auth/emails-section.js +1 -1
- package/dist/esm/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
- package/dist/esm/components-page/account-settings/email-and-auth/password-section.js +1 -1
- package/dist/esm/components-page/account-settings/teams/team-creation-page.js +1 -1
- package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
- package/dist/esm/components-page/auth-page.js +3 -3
- package/dist/esm/components-page/auth-page.js.map +1 -1
- package/dist/esm/components-page/cli-auth-confirm.js +1 -1
- package/dist/esm/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/esm/components-page/forgot-password.js +6 -2
- package/dist/esm/components-page/forgot-password.js.map +1 -1
- package/dist/esm/components-page/oauth-callback.js +7 -2
- package/dist/esm/components-page/oauth-callback.js.map +1 -1
- package/dist/esm/components-page/onboarding.js +1 -1
- package/dist/esm/components-page/password-reset.js +1 -1
- package/dist/esm/components-page/team-creation.js +3 -4
- package/dist/esm/components-page/team-creation.js.map +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js +1 -1
- package/dist/esm/dev-tool/index.js +1 -1
- package/dist/esm/generated/quetzal-translations.d.ts +2 -2
- package/dist/esm/lib/auth.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
- 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.cross-domain.test.js +34 -5
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
- 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.js +26 -6
- 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/event-tracker.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +18 -14
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +3 -3
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +20 -14
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +3 -2
- 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/project-configs/index.d.ts +7 -0
- package/dist/esm/lib/hexclave-app/project-configs/index.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/projects/index.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/projects/index.js +1 -1
- package/dist/esm/lib/hexclave-app/projects/index.js.map +1 -1
- package/dist/esm/providers/theme-provider.js +1 -1
- package/dist/generated/quetzal-translations.d.ts +2 -2
- package/dist/lib/auth.js +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +34 -5
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +26 -6
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +17 -13
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +3 -3
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js +20 -13
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +3 -2
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/lib/hexclave-app/project-configs/index.d.ts +7 -0
- package/dist/lib/hexclave-app/project-configs/index.d.ts.map +1 -1
- package/dist/lib/hexclave-app/projects/index.d.ts.map +1 -1
- package/dist/lib/hexclave-app/projects/index.js +1 -1
- package/dist/lib/hexclave-app/projects/index.js.map +1 -1
- package/dist/providers/theme-provider.js +1 -1
- package/package.json +3 -3
- package/src/components/credential-sign-in.tsx +8 -1
- package/src/components/team-switcher.tsx +3 -5
- package/src/components-page/auth-page.tsx +2 -2
- package/src/components-page/forgot-password.tsx +7 -1
- package/src/components-page/oauth-callback.tsx +9 -1
- package/src/components-page/team-creation.tsx +2 -3
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +36 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +43 -4
- package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +5 -13
- package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +19 -14
- package/src/lib/hexclave-app/apps/implementations/server-app-impl.ts +2 -2
- package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +4 -20
- package/src/lib/hexclave-app/apps/implementations/session-replay.ts +19 -12
- package/src/lib/hexclave-app/apps/interfaces/client-app.ts +3 -2
- package/src/lib/hexclave-app/project-configs/index.ts +8 -0
- package/src/lib/hexclave-app/projects/index.ts +13 -11
|
@@ -31,7 +31,6 @@ export function TeamCreation(props: { fullPage?: boolean }) {
|
|
|
31
31
|
const project = app.useProject();
|
|
32
32
|
const user = useUser({ or: 'redirect' });
|
|
33
33
|
const [loading, setLoading] = useState(false);
|
|
34
|
-
const navigate = app.useNavigate();
|
|
35
34
|
|
|
36
35
|
if (!project.config.clientTeamCreationEnabled) {
|
|
37
36
|
return <MessageCard title={t('Team creation is not enabled')} />;
|
|
@@ -41,8 +40,8 @@ export function TeamCreation(props: { fullPage?: boolean }) {
|
|
|
41
40
|
setLoading(true);
|
|
42
41
|
|
|
43
42
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
await user.createTeam({ displayName: data.displayName });
|
|
44
|
+
await app.redirectToAccountSettings();
|
|
46
45
|
} finally {
|
|
47
46
|
setLoading(false);
|
|
48
47
|
}
|
|
@@ -273,6 +273,8 @@ describe("StackClientApp cross-domain auth", () => {
|
|
|
273
273
|
protocol: "https:",
|
|
274
274
|
hostname: "demo.stack-auth.com",
|
|
275
275
|
},
|
|
276
|
+
addEventListener: () => {},
|
|
277
|
+
removeEventListener: () => {},
|
|
276
278
|
} as any;
|
|
277
279
|
|
|
278
280
|
const clientApp = new StackClientApp({
|
|
@@ -460,6 +462,40 @@ describe("StackClientApp cross-domain auth", () => {
|
|
|
460
462
|
}
|
|
461
463
|
});
|
|
462
464
|
|
|
465
|
+
it("throws when public app.urls reads would return hosted component URLs", () => {
|
|
466
|
+
const clientApp = new StackClientApp({
|
|
467
|
+
baseUrl: "http://localhost:12345",
|
|
468
|
+
projectId: "00000000-0000-4000-8000-000000000003",
|
|
469
|
+
publishableClientKey: "stack-pk-test",
|
|
470
|
+
tokenStore: "memory",
|
|
471
|
+
redirectMethod: "window",
|
|
472
|
+
urls: {
|
|
473
|
+
default: { type: "hosted" },
|
|
474
|
+
},
|
|
475
|
+
noAutomaticPrefetch: true,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(() => clientApp.urls.signIn).toThrowError(/app\.urls\.signIn cannot be used when this app is configured to use hosted components.*Use app\.redirectToSignIn\(\) instead/s);
|
|
479
|
+
expect(() => clientApp.urls.signOut).toThrowError(/app\.urls\.signOut cannot be used when this app is configured to use hosted components.*Use app\.redirectToSignOut\(\) instead/s);
|
|
480
|
+
expect(clientApp.urls.afterSignIn).toBe("/");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("keeps public app.urls reads available for non-hosted targets", () => {
|
|
484
|
+
const clientApp = new StackClientApp({
|
|
485
|
+
baseUrl: "http://localhost:12345",
|
|
486
|
+
projectId: "00000000-0000-4000-8000-000000000003",
|
|
487
|
+
publishableClientKey: "stack-pk-test",
|
|
488
|
+
tokenStore: "memory",
|
|
489
|
+
redirectMethod: "window",
|
|
490
|
+
urls: {
|
|
491
|
+
handler: "/custom-handler",
|
|
492
|
+
},
|
|
493
|
+
noAutomaticPrefetch: true,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
expect(clientApp.urls.signIn).toBe("/custom-handler/sign-in");
|
|
497
|
+
});
|
|
498
|
+
|
|
463
499
|
it("keeps default hosted signOut() on the source domain when afterSignOut is not configured", async () => {
|
|
464
500
|
const clientApp = new StackClientApp({
|
|
465
501
|
baseUrl: "http://localhost:12345",
|
|
@@ -88,6 +88,38 @@ const nestedCrossDomainAuthQueryParams = {
|
|
|
88
88
|
afterCallbackRedirectUrl: "after_callback_redirect_url",
|
|
89
89
|
} as const;
|
|
90
90
|
|
|
91
|
+
function getRedirectHelperInstruction(handlerName: string): string {
|
|
92
|
+
if (handlerName === "handler") {
|
|
93
|
+
return "Use a page-specific redirect helper such as app.redirectToSignIn() instead.";
|
|
94
|
+
}
|
|
95
|
+
const redirectMethodName = `redirectTo${handlerName.slice(0, 1).toUpperCase()}${handlerName.slice(1)}`;
|
|
96
|
+
return `Use app.${redirectMethodName}() instead.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createUrlsForPublicAccess(options: {
|
|
100
|
+
urls: ResolvedHandlerUrls,
|
|
101
|
+
projectId: string,
|
|
102
|
+
}): Readonly<ResolvedHandlerUrls> {
|
|
103
|
+
const hostedUrlNames = new Set(
|
|
104
|
+
Object.entries(options.urls)
|
|
105
|
+
.filter(([, url]) => isHostedHandlerUrlForProject({ url, projectId: options.projectId }))
|
|
106
|
+
.map(([handlerName]) => handlerName),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return new Proxy(options.urls, {
|
|
110
|
+
get(target, property, receiver) {
|
|
111
|
+
if (typeof property === "string" && hostedUrlNames.has(property)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`app.urls.${property} cannot be used when this app is configured to use hosted components. ` +
|
|
114
|
+
"`app.urls` is static and does not include the runtime redirect-back, cross-domain auth, or sign-out state required by hosted components. " +
|
|
115
|
+
getRedirectHelperInstruction(property),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return Reflect.get(target, property, receiver);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
const oauthCallbackResponseQueryParams = ["code", "state", "error", "error_description", "errorCode", "message", "details"] as const;
|
|
92
124
|
|
|
93
125
|
const allClientApps = new Map<string, [checkString: string | undefined, app: StackClientApp<any, any>]>();
|
|
@@ -1695,7 +1727,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1695
1727
|
teamId: crud.id,
|
|
1696
1728
|
email: options.email,
|
|
1697
1729
|
session,
|
|
1698
|
-
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.
|
|
1730
|
+
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app._getUrls().teamInvitation, "callbackUrl"),
|
|
1699
1731
|
});
|
|
1700
1732
|
await app._teamInvitationsCache.refresh([session, crud.id]);
|
|
1701
1733
|
},
|
|
@@ -1759,7 +1791,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1759
1791
|
async sendVerificationEmail(options?: { callbackUrl?: string }) {
|
|
1760
1792
|
await app._interface.sendCurrentUserContactChannelVerificationEmail(
|
|
1761
1793
|
crud.id,
|
|
1762
|
-
options?.callbackUrl || constructRedirectUrl(app.
|
|
1794
|
+
options?.callbackUrl || constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"),
|
|
1763
1795
|
session
|
|
1764
1796
|
);
|
|
1765
1797
|
},
|
|
@@ -2113,7 +2145,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
2113
2145
|
{
|
|
2114
2146
|
provider,
|
|
2115
2147
|
redirectUrl: app._getOAuthCallbackRedirectUri(),
|
|
2116
|
-
errorRedirectUrl: app.
|
|
2148
|
+
errorRedirectUrl: app._getUrls().error,
|
|
2117
2149
|
providerScope: mergeScopeStrings(scopeString, (app._oauthScopesOnSignIn[provider as ProviderType] ?? []).join(" ")),
|
|
2118
2150
|
},
|
|
2119
2151
|
session,
|
|
@@ -2242,7 +2274,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
2242
2274
|
}
|
|
2243
2275
|
return await app._interface.sendVerificationEmail(
|
|
2244
2276
|
crud.primary_email,
|
|
2245
|
-
options?.callbackUrl ?? constructRedirectUrl(app.
|
|
2277
|
+
options?.callbackUrl ?? constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"),
|
|
2246
2278
|
session
|
|
2247
2279
|
);
|
|
2248
2280
|
},
|
|
@@ -2697,6 +2729,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
2697
2729
|
}
|
|
2698
2730
|
|
|
2699
2731
|
get urls(): Readonly<ResolvedHandlerUrls> {
|
|
2732
|
+
return createUrlsForPublicAccess({
|
|
2733
|
+
urls: this._getUrls(),
|
|
2734
|
+
projectId: this.projectId,
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
protected _getUrls(): Readonly<ResolvedHandlerUrls> {
|
|
2700
2739
|
return getUrls(this._urlOptions, { projectId: this.projectId });
|
|
2701
2740
|
}
|
|
2702
2741
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
//===========================================
|
|
5
5
|
// @vitest-environment jsdom
|
|
6
6
|
|
|
7
|
+
import { KnownErrors } from "@hexclave/shared/dist/known-errors";
|
|
7
8
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
8
9
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
10
|
import { EventTracker } from "./event-tracker";
|
|
@@ -390,7 +391,7 @@ describe("EventTracker", () => {
|
|
|
390
391
|
}
|
|
391
392
|
});
|
|
392
393
|
|
|
393
|
-
it("silently disables when
|
|
394
|
+
it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
|
|
394
395
|
vi.useFakeTimers();
|
|
395
396
|
document.body.innerHTML = "<button>Click me</button>";
|
|
396
397
|
|
|
@@ -400,28 +401,19 @@ describe("EventTracker", () => {
|
|
|
400
401
|
projectId: "internal",
|
|
401
402
|
sendBatch: async (body) => {
|
|
402
403
|
sentBodies.push(body);
|
|
403
|
-
return Result.
|
|
404
|
-
JSON.stringify({ code: "ANALYTICS_NOT_ENABLED", error: "Analytics is not enabled for this project." }),
|
|
405
|
-
{
|
|
406
|
-
status: 400,
|
|
407
|
-
headers: { "x-stack-known-error": "ANALYTICS_NOT_ENABLED" },
|
|
408
|
-
},
|
|
409
|
-
));
|
|
404
|
+
return Result.error(new KnownErrors.AnalyticsNotEnabled());
|
|
410
405
|
},
|
|
411
406
|
});
|
|
412
407
|
|
|
413
408
|
try {
|
|
414
409
|
tracker.start();
|
|
415
410
|
|
|
416
|
-
// First flush sends the initial page-view event; server rejects it.
|
|
417
411
|
await advancePastFlush();
|
|
418
412
|
expect(sentBodies).toHaveLength(1);
|
|
419
|
-
|
|
420
|
-
// No console.warn should have been emitted.
|
|
421
413
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
414
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
415
|
+
expect((tracker as any)._flushTimer).toBeNull();
|
|
422
416
|
|
|
423
|
-
// After disabling, new events should not accumulate or trigger further
|
|
424
|
-
// flushes.
|
|
425
417
|
document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
426
418
|
await advancePastFlush();
|
|
427
419
|
expect(sentBodies).toHaveLength(1);
|
|
@@ -8,7 +8,7 @@ import { cssEscapeIdent } from "@hexclave/shared/dist/utils/dom";
|
|
|
8
8
|
import { buildElementsChain, ELEMENTS_CHAIN_MAX_DEPTH } from "@hexclave/shared/dist/utils/elements-chain";
|
|
9
9
|
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
|
10
10
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
11
|
-
import { generateUuid } from "./session-replay";
|
|
11
|
+
import { generateUuid, isAnalyticsNotEnabledError } from "./session-replay";
|
|
12
12
|
|
|
13
13
|
const FLUSH_INTERVAL_MS = 10_000;
|
|
14
14
|
const MAX_EVENTS_PER_BATCH = 50;
|
|
@@ -34,6 +34,10 @@ function hasHistoryMethods(value: unknown): value is { pushState: History["pushS
|
|
|
34
34
|
return typeof value.pushState === "function" && typeof value.replaceState === "function";
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function getTextSnippet(textContent: string | null): string {
|
|
38
|
+
return textContent == null ? "" : textContent.trim().substring(0, 200);
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
// Pixel quantization factor for x/y/viewport in stored click events. Matches the
|
|
38
42
|
// SCALE_FACTOR used by the ClickHouse clickmap_events MV — keep them in sync.
|
|
39
43
|
const CLICKMAP_SCALE_FACTOR = 16;
|
|
@@ -321,7 +325,7 @@ export class EventTracker {
|
|
|
321
325
|
event_at_ms: Date.now(),
|
|
322
326
|
data: {
|
|
323
327
|
tag_name: target.tagName.toLowerCase(),
|
|
324
|
-
text: target.textContent
|
|
328
|
+
text: getTextSnippet(target.textContent),
|
|
325
329
|
href: this._findNearestAnchorHref(target),
|
|
326
330
|
selector: this._buildSelector(target),
|
|
327
331
|
elements_chain: buildElementsChain(target),
|
|
@@ -503,27 +507,28 @@ export class EventTracker {
|
|
|
503
507
|
);
|
|
504
508
|
|
|
505
509
|
if (res.status === "error") {
|
|
510
|
+
if (isAnalyticsNotEnabledError(res.error)) {
|
|
511
|
+
this._disable();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
506
514
|
console.warn("EventTracker flush failed:", res.error);
|
|
507
515
|
return;
|
|
508
516
|
}
|
|
509
517
|
|
|
510
518
|
if (!res.data.ok) {
|
|
511
|
-
// If the server tells us analytics is not enabled for this project,
|
|
512
|
-
// silently disable the tracker — no point retrying or warning the user.
|
|
513
|
-
const knownError = res.data.headers.get("x-hexclave-known-error") ?? res.data.headers.get("x-stack-known-error");
|
|
514
|
-
if (knownError === "ANALYTICS_NOT_ENABLED") {
|
|
515
|
-
this._disabled = true;
|
|
516
|
-
if (this._flushTimer !== null) {
|
|
517
|
-
clearInterval(this._flushTimer);
|
|
518
|
-
this._flushTimer = null;
|
|
519
|
-
}
|
|
520
|
-
this._teardown();
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
519
|
console.warn("EventTracker flush failed:", res.data.status, await res.data.text());
|
|
524
520
|
}
|
|
525
521
|
}
|
|
526
522
|
|
|
523
|
+
private _disable() {
|
|
524
|
+
this._disabled = true;
|
|
525
|
+
if (this._flushTimer !== null) {
|
|
526
|
+
clearInterval(this._flushTimer);
|
|
527
|
+
this._flushTimer = null;
|
|
528
|
+
}
|
|
529
|
+
this._teardown();
|
|
530
|
+
}
|
|
531
|
+
|
|
527
532
|
private _tick() {
|
|
528
533
|
if (this._cancelled) return;
|
|
529
534
|
if (this._events.length > 0) {
|
|
@@ -362,7 +362,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
362
362
|
isPrimary: crud.is_primary,
|
|
363
363
|
usedForAuth: crud.used_for_auth,
|
|
364
364
|
async sendVerificationEmail(options?: { callbackUrl?: string }) {
|
|
365
|
-
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app.
|
|
365
|
+
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"));
|
|
366
366
|
},
|
|
367
367
|
async update(data: ServerContactChannelUpdateOptions) {
|
|
368
368
|
await app._interface.updateServerContactChannel(userId, crud.id, serverContactChannelUpdateOptionsToCrud(data));
|
|
@@ -1042,7 +1042,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1042
1042
|
await app._interface.sendServerTeamInvitation({
|
|
1043
1043
|
teamId: crud.id,
|
|
1044
1044
|
email: options.email,
|
|
1045
|
-
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.
|
|
1045
|
+
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app._getUrls().teamInvitation, "callbackUrl"),
|
|
1046
1046
|
});
|
|
1047
1047
|
await app._serverTeamInvitationsCache.refresh([crud.id]);
|
|
1048
1048
|
},
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
//===========================================
|
|
5
5
|
// @vitest-environment jsdom
|
|
6
6
|
|
|
7
|
+
import { KnownErrors } from "@hexclave/shared/dist/known-errors";
|
|
7
8
|
import { describe, expect, it, vi } from "vitest";
|
|
8
9
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
9
10
|
import { analyticsOptionsFromJson, analyticsOptionsToJson, getSessionReplayOptions, SessionRecorder } from "./session-replay";
|
|
@@ -48,10 +49,9 @@ describe("analytics option JSON conversion", () => {
|
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
describe("SessionRecorder flush", () => {
|
|
51
|
-
it("silently disables when
|
|
52
|
+
it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
|
|
52
53
|
vi.useFakeTimers();
|
|
53
54
|
|
|
54
|
-
// Seed localStorage with a valid session so _flush doesn't fail on getOrRotateSession
|
|
55
55
|
const storageKey = `hexclave:session-replay:v1:test-project`;
|
|
56
56
|
localStorage.setItem(storageKey, JSON.stringify({
|
|
57
57
|
session_id: "test-session",
|
|
@@ -65,13 +65,7 @@ describe("SessionRecorder flush", () => {
|
|
|
65
65
|
projectId: "test-project",
|
|
66
66
|
sendBatch: async (body) => {
|
|
67
67
|
sentBodies.push(body);
|
|
68
|
-
return Result.
|
|
69
|
-
JSON.stringify({ code: "ANALYTICS_NOT_ENABLED", error: "Analytics is not enabled for this project." }),
|
|
70
|
-
{
|
|
71
|
-
status: 400,
|
|
72
|
-
headers: { "x-stack-known-error": "ANALYTICS_NOT_ENABLED" },
|
|
73
|
-
},
|
|
74
|
-
));
|
|
68
|
+
return Result.error(new KnownErrors.AnalyticsNotEnabled());
|
|
75
69
|
},
|
|
76
70
|
},
|
|
77
71
|
{},
|
|
@@ -80,26 +74,16 @@ describe("SessionRecorder flush", () => {
|
|
|
80
74
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
81
75
|
|
|
82
76
|
try {
|
|
83
|
-
// Inject an event directly into the recorder's buffer to test flush behavior
|
|
84
|
-
// without needing rrweb. We access private fields for testing purposes.
|
|
85
77
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
86
78
|
(recorder as any)._events = [{ type: 2, timestamp: Date.now(), data: {} }];
|
|
87
79
|
|
|
88
|
-
// Manually trigger a tick (which calls _flush)
|
|
89
80
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
90
81
|
(recorder as any)._tick();
|
|
91
82
|
await vi.advanceTimersByTimeAsync(0);
|
|
92
83
|
|
|
93
|
-
// One batch should have been sent
|
|
94
84
|
expect(sentBodies).toHaveLength(1);
|
|
85
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
95
86
|
|
|
96
|
-
// No console.warn about "SessionRecorder flush failed" should have been emitted
|
|
97
|
-
const flushWarnings = warnSpy.mock.calls.filter(
|
|
98
|
-
(args) => typeof args[0] === "string" && args[0].includes("SessionRecorder")
|
|
99
|
-
);
|
|
100
|
-
expect(flushWarnings).toHaveLength(0);
|
|
101
|
-
|
|
102
|
-
// After disabling, pushing new events and triggering another tick should not send
|
|
103
87
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
104
88
|
(recorder as any)._events = [{ type: 3, timestamp: Date.now(), data: {} }];
|
|
105
89
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//===========================================
|
|
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
|
+
import { KnownErrors } from "@hexclave/shared/dist/known-errors";
|
|
5
6
|
import { isBrowserLike } from "@hexclave/shared/dist/utils/env";
|
|
6
7
|
import { captureWarning } from "@hexclave/shared/dist/utils/errors";
|
|
7
8
|
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
|
@@ -165,6 +166,10 @@ export type SessionRecorderDeps = {
|
|
|
165
166
|
sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,
|
|
166
167
|
};
|
|
167
168
|
|
|
169
|
+
export function isAnalyticsNotEnabledError(error: unknown): boolean {
|
|
170
|
+
return KnownErrors.AnalyticsNotEnabled.isInstance(error);
|
|
171
|
+
}
|
|
172
|
+
|
|
168
173
|
export class SessionRecorder {
|
|
169
174
|
private _started = false;
|
|
170
175
|
private _cancelled = false;
|
|
@@ -269,23 +274,15 @@ export class SessionRecorder {
|
|
|
269
274
|
);
|
|
270
275
|
|
|
271
276
|
if (res.status === "error") {
|
|
277
|
+
if (isAnalyticsNotEnabledError(res.error)) {
|
|
278
|
+
this._disable();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
272
281
|
captureWarning("SessionRecorder.flush", res.error);
|
|
273
282
|
return;
|
|
274
283
|
}
|
|
275
284
|
|
|
276
285
|
if (!res.data.ok) {
|
|
277
|
-
// If the server tells us analytics is not enabled for this project,
|
|
278
|
-
// silently disable the recorder — no point retrying or warning the user.
|
|
279
|
-
const knownError = res.data.headers.get("x-hexclave-known-error") ?? res.data.headers.get("x-stack-known-error");
|
|
280
|
-
if (knownError === "ANALYTICS_NOT_ENABLED") {
|
|
281
|
-
this._disabled = true;
|
|
282
|
-
if (this._flushTimer !== null) {
|
|
283
|
-
clearInterval(this._flushTimer);
|
|
284
|
-
this._flushTimer = null;
|
|
285
|
-
}
|
|
286
|
-
this._stopCurrentRecording();
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
286
|
captureWarning("SessionRecorder.flush", new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));
|
|
290
287
|
}
|
|
291
288
|
} finally {
|
|
@@ -293,6 +290,16 @@ export class SessionRecorder {
|
|
|
293
290
|
}
|
|
294
291
|
}
|
|
295
292
|
|
|
293
|
+
private _disable() {
|
|
294
|
+
this._disabled = true;
|
|
295
|
+
this.clearBuffer();
|
|
296
|
+
if (this._flushTimer !== null) {
|
|
297
|
+
clearInterval(this._flushTimer);
|
|
298
|
+
this._flushTimer = null;
|
|
299
|
+
}
|
|
300
|
+
this._stopCurrentRecording();
|
|
301
|
+
}
|
|
302
|
+
|
|
296
303
|
private async _startRecording() {
|
|
297
304
|
if (this._recording || this._cancelled) return;
|
|
298
305
|
|
|
@@ -68,8 +68,9 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
|
|
68
68
|
readonly version: string,
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* @deprecated `app.urls` is static and does not include runtime redirect-back
|
|
72
|
-
*
|
|
71
|
+
* @deprecated Do not use `app.urls` for navigation. It is static and does not include runtime redirect-back,
|
|
72
|
+
* cross-domain auth, or sign-out state. Use the matching `redirectToXyz()` method instead, for example
|
|
73
|
+
* `redirectToSignIn()`, `redirectToSignUp()`, `redirectToSignOut()`, or `redirectToAccountSettings()`.
|
|
73
74
|
*/
|
|
74
75
|
readonly urls: Readonly<ResolvedHandlerUrls>,
|
|
75
76
|
|
|
@@ -76,6 +76,14 @@ export type AdminOAuthProviderConfig = {
|
|
|
76
76
|
microsoftTenantId?: string,
|
|
77
77
|
appleBundleIds?: string[],
|
|
78
78
|
}
|
|
79
|
+
| {
|
|
80
|
+
type: 'custom_oidc',
|
|
81
|
+
clientId: string,
|
|
82
|
+
clientSecret: string,
|
|
83
|
+
issuerUrl: string,
|
|
84
|
+
scope?: string,
|
|
85
|
+
displayName?: string,
|
|
86
|
+
}
|
|
79
87
|
) & OAuthProviderConfig;
|
|
80
88
|
|
|
81
89
|
export type AdminProjectConfigUpdateOptions = {
|
|
@@ -178,17 +178,19 @@ export function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptio
|
|
|
178
178
|
domain: d.domain,
|
|
179
179
|
handler_path: d.handlerPath
|
|
180
180
|
})),
|
|
181
|
-
oauth_providers: options.config?.oauthProviders
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
181
|
+
oauth_providers: options.config?.oauthProviders
|
|
182
|
+
?.filter((p): p is Exclude<typeof p, { type: 'custom_oidc' }> => p.type !== 'custom_oidc')
|
|
183
|
+
.map((p) => ({
|
|
184
|
+
id: p.id as any,
|
|
185
|
+
type: p.type,
|
|
186
|
+
...(p.type === 'standard' && {
|
|
187
|
+
client_id: p.clientId,
|
|
188
|
+
client_secret: p.clientSecret,
|
|
189
|
+
facebook_config_id: p.facebookConfigId,
|
|
190
|
+
microsoft_tenant_id: p.microsoftTenantId,
|
|
191
|
+
apple_bundle_ids: p.appleBundleIds,
|
|
192
|
+
}),
|
|
193
|
+
})),
|
|
192
194
|
email_config: options.config?.emailConfig && (
|
|
193
195
|
options.config.emailConfig.type === 'shared' ? {
|
|
194
196
|
type: 'shared',
|