| … | … | @@ -1579,6 +1579,23 @@ export class Sidecar { |
| 1579 | 1579 | replicas: this._nameReplicas.map(r => ({ keyHex: r.keyHex, writable: r.registry.writable })) |
| 1580 | 1580 | })) |
| 1581 | 1581 | |
| 1582 | // The identity acting on a name claim, in the shape NameRegistry's write |
| 1583 | // methods take. A signed (v2) op needs the device secret AND the keet |
| 1584 | // attestation chain; an identity with neither — the raw/legacy fallback, |
| 1585 | // which has no chain to offer — degrades to the legacy string form so it |
| 1586 | // keeps working exactly as before rather than throwing. See org #143. |
| 1587 | this._nameSigner = () => { |
| 1588 | const id = this._identity |
| 1589 | const root = identityRootHex(id) |
| 1590 | if (!id || !id.chain || !id.secretKey) return root |
| 1591 | return { |
| 1592 | identityRoot: root, |
| 1593 | devicePubkey: id.publicKeyHex, |
| 1594 | deviceSecretKey: id.secretKey, |
| 1595 | chain: id.chain |
| 1596 | } |
| 1597 | } |
| 1598 | |
| 1582 | 1599 | svc.handle('claim-name', async (ctx, params) => { |
| 1583 | 1600 | if (!params || !params.name) throw new Error('claim-name requires { name }') |
| 1584 | 1601 | if (!this._nameRegistry.writable) { |
| … | … | @@ -1608,13 +1625,42 @@ export class Sidecar { |
| 1608 | 1625 | |
| 1609 | 1626 | svc.handle('release-name', async (ctx, params) => { |
| 1610 | 1627 | if (!params || !params.name) throw new Error('release-name requires { name }') |
| 1611 | | // Same anchor the claim was written with (org #142) — a release that |
| 1612 | | // computes it differently from the claim can never match. |
| 1613 | | const identityRoot = identityRootHex(this._identity) |
| 1614 | | await this._nameRegistry.release(params.name, identityRoot) |
| 1628 | await this._nameRegistry.release(params.name, this._nameSigner()) |
| 1615 | 1629 | return { ok: true, released: params.name } |
| 1616 | 1630 | }) |
| 1617 | 1631 | |
| 1632 | svc.handle('update-name', async (ctx, params) => { |
| 1633 | if (!params || !params.name) throw new Error('update-name requires { name }') |
| 1634 | if (!this._nameRegistry.writable) { |
| 1635 | throw new Error('name registry is not writable on this sidecar (join via invite first)') |
| 1636 | } |
| 1637 | const existing = await this._nameRegistry.resolve(params.name) |
| 1638 | if (!existing) throw new Error('name not claimed: ' + params.name) |
| 1639 | |
| 1640 | // Fail here rather than appending an op apply will drop — a dropped op |
| 1641 | // is indistinguishable from replication lag at the call site. |
| 1642 | const signer = this._nameSigner() |
| 1643 | const mine = typeof signer === 'string' ? signer : signer.identityRoot |
| 1644 | if (existing.publisher.identityRoot !== mine) { |
| 1645 | throw new Error( |
| 1646 | 'name "' + params.name + '" is owned by ' + |
| 1647 | String(existing.publisher.identityRoot).slice(0, 12) + '… — this identity is ' + |
| 1648 | String(mine).slice(0, 12) + '…' |
| 1649 | ) |
| 1650 | } |
| 1651 | |
| 1652 | const updates = {} |
| 1653 | if (params.registryKey) updates.registryKey = params.registryKey |
| 1654 | if (params.serverKey) updates.serverKey = params.serverKey |
| 1655 | if (params.description !== undefined) updates.description = params.description |
| 1656 | if (Object.keys(updates).length === 0) { |
| 1657 | throw new Error('update-name requires at least one of { registryKey, serverKey, description }') |
| 1658 | } |
| 1659 | |
| 1660 | await this._nameRegistry.updateName(params.name, updates, signer) |
| 1661 | return await this._nameRegistry.resolve(params.name) |
| 1662 | }) |
| 1663 | |
| 1618 | 1664 | svc.handle('list-names', async (ctx, params) => { |
| 1619 | 1665 | // Merge own registry + subscribed replicas, dedup by name (own wins). |
| 1620 | 1666 | const seen = new Map() |