@hexclave/tanstack-start 1.0.24 → 1.0.26

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 (102) hide show
  1. package/dist/components/elements/sidebar-layout.js +1 -1
  2. package/dist/components-page/hexclave-handler-client.js +6 -5
  3. package/dist/components-page/hexclave-handler-client.js.map +1 -1
  4. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  5. package/dist/dev-tool/dev-tool-core.js +4 -68
  6. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  7. package/dist/esm/components/elements/sidebar-layout.js +1 -1
  8. package/dist/esm/components-page/hexclave-handler-client.js +6 -5
  9. package/dist/esm/components-page/hexclave-handler-client.js.map +1 -1
  10. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  11. package/dist/esm/dev-tool/dev-tool-core.js +4 -68
  12. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  13. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +7 -0
  14. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  15. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +35 -1
  16. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  17. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
  18. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  19. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +18 -11
  20. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  21. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
  22. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  23. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +2 -1
  24. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  25. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +26 -0
  26. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  27. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +7 -1
  28. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  29. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +11 -1
  30. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  31. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +43 -0
  32. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  33. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +2 -1
  34. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  35. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  36. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
  37. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  38. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  39. package/dist/esm/lib/hexclave-app/index.d.ts +2 -1
  40. package/dist/esm/lib/hexclave-app/plan-usage/index.d.ts +27 -0
  41. package/dist/esm/lib/hexclave-app/plan-usage/index.d.ts.map +1 -0
  42. package/dist/esm/lib/hexclave-app/plan-usage/index.js +1 -0
  43. package/dist/esm/lib/hexclave-app/projects/index.d.ts +7 -0
  44. package/dist/esm/lib/hexclave-app/projects/index.d.ts.map +1 -1
  45. package/dist/esm/lib/hexclave-app/projects/index.js.map +1 -1
  46. package/dist/esm/pushed-config-error-overlay/index.d.ts +7 -0
  47. package/dist/esm/pushed-config-error-overlay/index.d.ts.map +1 -0
  48. package/dist/esm/pushed-config-error-overlay/index.js +464 -0
  49. package/dist/esm/pushed-config-error-overlay/index.js.map +1 -0
  50. package/dist/index.d.ts +2 -1
  51. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +7 -0
  52. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  53. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +35 -1
  54. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  55. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
  56. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  57. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +18 -11
  58. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  59. package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
  60. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  61. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +1 -0
  62. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  63. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +26 -0
  64. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  65. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +7 -1
  66. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  67. package/dist/lib/hexclave-app/apps/implementations/session-replay.js +11 -0
  68. package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  69. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +43 -0
  70. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  71. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +2 -1
  72. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  73. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  74. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
  75. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  76. package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  77. package/dist/lib/hexclave-app/index.d.ts +2 -1
  78. package/dist/lib/hexclave-app/plan-usage/index.d.ts +27 -0
  79. package/dist/lib/hexclave-app/plan-usage/index.d.ts.map +1 -0
  80. package/dist/lib/hexclave-app/plan-usage/index.js +0 -0
  81. package/dist/lib/hexclave-app/projects/index.d.ts +7 -0
  82. package/dist/lib/hexclave-app/projects/index.d.ts.map +1 -1
  83. package/dist/lib/hexclave-app/projects/index.js.map +1 -1
  84. package/dist/pushed-config-error-overlay/index.d.ts +7 -0
  85. package/dist/pushed-config-error-overlay/index.d.ts.map +1 -0
  86. package/dist/pushed-config-error-overlay/index.js +466 -0
  87. package/dist/pushed-config-error-overlay/index.js.map +1 -0
  88. package/package.json +3 -3
  89. package/src/components-page/hexclave-handler-client.tsx +6 -5
  90. package/src/dev-tool/dev-tool-core.ts +5 -59
  91. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +44 -1
  92. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +20 -11
  93. package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +33 -0
  94. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +6 -1
  95. package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +52 -0
  96. package/src/lib/hexclave-app/apps/implementations/session-replay.ts +20 -0
  97. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +2 -0
  98. package/src/lib/hexclave-app/apps/interfaces/client-app.ts +1 -0
  99. package/src/lib/hexclave-app/index.ts +8 -0
  100. package/src/lib/hexclave-app/plan-usage/index.ts +29 -0
  101. package/src/lib/hexclave-app/projects/index.ts +3 -0
  102. package/src/pushed-config-error-overlay/index.ts +548 -0
