test(web-e2e): close the last failures — 36/36 against a local Neon stack#2511
Conversation
ghcr.io/timowilhelm/local-neon-http-proxy lets the @neondatabase/serverless HTTP and WS drivers talk to local Postgres on a single port. We add it alongside the existing wsproxy so the Playwright E2E setup (wrangler dev against db.localtest.me) shares the exact code paths Better Auth and Eden Treaty use in prod — no driver/adapter switching.
…outes Two pieces that together let `wrangler dev` work from the local web app: 1. `maybeConfigureLocalNeon` in the Worker entry flips `neonConfig.fetchEndpoint` (HTTP /sql) and `neonConfig.wsProxy` (WS /v2) at port 4444 when NEON_DATABASE_URL points at db.localtest.me. Combined with skipping the db.localtest.me host in src/db/index.ts' isStandardPostgresUrl check, every component (Better Auth + Eden Treaty + queue workers) routes through the same neon-http driver as prod. Eliminates the "Cannot perform I/O on behalf of a different request" hang from cross-request Pool WebSocket reuse. 2. CORS preflight handler for /api/auth/* — Better Auth's handler returns 404 on OPTIONS, so browser-based sign-in from a different origin (Expo web on :8082, API on :8787) never reached Better Auth. We mirror the Elysia CORS allowlist (packrat.world subdomains, localhost, exp://) and append Access-Control headers to Better Auth's response too.
Better Auth's `trustedOrigins` previously only allowed the API base URL
itself (`http://localhost:8787`), so sign-in calls from the Expo web app
at `http://localhost:8082` were rejected by the CSRF check.
Gate the extra entries on `env.BETTER_AUTH_URL.startsWith('http://localhost')`
so the trust list never widens in production. (Can't use ENVIRONMENT here —
its Zod default is 'production' when unset, which would dead-code this.)
…n sign in the E2E user Better Auth's email/password sign-in reads the password from `account.password` (with `providerId='credential'`), not `users.password_hash`. The 0042 data-migration copies users → account at migrate time, before any user exists; the E2E user is created *after* that migration runs, so it had no credential account row and sign-in returned 401 against a fresh local DB. This commit also captures the user id from the create branch so we don't re-query for it before inserting the account row.
…nAI embeddings)
Production populates `catalog_items` via the ETL workflow scraping product
pages. A fresh local DB has zero rows, which breaks any test that scrolls
the catalog tab, runs a similarity search, or adds an item from catalog
to a pack.
Ten hand-picked items spanning Sleep System / Shelter / Packs / Cooking /
Lighting / Water / Apparel give the catalog tab something to render and
make "sleeping bag" a meaningful search query. Embeddings are generated
once via OpenAI text-embedding-3-small (matches the schema's 1536-dim
vector column). Idempotent on SKU — safe to re-run.
Usage:
NEON_DATABASE_URL=... OPENAI_API_KEY=... \
bun run packages/api/src/db/seed-e2e-catalog.ts
…wright config - `channel: 'chrome'` so the suite runs against the system Chrome — Playwright's bundled chromium has no Ubuntu 26.04 build yet, so installing the default browser would fail on newer Linux dev boxes. - `--incognito` / `--no-default-browser-check` / `--no-first-run` / `--password-store=basic` make the browser explicitly ephemeral so it never reads from the developer's personal Chrome profile. - Headless follows env: `CI=true` or `PWHEADLESS=1` → headless; otherwise headed locally so you can watch the run. - `fullyParallel: true` and 4 workers (override with `PW_WORKERS`) — tests use timestamped names so parallel runs don't collide. - globalSetup uses the same chrome channel + headless toggle so its initial sign-in matches the test browser environment.
…for session - The settings screen renders `PackRat (Dev) v2.0.26` in dev/preview builds but `PackRat v2.0.26` in production. Widen the regex to accept an optional `(...)` environment marker so the same assertion works on every build variant. - The AI chat transport's Authorization header is sourced from `authClient.useSession()`, which returns null on first render and only populates after the first `/api/auth/get-session` response resolves and React commits the new state. The test was clicking Send before that cycle finished, sending `Bearer null` → 401. Wait for the session network call AND a 500ms React-settle before clicking.
…ton in LargeTitleHeader
Extends the existing patch. `LargeTitleHeader` accepts a `searchBar.testID`
prop and the comment in CatalogItemsScreen explicitly notes "testID exists
in runtime 2.0.5 implementation but absent from published types" — except
the published 2.0.3 implementation also doesn't forward it to the actual
icon Button on Android/web. The catalog-search Playwright test waits for
`getByTestId('catalog:search-btn')` to be visible and times out because the
testID is set on a JSX object that's never propagated to the DOM.
Adds `testID={props.searchBar.testID}` to the search-icon Button so the
testID lands on the rendered element. No-op on iOS (separate platform file)
and prod parity since the underlying NativeWindUI 2.0.5 has this fix too.
…n't Bearer null `useSession()` returns `data: null` on first render and only populates after the first GET /api/auth/get-session resolves AND React commits the new state. The chat transport captured `token` in its memoized closure, so the very first send went out as `Authorization: Bearer null` and the API responded 401 — useChat then showed the generic "something went wrong" banner and the test timed out waiting for an item response. `DefaultChatTransport.headers` accepts `Resolvable<Record<string, string>>`, which means a function/promise. Read the token from `authClient.getSession()` lazily at each send: getSession is cached after the first call so this is cheap, and we never capture `null` in the closure. Removes the matching playwright wait-for-session band-aid from core.spec.ts since the race no longer exists.
…is trusted
Replaces the fixed list (8081/8082/3000/19006) with a single wildcard so
parallel dev agents on bumped ports (e.g. 18082) don't need a one-by-one
allowlist update. Better Auth's trustedOrigins supports wildcard patterns
via the internal wildcardMatch helper.
Still gated on `env.BETTER_AUTH_URL.startsWith('http://localhost')` so prod
never widens trust.
Each host port in docker-compose.test.yml now reads from an env var with
the original value as the default — POSTGRES_TEST_HOST_PORT,
WSPROXY_HOST_PORT, NEON_PROXY_HOST_PORT.
Lets multiple dev agents (or two worktrees) on the same machine spin up
the test stack on different host ports without modifying the compose file
or colliding with each other. Default behaviour is unchanged.
Usage:
POSTGRES_TEST_HOST_PORT=15433 WSPROXY_HOST_PORT=15434 \
NEON_PROXY_HOST_PORT=14444 COMPOSE_PROJECT_NAME=foo \
docker compose -f docker-compose.test.yml up -d
…EADED=1 Previously the local default was headed so the developer could watch the run — but that pops Chrome windows on the desktop unexpectedly when an agent or background script triggers the suite. Invert the default: the suite is headless unless `PWHEADED=1` is explicitly set. Both playwright.config.ts and globalSetup.ts now share this gate.
…XY_PORT The repo's clean-checks pre-push hook flags raw `process.env.X` reads in non-bootstrap code. Three fixes: - Add `OPENAI_API_KEY` to nodeEnvSchema so seed-e2e-catalog.ts reads it via `nodeEnv.OPENAI_API_KEY` like every other Node/Bun script. - Add `NEON_LOCAL_PROXY_PORT` (optional) to the Worker's apiEnvSchema so the local-dev neon proxy port comes from the validated env binding, not from a raw `process.env` read inside the Worker entry. - Thread the proxy port from `e.NEON_LOCAL_PROXY_PORT` into `maybeConfigureLocalNeon` as an explicit argument.
|
Warning Review limit reached
More reviews will be available in 43 minutes and 42 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR establishes E2E testing infrastructure with local Neon proxy support and refactors AI chat transport authentication. Changes span mobile (chat auth), API (Worker routing, proxy config, seeding), database (URL classification, migrations), and Playwright test configuration. ChangesAI Chat Transport Session Auth
E2E Testing Infrastructure Setup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage Report for Expo Unit Tests Coverage (./apps/expo)
File CoverageNo changed files found. |
Coverage Report for API Unit Tests Coverage (./packages/api)
File CoverageNo changed files found. |
There was a problem hiding this comment.
Pull request overview
This PR stabilizes and unblocks the Expo web Playwright E2E suite against a fully local stack by adding local Neon proxy infrastructure, seeding required DB data, and fixing a couple of product issues surfaced by E2E (Better Auth CORS preflight + AI chat auth header timing).
Changes:
- Add a local Neon HTTP+WS proxy path for
wrangler dev, plus explicit CORS preflight handling for/api/auth/*. - Add/adjust DB seed scripts for E2E (credential account row + a small catalog dataset with embeddings).
- Update Playwright configuration to run headless by default and safely in parallel; fix AI chat transport to resolve auth headers per-request.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| patches/@PackRat-AI%2Fnativewindui@2.0.3.patch | Patch NativeWindUI LargeTitleHeader to propagate testID to the rendered button. |
| packages/env/src/node.ts | Add OPENAI_API_KEY to node/bun env schema for the new catalog seed script. |
| packages/api/src/utils/env-validation.ts | Add validated NEON_LOCAL_PROXY_PORT for worker local-neon proxy routing. |
| packages/api/src/index.ts | Configure Neon driver routing for local proxy + add /api/auth/* OPTIONS/CORS handling and CORS header propagation. |
| packages/api/src/db/seed-e2e-user.ts | Ensure seeded E2E user is sign-in-able by inserting/updating the Better Auth account credential row. |
| packages/api/src/db/seed-e2e-catalog.ts | New catalog seed script that generates embeddings and inserts a small, idempotent dataset. |
| packages/api/src/db/index.ts | Exclude db.localtest.me from “standard Postgres” routing so Workers stay on Neon driver paths in local dev. |
| packages/api/src/auth/index.ts | Expand Better Auth trustedOrigins to allow any http://localhost:* when running locally. |
| packages/api/docker-compose.test.yml | Add Neon local HTTP+WS proxy service and allow host-port overrides for parallel stacks. |
| bun.lock | Update dependency range for @packrat-ai/nativewindui (but currently inconsistent with overrides/patched deps). |
| apps/expo/playwright/tests/globalSetup.ts | Launch Chrome headless by default and add hardening flags to avoid using real Chrome profiles. |
| apps/expo/playwright/tests/core.spec.ts | Relax settings version text assertion to tolerate “(Dev)” build tags. |
| apps/expo/playwright/playwright.config.ts | Enable safe parallelism, configurable workers, default headless, and use installed Chrome channel. |
| apps/expo/app/(app)/ai-chat.tsx | Fix first-message 401 by resolving the Authorization header lazily via authClient.getSession(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| neon-proxy: | ||
| image: ghcr.io/timowilhelm/local-neon-http-proxy:main | ||
| environment: | ||
| PG_CONNECTION_STRING: postgres://test_user:test_password@postgres-test:5432/packrat_test | ||
| ports: | ||
| - "${NEON_PROXY_HOST_PORT:-4444}:4444" |
| // Better Auth does not implement CORS preflight (OPTIONS) responses, so | ||
| // we mirror the Elysia CORS allowlist here. Without this, browser-based | ||
| // sign-in calls from the web app (a different origin than the API) fail | ||
| // the preflight and never reach Better Auth. | ||
| const origin = request.headers.get('Origin'); | ||
| const isAllowedOrigin = | ||
| !!origin && | ||
| [ | ||
| /^https:\/\/(www\.)?packrat\.world$/, | ||
| /^https:\/\/[\w-]+\.packrat\.world$/, | ||
| /^https:\/\/[\w-]+\.packratai\.com$/, | ||
| /^https?:\/\/[\w-]+\.workers\.dev$/, | ||
| /^http:\/\/localhost:\d+$/, | ||
| /^exp:\/\//, | ||
| ].some((re) => re.test(origin)); | ||
|
|
| const isStandardPostgresUrl = (url: string) => { | ||
| try { | ||
| const u = new URL(url); | ||
| const host = u.hostname.toLowerCase(); | ||
| const isNeonTech = host === 'neon.tech' || host.endsWith('.neon.tech'); | ||
| const isNeonCom = host === 'neon.com' || host.endsWith('.neon.com'); | ||
| return u.protocol === 'postgres:' && !isNeonTech && !isNeonCom; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
patches/@PackRat-AI%2Fnativewindui@2.0.3.patch (1)
6-6:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid
//@ts-nocheck`` in patched TS/TSX sources.
patches/@packrat-ai%2Fnativewindui@2.0.3.patchadds//@ts-nocheckat lines 6, 15, and 24, disabling all type checking for the affected files. Remove it and address the actual type errors; if suppression is unavoidable, use a targeted `// `@ts-expect-erroron the specific failing line with a brief rationale.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@patches/`@packrat-ai%2Fnativewindui@2.0.3.patch at line 6, Remove the global suppression added by the token "// `@ts-nocheck`" in the patch file (present at the top of the patched source) and instead fix the underlying TypeScript errors: run the TypeScript build/tests to surface the compile errors, correct the offending types/usages, and only where a specific line cannot be typed safely add a narrowly scoped "// `@ts-expect-error` <short rationale>" immediately above that single failing statement. Target the exact "// `@ts-nocheck`" occurrences introduced by the patch and replace them with either proper type fixes or line-level "// `@ts-expect-error`" annotations with a brief rationale so type checking remains enabled elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/expo/app/`(app)/ai-chat.tsx:
- Around line 142-146: The headers async function using authClient.getSession
returns a union where the empty-object branch doesn't satisfy
Record<string,string>; fix by ensuring both branches return a
Record<string,string> for the headers in headers (the token variable comes from
data?.session?.token). Update the return so the empty branch is typed as a
Record<string,string> (e.g., return an explicitly typed empty record) or add an
explicit return type for headers that ensures {} is treated as
Record<string,string>, and keep the Authorization branch returning a
Record<string,string> with `Bearer ${token}`.
In `@apps/expo/playwright/tests/globalSetup.ts`:
- Around line 19-23: The setup creates a Playwright browser via chromium.launch
but doesn’t guarantee browser.close() runs if subsequent setup steps throw; wrap
the code that uses the returned browser in a try/finally (use the existing
browser variable from chromium.launch) and call await browser.close() in the
finally block so the browser is always closed, rethrow or propagate any caught
error after cleanup; apply the same try/finally pattern around any other setup
sequence that opens the browser and relies on browser.close() (the block
currently calling browser.close()).
In `@packages/api/src/db/seed-e2e-catalog.ts`:
- Around line 26-36: Extract the duplicated isStandardPostgresUrl function into
a shared helper module (e.g., create a new exported function
isStandardPostgresUrl in a db utils module) and replace the local
implementations in seed-e2e-catalog.ts and seed-e2e-user.ts (and the similar
logic in db/index.ts) with imports from that new module; ensure the new module
exports the function, update the import statements in the three files to use the
shared utility, and run a quick build/TS check to confirm no type or import
errors.
In `@packages/api/src/index.ts`:
- Around line 130-180: Extract the hard-coded regex array used to compute
isAllowedOrigin into a shared constant named CORS_ORIGIN_PATTERNS and import/use
it wherever CORS is configured (replace the inline array used to build
isAllowedOrigin in the auth handler and the Elysia CORS config), then compute
isAllowedOrigin via CORS_ORIGIN_PATTERNS.some(re => re.test(origin)) so both the
auth handler (code around request.headers.get('Origin') / isAllowedOrigin /
corsHeaders) and the main Elysia CORS setup reference the same source of truth.
In `@packages/env/src/node.ts`:
- Around line 76-77: The OPENAI_API_KEY env schema currently uses
z.string().min(1).optional(), which doesn't enforce the required sk- prefix;
update the validation for OPENAI_API_KEY to require the OpenAI secret prefix
(e.g. use z.string().min(1).startsWith('sk-') or an equivalent regex) so the
node schema matches the API schema's behavior and fails fast; modify the
OPENAI_API_KEY schema entry accordingly (preserving .optional() if intended).
---
Outside diff comments:
In `@patches/`@packrat-ai%2Fnativewindui@2.0.3.patch:
- Line 6: Remove the global suppression added by the token "// `@ts-nocheck`" in
the patch file (present at the top of the patched source) and instead fix the
underlying TypeScript errors: run the TypeScript build/tests to surface the
compile errors, correct the offending types/usages, and only where a specific
line cannot be typed safely add a narrowly scoped "// `@ts-expect-error` <short
rationale>" immediately above that single failing statement. Target the exact
"// `@ts-nocheck`" occurrences introduced by the patch and replace them with
either proper type fixes or line-level "// `@ts-expect-error`" annotations with a
brief rationale so type checking remains enabled elsewhere.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c5bb9e07-c422-42bb-be7a-96f3c83f6211
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock,!bun.lock
📒 Files selected for processing (13)
apps/expo/app/(app)/ai-chat.tsxapps/expo/playwright/playwright.config.tsapps/expo/playwright/tests/core.spec.tsapps/expo/playwright/tests/globalSetup.tspackages/api/docker-compose.test.ymlpackages/api/src/auth/index.tspackages/api/src/db/index.tspackages/api/src/db/seed-e2e-catalog.tspackages/api/src/db/seed-e2e-user.tspackages/api/src/index.tspackages/api/src/utils/env-validation.tspackages/env/src/node.tspatches/@packrat-ai%2Fnativewindui@2.0.3.patch
| const browser = await chromium.launch({ | ||
| channel: 'chrome', | ||
| headless: process.env.PWHEADED !== '1', | ||
| args: ['--incognito', '--no-default-browser-check', '--no-first-run', '--password-store=basic'], | ||
| }); |
There was a problem hiding this comment.
Ensure browser cleanup on setup failures.
If any step throws after chromium.launch (Line 19), browser.close() (Line 65) is skipped. Wrap setup steps in try/finally so the browser always closes.
Proposed fix
- const browser = await chromium.launch({
+ const browser = await chromium.launch({
channel: 'chrome',
headless: process.env.PWHEADED !== '1',
args: ['--incognito', '--no-default-browser-check', '--no-first-run', '--password-store=basic'],
});
- const context = await browser.newContext();
- const page = await context.newPage();
+ try {
+ const context = await browser.newContext();
+ const page = await context.newPage();
- // Start from the auth entry screen, then click through to login
- await page.goto(`${BASE_URL}/auth`, { waitUntil: 'load' });
+ // Start from the auth entry screen, then click through to login
+ await page.goto(`${BASE_URL}/auth`, { waitUntil: 'load' });
- // ... existing setup steps ...
+ // ... existing setup steps ...
- await context.storageState({ path: AUTH_STATE_PATH });
- console.log(`[globalSetup] Logged in as ${email}`);
-
- await browser.close();
+ await context.storageState({ path: AUTH_STATE_PATH });
+ console.log(`[globalSetup] Logged in as ${email}`);
+ } finally {
+ await browser.close();
+ }Also applies to: 65-65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/playwright/tests/globalSetup.ts` around lines 19 - 23, The setup
creates a Playwright browser via chromium.launch but doesn’t guarantee
browser.close() runs if subsequent setup steps throw; wrap the code that uses
the returned browser in a try/finally (use the existing browser variable from
chromium.launch) and call await browser.close() in the finally block so the
browser is always closed, rethrow or propagate any caught error after cleanup;
apply the same try/finally pattern around any other setup sequence that opens
the browser and relies on browser.close() (the block currently calling
browser.close()).
| const isStandardPostgresUrl = (url: string) => { | ||
| try { | ||
| const u = new URL(url); | ||
| const host = u.hostname.toLowerCase(); | ||
| const isNeonTech = host === 'neon.tech' || host.endsWith('.neon.tech'); | ||
| const isNeonCom = host === 'neon.com' || host.endsWith('.neon.com'); | ||
| return u.protocol === 'postgres:' && !isNeonTech && !isNeonCom; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Extract isStandardPostgresUrl to a shared utility.
This helper is duplicated verbatim in seed-e2e-user.ts and has similar logic in packages/api/src/db/index.ts. Extract to a common module (e.g., packages/api/src/db/utils.ts) to keep the seed scripts DRY.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/api/src/db/seed-e2e-catalog.ts` around lines 26 - 36, Extract the
duplicated isStandardPostgresUrl function into a shared helper module (e.g.,
create a new exported function isStandardPostgresUrl in a db utils module) and
replace the local implementations in seed-e2e-catalog.ts and seed-e2e-user.ts
(and the similar logic in db/index.ts) with imports from that new module; ensure
the new module exports the function, update the import statements in the three
files to use the shared utility, and run a quick build/TS check to confirm no
type or import errors.
| // Better Auth does not implement CORS preflight (OPTIONS) responses, so | ||
| // we mirror the Elysia CORS allowlist here. Without this, browser-based | ||
| // sign-in calls from the web app (a different origin than the API) fail | ||
| // the preflight and never reach Better Auth. | ||
| const origin = request.headers.get('Origin'); | ||
| const isAllowedOrigin = | ||
| !!origin && | ||
| [ | ||
| /^https:\/\/(www\.)?packrat\.world$/, | ||
| /^https:\/\/[\w-]+\.packrat\.world$/, | ||
| /^https:\/\/[\w-]+\.packratai\.com$/, | ||
| /^https?:\/\/[\w-]+\.workers\.dev$/, | ||
| /^http:\/\/localhost:\d+$/, | ||
| /^exp:\/\//, | ||
| ].some((re) => re.test(origin)); | ||
|
|
||
| const corsHeaders: Record<string, string> = isAllowedOrigin | ||
| ? { | ||
| 'Access-Control-Allow-Origin': origin, | ||
| 'Access-Control-Allow-Credentials': 'true', | ||
| Vary: 'Origin', | ||
| } | ||
| : {}; | ||
|
|
||
| if (request.method === 'OPTIONS') { | ||
| return new Response(null, { | ||
| status: 204, | ||
| headers: { | ||
| ...corsHeaders, | ||
| 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS', | ||
| 'Access-Control-Allow-Headers': | ||
| request.headers.get('Access-Control-Request-Headers') ?? | ||
| 'Content-Type, Authorization, X-API-Key', | ||
| 'Access-Control-Max-Age': '86400', | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| const validatedEnv = getEnv(); | ||
| const auth = await getAuth(validatedEnv); | ||
| return auth.handler(request); | ||
| const authResponse = await auth.handler(request); | ||
| if (!isAllowedOrigin) return authResponse; | ||
| // Copy Better Auth's response and append CORS headers so cookies/JSON | ||
| // payloads reach the cross-origin caller. | ||
| const headers = new Headers(authResponse.headers); | ||
| for (const [k, v] of Object.entries(corsHeaders)) headers.set(k, v); | ||
| return new Response(authResponse.body, { | ||
| status: authResponse.status, | ||
| statusText: authResponse.statusText, | ||
| headers, | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
CORS allowlists diverge between Elysia and auth handler.
The auth handler adds patterns (packratai.com, workers.dev) not present in the Elysia CORS config (lines 63-69). If these origins need auth access, they likely need access to other API routes too. This duplication will drift over time.
Extract the allowlist to a shared constant.
♻️ Proposed fix
+const CORS_ORIGIN_PATTERNS = [
+ /^https:\/\/(www\.)?packrat\.world$/,
+ /^https:\/\/[\w-]+\.packrat\.world$/,
+ /^https:\/\/[\w-]+\.packratai\.com$/,
+ /^https?:\/\/[\w-]+\.workers\.dev$/,
+ /^http:\/\/localhost:\d+$/,
+ /^exp:\/\//,
+];
+
export const app = new Elysia({ adapter: CloudflareAdapter })
.use(
cors({
credentials: true,
origin: (request) => {
const origin = request.headers.get('Origin');
if (!origin) return false;
- const allowed = [
- /^https:\/\/(www\.)?packrat\.world$/,
- /^https:\/\/[\w-]+\.packrat\.world$/,
- /^http:\/\/localhost:\d+$/,
- /^exp:\/\//,
- ];
- return allowed.some((re) => re.test(origin));
+ return CORS_ORIGIN_PATTERNS.some((re) => re.test(origin));
},Then use CORS_ORIGIN_PATTERNS in the auth handler too (line 137).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/api/src/index.ts` around lines 130 - 180, Extract the hard-coded
regex array used to compute isAllowedOrigin into a shared constant named
CORS_ORIGIN_PATTERNS and import/use it wherever CORS is configured (replace the
inline array used to build isAllowedOrigin in the auth handler and the Elysia
CORS config), then compute isAllowedOrigin via CORS_ORIGIN_PATTERNS.some(re =>
re.test(origin)) so both the auth handler (code around
request.headers.get('Origin') / isAllowedOrigin / corsHeaders) and the main
Elysia CORS setup reference the same source of truth.
| // ── OpenAI (packages/api/src/db/seed-e2e-catalog.ts) ────────────── | ||
| OPENAI_API_KEY: z.string().min(1).optional(), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Consider adding sk- prefix validation for consistency.
The API env schema (packages/api/src/utils/env-validation.ts:49) validates OPENAI_API_KEY with .startsWith('sk-'). This node schema only checks min(1). Misconfigurations will fail later at the OpenAI API rather than at parse time.
♻️ Suggested change
- OPENAI_API_KEY: z.string().min(1).optional(),
+ OPENAI_API_KEY: z.string().startsWith('sk-').optional(),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ── OpenAI (packages/api/src/db/seed-e2e-catalog.ts) ────────────── | |
| OPENAI_API_KEY: z.string().min(1).optional(), | |
| // ── OpenAI (packages/api/src/db/seed-e2e-catalog.ts) ────────────── | |
| OPENAI_API_KEY: z.string().startsWith('sk-').optional(), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/env/src/node.ts` around lines 76 - 77, The OPENAI_API_KEY env schema
currently uses z.string().min(1).optional(), which doesn't enforce the required
sk- prefix; update the validation for OPENAI_API_KEY to require the OpenAI
secret prefix (e.g. use z.string().min(1).startsWith('sk-') or an equivalent
regex) so the node schema matches the API schema's behavior and fails fast;
modify the OPENAI_API_KEY schema entry accordingly (preserving .optional() if
intended).
… string>
CI's tsc caught a union-vs-index-signature issue from the previous commit's
conditional return (`token ? {Authorization: …} : {}`) — the empty-object
branch isn't assignable to `Record<string, string>` because TypeScript
can't infer an index signature from a literal `{}`.
Build the object explicitly so the inferred return is a single concrete
`Record<string, string>`.
…al patches
Upstream `@packrat-ai/nativewindui@2.0.6` ships everything our local patch
was working around:
- LargeTitleHeader: `testID={props.searchBar.testID}` on the search-icon
Button (the catalog-search testID fix we added in PR #2511).
- Type-only fixes from the v2.0.5 type-issue cleanup
(`chore: fix type issues` + `🏷️ fix all TypeScript errors after fresh
component sync`), which made the `@ts-nocheck` hunks on
AdaptiveSearchHeader / Icon/types / LargeTitleHeader obsolete.
Updates:
- `packages/ui/package.json` was already at `^2.0.6`; bump the root
`overrides`, root direct dep, and the locked version in bun.lock so
installs actually pick up 2.0.6 instead of being pinned to 2.0.3.
- Delete `patches/@PackRat-AI%2Fnativewindui@2.0.3.patch` and remove the
`patchedDependencies` entry — no patches needed against 2.0.6.
Verified:
- `bun install` resolves to 2.0.6 cleanly.
- `tsc --noEmit` clean against `apps/expo` (only the pre-existing `uuid`
type-def error remains, unrelated).
- Playwright web E2E suite is 36/36 green against the bumped version on
the same local Neon stack from PR #2511.
…al patches
Upstream `@packrat-ai/nativewindui@2.0.6` ships everything our local patch
was working around:
- LargeTitleHeader: `testID={props.searchBar.testID}` on the search-icon
Button (the catalog-search testID fix we added in PR #2511).
- Type-only fixes from the v2.0.5 type-issue cleanup
(`chore: fix type issues` + `🏷️ fix all TypeScript errors after fresh
component sync`), which made the `@ts-nocheck` hunks on
AdaptiveSearchHeader / Icon/types / LargeTitleHeader obsolete.
Updates:
- `packages/ui/package.json` was already at `^2.0.6`; bump the root
`overrides`, root direct dep, and the locked version in bun.lock so
installs actually pick up 2.0.6 instead of being pinned to 2.0.3.
- Delete `patches/@PackRat-AI%2Fnativewindui@2.0.3.patch` and remove the
`patchedDependencies` entry — no patches needed against 2.0.6.
Verified:
- `bun install` resolves to 2.0.6 cleanly.
- `tsc --noEmit` clean against `apps/expo` (only the pre-existing `uuid`
type-def error remains, unrelated).
- Playwright web E2E suite is 36/36 green against the bumped version on
the same local Neon stack from PR #2511.
…cal patches
Upstream `@packrat-ai/nativewindui@2.0.6` ships everything the local patch
was working around:
- LargeTitleHeader: `testID={props.searchBar.testID}` on the search-icon
Button (the catalog-search testID fix we added in PR #2511).
- Type fixes from `chore: fix type issues` + `🏷️ fix all TypeScript errors
after fresh component sync` that made the local `@ts-nocheck` hunks on
AdaptiveSearchHeader / Icon/types / LargeTitleHeader obsolete.
Changes:
- `packages/ui/package.json` is already the canonical declarer at `^2.0.6`.
- **Drop the root `overrides` pin** for nativewindui. It was added in
`8a558939e` to freeze us at 2.0.3 because 2.0.6 had a type regression
against react-native 0.81 autocapitalize types. We're on RN 0.83.6 now,
the regression no longer reproduces, and overrides should be reserved
for emergency pins, not normal version management. With the override
gone, the `^2.0.6` range in `packages/ui` is the single source of
truth — future releases bump naturally without touching the root.
- Empty `patchedDependencies` block and delete the patch file — nothing
to patch against 2.0.6.
Verified:
- `bun install` resolves to 2.0.6 cleanly with no root override.
- `bunx tsc --noEmit` clean on `apps/expo` (only the pre-existing `uuid`
type-def error remains, unrelated).
- Playwright web E2E suite is 36/36 green against 2.0.6 with no patches.
Resolution strategy (per fly-on-the-wall rule — development's bug fixes win
for everything except the e2e test infrastructure itself):
* App code conflicts (ai-chat.tsx, _layout.tsx, one-time-password.tsx,
UserAvatar.tsx, pack/trip components, hooks, trips route): take theirs.
* packages/api/src/db/seed-e2e-user.ts: take theirs — uses accountId=email
(better-auth's actual lookup key) and proper .onConflictDoUpdate().
* .github/workflows/web-e2e-tests.yml: take theirs — adds e2e-gate job,
smart path filters, and a 'seed E2E user' step.
* apps/expo/playwright/{playwright.config.ts,tests/*.ts}: take ours.
Development's parallel attempt still uses retired /api/auth/login etc;
ours is the working 36/36 Better Auth suite from #2511.
* Root package.json: keep our #2528 cleanup — drop the
'@packrat-ai/nativewindui: 2.0.3-2' override block, keep
'patchedDependencies: {}'.
* packages/ui/package.json: take ours (^2.0.6).
* bun.lock: regenerated via 'bun install'.
* packages/api/src/db/seed-e2e-catalog.ts: switch import from local
'./schema' to '@packrat/db/schema' — development moved the schema
into the @packrat/db workspace.
Verified:
* bun test:expo — 357/357
* bun test:api:unit — 328/328
* bun check-types — 0 errors
* @packrat-ai/nativewindui resolves to 2.0.6
Summary
Brings the Playwright web suite from 0/36 (couldn't even start against this branch's setup) to 36/36 running headless on a fully local stack — and unblocks running it from
wrangler devagainst a local Postgres at all.Sits on top of
feat/web-e2e-fixso the diff is focused on what's new: local-dev infrastructure, two genuine product bugs surfaced by E2E, and a few test polish items.What's in here (in commit order)
Local Neon stack for
wrangler devdocker-compose.test.ymladdsghcr.io/timowilhelm/local-neon-http-proxyon port 4444 alongside the existing wsproxy. Lets@neondatabase/serverless(the HTTP driver Better Auth uses, plus the WS Pool) talk to the local pg without any driver/adapter switch. No code change toauth/index.ts's DB connection.neonConfig.fetchEndpoint+wsProxywhenNEON_DATABASE_URLhost isdb.localtest.me, andsrc/db/index.tsexcludes that host from the standard-pg routing so every component (Better Auth + Eden Treaty + queue workers) stays on the sameneon-httpdriver path as prod. Also adds CORS preflight handling for/api/auth/*— Better Auth's handler returns 404 onOPTIONS, so browser-based sign-in from a different origin never reached it.trustedOriginsacceptshttp://localhost:*(gated onBETTER_AUTH_URL.startsWith('http://localhost')), so any local port works for parallel dev agents without an allowlist update.docker-compose.test.ymlhost ports are env-var overrideable (POSTGRES_TEST_HOST_PORT,WSPROXY_HOST_PORT,NEON_PROXY_HOST_PORT) — defaults unchanged. Lets two worktrees / two agents bring up the stack on the same box without colliding.Seed fixes
seed-e2e-user.tsnow also inserts theaccountcredential row. Better Auth's email/password flow reads the password fromaccount.password, notusers.password_hash. Migration0042_migrate_auth_datacopies users → account at migrate time — before the E2E user exists — so the seeded user was silently un-signable against a fresh local DB.seed-e2e-catalog.ts— 10 hand-picked items spanning Sleep System / Shelter / Packs / Cooking / Lighting / Water / Apparel, embeddings via OpenAItext-embedding-3-small, idempotent on SKU. Production populatescatalog_itemsvia the ETL workflow; a fresh local DB has zero rows, which broke every catalog test.Two real product bugs surfaced by the E2E pass
LargeTitleHeader—searchBar.testIDwas set on the JSX prop object but never propagated to the rendered icon Button. Patched inpatches/@packrat-ai%2Fnativewindui@2.0.3.patchso the testID lands on the actual DOM element. Worth a parallel upstream PR (see Follow-ups).Authorizationheader was captured fromauthClient.useSession()data at transport-build time. On first render that'snull→ the very first send went out asBearer null→ 401 → useChat showed the generic "something went wrong" banner. Switched toDefaultChatTransport'sResolvable<Record<string, string>>form: read the token fromauthClient.getSession()lazily at each send (it's cached on native via SecureStore, on web via Better Auth's session atom).Test polish
channel: 'chrome'(Playwright's bundled chromium has no Ubuntu 26.04 build yet),--incognito+--no-default-browser-check+--no-first-run+--password-store=basicso the test browser never reads from the dev's real Chrome profile,fullyParallel: true, 4 workers (override viaPW_WORKERS).PWHEADED=1. Previously local dev was headed by default which pops Chrome windows on the dev's desktop unexpectedly.settings screen loadsregex tolerates the(Dev)build tag — production builds renderPackRat v…, dev/preview renderPackRat (Dev) v….OPENAI_API_KEYinnodeEnvSchemafor the catalog seed;NEON_LOCAL_PROXY_PORT(optional) inapiEnvSchemaso the local proxy port comes from the validated Worker env, not rawprocess.env.Test plan
Verified locally:
bun test:expo— 326/326 passbun test:api:unit— 215/215 passbunx tsc --noEmitinapps/expo— no new errors (1 pre-existinguuidtype def issue)/sqlendpoint).maestro/**flow files touched; only runtime change inapps/expois the chat transport, which uses the sameauthClientAPI on iOS/AndroidLocal quickstart
For a parallel agent / second worktree, bump ports:
Follow-ups (not in this PR)
PackRat-AI/nativewinduifor thesearchBar.testIDpropagation fix so the local patch can be deleted on next release.useSession()call is left in place purely to keep the session-fetch on mount; consider removing it once the call-side migration is complete elsewhere.Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores