# DashCaddy License Server

Production billing and license fulfillment for purchases made on [dashcaddy.net](https://dashcaddy.net).

Requires **Node.js 22.5 or newer** for the built-in `node:sqlite` transactional store.

## What it does

- Creates Stripe Checkout sessions for one-time purchases and auto-renewing subscriptions.
- Verifies signed Stripe webhooks with durable event idempotency.
- Creates one stable DashCaddy license key per customer and extends that key on later purchases or renewals.
- Tracks one-time and subscription paid-through components separately, so subscription failure cannot erase valid one-time access.
- Emails the exact key accepted by the validation API and tracks each renewal email per Stripe invoice.
- Keeps subscriptions active through their paid period; failed renewals receive a seven-day grace period.
- Supports one-machine activation and deactivation.
- Migrates the previous JSON store into a transactional SQLite database on first startup.

## Plans

| Plan | One-time / renewal amount | Subscription interval |
|---|---:|---:|
| `premium_30d` | $20 | 1 month |
| `premium_90d` | $50 | 3 months |
| `premium_180d` | $70 | 6 months |
| `premium_365d` | $99 | 1 year |

Stripe remains the billing source of truth. SQLite is the entitlement and fulfillment source of truth.

Server-managed keys use DashCaddy's HMAC-compatible code format for initial activation, but renewed expiry is authoritative online because a stable signed code cannot encode changing renewal dates. The DashCaddy client refreshes server-managed entitlements from the license server and does not create a new activation through offline fallback when that server is configured.

## Environment

```env
PORT=3010
APP_BASE_URL=https://licenses.dashcaddy.net
DASHCADDY_WEBSITE_URL=https://dashcaddy.net
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
DASHCADDY_LICENSE_SECRET=64_hex_characters_shared_with_dashcaddy
DATA_DIR=./data
ADMIN_TOKEN=generate-a-long-random-token

SMTP_HOST=mail.sami-ahmed.net
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USERNAME=licenses@dashcaddy.net
SMTP_PASSWORD=...
SMTP_FROM=licenses@dashcaddy.net
# Keep certificate verification enabled in production.
SMTP_TLS_REJECT_UNAUTHORIZED=true
```

## Public HTTP endpoints

- `GET /health`
- `GET /api/public/config`
- `GET /api/public/plans`
- `POST /api/checkout/one-time`
- `POST /api/checkout/subscription`
- `GET /api/checkout/session/:sessionId`
- `POST /api/stripe/webhook`
- `POST /api/license/validate`
- `POST /api/license/deactivate`

`GET /api/admin/debug/store` requires `Authorization: Bearer $ADMIN_TOKEN` and is hidden with a 404 when no admin token is configured.

## Checkout request

```json
{
  "planCode": "premium_30d",
  "customerEmail": "buyer@example.com"
}
```

A successful checkout request returns a Stripe-hosted `url` and a `sessionId`. The website redirects to Stripe. After payment, Stripe calls the webhook, the license is created or extended, SMTP delivery is recorded, and the website polls `/api/checkout/session/:sessionId` to display the same key sent by email.

## Safety properties

- Checkout accepts only valid plan codes and email addresses.
- Browser CORS is restricted to `dashcaddy.net` and `www.dashcaddy.net`.
- Checkout creation is rate limited.
- Webhook event IDs and payment intent IDs are idempotent.
- Store changes use SQLite transactions and WAL durability.
- Repeated checkout sessions are stored independently.
- Admin customer/license data is not public.
- SMTP certificate verification is enabled by default.

An email marked `delivered` means the configured SMTP provider accepted it; final inbox placement remains the receiving mail system's responsibility. The success page also displays the same valid key, so fulfillment does not depend on inbox delivery.

## Test

```bash
npm test
```

The tests cover CORS, invalid checkout input, admin isolation, exact-key lookup and validation, payment idempotency, rate limiting, durable webhook claims, repeat checkout lookup, cancellation, and payment-grace expiry.
