Skip to content
Draft
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
40 changes: 40 additions & 0 deletions .github/workflows/deployed-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ jobs:
set -euo pipefail

PRODUCTION_BASE_URL="${DEPLOYMENT_STATUS_BASE_URL:-${PRODUCTION_BASE_URL_OVERRIDE:-${PRODUCTION_BASE_URL_VAR:-}}}"
PRODUCTION_CANONICAL_BASE_URL="${PRODUCTION_BASE_URL_OVERRIDE:-${PRODUCTION_BASE_URL_VAR:-}}"

if [ -z "$PRODUCTION_BASE_URL" ]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
Expand All @@ -176,6 +177,7 @@ jobs:

echo "enabled=true" >> "$GITHUB_OUTPUT"
echo "base_url=$PRODUCTION_BASE_URL" >> "$GITHUB_OUTPUT"
echo "canonical_base_url=$PRODUCTION_CANONICAL_BASE_URL" >> "$GITHUB_OUTPUT"

- name: Setup pnpm
if: steps.gate.outputs.enabled == 'true'
Expand Down Expand Up @@ -206,9 +208,42 @@ jobs:
PLAYWRIGHT_SKIP_WEBSERVERS: "true"
run: pnpm test:e2e:hosted:smoke

- name: Check production authenticated smoke configuration
if: steps.gate.outputs.enabled == 'true'
id: auth_gate
env:
PRODUCTION_AUTH_BASE_URL: ${{ steps.gate.outputs.canonical_base_url }}
PRODUCTION_AUTH_STORAGE_STATE_B64: ${{ secrets.VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64 }}
run: |
set -euo pipefail

