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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

jobs:
check:
name: Typecheck & build
name: Test, typecheck & build
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -23,6 +23,7 @@ jobs:
- run: cp .dev.vars.example .dev.vars
- run: npx wrangler types
- run: npm run typecheck
- run: npm test
- run: npm run build

deploy:
Expand Down
12 changes: 2 additions & 10 deletions app/.dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,9 @@
# optional secrets show up on Env.
ANTHROPIC_API_KEY=""
ENGINE_MCP_TOKEN=""
# Entra ID sign-in. Use a separate dev app registration with a
# http://localhost:5173/auth/callback redirect URI; these override the vars
# in wrangler.jsonc during local dev.
ENTRA_TENANT_ID=""
ENTRA_CLIENT_ID=""
ENTRA_CLIENT_SECRET=""
# Any long random string; signs the session cookie.
# Princeton CAS needs no client secret. Set a long random string to sign
# sessions and pending login cookies (use a separate value for local dev).
SESSION_SECRET=""
# OIT API store credentials (ActiveDirectory subscription) for alias->netid.
OIT_CONSUMER_KEY=""
OIT_CONSUMER_SECRET=""
# Google Calendar MCP (dev needs its own OAuth client with a
# http://localhost:5173/oauth/google/callback redirect URI).
GOOGLE_OAUTH_CLIENT_ID=""
Expand Down
34 changes: 25 additions & 9 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,23 @@ browser (React + useAgentChat over WebSocket)
- **Models**: Claude Opus 5 by default, with Sonnet 5 and a Workers AI
fallback ("Campus") in the switcher. Claude models require the
`ANTHROPIC_API_KEY` secret; without it every choice falls back to Campus.
- **Identity**: Princeton sign-in via Microsoft Entra ID (`src/server/auth.ts`
— OIDC auth code + PKCE, confidential client, signed HttpOnly session
cookie). netid is derived from the account email's local part; there is no
way to choose one. Every agent Durable Object is named `u-<netid>-…` and
- **Identity**: Princeton CAS sign-in (`src/server/auth.ts`, using
`https://authenticate.princeton.edu/cas`). The Worker validates service
tickets through `/p3/serviceValidate` over HTTPS and takes the NetID from
CAS's authenticated `user`, never from an email alias or browser input.
Display name and email use CAS attributes when released, falling back to
NetID and `<netid>@princeton.edu`. Every agent Durable Object is named `u-<netid>-…` and
the Worker rejects any request whose session doesn't own that prefix, so
chats (messages *and* workspace files) are strictly per user. Requires the
`ENTRA_TENANT_ID` / `ENTRA_CLIENT_ID` vars plus `ENTRA_CLIENT_SECRET` and
`SESSION_SECRET` secrets, and an app-registration redirect URI of
`<origin>/auth/callback` for every origin the app is served from.
chats (messages *and* workspace files) are strictly per user. Requires only
`SESSION_SECRET`, which signs the seven-day HttpOnly session cookie and a
ten-minute pending-login cookie. A random state is included in the CAS
service URL and checked against that cookie before ticket validation.
The service URL is `<origin>/auth/callback?state=<random-value>`; CAS must
allow that callback path and its query parameters for each deployed/dev
origin. Logout ends the PI session only, leaving Princeton SSO active.
Sessions issued before the CAS migration require a fresh sign-in. Existing
chats remain under their NetID; any historical email-alias namespaces are
not automatically reassigned.
- **Planner**: mirrors TigerJunction's ReCal calendar — its default color
palette, solid blocks with an ink left border for locked-in sections, and
striped translucent blocks for section options not picked yet. The engine
Expand Down Expand Up @@ -67,6 +75,11 @@ npm run dev
`npm run typecheck` needs `worker-configuration.d.ts` — generate it with
`npx wrangler types` (CI does this automatically).

`npm test` runs the auth flow tests with Node 22.6 or newer, including ticket
validation, browser state, expired/tampered cookies, and session migration.
Local CAS sign-in requires a callback origin permitted by Princeton CAS;
use an approved HTTPS development origin if localhost is not permitted.

The Workers AI "Campus" model runs remotely even in dev, so wrangler needs
Cloudflare credentials (`wrangler login`, or `CLOUDFLARE_API_TOKEN` +
`CLOUDFLARE_ACCOUNT_ID`). The Claude path only needs `ANTHROPIC_API_KEY` in
Expand All @@ -81,10 +94,13 @@ Pushes to `main` deploy automatically via GitHub Actions
```bash
npm run deploy
npx wrangler secret put ANTHROPIC_API_KEY # enables the Claude models
npx wrangler secret put ENTRA_CLIENT_SECRET # Entra app registration secret
npx wrangler secret put SESSION_SECRET # any long random string
```

CAS needs no client ID, tenant ID, or client secret. The old Entra and OIT
directory credentials are no longer read and can be removed from the Worker
after deployment. Keep the existing `SESSION_SECRET` configured.

Config knobs (in `wrangler.jsonc`): `ENGINE_MCP_BASE` (engine base URL) and
the optional `ENGINE_MCP_TOKEN` secret if the engine ever requires a bearer
token.
Expand Down
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"dev": "vite dev",
"build": "vite build",
"deploy": "vite build && wrangler deploy -c dist/pi/wrangler.json",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "node --experimental-strip-types --test tests/*.test.ts"
},
"dependencies": {
"@ai-sdk/anthropic": "^4.0.0",
Expand Down
4 changes: 2 additions & 2 deletions app/src/client/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type AuthState =
/** Signed in when the tab opened; the session has since lapsed. */
| { status: "expired"; identity: Identity };

/** Where signIn() parks the page the student was on, across the Entra hop. */
/** Where signIn() parks the page the student was on, across the CAS hop. */
const RETURN_KEY = "pi:return-to";

/**
Expand Down Expand Up @@ -108,7 +108,7 @@ export function useIdentity(): { auth: AuthState; revalidate: () => void } {
}

/**
* Hands off to the Worker's Entra flow, remembering the page the student asked
* Hands off to the Worker's CAS flow, remembering the page the student asked
* for. The callback always lands on "/", so the shell replays this on arrival.
*/
export function signIn(returnTo = location.pathname + location.search) {
Expand Down
Loading
Loading