Skip to content

Phase 1: identity — email-code sign-in, accounts, workspaces, roles, invites - #2

Merged
erikgoins merged 8 commits into
mainfrom
feature/identity
Oct 15, 2026
Merged

erikgoins merged 8 commits into
mainfrom
feature/identity

Conversation

@erikgoins

Copy link
Copy Markdown
Contributor

What

Phase 1 from docs/spec.md. Feature doc: docs/features/identity.md. Decision 017 records the owner's calls (GitHub link needed for GitHub tasks only; sign-up policy is an instance setting).

  • Role matrix in packages/core (authorize, can, invitableRoles, canManageMember, GITHUB_CAPABILITIES).
  • Sign-in: Better Auth email OTP. One email carries a code and a link (code in the URL fragment, verified only on click). Codes hashed; limits per email and per trusted IP, stored in the database.
  • Accounts → workspaces → members → invites in a new packages/server, with SQL guards inside each write (D1 has no transactions).
  • /api/v1 framework-free router: cookie writes must be same-origin; bearer tokens for the Mac app.
  • Web UI: sign-in, onboarding, members and invites, invite acceptance, GitHub link (plain Tailwind; shadcn/ui arrives with Phase 3 views).

Verification

  • 180 unit tests. DB, auth, and API suites run on libsql and D1.
  • 4 Playwright tests: owner invites a client who joins as Read Only; email link signs in only after a click.
  • pnpm smoke: a full email-code sign-in inside the Worker bundle (D1 + local send_email).
  • Proven red when weakened: a flipped matrix cell, in-memory limits, plain codes, a removed invite guard, the old IN-list query on D1.

Not covered yet

The GitHub OAuth callback needs a real GitHub App (Phase 2).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg

erikgoins and others added 8 commits October 15, 2026 18:54
WIP doc for Phase 1 (identity). Decision 017 records the owner's
calls: Better Auth for identity only, GitHub link needed for GitHub
tasks only, sign-up policy as an instance setting. Phase 0 docs now
say CI passed on PR #1. Adds better-auth 1.7.8 to apps/web.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
authorize()/can() implement the spec's role matrix, the two Read Only
switches, and the GitHub gate from decision 017 (GitHub tasks only).
invitableRoles() and canManageMember() bound invites and role changes.
78 tests check every role x capability cell against a hand-written
copy of the spec; flipping one cell turns the suite red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
Identity and domain schema (migration 0001) with CHECKs for roles and
lowercase invite emails. New @gitwork/server package: Better Auth
factory (email OTP, hashed codes, database rate limits, encrypted
OAuth tokens, GitHub link-only), email adapters, and the sign-up rule
from decision 017. Sign-in tests run through the HTTP handler on both
drivers; weakening rate-limit storage or code hashing turns them red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
Services in @gitwork/server enforce the role matrix through
packages/core. Invite acceptance guards the membership INSERT itself
(INSERT ... SELECT ... WHERE pending) and takes the role from the
invite row; dropping the guard fails the expired/revoked tests on both
drivers. Role changes and removals are guarded on the checked role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
A framework-free router in @gitwork/server serves /api/v1: me,
accounts, workspaces, members, invites. Cookie-authenticated writes
must be same-origin; bearer tokens (Mac app) are exempt. Tests cover
401, 403, 400, 404/405, cross-site writes, bearer auth, and the invite
flow over HTTP on both drivers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
… link

Pages for email-code sign-in (code or click-to-verify link), account
onboarding, workspace members and invites, invite acceptance, and
GitHub linking. /api/auth and /api/v1 mount Better Auth and the
@gitwork/server router per request. E2E covers the owner-invites-
client flow and the link verify; smoke now signs in inside the Worker
bundle. Feature doc replaces the WIP doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
- Per-IP limits trust only cf-connecting-ip (Workers) or an opt-in
  TRUSTED_IP_HEADER; otherwise off. A per-email limit (5/15 min) and a
  per-user invite cap (30/hour) use one atomic upsert in rate_limits.
- listMyWorkspaces uses subqueries: D1 caps a query at 100 bound
  parameters; the old IN-list failed on D1 at 101 workspaces.
- safeRedirectPath() resolves ?next= like a browser (/\evil.com).
- BETTER_AUTH_URL is required in production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
CI's clean install failed on URL in redirect.ts. Locally, URL resolved
only through types that vitest pulls in transitively.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnDXTQ6MVbAXETYsq6fvqg
@erikgoins
erikgoins merged commit 3cb4005 into main Oct 15, 2026
1 check passed
@erikgoins
erikgoins deleted the feature/identity branch October 15, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant