Files
app/db/schema.surql
Albert 6ff6bae270 feat: Implement OAuth with DPoP using @atproto/oauth-client-node
Replace manual OAuth implementation with official @atproto/oauth-client-node library to properly support DPoP (Demonstrating Proof of Possession) authentication.

Changes:
- Added @atproto/oauth-client-node dependency
- Created OAuth state store (SurrealDB-backed) for CSRF protection
- Created OAuth session store (SurrealDB-backed) for token persistence
- Created OAuth client singleton with localhost exception for development
- Rewrote /api/auth/login to use client.authorize()
- Rewrote /api/auth/callback to use client.callback() with DPoP
- Updated lib/auth/session.ts with getAuthenticatedAgent() for ATproto API calls
- Updated db/schema.surql with oauth_state and oauth_session tables
- Added scripts/apply-schema.js for database schema management
- Created plans/oauth-dpop-implementation.md with detailed implementation plan
- Removed legacy lib/auth/atproto.ts and lib/auth/oauth-state.ts
- Updated .env to use localhost exception (removed BLUESKY_CLIENT_ID)

The OAuth client now handles:
- PKCE code generation and verification
- DPoP proof generation and signing
- Automatic token refresh
- Session persistence across server restarts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 01:40:04 +00:00

143 lines
4.9 KiB
Plaintext

-- --------------------------------------------------
-- Ponderants :: SurrealDB Schema
-- --------------------------------------------------
-- --------------------------------------------------
-- Access Control (JWT)
-- --------------------------------------------------
-- Define the JWT access method. This tells SurrealDB to trust
-- JWTs signed by our Next.js backend using the HS512 algorithm
-- and the secret key provided in the environment.
-- (Note: DEFINE TOKEN is deprecated as of 2.x)
DEFINE ACCESS app_jwt
ON DATABASE
TYPE JWT
ALGORITHM HS512
KEY $env.SURREALDB_JWT_SECRET;
-- --------------------------------------------------
-- Table: user
-- --------------------------------------------------
-- Stores basic user information, cached from ATproto.
DEFINE TABLE user SCHEMAFULL;
-- The user's decentralized identifier (DID) is their primary key.
DEFINE FIELD did ON TABLE user TYPE string
ASSERT $value != NONE;
DEFINE FIELD handle ON TABLE user TYPE string;
-- Ensure DIDs are unique.
DEFINE INDEX user_did_idx ON TABLE user COLUMNS did UNIQUE;
-- --------------------------------------------------
-- Table: node
-- --------------------------------------------------
-- Stores a single "thought node." This is the cache record for
-- the com.ponderants.node lexicon.
DEFINE TABLE node SCHEMAFULL
-- THIS IS THE CORE SECURITY MODEL:
-- Users can only perform actions on nodes where the
-- node's 'user_did' field matches the 'did' claim
-- from their validated JWT ('$token.did').
PERMISSIONS
FOR select, create, update, delete
WHERE user_did = $token.did;
-- Foreign key linking to the user table (via DID).
DEFINE FIELD user_did ON TABLE node TYPE string
ASSERT $value != NONE;
-- The canonical URI of the record on the ATproto PDS.
DEFINE FIELD atp_uri ON TABLE node TYPE string;
DEFINE FIELD title ON TABLE node TYPE string;
DEFINE FIELD body ON TABLE node TYPE string;
-- The AI-generated vector embedding for the 'body'.
-- We use array<number> for the vector.
DEFINE FIELD embedding ON TABLE node TYPE array<number>;
-- The 3D coordinates calculated by UMAP.
DEFINE FIELD coords_3d ON TABLE node TYPE array<number>
-- Must be a 3-point array [x, y, z] or empty.
ASSERT $value = NONE OR array::len($value) = 3;
-- Define the vector search index.
-- We use MTREE (or HNSW) for high-performance k-NN search.
-- The dimension (768) MUST match the output of the
-- 'text-embedding-004' model.
DEFINE INDEX node_embedding_idx ON TABLE node FIELDS embedding MTREE DIMENSION 768;
-- --------------------------------------------------
-- Relation: links_to
-- --------------------------------------------------
-- This is a graph edge table, relating (node)->(node).
DEFINE TABLE links_to SCHEMAFULL
-- Security for graph edges: A user can only create/view/delete
-- links between two nodes that BOTH belong to them.
PERMISSIONS
FOR select, create, delete
WHERE
(SELECT user_did FROM $from) = $token.did
AND
(SELECT user_did FROM $to) = $token.did;
-- (No fields needed, it's a simple relation)
-- Example usage: RELATE (node:1)-[links_to]->(node:2);
-- --------------------------------------------------
-- Table: oauth_state
-- --------------------------------------------------
-- Stores temporary OAuth state during the authorization flow.
-- Used for CSRF protection. States should expire after 1 hour.
DEFINE TABLE oauth_state SCHEMAFULL;
-- The state key (random string generated during authorize)
DEFINE FIELD key ON TABLE oauth_state TYPE string
ASSERT $value != NONE;
-- The state value (contains PKCE verifier, DPoP key, etc.)
DEFINE FIELD value ON TABLE oauth_state TYPE object
ASSERT $value != NONE;
-- Timestamp for cleanup
DEFINE FIELD created_at ON TABLE oauth_state TYPE datetime
DEFAULT time::now();
-- Index for fast lookups by key
DEFINE INDEX oauth_state_key_idx ON TABLE oauth_state COLUMNS key UNIQUE;
-- Event to auto-delete expired states (older than 1 hour)
DEFINE EVENT oauth_state_cleanup ON TABLE oauth_state WHEN time::now() - created_at > 1h THEN (
DELETE oauth_state WHERE id = $event.id
);
-- --------------------------------------------------
-- Table: oauth_session
-- --------------------------------------------------
-- Stores persistent OAuth sessions (access/refresh tokens).
-- Sessions are managed by the @atproto/oauth-client-node library.
DEFINE TABLE oauth_session SCHEMAFULL;
-- The user's DID (unique identifier)
DEFINE FIELD did ON TABLE oauth_session TYPE string
ASSERT $value != NONE;
-- The session data (contains tokens, DPoP key, etc.)
DEFINE FIELD session_data ON TABLE oauth_session TYPE object
ASSERT $value != NONE;
-- Timestamp for last update (useful for debugging)
DEFINE FIELD updated_at ON TABLE oauth_session TYPE datetime
DEFAULT time::now();
-- Index for fast lookups by DID
DEFINE INDEX oauth_session_did_idx ON TABLE oauth_session COLUMNS did UNIQUE;