@hexclave/react 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 (100) hide show
  1. package/dist/components-page/hexclave-handler-client.js +6 -5
  2. package/dist/components-page/hexclave-handler-client.js.map +1 -1
  3. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  4. package/dist/dev-tool/dev-tool-core.js +4 -68
  5. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  6. package/dist/esm/components-page/hexclave-handler-client.js +6 -5
  7. package/dist/esm/components-page/hexclave-handler-client.js.map +1 -1
  8. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  9. package/dist/esm/dev-tool/dev-tool-core.js +4 -68
  10. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  11. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +7 -0
  12. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  13. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +35 -1
  14. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  15. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
  16. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  17. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +18 -11
  18. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  19. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
  20. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  21. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +2 -1
  22. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  23. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +26 -0
  24. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  25. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +7 -1
  26. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  27. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +11 -1
  28. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  29. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +43 -0
  30. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  31. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +2 -1
  32. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  33. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  34. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
  35. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  36. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  37. package/dist/esm/lib/hexclave-app/index.d.ts +2 -1
  38. package/dist/esm/lib/hexclave-app/plan-usage/index.d.ts +27 -0
  39. package/dist/esm/lib/hexclave-app/plan-usage/index.d.ts.map +1 -0
  40. package/dist/esm/lib/hexclave-app/plan-usage/index.js +1 -0
  41. package/dist/esm/lib/hexclave-app/projects/index.d.ts +7 -0
  42. package/dist/esm/lib/hexclave-app/projects/index.d.ts.map +1 -1
  43. package/dist/esm/lib/hexclave-app/projects/index.js.map +1 -1
  44. package/dist/esm/pushed-config-error-overlay/index.d.ts +7 -0
  45. package/dist/esm/pushed-config-error-overlay/index.d.ts.map +1 -0
  46. package/dist/esm/pushed-config-error-overlay/index.js +464 -0
  47. package/dist/esm/pushed-config-error-overlay/index.js.map +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +7 -0
  50. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  51. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +35 -1
  52. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  53. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
  54. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  55. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +18 -11
  56. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  57. package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
  58. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  59. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +1 -0
  60. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  61. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +26 -0
  62. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  63. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +7 -1
  64. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  65. package/dist/lib/hexclave-app/apps/implementations/session-replay.js +11 -0
  66. package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  67. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +43 -0
  68. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  69. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +2 -1
  70. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  71. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  72. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
  73. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  74. package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  75. package/dist/lib/hexclave-app/index.d.ts +2 -1
  76. package/dist/lib/hexclave-app/plan-usage/index.d.ts +27 -0
  77. package/dist/lib/hexclave-app/plan-usage/index.d.ts.map +1 -0
  78. package/dist/lib/hexclave-app/plan-usage/index.js +0 -0
  79. package/dist/lib/hexclave-app/projects/index.d.ts +7 -0
  80. package/dist/lib/hexclave-app/projects/index.d.ts.map +1 -1
  81. package/dist/lib/hexclave-app/projects/index.js.map +1 -1
  82. package/dist/pushed-config-error-overlay/index.d.ts +7 -0
  83. package/dist/pushed-config-error-overlay/index.d.ts.map +1 -0
  84. package/dist/pushed-config-error-overlay/index.js +466 -0
  85. package/dist/pushed-config-error-overlay/index.js.map +1 -0
  86. package/package.json +3 -3
  87. package/src/components-page/hexclave-handler-client.tsx +6 -5
  88. package/src/dev-tool/dev-tool-core.ts +5 -59
  89. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +44 -1
  90. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +20 -11
  91. package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +33 -0
  92. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +6 -1
  93. package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +52 -0
  94. package/src/lib/hexclave-app/apps/implementations/session-replay.ts +20 -0
  95. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +2 -0
  96. package/src/lib/hexclave-app/apps/interfaces/client-app.ts +1 -0
  97. package/src/lib/hexclave-app/index.ts +8 -0
  98. package/src/lib/hexclave-app/plan-usage/index.ts +29 -0
  99. package/src/lib/hexclave-app/projects/index.ts +3 -0
  100. 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";
