Skip to content

xmtp_proto: bump to xmtp/proto XIP-82 revisions (typed invite primitives) + adapt invite::payload#3751

Open
tylerhawkes wants to merge 1 commit into
mainfrom
tyler/qr-invite-l4b-proto-rebump
Open

xmtp_proto: bump to xmtp/proto XIP-82 revisions (typed invite primitives) + adapt invite::payload#3751
tylerhawkes wants to merge 1 commit into
mainfrom
tyler/qr-invite-l4b-proto-rebump

Conversation

@tylerhawkes

@tylerhawkes tylerhawkes commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What

Bumps xmtp_proto to xmtp/proto#336 (the XIP-82 review revisions: typed invite primitives, max_uses, refresh_pointers, blob AAD + effective-expiry semantics, the admitted_via_external_group_id membership tag) and adapts the code already on main to the new wire shape. New behavior (AAD encryption, validators, policy fields) lands layer-by-layer in the open QR-invite stack, which rebases onto this PR: #3671#3666#3667#3668#3673#3674.

invite::payload (merged in #3670, adapted here)

  • symmetric_key is now a SymmetricKey submessage: validate distinguishes absent (MissingSymmetricKey — absence is only a legal encoding on the policy component, never the payload) from wrong-length material.
  • service_pointer is now an optional ServicePointer: absent = application-resolved (first-party scanner with a baked-in endpoint; the QR carries no fetch target), present requires exactly one location variant (present-but-empty fails closed), and https_url locations are parsed and scheme-checked (url crate, already a workspace dep).
  • New helpers: https_service_pointer / opaque_service_pointer / validate_service_pointer; build_payload takes the typed pointer.
  • Tests extended: absent-pointer accepted, empty-pointer rejected, non-https/garbage URLs rejected, missing-key rejected.

admitted_via_external_group_id construction sites

All set to empty (= absent on the wire), which is correct on this branch because nothing can write the tag yet (the external-commit join path doesn't exist until #3674):

  • AppData migrator (migration.rs): synthesized entries are all Welcome/legacy members — absent is permanently correct for them.
  • update_group_membership.rs (encode_membership_entry): the Update arm rewrites entries from scratch, so once joins can tag entries this must carry the existing tag through (the tag is write-once; validators will reject member commits that clear it). That preservation deliberately lands together with the validator's write-once enforcement in the stack, so the two halves are reviewable as one unit — documented in a comment at the site.

Verification

  • cargo check --locked clean; just lint-rust clean.
  • cargo nextest -p xmtp_mls_common: 806 passed. Touched xmtp_mls module tests (app_data / membership): 149 passed.

🤖 Generated with Claude Code

Note

Bump xmtp_proto to XIP-82 and adapt invite payload to typed primitives

  • Updates generated protobuf bindings to XIP-82, introducing typed SymmetricKey, GroupStateHash, ServicePointer, ExternalInvitePayloadV1, and ExternalCommitPolicyV1 message types in xmtp.mls.message_contents.rs.
  • Reworks invite/payload.rs to accept Option<ServicePointer> in build_payload, wrap symmetric key bytes in nested SymmetricKey.material, and validate https URLs and pointer presence.
  • Extends InvitePayloadError with MissingSymmetricKey, EmptyServicePointer, and InvalidHttpsUrl variants for finer-grained validation failures.
  • Adds admitted_via_external_group_id field (defaulting to empty bytes) to GroupMembershipEntry.V1 across membership construction, migration, and sync paths.
  • Risk: serde impls for several message types changed from base64 raw bytes to structured nested objects, breaking JSON wire compatibility for any callers serializing these types.

Macroscope summarized 650ba97.

@tylerhawkes tylerhawkes requested a review from a team as a code owner June 9, 2026 22:53
@macroscopeapp

macroscopeapp Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@tylerhawkes tylerhawkes force-pushed the tyler/qr-invite-l4b-proto-rebump branch from 51ada44 to a96dde2 Compare June 9, 2026 23:15
Comment on lines +134 to +144
fn validate_https_url(raw: &str) -> Result<(), InvitePayloadError> {
let parsed =
url::Url::parse(raw).map_err(|e| InvitePayloadError::InvalidHttpsUrl(e.to_string()))?;
if parsed.scheme() != "https" {
return Err(InvitePayloadError::InvalidHttpsUrl(format!(
"scheme must be https (got {})",
parsed.scheme()
)));
}
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low invite/payload.rs:134

validate_https_url accepts URLs with an empty authority like https:///blob because it only checks the scheme, not that a host is present. These URLs parse successfully but provide no fetch target, so the joiner cannot resolve the service pointer. Consider requiring parsed.host_str().is_some() to reject authority-less URLs.

fn validate_https_url(raw: &str) -> Result<(), InvitePayloadError> {
    let parsed =
        url::Url::parse(raw).map_err(|e| InvitePayloadError::InvalidHttpsUrl(e.to_string()))?;
    if parsed.scheme() != "https" {
        return Err(InvitePayloadError::InvalidHttpsUrl(format!(
            "scheme must be https (got {})",
            parsed.scheme()
        )));
    }
+    if parsed.host_str().is_none() {
+        return Err(InvitePayloadError::InvalidHttpsUrl(
+            "URL must have a host".to_string(),
+        ));
+    }
    Ok(())
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/xmtp_mls_common/src/invite/payload.rs around lines 134-144:

`validate_https_url` accepts URLs with an empty authority like `https:///blob` because it only checks the scheme, not that a host is present. These URLs parse successfully but provide no fetch target, so the joiner cannot resolve the service pointer. Consider requiring `parsed.host_str().is_some()` to reject authority-less URLs.

@tylerhawkes tylerhawkes force-pushed the tyler/qr-invite-l4b-proto-rebump branch from a96dde2 to bd245bf Compare June 9, 2026 23:25
@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown

Claude finished @tylerhawkes's task —— View job


Code Review - 2026-06-11 23:42 UTC

Review Summary

I've reviewed the XIP-82 proto bump and invite payload adaptation. The changes are well-structured and follow good defensive programming practices. Here are my findings:

✅ Strengths

  1. Excellent documentation - The invite/payload.rs module has clear, comprehensive documentation explaining the validation logic and constraints
  2. Defense-in-depth validation - Both construction-time (build_payload) and wire-time (validate) checks prevent malformed payloads
  3. Comprehensive test coverage - Tests cover edge cases including absent pointers, empty pointers, URL validation, and key/ID length checks
  4. Clear migration path - The admitted_via_external_group_id field is properly initialized as empty with clear comments explaining why (crates/xmtp_mls/src/groups/mls_sync/update_group_membership.rs:95-102, crates/xmtp_mls/src/groups/app_data/migration.rs:288-291)

🔍 Issues Found

1. URL Host Validation Gap (already noted by macroscopeapp)

The validate_https_url function (invite/payload.rs:134-144) only checks the scheme but not whether a host is present. URLs like https:///blob would pass validation but provide no fetch target.

Suggested fix:

if parsed.host_str().is_none() {
    return Err(InvitePayloadError::InvalidHttpsUrl(
        "URL must have a host".to_string(),
    ));
}

2. Write-Once Tag Preservation Incomplete (acknowledged in code)

In update_group_membership.rs:103, encode_membership_entry creates entries with empty admitted_via_external_group_id. The comment correctly notes this will need to preserve existing tags once the external-commit path lands, but the Update arm in build_group_membership_app_data_payload (line 68) doesn't preserve the tag yet.

Risk: When the external-commit join path lands in the stack, the Update arm could inadvertently clear write-once tags, which validators would reject.

Recommendation: Add a TODO or tracking issue reference at line 68 to ensure this gets addressed when #3674 lands.

📝 Minor Observations

  1. Large generated file diffs - The serde changes in xmtp.mls.message_contents.serde.rs are extensive (392 additions) but appear to be correctly generated protobuf code. Spot-checking shows proper handling of typed fields.

  2. Migration code is well-isolated - The admitted_via_external_group_id changes are properly scoped to construction sites with clear explanations of correctness on this branch.

  3. Test determinism - The migration tests (e.g., synthesize_initial_component_values_is_deterministic) properly verify byte-stability, which is critical for cross-peer validation.

🎯 Verdict

Approve with minor recommendation: The code quality is high and the adaptation is correct for this branch. The URL validation gap should be addressed (already flagged by macroscopeapp), and I recommend adding a tracking reference for the tag preservation issue to ensure it's handled when the dependent PRs land.


@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.45%. Comparing base (8ad390a) to head (bd245bf).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3751      +/-   ##
==========================================
- Coverage   84.45%   84.45%   -0.01%     
==========================================
  Files         408      408              
  Lines       59772    59869      +97     
==========================================
+ Hits        50483    50561      +78     
- Misses       9289     9308      +19     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…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.
@tylerhawkes tylerhawkes force-pushed the tyler/qr-invite-l4b-proto-rebump branch from bb4f688 to 650ba97 Compare June 11, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants