feat: XIP-82 review revisions — typed invite primitives, max_uses, refresh_pointers, AAD + effective expiry, admitted_via tag#336
Open
tylerhawkes wants to merge 1 commit into
Conversation
Contributor
ApprovabilityVerdict: Needs human review This PR introduces new protocol message types, new fields with validation semantics (max_uses admission limits, admitted_via tracking), and wire-format changes (bytes to typed messages). These schema changes define new feature capabilities and enforcement logic that warrant human review. You can customize Macroscope's approvability policy. Learn more. |
5c762f0 to
160192a
Compare
tylerhawkes
added a commit
to xmtp/libxmtp
that referenced
this pull request
Jun 11, 2026
…ves) + adapt invite::payload Regenerates xmtp_proto from xmtp/proto#336 (typed SymmetricKey / GroupStateHash / ServicePointer newtypes, ExternalCommitPolicyV1 max_uses + refresh_pointers, EncryptedGroupInfoBlobV1 AAD + effective-expiry semantics, GroupMembershipEntry.V1 admitted_via_external_group_id). Consuming changes kept to what main already contains: - invite::payload rewritten for the typed shape: symmetric_key is a SymmetricKey submessage (MissingSymmetricKey + material-length checks), service_pointer is an optional ServicePointer (absent = application-resolved; present-but-empty oneof fails closed; https_url parsed and scheme-checked via url). New https_service_pointer / opaque_service_pointer / validate_service_pointer helpers. - GroupMembershipEntry.V1 construction sites gain the new admitted_via_external_group_id field, empty everywhere: the migrator's synthesized entries are all Welcome/legacy members (absent is permanently correct), and the membership-update rewrite path documents that tag preservation MUST land together with the validator's write-once enforcement in the external-commit stack — nothing on main can set the tag yet, so empty is correct today. The rest of the QR-invite stack (#3671 -> #3666 -> #3667 -> #3668 -> #3673 -> #3674) rebases onto this and picks up the new fields layer by layer.
allow_external_commit itself authorizes the joiner-self-entry insert, so the enable is a single-component write again and the multi-update intent extension comes out entirely (it never existed on main). external_committer_permissions comments reframed as the reserved deny-by-default surface for writes beyond the v1 atomic shape.
26de115 to
c78e379
Compare
tylerhawkes
added a commit
to xmtp/libxmtp
that referenced
this pull request
Jun 11, 2026
…ves) + adapt invite::payload Regenerates xmtp_proto from xmtp/proto#336 (typed SymmetricKey / GroupStateHash / ServicePointer newtypes, ExternalCommitPolicyV1 max_uses + refresh_pointers, EncryptedGroupInfoBlobV1 AAD + effective-expiry semantics, GroupMembershipEntry.V1 admitted_via_external_group_id). Consuming changes kept to what main already contains: - invite::payload rewritten for the typed shape: symmetric_key is a SymmetricKey submessage (MissingSymmetricKey + material-length checks), service_pointer is an optional ServicePointer (absent = application-resolved; present-but-empty oneof fails closed; https_url parsed and scheme-checked via url). New https_service_pointer / opaque_service_pointer / validate_service_pointer helpers. - GroupMembershipEntry.V1 construction sites gain the new admitted_via_external_group_id field, empty everywhere: the migrator's synthesized entries are all Welcome/legacy members (absent is permanently correct), and the membership-update rewrite path documents that tag preservation MUST land together with the validator's write-once enforcement in the external-commit stack — nothing on main can set the tag yet, so empty is correct today. The rest of the QR-invite stack (#3671 -> #3666 -> #3667 -> #3668 -> #3673 -> #3674) rebases onto this and picks up the new fields layer by layer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Follow-up to #334, mirroring the review-revised XIP-82 (xmtp/XIPs#140). Wire note up front: wrapping
bytesin a submessage is a wire-format change, but the raw layout never shipped (#334 is unreleased), so there is no compatibility constraint to honor.external_invite.protoSymmetricKey {material},GroupStateHash {digest},ServicePointer {oneof https_url | opaque}— replace rawbytes. No algorithm enums: the versioned envelope pins the AEAD, the group's ciphersuite pins the hash, so an in-message tag could only contradict them. Length rules (32-byte key, ciphersuite-length digest) are validator/setter checks, documented on the types.ExternalInvitePayloadV1.service_pointer→ServicePointer, MAY be absent (application-resolved service; keeps the fetch target out of the QR). Present-but-empty oneof = parse failure (fail closed).EncryptedGroupInfoBlobV1:epoch || expires_at_ns(8-byte BE each)|| group_state_hash.digest— all plaintext metadata is authenticated, so a keyless writer can't doctor a captured blob (e.g. extend its expiry).expires_at_nsis redefined as the uploader-computed effective expiry:min(policy.expires_at_ns, epoch_start + policy.expire_in_ns)(0 = no bound drops out). The joiner's single expiry check then also skips candidates validators would reject as stale — no zombie joins — and service TTL-GC collects staleness-dead blobs.epoch = u64::MAXupload permanently wedge a slot.external_commit_policy.protosymmetric_key→SymmetricKey(absent = the only cleared encoding; empty submessage / emptymaterialinvalid).uint32 max_uses = 6: concurrent per-invite cap, counted as the number of liveGROUP_MEMBERSHIPentries tagged with the activeexternal_group_id— state-derived, so every member (including post-issuance joiners) converges. Durable setting.repeated ServicePointer refresh_pointers = 7: locations members use to keep the blob fresh across epoch advances (jittered check-before-write; see XIP-82 "Keeping the blob fresh"). Optional — empty preserves pointer secrecy at a liveness cost. Per-invite field.expire_in_ns,max_uses) vs per-invite (symmetric_key,external_group_id,expires_at_ns,refresh_pointers); revoke leaves every per-invite field absent (validator-enforced post-state invariant — a revoked policy is byte-identical to one that never had an invite); enable couples theGROUP_MEMBERSHIPexternal_committer_permissionsgrant in the same commit; fresh-CSPRNG-key-on-every-enable replaces key-history tracking; both time bounds are evaluated against delivery-service envelope timestamps (never validator wall-clock).group_membership.protoGroupMembershipEntry.V1gainsbytes admitted_via_external_group_id = 3: themax_usesaccounting substrate. Recorded on every external commit (evenmax_uses = 0), write-once (member-sender commits that set/clear/alter it are rejected; entry rewrites must carry it through), absent for Welcome-added members.Consumers
libxmtp picks this up via an
xmtp_protore-bump; the consuming changes land as follow-ups on the open QR-invite PR stack (xmtp/libxmtp#3671 → #3666 → #3667 → #3668 → #3673 → #3674).🤖 Generated with Claude Code
Note
Add typed invite primitives,
max_uses,refresh_pointers, AAD fields, andadmitted_viatag for XIP-82SymmetricKey,GroupStateHash, andServicePointerin external_invite.proto, replacing rawbytesfields acrossExternalInvitePayloadV1andEncryptedGroupInfoBlobV1.ExternalCommitPolicyV1in external_commit_policy.proto to useSymmetricKeyforsymmetric_key, and addsmax_uses(uint32) andrefresh_pointers(repeatedServicePointer) fields.admitted_via_external_group_id(bytes, write-once) toGroupMembershipEntry.V1in group_membership.proto to tag members admitted via an external commit.symmetric_keytype changes frombytestoSymmetricKeyandservice_pointer/group_state_hashsimilarly change frombytesto structured messages — wire-incompatible with any prior serialized messages using these fields.Macroscope summarized c78e379.
Review notes (self-review)
xmtp.mls.message_contentspackage; the newexternal_commit_policy.proto → external_invite.protoimport is acyclic and resolves underbuf build; field numbers are additive; protolint-clean (≤80 cols, existing style).dev/ts/generateandts/index.tsdo not includeexternal_invite.proto/external_commit_policy.proto/component_permissions.proto, so the TS package has never exposed these types. Out of scope for this PR — the consumer is libxmtp (Rust); worth a tiny follow-up when the JS SDK needs invites.