@hexclave/next 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.
Files changed (105) hide show
  1. package/dist/components/credential-sign-in.js +5 -1
  2. package/dist/components/credential-sign-in.js.map +1 -1
  3. package/dist/components/team-switcher.js +3 -5
  4. package/dist/components/team-switcher.js.map +1 -1
  5. package/dist/components-page/auth-page.js +2 -2
  6. package/dist/components-page/auth-page.js.map +1 -1
  7. package/dist/components-page/forgot-password.js +5 -1
  8. package/dist/components-page/forgot-password.js.map +1 -1
  9. package/dist/components-page/oauth-callback.js +6 -1
  10. package/dist/components-page/oauth-callback.js.map +1 -1
  11. package/dist/components-page/team-creation.js +2 -3
  12. package/dist/components-page/team-creation.js.map +1 -1
  13. package/dist/esm/components/credential-sign-in.js +5 -1
  14. package/dist/esm/components/credential-sign-in.js.map +1 -1
  15. package/dist/esm/components/team-switcher.js +3 -5
  16. package/dist/esm/components/team-switcher.js.map +1 -1
  17. package/dist/esm/components-page/auth-page.js +2 -2
  18. package/dist/esm/components-page/auth-page.js.map +1 -1
  19. package/dist/esm/components-page/forgot-password.js +5 -1
  20. package/dist/esm/components-page/forgot-password.js.map +1 -1
  21. package/dist/esm/components-page/oauth-callback.js +6 -1
  22. package/dist/esm/components-page/oauth-callback.js.map +1 -1
  23. package/dist/esm/components-page/team-creation.js +2 -3
  24. package/dist/esm/components-page/team-creation.js.map +1 -1
  25. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  26. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  27. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +34 -5
  28. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  29. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
  30. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  31. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +24 -4
  32. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  33. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
  34. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -0
  35. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  36. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +17 -13
  37. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  38. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
  39. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  40. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  41. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +2 -2
  42. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
  43. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
  44. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  45. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +19 -13
  46. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  47. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
  48. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  49. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +3 -2
  50. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  51. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  52. package/dist/esm/lib/hexclave-app/project-configs/index.d.ts +7 -0
  53. package/dist/esm/lib/hexclave-app/project-configs/index.d.ts.map +1 -1
  54. package/dist/esm/lib/hexclave-app/projects/index.d.ts.map +1 -1
  55. package/dist/esm/lib/hexclave-app/projects/index.js +1 -1
  56. package/dist/esm/lib/hexclave-app/projects/index.js.map +1 -1
  57. package/dist/generated/quetzal-translations.d.ts +2 -2
  58. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  59. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +34 -5
  60. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  61. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
  62. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  63. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +24 -4
  64. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  65. package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
  66. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -0
  67. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  68. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +16 -12
  69. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  70. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
  71. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  72. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  73. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +2 -2
  74. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
  75. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
  76. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  77. package/dist/lib/hexclave-app/apps/implementations/session-replay.js +19 -12
  78. package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  79. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
  80. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  81. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +3 -2
  82. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  83. package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  84. package/dist/lib/hexclave-app/project-configs/index.d.ts +7 -0
  85. package/dist/lib/hexclave-app/project-configs/index.d.ts.map +1 -1
  86. package/dist/lib/hexclave-app/projects/index.d.ts.map +1 -1
  87. package/dist/lib/hexclave-app/projects/index.js +1 -1
  88. package/dist/lib/hexclave-app/projects/index.js.map +1 -1
  89. package/package.json +4 -4
  90. package/src/components/credential-sign-in.tsx +8 -1
  91. package/src/components/team-switcher.tsx +3 -5
  92. package/src/components-page/auth-page.tsx +2 -2
  93. package/src/components-page/forgot-password.tsx +7 -1
  94. package/src/components-page/oauth-callback.tsx +9 -1
  95. package/src/components-page/team-creation.tsx +2 -3
  96. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +36 -0
  97. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +43 -4
  98. package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +5 -13
  99. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +19 -14
  100. package/src/lib/hexclave-app/apps/implementations/server-app-impl.ts +2 -2
  101. package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +4 -20
  102. package/src/lib/hexclave-app/apps/implementations/session-replay.ts +19 -12
  103. package/src/lib/hexclave-app/apps/interfaces/client-app.ts +3 -2
  104. package/src/lib/hexclave-app/project-configs/index.ts +8 -0
  105. package/src/lib/hexclave-app/projects/index.ts +13 -11
@@ -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",
@@ -93,6 +93,38 @@ const nestedCrossDomainAuthQueryParams = {
93
93
  afterCallbackRedirectUrl: "after_callback_redirect_url",
94
94
  } as const;
95
95
 
