mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
refactor(concord): invite bundle (33301) gets its own addressable event
Third refactor slice (plan: quartz/plans/2026-07-11-concord-event-classes.md).
The kind-33301 public invite bundle is now a proper addressable Event class:
- New cord05Invites/bundle/ConcordInviteBundleEvent (BaseAddressableEvent), with a
build{} template that emits the ['d',''] + ['vsk','6'] tags via the standard DTag
ext and the shared VskTag (ControlEntityKind.INVITE_LIVE). Registered kind 33301
-> ConcordInviteBundleEvent in EventFactory so fetched bundles parse as the class.
- ConcordInviteBundle.build now signs ConcordInviteBundleEvent.build(...) with the
per-link key; the raw TAG_D/TAG_VSK/VSK_LIVE constants are gone. Tag order (d, vsk)
is preserved so the bundle event id / naddr are unchanged — the cord05Invites
tests (bundle round-trip + join flow) pass.
Remaining invite kinds: 3313 direct-invite (rumor has empty tags; p/k index is on
the giftwrap) and 13303 invite-list (unimplemented stub) — tracked in the plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
@@ -77,8 +77,12 @@ control, rekey, guestbook, invites, voice), not the standard-Nostr aliases.
|
||||
ext; dropped `MESSAGE/REACTION/DELETE/COMMENT` aliases. (commit c2cbd602)
|
||||
2. ✅ Control plane 3308 → `cord04Roles/control/ControlEditionEvent` package +
|
||||
`tags/` (vsk/eid/ev/ep/vac) + ext; `EventFactory` registers 3308. (commit dc2abc0e)
|
||||
3. Invites 33301 / 3313 / 13303. **← next**
|
||||
4. Guestbook (3306/3309/3312) + voice (23313/23311) + rekey (3303).
|
||||
3. Invites: ✅ 33301 bundle → `cord05Invites/bundle/ConcordInviteBundleEvent`
|
||||
(addressable, reuses `DTag` + `VskTag`; registered in `EventFactory`). Remaining:
|
||||
3313 direct-invite → `ConcordDirectInviteEvent` (the rumor has empty tags; the
|
||||
`p`/`k` index lives on the giftwrap). 13303 invite-list has no implementation yet
|
||||
(a bare `ConcordKinds` constant) — build it when the private-invite feature lands.
|
||||
4. Guestbook (3306/3309/3312) + voice (23313/23311) + rekey (3303). **← next**
|
||||
5. Envelope seals (20013/20014) / wrap (1059/21059).
|
||||
6. Shrink `ConcordKinds` to only kinds without their own event class; audit
|
||||
`EventFactory` coverage.
|
||||
|
||||
+3
-6
@@ -21,8 +21,8 @@
|
||||
package com.vitorpamplona.quartz.concord.cord05Invites
|
||||
|
||||
import com.vitorpamplona.quartz.concord.cord04Roles.ConcordJson
|
||||
import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent
|
||||
import com.vitorpamplona.quartz.concord.crypto.ConcordKeyDerivation
|
||||
import com.vitorpamplona.quartz.concord.events.ConcordKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
@@ -51,10 +51,7 @@ class MintedInviteLink(
|
||||
* bundle. Pinned to the Concord v2 reference client.
|
||||
*/
|
||||
object ConcordInviteBundle {
|
||||
const val KIND = ConcordKinds.INVITE_BUNDLE
|
||||
const val TAG_D = "d"
|
||||
const val TAG_VSK = "vsk"
|
||||
const val VSK_LIVE = "6"
|
||||
const val KIND = ConcordInviteBundleEvent.KIND
|
||||
|
||||
private fun json(invite: CommunityInvite) = ConcordJson.instance.encodeToString(CommunityInvite.serializer(), invite)
|
||||
|
||||
@@ -68,7 +65,7 @@ object ConcordInviteBundle {
|
||||
val bundleKey = ConcordKeyDerivation.inviteBundleKey(token)
|
||||
val content = Nip44.v2.encrypt(json(invite), bundleKey).encodePayload()
|
||||
val signer = NostrSignerSync(KeyPair(privKey = linkSignerPrivKey))
|
||||
return signer.signNormal(createdAt, KIND, arrayOf(arrayOf(TAG_D, ""), arrayOf(TAG_VSK, VSK_LIVE)), content)
|
||||
return signer.sign(ConcordInviteBundleEvent.build(content, createdAt))
|
||||
}
|
||||
|
||||
/** Decrypts a kind-33301 bundle [event] with the link [token], or null if it isn't a valid bundle. */
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.concord.cord05Invites.bundle
|
||||
|
||||
import com.vitorpamplona.quartz.concord.cord04Roles.ControlEntityKind
|
||||
import com.vitorpamplona.quartz.concord.cord04Roles.control.tags.VskTag
|
||||
import com.vitorpamplona.quartz.concord.cord04Roles.control.vsk
|
||||
import com.vitorpamplona.quartz.concord.events.ConcordKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* The public invite bundle (CORD-05): a kind-33301 **addressable** event whose
|
||||
* content is a [com.vitorpamplona.quartz.concord.cord05Invites.CommunityInvite]
|
||||
* NIP-44-encrypted under the bundle key derived from the link's 16-byte unlock
|
||||
* token. Tagged `["d",""]` (so `link_signer` has exactly one live bundle) and
|
||||
* `["vsk","6"]` ([ControlEntityKind.INVITE_LIVE]).
|
||||
*
|
||||
* A relay that indexes the naddr never holds the token, so it can never open the
|
||||
* bundle. Minting/parsing/validation live in
|
||||
* [com.vitorpamplona.quartz.concord.cord05Invites.ConcordInviteBundle]; this is the
|
||||
* wire event it builds and that [com.vitorpamplona.quartz.utils.EventFactory] parses.
|
||||
*/
|
||||
class ConcordInviteBundleEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
/** The versioned sub-kind marker (`vsk`), expected to be [ControlEntityKind.INVITE_LIVE]. */
|
||||
fun versionedSubKind() = tags.vsk()
|
||||
|
||||
companion object {
|
||||
const val KIND = ConcordKinds.INVITE_BUNDLE
|
||||
|
||||
/** Builds the addressable bundle template carrying the already-encrypted [encryptedInvite]. */
|
||||
fun build(
|
||||
encryptedInvite: String,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<ConcordInviteBundleEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, encryptedInvite, createdAt) {
|
||||
dTag("")
|
||||
addUnique(VskTag.assemble(ControlEntityKind.INVITE_LIVE))
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ package com.vitorpamplona.quartz.utils
|
||||
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent
|
||||
import com.vitorpamplona.quartz.concord.cord04Roles.control.ControlEditionEvent
|
||||
import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent
|
||||
import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
@@ -607,6 +608,7 @@ class EventFactory {
|
||||
RequestToVanishEvent.KIND -> RequestToVanishEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ConcordCommunityListEvent.KIND -> ConcordCommunityListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ControlEditionEvent.KIND -> ControlEditionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ConcordInviteBundleEvent.KIND -> ConcordInviteBundleEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
SealedRumorEvent.KIND -> SealedRumorEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
SearchRelayListEvent.KIND -> SearchRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
SimpleGroupListEvent.KIND -> SimpleGroupListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
Reference in New Issue
Block a user