@hexclave/tanstack-start 1.0.21 → 1.0.23

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 (74) hide show
  1. package/README.md +187 -7
  2. package/dist/components-page/oauth-callback.js +14 -19
  3. package/dist/components-page/oauth-callback.js.map +1 -1
  4. package/dist/components-page/oauth-callback.test.d.ts +1 -0
  5. package/dist/components-page/oauth-callback.test.js +90 -0
  6. package/dist/components-page/oauth-callback.test.js.map +1 -0
  7. package/dist/dev-tool/index.d.ts +6 -5
  8. package/dist/dev-tool/index.d.ts.map +1 -1
  9. package/dist/dev-tool/index.js +16 -6
  10. package/dist/dev-tool/index.js.map +1 -1
  11. package/dist/esm/components-page/oauth-callback.js +14 -19
  12. package/dist/esm/components-page/oauth-callback.js.map +1 -1
  13. package/dist/esm/components-page/oauth-callback.test.d.ts +1 -0
  14. package/dist/esm/components-page/oauth-callback.test.js +89 -0
  15. package/dist/esm/components-page/oauth-callback.test.js.map +1 -0
  16. package/dist/esm/dev-tool/index.d.ts +6 -5
  17. package/dist/esm/dev-tool/index.d.ts.map +1 -1
  18. package/dist/esm/dev-tool/index.js +16 -6
  19. package/dist/esm/dev-tool/index.js.map +1 -1
  20. package/dist/esm/lib/auth.d.ts.map +1 -1
  21. package/dist/esm/lib/auth.js +32 -11
  22. package/dist/esm/lib/auth.js.map +1 -1
  23. package/dist/esm/lib/auth.test.js +25 -10
  24. package/dist/esm/lib/auth.test.js.map +1 -1
  25. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +3 -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/admin-app-impl.js +1 -0
  28. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  29. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
  30. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  31. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +5 -2
  32. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  33. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +29 -5
  34. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  35. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
  36. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
  37. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  38. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  39. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +5 -3
  40. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  41. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  42. package/dist/lib/auth.d.ts.map +1 -1
  43. package/dist/lib/auth.js +31 -10
  44. package/dist/lib/auth.js.map +1 -1
  45. package/dist/lib/auth.test.js +23 -8
  46. package/dist/lib/auth.test.js.map +1 -1
  47. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +3 -2
  48. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  49. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
  50. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  51. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
  52. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  53. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +5 -2
  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 +28 -4
  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/interfaces/admin-app.d.ts +1 -0
  59. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  60. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  61. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +5 -3
  62. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  63. package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  64. package/package.json +3 -3
  65. package/src/components-page/oauth-callback.test.tsx +109 -0
  66. package/src/components-page/oauth-callback.tsx +14 -19
  67. package/src/dev-tool/index.ts +27 -6
  68. package/src/lib/auth.test.ts +32 -10
  69. package/src/lib/auth.ts +41 -7
  70. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +2 -1
  71. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +66 -0
  72. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +40 -4
  73. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +1 -0
  74. package/src/lib/hexclave-app/apps/interfaces/client-app.ts +5 -3
package/src/lib/auth.ts CHANGED
@@ -2,7 +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 { KnownError, HexclaveClientInterface } from "@hexclave/shared";
5
+ import { KnownError, KnownErrors, HexclaveClientInterface } from "@hexclave/shared";
6
6
  import { InternalSession } from "@hexclave/shared/dist/sessions";
7
7
  import { HexclaveAssertionError, throwErr } from "@hexclave/shared/dist/utils/errors";
8
8
  import { Result } from "@hexclave/shared/dist/utils/results";
@@ -50,10 +50,39 @@ type OAuthCallbackConsumptionResult =
50
50
  error: KnownError,
51
51
  };
52
52
 