@@ -41,13 +40,14 @@ import type { TurnstileAction } from "@hexclave/shared/dist/utils/turnstile";
41
40
  import { BotChallengeExecutionFailedError, BotChallengeUserCancelledError, withBotChallengeFlow } from "@hexclave/shared/dist/utils/turnstile-flow";
42
41
  import { createUrlIfValid, getRelativePart, isRelative } from "@hexclave/shared/dist/utils/urls";
43
42
  import { generateUuid } from "@hexclave/shared/dist/utils/uuids";
43
+ import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
44
44
  import * as cookie from "cookie";
45
45
  import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
46
46
  import type * as yup from "yup";
47
+ import { envVars } from "../../../../generated/env";
47
48
  import { constructRedirectUrl } from "../../../../utils/url";
48
49
  import { callOAuthCallback, getNewOAuthProviderOrScopeUrl } from "../../../auth";
49
50
  import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, getCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
50
- import { envVars } from "../../../../generated/env";
51
51
  import { ApiKey, ApiKeyCreationOptions, ApiKeyUpdateOptions, apiKeyCreationOptionsToCrud } from "../../api-keys";
52
52
  import { ConvexCtx, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, RequestLike, ResolvedHandlerUrls, TokenStoreInit, hexclaveAppInternalsSymbol } from "../../common";
53
53
  import { DeprecatedOAuthConnection, OAuthConnection } from "../../connected-accounts";
@@ -71,6 +71,7 @@ import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsO
71
71
  import { useAsyncCache } from "./common";
72
72
  import { mountClickmapOverlay } from "../../../../clickmap";
73
73
  import { mountDevTool } from "../../../../dev-tool";
74
+ import { mountPushedConfigErrorOverlay } from "../../../../pushed-config-error-overlay";
74
75
 
75
76
  let isReactServer = false;
76
77
 
@@ -347,7 +348,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
347
348
  {
348
349
  provider,
349
350
  redirectUrl: this._getOAuthCallbackRedirectUri(),
350
- errorRedirectUrl: this.urls.error,
351
+ errorRedirectUrl: this._getUrls().error,
351
352
  providerScope: mergeScopeStrings(scopeString, (this._oauthScopesOnSignIn[provider as ProviderType] ?? []).join(" ")),
352
353
  },
353
354
  session,
@@ -550,7 +551,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
550
551
  {
551
552
  provider: options.providerId,
552
553
  redirectUrl: this._getOAuthCallbackRedirectUri(),
553
- errorRedirectUrl: this.urls.error,
554
+ errorRedirectUrl: this._getUrls().error,
554
555
  providerScope: mergeScopeStrings(options.scope || "", (this._oauthScopesOnSignIn[options.providerId] ?? []).join(" ")),
555
556
  },
556
557
  options.session,
@@ -732,6 +733,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
732
733
  // when a dashboard-minted token is handed over, so the listener is
733
734
  // mounted unconditionally (the heavy UI is lazy-loaded on demand).
734
735
  mountClickmapOverlay(this as any);
736
+ mountPushedConfigErrorOverlay(this as any);
735
737
  }
736
738
  }
737
739
 
@@ -853,7 +855,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
853
855
 
