Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ Instructions for AI coding agents working in this repository.
- `src/platform-store.ts`: scoped records, serialized writes and encryption
- `src/backups.ts`: encrypted jobs, schedules, downloads and verification
- `src/postgres-archive.ts`: bounded portable application-data archives for serverless hosts
- `src/token-contract.ts`: legacy v4/v2, opt-in import-license v5/trial v3, and separate recovery v1 schemas
- `src/token-contract.ts`: clean v1 license, trial and separate recovery schemas
- `src/import-licensing.ts`: opt-in cumulative allocations, atomic renewal sales, account ownership and approved recovery
- `src/db.ts`: dual-engine historical ledger repository and migrations
- `src/db.ts`: current dual-engine scoped ledger, audit, backups and fresh v1 schema
- `src/sign.ts`: Ed25519 signing and token helpers
- `src/auth.ts`: Better Auth setup for the console (admin plugin, role backfill)
- `src/permissions.ts`: operator roles (admin/support) and auth.admin feature scope
Expand Down Expand Up @@ -73,7 +73,7 @@ Instructions for AI coding agents working in this repository.
- Do not change token signing format without coordinating client verification changes.
- Preserve fail-closed behavior when signing key is missing.
- Token payload changes require a version bump (`v`) and a compat story.
- Import licensing is explicit per-product opt-in. Never silently adopt a legacy license, infer zero from missing local usage, refill on payment/refresh, or accept a recovery artifact as a license. Server mutations and licensing events share the existing transaction; SDK import mutations require host-supplied atomic CAS and a shared media transaction. Preserve unknown lifetime history as unknown after recovery.
- Import licensing is explicit per-product opt-in. There are no previous-schema imports or token/state conversion paths. Never infer zero from missing local usage, refill on payment/refresh, or accept a recovery artifact as a license. Server mutations and licensing events share the existing transaction; SDK import mutations require host-supplied atomic CAS and a shared media transaction. Preserve unknown lifetime history as unknown after recovery.
- Run `vp run typecheck` and `vp run test` after code changes before proposing completion.
- Config schema changes must keep `packages/create-keyforge` generating valid
output; `test/create-keyforge.test.ts` is what catches it.
Expand Down
11 changes: 4 additions & 7 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Vercel-only internal worker: `GET /api/internal/backups/tick` requires `Authoriz

For dependent apps, use the [client OpenAPI document](client-openapi.json) and [TypeScript SDK guide](../packages/keyforge-client/README.md). The machine-readable contract covers current public routes and explicitly separates installation, private-request and trusted-backend credentials. Its request/response examples are exercised against the server by `test/client-sdk.test.ts`; regenerate with `vp run docs:client-api`.

This is the fresh product-scoped contract. Legacy unscoped routes are removed. License tokens are v4; trial tokens are v2. All timestamps are ISO 8601. API responses are non-cacheable.
This is the fresh product-scoped v1 contract. License, trial and usage-recovery tokens are v1; no older formats are accepted. All timestamps are ISO 8601. API responses are non-cacheable.

## Authentication and roles

Expand All @@ -21,7 +21,7 @@ Better Auth endpoints live under /api/auth/* (GET/POST): sign-in/email, sign-out
Product IDs are lowercase slugs. Definition IDs permit letters, digits, underscore, dot and hyphen (up to 64). Updates to existing catalog, product, meter, schedule and support records require their current integer revision; stale values return revision_conflict. Archive records instead of deleting data used by history.

- Product create: id/name; patch: name/archived/graceDays/trial/revision. Trial contains enabled, nullable nonnegative integer allowance and nullable positive durationDays.
- Definition PUT: id/type/description/archived, optional defaultValue/minimum/maximum/revision. Types are number/string/boolean/mixed; mixed legacy definitions must be resolved before new grants.
- Definition PUT: id/type/description/archived, optional defaultValue/minimum/maximum/revision. Types are number/string/boolean.
- Plan PUT: id/description/entitlements/durationDays/activationLimit/archived/revision, price (integer santim or null) and currency (ETB). Configure a price before issuance. Entitlements must match active definitions. durationDays is positive or null.
- Catalog import: catalog:{version:1,definitions,plans}, preview boolean; apply also requires the preview fingerprint. Export excludes secrets and licenses.
- License POST: requestId UUID and plan required; issuedTo, installationId or licenseRequestId, subjectKind installation/service, planRevision, expectedPrice optional. Optional payment:{date,method,reference} records the full plan price received on today’s UTC date. Terms cannot be overridden. Atomically returns HTTP 201 with license/key, immutable ETB sale and delivery status. Exact retries reuse the request ID; changed payloads conflict. No price means issuance is refused.
Expand Down Expand Up @@ -88,7 +88,6 @@ The table is generated from an isolated server's route registration. Product-sco
| POST | `/admin/products/:productId/licenses/:id/deactivate` |
| GET | `/admin/products/:productId/licenses/:id/delivery` |
| GET | `/admin/products/:productId/licenses/:id/import-account` |
| POST | `/admin/products/:productId/licenses/:id/import-account` |
| POST | `/admin/products/:productId/licenses/:id/key` |
| POST | `/admin/products/:productId/licenses/:id/renew` |
| POST | `/admin/products/:productId/licenses/:id/replace-key` |
Expand Down Expand Up @@ -196,7 +195,6 @@ Domain errors use error/message/retryable/serverTime; rate limiting may include
| `idempotency_conflict` |
| `import_account_missing` |
| `import_account_required` |
| `import_migration_required` |
| `initial_sale_exists` |
| `installation_unauthorized` |
| `insufficient_role` |
Expand All @@ -214,7 +212,6 @@ Domain errors use error/message/retryable/serverTime; rate limiting may include
| `invalid_request_secret` |
| `invalid_support_request` |
| `key_already_staged` |
| `key_mismatch` |
| `key_required` |
| `license_` |
| `license_expired` |
Expand Down Expand Up @@ -300,7 +297,7 @@ Financial routes require an admin session or privileged admin bearer token; supp

Sale and entry UUIDs provide idempotency: retries with identical normalized payloads return the original record; changed payloads return `idempotency_conflict`. Dates cannot be future-dated, and entries cannot precede their sale. Overpayments, excessive credits, and refunds exceeding the referenced receipt are rejected. Historical daily balances cannot become negative through backdating. Financial records are append-only through these APIs; corrections retain history using refunds, credits and new sale records.

A cash refund reopens the amount owed. A credit reduces the agreed charge; when cancelling a paid charge, record its refund then credit. This distinguishes money returned from debt cancelled. A zero-value sale is `complimentary`, never evidence of cash received. Legacy licenses have no sales record until explicitly recorded. Default price changes do not rewrite sale snapshots. Renewals record a new sale; expiry changes remain a separate explicit licensing action. New issuance atomically records a plan-priced sale, unpaid unless a payment is supplied. Existing licenses without sales remain unchanged. No token payload, signing, activation or validation behavior changes.
A cash refund reopens the amount owed. A credit reduces the agreed charge; when cancelling a paid charge, record its refund then credit. This distinguishes money returned from debt cancelled. A zero-value sale is `complimentary`, never evidence of cash received. Every issued v1 license has a linked sales snapshot. Default price changes do not rewrite sale snapshots. Renewals record a new sale; expiry changes remain a separate explicit licensing action. New issuance atomically records a plan-priced sale, unpaid unless a payment is supplied. Existing licenses without sales remain unchanged. No token payload, signing, activation or validation behavior changes.

New errors: `future_date`, `initial_sale_exists`, `unknown_sale`, `invalid_date`, `overpayment`, `reason_required`, `excess_credit`, `invalid_refund`, `invalid_payment_reference`, `historical_overpayment`. These are validation errors (400), except unknown sale (404) and initial sale conflict (409).

Expand All @@ -322,7 +319,7 @@ Notifications are a **pull-based current-attention feed**, not background email

Dependent apps must poll the authenticated notification endpoint, render unread items, open the existing support thread API, and POST read markers. Preference controls can use the corresponding PUT endpoint. Replying continues through the existing installation support message endpoint. Preinstallation requests now use a private status page; no email transport is introduced. A saved operator reply is available for retrieval; it is not falsely labelled delivered or read.

All new data uses versioned `platform_records` kinds (`price`, `sale`, `financial_entry`, `conversation_read`, `notification_read`, `notification_preferences`) and existing SQLite/Postgres transaction locking. No SQL schema migration or rewrite of legacy licenses/tickets is required. Existing full database backups include these records. Very large datasets may later warrant indexed relational tables; current list views are paginated and exports include all filtered records.
All new data uses versioned `platform_records` kinds (`price`, `sale`, `financial_entry`, `conversation_read`, `notification_read`, `notification_preferences`) and existing SQLite/Postgres transaction locking. The fresh v1 schema does not import previous development databases. Existing full database backups include these records. Very large datasets may later warrant indexed relational tables; current list views are paginated and exports include all filtered records.

## Notification history and bulk controls

Expand Down
8 changes: 4 additions & 4 deletions docs/backups.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

Admins use **Settings → Backups** to configure destinations, enable independent daily UTC schedules, run **Back up now**, download archives, inspect upload results and verify recovery. All features are available in the self-hosted open-source server. The backup API and console require admin access.

On Vercel, follow [the serverless deployment and recovery guide](vercel.md). That mode requires durable remote archives, supports private Vercel Blob configured in Settings (or imported once from `BLOB_READ_WRITE_TOKEN`), uses portable PostgreSQL archives, and replaces the continuous worker with authenticated hosted invocations. Local-only/custom upload-only destinations are disabled. Native-tool and persistent-directory instructions below describe persistent-server mode.
On Vercel, follow [the serverless deployment and recovery guide](vercel.md). That mode requires durable remote archives, supports private Vercel Blob configured in Settings, uses portable PostgreSQL archives, and replaces the continuous worker with authenticated hosted invocations. Local-only/custom upload-only destinations are disabled. Native-tool and persistent-directory instructions below describe persistent-server mode.

## Bring your own storage (BYOC)

Use **Configure storage** in Settings → Backups for private Vercel Blob, S3, R2 or UploadThing. Enter dedicated upload/read/delete credentials, save, then choose **Test connection**. The test uploads random verification bytes, downloads and compares them, then deletes the test object. It does not prove database recovery; also create and verify an archive. Credentials are never returned by the settings API; blank secret fields retain their saved values. Nonsecret bucket/region/endpoint fields remain editable. Only admins may configure or test storage.

Provider settings are stored as versioned, authenticated ciphertext in the application database, using the external **data** keyring. Jobs pin the credential version used when created; later edits do not retarget older archives. Retain old credentials at the provider while those archives are needed. The **backup** keyring still encrypts archives and remains external, as do signing/authentication keys. Application reset retains storage configuration so its verified backup remains accessible. Keep independent recovery copies of provider credentials and keyrings: a completely unavailable database cannot supply its own recovery credentials.

On upgrade, explicitly configured environment credentials are imported once, without overwriting existing database settings. This requires a data keyring. After an upload/download/delete test and archive verification, remove the imported variables and redeploy; future changes need no restart. Automatic cloud credential chains remain a compatibility fallback, and are not copied from temporary instance roles into the database.
Configure provider credentials in Settings for durable, versioned jobs. Deployment-supplied provider defaults remain optional for operators using managed instance credentials; they are not imported into database settings. Jobs using those defaults depend on the deployment retaining them. There is no conversion of old jobs or schedules.

Custom upload endpoints accept an optional complete authorization header value directly; it is encrypted in the database too. Legacy environment references remain accepted for existing installations. These endpoints still require a persistent local archive and are unavailable on ephemeral Vercel hosts.
Custom upload endpoints accept a complete authorization header value, encrypted in the database. They require a persistent local archive and are unavailable on ephemeral Vercel hosts.

## Legacy environment imports
## Optional deployment defaults

| Destination | Server configuration |
| --- | --- |
Expand Down
5 changes: 2 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,12 @@ Restore current archives using `vp run backup:restore --file <archive> --job <UU