@@ -638,11 +638,6 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
638
638
 
639
639
  const actions = h('div', { className: 'sdt-ov-actions' });
640
640
  const toast = h('div', { className: 'sdt-ov-toast', style: { display: 'none' } });
641
- const emailRow = h('div', { className: 'sdt-ov-email-input' });
642
- const emailInput = h('input', { type: 'email', placeholder: 'Sign in as email\u2026' }) as HTMLInputElement;
643
- const emailBtn = h('button', null);
644
- setHtml(emailBtn, '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>');
645
- emailRow.append(emailInput, emailBtn);
646
641
 
647
642
  function isBestEffortOverviewError(error: unknown) {
648
643
  if (error instanceof DOMException && error.name === 'AbortError') {
@@ -704,15 +699,14 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
704
699
  });
705
700
  actions.append(signOutBtn, randomBtn);
706
701
  } else {
707
- const quickBtn = h('button', { className: 'sdt-ov-btn sdt-ov-btn-primary sdt-ov-btn-wide' }, loading ? 'Working\u2026' : 'Quick Sign In');
702
+ const quickBtn = h('button', { className: 'sdt-ov-btn sdt-ov-btn-primary sdt-ov-btn-wide' }, loading ? 'Working\u2026' : 'Quick Sign Up');
708
703
  quickBtn.disabled = loading;
709
704
  quickBtn.addEventListener('click', () => {
710
705
  runAsynchronously(doQuickSignIn());
711
706
  });
712
707
  actions.appendChild(quickBtn);
713
708
  }
714
- emailInput.placeholder = currentUser ? 'Switch to email\u2026' : 'Sign in as email\u2026';
715
- actions.appendChild(emailRow);
709
+
716
710
  }
717
711
 
718
712
  async function doQuickSignIn() {
@@ -744,54 +738,6 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
744
738
  await refreshUser();
745
739
  }
746
740
 
747
- async function doSignInAs(targetEmail: string) {
748
- if (!targetEmail.trim()) return;
749
- if (!isLocalhost(window.location.href)) {
750
- showToast('Quick sign-in is only available on localhost', 'error');
751
- return;
752
- }
753
- loading = true;
754
- rebuildActions();
755
- const trimmed = targetEmail.trim();
756
- try {
757
- const signInResult = await app.signInWithCredential({ email: trimmed, password: trimmed, noRedirect: true });
758
- if (signInResult.status === 'ok') {
759
- showToast(`Signed in as ${trimmed}`, 'success');
760
- emailInput.value = '';
761
- loading = false;
762
- await refreshUser();
763
- return;
764
- }
765
- const signUpResult = await app.signUpWithCredential({ email: trimmed, password: trimmed, noRedirect: true } as any);
766
- if (signUpResult.status === 'error') {
767
- showToast(`Failed: ${signUpResult.error.message}`, 'error');
768
- loading = false;
769
- rebuildActions();
770
- return;
771
- }
772
- const retryResult = await app.signInWithCredential({ email: trimmed, password: trimmed, noRedirect: true });
773
- if (retryResult.status === 'error') {
774
- showToast(`Sign in failed: ${retryResult.error.message}`, 'error');
775
- } else {
776
- showToast(`Signed in as ${trimmed}`, 'success');
777
- emailInput.value = '';
778
- }
779
- } catch (e: any) {
780
- showToast(e.message || 'Unknown error', 'error');
781
- }
782
- loading = false;
783
- await refreshUser();
784
- }
785
-
786
- emailBtn.addEventListener('click', () => {
787
- runAsynchronously(doSignInAs(emailInput.value));
788
- });
789
- emailInput.addEventListener('keydown', (e) => {
790
- if (e.key === 'Enter') {
791
- runAsynchronously(doSignInAs(emailInput.value));
792
- }
793
- });
794
-
795
741
  heroCard.append(actions, toast);
796
742
 
797
743
  // ── Auth methods card ──────────────────────────────────────────────────────
