| … | … | @@ -250,28 +250,42 @@ export class HyperOrgs { |
| 250 | 250 | const decoded = decodeInvite(invitation) |
| 251 | 251 | if (decoded.kind !== 'org') throw new Error(`invite is for "${decoded.kind}", not an org`) |
| 252 | 252 | if (isExpired(decoded)) throw new Error('invite expired') |
| 253 | if (!decoded.name) throw new Error('org invite missing name (cannot key the org)') |
| 254 | return this.mountOrg(decoded.name, decoded.bootstrapKey, decoded.roles[0] || 'reader') |
| 255 | } |
| 253 | 256 | |
| 254 | | const name = decoded.name |
| 255 | | if (!name) throw new Error('org invite missing name (cannot key the org)') |
| 256 | | const role = decoded.roles[0] || 'reader' |
| 257 | /** |
| 258 | * Mount an org's autobase by its bootstrap key directly (no invite |
| 259 | * ceremony). PEER mode uses joinOrg (invite → key). MIRROR mode uses |
| 260 | * this: the primary's listOrgKeys() yields {name, bootstrapKey} for |
| 261 | * every org, and a mirror mounts each → full replica. Idempotent. |
| 262 | * @returns {Promise<{ name, writerKey, role }>} |
| 263 | */ |
| 264 | async mountOrg (name, bootstrapKey, role = 'reader') { |
| 257 | 265 | if (this._orgs.has(name)) { |
| 258 | | return { name, writerKey: this._orgs.get(name).room.localKeyHex, role } |
| 266 | return { name, writerKey: this._orgs.get(name).room.localKeyHex, role: this._orgs.get(name).role } |
| 259 | 267 | } |
| 260 | | |
| 261 | | const room = new Room(this._corestore, { |
| 262 | | schema: orgsSchema, |
| 263 | | // Namespace by bootstrap key so a joiner's local writer is distinct |
| 264 | | // from the creator's; the autobase identity is the bootstrap key. |
| 265 | | namespace: 'org-join/' + decoded.bootstrapKey.slice(0, 16), |
| 266 | | bootstrapKey: decoded.bootstrapKey |
| 267 | | }) |
| 268 | | await room.ready() |
| 268 | const room = await this._openOrgRoom(name, bootstrapKey, { created: false, role }) |
| 269 | 269 | await room.base.update() |
| 270 | | this._orgs.set(name, { room, role, created: false }) |
| 271 | | await this._track(name, decoded.bootstrapKey, role, false) |
| 270 | await this._track(name, bootstrapKey, role, false) |
| 272 | 271 | return { name, writerKey: room.localKeyHex, role } |
| 273 | 272 | } |
| 274 | 273 | |
| 274 | /** |
| 275 | * Every org this instance tracks, with its bootstrap key. The primary |
| 276 | * exposes this so a mirror can mountOrg() each for a full replica |
| 277 | * (mirror mode). Peers never call this — they only get specific |
| 278 | * invites (peer mode), which is what prevents the post-mortem leak. |
| 279 | * @returns {Array<{ name, bootstrapKey, role, created }>} |
| 280 | */ |
| 281 | listOrgKeys () { |
| 282 | const out = [] |
| 283 | for (const [name, e] of this._orgs) { |
| 284 | out.push({ name, bootstrapKey: e.room.keyHex, role: e.role, created: e.created }) |
| 285 | } |
| 286 | return out |
| 287 | } |
| 288 | |
| 275 | 289 | // ── Resources (per-org) ────────────────────────────────────── |
| 276 | 290 | |
| 277 | 291 | async addResource (orgName, resourceName) { |