diff --git a/apps/noter/package/feature/auth/api/input.ts b/apps/noter/package/feature/auth/api/input.ts index 76d0620d..031eca55 100644 --- a/apps/noter/package/feature/auth/api/input.ts +++ b/apps/noter/package/feature/auth/api/input.ts @@ -79,7 +79,7 @@ export const connectWalletInput = z.object({ walletType: walletSessionInsertSchema.shape.walletType.pipe(z.enum(["slush"])), // Subset validation address: walletSessionInsertSchema.shape.walletAddress, // Maps to walletAddress in DB signature: walletSessionInsertSchema.shape.signature, - message: walletSessionInsertSchema.shape.signedMessage, // Maps to signedMessage in DB + challengeId: uuidv7Schema, // Server-issued challenge ID (replaces client message) }); export type ConnectWalletInput = z.infer; diff --git a/apps/noter/package/feature/auth/api/route.ts b/apps/noter/package/feature/auth/api/route.ts index 629e9090..c476cc40 100644 --- a/apps/noter/package/feature/auth/api/route.ts +++ b/apps/noter/package/feature/auth/api/route.ts @@ -7,6 +7,7 @@ import { router, procedure } from "@/shared/lib/trpc/init"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { verifyPersonalMessageSignature } from "@mysten/sui/verify"; +import { randomBytes } from "crypto"; import { uuidv7 } from "uuidv7"; import { initiateLoginInput, @@ -33,8 +34,8 @@ import { } from "../lib/zklogin-client"; import { OAUTH_PROVIDERS, OAUTH_SCOPES, AUTH_ERRORS } from "../constant"; import { buildOAuthUrl } from "../domain/zklogin"; -import { zkLoginSessions, walletSessions } from "@/shared/db/schema"; -import { eq } from "drizzle-orm"; +import { zkLoginSessions, walletSessions, walletChallenges } from "@/shared/db/schema"; +import { eq, lt } from "drizzle-orm"; import * as authService from "../domain/service"; export const authRouter = router({ @@ -239,35 +240,82 @@ export const authRouter = router({ return { success: true }; }), + /** + * Get a one-time challenge nonce for wallet authentication + * The nonce must be signed by the wallet and returned via connectWallet + */ + getChallenge: procedure + .mutation(async ({ ctx }) => { + // Clean up expired challenges to prevent table bloat + await ctx.db + .delete(walletChallenges) + .where(lt(walletChallenges.expiresAt, new Date())); + + const challengeId = uuidv7(); + const nonce = randomBytes(32).toString("hex"); + const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes + + await ctx.db.insert(walletChallenges).values({ + id: challengeId, + nonce, + expiresAt, + }); + + return { challengeId, nonce, expiresAt }; + }), + /** * Connect wallet - authenticate with Sui wallet (Slush, Sui Wallet) - * Verifies signature and creates session + * Verifies signature against a server-issued challenge nonce */ connectWallet: procedure .input(connectWalletInput) .mutation(async ({ ctx, input }) => { - const { walletType, address, signature, message } = input; + const { challengeId, walletType, address, signature } = input; try { - // Verify the wallet signature before creating a session + // 1. Atomically consume challenge + const [challenge] = await ctx.db + .delete(walletChallenges) + .where(eq(walletChallenges.id, challengeId)) + .returning(); + + if (!challenge) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Challenge not found or already used", + }); + } + + if (challenge.expiresAt < new Date()) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Challenge expired", + }); + } + + // 2. Verify signature against server-issued nonce const signerAddress = await verifyPersonalMessageSignature( - new TextEncoder().encode(message), + new TextEncoder().encode(challenge.nonce), signature, ).catch(() => { throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid signature" }); }); if (signerAddress.toSuiAddress() !== address) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Signature does not match address" }); + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Signature does not match address", + }); } - // Create or update user via service + // 4. Create or update user const user = await authService.upsertWalletUser(ctx.db, { address, walletType, }); - // Create wallet session + // 5. Create wallet session const sessionId = uuidv7(); const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + 24); // 24 hour session @@ -277,13 +325,12 @@ export const authRouter = router({ userId: user.id, walletAddress: address, walletType, - signedMessage: message, + signedMessage: challenge.nonce, signature, signedAt: new Date(), expiresAt, }); - // Return wallet session data (no ephemeral keys for wallet auth) return { user, sessionId, diff --git a/apps/noter/package/feature/auth/constant.ts b/apps/noter/package/feature/auth/constant.ts index ebd0d059..2def6e91 100644 --- a/apps/noter/package/feature/auth/constant.ts +++ b/apps/noter/package/feature/auth/constant.ts @@ -69,7 +69,7 @@ export const OAUTH_SCOPES = { export const STORAGE_KEYS = { ephemeralPrivateKey: "zklogin:ephemeral:private", ephemeralPublicKey: "zklogin:ephemeral:public", - sessionId: "zklogin:session:id", + sessionId: "auth:session:id", nonce: "zklogin:nonce", maxEpoch: "zklogin:maxEpoch", randomness: "zklogin:randomness", diff --git a/apps/noter/package/feature/auth/lib/wallet-client.ts b/apps/noter/package/feature/auth/lib/wallet-client.ts index 87e3e086..67325407 100644 --- a/apps/noter/package/feature/auth/lib/wallet-client.ts +++ b/apps/noter/package/feature/auth/lib/wallet-client.ts @@ -180,17 +180,3 @@ export async function disconnectWallet(type: WalletType): Promise { } } -/** - * Generate authentication message - */ -export function generateAuthMessage(): string { - const timestamp = Date.now(); - const nonce = Math.random().toString(36).substring(7); - - return `Sign this message to authenticate with Noter - -Timestamp: ${timestamp} -Nonce: ${nonce} - -This will not trigger any blockchain transaction or cost any gas fees.`; -} diff --git a/apps/noter/package/shared/db/migrations/0003_conscious_deathbird.sql b/apps/noter/package/shared/db/migrations/0003_conscious_deathbird.sql new file mode 100644 index 00000000..bb82e51f --- /dev/null +++ b/apps/noter/package/shared/db/migrations/0003_conscious_deathbird.sql @@ -0,0 +1,8 @@ +CREATE TABLE "wallet_challenges" ( + "id" uuid PRIMARY KEY NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "nonce" text NOT NULL, + "expiresAt" timestamp NOT NULL +); +--> statement-breakpoint +CREATE INDEX "wallet_challenges_expiresAt_index" ON "wallet_challenges" USING btree ("expiresAt"); \ No newline at end of file diff --git a/apps/noter/package/shared/db/migrations/meta/0003_snapshot.json b/apps/noter/package/shared/db/migrations/meta/0003_snapshot.json new file mode 100644 index 00000000..2c3c8066 --- /dev/null +++ b/apps/noter/package/shared/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,1103 @@ +{ + "id": "c37d77ab-d057-4524-a8df-e5767fc16d94", + "prevId": "b297cb0c-43b8-4e9b-a620-348289847be8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'anthropic/claude-sonnet-4'" + }, + "systemPrompt": { + "name": "systemPrompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "temperature": { + "name": "temperature", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chats_userId_index": { + "name": "chats_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chats_userId_createdAt_index": { + "name": "chats_userId_createdAt_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chats_userId_users_id_fk": { + "name": "chats_userId_users_id_fk", + "tableFrom": "chats", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "chatId": { + "name": "chatId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "message_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ai_message_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "promptTokens": { + "name": "promptTokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completionTokens": { + "name": "completionTokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "agentRunId": { + "name": "agentRunId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "turnIndex": { + "name": "turnIndex", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_chatId_index": { + "name": "messages_chatId_index", + "columns": [ + { + "expression": "chatId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_chatId_createdAt_index": { + "name": "messages_chatId_createdAt_index", + "columns": [ + { + "expression": "chatId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_agentRunId_index": { + "name": "messages_agentRunId_index", + "columns": [ + { + "expression": "agentRunId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_chatId_chats_id_fk": { + "name": "messages_chatId_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chatId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.note_memory_highlights": { + "name": "note_memory_highlights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "highlightedText": { + "name": "highlightedText", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "highlightedHtml": { + "name": "highlightedHtml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startOffset": { + "name": "startOffset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "endOffset": { + "name": "endOffset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "extractedText": { + "name": "extractedText", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryTitle": { + "name": "memoryTitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryContent": { + "name": "memoryContent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "relationships": { + "name": "relationships", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "memory_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "blobId": { + "name": "blobId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "graphObjectId": { + "name": "graphObjectId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transactionId": { + "name": "transactionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approvedAt": { + "name": "approvedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "savedAt": { + "name": "savedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "note_memory_highlights_noteId_index": { + "name": "note_memory_highlights_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_memory_highlights_userId_index": { + "name": "note_memory_highlights_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_memory_highlights_status_index": { + "name": "note_memory_highlights_status_index", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_memory_highlights_userId_createdAt_index": { + "name": "note_memory_highlights_userId_createdAt_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "note_memory_highlights_noteId_notes_id_fk": { + "name": "note_memory_highlights_noteId_notes_id_fk", + "tableFrom": "note_memory_highlights", + "tableTo": "notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "note_memory_highlights_userId_users_id_fk": { + "name": "note_memory_highlights_userId_users_id_fk", + "tableFrom": "note_memory_highlights", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notes": { + "name": "notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "plainText": { + "name": "plainText", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notes_userId_index": { + "name": "notes_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notes_userId_updatedAt_index": { + "name": "notes_userId_updatedAt_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updatedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notes_userId_users_id_fk": { + "name": "notes_userId_users_id_fk", + "tableFrom": "notes", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "suiAddress": { + "name": "suiAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authMethod": { + "name": "authMethod", + "type": "auth_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "providerSub": { + "name": "providerSub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walletType": { + "name": "walletType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastSeenAt": { + "name": "lastSeenAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_provider_providerSub_index": { + "name": "users_provider_providerSub_index", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "providerSub", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_authMethod_index": { + "name": "users_authMethod_index", + "columns": [ + { + "expression": "authMethod", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_suiAddress_index": { + "name": "users_suiAddress_index", + "columns": [ + { + "expression": "suiAddress", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_suiAddress_unique": { + "name": "users_suiAddress_unique", + "nullsNotDistinct": false, + "columns": [ + "suiAddress" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallet_challenges": { + "name": "wallet_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "wallet_challenges_expiresAt_index": { + "name": "wallet_challenges_expiresAt_index", + "columns": [ + { + "expression": "expiresAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallet_sessions": { + "name": "wallet_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "walletAddress": { + "name": "walletAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walletType": { + "name": "walletType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signedMessage": { + "name": "signedMessage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signedAt": { + "name": "signedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "wallet_sessions_userId_index": { + "name": "wallet_sessions_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wallet_sessions_expiresAt_index": { + "name": "wallet_sessions_expiresAt_index", + "columns": [ + { + "expression": "expiresAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wallet_sessions_walletAddress_index": { + "name": "wallet_sessions_walletAddress_index", + "columns": [ + { + "expression": "walletAddress", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wallet_sessions_userId_users_id_fk": { + "name": "wallet_sessions_userId_users_id_fk", + "tableFrom": "wallet_sessions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.zklogin_sessions": { + "name": "zklogin_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ephemeralPrivateKey": { + "name": "ephemeralPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ephemeralPublicKey": { + "name": "ephemeralPublicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maxEpoch": { + "name": "maxEpoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "randomness": { + "name": "randomness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zkProof": { + "name": "zkProof", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "zklogin_sessions_userId_index": { + "name": "zklogin_sessions_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "zklogin_sessions_expiresAt_index": { + "name": "zklogin_sessions_expiresAt_index", + "columns": [ + { + "expression": "expiresAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "zklogin_sessions_userId_users_id_fk": { + "name": "zklogin_sessions_userId_users_id_fk", + "tableFrom": "zklogin_sessions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.ai_message_status": { + "name": "ai_message_status", + "schema": "public", + "values": [ + "streaming", + "awaiting-approval", + "in-progress", + "completed", + "error" + ] + }, + "public.auth_method": { + "name": "auth_method", + "schema": "public", + "values": [ + "zklogin", + "wallet" + ] + }, + "public.memory_status": { + "name": "memory_status", + "schema": "public", + "values": [ + "preparing", + "pending", + "signing", + "uploading", + "indexing", + "saved", + "rejected", + "error" + ] + }, + "public.message_role": { + "name": "message_role", + "schema": "public", + "values": [ + "user", + "assistant" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/noter/package/shared/db/migrations/meta/_journal.json b/apps/noter/package/shared/db/migrations/meta/_journal.json index a855507c..4be2656a 100644 --- a/apps/noter/package/shared/db/migrations/meta/_journal.json +++ b/apps/noter/package/shared/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1743900000000, "tag": "0002_add_enoki_fields", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1774594396885, + "tag": "0003_conscious_deathbird", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/noter/package/shared/db/schema.ts b/apps/noter/package/shared/db/schema.ts index fca48aaa..0b2bd552 100644 --- a/apps/noter/package/shared/db/schema.ts +++ b/apps/noter/package/shared/db/schema.ts @@ -178,6 +178,27 @@ export const walletSessions = pgTable( (t) => [index().on(t.userId), index().on(t.expiresAt), index().on(t.walletAddress)] ); +// ════════════════════════════════════════════════════════════════ +// WALLET CHALLENGES (one-time nonces for replay protection) +// ════════════════════════════════════════════════════════════════ + +export const walletChallenges = pgTable( + "wallet_challenges", + { + id: uuid() + .primaryKey() + .$defaultFn(() => uuidv7()), + createdAt: timestamp().defaultNow().notNull(), + + // Server-generated random nonce + nonce: text().notNull(), + + // Challenge expiration (5 minutes) + expiresAt: timestamp().notNull(), + }, + (t) => [index().on(t.expiresAt)] +); + // ════════════════════════════════════════════════════════════════ // NOTES (Apple Notes / Notion-like) // ════════════════════════════════════════════════════════════════