Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/qemu-emulator-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,12 @@ jobs:
- name: Build stack-cli (for emulator CLI)
if: matrix.arch == 'amd64'
run: |
pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...'
# Turbo's trailing `...` filter builds stack-cli AND its workspace
# deps (@stackframe/js, @stackframe/stack-shared, etc.) — stack-cli
# imports them at runtime from their dist/ outputs.
# Turbo's task graph for stack-cli#build includes
# @stackframe/dashboard#build:rde-standalone, which transitively
# depends on @stackframe/stack#build (via dashboard → stack).
# The pnpm filter must cover the dashboard dep tree too so that
# devDependencies like tailwindcss are installed for the build.
pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...' --filter '@stackframe/dashboard...'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
pnpm exec turbo run build --filter='@stackframe/stack-cli...'

- name: Start emulator and verify
Expand Down Expand Up @@ -267,10 +269,8 @@ jobs:

- name: Install stack-cli deps + build
run: |
pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...'
# Turbo's trailing `...` filter builds stack-cli AND its workspace
# deps (@stackframe/js, @stackframe/stack-shared, etc.) — stack-cli
# imports them at runtime from their dist/ outputs.
# See "Build stack-cli" step comment for why dashboard filter is needed
pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...' --filter '@stackframe/dashboard...'
pnpm exec turbo run build --filter='@stackframe/stack-cli...'

