@hexclave/next 1.0.11 → 1.0.12
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/dist/esm/generated/quetzal-translations.d.ts +2 -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/client-app-impl.cross-domain.test.js +78 -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 +1 -1
- 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 +8 -4
- 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/generated/quetzal-translations.d.ts +2 -2
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +78 -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 +1 -1
- 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 +8 -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/package.json +4 -4
- package/src/integrations/convex/component/README.md +1 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +108 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +10 -2
|
@@ -17,7 +17,7 @@ let ____________generated_env_js = require("../../../../generated/env.js");
|
|
|
17
17
|
let ______url_targets_js = require("../../url-targets.js");
|
|
18
18
|
|
|
19
19
|
//#region src/lib/hexclave-app/apps/implementations/common.ts
|
|
20
|
-
const clientVersion = "js @hexclave/next@1.0.
|
|
20
|
+
const clientVersion = "js @hexclave/next@1.0.12";
|
|
21
21
|
if (clientVersion.startsWith("STACK_COMPILE_TIME")) throw new _hexclave_shared_dist_utils_errors.HexclaveAssertionError("Client version was not replaced. Something went wrong during build!");
|
|
22
22
|
const replaceHexclavePortPrefix = (input) => {
|
|
23
23
|
if (!input) return input;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
|
|
3
3
|
"name": "@hexclave/next",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.12",
|
|
5
5
|
"repository": "https://github.com/hexclave/hexclave",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"rrweb": "^1.1.3",
|
|
76
76
|
"tsx": "^4.21.0",
|
|
77
77
|
"yup": "^1.7.1",
|
|
78
|
-
"@hexclave/
|
|
79
|
-
"@hexclave/
|
|
80
|
-
"@hexclave/
|
|
78
|
+
"@hexclave/shared": "1.0.12",
|
|
79
|
+
"@hexclave/sc": "1.0.12",
|
|
80
|
+
"@hexclave/ui": "1.0.12"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
83
|
"@types/react": ">=18.0.0",
|
|
@@ -23,7 +23,7 @@ import { getConvexProvidersConfig } from "@hexclave/js/convex-auth.config"; //
|
|
|
23
23
|
|
|
24
24
|
export default {
|
|
25
25
|
providers: getConvexProvidersConfig({
|
|
26
|
-
projectId: process.env.
|
|
26
|
+
projectId: process.env.HEXCLAVE_PROJECT_ID, // or: process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID
|
|
27
27
|
}),
|
|
28
28
|
}
|
|
29
29
|
```
|
|
@@ -287,6 +287,12 @@ describe("StackClientApp cross-domain auth", () => {
|
|
|
287
287
|
const originalFetchNewAccessToken = Reflect.get(clientInterface, "fetchNewAccessToken");
|
|
288
288
|
const refreshedRawRefreshTokens: string[] = [];
|
|
289
289
|
|
|
290
|
+
// Cookie-store writes queue a background trusted-parent-domain lookup. Without this stub, that
|
|
291
|
+
// lookup fetches the (unreachable) baseUrl with retries while holding the global store lock,
|
|
292
|
+
// which starves any later test that needs the write lock (e.g. signOut). Not restored on
|
|
293
|
+
// purpose: queued tasks can still run after this test body finishes.
|
|
294
|
+
vi.spyOn(clientApp as any, "_getTrustedParentDomain").mockResolvedValue(null);
|
|
295
|
+
|
|
290
296
|
try {
|
|
291
297
|
const getBrowserCookieTokenStore = Reflect.get(clientApp, "_getBrowserCookieTokenStore");
|
|
292
298
|
if (typeof getBrowserCookieTokenStore !== "function") {
|
|
@@ -329,6 +335,108 @@ describe("StackClientApp cross-domain auth", () => {
|
|
|
329
335
|
expect(refreshedRawRefreshTokens).toEqual(["new-refresh-token"]);
|
|
330
336
|
});
|
|
331
337
|
|
|
338
|
+
it("does not re-bounce nested cross-domain auth after the OAuth callback consumed code+state from the URL", async () => {
|
|
339
|
+
const projectId = "00000000-0000-4000-8000-000000000008";
|
|
340
|
+
const previousWindow = globalThis.window;
|
|
341
|
+
const previousDocument = globalThis.document;
|
|
342
|
+
|
|
343
|
+
const strippedUrl = new URL(`https://${projectId}.example-stack-hosted.test/handler/sign-in`);
|
|
344
|
+
strippedUrl.searchParams.set("stack_nested_cross_domain_auth_refresh_token_id", "source-refresh-token-id");
|
|
345
|
+
strippedUrl.searchParams.set("stack_nested_cross_domain_auth_callback_url", "https://demo.stack-auth.com/");
|
|
346
|
+
const urlAtConstructionTime = new URL(strippedUrl);
|
|
347
|
+
urlAtConstructionTime.searchParams.set("code", "one-time-code");
|
|
348
|
+
urlAtConstructionTime.searchParams.set("state", "nested-oauth-state");
|
|
349
|
+
|
|
350
|
+
// Construct before installing the window mock so the constructor does not schedule its own
|
|
351
|
+
// nested-auth resolution; the assertions below drive the handler explicitly.
|
|
352
|
+
const clientApp = new StackClientApp({
|
|
353
|
+
baseUrl: "http://localhost:12345",
|
|
354
|
+
projectId,
|
|
355
|
+
publishableClientKey: "stack-pk-test",
|
|
356
|
+
tokenStore: "memory",
|
|
357
|
+
redirectMethod: "window",
|
|
358
|
+
noAutomaticPrefetch: true,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
globalThis.document = createMockDocument();
|
|
362
|
+
globalThis.window = {
|
|
363
|
+
location: {
|
|
364
|
+
href: strippedUrl.toString(),
|
|
365
|
+
replace: () => {
|
|
366
|
+
throw new Error("INTENTIONAL_TEST_ABORT");
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
} as any;
|
|
370
|
+
|
|
371
|
+
vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null);
|
|
372
|
+
vi.spyOn(clientApp as any, "_getCrossDomainHandoffParamsForRedirect").mockResolvedValue({
|
|
373
|
+
state: "fresh-nested-state",
|
|
374
|
+
codeChallenge: "fresh-nested-code-challenge",
|
|
375
|
+
});
|
|
376
|
+
vi.spyOn(clientApp as any, "_isTrusted").mockResolvedValue(true);
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
// Without the construction-time URL, the handler re-bounces (location.replace aborts).
|
|
380
|
+
await expect((clientApp as any)._maybeHandleNestedCrossDomainAuth()).rejects.toThrowError("INTENTIONAL_TEST_ABORT");
|
|
381
|
+
// With it, the in-flight OAuth callback wins and the handler stands down.
|
|
382
|
+
await expect((clientApp as any)._maybeHandleNestedCrossDomainAuth(urlAtConstructionTime)).resolves.toBe(false);
|
|
383
|
+
// The live-URL guard must also stand down on its own when code+state are still present.
|
|
384
|
+
(globalThis.window as any).location.href = urlAtConstructionTime.toString();
|
|
385
|
+
await expect((clientApp as any)._maybeHandleNestedCrossDomainAuth()).resolves.toBe(false);
|
|
386
|
+
} finally {
|
|
387
|
+
globalThis.window = previousWindow;
|
|
388
|
+
globalThis.document = previousDocument;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("passes the construction-time URL to the nested cross-domain auth handler", async () => {
|
|
393
|
+
const projectId = "00000000-0000-4000-8000-000000000009";
|
|
394
|
+
const previousWindow = globalThis.window;
|
|
395
|
+
const previousDocument = globalThis.document;
|
|
396
|
+
|
|
397
|
+
const callbackUrl = new URL(`https://${projectId}.example-stack-hosted.test/handler/sign-in`);
|
|
398
|
+
callbackUrl.searchParams.set("stack_nested_cross_domain_auth_refresh_token_id", "source-refresh-token-id");
|
|
399
|
+
callbackUrl.searchParams.set("code", "one-time-code");
|
|
400
|
+
callbackUrl.searchParams.set("state", "nested-oauth-state");
|
|
401
|
+
const strippedUrl = new URL(callbackUrl);
|
|
402
|
+
strippedUrl.searchParams.delete("code");
|
|
403
|
+
strippedUrl.searchParams.delete("state");
|
|
404
|
+
|
|
405
|
+
globalThis.document = createMockDocument();
|
|
406
|
+
globalThis.window = {
|
|
407
|
+
location: {
|
|
408
|
+
href: callbackUrl.toString(),
|
|
409
|
+
},
|
|
410
|
+
} as any;
|
|
411
|
+
|
|
412
|
+
const nestedAuthSpy = vi.spyOn(StackClientApp.prototype as any, "_maybeHandleNestedCrossDomainAuth").mockResolvedValue(false);
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
new StackClientApp({
|
|
416
|
+
baseUrl: "http://localhost:12345",
|
|
417
|
+
projectId,
|
|
418
|
+
publishableClientKey: "stack-pk-test",
|
|
419
|
+
tokenStore: "memory",
|
|
420
|
+
redirectMethod: "window",
|
|
421
|
+
noAutomaticPrefetch: true,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Simulate consumeOAuthCallbackQueryParams stripping code+state before microtasks run.
|
|
425
|
+
(globalThis.window as any).location.href = strippedUrl.toString();
|
|
426
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
427
|
+
|
|
428
|
+
expect(nestedAuthSpy).toHaveBeenCalledTimes(1);
|
|
429
|
+
const urlArgument = nestedAuthSpy.mock.calls[0][0] as URL;
|
|
430
|
+
expect(urlArgument).toBeInstanceOf(URL);
|
|
431
|
+
expect(urlArgument.searchParams.get("code")).toBe("one-time-code");
|
|
432
|
+
expect(urlArgument.searchParams.get("state")).toBe("nested-oauth-state");
|
|
433
|
+
} finally {
|
|
434
|
+
nestedAuthSpy.mockRestore();
|
|
435
|
+
globalThis.window = previousWindow;
|
|
436
|
+
globalThis.document = previousDocument;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
332
440
|
it("uses direct sign-out instead of hosted sign-out redirects when code execution is available", async () => {
|
|
333
441
|
const clientApp = new StackClientApp({
|
|
334
442
|
baseUrl: "http://localhost:12345",
|
|
@@ -690,8 +690,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
if (isBrowserLike()) {
|
|
693
|
+
// The OAuth callback resolution scheduled above synchronously strips `code` and `state`
|
|
694
|
+
// from the URL before its token exchange, so the nested handler must decide based on the
|
|
695
|
+
// URL the page was loaded with, not whatever is in the address bar when it runs.
|
|
696
|
+
const urlAtConstructionTime = new URL(window.location.href);
|
|
693
697
|
this._trackPendingAuthResolution(async () => {
|
|
694
|
-
await this._maybeHandleNestedCrossDomainAuth();
|
|
698
|
+
await this._maybeHandleNestedCrossDomainAuth(urlAtConstructionTime);
|
|
695
699
|
});
|
|
696
700
|
}
|
|
697
701
|
|
|
@@ -858,11 +862,15 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
858
862
|
return targetUrl.toString();
|
|
859
863
|
}
|
|
860
864
|
|
|
861
|
-
protected async _maybeHandleNestedCrossDomainAuth(): Promise<boolean> {
|
|
865
|
+
protected async _maybeHandleNestedCrossDomainAuth(urlAtConstructionTime?: URL): Promise<boolean> {
|
|
862
866
|
if (typeof window === "undefined") return false;
|
|
863
867
|
const currentUrl = new URL(window.location.href);
|
|
864
868
|
// A real OAuth callback wins over nested handoff detection on the final return to b.com.
|
|
869
|
+
// The OAuth callback resolution strips `code` and `state` from the live URL before this
|
|
870
|
+
// runs, so the check must also consult the URL captured at construction time — otherwise
|
|
871
|
+
// we'd re-bounce to the source domain while the token exchange is still in flight.
|
|
865
872
|
if (currentUrl.searchParams.has("code") && currentUrl.searchParams.has("state")) return false;
|
|
873
|
+
if (urlAtConstructionTime != null && urlAtConstructionTime.searchParams.has("code") && urlAtConstructionTime.searchParams.has("state")) return false;
|
|
866
874
|
const refreshTokenId = currentUrl.searchParams.get(nestedCrossDomainAuthQueryParams.refreshTokenId);
|
|
867
875
|
if (refreshTokenId == null) return false;
|
|
868
876
|
|