npm package: @riposte.co/server
Description: <p align="center">   <img src="assets/riposte-text.svg" alt="Riposte logo" width="280"> </p>
Latest version: 0.4.6

<p align="center">
  <img src="assets/riposte-text.svg" alt="Riposte logo" width="280">
</p>

# Riposte

**On-premise email sync service - Cloud-API alternative**

Riposte provides a unified API for multiple email providers (Gmail, Microsoft, future IMAP) with 90% cost savings compared to Cloud-APIs. All data stays on your infrastructure.

## 🚀 Quick Start

```bash
# Scaffold your configuration (riposte/sync.config.json + riposte/.env.example)
npx riposte-init

# Launch the daemon with the admin portal
npx riposte-run

# Skip the admin portal
npx riposte-run --no-admin
```

That's it! Your email sync service is running at:

- **API Server**: http://localhost:8080
- **Admin Portal**: Auto-selected port printed on startup (for example http://localhost:3001)

## Installation at a glance

Riposte follows a three-step flow so you always know what comes next:

1. **Initialize** – Run `npx riposte-init` to auto-detect local services, scaffold `riposte/sync.config.json`/`.env.example`, and hand off deeper customization to the admin onboarding wizard.
2. **Launch** – Start the runtime with `npx riposte-run` and watch the structured logs confirm database, queue, and migration health.
3. **Admin** – Visit the onboarding flow at the admin portal URL shown after startup. It now behaves like a linear checklist: choose Google or Microsoft first, paste credentials with the exact scopes we flag, wire databases/queues, and (optionally) authorize a pilot account before copying the API call examples into your app.

Each stage is documented in the [installation guide](docs/INSTALLATION.md), which expands on prerequisites, troubleshooting tips, and day-two operations once you are live.

## 📦 Installation Options

### What ships in the npm tarball?

Running `npm publish --dry-run` (or `npm pack`) lists every file that would be
included in the release. You will see a number of entries under
`admin-dist/.next/standalone/node_modules/…`. Those files come from the
Next.js "standalone" build that powers the bundled admin portal. They are not a
copy of your development `node_modules`; instead they are the trimmed runtime
dependencies that Next.js inlines alongside `server.js` so the `riposte-admin`
CLI can boot without relying on an external installation. The build script
already deletes caches and other transient output, so the additional files are
expected and safe to publish.

### Option 1: Global Installation (Recommended)

```bash
npm install -g @riposte.co/server
riposte-init
riposte-run
```

### Option 2: Local Installation

```bash
npm install @riposte.co/server
npx riposte-init
npx riposte-run
```

### Option 3: Development Setup

```bash
git clone <private-repo>
cd riposte
npm install
npm run setup
npm start
```

## ⚙️ Configuration

### Quick Start (SQLite + In-Memory)

- **Database**: SQLite file (`./riposte.db`)
- **Cache**: In-memory
- **Queues**: In-memory
- **Perfect for**: Trying out, development, small deployments

### Production Setup (PostgreSQL + Redis)

- **Database**: PostgreSQL
- **Cache**: Redis
- **Queues**: Redis
- **Perfect for**: Production, horizontal scaling

### Database Configuration & Migrations

- Set `DATABASE_URL` (or `TEST_DATABASE_URL`) to `file:./path/to.db` or any `sqlite:` URL to run with SQLite.
- Set the same variable to a PostgreSQL connection string (for example `postgresql://user:pass@localhost:5432/riposte`) to run with Postgres.
- The service automatically creates SQLite database files when using `file:` URLs.
- Generate a new migration with `npm run db:generate <migration-name>`. The command compares the current schema to your
  target database and writes the SQL files under `drizzle/sqlite` or `drizzle/postgres` as appropriate.
- Run migrations with `npm run db:migrate`. The script picks the correct migration directory (`drizzle/sqlite` or `drizzle/postgres`) based on the active provider.
- Generated migrations live under `drizzle/sqlite` or `drizzle/postgres` depending on the dialect selected when running `drizzle-kit`.
- Riposte now auto-applies pending migrations during startup. Set `AUTO_APPLY_MIGRATIONS=false` (or
  `riposte.migrations.autoApply` to `false` in `sync.config.json`) if you prefer to run the migration script
  manually.

### Authentication screen

Riposte now ships with a built-in authentication screen that runs alongside your deployment at `/auth/authorize`. The product creates short-lived sessions and returns links that you can hand to end users—no external services required. The screen detects the correct provider based on the email address, renders a consent experience, and drives the entire PKCE exchange on the server.

Terminology to avoid confusion:
- Provider callback URI: The OAuth callback URL registered with Google or Microsoft that Riposte listens on to receive the authorization code (e.g., `http://localhost:8080/providers/gmail/callback`). Configure this under `riposte.oauth.<provider>.redirectUri` or the matching environment variable.
- App redirect URL: The user-facing URL in your product where the browser should land after Riposte finishes authentication (e.g., `https://app.example.com/connect/success`). Configure this under `riposte.app.redirectUrl` or pass it when creating an auth session.

Once the provider completes the handshake, Riposte exchanges the code for tokens, associates the account with the originating session, and redirects the browser to your app redirect URL.

Customize the experience under `riposte.authScreen` inside `riposte/sync.config.json`. All of the values live in source control so you can review branding changes just like any other configuration update:

```jsonc
{
  "riposte": {
    "authScreen": {
      "enabled": true,
      "publicUrl": "https://mail.example.com",
      "brandName": "Acme Support",
      "logoText": "AS",
      "title": "Connect your email",
      "subtitle": "We will securely connect your inbox for {{brand}}.",
      "supportEmail": "support@example.com",
      "submitButtonLabel": "Continue",
      "customCssFile": "./branding/auth-screen.css",
      "cors": {
        "origin": "*",
      },
    },
  },
}
```

Point `customCssFile` at a stylesheet to control colors, fonts, or layout. The file is reloaded whenever it changes so you can iterate quickly in development. When `enabled` is set to `false` the server returns `503` for new session requests to make it clear that the experience has been turned off.

By default the screen is accessible from any origin. Set `riposte.authScreen.cors.origin` to a specific origin (or list of origins) if you need to lock it down for production embeds.

## 🧪 Live provider smoke tests

The repository includes a Vitest suite that exercises real providers end-to-end. Enable it locally by setting `RUN_LIVE_PROVIDER_TESTS=true` and pointing the harness at one or more seeded accounts via either `PROVIDER_TEST_RUNS` (JSON array) or the simpler `PROVIDER_TEST_SENDER_ID`/`PROVIDER_TEST_RECIPIENT_IDS` flags described in `product/src/__tests__/integration/live-provider.integration.test.ts`.

When calendar access is provisioned for the connected account the harness now validates basic calendar CRUD in addition to email send/receive. Specify `PROVIDER_TEST_CALENDAR_ID` to force the test to use a specific calendar; otherwise the first calendar returned by the provider will be used. Temporary events are deleted automatically even if an assertion fails.

## 🔐 Licensing

Riposte now enforces a license key for deployments that connect more than 100 accounts. Without a valid license the server will refuse new authorizations once 100 accounts are connected. A valid license lifts that limit (or applies a custom max if specified) and is verified with an Ed25519 signature at startup.

### Configure license verification

Set the following environment variable (typically inside `riposte/.env`):

| Variable              | Description                                  |
| --------------------- | -------------------------------------------- |
| `RIPOSTE_LICENSE_KEY` | The signed license string issued by Riposte. |

The verifier ships with a baked-in Ed25519 public key (`product/src/common/license-authority.ts`). Riposte ignores runtime overrides unless the process runs under `NODE_ENV=test` so operators cannot self-issue licenses by swapping keys. Unit tests rely on that test-only escape hatch to inject ephemeral key pairs.

> **Note:** If no valid license is configured the server logs a warning at startup and enforces the 100-account cap automatically.

### Generate a signing key pair

Use the bundled helper to create an Ed25519 key pair for signing licenses:

```bash
npm run license -- keypair --output ./license-keys
# or omit --output to print keys to the console only
```

The command prints:

- `riposte-license-private-key.pem` (PKCS8 private key for signing)
- `riposte-license-public-key.pem` (SPKI public key)
- Base64url versions of the public key for reference

After generating a new pair, update `product/src/common/license-authority.ts` and `license/canonical-public-key.raw` with the printed base64url raw public key. Distribute the corresponding private key securely for license issuance; it should never be committed to git.

### Sign a license key

Create licenses with custom expirations and limits using the same helper:

```bash
npm run license -- sign \
  --expires 2025-12-31 \
  --max-accounts 1000 \
  --name "Acme Corp" \
  --id enterprise-2025 \
  --private-key-path ./license-keys/riposte-license-private-key.pem
```

Key details:

- `--expires` accepts ISO-8601 dates or relative durations such as `180d` or `2y`.
- `--max-accounts` is optional; omit it for unlimited accounts on that license.
- `--name` and `--id` embed metadata that appears in server logs when the license is loaded.
- Provide the private key via `--private-key`, `--private-key-path`, or the environment variables `RIPOSTE_LICENSE_PRIVATE_KEY(_PATH)`.

The command prints the signed license payload and the final `RIPOSTE_LICENSE_KEY` value to distribute.

## 🔄 Migration

When you're ready to scale from SQLite to PostgreSQL:

```bash
riposte-migrate --to postgresql
```

This will:

1. Back up your SQLite data
2. Update configuration to PostgreSQL
3. Restart the service
4. Validate the migration

## 🏗️ Architecture

### Core Components

- **API Server**: Fastify-based REST API (port 8080)
- **Admin Portal**: Next.js web interface served from its own HTTP listener (auto-selected port, printed on startup)
- **Database**: SQLite (dev) or PostgreSQL (prod)
- **Cache/Queues**: In-memory (dev) or Redis (prod)

### Supported Providers

- **Gmail**: Full sync, labels, drafts, send
- **Microsoft**: Full sync, folders, drafts, send
- **IMAP**: Mailbox sync and raw message access (bring your own IMAP server)

#### IMAP provider setup

IMAP support is designed for legacy mailboxes that expose username/password or app-specific password credentials. Because IMAP
does not provide OAuth, credentials are stored as provider tokens. To enable IMAP:

1. Install the optional runtime dependencies in the environment where Riposte runs:

   ```bash
   npm install imapflow mailparser
   ```

2. Create an account in the `ProviderAuthToken` table (via the admin UI or directly through the database) using a JSON payload in
   the `accessToken` field. Example:

   ```json
   {
     "host": "imap.example.com",
     "port": 993,
     "secure": true,
     "username": "user@example.com",
     "password": "app-specific-password",
     "mailbox": "INBOX"
   }
   ```

   Additional `tls` options (for example `{"rejectUnauthorized": false}`) can be provided if your server uses a self-signed
   certificate.

3. Queue a historical sync or use the `/sync` endpoints to pull messages. IMAP support currently focuses on read-only
   synchronization and raw message retrieval. Sending, label management, and calendar/contacts are not yet implemented for IMAP
   accounts.

### Key Features

- **Real-time sync**: Push notifications + polling
- **Rate limiting**: Global queue with provider limits
- **Horizontal scaling**: Redis-based distributed system
- **Data sovereignty**: Everything stays on-premise
- **Cost savings**: 90% cheaper than Cloud-APIs

## 📊 Business Value

### Cost Comparison

- **Cloud-APIs**: $1.50 per connected account
- **Riposte**: $99/month unlimited accounts
- **Savings**: 90% cost reduction

### Target Market

- **SMBs**: Priced out of Cloud-APIs
- **Enterprises**: Want cost savings + data control
- **Agencies**: Managing multiple clients
- **Developers**: Building CRMs, ATSs, etc.

## 🔧 Development

### Prerequisites

- Node.js 18+
- npm or yarn
- PostgreSQL (for production)
- Redis (for production)
- [Optional] [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) when running against SQLite (install with `npm install better-sqlite3`)

### Setup

```bash
# Clone the repository
git clone <private-repo>
cd riposte

# Install dependencies
npm install

# Set up development environment
npm run setup

# Start development server
npm run dev
```

### Notifications

Each provider can use either webhook deliveries or polling to detect mailbox changes. Configure the preference in the `notifications` section of your `sync.config.json` (or via the corresponding environment variables). Gmail defaults to webhooks, while Microsoft starts in polling mode so the integration works without hosting a public webhook endpoint. Switch Microsoft to webhooks by setting `notifications.providers.microsoft.mode` to `"webhook"` once you have a reachable HTTPS callback.

The built-in polling engine runs automatically for every provider configured with `mode: "polling"`. Tune its behaviour under `notifications.polling`:

```jsonc
{
  "notifications": {
    "polling": {
      "intervalMs": 60000, // How often Riposte polls each account
      "batchSize": 25, // Messages fetched per account during a cycle
      "initialLookbackMs": 900000, // Look back window for the very first poll (in ms)
      "maxAccountsPerCycle": 10, // Accounts processed per polling loop
    },
  },
}
```

Override the same values via environment variables:

```
NOTIFICATIONS_POLLING_INTERVAL_MS=60000
NOTIFICATIONS_POLLING_BATCH_SIZE=25
NOTIFICATIONS_POLLING_INITIAL_LOOKBACK_MS=900000
NOTIFICATIONS_POLLING_MAX_ACCOUNTS=10
```

Whenever a new account connects we enqueue an immediate polling run so you receive `message.received` events without waiting for the next interval.

### Building for Production

```bash
# Build everything
npm run build

# Start production server
npm start
```

## 📝 API Documentation

Once running, visit:

- **Swagger UI**: http://localhost:8080/docs
- **API Reference**: http://localhost:8080/documentation/json

## 🛡️ Security

- **API Key Authentication**: All endpoints require API key
- **Data Encryption**: Sensitive data encrypted at rest
- **On-premise Only**: No external data sharing
- **Token Management**: Secure OAuth token storage

## 📈 Monitoring

- **Health Check**: `GET /healthz`
- **Metrics**: Built-in performance monitoring
- **Logs**: Structured logging with Pino
- **Alerts**: Configurable error notifications

## 🤝 Support

For support and questions:

- **Documentation**: See `/docs` endpoint
- **Issues**: Contact support team
- **Updates**: `npm update riposte`

## 📄 License

UNLICENSED - Proprietary software

---

**Riposte** - The Cloud-API alternative that saves you 90% on email integration costs.