From bd664283b7117062bddd521c2a48bccbcdf6b2b0 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Thu, 24 Sep 2026 13:36:24 -0700 Subject: [PATCH] Ship the migrators in the TwoMLSPQ product With TwoMLSPQBinding in two products, Xcode can link a second binding copy into one process, and a callback through it aborts on a stale handle. Removes the TwoMLSPQMigrate product. Co-Authored-By: Claude Opus 5.5 --- .changeset/single-binding-product.md | 14 +++++++++++++ Package.swift | 30 +++++++++++++--------------- book/src/introduction.md | 6 ++++-- 3 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .changeset/single-binding-product.md diff --git a/.changeset/single-binding-product.md b/.changeset/single-binding-product.md new file mode 100644 index 0000000..3c9d479 --- /dev/null +++ b/.changeset/single-binding-product.md @@ -0,0 +1,14 @@ +--- +"@germ-network/two-mls-pq": minor +--- + +**Breaking:** the `TwoMLSPQMigrate` product is removed. Its module now ships in the `TwoMLSPQ` +product, so `import TwoMLSPQMigrate` (and `import TwoMLSPQBinding`) keep working for any target +that depends on `TwoMLSPQ`. On every target that declared it, swap +`.product(name: "TwoMLSPQMigrate", package: "TwoMLSPQ")` for +`.product(name: "TwoMLSPQ", package: "TwoMLSPQ")`. Imports are unchanged. + +With the binding in two products, an Xcode build could link a second copy into one process, for +example the `TwoMLSPQ` product as a shared framework plus `TwoMLSPQMigrate` linked statically into a +test bundle. The first callback through the other copy then aborted with "Callback interface +failure" (`unexpectedStaleHandle`). The binding now lives in exactly one product. diff --git a/Package.swift b/Package.swift index ab4dd31..05144ba 100644 --- a/Package.swift +++ b/Package.swift @@ -43,13 +43,18 @@ let package = Package( platforms: [.iOS(.v18), .macOS(.v15)], products: [ // The forward-looking PUBLIC product: the concrete PQ types (`PQSession`, - // `PQInvitation`, `PQClient`, …), their value/currency types, and the UniFFI - // binding. The backward-compat shim PROTOCOLS live in the separate - // `AbstractTwoMLS` package (which depends on and re-exports this), keeping this - // product's surface clear of the legacy-shim abstraction. + // `PQInvitation`, `PQClient`, …), their value/currency types, the UniFFI + // binding, and the migrators (`import TwoMLSPQMigrate`). The backward-compat shim + // PROTOCOLS live in the separate `AbstractTwoMLS` package (which depends on and + // re-exports this), keeping this product's surface clear of the legacy-shim + // abstraction. + // Every target that depends on `TwoMLSPQBinding` must ship in THIS product and no + // other. Xcode dedups per product, so a second product carrying the binding links a + // second copy into the same process. Each copy keeps its own callback handle map + // while Rust keeps one vtable, and a callback then aborts on a stale handle. .library( name: "TwoMLSPQ", - targets: ["TwoMLSPQ"] + targets: ["TwoMLSPQ", "TwoMLSPQMigrate"] ), // The Rust-free slice: the pure-Swift currency types, no binding/xcframework. // For all-Swift consumers (e.g. Android builds, where the xcframework has no @@ -58,15 +63,9 @@ let package = Package( name: "TwoMLSPQTypes", targets: ["TwoMLSPQTypes"] ), - // The invitation migrator library (GER-2372 R3) — consumes the Rust migration - // export and mints a native twomlspq-swift invitation archive. - .library( - name: "TwoMLSPQMigrate", - targets: ["TwoMLSPQMigrate"] - ) ], dependencies: [ - // TEST-ONLY. The public product has no external Swift dependencies: digests and + // TEST-ONLY. The `TwoMLSPQ` module has no external Swift dependencies: digests and // routing ids cross its surface as self-describing `Data` this package owns (see // PQDigest.swift), so a suite change ships from here without a CommProtocol // release. The test target still mints client ids with `AgentPrivateKey` the way @@ -130,10 +129,9 @@ let package = Package( // `TwoMlsPqSession.migrationExport`, GER-2433 C1) onto twomlspq-swift's // `InvitationMigration.mintArchive` / `SessionMigration.mintArchive`, MINTING // native `SecretArchive`s from legacy Rust state (dual-read / single-write: - // the Rust engine stays a read-only legacy decoder). Separate target so its - // twomlspq-swift dependency — and that package's `Invitation`/`ClientID` type - // names, which collide with this package's — stay out of the public - // `TwoMLSPQ` product. + // the Rust engine stays a read-only legacy decoder). A separate module, shipped in + // the `TwoMLSPQ` product, so twomlspq-swift's `Invitation`/`ClientID` type names, + // which collide with this package's, stay out of the `TwoMLSPQ` module. .target( name: "TwoMLSPQMigrate", dependencies: [ diff --git a/book/src/introduction.md b/book/src/introduction.md index 58c54e7..a2d8cd2 100644 --- a/book/src/introduction.md +++ b/book/src/introduction.md @@ -23,8 +23,10 @@ App ``` -The Swift package has **no external Swift dependencies**. CommProtocol is a sibling under the -app, not a layer beneath: the two meet in app code, which carries values between them. +The `TwoMLSPQ` module has **no external Swift dependencies**. The product's migrator module +(`TwoMLSPQMigrate`) depends on twomlspq-swift, swift-mls and swift-secret-bytes, the native engine +it mints into. CommProtocol is a sibling under the app, not a layer beneath: the two meet in app +code, which carries values between them. Digests cross the Swift API as self-describing tagged `Data` — `[kind][digest]` — that this package derives (`PQDigest.over(_:)`) and compares. The kind tag matters: the digest algorithm