96
+ function getRedirectHelperInstruction(handlerName: string): string {
97
+ if (handlerName === "handler") {
98
+ return "Use a page-specific redirect helper such as app.redirectToSignIn() instead.";
99
+ }
100
+ const redirectMethodName = `redirectTo${handlerName.slice(0, 1).toUpperCase()}${handlerName.slice(1)}`;
101
+ return `Use app.${redirectMethodName}() instead.`;
102
+ }
103
+
104
+ function createUrlsForPublicAccess(options: {
105
+ urls: ResolvedHandlerUrls,
106
+ projectId: string,
107
+ }): Readonly<ResolvedHandlerUrls> {
108
+ const hostedUrlNames = new Set(
109
+ Object.entries(options.urls)
110
+ .filter(([, url]) => isHostedHandlerUrlForProject({ url, projectId: options.projectId }))
111
+ .map(([handlerName]) => handlerName),
112
+ );
113
+
114
+ return new Proxy(options.urls, {
115
+ get(target, property, receiver) {
116
+ if (typeof property === "string" && hostedUrlNames.has(property)) {
117
+ throw new Error(
118
+ `app.urls.${property} cannot be used when this app is configured to use hosted components. ` +
119
+ "`app.urls` is static and does not include the runtime redirect-back, cross-domain auth, or sign-out state required by hosted components. " +
120
+ getRedirectHelperInstruction(property),
121
+ );
122
+ }
123
+ return Reflect.get(target, property, receiver);
124
+ },
125
+ });
126
+ }
127
+
96
128
  const oauthCallbackResponseQueryParams = ["code", "state", "error", "error_description", "errorCode", "message", "details"] as const;
97
129
 
98
130
  const allClientApps = new Map<string, [checkString: string | undefined, app: StackClientApp<any, any>]>();
@@ -1688,7 +1720,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
1688
1720
  teamId: crud.id,
1689
1721
  email: options.email,
1690
1722
  session,
1691
- callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
1723
+ callbackUrl: options.callbackUrl ?? constructRedirectUrl(app._getUrls().teamInvitation, "callbackUrl"),
1692
1724
  });
1693
1725
  await app._teamInvitationsCache.refresh([session, crud.id]);
1694
1726
  },
@@ -1752,7 +1784,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
1752
1784
  async sendVerificationEmail(options?: { callbackUrl?: string }) {
1753
1785
  await app._interface.sendCurrentUserContactChannelVerificationEmail(
1754
1786
  crud.id,
1755
- options?.callbackUrl || constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
1787
+ options?.callbackUrl || constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"),
1756
1788
  session
1757
1789
  );
1758
1790
  },
@@ -2106,7 +2138,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
2106
2138
  {
2107
2139
  provider,
2108
2140
  redirectUrl: app._getOAuthCallbackRedirectUri(),
2109
- errorRedirectUrl: app.urls.error,
2141
+ errorRedirectUrl: app._getUrls().error,
2110
2142
  providerScope: mergeScopeStrings(scopeString, (app._oauthScopesOnSignIn[provider as ProviderType] ?? []).join(" ")),
2111
2143
  },
2112
2144
  session,
@@ -2235,7 +2267,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
2235
2267
  }
2236
2268
  return await app._interface.sendVerificationEmail(
2237
2269
  crud.primary_email,
2238
- options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
2270
+ options?.callbackUrl ?? constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"),
2239
2271
  session
2240
2272
  );
2241
2273
  },
@@ -2690,6 +2722,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
2690
2722
  }
2691
2723
 
2692
2724
  get urls(): Readonly<ResolvedHandlerUrls> {
2725
+ return createUrlsForPublicAccess({
2726
+ urls: this._getUrls(),
2727
+ projectId: this.projectId,
2728
+ });
2729
+ }
2730
+
2731
+ protected _getUrls(): Readonly<ResolvedHandlerUrls> {
2693
2732
  return getUrls(this._urlOptions, { projectId: this.projectId });
2694
2733
  }
2695
2734
 
@@ -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 server responds with ANALYTICS_NOT_ENABLED", async () => {
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.ok(new Response(
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.trim().substring(0, 200),
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.urls.emailVerification, "callbackUrl"));
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.urls.teamInvitation, "callbackUrl"),
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 server responds with ANALYTICS_NOT_ENABLED", async () => {
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.ok(new Response(
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 parameters.
72
- * For navigation, prefer `redirectToXyz()` methods (for example `redirectToSignIn()`).
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?.map((p) => ({
182
- id: p.id as any,
183
- type: p.type,
184
- ...(p.type === 'standard' && {
185
- client_id: p.clientId,
186
- client_secret: p.clientSecret,
187
- facebook_config_id: p.facebookConfigId,
188
- microsoft_tenant_id: p.microsoftTenantId,
189
- apple_bundle_ids: p.appleBundleIds,
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',