diff --git a/AGENTS.md b/AGENTS.md index 2a0eb16..f652bf0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2415,6 +2415,26 @@ Rules: - Any time the deploy output isn't verified by the smoke test, run `node scripts/smoke-test.mjs` manually before shipping. +### Trust, privacy and consistency invariants +- **No BFT / no relay quorum.** Nostr events are signed facts; relays are not + voters. Social feeds resolve conflicts locally, not by consensus. +- **Deterministic merge:** `src/lib/eventMerge.ts` is the single merge policy + (newest `created_at`, event-id tie-break, local tombstones). Do not add + "first relay wins" logic; relay arrival order must not change the result. +- **Trust boundaries are documented** in `docs/THREAT_MODEL.md`. Keep UI copy + honest: a confirmed trade means both parties signed; a payment proof means the + money leg was paid; escrow is time-locked, not arbitration. +- **No remote hotlinks for small brand assets:** OpenSats icons are local under + `public/images/` so rendering the footer does not leak the viewer's IP. +- **Android Auto Backup is off** (`android:allowBackup="false"` injected by + `scripts/inject-backup-policy.mjs`); identity/settings/escrow state stays on + the device and must be exported explicitly. +- **Image uploads re-encode decodable formats through canvas** to strip + EXIF/GPS before Blossom. GIF and HEIC/HEIF are passed through because they need + special handling; do not claim they are metadata-stripped. +- **No mandatory identity, deposit or global trust score.** Use warnings, + explicit consent and optional escrow at value boundaries. + ### Distribution without GitHub > **Owner decision (2026-10-30): no external identities.** Distribution must > stay self-hosted and Nostr-based. Do **not** create or ask for GitLab, diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md new file mode 100644 index 0000000..72ad445 --- /dev/null +++ b/docs/THREAT_MODEL.md @@ -0,0 +1,104 @@ +# SkateSpots threat model + +This is the short, technical version of what SkateSpots can and cannot guarantee. +It exists so product copy, support answers and future code do not accidentally +promise consensus, arbitration or delivery verification that the system does not +have. + +## The model in one sentence + +**SkateSpots verifies signed facts, keeps a local view, and uses social/economic +signals at value boundaries. It does not run a consensus protocol.** + +Nostr relays are not voters. A relay can withhold, censor, reorder or serve a +stale event, but it cannot forge a valid signature. Different clients can +therefore see different snapshots of the network without either side being +"wrong" in a Byzantine-consensus sense. + +## Assets + +- Nostr identity keys and local signer access. +- Local cache, drafts, pending posts and local deletion tombstones. +- Crew membership and Cordn group membership. +- Marketplace listings, offers, trade attestations and reputation. +- Cashu proofs and escrow tokens. +- Device storage and any local backup/export. + +## Actors and trust boundaries + +| Actor | Can do | Cannot do | +| --- | --- | --- | +| Author | Sign events with their key | Forge another author's signature | +| Relay | Withhold, censor, reorder, serve stale or conflicting signed events | Forge a valid event; guarantee availability | +| Cordn coordinator | Route opaque group ciphertext; drop or delay messages | Read plaintext; forge an author proof | +| Cashu mint | Issue/redeem proofs, enforce locktime, freeze or become insolvent | Spend a correctly locked P2PK proof without a valid witness | +| Founder/admin | Modify the crew record, moderate crew content | Forge members' signatures; guarantee global agreement | +| Buyer/seller | Publish attestations and payment/receipt claims | Make a false claim true by signing it | +| WebView / host | Run the client | Be assumed trustworthy after device compromise | + +## Guarantees + +- **Authenticity:** a valid Schnorr signature proves which key signed an event, + not that the content is true. +- **Availability:** the client can use cached data and multiple relays; there is + no guarantee that every relay has every event. +- **Local determinism:** when two valid versions of an addressable or replaceable + event exist, the client chooses the newest `created_at`, with an event-id + tie-breaker. This is deterministic, not globally agreed. +- **Deletion:** NIP-09 is a request. Local tombstones prevent a deleted item from + flashing back on that device; relays may ignore the request. +- **Payments:** a Lightning preimage proves the invoice was paid. It does not + prove goods were delivered. +- **Escrow:** a time-locked Cashu P2PK token protects the buyer after the lock + expires and lets the seller redeem on delivery. It is not arbitration and does + not prove delivery. +- **Reputation:** reciprocal signed attestations, distinct partners and + web-of-trust weighting are evidence. Two colluding accounts can still create a + fake trade; the app must not present that as proof of goods. + +## Non-guarantees + +- No global ordering or consensus over relays. +- No protection from a compromised identity key, signer or device. +- No guaranteed delivery or carrier verification. +- No neutral dispute resolution. +- No guarantee that every relay honours deletion or moderation. +- No guarantee that a mint remains solvent, online or P2PK-compatible. +- No anonymity guarantee: public events, relays, image hosts and map/weather + providers may observe network metadata. +- No guarantee that a Cordn coordinator cannot censor a group. It cannot read or + forge messages, but it can drop them. + +## Cordn specifically + +The current Cordn group scheme is NIP-44 group delivery with a signed author +proof inside the ciphertext. It is not full MLS yet: it does not provide MLS +forward secrecy or post-compromise security. A coordinator cannot read the +message or impersonate the author, but it can refuse to relay it. + +## Marketplace safety rules + +- Prefer escrowed or in-person deals for high-value trades. +- Never send a plain, unlocked Cashu token in a public event. +- Treat "confirmed trade" as "both parties signed", not "goods verified". +- Treat "payment verified" as "the money leg is verified". +- A new account with no distinct confirmed partners is a warning, not a ban. +- Cash, barter and off-platform settlement have no on-chain proof; use + reputation and judgement. + +## Privacy boundaries + +- Public Nostr events are public forever in practice; deletion is best-effort. +- DMs and Cordn ciphertext are encrypted, but metadata (who, when, which group) + may be visible to relays/coordinators depending on the feature. +- Images are public once uploaded; strip metadata before upload and do not put + addresses in public listing images. +- Self-hosted infrastructure avoids third-party analytics, but network observers + can still see traffic to relays, image hosts, map tiles and weather services. + +## What would change this model + +A real consensus or arbitration layer would need a separate design with explicit +participants, quorum, liveness, slashing/appeals and privacy trade-offs. It +should not be smuggled in as "trust" copy or a global score. Until then: +**signed facts, local merge, explicit boundaries, no fake guarantees.** diff --git a/package.json b/package.json index b567118..d1f3fd4 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "check:i18n": "node scripts/check-i18n.mjs", "test": "npm i --silent && tsc --noEmit && tsc -p cvm-server/tsconfig.json && tsc -p ai-proxy/tsconfig.json && tsc -p push-service/tsconfig.json && eslint && node scripts/check-i18n.mjs && vitest run --reporter=dot --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'All tests passed!'", "deploy": "npm i --silent && npm run build && npx -y nostr-deploy-cli deploy --skip-setup", - "android:init": "npm run android:build:apk && npx cap add android && npx cap sync android && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs", - "android:sync": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs", + "android:init": "npm run android:build:apk && npx cap add android && npx cap sync android && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-backup-policy.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs", + "android:sync": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-backup-policy.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs", "android:open": "npx cap open android", "android:version": "node scripts/sync-android-version.mjs", - "android:build": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs && cd android && ./gradlew assembleRelease", + "android:build": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-deep-links.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-backup-policy.mjs && node scripts/inject-signing-config.mjs && node scripts/make-android-splash.mjs && cd android && ./gradlew assembleRelease", "android:build:apk": "CAPACITOR_BUILD=1 vite build && sed -i 's|||g' dist/index.html && sed -i 's|