diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f5e95d4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @jongan69 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b51ca93 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,19 @@ +--- +name: Relay or protocol bug +about: Report reproducible backend, wire-contract, storage, or deployment behavior +title: "" +labels: bug +assignees: "" +--- + +## Observed behavior + +## Expected behavior + +## Reproduction + +Include the protocol version, message type, environment, and a minimal redacted request shape. Never include private keys, decrypted messages, raw identity data, or production secrets. + +## Evidence + +Provide random request IDs, timestamps, safe logs, failing test names, or a minimal repository reproduction. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c1910a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/jongan69/OpenDating/security/advisories/new + about: Report security issues privately; never include service keys, decrypted payloads, or personal data in a public issue. + - name: Mobile application issue + url: https://github.com/jongan69/opendating-mobile/issues/new/choose + about: File screens, device behavior, and mobile-client issues in the mobile repository. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..a9acd15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Protocol or relay proposal +about: Propose a contract, storage, reliability, privacy, or operations change +title: "" +labels: enhancement +assignees: "" +--- + +## Problem + +## Proposed outcome + +## Protocol and compatibility impact + +Describe roles, request/result types, versioning, migration behavior, and the mobile coordination required. + +## Privacy, abuse, and operations impact + +## Acceptance evidence diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..847fd94 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + github-actions: + patterns: + - "*" + open-pull-requests-limit: 5 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..48e8a31 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Summary + +## Contract and migration impact + +- [ ] No wire change, or the protocol version/schema/compatibility path is documented +- [ ] Migrations are additive and forward-only +- [ ] Generated protocol artifacts are current + +## Verification + +- [ ] `npm run ci` passes +- [ ] `npm audit --audit-level=low` reports no unresolved vulnerability +- [ ] `npx wrangler deploy --dry-run` passes when deployment configuration changes +- [ ] Protocol package builds and `npm pack --dry-run` contains only intended files +- [ ] No secrets, private keys, raw identity keys, or decrypted payloads are logged or committed +- [ ] Mobile contract changes are coordinated with `jongan69/opendating-mobile` + +## Release and rollback + +Describe deployment order, data migration, monitoring, rollback, and any feature flag or compatibility window. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a5676ac --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing to OpenDating + +Start with `docs/opendating/BACKEND-HANDOFF.md`, then read the architecture, security, privacy, deployment, and protocol documents relevant to the change. + +## Setup + +```bash +npm ci +npm run ci +``` + +Node.js 22 is the supported development and CI runtime. Local Worker development also requires Wrangler and local D1 state. + +## Change rules + +1. Create a branch from protected `main` and use a pull request. +2. Treat `packages/protocol` as the canonical client contract. Do not change request/result shapes without a compatibility and mobile-migration plan. +3. Keep backend implementation code out of the published protocol package. +4. Keep migrations additive and forward-only. +5. Never commit `.dev.vars`, service private keys, data/index keys, decrypted payloads, production exports, or personal data. +6. Do not claim a deployment is current without evidence from the exact commit. + +## Required checks + +```bash +npm run ci +npm audit --audit-level=low +npx wrangler deploy --dry-run + +cd packages/protocol +npm ci +npm run build +npm pack --dry-run +``` + +Lint warnings are tracked debt; new changes should not introduce additional warnings. + +## Documentation + +Update the protocol release note, handoff, deployment instructions, and mobile coordination notes whenever a change affects a service role, request route, migration, secret, binding, retention rule, or production procedure. diff --git a/README.md b/README.md index e1b6789..6ba6e16 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@

- The privacy-first, decentralized dating protocol.
+ A privacy-focused open dating protocol.
Protocol specification, reference relay, and opendating-protocol npm package.

License MIT npm - Relay status + Configured relay endpoint Protocol version

