Collapse the edition split — one app, Pro as a subscription

2026-07-30 design pass (with the user), superseding the 2026-07-27
two-app split and the 2026-07-29 App Group ruling. Rulings:

- One Mac App Store app (dev.rzen.indie.Kanban, free, 2.0 updates the
  existing record); Lanework Pro = auto-renewable subscription (StoreKit
  2) unlocking the git tier; libgit2 compiles in dormant. The KanbanPro
  target, module-alias tests, verify-editions, UTI/About/Types twins all
  retire. Teams stays deferred — probably a separate app, and never on a
  shared app group.
- No grandfathering: 1.x's free git users get the inert-.git posture;
  unsubscribed and lapsed are one state — histories freeze, resume on
  subscribe via the committer's catch-up commit.
- Entitlement = local read of StoreKit's signed transaction store at
  board-session composition; never a network call on the open path;
  subscribe effective at next open (purchase flow offers reopen); lapse
  never interrupts an open session; expiry-while-offline with auto-renew
  on holds in grace until a real refresh.
- App Group removed wholesale: registry, clipboard staging, template
  store, defaults return to ordinary Application Support; grant slots
  collapse to one bookmark, open-now to one flag; freshness stamp,
  pid-liveness line, group-id provisioning risk all dissolve.
- Quiet signposts carry over; Settings gains the Pro section (subscribe/
  manage/restore) as the third named Pro surface.

12-editions.md rewritten as the tiers doc; edition vocabulary swept to
tiers across 00-13, README, WISHLIST.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-07-30 15:04:09 -04:00
parent c741b02016
commit 0bec9a6be5
14 changed files with 62 additions and 53 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Sync & Collaboration
**Edition scope: Lanework Pro** (12-editions.md). Base Lanework ships mode:none only — the state machine below never leaves its first state there, `.git` encountered on disk is inert (12), and the Mode: none section's no-undo caveat is superseded in base by native undo (13-native-undo.md). Teams adds tracker-backed sync behind the same seam (deferred).
**Tier scope: Lanework Pro** (12-editions.md). The free tier ships mode:none only — the state machine below never leaves its first state there, `.git` encountered on disk is inert (12), and the Mode: none section's no-undo caveat is superseded in the free tier by native undo (13-native-undo.md). Teams adds tracker-backed sync behind the same seam (deferred).
Every board has exactly one **collab mode** at a time, but the mode is not fixed at creation — it can evolve over the board's lifetime:
@@ -35,7 +35,7 @@ Everything above assumes credentials exist; this is where they come from. Constr
- **HTTPS (primary)**: username + token (forge PATs; plain basic auth for generic hosts), stored as a Keychain internet password keyed by **host + username** — git's own scoping model, shared across boards: one GitHub token serves every board, and two accounts on one host coexist as two usernames. **Which username a board uses is the remote URL's business** (git's own answer, and the HTTPS analogue of the SSH per-host table): a username in the URL (`https://alice@host/…`) selects the Keychain item `host + alice`, and the popover's credential capture stamps the entered username into the remote URL in repo config — the URL is the assignment record, no app-side state (the secret itself stays in the Keychain). A URL naming no username resolves to the host's sole stored username; when a host has several, the popover's username field becomes a picker and saving stamps the choice into the URL, while background operations treat the ambiguity as **Authentication needed** (pause and badge, never guess — the same posture as auth failure).
- **SSH — Keychain-resident keys, never key files.** Each Mac has a **Lanework key**: an app-generated ed25519 keypair whose private half lives as an ACL-protected Keychain item and is handed to libssh2 from memory — it never exists on disk. The board popover shows the public key with a Copy affordance; the user adds it to their forge like any machine key. An **existing key imports by paste or drag** (a one-time read under user intent): copied into the Keychain — passphrase entered once at import, stored under Keychain protection thereafter — and the original file is never referenced again. Per-machine identity, per-Mac revocable on the forge — the ssh-idiomatic shape. (Secure Enclave-backed keys — non-exportable, custom sign callback, P-256 — are a possible later hardening, not v1.)
- **Key scope: app-level objects, per-host assignment.** Keys are never board state — the machine key plus any imports live app-wide (Keychain), and each SSH host maps to one of them: default the machine key; importing a key during a host's setup assigns it to that host. A "host" is `hostname[:port]` parsed from the remote URL — the same endpoint identity the TOFU fingerprint store uses (OpenSSH's own `[host]:port` convention); the URL's username (`git@`) disambiguates nothing and stays out of it. The assignment table holds **only overrides** — no entry means the machine key, so the default costs zero records and removing an override self-heals to it. The popover's key picker is labeled per-host ("key for github.com"), which teaches the one cross-board consequence: switching a host's key switches it for every board on that host — the same rotate-once-follow-everywhere behavior as HTTPS tokens. Housekeeping stays small: an import referenced by no host row can be removed; the machine key only regenerates (confirm-gated — it invalidates the old public half on every forge), and that is the entire rotation story. The board popover is only the surface — it shows the key for *that remote's host*, the way the commit-identity fields front repo-local config. Known limit, accepted: two accounts on the *same* host can't be told apart by key (forges bind key→account globally; git's own answer is ssh-config aliases, which live in files the sandbox can't read) — a per-remote key override joins the wishlist if it ever bites.
- **Host verification is trust-on-first-use**: with no `~/.ssh/known_hosts` readable, the first connection to an SSH host confirms its fingerprint with the user; accepted fingerprints live app-side in the shared App Group container (02-architecture.md's app-wide state home since the 2026-07-29 App Group ruling, host-scoped). A later mismatch **hard-blocks with an explanation** — that mismatch is the attack the check exists for.
- **Host verification is trust-on-first-use**: with no `~/.ssh/known_hosts` readable, the first connection to an SSH host confirms its fingerprint with the user; accepted fingerprints live app-side (02-architecture.md's app-wide state home, host-scoped). A later mismatch **hard-blocks with an explanation** — that mismatch is the attack the check exists for.
- **Setup verifies right there.** Adding or changing a remote (board popover — 03-board-ui.md) probes with authentication immediately (ls-remote): missing or rejected credentials surface **inline in the popover** — HTTPS shows username + token fields with a forge-appropriate hint; SSH shows the machine key to copy plus Verify. The user leaves the popover with a remote that demonstrably works, or knowingly not. Boards adopted from a terminal clone (whose auth lives outside the sandbox and can't be reused) hit the same inline flow at the first in-app operation that needs credentials.
- **Auth failures pause; they never nag and never hammer.** A push or pull rejected for authentication (expired token, revoked key) is not retried — a dead credential cannot succeed, and hammering invites rate limits and lockouts. The push queue pauses and the popover badge switches to a distinct **Authentication needed** state carrying the error; the popover presents the same inline fields, prefilled where possible. Updating the credential (or fixing forge-side and hitting Verify) resumes the queue. Network failures keep the quiet auto-resume above — only auth pauses.
- **Background operations never prompt.** Push-on-commit and the automatic fetch-rebase-push stay silent through auth trouble (badge only); credential capture happens exclusively in the popover, where the user already is when it matters (manual Pull/Push live there too).