Skip to content

Latest commit

 

History

History
79 lines (56 loc) · 3.49 KB

File metadata and controls

79 lines (56 loc) · 3.49 KB

Architecture

Packages

oshrin/
├── api/       Express + ts-rest backend
├── web/       React + Vite frontend (served via nginx in production)
├── shared/    Zod schemas, ts-rest contracts, shared types

shared is consumed by both api and web as @oshrin/shared via pnpm workspace.

Data Model

User
 └── Project (created by user)
      ├── Environment (development, staging, production, ...)
      │    └── Secret (key + AES-256-GCM encrypted value)
      ├── ProjectMember (shared access: editor or viewer)
      └── ProjectInvite (pending invite with token + expiry)
  • Project ownership is tracked via projects.createdBy.
  • Project slugs are unique per user (createdBy + slug).
  • Cascading deletes: User deletion cascades through Project → Environment → Secret. Activity log entries for deleted projects/environments are set to null (preserved for audit).

Authorization

All checks live in authorization.service.ts:

Method Who passes Used for
assertProjectAccess Owner or any member Read operations on project/environments
assertProjectOwner Owner only Delete project, manage invites/members
assertProjectWriteAccess Owner or editor Create/update environments
assertEnvironmentWriteAccess Owner or editor Create/update/delete secrets

Viewers can read everything but cannot modify environments or secrets.

Authentication

Dual-mode JWT authentication in auth.middleware.ts:

  • Cookie mode (web UI): access_token cookie + CSRF token validation on state-changing requests. CSRF comparison uses crypto.timingSafeEqual.
  • Header mode (API clients): Authorization: Bearer <token>. No CSRF needed.

Token lifecycle:

  • Access token: Short-lived JWT with userId, email, tokenVersion
  • Refresh token: Longer-lived JWT with userId, tokenVersion
  • Token versioning: users.tokenVersion is incremented on password change or "logout all". Any token with an old version is rejected.

Encryption

Secret values are encrypted at rest using AES-256-GCM (utils/encryption.ts):

  1. Generate random 64-byte salt + 16-byte IV per secret
  2. Derive key from ENCRYPTION_KEY via PBKDF2 (100k iterations, SHA-512)
  3. Encrypt with AES-256-GCM, store as base64: salt | iv | authTag | ciphertext

This is not a zero-knowledge model — the server can decrypt secrets. See SECURITY.md for tradeoffs.

API Layer

ts-rest contract (shared/)  →  router (api/src/routes/)  →  service  →  repository  →  Drizzle ORM  →  PostgreSQL
  • Contracts (shared/contracts/): Define routes, request/response schemas, and HTTP methods. Shared between frontend and backend.
  • Routers (api/src/routes/): Bind contracts to handlers, apply authenticate middleware.
  • Services (api/src/services/): Business logic, authorization checks, encryption/decryption.
  • Repositories (api/src/repositories/): Reusable database queries via Drizzle ORM.

Key Design Decisions

  • No zero-knowledge encryption. The server derives encryption keys from a master key. This allows server-side search and export at the cost of requiring trust in the server operator.
  • Resource limits are configurable. All limits default to reasonable values but can be disabled (0) for self-hosted instances.
  • Transactions are used sparingly. Only for multi-write operations: email verification, password reset, invite acceptance.