| … | … | @@ -186,7 +186,7 @@ The in-memory DTO is the seam — **keep it**. Add a 4th `source` value `'sheets |
| 186 | 186 | ### 4.6 UI + run-in-tab integration |
| 187 | 187 | |
| 188 | 188 | - **Search box:** replace the client-side `filteredApps` substring `useMemo` (`shell.js`) with a **debounced `CMD_SHEETS_LIST`** passing a JMESPath built from `{query, category, source}`. Keep the `useMemo` as a thin offline fallback. This is the "real search box that calls `sheets.list` with JMESPath." |
| 189 | - **Run-in-tab gating:** the app-card already has Launch/Install; add the `type` switch `FEATURED_APPS` uses — `type==='standalone'` → `CMD_LAUNCH_PEAR_LINK` (window), `type==='hypersite'` → `CMD_RUN_APP_IN_TAB`. Since `type` is now a **validated field on every row**, the hardcoded `FEATURED_APPS` array (`shell.js:33-94`) becomes **data**: seed those 6 rows into the canonical room and render Featured from `list('apps', {query:"[?author=='pearbrowser']"})` or a featured flag. | |
| 189 | - **Native-delivery gating:** a catalogue row may open browsable `hyper://` content, or carry `legacyMigrationId` plus `nativeDelivery.status:'migration-required'`. The latter displays publisher migration guidance and never starts a worker or window. Since delivery state is a **validated field on every row**, the hardcoded featured list becomes **data**: seed those rows into the canonical room and render Featured from `list('apps', {query:"[?author=='pearbrowser']"})` or a featured flag. | |
| 190 | 190 | - **Routing:** `catalogLoadPlan` (`shell.js:123`) + `parseCatalogRef` (`keys.js:138`) gain a `sheets://` scheme (or bare z32) → `CMD_SHEETS_LOAD`. A 52-char z32 decoding to 64 bytes (key+enc) routes to sheets; a 32-byte z32 / 64-hex stays drive. |
| 191 | 191 | - **Icons:** do NOT base64-inline at load. `iconRef` is either a drive path the UI lazily fetches (via `CMD_GET_DRIVE_INFO`/proxy) or a small `data:` URI inline in the row. Letter-fallback otherwise (already in `shell.js`). **Pick path-fetch as the default to avoid Autobase bloat** (open question §8). |
| 192 | 192 |
| … | … | @@ -71,7 +71,7 @@ Re-verified against the repo. This is the reuse budget; the synthesized design i |
| 71 | 71 | | **IndexRoomClient verify-and-drop** | `listRelayDirectory(rows, verify)` re-verifies each row's signed capability doc and drops failures (index-room-client.js:120-141); `verifyCapabilityDoc`. | **The "room is an index, not an authority" enforcement** — re-verify every relay-served claim client-side. | ✅ | |
| 72 | 72 | | **HyperDHT (via Hyperswarm)** | `swarm.join(topic)` peer discovery; `dht.mutablePut/mutableGet` (BEP44-style, ~1KB signed value, 48h TTL). **No mutable*/immutable* usage anywhere in backend today** — buildable but net-new. | **Tiny signed pointers/hints only** (room→key, contact→index-key). Never storage. | ✅ buildable, ❌ not currently wired | |
| 73 | 73 | | **Identity + swarm-grants + trust tiers + rate limits** | BIP-39 root seed, per-app sub-keys, per-app swarm grants with trust tiers (A/B/C) + rate limits. | **The trust-edge source** for ranking and the rate budget for pushes. | ✅ | |
| 74 | | **Contacts (social graph)** | Hyperbee keyed by **root pubkey**; signed `pear://contact?pk=..&sig=..` QR invite ceremony (contacts.js). | **The trust frontier / Sybil gate** for Constellation-style personal search. | ✅ — but keyed on root pubkey, which signForApp does NOT produce (binding gap) | | |
| 74 | | **Contacts (social graph)** | Hyperbee keyed by **root pubkey**; signed `p2p-contact://invite?...` data-invite ceremony (contacts.js). | **The trust frontier / Sybil gate** for Constellation-style personal search. | ✅ — but keyed on root pubkey, which signForApp does NOT produce (binding gap) | | |
| 75 | 75 | | **UserData history/bookmarks** | `addHistory`/`addBookmark` write hooks; inverted-timestamp Hyperbee. | **The free crawl source** — index what the user already browses. | ✅ | |
| 76 | 76 | | **safeJmesPath + the relay race** | `SAFE_JMESPATH` regex chokepoint (sheets-catalog.js:69); `_doHybridFetch` does `Promise.any([relay, p2p])` (hyper-proxy.js:686). | Query-template safety gate; **and the relay race must be flipped P2P-first** (it currently leaks nearly every fetch to the default-on relay). | ✅ both confirmed | |
| 77 | 77 | | **Collaborative-catalog (Autobee) rollout — just shipped** | create / invite-writer / op-log + pure deterministic reducer (autobee-catalog-apply.cjs, no `Date.now` in the fold). | **The pure-deterministic ranking discipline** and the multi-writer descriptor-room substrate. The roadmap below builds directly on this. | ✅ | |
| … | … | @@ -576,7 +576,7 @@ Feasibility claims that DON'T hold: |
| 576 | 576 | |
| 577 | 577 | **Ranking / anti-sybil.** Ranking is a PURE function (no wall-clock in the score, same discipline as autobee-catalog-apply.cjs) computed once after a bounded collection window over the assembled candidate set + a static snapshot of the trust frontier. Score(doc) = w1*socialProximity + w2*endorserBreadth + w3*tierWeight + w4*termMatch + w5*freshness, where: socialProximity = 1/(hop+1) (hop-1 friend beats hop-2 friend-of-friend); endorserBreadth = log(1 + distinct frontier endorsers of this doc) (the killer signal — 'this URL is endorsed by 5 of your friends' is near-unfakeable); tierWeight from the existing swarm-grants A/B/C tiers of each endorser; termMatch from the per-posting record (title hit > body hit, all-terms-present phrase bonus, tf); freshness = exp(-age/halflife) using publishedAt vs a query-time constant passed in (kept OUT of the per-doc fold so two runs over the same frontier produce identical order). SYBIL/SPAM RESISTANCE is structural and is the whole point: (1) An attacker's fake index is INVISIBLE — it is never in anyone's trust frontier until a real person adds them as a Contact (a signed QR/invite ceremony, not a free action). (2) Even after one honest edge, a Sybil contributes at most one endorser to endorserBreadth and sits at hop>=1 with whatever low tier the single honest contact assigned — it cannot manufacture breadth because breadth counts DISTINCT frontier pubkeys and the frontier is your real graph (this is the SybilLimit insight: attack edges, not attack nodes, bound influence). (3) Every posting is signature-verified against an endorser pubkey that MUST be a member of the current frontier, re-checked client-side at query time (the IndexRoomClient 'verify each row, drop the rest' pattern) — a relay serving a friend's index cannot inject postings signed by anyone outside your graph. (4) A misbehaving contact is dropped wholesale (remove the Contact -> their entire index leaves your frontier, the per-writer-core isolation property). Page-controlled features (meta tags, keyword stuffing) carry ZERO ranking weight — only signed social endorsements and term presence do, which is exactly what YaCy's gameable local page-feature ranking lacked. |
| 578 | 578 | |
| 579 | **Discovery.** This is where 'content addressed by opaque keys' is actually solved, because the social graph IS the naming layer (Zooko: the key stays secure+decentralized; HUMAN-MEANING is supplied by a person you trust who endorsed it under a title). KEYLESS CONTENT GETS A NAME the moment a real user bookmarks/endorses it: the PersonalIndexer captures driveKey+path AND the human title from the page AND the endorser's identity, so 'that hyper key' becomes 'the P2P blog Maya endorsed about Hypercore' in your index. NAMING/BOOTSTRAP: (a) You can always search your own browsed/bookmarked content from day one (cold-start via own data — no graph needed). (b) New keys enter the searchable universe ONLY through a human act of endorsement, which is also the Sybil gate — discovery and trust are the same edge. (c) FINDING WHO TO ASK: contact-pubkey -> index-key resolution via the DHT IndexPointer (schema below) plus contact-record fallback; the social graph itself is bootstrapped by the EXISTING pear://contact?pk=..&sig=.. signed QR invite ceremony (backend/contacts.js parseInviteURL) — no new discovery mechanism needed. (d) GRAPH COLD-START for a brand-new user with zero contacts: a default-trusted 'starter index' published by the relay as a schema-sheets index room (loaded via IndexRoomClient verbatim, every row re-verified) seeds hop-0 so search returns SOMETHING on install; the user then replaces it organically as they add contacts. The starter index is explicitly framed as a removable bootstrap, not an authority, to avoid ossifying into a central directory (the open #8 filter-bubble risk). Human-name->key aliasing is therefore emergent (titles carried on signed postings) rather than a separate petname system to build. | |
| 579 | **Discovery.** This is where 'content addressed by opaque keys' is actually solved, because the social graph IS the naming layer (Zooko: the key stays secure+decentralized; HUMAN-MEANING is supplied by a person you trust who endorsed it under a title). KEYLESS CONTENT GETS A NAME the moment a real user bookmarks/endorses it: the PersonalIndexer captures driveKey+path AND the human title from the page AND the endorser's identity, so 'that hyper key' becomes 'the P2P blog Maya endorsed about Hypercore' in your index. NAMING/BOOTSTRAP: (a) You can always search your own browsed/bookmarked content from day one (cold-start via own data — no graph needed). (b) New keys enter the searchable universe ONLY through a human act of endorsement, which is also the Sybil gate — discovery and trust are the same edge. (c) FINDING WHO TO ASK: contact-pubkey -> index-key resolution via the DHT IndexPointer (schema below) plus contact-record fallback; the social graph itself is bootstrapped by the existing signed `p2p-contact://invite?...` data-invite ceremony (backend/contacts.js parseInviteURL) — no new discovery mechanism needed. (d) GRAPH COLD-START for a brand-new user with zero contacts: a default-trusted 'starter index' published by the relay as a schema-sheets index room (loaded via IndexRoomClient verbatim, every row re-verified) seeds hop-0 so search returns SOMETHING on install; the user then replaces it organically as they add contacts. The starter index is explicitly framed as a removable bootstrap, not an authority, to avoid ossifying into a central directory (the open #8 filter-bubble risk). Human-name->key aliasing is therefore emergent (titles carried on signed postings) rather than a separate petname system to build. | |
| 580 | 580 | |
| 581 | 581 | **Privacy.** Local-first by construction, the strongest posture available. THE QUERY NEVER LEAVES THE DEVICE in the common path: to search a friend's index you REPLICATE their PersonalIndex Hyperbee (you pull blocks for the term! prefixes you scan) and run the range scan + intersection ON YOUR DEVICE. The friend's relay/peer sees Hyperbee block requests, not your query string. METADATA LEAKAGE — the honest hard part: (a) which blocks you request leaks the term prefix you scanned to whoever serves that core (the author's relay or the author online). Mitigation tier-1 (v1): request a small superset / pad with decoy term-prefix reads so the served pattern doesn't uniquely reveal the query; the served peer is by definition someone you already trust (a friend or a friend's relay), bounding the adversary. Mitigation tier-2 (later): batch-replicate a friend's whole small index (they are intentionally small — bounded to shared/endorsed pages) so you scan locally with zero per-query block-pattern leak. (b) The DHT IndexPointer lookup reveals WHICH friend's index you are resolving to the DHT — mitigated by resolving the whole frontier's pointers up front on app start, not per query, and by the contact-record fallback (no DHT hit at all for established contacts). PUBLISHING privacy: the user controls visibility per doc (shared / endorsed-only / private-never-published); raw browsing history is NEVER published by default — only explicit bookmarks and endorsements, the opposite of a surveillance index. RELAY query fast-path (the hyper-proxy relay race) is explicitly NOT used for queries: queries are P2P-only; the relay is used only to PIN/serve indexes, never to receive a plaintext query (avoiding YaCy's plaintext-Solr leak). This is also why we recommend flipping the hyper-proxy fetch to P2P-first generally. Hashed-terms/PIR are noted as a future research lane but explicitly out of v1 — the social bounding already makes the served adversary a trusted party. |
| 582 | 582 |
| … | … | @@ -6,7 +6,7 @@ |
| 6 | 6 | |
| 7 | 7 | ## 1. Purpose |
| 8 | 8 | |
| 9 | PearBrowser is a peer-to-peer browser that can host Pear apps. It decides — **purely from catalogue/app metadata, never from runtime probing** — whether your app renders **inline in a browser tab** or opens in **its own OS window**. This standard tells a Pear app author exactly what to do **at release time** so their app is *maximally PearBrowser-compatible*: ideally runs inline in a tab on **both desktop and mobile**, declares itself honestly to the catalogue, degrades gracefully when capabilities are absent, respects the browser's permission/consent and layout model, and — where inline hosting is impossible — releases for the best window/launcher experience and declares that truthfully. | |
| 9 | PearBrowser is a peer-to-peer browser that hosts browsable Hyper content. It decides — **purely from catalogue/app metadata, never from runtime probing** — whether content opens in a browser tab or is a legacy native record requiring a publisher-provided verified package. This standard tells an app author how to release static content that runs inline on **both desktop and mobile**, declares delivery state honestly, degrades gracefully when capabilities are absent, and respects the browser's permission/consent and layout model. PearBrowser does not execute native app code from a catalogue. | |
| 10 | 10 | |
| 11 | 11 | ### TL;DR for app authors |
| 12 | 12 | |
| … | … | @@ -85,13 +85,12 @@ PearBrowser apps fall into three deployable shapes. The single number that matte |
| 85 | 85 | |
| 86 | 86 | - **PB-TYPEMODEL-17 (SHOULD, both).** Serve **all your own bytes** from your own Hyperdrive (static) or worker (hypersite). PearBrowser proxies static drives unchanged and runs hypersite workers as separate processes; it does not bundle/ingest foreign app code. (Exception: on the `hyper://` ad-hoc hosting path the proxy *does* inject `window.pear.*` shims — but never your dependencies or your own bytes.) |
| 87 | 87 | |
| 88 | > **NOT CURRENTLY ENFORCED (aspirational / PROPOSED) — app lifecycle & launcher management.** The following describe behavior **not present** in current code (no `CMD_QUIT_PEAR_APP`, `CMD_FOCUS_PEAR_APP`, `launchedPearApps` registry, `EVT_PEAR_APP_EXITED`, launcher card, "Open in tab"/`openInTab()`, `kind:'pear'` tab, or `Pear.View` embedding spike exist). `CMD_LAUNCH_PEAR_LINK` is **fire-and-forget**: it spawns via pear-run, captures no pid, and cannot terminate or focus the app. | |
| 89 | > - *(PROPOSED)* A standalone app should be externally terminable (exit on pipe-destroy/SIGTERM, no detached children) so a future launcher can manage it. | |
| 90 | > - *(PROPOSED)* A standalone app should let window close/crash propagate so a future launcher card can flip to "stopped". | |
| 91 | > - *(PROPOSED)* A `pear://` standalone could expose a launcher-card "Open in tab" handle. | |
| 92 | > - *(PROPOSED)* Workflows must not depend on launcher-tab persistence across restart. | |
| 93 | > | |
| 94 | > Authors: design your app to **exit cleanly on process termination and keep all session state inside the app**, which satisfies these proposals if/when they land, but do not rely on PearBrowser managing your window today. | |
| 88 | > **Native-delivery boundary.** PearBrowser is not a native-app launcher. A | |
| 89 | > catalogue entry for a legacy native app must carry an opaque migration ID and | |
| 90 | > `nativeDelivery.status:'migration-required'`; it cannot expose executable | |
| 91 | > code, an app-worker address, or an “open in window” action. Publishers must | |
| 92 | > ship a verified signed package with their own lifecycle, update, and recovery | |
| 93 | > experience outside the browser process. | |
| 95 | 94 | |
| 96 | 95 | ### Anti-patterns |
| 97 | 96 | |
| … | … | @@ -346,9 +345,9 @@ Targeting the Pear Runtime global; top-of-script `await window.pear.X` with no g |
| 346 | 345 | - **PB-SECURITYSTORAGEUI-13 (MUST — origin clause is desktop-only).** Keep drive keys as bare 64-hex; keep paths free of `..`/NUL (→ 400). **Correction:** "non-loopback origins are 403'd" is **desktop-only** — mobile deliberately allows canonical `http(s)` origins through and defers to token/Origin checks. The traversal/hex-key rules apply on both. |
| 347 | 346 | - **PB-SECURITYSTORAGEUI-14 (SHOULD, both).** Keep any single bridge **sync-append** operation under 100KB (enforced on `/api/sync/append`); a separate ~1MB total request-body cap also applies. Chunk/stream larger work. |
| 348 | 347 | - **PB-SECURITYSTORAGEUI-15 (SHOULD, both).** Keep any single published file under **10MB** (enforced at write/publish). Reconcile with the 5MB cache/stream threshold (PB-STATICHYPERDRIVE-17): files 5–10MB publish but are never cached and re-fetched every load — prefer <5MB for frequently-loaded assets. |
| 349 | - **PB-SECURITYSTORAGEUI-17 (MUST, both).** A window-class/standalone app MUST declare itself honestly (window/launcher-only) rather than claiming tab-compatibility. Desktop launches it in its own window via `CMD_LAUNCH_PEAR_LINK`; mobile cannot load it (blank tab / WorkerError). | |
| 350 | - **PB-SECURITYSTORAGEUI-18 (SHOULD, both).** For a window-class app, design for single-instance: open your store once, close cleanly on teardown, and if a second launch finds the store locked, focus/hand off rather than crash. Don't leave a rocksdb LOCK held or a fixed port bound. *(The host survives this only by hard-exiting to release LOCKs and scanning to the next free port — don't depend on that.)* | |
| 351 | - **PB-SECURITYSTORAGEUI-19 (MAY, both).** Prefer the tab-able shapes (pear-request worker or static Hyperdrive) over a full-GUI window app when functionality allows, to be inline-in-tab on both platforms. | |
| 348 | - **PB-SECURITYSTORAGEUI-17 (MUST, both).** A legacy native app MUST declare `nativeDelivery.status:'migration-required'` until its publisher has a verified package; it must not claim browser-tab compatibility or present a remote executable target. | |
| 349 | - **PB-SECURITYSTORAGEUI-18 (SHOULD, both).** A native package should be single-instance safe and own its lifecycle/recovery outside PearBrowser; the browser neither starts nor manages it. | |
| 350 | - **PB-SECURITYSTORAGEUI-19 (MAY, both).** Prefer static Hyperdrive content when functionality allows, so it remains inline-in-tab on both platforms. | |
| 352 | 351 | |
| 353 | 352 | > **PB-SECURITYSTORAGEUI-16 (mobile-only; partly PROPOSED).** If your app needs the bridge on a real HTTPS origin (not loopback/`hyper://`), it is injected only when the user has **trusted** that origin in allowlist mode; prompt the user to trust and degrade read-only until granted. Use a stable canonical origin (scheme+host+non-default port) so the trust entry persists/replicates. **Scope correction:** the trusted-origins store exists **only on mobile** (no equivalent on desktop), and the `{allowed:false, reason:'untrusted'}` withholding is implemented at the session-mint layer (described in code comments) rather than in the trust-store module itself. Treat HTTPS-origin trust as a **mobile** concern. |
| 354 | 353 | |
| … | … | @@ -638,7 +637,7 @@ application. |
| 638 | 637 | 7. **`canonicalJSON` spec** for signed-catalog Ed25519 verification (key ordering, number/unicode normalization) so independent relays interoperate. |
| 639 | 638 | 8. **Cross-platform `type` unification** — either teach mobile to honor `type`, or formally bless "mobile does not route on `type`; static drive is the mobile app floor" as the contract. |
| 640 | 639 | 9. **Inter-app deep-link contract** — a canonical way to construct a shareable link to a hypersite/static app at a specific resource (beyond `hyper://` + ad-hoc hash params). |
| 641 | 10. **Standalone app management** — a real launcher (terminate/focus/relaunch, exit propagation, launcher-card persistence). `CMD_LAUNCH_PEAR_LINK` is fire-and-forget today; the §3 lifecycle block is entirely aspirational. | |
| 640 | 10. **Verified native-delivery contract** — package signing, update, recovery, and platform lifecycle are publisher responsibilities outside the browser; catalogue metadata must link only to the documented package channel, never executable bytes. | |
| 642 | 641 | |
| 643 | 642 | **Unresolved/underspecified (need a normative ruling once code stabilizes):** |
| 644 | 643 |