- name: Download built image
Expand Down
1 change: 1 addition & 0 deletions apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ it("gets current project (internal)", async ({ expect }) => {
"status": 200,
"body": {
"config": {
"allow_localhost": true,
"allow_team_api_keys": false,
"allow_user_api_keys": false,
"client_team_creation_enabled": true,
Expand Down
62 changes: 45 additions & 17 deletions apps/e2e/tests/js/cross-domain-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@ import { StackClientApp } from "@stackframe/js";
import { afterEach, vi } from "vitest";
import { it, localRedirectUrl } from "../helpers";

function createMockDocument(): Document {
const cookieJar = new Map<string, string>();
return {
get cookie() {
return [...cookieJar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
},
set cookie(str: string) {
const parts = str.split(';');
const [nameValue] = parts;
const eqIndex = nameValue.indexOf('=');
if (eqIndex >= 0) {
const name = nameValue.slice(0, eqIndex).trim();
const isExpired = parts.some(p => {
const trimmed = p.trim().toLowerCase();
if (!trimmed.startsWith('expires=')) return false;
return new Date(trimmed.slice('expires='.length)) <= new Date();
});
if (isExpired) {
cookieJar.delete(name);
} else {
cookieJar.set(name, nameValue.slice(eqIndex + 1).trim());
}
}
},
createElement: () => ({}),
} as any;
Comment on lines +5 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Cookie deletion not supported in mock

The createMockDocument setter only handles writing cookies but never removes them: if any code path under test calls document.cookie = "name=; expires=Thu, 01 Jan 1970 00:00:00 GMT" (the standard deletion pattern), the mock will silently store an empty string value instead of evicting the key. This won't break the two tests being fixed here, but the same mock is now shared infrastructure — any future test that relies on cookie expiry/deletion will see stale values and get a confusing false-positive pass.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/js/cross-domain-auth.test.ts
Line: 5-19

Comment:
**Cookie deletion not supported in mock**

The `createMockDocument` setter only handles writing cookies but never removes them: if any code path under test calls `document.cookie = "name=; expires=Thu, 01 Jan 1970 00:00:00 GMT"` (the standard deletion pattern), the mock will silently store an empty string value instead of evicting the key. This won't break the two tests being fixed here, but the same mock is now shared infrastructure — any future test that relies on cookie expiry/deletion will see stale values and get a confusing false-positive pass.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8cbe5db — the setter now parses expires= directives and evicts the key when the date is in the past.

}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const withHostedDomainSuffix = async (callback: () => Promise<void>) => {
const oldHostedHandlerDomainSuffix = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX;
const oldHostedHandlerUrlTemplate = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE;
Expand Down Expand Up @@ -48,7 +76,7 @@ it("adds secure cross-domain handoff parameters when redirecting to hosted sign-
const previousDocument = globalThis.document;
let redirectedUrl = "";

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: `${localRedirectUrl}/private-page?foo=bar`,
Expand Down Expand Up @@ -90,7 +118,7 @@ it("returns static app.urls.signIn for hosted flows", async ({ expect }) => {

const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentHref,
Expand Down Expand Up @@ -121,7 +149,7 @@ it("returns static app.urls.signOut for hosted flows", async ({ expect }) => {

const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentHref,
Expand Down Expand Up @@ -156,7 +184,7 @@ it("strips stale OAuth callback params from hosted current-page redirect URIs",
currentUrl.searchParams.set("message", "Known message");
currentUrl.searchParams.set("details", "{}");

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentUrl.toString(),
Expand All @@ -178,7 +206,7 @@ it("only treats hosted OAuth callback URLs as Stack callbacks when the matching
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: `${localRedirectUrl}/callback-page?code=oauth-code&state=oauth-state`,
Expand Down Expand Up @@ -221,7 +249,7 @@ it("does not await pending auth resolutions when post-callback redirect mints a
redirectBackUrl.searchParams.set("stack_cross_domain_auth", "1");
redirectBackUrl.searchParams.set("stack_cross_domain_state", "state");
redirectBackUrl.searchParams.set("stack_cross_domain_code_challenge", "challenge");
redirectBackUrl.searchParams.set("stack_cross_domain_after_callback_redirect_url", `${localRedirectUrl}/after`);
redirectBackUrl.searchParams.set("stack_cross_domain_after_callback_redirect_url", `https://${projectId}.example-stack-hosted.test/after`);
currentUrl.searchParams.set("after_auth_return_to", redirectBackUrl.toString());

const previousWindow = globalThis.window;
Expand All @@ -230,7 +258,7 @@ it("does not await pending auth resolutions when post-callback redirect mints a
.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl")
.mockResolvedValue(`https://${projectId}.example-stack-hosted.test/handler/final`);

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentUrl.toString(),
Expand Down Expand Up @@ -271,7 +299,7 @@ it("does not await pending auth resolutions when post-callback redirect adds nes

const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: `${localRedirectUrl}/callback-page`,
Expand Down Expand Up @@ -326,7 +354,7 @@ it("keeps cross-domain handoff working when top-level params are dropped before
.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl")
.mockResolvedValue(crossDomainAuthorizeRedirect);

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: hostedAfterSignInCallbackUrl.toString(),
Expand Down Expand Up @@ -382,7 +410,7 @@ it("keeps cross-domain handoff working when after_auth_return_to is rewritten to
.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl")
.mockResolvedValue(crossDomainAuthorizeRedirect);

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: hostedAfterSignInCallbackUrl.toString(),
Expand Down Expand Up @@ -423,7 +451,7 @@ it("adds nested cross-domain auth params when redirecting signed-in users to hos
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
let redirectedUrl = "";
globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentHref,
Expand Down Expand Up @@ -461,7 +489,7 @@ it("adds nested cross-domain auth params for other cross-domain handler redirect
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
let redirectedUrl = "";
globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentHref,
Expand Down Expand Up @@ -502,7 +530,7 @@ it("starts nested cross-domain auth from the target domain", async ({ expect })
codeChallenge: "nested-code-challenge",
});

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentHref,
Expand Down Expand Up @@ -554,7 +582,7 @@ it("continues nested cross-domain auth on the source domain", async ({ expect })
.mockResolvedValue(crossDomainRedirect);
vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(sourceRefreshTokenId);

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentUrl.toString(),
Expand Down Expand Up @@ -598,7 +626,7 @@ it("rejects nested cross-domain auth when the source redirect URI is untrusted",
const createCrossDomainAuthRedirectUrlSpy = vi.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl");
vi.spyOn(clientApp as any, "_isTrusted").mockResolvedValue(false);

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentUrl.toString(),
Expand Down Expand Up @@ -627,7 +655,7 @@ it("rejects nested cross-domain auth when the callback URL is untrusted", async
vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null);
vi.spyOn(clientApp as any, "_isTrusted").mockResolvedValue(false);

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentHref,
Expand Down Expand Up @@ -658,7 +686,7 @@ it("rejects nested cross-domain auth when the source session does not match", as
const createCrossDomainAuthRedirectUrlSpy = vi.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl");
vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue("different-source-session");

globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.document = createMockDocument();
globalThis.window = {
location: {
href: currentUrl.toString(),
Expand Down
13 changes: 13 additions & 0 deletions scripts/wait-for-dev-package-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import { setTimeout as sleep } from "timers/promises";
// This probe waits only for the package imports that the backend-side generator
// needs. It does not hide real runtime errors: we retry missing-module failures
// while package builds warm up, and fail immediately for other import failures.
//
// In addition to workspace packages, the probe checks that the generated Prisma
// client exists. When `turbo run dev` starts the backend, `codegen-prisma:watch`
// (`prisma generate --watch`) performs an initial generation that briefly removes
// and recreates `src/generated/prisma/`. If `codegen-docs` runs during that
// window it fails with ERR_MODULE_NOT_FOUND for `@/generated/prisma/client`.
const repoRoot = path.resolve(__dirname, "..");
const backendDir = path.join(repoRoot, "apps/backend");
const timeoutMs = 60_000;
Expand All @@ -35,6 +41,13 @@ const probeScript = `
(async () => {
await import('@stackframe/stack');
await import('@stackframe/stack-shared/dist/utils/env');
const { existsSync, readdirSync } = await import('node:fs');
const { join } = await import('node:path');
const generatedDir = join(process.cwd(), 'src', 'generated', 'prisma');
if (!existsSync(generatedDir) || readdirSync(generatedDir).length === 0) {
const err = new Error('ERR_MODULE_NOT_FOUND: Generated Prisma client not yet available at ' + generatedDir);
throw err;
}
})().then(
() => undefined,
(error) => {
Expand Down
Loading