53
+ const oauthErrorParams = ["error", "error_description", "errorCode", "message", "details"] as const;
54
+
55
+ function removeOAuthErrorParamsFromHistory(originalUrl: URL): void {
56
+ const newUrl = new URL(originalUrl);
57
+ for (const param of oauthErrorParams) {
58
+ newUrl.searchParams.delete(param);
59
+ }
60
+ window.history.replaceState({}, "", newUrl.toString());
61
+ }
62
+
63
+ function getProviderOAuthErrorFromUrl(originalUrl: URL): KnownError | null {
64
+ const providerError = originalUrl.searchParams.get("error");
65
+ const providerErrorDescription = originalUrl.searchParams.get("error_description");
66
+ if (providerError == null && providerErrorDescription == null) {
67
+ return null;
68
+ }
69
+
70
+ switch (providerError) {
71
+ case "access_denied":
72
+ case "consent_required": {
73
+ return new KnownErrors.OAuthProviderAccessDenied();
74
+ }
75
+ case "server_error":
76
+ case "temporarily_unavailable":
77
+ default: {
78
+ return new KnownErrors.OAuthProviderTemporarilyUnavailable();
79
+ }
80
+ }
81
+ }
82
+
53
83
  function consumeOAuthCallbackQueryParams(options?: {
54
84
  dontWarnAboutMissingQueryParams?: boolean,
55
85
  }): OAuthCallbackConsumptionResult | null {
56
- const oauthErrorParams = ["error", "error_description", "errorCode", "message", "details"] as const;
57
86
  const requiredParams = ["code", "state"];
58
87
  const originalUrl = new URL(window.location.href);
59
88
  const knownErrorCode = originalUrl.searchParams.get("errorCode");
@@ -72,11 +101,7 @@ function consumeOAuthCallbackQueryParams(options?: {
72
101
  }
73
102
  }
74
103
 
75
- const newUrl = new URL(originalUrl);
76
- for (const param of oauthErrorParams) {
77
- newUrl.searchParams.delete(param);
78
- }
79
- window.history.replaceState({}, "", newUrl.toString());
104
+ removeOAuthErrorParamsFromHistory(originalUrl);
80
105
 
81
106
  return {
82
107
  type: "known-error",
@@ -88,6 +113,15 @@ function consumeOAuthCallbackQueryParams(options?: {
88
113
  };
89
114
  }
90
115
 
116
+ const providerOAuthError = getProviderOAuthErrorFromUrl(originalUrl);
117
+ if (providerOAuthError != null && !requiredParams.every(param => originalUrl.searchParams.has(param))) {
118
+ removeOAuthErrorParamsFromHistory(originalUrl);
119
+ return {
120
+ type: "known-error",
121
+ error: providerOAuthError,
122
+ };
123
+ }
124
+
91
125
  for (const param of requiredParams) {
92
126
  if (!originalUrl.searchParams.has(param)) {
93
127
  if (!options?.dontWarnAboutMissingQueryParams) {
@@ -1092,10 +1092,11 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
1092
1092
  return result as AdminEmailOutbox;
1093
1093
  }
1094
1094
 
1095
- async listOutboxEmails(options?: { status?: string, simpleStatus?: string, limit?: number, cursor?: string }): Promise<{ items: AdminEmailOutbox[], nextCursor: string | null }> {
1095
+ async listOutboxEmails(options?: { status?: string, simpleStatus?: string, userId?: string, limit?: number, cursor?: string }): Promise<{ items: AdminEmailOutbox[], nextCursor: string | null }> {
1096
1096
  const response = await this._interface.listOutboxEmails({
1097
1097
  status: options?.status,
1098
1098
  simple_status: options?.simpleStatus,
1099
+ user_id: options?.userId,
1099
1100
  limit: options?.limit,
1100
1101
  cursor: options?.cursor,
1101
1102
  });
@@ -439,6 +439,72 @@ describe("StackClientApp cross-domain auth", () => {
439
439
  }
440
440
  });
441
441
 
442
+ it("redirects hosted current-page OAuth callback errors to the hosted error handler during startup", async () => {
443
+ const projectId = "00000000-0000-4000-8000-000000000010";
444
+ const previousWindow = globalThis.window;
445
+ const previousDocument = globalThis.document;
446
+ const callbackUrl = new URL("https://demo.stack-auth.com/dashboard");
447
+ callbackUrl.searchParams.set("errorCode", "SIGN_UP_REJECTED");
448
+ callbackUrl.searchParams.set("message", "Your sign up was rejected by an administrator's sign-up rule.");
449
+ callbackUrl.searchParams.set("details", JSON.stringify({
450
+ message: "Your sign up was rejected by an administrator's sign-up rule.",
451
+ }));
452
+ let currentHref = callbackUrl.toString();
453
+ let redirectedUrl = "";
454
+ const redirectSpy = vi.spyOn(StackClientApp.prototype as any, "_redirectTo").mockImplementation(async (...args: unknown[]) => {
455
+ const options = args[0] as { url: string | URL };
456
+ redirectedUrl = options.url.toString();
457
+ });
458
+
459
+ globalThis.document = createMockDocument();
460
+ globalThis.window = {
461
+ location: {
462
+ get href() {
463
+ return currentHref;
464
+ },
465
+ set href(value: string) {
466
+ currentHref = value;
467
+ },
468
+ origin: callbackUrl.origin,
469
+ },
470
+ history: {
471
+ replaceState: (_state: unknown, _title: string, url: string) => {
472
+ currentHref = new URL(url, currentHref).toString();
473
+ },
474
+ },
475
+ addEventListener: () => {},
476
+ removeEventListener: () => {},
477
+ } as any;
478
+
479
+ try {
480
+ new StackClientApp({
481
+ baseUrl: "http://localhost:12345",
482
+ projectId,
483
+ publishableClientKey: "stack-pk-test",
484
+ tokenStore: "memory",
485
+ redirectMethod: "window",
486
+ urls: {
487
+ default: { type: "hosted" },
488
+ },
489
+ noAutomaticPrefetch: true,
490
+ });
491
+
492
+ await new Promise((resolve) => setTimeout(resolve, 0));
493
+ await new Promise((resolve) => setTimeout(resolve, 0));
494
+ } finally {
495
+ redirectSpy.mockRestore();
496
+ globalThis.window = previousWindow;
497
+ globalThis.document = previousDocument;
498
+ }
499
+
500
+ const errorUrl = new URL(redirectedUrl);
501
+ expect(errorUrl.origin).toBe(`https://${projectId}.built-with-stack-auth.com`);
502
+ expect(errorUrl.pathname).toBe("/handler/error");
503
+ expect(errorUrl.searchParams.get("errorCode")).toBe("SIGN_UP_REJECTED");
504
+ expect(errorUrl.searchParams.get("message")).toBe("Your sign up was rejected by an administrator's sign-up rule.");
505
+ expect(new URL(currentHref).searchParams.has("errorCode")).toBe(false);
506
+ });
507
+
442
508
  it("uses direct sign-out instead of hosted sign-out redirects when code execution is available", async () => {
443
509
  const clientApp = new StackClientApp({
444
510
  baseUrl: "http://localhost:12345",
@@ -3,7 +3,7 @@
3
3
  // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
4
  //===========================================
5
5
  import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
6
- import { KnownErrors, HexclaveClientInterface } from "@hexclave/shared";
6
+ import { KnownError, KnownErrors, HexclaveClientInterface } from "@hexclave/shared";
7
7
  import type { RequestListener } from "@hexclave/shared/dist/interface/client-interface";
8
8
  import { ContactChannelsCrud } from "@hexclave/shared/dist/interface/crud/contact-channels";
9
9
  import { CurrentUserCrud } from "@hexclave/shared/dist/interface/crud/current-user";
@@ -715,11 +715,11 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
715
715
  if (
716
716
  isBrowserLike()
717
717
  && (this._isOAuthCallbackUrlHosted() || this._currentUrlLooksLikeNestedCrossDomainOAuthCallback())
718
- && this._currentUrlLooksLikeHexclaveOAuthCallback()
718
+ && (this._currentUrlLooksLikeHexclaveOAuthCallback() || this._currentUrlLooksLikeOAuthCallbackError())
719
719
  ) {
720
720
  this._trackPendingAuthResolution(async () => {
721
721
  if (isBrowserLike()) {
722
- await this.callOAuthCallback({ dontWarnAboutMissingQueryParams: true });
722
+ await this._handleHostedOAuthCallbackDuringStartup();
723
723
  }
724
724
  });
725
725
  }
@@ -735,7 +735,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
735
735
  }
736
736
 
737
737
  if (isBrowserLike() && resolvedOptions.devTool !== false) {
738
- mountDevTool(this as any);
738
+ mountDevTool(this as any, resolvedOptions.devTool);
739
739
  }
740
740
  if (isBrowserLike()) {
741
741
  // Independent of the dev tool: the clickmap overlay only ever renders
@@ -819,6 +819,22 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
819
819
  currentUrl.searchParams.has("code") && currentUrl.searchParams.has("state")
820
820
  ) || (
821
821
  currentUrl.searchParams.has("errorCode") && currentUrl.searchParams.has("message")
822
+ ) || (
823
+ this._currentUrlLooksLikeOAuthCallbackError()
824
+ );
825
+ }
826
+
827
+ protected _currentUrlLooksLikeOAuthCallbackError(): boolean {
828
+ if (typeof window === "undefined") {
829
+ return false;
830
+ }
831
+ const currentUrl = new URL(window.location.href);
832
+ if (currentUrl.searchParams.has("errorCode") && currentUrl.searchParams.has("message")) {
833
+ return true;
834
+ }
835
+ return (
836
+ (currentUrl.searchParams.has("error") || currentUrl.searchParams.has("error_description"))
837
+ && !(currentUrl.searchParams.has("code") && currentUrl.searchParams.has("state"))
822
838
  );
823
839
  }
824
840
 
@@ -860,6 +876,26 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
860
876
  return currentUrl.toString();
861
877
  }
862
878
 
879
+ protected async _redirectToOAuthCallbackError(error: KnownError): Promise<void> {
880
+ const errorUrl = new URL(this._getUrls().error, window.location.href);
881
+ errorUrl.searchParams.set("errorCode", error.errorCode);
882
+ errorUrl.searchParams.set("message", error.message);
883
+ errorUrl.searchParams.set("details", JSON.stringify(error.details ?? {}));
884
+ await this._redirectIfTrusted(errorUrl.toString(), { replace: true });
885
+ }
886
+
887
+ protected async _handleHostedOAuthCallbackDuringStartup(): Promise<void> {
888
+ try {
889
+ await this.callOAuthCallback({ dontWarnAboutMissingQueryParams: true });
890
+ } catch (error) {
891
+ if (KnownError.isKnownError(error)) {
892
+ await this._redirectToOAuthCallbackError(error);
893
+ return;
894
+ }
895
+ throw error;
896
+ }
897
+ }
898
+
863
899
  protected async _fetchCurrentRefreshTokenIdIfSignedIn(options?: {
864
900
  awaitPendingAuthResolutions?: boolean,
865
901
  overrideTokenStoreInit?: TokenStoreInit,
@@ -20,6 +20,7 @@ import { StackServerApp, StackServerAppConstructorOptions } from "./server-app";
20
20
  export type EmailOutboxListOptions = {
21
21
  status?: string,
22
22
  simpleStatus?: string,
23
+ userId?: string,
23
24
  limit?: number,
24
25
  cursor?: string,
25
26
  };
@@ -26,11 +26,13 @@ export type StackClientAppConstructorOptions<HasTokenStore extends boolean, Proj
26
26
  inheritsFrom?: StackClientApp<any, any>,
27
27
 
28
28
  /**
29
- * Whether to show the Hexclave dev tool indicator in browser-like development environments.
29
+ * Whether to show the Hexclave dev tool indicator in browser-like environments.
30
30
  *
31
- * Defaults to true.
31
+ * - `true`: always show
32
+ * - `false`: never show
33
+ * - `"auto"` (default): show based on NODE_ENV or origin heuristics
32
34
  */
33
- devTool?: boolean,
35
+ devTool?: boolean | "auto",
34
36
 
35
37
  /**
36
38
  * By default, the Stack app will automatically prefetch some data from Stack's server when this app is first