@@ -21,9 +21,9 @@ ## What is OpenDating? -**OpenDating** is a domain-specific protocol built on [Nostr](https://github.com/nostr-protocol/nostr) for private, decentralized dating. It provides: +**OpenDating** is a domain-specific open protocol built on [Nostr](https://github.com/nostr-protocol/nostr) for private dating. The current services are operated by OpenDating on one first-party Cloudflare stack; independent providers and federation are post-GA work. It provides: -- **Self-sovereign identity** — Your account is a cryptographic keypair you own, portable across any OpenDating client +- **Client-held identity** — A cryptographic keypair can be used by compatible clients; portable profile and cross-provider state transfer are not implemented yet - **Location privacy** — Only a 5-character geohash (~5 km precision) is shared; exact coordinates never reach the relay - **Private likes** — One-way likes are encrypted gift wraps visible only to the matcher service; no public reactions - **End-to-end encrypted messaging** — NIP-17 DMs via NIP-44 encryption, enforced match-only by relay policy @@ -31,7 +31,7 @@ The relay is a fork of [Nosflare](https://github.com/Spl0itable/nosflare) v7.9.45 (MIT), hardened and extended for dating-specific privacy and safety requirements. -> **Reference client:** [OpenDating Mobile](https://github.com/jongan69/opendating-mobile) — a production-quality Expo React Native app with Tinder-comparable UX. +> **Reference client:** [OpenDating Mobile](https://github.com/jongan69/opendating-mobile) — a pre-release Expo React Native client. It is explicitly blocked from production while its security, safety, legal, reliability, and marketplace gates remain open.
@@ -51,7 +51,7 @@ The relay is a fork of [Nosflare](https://github.com/Spl0itable/nosflare) v7.9.4 │ └──────────────────────────────────────┘ │ ├──────────────────────────────────────────────┤ │ System │ Profile │ Discovery │ Matcher │ ← Service roles -│ DM Policy │ Moderation │ +│ DM Policy │ Moderation │ Deletion │ └──────────────────────────────────────────────┘ ``` @@ -72,7 +72,7 @@ OpenDating/ │ ├── durable-object.ts # Durable Object mesh for real-time broadcast │ └── opendating/ # OpenDating extension registry ├── schemas/ # JSON Schema definitions -├── tests/ # 82-test suite (unit, integration, security, protocol) +├── tests/ # Unit, integration, security, protocol, and service coverage ├── migrations/ # D1 database migrations ├── docs/ # Protocol documentation ├── examples/ # Client demo scripts @@ -93,11 +93,12 @@ Accept: application/nostr+json | Service | Role | |---|---| | **System** | Health checks (`system.ping`), capability discovery | -| **Profile** | CRUD operations, visibility, pause/resume, verification | +| **Profile** | CRUD operations, visibility, pause/resume | | **Discovery** | Location updates (coarse geohash), preferences, candidate queries | -| **Matcher** | Private one-way likes, match creation, unmatch | -| **DM Policy** | Block/unblock management, relay-enforced message gating | +| **Matcher** | Private one-way likes and match creation | +| **DM Policy** | Block/unblock/unmatch management and relay-enforced message gating | | **Moderation** | Encrypted report submission with optional evidence | +| **Deletion** | Account deletion request and cascade orchestration | All service communication uses NIP-59 gift wraps. Every request carries a unique `request_id` for idempotency. @@ -108,17 +109,16 @@ All service communication uses NIP-59 gift wraps. Every request carries a unique ```bash git clone https://github.com/jongan69/OpenDating.git cd OpenDating -npm install +npm ci # Build the worker npm run build # Run tests -npm test # Full suite (245 tests at 0.1.1 branch cut) +npm test # Full suite (248 tests across 17 files at this handoff) npm run opendating:test:e2e # Protocol conformance tests -# Deploy (requires Cloudflare account + wrangler config) -npm run deploy +# Production deployment is separately gated; see docs/DEPLOYMENT.md. ```
@@ -153,6 +153,10 @@ See the [protocol package README](packages/protocol/README.md) for the full API | [PRD](PRD.md) | Product requirements document | | [Mobile Integration](SPEC.md) | Client integration guide for mobile developers | | [Conformance Tests](tests/opendating/conformance/) | Protocol compliance test suite | +| [Backend Handoff](docs/opendating/BACKEND-HANDOFF.md) | Current boundary, access inventory, blockers, and continuation order | +| [0.1.1 Release Note](docs/opendating/PROTOCOL-0.1.1-RELEASE.md) | Contract repair and publication gate | +| [Security](docs/SECURITY.md) | Current security controls and limitations | +| [Contributing](CONTRIBUTING.md) | Development, validation, privacy, and migration rules |
@@ -164,7 +168,7 @@ See the [protocol package README](packages/protocol/README.md) for the full API - **Gift-wrap privacy protections** — NIP-59 envelope validation - **Query complexity protection** — prevents enumeration attacks - **Extension registry** — domain-specific protocol extensions (OpenDating) -- **Free-tier-aware configuration** — runs on Cloudflare's free plan +- **Cloudflare-native configuration** — Workers, D1, Durable Objects, R2, KV, Queues, and AI bindings - **Database migration system** — versioned D1 schema management
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 3bb4223..5e1e954 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,19 +1,21 @@ # Deployment Guide +> **Production status:** repository CI proves the source builds; it does not prove the configured endpoint runs the same commit. Production deployment remains gated until the live Worker SHA, migrations, bindings, secret names, rollback, and restore evidence are recorded. GitHub `staging` and reviewer-gated `production` environments exist, but deployment workflows and isolated Cloudflare staging resources still need to consume them. + ## Profiles ### Local Development ```bash -npm install +npm ci cp .env.example .env npm run db:migrate:local npm run dev ``` -### Free Production +### Development and limited testing -Target: Cloudflare free tier (5GB D1, 10ms CPU per request) +Cloudflare quotas and product limits change over time. Check the current official limits before sizing or enabling a market; do not treat an old limit copied into this repository as capacity evidence. Configuration: - `RELAY_INFRA_PROFILE=free` @@ -21,9 +23,9 @@ Configuration: - Conservative rate limits - Pay-to-relay disabled -### Paid Production +### Production -Target: Cloudflare Workers Paid plan +Production requires a paid capacity plan validated by load tests, storage projections, alerting, backup/restore drills, and vendor/legal signoff. The current repository does not contain that evidence. Configuration: - Higher CPU limits in wrangler.toml @@ -33,10 +35,10 @@ Configuration: ## Deployment Steps -### 1. Create D1 Database +### 1. Provision an isolated environment ```bash -wrangler d1 create nostr-relay +wrangler d1 create opendating-relay- ``` Update `wrangler.toml` with the database ID. @@ -47,20 +49,28 @@ Update `wrangler.toml` with the database ID. npm run db:migrate:remote ``` -### 3. Set Secrets +### 3. Set every required secret ```bash -wrangler secret put RELAY_PRIVATE_KEY +wrangler secret put OD_INDEX_KEY_V1 +wrangler secret put OD_DATA_KEY_V1 +# Repeat for each OD__SERVICE_PRIVKEY in docs/SECRETS.md. ``` -### 4. Deploy +### 4. Validate without deploying ```bash npm run build -npm run deploy +npm run ci +npm audit --audit-level=low +npx wrangler deploy --dry-run ``` -### 5. Verify +### 5. Deploy through the approved environment + +Production deployment must run from protected `main`, require the GitHub `production` environment approval, record the exact source SHA and migration state, and use environment-specific Wrangler configuration. Direct workstation deployment is not a production handoff procedure. + +### 6. Verify ```bash curl https://your-relay.example.com -H "Accept: application/nostr+json" @@ -68,30 +78,7 @@ curl https://your-relay.example.com -H "Accept: application/nostr+json" ## Wrangler Configuration -```toml -name = "your-relay" -compatibility_date = "2025-01-04" -main = "worker.js" - -[[durable_objects.bindings]] -name = "RELAY_WEBSOCKET" -class_name = "RelayWebSocket" - -[[d1_databases]] -binding = "RELAY_DATABASE" -database_name = "nostr-relay" -database_id = "your-database-id" - -[triggers] -crons = ["0 0 * * *"] - -[limits] -cpu_ms = 30000 # 30s for free tier, 300000 for paid - -[[migrations]] -tag = "v4" -new_sqlite_classes = ["RelayWebSocket"] -``` +`wrangler.toml` is the current first-party configuration and binding inventory. Create explicit environment-specific configurations before staging deployment; never reuse production database, bucket, queue, KV, service keys, or encryption/index keys in development or staging. ## Post-Deployment @@ -100,4 +87,7 @@ new_sqlite_classes = ["RelayWebSocket"] 3. Test EVENT publish + REQ 4. Test NIP-42 auth 5. Monitor Cloudflare analytics -6. Check D1 storage growth +6. Verify OpenDating capabilities and every advertised service identity +7. Verify Queue, AI, media, cache, deletion, and moderation health explicitly +8. Record D1/R2/KV growth and queue age +9. Exercise rollback and restore procedures before public beta diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4ae895b..d309e8a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -2,14 +2,14 @@ ## Prerequisites -- Node.js 20+ +- Node.js 22 - npm - Cloudflare account (for deployment, not required for local dev) ## Quick Start ```bash -npm install +npm ci npm run dev # Start local Worker npm test # Run all tests npm run typecheck # TypeScript check @@ -63,9 +63,10 @@ docs/ Documentation ## Environment Setup -1. Copy `.env.example` to `.env` -2. Configure `wrangler.toml` with your D1 database -3. Run `npm run db:migrate:local` to initialize the database +1. Copy `.env.example` to `.env` for non-secret configuration. +2. Put local-only secrets in gitignored `.dev.vars`; never reuse production keys. +3. Run `npm run db:migrate:local` to initialize the local database. +4. Run `npm run ci` before opening a pull request. ## Testing @@ -94,5 +95,5 @@ npm run test:watch 1. Add tests first 2. Implement in the appropriate module -3. Verify build + typecheck + tests +3. Verify typecheck, lint, build, tests, and the dependency audit 4. Update documentation if needed diff --git a/docs/FOUNDATION-COMPLETE.md b/docs/FOUNDATION-COMPLETE.md index e0024c0..eb646da 100644 --- a/docs/FOUNDATION-COMPLETE.md +++ b/docs/FOUNDATION-COMPLETE.md @@ -1,5 +1,7 @@ # Foundation Phase — Complete +> **Historical baseline:** this August 6 snapshot records completion of an early relay-foundation milestone. Counts, deployment claims, and production implications are not current. Use `docs/opendating/BACKEND-HANDOFF.md` and protected `main` CI for the present state. + > Date: 2026-08-06 ## Architecture Implemented diff --git a/docs/PRIVACY-FOUNDATION.md b/docs/PRIVACY-FOUNDATION.md index f30057c..5199486 100644 --- a/docs/PRIVACY-FOUNDATION.md +++ b/docs/PRIVACY-FOUNDATION.md @@ -22,7 +22,7 @@ Kind 1059 (NIP-59 gift wraps) receive special handling: - Queries are always PRIVATE_NO_CACHE - Auth identity used for rate limiting (not wrapper pubkey) -- Recipient-only access enforcement planned +- Recipient-only access is enforced for gift-wrap queries - Never enters shared/global cache ## Future Protocol Privacy diff --git a/docs/SECRETS.md b/docs/SECRETS.md index 7ebf53e..509d599 100644 --- a/docs/SECRETS.md +++ b/docs/SECRETS.md @@ -18,21 +18,21 @@ wrangler secret list wrangler secret delete ``` -## Required Secrets +## Required OpenDating Secrets | Secret | Purpose | |--------|---------| -| (none yet) | The generic relay requires no additional secrets | - -## Future Secrets (for domain protocols) - -Future domain protocols (e.g., OpenDating) may require: - -| Secret | Purpose | -|--------|---------| -| `SERVICE_PRIVATE_KEY` | Private key for relay service identity | -| `ENCRYPTION_KEY` | Key for at-rest data encryption | -| `ADMIN_API_KEY` | Admin endpoint authentication | +| `OD_INDEX_KEY_V1` | HMAC key for pseudonymous member indexes; at least 32 characters | +| `OD_DATA_KEY_V1` | Encryption key material for protected member data; at least 32 characters | +| `OD_SYSTEM_SERVICE_PRIVKEY` | System service signer | +| `OD_PROFILE_SERVICE_PRIVKEY` | Profile service signer | +| `OD_DISCOVERY_SERVICE_PRIVKEY` | Discovery service signer | +| `OD_MATCHER_SERVICE_PRIVKEY` | Matcher service signer | +| `OD_DM_POLICY_SERVICE_PRIVKEY` | DM policy/block service signer | +| `OD_MODERATION_SERVICE_PRIVKEY` | Moderation service signer | +| `OD_DELETION_SERVICE_PRIVKEY` | Account deletion service signer | + +`OD_ALLOW_DEV_KEYS=true` is a local-development escape hatch, not a production secret. The data/index layer fails closed without its keys, but the current service-identity loader omits roles whose signer is missing and logs a warning. The production deployment workflow must therefore verify that every required role is advertised and fail the release if any signer is absent. ## Secret Handling Rules @@ -48,8 +48,7 @@ Future domain protocols (e.g., OpenDating) may require: The relay has a public key for NIP-11 identification. This is NOT a secret — it's in the public relay info document. -Future service identities (for signing domain protocol events) -will use separate keypairs stored as Cloudflare secrets. +Each active service role uses a separate secp256k1 private key stored as a Cloudflare secret. Its derived public key is advertised through signed relay capabilities. Rotate roles independently with a documented overlap and client capability-refresh plan. ## Development @@ -57,7 +56,10 @@ For local development, use `.dev.vars` (gitignored): ```bash # .dev.vars (never committed) -SERVICE_PRIVATE_KEY=your-dev-key +OD_ALLOW_DEV_KEYS=true +OD_SYSTEM_SERVICE_PRIVKEY=<64-hex-development-key> ``` For production, use `wrangler secret put`. + +Before handoff, transfer secret-management access and record names, owners, creation/rotation dates, and environment placement without exporting values. Development, staging, and production must use different keys. diff --git a/docs/opendating/BACKEND-HANDOFF.md b/docs/opendating/BACKEND-HANDOFF.md index 0470ef3..2a24b8b 100644 --- a/docs/opendating/BACKEND-HANDOFF.md +++ b/docs/opendating/BACKEND-HANDOFF.md @@ -1,285 +1,78 @@ -# Backend Handoff — OpenDating Relay +# OpenDating Relay and Protocol Handoff -**Purpose:** everything the backend still needs so the mobile app can be -finished independently. Work only in this repository -(`/Users/jonathangan/LocalCode/OpenDating`). Do not modify the mobile app — -its contract is fixed and documented below. +Last reviewed: August 9, 2026. ---- +## Handoff status -## Where things stand +The repository is ready for another engineer to clone, test, and continue. It is not a production-complete marketplace backend. Source `main` contains the `opendating-protocol` 0.1.1 deletion/routing repair and a tested relay baseline, but npm publication, current-production deployment evidence, isolated Cloudflare staging resources, and the 0.2 security/operations program remain outstanding. -The relay is deployed and **the core protocol works end to end in -production**, verified against the live worker on 2026-08-07: +Do not infer live deployment state from a green repository build. The exact deployed Worker SHA, migration state, binding inventory, secret versions, restore evidence, and rollback record must be captured separately for every environment. -| Flow | Status | -|---|---| -| NIP-42 auth | ✅ | -| `profile.create` / `update` / `get` | ✅ | -| `discovery.update_location` / `update_preferences` | ✅ | -| `discovery.get_candidates` → real pubkeys, profile content, grants | ✅ | -| `intent.like` → `match_created: true`, `match.list` | ✅ | -| NIP-17 direct messages, correct sender attribution | ✅ | -| Blossom media endpoints (auth enforced, R2 bucket live) | ✅ routes only | -| Workers AI moderation blocking abusive bios | ✅ blocks correctly | +## Repository boundary -Three protocol defects were found and fixed by live testing (see -`git log`): OpenDating was never initialised in the Durable Object, service -responses were persisted but never broadcast, and candidate matching bound -four parameters to five placeholders. +- `packages/protocol` is the canonical backend-independent protocol package. +- `src/protocols/opendating` is the reference service implementation. +- `src/relay-worker.ts`, Durable Objects, D1, R2, KV, Queues, and Workers AI form the current first-party operator stack. +- The mobile reference client lives in `jongan69/opendating-mobile` and must not import backend source. +- Protocol `0.1` remains experimental. Federation and interchangeable operators are post-GA work. -**The one thing that must ship first:** commit `2ca3f52` fixes moderation -rejections never reaching the client. It is committed but **not deployed**. - ---- - -## Your verification tool - -`scripts/od-client.ts` is a scriptable member: it completes NIP-42 and drives -real service calls. Use it to verify every change. It is the fastest way to -prove something works, and it needs no device. - -```bash -# Full flow: profile → location → preferences → candidates → like → matches -npx tsx scripts/od-client.ts verify --name Ava --age 27 --gender woman --geohash dr5ru - -# Seed a member without running discovery (a viewer needs someone to find) -npx tsx scripts/od-client.ts seed --key --name Ben --age 31 --gender man --geohash dr5ru - -# NIP-17 message delivery between two members -npx tsx scripts/od-client.ts dm --from --to - -# Point at a local worker instead of production -OD_RELAY_WS=ws://localhost:8787 OD_RELAY_HTTP=http://localhost:8787 npx tsx scripts/od-client.ts verify -``` - -**Two members are required to test discovery.** A single account always sees -an empty deck — that is correct behaviour, not a bug. Both members need a -profile with an age, a location in the same geohash-5 cell, and preferences -that admit each other. - -### Local development - -```bash -npx tsx scripts/opendating-keys-generate.ts | grep '^OD_' > .dev.vars -echo "OD_ALLOW_DEV_KEYS=true" >> .dev.vars # .dev.vars is git-ignored -npm run db:migrate:local -npx wrangler dev --port 8787 --local -``` - -**Gotcha that will cost you an hour:** the relay drops a `REQ` sent before -NIP-42 completes, silently. Subscribe only after the auth `OK`. A response -that never arrives looks identical to a service that never ran. - ---- - -## Tasks, in order - -### 1. Deploy the moderation rejection fix — BLOCKING - -Commit `2ca3f52` is written and tested but not live. Without it, a member -whose bio is rejected sees a 30-second spinner then "request timed out", with -no idea what was wrong or that they should edit anything. +## Bootstrap and quality gates ```bash -npm run ci && npm run deploy +npm ci +npm run ci +npm audit --audit-level=low +npx wrangler deploy --dry-run + +cd packages/protocol +npm ci +npm run build +npm pack --dry-run ``` -**Verify:** submit a deliberately abusive bio and confirm the client receives -a `content_rejected` error rather than timing out. - ---- - -### 2. Verify `candidate_grant` in `intent.like` — SECURITY - -**Problem:** `src/protocols/opendating/services/matcher/service.ts` accepts -`target_pubkey` and ignores `candidate_grant` entirely (grep returns zero -occurrences). Anyone can like any pubkey they can name, without ever having -been shown that person. - -This defeats the anti-enumeration design: grants exist precisely so that a -viewer can only act on people discovery chose to show them. - -**Do:** in `handleLike`, look up `od_candidate_grants` for -`(viewer_id = , candidate_id = )`, -confirm the row exists, is unexpired, and its `grant_token` equals the -submitted `candidate_grant`. Reject with `invalid_candidate_grant` otherwise — -that error code already maps to a user-facing message in the mobile client -("This profile is no longer available."). - -**Verify:** `od-client.ts verify` should still match; a like with a forged or -absent grant should be rejected. - ---- - -### 3. Fix the N+1 in candidate hydration — PERFORMANCE - -**Problem:** `DiscoveryService.hydrate()` awaits two D1 reads per candidate — -`getPubkeyByMemberId` then `getProfileContentByMemberId` — sequentially. A -20-card page is 40 serialised round trips. This is the hottest path in the -product and will dominate discovery latency. - -**Do:** fetch all granted members in one query joining `od_members` and -`od_profiles`, then decrypt in memory. Decryption is CPU-bound and fine to do -per row; the round trips are the cost. - -**Verify:** a 20-candidate page should issue a single query. Compare wall time -before and after with the harness. - ---- - -### 4. Implement discovery pagination — FUNCTIONAL GAP +The current protected CI baseline is 17 test files and 248 tests. Existing lint warnings are non-failing debt; do not add new warnings. -**Problem:** `getCandidates` hardcodes `cursor: null`. The mobile client sends -a cursor and prefetches the next page when the deck runs low -(`src/features/discovery/use-discovery.ts`), so today it can never page past -the first batch — the deck simply ends. +## Current release boundary -**Do:** return an opaque cursor encoding the last position, and accept it to -resume. The `od_seen_candidates` ledger already prevents repeats, so the -cursor only needs to page within a session's grants. +- npm registry: `opendating-protocol@0.1.0`. +- repository package source: `0.1.1` contract repair. +- publication is blocked until an authenticated npm maintainer publishes the package and verifies the registry tarball. +- mobile must then pin the exact `0.1.1` artifact and remove its temporary routing mirror. +- no GitHub Release or signed production tag currently proves a GA-ready deployment. -**Verify:** request two pages and confirm the second returns different people -and a usable cursor. +## Access that must be transferred ---- +Never put credential values in GitHub issues, documentation, exports, or chat. -### 5. Enforce the daily like quota — FUNCTIONAL GAP +| System | Required access | Handoff requirement | +|---|---|---| +| GitHub | Admin or maintainer | `main` is protected; staging is protected-branch-only and production requires `jongan69` approval | +| Cloudflare | Workers, D1, R2, KV, Queues, AI, DNS, secrets, logs | Inventory development/staging/production resources and record exact owners and jurisdictions | +| npm | Publish rights for `opendating-protocol` | Current local session is unauthenticated | +| Backup account | Encrypted exports and restore credentials | Not yet established or drill-verified | +| Future vendors | Persona, Hive, RevenueCat, Sentry, moderation provider | Not approved to receive production data | -**Problem:** `od_discovery_quotas.daily_likes_sent` exists and the housekeeper -resets it, but nothing ever increments it. Only `daily_candidates_served` is -enforced. Likes are effectively unlimited. +## Current infrastructure caveat -**Do:** increment on each successful `intent.like` and reject over the cap -with `rate_limited` (already mapped client-side to "You've reached your daily -like limit. Come back tomorrow!"). +`wrangler.toml` identifies the existing first-party Worker and production-named bindings. It is not an isolated multi-environment configuration. GitHub staging/production approval environments now exist, but Cloudflare staging resources and deployment workflows that consume those environments still need to be created. Optional Queue, AI, media, and cache bindings currently have degraded behaviors; the production plan requires explicit health failure or feature disablement for required capabilities. ---- +## Immediate continuation order -### 6. Add DB-backed service tests — HIGHEST-VALUE TEST WORK +1. Authenticate npm, publish `opendating-protocol@0.1.1`, inspect the tarball, and coordinate the mobile pin. +2. Inventory the live Cloudflare deployment and compare its Worker SHA, migrations, bindings, secret names, and health behavior with protected `main`. +3. Create isolated Cloudflare development/staging/production resources and GitHub-environment-gated deployment workflows. +4. Verify deletion cascades across delivery events and object storage and return an idempotent receipt. +5. Begin protocol 0.2 schemas, generated validators, signer interface, authenticated route registry, and compatibility handlers. +6. Add backups, restore drills, passive endpoint/failover evidence, load tests, dashboards, alerts, and independent security review before public beta. -**Problem:** 224 tests pass, but **no test ever constructs a service with a D1 -handle.** The suite covers crypto, protocol shapes, and pure helpers. Every -line of SQL in `src/protocols/opendating/services/` is unexercised — which is -exactly how the five-placeholder bug reached production. +## Production data rules -**Do:** add a D1 harness (`@miniflare/d1` or `better-sqlite3` behind the -`D1Database` interface), apply `migrations/run-all.sql`, and test at minimum: +- Never log raw pubkeys, private keys, decrypted messages, exact GPS, profile content, moderation evidence, or vendor payloads. +- Never use deterministic or published test keys against production. +- Never rotate `OD_INDEX_KEY_V1` or `OD_DATA_KEY_V1` without an approved data migration/erasure plan; changing them makes existing protected member records unreadable. +- Never deploy with `OD_ALLOW_DEV_KEYS=true`. +- Treat moderation, deletion, Queue, AI, and media outages as explicit feature-health states; public beta must not silently degrade open. -- discovery excludes self, blocked members in both directions, already-granted - and already-seen candidates -- grants are issued with tokens and the seen ledger is written -- geohash tiers widen p5 → p4 → p3 and stop at the distance preference -- quota exhaustion returns `discovery_quota_exhausted` -- `profile.update` rejects under-18 and persists encrypted content -- a mutual like creates exactly one match - -This is the single most valuable thing on this list after task 1. - ---- - -### 7. End-to-end photo upload — UNVERIFIED - -**Problem:** the Blossom routes are live and enforce auth (verified: 401 -unauthenticated, 404 for a missing blob), but **no authenticated upload has -ever been performed.** The signature verification, hash binding, and R2 write -path are untested against the real bucket. - -**Do:** extend `od-client.ts` with an `upload` command that signs a kind-24242 -authorization and PUTs a real image, then fetches it back and confirms the -bytes match. The mobile client's implementation is -`src/lib/opendating/media.ts` — mirror it. - ---- - -### 8. Clean test data and rotate storage keys — BEFORE REAL USERS - -Live testing created member records on production D1 under **known, -published private keys**: - -| Name | Private key | -|---|---| -| Ava | `0x11…` (32 bytes of `0x11`) | -| Ben | `0x22…` | -| Cara | `0x33…` | -| Dana | `0x44…` (moderation-blocked) | - -**Do:** delete those members and their dependent rows, then rotate -`OD_INDEX_KEY_V1` and `OD_DATA_KEY_V1`. - -**Rotating those keys orphans every existing member row** — member IDs are -HMACs under the index key and stored pubkeys are AES-GCM under the data key. -That is fine now and impossible later, so do it before launch. Wipe the -OpenDating tables in the same pass. - ---- - -## The mobile contract — do not change without coordinating - -The app is built against these shapes. Changing them silently breaks it. - -**`profile.update`** — content travels in the payload; the service stores it -encrypted at rest. It is *not* a Nostr event reference (migration 0011 -replaced `profile_event_id` with `encrypted_profile_payload`). - -```jsonc -{ "profile": { "display_name": "Ava", "age": 27, "gender": "woman", - "bio": "…", "interests": ["…"], "relationship_intent": "long_term", - "prompts": [{"question":"…","answer":"…"}], - "photos": [{"id":"…","url":"https://…","order":0}], "v": "0.1" } } -``` - -**`discovery.get_candidates` result** — each candidate must carry a real -`pubkey`. A like is addressed to `target_pubkey` and a DM is NIP-44 encrypted -to that key, so a pseudonymous member id alone leaves the viewer unable to act -on anyone they are shown. - -```jsonc -{ "candidates": [{ "pubkey": "<64 hex>", - "profile": { "display_name": "…", "age": 27, "…": "…" }, - "distance_bucket": "nearby", - "candidate_grant": "" }], - "cursor": null, "remaining_today": 49 } -``` - -**Errors** use type `system.error` with `{ code, message }`. Codes already -mapped to user-facing copy live in the app's -`src/lib/opendating/errors.ts` — prefer an existing code over inventing one. - -**Missing services degrade gracefully.** The app treats an unadvertised role as -"not available yet" and shows an "Almost ready" screen, so a partial rollout is -safe. - ---- - -## Definition of done - -- [ ] `2ca3f52` deployed; a rejected bio returns `content_rejected`, not a timeout -- [ ] A like without a valid grant is rejected -- [ ] A 20-candidate page issues one query, not forty -- [ ] Two pages of candidates can be fetched with a cursor -- [ ] The daily like cap is enforced -- [ ] Service SQL has test coverage against a real database -- [ ] A photo uploads, is fetched back, and the bytes match -- [ ] Test members removed and storage keys rotated -- [ ] `npm run ci` green (typecheck + build + tests) - ---- - -## Known environment issue (blocks device testing, not the backend) - -`~/Library/Android/sdk` is a symlink to `/Volumes/T9/DevTools/AndroidSDK`, -which is **exFAT**. macOS stores extended attributes there as `._*` AppleDouble -sidecars — 3,030 inside the CMake install. CMake globs -`Modules/Compiler/*.cmake`, hits `._ADSP-DetermineCompiler.cmake`, and tries to -parse a binary resource fork as a script. `react-native-worklets` and -`react-native-screens` both fail to configure, so **no native Android build -succeeds on this machine.** - -```bash -dot_clean -m /Volumes/T9/DevTools/AndroidSDK -``` +## Evidence required for handoff completion -Safe — pure macOS metadata — but they regenerate whenever macOS writes to that -volume. Moving the SDK to the internal APFS disk is the durable fix. Note also -that the APKs in `android/app/build/outputs/` are stale and must not be used. +A production operator handoff needs named owners, access confirmation, a binding/secret-name inventory, data map, deployment and rollback runbook, current dashboards, incident contacts, backup/restore proof, retention jobs, vendor/legal approvals, and an exact release record. This repository provides the technical starting point; those operational artifacts do not yet exist in release-complete form. diff --git a/docs/opendating/BACKEND-V0.1-RELEASE.md b/docs/opendating/BACKEND-V0.1-RELEASE.md index d75a175..baf5d48 100644 --- a/docs/opendating/BACKEND-V0.1-RELEASE.md +++ b/docs/opendating/BACKEND-V0.1-RELEASE.md @@ -1,5 +1,7 @@ # OpenDating Backend v0.1 — Release +> **Historical snapshot:** deployment states and test counts below were recorded on August 6, 2026 and are not proof of the current live Worker or production readiness. The canonical package is `opendating-protocol`; registry `0.1.0` remains current while source `0.1.1` awaits publication. Use `BACKEND-HANDOFF.md` for current status. + > **Date**: 2026-08-06 > **Release Tag**: `opendating-v0.1.0` > **Protocol Version**: `0.1` (experimental) diff --git a/docs/opendating/PROTOCOL-CORE-COMPLETE.md b/docs/opendating/PROTOCOL-CORE-COMPLETE.md index f1b178f..40fc6ae 100644 --- a/docs/opendating/PROTOCOL-CORE-COMPLETE.md +++ b/docs/opendating/PROTOCOL-CORE-COMPLETE.md @@ -1,5 +1,7 @@ # OpenDating Protocol Core — Complete +> **Historical baseline:** this document records an early protocol-core milestone, not current production readiness. Use `docs/opendating/BACKEND-HANDOFF.md`, `docs/opendating/ROADMAP.md`, and protected `main` CI for current status. + > Date: 2026-08-06 > TypeScript: 0 errors | Build: 308KB | Tests: 123 passed