From 1aedac6ee68d9725b6b08ad661c720f264d86302 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 1 Aug 2025 13:45:36 +0200 Subject: [PATCH 01/14] refactor api with effect http api --- .gitignore | 1 + apps/server-new/.env.example | 13 + apps/server-new/CLAUDE.md | 191 ++++++++ apps/server-new/package.json | 36 ++ apps/server-new/patterns/README.md | 19 + apps/server-new/patterns/generic-testing.md | 365 +++++++++++++++ apps/server-new/patterns/http-api.md | 233 ++++++++++ apps/server-new/patterns/layer-composition.md | 428 ++++++++++++++++++ .../20241111122738_init/migration.sql | 31 ++ .../migration.sql | 25 + .../migration.sql | 9 + .../migration.sql | 24 + .../20241117202441_add_key/migration.sql | 20 + .../20241122132737_add_update/migration.sql | 9 + .../20241128184625_add_identity/migration.sql | 13 + .../migration.sql | 4 + .../migration.sql | 2 + .../migration.sql | 30 ++ .../20250428155829_add_inboxes/migration.sql | 50 ++ .../migration.sql | 191 ++++++++ .../migration.sql | 26 ++ .../migration.sql | 29 ++ .../prisma/migrations/migration_lock.toml | 3 + apps/server-new/prisma/schema.prisma | 189 ++++++++ apps/server-new/setupTests.ts | 1 + apps/server-new/specs/README.md | 20 + apps/server-new/src/config/database.ts | 6 + apps/server-new/src/config/privy.ts | 25 + apps/server-new/src/config/server.ts | 19 + apps/server-new/src/domain/models.ts | 174 +++++++ apps/server-new/src/http/api.ts | 208 +++++++++ apps/server-new/src/http/errors.ts | 105 +++++ apps/server-new/src/http/handlers.ts | 158 +++++++ apps/server-new/src/index.ts | 5 + apps/server-new/src/server.ts | 20 + apps/server-new/src/services/auth.ts | 62 +++ apps/server-new/src/services/database.ts | 64 +++ apps/server-new/tsconfig.app.json | 29 ++ apps/server-new/tsconfig.json | 29 ++ apps/server-new/tsconfig.node.json | 29 ++ apps/server-new/tsup.config.ts | 13 + apps/server-new/vitest.config.ts | 10 + apps/server/api-docs/api-summary.md | 81 ++++ apps/server/api-docs/domain-model-overview.md | 212 +++++++++ apps/server/api-docs/get-accounts-inbox.md | 60 +++ apps/server/api-docs/get-accounts-inboxes.md | 66 +++ .../api-docs/get-connect-app-identity.md | 69 +++ .../get-connect-identity-encrypted.md | 63 +++ apps/server/api-docs/get-connect-identity.md | 63 +++ apps/server/api-docs/get-connect-spaces.md | 103 +++++ apps/server/api-docs/get-identity.md | 82 ++++ apps/server/api-docs/get-root.md | 40 ++ apps/server/api-docs/get-spaces-inbox.md | 60 +++ apps/server/api-docs/get-spaces-inboxes.md | 66 +++ apps/server/api-docs/get-whoami.md | 49 ++ .../api-docs/post-accounts-inbox-messages.md | 85 ++++ ...post-connect-add-app-identity-to-spaces.md | 79 ++++ .../api-docs/post-connect-app-identity.md | 88 ++++ apps/server/api-docs/post-connect-identity.md | 89 ++++ apps/server/api-docs/post-connect-spaces.md | 102 +++++ .../api-docs/post-spaces-inbox-messages.md | 85 ++++ apps/server/api-docs/websocket-connection.md | 224 +++++++++ package.json | 1 + pnpm-lock.yaml | 248 ++++++---- 64 files changed, 4857 insertions(+), 76 deletions(-) create mode 100644 apps/server-new/.env.example create mode 100644 apps/server-new/CLAUDE.md create mode 100644 apps/server-new/package.json create mode 100644 apps/server-new/patterns/README.md create mode 100644 apps/server-new/patterns/generic-testing.md create mode 100644 apps/server-new/patterns/http-api.md create mode 100644 apps/server-new/patterns/layer-composition.md create mode 100644 apps/server-new/prisma/migrations/20241111122738_init/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241113083927_introduce_counter_and_state_to_space_events/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241114170708_add_invitation/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241116161206_extend_invitation/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241117202441_add_key/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241122132737_add_update/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241128184625_add_identity/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241212164639_add_session_nonce_token/migration.sql create mode 100644 apps/server-new/prisma/migrations/20241212184317_add_account_token_index/migration.sql create mode 100644 apps/server-new/prisma/migrations/20250129220359_add_update_signature/migration.sql create mode 100644 apps/server-new/prisma/migrations/20250428155829_add_inboxes/migration.sql create mode 100644 apps/server-new/prisma/migrations/20250611173316_introduce_connect/migration.sql create mode 100644 apps/server-new/prisma/migrations/20250620005807_add_connect_signer_address/migration.sql create mode 100644 apps/server-new/prisma/migrations/20250627185421_remove_app_identity_nonce/migration.sql create mode 100644 apps/server-new/prisma/migrations/migration_lock.toml create mode 100644 apps/server-new/prisma/schema.prisma create mode 100644 apps/server-new/setupTests.ts create mode 100644 apps/server-new/specs/README.md create mode 100644 apps/server-new/src/config/database.ts create mode 100644 apps/server-new/src/config/privy.ts create mode 100644 apps/server-new/src/config/server.ts create mode 100644 apps/server-new/src/domain/models.ts create mode 100644 apps/server-new/src/http/api.ts create mode 100644 apps/server-new/src/http/errors.ts create mode 100644 apps/server-new/src/http/handlers.ts create mode 100644 apps/server-new/src/index.ts create mode 100644 apps/server-new/src/server.ts create mode 100644 apps/server-new/src/services/auth.ts create mode 100644 apps/server-new/src/services/database.ts create mode 100644 apps/server-new/tsconfig.app.json create mode 100644 apps/server-new/tsconfig.json create mode 100644 apps/server-new/tsconfig.node.json create mode 100644 apps/server-new/tsup.config.ts create mode 100644 apps/server-new/vitest.config.ts create mode 100644 apps/server/api-docs/api-summary.md create mode 100644 apps/server/api-docs/domain-model-overview.md create mode 100644 apps/server/api-docs/get-accounts-inbox.md create mode 100644 apps/server/api-docs/get-accounts-inboxes.md create mode 100644 apps/server/api-docs/get-connect-app-identity.md create mode 100644 apps/server/api-docs/get-connect-identity-encrypted.md create mode 100644 apps/server/api-docs/get-connect-identity.md create mode 100644 apps/server/api-docs/get-connect-spaces.md create mode 100644 apps/server/api-docs/get-identity.md create mode 100644 apps/server/api-docs/get-root.md create mode 100644 apps/server/api-docs/get-spaces-inbox.md create mode 100644 apps/server/api-docs/get-spaces-inboxes.md create mode 100644 apps/server/api-docs/get-whoami.md create mode 100644 apps/server/api-docs/post-accounts-inbox-messages.md create mode 100644 apps/server/api-docs/post-connect-add-app-identity-to-spaces.md create mode 100644 apps/server/api-docs/post-connect-app-identity.md create mode 100644 apps/server/api-docs/post-connect-identity.md create mode 100644 apps/server/api-docs/post-connect-spaces.md create mode 100644 apps/server/api-docs/post-spaces-inbox-messages.md create mode 100644 apps/server/api-docs/websocket-connection.md diff --git a/.gitignore b/.gitignore index 80bd4648..21d45685 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ publish output # generated prisma client apps/server/prisma/generated/ +apps/server-new/prisma/generated/ # Logs logs diff --git a/apps/server-new/.env.example b/apps/server-new/.env.example new file mode 100644 index 00000000..96a52e1c --- /dev/null +++ b/apps/server-new/.env.example @@ -0,0 +1,13 @@ +# Server Configuration +PORT=3030 + +# Database +DATABASE_URL=file:./dev.db + +# Privy Configuration +PRIVY_APP_ID=your_privy_app_id +PRIVY_APP_SECRET=your_privy_app_secret + +# Hypergraph Configuration +HYPERGRAPH_CHAIN=geo-testnet +HYPERGRAPH_RPC_URL= \ No newline at end of file diff --git a/apps/server-new/CLAUDE.md b/apps/server-new/CLAUDE.md new file mode 100644 index 00000000..a9fec626 --- /dev/null +++ b/apps/server-new/CLAUDE.md @@ -0,0 +1,191 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Package Management +**Important**: Use pnpm for package management. +- `pnpm install` - Install dependencies +- `pnpm add ` - Add new dependency +- `pnpm remove ` - Remove dependency + +### Build and Run +- `pnpm dev` - Start development server with hot reload ⚠️ **DO NOT USE** - Never run dev mode during development +- `pnpm start` - Run the application in production mode +- `pnpm build` - Build the project for production (outputs to ./dist) + +**IMPORTANT**: Never run `pnpm dev` during development work. The development server should only be started by the user manually when they want to test the application. Use tests instead of running the dev server. + +### Code Quality +- `pnpm typecheck` - Run TypeScript type checking without emitting files +- `pnpm lint` - Run Biome on all TypeScript/JavaScript files +- `pnpm lint:fix` - Run Biome with automatic fixes on all files + +### Testing +- `pnpm test` - Run all tests once +- `pnpm test:watch` - Run tests in watch mode +- Uses Vitest with @effect/vitest for Effect-aware testing +- Test files: `test/**/*.test.ts` and `src/**/*.test.ts` + +### Database +- `pnpm prisma generate` - Generate Prisma client +- `pnpm prisma migrate dev` - Run database migrations in development +- `pnpm prisma studio` - Open Prisma Studio GUI + +**CRITICAL DEVELOPMENT RULE**: After EVERY file change, you MUST: +1. Run `pnpm lint:fix` immediately +2. Run `pnpm typecheck` immediately +3. Fix ALL lint errors and type errors before proceeding +4. Do NOT continue development until both commands pass without errors + +This is non-negotiable and applies to every single file modification. + +## Project Architecture + +### Technology Stack +- **Runtime**: Node.js with tsx for development +- **Language**: TypeScript with ES2022 target +- **Framework**: Effect Platform HTTP API +- **Database**: SQLite with Prisma ORM +- **Authentication**: Privy for external auth, custom session tokens for internal + +### Code Style +- Uses Biome for linting and formatting (monorepo configuration) +- Line width: 120 characters, 2-space indentation +- Single quotes for JavaScript/TypeScript + +### TypeScript Configuration +- Strict mode enabled +- Effect patterns preferred (Effect.fn over Effect.gen) +- No emit configuration (build handled by tsup) +- Path aliases configured: `server-new/*` maps to `./src/*` + +### Project Structure +- `src/` - Source code directory + - `config/` - Configuration modules + - `http/` - HTTP API definitions and handlers + - `services/` - Business logic services + - `domain/` - Domain models (Effect Schema) +- `prisma/` - Database schema and migrations +- `test/` - Test files +- `specs/` - Feature specifications +- `patterns/` - Implementation patterns documentation + +## Development Workflow - Spec-Driven Development + +This project follows a **spec-driven development** approach where every feature is thoroughly specified before implementation. + +**CRITICAL RULE: NEVER IMPLEMENT WITHOUT FOLLOWING THE COMPLETE SPEC FLOW** + +### Mandatory Workflow Steps + +**AUTHORIZATION PROTOCOL**: Before proceeding to any phase (2-5), you MUST: +1. Present the completed work from the current phase +2. Explicitly ask for user authorization to proceed +3. Wait for clear user approval before continuing +4. NEVER assume permission or proceed automatically + +### Phase-by-Phase Process + +**Phase 1**: Create `instructions.md` (initial requirements capture) +- Create feature folder and capture user requirements +- Document user stories, acceptance criteria, constraints + +**Phase 2**: Derive `requirements.md` from instructions - **REQUIRES USER APPROVAL** +- Structured analysis of functional/non-functional requirements +- STOP and ask for authorization before proceeding to Phase 3 + +**Phase 3**: Create `design.md` from requirements - **REQUIRES USER APPROVAL** +- Technical design and implementation strategy +- STOP and ask for authorization before proceeding to Phase 4 + +**Phase 4**: Generate `plan.md` from design - **REQUIRES USER APPROVAL** +- Implementation roadmap and task breakdown +- STOP and ask for authorization before proceeding to Phase 5 + +**Phase 5**: Execute implementation - **REQUIRES USER APPROVAL** +- Follow the plan exactly as specified +- NEVER start implementation without explicit user approval + +## Effect TypeScript Development Patterns + +### Core Principles +- **Type Safety First**: Never use `any` or type assertions - prefer explicit types +- **Effect Patterns**: Use Effect's composable abstractions (prefer Effect.fn) +- **Early Returns**: Prefer early returns over deep nesting +- **Input Validation**: Validate inputs at system boundaries with Effect Schema +- **Resource Safety**: Use Effect's resource management for automatic cleanup + +### Effect-Specific Patterns + +#### Sequential Operations (Effect.fn preferred) +```typescript +// Use Effect.fn for sequential operations +const program = Effect.fn(function* () { + const user = yield* getUser(id) + const profile = yield* getProfile(user.profileId) + return { user, profile } +}) +``` + +#### Error Handling +```typescript +// Use Data.TaggedError for custom errors +class UserNotFound extends Data.TaggedError("UserNotFound")<{ + readonly id: string +}> {} + +// Use Effect.tryPromise for Promise integration +const fetchUser = (id: string) => + Effect.tryPromise({ + try: () => prisma.user.findUniqueOrThrow({ where: { id } }), + catch: () => new UserNotFound({ id }) + }) +``` + +#### Testing with @effect/vitest + +**Use @effect/vitest for Effect code:** +- Import pattern: `import { assert, describe, it } from "@effect/vitest"` +- Test pattern: `it.effect("description", () => Effect.fn(function*() { ... }))` +- **FORBIDDEN**: Never use `expect` from vitest in Effect tests - use `assert` methods + +#### Correct it.effect Pattern + +```typescript +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" + +describe("UserService", () => { + it.effect("should fetch user successfully", () => + Effect.fn(function* () { + const user = yield* fetchUser("123") + + // Use assert methods, NOT expect + assert.strictEqual(user.id, "123") + assert.deepStrictEqual(user.profile, expectedProfile) + assert.isTrue(user.active) + })) +}) +``` + +## Implementation Patterns + +The project includes comprehensive pattern documentation for future reference and consistency: + +### Pattern Directory +**Location**: `patterns/` +- **Purpose**: Detailed documentation of all implementation patterns used in the project +- **Usage**: Reference material for maintaining consistency and best practices + +### Available Patterns +- **http-api.md**: HTTP API definition and implementation patterns +- **layer-composition.md**: Layer-based dependency injection patterns +- **generic-testing.md**: General testing patterns with @effect/vitest + +## Notes +- This is an Effect Platform HTTP API migration of the original Express server +- Focus on type safety, observability, and error handling +- WebSocket functionality excluded (to be migrated separately) +- Uses hardcoded port configuration (no portfinder) \ No newline at end of file diff --git a/apps/server-new/package.json b/apps/server-new/package.json new file mode 100644 index 00000000..83dfb3ac --- /dev/null +++ b/apps/server-new/package.json @@ -0,0 +1,36 @@ +{ + "name": "server-new", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch ./src/index.ts", + "start": "node ./dist/index.js", + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "lint": "biome check", + "lint:fix": "biome check --write --unsafe", + "prisma": "prisma", + "prebuild": "prisma generate" + }, + "dependencies": { + "@effect/platform": "^0.90.0", + "@effect/platform-node": "^0.94.0", + "@graphprotocol/hypergraph": "workspace:*", + "@prisma/client": "^6.7.0", + "@privy-io/server-auth": "^1.26.0", + "cors": "^2.8.5", + "effect": "^3.17.3" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/node": "^24.1.0", + "prisma": "^6.7.0", + "tsx": "^4.19.0", + "tsup": "^8.4.0", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } +} diff --git a/apps/server-new/patterns/README.md b/apps/server-new/patterns/README.md new file mode 100644 index 00000000..6fec1556 --- /dev/null +++ b/apps/server-new/patterns/README.md @@ -0,0 +1,19 @@ +# Implementation Patterns + +This directory contains detailed documentation of implementation patterns used throughout the project. These patterns provide reusable solutions and best practices for Effect TypeScript development. + +## Patterns + +- **[http-api.md](./http-api.md)** - HTTP API definition and implementation patterns using Effect platform +- **[layer-composition.md](./layer-composition.md)** - Layer-based dependency injection and service composition patterns +- **[generic-testing.md](./generic-testing.md)** - General testing patterns with @effect/vitest and Effect ecosystem + +## Usage + +Each pattern document includes: +- Core concepts and principles +- Code examples from the implementation +- Best practices and guidelines +- Common pitfalls to avoid + +These patterns serve as reference material for future development and help maintain consistency across the codebase. \ No newline at end of file diff --git a/apps/server-new/patterns/generic-testing.md b/apps/server-new/patterns/generic-testing.md new file mode 100644 index 00000000..da848211 --- /dev/null +++ b/apps/server-new/patterns/generic-testing.md @@ -0,0 +1,365 @@ +# Generic Testing Patterns + +This document describes general testing patterns used in the project with @effect/vitest and Effect ecosystem testing approaches. + +## Core Testing Framework Pattern + +### 1. @effect/vitest Integration + +```typescript +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" + +describe("Feature Name", () => { + it.effect("should do something", () => + Effect.gen(function* () { + const result = yield* someEffectOperation() + assert.strictEqual(result, expectedValue) + }) + ) +}) +``` + +**Key Elements:** +- **Import from @effect/vitest**: `assert`, `describe`, `it` for Effect-aware testing +- **it.effect()**: Special test function for Effect-based tests +- **Effect.gen()**: Generator-based Effect composition +- **assert methods**: Use `assert.*` instead of `expect` for Effect tests + +### 2. Effect Test Structure Pattern + +```typescript +it.effect("descriptive test name", () => + Effect.gen(function* () { + // Arrange: Set up test data/state + const input = "test data" + + // Act: Perform the operation + const result = yield* operationUnderTest(input) + + // Assert: Verify the outcome + assert.strictEqual(result, "expected output") + }) +) +``` + +**Structure Benefits:** +- **Effect Composition**: Natural Effect chaining with generators +- **Error Handling**: Automatic Effect error propagation +- **Type Safety**: Full TypeScript integration with Effect types + +## Service Mocking Pattern + +### 1. Mock Service Creation + +```typescript +export const createMockConsole = () => { + const messages: Array = [] + + // Unsafe implementation (plain functions) + const unsafeConsole: Console.UnsafeConsole = { + log: (...args: ReadonlyArray) => { + messages.push(args.join(" ")) + }, + error: (...args: ReadonlyArray) => { + messages.push(`error: ${args.join(" ")}`) + }, + // ... all console methods + } + + // Effect wrapper (Effect-based interface) + const mockConsole: Console.Console = { + [Console.TypeId]: Console.TypeId, + log: (...args: ReadonlyArray) => + Effect.sync(() => unsafeConsole.log(...args)), + error: (...args: ReadonlyArray) => + Effect.sync(() => unsafeConsole.error(...args)), + // ... all console methods + unsafe: unsafeConsole + } + + return { mockConsole, messages } +} +``` + +**Mock Service Pattern:** +1. **State Capture**: Array/object to capture calls and data +2. **Dual Interface**: Both unsafe and Effect-based implementations +3. **Type Safety**: Implement complete service interface +4. **Test Utilities**: Return both mock and captured data + +### 2. Service Interface Implementation + +```typescript +const mockConsole: Console.Console = { + [Console.TypeId]: Console.TypeId, // ← Type identifier + log: (...args) => Effect.sync(() => ...), // ← Effect wrapper + unsafe: unsafeConsole // ← Direct access +} +``` + +**Implementation Requirements:** +- **Complete Interface**: Implement every method from service interface +- **Type Identifier**: Include service type ID for runtime identification +- **Effect Integration**: Wrap unsafe operations in `Effect.sync()` +- **Unsafe Access**: Provide direct access for performance-critical operations + +## Test Data Management Pattern + +### 1. Captured Data Pattern + +```typescript +export const createMockConsole = () => { + const messages: Array = [] // ← Captured data + + const unsafeConsole = { + log: (...args) => { + messages.push(args.join(" ")) // ← Capture call + } + } + + return { mockConsole, messages } // ← Return both mock and data +} +``` + +**Data Capture Benefits:** +- **Inspection**: Tests can verify what was called +- **Debugging**: Easy to see what happened during test execution +- **Assertions**: Test can assert on captured data + +### 2. Test State Management + +```typescript +describe("Console Testing", () => { + const { mockConsole, messages } = createMockConsole() + + it.effect("should capture log messages", () => + Effect.gen(function* () { + yield* Console.log("test message").pipe( + Effect.provide(Console.setConsole(mockConsole)) + ) + + assert.strictEqual(messages.length, 1) + assert.strictEqual(messages[0], "test message") + }) + ) +}) +``` + +**State Management Principles:** +- **Per-Test State**: Each test gets clean mock state +- **Service Provision**: Provide mock via Effect service system +- **Assertion Access**: Test code can inspect captured state + +## Effect Service Testing Pattern + +### 1. Service Provision in Tests + +```typescript +it.effect("should use provided service", () => + Effect.gen(function* () { + yield* Console.log("test") + }).pipe( + Effect.provide(Console.setConsole(mockConsole)) // ← Provide mock service + ) +) +``` + +**Service Provision Methods:** +- **Effect.provide()**: Provide service implementation to Effect +- **Layer-based**: Use layers for complex service dependencies +- **Direct provision**: Simple service replacement + +### 2. Service Replacement Pattern + +```typescript +// Replace default service with mock +Effect.provide(Console.setConsole(mockConsole)) + +// Replace default with custom implementation +Effect.provide(Logger.replace(Logger.defaultLogger, testLogger)) + +// Add service instance +Effect.provide(Logger.add(Logger.defaultLogger)) +``` + +## Assertion Patterns + +### 1. Effect-Specific Assertions + +```typescript +import { assert } from "@effect/vitest" + +// Value assertions +assert.strictEqual(actual, expected) +assert.deepStrictEqual(actualObject, expectedObject) + +// Boolean assertions +assert.isTrue(condition) +assert.isFalse(condition) + +// Existence assertions +assert.isDefined(value) +assert.isUndefined(value) +``` + +**Assertion Guidelines:** +- **Use assert, not expect**: @effect/vitest provides assert methods +- **Type-safe**: Assertions work with Effect type system +- **Clear Messages**: Provide descriptive failure messages + +### 2. Error Testing Pattern + +```typescript +it.effect("should handle errors", () => + Effect.gen(function* () { + const result = yield* Effect.flip(failingOperation()) + assert.isTrue(result instanceof ExpectedError) + }) +) +``` + +**Error Testing Approaches:** +- **Effect.flip()**: Convert failure to success for testing +- **Effect.either()**: Get Either for pattern matching +- **Try/Catch with Effects**: Use Effect error handling patterns + +## Test Organization Patterns + +### 1. Describe Block Structure + +```typescript +describe("Feature/Module Name", () => { + // Setup shared across tests + const sharedResource = createSharedResource() + + describe("specific functionality", () => { + // Nested describe for grouping related tests + + it.effect("should handle normal case", () => ...) + it.effect("should handle error case", () => ...) + }) +}) +``` + +**Organization Benefits:** +- **Logical Grouping**: Related tests grouped together +- **Shared Setup**: Common resources defined once +- **Clear Hierarchy**: Easy to understand test structure + +### 2. Test Naming Convention + +```typescript +// ✅ Good: Descriptive and specific +it.effect("GET /healthz returns 200 status with success message", () => ...) +it.effect("should capture console messages in test environment", () => ...) + +// ❌ Poor: Vague or implementation-focused +it.effect("test endpoint", () => ...) +it.effect("should work", () => ...) +``` + +**Naming Guidelines:** +- **Behavior-focused**: Describe what the system should do +- **Specific**: Include key details (HTTP method, expected outcome) +- **Action + Result**: What action produces what result + +## Test Utility Patterns + +### 1. Factory Functions for Test Resources + +```typescript +export const createMockConsole = () => { + // Resource creation logic + return { mockConsole, messages } +} + +export const createTestData = () => { + return { + validUser: { id: 1, name: "Test User" }, + invalidUser: { id: -1, name: "" } + } +} +``` + +**Factory Benefits:** +- **Reusability**: Same setup across multiple tests +- **Consistency**: Standardized test data/mocks +- **Encapsulation**: Hide complex setup logic + +### 2. Test Helper Functions + +```typescript +const waitFor = (condition: () => boolean, timeout = 1000) => + Effect.gen(function* () { + const start = Date.now() + while (!condition() && Date.now() - start < timeout) { + yield* Effect.sleep("10 millis") + } + if (!condition()) { + yield* Effect.fail(new Error("Condition not met within timeout")) + } + }) +``` + +## Environment-Specific Testing + +### 1. Test vs Production Separation + +```typescript +// Test environment detection +const isTest = process.env.NODE_ENV === "test" + +// Test-specific configuration +const testConfig = { + port: 0, // Random port + logLevel: "silent", // Reduce test output + timeout: 5000 // Shorter timeouts +} +``` + +### 2. Resource Cleanup Pattern + +```typescript +describe("Resource Tests", () => { + let resource: SomeResource + + beforeEach(() => { + resource = createResource() + }) + + afterEach(() => { + resource.cleanup() + }) + + it.effect("should use resource", () => + Effect.gen(function* () { + yield* useResource(resource) + }) + ) +}) +``` + +## Best Practices + +### 1. Test Independence +- **No Shared State**: Each test should be independent +- **Clean Mocks**: Reset mocks between tests +- **Isolated Resources**: Tests shouldn't affect each other + +### 2. Effect Integration +- **Use it.effect()**: For any test that uses Effect operations +- **Effect.gen()**: For readable async test code +- **Service provision**: Use Effect service system for dependencies + +### 3. Assertion Quality +- **Specific Assertions**: Test exact values, not just truthiness +- **Multiple Assertions**: Verify all important aspects +- **Good Error Messages**: Make test failures easy to understand + +### 4. Test Coverage +- **Happy Path**: Test normal successful operations +- **Error Cases**: Test failure scenarios +- **Edge Cases**: Test boundary conditions and unusual inputs + +This testing approach provides reliable, maintainable tests that integrate well with the Effect ecosystem while maintaining excellent error handling and type safety. \ No newline at end of file diff --git a/apps/server-new/patterns/http-api.md b/apps/server-new/patterns/http-api.md new file mode 100644 index 00000000..667c1ed7 --- /dev/null +++ b/apps/server-new/patterns/http-api.md @@ -0,0 +1,233 @@ +# HTTP API Patterns + +This document describes the HTTP API implementation patterns used in this project with Effect's platform abstractions. + +## Core Pattern: Declarative API Definition + +### 1. Three-Layer API Structure + +```typescript +// Layer 1: Endpoint Definition +const statusEndpoint = HttpApiEndpoint + .get("status", "/healthz") + .addSuccess(Schema.String) + +// Layer 2: Group Definition +const healthGroup = HttpApiGroup + .make("Health") + .add(statusEndpoint) + +// Layer 3: API Definition +const todosApi = HttpApi + .make("TodosApi") + .add(healthGroup) +``` + +**Key Principles:** +- **Separation of Concerns**: Endpoints, groups, and APIs are defined separately +- **Composability**: Groups can contain multiple endpoints, APIs can contain multiple groups +- **Type Safety**: Schema definitions ensure request/response type safety +- **Declarative**: API structure is defined, not implemented + +### 2. Endpoint Definition Pattern + +```typescript +const statusEndpoint = HttpApiEndpoint + .get("status", "/healthz") // HTTP method and path + .addSuccess(Schema.String) // Response schema +``` + +**Pattern Elements:** +- **Method + Name + Path**: `get("status", "/healthz")` +- **Response Schema**: `.addSuccess(Schema.String)` for type-safe responses +- **Extensible**: Can add `.setPayload()`, `.setHeaders()`, `.addError()` as needed + +### 3. Group Organization Pattern + +```typescript +const healthGroup = HttpApiGroup + .make("Health") // Group name + .add(statusEndpoint) // Add endpoints +``` + +**Benefits:** +- **Logical Grouping**: Related endpoints grouped together +- **Namespace Organization**: Clear API structure +- **Handler Grouping**: Implementations grouped by API groups + +### 4. API Composition Pattern + +```typescript +const todosApi = HttpApi + .make("TodosApi") // API name + .add(healthGroup) // Add groups +``` + +**Scalability:** +- **Multiple Groups**: Can add todos, users, auth groups +- **Single Source of Truth**: Complete API definition in one place +- **Client Generation**: Same definition can generate typed clients + +## Implementation Pattern: Handler Definition + +### 1. Group Handler Implementation + +```typescript +export const healthLive = HttpApiBuilder.group( + todosApi, // API reference + "Health", // Group name + (handlers) => // Handler factory + handlers.handle( + "status", // Endpoint name + () => Effect.succeed("Server is running successfully") + ) +) +``` + +**Pattern Elements:** +- **API Reference**: Links handler to specific API definition +- **Group Name**: Must match the group name in API definition +- **Handler Factory**: Function that receives handlers object +- **Effect-based**: All handlers return Effects for composability + +### 2. Handler Function Pattern + +```typescript +handlers.handle( + "status", // Endpoint name (must match) + () => Effect.succeed("...") // Handler implementation +) +``` + +**Key Aspects:** +- **Name Matching**: Handler name must match endpoint name +- **Effect Return**: Always return an Effect for error handling +- **Pure Functions**: Handlers should be pure (no side effects) +- **Type Safety**: Return type must match endpoint schema + +## Server Configuration Pattern + +### 1. Layer Composition for Server + +```typescript +// API Implementation Layer +const apiLive = HttpApiBuilder.api(todosApi).pipe( + Layer.provide(healthLive) // Provide handler implementations +) + +// Server Layer +export const serverLive = HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), // Provide API implementation + HttpServer.withLogAddress, // Add address logging + Layer.provide(BunHttpServer.layer({ port: 3000 })) // Platform server +) +``` + +**Layer Stack (bottom to top):** +1. **Platform Layer**: `BunHttpServer.layer()` - Physical server +2. **Logging Layer**: `HttpServer.withLogAddress` - Address logging +3. **API Layer**: `apiLive` - API implementation with handlers +4. **Server Layer**: `HttpApiBuilder.serve()` - HTTP service + +### 2. Platform Abstraction Pattern + +```typescript +// Production: Bun Runtime +Layer.provide(BunHttpServer.layer({ port: 3000 })) + +// Testing: Node.js Runtime +Layer.provide(NodeHttpServer.layer(createServer, { port: 0 })) +``` + +**Benefits:** +- **Runtime Independence**: Same API works on different runtimes +- **Test Isolation**: Different server config for testing +- **Performance**: Use optimal runtime for each environment + +## Application Entry Pattern + +### 1. Simple Launch Pattern + +```typescript +if (import.meta.main) { + Layer.launch(serverLive).pipe(BunRuntime.runMain) +} +``` + +**Pattern Elements:** +- **Entry Guard**: `import.meta.main` prevents execution when imported +- **Layer Launch**: `Layer.launch()` starts the server layer +- **Runtime Integration**: `.pipe(BunRuntime.runMain)` for Bun runtime + +### 2. Layer-Based Architecture + +```typescript +// Dependency flow: +BunRuntime.runMain +├── Layer.launch(serverLive) + ├── HttpApiBuilder.serve() + ├── HttpServer.withLogAddress + ├── apiLive + │ └── healthLive (handlers) + └── BunHttpServer.layer() +``` + +**Advantages:** +- **Dependency Injection**: Automatic service resolution +- **Resource Management**: Automatic cleanup on shutdown +- **Testability**: Easy to swap layers for testing + +## Schema Integration Pattern + +### 1. Type-Safe Responses + +```typescript +.addSuccess(Schema.String) // Response will be string +``` + +**Type Flow:** +1. Schema defines the response type +2. Handler must return matching type +3. Client receives typed response +4. Automatic serialization/deserialization + +### 2. Future Extensions + +```typescript +// Request payload +.setPayload(Schema.Struct({ name: Schema.String })) + +// Error responses +.addError(UserNotFound, { status: 404 }) + +// URL parameters +.setPath(Schema.Struct({ id: Schema.NumberFromString })) +``` + +## Best Practices + +### 1. Naming Conventions +- **Endpoints**: Descriptive names (`status`, `getUser`, `createTodo`) +- **Groups**: Noun-based (`Health`, `Users`, `Todos`) +- **APIs**: Project-based (`TodosApi`, `UserManagementApi`) + +### 2. File Organization +``` +src/http/ +├── api.ts # API definitions only +├── handlers/ # Handler implementations +│ └── health.ts # Group-specific handlers +└── server.ts # Server configuration +``` + +### 3. Separation of Concerns +- **api.ts**: Pure definitions, no implementation +- **handlers/*.ts**: Implementation logic, one file per group +- **server.ts**: Layer composition and configuration + +### 4. Type Safety +- Always use Schema for request/response types +- Let TypeScript infer handler types from endpoint schemas +- No `any` types in HTTP layer + +This pattern provides a scalable, type-safe, and testable HTTP API architecture using Effect's declarative approach. \ No newline at end of file diff --git a/apps/server-new/patterns/layer-composition.md b/apps/server-new/patterns/layer-composition.md new file mode 100644 index 00000000..3f82a2c1 --- /dev/null +++ b/apps/server-new/patterns/layer-composition.md @@ -0,0 +1,428 @@ +# Layer Composition Patterns + +This document describes the Layer composition patterns used for dependency injection, service provision, and resource management in the Effect ecosystem. + +## Core Pattern: Layer-Based Architecture + +### 1. Layer Dependency Flow + +```typescript +// Production Server Layer Stack +export const serverLive = HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), // ← API implementation + HttpServer.withLogAddress, // ← Logging middleware + Layer.provide(BunHttpServer.layer({ port: 3000 })) // ← Platform server +) + +// API Layer depends on handlers +const apiLive = HttpApiBuilder.api(todosApi).pipe( + Layer.provide(healthLive) // ← Handler implementations +) +``` + +**Dependency Graph:** +``` +serverLive +├── HttpApiBuilder.serve() (top layer) +├── HttpServer.withLogAddress (middleware) +├── apiLive (API implementation) +│ └── healthLive (handlers) +└── BunHttpServer.layer() (platform) +``` + +### 2. Layer Types and Purposes + +| Layer Type | Purpose | Example | +|------------|---------|---------| +| **Platform Layer** | Physical runtime/server | `BunHttpServer.layer()` | +| **Service Layer** | Business logic | `healthLive` handlers | +| **Infrastructure Layer** | Cross-cutting concerns | `HttpServer.withLogAddress` | +| **Composition Layer** | Orchestration | `HttpApiBuilder.serve()` | + +## Provision Patterns + +### 1. Layer.provide() - Dependency Replacement + +```typescript +const apiLive = HttpApiBuilder.api(todosApi).pipe( + Layer.provide(healthLive) // Provides handlers to API +) +``` + +**Usage:** +- **Replaces Dependencies**: API needs handlers, we provide them +- **Bottom-Up**: Lower layers provide services to upper layers +- **Type Safety**: Compiler ensures all dependencies are satisfied + +### 2. Layer.provideMerge() - Service Extension + +```typescript +const testServerWithMockConsole = testServerLive.pipe( + Layer.provide(Logger.add(Logger.defaultLogger)), + Layer.provide(Console.setConsole(mockConsole)), + Layer.provideMerge(FetchHttpClient.layer) // ← Merge additional service +) +``` + +**When to Use:** +- **Add Services**: When you need to add services without replacing existing ones +- **Testing**: Add test-specific services (HTTP client, mock services) +- **Enhancement**: Extend functionality without breaking existing dependencies + +### 3. Middleware Pattern with Layers + +```typescript +export const serverLive = HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), // Core functionality + HttpServer.withLogAddress, // ← Middleware: adds logging + Layer.provide(BunHttpServer.layer({ port: 3000 })) +) +``` + +**Middleware Characteristics:** +- **Non-intrusive**: Doesn't change core API behavior +- **Composable**: Can stack multiple middleware +- **Order-dependent**: Middleware order matters + +## Environment-Specific Layer Patterns + +### 1. Production vs Test Layer Separation + +```typescript +// Production Layer (src/http/server.ts) +export const serverLive = HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), + HttpServer.withLogAddress, + Layer.provide(BunHttpServer.layer({ port: 3000 })) // ← Fixed port +) + +// Test Layer (test/utils/testServer.ts) +export const testServerLive = HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), // ← Same API implementation + HttpServer.withLogAddress, // ← Same logging + Layer.provide(NodeHttpServer.layer(createServer, { port: 0 })) // ← Random port +) +``` + +**Pattern Benefits:** +- **Same Logic**: Core API logic identical between environments +- **Different Infrastructure**: Platform-specific implementations +- **Test Isolation**: Random ports prevent test conflicts + +### 2. Service Configuration Pattern + +```typescript +// Service Creation +const mockConsole = createMockConsole() + +// Service Provision +const testServerWithMockConsole = testServerLive.pipe( + Layer.provide(Logger.add(Logger.defaultLogger)), // Add logger + Layer.provide(Console.setConsole(mockConsole)), // Replace console + Layer.provideMerge(FetchHttpClient.layer) // Add HTTP client +) +``` + +**Configuration Strategies:** +- **Add**: `Logger.add()` - Add new service instances +- **Replace**: `Console.setConsole()` - Replace default implementations +- **Merge**: `Layer.provideMerge()` - Extend service availability + +## Service Factory Pattern + +### 1. Factory Function for Test Services + +```typescript +export const createTestHttpServer = () => { + // Create services + const { mockConsole, messages } = createMockConsole() + + // Compose layer + const testServerWithMockConsole = testServerLive.pipe( + Layer.provide(Logger.add(Logger.defaultLogger)), + Layer.provide(Console.setConsole(mockConsole)), + Layer.provideMerge(FetchHttpClient.layer) + ) + + // Utility functions + const getServerUrl = () => { /* extract URL from logs */ } + + // Return composed services + return { + testServerLayer: testServerWithMockConsole, + getServerUrl, + messages + } +} +``` + +**Factory Pattern Benefits:** +- **Encapsulation**: Hides service creation complexity +- **Reusability**: Same factory for all HTTP tests +- **Consistency**: Standardized test server configuration +- **Flexibility**: Returns both layer and utilities + +### 2. Service Composition in Factories + +```typescript +// Inside createTestHttpServer() +const testServerWithMockConsole = testServerLive.pipe( + Layer.provide(Logger.add(Logger.defaultLogger)), // ← Service 1 + Layer.provide(Console.setConsole(mockConsole)), // ← Service 2 + Layer.provideMerge(FetchHttpClient.layer) // ← Service 3 +) +``` + +**Composition Order:** +1. **Base Layer**: `testServerLive` (server + API) +2. **Logging Services**: Logger and Console for testing +3. **HTTP Client**: FetchHttpClient for making requests + +## Application Launch Pattern + +### 1. Layer.launch() Pattern + +```typescript +// Application Entry Point +if (import.meta.main) { + Layer.launch(serverLive).pipe(BunRuntime.runMain) +} +``` + +**Launch Process:** +1. **Layer.launch()**: Starts the layer and keeps it running +2. **BunRuntime.runMain**: Integrates with Bun runtime lifecycle +3. **Resource Management**: Automatic cleanup on process termination + +### 2. Runtime Integration Pattern + +```typescript +// Production: Bun Runtime +Layer.launch(serverLive).pipe(BunRuntime.runMain) + +// Test: Effect Runtime with layer() helper +layer(testServerLayer)((it) => { + // Tests run with layer active +}) +``` + +**Runtime Differences:** +- **Production**: Long-running process with BunRuntime +- **Testing**: Scoped execution with automatic cleanup + +## Configuration-Driven Layer Patterns + +### 1. Layer.unwrapEffect() with Configuration + +```typescript +import { Config, Effect, Layer } from "effect" + +// Configuration definition +export const serverPortConfig = Config.port("PORT").pipe( + Config.withDefault(3000) +) + +// Layer that depends on configuration +export const serverLive = Layer.unwrapEffect( + Effect.gen(function* () { + const port = yield* serverPortConfig // ← Resolve config first + return HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), + HttpServer.withLogAddress, + Layer.provide(BunHttpServer.layer({ port })) // ← Use resolved port + ) + }) +) +``` + +**Layer.unwrapEffect() Pattern:** +- **Configuration Resolution**: Resolves Effect-based configuration during layer creation +- **Dynamic Layer Construction**: Creates layers based on runtime configuration +- **Type Safety**: Full Effect type checking for configuration errors +- **Fail-Fast**: Configuration errors surface during layer initialization + +### 2. Configuration Flow with unwrapEffect + +``` +Environment Variable → Config.port() → Layer.unwrapEffect → Server Layer + ↓ ↓ ↓ ↓ + PORT=8080 Validation [1-65535] Effect.gen BunHttpServer + ↓ ↓ + Config.withDefault(3000) port value +``` + +**Flow Characteristics:** +1. **Environment Reading**: `Config.port("PORT")` reads and validates PORT +2. **Default Application**: `Config.withDefault(3000)` provides fallback +3. **Effect Resolution**: `Layer.unwrapEffect` resolves configuration Effect +4. **Layer Construction**: Dynamic layer creation with resolved values + +### 3. Configuration Error Handling + +```typescript +// Configuration errors propagate through Effect system +const serverLive = Layer.unwrapEffect( + Effect.gen(function* () { + const port = yield* serverPortConfig // ← Can fail with ConfigError + return HttpApiBuilder.serve().pipe(/* ... */) + }) +) + +// Error types from Config.port() +type ConfigErrors = + | ConfigError.InvalidData // PORT=invalid + | ConfigError.InvalidData // PORT=70000 (out of range) +``` + +**Error Handling Benefits:** +- **Fail-Fast**: Invalid configuration prevents server startup +- **Type-Safe Errors**: ConfigError types are known at compile time +- **Clear Messages**: Effect's Config API provides descriptive error messages +- **Effect Integration**: Errors flow naturally through Effect error system + +### 4. Advanced Configuration Patterns + +```typescript +// Multiple configuration values +const serverConfig = Effect.gen(function* () { + const port = yield* Config.port("PORT").pipe(Config.withDefault(3000)) + const host = yield* Config.string("HOST").pipe(Config.withDefault("0.0.0.0")) + return { port, host } +}) + +// Configuration-dependent layer with multiple values +export const serverLive = Layer.unwrapEffect( + Effect.gen(function* () { + const { port, host } = yield* serverConfig + return HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), + HttpServer.withLogAddress, + Layer.provide(BunHttpServer.layer({ port, hostname: host })) + ) + }) +) +``` + +## Advanced Layer Patterns + +### 1. Platform Abstraction Layer + +```typescript +// Production (configurable port) +export const serverLive = Layer.unwrapEffect( + Effect.gen(function* () { + const port = yield* serverPortConfig + return HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), + HttpServer.withLogAddress, + Layer.provide(BunHttpServer.layer({ port })) // ← Dynamic port + ) + }) +) + +// Testing (fixed behavior) +Layer.provide(NodeHttpServer.layer(createServer, { port: 0 })) +``` + +**Abstraction Benefits:** +- **Same Interface**: Both provide HTTP server capability +- **Different Implementations**: Production configurable, testing fixed +- **Environment Separation**: Clear distinction between runtime environments + +### 2. Service Replacement Pattern + +```typescript +// Default console → Mock console +Layer.provide(Console.setConsole(mockConsole)) + +// Default logger → Test logger +Layer.provide(Logger.add(Logger.defaultLogger)) +``` + +**Replacement Use Cases:** +- **Testing**: Replace I/O services with mocks +- **Configuration**: Replace default with environment-specific +- **Debugging**: Replace with instrumented versions + +## Layer Composition Best Practices + +### 1. Dependency Direction +```typescript +// ✅ Correct: Dependencies flow upward +const serverLive = HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), // Server depends on API + Layer.provide(BunHttpServer.layer()) // Server depends on platform +) + +// ❌ Incorrect: Circular dependencies +const apiLive = HttpApiBuilder.api(todosApi).pipe( + Layer.provide(serverLive) // API cannot depend on server +) +``` + +### 2. Layer Naming Conventions +- **`*Live`**: Concrete implementations (`healthLive`, `apiLive`) +- **`*Layer`**: Infrastructure layers (`testServerLayer`) +- **`*Mock`**: Test doubles (`mockConsole`) + +### 3. File Organization +``` +src/ +├── http/ +│ ├── server.ts # Production layer composition +│ └── handlers/ # Service implementations +└── index.ts # Application launch + +test/utils/ +├── testServer.ts # Test layer composition +└── httpTestUtils.ts # Test service factories +``` + +### 4. Layer Composition Principles +- **Single Responsibility**: Each layer has one purpose +- **Composability**: Layers can be combined in different ways +- **Testability**: Easy to substitute test implementations +- **Resource Safety**: Automatic cleanup and error handling +- **Configuration-Driven**: Use Layer.unwrapEffect for runtime configuration +- **Fail-Fast Configuration**: Invalid configuration prevents layer initialization + +### 5. Layer.unwrapEffect Best Practices + +```typescript +// ✅ Good: Simple configuration resolution +export const serverLive = Layer.unwrapEffect( + Effect.gen(function* () { + const port = yield* serverPortConfig + return HttpApiBuilder.serve().pipe(/* ... */) + }) +) + +// ✅ Good: Multiple configuration values +export const serverLive = Layer.unwrapEffect( + Effect.gen(function* () { + const config = yield* Effect.all({ + port: serverPortConfig, + host: serverHostConfig + }) + return HttpApiBuilder.serve().pipe(/* ... */) + }) +) + +// ❌ Avoid: Complex logic in unwrapEffect +export const serverLive = Layer.unwrapEffect( + Effect.gen(function* () { + const port = yield* serverPortConfig + // Avoid heavy computation or complex business logic here + const processedData = yield* heavyProcessing(port) + return HttpApiBuilder.serve().pipe(/* ... */) + }) +) +``` + +**unwrapEffect Guidelines:** +- **Configuration Only**: Use for resolving configuration values +- **Keep Simple**: Avoid complex business logic in unwrapEffect +- **Error Handling**: Let configuration errors bubble up naturally +- **Type Safety**: Trust Effect's configuration validation + +This layer-based approach provides powerful dependency injection, clear separation of concerns, excellent testability, and flexible configuration management while maintaining type safety throughout the application. \ No newline at end of file diff --git a/apps/server-new/prisma/migrations/20241111122738_init/migration.sql b/apps/server-new/prisma/migrations/20241111122738_init/migration.sql new file mode 100644 index 00000000..3eb1ca6a --- /dev/null +++ b/apps/server-new/prisma/migrations/20241111122738_init/migration.sql @@ -0,0 +1,31 @@ +-- CreateTable +CREATE TABLE "SpaceEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "event" TEXT NOT NULL, + "spaceId" TEXT NOT NULL, + CONSTRAINT "SpaceEvent_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Space" ( + "id" TEXT NOT NULL PRIMARY KEY +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL PRIMARY KEY +); + +-- CreateTable +CREATE TABLE "_AccountToSpace" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_AccountToSpace_A_fkey" FOREIGN KEY ("A") REFERENCES "Account" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_AccountToSpace_B_fkey" FOREIGN KEY ("B") REFERENCES "Space" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "_AccountToSpace_AB_unique" ON "_AccountToSpace"("A", "B"); + +-- CreateIndex +CREATE INDEX "_AccountToSpace_B_index" ON "_AccountToSpace"("B"); diff --git a/apps/server-new/prisma/migrations/20241113083927_introduce_counter_and_state_to_space_events/migration.sql b/apps/server-new/prisma/migrations/20241113083927_introduce_counter_and_state_to_space_events/migration.sql new file mode 100644 index 00000000..40c7bd15 --- /dev/null +++ b/apps/server-new/prisma/migrations/20241113083927_introduce_counter_and_state_to_space_events/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - Added the required column `counter` to the `SpaceEvent` table without a default value. This is not possible if the table is not empty. + - Added the required column `state` to the `SpaceEvent` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_SpaceEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "event" TEXT NOT NULL, + "state" TEXT NOT NULL, + "counter" INTEGER NOT NULL, + "spaceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SpaceEvent_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_SpaceEvent" ("event", "id", "spaceId") SELECT "event", "id", "spaceId" FROM "SpaceEvent"; +DROP TABLE "SpaceEvent"; +ALTER TABLE "new_SpaceEvent" RENAME TO "SpaceEvent"; +CREATE UNIQUE INDEX "SpaceEvent_spaceId_counter_key" ON "SpaceEvent"("spaceId", "counter"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/apps/server-new/prisma/migrations/20241114170708_add_invitation/migration.sql b/apps/server-new/prisma/migrations/20241114170708_add_invitation/migration.sql new file mode 100644 index 00000000..9c67be40 --- /dev/null +++ b/apps/server-new/prisma/migrations/20241114170708_add_invitation/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "Invitation" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceId" TEXT NOT NULL, + "accountAddress" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Invitation_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Invitation_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/apps/server-new/prisma/migrations/20241116161206_extend_invitation/migration.sql b/apps/server-new/prisma/migrations/20241116161206_extend_invitation/migration.sql new file mode 100644 index 00000000..c7ad34dc --- /dev/null +++ b/apps/server-new/prisma/migrations/20241116161206_extend_invitation/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - Added the required column `inviteeAccountAddress` to the `Invitation` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Invitation" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceId" TEXT NOT NULL, + "accountAddress" TEXT NOT NULL, + "inviteeAccountAddress" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Invitation_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Invitation_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Invitation" ("accountAddress", "createdAt", "id", "spaceId") SELECT "accountAddress", "createdAt", "id", "spaceId" FROM "Invitation"; +DROP TABLE "Invitation"; +ALTER TABLE "new_Invitation" RENAME TO "Invitation"; +CREATE UNIQUE INDEX "Invitation_spaceId_inviteeAccountAddress_key" ON "Invitation"("spaceId", "inviteeAccountAddress"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/apps/server-new/prisma/migrations/20241117202441_add_key/migration.sql b/apps/server-new/prisma/migrations/20241117202441_add_key/migration.sql new file mode 100644 index 00000000..40ce36bf --- /dev/null +++ b/apps/server-new/prisma/migrations/20241117202441_add_key/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "SpaceKey" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SpaceKey_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "SpaceKeyBox" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceKeyId" TEXT NOT NULL, + "accountAddress" TEXT NOT NULL, + "ciphertext" TEXT NOT NULL, + "nonce" TEXT NOT NULL, + "authorPublicKey" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SpaceKeyBox_spaceKeyId_fkey" FOREIGN KEY ("spaceKeyId") REFERENCES "SpaceKey" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "SpaceKeyBox_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/apps/server-new/prisma/migrations/20241122132737_add_update/migration.sql b/apps/server-new/prisma/migrations/20241122132737_add_update/migration.sql new file mode 100644 index 00000000..b7d98cf3 --- /dev/null +++ b/apps/server-new/prisma/migrations/20241122132737_add_update/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "Update" ( + "spaceId" TEXT NOT NULL, + "clock" INTEGER NOT NULL, + "content" BLOB NOT NULL, + + PRIMARY KEY ("spaceId", "clock"), + CONSTRAINT "Update_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/apps/server-new/prisma/migrations/20241128184625_add_identity/migration.sql b/apps/server-new/prisma/migrations/20241128184625_add_identity/migration.sql new file mode 100644 index 00000000..0b475fc2 --- /dev/null +++ b/apps/server-new/prisma/migrations/20241128184625_add_identity/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "Identity" ( + "accountAddress" TEXT NOT NULL, + "ciphertext" TEXT NOT NULL, + "nonce" TEXT NOT NULL, + "signaturePublicKey" TEXT NOT NULL, + "encryptionPublicKey" TEXT NOT NULL, + "accountProof" TEXT NOT NULL, + "keyProof" TEXT NOT NULL, + + PRIMARY KEY ("accountAddress", "nonce"), + CONSTRAINT "Identity_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/apps/server-new/prisma/migrations/20241212164639_add_session_nonce_token/migration.sql b/apps/server-new/prisma/migrations/20241212164639_add_session_nonce_token/migration.sql new file mode 100644 index 00000000..2a501af8 --- /dev/null +++ b/apps/server-new/prisma/migrations/20241212164639_add_session_nonce_token/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "sessionNonce" TEXT; +ALTER TABLE "Account" ADD COLUMN "sessionToken" TEXT; +ALTER TABLE "Account" ADD COLUMN "sessionTokenExpires" DATETIME; diff --git a/apps/server-new/prisma/migrations/20241212184317_add_account_token_index/migration.sql b/apps/server-new/prisma/migrations/20241212184317_add_account_token_index/migration.sql new file mode 100644 index 00000000..a72cc7b4 --- /dev/null +++ b/apps/server-new/prisma/migrations/20241212184317_add_account_token_index/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "Account_sessionToken_idx" ON "Account"("sessionToken"); diff --git a/apps/server-new/prisma/migrations/20250129220359_add_update_signature/migration.sql b/apps/server-new/prisma/migrations/20250129220359_add_update_signature/migration.sql new file mode 100644 index 00000000..b4b7a68e --- /dev/null +++ b/apps/server-new/prisma/migrations/20250129220359_add_update_signature/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - Added the required column `accountAddress` to the `Update` table without a default value. This is not possible if the table is not empty. + - Added the required column `updateId` to the `Update` table without a default value. This is not possible if the table is not empty. + - Added the required column `signatureHex` to the `Update` table without a default value. This is not possible if the table is not empty. + - Added the required column `signatureRecovery` to the `Update` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Update" ( + "spaceId" TEXT NOT NULL, + "clock" INTEGER NOT NULL, + "content" BLOB NOT NULL, + "accountAddress" TEXT NOT NULL, + "signatureHex" TEXT NOT NULL, + "signatureRecovery" INTEGER NOT NULL, + "updateId" TEXT NOT NULL, + + PRIMARY KEY ("spaceId", "clock"), + CONSTRAINT "Update_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Update_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Update" ("clock", "content", "spaceId") SELECT "clock", "content", "spaceId" FROM "Update"; +DROP TABLE "Update"; +ALTER TABLE "new_Update" RENAME TO "Update"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/apps/server-new/prisma/migrations/20250428155829_add_inboxes/migration.sql b/apps/server-new/prisma/migrations/20250428155829_add_inboxes/migration.sql new file mode 100644 index 00000000..1e7419f0 --- /dev/null +++ b/apps/server-new/prisma/migrations/20250428155829_add_inboxes/migration.sql @@ -0,0 +1,50 @@ +-- CreateTable +CREATE TABLE "SpaceInbox" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceId" TEXT NOT NULL, + "isPublic" BOOLEAN NOT NULL, + "authPolicy" TEXT NOT NULL, + "encryptionPublicKey" TEXT NOT NULL, + "encryptedSecretKey" TEXT NOT NULL, + "spaceEventId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SpaceInbox_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "SpaceInbox_spaceEventId_fkey" FOREIGN KEY ("spaceEventId") REFERENCES "SpaceEvent" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "SpaceInboxMessage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceInboxId" TEXT NOT NULL, + "ciphertext" TEXT NOT NULL, + "signatureHex" TEXT, + "signatureRecovery" INTEGER, + "authorAccountAddress" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SpaceInboxMessage_spaceInboxId_fkey" FOREIGN KEY ("spaceInboxId") REFERENCES "SpaceInbox" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AccountInbox" ( + "id" TEXT NOT NULL PRIMARY KEY, + "accountAddress" TEXT NOT NULL, + "isPublic" BOOLEAN NOT NULL, + "authPolicy" TEXT NOT NULL, + "encryptionPublicKey" TEXT NOT NULL, + "signatureHex" TEXT NOT NULL, + "signatureRecovery" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "AccountInbox_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AccountInboxMessage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "accountInboxId" TEXT NOT NULL, + "ciphertext" TEXT NOT NULL, + "signatureHex" TEXT, + "signatureRecovery" INTEGER, + "authorAccountAddress" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "AccountInboxMessage_accountInboxId_fkey" FOREIGN KEY ("accountInboxId") REFERENCES "AccountInbox" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/apps/server-new/prisma/migrations/20250611173316_introduce_connect/migration.sql b/apps/server-new/prisma/migrations/20250611173316_introduce_connect/migration.sql new file mode 100644 index 00000000..35cd3579 --- /dev/null +++ b/apps/server-new/prisma/migrations/20250611173316_introduce_connect/migration.sql @@ -0,0 +1,191 @@ +/* + Warnings: + + - You are about to drop the `Identity` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_AccountToSpace` table. If the table is not empty, all the data it contains will be lost. + - The primary key for the `Account` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `id` on the `Account` table. All the data in the column will be lost. + - You are about to drop the column `sessionNonce` on the `Account` table. All the data in the column will be lost. + - You are about to drop the column `sessionToken` on the `Account` table. All the data in the column will be lost. + - You are about to drop the column `sessionTokenExpires` on the `Account` table. All the data in the column will be lost. + - Added the required column `address` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `connectAccountProof` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `connectAddress` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `connectCiphertext` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `connectEncryptionPublicKey` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `connectKeyProof` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `connectNonce` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `connectSignaturePublicKey` to the `Account` table without a default value. This is not possible if the table is not empty. + - Added the required column `infoAuthorAddress` to the `Space` table without a default value. This is not possible if the table is not empty. + - Added the required column `infoContent` to the `Space` table without a default value. This is not possible if the table is not empty. + - Added the required column `infoSignatureHex` to the `Space` table without a default value. This is not possible if the table is not empty. + - Added the required column `infoSignatureRecovery` to the `Space` table without a default value. This is not possible if the table is not empty. + - Added the required column `name` to the `Space` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "_AccountToSpace_B_index"; + +-- DropIndex +DROP INDEX "_AccountToSpace_AB_unique"; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "Identity"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "_AccountToSpace"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "AppIdentity" ( + "address" TEXT NOT NULL PRIMARY KEY, + "ciphertext" TEXT NOT NULL, + "nonce" TEXT NOT NULL, + "signaturePublicKey" TEXT NOT NULL, + "encryptionPublicKey" TEXT NOT NULL, + "accountProof" TEXT NOT NULL, + "keyProof" TEXT NOT NULL, + "accountAddress" TEXT NOT NULL, + "appId" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "sessionTokenExpires" DATETIME NOT NULL, + CONSTRAINT "AppIdentity_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "InvitationTargetApp" ( + "id" TEXT NOT NULL PRIMARY KEY, + "invitationId" TEXT NOT NULL, + CONSTRAINT "InvitationTargetApp_invitationId_fkey" FOREIGN KEY ("invitationId") REFERENCES "Invitation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_space-members" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_space-members_A_fkey" FOREIGN KEY ("A") REFERENCES "Account" ("address") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_space-members_B_fkey" FOREIGN KEY ("B") REFERENCES "Space" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_AppIdentityToSpace" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_AppIdentityToSpace_A_fkey" FOREIGN KEY ("A") REFERENCES "AppIdentity" ("address") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_AppIdentityToSpace_B_fkey" FOREIGN KEY ("B") REFERENCES "Space" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Account" ( + "address" TEXT NOT NULL PRIMARY KEY, + "connectAddress" TEXT NOT NULL, + "connectCiphertext" TEXT NOT NULL, + "connectNonce" TEXT NOT NULL, + "connectSignaturePublicKey" TEXT NOT NULL, + "connectEncryptionPublicKey" TEXT NOT NULL, + "connectAccountProof" TEXT NOT NULL, + "connectKeyProof" TEXT NOT NULL +); +DROP TABLE "Account"; +ALTER TABLE "new_Account" RENAME TO "Account"; +CREATE UNIQUE INDEX "Account_connectAddress_key" ON "Account"("connectAddress"); +CREATE TABLE "new_AccountInbox" ( + "id" TEXT NOT NULL PRIMARY KEY, + "accountAddress" TEXT NOT NULL, + "isPublic" BOOLEAN NOT NULL, + "authPolicy" TEXT NOT NULL, + "encryptionPublicKey" TEXT NOT NULL, + "signatureHex" TEXT NOT NULL, + "signatureRecovery" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "AccountInbox_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_AccountInbox" ("accountAddress", "authPolicy", "createdAt", "encryptionPublicKey", "id", "isPublic", "signatureHex", "signatureRecovery") SELECT "accountAddress", "authPolicy", "createdAt", "encryptionPublicKey", "id", "isPublic", "signatureHex", "signatureRecovery" FROM "AccountInbox"; +DROP TABLE "AccountInbox"; +ALTER TABLE "new_AccountInbox" RENAME TO "AccountInbox"; +CREATE TABLE "new_Invitation" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceId" TEXT NOT NULL, + "accountAddress" TEXT NOT NULL, + "inviteeAccountAddress" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Invitation_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Invitation_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Invitation" ("accountAddress", "createdAt", "id", "inviteeAccountAddress", "spaceId") SELECT "accountAddress", "createdAt", "id", "inviteeAccountAddress", "spaceId" FROM "Invitation"; +DROP TABLE "Invitation"; +ALTER TABLE "new_Invitation" RENAME TO "Invitation"; +CREATE UNIQUE INDEX "Invitation_spaceId_inviteeAccountAddress_key" ON "Invitation"("spaceId", "inviteeAccountAddress"); +CREATE TABLE "new_Space" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "infoContent" BLOB NOT NULL, + "infoAuthorAddress" TEXT NOT NULL, + "infoSignatureHex" TEXT NOT NULL, + "infoSignatureRecovery" INTEGER NOT NULL, + CONSTRAINT "Space_infoAuthorAddress_fkey" FOREIGN KEY ("infoAuthorAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Space" ("id") SELECT "id" FROM "Space"; +DROP TABLE "Space"; +ALTER TABLE "new_Space" RENAME TO "Space"; +CREATE TABLE "new_SpaceKeyBox" ( + "id" TEXT NOT NULL PRIMARY KEY, + "spaceKeyId" TEXT NOT NULL, + "ciphertext" TEXT NOT NULL, + "nonce" TEXT NOT NULL, + "authorPublicKey" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "accountAddress" TEXT NOT NULL, + "appIdentityAddress" TEXT, + CONSTRAINT "SpaceKeyBox_spaceKeyId_fkey" FOREIGN KEY ("spaceKeyId") REFERENCES "SpaceKey" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "SpaceKeyBox_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "SpaceKeyBox_appIdentityAddress_fkey" FOREIGN KEY ("appIdentityAddress") REFERENCES "AppIdentity" ("address") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_SpaceKeyBox" ("accountAddress", "authorPublicKey", "ciphertext", "createdAt", "id", "nonce", "spaceKeyId") SELECT "accountAddress", "authorPublicKey", "ciphertext", "createdAt", "id", "nonce", "spaceKeyId" FROM "SpaceKeyBox"; +DROP TABLE "SpaceKeyBox"; +ALTER TABLE "new_SpaceKeyBox" RENAME TO "SpaceKeyBox"; +CREATE UNIQUE INDEX "SpaceKeyBox_spaceKeyId_nonce_key" ON "SpaceKeyBox"("spaceKeyId", "nonce"); +CREATE TABLE "new_Update" ( + "spaceId" TEXT NOT NULL, + "clock" INTEGER NOT NULL, + "content" BLOB NOT NULL, + "accountAddress" TEXT NOT NULL, + "signatureHex" TEXT NOT NULL, + "signatureRecovery" INTEGER NOT NULL, + "updateId" TEXT NOT NULL, + + PRIMARY KEY ("spaceId", "clock"), + CONSTRAINT "Update_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Update_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Update" ("accountAddress", "clock", "content", "signatureHex", "signatureRecovery", "spaceId", "updateId") SELECT "accountAddress", "clock", "content", "signatureHex", "signatureRecovery", "spaceId", "updateId" FROM "Update"; +DROP TABLE "Update"; +ALTER TABLE "new_Update" RENAME TO "Update"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; + +-- CreateIndex +CREATE INDEX "AppIdentity_sessionToken_idx" ON "AppIdentity"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "AppIdentity_accountAddress_appId_key" ON "AppIdentity"("accountAddress", "appId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AppIdentity_accountAddress_nonce_key" ON "AppIdentity"("accountAddress", "nonce"); + +-- CreateIndex +CREATE UNIQUE INDEX "_space-members_AB_unique" ON "_space-members"("A", "B"); + +-- CreateIndex +CREATE INDEX "_space-members_B_index" ON "_space-members"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_AppIdentityToSpace_AB_unique" ON "_AppIdentityToSpace"("A", "B"); + +-- CreateIndex +CREATE INDEX "_AppIdentityToSpace_B_index" ON "_AppIdentityToSpace"("B"); diff --git a/apps/server-new/prisma/migrations/20250620005807_add_connect_signer_address/migration.sql b/apps/server-new/prisma/migrations/20250620005807_add_connect_signer_address/migration.sql new file mode 100644 index 00000000..61bca61a --- /dev/null +++ b/apps/server-new/prisma/migrations/20250620005807_add_connect_signer_address/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - Added the required column `connectSignerAddress` to the `Account` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Account" ( + "address" TEXT NOT NULL PRIMARY KEY, + "connectAddress" TEXT NOT NULL, + "connectCiphertext" TEXT NOT NULL, + "connectNonce" TEXT NOT NULL, + "connectSignaturePublicKey" TEXT NOT NULL, + "connectEncryptionPublicKey" TEXT NOT NULL, + "connectAccountProof" TEXT NOT NULL, + "connectKeyProof" TEXT NOT NULL, + "connectSignerAddress" TEXT NOT NULL +); +INSERT INTO "new_Account" ("address", "connectAccountProof", "connectAddress", "connectCiphertext", "connectEncryptionPublicKey", "connectKeyProof", "connectNonce", "connectSignaturePublicKey") SELECT "address", "connectAccountProof", "connectAddress", "connectCiphertext", "connectEncryptionPublicKey", "connectKeyProof", "connectNonce", "connectSignaturePublicKey" FROM "Account"; +DROP TABLE "Account"; +ALTER TABLE "new_Account" RENAME TO "Account"; +CREATE UNIQUE INDEX "Account_connectAddress_key" ON "Account"("connectAddress"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/apps/server-new/prisma/migrations/20250627185421_remove_app_identity_nonce/migration.sql b/apps/server-new/prisma/migrations/20250627185421_remove_app_identity_nonce/migration.sql new file mode 100644 index 00000000..b5aed7b7 --- /dev/null +++ b/apps/server-new/prisma/migrations/20250627185421_remove_app_identity_nonce/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to drop the column `nonce` on the `AppIdentity` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_AppIdentity" ( + "address" TEXT NOT NULL PRIMARY KEY, + "ciphertext" TEXT NOT NULL, + "signaturePublicKey" TEXT NOT NULL, + "encryptionPublicKey" TEXT NOT NULL, + "accountProof" TEXT NOT NULL, + "keyProof" TEXT NOT NULL, + "accountAddress" TEXT NOT NULL, + "appId" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "sessionTokenExpires" DATETIME NOT NULL, + CONSTRAINT "AppIdentity_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_AppIdentity" ("accountAddress", "accountProof", "address", "appId", "ciphertext", "encryptionPublicKey", "keyProof", "sessionToken", "sessionTokenExpires", "signaturePublicKey") SELECT "accountAddress", "accountProof", "address", "appId", "ciphertext", "encryptionPublicKey", "keyProof", "sessionToken", "sessionTokenExpires", "signaturePublicKey" FROM "AppIdentity"; +DROP TABLE "AppIdentity"; +ALTER TABLE "new_AppIdentity" RENAME TO "AppIdentity"; +CREATE INDEX "AppIdentity_sessionToken_idx" ON "AppIdentity"("sessionToken"); +CREATE UNIQUE INDEX "AppIdentity_accountAddress_appId_key" ON "AppIdentity"("accountAddress", "appId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/apps/server-new/prisma/migrations/migration_lock.toml b/apps/server-new/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..2a5a4441 --- /dev/null +++ b/apps/server-new/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/apps/server-new/prisma/schema.prisma b/apps/server-new/prisma/schema.prisma new file mode 100644 index 00000000..d5f629e2 --- /dev/null +++ b/apps/server-new/prisma/schema.prisma @@ -0,0 +1,189 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client" + output = "generated/client" + moduleFormat = "esm" + binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"] // linux needed for the deployment +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model SpaceEvent { + id String @id + event String + state String + counter Int + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + createdAt DateTime @default(now()) + inboxes SpaceInbox[] + + @@unique([spaceId, counter]) +} + +model Space { + id String @id + events SpaceEvent[] + members Account[] @relation("space-members") + invitations Invitation[] + keys SpaceKey[] + updates Update[] + inboxes SpaceInbox[] + appIdentities AppIdentity[] + name String // TODO: remove this field and use infoContent instead + infoContent Bytes + infoAuthor Account @relation(fields: [infoAuthorAddress], references: [address]) + infoAuthorAddress String + infoSignatureHex String + infoSignatureRecovery Int +} + +model SpaceKey { + id String @id + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + createdAt DateTime @default(now()) + keyBoxes SpaceKeyBox[] +} + +model SpaceKeyBox { + id String @id + spaceKey SpaceKey @relation(fields: [spaceKeyId], references: [id]) + spaceKeyId String + ciphertext String + nonce String + authorPublicKey String + createdAt DateTime @default(now()) + account Account @relation(fields: [accountAddress], references: [address]) + accountAddress String + appIdentity AppIdentity? @relation(fields: [appIdentityAddress], references: [address]) + appIdentityAddress String? + + @@unique([spaceKeyId, nonce]) +} + +model SpaceInbox { + id String @id + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + isPublic Boolean + authPolicy String + encryptionPublicKey String + encryptedSecretKey String + spaceEvent SpaceEvent @relation(fields: [spaceEventId], references: [id]) + spaceEventId String + messages SpaceInboxMessage[] + createdAt DateTime @default(now()) +} + +model SpaceInboxMessage { + id String @id @default(uuid(4)) + spaceInbox SpaceInbox @relation(fields: [spaceInboxId], references: [id]) + spaceInboxId String + ciphertext String + signatureHex String? + signatureRecovery Int? + authorAccountAddress String? + createdAt DateTime @default(now()) +} + +model Account { + address String @id + spaces Space[] @relation("space-members") + invitations Invitation[] + appIdentities AppIdentity[] + + updates Update[] + inboxes AccountInbox[] + connectAddress String @unique + connectCiphertext String + connectNonce String + connectSignaturePublicKey String + connectEncryptionPublicKey String + connectAccountProof String + connectKeyProof String + infoAuthor Space[] + spaceKeyBoxes SpaceKeyBox[] + connectSignerAddress String +} + +model AppIdentity { + address String @id + ciphertext String + signaturePublicKey String + encryptionPublicKey String + accountProof String + keyProof String + account Account @relation(fields: [accountAddress], references: [address]) + accountAddress String + spaces Space[] + spaceKeyBoxes SpaceKeyBox[] + appId String + sessionToken String + sessionTokenExpires DateTime + + @@unique([accountAddress, appId]) + @@index([sessionToken]) +} + +model AccountInbox { + id String @id + account Account @relation(fields: [accountAddress], references: [address]) + accountAddress String + isPublic Boolean + authPolicy String + encryptionPublicKey String + signatureHex String + signatureRecovery Int + messages AccountInboxMessage[] + createdAt DateTime @default(now()) +} + +model AccountInboxMessage { + id String @id @default(uuid(7)) + accountInbox AccountInbox @relation(fields: [accountInboxId], references: [id]) + accountInboxId String + ciphertext String + signatureHex String? + signatureRecovery Int? + authorAccountAddress String? + createdAt DateTime @default(now()) +} + +model Invitation { + id String @id + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + account Account @relation(fields: [accountAddress], references: [address]) + accountAddress String + inviteeAccountAddress String + createdAt DateTime @default(now()) + targetApps InvitationTargetApp[] + + @@unique([spaceId, inviteeAccountAddress]) +} + +model InvitationTargetApp { + id String @id + invitation Invitation @relation(fields: [invitationId], references: [id]) + invitationId String +} + +model Update { + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + clock Int + content Bytes + account Account @relation(fields: [accountAddress], references: [address]) + accountAddress String + signatureHex String + signatureRecovery Int + updateId String + + @@id([spaceId, clock]) +} diff --git a/apps/server-new/setupTests.ts b/apps/server-new/setupTests.ts new file mode 100644 index 00000000..908ec455 --- /dev/null +++ b/apps/server-new/setupTests.ts @@ -0,0 +1 @@ +import '@effect/vitest'; diff --git a/apps/server-new/specs/README.md b/apps/server-new/specs/README.md new file mode 100644 index 00000000..99164d04 --- /dev/null +++ b/apps/server-new/specs/README.md @@ -0,0 +1,20 @@ +# Specifications + +This directory contains feature specifications following our spec-driven development approach. + +## Structure + +Each feature has its own directory containing: + +- `instructions.md` - Initial feature requirements and user stories +- `requirements.md` - Detailed structured requirements +- `design.md` - Technical design and implementation strategy +- `plan.md` - Implementation plan and progress tracking + +## Workflow + +1. **Specification Phase**: Create feature folder and document requirements/design +2. **Implementation Phase**: Follow design specifications and track progress +3. **Validation Phase**: Ensure implementation matches specifications + +## Features diff --git a/apps/server-new/src/config/database.ts b/apps/server-new/src/config/database.ts new file mode 100644 index 00000000..f0647257 --- /dev/null +++ b/apps/server-new/src/config/database.ts @@ -0,0 +1,6 @@ +import { Config } from 'effect'; + +/** + * Database configuration + */ +export const databaseUrlConfig = Config.string('DATABASE_URL').pipe(Config.withDefault('file:./dev.db')); diff --git a/apps/server-new/src/config/privy.ts b/apps/server-new/src/config/privy.ts new file mode 100644 index 00000000..d71684ce --- /dev/null +++ b/apps/server-new/src/config/privy.ts @@ -0,0 +1,25 @@ +import { Config, Effect } from 'effect'; +import { PrivyConfigError } from '../http/errors.js'; + +/** + * Privy configuration + */ +export const privyAppIdConfig = Config.string('PRIVY_APP_ID'); +export const privyAppSecretConfig = Config.redacted('PRIVY_APP_SECRET'); + +/** + * Load and validate Privy configuration + */ +export const privyConfig = Effect.fn(function* () { + const appId = yield* privyAppIdConfig; + const appSecret = yield* privyAppSecretConfig; + + if (!appId || !appSecret) { + return yield* Effect.fail(new PrivyConfigError({ message: 'Missing Privy configuration' })); + } + + return { + appId, + appSecret, + }; +}); diff --git a/apps/server-new/src/config/server.ts b/apps/server-new/src/config/server.ts new file mode 100644 index 00000000..5b2f3bb3 --- /dev/null +++ b/apps/server-new/src/config/server.ts @@ -0,0 +1,19 @@ +import { Config, Effect } from 'effect'; + +/** + * Server configuration + */ +export const serverPortConfig = Config.number('PORT').pipe(Config.withDefault(3030)); + +export const hypergraphChainConfig = Config.string('HYPERGRAPH_CHAIN').pipe(Config.withDefault('geo-testnet')); + +export const hypergraphRpcUrlConfig = Config.string('HYPERGRAPH_RPC_URL').pipe(Config.option); + +/** + * Load all server configuration + */ +export const serverConfig = Effect.all({ + port: serverPortConfig, + hypergraphChain: hypergraphChainConfig, + hypergraphRpcUrl: hypergraphRpcUrlConfig, +}); diff --git a/apps/server-new/src/domain/models.ts b/apps/server-new/src/domain/models.ts new file mode 100644 index 00000000..a6600160 --- /dev/null +++ b/apps/server-new/src/domain/models.ts @@ -0,0 +1,174 @@ +import { Messages, SignatureWithRecovery } from '@graphprotocol/hypergraph'; +import { Schema } from 'effect'; + +/** + * Re-export existing schemas from Hypergraph Messages + */ +export { SignatureWithRecovery }; +export const KeyBox = Messages.KeyBox; +export const KeyBoxWithKeyId = Messages.KeyBoxWithKeyId; +export const IdentityKeyBox = Messages.IdentityKeyBox; +export const SignedUpdate = Messages.SignedUpdate; +export const Updates = Messages.Updates; +export const InboxMessage = Messages.InboxMessage; + +/** + * Inbox auth policy (from Hypergraph) + */ +export const InboxSenderAuthPolicy = Schema.Literal('requires_auth', 'anonymous', 'optional_auth'); + +/** + * Database entity schemas (Prisma-based) + */ +export const Account = Schema.Struct({ + address: Schema.String, + connectAddress: Schema.String, + connectCiphertext: Schema.String, + connectNonce: Schema.String, + connectSignaturePublicKey: Schema.String, + connectEncryptionPublicKey: Schema.String, + connectAccountProof: Schema.String, + connectKeyProof: Schema.String, + connectSignerAddress: Schema.String, +}); + +export const AppIdentity = Schema.Struct({ + address: Schema.String, + ciphertext: Schema.String, + signaturePublicKey: Schema.String, + encryptionPublicKey: Schema.String, + accountProof: Schema.String, + keyProof: Schema.String, + accountAddress: Schema.String, + appId: Schema.String, + sessionToken: Schema.String, + sessionTokenExpires: Schema.DateFromSelf, +}); + +export const Space = Schema.Struct({ + id: Schema.String, + name: Schema.String, + infoContent: Schema.Uint8Array, + infoAuthorAddress: Schema.String, + infoSignatureHex: Schema.String, + infoSignatureRecovery: Schema.Number, +}); + +export const SpaceEvent = Schema.Struct({ + id: Schema.String, + event: Schema.String, + state: Schema.String, + counter: Schema.Number, + spaceId: Schema.String, + createdAt: Schema.DateFromSelf, +}); + +export const SpaceKey = Schema.Struct({ + id: Schema.String, + spaceId: Schema.String, + createdAt: Schema.DateFromSelf, +}); + +export const SpaceKeyBox = Schema.Struct({ + id: Schema.String, + spaceKeyId: Schema.String, + ciphertext: Schema.String, + nonce: Schema.String, + authorPublicKey: Schema.String, + accountAddress: Schema.String, + appIdentityAddress: Schema.optional(Schema.String), + createdAt: Schema.DateFromSelf, +}); + +export const Update = Schema.Struct({ + spaceId: Schema.String, + clock: Schema.Number, + content: Schema.Uint8Array, + accountAddress: Schema.String, + signatureHex: Schema.String, + signatureRecovery: Schema.Number, + updateId: Schema.String, +}); + +export const SpaceInbox = Schema.Struct({ + id: Schema.String, + spaceId: Schema.String, + isPublic: Schema.Boolean, + authPolicy: InboxSenderAuthPolicy, + encryptionPublicKey: Schema.String, + encryptedSecretKey: Schema.String, + spaceEventId: Schema.String, + createdAt: Schema.DateFromSelf, +}); + +export const SpaceInboxMessage = Schema.Struct({ + id: Schema.String, + spaceInboxId: Schema.String, + ciphertext: Schema.String, + signatureHex: Schema.optional(Schema.String), + signatureRecovery: Schema.optional(Schema.Number), + authorAccountAddress: Schema.optional(Schema.String), + createdAt: Schema.DateFromSelf, +}); + +export const AccountInbox = Schema.Struct({ + id: Schema.String, + accountAddress: Schema.String, + isPublic: Schema.Boolean, + authPolicy: InboxSenderAuthPolicy, + encryptionPublicKey: Schema.String, + signatureHex: Schema.String, + signatureRecovery: Schema.Number, + createdAt: Schema.DateFromSelf, +}); + +export const AccountInboxMessage = Schema.Struct({ + id: Schema.String, + accountInboxId: Schema.String, + ciphertext: Schema.String, + signatureHex: Schema.optional(Schema.String), + signatureRecovery: Schema.optional(Schema.Number), + authorAccountAddress: Schema.optional(Schema.String), + createdAt: Schema.DateFromSelf, +}); + +export const Invitation = Schema.Struct({ + id: Schema.String, + spaceId: Schema.String, + accountAddress: Schema.String, + inviteeAccountAddress: Schema.String, + createdAt: Schema.DateFromSelf, +}); + +export const InvitationTargetApp = Schema.Struct({ + id: Schema.String, + invitationId: Schema.String, +}); + +/** + * API response schemas + */ +export const SpaceInboxPublic = Schema.Struct({ + id: Schema.String, + spaceId: Schema.String, + isPublic: Schema.Boolean, + authPolicy: InboxSenderAuthPolicy, + encryptionPublicKey: Schema.String, +}); + +export const AccountInboxPublic = Schema.Struct({ + id: Schema.String, + accountAddress: Schema.String, + isPublic: Schema.Boolean, + authPolicy: InboxSenderAuthPolicy, + encryptionPublicKey: Schema.String, +}); + +export const PublicIdentity = Schema.Struct({ + accountAddress: Schema.String, + signaturePublicKey: Schema.String, + encryptionPublicKey: Schema.String, + accountProof: Schema.String, + keyProof: Schema.String, + appId: Schema.optional(Schema.String), +}); diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts new file mode 100644 index 00000000..577a5c4d --- /dev/null +++ b/apps/server-new/src/http/api.ts @@ -0,0 +1,208 @@ +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from '@effect/platform'; +import { Messages } from '@graphprotocol/hypergraph'; +import { Schema } from 'effect'; +import * as Models from '../domain/models.js'; +import * as Errors from './errors.js'; + +/** + * Path Parameters + */ +export const appId = HttpApiSchema.param('appId', Schema.String); +export const spaceId = HttpApiSchema.param('spaceId', Schema.String); +export const inboxId = HttpApiSchema.param('inboxId', Schema.String); +export const accountAddress = HttpApiSchema.param('accountAddress', Schema.String); + +/** + * API Request/Response Schemas + */ +export class AppIdentityInfo extends Schema.Class('AppIdentityInfo')({ + appId: Schema.String, + address: Schema.String, +}) {} + +export class SpaceKeyBoxInfo extends Schema.Class('SpaceKeyBoxInfo')({ + id: Schema.String, + ciphertext: Schema.String, + nonce: Schema.String, + authorPublicKey: Schema.String, +}) {} + +export class SpaceInfo extends Schema.Class('SpaceInfo')({ + id: Schema.String, + infoContent: Schema.String, + infoAuthorAddress: Schema.String, + infoSignatureHex: Schema.String, + infoSignatureRecovery: Schema.Number, + name: Schema.String, + appIdentities: Schema.Array(AppIdentityInfo), + keyBoxes: Schema.Array(SpaceKeyBoxInfo), +}) {} + +export class ConnectSpacesResponse extends Schema.Class('ConnectSpacesResponse')({ + spaces: Schema.Array(SpaceInfo), +}) {} + +export class SpaceCreationResponse extends Schema.Class('SpaceCreationResponse')({ + space: Schema.Struct({ + id: Schema.String, + }), +}) {} + +export class AppIdentityResponse extends Schema.Class('AppIdentityResponse')({ + appIdentity: Models.AppIdentity, +}) {} + +export class ConnectIdentityQuery extends Schema.Class('ConnectIdentityQuery')({ + accountAddress: Schema.String, +}) {} + +export class IdentityQuery extends Schema.Class('IdentityQuery')({ + accountAddress: Schema.String, + signaturePublicKey: Schema.optional(Schema.String), + appId: Schema.optional(Schema.String), +}) {} + +/** + * Health endpoints + */ +export const statusEndpoint = HttpApiEndpoint.get('status')`/`.addSuccess(Schema.String); + +export const healthGroup = HttpApiGroup.make('Health').add(statusEndpoint); + +/** + * Connect API endpoints (Privy authentication) + */ +export const getConnectSpacesEndpoint = HttpApiEndpoint.get('getConnectSpaces')`/connect/spaces` + .addSuccess(ConnectSpacesResponse) + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.PrivyConfigError, { status: 500 }); + +export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces')`/connect/spaces` + .setPayload(Messages.RequestConnectCreateSpaceEvent) + // .addSuccess(SpaceCreationResponse) + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.ValidationError, { status: 400 }) + .addError(Errors.PrivyConfigError, { status: 500 }); + +export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( + 'postConnectAddAppIdentityToSpaces', +)`/connect/add-app-identity-to-spaces` + .setPayload(Messages.RequestConnectAddAppIdentityToSpaces) + // .addSuccess(Schema.Struct({ space: Schema.Unknown })) + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.ValidationError, { status: 400 }); + +export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIdentity')`/connect/identity` + .setPayload(Messages.RequestConnectCreateIdentity) + // .addSuccess(Messages.ResponseConnectCreateIdentity) + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.ResourceAlreadyExistsError, { status: 400 }) + .addError(Errors.OwnershipProofError, { status: 401 }); + +export const getConnectIdentityEncryptedEndpoint = HttpApiEndpoint.get( + 'getConnectIdentityEncrypted', +)`/connect/identity/encrypted` + // .addSuccess(Messages.ResponseIdentityEncrypted) + .addError(Errors.AuthenticationError, { status: 401 }); + +export const getConnectAppIdentityEndpoint = HttpApiEndpoint.get( + 'getConnectAppIdentity', +)`/connect/app-identity/${appId}` + // .addSuccess(AppIdentityResponse) + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.ResourceNotFoundError, { status: 404 }); + +export const postConnectAppIdentityEndpoint = HttpApiEndpoint.post('postConnectAppIdentity')`/connect/app-identity` + .setPayload(Messages.RequestConnectCreateAppIdentity) + // .addSuccess(AppIdentityResponse) + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.OwnershipProofError, { status: 401 }); + +export const connectGroup = HttpApiGroup.make('Connect') + .add(getConnectSpacesEndpoint) + .add(postConnectSpacesEndpoint) + .add(postConnectAddAppIdentityToSpacesEndpoint) + .add(postConnectIdentityEndpoint) + .add(getConnectIdentityEncryptedEndpoint) + .add(getConnectAppIdentityEndpoint) + .add(postConnectAppIdentityEndpoint); + +/** + * Identity endpoints + */ +export const getWhoamiEndpoint = HttpApiEndpoint.get('getWhoami')`/whoami` + .addSuccess(Schema.String) + .addError(Errors.AuthenticationError, { status: 401 }); + +export const getConnectIdentityEndpoint = HttpApiEndpoint.get('getConnectIdentity')`/connect/identity` + .setUrlParams(ConnectIdentityQuery) + // .addSuccess(Messages.ResponseIdentity) + .addError(Errors.ResourceNotFoundError, { status: 404 }); + +export const getIdentityEndpoint = HttpApiEndpoint.get('getIdentity')`/identity` + .setUrlParams(IdentityQuery) + // .addSuccess(Messages.ResponseIdentity) + .addError(Errors.ValidationError, { status: 400 }) + .addError(Errors.ResourceNotFoundError, { status: 404 }); + +export const identityGroup = HttpApiGroup.make('Identity') + .add(getWhoamiEndpoint) + .add(getConnectIdentityEndpoint) + .add(getIdentityEndpoint); + +/** + * Inbox endpoints + */ +export const getSpaceInboxesEndpoint = HttpApiEndpoint.get('getSpaceInboxes')`/spaces/${spaceId}/inboxes` + // .addSuccess(Messages.ResponseListSpaceInboxesPublic) + .addError(Errors.DatabaseError, { status: 500 }); + +export const getSpaceInboxEndpoint = HttpApiEndpoint.get('getSpaceInbox')`/spaces/${spaceId}/inboxes/${inboxId}` + // .addSuccess(Messages.ResponseSpaceInboxPublic) + .addError(Errors.DatabaseError, { status: 500 }); + +export const postSpaceInboxMessageEndpoint = HttpApiEndpoint.post( + 'postSpaceInboxMessage', +)`/spaces/${spaceId}/inboxes/${inboxId}/messages` + .setPayload(Messages.RequestCreateSpaceInboxMessage) + .addSuccess(Schema.Void) + .addError(Errors.ValidationError, { status: 400 }) + .addError(Errors.AuthorizationError, { status: 403 }) + .addError(Errors.ResourceNotFoundError, { status: 404 }); + +export const getAccountInboxesEndpoint = HttpApiEndpoint.get('getAccountInboxes')`/accounts/${accountAddress}/inboxes` + // .addSuccess(Messages.ResponseListAccountInboxesPublic) + .addError(Errors.DatabaseError, { status: 500 }); + +export const getAccountInboxEndpoint = HttpApiEndpoint.get( + 'getAccountInbox', +)`/accounts/${accountAddress}/inboxes/${inboxId}` + // .addSuccess(Messages.ResponseAccountInboxPublic) + .addError(Errors.DatabaseError, { status: 500 }); + +export const postAccountInboxMessageEndpoint = HttpApiEndpoint.post( + 'postAccountInboxMessage', +)`/accounts/${accountAddress}/inboxes/${inboxId}/messages` + .setPayload(Messages.RequestCreateAccountInboxMessage) + .addSuccess(Schema.Void) + .addError(Errors.ValidationError, { status: 400 }) + .addError(Errors.AuthorizationError, { status: 403 }) + .addError(Errors.ResourceNotFoundError, { status: 404 }); + +export const inboxGroup = HttpApiGroup.make('Inbox') + .add(getSpaceInboxesEndpoint) + .add(getSpaceInboxEndpoint) + .add(postSpaceInboxMessageEndpoint) + .add(getAccountInboxesEndpoint) + .add(getAccountInboxEndpoint) + .add(postAccountInboxMessageEndpoint); + +/** + * Main API definition + */ +export const hypergraphApi = HttpApi.make('HypergraphApi') + .add(healthGroup) + .add(connectGroup) + .add(identityGroup) + .add(inboxGroup) + .addError(Errors.ResourceNotFoundError, { status: 500 }); diff --git a/apps/server-new/src/http/errors.ts b/apps/server-new/src/http/errors.ts new file mode 100644 index 00000000..dc425e07 --- /dev/null +++ b/apps/server-new/src/http/errors.ts @@ -0,0 +1,105 @@ +import { Schema } from 'effect'; + +/** + * Authentication-related errors + */ +export class AuthenticationError extends Schema.TaggedError()('AuthenticationError', { + message: Schema.String, +}) {} + +export class AuthorizationError extends Schema.TaggedError()('AuthorizationError', { + message: Schema.String, + accountAddress: Schema.optional(Schema.String), +}) {} + +export class InvalidTokenError extends Schema.TaggedError()('InvalidTokenError', { + tokenType: Schema.Literal('privy', 'session'), +}) {} + +export class TokenExpiredError extends Schema.TaggedError()('TokenExpiredError', { + tokenType: Schema.Literal('session'), +}) {} + +/** + * Resource-related errors + */ +export class ResourceNotFoundError extends Schema.TaggedError()('ResourceNotFoundError', { + resource: Schema.String, + id: Schema.String, +}) {} + +export class ResourceAlreadyExistsError extends Schema.TaggedError()( + 'ResourceAlreadyExistsError', + { + resource: Schema.String, + id: Schema.String, + }, +) {} + +/** + * Validation errors + */ +export class ValidationError extends Schema.TaggedError()('ValidationError', { + field: Schema.String, + message: Schema.String, +}) {} + +export class InvalidSignatureError extends Schema.TaggedError()('InvalidSignatureError', { + context: Schema.String, +}) {} + +export class OwnershipProofError extends Schema.TaggedError()('OwnershipProofError', { + accountAddress: Schema.String, + reason: Schema.String, +}) {} + +/** + * External service errors + */ +export class PrivyConfigError extends Schema.TaggedError()('PrivyConfigError', { + message: Schema.String, +}) {} + +export class PrivyTokenError extends Schema.TaggedError()('PrivyTokenError', { + message: Schema.String, +}) {} + +/** + * Database errors + */ +export class DatabaseError extends Schema.TaggedError()('DatabaseError', { + operation: Schema.String, + cause: Schema.Unknown, +}) {} + +export class TransactionError extends Schema.TaggedError()('TransactionError', { + message: Schema.String, + cause: Schema.Unknown, +}) {} + +/** + * Business logic errors + */ +export class InsufficientPermissionsError extends Schema.TaggedError()( + 'InsufficientPermissionsError', + { + resource: Schema.String, + requiredRole: Schema.String, + currentRole: Schema.optional(Schema.String), + }, +) {} + +export class InboxPolicyViolationError extends Schema.TaggedError()( + 'InboxPolicyViolationError', + { + inboxId: Schema.String, + authPolicy: Schema.String, + violation: Schema.String, + }, +) {} + +export class SpaceEventError extends Schema.TaggedError()('SpaceEventError', { + spaceId: Schema.String, + eventType: Schema.String, + reason: Schema.String, +}) {} diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts new file mode 100644 index 00000000..584471ad --- /dev/null +++ b/apps/server-new/src/http/handlers.ts @@ -0,0 +1,158 @@ +import { HttpApiBuilder } from '@effect/platform'; +import { Effect, Layer } from 'effect'; +import * as Api from './api.js'; +import * as Errors from './errors.js'; + +/** + * Health Group Handlers + */ +const HealthGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Health', (handlers) => { + return handlers.handle('status', () => Effect.succeed('OK')); +}); + +/** + * Connect Group Handlers + */ +const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (handlers) => { + return handlers + .handle( + 'getConnectSpaces', + Effect.fn(function* ({ request }) { + yield* Effect.logInfo('Getting connect spaces'); + return { spaces: [] }; + }), + ) + .handle( + 'postConnectSpaces', + Effect.fn(function* ({ payload }) { + yield* Effect.logInfo('Creating space', payload); + yield* new Errors.ResourceNotFoundError({ + resource: 'postConnectSpaces', + id: 'postConnectSpaces', + }); + }), + ) + .handle( + 'postConnectAddAppIdentityToSpaces', + Effect.fn(function* ({ payload }) { + yield* Effect.logInfo('Adding app identity to spaces', payload); + yield* new Errors.ResourceNotFoundError({ + resource: 'postConnectAddAppIdentityToSpaces', + id: 'postConnectAddAppIdentityToSpaces', + }); + }), + ) + .handle( + 'postConnectIdentity', + Effect.fn(function* ({ payload }) { + yield* Effect.logInfo('Creating connect identity', payload); + yield* new Errors.ResourceNotFoundError({ resource: 'postConnectIdentity', id: 'postConnectIdentity' }); + }), + ) + .handle( + 'getConnectIdentityEncrypted', + Effect.fn(function* ({ request }) { + yield* Effect.logInfo('Getting encrypted identity'); + yield* new Errors.ResourceNotFoundError({ + resource: 'getConnectIdentityEncrypted', + id: 'getConnectIdentityEncrypted', + }); + }), + ) + .handle( + 'getConnectAppIdentity', + Effect.fn(function* ({ path: { appId } }) { + yield* Effect.logInfo(`Getting app identity for appId: ${appId}`); + yield* new Errors.ResourceNotFoundError({ resource: 'getConnectAppIdentity', id: 'getConnectAppIdentity' }); + }), + ) + .handle( + 'postConnectAppIdentity', + Effect.fn(function* ({ payload }) { + yield* Effect.logInfo('Creating app identity', payload); + yield* new Errors.ResourceNotFoundError({ resource: 'postConnectAppIdentity', id: 'postConnectAppIdentity' }); + }), + ); +}); + +/** + * Identity Group Handlers + */ +const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (handlers) => { + return handlers + .handle('getWhoami', () => Effect.succeed('Hypergraph Server v2')) + .handle( + 'getConnectIdentity', + Effect.fn(function* ({ urlParams }) { + yield* Effect.logInfo('Getting connect identity', urlParams); + yield* new Errors.ResourceNotFoundError({ resource: 'Identity', id: 'connect' }); + }), + ) + .handle( + 'getIdentity', + Effect.fn(function* ({ urlParams }) { + yield* Effect.logInfo('Getting identity', urlParams); + yield* new Errors.ResourceNotFoundError({ resource: 'Identity', id: 'general' }); + }), + ); +}); + +/** + * Inbox Group Handlers + */ +const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handlers) => { + return handlers + .handle( + 'getSpaceInboxes', + Effect.fn(function* ({ path: { spaceId } }) { + yield* Effect.logInfo(`Getting space inboxes: ${spaceId}`); + yield* new Errors.ResourceNotFoundError({ + resource: 'getSpaceInboxes', + id: 'getSpaceInboxes', + }); + }), + ) + .handle( + 'getSpaceInbox', + Effect.fn(function* ({ path: { spaceId, inboxId } }) { + yield* Effect.logInfo(`Getting space inbox: ${spaceId}/${inboxId}`); + yield* new Errors.ResourceNotFoundError({ resource: 'SpaceInbox', id: inboxId }); + }), + ) + .handle( + 'postSpaceInboxMessage', + Effect.fn(function* ({ path: { spaceId, inboxId }, payload }) { + yield* Effect.logInfo(`Posting message to space inbox: ${spaceId}/${inboxId}`, payload); + return { success: true }; + }), + ) + .handle( + 'getAccountInboxes', + Effect.fn(function* ({ path: { accountAddress } }) { + yield* Effect.logInfo(`Getting account inboxes: ${accountAddress}`); + yield* new Errors.ResourceNotFoundError({ + resource: 'getAccountInboxes', + id: 'getAccountInboxes', + }); + }), + ) + .handle( + 'getAccountInbox', + Effect.fn(function* ({ path: { accountAddress, inboxId } }) { + yield* Effect.logInfo(`Getting account inbox: ${accountAddress}/${inboxId}`); + yield* new Errors.ResourceNotFoundError({ resource: 'AccountInbox', id: inboxId }); + }), + ) + .handle( + 'postAccountInboxMessage', + Effect.fn(function* ({ path: { accountAddress, inboxId }, payload }) { + yield* Effect.logInfo(`Posting message to account inbox: ${accountAddress}/${inboxId}`, payload); + return { success: true }; + }), + ); +}); + +/** + * All handlers combined + */ +export const HandlersLive = Layer.mergeAll(HealthGroupLive, ConnectGroupLive, IdentityGroupLive, InboxGroupLive); diff --git a/apps/server-new/src/index.ts b/apps/server-new/src/index.ts new file mode 100644 index 00000000..f84501f6 --- /dev/null +++ b/apps/server-new/src/index.ts @@ -0,0 +1,5 @@ +import { NodeRuntime } from '@effect/platform-node'; +import { Layer } from 'effect'; +import { server } from './server.ts'; + +NodeRuntime.runMain(Layer.launch(server)); diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts new file mode 100644 index 00000000..15dbb93e --- /dev/null +++ b/apps/server-new/src/server.ts @@ -0,0 +1,20 @@ +import { HttpApiBuilder, HttpServer } from '@effect/platform'; +import { NodeHttpServer } from '@effect/platform-node'; +import { Effect, Layer } from 'effect'; +import { createServer } from 'node:http'; +import { serverPortConfig } from './config/server.ts'; +import { hypergraphApi } from './http/api.ts'; +import { HandlersLive } from './http/handlers.ts'; + +const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive)); + +export const server = Layer.unwrapEffect( + Effect.gen(function* () { + const port = yield* serverPortConfig; + return HttpApiBuilder.serve().pipe( + Layer.provide(apiLive), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port })), + ); + }), +); diff --git a/apps/server-new/src/services/auth.ts b/apps/server-new/src/services/auth.ts new file mode 100644 index 00000000..24110f5f --- /dev/null +++ b/apps/server-new/src/services/auth.ts @@ -0,0 +1,62 @@ +import { PrivyClient } from '@privy-io/server-auth'; +import { Context, Effect, Layer, Redacted } from 'effect'; +import * as Config from '../config/privy.js'; + +/** + * Auth service interface + */ +export interface AuthService { + readonly privy: PrivyClient; + readonly verifyAuthToken: (token: string) => Effect.Effect<{ userId: string }, Error>; + readonly verifySessionToken: (token: string) => Effect.Effect<{ address: string }, Error>; +} + +/** + * Auth service tag + */ +export const AuthService = Context.GenericTag('AuthService'); + +/** + * Auth service implementation + */ +export const makeAuthService = Effect.fn(function* () { + const config = yield* Config.privyConfig(); + + const privy = new PrivyClient(config.appId, Redacted.value(config.appSecret)); + + const verifyAuthToken = Effect.fn(function* (token: string) { + const user = yield* Effect.tryPromise({ + try: () => privy.getUser({ idToken: token }), + catch: (error) => new Error(`Failed to verify auth token: ${error}`), + }); + + if (!user) { + yield* Effect.fail(new Error('User not found')); + } + + return { userId: user.id }; + }); + + const verifySessionToken = Effect.fn(function* (_token: string) { + // TODO: Implement session token verification logic + // This would typically involve: + // 1. Decoding the JWT token + // 2. Verifying the signature + // 3. Checking expiration + // 4. Extracting the address + + // For now, return a placeholder + return { address: 'placeholder' }; + }); + + return { + privy, + verifyAuthToken, + verifySessionToken, + } as const; +}); + +/** + * Auth service layer + */ +export const AuthServiceLive = Layer.effect(AuthService, makeAuthService()); diff --git a/apps/server-new/src/services/database.ts b/apps/server-new/src/services/database.ts new file mode 100644 index 00000000..5948d962 --- /dev/null +++ b/apps/server-new/src/services/database.ts @@ -0,0 +1,64 @@ +import { PrismaClient } from '@prisma/client'; +import { Context, Effect, Layer } from 'effect'; + +/** + * Database service interface + */ +export interface DatabaseService { + readonly client: PrismaClient; +} + +/** + * Database service tag + */ +export const DatabaseService = Context.GenericTag('DatabaseService'); + +/** + * Database service implementation + */ +export const makeDatabaseService = Effect.fn(function* () { + const client = new PrismaClient(); + + // Connect to database + yield* Effect.tryPromise({ + try: () => client.$connect(), + catch: (error) => new Error(`Failed to connect to database: ${error}`), + }); + + return { + client, + } as const; +}); + +/** + * Database service layer + */ +export const DatabaseServiceLive = Layer.effect(DatabaseService, makeDatabaseService()); + +/** + * Database service layer with resource management + */ +export const DatabaseServiceLiveWithCleanup = Layer.scoped( + DatabaseService, + Effect.fn(function* () { + const client = new PrismaClient(); + + // Connect to database + yield* Effect.tryPromise({ + try: () => client.$connect(), + catch: (error) => new Error(`Failed to connect to database: ${error}`), + }); + + // Register cleanup + yield* Effect.addFinalizer(() => + Effect.tryPromise({ + try: () => client.$disconnect(), + catch: (error) => new Error(`Failed to disconnect from database: ${error}`), + }).pipe(Effect.ignore), + ); + + return { + client, + } as const; + })(), +); diff --git a/apps/server-new/tsconfig.app.json b/apps/server-new/tsconfig.app.json new file mode 100644 index 00000000..706602e6 --- /dev/null +++ b/apps/server-new/tsconfig.app.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "tsup.config.ts"], + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "composite": false, + "incremental": false, + "declaration": false, + "declarationMap": false, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} \ No newline at end of file diff --git a/apps/server-new/tsconfig.json b/apps/server-new/tsconfig.json new file mode 100644 index 00000000..706602e6 --- /dev/null +++ b/apps/server-new/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "tsup.config.ts"], + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "composite": false, + "incremental": false, + "declaration": false, + "declarationMap": false, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} \ No newline at end of file diff --git a/apps/server-new/tsconfig.node.json b/apps/server-new/tsconfig.node.json new file mode 100644 index 00000000..d462d771 --- /dev/null +++ b/apps/server-new/tsconfig.node.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["vitest.config.ts"], + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "composite": false, + "incremental": false, + "declaration": false, + "declarationMap": false, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} \ No newline at end of file diff --git a/apps/server-new/tsup.config.ts b/apps/server-new/tsup.config.ts new file mode 100644 index 00000000..2ca0402c --- /dev/null +++ b/apps/server-new/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + target: 'node22', + clean: true, + sourcemap: true, + minify: false, + splitting: false, + dts: false, + external: ['@prisma/client'], +}); diff --git a/apps/server-new/vitest.config.ts b/apps/server-new/vitest.config.ts new file mode 100644 index 00000000..60051f09 --- /dev/null +++ b/apps/server-new/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: false, + include: ['test/**/*.test.ts', 'src/**/*.test.ts'], + setupFiles: ['./setupTests.ts'], + testTimeout: 30000, + }, +}); diff --git a/apps/server/api-docs/api-summary.md b/apps/server/api-docs/api-summary.md new file mode 100644 index 00000000..9a6dff6e --- /dev/null +++ b/apps/server/api-docs/api-summary.md @@ -0,0 +1,81 @@ +# API Routes Summary + +## HTTP Endpoints + +### Health Check +- **GET /** - Server status check + +### Connect API (Privy Authentication) +- **GET /connect/spaces** - List spaces for authenticated account +- **POST /connect/spaces** - Create new space +- **POST /connect/add-app-identity-to-spaces** - Add app identity to spaces +- **POST /connect/identity** - Create connect identity +- **GET /connect/identity/encrypted** - Get encrypted identity data +- **GET /connect/app-identity/:appId** - Get app identity by ID +- **POST /connect/app-identity** - Create app identity + +### Identity API +- **GET /whoami** - Get current account from session token +- **GET /connect/identity** - Get public connect identity (public) +- **GET /identity** - Get identity by public key or app ID (public) + +### Inbox API (Public) +- **GET /spaces/:spaceId/inboxes** - List public space inboxes +- **GET /spaces/:spaceId/inboxes/:inboxId** - Get space inbox details +- **POST /spaces/:spaceId/inboxes/:inboxId/messages** - Post to space inbox +- **GET /accounts/:accountAddress/inboxes** - List public account inboxes +- **GET /accounts/:accountAddress/inboxes/:inboxId** - Get account inbox details +- **POST /accounts/:accountAddress/inboxes/:inboxId/messages** - Post to account inbox + +## WebSocket Endpoints + +### Connection +- **ws://?token=[sessionToken]** - Establish WebSocket connection + +### Message Types +- **subscribe-space** - Subscribe to space updates +- **list-spaces** - List accessible spaces +- **list-invitations** - List pending invitations +- **create-space-event** - Create new space +- **create-invitation-event** - Invite to space +- **accept-invitation-event** - Accept invitation +- **create-space-inbox-event** - Create space inbox +- **create-account-inbox** - Create account inbox +- **get-latest-space-inbox-messages** - Get recent inbox messages +- **get-latest-account-inbox-messages** - Get recent account messages +- **get-account-inboxes** - List account inboxes +- **create-update** - Create CRDT update + +### Broadcast Events +- **space-event** - Space event notification +- **updates-notification** - CRDT updates +- **space-inbox-message** - New inbox message +- **account-inbox-message** - New account message +- **account-inbox** - Account inbox created + +## Authentication Methods + +1. **Privy ID Token**: Used by Connect app endpoints + - Header: `privy-id-token` + - Validates signer permissions + +2. **Session Token**: Used by app identities + - Header: `Authorization: Bearer ` + - 30-day expiry + +3. **Public Endpoints**: No authentication required + - Identity lookups + - Public inbox access + +## Common Response Formats + +### Success +- 200 OK with JSON response +- Empty object `{}` for successful writes + +### Errors +- 400 Bad Request - Invalid parameters +- 401 Unauthorized - Authentication failed +- 403 Forbidden - Insufficient permissions +- 404 Not Found - Resource not found +- 500 Internal Server Error - Server error \ No newline at end of file diff --git a/apps/server/api-docs/domain-model-overview.md b/apps/server/api-docs/domain-model-overview.md new file mode 100644 index 00000000..cffcfff6 --- /dev/null +++ b/apps/server/api-docs/domain-model-overview.md @@ -0,0 +1,212 @@ +# Domain Model Overview + +## Core Entities + +### Account +Central user entity representing an identity in the system. +```prisma +model Account { + address String @id + spaces Space[] @relation("space-members") + invitations Invitation[] + appIdentities AppIdentity[] + updates Update[] + inboxes AccountInbox[] + connectAddress String @unique + connectCiphertext String + connectNonce String + connectSignaturePublicKey String + connectEncryptionPublicKey String + connectAccountProof String + connectKeyProof String + connectSignerAddress String + spaceKeyBoxes SpaceKeyBox[] + infoAuthor Space[] +} +``` + +### Space +Collaborative workspace containing members, events, and data. +```prisma +model Space { + id String @id + events SpaceEvent[] + members Account[] @relation("space-members") + invitations Invitation[] + keys SpaceKey[] + updates Update[] + inboxes SpaceInbox[] + appIdentities AppIdentity[] + name String + infoContent Bytes + infoAuthorAddress String + infoSignatureHex String + infoSignatureRecovery Int +} +``` + +### AppIdentity +Application-specific identity linked to an account. +```prisma +model AppIdentity { + address String @id + ciphertext String + signaturePublicKey String + encryptionPublicKey String + accountProof String + keyProof String + accountAddress String + spaces Space[] + spaceKeyBoxes SpaceKeyBox[] + appId String + sessionToken String + sessionTokenExpires DateTime + @@unique([accountAddress, appId]) +} +``` + +### SpaceEvent +Append-only log of events within a space. +```prisma +model SpaceEvent { + id String @id + event String + state String + counter Int + spaceId String + createdAt DateTime @default(now()) + inboxes SpaceInbox[] + @@unique([spaceId, counter]) +} +``` + +### SpaceKey & SpaceKeyBox +Encryption key management for spaces. +```prisma +model SpaceKey { + id String @id + spaceId String + createdAt DateTime @default(now()) + keyBoxes SpaceKeyBox[] +} + +model SpaceKeyBox { + id String @id + spaceKeyId String + ciphertext String + nonce String + authorPublicKey String + accountAddress String + appIdentityAddress String? + @@unique([spaceKeyId, nonce]) +} +``` + +### Update +CRDT updates for collaborative editing. +```prisma +model Update { + spaceId String + clock Int + content Bytes + accountAddress String + signatureHex String + signatureRecovery Int + updateId String + @@id([spaceId, clock]) +} +``` + +### Inbox System +Message queuing for spaces and accounts. + +#### SpaceInbox & SpaceInboxMessage +```prisma +model SpaceInbox { + id String @id + spaceId String + isPublic Boolean + authPolicy String + encryptionPublicKey String + encryptedSecretKey String + spaceEventId String + messages SpaceInboxMessage[] + createdAt DateTime @default(now()) +} + +model SpaceInboxMessage { + id String @id @default(uuid(4)) + spaceInboxId String + ciphertext String + signatureHex String? + signatureRecovery Int? + authorAccountAddress String? + createdAt DateTime @default(now()) +} +``` + +#### AccountInbox & AccountInboxMessage +```prisma +model AccountInbox { + id String @id + accountAddress String + isPublic Boolean + authPolicy String + encryptionPublicKey String + signatureHex String + signatureRecovery Int + messages AccountInboxMessage[] + createdAt DateTime @default(now()) +} + +model AccountInboxMessage { + id String @id @default(uuid(7)) + accountInboxId String + ciphertext String + signatureHex String? + signatureRecovery Int? + authorAccountAddress String? + createdAt DateTime @default(now()) +} +``` + +### Invitation System +```prisma +model Invitation { + id String @id + spaceId String + accountAddress String + inviteeAccountAddress String + createdAt DateTime @default(now()) + targetApps InvitationTargetApp[] + @@unique([spaceId, inviteeAccountAddress]) +} + +model InvitationTargetApp { + id String @id + invitationId String +} +``` + +## Key Relationships + +1. **Account ↔ Space**: Many-to-many through membership +2. **Account ↔ AppIdentity**: One-to-many, unique per app +3. **Space ↔ SpaceEvent**: One-to-many, append-only log +4. **Space ↔ Update**: One-to-many, CRDT updates +5. **SpaceKey ↔ SpaceKeyBox**: One-to-many, encrypted for each member +6. **Inbox ↔ Message**: One-to-many for both Space and Account inboxes + +## Authentication & Authorization + +1. **Connect Identity**: Primary identity with signature/encryption keys +2. **App Identity**: App-specific identity with session tokens +3. **Session Tokens**: 30-day expiry for app authentication +4. **Privy Integration**: External auth provider for Connect app + +## Security Features + +1. **E2E Encryption**: All sensitive data encrypted client-side +2. **Key Rotation**: New keys generated when members removed +3. **Signature Verification**: All events/messages signed +4. **Ownership Proofs**: Cryptographic proofs for identity claims \ No newline at end of file diff --git a/apps/server/api-docs/get-accounts-inbox.md b/apps/server/api-docs/get-accounts-inbox.md new file mode 100644 index 00000000..09b3ef31 --- /dev/null +++ b/apps/server/api-docs/get-accounts-inbox.md @@ -0,0 +1,60 @@ +# GET /accounts/:accountAddress/inboxes/:inboxId + +## Overview +Retrieves details for a specific public inbox belonging to an account. + +## HTTP Method +GET + +## Route +`/accounts/:accountAddress/inboxes/:inboxId` + +## Authentication +None required (public endpoint) + +## Request Parameters +- `accountAddress`: The account address (URL parameter) +- `inboxId`: The inbox ID (URL parameter) + +## Request Headers +None + +## Request Body +None + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseAccountInboxPublic` +```json +{ + "inbox": { + "id": "string", + "accountAddress": "string", + "isPublic": true, + "authPolicy": "string", // "requires_auth" | "anonymous" | "optional_auth" + "encryptionPublicKey": "string" + } +} +``` + +### Error Responses +- 500 Internal Server Error: Database or server error + +## Domain Model +### AccountInbox +- `id`: string (primary key) +- `accountAddress`: string (foreign key to Account) +- `isPublic`: boolean +- `authPolicy`: string +- `encryptionPublicKey`: string +- `signatureHex`: string +- `signatureRecovery`: integer + +## Implementation Details +- No authentication required (public endpoint) +- Uses `getAccountInbox` handler to fetch specific inbox +- Returns public inbox details if found +- Note: Handler should verify inbox is public before returning + +## Dependencies +- `getAccountInbox`: Fetches specific inbox from database \ No newline at end of file diff --git a/apps/server/api-docs/get-accounts-inboxes.md b/apps/server/api-docs/get-accounts-inboxes.md new file mode 100644 index 00000000..0b6644c0 --- /dev/null +++ b/apps/server/api-docs/get-accounts-inboxes.md @@ -0,0 +1,66 @@ +# GET /accounts/:accountAddress/inboxes + +## Overview +Lists all public inboxes for a specific account. + +## HTTP Method +GET + +## Route +`/accounts/:accountAddress/inboxes` + +## Authentication +None required (public endpoint) + +## Request Parameters +- `accountAddress`: The account address (URL parameter) + +## Request Headers +None + +## Request Body +None + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseListAccountInboxesPublic` +```json +{ + "inboxes": [ + { + "id": "string", + "accountAddress": "string", + "isPublic": true, + "authPolicy": "string", // "requires_auth" | "anonymous" | "optional_auth" + "encryptionPublicKey": "string" + } + ] +} +``` + +### Error Responses +- 500 Internal Server Error: Database or server error + +## Domain Model +### AccountInbox +- `id`: string (primary key) +- `accountAddress`: string (foreign key to Account) +- `isPublic`: boolean +- `authPolicy`: string +- `encryptionPublicKey`: string +- `signatureHex`: string +- `signatureRecovery`: integer + +### Account +- `address`: string (primary key) +- `inboxes`: AccountInbox[] relation + +## Implementation Details +- No authentication required (public endpoint) +- Uses `listPublicAccountInboxes` handler to: + - Query all inboxes for the account + - Filter to only public inboxes (isPublic = true) +- Returns list of public inbox metadata + +## Dependencies +- `listPublicAccountInboxes`: Fetches public inboxes from database \ No newline at end of file diff --git a/apps/server/api-docs/get-connect-app-identity.md b/apps/server/api-docs/get-connect-app-identity.md new file mode 100644 index 00000000..c01c2e2f --- /dev/null +++ b/apps/server/api-docs/get-connect-app-identity.md @@ -0,0 +1,69 @@ +# GET /connect/app-identity/:appId + +## Overview +Retrieves app identity information for a specific app ID and authenticated account. + +## HTTP Method +GET + +## Route +`/connect/app-identity/:appId` + +## Authentication +Required - Privy ID token authentication + +## Request Parameters +- `appId`: The application ID (URL parameter) + +## Request Headers +- `privy-id-token`: Privy authentication token (required) +- `account-address`: The account address (required) + +## Request Body +None + +## Response +### Success Response (200 OK) +```json +{ + "appIdentity": { + "address": "string", + "appId": "string", + "accountAddress": "string", + "sessionToken": "string", + "sessionTokenExpires": "datetime", + // Additional fields from AppIdentity model + } +} +``` + +### Error Responses +- 401 Unauthorized: Invalid authentication or insufficient permissions +- 404 Not Found: App identity not found +- 500 Internal Server Error: Missing Privy configuration + +## Domain Model +### AppIdentity +- `address`: string (primary key) +- `appId`: string +- `accountAddress`: string (foreign key to Account) +- `ciphertext`: string +- `signaturePublicKey`: string +- `encryptionPublicKey`: string +- `accountProof`: string +- `keyProof`: string +- `sessionToken`: string +- `sessionTokenExpires`: datetime + +## Implementation Details +- Validates Privy token to get signer address +- Verifies signer has permission for the specified account +- Uses `findAppIdentity` handler to search for app identity by: + - Account address + - App ID +- Returns app identity if found, 404 if not found + +## Dependencies +- `getAddressByPrivyToken`: Validates Privy token +- `isSignerForAccount`: Verifies signer permissions +- `findAppIdentity`: Searches for app identity in database \ No newline at end of file diff --git a/apps/server/api-docs/get-connect-identity-encrypted.md b/apps/server/api-docs/get-connect-identity-encrypted.md new file mode 100644 index 00000000..d30444e4 --- /dev/null +++ b/apps/server/api-docs/get-connect-identity-encrypted.md @@ -0,0 +1,63 @@ +# GET /connect/identity/encrypted + +## Overview +Retrieves the encrypted identity data for an authenticated account. + +## HTTP Method +GET + +## Route +`/connect/identity/encrypted` + +## Authentication +Required - Privy ID token authentication + +## Request Parameters +None + +## Request Headers +- `privy-id-token`: Privy authentication token (required) +- `account-address`: The account address to retrieve identity for (required) + +## Request Body +None + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseIdentityEncrypted` +```json +{ + "keyBox": { + "accountAddress": "string", + "ciphertext": "string", + "nonce": "string", + "signer": "string" + } +} +``` + +### Error Responses +- 401 Unauthorized: Invalid authentication or insufficient permissions +- 500 Internal Server Error: Missing Privy configuration + +## Domain Model +### Account +- `address`: string (primary key) +- `connectCiphertext`: string +- `connectNonce`: string +- `connectSignerAddress`: string + +## Implementation Details +- Validates Privy token to get signer address +- Verifies signer has permission for the specified account +- Uses `getConnectIdentity` handler to fetch encrypted identity data +- Returns encrypted keyBox with: + - Account address + - Encrypted ciphertext + - Nonce for decryption + - Signer address + +## Dependencies +- `getAddressByPrivyToken`: Validates Privy token +- `isSignerForAccount`: Verifies signer permissions +- `getConnectIdentity`: Fetches identity from database \ No newline at end of file diff --git a/apps/server/api-docs/get-connect-identity.md b/apps/server/api-docs/get-connect-identity.md new file mode 100644 index 00000000..013c3a81 --- /dev/null +++ b/apps/server/api-docs/get-connect-identity.md @@ -0,0 +1,63 @@ +# GET /connect/identity + +## Overview +Retrieves public identity information for a given account address. + +## HTTP Method +GET + +## Route +`/connect/identity` + +## Authentication +None required (public endpoint) + +## Request Parameters +- `accountAddress`: The account address to look up (query parameter, required) + +## Request Headers +None + +## Request Body +None + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseIdentity` +```json +{ + "accountAddress": "string", + "signaturePublicKey": "string", + "encryptionPublicKey": "string", + "accountProof": "string", + "keyProof": "string" +} +``` + +### Error Responses +- 400 Bad Request: Missing accountAddress parameter +- 404 Not Found: Identity not found + Schema: `Messages.ResponseIdentityNotFoundError` + ```json + { + "accountAddress": "string" + } + ``` + +## Domain Model +### Account +- `address`: string (primary key) +- `connectSignaturePublicKey`: string +- `connectEncryptionPublicKey`: string +- `connectAccountProof`: string +- `connectKeyProof`: string + +## Implementation Details +- No authentication required (public endpoint) +- Validates required accountAddress query parameter +- Uses `getConnectIdentity` handler to fetch identity +- Returns public keys and proofs (no encrypted data) +- Returns 404 with specific error format if identity not found + +## Dependencies +- `getConnectIdentity`: Fetches identity from database \ No newline at end of file diff --git a/apps/server/api-docs/get-connect-spaces.md b/apps/server/api-docs/get-connect-spaces.md new file mode 100644 index 00000000..8481079c --- /dev/null +++ b/apps/server/api-docs/get-connect-spaces.md @@ -0,0 +1,103 @@ +# GET /connect/spaces + +## Overview +Retrieves all spaces associated with an authenticated account, including space information, app identities, and key boxes. + +## HTTP Method +GET + +## Route +`/connect/spaces` + +## Authentication +Required - Privy ID token authentication + +## Request Parameters +None + +## Request Headers +- `privy-id-token`: Privy authentication token (required) +- `account-address`: The account address to retrieve spaces for (required) + +## Request Body +None + +## Response +### Success Response (200 OK) +```json +{ + "spaces": [ + { + "id": "string", + "infoContent": "hex string", + "infoAuthorAddress": "string", + "infoSignatureHex": "string", + "infoSignatureRecovery": "number", + "name": "string", + "appIdentities": [ + { + "appId": "string", + "address": "string" + } + ], + "keyBoxes": [ + { + "id": "string", + "ciphertext": "string", + "nonce": "string", + "authorPublicKey": "string" + } + ] + } + ] +} +``` + +### Error Responses +- 401 Unauthorized: Invalid or missing authentication +- 500 Internal Server Error: Missing Privy configuration + +## Domain Model +### Space +- `id`: string (primary key) +- `name`: string +- `infoContent`: bytes +- `infoAuthorAddress`: string (foreign key to Account) +- `infoSignatureHex`: string +- `infoSignatureRecovery`: integer +- `events`: SpaceEvent[] relation +- `members`: Account[] relation +- `keys`: SpaceKey[] relation +- `appIdentities`: AppIdentity[] relation + +### SpaceKey +- `id`: string (primary key) +- `spaceId`: string (foreign key to Space) +- `keyBoxes`: SpaceKeyBox[] relation + +### SpaceKeyBox +- `id`: string (primary key) +- `spaceKeyId`: string (foreign key to SpaceKey) +- `ciphertext`: string +- `nonce`: string +- `authorPublicKey`: string +- `accountAddress`: string (foreign key to Account) + +### AppIdentity +- `appId`: string +- `address`: string (primary key) +- `accountAddress`: string (foreign key to Account) + +## Implementation Details +- Uses `getAddressByPrivyToken` to validate the Privy token and get signer address +- Uses `isSignerForAccount` to verify the signer has permission for the account +- Uses `listSpacesByAccount` handler to fetch spaces +- Converts space data to response format, including: + - Converting `infoContent` from bytes to hex string + - Filtering key boxes to only include those with content + - Mapping app identities to simplified format + +## Dependencies +- `getAddressByPrivyToken`: Validates Privy token +- `isSignerForAccount`: Verifies signer permissions +- `listSpacesByAccount`: Fetches spaces from database \ No newline at end of file diff --git a/apps/server/api-docs/get-identity.md b/apps/server/api-docs/get-identity.md new file mode 100644 index 00000000..d3254b3d --- /dev/null +++ b/apps/server/api-docs/get-identity.md @@ -0,0 +1,82 @@ +# GET /identity + +## Overview +Retrieves public identity information for either a Connect identity or App identity based on provided parameters. + +## HTTP Method +GET + +## Route +`/identity` + +## Authentication +None required (public endpoint) + +## Request Parameters +Query parameters: +- `accountAddress`: The account address (required) +- `signaturePublicKey`: The signature public key (optional, mutually exclusive with appId) +- `appId`: The application ID (optional, mutually exclusive with signaturePublicKey) + +Note: Either `signaturePublicKey` OR `appId` must be provided, but not both. + +## Request Headers +None + +## Request Body +None + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseIdentity` +```json +{ + "accountAddress": "string", + "signaturePublicKey": "string", + "encryptionPublicKey": "string", + "accountProof": "string", + "keyProof": "string", + "appId": "string" // Optional, only present for app identities +} +``` + +### Error Responses +- 400 Bad Request: + - Missing accountAddress + - Missing both signaturePublicKey and appId +- 404 Not Found: Identity not found + Schema: `Messages.ResponseIdentityNotFoundError` + ```json + { + "accountAddress": "string" + } + ``` + +## Domain Model +### Account (Connect Identity) +- `address`: string (primary key) +- `connectSignaturePublicKey`: string +- `connectEncryptionPublicKey`: string +- `connectAccountProof`: string +- `connectKeyProof`: string + +### AppIdentity +- `address`: string (primary key) +- `appId`: string +- `accountAddress`: string (foreign key to Account) +- `signaturePublicKey`: string +- `encryptionPublicKey`: string +- `accountProof`: string +- `keyProof`: string + +## Implementation Details +- No authentication required (public endpoint) +- Validates required parameters +- Uses `getAppOrConnectIdentity` handler which: + - If signaturePublicKey provided: searches for matching identity + - If appId provided: searches for app identity with that appId +- Returns unified identity response format +- App identities include optional `appId` field in response + +## Dependencies +- `getAppOrConnectIdentity`: Flexible identity lookup handler \ No newline at end of file diff --git a/apps/server/api-docs/get-root.md b/apps/server/api-docs/get-root.md new file mode 100644 index 00000000..c4d76368 --- /dev/null +++ b/apps/server/api-docs/get-root.md @@ -0,0 +1,40 @@ +# GET / + +## Overview +Health check endpoint that returns server status and version information. + +## HTTP Method +GET + +## Route +`/` + +## Authentication +None required + +## Request Parameters +None + +## Request Headers +None + +## Request Body +None + +## Response +### Success Response (200 OK) +``` +Server is running (v0.0.14) +``` + +## Domain Model +This endpoint does not interact with any domain models. + +## Implementation Details +- Simple health check endpoint +- Returns plain text response +- Hardcoded version string in the response +- No database queries or complex logic + +## Error Handling +This endpoint does not have specific error handling. \ No newline at end of file diff --git a/apps/server/api-docs/get-spaces-inbox.md b/apps/server/api-docs/get-spaces-inbox.md new file mode 100644 index 00000000..01988479 --- /dev/null +++ b/apps/server/api-docs/get-spaces-inbox.md @@ -0,0 +1,60 @@ +# GET /spaces/:spaceId/inboxes/:inboxId + +## Overview +Retrieves details for a specific public inbox within a space. + +## HTTP Method +GET + +## Route +`/spaces/:spaceId/inboxes/:inboxId` + +## Authentication +None required (public endpoint) + +## Request Parameters +- `spaceId`: The space ID (URL parameter) +- `inboxId`: The inbox ID (URL parameter) + +## Request Headers +None + +## Request Body +None + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseSpaceInboxPublic` +```json +{ + "inbox": { + "id": "string", + "spaceId": "string", + "isPublic": true, + "authPolicy": "string", // "requires_auth" | "anonymous" | "optional_auth" + "encryptionPublicKey": "string" + } +} +``` + +### Error Responses +- 500 Internal Server Error: Database or server error + +## Domain Model +### SpaceInbox +- `id`: string (primary key) +- `spaceId`: string (foreign key to Space) +- `isPublic`: boolean +- `authPolicy`: string +- `encryptionPublicKey`: string +- `encryptedSecretKey`: string +- `spaceEventId`: string (foreign key to SpaceEvent) + +## Implementation Details +- No authentication required (public endpoint) +- Uses `getSpaceInbox` handler to fetch specific inbox +- Returns public inbox details if found +- Note: Handler should verify inbox is public before returning + +## Dependencies +- `getSpaceInbox`: Fetches specific inbox from database \ No newline at end of file diff --git a/apps/server/api-docs/get-spaces-inboxes.md b/apps/server/api-docs/get-spaces-inboxes.md new file mode 100644 index 00000000..2f3ceec7 --- /dev/null +++ b/apps/server/api-docs/get-spaces-inboxes.md @@ -0,0 +1,66 @@ +# GET /spaces/:spaceId/inboxes + +## Overview +Lists all public inboxes for a specific space. + +## HTTP Method +GET + +## Route +`/spaces/:spaceId/inboxes` + +## Authentication +None required (public endpoint) + +## Request Parameters +- `spaceId`: The space ID (URL parameter) + +## Request Headers +None + +## Request Body +None + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseListSpaceInboxesPublic` +```json +{ + "inboxes": [ + { + "id": "string", + "spaceId": "string", + "isPublic": true, + "authPolicy": "string", // "requires_auth" | "anonymous" | "optional_auth" + "encryptionPublicKey": "string" + } + ] +} +``` + +### Error Responses +- 500 Internal Server Error: Database or server error + +## Domain Model +### SpaceInbox +- `id`: string (primary key) +- `spaceId`: string (foreign key to Space) +- `isPublic`: boolean +- `authPolicy`: string +- `encryptionPublicKey`: string +- `encryptedSecretKey`: string +- `spaceEventId`: string (foreign key to SpaceEvent) + +### Space +- `id`: string (primary key) +- `inboxes`: SpaceInbox[] relation + +## Implementation Details +- No authentication required (public endpoint) +- Uses `listPublicSpaceInboxes` handler to: + - Query all inboxes for the space + - Filter to only public inboxes (isPublic = true) +- Returns list of public inbox metadata + +## Dependencies +- `listPublicSpaceInboxes`: Fetches public inboxes from database \ No newline at end of file diff --git a/apps/server/api-docs/get-whoami.md b/apps/server/api-docs/get-whoami.md new file mode 100644 index 00000000..8dbdb030 --- /dev/null +++ b/apps/server/api-docs/get-whoami.md @@ -0,0 +1,49 @@ +# GET /whoami + +## Overview +Returns the account address associated with the current session token. + +## HTTP Method +GET + +## Route +`/whoami` + +## Authentication +Required - Bearer token authentication (session token) + +## Request Parameters +None + +## Request Headers +- `Authorization`: Bearer token (required) - Format: `Bearer ` + +## Request Body +None + +## Response +### Success Response (200 OK) +``` + +``` +Returns the account address as plain text. + +### Error Responses +- 401 Unauthorized: Invalid or missing session token + +## Domain Model +### AppIdentity +- `sessionToken`: string (indexed) +- `accountAddress`: string (foreign key to Account) +- `sessionTokenExpires`: datetime + +## Implementation Details +- Extracts session token from Authorization header +- Uses `getAppIdentityBySessionToken` handler to: + - Look up app identity by session token + - Verify token is not expired + - Return associated account address +- Returns account address as plain text response + +## Dependencies +- `getAppIdentityBySessionToken`: Validates session token and retrieves account \ No newline at end of file diff --git a/apps/server/api-docs/post-accounts-inbox-messages.md b/apps/server/api-docs/post-accounts-inbox-messages.md new file mode 100644 index 00000000..38bc692d --- /dev/null +++ b/apps/server/api-docs/post-accounts-inbox-messages.md @@ -0,0 +1,85 @@ +# POST /accounts/:accountAddress/inboxes/:inboxId/messages + +## Overview +Posts a new message to an account inbox. Authentication requirements depend on the inbox's auth policy. + +## HTTP Method +POST + +## Route +`/accounts/:accountAddress/inboxes/:inboxId/messages` + +## Authentication +Depends on inbox auth policy: +- `requires_auth`: Signature and account address required +- `anonymous`: No authentication allowed +- `optional_auth`: Authentication optional + +## Request Parameters +- `accountAddress`: The account address (URL parameter) +- `inboxId`: The inbox ID (URL parameter) + +## Request Headers +- `Content-Type`: application/json + +## Request Body +Schema: `Messages.RequestCreateAccountInboxMessage` +```json +{ + "ciphertext": "string", + "signature": { + "hex": "string", + "recovery": "number" + }, // Optional based on auth policy + "authorAccountAddress": "string" // Optional based on auth policy +} +``` + +## Response +### Success Response (200 OK) +```json +{} +``` +Empty object on success. Message is also broadcast via WebSocket. + +### Error Responses +- 400 Bad Request: Invalid authentication for inbox policy +- 403 Forbidden: Not authorized to post to inbox +- 404 Not Found: Inbox not found +- 500 Internal Server Error: Server error + +## Domain Model +### AccountInbox +- `id`: string (primary key) +- `accountAddress`: string (foreign key to Account) +- `authPolicy`: string ("requires_auth" | "anonymous" | "optional_auth") +- `messages`: AccountInboxMessage[] relation + +### AccountInboxMessage +- `id`: string (auto-generated UUID) +- `accountInboxId`: string (foreign key to AccountInbox) +- `ciphertext`: string +- `signatureHex`: string (optional) +- `signatureRecovery`: integer (optional) +- `authorAccountAddress`: string (optional) +- `createdAt`: datetime + +## Implementation Details +- Fetches inbox to check auth policy +- Validates authentication based on policy: + - `requires_auth`: Both signature and authorAccountAddress required + - `anonymous`: Neither allowed + - `optional_auth`: Both must be provided together or neither +- If authenticated: + - Recovers public key from signature using `Inboxes.recoverAccountInboxMessageSigner` + - Verifies public key belongs to claimed account via `getAppOrConnectIdentity` +- Creates message using `createAccountInboxMessage` +- Broadcasts message to WebSocket subscribers via `broadcastAccountInboxMessage` + +## Dependencies +- `getAccountInbox`: Fetches inbox configuration +- `Inboxes.recoverAccountInboxMessageSigner`: Recovers signer from signature +- `getAppOrConnectIdentity`: Verifies identity ownership +- `createAccountInboxMessage`: Persists message +- `broadcastAccountInboxMessage`: WebSocket broadcast +- `Schema.decodeUnknownSync`: Validates request body \ No newline at end of file diff --git a/apps/server/api-docs/post-connect-add-app-identity-to-spaces.md b/apps/server/api-docs/post-connect-add-app-identity-to-spaces.md new file mode 100644 index 00000000..53676c9d --- /dev/null +++ b/apps/server/api-docs/post-connect-add-app-identity-to-spaces.md @@ -0,0 +1,79 @@ +# POST /connect/add-app-identity-to-spaces + +## Overview +Adds an app identity to multiple spaces, granting the app access to those spaces. + +## HTTP Method +POST + +## Route +`/connect/add-app-identity-to-spaces` + +## Authentication +Required - Privy ID token authentication + +## Request Parameters +None + +## Request Headers +- `privy-id-token`: Privy authentication token (required) +- `Content-Type`: application/json + +## Request Body +Schema: `Messages.RequestConnectAddAppIdentityToSpaces` +```json +{ + "accountAddress": "string", + "appIdentityAddress": "string", + "spacesInput": [ + { + "spaceId": "string", + // Additional space-specific data + } + ] +} +``` + +## Response +### Success Response (200 OK) +```json +{ + "space": { + // Space object or array of spaces + } +} +``` + +### Error Responses +- 401 Unauthorized: Invalid authentication or insufficient permissions +- 500 Internal Server Error: Missing Privy configuration + +## Domain Model +### AppIdentity +- `address`: string (primary key) +- `accountAddress`: string (foreign key to Account) +- `appId`: string +- `spaces`: Space[] relation (many-to-many) + +### Space +- `id`: string (primary key) +- `appIdentities`: AppIdentity[] relation (many-to-many) + +### Account +- `address`: string (primary key) +- `appIdentities`: AppIdentity[] relation + +## Implementation Details +- Validates Privy token to get signer address +- Verifies signer has permission for the specified account +- Uses `addAppIdentityToSpaces` handler to: + - Verify the app identity belongs to the account + - Add the app identity to each specified space + - Update the many-to-many relationship +- Returns updated space information + +## Dependencies +- `getAddressByPrivyToken`: Validates Privy token +- `isSignerForAccount`: Verifies signer permissions +- `addAppIdentityToSpaces`: Updates space-app identity relationships +- `Schema.decodeUnknownSync`: Validates request body schema \ No newline at end of file diff --git a/apps/server/api-docs/post-connect-app-identity.md b/apps/server/api-docs/post-connect-app-identity.md new file mode 100644 index 00000000..4a499cd7 --- /dev/null +++ b/apps/server/api-docs/post-connect-app-identity.md @@ -0,0 +1,88 @@ +# POST /connect/app-identity + +## Overview +Creates a new app identity for an account with session token generation and ownership verification. + +## HTTP Method +POST + +## Route +`/connect/app-identity` + +## Authentication +Required - Privy ID token authentication + +## Request Parameters +None + +## Request Headers +- `privy-id-token`: Privy authentication token (required) +- `Content-Type`: application/json + +## Request Body +Schema: `Messages.RequestConnectCreateAppIdentity` +```json +{ + "accountAddress": "string", + "appId": "string", + "address": "string", + "ciphertext": "string", + "signaturePublicKey": "string", + "encryptionPublicKey": "string", + "accountProof": "string", + "keyProof": "string" +} +``` + +## Response +### Success Response (200 OK) +```json +{ + "appIdentity": { + "address": "string", + "appId": "string", + "accountAddress": "string", + "sessionToken": "string", + "sessionTokenExpires": "datetime", + // Additional fields + } +} +``` + +### Error Responses +- 401 Unauthorized: Invalid authentication or ownership proof +- 500 Internal Server Error: Missing Privy configuration + +## Domain Model +### AppIdentity +- `address`: string (primary key) +- `appId`: string +- `accountAddress`: string (foreign key to Account) +- `ciphertext`: string +- `signaturePublicKey`: string +- `encryptionPublicKey`: string +- `accountProof`: string +- `keyProof`: string +- `sessionToken`: string +- `sessionTokenExpires`: datetime +- Unique constraint: [accountAddress, appId] + +## Implementation Details +- Validates Privy token and verifies signer permissions +- Verifies ownership proof using `Identity.verifyIdentityOwnership` +- Generates: + - Random 32-byte session token (as hex string) + - Session expiration date (30 days from creation) +- Uses `createAppIdentity` handler to: + - Create the app identity record + - Store encrypted data and public keys + - Save session token for future authentication +- Returns created app identity with session token + +## Dependencies +- `getAddressByPrivyToken`: Validates Privy token +- `isSignerForAccount`: Verifies signer permissions +- `Identity.verifyIdentityOwnership`: Validates ownership proofs +- `createAppIdentity`: Creates app identity record +- `bytesToHex`, `randomBytes`: For session token generation +- `Schema.decodeUnknownSync`: Validates request body schema \ No newline at end of file diff --git a/apps/server/api-docs/post-connect-identity.md b/apps/server/api-docs/post-connect-identity.md new file mode 100644 index 00000000..cca73b3f --- /dev/null +++ b/apps/server/api-docs/post-connect-identity.md @@ -0,0 +1,89 @@ +# POST /connect/identity + +## Overview +Creates a new identity for an account with encryption and signature keys. Includes ownership verification. + +## HTTP Method +POST + +## Route +`/connect/identity` + +## Authentication +Required - Privy ID token authentication + +## Request Parameters +None + +## Request Headers +- `privy-id-token`: Privy authentication token (required) +- `Content-Type`: application/json + +## Request Body +Schema: `Messages.RequestConnectCreateIdentity` +```json +{ + "keyBox": { + "accountAddress": "string", + "signer": "string", + "ciphertext": "string", + "nonce": "string" + }, + "signaturePublicKey": "string", + "encryptionPublicKey": "string", + "accountProof": "string", + "keyProof": "string" +} +``` + +## Response +### Success Response (200 OK) +Schema: `Messages.ResponseConnectCreateIdentity` +```json +{ + "success": true +} +``` + +### Error Response (400 Bad Request) +Schema: `Messages.ResponseIdentityExistsError` +```json +{ + "accountAddress": "string" +} +``` + +### Other Error Responses +- 401 Unauthorized: Invalid authentication or ownership proof +- 500 Internal Server Error: Missing Privy configuration + +## Domain Model +### Account +- `address`: string (primary key) +- `connectAddress`: string (unique) +- `connectCiphertext`: string +- `connectNonce`: string +- `connectSignaturePublicKey`: string +- `connectEncryptionPublicKey`: string +- `connectAccountProof`: string +- `connectKeyProof`: string +- `connectSignerAddress`: string + +## Implementation Details +- Validates Privy token and ensures it matches the signer in the keyBox +- Verifies ownership proof using `Identity.verifyIdentityOwnership`: + - Validates account ownership + - Validates signature public key + - Validates proofs against the blockchain +- Uses `createIdentity` handler to: + - Create or update the Account record + - Store encrypted identity data + - Store public keys and proofs +- Returns success or specific error for existing identity + +## Dependencies +- `getAddressByPrivyToken`: Validates Privy token +- `Identity.verifyIdentityOwnership`: Validates ownership proofs +- `createIdentity`: Creates identity record +- `Schema.decodeUnknownSync`: Validates request body schema +- Chain configuration (CHAIN, RPC_URL) for blockchain verification \ No newline at end of file diff --git a/apps/server/api-docs/post-connect-spaces.md b/apps/server/api-docs/post-connect-spaces.md new file mode 100644 index 00000000..dcfdbd57 --- /dev/null +++ b/apps/server/api-docs/post-connect-spaces.md @@ -0,0 +1,102 @@ +# POST /connect/spaces + +## Overview +Creates a new space with initial configuration, including encryption keys and space information. + +## HTTP Method +POST + +## Route +`/connect/spaces` + +## Authentication +Required - Privy ID token authentication + +## Request Parameters +None + +## Request Headers +- `privy-id-token`: Privy authentication token (required) +- `Content-Type`: application/json + +## Request Body +Schema: `Messages.RequestConnectCreateSpaceEvent` +```json +{ + "accountAddress": "string", + "event": { + // SpaceEvent object + }, + "keyBox": { + // KeyBox object + }, + "infoContent": "hex string", + "infoSignature": { + "hex": "string", + "recovery": "number" + }, + "name": "string" +} +``` + +## Response +### Success Response (200 OK) +```json +{ + "space": { + "id": "string", + // Space object + } +} +``` + +### Error Responses +- 401 Unauthorized: Invalid authentication or insufficient permissions +- 500 Internal Server Error: Missing Privy configuration + +## Domain Model +### Space +- `id`: string (primary key) +- `name`: string +- `infoContent`: bytes +- `infoAuthorAddress`: string (foreign key to Account) +- `infoSignatureHex`: string +- `infoSignatureRecovery`: integer +- `events`: SpaceEvent[] relation +- `members`: Account[] relation +- `keys`: SpaceKey[] relation + +### SpaceEvent +- `id`: string (primary key) +- `event`: string (serialized event data) +- `state`: string +- `counter`: integer +- `spaceId`: string (foreign key to Space) + +### SpaceKey +- `id`: string (primary key) +- `spaceId`: string (foreign key to Space) +- `keyBoxes`: SpaceKeyBox[] relation + +### SpaceKeyBox +- `ciphertext`: string +- `nonce`: string +- `authorPublicKey`: string +- `accountAddress`: string (foreign key to Account) + +## Implementation Details +- Validates Privy token to get signer address +- Verifies signer has permission for the specified account +- Converts hex info content to bytes +- Uses `createSpace` handler to: + - Create the space record + - Store the initial space event + - Create encryption key boxes +- Returns the created space object + +## Dependencies +- `getAddressByPrivyToken`: Validates Privy token +- `isSignerForAccount`: Verifies signer permissions +- `createSpace`: Creates space and related records +- `Schema.decodeUnknownSync`: Validates request body schema +- `Utils.hexToBytes`: Converts hex strings to byte arrays \ No newline at end of file diff --git a/apps/server/api-docs/post-spaces-inbox-messages.md b/apps/server/api-docs/post-spaces-inbox-messages.md new file mode 100644 index 00000000..619f77cc --- /dev/null +++ b/apps/server/api-docs/post-spaces-inbox-messages.md @@ -0,0 +1,85 @@ +# POST /spaces/:spaceId/inboxes/:inboxId/messages + +## Overview +Posts a new message to a space inbox. Authentication requirements depend on the inbox's auth policy. + +## HTTP Method +POST + +## Route +`/spaces/:spaceId/inboxes/:inboxId/messages` + +## Authentication +Depends on inbox auth policy: +- `requires_auth`: Signature and account address required +- `anonymous`: No authentication allowed +- `optional_auth`: Authentication optional + +## Request Parameters +- `spaceId`: The space ID (URL parameter) +- `inboxId`: The inbox ID (URL parameter) + +## Request Headers +- `Content-Type`: application/json + +## Request Body +Schema: `Messages.RequestCreateSpaceInboxMessage` +```json +{ + "ciphertext": "string", + "signature": { + "hex": "string", + "recovery": "number" + }, // Optional based on auth policy + "authorAccountAddress": "string" // Optional based on auth policy +} +``` + +## Response +### Success Response (200 OK) +```json +{} +``` +Empty object on success. Message is also broadcast via WebSocket. + +### Error Responses +- 400 Bad Request: Invalid authentication for inbox policy +- 403 Forbidden: Not authorized to post to inbox +- 404 Not Found: Inbox not found +- 500 Internal Server Error: Server error + +## Domain Model +### SpaceInbox +- `id`: string (primary key) +- `spaceId`: string (foreign key to Space) +- `authPolicy`: string ("requires_auth" | "anonymous" | "optional_auth") +- `messages`: SpaceInboxMessage[] relation + +### SpaceInboxMessage +- `id`: string (auto-generated UUID) +- `spaceInboxId`: string (foreign key to SpaceInbox) +- `ciphertext`: string +- `signatureHex`: string (optional) +- `signatureRecovery`: integer (optional) +- `authorAccountAddress`: string (optional) +- `createdAt`: datetime + +## Implementation Details +- Fetches inbox to check auth policy +- Validates authentication based on policy: + - `requires_auth`: Both signature and authorAccountAddress required + - `anonymous`: Neither allowed + - `optional_auth`: Both must be provided together or neither +- If authenticated: + - Recovers public key from signature using `Inboxes.recoverSpaceInboxMessageSigner` + - Verifies public key belongs to claimed account via `getAppOrConnectIdentity` +- Creates message using `createSpaceInboxMessage` +- Broadcasts message to WebSocket subscribers via `broadcastSpaceInboxMessage` + +## Dependencies +- `getSpaceInbox`: Fetches inbox configuration +- `Inboxes.recoverSpaceInboxMessageSigner`: Recovers signer from signature +- `getAppOrConnectIdentity`: Verifies identity ownership +- `createSpaceInboxMessage`: Persists message +- `broadcastSpaceInboxMessage`: WebSocket broadcast +- `Schema.decodeUnknownSync`: Validates request body \ No newline at end of file diff --git a/apps/server/api-docs/websocket-connection.md b/apps/server/api-docs/websocket-connection.md new file mode 100644 index 00000000..d045da1e --- /dev/null +++ b/apps/server/api-docs/websocket-connection.md @@ -0,0 +1,224 @@ +# WebSocket Connection + +## Overview +WebSocket endpoint for real-time communication between clients and server. Handles space subscriptions, updates, events, and inbox messages. + +## Connection URL +`ws://[host]:[port]/?token=[sessionToken]` + +## Authentication +Required - Session token as query parameter + +## Connection Process +1. Client connects with session token in query parameter +2. Server validates token via `getAppIdentityBySessionToken` +3. If valid, connection established with: + - `accountAddress`: Associated account + - `appIdentityAddress`: App identity address + - `subscribedSpaces`: Empty set (populated via subscriptions) +4. If invalid, connection closed + +## WebSocket Message Types + +### Client to Server Messages +All messages use `Messages.RequestMessage` schema, serialized/deserialized via `Messages.serialize/deserialize`. + +#### subscribe-space +Subscribe to updates for a specific space. +```json +{ + "type": "subscribe-space", + "id": "spaceId" +} +``` +Response: `ResponseSpace` message with full space data + +#### list-spaces +List all spaces accessible by the app identity. +```json +{ + "type": "list-spaces" +} +``` +Response: `ResponseListSpaces` with array of spaces + +#### list-invitations +List all invitations for the account. +```json +{ + "type": "list-invitations" +} +``` +Response: `ResponseListInvitations` with invitations + +#### create-space-event +Create a new space with initial event. +```json +{ + "type": "create-space-event", + "event": { /* SpaceEvent */ }, + "keyBox": { /* KeyBox */ }, + "name": "string" +} +``` +Response: `ResponseSpace` with created space + +#### create-invitation-event +Create an invitation to a space. +```json +{ + "type": "create-invitation-event", + "spaceId": "string", + "event": { /* SpaceEvent */ }, + "keyBoxes": [ /* KeyBox[] */ ] +} +``` +Response: `ResponseSpace` and broadcasts to invitee + +#### accept-invitation-event +Accept a space invitation. +```json +{ + "type": "accept-invitation-event", + "spaceId": "string", + "event": { /* SpaceEvent */ } +} +``` +Response: `ResponseSpace` and broadcasts event + +#### create-space-inbox-event +Create an inbox for a space. +```json +{ + "type": "create-space-inbox-event", + "spaceId": "string", + "event": { /* SpaceEvent */ } +} +``` +Response: `ResponseSpace` and broadcasts event + +#### create-account-inbox +Create an inbox for the account. +```json +{ + "type": "create-account-inbox", + "accountAddress": "string", + "id": "string", + "isPublic": boolean, + "authPolicy": "string", + "encryptionPublicKey": "string", + "signature": { "hex": "string", "recovery": number } +} +``` +Broadcasts to other clients of same account + +#### get-latest-space-inbox-messages +Retrieve recent messages from a space inbox. +```json +{ + "type": "get-latest-space-inbox-messages", + "spaceId": "string", + "inboxId": "string", + "since": "datetime" // Optional +} +``` +Response: `ResponseSpaceInboxMessages` + +#### get-latest-account-inbox-messages +Retrieve recent messages from an account inbox. +```json +{ + "type": "get-latest-account-inbox-messages", + "inboxId": "string", + "since": "datetime" // Optional +} +``` +Response: `ResponseAccountInboxMessages` + +#### get-account-inboxes +List all inboxes for the account. +```json +{ + "type": "get-account-inboxes" +} +``` +Response: `ResponseAccountInboxes` + +#### create-update +Create a CRDT update for a space. +```json +{ + "type": "create-update", + "accountAddress": "string", + "spaceId": "string", + "update": "string", // Serialized update + "signature": { "hex": "string", "recovery": number }, + "updateId": "string" +} +``` +Response: `ResponseUpdateConfirmed` and broadcasts to subscribers + +### Server to Client Messages + +#### space-event (broadcast) +```json +{ + "type": "space-event", + "spaceId": "string", + "event": { /* SpaceEvent */ } +} +``` + +#### updates-notification (broadcast) +```json +{ + "type": "updates-notification", + "spaceId": "string", + "updates": { + "updates": [ /* Update[] */ ], + "firstUpdateClock": number, + "lastUpdateClock": number + } +} +``` + +#### space-inbox-message (broadcast) +```json +{ + "type": "space-inbox-message", + "spaceId": "string", + "inboxId": "string", + "message": { /* InboxMessage */ } +} +``` + +#### account-inbox-message (broadcast) +```json +{ + "type": "account-inbox-message", + "accountAddress": "string", + "inboxId": "string", + "message": { /* InboxMessage */ } +} +``` + +#### account-inbox (broadcast) +```json +{ + "type": "account-inbox", + "inbox": { /* AccountInboxPublic */ } +} +``` + +## Domain Models +See individual route documentation for detailed model descriptions. + +## Broadcasting Rules +- **Space events/updates**: Broadcast to all clients subscribed to the space +- **Account inbox messages**: Broadcast to all clients with same account address +- **Invitations**: Broadcast to invitee's connected clients + +## Error Handling +- Invalid messages are logged but don't close connection +- Authentication failures close the connection immediately +- Database errors are logged, client may not receive response \ No newline at end of file diff --git a/package.json b/package.json index 2214e2fb..bdbff3ca 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@biomejs/biome": "^2.2.0", "@changesets/cli": "^2.29.6", "@graphprotocol/grc-20": "^0.24.1", + "@effect/vitest": "^0.25.1", "babel-plugin-annotate-pure-calls": "^0.5.0", "glob": "^11.0.3", "pkg-pr-new": "^0.0.56", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38f0cab4..da9fef25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@changesets/cli': specifier: ^2.29.6 version: 2.29.6(@types/node@24.3.0) + '@effect/vitest': + specifier: ^0.25.1 + version: 0.25.1(effect@3.17.8)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) '@graphprotocol/grc-20': specifier: ^0.24.1 version: 0.24.1(bufferutil@4.0.9)(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -355,6 +358,52 @@ importers: specifier: ^5.9.2 version: 5.9.2 + apps/server-new: + dependencies: + '@effect/platform': + specifier: ^0.90.0 + version: 0.90.6(effect@3.17.8) + '@effect/platform-node': + specifier: ^0.94.0 + version: 0.94.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) + '@graphprotocol/hypergraph': + specifier: workspace:* + version: link:../../packages/hypergraph/publish + '@prisma/client': + specifier: ^6.7.0 + version: 6.14.0(prisma@6.14.0(typescript@5.9.2))(typescript@5.9.2) + '@privy-io/server-auth': + specifier: ^1.26.0 + version: 1.31.1(bufferutil@4.0.9)(encoding@0.1.13)(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + cors: + specifier: ^2.8.5 + version: 2.8.5 + effect: + specifier: ^3.17.3 + version: 3.17.8 + devDependencies: + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/node': + specifier: ^24.1.0 + version: 24.3.0 + prisma: + specifier: ^6.7.0 + version: 6.14.0(typescript@5.9.2) + tsup: + specifier: ^8.4.0 + version: 8.5.0(@swc/core@1.11.24(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2)(yaml@2.8.1) + tsx: + specifier: ^4.19.0 + version: 4.20.4 + typescript: + specifier: ^5.8.3 + version: 5.9.2 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + apps/template-nextjs: dependencies: '@graphprotocol/hypergraph': @@ -2298,6 +2347,15 @@ packages: resolution: {integrity: sha512-J7GbtthuYeruD4kYUHn3QEZtbl9v7OX9+ElD20mDBGBMA+Q6W4KnVMxZc+yDvKQBBYvfXImVUSzBbXzbrZJpyg==} hasBin: true + '@effect/platform-node-shared@0.47.2': + resolution: {integrity: sha512-mtXNAx7Rzbfmp8hsMnyYebIkdCoKKOMa61uRLfOgbOV0p/ksO99YHYZSE/UTE2fFeoF9f2oi0mwOj/G7EMqzng==} + peerDependencies: + '@effect/cluster': ^0.46.4 + '@effect/platform': ^0.90.0 + '@effect/rpc': ^0.68.3 + '@effect/sql': ^0.44.1 + effect: ^3.17.6 + '@effect/platform-node-shared@0.49.0': resolution: {integrity: sha512-6ufPQUtofYW+jsADRI4Pa4sMY+kc0dcoXWpH1ozH/bD6I5c2au1n/wDffnLoXMeHGYSpt/54Dd7WOqqNcOdXlg==} peerDependencies: @@ -2307,6 +2365,15 @@ packages: '@effect/sql': ^0.44.1 effect: ^3.17.7 + '@effect/platform-node@0.94.2': + resolution: {integrity: sha512-iI7vUjNqd1DOFCa/9Tyf6Cu00Y4oLKMrpa2lx8+bUIHxtYbk696Yd9VFIDLMXVWrKFUru4Fw7WgWaA/YDor/sw==} + peerDependencies: + '@effect/cluster': ^0.46.4 + '@effect/platform': ^0.90.0 + '@effect/rpc': ^0.68.3 + '@effect/sql': ^0.44.1 + effect: ^3.17.6 + '@effect/platform-node@0.96.0': resolution: {integrity: sha512-9v6UJnSiQGq90gYPdakcLjkyX951ZODLwtkZgXjdKwjvcpx5C1Feq+LDsSifF3aOg1NgamwAGYDKi00JQxK6Cg==} peerDependencies: @@ -3657,12 +3724,12 @@ packages: resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} engines: {node: '>=14'} - '@oxc-project/runtime@0.82.2': - resolution: {integrity: sha512-cYxcj5CPn/vo5QSpCZcYzBiLidU5+GlFSqIeNaMgBDtcVRBsBJHZg3pHw999W6nHamFQ1EHuPPByB26tjaJiJw==} + '@oxc-project/runtime@0.82.3': + resolution: {integrity: sha512-LNh5GlJvYHAnMurO+EyA8jJwN1rki7l3PSHuosDh2I7h00T6/u9rCkUjg/SvPmT1CZzvhuW0y+gf7jcqUy/Usg==} engines: {node: '>=6.9.0'} - '@oxc-project/types@0.82.2': - resolution: {integrity: sha512-WMGSwd9FsNBs/WfqIOH0h3k1LBdjZJQGYjGnC+vla/fh6HUsu5HzGPerRljiq1hgMQ6gs031YJR12VyP57b/hQ==} + '@oxc-project/types@0.82.3': + resolution: {integrity: sha512-6nCUxBnGX0c6qfZW5MaF6/fmu5dHJDMiMPaioKHKs5mi5+8/FHQ7WGjgQIz1zxpmceMYfdIXkOaLYE+ejbuOtA==} '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} @@ -4256,81 +4323,81 @@ packages: peerDependencies: viem: ^2.0.0 - '@rolldown/binding-android-arm64@1.0.0-beta.33': - resolution: {integrity: sha512-xhDQXKftRkEULIxCddrKMR8y0YO/Y+6BKk/XrQP2B29YjV2wr8DByoEz+AHX9BfLHb2srfpdN46UquBW2QXWpQ==} + '@rolldown/binding-android-arm64@1.0.0-beta.34': + resolution: {integrity: sha512-jf5GNe5jP3Sr1Tih0WKvg2bzvh5T/1TA0fn1u32xSH7ca/p5t+/QRr4VRFCV/na5vjwKEhwWrChsL2AWlY+eoA==} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.33': - resolution: {integrity: sha512-7lhhY08v5ZtRq8JJQaJ49fnJombAPnqllKKCDLU/UvaqNAOEyTGC8J1WVOLC4EA4zbXO5U3CCRgVGyAFNH2VtQ==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.34': + resolution: {integrity: sha512-2F/TqH4QuJQ34tgWxqBjFL3XV1gMzeQgUO8YRtCPGBSP0GhxtoFzsp7KqmQEothsxztlv+KhhT9Dbg3HHwHViQ==} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.33': - resolution: {integrity: sha512-U2iGjcDV7NWyYyhap8YuY0nwrLX6TvX/9i7gBtdEMPm9z3wIUVGNMVdGlA43uqg7xDpRGpEqGnxbeDgiEwYdnA==} + '@rolldown/binding-darwin-x64@1.0.0-beta.34': + resolution: {integrity: sha512-E1QuFslgLWbHQ8Qli/AqUKdfg0pockQPwRxVbhNQ74SciZEZpzLaujkdmOLSccMlSXDfFCF8RPnMoRAzQ9JV8Q==} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.33': - resolution: {integrity: sha512-gd6ASromVHFLlzrjJWMG5CXHkS7/36DEZ8HhvGt2NN8eZALCIuyEx8HMMLqvKA7z4EAztVkdToVrdxpGMsKZxw==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.34': + resolution: {integrity: sha512-VS8VInNCwnkpI9WeQaWu3kVBq9ty6g7KrHdLxYMzeqz24+w9hg712TcWdqzdY6sn+24lUoMD9jTZrZ/qfVpk0g==} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.33': - resolution: {integrity: sha512-xmeLfkfGthuynO1EpCdyTVr0r4G+wqvnKCuyR6rXOet+hLrq5HNAC2XtP/jU2TB4Bc6aiLYxl868B8CGtFDhcw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.34': + resolution: {integrity: sha512-4St4emjcnULnxJYb/5ZDrH/kK/j6PcUgc3eAqH5STmTrcF+I9m/X2xvSF2a2bWv1DOQhxBewThu0KkwGHdgu5w==} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.33': - resolution: {integrity: sha512-cHGp8yfHL4pes6uaLbO5L58ceFkUK4efd8iE86jClD1QPPDLKiqEXJCFYeuK3OfODuF5EBOmf0SlcUZNEYGdmw==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.34': + resolution: {integrity: sha512-a737FTqhFUoWfnebS2SnQ2BS50p0JdukdkUBwy2J06j4hZ6Eej0zEB8vTfAqoCjn8BQKkXBy+3Sx0IRkgwz1gA==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.33': - resolution: {integrity: sha512-wZ1t7JAvVeFgskH1L9y7c47ITitPytpL0s8FmAT8pVfXcaTmS58ZyoXT+y6cz8uCkQnETjrX3YezTGI18u3ecg==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.34': + resolution: {integrity: sha512-NH+FeQWKyuw0k+PbXqpFWNfvD8RPvfJk766B/njdaWz4TmiEcSB0Nb6guNw1rBpM1FmltQYb3fFnTumtC6pRfA==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.33': - resolution: {integrity: sha512-cDndWo3VEYbm7yeujOV6Ie2XHz0K8YX/R/vbNmMo03m1QwtBKKvbYNSyJb3B9+8igltDjd8zNM9mpiNNrq/ekQ==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.34': + resolution: {integrity: sha512-Q3RSCivp8pNadYK8ke3hLnQk08BkpZX9BmMjgwae2FWzdxhxxUiUzd9By7kneUL0vRQ4uRnhD9VkFQ+Haeqdvw==} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.33': - resolution: {integrity: sha512-bl7uzi6es/l6LT++NZcBpiX43ldLyKXCPwEZGY1rZJ99HQ7m1g3KxWwYCcGxtKjlb2ExVvDZicF6k+96vxOJKg==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.34': + resolution: {integrity: sha512-wDd/HrNcVoBhWWBUW3evJHoo7GJE/RofssBy3Dsiip05YUBmokQVrYAyrboOY4dzs/lJ7HYeBtWQ9hj8wlyF0A==} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.33': - resolution: {integrity: sha512-TrgzQanpLgcmmzolCbYA9BPZgF1gYxkIGZhU/HROnJPsq67gcyaYw/JBLioqQLjIwMipETkn25YY799D2OZzJA==} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.34': + resolution: {integrity: sha512-dH3FTEV6KTNWpYSgjSXZzeX7vLty9oBYn6R3laEdhwZftQwq030LKL+5wyQdlbX5pnbh4h127hpv3Hl1+sj8dg==} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.33': - resolution: {integrity: sha512-z0LltdUfvoKak9SuaLz/M9AVSg+RTOZjFksbZXzC6Svl1odyW4ai21VHhZy3m2Faeeb/rl/9efVLayj+qYEGxw==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.34': + resolution: {integrity: sha512-y5BUf+QtO0JsIDKA51FcGwvhJmv89BYjUl8AmN7jqD6k/eU55mH6RJYnxwCsODq5m7KSSTigVb6O7/GqB8wbPw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.33': - resolution: {integrity: sha512-CpvOHyqDNOYx9riD4giyXQDIu72bWRU2Dwt1xFSPlBudk6NumK0OJl6Ch+LPnkp5podQHcQg0mMauAXPVKct7g==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': + resolution: {integrity: sha512-ga5hFhdTwpaNxEiuxZHWnD3ed0GBAzbgzS5tRHpe0ObptxM1a9Xrq6TVfNQirBLwb5Y7T/FJmJi3pmdLy95ljg==} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.33': - resolution: {integrity: sha512-/tNTvZTWHz6HiVuwpR3zR0kGIyCNb+/tFhnJmti+Aw2fAXs3l7Aj0DcXd0646eFKMX8L2w5hOW9H08FXTUkN0g==} + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.34': + resolution: {integrity: sha512-4/MBp9T9eRnZskxWr8EXD/xHvLhdjWaeX/qY9LPRG1JdCGV3DphkLTy5AWwIQ5jhAy2ZNJR5z2fYRlpWU0sIyQ==} cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.33': - resolution: {integrity: sha512-Bb2qK3z7g2mf4zaKRvkohHzweaP1lLbaoBmXZFkY6jJWMm0Z8Pfnh8cOoRlH1IVM1Ufbo8ZZ1WXp1LbOpRMtXw==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.34': + resolution: {integrity: sha512-7O5iUBX6HSBKlQU4WykpUoEmb0wQmonb6ziKFr3dJTHud2kzDnWMqk344T0qm3uGv9Ddq6Re/94pInxo1G2d4w==} cpu: [x64] os: [win32] '@rolldown/pluginutils@1.0.0-beta.32': resolution: {integrity: sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==} - '@rolldown/pluginutils@1.0.0-beta.33': - resolution: {integrity: sha512-she25NCG6NoEPC/SEB4pHs5STcnfI4VBFOzjeI63maSPrWME5J2XC8ogrBgp8NaE/xzj28/kbpSaebiMvFRj+w==} + '@rolldown/pluginutils@1.0.0-beta.34': + resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} '@rollup/plugin-inject@5.0.5': resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} @@ -11001,8 +11068,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.33: - resolution: {integrity: sha512-mgu118ZuRguC8unhPCbdZbyRbjQfEMiWqlojBA5aRIncBelRaBomnHNpGKYkYWeK7twRz5Cql30xgqqrA3Xelw==} + rolldown@1.0.0-beta.34: + resolution: {integrity: sha512-Wwh7EwalMzzX3Yy3VN58VEajeR2Si8+HDNMf706jPLIqU7CxneRW+dQVfznf5O0TWTnJyu4npelwg2bzTXB1Nw==} hasBin: true rollup@4.39.0: @@ -15187,6 +15254,20 @@ snapshots: '@effect/language-service@0.35.2': {} + '@effect/platform-node-shared@0.47.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': + dependencies: + '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) + '@effect/platform': 0.90.6(effect@3.17.8) + '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) + '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) + '@parcel/watcher': 2.5.1 + effect: 3.17.8 + multipasta: 0.2.7 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@effect/platform-node-shared@0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': dependencies: '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) @@ -15201,6 +15282,21 @@ snapshots: - bufferutil - utf-8-validate + '@effect/platform-node@0.94.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': + dependencies: + '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) + '@effect/platform': 0.90.6(effect@3.17.8) + '@effect/platform-node-shared': 0.47.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) + '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) + '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) + effect: 3.17.8 + mime: 3.0.0 + undici: 7.14.0 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@effect/platform-node@0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': dependencies: '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) @@ -16827,9 +16923,9 @@ snapshots: '@opentelemetry/semantic-conventions@1.36.0': {} - '@oxc-project/runtime@0.82.2': {} + '@oxc-project/runtime@0.82.3': {} - '@oxc-project/types@0.82.2': {} + '@oxc-project/types@0.82.3': {} '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -17930,53 +18026,53 @@ snapshots: tslib: 2.8.1 viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@rolldown/binding-android-arm64@1.0.0-beta.33': + '@rolldown/binding-android-arm64@1.0.0-beta.34': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.33': + '@rolldown/binding-darwin-arm64@1.0.0-beta.34': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.33': + '@rolldown/binding-darwin-x64@1.0.0-beta.34': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.33': + '@rolldown/binding-freebsd-x64@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.33': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.33': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.33': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.33': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.33': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.34': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.33': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.34': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.33': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.34': dependencies: '@napi-rs/wasm-runtime': 1.0.3 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.33': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.33': + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.34': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.33': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.34': optional: true '@rolldown/pluginutils@1.0.0-beta.32': {} - '@rolldown/pluginutils@1.0.0-beta.33': {} + '@rolldown/pluginutils@1.0.0-beta.34': {} '@rollup/plugin-inject@5.0.5(rollup@4.47.1)': dependencies: @@ -18968,7 +19064,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 17.0.45 + '@types/node': 24.3.0 '@types/send@0.17.5': dependencies: @@ -26137,7 +26233,7 @@ snapshots: hash-base: 3.0.5 inherits: 2.0.4 - rolldown-plugin-dts@0.15.7(rolldown@1.0.0-beta.33)(typescript@5.9.2): + rolldown-plugin-dts@0.15.7(rolldown@1.0.0-beta.34)(typescript@5.9.2): dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.3 @@ -26147,34 +26243,34 @@ snapshots: debug: 4.4.1 dts-resolver: 2.1.1 get-tsconfig: 4.10.1 - rolldown: 1.0.0-beta.33 + rolldown: 1.0.0-beta.34 optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-beta.33: + rolldown@1.0.0-beta.34: dependencies: - '@oxc-project/runtime': 0.82.2 - '@oxc-project/types': 0.82.2 - '@rolldown/pluginutils': 1.0.0-beta.33 + '@oxc-project/runtime': 0.82.3 + '@oxc-project/types': 0.82.3 + '@rolldown/pluginutils': 1.0.0-beta.34 ansis: 4.1.0 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.33 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.33 - '@rolldown/binding-darwin-x64': 1.0.0-beta.33 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.33 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.33 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.33 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.33 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.33 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.33 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.33 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.33 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.33 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.33 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.33 + '@rolldown/binding-android-arm64': 1.0.0-beta.34 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.34 + '@rolldown/binding-darwin-x64': 1.0.0-beta.34 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.34 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.34 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.34 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.34 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.34 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.34 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.34 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.34 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.34 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.34 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.34 rollup@4.39.0: dependencies: @@ -27131,8 +27227,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.33 - rolldown-plugin-dts: 0.15.7(rolldown@1.0.0-beta.33)(typescript@5.9.2) + rolldown: 1.0.0-beta.34 + rolldown-plugin-dts: 0.15.7(rolldown@1.0.0-beta.34)(typescript@5.9.2) semver: 7.7.2 tinyexec: 1.0.1 tinyglobby: 0.2.14 From 527fbda815f7b1b0c0e11c9bc8aaba410e843b4e Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 1 Aug 2025 17:11:37 +0200 Subject: [PATCH 02/14] add tracing --- apps/server-new/.env.example | 5 +++- apps/server-new/package.json | 3 +- apps/server-new/src/config/honeycomb.ts | 6 ++++ apps/server-new/src/config/privy.ts | 18 +++--------- apps/server-new/src/http/handlers.ts | 10 ++++++- apps/server-new/src/index.ts | 37 ++++++++++++++++++++++-- apps/server-new/src/services/auth.ts | 3 +- pnpm-lock.yaml | 38 +++++++++++++++++++++++++ 8 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 apps/server-new/src/config/honeycomb.ts diff --git a/apps/server-new/.env.example b/apps/server-new/.env.example index 96a52e1c..ecd0561f 100644 --- a/apps/server-new/.env.example +++ b/apps/server-new/.env.example @@ -10,4 +10,7 @@ PRIVY_APP_SECRET=your_privy_app_secret # Hypergraph Configuration HYPERGRAPH_CHAIN=geo-testnet -HYPERGRAPH_RPC_URL= \ No newline at end of file +HYPERGRAPH_RPC_URL= + +# Honeycomb Configuration +HONEYCOMB_API_KEY= diff --git a/apps/server-new/package.json b/apps/server-new/package.json index 83dfb3ac..1d0ef98a 100644 --- a/apps/server-new/package.json +++ b/apps/server-new/package.json @@ -16,6 +16,7 @@ "prebuild": "prisma generate" }, "dependencies": { + "@effect/opentelemetry": "^0.56.0", "@effect/platform": "^0.90.0", "@effect/platform-node": "^0.94.0", "@graphprotocol/hypergraph": "workspace:*", @@ -28,8 +29,8 @@ "@types/cors": "^2.8.17", "@types/node": "^24.1.0", "prisma": "^6.7.0", - "tsx": "^4.19.0", "tsup": "^8.4.0", + "tsx": "^4.19.0", "typescript": "^5.8.3", "vitest": "^3.2.4" } diff --git a/apps/server-new/src/config/honeycomb.ts b/apps/server-new/src/config/honeycomb.ts new file mode 100644 index 00000000..b12d90c7 --- /dev/null +++ b/apps/server-new/src/config/honeycomb.ts @@ -0,0 +1,6 @@ +import { Config } from 'effect'; + +/** + * Honeycomb configuration + */ +export const honeycombApiKeyConfig = Config.redacted('HONEYCOMB_API_KEY').pipe(Config.option); diff --git a/apps/server-new/src/config/privy.ts b/apps/server-new/src/config/privy.ts index d71684ce..8641703f 100644 --- a/apps/server-new/src/config/privy.ts +++ b/apps/server-new/src/config/privy.ts @@ -1,5 +1,4 @@ -import { Config, Effect } from 'effect'; -import { PrivyConfigError } from '../http/errors.js'; +import { Config } from 'effect'; /** * Privy configuration @@ -10,16 +9,7 @@ export const privyAppSecretConfig = Config.redacted('PRIVY_APP_SECRET'); /** * Load and validate Privy configuration */ -export const privyConfig = Effect.fn(function* () { - const appId = yield* privyAppIdConfig; - const appSecret = yield* privyAppSecretConfig; - - if (!appId || !appSecret) { - return yield* Effect.fail(new PrivyConfigError({ message: 'Missing Privy configuration' })); - } - - return { - appId, - appSecret, - }; +export const privyConfig = Config.all({ + appId: privyAppIdConfig, + appSecret: privyAppSecretConfig, }); diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index 584471ad..dfe7e604 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -80,7 +80,15 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han */ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (handlers) => { return handlers - .handle('getWhoami', () => Effect.succeed('Hypergraph Server v2')) + .handle( + 'getWhoami', + Effect.fn(function* () { + yield* Effect.log('Getting whoami'); + yield* Effect.sleep('1 second').pipe(Effect.withSpan('sleeping')); + yield* Effect.sleep('2 second').pipe(Effect.withSpan('sleeping again')); + return 'Hypergraph Server v3'; + }), + ) .handle( 'getConnectIdentity', Effect.fn(function* ({ urlParams }) { diff --git a/apps/server-new/src/index.ts b/apps/server-new/src/index.ts index f84501f6..1a7bdb7b 100644 --- a/apps/server-new/src/index.ts +++ b/apps/server-new/src/index.ts @@ -1,5 +1,36 @@ -import { NodeRuntime } from '@effect/platform-node'; -import { Layer } from 'effect'; +import * as Otlp from '@effect/opentelemetry/Otlp'; +import { FetchHttpClient, PlatformConfigProvider } from '@effect/platform'; +import { NodeContext, NodeRuntime } from '@effect/platform-node'; +import { Effect, Layer, Logger, Option, Redacted } from 'effect'; +import * as Config from './config/honeycomb.ts'; import { server } from './server.ts'; -NodeRuntime.runMain(Layer.launch(server)); +const Observability = Layer.unwrapEffect( + Effect.gen(function* () { + const apiKey = yield* Config.honeycombApiKeyConfig; + if (Option.isNone(apiKey)) { + return Layer.empty; + } + + return Otlp.layer({ + baseUrl: 'https://api.honeycomb.io', + headers: { + 'x-honeycomb-team': Redacted.value(apiKey.value), + }, + resource: { + serviceName: 'hypergraph-server', + }, + }).pipe(Layer.provide(FetchHttpClient.layer)); + }), +); + +const layer = server.pipe( + Layer.provide(Logger.structured), + Layer.provide(Observability), + Layer.provide(PlatformConfigProvider.layerDotEnvAdd('.env')), + Layer.provide(NodeContext.layer), +); + +NodeRuntime.runMain(Layer.launch(layer), { + disablePrettyLogger: true, +}); diff --git a/apps/server-new/src/services/auth.ts b/apps/server-new/src/services/auth.ts index 24110f5f..cca4a68d 100644 --- a/apps/server-new/src/services/auth.ts +++ b/apps/server-new/src/services/auth.ts @@ -20,8 +20,7 @@ export const AuthService = Context.GenericTag('AuthService'); * Auth service implementation */ export const makeAuthService = Effect.fn(function* () { - const config = yield* Config.privyConfig(); - + const config = yield* Config.privyConfig; const privy = new PrivyClient(config.appId, Redacted.value(config.appSecret)); const verifyAuthToken = Effect.fn(function* (token: string) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da9fef25..62f76014 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -360,6 +360,9 @@ importers: apps/server-new: dependencies: + '@effect/opentelemetry': + specifier: ^0.56.0 + version: 0.56.0(@effect/platform@0.90.6(effect@3.17.8))(@opentelemetry/semantic-conventions@1.36.0)(effect@3.17.8) '@effect/platform': specifier: ^0.90.0 version: 0.90.6(effect@3.17.8) @@ -2347,6 +2350,35 @@ packages: resolution: {integrity: sha512-J7GbtthuYeruD4kYUHn3QEZtbl9v7OX9+ElD20mDBGBMA+Q6W4KnVMxZc+yDvKQBBYvfXImVUSzBbXzbrZJpyg==} hasBin: true + '@effect/opentelemetry@0.56.0': + resolution: {integrity: sha512-WRqSnhF5bTMXlyC7q7fH7k+J2cbh7JRD+o2Vpj/H7AtAhdKmPilYMj9eudTyPcM14tgw555mnNRgTS7v6TCp3Q==} + peerDependencies: + '@effect/platform': ^0.90.0 + '@opentelemetry/api': ^1.9 + '@opentelemetry/resources': ^2.0.0 + '@opentelemetry/sdk-logs': ^0.203.0 + '@opentelemetry/sdk-metrics': ^2.0.0 + '@opentelemetry/sdk-trace-base': ^2.0.0 + '@opentelemetry/sdk-trace-node': ^2.0.0 + '@opentelemetry/sdk-trace-web': ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.33.0 + effect: ^3.17.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/resources': + optional: true + '@opentelemetry/sdk-logs': + optional: true + '@opentelemetry/sdk-metrics': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/sdk-trace-node': + optional: true + '@opentelemetry/sdk-trace-web': + optional: true + '@effect/platform-node-shared@0.47.2': resolution: {integrity: sha512-mtXNAx7Rzbfmp8hsMnyYebIkdCoKKOMa61uRLfOgbOV0p/ksO99YHYZSE/UTE2fFeoF9f2oi0mwOj/G7EMqzng==} peerDependencies: @@ -15254,6 +15286,12 @@ snapshots: '@effect/language-service@0.35.2': {} + '@effect/opentelemetry@0.56.0(@effect/platform@0.90.6(effect@3.17.8))(@opentelemetry/semantic-conventions@1.36.0)(effect@3.17.8)': + dependencies: + '@effect/platform': 0.90.6(effect@3.17.8) + '@opentelemetry/semantic-conventions': 1.36.0 + effect: 3.17.8 + '@effect/platform-node-shared@0.47.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': dependencies: '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) From 46ee98230804c1c4fdaa3cbbcc0a9a74eb812946 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Wed, 27 Aug 2025 06:48:07 +0200 Subject: [PATCH 03/14] update dependencies --- apps/server-new/package.json | 14 +- pnpm-lock.yaml | 505 +++++++++++++++++++++++++---------- 2 files changed, 377 insertions(+), 142 deletions(-) diff --git a/apps/server-new/package.json b/apps/server-new/package.json index 1d0ef98a..c103a00d 100644 --- a/apps/server-new/package.json +++ b/apps/server-new/package.json @@ -16,21 +16,21 @@ "prebuild": "prisma generate" }, "dependencies": { - "@effect/opentelemetry": "^0.56.0", + "@effect/opentelemetry": "^0.56.4", "@effect/platform": "^0.90.0", - "@effect/platform-node": "^0.94.0", + "@effect/platform-node": "^0.96.0", "@graphprotocol/hypergraph": "workspace:*", - "@prisma/client": "^6.7.0", - "@privy-io/server-auth": "^1.26.0", + "@prisma/client": "^6.14.0", + "@privy-io/server-auth": "^1.32.0", "cors": "^2.8.5", - "effect": "^3.17.3" + "effect": "^3.17.9" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/node": "^24.1.0", - "prisma": "^6.7.0", + "prisma": "^6.14.0", "tsup": "^8.4.0", - "tsx": "^4.19.0", + "tsx": "^4.20.5", "typescript": "^5.8.3", "vitest": "^3.2.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62f76014..ee967cff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 2.29.6(@types/node@24.3.0) '@effect/vitest': specifier: ^0.25.1 - version: 0.25.1(effect@3.17.8)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 0.25.1(effect@3.17.9)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@graphprotocol/grc-20': specifier: ^0.24.1 version: 0.24.1(bufferutil@4.0.9)(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -40,7 +40,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) apps/connect: dependencies: @@ -97,14 +97,14 @@ importers: version: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) vite: specifier: ^7.1.3 - version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) devDependencies: '@tailwindcss/vite': specifier: ^4.1.12 - version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@tanstack/router-plugin': specifier: ^1.131.27 - version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(webpack@5.101.0) + version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(webpack@5.101.0) '@types/node': specifier: ^24.3.0 version: 24.3.0 @@ -116,7 +116,7 @@ importers: version: 19.1.7(@types/react@19.1.10) '@vitejs/plugin-react': specifier: ^5.0.1 - version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -128,13 +128,13 @@ importers: version: 4.1.12 unplugin-fonts: specifier: ^1.4.0 - version: 1.4.0(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 1.4.0(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) vite-plugin-node-polyfills: specifier: ^0.24.0 - version: 0.24.0(rollup@4.47.1)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 0.24.0(rollup@4.47.1)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.47.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 4.3.0(rollup@4.47.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) apps/events: dependencies: @@ -221,17 +221,17 @@ importers: version: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) vite: specifier: ^7.1.3 - version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) devDependencies: '@biomejs/biome': specifier: 2.2.0 version: 2.2.0 '@tailwindcss/vite': specifier: ^4.1.12 - version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@tanstack/router-plugin': specifier: ^1.131.27 - version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(webpack@5.101.0) + version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(webpack@5.101.0) '@types/node': specifier: ^24.3.0 version: 24.3.0 @@ -246,7 +246,7 @@ importers: version: 10.0.0 '@vitejs/plugin-react': specifier: ^5.0.1 - version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) globals: specifier: ^16.3.0 version: 16.3.0 @@ -353,7 +353,7 @@ importers: version: 8.18.1 tsup: specifier: ^8.5.0 - version: 8.5.0(@swc/core@1.11.24(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2)(yaml@2.8.1) + version: 8.5.0(@swc/core@1.11.24(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.2 @@ -361,29 +361,29 @@ importers: apps/server-new: dependencies: '@effect/opentelemetry': - specifier: ^0.56.0 - version: 0.56.0(@effect/platform@0.90.6(effect@3.17.8))(@opentelemetry/semantic-conventions@1.36.0)(effect@3.17.8) + specifier: ^0.56.4 + version: 0.56.4(@effect/platform@0.90.6(effect@3.17.9))(@opentelemetry/semantic-conventions@1.36.0)(effect@3.17.9) '@effect/platform': specifier: ^0.90.0 - version: 0.90.6(effect@3.17.8) + version: 0.90.6(effect@3.17.9) '@effect/platform-node': - specifier: ^0.94.0 - version: 0.94.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) + specifier: ^0.96.0 + version: 0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10) '@graphprotocol/hypergraph': specifier: workspace:* version: link:../../packages/hypergraph/publish '@prisma/client': - specifier: ^6.7.0 + specifier: ^6.14.0 version: 6.14.0(prisma@6.14.0(typescript@5.9.2))(typescript@5.9.2) '@privy-io/server-auth': - specifier: ^1.26.0 - version: 1.31.1(bufferutil@4.0.9)(encoding@0.1.13)(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + specifier: ^1.32.0 + version: 1.32.0(bufferutil@4.0.9)(encoding@0.1.13)(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) cors: specifier: ^2.8.5 version: 2.8.5 effect: - specifier: ^3.17.3 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 devDependencies: '@types/cors': specifier: ^2.8.17 @@ -392,20 +392,20 @@ importers: specifier: ^24.1.0 version: 24.3.0 prisma: - specifier: ^6.7.0 + specifier: ^6.14.0 version: 6.14.0(typescript@5.9.2) tsup: specifier: ^8.4.0 - version: 8.5.0(@swc/core@1.11.24(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2)(yaml@2.8.1) + version: 8.5.0(@swc/core@1.11.24(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: - specifier: ^4.19.0 - version: 4.20.4 + specifier: ^4.20.5 + version: 4.20.5 typescript: specifier: ^5.8.3 version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) apps/template-nextjs: dependencies: @@ -502,7 +502,7 @@ importers: version: 1.2.8(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tailwindcss/vite': specifier: ^4.1.12 - version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@tanstack/react-query': specifier: ^5.85.5 version: 5.85.5(react@19.1.1) @@ -535,14 +535,14 @@ importers: version: 4.1.12 vite: specifier: ^7.1.3 - version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) devDependencies: '@eslint/js': specifier: ^9.33.0 version: 9.33.0 '@tanstack/router-plugin': specifier: ^1.131.27 - version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(webpack@5.101.0) + version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(webpack@5.101.0) '@types/node': specifier: ^24.3.0 version: 24.3.0 @@ -554,7 +554,7 @@ importers: version: 19.1.7(@types/react@19.1.10) '@vitejs/plugin-react': specifier: ^5.0.1 - version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) eslint: specifier: ^9.33.0 version: 9.33.0(jiti@2.5.1) @@ -796,7 +796,7 @@ importers: version: 19.1.10 '@vitejs/plugin-react': specifier: ^5.0.1 - version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@xstate/store': specifier: ^3.9.2 version: 3.9.2(react@19.1.1)(solid-js@1.9.5) @@ -848,7 +848,7 @@ importers: version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.27)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5)(tiny-invariant@1.3.3) '@tanstack/router-plugin': specifier: ^1.131.27 - version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(webpack@5.101.0) + version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(webpack@5.101.0) effect: specifier: 3.17.8 version: 3.17.8 @@ -888,7 +888,7 @@ importers: version: 4.6.1(@babel/core@7.28.3)(encoding@0.1.13)(graphql@16.11.0) '@tailwindcss/vite': specifier: ^4.1.12 - version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -909,13 +909,13 @@ importers: version: 19.1.7(@types/react@19.1.10) '@vitejs/plugin-react': specifier: ^5.0.1 - version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) jsdom: specifier: ^26.1.0 version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) vite: specifier: ^7.1.3 - version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -2350,10 +2350,10 @@ packages: resolution: {integrity: sha512-J7GbtthuYeruD4kYUHn3QEZtbl9v7OX9+ElD20mDBGBMA+Q6W4KnVMxZc+yDvKQBBYvfXImVUSzBbXzbrZJpyg==} hasBin: true - '@effect/opentelemetry@0.56.0': - resolution: {integrity: sha512-WRqSnhF5bTMXlyC7q7fH7k+J2cbh7JRD+o2Vpj/H7AtAhdKmPilYMj9eudTyPcM14tgw555mnNRgTS7v6TCp3Q==} + '@effect/opentelemetry@0.56.4': + resolution: {integrity: sha512-Gwui1u/n1zgkPAt6oDdmpHVxkO7zxukRQU+Lj/6+C5tjt+O7mFLZD6LJWHnA68l2Rmt+vvfkYHk+JgrPNziwOA==} peerDependencies: - '@effect/platform': ^0.90.0 + '@effect/platform': ^0.90.5 '@opentelemetry/api': ^1.9 '@opentelemetry/resources': ^2.0.0 '@opentelemetry/sdk-logs': ^0.203.0 @@ -2362,7 +2362,7 @@ packages: '@opentelemetry/sdk-trace-node': ^2.0.0 '@opentelemetry/sdk-trace-web': ^2.0.0 '@opentelemetry/semantic-conventions': ^1.33.0 - effect: ^3.17.0 + effect: ^3.17.8 peerDependenciesMeta: '@opentelemetry/api': optional: true @@ -2379,15 +2379,6 @@ packages: '@opentelemetry/sdk-trace-web': optional: true - '@effect/platform-node-shared@0.47.2': - resolution: {integrity: sha512-mtXNAx7Rzbfmp8hsMnyYebIkdCoKKOMa61uRLfOgbOV0p/ksO99YHYZSE/UTE2fFeoF9f2oi0mwOj/G7EMqzng==} - peerDependencies: - '@effect/cluster': ^0.46.4 - '@effect/platform': ^0.90.0 - '@effect/rpc': ^0.68.3 - '@effect/sql': ^0.44.1 - effect: ^3.17.6 - '@effect/platform-node-shared@0.49.0': resolution: {integrity: sha512-6ufPQUtofYW+jsADRI4Pa4sMY+kc0dcoXWpH1ozH/bD6I5c2au1n/wDffnLoXMeHGYSpt/54Dd7WOqqNcOdXlg==} peerDependencies: @@ -2397,15 +2388,6 @@ packages: '@effect/sql': ^0.44.1 effect: ^3.17.7 - '@effect/platform-node@0.94.2': - resolution: {integrity: sha512-iI7vUjNqd1DOFCa/9Tyf6Cu00Y4oLKMrpa2lx8+bUIHxtYbk696Yd9VFIDLMXVWrKFUru4Fw7WgWaA/YDor/sw==} - peerDependencies: - '@effect/cluster': ^0.46.4 - '@effect/platform': ^0.90.0 - '@effect/rpc': ^0.68.3 - '@effect/sql': ^0.44.1 - effect: ^3.17.6 - '@effect/platform-node@0.96.0': resolution: {integrity: sha512-9v6UJnSiQGq90gYPdakcLjkyX951ZODLwtkZgXjdKwjvcpx5C1Feq+LDsSifF3aOg1NgamwAGYDKi00JQxK6Cg==} peerDependencies: @@ -3904,6 +3886,9 @@ packages: '@privy-io/api-base@1.6.0': resolution: {integrity: sha512-ftlqjFw0Ww7Xn6Ad/1kEUsXRfKqNdmJYKat4ryJl2uPh60QXXlPfnf4y17dDFHJlnVb7qY10cCvKVz5ev5gAeg==} + '@privy-io/api-base@1.6.1': + resolution: {integrity: sha512-GUGpW8FlwL+oTlKRNcuDRc5rfz2fPhfcqx2lHT58T4D3F54VxoXnX+NI8vsowogCddNq640d/p5FSKzBQJViZg==} + '@privy-io/chains@0.0.2': resolution: {integrity: sha512-vT+EcPstcKbvrPyGA2YDD1W8YxaJhKFKYGmS9PaycODpL9HvMsPpkJ1y6SddmVAKL+WIow+nH9cV1/q0aCmPXA==} @@ -3926,6 +3911,9 @@ packages: '@privy-io/public-api@2.43.1': resolution: {integrity: sha512-zhGBTghZiwnqdA4YvrXXM7fsz3fWUltSkxNdnQTqKGb/IfV8aZ14ryuWvD4v5oPJGtqVcwKRfdDmW8TMPGZHog==} + '@privy-io/public-api@2.44.0': + resolution: {integrity: sha512-89+jDodeOTCGrs+uazwcnj7b+gdW935yvBBzfuQF7WY3iuZPlT5/FgrvS/91e8NWGTh4tuFEa189q5V+9SdjCw==} + '@privy-io/react-auth@2.21.4': resolution: {integrity: sha512-SHLIEWHmmRrEVJiKl1G49RBPlN9XhQN59+Z61rBhfdW89mf/KYTeV520WIuxRwzYTVVbrAgOMFoMMj4uztPS7Q==} peerDependencies: @@ -3956,6 +3944,17 @@ packages: viem: optional: true + '@privy-io/server-auth@1.32.0': + resolution: {integrity: sha512-X2EaTvRxJy7w4XKt2Tdl+rbbpIFCit8gmz7rFyBQ5T9WeKUFBF3vtBNHz4o8dZxlCgc29yN1hDZV0jVhdeKpVQ==} + peerDependencies: + ethers: ^6 + viem: ^2.24.1 + peerDependenciesMeta: + ethers: + optional: true + viem: + optional: true + '@quansync/fs@0.1.5': resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} @@ -4685,17 +4684,17 @@ packages: '@serenity-kit/noble-sodium@0.2.1': resolution: {integrity: sha512-023EjSl/ZMl8yNmnzeeWJh/V44QyBC82I8xuHltITeWdcyrQHbGnmMZRZOm/uTRinhgqoMzRBNQqbrfyuI5idg==} - '@shikijs/engine-oniguruma@3.11.0': - resolution: {integrity: sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw==} + '@shikijs/engine-oniguruma@3.12.0': + resolution: {integrity: sha512-IfDl3oXPbJ/Jr2K8mLeQVpnF+FxjAc7ZPDkgr38uEw/Bg3u638neSrpwqOTnTHXt1aU0Fk1/J+/RBdst1kVqLg==} - '@shikijs/langs@3.11.0': - resolution: {integrity: sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw==} + '@shikijs/langs@3.12.0': + resolution: {integrity: sha512-HIca0daEySJ8zuy9bdrtcBPhcYBo8wR1dyHk1vKrOuwDsITtZuQeGhEkcEfWc6IDyTcom7LRFCH6P7ljGSCEiQ==} - '@shikijs/themes@3.11.0': - resolution: {integrity: sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q==} + '@shikijs/themes@3.12.0': + resolution: {integrity: sha512-/lxvQxSI5s4qZLV/AuFaA4Wt61t/0Oka/P9Lmpr1UV+HydNCczO3DMHOC/CsXCCpbv4Zq8sMD0cDa7mvaVoj0Q==} - '@shikijs/types@3.11.0': - resolution: {integrity: sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q==} + '@shikijs/types@3.12.0': + resolution: {integrity: sha512-jsFzm8hCeTINC3OCmTZdhR9DOl/foJWplH2Px0bTi4m8z59fnsueLsweX82oGcjRQ7mfQAluQYKGoH2VzsWY4A==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -4714,6 +4713,7 @@ packages: '@simplewebauthn/types@9.0.1': resolution: {integrity: sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -5354,6 +5354,9 @@ packages: '@types/node@22.17.2': resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} + '@types/node@22.18.0': + resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==} + '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} @@ -6152,8 +6155,8 @@ packages: base-x@3.0.11: resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} - base-x@4.0.0: - resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} + base-x@4.0.1: + resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} base-x@5.0.0: resolution: {integrity: sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==} @@ -7258,12 +7261,12 @@ packages: effect@3.16.12: resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} - effect@3.17.6: - resolution: {integrity: sha512-BDVr3TEI6JpTnsZwDzXlzxDtyMS0cwtfWmhqfL3nl7Be/443+geFYAlVpCy7SCkLCck0NbmFX86LtlCZtCgdxA==} - effect@3.17.8: resolution: {integrity: sha512-3X2DahqmaTwDdvdYuX/MFhYA4srjO21NodMWhCXPMRK/3IQlByJyNFpZrXCWfnMrlr6DsLI+EgI3rqqAQtWrIA==} + effect@3.17.9: + resolution: {integrity: sha512-Nkkn9n1zhy30Dq0MpQatDCH7nfYnOIiebkOHNxmmvoVnEDKCto+2ZwDDWFGzcN/ojwfqjRXWGC9Lo91K5kwZCg==} + electron-to-chromium@1.5.152: resolution: {integrity: sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==} @@ -8884,6 +8887,9 @@ packages: libphonenumber-js@1.12.13: resolution: {integrity: sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==} + libphonenumber-js@1.12.14: + resolution: {integrity: sha512-HBAMAV7f3yGYy7ZZN5FxQ1tXJTwC77G5/96Yn/SH/HPyKX2EMLGFuCIYUmdLU7CxxJlQcvJymP/PGLzyapurhQ==} + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -11691,6 +11697,9 @@ packages: svix@1.74.1: resolution: {integrity: sha512-99J8jSsk0viwkoAENVy/zpVoZxwn3kBdUvvpFaWiKjCkkTcqNZdoBqMmariDFceL4Q41ntWfUYxaWD37IAk9Kg==} + svix@1.75.0: + resolution: {integrity: sha512-3GLI1DN4Pfd8Ze+7yxoV3Ocp8hW07/WRDsoIBdjDvR916Fx3gRHaYSoa0WWRdKSqJNj+OTCeDSSv47mSp9ibqw==} + swap-case@2.0.2: resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} @@ -11946,6 +11955,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + engines: {node: '>=18.0.0'} + hasBin: true + tty-browserify@0.0.1: resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} @@ -12086,8 +12100,8 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} - undici@7.14.0: - resolution: {integrity: sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==} + undici@7.15.0: + resolution: {integrity: sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==} engines: {node: '>=20.18.1'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -12416,6 +12430,14 @@ packages: typescript: optional: true + viem@2.35.1: + resolution: {integrity: sha512-BVGrI2xzMa+cWaUhhMuq+RV6t/8aHN08QAPG07OMFb3PBWc0AYubRMyIuxMKncFe8lJdxfRWNRYv1agoM/xSlQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -15278,21 +15300,35 @@ snapshots: '@effect/workflow': 0.1.2(effect@3.17.8) effect: 3.17.8 + '@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9)': + dependencies: + '@effect/platform': 0.90.6(effect@3.17.9) + '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) + '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) + '@effect/workflow': 0.1.2(effect@3.17.9) + effect: 3.17.9 + '@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8)': dependencies: '@effect/platform': 0.90.6(effect@3.17.8) effect: 3.17.8 uuid: 11.1.0 + '@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9)': + dependencies: + '@effect/platform': 0.90.6(effect@3.17.9) + effect: 3.17.9 + uuid: 11.1.0 + '@effect/language-service@0.35.2': {} - '@effect/opentelemetry@0.56.0(@effect/platform@0.90.6(effect@3.17.8))(@opentelemetry/semantic-conventions@1.36.0)(effect@3.17.8)': + '@effect/opentelemetry@0.56.4(@effect/platform@0.90.6(effect@3.17.9))(@opentelemetry/semantic-conventions@1.36.0)(effect@3.17.9)': dependencies: - '@effect/platform': 0.90.6(effect@3.17.8) + '@effect/platform': 0.90.6(effect@3.17.9) '@opentelemetry/semantic-conventions': 1.36.0 - effect: 3.17.8 + effect: 3.17.9 - '@effect/platform-node-shared@0.47.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': + '@effect/platform-node-shared@0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': dependencies: '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) '@effect/platform': 0.90.6(effect@3.17.8) @@ -15306,45 +15342,45 @@ snapshots: - bufferutil - utf-8-validate - '@effect/platform-node-shared@0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': + '@effect/platform-node-shared@0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10)': dependencies: - '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) - '@effect/platform': 0.90.6(effect@3.17.8) - '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) + '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9) + '@effect/platform': 0.90.6(effect@3.17.9) + '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) + '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) '@parcel/watcher': 2.5.1 - effect: 3.17.8 + effect: 3.17.9 multipasta: 0.2.7 ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@0.94.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': + '@effect/platform-node@0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': dependencies: '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) '@effect/platform': 0.90.6(effect@3.17.8) - '@effect/platform-node-shared': 0.47.2(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) + '@effect/platform-node-shared': 0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) effect: 3.17.8 mime: 3.0.0 - undici: 7.14.0 + undici: 7.15.0 ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': + '@effect/platform-node@0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10)': dependencies: - '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) - '@effect/platform': 0.90.6(effect@3.17.8) - '@effect/platform-node-shared': 0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) - '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - effect: 3.17.8 + '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9) + '@effect/platform': 0.90.6(effect@3.17.9) + '@effect/platform-node-shared': 0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10) + '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) + '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) + effect: 3.17.9 mime: 3.0.0 - undici: 7.14.0 + undici: 7.15.0 ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil @@ -15357,6 +15393,13 @@ snapshots: msgpackr: 1.11.5 multipasta: 0.2.7 + '@effect/platform@0.90.6(effect@3.17.9)': + dependencies: + effect: 3.17.9 + find-my-way-ts: 0.1.6 + msgpackr: 1.11.5 + multipasta: 0.2.7 + '@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8)': dependencies: '@effect/printer': 0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8) @@ -15373,6 +15416,11 @@ snapshots: '@effect/platform': 0.90.6(effect@3.17.8) effect: 3.17.8 + '@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9)': + dependencies: + '@effect/platform': 0.90.6(effect@3.17.9) + effect: 3.17.9 + '@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8)': dependencies: '@effect/experimental': 0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) @@ -15381,6 +15429,14 @@ snapshots: effect: 3.17.8 uuid: 11.1.0 + '@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9)': + dependencies: + '@effect/experimental': 0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) + '@effect/platform': 0.90.6(effect@3.17.9) + '@opentelemetry/semantic-conventions': 1.36.0 + effect: 3.17.9 + uuid: 11.1.0 + '@effect/typeclass@0.31.10(effect@3.17.8)': dependencies: effect: 3.17.8 @@ -15390,10 +15446,19 @@ snapshots: effect: 3.17.8 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + '@effect/vitest@0.25.1(effect@3.17.9)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + effect: 3.17.9 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + '@effect/workflow@0.1.2(effect@3.17.8)': dependencies: effect: 3.17.8 + '@effect/workflow@0.1.2(effect@3.17.9)': + dependencies: + effect: 3.17.9 + '@emnapi/core@1.4.5': dependencies: '@emnapi/wasi-threads': 1.0.4 @@ -15940,16 +16005,16 @@ snapshots: '@gerrit0/mini-shiki@3.11.0': dependencies: - '@shikijs/engine-oniguruma': 3.11.0 - '@shikijs/langs': 3.11.0 - '@shikijs/themes': 3.11.0 - '@shikijs/types': 3.11.0 + '@shikijs/engine-oniguruma': 3.12.0 + '@shikijs/langs': 3.12.0 + '@shikijs/themes': 3.12.0 + '@shikijs/types': 3.12.0 '@shikijs/vscode-textmate': 10.0.2 '@graphprotocol/grc-20@0.24.1(bufferutil@4.0.9)(ox@0.6.7(typescript@5.9.2)(zod@3.25.76))(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@bufbuild/protobuf': 1.10.1 - effect: 3.17.6 + effect: 3.17.9 fflate: 0.8.2 fractional-indexing-jittered: 1.0.0 image-size: 2.0.2 @@ -15966,7 +16031,7 @@ snapshots: '@graphprotocol/grc-20@0.24.1(bufferutil@4.0.9)(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@bufbuild/protobuf': 1.10.1 - effect: 3.17.6 + effect: 3.17.9 fflate: 0.8.2 fractional-indexing-jittered: 1.0.0 image-size: 2.0.2 @@ -15983,7 +16048,7 @@ snapshots: '@graphprotocol/grc-20@0.24.1(bufferutil@4.0.9)(ox@0.6.9(typescript@5.9.2)(zod@4.0.17))(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.0.17)': dependencies: '@bufbuild/protobuf': 1.10.1 - effect: 3.17.6 + effect: 3.17.9 fflate: 0.8.2 fractional-indexing-jittered: 1.0.0 image-size: 2.0.2 @@ -17086,6 +17151,10 @@ snapshots: dependencies: zod: 3.25.76 + '@privy-io/api-base@1.6.1': + dependencies: + zod: 3.25.76 + '@privy-io/chains@0.0.2': {} '@privy-io/ethereum@0.0.2(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': @@ -17131,6 +17200,18 @@ snapshots: - typescript - utf-8-validate + '@privy-io/public-api@2.44.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@privy-io/api-base': 1.6.1 + bs58: 5.0.0 + libphonenumber-js: 1.12.14 + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + '@privy-io/react-auth@2.21.4(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(@types/react@19.1.10)(bs58@6.0.0)(bufferutil@4.0.9)(immer@9.0.21)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@base-org/account': 1.1.1(@types/react@19.1.10)(bufferutil@4.0.9)(immer@9.0.21)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -17235,6 +17316,32 @@ snapshots: - typescript - utf-8-validate + '@privy-io/server-auth@1.32.0(bufferutil@4.0.9)(encoding@0.1.13)(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + '@hpke/chacha20poly1305': 1.7.1 + '@hpke/core': 1.7.4 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@privy-io/public-api': 2.44.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@scure/base': 1.2.6 + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) + canonicalize: 2.1.0 + dotenv: 16.6.1 + jose: 4.15.9 + node-fetch-native: 1.6.7 + redaxios: 0.5.1 + svix: 1.75.0 + ts-case-convert: 2.1.0 + type-fest: 3.13.1 + optionalDependencies: + ethers: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + '@quansync/fs@0.1.5': dependencies: quansync: 0.2.11 @@ -18297,20 +18404,20 @@ snapshots: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 - '@shikijs/engine-oniguruma@3.11.0': + '@shikijs/engine-oniguruma@3.12.0': dependencies: - '@shikijs/types': 3.11.0 + '@shikijs/types': 3.12.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.11.0': + '@shikijs/langs@3.12.0': dependencies: - '@shikijs/types': 3.11.0 + '@shikijs/types': 3.12.0 - '@shikijs/themes@3.11.0': + '@shikijs/themes@3.12.0': dependencies: - '@shikijs/types': 3.11.0 + '@shikijs/types': 3.12.0 - '@shikijs/types@3.11.0': + '@shikijs/types@3.12.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -18701,12 +18808,12 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.12 - '@tailwindcss/vite@4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@tailwindcss/node': 4.1.12 '@tailwindcss/oxide': 4.1.12 tailwindcss: 4.1.12 - vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) '@tanstack/form-core@1.19.2': dependencies: @@ -18803,12 +18910,12 @@ snapshots: prettier: 3.6.2 recast: 0.23.11 source-map: 0.7.6 - tsx: 4.20.4 + tsx: 4.20.5 zod: 3.25.76 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))(webpack@5.101.0)': + '@tanstack/router-plugin@1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(webpack@5.101.0)': dependencies: '@babel/core': 7.28.3 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) @@ -18826,7 +18933,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) webpack: 5.101.0 transitivePeerDependencies: - supports-color @@ -19047,6 +19154,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.18.0': + dependencies: + undici-types: 6.21.0 + '@types/node@22.7.5': dependencies: undici-types: 6.19.8 @@ -19102,7 +19213,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 24.3.0 + '@types/node': 17.0.45 '@types/send@0.17.5': dependencies: @@ -19303,7 +19414,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-react@5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))': + '@vitejs/plugin-react@5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.3 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) @@ -19311,7 +19422,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.32 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -19331,6 +19442,14 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -20401,7 +20520,7 @@ snapshots: dependencies: safe-buffer: 5.2.1 - base-x@4.0.0: {} + base-x@4.0.1: {} base-x@5.0.0: {} @@ -20591,7 +20710,7 @@ snapshots: bs58@5.0.0: dependencies: - base-x: 4.0.0 + base-x: 4.0.1 bs58@6.0.0: dependencies: @@ -21593,12 +21712,12 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - effect@3.17.6: + effect@3.17.8: dependencies: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - effect@3.17.8: + effect@3.17.9: dependencies: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 @@ -23608,6 +23727,8 @@ snapshots: libphonenumber-js@1.12.13: {} + libphonenumber-js@1.12.14: {} + lightningcss-darwin-arm64@1.30.1: optional: true @@ -25341,13 +25462,13 @@ snapshots: '@csstools/utilities': 2.0.0(postcss@8.5.6) postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.4)(yaml@2.8.1): + postcss-load-config@6.0.1(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.5)(yaml@2.8.1): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.5.1 postcss: 8.5.6 - tsx: 4.20.4 + tsx: 4.20.5 yaml: 2.8.1 postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.0): @@ -27086,6 +27207,15 @@ snapshots: url-parse: 1.5.10 uuid: 10.0.0 + svix@1.75.0: + dependencies: + '@stablelib/base64': 1.0.1 + '@types/node': 22.18.0 + es6-promise: 4.2.8 + fast-sha256: 1.3.0 + url-parse: 1.5.10 + uuid: 10.0.0 + swap-case@2.0.2: dependencies: tslib: 2.8.1 @@ -27288,7 +27418,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(@swc/core@1.11.24(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2)(yaml@2.8.1): + tsup@8.5.0(@swc/core@1.11.24(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.9) cac: 6.7.14 @@ -27299,7 +27429,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.4)(yaml@2.8.1) + postcss-load-config: 6.0.1(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.5)(yaml@2.8.1) resolve-from: 5.0.0 rollup: 4.47.1 source-map: 0.8.0-beta.0 @@ -27324,6 +27454,13 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tsx@4.20.5: + dependencies: + esbuild: 0.25.9 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + tty-browserify@0.0.1: {} tw-animate-css@1.3.7: {} @@ -27466,7 +27603,7 @@ snapshots: undici@6.21.3: {} - undici@7.14.0: {} + undici@7.15.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -27536,11 +27673,11 @@ snapshots: unpipe@1.0.0: {} - unplugin-fonts@1.4.0(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)): + unplugin-fonts@1.4.0(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)): dependencies: fast-glob: 3.3.3 unplugin: 2.3.5 - vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) unplugin@2.3.5: dependencies: @@ -27840,6 +27977,23 @@ snapshots: - utf-8-validate - zod + viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.9.6 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.8.7(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -27861,20 +28015,41 @@ snapshots: - tsx - yaml - vite-plugin-node-polyfills@0.24.0(rollup@4.47.1)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)): + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-node-polyfills@0.24.0(rollup@4.47.1)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.47.1) node-stdlib-browser: 1.3.1 - vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - rollup - vite-plugin-svgr@4.3.0(rollup@4.47.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)): + vite-plugin-svgr@4.3.0(rollup@4.47.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.47.1) '@svgr/core': 8.1.0(typescript@5.9.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.2)) - vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color @@ -27897,7 +28072,24 @@ snapshots: tsx: 4.20.4 yaml: 2.8.1 - vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1): + vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + esbuild: 0.25.2 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.39.0 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 24.3.0 + fsevents: 2.3.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + terser: 5.43.1 + tsx: 4.20.5 + yaml: 2.8.1 + + vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -27911,7 +28103,7 @@ snapshots: jiti: 2.5.1 lightningcss: 1.30.1 terser: 5.43.1 - tsx: 4.20.4 + tsx: 4.20.5 yaml: 2.8.1 vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1): @@ -27957,6 +28149,49 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.3.0 + jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vm-browserify@1.1.2: {} w3c-xmlserializer@5.0.0: From cb729926d70f00328aa5421c832b02b6824f9f09 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Wed, 27 Aug 2025 09:11:09 +0200 Subject: [PATCH 04/14] implement whoami --- apps/server-new/src/http/api.ts | 6 +- apps/server-new/src/http/handlers.ts | 20 +++++-- apps/server-new/src/server.ts | 8 ++- apps/server-new/src/services/app-identity.ts | 58 ++++++++++++++++++++ apps/server-new/src/services/database.ts | 18 ++++-- 5 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 apps/server-new/src/services/app-identity.ts diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 577a5c4d..1dd17f29 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -131,8 +131,12 @@ export const connectGroup = HttpApiGroup.make('Connect') * Identity endpoints */ export const getWhoamiEndpoint = HttpApiEndpoint.get('getWhoami')`/whoami` + .setHeaders(Schema.Struct({ authorization: Schema.String })) .addSuccess(Schema.String) - .addError(Errors.AuthenticationError, { status: 401 }); + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.InvalidTokenError, { status: 401 }) + .addError(Errors.TokenExpiredError, { status: 401 }) + .addError(Errors.ResourceNotFoundError, { status: 404 }); export const getConnectIdentityEndpoint = HttpApiEndpoint.get('getConnectIdentity')`/connect/identity` .setUrlParams(ConnectIdentityQuery) diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index dfe7e604..4afe64ac 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -1,5 +1,6 @@ import { HttpApiBuilder } from '@effect/platform'; import { Effect, Layer } from 'effect'; +import { AppIdentityService } from '../services/app-identity.js'; import * as Api from './api.js'; import * as Errors from './errors.js'; @@ -82,11 +83,20 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h return handlers .handle( 'getWhoami', - Effect.fn(function* () { - yield* Effect.log('Getting whoami'); - yield* Effect.sleep('1 second').pipe(Effect.withSpan('sleeping')); - yield* Effect.sleep('2 second').pipe(Effect.withSpan('sleeping again')); - return 'Hypergraph Server v3'; + Effect.fn(function* ({ headers }) { + yield* Effect.logInfo('GET /whoami'); + + const authHeader = headers.authorization; + const sessionToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader; + + if (!sessionToken) { + yield* new Errors.AuthenticationError({ message: 'No session token provided' }); + } + + const appIdentityService = yield* AppIdentityService; + const { accountAddress } = yield* appIdentityService.getBySessionToken(sessionToken); + + return accountAddress; }), ) .handle( diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts index 15dbb93e..2f1cd45c 100644 --- a/apps/server-new/src/server.ts +++ b/apps/server-new/src/server.ts @@ -1,12 +1,16 @@ +import { createServer } from 'node:http'; import { HttpApiBuilder, HttpServer } from '@effect/platform'; import { NodeHttpServer } from '@effect/platform-node'; import { Effect, Layer } from 'effect'; -import { createServer } from 'node:http'; import { serverPortConfig } from './config/server.ts'; import { hypergraphApi } from './http/api.ts'; import { HandlersLive } from './http/handlers.ts'; +import { AppIdentityServiceLive } from './services/app-identity.ts'; +import { DatabaseServiceLive } from './services/database.ts'; + +const ServicesLive = Layer.mergeAll(DatabaseServiceLive, Layer.provide(AppIdentityServiceLive, DatabaseServiceLive)); -const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive)); +const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive), Layer.provide(ServicesLive)); export const server = Layer.unwrapEffect( Effect.gen(function* () { diff --git a/apps/server-new/src/services/app-identity.ts b/apps/server-new/src/services/app-identity.ts new file mode 100644 index 00000000..447e131d --- /dev/null +++ b/apps/server-new/src/services/app-identity.ts @@ -0,0 +1,58 @@ +import { Context, Effect, Layer } from 'effect'; +import { InvalidTokenError, ResourceNotFoundError, TokenExpiredError } from '../http/errors.js'; +import { DatabaseService } from './database.js'; + +export interface AppIdentityService { + readonly getBySessionToken: ( + sessionToken: string, + ) => Effect.Effect< + { address: string; accountAddress: string }, + InvalidTokenError | ResourceNotFoundError | TokenExpiredError + >; +} + +export const AppIdentityService = Context.GenericTag('AppIdentityService'); + +export const makeAppIdentityService = Effect.fn(function* () { + const { client } = yield* DatabaseService; + + const getBySessionToken = (sessionToken: string) => + Effect.fn(function* () { + const appIdentity = yield* Effect.tryPromise({ + try: () => + client.appIdentity.findFirst({ + where: { + sessionToken, + }, + select: { + address: true, + sessionTokenExpires: true, + accountAddress: true, + }, + }), + catch: () => new InvalidTokenError({ tokenType: 'session' }), + }); + + if (!appIdentity) { + yield* new ResourceNotFoundError({ + resource: 'AppIdentity', + id: 'session-token', + }); + } + + if (appIdentity.sessionTokenExpires && appIdentity.sessionTokenExpires < new Date()) { + yield* new TokenExpiredError({ tokenType: 'session' }); + } + + return { + address: appIdentity.address, + accountAddress: appIdentity.accountAddress, + }; + })(); + + return { + getBySessionToken, + } as const; +})(); + +export const AppIdentityServiceLive = Layer.effect(AppIdentityService, makeAppIdentityService); diff --git a/apps/server-new/src/services/database.ts b/apps/server-new/src/services/database.ts index 5948d962..6619aa4c 100644 --- a/apps/server-new/src/services/database.ts +++ b/apps/server-new/src/services/database.ts @@ -1,5 +1,5 @@ -import { PrismaClient } from '@prisma/client'; -import { Context, Effect, Layer } from 'effect'; +import { Config, Context, Effect, Layer } from 'effect'; +import { PrismaClient } from '../../prisma/generated/client/client'; /** * Database service interface @@ -17,7 +17,12 @@ export const DatabaseService = Context.GenericTag('DatabaseServ * Database service implementation */ export const makeDatabaseService = Effect.fn(function* () { - const client = new PrismaClient(); + // Get the DATABASE_URL from config + const databaseUrl = yield* Config.string('DATABASE_URL').pipe(Config.withDefault('file:./dev.db')); + + const client = new PrismaClient({ + datasourceUrl: databaseUrl, + }); // Connect to database yield* Effect.tryPromise({ @@ -41,7 +46,12 @@ export const DatabaseServiceLive = Layer.effect(DatabaseService, makeDatabaseSer export const DatabaseServiceLiveWithCleanup = Layer.scoped( DatabaseService, Effect.fn(function* () { - const client = new PrismaClient(); + // Get the DATABASE_URL from config + const databaseUrl = yield* Config.string('DATABASE_URL').pipe(Config.withDefault('file:./dev.db')); + + const client = new PrismaClient({ + datasourceUrl: databaseUrl, + }); // Connect to database yield* Effect.tryPromise({ From c665f92cb04405010a6a63f22af8e43b4eb03c57 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 29 Aug 2025 07:12:35 +0200 Subject: [PATCH 05/14] implement more handlers --- apps/server-new/src/http/api.ts | 9 +- apps/server-new/src/http/handlers.ts | 41 ++++++- apps/server-new/src/server.ts | 11 +- .../src/services/connect-identity.ts | 66 +++++++++++ apps/server-new/src/services/privy-auth.ts | 110 ++++++++++++++++++ apps/server-new/src/services/spaces.ts | 98 ++++++++++++++++ 6 files changed, 328 insertions(+), 7 deletions(-) create mode 100644 apps/server-new/src/services/connect-identity.ts create mode 100644 apps/server-new/src/services/privy-auth.ts create mode 100644 apps/server-new/src/services/spaces.ts diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 1dd17f29..9fbcfa3a 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -73,8 +73,14 @@ export const healthGroup = HttpApiGroup.make('Health').add(statusEndpoint); * Connect API endpoints (Privy authentication) */ export const getConnectSpacesEndpoint = HttpApiEndpoint.get('getConnectSpaces')`/connect/spaces` + .setHeaders(Schema.Struct({ + 'privy-id-token': Schema.String, + 'account-address': Schema.String, + })) .addSuccess(ConnectSpacesResponse) .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.AuthorizationError, { status: 401 }) + .addError(Errors.PrivyTokenError, { status: 401 }) .addError(Errors.PrivyConfigError, { status: 500 }); export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces')`/connect/spaces` @@ -140,7 +146,8 @@ export const getWhoamiEndpoint = HttpApiEndpoint.get('getWhoami')`/whoami` export const getConnectIdentityEndpoint = HttpApiEndpoint.get('getConnectIdentity')`/connect/identity` .setUrlParams(ConnectIdentityQuery) - // .addSuccess(Messages.ResponseIdentity) + .addSuccess(Messages.ResponseIdentity) + .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.ResourceNotFoundError, { status: 404 }); export const getIdentityEndpoint = HttpApiEndpoint.get('getIdentity')`/identity` diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index 4afe64ac..d3423aaa 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -1,6 +1,10 @@ import { HttpApiBuilder } from '@effect/platform'; +import { Messages } from '@graphprotocol/hypergraph'; import { Effect, Layer } from 'effect'; import { AppIdentityService } from '../services/app-identity.js'; +import { ConnectIdentityService } from '../services/connect-identity.js'; +import { PrivyAuthService } from '../services/privy-auth.js'; +import { SpacesService } from '../services/spaces.js'; import * as Api from './api.js'; import * as Errors from './errors.js'; @@ -18,9 +22,17 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han return handlers .handle( 'getConnectSpaces', - Effect.fn(function* ({ request }) { - yield* Effect.logInfo('Getting connect spaces'); - return { spaces: [] }; + Effect.fn(function* ({ headers }) { + yield* Effect.logInfo('GET /connect/spaces'); + + const privyAuthService = yield* PrivyAuthService; + const spacesService = yield* SpacesService; + + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']); + + const spaces = yield* spacesService.listByAccount(headers['account-address']); + + return { spaces }; }), ) .handle( @@ -102,8 +114,27 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h .handle( 'getConnectIdentity', Effect.fn(function* ({ urlParams }) { - yield* Effect.logInfo('Getting connect identity', urlParams); - yield* new Errors.ResourceNotFoundError({ resource: 'Identity', id: 'connect' }); + yield* Effect.logInfo('GET /connect/identity', { accountAddress: urlParams.accountAddress }); + + if (!urlParams.accountAddress) { + yield* new Errors.ValidationError({ + field: 'accountAddress', + message: 'accountAddress is required', + }); + } + + const connectIdentityService = yield* ConnectIdentityService; + const identity = yield* connectIdentityService.getByAccountAddress(urlParams.accountAddress); + + const response: Messages.ResponseIdentity = { + accountAddress: identity.accountAddress, + signaturePublicKey: identity.signaturePublicKey, + encryptionPublicKey: identity.encryptionPublicKey, + accountProof: identity.accountProof, + keyProof: identity.keyProof, + }; + + return response; }), ) .handle( diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts index 2f1cd45c..f26b0c12 100644 --- a/apps/server-new/src/server.ts +++ b/apps/server-new/src/server.ts @@ -6,9 +6,18 @@ import { serverPortConfig } from './config/server.ts'; import { hypergraphApi } from './http/api.ts'; import { HandlersLive } from './http/handlers.ts'; import { AppIdentityServiceLive } from './services/app-identity.ts'; +import { ConnectIdentityServiceLive } from './services/connect-identity.ts'; import { DatabaseServiceLive } from './services/database.ts'; +import { PrivyAuthServiceLive } from './services/privy-auth.ts'; +import { SpacesServiceLive } from './services/spaces.ts'; -const ServicesLive = Layer.mergeAll(DatabaseServiceLive, Layer.provide(AppIdentityServiceLive, DatabaseServiceLive)); +const ServicesLive = Layer.mergeAll( + DatabaseServiceLive, + Layer.provide(AppIdentityServiceLive, DatabaseServiceLive), + Layer.provide(ConnectIdentityServiceLive, DatabaseServiceLive), + Layer.provide(PrivyAuthServiceLive, DatabaseServiceLive), + Layer.provide(SpacesServiceLive, DatabaseServiceLive), +); const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive), Layer.provide(ServicesLive)); diff --git a/apps/server-new/src/services/connect-identity.ts b/apps/server-new/src/services/connect-identity.ts new file mode 100644 index 00000000..dcaa1079 --- /dev/null +++ b/apps/server-new/src/services/connect-identity.ts @@ -0,0 +1,66 @@ +import { Context, Effect, Layer } from 'effect'; +import { ResourceNotFoundError } from '../http/errors.js'; +import { DatabaseService } from './database.js'; + +export interface ConnectIdentityResult { + accountAddress: string; + signaturePublicKey: string; + encryptionPublicKey: string; + accountProof: string; + keyProof: string; +} + +export interface ConnectIdentityService { + readonly getByAccountAddress: (accountAddress: string) => Effect.Effect; +} + +export const ConnectIdentityService = Context.GenericTag('ConnectIdentityService'); + +export const makeConnectIdentityService = Effect.fn(function* () { + const { client } = yield* DatabaseService; + + const getByAccountAddress = (accountAddress: string) => + Effect.fn(function* () { + const account = yield* Effect.tryPromise({ + try: () => + client.account.findFirst({ + where: { address: accountAddress }, + select: { + address: true, + connectSignaturePublicKey: true, + connectEncryptionPublicKey: true, + connectAccountProof: true, + connectKeyProof: true, + }, + }), + catch: () => + new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, + }), + }); + + if (!account) { + return yield* Effect.fail( + new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, + }), + ); + } + + return { + accountAddress: account.address, + signaturePublicKey: account.connectSignaturePublicKey, + encryptionPublicKey: account.connectEncryptionPublicKey, + accountProof: account.connectAccountProof, + keyProof: account.connectKeyProof, + }; + })(); + + return { + getByAccountAddress, + } as const; +})(); + +export const ConnectIdentityServiceLive = Layer.effect(ConnectIdentityService, makeConnectIdentityService); diff --git a/apps/server-new/src/services/privy-auth.ts b/apps/server-new/src/services/privy-auth.ts new file mode 100644 index 00000000..bcbb1a5f --- /dev/null +++ b/apps/server-new/src/services/privy-auth.ts @@ -0,0 +1,110 @@ +import { PrivyClient, type Wallet } from '@privy-io/server-auth'; +import { Config, Context, Effect, Layer } from 'effect'; +import { AuthenticationError, AuthorizationError, PrivyConfigError, PrivyTokenError } from '../http/errors.js'; +import { DatabaseService } from './database.js'; + +export interface PrivyAuthService { + readonly verifyPrivyToken: (idToken: string) => Effect.Effect; + readonly isSignerForAccount: ( + signerAddress: string, + accountAddress: string, + ) => Effect.Effect; + readonly authenticateRequest: ( + idToken: string | undefined, + accountAddress: string, + ) => Effect.Effect; +} + +export const PrivyAuthService = Context.GenericTag('PrivyAuthService'); + +export const makePrivyAuthService = Effect.fn(function* () { + const { client } = yield* DatabaseService; + + const privyAppId = yield* Config.string('PRIVY_APP_ID').pipe(Config.orElse(() => Config.succeed(''))); + const privyAppSecret = yield* Config.string('PRIVY_APP_SECRET').pipe(Config.orElse(() => Config.succeed(''))); + + const verifyPrivyToken = (idToken: string) => + Effect.fn(function* () { + if (!privyAppId || !privyAppSecret) { + yield* new PrivyConfigError({ message: 'Missing Privy configuration' }); + } + + const privy = new PrivyClient(privyAppId, privyAppSecret); + + const user = yield* Effect.tryPromise({ + try: () => privy.getUser({ idToken }), + catch: (error) => + new PrivyTokenError({ + message: `Invalid Privy token: ${error}`, + }), + }); + + if (!user) { + return yield* Effect.fail(new PrivyTokenError({ message: 'Invalid Privy user' })); + } + + const wallet = user.linkedAccounts.find( + (account) => account.type === 'wallet' && account.walletClientType === 'privy', + ) as Wallet | undefined; + + if (!wallet) { + return yield* Effect.fail(new PrivyTokenError({ message: 'No Privy wallet found' })); + } + + return wallet.address; + })(); + + const isSignerForAccount = (signerAddress: string, accountAddress: string) => + Effect.fn(function* () { + const account = yield* Effect.tryPromise({ + try: () => + client.account.findUnique({ + where: { + address: accountAddress, + }, + }), + catch: () => + new AuthorizationError({ + message: 'Failed to verify signer', + accountAddress, + }), + }); + + if (!account) { + return yield* Effect.fail( + new AuthorizationError({ + message: 'Account not found', + accountAddress, + }), + ); + } + + const isAuthorized = account.connectSignerAddress === signerAddress; + if (!isAuthorized) { + yield* new AuthorizationError({ + message: 'Signer not authorized for account', + accountAddress, + }); + } + + return true; + })(); + + const authenticateRequest = (idToken: string | undefined, accountAddress: string) => + Effect.fn(function* () { + if (!idToken) { + return yield* Effect.fail(new AuthenticationError({ message: 'No Privy ID token provided' })); + } + + const signerAddress = yield* verifyPrivyToken(idToken); + yield* isSignerForAccount(signerAddress, accountAddress); + })(); + + return { + verifyPrivyToken, + isSignerForAccount, + authenticateRequest, + } as const; +})(); + +export const PrivyAuthServiceLive = Layer.effect(PrivyAuthService, makePrivyAuthService); diff --git a/apps/server-new/src/services/spaces.ts b/apps/server-new/src/services/spaces.ts new file mode 100644 index 00000000..3aede443 --- /dev/null +++ b/apps/server-new/src/services/spaces.ts @@ -0,0 +1,98 @@ +import { Utils } from '@graphprotocol/hypergraph'; +import { Context, Effect, Layer } from 'effect'; +import { DatabaseError } from '../http/errors.js'; +import { DatabaseService } from './database.js'; + +export interface SpaceInfo { + id: string; + infoContent: string; + infoAuthorAddress: string; + infoSignatureHex: string; + infoSignatureRecovery: number; + name: string; + appIdentities: Array<{ + appId: string; + address: string; + }>; + keyBoxes: Array<{ + id: string; + ciphertext: string; + nonce: string; + authorPublicKey: string; + }>; +} + +export interface SpacesService { + readonly listByAccount: (accountAddress: string) => Effect.Effect; +} + +export const SpacesService = Context.GenericTag('SpacesService'); + +export const makeSpacesService = Effect.fn(function* () { + const { client } = yield* DatabaseService; + + const listByAccount = (accountAddress: string) => + Effect.fn(function* () { + const spaces = yield* Effect.tryPromise({ + try: () => + client.space.findMany({ + where: { + members: { + some: { + address: accountAddress, + }, + }, + }, + include: { + appIdentities: { + select: { + address: true, + appId: true, + }, + }, + keys: { + include: { + keyBoxes: { + where: { + accountAddress, + }, + }, + }, + }, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'listSpacesByAccount', + cause: error, + }), + }); + + return spaces.map((space) => ({ + id: space.id, + infoContent: Utils.bytesToHex(space.infoContent), + infoAuthorAddress: space.infoAuthorAddress, + infoSignatureHex: space.infoSignatureHex, + infoSignatureRecovery: space.infoSignatureRecovery, + name: space.name, + appIdentities: space.appIdentities.map((appIdentity) => ({ + appId: appIdentity.appId, + address: appIdentity.address, + })), + keyBoxes: space.keys + .filter((key) => key.keyBoxes.length > 0) + .map((key) => ({ + id: key.id, + ciphertext: key.keyBoxes[0].ciphertext, + nonce: key.keyBoxes[0].nonce, + authorPublicKey: key.keyBoxes[0].authorPublicKey, + })), + })); + })(); + + return { + listByAccount, + } as const; +})(); + +export const SpacesServiceLive = Layer.effect(SpacesService, makeSpacesService); \ No newline at end of file From 88e783cff9727cafb650c3ba6a60a0d1ab5f66dd Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 29 Aug 2025 07:36:23 +0200 Subject: [PATCH 06/14] add create space handler --- apps/server-new/src/http/api.ts | 10 ++- apps/server-new/src/http/handlers.ts | 27 ++++-- apps/server-new/src/server.ts | 4 +- apps/server-new/src/services/identity.ts | 101 ++++++++++++++++++++++ apps/server-new/src/services/spaces.ts | 105 ++++++++++++++++++++++- 5 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 apps/server-new/src/services/identity.ts diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 9fbcfa3a..7fa5d0dc 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -84,11 +84,17 @@ export const getConnectSpacesEndpoint = HttpApiEndpoint.get('getConnectSpaces')` .addError(Errors.PrivyConfigError, { status: 500 }); export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces')`/connect/spaces` + .setHeaders(Schema.Struct({ + 'privy-id-token': Schema.String, + })) .setPayload(Messages.RequestConnectCreateSpaceEvent) - // .addSuccess(SpaceCreationResponse) + .addSuccess(SpaceCreationResponse) .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.ValidationError, { status: 400 }) - .addError(Errors.PrivyConfigError, { status: 500 }); + .addError(Errors.PrivyTokenError, { status: 401 }) + .addError(Errors.PrivyConfigError, { status: 500 }) + .addError(Errors.DatabaseError, { status: 500 }); export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( 'postConnectAddAppIdentityToSpaces', diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index d3423aaa..f9d7efdd 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -1,5 +1,5 @@ import { HttpApiBuilder } from '@effect/platform'; -import { Messages } from '@graphprotocol/hypergraph'; +import { Messages, Utils } from '@graphprotocol/hypergraph'; import { Effect, Layer } from 'effect'; import { AppIdentityService } from '../services/app-identity.js'; import { ConnectIdentityService } from '../services/connect-identity.js'; @@ -37,12 +37,27 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'postConnectSpaces', - Effect.fn(function* ({ payload }) { - yield* Effect.logInfo('Creating space', payload); - yield* new Errors.ResourceNotFoundError({ - resource: 'postConnectSpaces', - id: 'postConnectSpaces', + Effect.fn(function* ({ headers, payload }) { + yield* Effect.logInfo('POST /connect/spaces'); + + const privyAuthService = yield* PrivyAuthService; + const spacesService = yield* SpacesService; + + // Authenticate the request with Privy token + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress); + + // Create the space + const space = yield* spacesService.createSpace({ + accountAddress: payload.accountAddress, + event: payload.event, + keyBox: payload.keyBox, + infoContent: Utils.hexToBytes(payload.infoContent), + infoSignatureHex: payload.infoSignature.hex, + infoSignatureRecovery: payload.infoSignature.recovery, + name: payload.name, }); + + return { space }; }), ) .handle( diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts index f26b0c12..7d817199 100644 --- a/apps/server-new/src/server.ts +++ b/apps/server-new/src/server.ts @@ -8,6 +8,7 @@ import { HandlersLive } from './http/handlers.ts'; import { AppIdentityServiceLive } from './services/app-identity.ts'; import { ConnectIdentityServiceLive } from './services/connect-identity.ts'; import { DatabaseServiceLive } from './services/database.ts'; +import { IdentityServiceLive } from './services/identity.ts'; import { PrivyAuthServiceLive } from './services/privy-auth.ts'; import { SpacesServiceLive } from './services/spaces.ts'; @@ -15,8 +16,9 @@ const ServicesLive = Layer.mergeAll( DatabaseServiceLive, Layer.provide(AppIdentityServiceLive, DatabaseServiceLive), Layer.provide(ConnectIdentityServiceLive, DatabaseServiceLive), + Layer.provide(IdentityServiceLive, DatabaseServiceLive), Layer.provide(PrivyAuthServiceLive, DatabaseServiceLive), - Layer.provide(SpacesServiceLive, DatabaseServiceLive), + Layer.provide(SpacesServiceLive, Layer.mergeAll(DatabaseServiceLive, IdentityServiceLive)), ); const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive), Layer.provide(ServicesLive)); diff --git a/apps/server-new/src/services/identity.ts b/apps/server-new/src/services/identity.ts new file mode 100644 index 00000000..7740c9e1 --- /dev/null +++ b/apps/server-new/src/services/identity.ts @@ -0,0 +1,101 @@ +import { Context, Effect, Layer } from 'effect'; +import { ResourceNotFoundError } from '../http/errors.js'; +import { DatabaseService } from './database.js'; + +export interface IdentityResult { + accountAddress: string; + ciphertext: string; + nonce?: string; + signaturePublicKey: string; + encryptionPublicKey: string; + accountProof: string; + keyProof: string; + appId: string | null; +} + +export interface IdentityService { + readonly getAppOrConnectIdentity: (params: { + accountAddress: string; + signaturePublicKey: string; + }) => Effect.Effect; +} + +export const IdentityService = Context.GenericTag('IdentityService'); + +export const makeIdentityService = Effect.fn(function* () { + const { client } = yield* DatabaseService; + + const getAppOrConnectIdentity = (params: { accountAddress: string; signaturePublicKey: string }) => + Effect.fn(function* () { + // First try to find Connect identity + const account = yield* Effect.tryPromise({ + try: () => + client.account.findFirst({ + where: { + address: params.accountAddress, + connectSignaturePublicKey: params.signaturePublicKey, + }, + }), + catch: () => + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + }); + + if (account) { + return { + accountAddress: account.address, + ciphertext: account.connectCiphertext, + nonce: account.connectNonce, + signaturePublicKey: account.connectSignaturePublicKey, + encryptionPublicKey: account.connectEncryptionPublicKey, + accountProof: account.connectAccountProof, + keyProof: account.connectKeyProof, + appId: null, + }; + } + + // Try to find App identity + const appIdentity = yield* Effect.tryPromise({ + try: () => + client.appIdentity.findFirst({ + where: { + accountAddress: params.accountAddress, + signaturePublicKey: params.signaturePublicKey, + }, + }), + catch: () => + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + }); + + if (!appIdentity) { + return yield* Effect.fail( + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + ); + } + + return { + accountAddress: appIdentity.accountAddress, + ciphertext: appIdentity.ciphertext, + nonce: undefined, + signaturePublicKey: appIdentity.signaturePublicKey, + encryptionPublicKey: appIdentity.encryptionPublicKey, + accountProof: appIdentity.accountProof, + keyProof: appIdentity.keyProof, + appId: appIdentity.appId, + }; + })(); + + return { + getAppOrConnectIdentity, + } as const; +})(); + +export const IdentityServiceLive = Layer.effect(IdentityService, makeIdentityService); diff --git a/apps/server-new/src/services/spaces.ts b/apps/server-new/src/services/spaces.ts index 3aede443..df755ccd 100644 --- a/apps/server-new/src/services/spaces.ts +++ b/apps/server-new/src/services/spaces.ts @@ -1,7 +1,8 @@ -import { Utils } from '@graphprotocol/hypergraph'; +import { Identity, Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph'; import { Context, Effect, Layer } from 'effect'; -import { DatabaseError } from '../http/errors.js'; +import { DatabaseError, ValidationError } from '../http/errors.js'; import { DatabaseService } from './database.js'; +import { IdentityService } from './identity.js'; export interface SpaceInfo { id: string; @@ -22,14 +23,26 @@ export interface SpaceInfo { }>; } +export interface CreateSpaceParams { + accountAddress: string; + event: SpaceEvents.CreateSpaceEvent; + keyBox: Messages.KeyBoxWithKeyId; + infoContent: Uint8Array; + infoSignatureHex: string; + infoSignatureRecovery: number; + name: string; +} + export interface SpacesService { readonly listByAccount: (accountAddress: string) => Effect.Effect; + readonly createSpace: (params: CreateSpaceParams) => Effect.Effect<{ id: string }, ValidationError | DatabaseError>; } export const SpacesService = Context.GenericTag('SpacesService'); export const makeSpacesService = Effect.fn(function* () { const { client } = yield* DatabaseService; + const identityService = yield* IdentityService; const listByAccount = (accountAddress: string) => Effect.fn(function* () { @@ -90,9 +103,95 @@ export const makeSpacesService = Effect.fn(function* () { })); })(); + const createSpace = (params: CreateSpaceParams) => + Effect.fn(function* () { + const { accountAddress, event, keyBox, infoContent, infoSignatureHex, infoSignatureRecovery, name } = params; + + // Create the getVerifiedIdentity function for space event validation + const getVerifiedIdentity = (accountAddressToFetch: string, publicKey: string) => { + // applySpaceEvent is only allowed to be called by the account that is applying the event + if (accountAddressToFetch !== accountAddress) { + return Effect.fail(new Identity.InvalidIdentityError()); + } + + return Effect.fn(function* () { + const identity = yield* identityService + .getAppOrConnectIdentity({ + accountAddress: accountAddressToFetch, + signaturePublicKey: publicKey, + }) + .pipe(Effect.mapError(() => new Identity.InvalidIdentityError())); + return identity; + })(); + }; + + // Validate the space event + const result = yield* SpaceEvents.applyEvent({ + event, + state: undefined, + getVerifiedIdentity, + }); + + const keyBoxId = `${keyBox.id}-${accountAddress}`; + + // Create the space in the database + const spaceEvent = yield* Effect.tryPromise({ + try: () => + client.spaceEvent.create({ + data: { + event: JSON.stringify(event), + id: event.transaction.id, + counter: 0, + state: JSON.stringify(result), + space: { + create: { + id: event.transaction.id, + infoContent, + infoSignatureHex, + infoSignatureRecovery, + infoAuthorAddress: accountAddress, + name, + members: { + connect: { + address: accountAddress, + }, + }, + keys: { + create: { + id: keyBox.id, + keyBoxes: { + create: { + id: keyBoxId, + nonce: keyBox.nonce, + ciphertext: keyBox.ciphertext, + authorPublicKey: keyBox.authorPublicKey, + account: { + connect: { + address: accountAddress, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'createSpace', + cause: error, + }), + }); + + return { id: spaceEvent.id }; + })(); + return { listByAccount, + createSpace, } as const; })(); -export const SpacesServiceLive = Layer.effect(SpacesService, makeSpacesService); \ No newline at end of file +export const SpacesServiceLive = Layer.effect(SpacesService, makeSpacesService); From 7cb50a8d1e611e13a4ef41d8d033406ed42336f9 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 29 Aug 2025 08:00:42 +0200 Subject: [PATCH 07/14] migrate create space handler --- apps/server-new/src/http/api.ts | 11 ++++- apps/server-new/src/http/handlers.ts | 19 ++++++--- apps/server-new/src/services/spaces.ts | 57 ++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 7fa5d0dc..80ec99ef 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -99,10 +99,17 @@ export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( 'postConnectAddAppIdentityToSpaces', )`/connect/add-app-identity-to-spaces` + .setHeaders(Schema.Struct({ + 'privy-id-token': Schema.String, + })) .setPayload(Messages.RequestConnectAddAppIdentityToSpaces) - // .addSuccess(Schema.Struct({ space: Schema.Unknown })) + .addSuccess(Schema.Void) .addError(Errors.AuthenticationError, { status: 401 }) - .addError(Errors.ValidationError, { status: 400 }); + .addError(Errors.AuthorizationError, { status: 401 }) + .addError(Errors.ValidationError, { status: 400 }) + .addError(Errors.PrivyTokenError, { status: 401 }) + .addError(Errors.PrivyConfigError, { status: 500 }) + .addError(Errors.DatabaseError, { status: 500 }); export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIdentity')`/connect/identity` .setPayload(Messages.RequestConnectCreateIdentity) diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index f9d7efdd..4d08103e 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -62,11 +62,20 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'postConnectAddAppIdentityToSpaces', - Effect.fn(function* ({ payload }) { - yield* Effect.logInfo('Adding app identity to spaces', payload); - yield* new Errors.ResourceNotFoundError({ - resource: 'postConnectAddAppIdentityToSpaces', - id: 'postConnectAddAppIdentityToSpaces', + Effect.fn(function* ({ headers, payload }) { + yield* Effect.logInfo('POST /connect/add-app-identity-to-spaces'); + + const privyAuthService = yield* PrivyAuthService; + const spacesService = yield* SpacesService; + + // Authenticate the request with Privy token + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress); + + // Add app identity to spaces + yield* spacesService.addAppIdentityToSpaces({ + appIdentityAddress: payload.appIdentityAddress, + accountAddress: payload.accountAddress, + spacesInput: payload.spacesInput, }); }), ) diff --git a/apps/server-new/src/services/spaces.ts b/apps/server-new/src/services/spaces.ts index df755ccd..fa560a1d 100644 --- a/apps/server-new/src/services/spaces.ts +++ b/apps/server-new/src/services/spaces.ts @@ -33,9 +33,16 @@ export interface CreateSpaceParams { name: string; } +export interface AddAppIdentityToSpacesParams { + appIdentityAddress: string; + accountAddress: string; + spacesInput: Messages.RequestConnectAddAppIdentityToSpaces['spacesInput']; +} + export interface SpacesService { readonly listByAccount: (accountAddress: string) => Effect.Effect; readonly createSpace: (params: CreateSpaceParams) => Effect.Effect<{ id: string }, ValidationError | DatabaseError>; + readonly addAppIdentityToSpaces: (params: AddAppIdentityToSpacesParams) => Effect.Effect; } export const SpacesService = Context.GenericTag('SpacesService'); @@ -188,9 +195,59 @@ export const makeSpacesService = Effect.fn(function* () { return { id: spaceEvent.id }; })(); + const addAppIdentityToSpaces = (params: AddAppIdentityToSpacesParams) => + Effect.fn(function* () { + const { appIdentityAddress, accountAddress, spacesInput } = params; + + yield* Effect.tryPromise({ + try: () => + client.$transaction(async (prisma) => { + // Update app identity to connect it to spaces + await prisma.appIdentity.update({ + where: { + address: appIdentityAddress, + accountAddress, + }, + data: { + spaces: { + connect: spacesInput.map((space) => ({ id: space.id })), + }, + }, + }); + + // Create key boxes for the app identity + const keyBoxes = spacesInput.flatMap((entry) => { + return entry.keyBoxes.map((keyBox) => { + const keyBoxId = `${keyBox.id}-${appIdentityAddress}`; + + return { + id: keyBoxId, + spaceKeyId: keyBox.id, + ciphertext: keyBox.ciphertext, + nonce: keyBox.nonce, + authorPublicKey: keyBox.authorPublicKey, + accountAddress, + appIdentityAddress, + }; + }); + }); + + await prisma.spaceKeyBox.createMany({ + data: keyBoxes, + }); + }), + catch: (error) => + new DatabaseError({ + operation: 'addAppIdentityToSpaces', + cause: error, + }), + }); + })(); + return { listByAccount, createSpace, + addAppIdentityToSpaces, } as const; })(); From 8a20047e9ac572bf058ed3467c53543fd6b37506 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 29 Aug 2025 08:10:36 +0200 Subject: [PATCH 08/14] add postConnectIdentity --- apps/server-new/src/config/hypergraph.ts | 15 ++++ apps/server-new/src/http/api.ts | 11 ++- apps/server-new/src/http/handlers.ts | 74 ++++++++++++++++++- .../src/services/connect-identity.ts | 63 +++++++++++++++- 4 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 apps/server-new/src/config/hypergraph.ts diff --git a/apps/server-new/src/config/hypergraph.ts b/apps/server-new/src/config/hypergraph.ts new file mode 100644 index 00000000..ef5bf088 --- /dev/null +++ b/apps/server-new/src/config/hypergraph.ts @@ -0,0 +1,15 @@ +import { Connect } from '@graphprotocol/hypergraph'; +import { Config } from 'effect'; + +export const hypergraphChainConfig = Config.string('HYPERGRAPH_CHAIN').pipe( + Config.map((chain) => (chain === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET)), + Config.withDefault(Connect.GEO_TESTNET), +); + +export const hypergraphRpcUrlConfig = Config.string('HYPERGRAPH_RPC_URL').pipe( + Config.orElse(() => + hypergraphChainConfig.pipe( + Config.map((chain) => chain.rpcUrls.default.http[0]), + ), + ), +); \ No newline at end of file diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 80ec99ef..297abe0d 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -112,11 +112,18 @@ export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( .addError(Errors.DatabaseError, { status: 500 }); export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIdentity')`/connect/identity` + .setHeaders(Schema.Struct({ + 'privy-id-token': Schema.String, + })) .setPayload(Messages.RequestConnectCreateIdentity) - // .addSuccess(Messages.ResponseConnectCreateIdentity) + .addSuccess(Messages.ResponseConnectCreateIdentity) .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.ResourceAlreadyExistsError, { status: 400 }) - .addError(Errors.OwnershipProofError, { status: 401 }); + .addError(Errors.OwnershipProofError, { status: 401 }) + .addError(Errors.PrivyTokenError, { status: 401 }) + .addError(Errors.PrivyConfigError, { status: 500 }) + .addError(Errors.DatabaseError, { status: 500 }); export const getConnectIdentityEncryptedEndpoint = HttpApiEndpoint.get( 'getConnectIdentityEncrypted', diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index 4d08103e..21ed7435 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -1,6 +1,7 @@ import { HttpApiBuilder } from '@effect/platform'; -import { Messages, Utils } from '@graphprotocol/hypergraph'; +import { Identity, Messages, Utils } from '@graphprotocol/hypergraph'; import { Effect, Layer } from 'effect'; +import { hypergraphChainConfig, hypergraphRpcUrlConfig } from '../config/hypergraph.js'; import { AppIdentityService } from '../services/app-identity.js'; import { ConnectIdentityService } from '../services/connect-identity.js'; import { PrivyAuthService } from '../services/privy-auth.js'; @@ -81,9 +82,74 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'postConnectIdentity', - Effect.fn(function* ({ payload }) { - yield* Effect.logInfo('Creating connect identity', payload); - yield* new Errors.ResourceNotFoundError({ resource: 'postConnectIdentity', id: 'postConnectIdentity' }); + Effect.fn(function* ({ headers, payload }) { + yield* Effect.logInfo('POST /connect/identity'); + + const privyAuthService = yield* PrivyAuthService; + const connectIdentityService = yield* ConnectIdentityService; + const chain = yield* hypergraphChainConfig; + const rpcUrl = yield* hypergraphRpcUrlConfig; + + // Verify the Privy token and get signer address + const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']); + const accountAddress = payload.keyBox.accountAddress; + + // Verify that the signer matches the one in the keyBox + if (signerAddress !== payload.keyBox.signer) { + return yield* Effect.fail( + new Errors.AuthorizationError({ + message: 'Signer mismatch', + accountAddress, + }), + ); + } + + // Verify identity ownership proof + const isValid = yield* Effect.tryPromise({ + try: () => + Identity.verifyIdentityOwnership( + accountAddress, + payload.signaturePublicKey, + payload.accountProof, + payload.keyProof, + chain, + rpcUrl, + ), + catch: () => + new Errors.OwnershipProofError({ + accountAddress, + reason: 'Failed to verify identity ownership', + }), + }); + + if (!isValid) { + return yield* Effect.fail( + new Errors.OwnershipProofError({ + accountAddress, + reason: 'Invalid ownership proof', + }), + ); + } + + yield* Effect.logInfo('Ownership proof is valid'); + + // Create the identity + yield* connectIdentityService.createIdentity({ + signerAddress, + accountAddress, + ciphertext: payload.keyBox.ciphertext, + nonce: payload.keyBox.nonce, + signaturePublicKey: payload.signaturePublicKey, + encryptionPublicKey: payload.encryptionPublicKey, + accountProof: payload.accountProof, + keyProof: payload.keyProof, + }); + + const response: Messages.ResponseConnectCreateIdentity = { + success: true, + }; + + return response; }), ) .handle( diff --git a/apps/server-new/src/services/connect-identity.ts b/apps/server-new/src/services/connect-identity.ts index dcaa1079..56236d73 100644 --- a/apps/server-new/src/services/connect-identity.ts +++ b/apps/server-new/src/services/connect-identity.ts @@ -1,5 +1,5 @@ import { Context, Effect, Layer } from 'effect'; -import { ResourceNotFoundError } from '../http/errors.js'; +import { DatabaseError, ResourceAlreadyExistsError, ResourceNotFoundError } from '../http/errors.js'; import { DatabaseService } from './database.js'; export interface ConnectIdentityResult { @@ -10,8 +10,20 @@ export interface ConnectIdentityResult { keyProof: string; } +export interface CreateConnectIdentityParams { + signerAddress: string; + accountAddress: string; + ciphertext: string; + nonce: string; + signaturePublicKey: string; + encryptionPublicKey: string; + accountProof: string; + keyProof: string; +} + export interface ConnectIdentityService { readonly getByAccountAddress: (accountAddress: string) => Effect.Effect; + readonly createIdentity: (params: CreateConnectIdentityParams) => Effect.Effect; } export const ConnectIdentityService = Context.GenericTag('ConnectIdentityService'); @@ -58,8 +70,57 @@ export const makeConnectIdentityService = Effect.fn(function* () { }; })(); + const createIdentity = (params: CreateConnectIdentityParams) => + Effect.fn(function* () { + // Check if identity already exists for this account + const existingIdentity = yield* Effect.tryPromise({ + try: () => + client.account.findFirst({ + where: { + address: params.accountAddress, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'createIdentity', + cause: error, + }), + }); + + if (existingIdentity) { + yield* new ResourceAlreadyExistsError({ + resource: 'ConnectIdentity', + id: params.accountAddress, + }); + } + + // Create the new identity + yield* Effect.tryPromise({ + try: () => + client.account.create({ + data: { + connectSignerAddress: params.signerAddress, + address: params.accountAddress, + connectAccountProof: params.accountProof, + connectKeyProof: params.keyProof, + connectSignaturePublicKey: params.signaturePublicKey, + connectEncryptionPublicKey: params.encryptionPublicKey, + connectCiphertext: params.ciphertext, + connectNonce: params.nonce, + connectAddress: params.accountAddress, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'createIdentity', + cause: error, + }), + }); + })(); + return { getByAccountAddress, + createIdentity, } as const; })(); From b22ee7cf02af8670ee45ef70a0cb84ff017645ea Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 29 Aug 2025 08:27:46 +0200 Subject: [PATCH 09/14] add new handlers --- apps/server-new/src/http/api.ts | 38 ++++- apps/server-new/src/http/handlers.ts | 160 ++++++++++++++++-- apps/server-new/src/services/app-identity.ts | 116 ++++++++++++- .../src/services/connect-identity.ts | 49 +++++- apps/server-new/src/services/identity.ts | 104 ++++++++---- 5 files changed, 413 insertions(+), 54 deletions(-) diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 297abe0d..ab0edd4c 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -128,21 +128,45 @@ export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIden export const getConnectIdentityEncryptedEndpoint = HttpApiEndpoint.get( 'getConnectIdentityEncrypted', )`/connect/identity/encrypted` - // .addSuccess(Messages.ResponseIdentityEncrypted) - .addError(Errors.AuthenticationError, { status: 401 }); + .setHeaders(Schema.Struct({ + 'privy-id-token': Schema.String, + 'account-address': Schema.String, + })) + .addSuccess(Messages.ResponseIdentityEncrypted) + .addError(Errors.AuthenticationError, { status: 401 }) + .addError(Errors.AuthorizationError, { status: 401 }) + .addError(Errors.ResourceNotFoundError, { status: 404 }) + .addError(Errors.PrivyTokenError, { status: 401 }) + .addError(Errors.PrivyConfigError, { status: 500 }); export const getConnectAppIdentityEndpoint = HttpApiEndpoint.get( 'getConnectAppIdentity', )`/connect/app-identity/${appId}` - // .addSuccess(AppIdentityResponse) + .setHeaders(Schema.Struct({ + 'privy-id-token': Schema.String, + 'account-address': Schema.String, + })) + .addSuccess(AppIdentityResponse) .addError(Errors.AuthenticationError, { status: 401 }) - .addError(Errors.ResourceNotFoundError, { status: 404 }); + .addError(Errors.AuthorizationError, { status: 401 }) + .addError(Errors.ResourceNotFoundError, { status: 404 }) + .addError(Errors.PrivyTokenError, { status: 401 }) + .addError(Errors.PrivyConfigError, { status: 500 }) + .addError(Errors.DatabaseError, { status: 500 }); export const postConnectAppIdentityEndpoint = HttpApiEndpoint.post('postConnectAppIdentity')`/connect/app-identity` + .setHeaders(Schema.Struct({ + 'privy-id-token': Schema.String, + })) .setPayload(Messages.RequestConnectCreateAppIdentity) - // .addSuccess(AppIdentityResponse) + .addSuccess(AppIdentityResponse) .addError(Errors.AuthenticationError, { status: 401 }) - .addError(Errors.OwnershipProofError, { status: 401 }); + .addError(Errors.AuthorizationError, { status: 401 }) + .addError(Errors.ResourceAlreadyExistsError, { status: 400 }) + .addError(Errors.OwnershipProofError, { status: 401 }) + .addError(Errors.PrivyTokenError, { status: 401 }) + .addError(Errors.PrivyConfigError, { status: 500 }) + .addError(Errors.DatabaseError, { status: 500 }); export const connectGroup = HttpApiGroup.make('Connect') .add(getConnectSpacesEndpoint) @@ -172,7 +196,7 @@ export const getConnectIdentityEndpoint = HttpApiEndpoint.get('getConnectIdentit export const getIdentityEndpoint = HttpApiEndpoint.get('getIdentity')`/identity` .setUrlParams(IdentityQuery) - // .addSuccess(Messages.ResponseIdentity) + .addSuccess(Messages.ResponseIdentity) .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.ResourceNotFoundError, { status: 404 }); diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index 21ed7435..317cdb33 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -1,9 +1,11 @@ import { HttpApiBuilder } from '@effect/platform'; import { Identity, Messages, Utils } from '@graphprotocol/hypergraph'; +import { bytesToHex, randomBytes } from '@noble/hashes/utils.js'; import { Effect, Layer } from 'effect'; import { hypergraphChainConfig, hypergraphRpcUrlConfig } from '../config/hypergraph.js'; import { AppIdentityService } from '../services/app-identity.js'; import { ConnectIdentityService } from '../services/connect-identity.js'; +import { IdentityService } from '../services/identity.js'; import { PrivyAuthService } from '../services/privy-auth.js'; import { SpacesService } from '../services/spaces.js'; import * as Api from './api.js'; @@ -154,26 +156,122 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'getConnectIdentityEncrypted', - Effect.fn(function* ({ request }) { - yield* Effect.logInfo('Getting encrypted identity'); - yield* new Errors.ResourceNotFoundError({ - resource: 'getConnectIdentityEncrypted', - id: 'getConnectIdentityEncrypted', - }); + Effect.fn(function* ({ headers }) { + yield* Effect.logInfo('GET /connect/identity/encrypted'); + + const privyAuthService = yield* PrivyAuthService; + const connectIdentityService = yield* ConnectIdentityService; + + // Authenticate the request with Privy token + const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']); + const accountAddress = headers['account-address']; + + // Verify the signer is authorized for this account + yield* privyAuthService.isSignerForAccount(signerAddress, accountAddress); + + // Get the encrypted identity + const identity = yield* connectIdentityService.getIdentityEncrypted(accountAddress); + + const response: Messages.ResponseIdentityEncrypted = { + keyBox: { + accountAddress, + ciphertext: identity.ciphertext, + nonce: identity.nonce, + signer: signerAddress, + }, + }; + + return response; }), ) .handle( 'getConnectAppIdentity', - Effect.fn(function* ({ path: { appId } }) { - yield* Effect.logInfo(`Getting app identity for appId: ${appId}`); - yield* new Errors.ResourceNotFoundError({ resource: 'getConnectAppIdentity', id: 'getConnectAppIdentity' }); + Effect.fn(function* ({ headers, path: { appId } }) { + yield* Effect.logInfo(`GET /connect/app-identity/${appId}`); + + const privyAuthService = yield* PrivyAuthService; + const appIdentityService = yield* AppIdentityService; + + // Authenticate the request with Privy token + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']); + + // Find the app identity + const appIdentity = yield* appIdentityService.findByAppId({ + accountAddress: headers['account-address'], + appId, + }); + + if (!appIdentity) { + return yield* new Errors.ResourceNotFoundError({ + resource: 'AppIdentity', + id: appId, + }); + } + + return { appIdentity }; }), ) .handle( 'postConnectAppIdentity', - Effect.fn(function* ({ payload }) { - yield* Effect.logInfo('Creating app identity', payload); - yield* new Errors.ResourceNotFoundError({ resource: 'postConnectAppIdentity', id: 'postConnectAppIdentity' }); + Effect.fn(function* ({ headers, payload }) { + yield* Effect.logInfo('POST /connect/app-identity'); + + const privyAuthService = yield* PrivyAuthService; + const appIdentityService = yield* AppIdentityService; + const chain = yield* hypergraphChainConfig; + const rpcUrl = yield* hypergraphRpcUrlConfig; + + // Verify the Privy token and get signer address + const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']); + const accountAddress = payload.accountAddress; + + // Verify signer is authorized for this account + yield* privyAuthService.isSignerForAccount(signerAddress, accountAddress); + + // Verify identity ownership proof + const isValid = yield* Effect.tryPromise({ + try: () => + Identity.verifyIdentityOwnership( + accountAddress, + payload.signaturePublicKey, + payload.accountProof, + payload.keyProof, + chain, + rpcUrl, + ), + catch: () => + new Errors.OwnershipProofError({ + accountAddress, + reason: 'Failed to verify identity ownership', + }), + }); + + if (!isValid) { + yield* new Errors.OwnershipProofError({ + accountAddress, + reason: 'Invalid ownership proof', + }); + } + + // Generate session token + const sessionToken = bytesToHex(randomBytes(32)); + const sessionTokenExpires = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); // 30 days + + // Create the app identity + const appIdentity = yield* appIdentityService.createAppIdentity({ + accountAddress, + appId: payload.appId, + address: payload.address, + ciphertext: payload.ciphertext, + signaturePublicKey: payload.signaturePublicKey, + encryptionPublicKey: payload.encryptionPublicKey, + accountProof: payload.accountProof, + keyProof: payload.keyProof, + sessionToken, + sessionTokenExpires, + }); + + return { appIdentity }; }), ); }); @@ -230,8 +328,42 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h .handle( 'getIdentity', Effect.fn(function* ({ urlParams }) { - yield* Effect.logInfo('Getting identity', urlParams); - yield* new Errors.ResourceNotFoundError({ resource: 'Identity', id: 'general' }); + yield* Effect.logInfo('GET /identity', urlParams); + + const identityService = yield* IdentityService; + + // Validate required parameters + if (!urlParams.accountAddress) { + yield* new Errors.ValidationError({ + field: 'accountAddress', + message: 'accountAddress is required', + }); + } + + if (!urlParams.signaturePublicKey && !urlParams.appId) { + yield* new Errors.ValidationError({ + field: 'signaturePublicKey or appId', + message: 'Either signaturePublicKey or appId is required', + }); + } + + // Build params based on what's provided + const params = urlParams.signaturePublicKey + ? { accountAddress: urlParams.accountAddress, signaturePublicKey: urlParams.signaturePublicKey } + : { accountAddress: urlParams.accountAddress, appId: urlParams.appId! }; + + const identity = yield* identityService.getAppOrConnectIdentity(params); + + const response: Messages.ResponseIdentity = { + accountAddress: urlParams.accountAddress, + signaturePublicKey: identity.signaturePublicKey, + encryptionPublicKey: identity.encryptionPublicKey, + accountProof: identity.accountProof, + keyProof: identity.keyProof, + appId: identity.appId ?? undefined, + }; + + return response; }), ); }); diff --git a/apps/server-new/src/services/app-identity.ts b/apps/server-new/src/services/app-identity.ts index 447e131d..0a74fefd 100644 --- a/apps/server-new/src/services/app-identity.ts +++ b/apps/server-new/src/services/app-identity.ts @@ -1,7 +1,42 @@ import { Context, Effect, Layer } from 'effect'; -import { InvalidTokenError, ResourceNotFoundError, TokenExpiredError } from '../http/errors.js'; +import { + DatabaseError, + InvalidTokenError, + ResourceAlreadyExistsError, + ResourceNotFoundError, + TokenExpiredError +} from '../http/errors.js'; import { DatabaseService } from './database.js'; +export interface AppIdentityResult { + address: string; + accountAddress: string; + appId: string; + signaturePublicKey: string; + encryptionPublicKey: string; + ciphertext: string; + accountProof: string; + keyProof: string; + sessionToken: string | null; + sessionNonce: string | null; + sessionTokenExpires: Date | null; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateAppIdentityParams { + accountAddress: string; + address: string; + appId: string; + ciphertext: string; + signaturePublicKey: string; + encryptionPublicKey: string; + accountProof: string; + keyProof: string; + sessionToken: string; + sessionTokenExpires: Date; +} + export interface AppIdentityService { readonly getBySessionToken: ( sessionToken: string, @@ -9,6 +44,13 @@ export interface AppIdentityService { { address: string; accountAddress: string }, InvalidTokenError | ResourceNotFoundError | TokenExpiredError >; + readonly findByAppId: (params: { + accountAddress: string; + appId: string; + }) => Effect.Effect; + readonly createAppIdentity: ( + params: CreateAppIdentityParams + ) => Effect.Effect; } export const AppIdentityService = Context.GenericTag('AppIdentityService'); @@ -50,8 +92,80 @@ export const makeAppIdentityService = Effect.fn(function* () { }; })(); + const findByAppId = ({ accountAddress, appId }: { accountAddress: string; appId: string }) => + Effect.fn(function* () { + const appIdentity = yield* Effect.tryPromise({ + try: () => + client.appIdentity.findFirst({ + where: { + accountAddress, + appId, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'findAppIdentity', + cause: error, + }), + }); + + return appIdentity; + })(); + + const createAppIdentity = (params: CreateAppIdentityParams) => + Effect.fn(function* () { + const appIdentity = yield* Effect.tryPromise({ + try: () => + client.$transaction(async (prisma) => { + // Check if app identity already exists + const existingIdentity = await prisma.appIdentity.findFirst({ + where: { + accountAddress: params.accountAddress, + appId: params.appId, + }, + }); + + if (existingIdentity) { + throw new Error('App identity already exists'); + } + + // Create the new app identity + return await prisma.appIdentity.create({ + data: { + address: params.address, + accountAddress: params.accountAddress, + appId: params.appId, + ciphertext: params.ciphertext, + signaturePublicKey: params.signaturePublicKey, + encryptionPublicKey: params.encryptionPublicKey, + accountProof: params.accountProof, + keyProof: params.keyProof, + sessionToken: params.sessionToken, + sessionTokenExpires: params.sessionTokenExpires, + }, + }); + }), + catch: (error) => { + if (error instanceof Error && error.message === 'App identity already exists') { + return new ResourceAlreadyExistsError({ + resource: 'AppIdentity', + id: `${params.accountAddress}:${params.appId}`, + }); + } + return new DatabaseError({ + operation: 'createAppIdentity', + cause: error, + }); + }, + }); + + return appIdentity; + })(); + return { getBySessionToken, + findByAppId, + createAppIdentity, } as const; })(); diff --git a/apps/server-new/src/services/connect-identity.ts b/apps/server-new/src/services/connect-identity.ts index 56236d73..e848852c 100644 --- a/apps/server-new/src/services/connect-identity.ts +++ b/apps/server-new/src/services/connect-identity.ts @@ -10,6 +10,12 @@ export interface ConnectIdentityResult { keyProof: string; } +export interface ConnectIdentityEncrypted { + accountAddress: string; + ciphertext: string; + nonce: string; +} + export interface CreateConnectIdentityParams { signerAddress: string; accountAddress: string; @@ -23,7 +29,12 @@ export interface CreateConnectIdentityParams { export interface ConnectIdentityService { readonly getByAccountAddress: (accountAddress: string) => Effect.Effect; - readonly createIdentity: (params: CreateConnectIdentityParams) => Effect.Effect; + readonly getIdentityEncrypted: ( + accountAddress: string, + ) => Effect.Effect; + readonly createIdentity: ( + params: CreateConnectIdentityParams, + ) => Effect.Effect; } export const ConnectIdentityService = Context.GenericTag('ConnectIdentityService'); @@ -118,8 +129,44 @@ export const makeConnectIdentityService = Effect.fn(function* () { }); })(); + const getIdentityEncrypted = (accountAddress: string) => + Effect.fn(function* () { + const account = yield* Effect.tryPromise({ + try: () => + client.account.findFirst({ + where: { address: accountAddress }, + select: { + address: true, + connectCiphertext: true, + connectNonce: true, + }, + }), + catch: () => + new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, + }), + }); + + if (!account) { + return yield* Effect.fail( + new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, + }), + ); + } + + return { + accountAddress: account.address, + ciphertext: account.connectCiphertext, + nonce: account.connectNonce, + }; + })(); + return { getByAccountAddress, + getIdentityEncrypted, createIdentity, } as const; })(); diff --git a/apps/server-new/src/services/identity.ts b/apps/server-new/src/services/identity.ts index 7740c9e1..2a0edc05 100644 --- a/apps/server-new/src/services/identity.ts +++ b/apps/server-new/src/services/identity.ts @@ -14,10 +14,10 @@ export interface IdentityResult { } export interface IdentityService { - readonly getAppOrConnectIdentity: (params: { - accountAddress: string; - signaturePublicKey: string; - }) => Effect.Effect; + readonly getAppOrConnectIdentity: (params: + | { accountAddress: string; signaturePublicKey: string } + | { accountAddress: string; appId: string } + ) => Effect.Effect; } export const IdentityService = Context.GenericTag('IdentityService'); @@ -25,44 +25,86 @@ export const IdentityService = Context.GenericTag('IdentityServ export const makeIdentityService = Effect.fn(function* () { const { client } = yield* DatabaseService; - const getAppOrConnectIdentity = (params: { accountAddress: string; signaturePublicKey: string }) => + const getAppOrConnectIdentity = (params: + | { accountAddress: string; signaturePublicKey: string } + | { accountAddress: string; appId: string } + ) => Effect.fn(function* () { - // First try to find Connect identity - const account = yield* Effect.tryPromise({ - try: () => - client.account.findFirst({ - where: { - address: params.accountAddress, - connectSignaturePublicKey: params.signaturePublicKey, - }, - }), - catch: () => - new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }), - }); + // If we have signaturePublicKey, search by that + if ('signaturePublicKey' in params) { + // First try to find Connect identity + const account = yield* Effect.tryPromise({ + try: () => + client.account.findFirst({ + where: { + address: params.accountAddress, + connectSignaturePublicKey: params.signaturePublicKey, + }, + }), + catch: () => + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + }); + + if (account) { + return { + accountAddress: account.address, + ciphertext: account.connectCiphertext, + nonce: account.connectNonce, + signaturePublicKey: account.connectSignaturePublicKey, + encryptionPublicKey: account.connectEncryptionPublicKey, + accountProof: account.connectAccountProof, + keyProof: account.connectKeyProof, + appId: null, + }; + } + + // Try to find App identity by signaturePublicKey + const appIdentity = yield* Effect.tryPromise({ + try: () => + client.appIdentity.findFirst({ + where: { + accountAddress: params.accountAddress, + signaturePublicKey: params.signaturePublicKey, + }, + }), + catch: () => + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + }); + + if (!appIdentity) { + return yield* Effect.fail( + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + ); + } - if (account) { return { - accountAddress: account.address, - ciphertext: account.connectCiphertext, - nonce: account.connectNonce, - signaturePublicKey: account.connectSignaturePublicKey, - encryptionPublicKey: account.connectEncryptionPublicKey, - accountProof: account.connectAccountProof, - keyProof: account.connectKeyProof, - appId: null, + accountAddress: appIdentity.accountAddress, + ciphertext: appIdentity.ciphertext, + nonce: undefined, + signaturePublicKey: appIdentity.signaturePublicKey, + encryptionPublicKey: appIdentity.encryptionPublicKey, + accountProof: appIdentity.accountProof, + keyProof: appIdentity.keyProof, + appId: appIdentity.appId, }; } - // Try to find App identity + // If we have appId, search by that const appIdentity = yield* Effect.tryPromise({ try: () => client.appIdentity.findFirst({ where: { accountAddress: params.accountAddress, - signaturePublicKey: params.signaturePublicKey, + appId: params.appId, }, }), catch: () => From 094a66affa94593c1f1cf19dc91eaed7498943fc Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 29 Aug 2025 10:07:02 +0200 Subject: [PATCH 10/14] cleanup --- apps/server-new/package.json | 1 + apps/server-new/src/http/api.ts | 16 +- apps/server-new/src/http/handlers.ts | 75 +++-- apps/server-new/src/server.ts | 17 +- apps/server-new/src/services/account-inbox.ts | 288 ++++++++++++++++++ apps/server-new/src/services/app-identity.ts | 20 +- apps/server-new/src/services/identity.ts | 10 +- apps/server-new/src/services/space-inbox.ts | 274 +++++++++++++++++ apps/server-new/src/services/spaces.ts | 4 +- pnpm-lock.yaml | 5 +- 10 files changed, 664 insertions(+), 46 deletions(-) create mode 100644 apps/server-new/src/services/account-inbox.ts create mode 100644 apps/server-new/src/services/space-inbox.ts diff --git a/apps/server-new/package.json b/apps/server-new/package.json index c103a00d..e114cc98 100644 --- a/apps/server-new/package.json +++ b/apps/server-new/package.json @@ -20,6 +20,7 @@ "@effect/platform": "^0.90.0", "@effect/platform-node": "^0.96.0", "@graphprotocol/hypergraph": "workspace:*", + "@noble/hashes": "^1.8.0", "@prisma/client": "^6.14.0", "@privy-io/server-auth": "^1.32.0", "cors": "^2.8.5", diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index ab0edd4c..46d895d8 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -209,11 +209,12 @@ export const identityGroup = HttpApiGroup.make('Identity') * Inbox endpoints */ export const getSpaceInboxesEndpoint = HttpApiEndpoint.get('getSpaceInboxes')`/spaces/${spaceId}/inboxes` - // .addSuccess(Messages.ResponseListSpaceInboxesPublic) + .addSuccess(Messages.ResponseListSpaceInboxesPublic) .addError(Errors.DatabaseError, { status: 500 }); export const getSpaceInboxEndpoint = HttpApiEndpoint.get('getSpaceInbox')`/spaces/${spaceId}/inboxes/${inboxId}` - // .addSuccess(Messages.ResponseSpaceInboxPublic) + .addSuccess(Messages.ResponseSpaceInboxPublic) + .addError(Errors.ResourceNotFoundError, { status: 404 }) .addError(Errors.DatabaseError, { status: 500 }); export const postSpaceInboxMessageEndpoint = HttpApiEndpoint.post( @@ -223,16 +224,18 @@ export const postSpaceInboxMessageEndpoint = HttpApiEndpoint.post( .addSuccess(Schema.Void) .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.AuthorizationError, { status: 403 }) - .addError(Errors.ResourceNotFoundError, { status: 404 }); + .addError(Errors.ResourceNotFoundError, { status: 404 }) + .addError(Errors.DatabaseError, { status: 500 }); export const getAccountInboxesEndpoint = HttpApiEndpoint.get('getAccountInboxes')`/accounts/${accountAddress}/inboxes` - // .addSuccess(Messages.ResponseListAccountInboxesPublic) + .addSuccess(Messages.ResponseListAccountInboxesPublic) .addError(Errors.DatabaseError, { status: 500 }); export const getAccountInboxEndpoint = HttpApiEndpoint.get( 'getAccountInbox', )`/accounts/${accountAddress}/inboxes/${inboxId}` - // .addSuccess(Messages.ResponseAccountInboxPublic) + .addSuccess(Messages.ResponseAccountInboxPublic) + .addError(Errors.ResourceNotFoundError, { status: 404 }) .addError(Errors.DatabaseError, { status: 500 }); export const postAccountInboxMessageEndpoint = HttpApiEndpoint.post( @@ -242,7 +245,8 @@ export const postAccountInboxMessageEndpoint = HttpApiEndpoint.post( .addSuccess(Schema.Void) .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.AuthorizationError, { status: 403 }) - .addError(Errors.ResourceNotFoundError, { status: 404 }); + .addError(Errors.ResourceNotFoundError, { status: 404 }) + .addError(Errors.DatabaseError, { status: 500 }); export const inboxGroup = HttpApiGroup.make('Inbox') .add(getSpaceInboxesEndpoint) diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index 317cdb33..b341df67 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -3,10 +3,12 @@ import { Identity, Messages, Utils } from '@graphprotocol/hypergraph'; import { bytesToHex, randomBytes } from '@noble/hashes/utils.js'; import { Effect, Layer } from 'effect'; import { hypergraphChainConfig, hypergraphRpcUrlConfig } from '../config/hypergraph.js'; +import { AccountInboxService } from '../services/account-inbox.js'; import { AppIdentityService } from '../services/app-identity.js'; import { ConnectIdentityService } from '../services/connect-identity.js'; import { IdentityService } from '../services/identity.js'; import { PrivyAuthService } from '../services/privy-auth.js'; +import { SpaceInboxService } from '../services/space-inbox.js'; import { SpacesService } from '../services/spaces.js'; import * as Api from './api.js'; import * as Errors from './errors.js'; @@ -376,49 +378,81 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler .handle( 'getSpaceInboxes', Effect.fn(function* ({ path: { spaceId } }) { - yield* Effect.logInfo(`Getting space inboxes: ${spaceId}`); - yield* new Errors.ResourceNotFoundError({ - resource: 'getSpaceInboxes', - id: 'getSpaceInboxes', - }); + yield* Effect.logInfo(`GET /spaces/${spaceId}/inboxes`); + + const spaceInboxService = yield* SpaceInboxService; + + const inboxes = yield* spaceInboxService.listPublicSpaceInboxes({ spaceId }); + + return { inboxes }; }), ) .handle( 'getSpaceInbox', Effect.fn(function* ({ path: { spaceId, inboxId } }) { - yield* Effect.logInfo(`Getting space inbox: ${spaceId}/${inboxId}`); - yield* new Errors.ResourceNotFoundError({ resource: 'SpaceInbox', id: inboxId }); + yield* Effect.logInfo(`GET /spaces/${spaceId}/inboxes/${inboxId}`); + + const spaceInboxService = yield* SpaceInboxService; + + const inbox = yield* spaceInboxService.getSpaceInbox({ spaceId, inboxId }); + + return { inbox }; }), ) .handle( 'postSpaceInboxMessage', Effect.fn(function* ({ path: { spaceId, inboxId }, payload }) { - yield* Effect.logInfo(`Posting message to space inbox: ${spaceId}/${inboxId}`, payload); - return { success: true }; + yield* Effect.logInfo(`POST /spaces/${spaceId}/inboxes/${inboxId}/messages`); + + const spaceInboxService = yield* SpaceInboxService; + + yield* spaceInboxService.postSpaceInboxMessage({ + spaceId, + inboxId, + message: payload, + }); + + // Return void as per the API endpoint definition }), ) .handle( 'getAccountInboxes', Effect.fn(function* ({ path: { accountAddress } }) { - yield* Effect.logInfo(`Getting account inboxes: ${accountAddress}`); - yield* new Errors.ResourceNotFoundError({ - resource: 'getAccountInboxes', - id: 'getAccountInboxes', - }); + yield* Effect.logInfo(`GET /accounts/${accountAddress}/inboxes`); + + const accountInboxService = yield* AccountInboxService; + + const inboxes = yield* accountInboxService.listPublicAccountInboxes({ accountAddress }); + + return { inboxes }; }), ) .handle( 'getAccountInbox', Effect.fn(function* ({ path: { accountAddress, inboxId } }) { - yield* Effect.logInfo(`Getting account inbox: ${accountAddress}/${inboxId}`); - yield* new Errors.ResourceNotFoundError({ resource: 'AccountInbox', id: inboxId }); + yield* Effect.logInfo(`GET /accounts/${accountAddress}/inboxes/${inboxId}`); + + const accountInboxService = yield* AccountInboxService; + + const inbox = yield* accountInboxService.getAccountInbox({ accountAddress, inboxId }); + + return { inbox }; }), ) .handle( 'postAccountInboxMessage', Effect.fn(function* ({ path: { accountAddress, inboxId }, payload }) { - yield* Effect.logInfo(`Posting message to account inbox: ${accountAddress}/${inboxId}`, payload); - return { success: true }; + yield* Effect.logInfo(`POST /accounts/${accountAddress}/inboxes/${inboxId}/messages`); + + const accountInboxService = yield* AccountInboxService; + + yield* accountInboxService.postAccountInboxMessage({ + accountAddress, + inboxId, + message: payload, + }); + + // Return void as per the API endpoint definition }), ); }); @@ -426,4 +460,7 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler /** * All handlers combined */ -export const HandlersLive = Layer.mergeAll(HealthGroupLive, ConnectGroupLive, IdentityGroupLive, InboxGroupLive); +export const HandlersLive = Layer.mergeAll( + HealthGroupLive, + // ConnectGroupLive, IdentityGroupLive, InboxGroupLive +); diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts index 7d817199..3f2f57e2 100644 --- a/apps/server-new/src/server.ts +++ b/apps/server-new/src/server.ts @@ -1,24 +1,37 @@ -import { createServer } from 'node:http'; import { HttpApiBuilder, HttpServer } from '@effect/platform'; import { NodeHttpServer } from '@effect/platform-node'; import { Effect, Layer } from 'effect'; +import { createServer } from 'node:http'; import { serverPortConfig } from './config/server.ts'; import { hypergraphApi } from './http/api.ts'; import { HandlersLive } from './http/handlers.ts'; +import { AccountInboxServiceLive } from './services/account-inbox.ts'; import { AppIdentityServiceLive } from './services/app-identity.ts'; import { ConnectIdentityServiceLive } from './services/connect-identity.ts'; import { DatabaseServiceLive } from './services/database.ts'; import { IdentityServiceLive } from './services/identity.ts'; import { PrivyAuthServiceLive } from './services/privy-auth.ts'; +import { SpaceInboxServiceLive } from './services/space-inbox.ts'; import { SpacesServiceLive } from './services/spaces.ts'; const ServicesLive = Layer.mergeAll( DatabaseServiceLive, + Layer.provide( + AccountInboxServiceLive, + Layer.mergeAll(Layer.provide(IdentityServiceLive, DatabaseServiceLive), DatabaseServiceLive), + ), Layer.provide(AppIdentityServiceLive, DatabaseServiceLive), Layer.provide(ConnectIdentityServiceLive, DatabaseServiceLive), Layer.provide(IdentityServiceLive, DatabaseServiceLive), Layer.provide(PrivyAuthServiceLive, DatabaseServiceLive), - Layer.provide(SpacesServiceLive, Layer.mergeAll(DatabaseServiceLive, IdentityServiceLive)), + Layer.provide( + SpaceInboxServiceLive, + Layer.mergeAll(Layer.provide(IdentityServiceLive, DatabaseServiceLive), DatabaseServiceLive), + ), + Layer.provide( + SpacesServiceLive, + Layer.mergeAll(DatabaseServiceLive, Layer.provide(IdentityServiceLive, DatabaseServiceLive)), + ), ); const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive), Layer.provide(ServicesLive)); diff --git a/apps/server-new/src/services/account-inbox.ts b/apps/server-new/src/services/account-inbox.ts new file mode 100644 index 00000000..528624da --- /dev/null +++ b/apps/server-new/src/services/account-inbox.ts @@ -0,0 +1,288 @@ +import { Inboxes, type Messages } from '@graphprotocol/hypergraph'; +import { Context, Effect, Layer } from 'effect'; +import { AuthorizationError, DatabaseError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; +import { DatabaseService } from './database.js'; +import { IdentityService } from './identity.js'; + +export interface AccountInboxResult { + inboxId: string; + accountAddress: string; + isPublic: boolean; + authPolicy: Inboxes.InboxSenderAuthPolicy; + encryptionPublicKey: string | null; + signature: { + hex: string; + recovery: number; + }; +} + +export interface AccountInboxService { + readonly listPublicAccountInboxes: (params: { + accountAddress: string; + }) => Effect.Effect; + readonly getAccountInbox: (params: { + accountAddress: string; + inboxId: string; + }) => Effect.Effect; + readonly postAccountInboxMessage: (params: { + accountAddress: string; + inboxId: string; + message: Messages.RequestCreateAccountInboxMessage; + }) => Effect.Effect< + Messages.InboxMessage, + ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseError + >; +} + +export const AccountInboxService = Context.GenericTag('AccountInboxService'); + +export const makeAccountInboxService = Effect.fn(function* () { + const { client } = yield* DatabaseService; + const identityService = yield* IdentityService; + + const listPublicAccountInboxes = ({ accountAddress }: { accountAddress: string }) => + Effect.fn(function* () { + const inboxes = yield* Effect.tryPromise({ + try: () => + client.accountInbox.findMany({ + where: { accountAddress, isPublic: true }, + select: { + id: true, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + account: { + select: { + address: true, + }, + }, + signatureHex: true, + signatureRecovery: true, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'listPublicAccountInboxes', + cause: error, + }), + }); + + return inboxes.map((inbox) => ({ + inboxId: inbox.id, + accountAddress: inbox.account.address, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + signature: { + hex: inbox.signatureHex, + recovery: inbox.signatureRecovery, + }, + })); + })(); + + const getAccountInbox = ({ accountAddress, inboxId }: { accountAddress: string; inboxId: string }) => + Effect.fn(function* () { + const inbox = yield* Effect.tryPromise({ + try: () => + client.accountInbox.findUnique({ + where: { id: inboxId, accountAddress }, + select: { + id: true, + account: { + select: { + address: true, + }, + }, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + signatureHex: true, + signatureRecovery: true, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'getAccountInbox', + cause: error, + }), + }); + + if (!inbox) { + return yield* Effect.fail( + new ResourceNotFoundError({ + resource: 'AccountInbox', + id: inboxId, + }), + ); + } + + return { + inboxId: inbox.id, + accountAddress: inbox.account.address, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + signature: { + hex: inbox.signatureHex, + recovery: inbox.signatureRecovery, + }, + }; + })(); + + const postAccountInboxMessage = ({ + accountAddress, + inboxId, + message, + }: { + accountAddress: string; + inboxId: string; + message: Messages.RequestCreateAccountInboxMessage; + }) => + Effect.fn(function* () { + // First get the inbox to validate it exists and get auth policy + const accountInbox = yield* getAccountInbox({ accountAddress, inboxId }); + + // Validate auth policy requirements + switch (accountInbox.authPolicy) { + case 'requires_auth': + if (!message.signature || !message.authorAccountAddress) { + return yield* Effect.fail( + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress required', + }), + ); + } + break; + case 'anonymous': + if (message.signature || message.authorAccountAddress) { + return yield* Effect.fail( + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress not allowed', + }), + ); + } + break; + case 'optional_auth': + if ( + (message.signature && !message.authorAccountAddress) || + (!message.signature && message.authorAccountAddress) + ) { + return yield* Effect.fail( + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress must be provided together', + }), + ); + } + break; + default: + return yield* Effect.fail( + new ValidationError({ + field: 'authPolicy', + message: 'Unknown auth policy', + }), + ); + } + + // If signature and account provided, verify authorization + if (message.signature && message.authorAccountAddress) { + // Recover the public key from the signature + const authorPublicKey = yield* Effect.try({ + try: () => Inboxes.recoverAccountInboxMessageSigner(message, accountAddress, inboxId), + catch: () => + new ValidationError({ + field: 'signature', + message: 'Invalid signature', + }), + }); + + // Check if this public key corresponds to a user's identity + const authorIdentity = yield* identityService + .getAppOrConnectIdentity({ + accountAddress: message.authorAccountAddress, + signaturePublicKey: authorPublicKey, + }) + .pipe( + Effect.catchAll(() => + Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }), + ), + ), + ); + + if (authorIdentity.accountAddress !== message.authorAccountAddress) { + return yield* Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }), + ); + } + } + + // Create the message in the database + const createdMessage = yield* Effect.tryPromise({ + try: () => + client.$transaction(async (prisma) => { + // Double-check the inbox exists and belongs to the correct account + const inbox = await prisma.accountInbox.findUnique({ + where: { id: inboxId, accountAddress }, + }); + + if (!inbox) { + throw new Error('Account inbox not found'); + } + + // Create the message + const created = await prisma.accountInboxMessage.create({ + data: { + ciphertext: message.ciphertext, + signatureHex: message.signature?.hex ?? null, + signatureRecovery: message.signature?.recovery ?? null, + authorAccountAddress: message.authorAccountAddress ?? null, + accountInbox: { + connect: { id: inbox.id }, + }, + }, + }); + + return { + id: created.id, + ciphertext: created.ciphertext, + signature: + created.signatureHex != null && created.signatureRecovery != null + ? { + hex: created.signatureHex, + recovery: created.signatureRecovery, + } + : undefined, + authorAccountAddress: created.authorAccountAddress ?? undefined, + createdAt: created.createdAt, + } as Messages.InboxMessage; + }), + catch: (error) => + new DatabaseError({ + operation: 'postAccountInboxMessage', + cause: error, + }), + }); + + // TODO: Broadcast the message (WebSocket functionality would go here) + // broadcastAccountInboxMessage({ accountAddress, inboxId, message: createdMessage }); + + return createdMessage; + })(); + + return { + listPublicAccountInboxes, + getAccountInbox, + postAccountInboxMessage, + } as const; +})(); + +export const AccountInboxServiceLive = Layer.effect(AccountInboxService, makeAccountInboxService); diff --git a/apps/server-new/src/services/app-identity.ts b/apps/server-new/src/services/app-identity.ts index 0a74fefd..9fa2d9e1 100644 --- a/apps/server-new/src/services/app-identity.ts +++ b/apps/server-new/src/services/app-identity.ts @@ -1,10 +1,10 @@ import { Context, Effect, Layer } from 'effect'; -import { - DatabaseError, - InvalidTokenError, - ResourceAlreadyExistsError, - ResourceNotFoundError, - TokenExpiredError +import { + DatabaseError, + InvalidTokenError, + ResourceAlreadyExistsError, + ResourceNotFoundError, + TokenExpiredError, } from '../http/errors.js'; import { DatabaseService } from './database.js'; @@ -49,7 +49,7 @@ export interface AppIdentityService { appId: string; }) => Effect.Effect; readonly createAppIdentity: ( - params: CreateAppIdentityParams + params: CreateAppIdentityParams, ) => Effect.Effect; } @@ -76,7 +76,7 @@ export const makeAppIdentityService = Effect.fn(function* () { }); if (!appIdentity) { - yield* new ResourceNotFoundError({ + return yield* new ResourceNotFoundError({ resource: 'AppIdentity', id: 'session-token', }); @@ -124,11 +124,11 @@ export const makeAppIdentityService = Effect.fn(function* () { appId: params.appId, }, }); - + if (existingIdentity) { throw new Error('App identity already exists'); } - + // Create the new app identity return await prisma.appIdentity.create({ data: { diff --git a/apps/server-new/src/services/identity.ts b/apps/server-new/src/services/identity.ts index 2a0edc05..6762e469 100644 --- a/apps/server-new/src/services/identity.ts +++ b/apps/server-new/src/services/identity.ts @@ -14,9 +14,8 @@ export interface IdentityResult { } export interface IdentityService { - readonly getAppOrConnectIdentity: (params: - | { accountAddress: string; signaturePublicKey: string } - | { accountAddress: string; appId: string } + readonly getAppOrConnectIdentity: ( + params: { accountAddress: string; signaturePublicKey: string } | { accountAddress: string; appId: string }, ) => Effect.Effect; } @@ -25,9 +24,8 @@ export const IdentityService = Context.GenericTag('IdentityServ export const makeIdentityService = Effect.fn(function* () { const { client } = yield* DatabaseService; - const getAppOrConnectIdentity = (params: - | { accountAddress: string; signaturePublicKey: string } - | { accountAddress: string; appId: string } + const getAppOrConnectIdentity = ( + params: { accountAddress: string; signaturePublicKey: string } | { accountAddress: string; appId: string }, ) => Effect.fn(function* () { // If we have signaturePublicKey, search by that diff --git a/apps/server-new/src/services/space-inbox.ts b/apps/server-new/src/services/space-inbox.ts new file mode 100644 index 00000000..7a57dba5 --- /dev/null +++ b/apps/server-new/src/services/space-inbox.ts @@ -0,0 +1,274 @@ +import { Inboxes, type Messages, type SpaceEvents } from '@graphprotocol/hypergraph'; +import { Context, Effect, Layer } from 'effect'; +import { AuthorizationError, DatabaseError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; +import { DatabaseService } from './database.js'; +import { IdentityService } from './identity.js'; + +export interface SpaceInboxResult { + inboxId: string; + isPublic: boolean; + authPolicy: Inboxes.InboxSenderAuthPolicy; + encryptionPublicKey: string | null; + creationEvent: SpaceEvents.CreateSpaceInboxEvent; +} + +export interface SpaceInboxService { + readonly listPublicSpaceInboxes: (params: { spaceId: string }) => Effect.Effect; + readonly getSpaceInbox: (params: { + spaceId: string; + inboxId: string; + }) => Effect.Effect; + readonly postSpaceInboxMessage: (params: { + spaceId: string; + inboxId: string; + message: Messages.RequestCreateSpaceInboxMessage; + }) => Effect.Effect< + Messages.InboxMessage, + ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseError + >; +} + +export const SpaceInboxService = Context.GenericTag('SpaceInboxService'); + +export const makeSpaceInboxService = Effect.fn(function* () { + const { client } = yield* DatabaseService; + const identityService = yield* IdentityService; + + const listPublicSpaceInboxes = ({ spaceId }: { spaceId: string }) => + Effect.fn(function* () { + const inboxes = yield* Effect.tryPromise({ + try: () => + client.spaceInbox.findMany({ + where: { spaceId, isPublic: true }, + select: { + id: true, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + spaceEvent: { + select: { + event: true, + }, + }, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'listPublicSpaceInboxes', + cause: error, + }), + }); + + return inboxes.map((inbox) => ({ + inboxId: inbox.id, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, + })); + })(); + + const getSpaceInbox = ({ spaceId, inboxId }: { spaceId: string; inboxId: string }) => + Effect.fn(function* () { + const inbox = yield* Effect.tryPromise({ + try: () => + client.spaceInbox.findUnique({ + where: { id: inboxId, spaceId }, + select: { + id: true, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + spaceEvent: { + select: { + event: true, + }, + }, + }, + }), + catch: (error) => + new DatabaseError({ + operation: 'getSpaceInbox', + cause: error, + }), + }); + + if (!inbox) { + return yield* Effect.fail( + new ResourceNotFoundError({ + resource: 'SpaceInbox', + id: inboxId, + }), + ); + } + + return { + inboxId: inbox.id, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, + }; + })(); + + const postSpaceInboxMessage = ({ + spaceId, + inboxId, + message, + }: { + spaceId: string; + inboxId: string; + message: Messages.RequestCreateSpaceInboxMessage; + }) => + Effect.fn(function* () { + // First get the inbox to validate it exists and get auth policy + const spaceInbox = yield* getSpaceInbox({ spaceId, inboxId }); + + // Validate auth policy requirements + switch (spaceInbox.authPolicy) { + case 'requires_auth': + if (!message.signature || !message.authorAccountAddress) { + return yield* Effect.fail( + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress required', + }), + ); + } + break; + case 'anonymous': + if (message.signature || message.authorAccountAddress) { + return yield* Effect.fail( + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress not allowed', + }), + ); + } + break; + case 'optional_auth': + if ( + (message.signature && !message.authorAccountAddress) || + (!message.signature && message.authorAccountAddress) + ) { + return yield* Effect.fail( + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress must be provided together', + }), + ); + } + break; + default: + return yield* Effect.fail( + new ValidationError({ + field: 'authPolicy', + message: 'Unknown auth policy', + }), + ); + } + + // If signature and account provided, verify authorization + if (message.signature && message.authorAccountAddress) { + // Recover the public key from the signature + const authorPublicKey = yield* Effect.try({ + try: () => Inboxes.recoverSpaceInboxMessageSigner(message, spaceId, inboxId), + catch: () => + new ValidationError({ + field: 'signature', + message: 'Invalid signature', + }), + }); + + // Check if this public key corresponds to a user's identity + const authorIdentity = yield* identityService + .getAppOrConnectIdentity({ + accountAddress: message.authorAccountAddress, + signaturePublicKey: authorPublicKey, + }) + .pipe( + Effect.catchAll(() => + Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }), + ), + ), + ); + + if (authorIdentity.accountAddress !== message.authorAccountAddress) { + return yield* Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }), + ); + } + } + + // Create the message in the database + const createdMessage = yield* Effect.tryPromise({ + try: () => + client.$transaction(async (prisma) => { + // Double-check the inbox exists and belongs to the correct space + const inbox = await prisma.spaceInbox.findUnique({ + where: { id: inboxId }, + }); + + if (!inbox) { + throw new Error('Space inbox not found'); + } + + if (inbox.spaceId !== spaceId) { + throw new Error('Incorrect space'); + } + + // Create the message + const created = await prisma.spaceInboxMessage.create({ + data: { + spaceInbox: { + connect: { id: inbox.id }, + }, + ciphertext: message.ciphertext, + signatureHex: message.signature?.hex ?? null, + signatureRecovery: message.signature?.recovery ?? null, + authorAccountAddress: message.authorAccountAddress ?? null, + }, + }); + + return { + id: created.id, + ciphertext: created.ciphertext, + signature: + created.signatureHex != null && created.signatureRecovery != null + ? { + hex: created.signatureHex, + recovery: created.signatureRecovery, + } + : undefined, + authorAccountAddress: created.authorAccountAddress ?? undefined, + createdAt: created.createdAt, + } as Messages.InboxMessage; + }), + catch: (error) => + new DatabaseError({ + operation: 'postSpaceInboxMessage', + cause: error, + }), + }); + + // TODO: Broadcast the message (WebSocket functionality would go here) + // broadcastSpaceInboxMessage({ spaceId, inboxId, message: createdMessage }); + + return createdMessage; + })(); + + return { + listPublicSpaceInboxes, + getSpaceInbox, + postSpaceInboxMessage, + } as const; +})(); + +export const SpaceInboxServiceLive = Layer.effect(SpaceInboxService, makeSpaceInboxService); diff --git a/apps/server-new/src/services/spaces.ts b/apps/server-new/src/services/spaces.ts index fa560a1d..991cdb36 100644 --- a/apps/server-new/src/services/spaces.ts +++ b/apps/server-new/src/services/spaces.ts @@ -1,6 +1,6 @@ -import { Identity, Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph'; +import { Identity, type Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph'; import { Context, Effect, Layer } from 'effect'; -import { DatabaseError, ValidationError } from '../http/errors.js'; +import { DatabaseError, type ValidationError } from '../http/errors.js'; import { DatabaseService } from './database.js'; import { IdentityService } from './identity.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee967cff..e895dcf8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -372,6 +372,9 @@ importers: '@graphprotocol/hypergraph': specifier: workspace:* version: link:../../packages/hypergraph/publish + '@noble/hashes': + specifier: ^1.8.0 + version: 1.8.0 '@prisma/client': specifier: ^6.14.0 version: 6.14.0(prisma@6.14.0(typescript@5.9.2))(typescript@5.9.2) @@ -24880,7 +24883,7 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.0 '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) From 38048501d3f2b413b7ace9b0ab9121929cda8e50 Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Fri, 29 Aug 2025 13:07:09 +0200 Subject: [PATCH 11/14] wip --- .vscode/settings.json | 2 + apps/connect/package.json | 2 +- apps/events/package.json | 2 +- apps/server-new/src/domain/models.ts | 4 +- apps/server-new/src/http/api.ts | 33 +- apps/server-new/src/http/errors.ts | 22 +- apps/server-new/src/http/handlers.ts | 179 ++++---- apps/server-new/src/server.ts | 30 +- apps/server-new/src/services/account-inbox.ts | 396 ++++++++---------- apps/server-new/src/services/app-identity.ts | 197 ++++----- apps/server-new/src/services/auth.ts | 18 +- .../src/services/connect-identity.ts | 211 ++++------ apps/server-new/src/services/database.ts | 89 ++-- apps/server-new/src/services/identity.ts | 175 ++++---- apps/server-new/src/services/privy-auth.ts | 156 ++++--- apps/server-new/src/services/space-inbox.ts | 131 +++--- apps/server-new/src/services/spaces.ts | 146 +++---- apps/server-new/tsconfig.app.json | 29 -- apps/server-new/tsconfig.json | 4 +- apps/server-new/tsconfig.node.json | 29 -- apps/server/package.json | 2 +- apps/template-vite-react/package.json | 2 +- package.json | 3 +- packages/create-hypergraph/package.json | 3 +- packages/hypergraph-react/package.json | 2 +- packages/hypergraph/package.json | 2 +- packages/hypergraph/src/identity/types.ts | 7 +- .../src/space-events/apply-event.ts | 17 +- packages/typesync-studio/package.json | 2 +- pnpm-lock.yaml | 222 ++++------ tsconfig.base.json | 6 +- tsconfig.json | 1 + 32 files changed, 879 insertions(+), 1245 deletions(-) delete mode 100644 apps/server-new/tsconfig.app.json delete mode 100644 apps/server-new/tsconfig.node.json diff --git a/.vscode/settings.json b/.vscode/settings.json index b670e734..7184d448 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,6 @@ { + "typescript.tsdk": "./node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, "files.watcherExclude": { "**/routeTree.gen.ts": true }, diff --git a/apps/connect/package.json b/apps/connect/package.json index 7a074bc9..f107c4ad 100644 --- a/apps/connect/package.json +++ b/apps/connect/package.json @@ -25,7 +25,7 @@ "@tanstack/react-router-devtools": "^1.131.27", "@xstate/store": "^3.9.2", "clsx": "^2.1.1", - "effect": "^3.17.8", + "effect": "^3.17.9", "graphql-request": "^7.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/apps/events/package.json b/apps/events/package.json index 8973a601..15587ef7 100644 --- a/apps/events/package.json +++ b/apps/events/package.json @@ -23,7 +23,7 @@ "@xstate/store": "^3.9.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "effect": "^3.17.8", + "effect": "^3.17.9", "framer-motion": "^12.23.12", "graphql-request": "^7.2.0", "isomorphic-ws": "^5.0.0", diff --git a/apps/server-new/src/domain/models.ts b/apps/server-new/src/domain/models.ts index a6600160..556a5064 100644 --- a/apps/server-new/src/domain/models.ts +++ b/apps/server-new/src/domain/models.ts @@ -41,8 +41,8 @@ export const AppIdentity = Schema.Struct({ keyProof: Schema.String, accountAddress: Schema.String, appId: Schema.String, - sessionToken: Schema.String, - sessionTokenExpires: Schema.DateFromSelf, + sessionToken: Schema.String.pipe(Schema.NullOr), + sessionTokenExpires: Schema.DateFromSelf.pipe(Schema.NullOr), }); export const Space = Schema.Struct({ diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 46d895d8..17030edc 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -81,7 +81,7 @@ export const getConnectSpacesEndpoint = HttpApiEndpoint.get('getConnectSpaces')` .addError(Errors.AuthenticationError, { status: 401 }) .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.PrivyTokenError, { status: 401 }) - .addError(Errors.PrivyConfigError, { status: 500 }); + .addError(Errors.InternalServerError); export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces')`/connect/spaces` .setHeaders(Schema.Struct({ @@ -93,8 +93,7 @@ export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.PrivyTokenError, { status: 401 }) - .addError(Errors.PrivyConfigError, { status: 500 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( 'postConnectAddAppIdentityToSpaces', @@ -108,8 +107,7 @@ export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.PrivyTokenError, { status: 401 }) - .addError(Errors.PrivyConfigError, { status: 500 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIdentity')`/connect/identity` .setHeaders(Schema.Struct({ @@ -122,8 +120,7 @@ export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIden .addError(Errors.ResourceAlreadyExistsError, { status: 400 }) .addError(Errors.OwnershipProofError, { status: 401 }) .addError(Errors.PrivyTokenError, { status: 401 }) - .addError(Errors.PrivyConfigError, { status: 500 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const getConnectIdentityEncryptedEndpoint = HttpApiEndpoint.get( 'getConnectIdentityEncrypted', @@ -137,7 +134,7 @@ export const getConnectIdentityEncryptedEndpoint = HttpApiEndpoint.get( .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.ResourceNotFoundError, { status: 404 }) .addError(Errors.PrivyTokenError, { status: 401 }) - .addError(Errors.PrivyConfigError, { status: 500 }); + .addError(Errors.InternalServerError); export const getConnectAppIdentityEndpoint = HttpApiEndpoint.get( 'getConnectAppIdentity', @@ -151,22 +148,20 @@ export const getConnectAppIdentityEndpoint = HttpApiEndpoint.get( .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.ResourceNotFoundError, { status: 404 }) .addError(Errors.PrivyTokenError, { status: 401 }) - .addError(Errors.PrivyConfigError, { status: 500 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const postConnectAppIdentityEndpoint = HttpApiEndpoint.post('postConnectAppIdentity')`/connect/app-identity` .setHeaders(Schema.Struct({ 'privy-id-token': Schema.String, })) .setPayload(Messages.RequestConnectCreateAppIdentity) - .addSuccess(AppIdentityResponse) + .addSuccess(Schema.Void) .addError(Errors.AuthenticationError, { status: 401 }) .addError(Errors.AuthorizationError, { status: 401 }) .addError(Errors.ResourceAlreadyExistsError, { status: 400 }) .addError(Errors.OwnershipProofError, { status: 401 }) .addError(Errors.PrivyTokenError, { status: 401 }) - .addError(Errors.PrivyConfigError, { status: 500 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const connectGroup = HttpApiGroup.make('Connect') .add(getConnectSpacesEndpoint) @@ -210,12 +205,12 @@ export const identityGroup = HttpApiGroup.make('Identity') */ export const getSpaceInboxesEndpoint = HttpApiEndpoint.get('getSpaceInboxes')`/spaces/${spaceId}/inboxes` .addSuccess(Messages.ResponseListSpaceInboxesPublic) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const getSpaceInboxEndpoint = HttpApiEndpoint.get('getSpaceInbox')`/spaces/${spaceId}/inboxes/${inboxId}` .addSuccess(Messages.ResponseSpaceInboxPublic) .addError(Errors.ResourceNotFoundError, { status: 404 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const postSpaceInboxMessageEndpoint = HttpApiEndpoint.post( 'postSpaceInboxMessage', @@ -225,18 +220,18 @@ export const postSpaceInboxMessageEndpoint = HttpApiEndpoint.post( .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.AuthorizationError, { status: 403 }) .addError(Errors.ResourceNotFoundError, { status: 404 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const getAccountInboxesEndpoint = HttpApiEndpoint.get('getAccountInboxes')`/accounts/${accountAddress}/inboxes` .addSuccess(Messages.ResponseListAccountInboxesPublic) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const getAccountInboxEndpoint = HttpApiEndpoint.get( 'getAccountInbox', )`/accounts/${accountAddress}/inboxes/${inboxId}` .addSuccess(Messages.ResponseAccountInboxPublic) .addError(Errors.ResourceNotFoundError, { status: 404 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const postAccountInboxMessageEndpoint = HttpApiEndpoint.post( 'postAccountInboxMessage', @@ -246,7 +241,7 @@ export const postAccountInboxMessageEndpoint = HttpApiEndpoint.post( .addError(Errors.ValidationError, { status: 400 }) .addError(Errors.AuthorizationError, { status: 403 }) .addError(Errors.ResourceNotFoundError, { status: 404 }) - .addError(Errors.DatabaseError, { status: 500 }); + .addError(Errors.InternalServerError); export const inboxGroup = HttpApiGroup.make('Inbox') .add(getSpaceInboxesEndpoint) diff --git a/apps/server-new/src/http/errors.ts b/apps/server-new/src/http/errors.ts index dc425e07..acdf96e3 100644 --- a/apps/server-new/src/http/errors.ts +++ b/apps/server-new/src/http/errors.ts @@ -1,5 +1,14 @@ +import { HttpApiSchema } from '@effect/platform'; import { Schema } from 'effect'; +export class InternalServerError extends Schema.TaggedError()('InternalServerError', { + message: Schema.String.pipe(Schema.optionalWith({ + default: () => 'Internal server error', + })), +}, { + [HttpApiSchema.AnnotationStatus]: 500, +}) {} + /** * Authentication-related errors */ @@ -64,19 +73,6 @@ export class PrivyTokenError extends Schema.TaggedError()('Priv message: Schema.String, }) {} -/** - * Database errors - */ -export class DatabaseError extends Schema.TaggedError()('DatabaseError', { - operation: Schema.String, - cause: Schema.Unknown, -}) {} - -export class TransactionError extends Schema.TaggedError()('TransactionError', { - message: Schema.String, - cause: Schema.Unknown, -}) {} - /** * Business logic errors */ diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index b341df67..b29c9d92 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -3,13 +3,13 @@ import { Identity, Messages, Utils } from '@graphprotocol/hypergraph'; import { bytesToHex, randomBytes } from '@noble/hashes/utils.js'; import { Effect, Layer } from 'effect'; import { hypergraphChainConfig, hypergraphRpcUrlConfig } from '../config/hypergraph.js'; -import { AccountInboxService } from '../services/account-inbox.js'; -import { AppIdentityService } from '../services/app-identity.js'; -import { ConnectIdentityService } from '../services/connect-identity.js'; -import { IdentityService } from '../services/identity.js'; -import { PrivyAuthService } from '../services/privy-auth.js'; -import { SpaceInboxService } from '../services/space-inbox.js'; -import { SpacesService } from '../services/spaces.js'; +import * as AccountInboxService from '../services/account-inbox.js'; +import * as AppIdentityService from '../services/app-identity.js'; +import * as ConnectIdentityService from '../services/connect-identity.js'; +import * as IdentityService from '../services/identity.js'; +import * as PrivyAuthService from '../services/privy-auth.js'; +import * as SpaceInboxService from '../services/space-inbox.js'; +import * as SpacesService from '../services/spaces.js'; import * as Api from './api.js'; import * as Errors from './errors.js'; @@ -30,26 +30,25 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han Effect.fn(function* ({ headers }) { yield* Effect.logInfo('GET /connect/spaces'); - const privyAuthService = yield* PrivyAuthService; - const spacesService = yield* SpacesService; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; + const spacesService = yield* SpacesService.SpacesService; - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']); - - const spaces = yield* spacesService.listByAccount(headers['account-address']); + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']).pipe(Effect.orDie); + const spaces = yield* spacesService.listByAccount(headers['account-address']).pipe(Effect.orDie); return { spaces }; - }), + }) ) .handle( 'postConnectSpaces', Effect.fn(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/spaces'); - const privyAuthService = yield* PrivyAuthService; - const spacesService = yield* SpacesService; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; + const spacesService = yield* SpacesService.SpacesService; // Authenticate the request with Privy token - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress); + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress).pipe(Effect.orDie); // Create the space const space = yield* spacesService.createSpace({ @@ -60,7 +59,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han infoSignatureHex: payload.infoSignature.hex, infoSignatureRecovery: payload.infoSignature.recovery, name: payload.name, - }); + }).pipe(Effect.orDie); return { space }; }), @@ -70,18 +69,18 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han Effect.fn(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/add-app-identity-to-spaces'); - const privyAuthService = yield* PrivyAuthService; - const spacesService = yield* SpacesService; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; + const spacesService = yield* SpacesService.SpacesService; // Authenticate the request with Privy token - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress); + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress).pipe(Effect.orDie); // Add app identity to spaces yield* spacesService.addAppIdentityToSpaces({ appIdentityAddress: payload.appIdentityAddress, accountAddress: payload.accountAddress, spacesInput: payload.spacesInput, - }); + }).pipe(Effect.orDie); }), ) .handle( @@ -89,23 +88,21 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han Effect.fn(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/identity'); - const privyAuthService = yield* PrivyAuthService; - const connectIdentityService = yield* ConnectIdentityService; - const chain = yield* hypergraphChainConfig; - const rpcUrl = yield* hypergraphRpcUrlConfig; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; + const connectIdentityService = yield* ConnectIdentityService.ConnectIdentityService; + const chain = yield* hypergraphChainConfig.pipe(Effect.orDie); + const rpcUrl = yield* hypergraphRpcUrlConfig.pipe(Effect.orDie); // Verify the Privy token and get signer address - const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']); + const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']).pipe(Effect.orDie); const accountAddress = payload.keyBox.accountAddress; // Verify that the signer matches the one in the keyBox if (signerAddress !== payload.keyBox.signer) { - return yield* Effect.fail( - new Errors.AuthorizationError({ - message: 'Signer mismatch', - accountAddress, - }), - ); + return yield* new Errors.AuthorizationError({ + message: 'Signer mismatch', + accountAddress, + }); } // Verify identity ownership proof @@ -127,12 +124,10 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han }); if (!isValid) { - return yield* Effect.fail( - new Errors.OwnershipProofError({ - accountAddress, - reason: 'Invalid ownership proof', - }), - ); + return yield* new Errors.OwnershipProofError({ + accountAddress, + reason: 'Invalid ownership proof', + }) } yield* Effect.logInfo('Ownership proof is valid'); @@ -147,7 +142,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han encryptionPublicKey: payload.encryptionPublicKey, accountProof: payload.accountProof, keyProof: payload.keyProof, - }); + }).pipe(Effect.orDie); const response: Messages.ResponseConnectCreateIdentity = { success: true, @@ -161,18 +156,18 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han Effect.fn(function* ({ headers }) { yield* Effect.logInfo('GET /connect/identity/encrypted'); - const privyAuthService = yield* PrivyAuthService; - const connectIdentityService = yield* ConnectIdentityService; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; + const connectIdentityService = yield* ConnectIdentityService.ConnectIdentityService; // Authenticate the request with Privy token - const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']); + const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']).pipe(Effect.orDie); const accountAddress = headers['account-address']; // Verify the signer is authorized for this account - yield* privyAuthService.isSignerForAccount(signerAddress, accountAddress); + yield* privyAuthService.isSignerForAccount(signerAddress, accountAddress).pipe(Effect.orDie); // Get the encrypted identity - const identity = yield* connectIdentityService.getIdentityEncrypted(accountAddress); + const identity = yield* connectIdentityService.getIdentityEncrypted(accountAddress).pipe(Effect.orDie); const response: Messages.ResponseIdentityEncrypted = { keyBox: { @@ -191,17 +186,17 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han Effect.fn(function* ({ headers, path: { appId } }) { yield* Effect.logInfo(`GET /connect/app-identity/${appId}`); - const privyAuthService = yield* PrivyAuthService; - const appIdentityService = yield* AppIdentityService; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; + const appIdentityService = yield* AppIdentityService.AppIdentityService; // Authenticate the request with Privy token - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']); + yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']).pipe(Effect.orDie); // Find the app identity const appIdentity = yield* appIdentityService.findByAppId({ accountAddress: headers['account-address'], appId, - }); + }).pipe(Effect.orDie); if (!appIdentity) { return yield* new Errors.ResourceNotFoundError({ @@ -218,17 +213,17 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han Effect.fn(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/app-identity'); - const privyAuthService = yield* PrivyAuthService; - const appIdentityService = yield* AppIdentityService; - const chain = yield* hypergraphChainConfig; - const rpcUrl = yield* hypergraphRpcUrlConfig; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; + const appIdentityService = yield* AppIdentityService.AppIdentityService; + const chain = yield* hypergraphChainConfig.pipe(Effect.orDie); + const rpcUrl = yield* hypergraphRpcUrlConfig.pipe(Effect.orDie); // Verify the Privy token and get signer address - const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']); + const signerAddress = yield* privyAuthService.verifyPrivyToken(headers['privy-id-token']).pipe(Effect.orDie); const accountAddress = payload.accountAddress; // Verify signer is authorized for this account - yield* privyAuthService.isSignerForAccount(signerAddress, accountAddress); + yield* privyAuthService.isSignerForAccount(signerAddress, accountAddress).pipe(Effect.orDie); // Verify identity ownership proof const isValid = yield* Effect.tryPromise({ @@ -246,13 +241,13 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han accountAddress, reason: 'Failed to verify identity ownership', }), - }); + }).pipe(Effect.orDie); if (!isValid) { - yield* new Errors.OwnershipProofError({ + return yield* new Errors.OwnershipProofError({ accountAddress, reason: 'Invalid ownership proof', - }); + }) } // Generate session token @@ -260,7 +255,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han const sessionTokenExpires = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); // 30 days // Create the app identity - const appIdentity = yield* appIdentityService.createAppIdentity({ + yield* appIdentityService.createAppIdentity({ accountAddress, appId: payload.appId, address: payload.address, @@ -271,12 +266,15 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han keyProof: payload.keyProof, sessionToken, sessionTokenExpires, - }); - - return { appIdentity }; + }).pipe(Effect.orDie); }), ); -}); +}).pipe( + Layer.provide(PrivyAuthService.layer), + Layer.provide(AppIdentityService.layer), + Layer.provide(ConnectIdentityService.layer), + Layer.provide(SpacesService.layer), +); /** * Identity Group Handlers @@ -292,11 +290,11 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h const sessionToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader; if (!sessionToken) { - yield* new Errors.AuthenticationError({ message: 'No session token provided' }); + return yield* new Errors.AuthenticationError({ message: 'No session token provided' }); } - const appIdentityService = yield* AppIdentityService; - const { accountAddress } = yield* appIdentityService.getBySessionToken(sessionToken); + const appIdentityService = yield* AppIdentityService.AppIdentityService; + const { accountAddress } = yield* appIdentityService.getBySessionToken(sessionToken).pipe(Effect.orDie); return accountAddress; }), @@ -307,14 +305,14 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h yield* Effect.logInfo('GET /connect/identity', { accountAddress: urlParams.accountAddress }); if (!urlParams.accountAddress) { - yield* new Errors.ValidationError({ + return yield* new Errors.ValidationError({ field: 'accountAddress', message: 'accountAddress is required', }); } - const connectIdentityService = yield* ConnectIdentityService; - const identity = yield* connectIdentityService.getByAccountAddress(urlParams.accountAddress); + const connectIdentityService = yield* ConnectIdentityService.ConnectIdentityService; + const identity = yield* connectIdentityService.getByAccountAddress(urlParams.accountAddress).pipe(Effect.orDie); const response: Messages.ResponseIdentity = { accountAddress: identity.accountAddress, @@ -332,18 +330,18 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h Effect.fn(function* ({ urlParams }) { yield* Effect.logInfo('GET /identity', urlParams); - const identityService = yield* IdentityService; + const identityService = yield* IdentityService.IdentityService; // Validate required parameters if (!urlParams.accountAddress) { - yield* new Errors.ValidationError({ + return yield* new Errors.ValidationError({ field: 'accountAddress', message: 'accountAddress is required', }); } if (!urlParams.signaturePublicKey && !urlParams.appId) { - yield* new Errors.ValidationError({ + return yield* new Errors.ValidationError({ field: 'signaturePublicKey or appId', message: 'Either signaturePublicKey or appId is required', }); @@ -354,7 +352,7 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h ? { accountAddress: urlParams.accountAddress, signaturePublicKey: urlParams.signaturePublicKey } : { accountAddress: urlParams.accountAddress, appId: urlParams.appId! }; - const identity = yield* identityService.getAppOrConnectIdentity(params); + const identity = yield* identityService.getAppOrConnectIdentity(params).pipe(Effect.orDie); const response: Messages.ResponseIdentity = { accountAddress: urlParams.accountAddress, @@ -368,7 +366,11 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h return response; }), ); -}); +}).pipe( + Layer.provide(AppIdentityService.layer), + Layer.provide(ConnectIdentityService.layer), + Layer.provide(IdentityService.layer), +); /** * Inbox Group Handlers @@ -380,9 +382,9 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler Effect.fn(function* ({ path: { spaceId } }) { yield* Effect.logInfo(`GET /spaces/${spaceId}/inboxes`); - const spaceInboxService = yield* SpaceInboxService; + const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; - const inboxes = yield* spaceInboxService.listPublicSpaceInboxes({ spaceId }); + const inboxes = yield* spaceInboxService.listPublicSpaceInboxes({ spaceId }).pipe(Effect.orDie); return { inboxes }; }), @@ -392,9 +394,9 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler Effect.fn(function* ({ path: { spaceId, inboxId } }) { yield* Effect.logInfo(`GET /spaces/${spaceId}/inboxes/${inboxId}`); - const spaceInboxService = yield* SpaceInboxService; + const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; - const inbox = yield* spaceInboxService.getSpaceInbox({ spaceId, inboxId }); + const inbox = yield* spaceInboxService.getSpaceInbox({ spaceId, inboxId }).pipe(Effect.orDie); return { inbox }; }), @@ -404,13 +406,13 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler Effect.fn(function* ({ path: { spaceId, inboxId }, payload }) { yield* Effect.logInfo(`POST /spaces/${spaceId}/inboxes/${inboxId}/messages`); - const spaceInboxService = yield* SpaceInboxService; + const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; yield* spaceInboxService.postSpaceInboxMessage({ spaceId, inboxId, message: payload, - }); + }).pipe(Effect.orDie); // Return void as per the API endpoint definition }), @@ -420,9 +422,9 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler Effect.fn(function* ({ path: { accountAddress } }) { yield* Effect.logInfo(`GET /accounts/${accountAddress}/inboxes`); - const accountInboxService = yield* AccountInboxService; + const accountInboxService = yield* AccountInboxService.AccountInboxService; - const inboxes = yield* accountInboxService.listPublicAccountInboxes({ accountAddress }); + const inboxes = yield* accountInboxService.listPublicAccountInboxes({ accountAddress }).pipe(Effect.orDie); return { inboxes }; }), @@ -432,9 +434,9 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler Effect.fn(function* ({ path: { accountAddress, inboxId } }) { yield* Effect.logInfo(`GET /accounts/${accountAddress}/inboxes/${inboxId}`); - const accountInboxService = yield* AccountInboxService; + const accountInboxService = yield* AccountInboxService.AccountInboxService; - const inbox = yield* accountInboxService.getAccountInbox({ accountAddress, inboxId }); + const inbox = yield* accountInboxService.getAccountInbox({ accountAddress, inboxId }).pipe(Effect.orDie); return { inbox }; }), @@ -444,23 +446,26 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler Effect.fn(function* ({ path: { accountAddress, inboxId }, payload }) { yield* Effect.logInfo(`POST /accounts/${accountAddress}/inboxes/${inboxId}/messages`); - const accountInboxService = yield* AccountInboxService; + const accountInboxService = yield* AccountInboxService.AccountInboxService; yield* accountInboxService.postAccountInboxMessage({ accountAddress, inboxId, message: payload, - }); + }).pipe(Effect.orDie); // Return void as per the API endpoint definition }), ); -}); +}).pipe( + Layer.provide(AccountInboxService.layer), + Layer.provide(SpaceInboxService.layer), +); /** * All handlers combined */ export const HandlersLive = Layer.mergeAll( HealthGroupLive, - // ConnectGroupLive, IdentityGroupLive, InboxGroupLive + ConnectGroupLive, IdentityGroupLive, InboxGroupLive ); diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts index 3f2f57e2..15dbb93e 100644 --- a/apps/server-new/src/server.ts +++ b/apps/server-new/src/server.ts @@ -5,36 +5,8 @@ import { createServer } from 'node:http'; import { serverPortConfig } from './config/server.ts'; import { hypergraphApi } from './http/api.ts'; import { HandlersLive } from './http/handlers.ts'; -import { AccountInboxServiceLive } from './services/account-inbox.ts'; -import { AppIdentityServiceLive } from './services/app-identity.ts'; -import { ConnectIdentityServiceLive } from './services/connect-identity.ts'; -import { DatabaseServiceLive } from './services/database.ts'; -import { IdentityServiceLive } from './services/identity.ts'; -import { PrivyAuthServiceLive } from './services/privy-auth.ts'; -import { SpaceInboxServiceLive } from './services/space-inbox.ts'; -import { SpacesServiceLive } from './services/spaces.ts'; -const ServicesLive = Layer.mergeAll( - DatabaseServiceLive, - Layer.provide( - AccountInboxServiceLive, - Layer.mergeAll(Layer.provide(IdentityServiceLive, DatabaseServiceLive), DatabaseServiceLive), - ), - Layer.provide(AppIdentityServiceLive, DatabaseServiceLive), - Layer.provide(ConnectIdentityServiceLive, DatabaseServiceLive), - Layer.provide(IdentityServiceLive, DatabaseServiceLive), - Layer.provide(PrivyAuthServiceLive, DatabaseServiceLive), - Layer.provide( - SpaceInboxServiceLive, - Layer.mergeAll(Layer.provide(IdentityServiceLive, DatabaseServiceLive), DatabaseServiceLive), - ), - Layer.provide( - SpacesServiceLive, - Layer.mergeAll(DatabaseServiceLive, Layer.provide(IdentityServiceLive, DatabaseServiceLive)), - ), -); - -const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive), Layer.provide(ServicesLive)); +const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive)); export const server = Layer.unwrapEffect( Effect.gen(function* () { diff --git a/apps/server-new/src/services/account-inbox.ts b/apps/server-new/src/services/account-inbox.ts index 528624da..00e48137 100644 --- a/apps/server-new/src/services/account-inbox.ts +++ b/apps/server-new/src/services/account-inbox.ts @@ -1,120 +1,99 @@ import { Inboxes, type Messages } from '@graphprotocol/hypergraph'; -import { Context, Effect, Layer } from 'effect'; -import { AuthorizationError, DatabaseError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; -import { DatabaseService } from './database.js'; -import { IdentityService } from './identity.js'; +import { AuthorizationError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; +import * as DatabaseService from './database.js'; +import * as IdentityService from './identity.js'; +import * as Predicate from "effect/Predicate"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; export interface AccountInboxResult { inboxId: string; accountAddress: string; isPublic: boolean; authPolicy: Inboxes.InboxSenderAuthPolicy; - encryptionPublicKey: string | null; + encryptionPublicKey: string; signature: { hex: string; recovery: number; }; } -export interface AccountInboxService { +export class AccountInboxService extends Context.Tag('AccountInboxService') Effect.Effect; + }) => Effect.Effect; readonly getAccountInbox: (params: { accountAddress: string; inboxId: string; - }) => Effect.Effect; + }) => Effect.Effect; readonly postAccountInboxMessage: (params: { accountAddress: string; inboxId: string; message: Messages.RequestCreateAccountInboxMessage; }) => Effect.Effect< Messages.InboxMessage, - ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseError + ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseService.DatabaseError >; -} - -export const AccountInboxService = Context.GenericTag('AccountInboxService'); +}>() {} -export const makeAccountInboxService = Effect.fn(function* () { - const { client } = yield* DatabaseService; - const identityService = yield* IdentityService; +export const layer = Effect.gen(function* () { + const { use } = yield* DatabaseService.DatabaseService; + const { getAppOrConnectIdentity } = yield* IdentityService.IdentityService; - const listPublicAccountInboxes = ({ accountAddress }: { accountAddress: string }) => - Effect.fn(function* () { - const inboxes = yield* Effect.tryPromise({ - try: () => - client.accountInbox.findMany({ - where: { accountAddress, isPublic: true }, + const listPublicAccountInboxes = Effect.fn(function* ({ accountAddress }: { accountAddress: string }) { + const inboxes = yield* use((client) => + client.accountInbox.findMany({ + where: { accountAddress, isPublic: true }, + select: { + id: true, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + account: { select: { - id: true, - isPublic: true, - authPolicy: true, - encryptionPublicKey: true, - account: { - select: { - address: true, - }, - }, - signatureHex: true, - signatureRecovery: true, + address: true, }, - }), - catch: (error) => - new DatabaseError({ - operation: 'listPublicAccountInboxes', - cause: error, - }), - }); - - return inboxes.map((inbox) => ({ - inboxId: inbox.id, - accountAddress: inbox.account.address, - isPublic: inbox.isPublic, - authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, - encryptionPublicKey: inbox.encryptionPublicKey, - signature: { - hex: inbox.signatureHex, - recovery: inbox.signatureRecovery, + }, + signatureHex: true, + signatureRecovery: true, }, - })); - })(); + })) - const getAccountInbox = ({ accountAddress, inboxId }: { accountAddress: string; inboxId: string }) => - Effect.fn(function* () { - const inbox = yield* Effect.tryPromise({ - try: () => - client.accountInbox.findUnique({ - where: { id: inboxId, accountAddress }, - select: { - id: true, - account: { - select: { - address: true, - }, + return inboxes.map((inbox) => ({ + inboxId: inbox.id, + accountAddress: inbox.account.address, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + signature: { + hex: inbox.signatureHex, + recovery: inbox.signatureRecovery, + }, + })); + }); + + const getAccountInbox = Effect.fn(function* ({ accountAddress, inboxId }: { accountAddress: string; inboxId: string }) { + const inbox = yield* use((client) => + client.accountInbox.findUnique({ + where: { id: inboxId, accountAddress }, + select: { + id: true, + account: { + select: { + address: true, }, - isPublic: true, - authPolicy: true, - encryptionPublicKey: true, - signatureHex: true, - signatureRecovery: true, }, - }), - catch: (error) => - new DatabaseError({ - operation: 'getAccountInbox', - cause: error, - }), - }); - - if (!inbox) { - return yield* Effect.fail( - new ResourceNotFoundError({ - resource: 'AccountInbox', - id: inboxId, - }), - ); - } + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + signatureHex: true, + signatureRecovery: true, + }, + })).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ + id: inboxId, + resource: 'AccountInbox', + }))) return { inboxId: inbox.id, @@ -127,162 +106,151 @@ export const makeAccountInboxService = Effect.fn(function* () { recovery: inbox.signatureRecovery, }, }; - })(); + }); - const postAccountInboxMessage = ({ + const postAccountInboxMessage = Effect.fn(function* ({ accountAddress, inboxId, message, - }: { - accountAddress: string; - inboxId: string; - message: Messages.RequestCreateAccountInboxMessage; - }) => - Effect.fn(function* () { - // First get the inbox to validate it exists and get auth policy - const accountInbox = yield* getAccountInbox({ accountAddress, inboxId }); + }: { accountAddress: string; inboxId: string; message: Messages.RequestCreateAccountInboxMessage }) { + const accountInbox = yield* getAccountInbox({ accountAddress, inboxId }); - // Validate auth policy requirements - switch (accountInbox.authPolicy) { - case 'requires_auth': - if (!message.signature || !message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress required', - }), - ); - } - break; - case 'anonymous': - if (message.signature || message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress not allowed', - }), - ); - } - break; - case 'optional_auth': - if ( - (message.signature && !message.authorAccountAddress) || - (!message.signature && message.authorAccountAddress) - ) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress must be provided together', - }), - ); - } - break; - default: + // Validate auth policy requirements + switch (accountInbox.authPolicy) { + case 'requires_auth': + if (!message.signature || !message.authorAccountAddress) { return yield* Effect.fail( new ValidationError({ - field: 'authPolicy', - message: 'Unknown auth policy', + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress required', }), ); - } - - // If signature and account provided, verify authorization - if (message.signature && message.authorAccountAddress) { - // Recover the public key from the signature - const authorPublicKey = yield* Effect.try({ - try: () => Inboxes.recoverAccountInboxMessageSigner(message, accountAddress, inboxId), - catch: () => + } + break; + case 'anonymous': + if (message.signature || message.authorAccountAddress) { + return yield* Effect.fail( new ValidationError({ - field: 'signature', - message: 'Invalid signature', + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress not allowed', }), - }); - - // Check if this public key corresponds to a user's identity - const authorIdentity = yield* identityService - .getAppOrConnectIdentity({ - accountAddress: message.authorAccountAddress, - signaturePublicKey: authorPublicKey, - }) - .pipe( - Effect.catchAll(() => - Effect.fail( - new AuthorizationError({ - message: 'Not authorized to post to this inbox', - accountAddress: message.authorAccountAddress, - }), - ), - ), ); - - if (authorIdentity.accountAddress !== message.authorAccountAddress) { + } + break; + case 'optional_auth': + if ( + (message.signature && !message.authorAccountAddress) || + (!message.signature && message.authorAccountAddress) + ) { return yield* Effect.fail( - new AuthorizationError({ - message: 'Not authorized to post to this inbox', - accountAddress: message.authorAccountAddress, + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress must be provided together', }), ); } + break; + default: + return yield* Effect.fail( + new ValidationError({ + field: 'authPolicy', + message: 'Unknown auth policy', + }), + ); + } + + // If signature and account provided, verify authorization + if (message.signature && message.authorAccountAddress) { + // Recover the public key from the signature + const authorPublicKey = yield* Effect.try({ + try: () => Inboxes.recoverAccountInboxMessageSigner(message, accountAddress, inboxId), + catch: () => + new ValidationError({ + field: 'signature', + message: 'Invalid signature', + }), + }); + + // Check if this public key corresponds to a user's identity + const authorIdentity = yield* getAppOrConnectIdentity({ + accountAddress: message.authorAccountAddress, + signaturePublicKey: authorPublicKey, + }) + .pipe( + Effect.catchAll(() => + Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }), + ), + ), + ); + + if (authorIdentity.accountAddress !== message.authorAccountAddress) { + return yield* Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }), + ); } + } - // Create the message in the database - const createdMessage = yield* Effect.tryPromise({ - try: () => - client.$transaction(async (prisma) => { - // Double-check the inbox exists and belongs to the correct account - const inbox = await prisma.accountInbox.findUnique({ - where: { id: inboxId, accountAddress }, - }); + // Create the message in the database + const createdMessage = yield* use((client) => + client.$transaction(async (prisma) => { + // Double-check the inbox exists and belongs to the correct account + const inbox = await prisma.accountInbox.findUnique({ + where: { id: inboxId, accountAddress }, + }); - if (!inbox) { - throw new Error('Account inbox not found'); - } + if (!inbox) { + throw new Error('Account inbox not found'); + } - // Create the message - const created = await prisma.accountInboxMessage.create({ - data: { - ciphertext: message.ciphertext, - signatureHex: message.signature?.hex ?? null, - signatureRecovery: message.signature?.recovery ?? null, - authorAccountAddress: message.authorAccountAddress ?? null, - accountInbox: { - connect: { id: inbox.id }, - }, - }, - }); + // Create the message + const created = await prisma.accountInboxMessage.create({ + data: { + ciphertext: message.ciphertext, + signatureHex: message.signature?.hex ?? null, + signatureRecovery: message.signature?.recovery ?? null, + authorAccountAddress: message.authorAccountAddress ?? null, + accountInbox: { + connect: { id: inbox.id }, + }, + }, + }); - return { - id: created.id, - ciphertext: created.ciphertext, - signature: - created.signatureHex != null && created.signatureRecovery != null - ? { - hex: created.signatureHex, - recovery: created.signatureRecovery, - } - : undefined, - authorAccountAddress: created.authorAccountAddress ?? undefined, - createdAt: created.createdAt, - } as Messages.InboxMessage; - }), - catch: (error) => - new DatabaseError({ - operation: 'postAccountInboxMessage', - cause: error, - }), - }); + return { + id: created.id, + ciphertext: created.ciphertext, + signature: + created.signatureHex != null && created.signatureRecovery != null + ? { + hex: created.signatureHex, + recovery: created.signatureRecovery, + } + : undefined, + authorAccountAddress: created.authorAccountAddress ?? undefined, + createdAt: created.createdAt, + } as Messages.InboxMessage; + }) + ); - // TODO: Broadcast the message (WebSocket functionality would go here) - // broadcastAccountInboxMessage({ accountAddress, inboxId, message: createdMessage }); + // TODO: Broadcast the message (WebSocket functionality would go here) + // broadcastAccountInboxMessage({ accountAddress, inboxId, message: createdMessage }); - return createdMessage; - })(); + return createdMessage; + }); return { listPublicAccountInboxes, getAccountInbox, postAccountInboxMessage, } as const; -})(); - -export const AccountInboxServiceLive = Layer.effect(AccountInboxService, makeAccountInboxService); +}).pipe( + Layer.effect(AccountInboxService), + Layer.provide(DatabaseService.layer), + Layer.provide(IdentityService.layer) +); diff --git a/apps/server-new/src/services/app-identity.ts b/apps/server-new/src/services/app-identity.ts index 9fa2d9e1..2030e33a 100644 --- a/apps/server-new/src/services/app-identity.ts +++ b/apps/server-new/src/services/app-identity.ts @@ -1,12 +1,14 @@ -import { Context, Effect, Layer } from 'effect'; +import * as Effect from "effect/Effect"; +import * as Context from "effect/Context"; +import * as Layer from "effect/Layer"; import { - DatabaseError, InvalidTokenError, ResourceAlreadyExistsError, ResourceNotFoundError, TokenExpiredError, } from '../http/errors.js'; -import { DatabaseService } from './database.js'; +import * as DatabaseService from './database.js'; +import * as Predicate from "effect/Predicate"; export interface AppIdentityResult { address: string; @@ -37,136 +39,107 @@ export interface CreateAppIdentityParams { sessionTokenExpires: Date; } -export interface AppIdentityService { +export class AppIdentityService extends Context.Tag('AppIdentityService') Effect.Effect< { address: string; accountAddress: string }, - InvalidTokenError | ResourceNotFoundError | TokenExpiredError + InvalidTokenError | DatabaseService.DatabaseError | TokenExpiredError >; readonly findByAppId: (params: { accountAddress: string; appId: string; - }) => Effect.Effect; + }) => Effect.Effect; readonly createAppIdentity: ( params: CreateAppIdentityParams, - ) => Effect.Effect; -} - -export const AppIdentityService = Context.GenericTag('AppIdentityService'); - -export const makeAppIdentityService = Effect.fn(function* () { - const { client } = yield* DatabaseService; - - const getBySessionToken = (sessionToken: string) => - Effect.fn(function* () { - const appIdentity = yield* Effect.tryPromise({ - try: () => - client.appIdentity.findFirst({ - where: { - sessionToken, - }, - select: { - address: true, - sessionTokenExpires: true, - accountAddress: true, - }, - }), - catch: () => new InvalidTokenError({ tokenType: 'session' }), + ) => Effect.Effect; +}>() {} + + +export const layer = Effect.gen(function* () { + const { use } = yield* DatabaseService.DatabaseService; + + const getBySessionToken = Effect.fn("getBySessionToken")(function* (sessionToken: string) { + const appIdentity = yield* use((client) => client.appIdentity.findFirst({ + where: { + sessionToken, + }, + select: { + address: true, + sessionTokenExpires: true, + accountAddress: true, + }, + })).pipe( + Effect.filterOrFail(Predicate.isNotNull, () => new InvalidTokenError({ tokenType: 'session' })) + ); + + if (appIdentity.sessionTokenExpires && appIdentity.sessionTokenExpires < new Date()) { + return yield* new TokenExpiredError({ tokenType: 'session' }); + } + + return { + address: appIdentity.address, + accountAddress: appIdentity.accountAddress, + }; + }); + + const findByAppId = Effect.fn("findByAppId")(function* ({ accountAddress, appId }: { accountAddress: string; appId: string; }) { + const appIdentity = yield* use((client) => client.appIdentity.findFirst({ + where: { + accountAddress, + appId, + }, + })).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ + resource: 'AppIdentity', + id: appId, + }))); + + return appIdentity as AppIdentityResult; + }); + + const createAppIdentity = Effect.fn("createAppIdentity")(function* (params: CreateAppIdentityParams) { + const appIdentity = yield* use((client) => client.$transaction(async (prisma) => { + // Check if app identity already exists + const existingIdentity = await prisma.appIdentity.findFirst({ + where: { + accountAddress: params.accountAddress, + appId: params.appId, + }, }); - if (!appIdentity) { - return yield* new ResourceNotFoundError({ + if (existingIdentity) { + throw new ResourceAlreadyExistsError({ resource: 'AppIdentity', - id: 'session-token', + id: params.appId, }); } - if (appIdentity.sessionTokenExpires && appIdentity.sessionTokenExpires < new Date()) { - yield* new TokenExpiredError({ tokenType: 'session' }); - } - - return { - address: appIdentity.address, - accountAddress: appIdentity.accountAddress, - }; - })(); - - const findByAppId = ({ accountAddress, appId }: { accountAddress: string; appId: string }) => - Effect.fn(function* () { - const appIdentity = yield* Effect.tryPromise({ - try: () => - client.appIdentity.findFirst({ - where: { - accountAddress, - appId, - }, - }), - catch: (error) => - new DatabaseError({ - operation: 'findAppIdentity', - cause: error, - }), - }); - - return appIdentity; - })(); - - const createAppIdentity = (params: CreateAppIdentityParams) => - Effect.fn(function* () { - const appIdentity = yield* Effect.tryPromise({ - try: () => - client.$transaction(async (prisma) => { - // Check if app identity already exists - const existingIdentity = await prisma.appIdentity.findFirst({ - where: { - accountAddress: params.accountAddress, - appId: params.appId, - }, - }); - - if (existingIdentity) { - throw new Error('App identity already exists'); - } - - // Create the new app identity - return await prisma.appIdentity.create({ - data: { - address: params.address, - accountAddress: params.accountAddress, - appId: params.appId, - ciphertext: params.ciphertext, - signaturePublicKey: params.signaturePublicKey, - encryptionPublicKey: params.encryptionPublicKey, - accountProof: params.accountProof, - keyProof: params.keyProof, - sessionToken: params.sessionToken, - sessionTokenExpires: params.sessionTokenExpires, - }, - }); - }), - catch: (error) => { - if (error instanceof Error && error.message === 'App identity already exists') { - return new ResourceAlreadyExistsError({ - resource: 'AppIdentity', - id: `${params.accountAddress}:${params.appId}`, - }); - } - return new DatabaseError({ - operation: 'createAppIdentity', - cause: error, - }); + // Create the new app identity + return await prisma.appIdentity.create({ + data: { + address: params.address, + accountAddress: params.accountAddress, + appId: params.appId, + ciphertext: params.ciphertext, + signaturePublicKey: params.signaturePublicKey, + encryptionPublicKey: params.encryptionPublicKey, + accountProof: params.accountProof, + keyProof: params.keyProof, + sessionToken: params.sessionToken, + sessionTokenExpires: params.sessionTokenExpires, }, }); + })); - return appIdentity; - })(); + return appIdentity; + }); return { getBySessionToken, findByAppId, createAppIdentity, - } as const; -})(); - -export const AppIdentityServiceLive = Layer.effect(AppIdentityService, makeAppIdentityService); + }; +}).pipe( + Layer.effect(AppIdentityService), + Layer.provide(DatabaseService.layer) +) \ No newline at end of file diff --git a/apps/server-new/src/services/auth.ts b/apps/server-new/src/services/auth.ts index cca4a68d..6c14ee1e 100644 --- a/apps/server-new/src/services/auth.ts +++ b/apps/server-new/src/services/auth.ts @@ -1,20 +1,18 @@ import { PrivyClient } from '@privy-io/server-auth'; -import { Context, Effect, Layer, Redacted } from 'effect'; import * as Config from '../config/privy.js'; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as Context from "effect/Context"; +import * as Layer from "effect/Layer"; /** - * Auth service interface + * Auth service tag */ -export interface AuthService { +export class AuthService extends Context.Tag("AuthService") Effect.Effect<{ userId: string }, Error>; readonly verifySessionToken: (token: string) => Effect.Effect<{ address: string }, Error>; -} - -/** - * Auth service tag - */ -export const AuthService = Context.GenericTag('AuthService'); +}>() {} /** * Auth service implementation @@ -30,7 +28,7 @@ export const makeAuthService = Effect.fn(function* () { }); if (!user) { - yield* Effect.fail(new Error('User not found')); + return yield* Effect.fail(new Error('User not found')); } return { userId: user.id }; diff --git a/apps/server-new/src/services/connect-identity.ts b/apps/server-new/src/services/connect-identity.ts index e848852c..6cc95bba 100644 --- a/apps/server-new/src/services/connect-identity.ts +++ b/apps/server-new/src/services/connect-identity.ts @@ -1,6 +1,9 @@ -import { Context, Effect, Layer } from 'effect'; -import { DatabaseError, ResourceAlreadyExistsError, ResourceNotFoundError } from '../http/errors.js'; -import { DatabaseService } from './database.js'; +import * as Effect from "effect/Effect"; +import { ResourceAlreadyExistsError, ResourceNotFoundError } from '../http/errors.js'; +import * as DatabaseService from './database.js'; +import * as Predicate from "effect/Predicate"; +import * as Context from "effect/Context"; +import * as Layer from "effect/Layer"; export interface ConnectIdentityResult { accountAddress: string; @@ -27,113 +30,79 @@ export interface CreateConnectIdentityParams { keyProof: string; } -export interface ConnectIdentityService { - readonly getByAccountAddress: (accountAddress: string) => Effect.Effect; +export class ConnectIdentityService extends Context.Tag('ConnectIdentityService') Effect.Effect; readonly getIdentityEncrypted: ( accountAddress: string, - ) => Effect.Effect; + ) => Effect.Effect; readonly createIdentity: ( params: CreateConnectIdentityParams, - ) => Effect.Effect; -} - -export const ConnectIdentityService = Context.GenericTag('ConnectIdentityService'); - -export const makeConnectIdentityService = Effect.fn(function* () { - const { client } = yield* DatabaseService; - - const getByAccountAddress = (accountAddress: string) => - Effect.fn(function* () { - const account = yield* Effect.tryPromise({ - try: () => - client.account.findFirst({ - where: { address: accountAddress }, - select: { - address: true, - connectSignaturePublicKey: true, - connectEncryptionPublicKey: true, - connectAccountProof: true, - connectKeyProof: true, - }, - }), - catch: () => - new ResourceNotFoundError({ - resource: 'ConnectIdentity', - id: accountAddress, - }), - }); - - if (!account) { - return yield* Effect.fail( - new ResourceNotFoundError({ - resource: 'ConnectIdentity', - id: accountAddress, - }), - ); - } - - return { - accountAddress: account.address, - signaturePublicKey: account.connectSignaturePublicKey, - encryptionPublicKey: account.connectEncryptionPublicKey, - accountProof: account.connectAccountProof, - keyProof: account.connectKeyProof, - }; - })(); - - const createIdentity = (params: CreateConnectIdentityParams) => - Effect.fn(function* () { - // Check if identity already exists for this account - const existingIdentity = yield* Effect.tryPromise({ - try: () => - client.account.findFirst({ - where: { - address: params.accountAddress, - }, - }), - catch: (error) => - new DatabaseError({ - operation: 'createIdentity', - cause: error, - }), - }); - - if (existingIdentity) { - yield* new ResourceAlreadyExistsError({ - resource: 'ConnectIdentity', - id: params.accountAddress, - }); - } - - // Create the new identity - yield* Effect.tryPromise({ - try: () => - client.account.create({ - data: { - connectSignerAddress: params.signerAddress, - address: params.accountAddress, - connectAccountProof: params.accountProof, - connectKeyProof: params.keyProof, - connectSignaturePublicKey: params.signaturePublicKey, - connectEncryptionPublicKey: params.encryptionPublicKey, - connectCiphertext: params.ciphertext, - connectNonce: params.nonce, - connectAddress: params.accountAddress, - }, - }), - catch: (error) => - new DatabaseError({ - operation: 'createIdentity', - cause: error, - }), - }); - })(); - - const getIdentityEncrypted = (accountAddress: string) => - Effect.fn(function* () { - const account = yield* Effect.tryPromise({ - try: () => - client.account.findFirst({ + ) => Effect.Effect; +}>() {} + +export const layer = Effect.gen(function* () { + const { use } = yield* DatabaseService.DatabaseService; + + const getByAccountAddress = Effect.fn("getByAccountAddress")(function* (accountAddress: string) { + const account = yield* use((client) => + client.account.findFirst({ + where: { address: accountAddress }, + select: { + address: true, + connectSignaturePublicKey: true, + connectEncryptionPublicKey: true, + connectAccountProof: true, + connectKeyProof: true, + }, + }) + ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, + }))); + + return { + accountAddress: account.address, + signaturePublicKey: account.connectSignaturePublicKey, + encryptionPublicKey: account.connectEncryptionPublicKey, + accountProof: account.connectAccountProof, + keyProof: account.connectKeyProof, + }; + }); + + const createIdentity = Effect.fn("createIdentity")(function* (params: CreateConnectIdentityParams) { + // Check if identity already exists for this account + yield* use((client) => + client.account.findFirst({ + where: { + address: params.accountAddress, + }, + }), + ).pipe(Effect.filterOrFail(Predicate.isNull, () => new ResourceAlreadyExistsError({ + resource: 'ConnectIdentity', + id: params.accountAddress, + }))); + + // Create the new identity + yield* use((client) => + client.account.create({ + data: { + connectSignerAddress: params.signerAddress, + address: params.accountAddress, + connectAccountProof: params.accountProof, + connectKeyProof: params.keyProof, + connectSignaturePublicKey: params.signaturePublicKey, + connectEncryptionPublicKey: params.encryptionPublicKey, + connectCiphertext: params.ciphertext, + connectNonce: params.nonce, + connectAddress: params.accountAddress, + }, + }), + ); + }); + + const getIdentityEncrypted = Effect.fn("getIdentityEncrypted")(function* (accountAddress: string) { + const account = yield* use((client) => + client.account.findFirst({ where: { address: accountAddress }, select: { address: true, @@ -141,34 +110,24 @@ export const makeConnectIdentityService = Effect.fn(function* () { connectNonce: true, }, }), - catch: () => - new ResourceNotFoundError({ - resource: 'ConnectIdentity', - id: accountAddress, - }), - }); - - if (!account) { - return yield* Effect.fail( - new ResourceNotFoundError({ - resource: 'ConnectIdentity', - id: accountAddress, - }), - ); - } + ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, + }))); return { accountAddress: account.address, ciphertext: account.connectCiphertext, nonce: account.connectNonce, }; - })(); + }); return { getByAccountAddress, getIdentityEncrypted, createIdentity, - } as const; -})(); - -export const ConnectIdentityServiceLive = Layer.effect(ConnectIdentityService, makeConnectIdentityService); + }; +}).pipe( + Layer.effect(ConnectIdentityService), + Layer.provide(DatabaseService.layer) +) diff --git a/apps/server-new/src/services/database.ts b/apps/server-new/src/services/database.ts index 6619aa4c..642d2d34 100644 --- a/apps/server-new/src/services/database.ts +++ b/apps/server-new/src/services/database.ts @@ -1,74 +1,49 @@ import { Config, Context, Effect, Layer } from 'effect'; import { PrismaClient } from '../../prisma/generated/client/client'; +import * as Data from "effect/Data"; -/** - * Database service interface - */ -export interface DatabaseService { - readonly client: PrismaClient; -} +export class DatabaseError extends Data.TaggedError('DatabaseError')<{ + readonly cause: unknown; +}> {} /** * Database service tag */ -export const DatabaseService = Context.GenericTag('DatabaseService'); - -/** - * Database service implementation - */ -export const makeDatabaseService = Effect.fn(function* () { - // Get the DATABASE_URL from config - const databaseUrl = yield* Config.string('DATABASE_URL').pipe(Config.withDefault('file:./dev.db')); - - const client = new PrismaClient({ - datasourceUrl: databaseUrl, - }); - - // Connect to database - yield* Effect.tryPromise({ - try: () => client.$connect(), - catch: (error) => new Error(`Failed to connect to database: ${error}`), - }); - - return { - client, - } as const; -}); - -/** - * Database service layer - */ -export const DatabaseServiceLive = Layer.effect(DatabaseService, makeDatabaseService()); +export class DatabaseService extends Context.Tag('DatabaseService')(fn: (client: PrismaClient, signal: AbortSignal) => Promise) => Effect.Effect; +}>() {} /** * Database service layer with resource management */ -export const DatabaseServiceLiveWithCleanup = Layer.scoped( +export const layer = Layer.scoped( DatabaseService, - Effect.fn(function* () { - // Get the DATABASE_URL from config + Effect.gen(function* () { const databaseUrl = yield* Config.string('DATABASE_URL').pipe(Config.withDefault('file:./dev.db')); - - const client = new PrismaClient({ - datasourceUrl: databaseUrl, - }); - - // Connect to database - yield* Effect.tryPromise({ - try: () => client.$connect(), - catch: (error) => new Error(`Failed to connect to database: ${error}`), - }); - - // Register cleanup - yield* Effect.addFinalizer(() => - Effect.tryPromise({ - try: () => client.$disconnect(), - catch: (error) => new Error(`Failed to disconnect from database: ${error}`), - }).pipe(Effect.ignore), - ); + const client = yield* Effect.acquireRelease(Effect.tryPromise({ + try: async () => { + const client = new PrismaClient({ + datasourceUrl: databaseUrl, + }); + + await client.$connect(); + + return client; + }, + catch: (cause) => new DatabaseError({ cause }), + }), (client) => Effect.tryPromise(() => client.$disconnect()).pipe(Effect.ignore)); + + const use = Effect.fn(function* (fn: (client: PrismaClient, signal: AbortSignal) => Promise) { + return yield* Effect.tryPromise({ + try: (signal) => fn(client, signal), + catch: (cause) => new DatabaseError({ cause }), + }) as Effect.Effect; + }) as any; return { client, - } as const; - })(), + use, + }; + }), ); diff --git a/apps/server-new/src/services/identity.ts b/apps/server-new/src/services/identity.ts index 6762e469..60b0eaaf 100644 --- a/apps/server-new/src/services/identity.ts +++ b/apps/server-new/src/services/identity.ts @@ -1,11 +1,12 @@ import { Context, Effect, Layer } from 'effect'; import { ResourceNotFoundError } from '../http/errors.js'; -import { DatabaseService } from './database.js'; +import * as DatabaseService from './database.js'; +import * as Predicate from "effect/Predicate"; export interface IdentityResult { accountAddress: string; ciphertext: string; - nonce?: string; + nonce?: string | undefined; signaturePublicKey: string; encryptionPublicKey: string; accountProof: string; @@ -13,113 +14,55 @@ export interface IdentityResult { appId: string | null; } -export interface IdentityService { +export class IdentityService extends Context.Tag('IdentityService') Effect.Effect; -} - -export const IdentityService = Context.GenericTag('IdentityService'); + ) => Effect.Effect; +}>() {} -export const makeIdentityService = Effect.fn(function* () { - const { client } = yield* DatabaseService; +export const layer = Effect.gen(function* () { + const { use } = yield* DatabaseService.DatabaseService; - const getAppOrConnectIdentity = ( + const getAppOrConnectIdentity = Effect.fn(function* ( params: { accountAddress: string; signaturePublicKey: string } | { accountAddress: string; appId: string }, - ) => - Effect.fn(function* () { - // If we have signaturePublicKey, search by that - if ('signaturePublicKey' in params) { - // First try to find Connect identity - const account = yield* Effect.tryPromise({ - try: () => - client.account.findFirst({ - where: { - address: params.accountAddress, - connectSignaturePublicKey: params.signaturePublicKey, - }, - }), - catch: () => - new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }), - }); - - if (account) { - return { - accountAddress: account.address, - ciphertext: account.connectCiphertext, - nonce: account.connectNonce, - signaturePublicKey: account.connectSignaturePublicKey, - encryptionPublicKey: account.connectEncryptionPublicKey, - accountProof: account.connectAccountProof, - keyProof: account.connectKeyProof, - appId: null, - }; - } - - // Try to find App identity by signaturePublicKey - const appIdentity = yield* Effect.tryPromise({ - try: () => - client.appIdentity.findFirst({ - where: { - accountAddress: params.accountAddress, - signaturePublicKey: params.signaturePublicKey, - }, - }), - catch: () => - new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }), - }); - - if (!appIdentity) { - return yield* Effect.fail( - new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }), - ); - } + ) { + // If we have signaturePublicKey, search by that + if ('signaturePublicKey' in params) { + // First try to find Connect identity + const account = yield* use((client) => + client.account.findFirst({ + where: { + address: params.accountAddress, + connectSignaturePublicKey: params.signaturePublicKey, + }, + }), + ); + if (account !== null) { return { - accountAddress: appIdentity.accountAddress, - ciphertext: appIdentity.ciphertext, - nonce: undefined, - signaturePublicKey: appIdentity.signaturePublicKey, - encryptionPublicKey: appIdentity.encryptionPublicKey, - accountProof: appIdentity.accountProof, - keyProof: appIdentity.keyProof, - appId: appIdentity.appId, + accountAddress: account.address, + ciphertext: account.connectCiphertext, + nonce: account.connectNonce, + signaturePublicKey: account.connectSignaturePublicKey, + encryptionPublicKey: account.connectEncryptionPublicKey, + accountProof: account.connectAccountProof, + keyProof: account.connectKeyProof, + appId: null, }; } - // If we have appId, search by that - const appIdentity = yield* Effect.tryPromise({ - try: () => - client.appIdentity.findFirst({ - where: { - accountAddress: params.accountAddress, - appId: params.appId, - }, - }), - catch: () => - new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }), - }); - - if (!appIdentity) { - return yield* Effect.fail( - new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }), - ); - } + // If we don't have a Connect identity, try to find an App identity + const appIdentity = yield* use((client) => + client.appIdentity.findFirst({ + where: { + accountAddress: params.accountAddress, + signaturePublicKey: params.signaturePublicKey, + }, + }), + ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }))); return { accountAddress: appIdentity.accountAddress, @@ -131,11 +74,37 @@ export const makeIdentityService = Effect.fn(function* () { keyProof: appIdentity.keyProof, appId: appIdentity.appId, }; - })(); + } + + // If we have appId, search by that + const appIdentity = yield* use((client) => + client.appIdentity.findFirst({ + where: { + accountAddress: params.accountAddress, + appId: params.appId, + }, + }), + ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }))); + + return { + accountAddress: appIdentity.accountAddress, + ciphertext: appIdentity.ciphertext, + nonce: undefined, + signaturePublicKey: appIdentity.signaturePublicKey, + encryptionPublicKey: appIdentity.encryptionPublicKey, + accountProof: appIdentity.accountProof, + keyProof: appIdentity.keyProof, + appId: appIdentity.appId, + }; + }); return { getAppOrConnectIdentity, } as const; -})(); - -export const IdentityServiceLive = Layer.effect(IdentityService, makeIdentityService); +}).pipe( + Layer.effect(IdentityService), + Layer.provide(DatabaseService.layer) +) ; diff --git a/apps/server-new/src/services/privy-auth.ts b/apps/server-new/src/services/privy-auth.ts index bcbb1a5f..36e2700e 100644 --- a/apps/server-new/src/services/privy-auth.ts +++ b/apps/server-new/src/services/privy-auth.ts @@ -1,110 +1,96 @@ import { PrivyClient, type Wallet } from '@privy-io/server-auth'; import { Config, Context, Effect, Layer } from 'effect'; import { AuthenticationError, AuthorizationError, PrivyConfigError, PrivyTokenError } from '../http/errors.js'; -import { DatabaseService } from './database.js'; +import * as DatabaseService from './database.js'; +import * as Predicate from "effect/Predicate"; -export interface PrivyAuthService { +export class PrivyAuthService extends Context.Tag('PrivyAuthService') Effect.Effect; readonly isSignerForAccount: ( signerAddress: string, accountAddress: string, - ) => Effect.Effect; + ) => Effect.Effect; readonly authenticateRequest: ( idToken: string | undefined, accountAddress: string, - ) => Effect.Effect; -} + ) => Effect.Effect; +}>() {} -export const PrivyAuthService = Context.GenericTag('PrivyAuthService'); - -export const makePrivyAuthService = Effect.fn(function* () { - const { client } = yield* DatabaseService; +export const layer = Effect.gen(function* () { + const { use } = yield* DatabaseService.DatabaseService; const privyAppId = yield* Config.string('PRIVY_APP_ID').pipe(Config.orElse(() => Config.succeed(''))); const privyAppSecret = yield* Config.string('PRIVY_APP_SECRET').pipe(Config.orElse(() => Config.succeed(''))); - const verifyPrivyToken = (idToken: string) => - Effect.fn(function* () { - if (!privyAppId || !privyAppSecret) { - yield* new PrivyConfigError({ message: 'Missing Privy configuration' }); - } - - const privy = new PrivyClient(privyAppId, privyAppSecret); - - const user = yield* Effect.tryPromise({ - try: () => privy.getUser({ idToken }), - catch: (error) => - new PrivyTokenError({ - message: `Invalid Privy token: ${error}`, - }), + const verifyPrivyToken = Effect.fn("verifyPrivyToken")(function* (idToken: string) { + if (!privyAppId || !privyAppSecret) { + return yield* new PrivyConfigError({ message: 'Missing Privy configuration' }); + } + + const privy = new PrivyClient(privyAppId, privyAppSecret); + + const user = yield* Effect.tryPromise({ + try: () => privy.getUser({ idToken }), + catch: (error) => + new PrivyTokenError({ + message: `Invalid Privy token: ${error}`, + }), + }); + + if (!user) { + return yield* Effect.fail(new PrivyTokenError({ message: 'Invalid Privy user' })); + } + + const wallet = user.linkedAccounts.find( + (account) => account.type === 'wallet' && account.walletClientType === 'privy', + ) as Wallet | undefined; + + if (!wallet) { + return yield* Effect.fail(new PrivyTokenError({ message: 'No Privy wallet found' })); + } + + return wallet.address; + }); + + const isSignerForAccount = Effect.fn("isSignerForAccount")(function* (signerAddress: string, accountAddress: string) { + const account = yield* use((client) => + client.account.findUnique({ + where: { + address: accountAddress, + }, + }), + ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new AuthorizationError({ + message: 'Account not found', + accountAddress, + }))); + + const isAuthorized = account.connectSignerAddress === signerAddress; + if (!isAuthorized) { + return yield* new AuthorizationError({ + message: 'Signer not authorized for account', + accountAddress, }); + } - if (!user) { - return yield* Effect.fail(new PrivyTokenError({ message: 'Invalid Privy user' })); - } - - const wallet = user.linkedAccounts.find( - (account) => account.type === 'wallet' && account.walletClientType === 'privy', - ) as Wallet | undefined; - - if (!wallet) { - return yield* Effect.fail(new PrivyTokenError({ message: 'No Privy wallet found' })); - } - - return wallet.address; - })(); - - const isSignerForAccount = (signerAddress: string, accountAddress: string) => - Effect.fn(function* () { - const account = yield* Effect.tryPromise({ - try: () => - client.account.findUnique({ - where: { - address: accountAddress, - }, - }), - catch: () => - new AuthorizationError({ - message: 'Failed to verify signer', - accountAddress, - }), - }); + return true; + }); + + const authenticateRequest = Effect.fn("authenticateRequest")(function* (idToken: string | undefined, accountAddress: string) { + if (!idToken) { + return yield* Effect.fail(new AuthenticationError({ message: 'No Privy ID token provided' })); + } - if (!account) { - return yield* Effect.fail( - new AuthorizationError({ - message: 'Account not found', - accountAddress, - }), - ); - } - - const isAuthorized = account.connectSignerAddress === signerAddress; - if (!isAuthorized) { - yield* new AuthorizationError({ - message: 'Signer not authorized for account', - accountAddress, - }); - } - - return true; - })(); - - const authenticateRequest = (idToken: string | undefined, accountAddress: string) => - Effect.fn(function* () { - if (!idToken) { - return yield* Effect.fail(new AuthenticationError({ message: 'No Privy ID token provided' })); - } - - const signerAddress = yield* verifyPrivyToken(idToken); - yield* isSignerForAccount(signerAddress, accountAddress); - })(); + const signerAddress = yield* verifyPrivyToken(idToken); + yield* isSignerForAccount(signerAddress, accountAddress); + }); return { verifyPrivyToken, isSignerForAccount, authenticateRequest, - } as const; -})(); + }; +}).pipe( + Layer.effect(PrivyAuthService), + Layer.provide(DatabaseService.layer) +); -export const PrivyAuthServiceLive = Layer.effect(PrivyAuthService, makePrivyAuthService); diff --git a/apps/server-new/src/services/space-inbox.ts b/apps/server-new/src/services/space-inbox.ts index 7a57dba5..235ccc3f 100644 --- a/apps/server-new/src/services/space-inbox.ts +++ b/apps/server-new/src/services/space-inbox.ts @@ -1,44 +1,43 @@ import { Inboxes, type Messages, type SpaceEvents } from '@graphprotocol/hypergraph'; -import { Context, Effect, Layer } from 'effect'; -import { AuthorizationError, DatabaseError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; -import { DatabaseService } from './database.js'; -import { IdentityService } from './identity.js'; +import { AuthorizationError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; +import * as DatabaseService from './database.js'; +import * as IdentityService from './identity.js'; +import * as Predicate from "effect/Predicate"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; export interface SpaceInboxResult { inboxId: string; isPublic: boolean; authPolicy: Inboxes.InboxSenderAuthPolicy; - encryptionPublicKey: string | null; + encryptionPublicKey: string; creationEvent: SpaceEvents.CreateSpaceInboxEvent; } -export interface SpaceInboxService { - readonly listPublicSpaceInboxes: (params: { spaceId: string }) => Effect.Effect; +export class SpaceInboxService extends Context.Tag('SpaceInboxService') Effect.Effect; readonly getSpaceInbox: (params: { spaceId: string; inboxId: string; - }) => Effect.Effect; + }) => Effect.Effect; readonly postSpaceInboxMessage: (params: { spaceId: string; inboxId: string; message: Messages.RequestCreateSpaceInboxMessage; }) => Effect.Effect< Messages.InboxMessage, - ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseError + ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseService.DatabaseError >; -} - -export const SpaceInboxService = Context.GenericTag('SpaceInboxService'); +}>() {} -export const makeSpaceInboxService = Effect.fn(function* () { - const { client } = yield* DatabaseService; - const identityService = yield* IdentityService; +export const layer = Effect.gen(function* () { + const { use } = yield* DatabaseService.DatabaseService; + const { getAppOrConnectIdentity } = yield* IdentityService.IdentityService; - const listPublicSpaceInboxes = ({ spaceId }: { spaceId: string }) => - Effect.fn(function* () { - const inboxes = yield* Effect.tryPromise({ - try: () => - client.spaceInbox.findMany({ + const listPublicSpaceInboxes = Effect.fn("listPublicSpaceInboxes")(function* ({ spaceId }: { spaceId: string }) { + const inboxes = yield* use((client) => + client.spaceInbox.findMany({ where: { spaceId, isPublic: true }, select: { id: true, @@ -52,12 +51,7 @@ export const makeSpaceInboxService = Effect.fn(function* () { }, }, }), - catch: (error) => - new DatabaseError({ - operation: 'listPublicSpaceInboxes', - cause: error, - }), - }); + ); return inboxes.map((inbox) => ({ inboxId: inbox.id, @@ -66,41 +60,28 @@ export const makeSpaceInboxService = Effect.fn(function* () { encryptionPublicKey: inbox.encryptionPublicKey, creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, })); - })(); - - const getSpaceInbox = ({ spaceId, inboxId }: { spaceId: string; inboxId: string }) => - Effect.fn(function* () { - const inbox = yield* Effect.tryPromise({ - try: () => - client.spaceInbox.findUnique({ - where: { id: inboxId, spaceId }, - select: { - id: true, - isPublic: true, - authPolicy: true, - encryptionPublicKey: true, - spaceEvent: { - select: { - event: true, - }, + }); + + const getSpaceInbox = Effect.fn("getSpaceInbox")(function* ({ spaceId, inboxId }: { spaceId: string; inboxId: string }) { + const inbox = yield* use((client) => + client.spaceInbox.findUnique({ + where: { id: inboxId, spaceId }, + select: { + id: true, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + spaceEvent: { + select: { + event: true, }, }, - }), - catch: (error) => - new DatabaseError({ - operation: 'getSpaceInbox', - cause: error, - }), - }); - - if (!inbox) { - return yield* Effect.fail( - new ResourceNotFoundError({ - resource: 'SpaceInbox', - id: inboxId, - }), - ); - } + }, + }), + ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ + resource: 'SpaceInbox', + id: inboxId, + }))); return { inboxId: inbox.id, @@ -109,9 +90,9 @@ export const makeSpaceInboxService = Effect.fn(function* () { encryptionPublicKey: inbox.encryptionPublicKey, creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, }; - })(); + }); - const postSpaceInboxMessage = ({ + const postSpaceInboxMessage = Effect.fn(function* ({ spaceId, inboxId, message, @@ -119,8 +100,7 @@ export const makeSpaceInboxService = Effect.fn(function* () { spaceId: string; inboxId: string; message: Messages.RequestCreateSpaceInboxMessage; - }) => - Effect.fn(function* () { + }) { // First get the inbox to validate it exists and get auth policy const spaceInbox = yield* getSpaceInbox({ spaceId, inboxId }); @@ -181,8 +161,7 @@ export const makeSpaceInboxService = Effect.fn(function* () { }); // Check if this public key corresponds to a user's identity - const authorIdentity = yield* identityService - .getAppOrConnectIdentity({ + const authorIdentity = yield* getAppOrConnectIdentity({ accountAddress: message.authorAccountAddress, signaturePublicKey: authorPublicKey, }) @@ -208,9 +187,8 @@ export const makeSpaceInboxService = Effect.fn(function* () { } // Create the message in the database - const createdMessage = yield* Effect.tryPromise({ - try: () => - client.$transaction(async (prisma) => { + const createdMessage = yield* use((client) => + client.$transaction(async (prisma) => { // Double-check the inbox exists and belongs to the correct space const inbox = await prisma.spaceInbox.findUnique({ where: { id: inboxId }, @@ -251,24 +229,21 @@ export const makeSpaceInboxService = Effect.fn(function* () { createdAt: created.createdAt, } as Messages.InboxMessage; }), - catch: (error) => - new DatabaseError({ - operation: 'postSpaceInboxMessage', - cause: error, - }), - }); + ) // TODO: Broadcast the message (WebSocket functionality would go here) // broadcastSpaceInboxMessage({ spaceId, inboxId, message: createdMessage }); return createdMessage; - })(); + }); return { listPublicSpaceInboxes, getSpaceInbox, postSpaceInboxMessage, } as const; -})(); - -export const SpaceInboxServiceLive = Layer.effect(SpaceInboxService, makeSpaceInboxService); +}).pipe( + Layer.effect(SpaceInboxService), + Layer.provide(DatabaseService.layer), + Layer.provide(IdentityService.layer) +); diff --git a/apps/server-new/src/services/spaces.ts b/apps/server-new/src/services/spaces.ts index 991cdb36..2210f59d 100644 --- a/apps/server-new/src/services/spaces.ts +++ b/apps/server-new/src/services/spaces.ts @@ -1,8 +1,7 @@ import { Identity, type Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph'; import { Context, Effect, Layer } from 'effect'; -import { DatabaseError, type ValidationError } from '../http/errors.js'; -import { DatabaseService } from './database.js'; -import { IdentityService } from './identity.js'; +import * as DatabaseService from './database.js'; +import * as IdentityService from './identity.js'; export interface SpaceInfo { id: string; @@ -39,54 +38,44 @@ export interface AddAppIdentityToSpacesParams { spacesInput: Messages.RequestConnectAddAppIdentityToSpaces['spacesInput']; } -export interface SpacesService { - readonly listByAccount: (accountAddress: string) => Effect.Effect; - readonly createSpace: (params: CreateSpaceParams) => Effect.Effect<{ id: string }, ValidationError | DatabaseError>; - readonly addAppIdentityToSpaces: (params: AddAppIdentityToSpacesParams) => Effect.Effect; -} - -export const SpacesService = Context.GenericTag('SpacesService'); - -export const makeSpacesService = Effect.fn(function* () { - const { client } = yield* DatabaseService; - const identityService = yield* IdentityService; - - const listByAccount = (accountAddress: string) => - Effect.fn(function* () { - const spaces = yield* Effect.tryPromise({ - try: () => - client.space.findMany({ - where: { - members: { - some: { - address: accountAddress, - }, - }, +export class SpacesService extends Context.Tag('SpacesService') Effect.Effect; + readonly createSpace: (params: CreateSpaceParams) => Effect.Effect<{ id: string }, SpaceEvents.ApplyError | DatabaseService.DatabaseError>; + readonly addAppIdentityToSpaces: (params: AddAppIdentityToSpacesParams) => Effect.Effect; +}>() {} + +export const layer = Effect.gen(function* () { + const { use } = yield* DatabaseService.DatabaseService; + const { getAppOrConnectIdentity } = yield* IdentityService.IdentityService; + + const listByAccount = Effect.fn(function* (accountAddress: string) { + const spaces = yield* use((client) => + client.space.findMany({ + where: { + members: { + some: { + address: accountAddress, + }, + }, + }, + include: { + appIdentities: { + select: { + address: true, + appId: true, }, + }, + keys: { include: { - appIdentities: { - select: { - address: true, - appId: true, - }, - }, - keys: { - include: { - keyBoxes: { - where: { - accountAddress, - }, - }, + keyBoxes: { + where: { + accountAddress, }, }, }, - }), - catch: (error) => - new DatabaseError({ - operation: 'listSpacesByAccount', - cause: error, - }), - }); + }, + }, + })); return spaces.map((space) => ({ id: space.id, @@ -108,29 +97,25 @@ export const makeSpacesService = Effect.fn(function* () { authorPublicKey: key.keyBoxes[0].authorPublicKey, })), })); - })(); + }); - const createSpace = (params: CreateSpaceParams) => - Effect.fn(function* () { + const createSpace = Effect.fn(function* (params: CreateSpaceParams) { const { accountAddress, event, keyBox, infoContent, infoSignatureHex, infoSignatureRecovery, name } = params; // Create the getVerifiedIdentity function for space event validation - const getVerifiedIdentity = (accountAddressToFetch: string, publicKey: string) => { + const getVerifiedIdentity = Effect.fn(function* (accountAddressToFetch: string, publicKey: string) { // applySpaceEvent is only allowed to be called by the account that is applying the event if (accountAddressToFetch !== accountAddress) { - return Effect.fail(new Identity.InvalidIdentityError()); + return yield* new Identity.InvalidIdentityError(); } - return Effect.fn(function* () { - const identity = yield* identityService - .getAppOrConnectIdentity({ - accountAddress: accountAddressToFetch, - signaturePublicKey: publicKey, - }) - .pipe(Effect.mapError(() => new Identity.InvalidIdentityError())); - return identity; - })(); - }; + const identity = yield* getAppOrConnectIdentity({ + accountAddress: accountAddressToFetch, + signaturePublicKey: publicKey, + }).pipe(Effect.mapError(() => new Identity.InvalidIdentityError())); + + return identity; + }); // Validate the space event const result = yield* SpaceEvents.applyEvent({ @@ -142,9 +127,8 @@ export const makeSpacesService = Effect.fn(function* () { const keyBoxId = `${keyBox.id}-${accountAddress}`; // Create the space in the database - const spaceEvent = yield* Effect.tryPromise({ - try: () => - client.spaceEvent.create({ + const spaceEvent = yield* use((client) => + client.spaceEvent.create({ data: { event: JSON.stringify(event), id: event.transaction.id, @@ -185,23 +169,16 @@ export const makeSpacesService = Effect.fn(function* () { }, }, }), - catch: (error) => - new DatabaseError({ - operation: 'createSpace', - cause: error, - }), - }); + ); return { id: spaceEvent.id }; - })(); + }); - const addAppIdentityToSpaces = (params: AddAppIdentityToSpacesParams) => - Effect.fn(function* () { + const addAppIdentityToSpaces = Effect.fn(function* (params: AddAppIdentityToSpacesParams) { const { appIdentityAddress, accountAddress, spacesInput } = params; - yield* Effect.tryPromise({ - try: () => - client.$transaction(async (prisma) => { + yield* use((client) => + client.$transaction(async (prisma) => { // Update app identity to connect it to spaces await prisma.appIdentity.update({ where: { @@ -236,19 +213,16 @@ export const makeSpacesService = Effect.fn(function* () { data: keyBoxes, }); }), - catch: (error) => - new DatabaseError({ - operation: 'addAppIdentityToSpaces', - cause: error, - }), - }); - })(); + ); + }); return { listByAccount, createSpace, addAppIdentityToSpaces, } as const; -})(); - -export const SpacesServiceLive = Layer.effect(SpacesService, makeSpacesService); +}).pipe( + Layer.effect(SpacesService), + Layer.provide(DatabaseService.layer), + Layer.provide(IdentityService.layer) +); diff --git a/apps/server-new/tsconfig.app.json b/apps/server-new/tsconfig.app.json deleted file mode 100644 index 706602e6..00000000 --- a/apps/server-new/tsconfig.app.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "tsup.config.ts"], - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - "composite": false, - "incremental": false, - "declaration": false, - "declarationMap": false, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "exactOptionalPropertyTypes": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - } -} \ No newline at end of file diff --git a/apps/server-new/tsconfig.json b/apps/server-new/tsconfig.json index 706602e6..1f557c70 100644 --- a/apps/server-new/tsconfig.json +++ b/apps/server-new/tsconfig.json @@ -24,6 +24,6 @@ "exactOptionalPropertyTypes": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, } -} \ No newline at end of file +} diff --git a/apps/server-new/tsconfig.node.json b/apps/server-new/tsconfig.node.json deleted file mode 100644 index d462d771..00000000 --- a/apps/server-new/tsconfig.node.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["vitest.config.ts"], - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - "composite": false, - "incremental": false, - "declaration": false, - "declarationMap": false, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "exactOptionalPropertyTypes": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - } -} \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 2f131960..d5fb1ac5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -19,7 +19,7 @@ "@privy-io/server-auth": "^1.31.1", "body-parser": "^2.2.0", "cors": "^2.8.5", - "effect": "^3.17.8", + "effect": "^3.17.9", "express": "^5.1.0", "prisma": "^6.14.0", "siwe": "^3.0.0", diff --git a/apps/template-vite-react/package.json b/apps/template-vite-react/package.json index 4c3abbf0..da916dbd 100644 --- a/apps/template-vite-react/package.json +++ b/apps/template-vite-react/package.json @@ -22,7 +22,7 @@ "@tanstack/react-router": "^1.131.27", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "effect": "^3.17.8", + "effect": "^3.17.9", "lucide-react": "^0.541.0", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/package.json b/package.json index bdbff3ca..e42c647c 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "@babel/core": "^7.28.3", "@biomejs/biome": "^2.2.0", "@changesets/cli": "^2.29.6", - "@graphprotocol/grc-20": "^0.24.1", + "@effect/language-service": "^0.36.0", "@effect/vitest": "^0.25.1", + "@graphprotocol/grc-20": "^0.24.1", "babel-plugin-annotate-pure-calls": "^0.5.0", "glob": "^11.0.3", "pkg-pr-new": "^0.0.56", diff --git a/packages/create-hypergraph/package.json b/packages/create-hypergraph/package.json index 8eb85e3f..2ba4bf42 100644 --- a/packages/create-hypergraph/package.json +++ b/packages/create-hypergraph/package.json @@ -55,13 +55,12 @@ "homepage": "https://github.com/graphprotocol/hypergraph/tree/main/packages/create-hypergraph-app#readme", "devDependencies": { "@effect/cli": "^0.69.2", - "@effect/language-service": "^0.35.2", "@effect/platform": "^0.90.6", "@effect/platform-node": "^0.96.0", "@effect/printer-ansi": "^0.45.0", "@effect/vitest": "^0.25.1", "@types/node": "^24.3.0", - "effect": "^3.17.8", + "effect": "^3.17.9", "execa": "^9.6.0", "tsdown": "^0.14.1", "tsx": "^4.20.4" diff --git a/packages/hypergraph-react/package.json b/packages/hypergraph-react/package.json index bad7014d..945a9a49 100644 --- a/packages/hypergraph-react/package.json +++ b/packages/hypergraph-react/package.json @@ -50,7 +50,7 @@ "@graphprotocol/grc-20": "^0.24.1", "@noble/hashes": "^1.8.0", "@tanstack/react-query": "^5.85.5", - "effect": "^3.17.8", + "effect": "^3.17.9", "graphql-request": "^7.2.0", "siwe": "^3.0.0", "uuid": "^11.1.0", diff --git a/packages/hypergraph/package.json b/packages/hypergraph/package.json index 6f0d6e0f..969397cf 100644 --- a/packages/hypergraph/package.json +++ b/packages/hypergraph/package.json @@ -80,7 +80,7 @@ "@serenity-kit/noble-sodium": "^0.2.1", "@xstate/store": "^3.9.2", "bs58check": "^4.0.0", - "effect": "^3.17.8", + "effect": "^3.17.9", "open": "^10.2.0", "permissionless": "^0.2.47", "siwe": "^3.0.0", diff --git a/packages/hypergraph/src/identity/types.ts b/packages/hypergraph/src/identity/types.ts index 19c0ae5b..ce0e6b0c 100644 --- a/packages/hypergraph/src/identity/types.ts +++ b/packages/hypergraph/src/identity/types.ts @@ -1,4 +1,5 @@ -import { Schema } from 'effect'; +import * as Schema from "effect/Schema"; +import * as Data from "effect/Data"; export type Storage = { getItem: (key: string) => string | null; @@ -39,6 +40,4 @@ export type PublicIdentity = { signaturePublicKey: string; }; -export class InvalidIdentityError { - readonly _tag = 'InvalidIdentityError'; -} +export class InvalidIdentityError extends Data.TaggedError('InvalidIdentityError') {} diff --git a/packages/hypergraph/src/space-events/apply-event.ts b/packages/hypergraph/src/space-events/apply-event.ts index cbf1cefd..147455b2 100644 --- a/packages/hypergraph/src/space-events/apply-event.ts +++ b/packages/hypergraph/src/space-events/apply-event.ts @@ -55,7 +55,7 @@ export const applyEvent = ({ return Effect.gen(function* () { const identity = yield* getVerifiedIdentity(event.author.accountAddress, authorPublicKey); if (authorPublicKey !== identity.signaturePublicKey) { - yield* Effect.fail(new VerifySignatureError()); + return yield* Effect.fail(new VerifySignatureError()); } let id = ''; @@ -78,7 +78,7 @@ export const applyEvent = ({ if (event.transaction.type === 'accept-invitation') { // is already a member if (members[event.author.accountAddress] !== undefined) { - yield* Effect.fail(new InvalidEventError()); + return yield* Effect.fail(new InvalidEventError()); } // find the invitation @@ -86,10 +86,9 @@ export const applyEvent = ({ ([, invitation]) => invitation.inviteeAccountAddress === event.author.accountAddress, ); if (!result) { - yield* Effect.fail(new InvalidEventError()); + return yield* Effect.fail(new InvalidEventError()); } - // @ts-expect-error type issue? we checked that result is not undefined before const [id, invitation] = result; members[invitation.inviteeAccountAddress] = { @@ -103,7 +102,7 @@ export const applyEvent = ({ } else { // check if the author is an admin if (members[event.author.accountAddress]?.role !== 'admin') { - yield* Effect.fail(new InvalidEventError()); + return yield* Effect.fail(new InvalidEventError()); } if (event.transaction.type === 'delete-space') { @@ -112,11 +111,11 @@ export const applyEvent = ({ invitations = {}; } else if (event.transaction.type === 'create-invitation') { if (members[event.transaction.inviteeAccountAddress] !== undefined) { - yield* Effect.fail(new InvalidEventError()); + return yield* Effect.fail(new InvalidEventError()); } for (const invitation of Object.values(invitations)) { if (invitation.inviteeAccountAddress === event.transaction.inviteeAccountAddress) { - yield* Effect.fail(new InvalidEventError()); + return yield* Effect.fail(new InvalidEventError()); } } @@ -125,7 +124,7 @@ export const applyEvent = ({ }; } else if (event.transaction.type === 'create-space-inbox') { if (inboxes[event.transaction.inboxId] !== undefined) { - yield* Effect.fail(new InvalidEventError()); + return yield* Effect.fail(new InvalidEventError()); } inboxes[event.transaction.inboxId] = { inboxId: event.transaction.inboxId, @@ -136,7 +135,7 @@ export const applyEvent = ({ }; } else { // state is required for all events except create-space - yield* Effect.fail(new InvalidEventError()); + return yield* Effect.fail(new InvalidEventError()); } } } diff --git a/packages/typesync-studio/package.json b/packages/typesync-studio/package.json index c99006e0..fd293ca5 100644 --- a/packages/typesync-studio/package.json +++ b/packages/typesync-studio/package.json @@ -26,7 +26,7 @@ "@tanstack/react-router": "^1.131.27", "@tanstack/react-router-devtools": "^1.131.27", "@tanstack/router-plugin": "^1.131.27", - "effect": "3.17.8", + "effect": "3.17.9", "graphql": "^16.11.0", "graphql-request": "^7.2.0", "lodash.debounce": "^4.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e895dcf8..4989c1f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@changesets/cli': specifier: ^2.29.6 version: 2.29.6(@types/node@24.3.0) + '@effect/language-service': + specifier: ^0.36.0 + version: 0.36.0 '@effect/vitest': specifier: ^0.25.1 version: 0.25.1(effect@3.17.9)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) @@ -78,8 +81,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 effect: - specifier: ^3.17.8 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 graphql-request: specifier: ^7.2.0 version: 7.2.0(graphql@16.11.0) @@ -181,8 +184,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 effect: - specifier: ^3.17.8 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 framer-motion: specifier: ^12.23.12 version: 12.23.12(@emotion/is-prop-valid@1.2.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -318,8 +321,8 @@ importers: specifier: ^2.8.5 version: 2.8.5 effect: - specifier: ^3.17.8 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 express: specifier: ^5.1.0 version: 5.1.0 @@ -519,8 +522,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 effect: - specifier: ^3.17.8 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 lucide-react: specifier: ^0.541.0 version: 0.541.0(react@19.1.1) @@ -624,28 +627,25 @@ importers: devDependencies: '@effect/cli': specifier: ^0.69.2 - version: 0.69.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8))(@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8))(effect@3.17.8) - '@effect/language-service': - specifier: ^0.35.2 - version: 0.35.2 + version: 0.69.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9))(@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9))(effect@3.17.9) '@effect/platform': specifier: ^0.90.6 - version: 0.90.6(effect@3.17.8) + version: 0.90.6(effect@3.17.9) '@effect/platform-node': specifier: ^0.96.0 - version: 0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) + version: 0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10) '@effect/printer-ansi': specifier: ^0.45.0 - version: 0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8) + version: 0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9) '@effect/vitest': specifier: ^0.25.1 - version: 0.25.1(effect@3.17.8)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 0.25.1(effect@3.17.9)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) '@types/node': specifier: ^24.3.0 version: 24.3.0 effect: - specifier: ^3.17.8 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 execa: specifier: ^9.6.0 version: 9.6.0 @@ -667,22 +667,22 @@ importers: version: 2.2.0 '@effect/cli': specifier: ^0.69.2 - version: 0.69.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8))(@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8))(effect@3.17.8) + version: 0.69.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9))(@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9))(effect@3.17.9) '@effect/experimental': specifier: ^0.54.6 - version: 0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) + version: 0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) '@effect/platform': specifier: ^0.90.6 - version: 0.90.6(effect@3.17.8) + version: 0.90.6(effect@3.17.9) '@effect/platform-node': specifier: ^0.96.0 - version: 0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) + version: 0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10) '@effect/printer': specifier: ^0.45.0 - version: 0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8) + version: 0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9) '@effect/printer-ansi': specifier: ^0.45.0 - version: 0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8) + version: 0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9) '@graphprotocol/grc-20': specifier: ^0.24.1 version: 0.24.1(bufferutil@4.0.9)(ox@0.6.7(typescript@5.9.2)(zod@3.25.76))(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -711,8 +711,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 effect: - specifier: ^3.17.8 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 open: specifier: ^10.2.0 version: 10.2.0 @@ -731,7 +731,7 @@ importers: devDependencies: '@effect/vitest': specifier: ^0.25.1 - version: 0.25.1(effect@3.17.8)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 0.25.1(effect@3.17.9)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) '@types/node': specifier: ^24.3.0 version: 24.3.0 @@ -770,8 +770,8 @@ importers: specifier: ^5.85.5 version: 5.85.5(react@19.1.1) effect: - specifier: ^3.17.8 - version: 3.17.8 + specifier: ^3.17.9 + version: 3.17.9 graphql-request: specifier: ^7.2.0 version: 7.2.0(graphql@16.11.0) @@ -853,8 +853,8 @@ importers: specifier: ^1.131.27 version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(webpack@5.101.0) effect: - specifier: 3.17.8 - version: 3.17.8 + specifier: 3.17.9 + version: 3.17.9 graphql: specifier: ^16.11.0 version: 16.11.0 @@ -2349,8 +2349,8 @@ packages: lmdb: optional: true - '@effect/language-service@0.35.2': - resolution: {integrity: sha512-J7GbtthuYeruD4kYUHn3QEZtbl9v7OX9+ElD20mDBGBMA+Q6W4KnVMxZc+yDvKQBBYvfXImVUSzBbXzbrZJpyg==} + '@effect/language-service@0.36.0': + resolution: {integrity: sha512-KoE5e+7vhcd29fBTDBvttWHv1z1KPg9Ji2DyhD0hETBaoM9IsFymboD6WqpDoGH1QZRr6CmTD3DfaZUs8K6Mrg==} hasBin: true '@effect/opentelemetry@0.56.4': @@ -2449,12 +2449,21 @@ packages: '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + '@emnapi/runtime@1.4.5': resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emnapi/wasi-threads@1.0.4': resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -2982,8 +2991,8 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@gerrit0/mini-shiki@3.11.0': - resolution: {integrity: sha512-ooCDMAOKv71O7MszbXjSQGcI6K5T6NKlemQZOBHLq7Sv/oXCRfYbZ7UgbzFdl20lSXju6Juds4I3y30R6rHA4Q==} + '@gerrit0/mini-shiki@3.12.0': + resolution: {integrity: sha512-CF1vkfe2ViPtmoFEvtUWilEc4dOCiFzV8+J7/vEISSsslKQ97FjeTPNMCqUhZEiKySmKRgK3UO/CxtkyOp7DvA==} '@graphprotocol/grc-20@0.24.1': resolution: {integrity: sha512-oP8E4GmqSIM3hYzRTIhPtUS0Szg1wdzo1AUtLTTZazWoUycMm+K2FgfwANpN7q3CzOyuVtBeEhmdogw74q/0jw==} @@ -7264,9 +7273,6 @@ packages: effect@3.16.12: resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} - effect@3.17.8: - resolution: {integrity: sha512-3X2DahqmaTwDdvdYuX/MFhYA4srjO21NodMWhCXPMRK/3IQlByJyNFpZrXCWfnMrlr6DsLI+EgI3rqqAQtWrIA==} - effect@3.17.9: resolution: {integrity: sha512-Nkkn9n1zhy30Dq0MpQatDCH7nfYnOIiebkOHNxmmvoVnEDKCto+2ZwDDWFGzcN/ojwfqjRXWGC9Lo91K5kwZCg==} @@ -15285,24 +15291,16 @@ snapshots: - uglify-js - webpack-cli - '@effect/cli@0.69.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8))(@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8))(effect@3.17.8)': + '@effect/cli@0.69.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9))(@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9))(effect@3.17.9)': dependencies: - '@effect/platform': 0.90.6(effect@3.17.8) - '@effect/printer': 0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8) - '@effect/printer-ansi': 0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8) - effect: 3.17.8 + '@effect/platform': 0.90.6(effect@3.17.9) + '@effect/printer': 0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9) + '@effect/printer-ansi': 0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9) + effect: 3.17.9 ini: 4.1.3 toml: 3.0.0 yaml: 2.8.1 - '@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8)': - dependencies: - '@effect/platform': 0.90.6(effect@3.17.8) - '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@effect/workflow': 0.1.2(effect@3.17.8) - effect: 3.17.8 - '@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9)': dependencies: '@effect/platform': 0.90.6(effect@3.17.9) @@ -15311,19 +15309,13 @@ snapshots: '@effect/workflow': 0.1.2(effect@3.17.9) effect: 3.17.9 - '@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8)': - dependencies: - '@effect/platform': 0.90.6(effect@3.17.8) - effect: 3.17.8 - uuid: 11.1.0 - '@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9)': dependencies: '@effect/platform': 0.90.6(effect@3.17.9) effect: 3.17.9 uuid: 11.1.0 - '@effect/language-service@0.35.2': {} + '@effect/language-service@0.36.0': {} '@effect/opentelemetry@0.56.4(@effect/platform@0.90.6(effect@3.17.9))(@opentelemetry/semantic-conventions@1.36.0)(effect@3.17.9)': dependencies: @@ -15331,20 +15323,6 @@ snapshots: '@opentelemetry/semantic-conventions': 1.36.0 effect: 3.17.9 - '@effect/platform-node-shared@0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': - dependencies: - '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) - '@effect/platform': 0.90.6(effect@3.17.8) - '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@parcel/watcher': 2.5.1 - effect: 3.17.8 - multipasta: 0.2.7 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@effect/platform-node-shared@0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10)': dependencies: '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9) @@ -15359,21 +15337,6 @@ snapshots: - bufferutil - utf-8-validate - '@effect/platform-node@0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10)': - dependencies: - '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8) - '@effect/platform': 0.90.6(effect@3.17.8) - '@effect/platform-node-shared': 0.49.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/workflow@0.1.2(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(bufferutil@4.0.9)(effect@3.17.8)(utf-8-validate@5.0.10) - '@effect/rpc': 0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@effect/sql': 0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - effect: 3.17.8 - mime: 3.0.0 - undici: 7.15.0 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@effect/platform-node@0.96.0(@effect/cluster@0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(bufferutil@4.0.9)(effect@3.17.9)(utf-8-validate@5.0.10)': dependencies: '@effect/cluster': 0.37.2(@effect/platform@0.90.6(effect@3.17.9))(@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/workflow@0.1.2(effect@3.17.9))(effect@3.17.9) @@ -15389,13 +15352,6 @@ snapshots: - bufferutil - utf-8-validate - '@effect/platform@0.90.6(effect@3.17.8)': - dependencies: - effect: 3.17.8 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.5 - multipasta: 0.2.7 - '@effect/platform@0.90.6(effect@3.17.9)': dependencies: effect: 3.17.9 @@ -15403,35 +15359,22 @@ snapshots: msgpackr: 1.11.5 multipasta: 0.2.7 - '@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8)': - dependencies: - '@effect/printer': 0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8) - '@effect/typeclass': 0.31.10(effect@3.17.8) - effect: 3.17.8 - - '@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.8))(effect@3.17.8)': + '@effect/printer-ansi@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9)': dependencies: - '@effect/typeclass': 0.31.10(effect@3.17.8) - effect: 3.17.8 + '@effect/printer': 0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9) + '@effect/typeclass': 0.31.10(effect@3.17.9) + effect: 3.17.9 - '@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8)': + '@effect/printer@0.45.0(@effect/typeclass@0.31.10(effect@3.17.9))(effect@3.17.9)': dependencies: - '@effect/platform': 0.90.6(effect@3.17.8) - effect: 3.17.8 + '@effect/typeclass': 0.31.10(effect@3.17.9) + effect: 3.17.9 '@effect/rpc@0.61.4(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9)': dependencies: '@effect/platform': 0.90.6(effect@3.17.9) effect: 3.17.9 - '@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8))(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8)': - dependencies: - '@effect/experimental': 0.54.6(@effect/platform@0.90.6(effect@3.17.8))(effect@3.17.8) - '@effect/platform': 0.90.6(effect@3.17.8) - '@opentelemetry/semantic-conventions': 1.36.0 - effect: 3.17.8 - uuid: 11.1.0 - '@effect/sql@0.44.0(@effect/experimental@0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9))(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9)': dependencies: '@effect/experimental': 0.54.6(@effect/platform@0.90.6(effect@3.17.9))(effect@3.17.9) @@ -15440,13 +15383,13 @@ snapshots: effect: 3.17.9 uuid: 11.1.0 - '@effect/typeclass@0.31.10(effect@3.17.8)': + '@effect/typeclass@0.31.10(effect@3.17.9)': dependencies: - effect: 3.17.8 + effect: 3.17.9 - '@effect/vitest@0.25.1(effect@3.17.8)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))': + '@effect/vitest@0.25.1(effect@3.17.9)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))': dependencies: - effect: 3.17.8 + effect: 3.17.9 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) '@effect/vitest@0.25.1(effect@3.17.9)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': @@ -15454,10 +15397,6 @@ snapshots: effect: 3.17.9 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - '@effect/workflow@0.1.2(effect@3.17.8)': - dependencies: - effect: 3.17.8 - '@effect/workflow@0.1.2(effect@3.17.9)': dependencies: effect: 3.17.9 @@ -15468,16 +15407,32 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.4.5': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.0.4': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.27.1 @@ -16006,7 +15961,7 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@gerrit0/mini-shiki@3.11.0': + '@gerrit0/mini-shiki@3.12.0': dependencies: '@shikijs/engine-oniguruma': 3.12.0 '@shikijs/langs': 3.12.0 @@ -16855,8 +16810,8 @@ snapshots: '@napi-rs/wasm-runtime@1.0.3': dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.4.5 + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 '@tybys/wasm-util': 0.10.0 optional: true @@ -19437,14 +19392,6 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -21715,11 +21662,6 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - effect@3.17.8: - dependencies: - '@standard-schema/spec': 1.0.0 - fast-check: 3.23.2 - effect@3.17.9: dependencies: '@standard-schema/spec': 1.0.0 @@ -27544,7 +27486,7 @@ snapshots: typedoc@0.28.10(typescript@5.9.2): dependencies: - '@gerrit0/mini-shiki': 3.11.0 + '@gerrit0/mini-shiki': 3.12.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 @@ -28113,7 +28055,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 diff --git a/tsconfig.base.json b/tsconfig.base.json index 493bc634..4330b0ab 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -41,6 +41,10 @@ "@graphprotocol/hypergraph/*": ["./packages/hypergraph/src/*.js"], "@graphprotocol/hypergraph-react": ["./packages/hypergraph-react/src/index.js"], "@graphprotocol/hypergraph-react/*": ["./packages/hypergraph-react/src/*.js"] - } + }, + "plugins": [{ + "name": "@effect/language-service", + "namespaceImportPackages": ["effect", "@effect/*"] + }] } } diff --git a/tsconfig.json b/tsconfig.json index cbe80ab1..8a8ce949 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ { "path": "packages/hypergraph-react" }, { "path": "packages/typesync-studio" }, { "path": "apps/server" }, + { "path": "apps/server-new" }, { "path": "apps/connect" }, { "path": "apps/events" }, { "path": "apps/template-nextjs" }, From 1d342cc97fd4045399b7b29e0fdd2d32d50eb27a Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 29 Aug 2025 14:23:08 +0200 Subject: [PATCH 12/14] add cors and apply linting --- apps/server-new/src/config/hypergraph.ts | 8 +- apps/server-new/src/http/api.ts | 65 +-- apps/server-new/src/http/errors.ts | 20 +- apps/server-new/src/http/handlers.ts | 148 ++++--- apps/server-new/src/server.ts | 3 +- apps/server-new/src/services/account-inbox.ts | 160 +++---- apps/server-new/src/services/app-identity.ts | 171 ++++---- apps/server-new/src/services/auth.ts | 21 +- .../src/services/connect-identity.ts | 138 +++--- apps/server-new/src/services/database.ts | 36 +- apps/server-new/src/services/identity.ts | 48 ++- apps/server-new/src/services/privy-auth.ts | 59 +-- apps/server-new/src/services/space-inbox.ts | 394 +++++++++--------- apps/server-new/src/services/spaces.ts | 272 ++++++------ packages/hypergraph/src/identity/types.ts | 4 +- 15 files changed, 839 insertions(+), 708 deletions(-) diff --git a/apps/server-new/src/config/hypergraph.ts b/apps/server-new/src/config/hypergraph.ts index ef5bf088..8446b0c0 100644 --- a/apps/server-new/src/config/hypergraph.ts +++ b/apps/server-new/src/config/hypergraph.ts @@ -7,9 +7,5 @@ export const hypergraphChainConfig = Config.string('HYPERGRAPH_CHAIN').pipe( ); export const hypergraphRpcUrlConfig = Config.string('HYPERGRAPH_RPC_URL').pipe( - Config.orElse(() => - hypergraphChainConfig.pipe( - Config.map((chain) => chain.rpcUrls.default.http[0]), - ), - ), -); \ No newline at end of file + Config.orElse(() => hypergraphChainConfig.pipe(Config.map((chain) => chain.rpcUrls.default.http[0]))), +); diff --git a/apps/server-new/src/http/api.ts b/apps/server-new/src/http/api.ts index 17030edc..8194564e 100644 --- a/apps/server-new/src/http/api.ts +++ b/apps/server-new/src/http/api.ts @@ -73,10 +73,12 @@ export const healthGroup = HttpApiGroup.make('Health').add(statusEndpoint); * Connect API endpoints (Privy authentication) */ export const getConnectSpacesEndpoint = HttpApiEndpoint.get('getConnectSpaces')`/connect/spaces` - .setHeaders(Schema.Struct({ - 'privy-id-token': Schema.String, - 'account-address': Schema.String, - })) + .setHeaders( + Schema.Struct({ + 'privy-id-token': Schema.String, + 'account-address': Schema.String, + }), + ) .addSuccess(ConnectSpacesResponse) .addError(Errors.AuthenticationError, { status: 401 }) .addError(Errors.AuthorizationError, { status: 401 }) @@ -84,9 +86,11 @@ export const getConnectSpacesEndpoint = HttpApiEndpoint.get('getConnectSpaces')` .addError(Errors.InternalServerError); export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces')`/connect/spaces` - .setHeaders(Schema.Struct({ - 'privy-id-token': Schema.String, - })) + .setHeaders( + Schema.Struct({ + 'privy-id-token': Schema.String, + }), + ) .setPayload(Messages.RequestConnectCreateSpaceEvent) .addSuccess(SpaceCreationResponse) .addError(Errors.AuthenticationError, { status: 401 }) @@ -98,9 +102,11 @@ export const postConnectSpacesEndpoint = HttpApiEndpoint.post('postConnectSpaces export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( 'postConnectAddAppIdentityToSpaces', )`/connect/add-app-identity-to-spaces` - .setHeaders(Schema.Struct({ - 'privy-id-token': Schema.String, - })) + .setHeaders( + Schema.Struct({ + 'privy-id-token': Schema.String, + }), + ) .setPayload(Messages.RequestConnectAddAppIdentityToSpaces) .addSuccess(Schema.Void) .addError(Errors.AuthenticationError, { status: 401 }) @@ -110,9 +116,11 @@ export const postConnectAddAppIdentityToSpacesEndpoint = HttpApiEndpoint.post( .addError(Errors.InternalServerError); export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIdentity')`/connect/identity` - .setHeaders(Schema.Struct({ - 'privy-id-token': Schema.String, - })) + .setHeaders( + Schema.Struct({ + 'privy-id-token': Schema.String, + }), + ) .setPayload(Messages.RequestConnectCreateIdentity) .addSuccess(Messages.ResponseConnectCreateIdentity) .addError(Errors.AuthenticationError, { status: 401 }) @@ -125,10 +133,12 @@ export const postConnectIdentityEndpoint = HttpApiEndpoint.post('postConnectIden export const getConnectIdentityEncryptedEndpoint = HttpApiEndpoint.get( 'getConnectIdentityEncrypted', )`/connect/identity/encrypted` - .setHeaders(Schema.Struct({ - 'privy-id-token': Schema.String, - 'account-address': Schema.String, - })) + .setHeaders( + Schema.Struct({ + 'privy-id-token': Schema.String, + 'account-address': Schema.String, + }), + ) .addSuccess(Messages.ResponseIdentityEncrypted) .addError(Errors.AuthenticationError, { status: 401 }) .addError(Errors.AuthorizationError, { status: 401 }) @@ -139,10 +149,12 @@ export const getConnectIdentityEncryptedEndpoint = HttpApiEndpoint.get( export const getConnectAppIdentityEndpoint = HttpApiEndpoint.get( 'getConnectAppIdentity', )`/connect/app-identity/${appId}` - .setHeaders(Schema.Struct({ - 'privy-id-token': Schema.String, - 'account-address': Schema.String, - })) + .setHeaders( + Schema.Struct({ + 'privy-id-token': Schema.String, + 'account-address': Schema.String, + }), + ) .addSuccess(AppIdentityResponse) .addError(Errors.AuthenticationError, { status: 401 }) .addError(Errors.AuthorizationError, { status: 401 }) @@ -151,9 +163,11 @@ export const getConnectAppIdentityEndpoint = HttpApiEndpoint.get( .addError(Errors.InternalServerError); export const postConnectAppIdentityEndpoint = HttpApiEndpoint.post('postConnectAppIdentity')`/connect/app-identity` - .setHeaders(Schema.Struct({ - 'privy-id-token': Schema.String, - })) + .setHeaders( + Schema.Struct({ + 'privy-id-token': Schema.String, + }), + ) .setPayload(Messages.RequestConnectCreateAppIdentity) .addSuccess(Schema.Void) .addError(Errors.AuthenticationError, { status: 401 }) @@ -258,5 +272,4 @@ export const hypergraphApi = HttpApi.make('HypergraphApi') .add(healthGroup) .add(connectGroup) .add(identityGroup) - .add(inboxGroup) - .addError(Errors.ResourceNotFoundError, { status: 500 }); + .add(inboxGroup); diff --git a/apps/server-new/src/http/errors.ts b/apps/server-new/src/http/errors.ts index acdf96e3..fe6ca708 100644 --- a/apps/server-new/src/http/errors.ts +++ b/apps/server-new/src/http/errors.ts @@ -1,13 +1,19 @@ import { HttpApiSchema } from '@effect/platform'; import { Schema } from 'effect'; -export class InternalServerError extends Schema.TaggedError()('InternalServerError', { - message: Schema.String.pipe(Schema.optionalWith({ - default: () => 'Internal server error', - })), -}, { - [HttpApiSchema.AnnotationStatus]: 500, -}) {} +export class InternalServerError extends Schema.TaggedError()( + 'InternalServerError', + { + message: Schema.String.pipe( + Schema.optionalWith({ + default: () => 'Internal server error', + }), + ), + }, + { + [HttpApiSchema.AnnotationStatus]: 500, + }, +) {} /** * Authentication-related errors diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index b29c9d92..349a4d72 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -1,5 +1,5 @@ import { HttpApiBuilder } from '@effect/platform'; -import { Identity, Messages, Utils } from '@graphprotocol/hypergraph'; +import { Identity, type Messages, Utils } from '@graphprotocol/hypergraph'; import { bytesToHex, randomBytes } from '@noble/hashes/utils.js'; import { Effect, Layer } from 'effect'; import { hypergraphChainConfig, hypergraphRpcUrlConfig } from '../config/hypergraph.js'; @@ -33,11 +33,13 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han const privyAuthService = yield* PrivyAuthService.PrivyAuthService; const spacesService = yield* SpacesService.SpacesService; - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']).pipe(Effect.orDie); + yield* privyAuthService + .authenticateRequest(headers['privy-id-token'], headers['account-address']) + .pipe(Effect.orDie); const spaces = yield* spacesService.listByAccount(headers['account-address']).pipe(Effect.orDie); return { spaces }; - }) + }), ) .handle( 'postConnectSpaces', @@ -48,18 +50,22 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han const spacesService = yield* SpacesService.SpacesService; // Authenticate the request with Privy token - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress).pipe(Effect.orDie); + yield* privyAuthService + .authenticateRequest(headers['privy-id-token'], payload.accountAddress) + .pipe(Effect.orDie); // Create the space - const space = yield* spacesService.createSpace({ - accountAddress: payload.accountAddress, - event: payload.event, - keyBox: payload.keyBox, - infoContent: Utils.hexToBytes(payload.infoContent), - infoSignatureHex: payload.infoSignature.hex, - infoSignatureRecovery: payload.infoSignature.recovery, - name: payload.name, - }).pipe(Effect.orDie); + const space = yield* spacesService + .createSpace({ + accountAddress: payload.accountAddress, + event: payload.event, + keyBox: payload.keyBox, + infoContent: Utils.hexToBytes(payload.infoContent), + infoSignatureHex: payload.infoSignature.hex, + infoSignatureRecovery: payload.infoSignature.recovery, + name: payload.name, + }) + .pipe(Effect.orDie); return { space }; }), @@ -73,14 +79,18 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han const spacesService = yield* SpacesService.SpacesService; // Authenticate the request with Privy token - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], payload.accountAddress).pipe(Effect.orDie); + yield* privyAuthService + .authenticateRequest(headers['privy-id-token'], payload.accountAddress) + .pipe(Effect.orDie); // Add app identity to spaces - yield* spacesService.addAppIdentityToSpaces({ - appIdentityAddress: payload.appIdentityAddress, - accountAddress: payload.accountAddress, - spacesInput: payload.spacesInput, - }).pipe(Effect.orDie); + yield* spacesService + .addAppIdentityToSpaces({ + appIdentityAddress: payload.appIdentityAddress, + accountAddress: payload.accountAddress, + spacesInput: payload.spacesInput, + }) + .pipe(Effect.orDie); }), ) .handle( @@ -127,22 +137,24 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han return yield* new Errors.OwnershipProofError({ accountAddress, reason: 'Invalid ownership proof', - }) + }); } yield* Effect.logInfo('Ownership proof is valid'); // Create the identity - yield* connectIdentityService.createIdentity({ - signerAddress, - accountAddress, - ciphertext: payload.keyBox.ciphertext, - nonce: payload.keyBox.nonce, - signaturePublicKey: payload.signaturePublicKey, - encryptionPublicKey: payload.encryptionPublicKey, - accountProof: payload.accountProof, - keyProof: payload.keyProof, - }).pipe(Effect.orDie); + yield* connectIdentityService + .createIdentity({ + signerAddress, + accountAddress, + ciphertext: payload.keyBox.ciphertext, + nonce: payload.keyBox.nonce, + signaturePublicKey: payload.signaturePublicKey, + encryptionPublicKey: payload.encryptionPublicKey, + accountProof: payload.accountProof, + keyProof: payload.keyProof, + }) + .pipe(Effect.orDie); const response: Messages.ResponseConnectCreateIdentity = { success: true, @@ -190,13 +202,17 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han const appIdentityService = yield* AppIdentityService.AppIdentityService; // Authenticate the request with Privy token - yield* privyAuthService.authenticateRequest(headers['privy-id-token'], headers['account-address']).pipe(Effect.orDie); + yield* privyAuthService + .authenticateRequest(headers['privy-id-token'], headers['account-address']) + .pipe(Effect.orDie); // Find the app identity - const appIdentity = yield* appIdentityService.findByAppId({ - accountAddress: headers['account-address'], - appId, - }).pipe(Effect.orDie); + const appIdentity = yield* appIdentityService + .findByAppId({ + accountAddress: headers['account-address'], + appId, + }) + .pipe(Effect.orDie); if (!appIdentity) { return yield* new Errors.ResourceNotFoundError({ @@ -247,7 +263,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han return yield* new Errors.OwnershipProofError({ accountAddress, reason: 'Invalid ownership proof', - }) + }); } // Generate session token @@ -255,18 +271,20 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han const sessionTokenExpires = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); // 30 days // Create the app identity - yield* appIdentityService.createAppIdentity({ - accountAddress, - appId: payload.appId, - address: payload.address, - ciphertext: payload.ciphertext, - signaturePublicKey: payload.signaturePublicKey, - encryptionPublicKey: payload.encryptionPublicKey, - accountProof: payload.accountProof, - keyProof: payload.keyProof, - sessionToken, - sessionTokenExpires, - }).pipe(Effect.orDie); + yield* appIdentityService + .createAppIdentity({ + accountAddress, + appId: payload.appId, + address: payload.address, + ciphertext: payload.ciphertext, + signaturePublicKey: payload.signaturePublicKey, + encryptionPublicKey: payload.encryptionPublicKey, + accountProof: payload.accountProof, + keyProof: payload.keyProof, + sessionToken, + sessionTokenExpires, + }) + .pipe(Effect.orDie); }), ); }).pipe( @@ -408,11 +426,13 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; - yield* spaceInboxService.postSpaceInboxMessage({ - spaceId, - inboxId, - message: payload, - }).pipe(Effect.orDie); + yield* spaceInboxService + .postSpaceInboxMessage({ + spaceId, + inboxId, + message: payload, + }) + .pipe(Effect.orDie); // Return void as per the API endpoint definition }), @@ -448,24 +468,20 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler const accountInboxService = yield* AccountInboxService.AccountInboxService; - yield* accountInboxService.postAccountInboxMessage({ - accountAddress, - inboxId, - message: payload, - }).pipe(Effect.orDie); + yield* accountInboxService + .postAccountInboxMessage({ + accountAddress, + inboxId, + message: payload, + }) + .pipe(Effect.orDie); // Return void as per the API endpoint definition }), ); -}).pipe( - Layer.provide(AccountInboxService.layer), - Layer.provide(SpaceInboxService.layer), -); +}).pipe(Layer.provide(AccountInboxService.layer), Layer.provide(SpaceInboxService.layer)); /** * All handlers combined */ -export const HandlersLive = Layer.mergeAll( - HealthGroupLive, - ConnectGroupLive, IdentityGroupLive, InboxGroupLive -); +export const HandlersLive = Layer.mergeAll(HealthGroupLive, ConnectGroupLive, IdentityGroupLive, InboxGroupLive); diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts index 15dbb93e..fdc13e28 100644 --- a/apps/server-new/src/server.ts +++ b/apps/server-new/src/server.ts @@ -1,7 +1,7 @@ +import { createServer } from 'node:http'; import { HttpApiBuilder, HttpServer } from '@effect/platform'; import { NodeHttpServer } from '@effect/platform-node'; import { Effect, Layer } from 'effect'; -import { createServer } from 'node:http'; import { serverPortConfig } from './config/server.ts'; import { hypergraphApi } from './http/api.ts'; import { HandlersLive } from './http/handlers.ts'; @@ -12,6 +12,7 @@ export const server = Layer.unwrapEffect( Effect.gen(function* () { const port = yield* serverPortConfig; return HttpApiBuilder.serve().pipe( + Layer.provide(HttpApiBuilder.middlewareCors()), Layer.provide(apiLive), HttpServer.withLogAddress, Layer.provide(NodeHttpServer.layer(createServer, { port })), diff --git a/apps/server-new/src/services/account-inbox.ts b/apps/server-new/src/services/account-inbox.ts index 00e48137..1c250e9e 100644 --- a/apps/server-new/src/services/account-inbox.ts +++ b/apps/server-new/src/services/account-inbox.ts @@ -1,11 +1,11 @@ import { Inboxes, type Messages } from '@graphprotocol/hypergraph'; +import * as Context from 'effect/Context'; +import * as Effect from 'effect/Effect'; +import * as Layer from 'effect/Layer'; +import * as Predicate from 'effect/Predicate'; import { AuthorizationError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; import * as DatabaseService from './database.js'; import * as IdentityService from './identity.js'; -import * as Predicate from "effect/Predicate"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; export interface AccountInboxResult { inboxId: string; @@ -19,23 +19,26 @@ export interface AccountInboxResult { }; } -export class AccountInboxService extends Context.Tag('AccountInboxService') Effect.Effect; - readonly getAccountInbox: (params: { - accountAddress: string; - inboxId: string; - }) => Effect.Effect; - readonly postAccountInboxMessage: (params: { - accountAddress: string; - inboxId: string; - message: Messages.RequestCreateAccountInboxMessage; - }) => Effect.Effect< - Messages.InboxMessage, - ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseService.DatabaseError - >; -}>() {} +export class AccountInboxService extends Context.Tag('AccountInboxService')< + AccountInboxService, + { + readonly listPublicAccountInboxes: (params: { + accountAddress: string; + }) => Effect.Effect; + readonly getAccountInbox: (params: { + accountAddress: string; + inboxId: string; + }) => Effect.Effect; + readonly postAccountInboxMessage: (params: { + accountAddress: string; + inboxId: string; + message: Messages.RequestCreateAccountInboxMessage; + }) => Effect.Effect< + Messages.InboxMessage, + ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseService.DatabaseError + >; + } +>() {} export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; @@ -58,7 +61,8 @@ export const layer = Effect.gen(function* () { signatureHex: true, signatureRecovery: true, }, - })) + }), + ); return inboxes.map((inbox) => ({ inboxId: inbox.id, @@ -73,46 +77,63 @@ export const layer = Effect.gen(function* () { })); }); - const getAccountInbox = Effect.fn(function* ({ accountAddress, inboxId }: { accountAddress: string; inboxId: string }) { - const inbox = yield* use((client) => - client.accountInbox.findUnique({ - where: { id: inboxId, accountAddress }, - select: { - id: true, - account: { - select: { - address: true, - }, + const getAccountInbox = Effect.fn(function* ({ + accountAddress, + inboxId, + }: { + accountAddress: string; + inboxId: string; + }) { + const inbox = yield* use((client) => + client.accountInbox.findUnique({ + where: { id: inboxId, accountAddress }, + select: { + id: true, + account: { + select: { + address: true, }, - isPublic: true, - authPolicy: true, - encryptionPublicKey: true, - signatureHex: true, - signatureRecovery: true, }, - })).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ - id: inboxId, - resource: 'AccountInbox', - }))) - - return { - inboxId: inbox.id, - accountAddress: inbox.account.address, - isPublic: inbox.isPublic, - authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, - encryptionPublicKey: inbox.encryptionPublicKey, - signature: { - hex: inbox.signatureHex, - recovery: inbox.signatureRecovery, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + signatureHex: true, + signatureRecovery: true, }, - }; - }); + }), + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new ResourceNotFoundError({ + id: inboxId, + resource: 'AccountInbox', + }), + ), + ); + + return { + inboxId: inbox.id, + accountAddress: inbox.account.address, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + signature: { + hex: inbox.signatureHex, + recovery: inbox.signatureRecovery, + }, + }; + }); const postAccountInboxMessage = Effect.fn(function* ({ accountAddress, inboxId, message, - }: { accountAddress: string; inboxId: string; message: Messages.RequestCreateAccountInboxMessage }) { + }: { + accountAddress: string; + inboxId: string; + message: Messages.RequestCreateAccountInboxMessage; + }) { const accountInbox = yield* getAccountInbox({ accountAddress, inboxId }); // Validate auth policy requirements @@ -173,19 +194,18 @@ export const layer = Effect.gen(function* () { // Check if this public key corresponds to a user's identity const authorIdentity = yield* getAppOrConnectIdentity({ - accountAddress: message.authorAccountAddress, - signaturePublicKey: authorPublicKey, - }) - .pipe( - Effect.catchAll(() => - Effect.fail( - new AuthorizationError({ - message: 'Not authorized to post to this inbox', - accountAddress: message.authorAccountAddress, - }), - ), + accountAddress: message.authorAccountAddress, + signaturePublicKey: authorPublicKey, + }).pipe( + Effect.catchAll(() => + Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }), ), - ); + ), + ); if (authorIdentity.accountAddress !== message.authorAccountAddress) { return yield* Effect.fail( @@ -235,7 +255,7 @@ export const layer = Effect.gen(function* () { authorAccountAddress: created.authorAccountAddress ?? undefined, createdAt: created.createdAt, } as Messages.InboxMessage; - }) + }), ); // TODO: Broadcast the message (WebSocket functionality would go here) @@ -249,8 +269,4 @@ export const layer = Effect.gen(function* () { getAccountInbox, postAccountInboxMessage, } as const; -}).pipe( - Layer.effect(AccountInboxService), - Layer.provide(DatabaseService.layer), - Layer.provide(IdentityService.layer) -); +}).pipe(Layer.effect(AccountInboxService), Layer.provide(DatabaseService.layer), Layer.provide(IdentityService.layer)); diff --git a/apps/server-new/src/services/app-identity.ts b/apps/server-new/src/services/app-identity.ts index 2030e33a..8326b579 100644 --- a/apps/server-new/src/services/app-identity.ts +++ b/apps/server-new/src/services/app-identity.ts @@ -1,6 +1,7 @@ -import * as Effect from "effect/Effect"; -import * as Context from "effect/Context"; -import * as Layer from "effect/Layer"; +import * as Context from 'effect/Context'; +import * as Effect from 'effect/Effect'; +import * as Layer from 'effect/Layer'; +import * as Predicate from 'effect/Predicate'; import { InvalidTokenError, ResourceAlreadyExistsError, @@ -8,7 +9,6 @@ import { TokenExpiredError, } from '../http/errors.js'; import * as DatabaseService from './database.js'; -import * as Predicate from "effect/Predicate"; export interface AppIdentityResult { address: string; @@ -39,39 +39,41 @@ export interface CreateAppIdentityParams { sessionTokenExpires: Date; } -export class AppIdentityService extends Context.Tag('AppIdentityService') Effect.Effect< - { address: string; accountAddress: string }, - InvalidTokenError | DatabaseService.DatabaseError | TokenExpiredError - >; - readonly findByAppId: (params: { - accountAddress: string; - appId: string; - }) => Effect.Effect; - readonly createAppIdentity: ( - params: CreateAppIdentityParams, - ) => Effect.Effect; -}>() {} - +export class AppIdentityService extends Context.Tag('AppIdentityService')< + AppIdentityService, + { + readonly getBySessionToken: ( + sessionToken: string, + ) => Effect.Effect< + { address: string; accountAddress: string }, + InvalidTokenError | DatabaseService.DatabaseError | TokenExpiredError + >; + readonly findByAppId: (params: { + accountAddress: string; + appId: string; + }) => Effect.Effect; + readonly createAppIdentity: ( + params: CreateAppIdentityParams, + ) => Effect.Effect; + } +>() {} export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; - const getBySessionToken = Effect.fn("getBySessionToken")(function* (sessionToken: string) { - const appIdentity = yield* use((client) => client.appIdentity.findFirst({ - where: { - sessionToken, - }, - select: { - address: true, - sessionTokenExpires: true, - accountAddress: true, - }, - })).pipe( - Effect.filterOrFail(Predicate.isNotNull, () => new InvalidTokenError({ tokenType: 'session' })) - ); + const getBySessionToken = Effect.fn('getBySessionToken')(function* (sessionToken: string) { + const appIdentity = yield* use((client) => + client.appIdentity.findFirst({ + where: { + sessionToken, + }, + select: { + address: true, + sessionTokenExpires: true, + accountAddress: true, + }, + }), + ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new InvalidTokenError({ tokenType: 'session' }))); if (appIdentity.sessionTokenExpires && appIdentity.sessionTokenExpires < new Date()) { return yield* new TokenExpiredError({ tokenType: 'session' }); @@ -83,53 +85,69 @@ export const layer = Effect.gen(function* () { }; }); - const findByAppId = Effect.fn("findByAppId")(function* ({ accountAddress, appId }: { accountAddress: string; appId: string; }) { - const appIdentity = yield* use((client) => client.appIdentity.findFirst({ - where: { - accountAddress, - appId, - }, - })).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ - resource: 'AppIdentity', - id: appId, - }))); + const findByAppId = Effect.fn('findByAppId')(function* ({ + accountAddress, + appId, + }: { + accountAddress: string; + appId: string; + }) { + const appIdentity = yield* use((client) => + client.appIdentity.findFirst({ + where: { + accountAddress, + appId, + }, + }), + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new ResourceNotFoundError({ + resource: 'AppIdentity', + id: appId, + }), + ), + ); return appIdentity as AppIdentityResult; }); - const createAppIdentity = Effect.fn("createAppIdentity")(function* (params: CreateAppIdentityParams) { - const appIdentity = yield* use((client) => client.$transaction(async (prisma) => { - // Check if app identity already exists - const existingIdentity = await prisma.appIdentity.findFirst({ - where: { - accountAddress: params.accountAddress, - appId: params.appId, - }, - }); - - if (existingIdentity) { - throw new ResourceAlreadyExistsError({ - resource: 'AppIdentity', - id: params.appId, + const createAppIdentity = Effect.fn('createAppIdentity')(function* (params: CreateAppIdentityParams) { + const appIdentity = yield* use((client) => + client.$transaction(async (prisma) => { + // Check if app identity already exists + const existingIdentity = await prisma.appIdentity.findFirst({ + where: { + accountAddress: params.accountAddress, + appId: params.appId, + }, }); - } - // Create the new app identity - return await prisma.appIdentity.create({ - data: { - address: params.address, - accountAddress: params.accountAddress, - appId: params.appId, - ciphertext: params.ciphertext, - signaturePublicKey: params.signaturePublicKey, - encryptionPublicKey: params.encryptionPublicKey, - accountProof: params.accountProof, - keyProof: params.keyProof, - sessionToken: params.sessionToken, - sessionTokenExpires: params.sessionTokenExpires, - }, - }); - })); + if (existingIdentity) { + throw new ResourceAlreadyExistsError({ + resource: 'AppIdentity', + id: params.appId, + }); + } + + // Create the new app identity + return await prisma.appIdentity.create({ + data: { + address: params.address, + accountAddress: params.accountAddress, + appId: params.appId, + ciphertext: params.ciphertext, + signaturePublicKey: params.signaturePublicKey, + encryptionPublicKey: params.encryptionPublicKey, + accountProof: params.accountProof, + keyProof: params.keyProof, + sessionToken: params.sessionToken, + sessionTokenExpires: params.sessionTokenExpires, + }, + }); + }), + ); return appIdentity; }); @@ -139,7 +157,4 @@ export const layer = Effect.gen(function* () { findByAppId, createAppIdentity, }; -}).pipe( - Layer.effect(AppIdentityService), - Layer.provide(DatabaseService.layer) -) \ No newline at end of file +}).pipe(Layer.effect(AppIdentityService), Layer.provide(DatabaseService.layer)); diff --git a/apps/server-new/src/services/auth.ts b/apps/server-new/src/services/auth.ts index 6c14ee1e..d6aaa3d4 100644 --- a/apps/server-new/src/services/auth.ts +++ b/apps/server-new/src/services/auth.ts @@ -1,18 +1,21 @@ import { PrivyClient } from '@privy-io/server-auth'; +import * as Context from 'effect/Context'; +import * as Effect from 'effect/Effect'; +import * as Layer from 'effect/Layer'; +import * as Redacted from 'effect/Redacted'; import * as Config from '../config/privy.js'; -import * as Effect from "effect/Effect"; -import * as Redacted from "effect/Redacted"; -import * as Context from "effect/Context"; -import * as Layer from "effect/Layer"; /** * Auth service tag */ -export class AuthService extends Context.Tag("AuthService") Effect.Effect<{ userId: string }, Error>; - readonly verifySessionToken: (token: string) => Effect.Effect<{ address: string }, Error>; -}>() {} +export class AuthService extends Context.Tag('AuthService')< + AuthService, + { + readonly privy: PrivyClient; + readonly verifyAuthToken: (token: string) => Effect.Effect<{ userId: string }, Error>; + readonly verifySessionToken: (token: string) => Effect.Effect<{ address: string }, Error>; + } +>() {} /** * Auth service implementation diff --git a/apps/server-new/src/services/connect-identity.ts b/apps/server-new/src/services/connect-identity.ts index 6cc95bba..59fca44b 100644 --- a/apps/server-new/src/services/connect-identity.ts +++ b/apps/server-new/src/services/connect-identity.ts @@ -1,9 +1,9 @@ -import * as Effect from "effect/Effect"; +import * as Context from 'effect/Context'; +import * as Effect from 'effect/Effect'; +import * as Layer from 'effect/Layer'; +import * as Predicate from 'effect/Predicate'; import { ResourceAlreadyExistsError, ResourceNotFoundError } from '../http/errors.js'; import * as DatabaseService from './database.js'; -import * as Predicate from "effect/Predicate"; -import * as Context from "effect/Context"; -import * as Layer from "effect/Layer"; export interface ConnectIdentityResult { accountAddress: string; @@ -30,20 +30,25 @@ export interface CreateConnectIdentityParams { keyProof: string; } -export class ConnectIdentityService extends Context.Tag('ConnectIdentityService') Effect.Effect; - readonly getIdentityEncrypted: ( - accountAddress: string, - ) => Effect.Effect; - readonly createIdentity: ( - params: CreateConnectIdentityParams, - ) => Effect.Effect; -}>() {} +export class ConnectIdentityService extends Context.Tag('ConnectIdentityService')< + ConnectIdentityService, + { + readonly getByAccountAddress: ( + accountAddress: string, + ) => Effect.Effect; + readonly getIdentityEncrypted: ( + accountAddress: string, + ) => Effect.Effect; + readonly createIdentity: ( + params: CreateConnectIdentityParams, + ) => Effect.Effect; + } +>() {} export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; - const getByAccountAddress = Effect.fn("getByAccountAddress")(function* (accountAddress: string) { + const getByAccountAddress = Effect.fn('getByAccountAddress')(function* (accountAddress: string) { const account = yield* use((client) => client.account.findFirst({ where: { address: accountAddress }, @@ -54,11 +59,17 @@ export const layer = Effect.gen(function* () { connectAccountProof: true, connectKeyProof: true, }, - }) - ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ - resource: 'ConnectIdentity', - id: accountAddress, - }))); + }), + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, + }), + ), + ); return { accountAddress: account.address, @@ -69,7 +80,7 @@ export const layer = Effect.gen(function* () { }; }); - const createIdentity = Effect.fn("createIdentity")(function* (params: CreateConnectIdentityParams) { + const createIdentity = Effect.fn('createIdentity')(function* (params: CreateConnectIdentityParams) { // Check if identity already exists for this account yield* use((client) => client.account.findFirst({ @@ -77,57 +88,66 @@ export const layer = Effect.gen(function* () { address: params.accountAddress, }, }), - ).pipe(Effect.filterOrFail(Predicate.isNull, () => new ResourceAlreadyExistsError({ - resource: 'ConnectIdentity', - id: params.accountAddress, - }))); + ).pipe( + Effect.filterOrFail( + Predicate.isNull, + () => + new ResourceAlreadyExistsError({ + resource: 'ConnectIdentity', + id: params.accountAddress, + }), + ), + ); // Create the new identity yield* use((client) => client.account.create({ - data: { - connectSignerAddress: params.signerAddress, - address: params.accountAddress, - connectAccountProof: params.accountProof, - connectKeyProof: params.keyProof, - connectSignaturePublicKey: params.signaturePublicKey, - connectEncryptionPublicKey: params.encryptionPublicKey, - connectCiphertext: params.ciphertext, - connectNonce: params.nonce, - connectAddress: params.accountAddress, - }, - }), + data: { + connectSignerAddress: params.signerAddress, + address: params.accountAddress, + connectAccountProof: params.accountProof, + connectKeyProof: params.keyProof, + connectSignaturePublicKey: params.signaturePublicKey, + connectEncryptionPublicKey: params.encryptionPublicKey, + connectCiphertext: params.ciphertext, + connectNonce: params.nonce, + connectAddress: params.accountAddress, + }, + }), ); }); - const getIdentityEncrypted = Effect.fn("getIdentityEncrypted")(function* (accountAddress: string) { - const account = yield* use((client) => - client.account.findFirst({ - where: { address: accountAddress }, - select: { - address: true, - connectCiphertext: true, - connectNonce: true, - }, + const getIdentityEncrypted = Effect.fn('getIdentityEncrypted')(function* (accountAddress: string) { + const account = yield* use((client) => + client.account.findFirst({ + where: { address: accountAddress }, + select: { + address: true, + connectCiphertext: true, + connectNonce: true, + }, + }), + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new ResourceNotFoundError({ + resource: 'ConnectIdentity', + id: accountAddress, }), - ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ - resource: 'ConnectIdentity', - id: accountAddress, - }))); + ), + ); - return { - accountAddress: account.address, - ciphertext: account.connectCiphertext, - nonce: account.connectNonce, - }; - }); + return { + accountAddress: account.address, + ciphertext: account.connectCiphertext, + nonce: account.connectNonce, + }; + }); return { getByAccountAddress, getIdentityEncrypted, createIdentity, }; -}).pipe( - Layer.effect(ConnectIdentityService), - Layer.provide(DatabaseService.layer) -) +}).pipe(Layer.effect(ConnectIdentityService), Layer.provide(DatabaseService.layer)); diff --git a/apps/server-new/src/services/database.ts b/apps/server-new/src/services/database.ts index 642d2d34..51748b02 100644 --- a/apps/server-new/src/services/database.ts +++ b/apps/server-new/src/services/database.ts @@ -1,6 +1,6 @@ import { Config, Context, Effect, Layer } from 'effect'; +import * as Data from 'effect/Data'; import { PrismaClient } from '../../prisma/generated/client/client'; -import * as Data from "effect/Data"; export class DatabaseError extends Data.TaggedError('DatabaseError')<{ readonly cause: unknown; @@ -9,10 +9,13 @@ export class DatabaseError extends Data.TaggedError('DatabaseError')<{ /** * Database service tag */ -export class DatabaseService extends Context.Tag('DatabaseService')(fn: (client: PrismaClient, signal: AbortSignal) => Promise) => Effect.Effect; -}>() {} +export class DatabaseService extends Context.Tag('DatabaseService')< + DatabaseService, + { + readonly client: PrismaClient; + readonly use: (fn: (client: PrismaClient, signal: AbortSignal) => Promise) => Effect.Effect; + } +>() {} /** * Database service layer with resource management @@ -21,18 +24,21 @@ export const layer = Layer.scoped( DatabaseService, Effect.gen(function* () { const databaseUrl = yield* Config.string('DATABASE_URL').pipe(Config.withDefault('file:./dev.db')); - const client = yield* Effect.acquireRelease(Effect.tryPromise({ - try: async () => { - const client = new PrismaClient({ - datasourceUrl: databaseUrl, - }); + const client = yield* Effect.acquireRelease( + Effect.tryPromise({ + try: async () => { + const client = new PrismaClient({ + datasourceUrl: databaseUrl, + }); - await client.$connect(); + await client.$connect(); - return client; - }, - catch: (cause) => new DatabaseError({ cause }), - }), (client) => Effect.tryPromise(() => client.$disconnect()).pipe(Effect.ignore)); + return client; + }, + catch: (cause) => new DatabaseError({ cause }), + }), + (client) => Effect.tryPromise(() => client.$disconnect()).pipe(Effect.ignore), + ); const use = Effect.fn(function* (fn: (client: PrismaClient, signal: AbortSignal) => Promise) { return yield* Effect.tryPromise({ diff --git a/apps/server-new/src/services/identity.ts b/apps/server-new/src/services/identity.ts index 60b0eaaf..ba2549cf 100644 --- a/apps/server-new/src/services/identity.ts +++ b/apps/server-new/src/services/identity.ts @@ -1,7 +1,7 @@ import { Context, Effect, Layer } from 'effect'; +import * as Predicate from 'effect/Predicate'; import { ResourceNotFoundError } from '../http/errors.js'; import * as DatabaseService from './database.js'; -import * as Predicate from "effect/Predicate"; export interface IdentityResult { accountAddress: string; @@ -14,11 +14,14 @@ export interface IdentityResult { appId: string | null; } -export class IdentityService extends Context.Tag('IdentityService') Effect.Effect; -}>() {} +export class IdentityService extends Context.Tag('IdentityService')< + IdentityService, + { + readonly getAppOrConnectIdentity: ( + params: { accountAddress: string; signaturePublicKey: string } | { accountAddress: string; appId: string }, + ) => Effect.Effect; + } +>() {} export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; @@ -59,10 +62,16 @@ export const layer = Effect.gen(function* () { signaturePublicKey: params.signaturePublicKey, }, }), - ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }))); + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + ), + ); return { accountAddress: appIdentity.accountAddress, @@ -84,10 +93,16 @@ export const layer = Effect.gen(function* () { appId: params.appId, }, }), - ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ - resource: 'Identity', - id: params.accountAddress, - }))); + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new ResourceNotFoundError({ + resource: 'Identity', + id: params.accountAddress, + }), + ), + ); return { accountAddress: appIdentity.accountAddress, @@ -104,7 +119,4 @@ export const layer = Effect.gen(function* () { return { getAppOrConnectIdentity, } as const; -}).pipe( - Layer.effect(IdentityService), - Layer.provide(DatabaseService.layer) -) ; +}).pipe(Layer.effect(IdentityService), Layer.provide(DatabaseService.layer)); diff --git a/apps/server-new/src/services/privy-auth.ts b/apps/server-new/src/services/privy-auth.ts index 36e2700e..c7324e43 100644 --- a/apps/server-new/src/services/privy-auth.ts +++ b/apps/server-new/src/services/privy-auth.ts @@ -1,20 +1,26 @@ import { PrivyClient, type Wallet } from '@privy-io/server-auth'; import { Config, Context, Effect, Layer } from 'effect'; +import * as Predicate from 'effect/Predicate'; import { AuthenticationError, AuthorizationError, PrivyConfigError, PrivyTokenError } from '../http/errors.js'; import * as DatabaseService from './database.js'; -import * as Predicate from "effect/Predicate"; -export class PrivyAuthService extends Context.Tag('PrivyAuthService') Effect.Effect; - readonly isSignerForAccount: ( - signerAddress: string, - accountAddress: string, - ) => Effect.Effect; - readonly authenticateRequest: ( - idToken: string | undefined, - accountAddress: string, - ) => Effect.Effect; -}>() {} +export class PrivyAuthService extends Context.Tag('PrivyAuthService')< + PrivyAuthService, + { + readonly verifyPrivyToken: (idToken: string) => Effect.Effect; + readonly isSignerForAccount: ( + signerAddress: string, + accountAddress: string, + ) => Effect.Effect; + readonly authenticateRequest: ( + idToken: string | undefined, + accountAddress: string, + ) => Effect.Effect< + void, + AuthenticationError | AuthorizationError | PrivyConfigError | PrivyTokenError | DatabaseService.DatabaseError + >; + } +>() {} export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; @@ -22,7 +28,7 @@ export const layer = Effect.gen(function* () { const privyAppId = yield* Config.string('PRIVY_APP_ID').pipe(Config.orElse(() => Config.succeed(''))); const privyAppSecret = yield* Config.string('PRIVY_APP_SECRET').pipe(Config.orElse(() => Config.succeed(''))); - const verifyPrivyToken = Effect.fn("verifyPrivyToken")(function* (idToken: string) { + const verifyPrivyToken = Effect.fn('verifyPrivyToken')(function* (idToken: string) { if (!privyAppId || !privyAppSecret) { return yield* new PrivyConfigError({ message: 'Missing Privy configuration' }); } @@ -52,17 +58,23 @@ export const layer = Effect.gen(function* () { return wallet.address; }); - const isSignerForAccount = Effect.fn("isSignerForAccount")(function* (signerAddress: string, accountAddress: string) { + const isSignerForAccount = Effect.fn('isSignerForAccount')(function* (signerAddress: string, accountAddress: string) { const account = yield* use((client) => client.account.findUnique({ where: { address: accountAddress, }, }), - ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new AuthorizationError({ - message: 'Account not found', - accountAddress, - }))); + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new AuthorizationError({ + message: 'Account not found', + accountAddress, + }), + ), + ); const isAuthorized = account.connectSignerAddress === signerAddress; if (!isAuthorized) { @@ -75,7 +87,10 @@ export const layer = Effect.gen(function* () { return true; }); - const authenticateRequest = Effect.fn("authenticateRequest")(function* (idToken: string | undefined, accountAddress: string) { + const authenticateRequest = Effect.fn('authenticateRequest')(function* ( + idToken: string | undefined, + accountAddress: string, + ) { if (!idToken) { return yield* Effect.fail(new AuthenticationError({ message: 'No Privy ID token provided' })); } @@ -89,8 +104,4 @@ export const layer = Effect.gen(function* () { isSignerForAccount, authenticateRequest, }; -}).pipe( - Layer.effect(PrivyAuthService), - Layer.provide(DatabaseService.layer) -); - +}).pipe(Layer.effect(PrivyAuthService), Layer.provide(DatabaseService.layer)); diff --git a/apps/server-new/src/services/space-inbox.ts b/apps/server-new/src/services/space-inbox.ts index 235ccc3f..63a9be34 100644 --- a/apps/server-new/src/services/space-inbox.ts +++ b/apps/server-new/src/services/space-inbox.ts @@ -1,11 +1,11 @@ import { Inboxes, type Messages, type SpaceEvents } from '@graphprotocol/hypergraph'; +import * as Context from 'effect/Context'; +import * as Effect from 'effect/Effect'; +import * as Layer from 'effect/Layer'; +import * as Predicate from 'effect/Predicate'; import { AuthorizationError, ResourceNotFoundError, ValidationError } from '../http/errors.js'; import * as DatabaseService from './database.js'; import * as IdentityService from './identity.js'; -import * as Predicate from "effect/Predicate"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; export interface SpaceInboxResult { inboxId: string; @@ -15,82 +15,99 @@ export interface SpaceInboxResult { creationEvent: SpaceEvents.CreateSpaceInboxEvent; } -export class SpaceInboxService extends Context.Tag('SpaceInboxService') Effect.Effect; - readonly getSpaceInbox: (params: { - spaceId: string; - inboxId: string; - }) => Effect.Effect; - readonly postSpaceInboxMessage: (params: { - spaceId: string; - inboxId: string; - message: Messages.RequestCreateSpaceInboxMessage; - }) => Effect.Effect< - Messages.InboxMessage, - ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseService.DatabaseError - >; -}>() {} +export class SpaceInboxService extends Context.Tag('SpaceInboxService')< + SpaceInboxService, + { + readonly listPublicSpaceInboxes: (params: { + spaceId: string; + }) => Effect.Effect; + readonly getSpaceInbox: (params: { + spaceId: string; + inboxId: string; + }) => Effect.Effect; + readonly postSpaceInboxMessage: (params: { + spaceId: string; + inboxId: string; + message: Messages.RequestCreateSpaceInboxMessage; + }) => Effect.Effect< + Messages.InboxMessage, + ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseService.DatabaseError + >; + } +>() {} export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; const { getAppOrConnectIdentity } = yield* IdentityService.IdentityService; - const listPublicSpaceInboxes = Effect.fn("listPublicSpaceInboxes")(function* ({ spaceId }: { spaceId: string }) { - const inboxes = yield* use((client) => - client.spaceInbox.findMany({ - where: { spaceId, isPublic: true }, + const listPublicSpaceInboxes = Effect.fn('listPublicSpaceInboxes')(function* ({ spaceId }: { spaceId: string }) { + const inboxes = yield* use((client) => + client.spaceInbox.findMany({ + where: { spaceId, isPublic: true }, + select: { + id: true, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + spaceEvent: { select: { - id: true, - isPublic: true, - authPolicy: true, - encryptionPublicKey: true, - spaceEvent: { - select: { - event: true, - }, - }, + event: true, }, - }), - ); + }, + }, + }), + ); - return inboxes.map((inbox) => ({ - inboxId: inbox.id, - isPublic: inbox.isPublic, - authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, - encryptionPublicKey: inbox.encryptionPublicKey, - creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, - })); - }); + return inboxes.map((inbox) => ({ + inboxId: inbox.id, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, + })); + }); - const getSpaceInbox = Effect.fn("getSpaceInbox")(function* ({ spaceId, inboxId }: { spaceId: string; inboxId: string }) { + const getSpaceInbox = Effect.fn('getSpaceInbox')(function* ({ + spaceId, + inboxId, + }: { + spaceId: string; + inboxId: string; + }) { const inbox = yield* use((client) => client.spaceInbox.findUnique({ - where: { id: inboxId, spaceId }, - select: { - id: true, - isPublic: true, - authPolicy: true, - encryptionPublicKey: true, - spaceEvent: { - select: { - event: true, - }, + where: { id: inboxId, spaceId }, + select: { + id: true, + isPublic: true, + authPolicy: true, + encryptionPublicKey: true, + spaceEvent: { + select: { + event: true, }, }, - }), - ).pipe(Effect.filterOrFail(Predicate.isNotNull, () => new ResourceNotFoundError({ - resource: 'SpaceInbox', - id: inboxId, - }))); - - return { - inboxId: inbox.id, - isPublic: inbox.isPublic, - authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, - encryptionPublicKey: inbox.encryptionPublicKey, - creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, - }; - }); + }, + }), + ).pipe( + Effect.filterOrFail( + Predicate.isNotNull, + () => + new ResourceNotFoundError({ + resource: 'SpaceInbox', + id: inboxId, + }), + ), + ); + + return { + inboxId: inbox.id, + isPublic: inbox.isPublic, + authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, + encryptionPublicKey: inbox.encryptionPublicKey, + creationEvent: JSON.parse(inbox.spaceEvent.event) as SpaceEvents.CreateSpaceInboxEvent, + }; + }); const postSpaceInboxMessage = Effect.fn(function* ({ spaceId, @@ -101,149 +118,144 @@ export const layer = Effect.gen(function* () { inboxId: string; message: Messages.RequestCreateSpaceInboxMessage; }) { - // First get the inbox to validate it exists and get auth policy - const spaceInbox = yield* getSpaceInbox({ spaceId, inboxId }); - - // Validate auth policy requirements - switch (spaceInbox.authPolicy) { - case 'requires_auth': - if (!message.signature || !message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress required', - }), - ); - } - break; - case 'anonymous': - if (message.signature || message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress not allowed', - }), - ); - } - break; - case 'optional_auth': - if ( - (message.signature && !message.authorAccountAddress) || - (!message.signature && message.authorAccountAddress) - ) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress must be provided together', - }), - ); - } - break; - default: + // First get the inbox to validate it exists and get auth policy + const spaceInbox = yield* getSpaceInbox({ spaceId, inboxId }); + + // Validate auth policy requirements + switch (spaceInbox.authPolicy) { + case 'requires_auth': + if (!message.signature || !message.authorAccountAddress) { return yield* Effect.fail( new ValidationError({ - field: 'authPolicy', - message: 'Unknown auth policy', + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress required', }), ); - } - - // If signature and account provided, verify authorization - if (message.signature && message.authorAccountAddress) { - // Recover the public key from the signature - const authorPublicKey = yield* Effect.try({ - try: () => Inboxes.recoverSpaceInboxMessageSigner(message, spaceId, inboxId), - catch: () => + } + break; + case 'anonymous': + if (message.signature || message.authorAccountAddress) { + return yield* Effect.fail( new ValidationError({ - field: 'signature', - message: 'Invalid signature', + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress not allowed', }), - }); - - // Check if this public key corresponds to a user's identity - const authorIdentity = yield* getAppOrConnectIdentity({ - accountAddress: message.authorAccountAddress, - signaturePublicKey: authorPublicKey, - }) - .pipe( - Effect.catchAll(() => - Effect.fail( - new AuthorizationError({ - message: 'Not authorized to post to this inbox', - accountAddress: message.authorAccountAddress, - }), - ), - ), ); - - if (authorIdentity.accountAddress !== message.authorAccountAddress) { + } + break; + case 'optional_auth': + if ( + (message.signature && !message.authorAccountAddress) || + (!message.signature && message.authorAccountAddress) + ) { return yield* Effect.fail( + new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress must be provided together', + }), + ); + } + break; + default: + return yield* Effect.fail( + new ValidationError({ + field: 'authPolicy', + message: 'Unknown auth policy', + }), + ); + } + + // If signature and account provided, verify authorization + if (message.signature && message.authorAccountAddress) { + // Recover the public key from the signature + const authorPublicKey = yield* Effect.try({ + try: () => Inboxes.recoverSpaceInboxMessageSigner(message, spaceId, inboxId), + catch: () => + new ValidationError({ + field: 'signature', + message: 'Invalid signature', + }), + }); + + // Check if this public key corresponds to a user's identity + const authorIdentity = yield* getAppOrConnectIdentity({ + accountAddress: message.authorAccountAddress, + signaturePublicKey: authorPublicKey, + }).pipe( + Effect.catchAll(() => + Effect.fail( new AuthorizationError({ message: 'Not authorized to post to this inbox', accountAddress: message.authorAccountAddress, }), - ); - } - } + ), + ), + ); - // Create the message in the database - const createdMessage = yield* use((client) => - client.$transaction(async (prisma) => { - // Double-check the inbox exists and belongs to the correct space - const inbox = await prisma.spaceInbox.findUnique({ - where: { id: inboxId }, - }); - - if (!inbox) { - throw new Error('Space inbox not found'); - } - - if (inbox.spaceId !== spaceId) { - throw new Error('Incorrect space'); - } - - // Create the message - const created = await prisma.spaceInboxMessage.create({ - data: { - spaceInbox: { - connect: { id: inbox.id }, - }, - ciphertext: message.ciphertext, - signatureHex: message.signature?.hex ?? null, - signatureRecovery: message.signature?.recovery ?? null, - authorAccountAddress: message.authorAccountAddress ?? null, - }, - }); - - return { - id: created.id, - ciphertext: created.ciphertext, - signature: - created.signatureHex != null && created.signatureRecovery != null - ? { - hex: created.signatureHex, - recovery: created.signatureRecovery, - } - : undefined, - authorAccountAddress: created.authorAccountAddress ?? undefined, - createdAt: created.createdAt, - } as Messages.InboxMessage; + if (authorIdentity.accountAddress !== message.authorAccountAddress) { + return yield* Effect.fail( + new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, }), - ) + ); + } + } + + // Create the message in the database + const createdMessage = yield* use((client) => + client.$transaction(async (prisma) => { + // Double-check the inbox exists and belongs to the correct space + const inbox = await prisma.spaceInbox.findUnique({ + where: { id: inboxId }, + }); + + if (!inbox) { + throw new Error('Space inbox not found'); + } + + if (inbox.spaceId !== spaceId) { + throw new Error('Incorrect space'); + } + + // Create the message + const created = await prisma.spaceInboxMessage.create({ + data: { + spaceInbox: { + connect: { id: inbox.id }, + }, + ciphertext: message.ciphertext, + signatureHex: message.signature?.hex ?? null, + signatureRecovery: message.signature?.recovery ?? null, + authorAccountAddress: message.authorAccountAddress ?? null, + }, + }); + + return { + id: created.id, + ciphertext: created.ciphertext, + signature: + created.signatureHex != null && created.signatureRecovery != null + ? { + hex: created.signatureHex, + recovery: created.signatureRecovery, + } + : undefined, + authorAccountAddress: created.authorAccountAddress ?? undefined, + createdAt: created.createdAt, + } as Messages.InboxMessage; + }), + ); - // TODO: Broadcast the message (WebSocket functionality would go here) - // broadcastSpaceInboxMessage({ spaceId, inboxId, message: createdMessage }); + // TODO: Broadcast the message (WebSocket functionality would go here) + // broadcastSpaceInboxMessage({ spaceId, inboxId, message: createdMessage }); - return createdMessage; - }); + return createdMessage; + }); return { listPublicSpaceInboxes, getSpaceInbox, postSpaceInboxMessage, } as const; -}).pipe( - Layer.effect(SpaceInboxService), - Layer.provide(DatabaseService.layer), - Layer.provide(IdentityService.layer) -); +}).pipe(Layer.effect(SpaceInboxService), Layer.provide(DatabaseService.layer), Layer.provide(IdentityService.layer)); diff --git a/apps/server-new/src/services/spaces.ts b/apps/server-new/src/services/spaces.ts index 2210f59d..14f6fcbc 100644 --- a/apps/server-new/src/services/spaces.ts +++ b/apps/server-new/src/services/spaces.ts @@ -38,11 +38,18 @@ export interface AddAppIdentityToSpacesParams { spacesInput: Messages.RequestConnectAddAppIdentityToSpaces['spacesInput']; } -export class SpacesService extends Context.Tag('SpacesService') Effect.Effect; - readonly createSpace: (params: CreateSpaceParams) => Effect.Effect<{ id: string }, SpaceEvents.ApplyError | DatabaseService.DatabaseError>; - readonly addAppIdentityToSpaces: (params: AddAppIdentityToSpacesParams) => Effect.Effect; -}>() {} +export class SpacesService extends Context.Tag('SpacesService')< + SpacesService, + { + readonly listByAccount: (accountAddress: string) => Effect.Effect; + readonly createSpace: ( + params: CreateSpaceParams, + ) => Effect.Effect<{ id: string }, SpaceEvents.ApplyError | DatabaseService.DatabaseError>; + readonly addAppIdentityToSpaces: ( + params: AddAppIdentityToSpacesParams, + ) => Effect.Effect; + } +>() {} export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; @@ -75,92 +82,91 @@ export const layer = Effect.gen(function* () { }, }, }, - })); - - return spaces.map((space) => ({ - id: space.id, - infoContent: Utils.bytesToHex(space.infoContent), - infoAuthorAddress: space.infoAuthorAddress, - infoSignatureHex: space.infoSignatureHex, - infoSignatureRecovery: space.infoSignatureRecovery, - name: space.name, - appIdentities: space.appIdentities.map((appIdentity) => ({ - appId: appIdentity.appId, - address: appIdentity.address, + }), + ); + + return spaces.map((space) => ({ + id: space.id, + infoContent: Utils.bytesToHex(space.infoContent), + infoAuthorAddress: space.infoAuthorAddress, + infoSignatureHex: space.infoSignatureHex, + infoSignatureRecovery: space.infoSignatureRecovery, + name: space.name, + appIdentities: space.appIdentities.map((appIdentity) => ({ + appId: appIdentity.appId, + address: appIdentity.address, + })), + keyBoxes: space.keys + .filter((key) => key.keyBoxes.length > 0) + .map((key) => ({ + id: key.id, + ciphertext: key.keyBoxes[0].ciphertext, + nonce: key.keyBoxes[0].nonce, + authorPublicKey: key.keyBoxes[0].authorPublicKey, })), - keyBoxes: space.keys - .filter((key) => key.keyBoxes.length > 0) - .map((key) => ({ - id: key.id, - ciphertext: key.keyBoxes[0].ciphertext, - nonce: key.keyBoxes[0].nonce, - authorPublicKey: key.keyBoxes[0].authorPublicKey, - })), - })); - }); + })); + }); const createSpace = Effect.fn(function* (params: CreateSpaceParams) { - const { accountAddress, event, keyBox, infoContent, infoSignatureHex, infoSignatureRecovery, name } = params; - - // Create the getVerifiedIdentity function for space event validation - const getVerifiedIdentity = Effect.fn(function* (accountAddressToFetch: string, publicKey: string) { - // applySpaceEvent is only allowed to be called by the account that is applying the event - if (accountAddressToFetch !== accountAddress) { - return yield* new Identity.InvalidIdentityError(); - } - - const identity = yield* getAppOrConnectIdentity({ - accountAddress: accountAddressToFetch, - signaturePublicKey: publicKey, - }).pipe(Effect.mapError(() => new Identity.InvalidIdentityError())); - - return identity; - }); - - // Validate the space event - const result = yield* SpaceEvents.applyEvent({ - event, - state: undefined, - getVerifiedIdentity, - }); - - const keyBoxId = `${keyBox.id}-${accountAddress}`; - - // Create the space in the database - const spaceEvent = yield* use((client) => - client.spaceEvent.create({ - data: { - event: JSON.stringify(event), + const { accountAddress, event, keyBox, infoContent, infoSignatureHex, infoSignatureRecovery, name } = params; + + // Create the getVerifiedIdentity function for space event validation + const getVerifiedIdentity = Effect.fn(function* (accountAddressToFetch: string, publicKey: string) { + // applySpaceEvent is only allowed to be called by the account that is applying the event + if (accountAddressToFetch !== accountAddress) { + return yield* new Identity.InvalidIdentityError(); + } + + const identity = yield* getAppOrConnectIdentity({ + accountAddress: accountAddressToFetch, + signaturePublicKey: publicKey, + }).pipe(Effect.mapError(() => new Identity.InvalidIdentityError())); + + return identity; + }); + + // Validate the space event + const result = yield* SpaceEvents.applyEvent({ + event, + state: undefined, + getVerifiedIdentity, + }); + + const keyBoxId = `${keyBox.id}-${accountAddress}`; + + // Create the space in the database + const spaceEvent = yield* use((client) => + client.spaceEvent.create({ + data: { + event: JSON.stringify(event), + id: event.transaction.id, + counter: 0, + state: JSON.stringify(result), + space: { + create: { id: event.transaction.id, - counter: 0, - state: JSON.stringify(result), - space: { + infoContent, + infoSignatureHex, + infoSignatureRecovery, + infoAuthorAddress: accountAddress, + name, + members: { + connect: { + address: accountAddress, + }, + }, + keys: { create: { - id: event.transaction.id, - infoContent, - infoSignatureHex, - infoSignatureRecovery, - infoAuthorAddress: accountAddress, - name, - members: { - connect: { - address: accountAddress, - }, - }, - keys: { + id: keyBox.id, + keyBoxes: { create: { - id: keyBox.id, - keyBoxes: { - create: { - id: keyBoxId, - nonce: keyBox.nonce, - ciphertext: keyBox.ciphertext, - authorPublicKey: keyBox.authorPublicKey, - account: { - connect: { - address: accountAddress, - }, - }, + id: keyBoxId, + nonce: keyBox.nonce, + ciphertext: keyBox.ciphertext, + authorPublicKey: keyBox.authorPublicKey, + account: { + connect: { + address: accountAddress, }, }, }, @@ -168,61 +174,59 @@ export const layer = Effect.gen(function* () { }, }, }, - }), - ); + }, + }, + }), + ); - return { id: spaceEvent.id }; - }); + return { id: spaceEvent.id }; + }); const addAppIdentityToSpaces = Effect.fn(function* (params: AddAppIdentityToSpacesParams) { - const { appIdentityAddress, accountAddress, spacesInput } = params; - - yield* use((client) => - client.$transaction(async (prisma) => { - // Update app identity to connect it to spaces - await prisma.appIdentity.update({ - where: { - address: appIdentityAddress, - accountAddress, - }, - data: { - spaces: { - connect: spacesInput.map((space) => ({ id: space.id })), - }, - }, - }); - - // Create key boxes for the app identity - const keyBoxes = spacesInput.flatMap((entry) => { - return entry.keyBoxes.map((keyBox) => { - const keyBoxId = `${keyBox.id}-${appIdentityAddress}`; - - return { - id: keyBoxId, - spaceKeyId: keyBox.id, - ciphertext: keyBox.ciphertext, - nonce: keyBox.nonce, - authorPublicKey: keyBox.authorPublicKey, - accountAddress, - appIdentityAddress, - }; - }); - }); - - await prisma.spaceKeyBox.createMany({ - data: keyBoxes, - }); - }), - ); - }); + const { appIdentityAddress, accountAddress, spacesInput } = params; + + yield* use((client) => + client.$transaction(async (prisma) => { + // Update app identity to connect it to spaces + await prisma.appIdentity.update({ + where: { + address: appIdentityAddress, + accountAddress, + }, + data: { + spaces: { + connect: spacesInput.map((space) => ({ id: space.id })), + }, + }, + }); + + // Create key boxes for the app identity + const keyBoxes = spacesInput.flatMap((entry) => { + return entry.keyBoxes.map((keyBox) => { + const keyBoxId = `${keyBox.id}-${appIdentityAddress}`; + + return { + id: keyBoxId, + spaceKeyId: keyBox.id, + ciphertext: keyBox.ciphertext, + nonce: keyBox.nonce, + authorPublicKey: keyBox.authorPublicKey, + accountAddress, + appIdentityAddress, + }; + }); + }); + + await prisma.spaceKeyBox.createMany({ + data: keyBoxes, + }); + }), + ); + }); return { listByAccount, createSpace, addAppIdentityToSpaces, } as const; -}).pipe( - Layer.effect(SpacesService), - Layer.provide(DatabaseService.layer), - Layer.provide(IdentityService.layer) -); +}).pipe(Layer.effect(SpacesService), Layer.provide(DatabaseService.layer), Layer.provide(IdentityService.layer)); diff --git a/packages/hypergraph/src/identity/types.ts b/packages/hypergraph/src/identity/types.ts index ce0e6b0c..84ca43e4 100644 --- a/packages/hypergraph/src/identity/types.ts +++ b/packages/hypergraph/src/identity/types.ts @@ -1,5 +1,5 @@ -import * as Schema from "effect/Schema"; -import * as Data from "effect/Data"; +import * as Data from 'effect/Data'; +import * as Schema from 'effect/Schema'; export type Storage = { getItem: (key: string) => string | null; From 147a1a3b83f10106a8549d244d09db7e03491ca6 Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Fri, 29 Aug 2025 14:33:08 +0200 Subject: [PATCH 13/14] wip --- apps/server-new/src/http/handlers.ts | 34 +++++------ apps/server-new/src/services/account-inbox.ts | 60 +++++++++---------- apps/server-new/src/services/auth.ts | 10 ++-- apps/server-new/src/services/database.ts | 4 +- apps/server-new/src/services/identity.ts | 2 +- apps/server-new/src/services/privy-auth.ts | 6 +- apps/server-new/src/services/space-inbox.ts | 57 +++++++----------- apps/server-new/src/services/spaces.ts | 11 ++-- .../src/space-events/apply-event.ts | 16 ++--- packages/hypergraph/src/space-events/types.ts | 9 +-- 10 files changed, 96 insertions(+), 113 deletions(-) diff --git a/apps/server-new/src/http/handlers.ts b/apps/server-new/src/http/handlers.ts index 349a4d72..d882c06b 100644 --- a/apps/server-new/src/http/handlers.ts +++ b/apps/server-new/src/http/handlers.ts @@ -27,7 +27,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han return handlers .handle( 'getConnectSpaces', - Effect.fn(function* ({ headers }) { + Effect.fn('getConnectSpaces')(function* ({ headers }) { yield* Effect.logInfo('GET /connect/spaces'); const privyAuthService = yield* PrivyAuthService.PrivyAuthService; @@ -43,7 +43,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'postConnectSpaces', - Effect.fn(function* ({ headers, payload }) { + Effect.fn('postConnectSpaces')(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/spaces'); const privyAuthService = yield* PrivyAuthService.PrivyAuthService; @@ -72,7 +72,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'postConnectAddAppIdentityToSpaces', - Effect.fn(function* ({ headers, payload }) { + Effect.fn('postConnectAddAppIdentityToSpaces')(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/add-app-identity-to-spaces'); const privyAuthService = yield* PrivyAuthService.PrivyAuthService; @@ -95,7 +95,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'postConnectIdentity', - Effect.fn(function* ({ headers, payload }) { + Effect.fn('postConnectIdentity')(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/identity'); const privyAuthService = yield* PrivyAuthService.PrivyAuthService; @@ -165,7 +165,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'getConnectIdentityEncrypted', - Effect.fn(function* ({ headers }) { + Effect.fn('getConnectIdentityEncrypted')(function* ({ headers }) { yield* Effect.logInfo('GET /connect/identity/encrypted'); const privyAuthService = yield* PrivyAuthService.PrivyAuthService; @@ -195,7 +195,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'getConnectAppIdentity', - Effect.fn(function* ({ headers, path: { appId } }) { + Effect.fn('getConnectAppIdentity')(function* ({ headers, path: { appId } }) { yield* Effect.logInfo(`GET /connect/app-identity/${appId}`); const privyAuthService = yield* PrivyAuthService.PrivyAuthService; @@ -226,7 +226,7 @@ const ConnectGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Connect', (han ) .handle( 'postConnectAppIdentity', - Effect.fn(function* ({ headers, payload }) { + Effect.fn('postConnectAppIdentity')(function* ({ headers, payload }) { yield* Effect.logInfo('POST /connect/app-identity'); const privyAuthService = yield* PrivyAuthService.PrivyAuthService; @@ -301,7 +301,7 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h return handlers .handle( 'getWhoami', - Effect.fn(function* ({ headers }) { + Effect.fn('getWhoami')(function* ({ headers }) { yield* Effect.logInfo('GET /whoami'); const authHeader = headers.authorization; @@ -319,7 +319,7 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h ) .handle( 'getConnectIdentity', - Effect.fn(function* ({ urlParams }) { + Effect.fn('getConnectIdentity')(function* ({ urlParams }) { yield* Effect.logInfo('GET /connect/identity', { accountAddress: urlParams.accountAddress }); if (!urlParams.accountAddress) { @@ -345,7 +345,7 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h ) .handle( 'getIdentity', - Effect.fn(function* ({ urlParams }) { + Effect.fn('getIdentity')(function* ({ urlParams }) { yield* Effect.logInfo('GET /identity', urlParams); const identityService = yield* IdentityService.IdentityService; @@ -368,7 +368,7 @@ const IdentityGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Identity', (h // Build params based on what's provided const params = urlParams.signaturePublicKey ? { accountAddress: urlParams.accountAddress, signaturePublicKey: urlParams.signaturePublicKey } - : { accountAddress: urlParams.accountAddress, appId: urlParams.appId! }; + : { accountAddress: urlParams.accountAddress, appId: urlParams.appId as string }; const identity = yield* identityService.getAppOrConnectIdentity(params).pipe(Effect.orDie); @@ -397,7 +397,7 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler return handlers .handle( 'getSpaceInboxes', - Effect.fn(function* ({ path: { spaceId } }) { + Effect.fn('getSpaceInboxes')(function* ({ path: { spaceId } }) { yield* Effect.logInfo(`GET /spaces/${spaceId}/inboxes`); const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; @@ -409,7 +409,7 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler ) .handle( 'getSpaceInbox', - Effect.fn(function* ({ path: { spaceId, inboxId } }) { + Effect.fn('getSpaceInbox')(function* ({ path: { spaceId, inboxId } }) { yield* Effect.logInfo(`GET /spaces/${spaceId}/inboxes/${inboxId}`); const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; @@ -421,7 +421,7 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler ) .handle( 'postSpaceInboxMessage', - Effect.fn(function* ({ path: { spaceId, inboxId }, payload }) { + Effect.fn('postSpaceInboxMessage')(function* ({ path: { spaceId, inboxId }, payload }) { yield* Effect.logInfo(`POST /spaces/${spaceId}/inboxes/${inboxId}/messages`); const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; @@ -439,7 +439,7 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler ) .handle( 'getAccountInboxes', - Effect.fn(function* ({ path: { accountAddress } }) { + Effect.fn('getAccountInboxes')(function* ({ path: { accountAddress } }) { yield* Effect.logInfo(`GET /accounts/${accountAddress}/inboxes`); const accountInboxService = yield* AccountInboxService.AccountInboxService; @@ -451,7 +451,7 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler ) .handle( 'getAccountInbox', - Effect.fn(function* ({ path: { accountAddress, inboxId } }) { + Effect.fn('getAccountInbox')(function* ({ path: { accountAddress, inboxId } }) { yield* Effect.logInfo(`GET /accounts/${accountAddress}/inboxes/${inboxId}`); const accountInboxService = yield* AccountInboxService.AccountInboxService; @@ -463,7 +463,7 @@ const InboxGroupLive = HttpApiBuilder.group(Api.hypergraphApi, 'Inbox', (handler ) .handle( 'postAccountInboxMessage', - Effect.fn(function* ({ path: { accountAddress, inboxId }, payload }) { + Effect.fn('postAccountInboxMessage')(function* ({ path: { accountAddress, inboxId }, payload }) { yield* Effect.logInfo(`POST /accounts/${accountAddress}/inboxes/${inboxId}/messages`); const accountInboxService = yield* AccountInboxService.AccountInboxService; diff --git a/apps/server-new/src/services/account-inbox.ts b/apps/server-new/src/services/account-inbox.ts index 1c250e9e..0ba6c40f 100644 --- a/apps/server-new/src/services/account-inbox.ts +++ b/apps/server-new/src/services/account-inbox.ts @@ -44,7 +44,11 @@ export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; const { getAppOrConnectIdentity } = yield* IdentityService.IdentityService; - const listPublicAccountInboxes = Effect.fn(function* ({ accountAddress }: { accountAddress: string }) { + const listPublicAccountInboxes = Effect.fn('listPublicAccountInboxes')(function* ({ + accountAddress, + }: { + accountAddress: string; + }) { const inboxes = yield* use((client) => client.accountInbox.findMany({ where: { accountAddress, isPublic: true }, @@ -77,7 +81,7 @@ export const layer = Effect.gen(function* () { })); }); - const getAccountInbox = Effect.fn(function* ({ + const getAccountInbox = Effect.fn('getAccountInbox')(function* ({ accountAddress, inboxId, }: { @@ -125,7 +129,7 @@ export const layer = Effect.gen(function* () { }; }); - const postAccountInboxMessage = Effect.fn(function* ({ + const postAccountInboxMessage = Effect.fn('postAccountInboxMessage')(function* ({ accountAddress, inboxId, message, @@ -140,22 +144,18 @@ export const layer = Effect.gen(function* () { switch (accountInbox.authPolicy) { case 'requires_auth': if (!message.signature || !message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress required', - }), - ); + return yield* new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress required', + }); } break; case 'anonymous': if (message.signature || message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress not allowed', - }), - ); + return yield* new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress not allowed', + }); } break; case 'optional_auth': @@ -163,21 +163,17 @@ export const layer = Effect.gen(function* () { (message.signature && !message.authorAccountAddress) || (!message.signature && message.authorAccountAddress) ) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress must be provided together', - }), - ); + return yield* new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress must be provided together', + }); } break; default: - return yield* Effect.fail( - new ValidationError({ - field: 'authPolicy', - message: 'Unknown auth policy', - }), - ); + return yield* new ValidationError({ + field: 'authPolicy', + message: 'Unknown auth policy', + }); } // If signature and account provided, verify authorization @@ -208,12 +204,10 @@ export const layer = Effect.gen(function* () { ); if (authorIdentity.accountAddress !== message.authorAccountAddress) { - return yield* Effect.fail( - new AuthorizationError({ - message: 'Not authorized to post to this inbox', - accountAddress: message.authorAccountAddress, - }), - ); + return yield* new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }); } } diff --git a/apps/server-new/src/services/auth.ts b/apps/server-new/src/services/auth.ts index d6aaa3d4..c1929c45 100644 --- a/apps/server-new/src/services/auth.ts +++ b/apps/server-new/src/services/auth.ts @@ -20,11 +20,11 @@ export class AuthService extends Context.Tag('AuthService')< /** * Auth service implementation */ -export const makeAuthService = Effect.fn(function* () { +export const makeAuthService = Effect.fn('makeAuthService')(function* () { const config = yield* Config.privyConfig; const privy = new PrivyClient(config.appId, Redacted.value(config.appSecret)); - const verifyAuthToken = Effect.fn(function* (token: string) { + const verifyAuthToken = Effect.fn('verifyAuthToken')(function* (token: string) { const user = yield* Effect.tryPromise({ try: () => privy.getUser({ idToken: token }), catch: (error) => new Error(`Failed to verify auth token: ${error}`), @@ -37,7 +37,7 @@ export const makeAuthService = Effect.fn(function* () { return { userId: user.id }; }); - const verifySessionToken = Effect.fn(function* (_token: string) { + const verifySessionToken = Effect.fn('verifySessionToken')((_token: string) => { // TODO: Implement session token verification logic // This would typically involve: // 1. Decoding the JWT token @@ -46,14 +46,14 @@ export const makeAuthService = Effect.fn(function* () { // 4. Extracting the address // For now, return a placeholder - return { address: 'placeholder' }; + return Effect.succeed({ address: 'placeholder' }); }); return { privy, verifyAuthToken, verifySessionToken, - } as const; + }; }); /** diff --git a/apps/server-new/src/services/database.ts b/apps/server-new/src/services/database.ts index 51748b02..8f7b7938 100644 --- a/apps/server-new/src/services/database.ts +++ b/apps/server-new/src/services/database.ts @@ -40,12 +40,12 @@ export const layer = Layer.scoped( (client) => Effect.tryPromise(() => client.$disconnect()).pipe(Effect.ignore), ); - const use = Effect.fn(function* (fn: (client: PrismaClient, signal: AbortSignal) => Promise) { + const use = Effect.fn('databaseUse')(function* (fn: (client: PrismaClient, signal: AbortSignal) => Promise) { return yield* Effect.tryPromise({ try: (signal) => fn(client, signal), catch: (cause) => new DatabaseError({ cause }), }) as Effect.Effect; - }) as any; + }); return { client, diff --git a/apps/server-new/src/services/identity.ts b/apps/server-new/src/services/identity.ts index ba2549cf..1bcb329b 100644 --- a/apps/server-new/src/services/identity.ts +++ b/apps/server-new/src/services/identity.ts @@ -26,7 +26,7 @@ export class IdentityService extends Context.Tag('IdentityService')< export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; - const getAppOrConnectIdentity = Effect.fn(function* ( + const getAppOrConnectIdentity = Effect.fn('getAppOrConnectIdentity')(function* ( params: { accountAddress: string; signaturePublicKey: string } | { accountAddress: string; appId: string }, ) { // If we have signaturePublicKey, search by that diff --git a/apps/server-new/src/services/privy-auth.ts b/apps/server-new/src/services/privy-auth.ts index c7324e43..fd5bd3b0 100644 --- a/apps/server-new/src/services/privy-auth.ts +++ b/apps/server-new/src/services/privy-auth.ts @@ -44,7 +44,7 @@ export const layer = Effect.gen(function* () { }); if (!user) { - return yield* Effect.fail(new PrivyTokenError({ message: 'Invalid Privy user' })); + return yield* new PrivyTokenError({ message: 'Invalid Privy user' }); } const wallet = user.linkedAccounts.find( @@ -52,7 +52,7 @@ export const layer = Effect.gen(function* () { ) as Wallet | undefined; if (!wallet) { - return yield* Effect.fail(new PrivyTokenError({ message: 'No Privy wallet found' })); + return yield* new PrivyTokenError({ message: 'No Privy wallet found' }); } return wallet.address; @@ -92,7 +92,7 @@ export const layer = Effect.gen(function* () { accountAddress: string, ) { if (!idToken) { - return yield* Effect.fail(new AuthenticationError({ message: 'No Privy ID token provided' })); + return yield* new AuthenticationError({ message: 'No Privy ID token provided' }); } const signerAddress = yield* verifyPrivyToken(idToken); diff --git a/apps/server-new/src/services/space-inbox.ts b/apps/server-new/src/services/space-inbox.ts index 63a9be34..1822b862 100644 --- a/apps/server-new/src/services/space-inbox.ts +++ b/apps/server-new/src/services/space-inbox.ts @@ -109,7 +109,7 @@ export const layer = Effect.gen(function* () { }; }); - const postSpaceInboxMessage = Effect.fn(function* ({ + const postSpaceInboxMessage = Effect.fn('postSpaceInboxMessage')(function* ({ spaceId, inboxId, message, @@ -125,22 +125,18 @@ export const layer = Effect.gen(function* () { switch (spaceInbox.authPolicy) { case 'requires_auth': if (!message.signature || !message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress required', - }), - ); + return yield* new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress required', + }); } break; case 'anonymous': if (message.signature || message.authorAccountAddress) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress not allowed', - }), - ); + return yield* new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress not allowed', + }); } break; case 'optional_auth': @@ -148,21 +144,17 @@ export const layer = Effect.gen(function* () { (message.signature && !message.authorAccountAddress) || (!message.signature && message.authorAccountAddress) ) { - return yield* Effect.fail( - new ValidationError({ - field: 'signature and authorAccountAddress', - message: 'Signature and authorAccountAddress must be provided together', - }), - ); + return yield* new ValidationError({ + field: 'signature and authorAccountAddress', + message: 'Signature and authorAccountAddress must be provided together', + }); } break; default: - return yield* Effect.fail( - new ValidationError({ - field: 'authPolicy', - message: 'Unknown auth policy', - }), - ); + return yield* new ValidationError({ + field: 'authPolicy', + message: 'Unknown auth policy', + }); } // If signature and account provided, verify authorization @@ -182,23 +174,20 @@ export const layer = Effect.gen(function* () { accountAddress: message.authorAccountAddress, signaturePublicKey: authorPublicKey, }).pipe( - Effect.catchAll(() => - Effect.fail( + Effect.catchAll( + () => new AuthorizationError({ message: 'Not authorized to post to this inbox', accountAddress: message.authorAccountAddress, }), - ), ), ); if (authorIdentity.accountAddress !== message.authorAccountAddress) { - return yield* Effect.fail( - new AuthorizationError({ - message: 'Not authorized to post to this inbox', - accountAddress: message.authorAccountAddress, - }), - ); + return yield* new AuthorizationError({ + message: 'Not authorized to post to this inbox', + accountAddress: message.authorAccountAddress, + }); } } diff --git a/apps/server-new/src/services/spaces.ts b/apps/server-new/src/services/spaces.ts index 14f6fcbc..b0a8c121 100644 --- a/apps/server-new/src/services/spaces.ts +++ b/apps/server-new/src/services/spaces.ts @@ -55,7 +55,7 @@ export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; const { getAppOrConnectIdentity } = yield* IdentityService.IdentityService; - const listByAccount = Effect.fn(function* (accountAddress: string) { + const listByAccount = Effect.fn('listByAccount')(function* (accountAddress: string) { const spaces = yield* use((client) => client.space.findMany({ where: { @@ -107,11 +107,14 @@ export const layer = Effect.gen(function* () { })); }); - const createSpace = Effect.fn(function* (params: CreateSpaceParams) { + const createSpace = Effect.fn('createSpace')(function* (params: CreateSpaceParams) { const { accountAddress, event, keyBox, infoContent, infoSignatureHex, infoSignatureRecovery, name } = params; // Create the getVerifiedIdentity function for space event validation - const getVerifiedIdentity = Effect.fn(function* (accountAddressToFetch: string, publicKey: string) { + const getVerifiedIdentity = Effect.fn('getVerifiedIdentity')(function* ( + accountAddressToFetch: string, + publicKey: string, + ) { // applySpaceEvent is only allowed to be called by the account that is applying the event if (accountAddressToFetch !== accountAddress) { return yield* new Identity.InvalidIdentityError(); @@ -182,7 +185,7 @@ export const layer = Effect.gen(function* () { return { id: spaceEvent.id }; }); - const addAppIdentityToSpaces = Effect.fn(function* (params: AddAppIdentityToSpacesParams) { + const addAppIdentityToSpaces = Effect.fn('addAppIdentityToSpaces')(function* (params: AddAppIdentityToSpacesParams) { const { appIdentityAddress, accountAddress, spacesInput } = params; yield* use((client) => diff --git a/packages/hypergraph/src/space-events/apply-event.ts b/packages/hypergraph/src/space-events/apply-event.ts index 147455b2..b26a3c2e 100644 --- a/packages/hypergraph/src/space-events/apply-event.ts +++ b/packages/hypergraph/src/space-events/apply-event.ts @@ -55,7 +55,7 @@ export const applyEvent = ({ return Effect.gen(function* () { const identity = yield* getVerifiedIdentity(event.author.accountAddress, authorPublicKey); if (authorPublicKey !== identity.signaturePublicKey) { - return yield* Effect.fail(new VerifySignatureError()); + return yield* new VerifySignatureError(); } let id = ''; @@ -78,7 +78,7 @@ export const applyEvent = ({ if (event.transaction.type === 'accept-invitation') { // is already a member if (members[event.author.accountAddress] !== undefined) { - return yield* Effect.fail(new InvalidEventError()); + return yield* new InvalidEventError(); } // find the invitation @@ -86,7 +86,7 @@ export const applyEvent = ({ ([, invitation]) => invitation.inviteeAccountAddress === event.author.accountAddress, ); if (!result) { - return yield* Effect.fail(new InvalidEventError()); + return yield* new InvalidEventError(); } const [id, invitation] = result; @@ -102,7 +102,7 @@ export const applyEvent = ({ } else { // check if the author is an admin if (members[event.author.accountAddress]?.role !== 'admin') { - return yield* Effect.fail(new InvalidEventError()); + return yield* new InvalidEventError(); } if (event.transaction.type === 'delete-space') { @@ -111,11 +111,11 @@ export const applyEvent = ({ invitations = {}; } else if (event.transaction.type === 'create-invitation') { if (members[event.transaction.inviteeAccountAddress] !== undefined) { - return yield* Effect.fail(new InvalidEventError()); + return yield* new InvalidEventError(); } for (const invitation of Object.values(invitations)) { if (invitation.inviteeAccountAddress === event.transaction.inviteeAccountAddress) { - return yield* Effect.fail(new InvalidEventError()); + return yield* new InvalidEventError(); } } @@ -124,7 +124,7 @@ export const applyEvent = ({ }; } else if (event.transaction.type === 'create-space-inbox') { if (inboxes[event.transaction.inboxId] !== undefined) { - return yield* Effect.fail(new InvalidEventError()); + return yield* new InvalidEventError(); } inboxes[event.transaction.inboxId] = { inboxId: event.transaction.inboxId, @@ -135,7 +135,7 @@ export const applyEvent = ({ }; } else { // state is required for all events except create-space - return yield* Effect.fail(new InvalidEventError()); + return yield* new InvalidEventError(); } } } diff --git a/packages/hypergraph/src/space-events/types.ts b/packages/hypergraph/src/space-events/types.ts index 85e20536..cdb1c75f 100644 --- a/packages/hypergraph/src/space-events/types.ts +++ b/packages/hypergraph/src/space-events/types.ts @@ -3,6 +3,7 @@ import * as Schema from 'effect/Schema'; import type { InvalidIdentityError } from '../identity/types.js'; import { InboxSenderAuthPolicy } from '../inboxes/types.js'; import { SignatureWithRecovery } from '../types.js'; +import * as Data from "effect/Data"; export const EventAuthor = Schema.Struct({ accountAddress: Schema.String, @@ -126,12 +127,8 @@ export const Author = Schema.Struct({ export type Author = Schema.Schema.Type; -export class VerifySignatureError { - readonly _tag = 'VerifySignatureError'; -} +export class VerifySignatureError extends Data.TaggedError('VerifySignatureError') {} -export class InvalidEventError { - readonly _tag = 'InvalidEventError'; -} +export class InvalidEventError extends Data.TaggedError('InvalidEventError') {} export type ApplyError = ParseError | VerifySignatureError | InvalidEventError | InvalidIdentityError; From 6f8e89f03caf06d2f0ee33d2bdcfc162c4688d22 Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Fri, 29 Aug 2025 14:35:15 +0200 Subject: [PATCH 14/14] wip --- packages/hypergraph/src/space-events/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hypergraph/src/space-events/types.ts b/packages/hypergraph/src/space-events/types.ts index cdb1c75f..99fc05a8 100644 --- a/packages/hypergraph/src/space-events/types.ts +++ b/packages/hypergraph/src/space-events/types.ts @@ -1,9 +1,9 @@ +import * as Data from 'effect/Data'; import type { ParseError } from 'effect/ParseResult'; import * as Schema from 'effect/Schema'; import type { InvalidIdentityError } from '../identity/types.js'; import { InboxSenderAuthPolicy } from '../inboxes/types.js'; import { SignatureWithRecovery } from '../types.js'; -import * as Data from "effect/Data"; export const EventAuthor = Schema.Struct({ accountAddress: Schema.String,