if [ -z "$PRODUCTION_AUTH_BASE_URL" ] || [ -z "$PRODUCTION_AUTH_STORAGE_STATE_B64" ]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
{
echo "## Production authenticated account smoke"
echo "Skipped because VRDEX_PRODUCTION_SMOKE_BASE_URL or VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64 is not configured."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

echo "enabled=true" >> "$GITHUB_OUTPUT"

- name: Run production authenticated account smoke
if: steps.auth_gate.outputs.enabled == 'true'
id: auth_smoke
env:
PLAYWRIGHT_BASE_URL: ${{ steps.gate.outputs.canonical_base_url }}
PLAYWRIGHT_RECORD_VIDEO: "true"
PLAYWRIGHT_SKIP_WEBSERVERS: "true"
VRDEX_PRODUCTION_AUTH_SMOKE_PROVIDER: ${{ vars.VRDEX_PRODUCTION_AUTH_SMOKE_PROVIDER }}
VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64: ${{ secrets.VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64 }}
run: pnpm test:e2e:hosted:auth-smoke

- name: Write production smoke summary
if: always() && steps.gate.outputs.enabled == 'true'
env:
AUTH_SMOKE_OUTCOME: ${{ steps.auth_smoke.outcome }}
PRODUCTION_AUTH_BASE_URL: ${{ steps.gate.outputs.canonical_base_url }}
SMOKE_OUTCOME: ${{ steps.smoke.outcome }}
PRODUCTION_BASE_URL: ${{ steps.gate.outputs.base_url }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Expand All @@ -221,9 +256,14 @@ jobs:

Target: ${PRODUCTION_BASE_URL}

Authenticated account smoke: ${AUTH_SMOKE_OUTCOME:-skipped}

Authenticated account target: ${PRODUCTION_AUTH_BASE_URL:-not configured}

Captured flow:
- hosted public route smoke checks
- hosted deployment and server-status pages
- optional authenticated account readback with a pre-authenticated production test-account storage state
- no mutation-backed E2E helpers

Run: ${RUN_URL}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
AGENTS.local.md
.opencode/state/
.playwright-mcp/
node_modules/
.husky/_/
.convex-home/
Expand Down
56 changes: 56 additions & 0 deletions apps/web/e2e/production-auth.smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test, type BrowserContextOptions } from "@playwright/test";

type BrowserStorageState = Exclude<BrowserContextOptions["storageState"], string | undefined>;

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

function isStorageState(value: unknown): value is BrowserStorageState {
return isRecord(value) && Array.isArray(value.cookies) && Array.isArray(value.origins);
}

function readStorageState() {
const encoded = process.env.VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64?.trim();

if (!encoded) {
return undefined;
}

const decoded = Buffer.from(encoded, "base64").toString("utf8");
const parsed: unknown = JSON.parse(decoded);

if (!isStorageState(parsed)) {
throw new Error("VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64 must decode to a Playwright storageState JSON object.");
}

return parsed;
}

function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

const storageState = readStorageState();
const expectedProvider = process.env.VRDEX_PRODUCTION_AUTH_SMOKE_PROVIDER?.trim().toLowerCase();

test.describe("production authenticated account smoke @production-auth", () => {
test.skip(!process.env.PLAYWRIGHT_BASE_URL, "Production auth smoke is hosted-only.");
test.skip(!storageState, "Configure VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64 to enable production auth smoke.");
test.use(storageState ? { storageState } : {});

test("OAuth-backed account session renders account readiness", async ({ page }) => {
await page.goto("/account");
await expect(page.getByRole("heading", { name: "Not signed in" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Sign out" })).toBeVisible();
await expect(page.getByText("Linked providers", { exact: true })).toBeVisible();
await expect(page.getByText("No providers linked yet", { exact: true })).toHaveCount(0);

if (expectedProvider) {
await expect(page.getByText(new RegExp(`^${escapeRegExp(expectedProvider)}$`, "i"))).toBeVisible();
return;
}

await expect(page.getByText(/^(discord|google)$/i).first()).toBeVisible();
});
});
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"lint": "eslint",
"test:e2e": "playwright test --grep-invert \"@visual|@snapshot|@storybook\"",
"test:e2e:hosted": "playwright test --grep @flow --project=desktop-chromium",
"test:e2e:hosted:smoke": "playwright test --grep-invert \"@visual|@flow|@snapshot|@storybook\"",
"test:e2e:hosted:auth-smoke": "playwright test --grep @production-auth --project=desktop-chromium",
"test:e2e:hosted:smoke": "playwright test --grep-invert \"@visual|@flow|@snapshot|@storybook|@production-auth\"",
"test:e2e:snapshots": "playwright test --grep @snapshot --grep-invert @storybook",
"test:e2e:snapshots:update": "playwright test --grep @snapshot --grep-invert @storybook --update-snapshots",
"test:e2e:visual": "playwright test --grep @visual",
Expand Down
15 changes: 13 additions & 2 deletions docs/deployment/convex-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Staging HTTP Actions domain bootstrap, started 2026-06-15:
5. Add staging OAuth redirect URIs for both providers:
- `https://db.staging.vrdex.net/api/auth/callback/discord`
- `https://db.staging.vrdex.net/api/auth/callback/google`
6. Keep the legacy `https://scrupulous-corgi-247.convex.site/api/auth/callback/...` redirects until the custom callback host is verified in end-to-end sign-in.
6. Keep the legacy `https://scrupulous-corgi-247.convex.site/api/auth/callback/...` redirects until the custom callback host is verified in end-to-end sign-in and has stayed stable through scheduled health checks.
7. Override the staging deployment's `CONVEX_SITE_URL` to `https://db.staging.vrdex.net` in the Convex custom domain settings.
8. Rerun staging auth smoke checks from `https://staging.vrdex.net/sign-in`.

Expand All @@ -131,12 +131,23 @@ Production HTTP Actions domain bootstrap, completed 2026-06-16:
5. Add production OAuth redirect URIs for each configured provider:
- `https://db.vrdex.net/api/auth/callback/google`
- `https://db.vrdex.net/api/auth/callback/discord`
6. Keep the legacy `https://superb-pig-954.convex.site/api/auth/callback/...` redirects until the custom callback host is verified in end-to-end sign-in.
6. Keep the legacy `https://superb-pig-954.convex.site/api/auth/callback/...` redirects until the custom callback host is verified in end-to-end sign-in and has stayed stable through scheduled health checks.
7. Select `https://db.vrdex.net` as the production deployment's canonical `CONVEX_SITE_URL`.
8. Rerun production auth smoke checks from `https://vrdex.net/sign-in`.

Current production status: `db.vrdex.net` is configured and verified for the production deployment, Google and Discord allow the new callback URLs, production `CONVEX_SITE_URL` is selected as `https://db.vrdex.net`, and Google and Discord sign-in from `https://vrdex.net/sign-in` return to an authenticated `/account` session.

## Legacy OAuth Callback Retention

Current recommendation: keep the legacy `.convex.site` OAuth callback URLs configured as provider fallbacks for now.

- staging fallback callbacks: `https://scrupulous-corgi-247.convex.site/api/auth/callback/...`
- production fallback callbacks: `https://superb-pig-954.convex.site/api/auth/callback/...`

Do not remove those provider callback URLs in the same change that enables or documents a custom Convex Auth callback host. Revisit removal only after the custom hosts have stayed stable through scheduled deployed health checks and at least one manual OAuth callback recheck per provider after any Auth, Convex custom-domain, or provider-app change.

Earliest removal-review target: 2026-07-06, assuming production and staging auth health remains green.

## Notes

Resolved 2026-06-15: the duplicate Convex project `vrdex-85631` was deleted after confirming the active project is `vrdex`, the duplicate deployments had no env variables or custom domains, and their `profiles` tables were empty.
14 changes: 14 additions & 0 deletions docs/deployment/vercel-preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,20 @@ Current production auth status:
- Discord sign-in from `https://vrdex.net/sign-in` returns to an authenticated `https://vrdex.net/account` session.
- Convex production includes `JWT_PRIVATE_KEY` and matching `JWKS`, required for Convex Auth to mint web session cookies after OAuth callbacks.

### Production authenticated account smoke

The `Deployed Health Checks` workflow always runs a production read-only route smoke when `VRDEX_PRODUCTION_SMOKE_BASE_URL` or a deployment status URL is available. It can also run a gated authenticated account smoke when a safe production test-account path exists.

Repository settings for the authenticated lane:

- variable `VRDEX_PRODUCTION_SMOKE_BASE_URL=https://vrdex.net`: required so auth cookies target the stable public domain instead of a generated Vercel deployment URL
- optional variable `VRDEX_PRODUCTION_AUTH_SMOKE_PROVIDER`: expected linked provider, usually `discord` or `google`; if unset, the smoke accepts either provider
- secret `VRDEX_PRODUCTION_AUTH_SMOKE_STORAGE_STATE_B64`: base64-encoded Playwright `storageState` JSON from a dedicated production test account that has completed OAuth sign-in

This smoke does not enable production E2E helpers, does not mutate data, and does not store OAuth provider passwords in CI. It checks that the pre-authenticated production account can load `/account`, sees a sign-out control, and has at least one linked OAuth provider. Refresh the storage-state secret by manually signing in as the dedicated test account and exporting Playwright storage state before the session expires or after OAuth/provider/callback changes.

This lane validates the signed-in production account path and linked-provider rendering. It is not a full automated provider-login robot; provider credential entry and fresh OAuth consent should remain manual or use a provider-approved non-interactive test-account mechanism.

## Validation

The Vercel build runs `pnpm build:vercel`, which executes `apps/web/scripts/check-vercel-env.mjs` before `next build`.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"lint:web": "pnpm --filter web lint",
"test:e2e": "pnpm --filter web test:e2e",
"test:e2e:hosted": "pnpm --filter web test:e2e:hosted",
"test:e2e:hosted:auth-smoke": "pnpm --filter web test:e2e:hosted:auth-smoke",
"test:e2e:hosted:smoke": "pnpm --filter web test:e2e:hosted:smoke",
"test:e2e:snapshots": "pnpm --filter web test:e2e:snapshots",
"test:e2e:snapshots:update": "pnpm --filter web test:e2e:snapshots:update",
Expand Down