Skip to content

Commit 5e5c0ae

Browse files
committed
merge dev
2 parents d6397fa + b0e7706 commit 5e5c0ae

File tree

27 files changed

+2212
-136
lines changed

27 files changed

+2212
-136
lines changed

.claude/CLAUDE-KNOWLEDGE.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# CLAUDE-KNOWLEDGE.md
2+
3+
This file documents key learnings from implementing wildcard domain support in Stack Auth, organized in Q&A format.
4+
5+
## OAuth Flow and Validation
6+
7+
### Q: Where does OAuth redirect URL validation happen in the flow?
8+
A: The validation happens in the callback endpoint (`/api/v1/auth/oauth/callback/[provider_id]/route.tsx`), not in the authorize endpoint. The authorize endpoint just stores the redirect URL and redirects to the OAuth provider. The actual validation occurs when the OAuth provider calls back, and the oauth2-server library validates the redirect URL.
9+
10+
### Q: How do you test OAuth flows that should fail?
11+
A: Use `Auth.OAuth.getMaybeFailingAuthorizationCode()` instead of `Auth.OAuth.getAuthorizationCode()`. The latter expects success (status 303), while the former allows you to test failure cases. The failure happens at the callback stage with a 400 status and specific error message.
12+
13+
### Q: What error is thrown for invalid redirect URLs in OAuth?
14+
A: The callback endpoint returns a 400 status with the message: "Invalid redirect URI. The URL you are trying to redirect to is not trusted. If it should be, add it to the list of trusted domains in the Stack Auth dashboard."
15+
16+
## Wildcard Pattern Implementation
17+
18+
### Q: How do you handle ** vs * precedence in regex patterns?
19+
A: Use a placeholder approach to prevent ** from being corrupted when replacing *:
20+
```typescript
21+
const doubleWildcardPlaceholder = '\x00DOUBLE_WILDCARD\x00';
22+
regexPattern = regexPattern.replace(/\*\*/g, doubleWildcardPlaceholder);
23+
regexPattern = regexPattern.replace(/\*/g, '[^.]*');
24+
regexPattern = regexPattern.replace(new RegExp(doubleWildcardPlaceholder, 'g'), '.*');
25+
```
26+
27+
### Q: Why can't you use `new URL()` with wildcard domains?
28+
A: Wildcard characters (* and **) are not valid in URLs and will cause parsing errors. For wildcard domains, you need to manually parse the URL components instead of using the URL constructor.
29+
30+
### Q: How do you validate URLs with wildcards?
31+
A: Extract the hostname pattern manually and use `matchHostnamePattern()`:
32+
```typescript
33+
const protocolEnd = domain.baseUrl.indexOf('://');
34+
const protocol = domain.baseUrl.substring(0, protocolEnd + 3);
35+
const afterProtocol = domain.baseUrl.substring(protocolEnd + 3);
36+
const pathStart = afterProtocol.indexOf('/');
37+
const hostnamePattern = pathStart === -1 ? afterProtocol : afterProtocol.substring(0, pathStart);
38+
```
39+
40+
## Testing Best Practices
41+
42+
### Q: How should you run multiple independent test commands?
43+
A: Use parallel execution by batching tool calls together:
44+
```typescript
45+
// Good - runs in parallel
46+
const [result1, result2] = await Promise.all([
47+
niceBackendFetch("/endpoint1"),
48+
niceBackendFetch("/endpoint2")
49+
]);
50+
51+
// In E2E tests, the framework handles this automatically when you
52+
// batch multiple tool calls in a single response
53+
```
54+
55+
### Q: What's the correct way to update project configuration in E2E tests?
56+
A: Use the `/api/v1/internal/config/override` endpoint with PATCH method and admin access token:
57+
```typescript
58+
await niceBackendFetch("/api/v1/internal/config/override", {
59+
method: "PATCH",
60+
accessType: "admin",
61+
headers: {
62+
'x-stack-admin-access-token': adminAccessToken,
63+
},
64+
body: {
65+
config_override_string: JSON.stringify({
66+
'domains.trustedDomains.name': { baseUrl: '...', handlerPath: '...' }
67+
}),
68+
},
69+
});
70+
```
71+
72+
## Code Organization
73+
74+
### Q: Where does domain validation logic belong?
75+
A: Core validation functions (`isValidHostnameWithWildcards`, `matchHostnamePattern`) belong in the shared utils package (`packages/stack-shared/src/utils/urls.tsx`) so they can be used by both frontend and backend.
76+
77+
### Q: How do you simplify validation logic with wildcards?
78+
A: Replace wildcards with valid placeholders before validation:
79+
```typescript
80+
const normalizedDomain = domain.replace(/\*+/g, 'wildcard-placeholder');
81+
url = new URL(normalizedDomain); // Now this won't throw
82+
```
83+
84+
## Debugging E2E Tests
85+
86+
### Q: What does "ECONNREFUSED" mean in E2E tests?
87+
A: The backend server isn't running. Make sure to start the backend with `pnpm dev` before running E2E tests.
88+
89+
### Q: How do you debug which stage of OAuth flow is failing?
90+
A: Check the error location:
91+
- Authorize endpoint (307 redirect) - Initial request succeeded
92+
- Callback endpoint (400 error) - Validation failed during callback
93+
- Token endpoint (400 error) - Validation failed during token exchange
94+
95+
## Git and Development Workflow
96+
97+
### Q: How should you format git commit messages in this project?
98+
A: Use a HEREDOC to ensure proper formatting:
99+
```bash
100+
git commit -m "$(cat <<'EOF'
101+
Commit message here.
102+
103+
🤖 Generated with [Claude Code](https://claude.ai/code)
104+
105+
Co-Authored-By: Claude <noreply@anthropic.com>
106+
EOF
107+
)"
108+
```
109+
110+
### Q: What commands should you run before considering a task complete?
111+
A: Always run:
112+
1. `pnpm test run <relevant-test-files>` - Run tests
113+
2. `pnpm lint` - Check for linting errors
114+
3. `pnpm typecheck` - Check for TypeScript errors
115+
116+
## Common Pitfalls
117+
118+
### Q: Why might imports get removed after running lint --fix?
119+
A: ESLint may remove "unused" imports. Always verify your changes after auto-fixing, especially if you're using imports in a way ESLint doesn't recognize (like in test expectations).
120+
121+
### Q: What's a common linting error in test files?
122+
A: Missing newline at end of file. ESLint requires files to end with a newline character.
123+
124+
### Q: How do you handle TypeScript errors about missing exports?
125+
A: Double-check that you're only importing what's actually exported from a module. The error "Module declares 'X' locally, but it is not exported" means you're trying to import something that isn't exported.