@@ -855,7 +801,7 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
855
801
  function buildChecklist() {
856
802
  checksCard.innerHTML = '';
857
803
  const currentUserCheck = hasPersistentTokenStore
858
- ? { ok: !!currentUser, label: 'Sign in a test user', hint: 'Use \u201cQuick Sign In\u201d above \u2192' }
804
+ ? { ok: !!currentUser, label: 'Sign in a test user', hint: 'Use \u201cQuick Sign Up\u201d above \u2192' }
859
805
  : { ok: true, label: 'Current-user tools unavailable', hint: null };
860
806
  const checks = [
861
807
  { ok: !!projectId && projectId !== 'default', label: 'Project configured', hint: null },
@@ -925,7 +871,7 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
925
871
  } else {
926
872
  avatar.textContent = initials;
927
873
  }
928
- userName.textContent = currentUser.displayName || 'Anonymous';
874
+ userName.textContent = currentUser.displayName || '(No display name)';
929
875
  userEmail.textContent = currentUser.primaryEmail || 'No email';
930
876
  authIndicator.style.display = '';
931
877
  } else {
@@ -1905,7 +1851,7 @@ function createSupportTab(app: StackClientApp<true>): HTMLElement {
1905
1851
  function createComponentsTab(app: StackClientApp<true>): HTMLElement {
1906
1852
  const container = h('div', { className: 'sdt-pg-layout' });
1907
1853
  const apiBaseUrl = resolveApiBaseUrl(app);
1908
- const urls = app.urls;
1854
+ const urls = app[hexclaveAppInternalsSymbol].getUrls();
1909
1855
  const urlOptions: HandlerUrlOptions = app[hexclaveAppInternalsSymbol].getConstructorOptions().urls ?? {};
1910
1856
 
1911
1857
  const PAGE_ENTRIES: { key: keyof HandlerUrls; label: string }[] = [
@@ -24,6 +24,7 @@ import { EmailConfig, hexclaveAppInternalsSymbol } from "../../common";
24
24
  import { AdminEmailTemplate } from "../../email-templates";
25
25
  import { InternalApiKey, InternalApiKeyBase, InternalApiKeyBaseCrudRead, InternalApiKeyCreateOptions, InternalApiKeyFirstView, internalApiKeyCreateOptionsToCrud } from "../../internal-api-keys";
26
26
  import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, adminProjectPermissionDefinitionCreateOptionsToCrud, adminProjectPermissionDefinitionUpdateOptionsToCrud, adminTeamPermissionDefinitionCreateOptionsToCrud, adminTeamPermissionDefinitionUpdateOptionsToCrud } from "../../permissions";
27
+ import type { PlanUsage } from "../../plan-usage";
27
28
  import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, PushConfigOptions, adminProjectUpdateOptionsToCrud } from "../../projects";
28
29
  import type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays";
29
30
  import { ManagedEmailProviderListItem, ManagedEmailProviderSetupResult, ManagedEmailProviderStatus, EmailOutboxUpdateOptions, StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app";
@@ -37,6 +38,7 @@ import { PushedConfigSource } from "../../projects";
37
38
  import { useAsyncCache } from "./common"; // THIS_LINE_PLATFORM react-like
38
39
 
39
40
  type BranchConfigSourceApi = yup.InferType<typeof branchConfigSourceSchema>;
41
+ type PlanUsageResponse = Awaited<ReturnType<HexclaveAdminInterface["getPlanUsage"]>>;
40
42
  /**
41
43
  * Converts a PushedConfigSource (SDK camelCase) to BranchConfigSourceApi (API snake_case).
42
44
  */
@@ -79,6 +81,9 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
79
81
  private readonly _adminProjectCache = createCache(async () => {
80
82
  return await this._interface.getProject();
81
83
  });
84
+ private readonly _planUsageCache = createCache(async () => {
85
+ return await this._interface.getPlanUsage();
86
+ });
82
87
  private readonly _internalApiKeysCache = createCache(async () => {
83
88
  const res = await this._interface.listInternalApiKeys();
84
89
  return res;
@@ -196,10 +201,17 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
196
201
  isDevelopmentEnvironment: data.is_development_environment,
197
202
  ownerTeamId: data.owner_team_id,
198
203
  onboardingStatus: data.onboarding_status,
204
+ onboardingState: data.onboarding_state ?? null,
199
205
  logoUrl: data.logo_url,
200
206
  logoFullUrl: data.logo_full_url,
201
207
  logoDarkModeUrl: data.logo_dark_mode_url,
202
208
  logoFullDarkModeUrl: data.logo_full_dark_mode_url,
209
+ pushedConfigError: data.pushed_config_error == null ? null : {
210
+ message: data.pushed_config_error.message,
211
+ },
212
+ configWarnings: data.config_warnings.map((warning) => ({
213
+ message: warning.message,
214
+ })),
203
215
  config: {
204
216
  signUpEnabled: data.config.sign_up_enabled,
205
217
  credentialEnabled: data.config.credential_enabled,
@@ -331,6 +343,28 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
331
343
  };
332
344
  }
333
345
 
346
+ _planUsageFromCrud(data: PlanUsageResponse): PlanUsage {
347
+ return {
348
+ ownerTeamId: data.owner_team_id,
349
+ ownerTeamDisplayName: data.owner_team_display_name,
350
+ planId: data.plan_id,
351
+ planDisplayName: data.plan_display_name,
352
+ periodStart: new Date(data.period_start_millis),
353
+ periodEnd: new Date(data.period_end_millis),
354
+ nextPlanId: data.next_plan_id,
355
+ rows: data.rows.map((row) => ({
356
+ itemId: row.item_id,
357
+ displayName: row.display_name,
358
+ kind: row.kind,
359
+ used: row.used,
360
+ limit: row.limit,
361
+ remaining: row.remaining,
362
+ overage: row.overage,
363
+ isUnlimited: row.is_unlimited,
364
+ })),
365
+ };
366
+ }
367
+
334
368
  override async getProject(): Promise<AdminProject> {
335
369
  return this._adminProjectFromCrud(
336
370
  Result.orThrow(await this._adminProjectCache.getOrWait([], "write-only")),
@@ -346,6 +380,15 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
346
380
  ), [crud]);
347
381
  }
348
382
 
383
+ async getPlanUsage(): Promise<PlanUsage> {
384
+ return this._planUsageFromCrud(Result.orThrow(await this._planUsageCache.getOrWait([], "write-only")));
385
+ }
386
+
387
+ usePlanUsage(): PlanUsage {
388
+ const crud = useAsyncCache(this._planUsageCache, [], "adminApp.usePlanUsage()");
389
+ return useMemo(() => this._planUsageFromCrud(crud), [crud]);
390
+ }
391
+
349
392
  protected _createInternalApiKeyBaseFromCrud(data: InternalApiKeyBaseCrudRead): InternalApiKeyBase {
350
393
  const app = this;
351
394
  return {
@@ -1140,7 +1183,7 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
1140
1183
  }
1141
1184
 
1142
1185
  async getStripeAccountInfo(): Promise<null | { account_id: string, charges_enabled: boolean, details_submitted: boolean, payouts_enabled: boolean }> {
1143
- return await this._interface.getStripeAccountInfo();
1186
+ return Result.orThrow(await this._stripeAccountInfoCache.getOrWait([], "write-only"));
1144
1187
  }
1145
1188
 
1146
1189
  useStripeAccountInfo(): { account_id: string, charges_enabled: boolean, details_submitted: boolean, payouts_enabled: boolean } | null {
@@ -2,8 +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 { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
6
- import { KnownError, KnownErrors, HexclaveClientInterface } from "@hexclave/shared";
5
+ import { HexclaveClientInterface, KnownError, KnownErrors } from "@hexclave/shared";
7
6
  import type { RequestListener } from "@hexclave/shared/dist/interface/client-interface";
8
7
  import { ContactChannelsCrud } from "@hexclave/shared/dist/interface/crud/contact-channels";
9
8
  import { CurrentUserCrud } from "@hexclave/shared/dist/interface/crud/current-user";
@@ -42,14 +41,15 @@ import { BotChallengeExecutionFailedError, BotChallengeUserCancelledError, withB
42
41
  import { createUrlIfValid, getRelativePart, isRelative } from "@hexclave/shared/dist/utils/urls";
43
42
  import { generateUuid } from "@hexclave/shared/dist/utils/uuids";
44
43
  import * as tanstackStartServerContext from "@hexclave/tanstack-start/tanstack-start-server-context"; // THIS_LINE_PLATFORM tanstack-start
44
+ import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
45
45
  import * as TanStackRouter from "@tanstack/react-router"; // THIS_LINE_PLATFORM tanstack-start
46
46
  import * as cookie from "cookie";
47
47
  import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
48
48
  import type * as yup from "yup";
49
+ import { envVars } from "../../../../generated/env";
49
50
  import { constructRedirectUrl } from "../../../../utils/url";
50
51
  import { callOAuthCallback, getNewOAuthProviderOrScopeUrl } from "../../../auth";
51
52
  import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, getCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
52
- import { envVars } from "../../../../generated/env";
53
53
  import { ApiKey, ApiKeyCreationOptions, ApiKeyUpdateOptions, apiKeyCreationOptionsToCrud } from "../../api-keys";
54
54
  import { ConvexCtx, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, RequestLike, ResolvedHandlerUrls, TokenStoreInit, hexclaveAppInternalsSymbol } from "../../common";
55
55
  import { DeprecatedOAuthConnection, OAuthConnection } from "../../connected-accounts";
@@ -73,6 +73,7 @@ import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsO
73
73
  import { useAsyncCache } from "./common";
74
74
  import { mountClickmapOverlay } from "../../../../clickmap";
75
75
  import { mountDevTool } from "../../../../dev-tool";
76
+ import { mountPushedConfigErrorOverlay } from "../../../../pushed-config-error-overlay";
76
77
 
77
78
  let isReactServer = false;
78
79
 
@@ -356,7 +357,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
356
357
  {
357
358
  provider,
358
359
  redirectUrl: this._getOAuthCallbackRedirectUri(),
359
- errorRedirectUrl: this.urls.error,
360
+ errorRedirectUrl: this._getUrls().error,
360
361
  providerScope: mergeScopeStrings(scopeString, (this._oauthScopesOnSignIn[provider as ProviderType] ?? []).join(" ")),
361
362
  },
362
363
  session,
@@ -559,7 +560,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
559
560
  {
560
561
  provider: options.providerId,
561
562
  redirectUrl: this._getOAuthCallbackRedirectUri(),
562
- errorRedirectUrl: this.urls.error,
563
+ errorRedirectUrl: this._getUrls().error,
563
564
  providerScope: mergeScopeStrings(options.scope || "", (this._oauthScopesOnSignIn[options.providerId] ?? []).join(" ")),
564
565
  },
565
566
  options.session,
@@ -742,6 +743,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
742
743
  // when a dashboard-minted token is handed over, so the listener is
743
744
  // mounted unconditionally (the heavy UI is lazy-loaded on demand).
744
745
  mountClickmapOverlay(this as any);
746
+ mountPushedConfigErrorOverlay(this as any);
745
747
  }
746
748
  }
747
749
 
@@ -863,7 +865,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
863
865
 
864
866
  protected _getOAuthCallbackRedirectUri(): string {
865
867
  if (!this._isOAuthCallbackUrlHosted()) {
866
- return this.urls.oauthCallback;
868
+ return this._getUrls().oauthCallback;
867
869
  }
868
870
  if (typeof window === "undefined") {
869
871
  throw new HexclaveAssertionError("Hosted OAuth callback URLs require a browser environment to use the current URL as the redirect URI");
@@ -1636,6 +1638,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
1636
1638
  return {
1637
1639
  id: crud.id,
1638
1640
  displayName: crud.display_name,
1641
+ pushedConfigError: crud.pushed_config_error == null ? null : {
1642
+ message: crud.pushed_config_error.message,
1643
+ },
1644
+ configWarnings: crud.config_warnings.map((warning) => ({
1645
+ message: warning.message,
1646
+ })),
1639
1647
  config: {
1640
1648
  signUpEnabled: crud.config.sign_up_enabled,
1641
1649
  credentialEnabled: crud.config.credential_enabled,
@@ -3030,13 +3038,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3030
3038
  async redirectToMfa(options?: RedirectToOptions) { return await this._redirectToHandler("mfa", options); }
3031
3039
 
3032
3040
  async sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<undefined, KnownErrors["UserNotFound"]>> {
3033
- return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset, "callbackUrl"));
3041
+ return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this._getUrls().passwordReset, "callbackUrl"));
3034
3042
  }
3035
3043
 
3036
3044
  async sendMagicLinkEmail(email: string, options?: {
3037
3045
  callbackUrl?: string,
3038
3046
  }): Promise<Result<{ nonce: string }, KnownErrors["RedirectUrlNotWhitelisted"] | KnownErrors["BotChallengeFailed"]>> {
3039
- const callbackUrl = options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback, "callbackUrl");
3047
+ const callbackUrl = options?.callbackUrl ?? constructRedirectUrl(this._getUrls().magicLinkCallback, "callbackUrl");
3040
3048
  return await this._executeResultWithBotChallengeFlow({
3041
3049
  action: "send_magic_link_email",
3042
3050
  execute: async (challenge) => {
@@ -3325,7 +3333,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3325
3333
  return await this._interface.authorizeOAuth({
3326
3334
  provider,
3327
3335
  redirectUrl: constructRedirectUrl(this._getOAuthCallbackRedirectUri(), "redirectUrl"),
3328
- errorRedirectUrl: constructRedirectUrl(this.urls.error, "errorRedirectUrl"),
3336
+ errorRedirectUrl: constructRedirectUrl(this._getUrls().error, "errorRedirectUrl"),
3329
3337
  afterCallbackRedirectUrl,
3330
3338
  type: "authenticate",
3331
3339
  providerScope: this._oauthScopesOnSignIn[provider]?.join(" "),
@@ -3445,7 +3453,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3445
3453
  }
3446
3454
  this._ensurePersistentTokenStore();
3447
3455
  const session = await this._getSession();
3448
- const emailVerificationRedirectUrl = options.noVerificationCallback ? undefined : options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification, "verificationCallbackUrl");
3456
+ const emailVerificationRedirectUrl = options.noVerificationCallback ? undefined : options.verificationCallbackUrl ?? constructRedirectUrl(this._getUrls().emailVerification, "verificationCallbackUrl");
3449
3457
 
3450
3458
  const executeSignUp = async (challenge: { token?: string, phase?: "invisible" | "visible", unavailable?: true }) => {
3451
3459
  let result = await this._interface.signUpWithCredential(
@@ -3608,7 +3616,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3608
3616
 
3609
3617
  // Step 2: Open the browser for the user to authenticate and display the verification code
3610
3618
  const url = buildCliAuthConfirmUrl({
3611
- cliAuthConfirmUrl: this.urls.cliAuthConfirm,
3619
+ cliAuthConfirmUrl: this._getUrls().cliAuthConfirm,
3612
3620
  appUrl: options.appUrl,
3613
3621
  loginCode,
3614
3622
  });
@@ -4046,6 +4054,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
4046
4054
  ) => {
4047
4055
  return await this._interface.sendClientRequest(path, requestOptions, await this._getSession(), requestType);
4048
4056
  },
4057
+ getUrls: () => this._getUrls(),
4049
4058
  getRedirectMethod: () => this._redirectMethod ?? throwErr("Redirect method should have been initialized in the Stack client app constructor"),
4050
4059
  redirectToUrl: async (url: string | URL, options?: { replace?: boolean }) => {
4051
4060
  await this._redirectTo({ url, ...options });
@@ -391,6 +391,39 @@ describe("EventTracker", () => {
391
391
  }
392
392
  });
393
393
 
394
+ it("silently ignores network errors caused by ad blockers", async () => {
395
+ vi.useFakeTimers();
396
+ document.body.innerHTML = "<button>Click me</button>";
397
+
398
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
399
+ const sentBodies: string[] = [];
400
+ const tracker = new EventTracker({
401
+ projectId: "internal",
402
+ sendBatch: async (body) => {
403
+ sentBodies.push(body);
404
+ return Result.error(new TypeError("Failed to fetch"));
405
+ },
406
+ });
407
+
408
+ try {
409
+ tracker.start();
410
+
411
+ await advancePastFlush();
412
+ expect(sentBodies).toHaveLength(1);
413
+ expect(warnSpy).not.toHaveBeenCalled();
414
+
415
+ // Unlike ANALYTICS_NOT_ENABLED, ad blocker errors do NOT disable the
416
+ // tracker — subsequent flushes continue attempting delivery.
417
+ document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
418
+ await advancePastFlush();
419
+ expect(sentBodies).toHaveLength(2);
420
+ expect(warnSpy).not.toHaveBeenCalled();
421
+ } finally {
422
+ tracker.stop();
423
+ warnSpy.mockRestore();
424
+ }
425
+ });
426
+
394
427
  it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
395
428
  vi.useFakeTimers();
396
429
  document.body.innerHTML = "<button>Click me</button>";
@@ -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, isAnalyticsNotEnabledError } from "./session-replay";
11
+ import { generateUuid, isAdBlockerNetworkError, isAnalyticsNotEnabledError } from "./session-replay";
12
12
 
13
13
  const FLUSH_INTERVAL_MS = 10_000;
14
14
  const MAX_EVENTS_PER_BATCH = 50;
@@ -511,6 +511,11 @@ export class EventTracker {
511
511
  this._disable();
512
512
  return;
513
513
  }
514
+ // Ad blockers commonly block analytics endpoints, causing network
515
+ // errors. These are expected and should not pollute the console.
516
+ if (isAdBlockerNetworkError(res.error)) {
517
+ return;
518
+ }
514
519
  console.warn("EventTracker flush failed:", res.error);
515
520
  return;
516
521
  }
@@ -49,6 +49,58 @@ describe("analytics option JSON conversion", () => {
49
49
  });
50
50
 
51
51
  describe("SessionRecorder flush", () => {
52
+ it("silently ignores network errors caused by ad blockers", async () => {
53
+ vi.useFakeTimers();
54
+
55
+ const storageKey = `hexclave:session-replay:v1:test-project`;
56
+ localStorage.setItem(storageKey, JSON.stringify({
57
+ session_id: "test-session",
58
+ created_at_ms: Date.now(),
59
+ last_activity_ms: Date.now(),
60
+ }));
61
+
62
+ const sentBodies: string[] = [];
63
+ const recorder = new SessionRecorder(
64
+ {
65
+ projectId: "test-project",
66
+ sendBatch: async (body) => {
67
+ sentBodies.push(body);
68
+ return Result.error(new TypeError("Failed to fetch"));
69
+ },
70
+ },
71
+ {},
72
+ );
73
+
74
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
75
+
76
+ try {
77
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
78
+ (recorder as any)._events = [{ type: 2, timestamp: Date.now(), data: {} }];
79
+
80
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
81
+ (recorder as any)._tick();
82
+ await vi.advanceTimersByTimeAsync(0);
83
+
84
+ expect(sentBodies).toHaveLength(1);
85
+ expect(warnSpy).not.toHaveBeenCalled();
86
+
87
+ // Unlike ANALYTICS_NOT_ENABLED, ad blocker errors do NOT disable the
88
+ // recorder — subsequent flushes continue attempting delivery.
89
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
90
+ (recorder as any)._events = [{ type: 3, timestamp: Date.now(), data: {} }];
91
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
92
+ (recorder as any)._tick();
93
+ await vi.advanceTimersByTimeAsync(0);
94
+ expect(sentBodies).toHaveLength(2);
95
+ expect(warnSpy).not.toHaveBeenCalled();
96
+ } finally {
97
+ recorder.stop();
98
+ warnSpy.mockRestore();
99
+ localStorage.removeItem(storageKey);
100
+ vi.useRealTimers();
101
+ }
102
+ });
103
+
52
104
  it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
53
105
  vi.useFakeTimers();
54
106
 
@@ -170,6 +170,21 @@ export function isAnalyticsNotEnabledError(error: unknown): boolean {
170
170
  return KnownErrors.AnalyticsNotEnabled.isInstance(error);
171
171
  }
172
172
 
173
+ /**
174
+ * Whether the error looks like a network failure caused by an ad blocker or
175
+ * similar extension blocking analytics requests. These are expected in
176
+ * production and should be silently ignored rather than logged as warnings.
177
+ */
178
+ export function isAdBlockerNetworkError(error: unknown): boolean {
179
+ if (error instanceof Error) {
180
+ return error.message.includes("Failed to fetch")
181
+ || error.message.includes("NetworkError")
182
+ || error.message.includes("Load failed")
183
+ || error.message.includes("network connection");
184
+ }
185
+ return false;
186
+ }
187
+
173
188
  export class SessionRecorder {
174
189
  private _started = false;
175
190
  private _cancelled = false;
@@ -278,6 +293,11 @@ export class SessionRecorder {
278
293
  this._disable();
279
294
  return;
280
295
  }
296
+ // Ad blockers commonly block analytics endpoints, causing network
297
+ // errors. These are expected and should not pollute the console.
298
+ if (isAdBlockerNetworkError(res.error)) {
299
+ return;
300
+ }
281
301
  captureWarning("SessionRecorder.flush", res.error);
282
302
  return;
283
303
  }
@@ -13,6 +13,7 @@ import { AsyncStoreProperty, EmailConfig } from "../../common";
13
13
  import { AdminEmailOutbox, AdminSentEmail } from "../../email";
14
14
  import { InternalApiKey, InternalApiKeyCreateOptions, InternalApiKeyFirstView } from "../../internal-api-keys";
15
15
  import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions } from "../../permissions";
16
+ import type { PlanUsage } from "../../plan-usage";
16
17
  import { AdminProject } from "../../projects";
17
18
  import { _HexclaveAdminAppImpl } from "../implementations";
18
19
  import { StackServerApp, StackServerAppConstructorOptions } from "./server-app";
@@ -75,6 +76,7 @@ export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, Proje
75
76
  /** @deprecated Use `HexclaveAdminApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
76
77
  export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
77
78
  & AsyncStoreProperty<"project", [], AdminProject, false>
79
+ & AsyncStoreProperty<"planUsage", [], PlanUsage, false>
78
80
  & AsyncStoreProperty<"internalApiKeys", [], InternalApiKey[], true>
79
81
  & AsyncStoreProperty<"teamPermissionDefinitions", [], AdminTeamPermissionDefinition[], true>
80
82
  & AsyncStoreProperty<"projectPermissionDefinitions", [], AdminProjectPermissionDefinition[], true>
@@ -131,6 +131,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
131
131
  sendAnalyticsEventBatch(body: string, options: { keepalive: boolean }): Promise<Result<Response, Error>>,
132
132
  addRequestListener(listener: RequestListener): () => void,
133
133
  sendRequest(path: string, requestOptions: RequestInit, requestType?: "client" | "server" | "admin"): Promise<Response>,
134
+ getUrls(): Readonly<ResolvedHandlerUrls>,
134
135
  getRedirectMethod(): RedirectMethod,
135
136
  redirectToUrl(url: string | URL, options?: { replace?: boolean }): Promise<void>,
136
137
  redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<void>,
@@ -118,6 +118,14 @@ export type {
118
118
  PushedConfigSource
119
119
  } from "./projects";
120
120
 
121
+ export type {
122
+ PlanUsage,
123
+ PlanUsageKind,
124
+ PlanUsageNextPlanId,
125
+ PlanUsagePlanId,
126
+ PlanUsageRow,
127
+ } from "./plan-usage";
128
+
121
129
  export type {
122
130
  EditableTeamMemberProfile, ReceivedTeamInvitation,
123
131
  SentTeamInvitation, ServerListUsersOptions,
@@ -0,0 +1,29 @@
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
+ export type PlanUsageKind = "current" | "metered" | "capability";
6
+ export type PlanUsagePlanId = "free" | "team" | "growth";
7
+ export type PlanUsageNextPlanId = "team" | "growth";
8
+
9
+ export type PlanUsageRow = {
10
+ itemId: string,
11
+ displayName: string,
12
+ kind: PlanUsageKind,
13
+ used: number | null,
14
+ limit: number | null,
15
+ remaining: number | null,
16
+ overage: number | null,
17
+ isUnlimited: boolean,
18
+ };
19
+
20
+ export type PlanUsage = {
21
+ ownerTeamId: string,
22
+ ownerTeamDisplayName: string,
23
+ planId: PlanUsagePlanId,
24
+ planDisplayName: string,
25
+ periodStart: Date,
26
+ periodEnd: Date,
27
+ nextPlanId: PlanUsageNextPlanId | null,
28
+ rows: PlanUsageRow[],
29
+ };
@@ -30,6 +30,8 @@ export type PushConfigOptions = {
30
30
  export type Project = {
31
31
  readonly id: string,
32
32
  readonly displayName: string,
33
+ readonly pushedConfigError: { message: string } | null,
34
+ readonly configWarnings: { message: string }[],
33
35
  readonly config: ProjectConfig,
34
36
  };
35
37
 
@@ -42,6 +44,7 @@ export type AdminProject = {
42
44
  readonly isDevelopmentEnvironment: boolean,
43
45
  readonly ownerTeamId: string | null,
44
46
  readonly onboardingStatus: ProjectOnboardingStatus,
47
+ readonly onboardingState: NonNullable<ProjectsCrud["Admin"]["Read"]["onboarding_state"]> | null,
45
48
  readonly logoUrl: string | null | undefined,
46
49
  readonly logoFullUrl: string | null | undefined,
47
50
  readonly logoDarkModeUrl: string | null | undefined,