See [import licensing and recovery](import-licensing.md) before opting a product in. These are explicit admin operations on the selected server; use `--product` and `--env-file` deliberately. No live product is changed by updating this source checkout.

- `vp run license imports-enable --entitlement imports --revision N`: enable future v5 issuance/v3 trials after updated clients are deployed.
- `vp run license imports-enable --entitlement imports --revision N`: enable v1 issuance/trials after updated clients are deployed.
- `vp run license imports-account LICENSE_ID`: inspect cumulative granted units and transactional licensing events; no customer usage is uploaded.
- `vp run license imports-adopt LICENSE_ID --granted N --revision N --reason TEXT --request-id UUID`: explicitly migrate a single-installation historical license without a new financial record. Client history must migrate separately.
- `vp run license renew LICENSE_ID --plan PLAN --mode renewal --request-id UUID`: add the selected plan allowance once and extend time. `--mode top_up` adds units only; `--mode extension` extends time only. Optional `--paid --method TEXT --reference TEXT` records today's full ETB receipt. Retrying the exact UUID/body never creates another sale or allocation.
- `vp run license replace-key LICENSE_ID --reason TEXT --request-id UUID`: replace the key on the same account/license without refilling allowance or creating a sale.
- `vp run license recovery-list`: list recovery requests for independent customer verification.
- `vp run license recovery-approve REQUEST_ID --revision N --mode restore --reason TEXT --request-id UUID`: approve a committed verified-backup restoration for 24 hours. `--mode lost_history` conservatively consumes previously granted allowance and keeps lifetime history unknown. Customer must claim from the authenticated target installation. It does not automatically grant new units.
- `vp run license recovery-reject REQUEST_ID --revision N --reason TEXT --request-id UUID`: reject a pending or approved recovery after independent review; repeated identical requests are safe.

These use the current product-scoped admin API. Existing manual sales creation is rejected for import-account licenses; purchases must use the atomic renewal endpoint. Marking an existing transaction paid remains a financial-only action. Deployed v4 products and existing sessions/records remain unchanged until explicit opt-in/migration.
These use the current product-scoped admin API. Existing manual sales creation is rejected for import-account licenses; purchases must use the atomic renewal endpoint. Marking an existing transaction paid remains a financial-only action. Only fresh v1 development records are supported. Production deployment is a separate operation.
Loading
Loading