@hexclave/react 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.
- package/README.md +187 -7
- package/dist/components-page/oauth-callback.js +14 -19
- package/dist/components-page/oauth-callback.js.map +1 -1
- package/dist/components-page/oauth-callback.test.d.ts +1 -0
- package/dist/components-page/oauth-callback.test.js +90 -0
- package/dist/components-page/oauth-callback.test.js.map +1 -0
- package/dist/dev-tool/index.d.ts +6 -5
- package/dist/dev-tool/index.d.ts.map +1 -1
- package/dist/dev-tool/index.js +16 -6
- package/dist/dev-tool/index.js.map +1 -1
- package/dist/esm/components-page/oauth-callback.js +14 -19
- package/dist/esm/components-page/oauth-callback.js.map +1 -1
- package/dist/esm/components-page/oauth-callback.test.d.ts +1 -0
- package/dist/esm/components-page/oauth-callback.test.js +89 -0
- package/dist/esm/components-page/oauth-callback.test.js.map +1 -0
- package/dist/esm/dev-tool/index.d.ts +6 -5
- package/dist/esm/dev-tool/index.d.ts.map +1 -1
- package/dist/esm/dev-tool/index.js +16 -6
- package/dist/esm/dev-tool/index.js.map +1 -1
- package/dist/esm/generated/quetzal-translations.d.ts +2 -2
- package/dist/esm/lib/auth.d.ts.map +1 -1
- package/dist/esm/lib/auth.js +32 -11
- package/dist/esm/lib/auth.js.map +1 -1
- package/dist/esm/lib/auth.test.js +25 -10
- package/dist/esm/lib/auth.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +3 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +5 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +29 -5
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +5 -3
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/generated/quetzal-translations.d.ts +2 -2
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +31 -10
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/auth.test.js +23 -8
- package/dist/lib/auth.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +3 -2
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +5 -2
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +28 -4
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +5 -3
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/package.json +3 -3
- package/src/components-page/oauth-callback.test.tsx +109 -0
- package/src/components-page/oauth-callback.tsx +14 -19
- package/src/dev-tool/index.ts +27 -6
- package/src/lib/auth.test.ts +32 -10
- package/src/lib/auth.ts +41 -7
- package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +2 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +66 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +40 -4
- package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +1 -0
- 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
|
-
|
|
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";
|
|
@@ -705,11 +705,11 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
705
705
|
if (
|
|
706
706
|
isBrowserLike()
|
|
707
707
|
&& (this._isOAuthCallbackUrlHosted() || this._currentUrlLooksLikeNestedCrossDomainOAuthCallback())
|
|
708
|
-
&& this._currentUrlLooksLikeHexclaveOAuthCallback()
|
|
708
|
+
&& (this._currentUrlLooksLikeHexclaveOAuthCallback() || this._currentUrlLooksLikeOAuthCallbackError())
|
|
709
709
|
) {
|
|
710
710
|
this._trackPendingAuthResolution(async () => {
|
|
711
711
|
if (isBrowserLike()) {
|
|
712
|
-
await this.
|
|
712
|
+
await this._handleHostedOAuthCallbackDuringStartup();
|
|
713
713
|
}
|
|
714
714
|
});
|
|
715
715
|
}
|
|
@@ -725,7 +725,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
725
725
|
}
|
|
726
726
|
|
|
727
727
|
if (isBrowserLike() && resolvedOptions.devTool !== false) {
|
|
728
|
-
mountDevTool(this as any);
|
|
728
|
+
mountDevTool(this as any, resolvedOptions.devTool);
|
|
729
729
|
}
|
|
730
730
|
if (isBrowserLike()) {
|
|
731
731
|
// Independent of the dev tool: the clickmap overlay only ever renders
|
|
@@ -809,6 +809,22 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
809
809
|
currentUrl.searchParams.has("code") && currentUrl.searchParams.has("state")
|
|
810
810
|
) || (
|
|
811
811
|
currentUrl.searchParams.has("errorCode") && currentUrl.searchParams.has("message")
|
|
812
|
+
) || (
|
|
813
|
+
this._currentUrlLooksLikeOAuthCallbackError()
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
protected _currentUrlLooksLikeOAuthCallbackError(): boolean {
|
|
818
|
+
if (typeof window === "undefined") {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
const currentUrl = new URL(window.location.href);
|
|
822
|
+
if (currentUrl.searchParams.has("errorCode") && currentUrl.searchParams.has("message")) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
return (
|
|
826
|
+
(currentUrl.searchParams.has("error") || currentUrl.searchParams.has("error_description"))
|
|
827
|
+
&& !(currentUrl.searchParams.has("code") && currentUrl.searchParams.has("state"))
|
|
812
828
|
);
|
|
813
829
|
}
|
|
814
830
|
|
|
@@ -850,6 +866,26 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
850
866
|
return currentUrl.toString();
|
|
851
867
|
}
|
|
852
868
|
|
|
869
|
+
protected async _redirectToOAuthCallbackError(error: KnownError): Promise<void> {
|
|
870
|
+
const errorUrl = new URL(this._getUrls().error, window.location.href);
|
|
871
|
+
errorUrl.searchParams.set("errorCode", error.errorCode);
|
|
872
|
+
errorUrl.searchParams.set("message", error.message);
|
|
873
|
+
errorUrl.searchParams.set("details", JSON.stringify(error.details ?? {}));
|
|
874
|
+
await this._redirectIfTrusted(errorUrl.toString(), { replace: true });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
protected async _handleHostedOAuthCallbackDuringStartup(): Promise<void> {
|
|
878
|
+
try {
|
|
879
|
+
await this.callOAuthCallback({ dontWarnAboutMissingQueryParams: true });
|
|
880
|
+
} catch (error) {
|
|
881
|
+
if (KnownError.isKnownError(error)) {
|
|
882
|
+
await this._redirectToOAuthCallbackError(error);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
853
889
|
protected async _fetchCurrentRefreshTokenIdIfSignedIn(options?: {
|
|
854
890
|
awaitPendingAuthResolutions?: boolean,
|
|
855
891
|
overrideTokenStoreInit?: TokenStoreInit,
|
|
@@ -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
|
|
29
|
+
* Whether to show the Hexclave dev tool indicator in browser-like environments.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
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
|