.claude/settings.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
3+
"permissions": {
4+
"allow": [
5+
"Bash(pnpm typecheck:*)",
6+
"Bash(pnpm test:*)",
7+
"Bash(pnpm build:*)",
8+
"Bash(pnpm lint:*)",
9+
"Bash(find:*)",
10+
"Bash(ls:*)",
11+
"Bash(pnpm codegen)",
12+
"Bash(pnpm vitest run:*)",
13+
"Bash(pnpm eslint:*)"
14+
],
15+
"deny": []
16+
},
17+
"includeCoAuthoredBy": false,
18+
"hooks": {
19+
"PostToolUse": [
20+
{
21+
"matcher": "Edit|MultiEdit|Write",
22+
"hooks": [
23+
{
24+
"type": "command",
25+
"command": "jq -r '.tool_input.file_path' | { read file_path; if [[ \"$file_path\" =~ \\.(js|jsx|ts|tsx)$ ]]; then pnpm run lint --fix \"$file_path\" || true; fi }"
26+
}
27+
]
28+
}
29+
],
30+
"Stop": [
31+
{
32+
"hooks": [
33+
{
34+
"type": "command",
35+
"command": "(pnpm run typecheck 1>&2 || exit 2) && (pnpm run lint 1>&2 || exit 2)"
36+
}
37+
]
38+
}
39+
]
40+
}
41+
}

.github/recurseml-rules/code_patterns.mdc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ The following conventions MUST be followed in new code.
88
DON'T report code patterns outside of the examples explicitly listed below:
99

1010
- Never use `void asyncFunction()` or `asyncFunction().catch(console.error)` - use `runAsynchronously(asyncFunction)` instead
11-
- Use `parseJson`/`stringifyJson` from `stack-shared/utils/json` instead of `JSON.parse`/`JSON.stringify`
1211
- Instead of Vercel `waitUntil`, use `runAsynchronously(promise, { promiseCallback: waitUntil })`
1312
- Don't concatenate URLs as strings - avoid patterns like `/users/${userId}`
1413
- Replace non-null assertions with `?? throwErr("message", { extraData })` pattern

.github/workflows/claude-code-review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Claude Code Review
22

33
on:
44
pull_request:
5-
types: [opened]
5+
types: [ready_for_review]
66
# Optional: Only run on specific file changes
77
# paths:
88
# - "src/**/*.ts"

