| … | … | @@ -147,22 +147,69 @@ primitive it is built on — a 32-byte identity pubkey as the authority — is t |
| 147 | 147 | |
| 148 | 148 | `at://` remains correct for **foreign** subjects (§3.2); that is genuinely where they live. |
| 149 | 149 | |
| 150 | | #### 3.4.1 ⚠ Precondition — never mint a URI from the device-pubkey fallback |
| 150 | #### 3.4.1 Minting — a mechanical check, not a discipline |
| 151 | 151 | |
| 152 | | Verified: a restore re-derives from the mnemonic (`identity-manager.js:174`, |
| 153 | | `IdentityKey.from({mnemonic})`) and SLIP-48 derivation is deterministic, so **a vault restore |
| 154 | | reproduces the same root** and previously-authored URIs keep resolving. This was the open worry from |
| 155 | | #48 and it is answered. |
| 152 | A proof is `{ version, epoch, identity, chain[] }` where **`identity` is the root pubkey**, and each |
| 153 | chain link signs `{epoch, identity, device}` with the parent key |
| 154 | (`keet-identity-key/index.js:56-92`). `verify()` walks the chain and returns |
| 155 | `{ receipt, identityPublicKey, devicePublicKey }` (`:133-186`), and `validateProof` accepts |
| 156 | **`opts.expectedIdentity`**, rejecting a mismatch outright (`:198-206`). |
| 156 | 157 | |
| 157 | | ⛔ But `identityRootHex` falls back to the **device pubkey** when an identity has no root at all — |
| 158 | | the raw/legacy identities, *"which are their own root"* (`identity.js:95-113`). A URI minted from |
| 159 | | that fallback would change the moment that identity gained a real root. **Minting must refuse the |
| 160 | | fallback and fail closed**, in the same direction as every other gate here. |
| 158 | So the URI's authority segment is verifiable with an existing call, offline and with no directory — |
| 159 | strictly better than `at://`, which needs a PLC lookup. The pattern is already implemented here: |
| 160 | `packages/hyper-identity/lib/hyperident-vault.js:242-258` verifies the chain, compares |
| 161 | `verified.identityPublicKey` to the expected root and returns `chain-root-mismatch`. |
| 161 | 162 | |
| 162 | | A linked device carries `rootPubkeyHex` in its join payload (`identity.js:116-127`), so a device |
| 163 | | that does not hold the seed can still name correctly. |
| 163 | **Minting rule.** A `favela://` authority may be minted only from a proof that |
| 164 | 164 | |
| 165 | | #### 3.4.2 What follows |
| 165 | 1. verifies (`IdentityKey.verify` returns non-null), **and** |
| 166 | 2. carries a **non-empty chain** whose `identity` differs from the signing device key. |
| 167 | |
| 168 | A raw/legacy identity that is *"its own root"* (`pgh/lib/identity.js:95-113`, the device-pubkey |
| 169 | fallback in `identityRootHex`) fails both, so the fallback is refused **mechanically** rather than by |
| 170 | discipline. Fail-closed, in the same direction as every other gate here. |
| 171 | |
| 172 | **A vault restore is safe.** Restore re-derives from the mnemonic |
| 173 | (`hyperident/lib/identity-manager.js:174`, `IdentityKey.from({ mnemonic })`) and SLIP-48 derivation is |
| 174 | deterministic, so the same root comes back and previously-authored URIs keep resolving. This was the |
| 175 | open worry carried from hyper-social #48 and it is answered. A linked device that does not hold the |
| 176 | seed still carries `rootPubkeyHex` in its join payload (`pgh/lib/identity.js:116-127`), so it can |
| 177 | name correctly without being able to re-derive. |
| 178 | |
| 179 | #### 3.4.2 ⛔ Attestation is for minting and admission — NEVER per-record |
| 180 | |
| 181 | `validateProof` enforces a **monotonic epoch floor** (`keet-identity-key/index.js:205-208`): |
| 182 | |
| 183 | ```js |
| 184 | if (opts.receipt) { |
| 185 | const { epoch } = c.decode(ReceiptEncoding, opts.receipt) |
| 186 | if (proof.epoch < epoch) return false |
| 187 | } |
| 188 | ``` |
| 189 | |
| 190 | and `bootstrap()` stamps `epoch: Date.now()` (`:63`). **Re-bootstrapping after a lost device mints a |
| 191 | new epoch, and every proof ever made falls below the floor.** A verifier holding the new receipt |
| 192 | rejects all of them. |
| 193 | |
| 194 | That is correct for Keet, where a proof authorises a **live session** and old device proofs should |
| 195 | die at once. It is wrong for archival records: "this was validly authored in 2026" must stay true |
| 196 | after a 2027 revocation. |
| 197 | |
| 198 | **So a record — post or edge — MUST NOT carry a live attestation proof.** Attestation is used at |
| 199 | exactly two moments: |
| 200 | |
| 201 | 1. **minting the URI / creating the `User`** — proving this device speaks for this root (§3.4.1) |
| 202 | 2. **writer admission** — the `DeviceLinker` path (`identity.md` §10 item 2) |
| 203 | |
| 204 | After admission, the autobase's own writer signatures carry authorship permanently, which is what |
| 205 | `design.md` §4 already resolves. Revocation then behaves correctly: bump the epoch, no new device |
| 206 | joins under an old proof, and nothing already written is disturbed. |
| 207 | |
| 208 | ⚠ This is a **negative** requirement, so nothing will fail if it is violated — a per-record proof |
| 209 | would work perfectly until the first device loss, then retroactively unauthor the archive. It is |
| 210 | called out here because that is the only moment it becomes visible. |
| 211 | |
| 212 | #### 3.4.3 What follows |
| 166 | 213 | |
| 167 | 214 | `record-vocabulary.md` §3.1's pairing extends from three names to four — canonical `favela://`, |
| 168 | 215 | `at://`, the AP object URI, and a display name — all still pure functions of one rkey. |