854
856
  protected _getOAuthCallbackRedirectUri(): string {
855
857
  if (!this._isOAuthCallbackUrlHosted()) {
856
- return this.urls.oauthCallback;
858
+ return this._getUrls().oauthCallback;
857
859
  }
858
860
  if (typeof window === "undefined") {
859
861
  throw new HexclaveAssertionError("Hosted OAuth callback URLs require a browser environment to use the current URL as the redirect URI");
@@ -1620,6 +1622,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
1620
1622
  return {
1621
1623
  id: crud.id,
1622
1624
  displayName: crud.display_name,
1625
+ pushedConfigError: crud.pushed_config_error == null ? null : {
1626
+ message: crud.pushed_config_error.message,
1627
+ },
1628
+ configWarnings: crud.config_warnings.map((warning) => ({
1629
+ message: warning.message,
1630
+ })),
1623
1631
  config: {
1624
1632
  signUpEnabled: crud.config.sign_up_enabled,
1625
1633
  credentialEnabled: crud.config.credential_enabled,
@@ -3002,13 +3010,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3002
3010
  async redirectToMfa(options?: RedirectToOptions) { return await this._redirectToHandler("mfa", options); }
3003
3011
 
3004
3012
  async sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<undefined, KnownErrors["UserNotFound"]>> {
3005
- return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset, "callbackUrl"));
3013
+ return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this._getUrls().passwordReset, "callbackUrl"));
3006
3014
  }
3007
3015
 
3008
3016
  async sendMagicLinkEmail(email: string, options?: {
3009
3017
  callbackUrl?: string,
3010
3018
  }): Promise<Result<{ nonce: string }, KnownErrors["RedirectUrlNotWhitelisted"] | KnownErrors["BotChallengeFailed"]>> {
3011
- const callbackUrl = options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback, "callbackUrl");
3019
+ const callbackUrl = options?.callbackUrl ?? constructRedirectUrl(this._getUrls().magicLinkCallback, "callbackUrl");
3012
3020
  return await this._executeResultWithBotChallengeFlow({
3013
3021
  action: "send_magic_link_email",
3014
3022
  execute: async (challenge) => {
@@ -3297,7 +3305,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3297
3305
  return await this._interface.authorizeOAuth({
3298
3306
  provider,
3299
3307
  redirectUrl: constructRedirectUrl(this._getOAuthCallbackRedirectUri(), "redirectUrl"),
3300
- errorRedirectUrl: constructRedirectUrl(this.urls.error, "errorRedirectUrl"),
3308
+ errorRedirectUrl: constructRedirectUrl(this._getUrls().error, "errorRedirectUrl"),
3301
3309
  afterCallbackRedirectUrl,
3302
3310
  type: "authenticate",
3303
3311
  providerScope: this._oauthScopesOnSignIn[provider]?.join(" "),
@@ -3417,7 +3425,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3417
3425
  }
3418
3426
  this._ensurePersistentTokenStore();
3419
3427
  const session = await this._getSession();
3420
- const emailVerificationRedirectUrl = options.noVerificationCallback ? undefined : options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification, "verificationCallbackUrl");
3428
+ const emailVerificationRedirectUrl = options.noVerificationCallback ? undefined : options.verificationCallbackUrl ?? constructRedirectUrl(this._getUrls().emailVerification, "verificationCallbackUrl");
3421
3429
 
3422
3430
  const executeSignUp = async (challenge: { token?: string, phase?: "invisible" | "visible", unavailable?: true }) => {
3423
3431
  let result = await this._interface.signUpWithCredential(
@@ -3580,7 +3588,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3580
3588
 
3581
3589
  // Step 2: Open the browser for the user to authenticate and display the verification code
3582
3590
  const url = buildCliAuthConfirmUrl({
3583
- cliAuthConfirmUrl: this.urls.cliAuthConfirm,
3591
+ cliAuthConfirmUrl: this._getUrls().cliAuthConfirm,
3584
3592
  appUrl: options.appUrl,
3585
3593
  loginCode,
3586
3594
  });
@@ -4018,6 +4026,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
4018
4026
  ) => {
4019
4027
  return await this._interface.sendClientRequest(path, requestOptions, await this._getSession(), requestType);
4020
4028
  },
4029
+ getUrls: () => this._getUrls(),
4021
4030
  getRedirectMethod: () => this._redirectMethod ?? throwErr("Redirect method should have been initialized in the Stack client app constructor"),
4022
4031
  redirectToUrl: async (url: string | URL, options?: { replace?: boolean }) => {
4023
4032
  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,