CLAUDE.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
### Essential Commands
88
- **Install dependencies**: `pnpm install`
9+
- **Run tests**: `pnpm test run` (uses Vitest). You can filter with `pnpm test run <file-filters>`. The `run` is important to not trigger watch mode
10+
- **Lint code**: `pnpm lint`. `pnpm lint --fix` will fix some of the linting errors, prefer that over fixing them manually.
11+
- **Type check**: `pnpm typecheck`
12+
13+
#### Extra commands
14+
These commands are usually already called by the user, but you can remind them to run it for you if they forgot to.
915
- **Build packages**: `pnpm build:packages`
1016
- **Generate code**: `pnpm codegen`
1117
- **Start dependencies**: `pnpm restart-deps` (resets & restarts Docker containers for DB, Inbucket, etc. Usually already called by the user)
1218
- **Run development**: `pnpm dev` (starts all services on different ports. Usually already started by the user in the background)
1319
- **Run minimal dev**: `pnpm dev:basic` (only backend and dashboard for resource-limited systems)
14-
- **Run tests**: `pnpm test --no-watch` (uses Vitest). You can filter with `pnpm test --no-watch <file-filters>`
15-
- **Lint code**: `pnpm lint`
16-
- **Type check**: `pnpm typecheck`
1720

1821
### Testing
19-
- **Run all tests**: `pnpm test --no-watch`
20-
- **Run some tests**: `pnpm test --no-watch <file-filters>`
22+
You should ALWAYS add new E2E tests when you change the API or SDK interface. Generally, err on the side of creating too many tests; it is super important that our codebase is well-tested, due to the nature of the industry we're building in.
23+
- **Run all tests**: `pnpm test run`
24+
- **Run some tests**: `pnpm test run <file-filters>`
2125

2226
### Database Commands
2327
- **Generate migration**: `pnpm db:migration-gen`
@@ -62,15 +66,15 @@ The API follows a RESTful design with routes organized by resource type:
6266
- OAuth providers: `/api/latest/oauth-providers/*`
6367

6468
### Development Ports
65-
- 8100: Dev launchpad
66-
- 8101: Dashboard
67-
- 8102: Backend API
68-
- 8103: Demo app
69-
- 8104: Documentation
70-
- 8105: Inbucket (email testing)
71-
- 8106: Prisma Studio
69+
To see all development ports, refer to the index.html of `apps/dev-launchpad/public/index.html`.
7270

7371
## Important Notes
7472
- Environment variables are pre-configured in `.env.development` files
75-
- Code generation (`pnpm codegen`) must be run after schema changes
73+
- Always run typecheck, lint, and test to make sure your changes are working as expected. You can save time by only linting and testing the files you've changed (and/or related E2E tests).
7674
- The project uses a custom route handler system in the backend for consistent API responses
75+
- Sometimes, the typecheck will give errors along the line of "Cannot assign Buffer to Uint8Array" or similar, on changes that are completely unrelated to your own changes. If that happens, tell the user to run `pnpm clean && pnpm i && pnpm run codegen && pnpm build:packages`, and restart the dev server (you cannot run this yourself). After that's done, the typecheck should pass.
76+
- When writing tests, prefer .toMatchInlineSnapshot over other selectors, if possible. You can check (and modify) the snapshot-serializer.ts file to see how the snapshots are formatted and how non-deterministic values are handled.
77+
- Whenever you learn something new, or at the latest right before you call the `Stop` tool, write whatever you learned into the ./claude/CLAUDE-KNOWLEDGE.md file, in the Q&A format in there. You will later be able to look up knowledge from there (based on the question you asked).
78+
79+
### Code-related
80+
- Use ES6 maps instead of records wherever you can.

apps/backend/.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ STACK_ACCESS_TOKEN_EXPIRATION_TIME=30s
3939
STACK_SVIX_SERVER_URL=http://localhost:8113
4040
STACK_SVIX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk
4141

42-
STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS=50
42+
STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS=500
4343

4444
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING=yes
4545

apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ const handler = createSmartRouteHandler({
398398
} catch (error) {
399399
if (error instanceof InvalidClientError) {
400400
if (error.message.includes("redirect_uri") || error.message.includes("redirectUri")) {
401+
console.log("User is trying to authorize OAuth with an invalid redirect URI", error, { redirectUri: oauthRequest.query?.redirect_uri, clientId: oauthRequest.query?.client_id });
401402
throw new KnownErrors.RedirectUrlNotWhitelisted();
402403
}
403404
} else if (error instanceof InvalidScopeError) {

apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { validateRedirectUrl } from "@/lib/redirect-urls";
12
import { getPrismaClientForTenancy, retryTransaction } from "@/prisma-client";
23
import { createVerificationCodeHandler } from "@/route-handlers/verification-code-handler";
34
import { VerificationCodeType } from "@prisma/client";
@@ -50,35 +51,16 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({
5051
}
5152

5253
// HACK: we validate origin and rpid outside of simpleauth, this should be replaced once we have a primary authentication domain
53-
54-
let expectedRPID = "";
55-
let expectedOrigin = "";
5654
const clientDataJSON = decodeClientDataJSON(credential.response.clientDataJSON);
5755
const { origin } = clientDataJSON;
58-
const localhostAllowed = tenancy.config.domains.allowLocalhost;
59-
const parsedOrigin = new URL(origin);
60-
const isLocalhost = parsedOrigin.hostname === "localhost";
61-
62-
if (!localhostAllowed && isLocalhost) {
63-
throw new KnownErrors.PasskeyAuthenticationFailed("Passkey registration failed because localhost is not allowed");
64-
}
6556

66-
if (localhostAllowed && isLocalhost) {
67-
expectedRPID = parsedOrigin.hostname;
68-
expectedOrigin = origin;
57+
if (!validateRedirectUrl(origin, tenancy)) {
58+
throw new KnownErrors.PasskeyRegistrationFailed("Passkey registration failed because the origin is not allowed");
6959
}
7060

71-
if (!isLocalhost) {
72-
if (!Object.values(tenancy.config.domains.trustedDomains)
73-
.filter(e => e.baseUrl)
74-
.map(e => e.baseUrl)
75-
.includes(parsedOrigin.origin)) {
76-
throw new KnownErrors.PasskeyAuthenticationFailed("Passkey registration failed because the origin is not allowed");
77-
} else {
78-
expectedRPID = parsedOrigin.hostname;
79-
expectedOrigin = origin;
80-
}
81-
}
61+
const parsedOrigin = new URL(origin);
62+
const expectedRPID = parsedOrigin.hostname;
63+
const expectedOrigin = origin;
8264

8365

8466
let verification;

apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { validateRedirectUrl } from "@/lib/redirect-urls";
12
import { createAuthTokens } from "@/lib/tokens";
23
import { getPrismaClientForTenancy } from "@/prisma-client";
34
import { createVerificationCodeHandler } from "@/route-handlers/verification-code-handler";
@@ -63,34 +64,16 @@ export const passkeySignInVerificationCodeHandler = createVerificationCodeHandle
6364
}
6465

6566
// HACK: we validate origin and rpid outside of simpleauth, this should be replaced once we have a primary authentication domain
66-
let expectedRPID = "";
67-
let expectedOrigin = "";
6867
const clientDataJSON = decodeClientDataJSON(authentication_response.response.clientDataJSON);
6968
const { origin } = clientDataJSON;
70-
const localhostAllowed = tenancy.config.domains.allowLocalhost;
71-
const parsedOrigin = new URL(origin);
72-
const isLocalhost = parsedOrigin.hostname === "localhost";
73-
74-
if (!localhostAllowed && isLocalhost) {
75-
throw new KnownErrors.PasskeyAuthenticationFailed("Passkey authentication failed because localhost is not allowed");
76-
}
7769

78-
if (localhostAllowed && isLocalhost) {
79-
expectedRPID = parsedOrigin.hostname;
80-
expectedOrigin = origin;
70+
if (!validateRedirectUrl(origin, tenancy)) {
71+
throw new KnownErrors.PasskeyAuthenticationFailed("Passkey authentication failed because the origin is not allowed");
8172
}
8273

83-
if (!isLocalhost) {
84-
if (!Object.values(tenancy.config.domains.trustedDomains)
85-
.filter(e => e.baseUrl)
86-
.map(e => e.baseUrl)
87-
.includes(parsedOrigin.origin)) {
88-
throw new KnownErrors.PasskeyAuthenticationFailed("Passkey authentication failed because the origin is not allowed");
89-
} else {
90-
expectedRPID = parsedOrigin.hostname;
91-
expectedOrigin = origin;
92-
}
93-
}
74+
const parsedOrigin = new URL(origin);
75+
const expectedRPID = parsedOrigin.hostname;
76+
const expectedOrigin = origin;
9477

9578
let authVerify;
9679
authVerify = await verifyAuthenticationResponse({

0 commit comments

Comments
 (0)