From 86a5c346290b8dc7fb550c828776b5ba3bb7004c Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Tue, 28 Jul 2026 00:14:53 -0700 Subject: [PATCH 1/3] save --- .github/workflows/ci.yml | 10 +- CLAUDE.md | 30 ++--- Package.swift | 42 +++--- README.md | 18 +-- Schemas/common.fbs | 4 +- Schemas/peer_hello.fbs | 8 +- Schemas/signal.fbs | 6 +- Schemas/stream_header.fbs | 6 +- Scripts/e2e-cli.sh | 4 +- Sources/MPCCompat/CompatCore.swift | 8 +- Sources/MPCCompat/MultipeerSession.swift | 20 +-- Sources/MPCCompat/NearbyService.swift | 2 +- Sources/PeerMesh/Configuration.swift | 4 +- Sources/PeerMesh/Events.swift | 2 +- Sources/PeerMesh/PeerSession.swift | 28 ++-- Sources/PeerMesh/PeerTransport.swift | 2 +- Sources/PeerMesh/Reexport.swift | 8 +- .../Security/IdentityCertificate.swift | 2 +- Sources/PeerMesh/Security/IdentityStore.swift | 4 +- .../PeerMesh/Transports/QUICConnection.swift | 2 +- Sources/PeerMesh/Transports/QUICSupport.swift | 30 ++--- Sources/PeerMesh/Transports/QUICTLS.swift | 10 +- .../PeerMesh/Transports/QUICTransport.swift | 16 +-- Sources/PeerMeshCLI/PeerMeshCLI.swift | 18 +-- .../Generated/common_generated.swift | 6 +- .../Generated/peer_hello_generated.swift | 18 +-- .../Generated/signal_generated.swift | 126 +++++++++--------- .../Generated/stream_header_generated.swift | 38 +++--- Sources/PeerMeshProtocol/LibP2PIdentity.swift | 6 +- Sources/PeerMeshProtocol/Locked.swift | 2 +- .../PeerMeshProtocol/ProtocolVersion.swift | 2 +- Sources/PeerMeshProtocol/Signal.swift | 52 ++++---- Sources/PeerMeshProtocol/SignalCodec.swift | 4 +- .../PeerMeshTestKit/InMemoryTransport.swift | 12 +- Sources/PeerMeshUI/PeerBrowserView.swift | 4 +- Tests/MPCCompatTests/MPCCompatE2ETests.swift | 6 +- Tests/MPCCompatTests/MPCCompatTests.swift | 4 +- .../ProtocolEngineTests.swift | 2 +- .../ProtocolVersionTests.swift | 2 +- .../SignalCodecTests.swift | 8 +- Tests/PeerMeshTests/DisconnectionTests.swift | 4 +- .../PeerMeshTests/EndToEndSessionTests.swift | 16 +-- .../PeerMeshTests/IdentitySecurityTests.swift | 4 +- Tests/PeerMeshTests/MeshHarness.swift | 2 +- .../PeerMeshTests/MeshReliabilityTests.swift | 4 +- Tests/PeerMeshTests/PeerHelloCodecTests.swift | 4 +- Tests/PeerMeshTests/PeerMeshTests.swift | 12 +- Tests/PeerMeshTests/QUICSmokeTests.swift | 4 +- .../PeerMeshTests/QUICStreamChurnTests.swift | 2 +- .../QUICWirePrimitiveTests.swift | 2 +- docs/IMPLEMENTATION-PLAN.md | 16 +-- docs/design-mpc-successor.md | 42 +++--- flake.nix | 4 +- 53 files changed, 346 insertions(+), 346 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f990dc7..f172e7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: QUIC_DEBUG_LOG: /tmp/quic-debug.log # Same-machine runs need p2p off (failure mode 10). Peer-to-peer # Wi-Fi coverage is tier 3 (hardware lab) by design. - PEERMESH_NO_P2P: "1" + Stromo_NO_P2P: "1" - name: Diagnostics on failure (QUIC log + crash reports) if: failure() run: | @@ -70,7 +70,7 @@ jobs: run: ./Scripts/e2e-cli.sh env: QUIC_DEBUG: "1" - PEERMESH_NO_P2P: "1" + Stromo_NO_P2P: "1" # Same test bundle per Apple destination. iOS Simulator runs on PRs (the # availability-floor check); Mac Catalyst only on pushes to main. @@ -106,7 +106,7 @@ jobs: # xcbeautify streams progress live — a hung xcodebuild shows where # it stalled instead of buffering silently until the timeout. xcodebuild test \ - -scheme PeerMesh-Package \ + -scheme Stromo-Package \ -destination '${{ steps.dest.outputs.destination }}' \ | xcbeautify --renderer github-actions @@ -122,7 +122,7 @@ jobs: run: | set -o pipefail xcodebuild test \ - -scheme PeerMesh-Package \ + -scheme Stromo-Package \ -destination 'platform=macOS,variant=Mac Catalyst' \ | xcbeautify --renderer github-actions @@ -145,7 +145,7 @@ jobs: - name: Regenerate and diff run: | /tmp/flatc-bin/flatc --swift -o /tmp/generated Schemas/*.fbs - diff -r /tmp/generated Sources/PeerMeshProtocol/Generated \ + diff -r /tmp/generated Sources/StromoProtocol/Generated \ || { echo '::error::Generated sources drifted from schemas (DD-5 rule 4)'; exit 1; } # TODO(S-6): nightly workflow_dispatch job — stream-churn benchmark and 32-peer diff --git a/CLAUDE.md b/CLAUDE.md index f0b0014..3a65767 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ -# CLAUDE.md — PeerMesh contributor guide (AI & human) +# CLAUDE.md — Stromo contributor guide (AI & human) -PeerMesh is an open-source replacement for Apple's deprecated +Stromo is an open-source replacement for Apple's deprecated MultipeerConnectivity, built on Network.framework + QUIC. The codebase is split by the sans-I/O discipline (DD-6): a pure, deterministic protocol engine that decides, and thin async drivers that move bytes. Read @@ -12,29 +12,29 @@ the hard-won platform-findings log and the source of the failure modes below. | Module | Kind | What lives here | |---|---|---| -| **PeerMeshProtocol** | library (sans-I/O) | `ProtocolEngine` state machine, `Signal` model + `SignalCodec` (FlatBuffers verifier), `PeerID`/`LibP2PIdentity`, `Delivery`/`Recipients`/`PeerMeshError`, `Locked`. Only dependency: FlatBuffers. | -| **PeerMesh** | library (runtime) | `PeerSession` actor (effect executor), `QUICTransport` + QUIC driver, Security (identity, certificate, keychain stores, `TrustEvaluator`), public event/config types. Re-exports PeerMeshProtocol so apps write only `import PeerMesh`. | +| **StromoProtocol** | library (sans-I/O) | `ProtocolEngine` state machine, `Signal` model + `SignalCodec` (FlatBuffers verifier), `PeerID`/`LibP2PIdentity`, `Delivery`/`Recipients`/`StromoError`, `Locked`. Only dependency: FlatBuffers. | +| **Stromo** | library (runtime) | `PeerSession` actor (effect executor), `QUICTransport` + QUIC driver, Security (identity, certificate, keychain stores, `TrustEvaluator`), public event/config types. Re-exports StromoProtocol so apps write only `import Stromo`. | | **MPCCompat** | library | Near-drop-in `MCSession`/`MCPeerID`/advertiser/browser analogs over `PeerSession` (FR-24). | -| **PeerMeshUI** | library | SwiftUI peer picker + invitation consent (FR-23). **Experimental preview, API unstable.** | -| **PeerMeshTestKit** | library | `InMemoryTransport` (+ mesh sim) and `ReorderingTransport` — CI without radios (QA-8). | -| **PeerMeshCLI** (`peermesh-cli`) | executable | Diagnostic advertise/browse/host/join over real Bonjour+QUIC between processes. | +| **StromoUI** | library | SwiftUI peer picker + invitation consent (FR-23). **Experimental preview, API unstable.** | +| **StromoTestKit** | library | `InMemoryTransport` (+ mesh sim) and `ReorderingTransport` — CI without radios (QA-8). | +| **StromoCLI** (`Stromo-cli`) | executable | Diagnostic advertise/browse/host/join over real Bonjour+QUIC between processes. | ## Build & test - `swift test` — tiers 1 & 2 (engine + loopback QUIC), fast macOS loop. 54 tests today. - `./Scripts/e2e-cli.sh` — cross-process Bonjour+QUIC exchange (host + joiner as separate processes). Caught the FIN and inbound-retention bugs. -- `xcodebuild test -scheme PeerMesh-Package -destination ''` for other Apple targets (same suite, N destinations): +- `xcodebuild test -scheme Stromo-Package -destination ''` for other Apple targets (same suite, N destinations): - iOS simulator: `-destination 'platform=iOS Simulator,id='` (resolve a UDID via `xcrun simctl list devices available`). - Mac Catalyst: `-destination 'platform=macOS,variant=Mac Catalyst'`. ### Environment flags - `QUIC_DEBUG=1` — enable the QUIC driver's diagnostic log to stdout. - `QUIC_DEBUG_LOG=` — also append that log to a file (survives sandboxing). -- `PEERMESH_NO_P2P=1` — disable `includePeerToPeer`. Required for **in-process** entitled E2E tests: `includePeerToPeer` breaks same-machine self-dials (failure mode 10). Never set it for real cross-device runs. +- `Stromo_NO_P2P=1` — disable `includePeerToPeer`. Required for **in-process** entitled E2E tests: `includePeerToPeer` breaks same-machine self-dials (failure mode 10). Never set it for real cross-device runs. ### FlatBuffers regeneration (DD-5 rule 4) ``` -flatc --swift -o Sources/PeerMeshProtocol/Generated Schemas/*.fbs +flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs ``` Generated sources are committed and CI fails on drift. **Exact-version pin rule:** the `flatc` in `flake.nix` MUST equal the `google/flatbuffers` runtime @@ -43,18 +43,18 @@ the runtime that reads it are one unit. Bump both together. ## Hard architectural rules -- **PeerMeshProtocol stays sans-I/O.** No sockets, no clocks, no async, no +- **StromoProtocol stays sans-I/O.** No sockets, no clocks, no async, no Foundation I/O. Its only dependency is FlatBuffers. Time enters the engine as an `Input`, never from a clock. (`Locked`/`NSLock` is a synchronization primitive, not I/O, and is allowed.) -- **Engine changes land with tier-1 tests first** (`Tests/PeerMeshProtocolTests`). +- **Engine changes land with tier-1 tests first** (`Tests/StromoProtocolTests`). The engine is `handle(Input) -> [Effect]`, pure and deterministic — assert on the effect list. - **FlatBuffers schema evolution:** field ids are append-only; never renumber, retype, or remove (only `(deprecated)`); no new `(required)` after 1.0; enum/union values append-only with `UNKNOWN = 0`. Every inbound buffer goes through the verifier (`getCheckedRoot`, hard caps). Generated code is - committed. Do NOT hand-edit `Schemas/` or `Sources/PeerMeshProtocol/Generated/`. + committed. Do NOT hand-edit `Schemas/` or `Sources/StromoProtocol/Generated/`. - **Platform floor: iOS 15 / macOS 12** (QUIC floor). No `Duration` (use `TimeInterval`). RFC 9221 datagrams are iOS 16/macOS 13, below the floor — see failure mode 1 and the `StreamKind.Datagram` channel mapping. @@ -106,7 +106,7 @@ code that guards them without understanding why it exists. interface assertion MUST be validated in the mesh-hardware spike before radio release. 10. **`includePeerToPeer` breaks same-machine self-dials.** Use - `PEERMESH_NO_P2P=1` for in-process tests; CI sets it for every + `Stromo_NO_P2P=1` for in-process tests; CI sets it for every same-machine job. Note `ProcessInfo.environment` caches at first access; the driver reads this flag via `getenv` so a test's `setenv` actually lands. @@ -153,7 +153,7 @@ In-code TODOs reference these by name: `// TODO(ledger-name): one line`. - **churn-benchmark** — S-6 formal stream-churn benchmark (nightly CI stub in `ci.yml`). - **pairing-code** — `.pairingCode` transcript binding (S-4, DD-2). - **compat-nsstream-bridge** — `MPCCompat.startStream` `NSStream` bridge over `PeerByteStream`. -- **ui-completion** — PeerMeshUI beyond the current skeleton. +- **ui-completion** — StromoUI beyond the current skeleton. - **mesh-join** — join via endpoint exchange for gossiped roster members (roster names peers we haven't discovered). - **send-ack** — send-acknowledgement API (`PeerSession.send` returns before transport handoff). - **mesh-hardware** — S-2 mesh-ceiling and S-5 backgrounding hardware spikes. diff --git a/Package.swift b/Package.swift index a226a9e..1a2dd74 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,7 @@ import PackageDescription let package = Package( - name: "PeerMesh", + name: "Stromo", platforms: [ .iOS(.v15), .macOS(.v12), @@ -12,21 +12,21 @@ let package = Package( products: [ // Sans-I/O protocol engine: deterministic state machine + signal model, // fully testable without any transport (DD-6). - .library(name: "PeerMeshProtocol", targets: ["PeerMeshProtocol"]), + .library(name: "StromoProtocol", targets: ["StromoProtocol"]), // Modern core: discovery, sessions, messaging, streams, resources over QUIC (DD-1). - .library(name: "PeerMesh", targets: ["PeerMesh"]), + .library(name: "Stromo", targets: ["Stromo"]), // Near-drop-in migration surface for MultipeerConnectivity codebases (FR-24). .library(name: "MPCCompat", targets: ["MPCCompat"]), // SwiftUI peer picker and invitation consent components (FR-23). - .library(name: "PeerMeshUI", targets: ["PeerMeshUI"]), + .library(name: "StromoUI", targets: ["StromoUI"]), // In-memory transport and mesh simulation for CI without radios (QA-8). - .library(name: "PeerMeshTestKit", targets: ["PeerMeshTestKit"]), + .library(name: "StromoTestKit", targets: ["StromoTestKit"]), // Diagnostic CLI: advertise/browse/chat between real processes over // Bonjour + QUIC (the production discovery path). - // Named "peermesh-cli", not "peermesh": a product differing from the - // PeerMesh library only by case collides on case-insensitive APFS when + // Named "Stromo-cli", not "Stromo": a product differing from the + // Stromo library only by case collides on case-insensitive APFS when // xcodebuild materializes package products as modules. - .executable(name: "peermesh-cli", targets: ["PeerMeshCLI"]), + .executable(name: "Stromo-cli", targets: ["StromoCLI"]), ], dependencies: [ // Signaling plane serialization (DD-5). Pinned EXACTLY to match the @@ -35,42 +35,42 @@ let package = Package( // doesn't import). Bump both together (DD-5 rule 4). .package(url: "https://github.com/google/flatbuffers.git", exact: "25.2.10"), // Pure-Swift X.509 for self-signed identity certificates (DD-2). Used by - // the PeerMesh target only — PeerMeshProtocol stays FlatBuffers-only. + // the Stromo target only — StromoProtocol stays FlatBuffers-only. .package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.0"), ], targets: [ // Sans-I/O (DD-6): the ONLY dependency is FlatBuffers (pure CPU). // No Network.framework, no clocks, no async — keep it that way. .target( - name: "PeerMeshProtocol", + name: "StromoProtocol", dependencies: [ .product(name: "FlatBuffers", package: "flatbuffers") ] ), // Runtime shell: executes engine Effects against real transports. .target( - name: "PeerMesh", + name: "Stromo", dependencies: [ - "PeerMeshProtocol", + "StromoProtocol", .product(name: "X509", package: "swift-certificates"), // The QUIC driver serializes the DD-7 `StreamHeader` prologue of // every dedicated stream (the generated FlatBuffers type lives in - // PeerMeshProtocol). Moving bytes onto the wire is driver work + // StromoProtocol). Moving bytes onto the wire is driver work // (DD-6); no protocol decisions live here. .product(name: "FlatBuffers", package: "flatbuffers"), ] ), - .target(name: "MPCCompat", dependencies: ["PeerMesh"]), - .target(name: "PeerMeshUI", dependencies: ["PeerMesh"]), - .target(name: "PeerMeshTestKit", dependencies: ["PeerMesh"]), - .executableTarget(name: "PeerMeshCLI", dependencies: ["PeerMesh"]), + .target(name: "MPCCompat", dependencies: ["Stromo"]), + .target(name: "StromoUI", dependencies: ["Stromo"]), + .target(name: "StromoTestKit", dependencies: ["Stromo"]), + .executableTarget(name: "StromoCLI", dependencies: ["Stromo"]), // Tier 1 (DD-6): engine tests — no transport, no radios, deterministic. - .testTarget(name: "PeerMeshProtocolTests", dependencies: ["PeerMeshProtocol"]), + .testTarget(name: "StromoProtocolTests", dependencies: ["StromoProtocol"]), .testTarget( - name: "PeerMeshTests", - dependencies: ["PeerMesh", "PeerMeshTestKit"] + name: "StromoTests", + dependencies: ["Stromo", "StromoTestKit"] ), - .testTarget(name: "MPCCompatTests", dependencies: ["MPCCompat", "PeerMeshTestKit"]), + .testTarget(name: "MPCCompatTests", dependencies: ["MPCCompat", "StromoTestKit"]), ], swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md index 44e98ab..47a908a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PeerMesh +# Stromo **The open-source replacement for Apple's deprecated MultipeerConnectivity framework** — peer-to-peer sessions over Network.framework + QUIC. @@ -10,7 +10,7 @@ camera app streaming live preview at 33 fps device-to-device with no infrastructure. ```swift -import PeerMesh +import Stromo let session = PeerSession(name: "Dario's iPhone", service: "_myapp._udp") try await session.startAdvertising() @@ -35,7 +35,7 @@ typealias file makes it a near-drop-in swap — see ## Install ```swift -.package(url: "https://github.com/security-union/PeerMesh.git", from: "1.0.0") +.package(url: "https://github.com/security-union/Stromo.git", from: "1.0.0") ``` Platforms: iOS 15+ · macOS 12+ · tvOS 15+ · visionOS 1+. Apps need @@ -49,29 +49,29 @@ The recipe below is exactly how [Remote Shutter](https://github.com/security-uni (a shipping App Store camera app) migrated off MPC — the app diff was a few imports, one new file, and its peer-ID cache. -**1. Add the package** to your app target, linking the `PeerMesh` and +**1. Add the package** to your app target, linking the `Stromo` and `MPCCompat` products: ```swift -.package(url: "https://github.com/security-union/PeerMesh.git", from: "1.0.0") +.package(url: "https://github.com/security-union/Stromo.git", from: "1.0.0") ``` **2. Check your `Info.plist`.** Same requirements as MPC: `NSLocalNetworkUsageDescription`, and your service type under -`NSBonjourServices` — PeerMesh uses the `_yourservice._udp` variant (Apple's +`NSBonjourServices` — Stromo uses the `_yourservice._udp` variant (Apple's guidance for MPC apps was to declare both `._tcp` and `._udp`, so most apps already have it). **3. Add one app-local typealias file** — this is the key to a thin diff. Your -app keeps MPC's type names; the implementations come from MPCCompat. PeerMesh +app keeps MPC's type names; the implementations come from MPCCompat. Stromo deliberately does not publish `MC`-prefixed names, so the mapping lives in *your* app: ```swift -// MultipeerCompatAliases.swift — the entire MPC → PeerMesh mapping. +// MultipeerCompatAliases.swift — the entire MPC → Stromo mapping. import MPCCompat -import PeerMesh +import Stromo public typealias MCPeerID = PeerID public typealias MCSession = MultipeerSession diff --git a/Schemas/common.fbs b/Schemas/common.fbs index fdf1700..e480e91 100644 --- a/Schemas/common.fbs +++ b/Schemas/common.fbs @@ -1,9 +1,9 @@ // Shared wire scalars (DD-5), included by signal.fbs and stream_header.fbs. // // Codegen (flatc version pinned in flake.nix, matched exactly by Package.swift): -// flatc --swift -o Sources/PeerMeshProtocol/Generated Schemas/*.fbs +// flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs -namespace PeerMesh.Wire; +namespace Stromo.Wire; /// A 128-bit transfer identifier: the two 8-byte halves of a `uuid_t`, in /// memory order. A fixed-size struct — no vector length word, and the diff --git a/Schemas/peer_hello.fbs b/Schemas/peer_hello.fbs index a20764e..1ba4c40 100644 --- a/Schemas/peer_hello.fbs +++ b/Schemas/peer_hello.fbs @@ -1,4 +1,4 @@ -// PeerMesh transport-level identity bootstrap (PeerHello). +// Stromo transport-level identity bootstrap (PeerHello). // // The first frame in each direction on the control stream, sent immediately // after the QUIC/mTLS handshake. TLS authenticates the peer's KEY; PeerHello @@ -10,15 +10,15 @@ // this table — it is how incompatible peers discover the mismatch and tell // the user to upgrade, instead of dying in an opaque handshake failure. // Breaking changes to this file are forbidden, forever: append-only field -// ids with no pre-1.0 exceptions. The QUIC ALPN ("peermesh/1") is frozen +// ids with no pre-1.0 exceptions. The QUIC ALPN ("Stromo/1") is frozen // for the same reason — bumping it would fail the handshake before hello. // // Codegen (flatc version pinned in flake.nix, matched exactly by Package.swift): -// flatc --swift -o Sources/PeerMeshProtocol/Generated Schemas/*.fbs +// flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs include "signal.fbs"; -namespace PeerMesh.Wire; +namespace Stromo.Wire; table PeerHello { /// The sender's identity. key_hash must match the TLS certificate's key diff --git a/Schemas/signal.fbs b/Schemas/signal.fbs index 95f7db3..754e773 100644 --- a/Schemas/signal.fbs +++ b/Schemas/signal.fbs @@ -1,4 +1,4 @@ -// PeerMesh signaling schema (DD-5: disciplined FlatBuffers). +// Stromo signaling schema (DD-5: disciplined FlatBuffers). // // DISCIPLINE (normative — see design doc DD-5; enforced by CI): // * Field ids are append-only. Never renumber, retype, or remove a field — @@ -12,11 +12,11 @@ // (64 KB control-message size) — see SignalCodec. // // Codegen (flatc version pinned in flake.nix): -// flatc --swift -o Sources/PeerMesh/Signaling/Generated Schemas/signal.fbs +// flatc --swift -o Sources/Stromo/Signaling/Generated Schemas/signal.fbs include "common.fbs"; -namespace PeerMesh.Wire; +namespace Stromo.Wire; /// A session member, as carried in rosters (FR-13). table PeerInfo { diff --git a/Schemas/stream_header.fbs b/Schemas/stream_header.fbs index e6332ad..33e98f5 100644 --- a/Schemas/stream_header.fbs +++ b/Schemas/stream_header.fbs @@ -1,4 +1,4 @@ -// PeerMesh data-plane stream header (DD-7, incl. the message-channel +// Stromo data-plane stream header (DD-7, incl. the message-channel // amendment). // // On the message channel (stream tag 0x02) every message is a size-prefixed @@ -11,11 +11,11 @@ // append-only enum values, verifier + caps on every inbound read. // // Codegen (flatc version pinned in flake.nix, matched exactly by Package.swift): -// flatc --swift -o Sources/PeerMeshProtocol/Generated Schemas/*.fbs +// flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs include "common.fbs"; -namespace PeerMesh.Wire; +namespace Stromo.Wire; enum StreamKind: ubyte { Unknown = 0, diff --git a/Scripts/e2e-cli.sh b/Scripts/e2e-cli.sh index bd940e9..14a3d64 100755 --- a/Scripts/e2e-cli.sh +++ b/Scripts/e2e-cli.sh @@ -1,6 +1,6 @@ #!/bin/bash # Cross-process E2E over the production Bonjour + QUIC path (tier 2.5): -# builds the peermesh CLI and runs host + joiner as SEPARATE processes. +# builds the Stromo CLI and runs host + joiner as SEPARATE processes. # Scenario 1: full discovery → invite → message → echo → departure cycle # (caught the QUIC FIN bug and the inbound-retention bug). # Scenario 2: silent peer death — kill -9 a lingering joiner; the host must @@ -16,7 +16,7 @@ HOST_PID=""; HOST2_PID=""; JOIN2_PID="" trap 'kill $HOST_PID $HOST2_PID $JOIN2_PID 2>/dev/null || true; rm -rf "$LOGDIR"' EXIT swift build > /dev/null -BIN="$(swift build --show-bin-path)/peermesh-cli" +BIN="$(swift build --show-bin-path)/Stromo-cli" # GNU timeout is absent on stock macOS (incl. GitHub runners); perl is not. with_timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; } diff --git a/Sources/MPCCompat/CompatCore.swift b/Sources/MPCCompat/CompatCore.swift index 9a28885..d6026ae 100644 --- a/Sources/MPCCompat/CompatCore.swift +++ b/Sources/MPCCompat/CompatCore.swift @@ -1,12 +1,12 @@ import Foundation -import PeerMesh +import Stromo /// The shared runtime backing an `MPCCompat` device. /// /// MultipeerConnectivity splits one logical peer across three objects — an /// `MCSession`, an `MCNearbyServiceAdvertiser`, and an `MCNearbyServiceBrowser` /// — that an app constructs with the *same* `MCPeerID` and expects to cooperate. -/// PeerMesh unifies discovery, advertising, invitation, membership, and +/// Stromo unifies discovery, advertising, invitation, membership, and /// messaging into a single ``PeerSession`` actor. `CompatCore` is the bridge: /// exactly one instance exists per `(PeerID, serviceType)` pair (see /// ``CompatRegistry``), owns the underlying `PeerSession`, runs long-lived pump @@ -57,7 +57,7 @@ final class CompatCore: @unchecked Sendable { self.serviceType = serviceType self.transport = transport // We only hold the app's PeerID (public-key hash + display name), never - // its private key, so the underlying session runs a PeerMesh identity + // its private key, so the underlying session runs a Stromo identity // keyed on the display name — persisted (FR-20), so every screen visit // and relaunch presents the SAME peer. Without persistence each core // minted a fresh key, and browsers piled up ghost entries of one @@ -72,7 +72,7 @@ final class CompatCore: @unchecked Sendable { /// MPC-style service types are bare identifiers (`"remotecam"`); /// `MCNearbyServiceAdvertiser` translated them to Bonjour registration - /// types internally. PeerMesh's Bonjour discovery needs the full form, so + /// types internally. Stromo's Bonjour discovery needs the full form, so /// the bridge performs the same translation: `"remotecam"` → /// `"_remotecam._udp"` (UDP — the QUIC transport). Apps must declare that /// type under `NSBonjourServices` (MPC apps already declare both `._tcp` diff --git a/Sources/MPCCompat/MultipeerSession.swift b/Sources/MPCCompat/MultipeerSession.swift index aee7147..7db56b9 100644 --- a/Sources/MPCCompat/MultipeerSession.swift +++ b/Sources/MPCCompat/MultipeerSession.swift @@ -1,5 +1,5 @@ import Foundation -import PeerMesh +import Stromo /// Near-drop-in replacement for `MCSession` (FR-24). /// @@ -8,7 +8,7 @@ import PeerMesh /// | MultipeerConnectivity | MPCCompat | /// |------------------------------|-------------------------------| /// | `MCSession` | `MultipeerSession` | -/// | `MCPeerID` | `PeerMesh.PeerID` | +/// | `MCPeerID` | `Stromo.PeerID` | /// | `MCSessionDelegate` | `MultipeerSessionDelegate` | /// | `MCSessionSendDataMode` | `MultipeerSession.SendDataMode` | /// | `MCSessionState` | `MultipeerSession.PeerState` | @@ -51,7 +51,7 @@ public final class MultipeerSession: @unchecked Sendable { case connected = 2 } - /// Source-compatibility analog of `MCEncryptionPreference`. PeerMesh is + /// Source-compatibility analog of `MCEncryptionPreference`. Stromo is /// ALWAYS encrypted (FR-19): `.optional` and `.none` are accepted for /// mechanical migration but behave as `.required`. public enum EncryptionPreference: Sendable { @@ -96,7 +96,7 @@ public final class MultipeerSession: @unchecked Sendable { /// `MCSession(peer:securityIdentity:encryptionPreference:)` analog. /// /// - `securityIdentity` is accepted for source compatibility and ignored: - /// PeerMesh identities are key-derived (FR-20) and managed automatically. + /// Stromo identities are key-derived (FR-20) and managed automatically. /// - `encryptionPreference` is accepted for source compatibility; traffic /// is always encrypted regardless (FR-19). /// - MCSession carries no service type (its advertiser/browser do); the @@ -105,7 +105,7 @@ public final class MultipeerSession: @unchecked Sendable { peer peerID: PeerID, securityIdentity: [Any]? = nil, encryptionPreference: EncryptionPreference = .required, - service: String = "_peermesh._udp" + service: String = "_Stromo._udp" ) { self.init(myPeerID: peerID, service: service, transport: nil) } @@ -114,7 +114,7 @@ public final class MultipeerSession: @unchecked Sendable { /// through the shared ``CompatCore`` to the underlying `PeerSession`. convenience init( peer peerID: PeerID, - service: String = "_peermesh._udp", + service: String = "_Stromo._udp", transport: any PeerTransport ) { self.init(myPeerID: peerID, service: service, transport: transport) @@ -166,9 +166,9 @@ public final class MultipeerSession: @unchecked Sendable { /// (no route exists); transient delivery failures surface on the delegate /// path, as they do in MCSession. public func send(_ data: Data, toPeers peerIDs: [PeerID], with mode: SendDataMode) throws { - guard let core else { throw PeerMeshError.peerUnreachable(myPeerID) } + guard let core else { throw StromoError.peerUnreachable(myPeerID) } // MPC's .unreliable allowed large payloads (raw UDP + IP - // fragmentation); PeerMesh datagrams are honest about the MTU and + // fragmentation); Stromo datagrams are honest about the MTU and // refuse them. Oversized unreliable sends degrade to .reliable — // unordered, guaranteed: a superset of MPC's "may be dropped" // promise, and the closest semantics that still deliver. @@ -195,7 +195,7 @@ public final class MultipeerSession: @unchecked Sendable { withCompletionHandler completionHandler: ((Error?) -> Void)? = nil ) -> Progress? { guard let core else { - completionHandler?(PeerMeshError.peerUnreachable(peerID)) + completionHandler?(StromoError.peerUnreachable(peerID)) return nil } return core.sendResource( @@ -208,7 +208,7 @@ public final class MultipeerSession: @unchecked Sendable { /// Modern callers should use `PeerSession.openStream` (byte streams /// themselves are implemented; only the `NSStream` shim is not). public func startStream(withName streamName: String, toPeer peerID: PeerID) throws -> OutputStream { - throw PeerMeshError.unimplemented("MultipeerSession.startStream") + throw StromoError.unimplemented("MultipeerSession.startStream") } public func disconnect() { diff --git a/Sources/MPCCompat/NearbyService.swift b/Sources/MPCCompat/NearbyService.swift index 92e7bb1..08fdadd 100644 --- a/Sources/MPCCompat/NearbyService.swift +++ b/Sources/MPCCompat/NearbyService.swift @@ -1,5 +1,5 @@ import Foundation -import PeerMesh +import Stromo /// Near-drop-in replacement for `MCNearbyServiceAdvertiser` (FR-24). /// diff --git a/Sources/PeerMesh/Configuration.swift b/Sources/PeerMesh/Configuration.swift index 8511d62..b207093 100644 --- a/Sources/PeerMesh/Configuration.swift +++ b/Sources/PeerMesh/Configuration.swift @@ -52,5 +52,5 @@ public enum SessionTopology: Sendable { public static let `default`: SessionTopology = .fullMesh(maxPeers: 32) } -// Delivery, Recipients, PeerMeshError, Signal, and ProtocolEngine live in the -// sans-I/O PeerMeshProtocol target (DD-6) and are re-exported by this module. +// Delivery, Recipients, StromoError, Signal, and ProtocolEngine live in the +// sans-I/O StromoProtocol target (DD-6) and are re-exported by this module. diff --git a/Sources/PeerMesh/Events.swift b/Sources/PeerMesh/Events.swift index 6bdf7dd..9c6599d 100644 --- a/Sources/PeerMesh/Events.swift +++ b/Sources/PeerMesh/Events.swift @@ -82,7 +82,7 @@ public struct Invitation: Sendable { /// `.automatic` trust; it is a distinct field because an app running /// `.pinned`/`.pairingCode` verifies against this authenticated value rather /// than the self-reported identity. Part of the public FR-22 surface even - /// though PeerMesh's own consumers (MPCCompat/UI) do not yet read it. + /// though Stromo's own consumers (MPCCompat/UI) do not yet read it. public let inviterKeyHash: Data public let accept: @Sendable () async -> Void diff --git a/Sources/PeerMesh/PeerSession.swift b/Sources/PeerMesh/PeerSession.swift index e545259..4875a89 100644 --- a/Sources/PeerMesh/PeerSession.swift +++ b/Sources/PeerMesh/PeerSession.swift @@ -1,6 +1,6 @@ import Foundation -/// The primary PeerMesh API: discovery, invitation, membership, and data +/// The primary Stromo API: discovery, invitation, membership, and data /// exchange with nearby peers (design document §7). /// /// MPC-simple by default: one line to construct, encryption always on, @@ -96,7 +96,7 @@ public actor PeerSession { } /// Full-control entry point (custom identity or transport injection; the - /// latter is how PeerMeshTestKit runs sessions without radios, QA-8). + /// latter is how StromoTestKit runs sessions without radios, QA-8). public init( identity: PeerIdentity, service: ServiceDescriptor, @@ -166,7 +166,7 @@ public actor PeerSession { knownEndpoints[peer.id] = peer return try await withCheckedThrowingContinuation { continuation in guard inviteWaiters[peer.id] == nil else { - continuation.resume(throwing: PeerMeshError.invitationAlreadyPending(peer.id)) + continuation.resume(throwing: StromoError.invitationAlreadyPending(peer.id)) return } inviteWaiters[peer.id] = continuation @@ -183,10 +183,10 @@ public actor PeerSession { ) async throws { // FR-16: datagrams never fragment, so the cap is part of the contract. if delivery == .datagram, payload.count > Delivery.maxDatagramPayload { - throw PeerMeshError.datagramTooLarge( + throw StromoError.datagramTooLarge( bytes: payload.count, limit: Delivery.maxDatagramPayload) } - guard !engine.members.isEmpty else { throw PeerMeshError.peerUnreachable(identity.id) } + guard !engine.members.isEmpty else { throw StromoError.peerUnreachable(identity.id) } run(.command(.send(payload, to: recipients, delivery: delivery))) } @@ -196,7 +196,7 @@ public actor PeerSession { /// dedicated `transferChunk` stream. Cancel via ``ResourceTransfer/progress``. public func sendResource(at url: URL, to peer: PeerID, name resourceName: String? = nil) async throws -> ResourceTransfer { guard engine.members.contains(peer), let connection = connections[peer] else { - throw PeerMeshError.peerUnreachable(peer) + throw StromoError.peerUnreachable(peer) } let attributes = try FileManager.default.attributesOfItem(atPath: url.path) let totalBytes = (attributes[.size] as? NSNumber)?.uint64Value ?? 0 @@ -225,7 +225,7 @@ public actor PeerSession { /// on the control stream (`StreamOpen`), then opens the dedicated `appStream`. public func openStream(_ label: String, with peer: PeerID) async throws -> any PeerByteStream { guard engine.members.contains(peer), let connection = connections[peer] else { - throw PeerMeshError.peerUnreachable(peer) + throw StromoError.peerUnreachable(peer) } try await connection.sendSignal(SignalCodec.encode(.streamOpen(label: label))) return try await connection.openOutgoingStream( @@ -246,7 +246,7 @@ public actor PeerSession { run(.command(.leave)) for timer in timers.values { timer.cancel() } timers.removeAll() - inviteWaiters.forEach { $0.value.resume(throwing: PeerMeshError.peerUnreachable($0.key)) } + inviteWaiters.forEach { $0.value.resume(throwing: StromoError.peerUnreachable($0.key)) } inviteWaiters.removeAll() } @@ -366,11 +366,11 @@ public actor PeerSession { membershipContinuation.yield(.left(peer)) case .invitationFailed(let peer, let reason): - let error: PeerMeshError + let error: StromoError switch reason { - case .declined: error = PeerMeshError.invitationDeclined - case .timedOut: error = PeerMeshError.invitationTimedOut - case .connectionLost: error = PeerMeshError.peerUnreachable(peer) + case .declined: error = StromoError.invitationDeclined + case .timedOut: error = StromoError.invitationTimedOut + case .connectionLost: error = StromoError.peerUnreachable(peer) } inviteWaiters.removeValue(forKey: peer)?.resume(throwing: error) @@ -470,7 +470,7 @@ public actor PeerSession { .appendingPathComponent("\(UUID().uuidString)-\(name)") FileManager.default.createFile(atPath: tempURL.path, contents: nil) guard let handle = try? FileHandle(forWritingTo: tempURL) else { - continuation.yield(.failed(name: name, from: from, error: PeerMeshError.resourceTransferIncomplete)) + continuation.yield(.failed(name: name, from: from, error: StromoError.resourceTransferIncomplete)) return } @@ -494,7 +494,7 @@ public actor PeerSession { try? FileManager.default.removeItem(at: tempURL) continuation.yield(.failed( name: name, from: from, - error: failure ?? PeerMeshError.resourceTransferIncomplete)) + error: failure ?? StromoError.resourceTransferIncomplete)) } } diff --git a/Sources/PeerMesh/PeerTransport.swift b/Sources/PeerMesh/PeerTransport.swift index d88cb0d..08a9b2a 100644 --- a/Sources/PeerMesh/PeerTransport.swift +++ b/Sources/PeerMesh/PeerTransport.swift @@ -6,7 +6,7 @@ import Foundation /// connections, and report lifecycle. Implementations: `QUICTransport` /// (primary, DD-1), `TCPTLSTransport` (contingency, pending Spike S-1), /// `WiFiAwareTransport` (post-1.0), and `InMemoryTransport` in -/// PeerMeshTestKit (QA-8). +/// StromoTestKit (QA-8). public protocol PeerTransport: Sendable { /// Advertise the service with metadata (Bonjour TXT record) on all /// eligible paths, including peer-to-peer Wi-Fi (FR-1, FR-3). diff --git a/Sources/PeerMesh/Reexport.swift b/Sources/PeerMesh/Reexport.swift index 16da0b6..1e018ac 100644 --- a/Sources/PeerMesh/Reexport.swift +++ b/Sources/PeerMesh/Reexport.swift @@ -1,5 +1,5 @@ -/// PeerMesh presents one unified namespace: the sans-I/O protocol layer +/// Stromo presents one unified namespace: the sans-I/O protocol layer /// (DD-6: `ProtocolEngine`, `Signal`, `PeerID`, `Delivery`, `Recipients`, -/// `PeerMeshError`, `SignalCodec`) is re-exported so app code only ever -/// writes `import PeerMesh`. -@_exported import PeerMeshProtocol +/// `StromoError`, `SignalCodec`) is re-exported so app code only ever +/// writes `import Stromo`. +@_exported import StromoProtocol diff --git a/Sources/PeerMesh/Security/IdentityCertificate.swift b/Sources/PeerMesh/Security/IdentityCertificate.swift index 0d56cf8..ebdec77 100644 --- a/Sources/PeerMesh/Security/IdentityCertificate.swift +++ b/Sources/PeerMesh/Security/IdentityCertificate.swift @@ -129,7 +129,7 @@ extension IdentityCertificate { let addKey: [String: Any] = [ kSecClass as String: kSecClassKey, kSecValueRef as String: secKey, - kSecAttrApplicationTag as String: Data("dev.securityunion.peermesh.tls".utf8), + kSecAttrApplicationTag as String: Data("dev.securityunion.Stromo.tls".utf8), kSecUseDataProtectionKeychain as String: true, ] let keyStatus = SecItemAdd(addKey as CFDictionary, nil) diff --git a/Sources/PeerMesh/Security/IdentityStore.swift b/Sources/PeerMesh/Security/IdentityStore.swift index 0e2fc24..aac20cc 100644 --- a/Sources/PeerMesh/Security/IdentityStore.swift +++ b/Sources/PeerMesh/Security/IdentityStore.swift @@ -100,14 +100,14 @@ public struct FileIdentityStore: IdentityStore { /// `SecureEnclave.isAvailable`; otherwise a software P-256 key is used /// (simulator/CI/Intel Macs without an enclave). Either way the serialized key /// material lives as a `kSecClassKey` item tagged -/// `dev.securityunion.peermesh.identity`, in the data-protection keychain. +/// `dev.securityunion.Stromo.identity`, in the data-protection keychain. /// /// Note: Secure Enclave keys can never export a private-key `derRepresentation` /// (only the public key can) — that is fine; we persist the enclave key's opaque /// `dataRepresentation` reference blob and the key signs in place. public struct KeychainIdentityStore: IdentityStore { /// Application-tag prefix for identity items (per-name suffix appended). - public static let applicationTag = "dev.securityunion.peermesh.identity" + public static let applicationTag = "dev.securityunion.Stromo.identity" public init() {} diff --git a/Sources/PeerMesh/Transports/QUICConnection.swift b/Sources/PeerMesh/Transports/QUICConnection.swift index d055c23..eda2207 100644 --- a/Sources/PeerMesh/Transports/QUICConnection.swift +++ b/Sources/PeerMesh/Transports/QUICConnection.swift @@ -1,5 +1,5 @@ import Foundation -import PeerMeshProtocol +import StromoProtocol #if canImport(Network) import Network diff --git a/Sources/PeerMesh/Transports/QUICSupport.swift b/Sources/PeerMesh/Transports/QUICSupport.swift index f696b4a..469190b 100644 --- a/Sources/PeerMesh/Transports/QUICSupport.swift +++ b/Sources/PeerMesh/Transports/QUICSupport.swift @@ -1,6 +1,6 @@ import Foundation import FlatBuffers -import PeerMeshProtocol +import StromoProtocol #if canImport(Network) import Network @@ -39,20 +39,20 @@ enum QUICError: Error, Sendable, LocalizedError { // LocalizedError puts the actual reason in localizedDescription. var errorDescription: String? { switch self { - case .connectionClosed: return "PeerMesh QUIC: connection closed" - case .shortRead: return "PeerMesh QUIC: stream ended mid-frame" - case .identityMismatch: return "PeerMesh QUIC: peer key hash does not match its certificate" + case .connectionClosed: return "Stromo QUIC: connection closed" + case .shortRead: return "Stromo QUIC: stream ended mid-frame" + case .identityMismatch: return "Stromo QUIC: peer key hash does not match its certificate" case .tlsIdentityUnavailable(let reason): - return "PeerMesh QUIC: no local TLS identity — \(reason)" - case .malformedStreamHeader: return "PeerMesh QUIC: malformed stream header" - case .listenerFailed(let reason): return "PeerMesh QUIC: listener failed — \(reason)" + return "Stromo QUIC: no local TLS identity — \(reason)" + case .malformedStreamHeader: return "Stromo QUIC: malformed stream header" + case .listenerFailed(let reason): return "Stromo QUIC: listener failed — \(reason)" case .serviceResolutionFailed: - return "PeerMesh QUIC: Bonjour service endpoint did not resolve" + return "Stromo QUIC: Bonjour service endpoint did not resolve" case .protocolVersionMismatch(let local, let remote): let hint = remote.major > local.major ? "this device needs an app upgrade" : "the peer needs an app upgrade" - return "PeerMesh QUIC: protocol version mismatch — local \(local), peer \(remote); \(hint)" + return "Stromo QUIC: protocol version mismatch — local \(local), peer \(remote); \(hint)" } } } @@ -366,7 +366,7 @@ enum PeerHello { let name = fbb.create(string: peer.displayName) let info = WirePeerInfo.createPeerInfo( &fbb, keyHashVectorOffset: keyHash, displayNameOffset: name) - let root = PeerMesh_Wire_PeerHello.createPeerHello( + let root = Stromo_Wire_PeerHello.createPeerHello( &fbb, peerOffset: info, protocolMajor: version.major, @@ -379,7 +379,7 @@ enum PeerHello { static func decode(_ data: Data) -> Decoded? { var buffer = ByteBuffer(data: data) guard - let root: PeerMesh_Wire_PeerHello = try? getCheckedRoot( + let root: Stromo_Wire_PeerHello = try? getCheckedRoot( byteBuffer: &buffer, options: VerifierOptions(maxDepth: 16, maxTableCount: 64, maxApparentSize: 1 << 16)) else { return nil } @@ -415,7 +415,7 @@ enum QUICStreamHeaderCodec { static func encode(_ info: StreamHeaderInfo) -> Data { var fbb = FlatBufferBuilder(initialSize: 128) let labelOffset = info.label.map { fbb.create(string: $0) } ?? Offset() - let root = PeerMesh_Wire_StreamHeader.createStreamHeader( + let root = Stromo_Wire_StreamHeader.createStreamHeader( &fbb, kind: wireKind(info.kind), sequence: info.sequence ?? 0, @@ -427,7 +427,7 @@ enum QUICStreamHeaderCodec { static func decode(_ data: Data) throws -> StreamHeaderInfo { var buffer = ByteBuffer(data: data) - let root: PeerMesh_Wire_StreamHeader + let root: Stromo_Wire_StreamHeader do { root = try getCheckedRoot( byteBuffer: &buffer, @@ -442,7 +442,7 @@ enum QUICStreamHeaderCodec { transferID: root.transferId?.uuidValue, label: root.label) } - private static func wireKind(_ kind: StreamHeaderInfo.Kind) -> PeerMesh_Wire_StreamKind { + private static func wireKind(_ kind: StreamHeaderInfo.Kind) -> Stromo_Wire_StreamKind { switch kind { case .message: return .message case .orderedMessage: return .orderedmessage @@ -452,7 +452,7 @@ enum QUICStreamHeaderCodec { } } - private static func infoKind(_ kind: PeerMesh_Wire_StreamKind) -> StreamHeaderInfo.Kind { + private static func infoKind(_ kind: Stromo_Wire_StreamKind) -> StreamHeaderInfo.Kind { switch kind { case .message, .unknown: return .message case .orderedmessage: return .orderedMessage diff --git a/Sources/PeerMesh/Transports/QUICTLS.swift b/Sources/PeerMesh/Transports/QUICTLS.swift index f32f6e9..d389ce6 100644 --- a/Sources/PeerMesh/Transports/QUICTLS.swift +++ b/Sources/PeerMesh/Transports/QUICTLS.swift @@ -1,6 +1,6 @@ import CryptoKit import Foundation -import PeerMeshProtocol +import StromoProtocol #if canImport(Network) import Network @@ -60,7 +60,7 @@ enum QUICTLS { /// FROZEN. Version gating is the PeerHello semver (same-major interop) — /// bumping the ALPN would fail the handshake before hello, turning a /// diagnosable "upgrade required" into an opaque connect failure. - static let alpn = "peermesh/1" + static let alpn = "Stromo/1" /// Forms the local `sec_identity_t`. /// @@ -106,7 +106,7 @@ enum QUICTLS { #if os(macOS) private static func createFileKeychain() throws -> (SecKeychain, URL) { let dir = FileManager.default.temporaryDirectory - .appendingPathComponent("peermesh-kc-\(UUID().uuidString)", isDirectory: true) + .appendingPathComponent("Stromo-kc-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) let url = dir.appendingPathComponent("id.keychain") let pass = UUID().uuidString @@ -188,7 +188,7 @@ enum QUICTLS { } #endif - /// Builds `NWParameters` for a QUIC endpoint: ALPN `peermesh/1`, local + /// Builds `NWParameters` for a QUIC endpoint: ALPN `Stromo/1`, local /// identity, mutual authentication (so both ends recover the peer's /// authenticated key hash — FR-22), a ``TrustPolicy`` verify block, and /// generous stream/flow limits for the stream-per-message data plane (DD-7, @@ -235,7 +235,7 @@ enum QUICTLS { /// apply the policy. `displayName`/key-hash bookkeeping happens later from the /// connection's authenticated metadata. private static func installVerifyBlock(_ sec: sec_protocol_options_t, trust: TrustPolicy) { - let queue = DispatchQueue(label: "dev.securityunion.peermesh.quic.verify") + let queue = DispatchQueue(label: "dev.securityunion.Stromo.quic.verify") sec_protocol_options_set_verify_block(sec, { _, secTrust, complete in quicDebug("verify block called") let trustRef = sec_trust_copy_ref(secTrust).takeRetainedValue() diff --git a/Sources/PeerMesh/Transports/QUICTransport.swift b/Sources/PeerMesh/Transports/QUICTransport.swift index acca7fb..12b2b80 100644 --- a/Sources/PeerMesh/Transports/QUICTransport.swift +++ b/Sources/PeerMesh/Transports/QUICTransport.swift @@ -1,5 +1,5 @@ import Foundation -import PeerMeshProtocol +import StromoProtocol #if canImport(Network) import Network @@ -72,7 +72,7 @@ public final class QUICTransport: PeerTransport, @unchecked Sendable { /// every stream's callbacks — runs here, never on main. `.userInitiated`: /// the frame path is interactive; default QoS gets deprioritized under /// system load. - private let queue = DispatchQueue(label: "dev.securityunion.peermesh.quic", qos: .userInitiated) + private let queue = DispatchQueue(label: "dev.securityunion.Stromo.quic", qos: .userInitiated) // Advertiser state. private let listenerBox = Locked(nil) @@ -98,13 +98,13 @@ public final class QUICTransport: PeerTransport, @unchecked Sendable { /// so this is `true`. On platforms with no entitlement-free route it is /// `false` — QUIC tests should skip cleanly and hosts should inject a /// `SecIdentity` via ``Configuration/tlsProvider``. - /// Peer-to-peer Wi-Fi opt-out (`PEERMESH_NO_P2P=1`): same-machine + /// Peer-to-peer Wi-Fi opt-out (`Stromo_NO_P2P=1`): same-machine /// self-dials fail with `includePeerToPeer` enabled /// (docs/spike-results.md), so same-machine test runs disable it. /// `getenv`, not `ProcessInfo`: the latter caches the environment at /// first access, which silently ignores a test's later `setenv`. static var peerToPeerEnabled: Bool { - getenv("PEERMESH_NO_P2P") == nil + getenv("Stromo_NO_P2P") == nil } /// Bonjour registration types are `_name._tcp|_udp` with a 1–15 char name. @@ -363,7 +363,7 @@ public final class QUICTransport: PeerTransport, @unchecked Sendable { case .bonjour: endpoint = endpoints.value[peer.id] } - guard let endpoint else { throw PeerMeshError.peerUnreachable(peer.id) } + guard let endpoint else { throw StromoError.peerUnreachable(peer.id) } let localIdentity = try makeLocalIdentity(for: identity) let isBonjour: Bool @@ -511,15 +511,15 @@ public struct QUICTransport: PeerTransport { public let inboundConnections: AsyncStream = AsyncStream { _ in } public init() {} public func startAdvertising(service: ServiceDescriptor, metadata: [String: String], identity: PeerIdentity) async throws { - throw PeerMeshError.unimplemented("QUICTransport requires Network.framework") + throw StromoError.unimplemented("QUICTransport requires Network.framework") } public func stopAdvertising() async {} public func discoveries(service: ServiceDescriptor) async throws -> AsyncStream { - throw PeerMeshError.unimplemented("QUICTransport requires Network.framework") + throw StromoError.unimplemented("QUICTransport requires Network.framework") } public func stopBrowsing() async {} public func connect(to peer: DiscoveredPeer, identity: PeerIdentity, trust: TrustPolicy) async throws -> any PeerConnection { - throw PeerMeshError.unimplemented("QUICTransport requires Network.framework") + throw StromoError.unimplemented("QUICTransport requires Network.framework") } } diff --git a/Sources/PeerMeshCLI/PeerMeshCLI.swift b/Sources/PeerMeshCLI/PeerMeshCLI.swift index d5f4595..438dab5 100644 --- a/Sources/PeerMeshCLI/PeerMeshCLI.swift +++ b/Sources/PeerMeshCLI/PeerMeshCLI.swift @@ -1,27 +1,27 @@ import Foundation -import PeerMesh +import Stromo /// Diagnostic CLI: exercises the production discovery + transport path /// (Bonjour + QUIC) between real processes. The debugging ladder for /// "devices can't see each other": two processes on one Mac → two Macs on a /// LAN → two iPhones (implementation plan Step 5). /// -/// peermesh advertise [--service _pmdemo._udp] [--name A] [--meta k=v]... -/// peermesh browse [--service _pmdemo._udp] [--timeout 30] -/// peermesh host [--service ...] [--name A] [--once] -/// peermesh join [--service ...] [--name B] [--peer A] [--send ping] [--linger] +/// Stromo advertise [--service _pmdemo._udp] [--name A] [--meta k=v]... +/// Stromo browse [--service _pmdemo._udp] [--timeout 30] +/// Stromo host [--service ...] [--name A] [--once] +/// Stromo join [--service ...] [--name B] [--peer A] [--send ping] [--linger] /// /// `host` advertises, auto-accepts invitations, echoes every message back /// ("pong: "). `join` browses, invites the first (or --peer named) peer, /// sends --send, waits for the echo, prints SUCCESS, exits 0. Both exit 2 on /// --timeout (default 30 s). Set QUIC_DEBUG=1 for driver logs. @main -struct PeerMeshCLI { +struct StromoCLI { static func main() async { var arguments = Array(CommandLine.arguments.dropFirst()) guard let command = arguments.first, ["advertise", "browse", "host", "join"].contains(command) else { - print("usage: peermesh advertise|browse|host|join [--service TYPE] [--name NAME] [--peer NAME] [--send TEXT] [--meta k=v] [--timeout SECS] [--once]") + print("usage: Stromo advertise|browse|host|join [--service TYPE] [--name NAME] [--peer NAME] [--send TEXT] [--meta k=v] [--timeout SECS] [--once]") exit(64) } arguments.removeFirst() @@ -51,7 +51,7 @@ struct PeerMeshCLI { let name = options["name"] ?? "\(ProcessInfo.processInfo.hostName)-\(getpid())" let timeout = TimeInterval(options["timeout"] ?? "30") ?? 30 - log("peermesh \(command) — name=\(name) service=\(service) pid=\(getpid())") + log("Stromo \(command) — name=\(name) service=\(service) pid=\(getpid())") // Global watchdog: a diagnostic tool must never hang silently. Task { @@ -146,7 +146,7 @@ struct PeerMeshCLI { log("inviting \(target.id.displayName)…") let member = try await session.invite( - target, context: Data("peermesh-cli".utf8), timeout: min(timeout, 15)) + target, context: Data("Stromo-cli".utf8), timeout: min(timeout, 15)) log("JOINED session with \(member.id.displayName)") let text = options["send"] ?? "ping" diff --git a/Sources/PeerMeshProtocol/Generated/common_generated.swift b/Sources/PeerMeshProtocol/Generated/common_generated.swift index 244f067..3e77df0 100644 --- a/Sources/PeerMeshProtocol/Generated/common_generated.swift +++ b/Sources/PeerMeshProtocol/Generated/common_generated.swift @@ -7,7 +7,7 @@ import FlatBuffers /// A 128-bit transfer identifier: the two 8-byte halves of a `uuid_t`, in /// memory order. A fixed-size struct — no vector length word, and the /// 16-byte contract is enforced by the schema instead of a runtime check. -public struct PeerMesh_Wire_TransferId: NativeStruct, Verifiable, FlatbuffersInitializable { +public struct Stromo_Wire_TransferId: NativeStruct, Verifiable, FlatbuffersInitializable { static func validateVersion() { FlatBuffersVersion_25_2_10() } @@ -34,14 +34,14 @@ public struct PeerMesh_Wire_TransferId: NativeStruct, Verifiable, FlatbuffersIni public var lo: UInt64 { _lo } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { - try verifier.inBuffer(position: position, of: PeerMesh_Wire_TransferId.self) + try verifier.inBuffer(position: position, of: Stromo_Wire_TransferId.self) } } /// A 128-bit transfer identifier: the two 8-byte halves of a `uuid_t`, in /// memory order. A fixed-size struct — no vector length word, and the /// 16-byte contract is enforced by the schema instead of a runtime check. -public struct PeerMesh_Wire_TransferId_Mutable: FlatBufferObject { +public struct Stromo_Wire_TransferId_Mutable: FlatBufferObject { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } diff --git a/Sources/PeerMeshProtocol/Generated/peer_hello_generated.swift b/Sources/PeerMeshProtocol/Generated/peer_hello_generated.swift index 7886551..176ef61 100644 --- a/Sources/PeerMeshProtocol/Generated/peer_hello_generated.swift +++ b/Sources/PeerMeshProtocol/Generated/peer_hello_generated.swift @@ -4,7 +4,7 @@ import FlatBuffers -public struct PeerMesh_Wire_PeerHello: FlatBufferObject, Verifiable { +public struct Stromo_Wire_PeerHello: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -24,7 +24,7 @@ public struct PeerMesh_Wire_PeerHello: FlatBufferObject, Verifiable { /// The sender's identity. key_hash must match the TLS certificate's key /// hash — divergence is identityMismatch and kills the connection. - public var peer: PeerMesh_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.peer.v); return o == 0 ? nil : PeerMesh_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } + public var peer: Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.peer.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } /// Protocol semver: peers interoperate iff `major` matches. `minor` is /// additive (append-only schema evolution; the newer side adapts to the /// older); `patch` never changes the wire — diagnostics only. @@ -44,17 +44,17 @@ public struct PeerMesh_Wire_PeerHello: FlatBufferObject, Verifiable { protocolMinor: UInt16 = 0, protocolPatch: UInt16 = 0 ) -> Offset { - let __start = PeerMesh_Wire_PeerHello.startPeerHello(&fbb) - PeerMesh_Wire_PeerHello.add(peer: peer, &fbb) - PeerMesh_Wire_PeerHello.add(protocolMajor: protocolMajor, &fbb) - PeerMesh_Wire_PeerHello.add(protocolMinor: protocolMinor, &fbb) - PeerMesh_Wire_PeerHello.add(protocolPatch: protocolPatch, &fbb) - return PeerMesh_Wire_PeerHello.endPeerHello(&fbb, start: __start) + let __start = Stromo_Wire_PeerHello.startPeerHello(&fbb) + Stromo_Wire_PeerHello.add(peer: peer, &fbb) + Stromo_Wire_PeerHello.add(protocolMajor: protocolMajor, &fbb) + Stromo_Wire_PeerHello.add(protocolMinor: protocolMinor, &fbb) + Stromo_Wire_PeerHello.add(protocolPatch: protocolPatch, &fbb) + return Stromo_Wire_PeerHello.endPeerHello(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.peer.p, fieldName: "peer", required: false, type: ForwardOffset.self) + try _v.visit(field: VTOFFSET.peer.p, fieldName: "peer", required: false, type: ForwardOffset.self) try _v.visit(field: VTOFFSET.protocolMajor.p, fieldName: "protocolMajor", required: false, type: UInt16.self) try _v.visit(field: VTOFFSET.protocolMinor.p, fieldName: "protocolMinor", required: false, type: UInt16.self) try _v.visit(field: VTOFFSET.protocolPatch.p, fieldName: "protocolPatch", required: false, type: UInt16.self) diff --git a/Sources/PeerMeshProtocol/Generated/signal_generated.swift b/Sources/PeerMeshProtocol/Generated/signal_generated.swift index f14e9a0..5f90d09 100644 --- a/Sources/PeerMeshProtocol/Generated/signal_generated.swift +++ b/Sources/PeerMeshProtocol/Generated/signal_generated.swift @@ -4,7 +4,7 @@ import FlatBuffers -public enum PeerMesh_Wire_SignalBody: UInt8, UnionEnum { +public enum Stromo_Wire_SignalBody: UInt8, UnionEnum { public typealias T = UInt8 public init?(value: T) { @@ -21,13 +21,13 @@ public enum PeerMesh_Wire_SignalBody: UInt8, UnionEnum { case transferoffer = 5 case streamopen = 6 - public static var max: PeerMesh_Wire_SignalBody { return .streamopen } - public static var min: PeerMesh_Wire_SignalBody { return .none_ } + public static var max: Stromo_Wire_SignalBody { return .streamopen } + public static var min: Stromo_Wire_SignalBody { return .none_ } } /// A session member, as carried in rosters (FR-13). -public struct PeerMesh_Wire_PeerInfo: FlatBufferObject, Verifiable { +public struct Stromo_Wire_PeerInfo: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -59,10 +59,10 @@ public struct PeerMesh_Wire_PeerInfo: FlatBufferObject, Verifiable { keyHashVectorOffset keyHash: Offset = Offset(), displayNameOffset displayName: Offset = Offset() ) -> Offset { - let __start = PeerMesh_Wire_PeerInfo.startPeerInfo(&fbb) - PeerMesh_Wire_PeerInfo.addVectorOf(keyHash: keyHash, &fbb) - PeerMesh_Wire_PeerInfo.add(displayName: displayName, &fbb) - return PeerMesh_Wire_PeerInfo.endPeerInfo(&fbb, start: __start) + let __start = Stromo_Wire_PeerInfo.startPeerInfo(&fbb) + Stromo_Wire_PeerInfo.addVectorOf(keyHash: keyHash, &fbb) + Stromo_Wire_PeerInfo.add(displayName: displayName, &fbb) + return Stromo_Wire_PeerInfo.endPeerInfo(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { @@ -75,7 +75,7 @@ public struct PeerMesh_Wire_PeerInfo: FlatBufferObject, Verifiable { /// Invitation to join a session (FR-6). First message on the control stream /// after the QUIC handshake (encrypted, not yet authorized). -public struct PeerMesh_Wire_Invite: FlatBufferObject, Verifiable { +public struct Stromo_Wire_Invite: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -91,7 +91,7 @@ public struct PeerMesh_Wire_Invite: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var inviter: PeerMesh_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.inviter.v); return o == 0 ? nil : PeerMesh_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } + public var inviter: Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.inviter.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } /// Opaque application context (FR-6). Capped by SignalCodec's control-message /// limit; larger contexts are chunked (DD-5 rule 3). public var hasContext: Bool { let o = _accessor.offset(VTOFFSET.context.v); return o == 0 ? false : true } @@ -107,22 +107,22 @@ public struct PeerMesh_Wire_Invite: FlatBufferObject, Verifiable { inviterOffset inviter: Offset = Offset(), contextVectorOffset context: Offset = Offset() ) -> Offset { - let __start = PeerMesh_Wire_Invite.startInvite(&fbb) - PeerMesh_Wire_Invite.add(inviter: inviter, &fbb) - PeerMesh_Wire_Invite.addVectorOf(context: context, &fbb) - return PeerMesh_Wire_Invite.endInvite(&fbb, start: __start) + let __start = Stromo_Wire_Invite.startInvite(&fbb) + Stromo_Wire_Invite.add(inviter: inviter, &fbb) + Stromo_Wire_Invite.addVectorOf(context: context, &fbb) + return Stromo_Wire_Invite.endInvite(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.inviter.p, fieldName: "inviter", required: false, type: ForwardOffset.self) + try _v.visit(field: VTOFFSET.inviter.p, fieldName: "inviter", required: false, type: ForwardOffset.self) try _v.visit(field: VTOFFSET.context.p, fieldName: "context", required: false, type: ForwardOffset>.self) _v.finish() } } /// Response to an Invite (FR-7). -public struct PeerMesh_Wire_InviteResponse: FlatBufferObject, Verifiable { +public struct Stromo_Wire_InviteResponse: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -143,7 +143,7 @@ public struct PeerMesh_Wire_InviteResponse: FlatBufferObject, Verifiable { /// one round-trip (FR-13). public var hasRoster: Bool { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? false : true } public var rosterCount: Int32 { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? 0 : _accessor.vector(count: o) } - public func roster(at index: Int32) -> PeerMesh_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? nil : PeerMesh_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } + public func roster(at index: Int32) -> Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } public static func startInviteResponse(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 2) } public static func add(accepted: Bool, _ fbb: inout FlatBufferBuilder) { fbb.add(element: accepted, def: false, at: VTOFFSET.accepted.p) } @@ -154,22 +154,22 @@ public struct PeerMesh_Wire_InviteResponse: FlatBufferObject, Verifiable { accepted: Bool = false, rosterVectorOffset roster: Offset = Offset() ) -> Offset { - let __start = PeerMesh_Wire_InviteResponse.startInviteResponse(&fbb) - PeerMesh_Wire_InviteResponse.add(accepted: accepted, &fbb) - PeerMesh_Wire_InviteResponse.addVectorOf(roster: roster, &fbb) - return PeerMesh_Wire_InviteResponse.endInviteResponse(&fbb, start: __start) + let __start = Stromo_Wire_InviteResponse.startInviteResponse(&fbb) + Stromo_Wire_InviteResponse.add(accepted: accepted, &fbb) + Stromo_Wire_InviteResponse.addVectorOf(roster: roster, &fbb) + return Stromo_Wire_InviteResponse.endInviteResponse(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) try _v.visit(field: VTOFFSET.accepted.p, fieldName: "accepted", required: false, type: Bool.self) - try _v.visit(field: VTOFFSET.roster.p, fieldName: "roster", required: false, type: ForwardOffset, PeerMesh_Wire_PeerInfo>>.self) + try _v.visit(field: VTOFFSET.roster.p, fieldName: "roster", required: false, type: ForwardOffset, Stromo_Wire_PeerInfo>>.self) _v.finish() } } /// Pairing-code transcript confirmation (DD-2, `.pairingCode` mode only). -public struct PeerMesh_Wire_CodeConfirm: FlatBufferObject, Verifiable { +public struct Stromo_Wire_CodeConfirm: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -196,9 +196,9 @@ public struct PeerMesh_Wire_CodeConfirm: FlatBufferObject, Verifiable { _ fbb: inout FlatBufferBuilder, transcriptMacVectorOffset transcriptMac: Offset = Offset() ) -> Offset { - let __start = PeerMesh_Wire_CodeConfirm.startCodeConfirm(&fbb) - PeerMesh_Wire_CodeConfirm.addVectorOf(transcriptMac: transcriptMac, &fbb) - return PeerMesh_Wire_CodeConfirm.endCodeConfirm(&fbb, start: __start) + let __start = Stromo_Wire_CodeConfirm.startCodeConfirm(&fbb) + Stromo_Wire_CodeConfirm.addVectorOf(transcriptMac: transcriptMac, &fbb) + return Stromo_Wire_CodeConfirm.endCodeConfirm(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { @@ -209,7 +209,7 @@ public struct PeerMesh_Wire_CodeConfirm: FlatBufferObject, Verifiable { } /// Membership gossip (FR-13, FR-14). -public struct PeerMesh_Wire_RosterUpdate: FlatBufferObject, Verifiable { +public struct Stromo_Wire_RosterUpdate: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -227,7 +227,7 @@ public struct PeerMesh_Wire_RosterUpdate: FlatBufferObject, Verifiable { public var hasMembers: Bool { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? false : true } public var membersCount: Int32 { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? 0 : _accessor.vector(count: o) } - public func members(at index: Int32) -> PeerMesh_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? nil : PeerMesh_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } + public func members(at index: Int32) -> Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } /// Monotonic epoch for last-writer-wins convergence. public var epoch: UInt64 { let o = _accessor.offset(VTOFFSET.epoch.v); return o == 0 ? 0 : _accessor.readBuffer(of: UInt64.self, at: o) } public static func startRosterUpdate(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 2) } @@ -239,22 +239,22 @@ public struct PeerMesh_Wire_RosterUpdate: FlatBufferObject, Verifiable { membersVectorOffset members: Offset = Offset(), epoch: UInt64 = 0 ) -> Offset { - let __start = PeerMesh_Wire_RosterUpdate.startRosterUpdate(&fbb) - PeerMesh_Wire_RosterUpdate.addVectorOf(members: members, &fbb) - PeerMesh_Wire_RosterUpdate.add(epoch: epoch, &fbb) - return PeerMesh_Wire_RosterUpdate.endRosterUpdate(&fbb, start: __start) + let __start = Stromo_Wire_RosterUpdate.startRosterUpdate(&fbb) + Stromo_Wire_RosterUpdate.addVectorOf(members: members, &fbb) + Stromo_Wire_RosterUpdate.add(epoch: epoch, &fbb) + return Stromo_Wire_RosterUpdate.endRosterUpdate(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.members.p, fieldName: "members", required: false, type: ForwardOffset, PeerMesh_Wire_PeerInfo>>.self) + try _v.visit(field: VTOFFSET.members.p, fieldName: "members", required: false, type: ForwardOffset, Stromo_Wire_PeerInfo>>.self) try _v.visit(field: VTOFFSET.epoch.p, fieldName: "epoch", required: false, type: UInt64.self) _v.finish() } } /// Announces an incoming resource transfer on a dedicated stream (FR-17). -public struct PeerMesh_Wire_TransferOffer: FlatBufferObject, Verifiable { +public struct Stromo_Wire_TransferOffer: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -271,32 +271,32 @@ public struct PeerMesh_Wire_TransferOffer: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var transferId: PeerMesh_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: PeerMesh_Wire_TransferId.self, at: o) } - public var mutableTransferId: PeerMesh_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : PeerMesh_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } + public var transferId: Stromo_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: Stromo_Wire_TransferId.self, at: o) } + public var mutableTransferId: Stromo_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : Stromo_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } public var name: String? { let o = _accessor.offset(VTOFFSET.name.v); return o == 0 ? nil : _accessor.string(at: o) } public var nameSegmentArray: [UInt8]? { return _accessor.getVector(at: VTOFFSET.name.v) } public var totalBytes: UInt64 { let o = _accessor.offset(VTOFFSET.totalBytes.v); return o == 0 ? 0 : _accessor.readBuffer(of: UInt64.self, at: o) } public static func startTransferOffer(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 3) } - public static func add(transferId: PeerMesh_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } + public static func add(transferId: Stromo_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } public static func add(name: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: name, at: VTOFFSET.name.p) } public static func add(totalBytes: UInt64, _ fbb: inout FlatBufferBuilder) { fbb.add(element: totalBytes, def: 0, at: VTOFFSET.totalBytes.p) } public static func endTransferOffer(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } public static func createTransferOffer( _ fbb: inout FlatBufferBuilder, - transferId: PeerMesh_Wire_TransferId? = nil, + transferId: Stromo_Wire_TransferId? = nil, nameOffset name: Offset = Offset(), totalBytes: UInt64 = 0 ) -> Offset { - let __start = PeerMesh_Wire_TransferOffer.startTransferOffer(&fbb) - PeerMesh_Wire_TransferOffer.add(transferId: transferId, &fbb) - PeerMesh_Wire_TransferOffer.add(name: name, &fbb) - PeerMesh_Wire_TransferOffer.add(totalBytes: totalBytes, &fbb) - return PeerMesh_Wire_TransferOffer.endTransferOffer(&fbb, start: __start) + let __start = Stromo_Wire_TransferOffer.startTransferOffer(&fbb) + Stromo_Wire_TransferOffer.add(transferId: transferId, &fbb) + Stromo_Wire_TransferOffer.add(name: name, &fbb) + Stromo_Wire_TransferOffer.add(totalBytes: totalBytes, &fbb) + return Stromo_Wire_TransferOffer.endTransferOffer(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: PeerMesh_Wire_TransferId.self) + try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: Stromo_Wire_TransferId.self) try _v.visit(field: VTOFFSET.name.p, fieldName: "name", required: false, type: ForwardOffset.self) try _v.visit(field: VTOFFSET.totalBytes.p, fieldName: "totalBytes", required: false, type: UInt64.self) _v.finish() @@ -304,7 +304,7 @@ public struct PeerMesh_Wire_TransferOffer: FlatBufferObject, Verifiable { } /// Announces an application byte stream opening on a dedicated stream (FR-18). -public struct PeerMesh_Wire_StreamOpen: FlatBufferObject, Verifiable { +public struct Stromo_Wire_StreamOpen: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -328,9 +328,9 @@ public struct PeerMesh_Wire_StreamOpen: FlatBufferObject, Verifiable { _ fbb: inout FlatBufferBuilder, labelOffset label: Offset = Offset() ) -> Offset { - let __start = PeerMesh_Wire_StreamOpen.startStreamOpen(&fbb) - PeerMesh_Wire_StreamOpen.add(label: label, &fbb) - return PeerMesh_Wire_StreamOpen.endStreamOpen(&fbb, start: __start) + let __start = Stromo_Wire_StreamOpen.startStreamOpen(&fbb) + Stromo_Wire_StreamOpen.add(label: label, &fbb) + return Stromo_Wire_StreamOpen.endStreamOpen(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { @@ -342,7 +342,7 @@ public struct PeerMesh_Wire_StreamOpen: FlatBufferObject, Verifiable { /// Envelope for every control-plane message (DD-5 rule 1). Size-prefixed on /// the wire. Unknown body variants are ignored-and-logged, never fatal (QA-11). -public struct PeerMesh_Wire_Signal: FlatBufferObject, Verifiable { +public struct Stromo_Wire_Signal: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -358,41 +358,41 @@ public struct PeerMesh_Wire_Signal: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var bodyType: PeerMesh_Wire_SignalBody { let o = _accessor.offset(VTOFFSET.bodyType.v); return o == 0 ? .none_ : PeerMesh_Wire_SignalBody(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .none_ } + public var bodyType: Stromo_Wire_SignalBody { let o = _accessor.offset(VTOFFSET.bodyType.v); return o == 0 ? .none_ : Stromo_Wire_SignalBody(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .none_ } public func body(type: T.Type) -> T? { let o = _accessor.offset(VTOFFSET.body.v); return o == 0 ? nil : _accessor.union(o) } public static func startSignal(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 2) } - public static func add(bodyType: PeerMesh_Wire_SignalBody, _ fbb: inout FlatBufferBuilder) { fbb.add(element: bodyType.rawValue, def: 0, at: VTOFFSET.bodyType.p) } + public static func add(bodyType: Stromo_Wire_SignalBody, _ fbb: inout FlatBufferBuilder) { fbb.add(element: bodyType.rawValue, def: 0, at: VTOFFSET.bodyType.p) } public static func add(body: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: body, at: VTOFFSET.body.p) } public static func endSignal(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } public static func createSignal( _ fbb: inout FlatBufferBuilder, - bodyType: PeerMesh_Wire_SignalBody = .none_, + bodyType: Stromo_Wire_SignalBody = .none_, bodyOffset body: Offset = Offset() ) -> Offset { - let __start = PeerMesh_Wire_Signal.startSignal(&fbb) - PeerMesh_Wire_Signal.add(bodyType: bodyType, &fbb) - PeerMesh_Wire_Signal.add(body: body, &fbb) - return PeerMesh_Wire_Signal.endSignal(&fbb, start: __start) + let __start = Stromo_Wire_Signal.startSignal(&fbb) + Stromo_Wire_Signal.add(bodyType: bodyType, &fbb) + Stromo_Wire_Signal.add(body: body, &fbb) + return Stromo_Wire_Signal.endSignal(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(unionKey: VTOFFSET.bodyType.p, unionField: VTOFFSET.body.p, unionKeyName: "bodyType", fieldName: "body", required: false, completion: { (verifier, key: PeerMesh_Wire_SignalBody, pos) in + try _v.visit(unionKey: VTOFFSET.bodyType.p, unionField: VTOFFSET.body.p, unionKeyName: "bodyType", fieldName: "body", required: false, completion: { (verifier, key: Stromo_Wire_SignalBody, pos) in switch key { case .none_: break // NOTE - SWIFT doesnt support none case .invite: - try ForwardOffset.verify(&verifier, at: pos, of: PeerMesh_Wire_Invite.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_Invite.self) case .inviteresponse: - try ForwardOffset.verify(&verifier, at: pos, of: PeerMesh_Wire_InviteResponse.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_InviteResponse.self) case .codeconfirm: - try ForwardOffset.verify(&verifier, at: pos, of: PeerMesh_Wire_CodeConfirm.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_CodeConfirm.self) case .rosterupdate: - try ForwardOffset.verify(&verifier, at: pos, of: PeerMesh_Wire_RosterUpdate.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_RosterUpdate.self) case .transferoffer: - try ForwardOffset.verify(&verifier, at: pos, of: PeerMesh_Wire_TransferOffer.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_TransferOffer.self) case .streamopen: - try ForwardOffset.verify(&verifier, at: pos, of: PeerMesh_Wire_StreamOpen.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_StreamOpen.self) } }) _v.finish() diff --git a/Sources/PeerMeshProtocol/Generated/stream_header_generated.swift b/Sources/PeerMeshProtocol/Generated/stream_header_generated.swift index b3309b5..f1c8308 100644 --- a/Sources/PeerMeshProtocol/Generated/stream_header_generated.swift +++ b/Sources/PeerMeshProtocol/Generated/stream_header_generated.swift @@ -4,7 +4,7 @@ import FlatBuffers -public enum PeerMesh_Wire_StreamKind: UInt8, Enum, Verifiable { +public enum Stromo_Wire_StreamKind: UInt8, Enum, Verifiable { public typealias T = UInt8 public static var byteSize: Int { return MemoryLayout.size } public var value: UInt8 { return self.rawValue } @@ -23,12 +23,12 @@ public enum PeerMesh_Wire_StreamKind: UInt8, Enum, Verifiable { /// (iOS 16+/macOS 13+) are the latency refinement (TODO datagrams). case datagram = 5 - public static var max: PeerMesh_Wire_StreamKind { return .datagram } - public static var min: PeerMesh_Wire_StreamKind { return .unknown } + public static var max: Stromo_Wire_StreamKind { return .datagram } + public static var min: Stromo_Wire_StreamKind { return .unknown } } -public struct PeerMesh_Wire_StreamHeader: FlatBufferObject, Verifiable { +public struct Stromo_Wire_StreamHeader: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -46,41 +46,41 @@ public struct PeerMesh_Wire_StreamHeader: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var kind: PeerMesh_Wire_StreamKind { let o = _accessor.offset(VTOFFSET.kind.v); return o == 0 ? .unknown : PeerMesh_Wire_StreamKind(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .unknown } + public var kind: Stromo_Wire_StreamKind { let o = _accessor.offset(VTOFFSET.kind.v); return o == 0 ? .unknown : Stromo_Wire_StreamKind(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .unknown } /// Sender-local FIFO sequence, per peer pair. Meaningful for OrderedMessage. public var sequence: UInt64 { let o = _accessor.offset(VTOFFSET.sequence.v); return o == 0 ? 0 : _accessor.readBuffer(of: UInt64.self, at: o) } /// Meaningful for TransferChunk. - public var transferId: PeerMesh_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: PeerMesh_Wire_TransferId.self, at: o) } - public var mutableTransferId: PeerMesh_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : PeerMesh_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } + public var transferId: Stromo_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: Stromo_Wire_TransferId.self, at: o) } + public var mutableTransferId: Stromo_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : Stromo_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } /// The app-chosen stream name. Meaningful for AppStream. public var label: String? { let o = _accessor.offset(VTOFFSET.label.v); return o == 0 ? nil : _accessor.string(at: o) } public var labelSegmentArray: [UInt8]? { return _accessor.getVector(at: VTOFFSET.label.v) } public static func startStreamHeader(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 4) } - public static func add(kind: PeerMesh_Wire_StreamKind, _ fbb: inout FlatBufferBuilder) { fbb.add(element: kind.rawValue, def: 0, at: VTOFFSET.kind.p) } + public static func add(kind: Stromo_Wire_StreamKind, _ fbb: inout FlatBufferBuilder) { fbb.add(element: kind.rawValue, def: 0, at: VTOFFSET.kind.p) } public static func add(sequence: UInt64, _ fbb: inout FlatBufferBuilder) { fbb.add(element: sequence, def: 0, at: VTOFFSET.sequence.p) } - public static func add(transferId: PeerMesh_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } + public static func add(transferId: Stromo_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } public static func add(label: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: label, at: VTOFFSET.label.p) } public static func endStreamHeader(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } public static func createStreamHeader( _ fbb: inout FlatBufferBuilder, - kind: PeerMesh_Wire_StreamKind = .unknown, + kind: Stromo_Wire_StreamKind = .unknown, sequence: UInt64 = 0, - transferId: PeerMesh_Wire_TransferId? = nil, + transferId: Stromo_Wire_TransferId? = nil, labelOffset label: Offset = Offset() ) -> Offset { - let __start = PeerMesh_Wire_StreamHeader.startStreamHeader(&fbb) - PeerMesh_Wire_StreamHeader.add(kind: kind, &fbb) - PeerMesh_Wire_StreamHeader.add(sequence: sequence, &fbb) - PeerMesh_Wire_StreamHeader.add(transferId: transferId, &fbb) - PeerMesh_Wire_StreamHeader.add(label: label, &fbb) - return PeerMesh_Wire_StreamHeader.endStreamHeader(&fbb, start: __start) + let __start = Stromo_Wire_StreamHeader.startStreamHeader(&fbb) + Stromo_Wire_StreamHeader.add(kind: kind, &fbb) + Stromo_Wire_StreamHeader.add(sequence: sequence, &fbb) + Stromo_Wire_StreamHeader.add(transferId: transferId, &fbb) + Stromo_Wire_StreamHeader.add(label: label, &fbb) + return Stromo_Wire_StreamHeader.endStreamHeader(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.kind.p, fieldName: "kind", required: false, type: PeerMesh_Wire_StreamKind.self) + try _v.visit(field: VTOFFSET.kind.p, fieldName: "kind", required: false, type: Stromo_Wire_StreamKind.self) try _v.visit(field: VTOFFSET.sequence.p, fieldName: "sequence", required: false, type: UInt64.self) - try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: PeerMesh_Wire_TransferId.self) + try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: Stromo_Wire_TransferId.self) try _v.visit(field: VTOFFSET.label.p, fieldName: "label", required: false, type: ForwardOffset.self) _v.finish() } diff --git a/Sources/PeerMeshProtocol/LibP2PIdentity.swift b/Sources/PeerMeshProtocol/LibP2PIdentity.swift index 31fdea2..2c8a824 100644 --- a/Sources/PeerMeshProtocol/LibP2PIdentity.swift +++ b/Sources/PeerMeshProtocol/LibP2PIdentity.swift @@ -3,11 +3,11 @@ import Foundation /// libp2p-compatible identity encoding (DD-8). /// -/// PeerMesh derives its ``PeerID`` from the peer's public key exactly the way +/// Stromo derives its ``PeerID`` from the peer's public key exactly the way /// libp2p does, so a future libp2p bridge stays identity-compatible. This helper /// is deliberately dependency-free (only CryptoKit's SHA-256, a pure CPU hash — -/// no I/O, consistent with the sans-I/O `PeerMeshProtocol` target, DD-6) and is -/// shared by both `PeerMeshProtocol` and `PeerMesh` so the two never disagree on +/// no I/O, consistent with the sans-I/O `StromoProtocol` target, DD-6) and is +/// shared by both `StromoProtocol` and `Stromo` so the two never disagree on /// the byte layout. /// /// ## PeerID multihash byte layout (34 bytes) diff --git a/Sources/PeerMeshProtocol/Locked.swift b/Sources/PeerMeshProtocol/Locked.swift index 9a169f1..ea4d44d 100644 --- a/Sources/PeerMeshProtocol/Locked.swift +++ b/Sources/PeerMeshProtocol/Locked.swift @@ -2,7 +2,7 @@ import Foundation /// A minimal mutex-guarded box for shared mutable state. /// -/// This lives in the sans-I/O `PeerMeshProtocol` target (its only dependency is +/// This lives in the sans-I/O `StromoProtocol` target (its only dependency is /// FlatBuffers, DD-6) purely so the runtime shell and its transports — which /// each need the same primitive — share one canonical implementation rather /// than re-declaring it per module. `NSLock` is a synchronization primitive, not diff --git a/Sources/PeerMeshProtocol/ProtocolVersion.swift b/Sources/PeerMeshProtocol/ProtocolVersion.swift index 6e1d4b9..1ca61c4 100644 --- a/Sources/PeerMeshProtocol/ProtocolVersion.swift +++ b/Sources/PeerMeshProtocol/ProtocolVersion.swift @@ -1,4 +1,4 @@ -/// PeerMesh wire-protocol version, negotiated once per connection via the +/// Stromo wire-protocol version, negotiated once per connection via the /// `PeerHello` bootstrap (peer_hello.fbs) — never per-signal. /// /// Semver interop rule: peers talk iff `major` matches. `minor` is additive diff --git a/Sources/PeerMeshProtocol/Signal.swift b/Sources/PeerMeshProtocol/Signal.swift index 384f96d..5796728 100644 --- a/Sources/PeerMeshProtocol/Signal.swift +++ b/Sources/PeerMeshProtocol/Signal.swift @@ -1,17 +1,17 @@ import FlatBuffers import Foundation -// Ergonomic aliases for the flatc-generated types (namespace PeerMesh.Wire). -public typealias WireSignal = PeerMesh_Wire_Signal -public typealias WireSignalBody = PeerMesh_Wire_SignalBody -public typealias WirePeerInfo = PeerMesh_Wire_PeerInfo -public typealias WireInvite = PeerMesh_Wire_Invite -public typealias WireInviteResponse = PeerMesh_Wire_InviteResponse -public typealias WireCodeConfirm = PeerMesh_Wire_CodeConfirm -public typealias WireRosterUpdate = PeerMesh_Wire_RosterUpdate -public typealias WireTransferOffer = PeerMesh_Wire_TransferOffer -public typealias WireStreamOpen = PeerMesh_Wire_StreamOpen -public typealias WireTransferId = PeerMesh_Wire_TransferId +// Ergonomic aliases for the flatc-generated types (namespace Stromo.Wire). +public typealias WireSignal = Stromo_Wire_Signal +public typealias WireSignalBody = Stromo_Wire_SignalBody +public typealias WirePeerInfo = Stromo_Wire_PeerInfo +public typealias WireInvite = Stromo_Wire_Invite +public typealias WireInviteResponse = Stromo_Wire_InviteResponse +public typealias WireCodeConfirm = Stromo_Wire_CodeConfirm +public typealias WireRosterUpdate = Stromo_Wire_RosterUpdate +public typealias WireTransferOffer = Stromo_Wire_TransferOffer +public typealias WireStreamOpen = Stromo_Wire_StreamOpen +public typealias WireTransferId = Stromo_Wire_TransferId /// A control-plane message: a verified FlatBuffers buffer read **in place** /// (DD-5, DD-6). Zero-copy discipline: @@ -197,8 +197,8 @@ extension WireTransferId { // Data-plane stream header (DD-7). Every non-control QUIC stream starts with // a size-prefixed StreamHeader; FIN delimits the payload. -public typealias WireStreamHeader = PeerMesh_Wire_StreamHeader -public typealias WireStreamKind = PeerMesh_Wire_StreamKind +public typealias WireStreamHeader = Stromo_Wire_StreamHeader +public typealias WireStreamKind = Stromo_Wire_StreamKind /// Delivery semantics for data (FR-15, FR-16, DD-7). public enum Delivery: Sendable, Equatable { @@ -217,7 +217,7 @@ public enum Delivery: Sendable, Equatable { case reliableOrdered /// Droppable, unordered, low-latency — true datagram semantics, so the /// payload must fit one datagram: sends over ``maxDatagramPayload`` throw - /// ``PeerMeshError/datagramTooLarge(bytes:limit:)``. For larger droppable + /// ``StromoError/datagramTooLarge(bytes:limit:)``. For larger droppable /// data, send `.reliable` and supersede at the application layer. case datagram } @@ -229,7 +229,7 @@ public enum Recipients: Sendable, Equatable { case peers([PeerID]) } -public enum PeerMeshError: Error, Sendable, Equatable { +public enum StromoError: Error, Sendable, Equatable { /// A surface that is deliberately not wired up yet on this platform or build. /// The associated string names it; each call site carries a comment pointing /// at the relevant TODO ledger entry (see CLAUDE.md). Current uses: the @@ -259,29 +259,29 @@ public enum PeerMeshError: Error, Sendable, Equatable { // NSError bridging renumbers payload cases (failure mode 11) — LocalizedError // keeps the diagnostic in localizedDescription. -extension PeerMeshError: LocalizedError { +extension StromoError: LocalizedError { public var errorDescription: String? { switch self { case .unimplemented(let surface): - return "PeerMesh: \(surface) is not implemented" + return "Stromo: \(surface) is not implemented" case .localNetworkPermissionDenied: - return "PeerMesh: Local Network permission denied" + return "Stromo: Local Network permission denied" case .invitationTimedOut: - return "PeerMesh: invitation timed out" + return "Stromo: invitation timed out" case .invitationDeclined: - return "PeerMesh: invitation declined" + return "Stromo: invitation declined" case .invitationAlreadyPending(let peer): - return "PeerMesh: an invitation to \(peer.displayName) is already pending" + return "Stromo: an invitation to \(peer.displayName) is already pending" case .peerUnreachable(let peer): - return "PeerMesh: peer \(peer.displayName) is unreachable" + return "Stromo: peer \(peer.displayName) is unreachable" case .datagramTooLarge(let bytes, let limit): - return "PeerMesh: .datagram payload is \(bytes) bytes; datagrams cannot exceed \(limit) bytes (they never fragment) — use .reliable for payloads this size" + return "Stromo: .datagram payload is \(bytes) bytes; datagrams cannot exceed \(limit) bytes (they never fragment) — use .reliable for payloads this size" case .malformedSignal: - return "PeerMesh: inbound signal failed verification" + return "Stromo: inbound signal failed verification" case .resourceTransferIncomplete: - return "PeerMesh: resource transfer ended before all bytes arrived" + return "Stromo: resource transfer ended before all bytes arrived" case .reorderBufferOverflow: - return "PeerMesh: ordered-message reorder buffer overflowed" + return "Stromo: ordered-message reorder buffer overflowed" } } } diff --git a/Sources/PeerMeshProtocol/SignalCodec.swift b/Sources/PeerMeshProtocol/SignalCodec.swift index 656ba2a..571d712 100644 --- a/Sources/PeerMeshProtocol/SignalCodec.swift +++ b/Sources/PeerMeshProtocol/SignalCodec.swift @@ -27,7 +27,7 @@ public enum SignalCodec { /// genuine structural malformation throws, and that is connection-fatal. public static func decode(_ data: Data) throws -> Signal { guard data.count <= maxControlMessageSize, !data.isEmpty else { - throw PeerMeshError.malformedSignal + throw StromoError.malformedSignal } var buffer = ByteBuffer(data: data) do { @@ -39,7 +39,7 @@ public enum SignalCodec { maxApparentSize: UInt32(maxControlMessageSize))) return Signal(verified: data, root: root) } catch { - throw PeerMeshError.malformedSignal + throw StromoError.malformedSignal } } } diff --git a/Sources/PeerMeshTestKit/InMemoryTransport.swift b/Sources/PeerMeshTestKit/InMemoryTransport.swift index 34fabf4..19136bb 100644 --- a/Sources/PeerMeshTestKit/InMemoryTransport.swift +++ b/Sources/PeerMeshTestKit/InMemoryTransport.swift @@ -1,5 +1,5 @@ import Foundation -import PeerMesh +import Stromo /// In-process transport for tests and mesh simulation (QA-8): exercises the /// full runtime — discovery, invitation, membership, messaging — with no @@ -68,7 +68,7 @@ public final class InMemoryTransport: PeerTransport, @unchecked Sendable { /// inbound stream receives the other. func connect(to peerID: PeerID, from identity: PeerIdentity) throws -> any PeerConnection { guard let ad = advertisements[peerID] else { - throw PeerMeshError.peerUnreachable(peerID) + throw StromoError.peerUnreachable(peerID) } let (dialerEnd, listenerEnd) = InMemoryConnection.pair( dialer: identity, listener: ad.identity) @@ -156,21 +156,21 @@ final class InMemoryConnection: PeerConnection, @unchecked Sendable { func sendSignal(_ bytes: Data) async throws { guard !closed.value, let partner = partnerBox.value else { - throw PeerMeshError.peerUnreachable(remotePeer) + throw StromoError.peerUnreachable(remotePeer) } partner.ownContinuation.yield(.signal(bytes)) } func sendData(_ payload: Data, delivery: Delivery, sequence: UInt64?) async throws { guard !closed.value, let partner = partnerBox.value else { - throw PeerMeshError.peerUnreachable(remotePeer) + throw StromoError.peerUnreachable(remotePeer) } partner.ownContinuation.yield(.data(payload, delivery, sequence: sequence)) } func openOutgoingStream(header: StreamHeaderInfo) async throws -> any PeerByteStream { guard !closed.value, let partner = partnerBox.value else { - throw PeerMeshError.peerUnreachable(remotePeer) + throw StromoError.peerUnreachable(remotePeer) } // The dedicated stream is a paired byte pipe; the writer stays local, // the reader surfaces on the partner's incomingStreams (with the header). @@ -221,7 +221,7 @@ final class InMemoryByteStream: PeerByteStream, @unchecked Sendable { func write(_ data: Data) async throws { guard !finished.value, let partner = partnerBox.value else { - throw PeerMeshError.resourceTransferIncomplete + throw StromoError.resourceTransferIncomplete } partner.incomingContinuation.yield(data) } diff --git a/Sources/PeerMeshUI/PeerBrowserView.swift b/Sources/PeerMeshUI/PeerBrowserView.swift index 1666f32..64a8c01 100644 --- a/Sources/PeerMeshUI/PeerBrowserView.swift +++ b/Sources/PeerMeshUI/PeerBrowserView.swift @@ -1,10 +1,10 @@ #if canImport(SwiftUI) -import PeerMesh +import Stromo import SwiftUI /// SwiftUI replacement for `MCBrowserViewController` (FR-23): lists discovered /// peers and lets the user invite them. Fully restyleable; built solely on -/// public PeerMesh APIs. +/// public Stromo APIs. /// /// - Warning: Experimental preview — the API is unstable and this view is a /// minimal skeleton. TODO(ui-completion): invite action with progress/error diff --git a/Tests/MPCCompatTests/MPCCompatE2ETests.swift b/Tests/MPCCompatTests/MPCCompatE2ETests.swift index 5548cbb..f742eb9 100644 --- a/Tests/MPCCompatTests/MPCCompatE2ETests.swift +++ b/Tests/MPCCompatTests/MPCCompatE2ETests.swift @@ -2,8 +2,8 @@ import Foundation import Testing @testable import MPCCompat -import PeerMesh -import PeerMeshTestKit +import Stromo +import StromoTestKit /// End-to-end bridge tests over `InMemoryTransport` (a shared `Hub`), no radios. /// @@ -304,7 +304,7 @@ extension MPCCompatE2ETests { /// retry fail instantly ("browser has already been cancelled"). @Test("Disconnect then re-invite over real QUIC (MCSession retry semantics)") func reinviteAfterDisconnectOverQUIC() async throws { - setenv("PEERMESH_NO_P2P", "1", 1) + setenv("Stromo_NO_P2P", "1", 1) let probe = PeerIdentity(name: "compat-retry-probe") guard QUICTransport.isTLSIdentityAvailable(for: probe) else { print("[skip] no TLS identity in this environment"); return diff --git a/Tests/MPCCompatTests/MPCCompatTests.swift b/Tests/MPCCompatTests/MPCCompatTests.swift index 2a1f281..d528ace 100644 --- a/Tests/MPCCompatTests/MPCCompatTests.swift +++ b/Tests/MPCCompatTests/MPCCompatTests.swift @@ -2,7 +2,7 @@ import Foundation import Testing @testable import MPCCompat -import PeerMesh +import Stromo @Suite("MPCCompat shim") struct MPCCompatTests { @@ -19,7 +19,7 @@ struct MPCCompatTests { // A session with no advertiser/browser attached has no CompatCore and // thus no route; send must surface that as a throw, not a trap. let session = MultipeerSession(peer: "T", service: "_compat._udp") - #expect(throws: PeerMeshError.self) { + #expect(throws: StromoError.self) { try session.send(Data([0x01]), toPeers: [], with: .reliable) } } diff --git a/Tests/PeerMeshProtocolTests/ProtocolEngineTests.swift b/Tests/PeerMeshProtocolTests/ProtocolEngineTests.swift index 49e3e57..1f1bdeb 100644 --- a/Tests/PeerMeshProtocolTests/ProtocolEngineTests.swift +++ b/Tests/PeerMeshProtocolTests/ProtocolEngineTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import PeerMeshProtocol +@testable import StromoProtocol /// Tier-1 tests (DD-6): the complete invitation protocol exercised with NO /// transport — no QUIC, no sockets, no async, no clocks. Two engines wired diff --git a/Tests/PeerMeshProtocolTests/ProtocolVersionTests.swift b/Tests/PeerMeshProtocolTests/ProtocolVersionTests.swift index d200657..d0d6e7c 100644 --- a/Tests/PeerMeshProtocolTests/ProtocolVersionTests.swift +++ b/Tests/PeerMeshProtocolTests/ProtocolVersionTests.swift @@ -1,6 +1,6 @@ import Testing -@testable import PeerMeshProtocol +@testable import StromoProtocol @Suite("ProtocolVersion — semver interop rule") struct ProtocolVersionTests { diff --git a/Tests/PeerMeshProtocolTests/SignalCodecTests.swift b/Tests/PeerMeshProtocolTests/SignalCodecTests.swift index 3c0b474..9965cb1 100644 --- a/Tests/PeerMeshProtocolTests/SignalCodecTests.swift +++ b/Tests/PeerMeshProtocolTests/SignalCodecTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import PeerMeshProtocol +@testable import StromoProtocol @Suite("SignalCodec — verified zero-copy boundary (DD-5)") struct SignalCodecTests { @@ -32,10 +32,10 @@ struct SignalCodecTests { @Test("Verifier rejects malformed bytes as malformedSignal, never traps") func rejectsGarbage() { let garbage = Data((0..<64).map { _ in UInt8.random(in: .min ... .max) }) - #expect(throws: PeerMeshError.malformedSignal) { + #expect(throws: StromoError.malformedSignal) { _ = try SignalCodec.decode(garbage) } - #expect(throws: PeerMeshError.malformedSignal) { + #expect(throws: StromoError.malformedSignal) { _ = try SignalCodec.decode(Data()) } } @@ -43,7 +43,7 @@ struct SignalCodecTests { @Test("Size cap enforced before any parsing (DD-5 rule 3)") func sizeCap() { let oversized = Data(count: SignalCodec.maxControlMessageSize + 1) - #expect(throws: PeerMeshError.malformedSignal) { + #expect(throws: StromoError.malformedSignal) { _ = try SignalCodec.decode(oversized) } } diff --git a/Tests/PeerMeshTests/DisconnectionTests.swift b/Tests/PeerMeshTests/DisconnectionTests.swift index 955a4f2..7222b05 100644 --- a/Tests/PeerMeshTests/DisconnectionTests.swift +++ b/Tests/PeerMeshTests/DisconnectionTests.swift @@ -1,9 +1,9 @@ import Foundation import Testing -import PeerMeshTestKit +import StromoTestKit -@testable import PeerMesh +@testable import Stromo // ============================================================================= // Disconnection detection (FR-14 bound) — the three departure shapes diff --git a/Tests/PeerMeshTests/EndToEndSessionTests.swift b/Tests/PeerMeshTests/EndToEndSessionTests.swift index 8a0c7a2..6ad17b7 100644 --- a/Tests/PeerMeshTests/EndToEndSessionTests.swift +++ b/Tests/PeerMeshTests/EndToEndSessionTests.swift @@ -2,8 +2,8 @@ import CryptoKit import Foundation import Testing -import PeerMesh -import PeerMeshTestKit +import Stromo +import StromoTestKit /// Tier-1.5 tests: the COMPLETE runtime — PeerSession effect executor, engine, /// codec, timers — over the in-memory transport. Real wire bytes (FlatBuffers @@ -103,7 +103,7 @@ struct EndToEndSessionTests { } } - await #expect(throws: PeerMeshError.invitationDeclined) { + await #expect(throws: StromoError.invitationDeclined) { try await monitor.invite(try #require(discovered)) } await declineTask.value @@ -129,7 +129,7 @@ struct EndToEndSessionTests { } } - await #expect(throws: PeerMeshError.invitationTimedOut) { + await #expect(throws: StromoError.invitationTimedOut) { try await monitor.invite(try #require(discovered), timeout: 0.2) } @@ -147,7 +147,7 @@ struct EndToEndSessionTests { transport: InMemoryTransport(hub: hub)) let oversized = Data(repeating: 0xD8, count: Delivery.maxDatagramPayload + 1) - await #expect(throws: PeerMeshError.datagramTooLarge( + await #expect(throws: StromoError.datagramTooLarge( bytes: Delivery.maxDatagramPayload + 1, limit: Delivery.maxDatagramPayload) ) { try await session.send(oversized, delivery: .datagram) @@ -155,7 +155,7 @@ struct EndToEndSessionTests { // Exactly at the cap: passes the size guard (then fails on membership, // proving the guard, not the payload, was the gate above). - await #expect(throws: PeerMeshError.peerUnreachable(identity.id)) { + await #expect(throws: StromoError.peerUnreachable(identity.id)) { try await session.send( Data(repeating: 0xD8, count: Delivery.maxDatagramPayload), delivery: .datagram) } @@ -169,7 +169,7 @@ struct EndToEndSessionTests { identity: PeerIdentity(name: "Lonely"), service: "_e2e._udp", transport: InMemoryTransport(hub: hub)) - await #expect(throws: PeerMeshError.self) { + await #expect(throws: StromoError.self) { try await session.send(Data([1])) } await session.disconnect() @@ -396,7 +396,7 @@ struct EndToEndSessionTests { var data = Data(capacity: bytes) while data.count < bytes { data.append(block) } let url = FileManager.default.temporaryDirectory - .appendingPathComponent("peermesh-src-\(UUID().uuidString).bin") + .appendingPathComponent("Stromo-src-\(UUID().uuidString).bin") try data.prefix(bytes).write(to: url) return url } diff --git a/Tests/PeerMeshTests/IdentitySecurityTests.swift b/Tests/PeerMeshTests/IdentitySecurityTests.swift index 1836514..048a1cf 100644 --- a/Tests/PeerMeshTests/IdentitySecurityTests.swift +++ b/Tests/PeerMeshTests/IdentitySecurityTests.swift @@ -2,7 +2,7 @@ import CryptoKit import Foundation import Testing -@testable import PeerMesh +@testable import Stromo #if canImport(Security) import Security @@ -200,7 +200,7 @@ struct IdentitySecurityTests { private static func tempDir() -> URL { let base = FileManager.default.temporaryDirectory - .appendingPathComponent("peermesh-tests-\(UUID().uuidString)") + .appendingPathComponent("Stromo-tests-\(UUID().uuidString)") try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true) return base } diff --git a/Tests/PeerMeshTests/MeshHarness.swift b/Tests/PeerMeshTests/MeshHarness.swift index 3fdf0ec..869a2e3 100644 --- a/Tests/PeerMeshTests/MeshHarness.swift +++ b/Tests/PeerMeshTests/MeshHarness.swift @@ -1,7 +1,7 @@ import Foundation import Testing -import PeerMesh +import Stromo /// N-peer mesh test harness (QA-2 / QA-8): full-mesh formation over any /// `PeerTransport`, a traffic pump with self-verifying payloads, and the diff --git a/Tests/PeerMeshTests/MeshReliabilityTests.swift b/Tests/PeerMeshTests/MeshReliabilityTests.swift index ed20e01..cd97429 100644 --- a/Tests/PeerMeshTests/MeshReliabilityTests.swift +++ b/Tests/PeerMeshTests/MeshReliabilityTests.swift @@ -1,9 +1,9 @@ import Foundation import Testing -import PeerMeshTestKit +import StromoTestKit -@testable import PeerMesh +@testable import Stromo // ============================================================================= // N-peer mesh reliability (QA-2 / QA-8) — what these suites prove and how diff --git a/Tests/PeerMeshTests/PeerHelloCodecTests.swift b/Tests/PeerMeshTests/PeerHelloCodecTests.swift index a077975..3a12cbd 100644 --- a/Tests/PeerMeshTests/PeerHelloCodecTests.swift +++ b/Tests/PeerMeshTests/PeerHelloCodecTests.swift @@ -1,8 +1,8 @@ import Foundation import Testing -@testable import PeerMesh -import PeerMeshProtocol +@testable import Stromo +import StromoProtocol #if canImport(Network) && canImport(Security) diff --git a/Tests/PeerMeshTests/PeerMeshTests.swift b/Tests/PeerMeshTests/PeerMeshTests.swift index c5fd515..622dc70 100644 --- a/Tests/PeerMeshTests/PeerMeshTests.swift +++ b/Tests/PeerMeshTests/PeerMeshTests.swift @@ -1,10 +1,10 @@ import Foundation import Testing -@testable import PeerMesh +@testable import Stromo -@Suite("PeerMesh core") -struct PeerMeshTests { +@Suite("Stromo core") +struct StromoTests { @Test("Identity derives PeerID from public key hash") func identityDerivesPeerID() { @@ -46,7 +46,7 @@ struct PeerMeshTests { service: "_t._udp", transport: InMemoryTransport(hub: hub) ) - await #expect(throws: PeerMeshError.self) { + await #expect(throws: StromoError.self) { try await session.send(Data([0x01])) } await session.disconnect() @@ -58,10 +58,10 @@ struct PeerMeshTests { } } -import PeerMeshTestKit +import StromoTestKit #if canImport(Network) && canImport(Security) -extension PeerMeshTests { +extension StromoTests { @Test("Invalid Bonjour service types are rejected loudly, not silently") func bonjourTypeValidation() throws { // The class of bug that reached physical devices: a bare MPC-style diff --git a/Tests/PeerMeshTests/QUICSmokeTests.swift b/Tests/PeerMeshTests/QUICSmokeTests.swift index a18dca8..26bfc9b 100644 --- a/Tests/PeerMeshTests/QUICSmokeTests.swift +++ b/Tests/PeerMeshTests/QUICSmokeTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -import PeerMesh +import Stromo @Suite("QUIC smoke", .serialized) struct QUICSmokeTests { @@ -77,7 +77,7 @@ extension QUICSmokeTests { guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { print("[skip] QUIC: no TLS identity in this environment"); return } - setenv("PEERMESH_NO_P2P", "1", 1) + setenv("Stromo_NO_P2P", "1", 1) let service = ServiceDescriptor(type: "_pmbj\(UInt16.random(in: 1000...9999))._udp") func transport() -> QUICTransport { QUICTransport(configuration: .init(discovery: .bonjour)) } let camera = PeerSession( diff --git a/Tests/PeerMeshTests/QUICStreamChurnTests.swift b/Tests/PeerMeshTests/QUICStreamChurnTests.swift index 500ba09..ce65f18 100644 --- a/Tests/PeerMeshTests/QUICStreamChurnTests.swift +++ b/Tests/PeerMeshTests/QUICStreamChurnTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import PeerMesh +@testable import Stromo #if canImport(Network) && canImport(Security) diff --git a/Tests/PeerMeshTests/QUICWirePrimitiveTests.swift b/Tests/PeerMeshTests/QUICWirePrimitiveTests.swift index 23b6973..7d9f391 100644 --- a/Tests/PeerMeshTests/QUICWirePrimitiveTests.swift +++ b/Tests/PeerMeshTests/QUICWirePrimitiveTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import PeerMesh +@testable import Stromo #if canImport(Network) && canImport(Security) diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md index 54f567b..4e6021b 100644 --- a/docs/IMPLEMENTATION-PLAN.md +++ b/docs/IMPLEMENTATION-PLAN.md @@ -1,4 +1,4 @@ -# PeerMesh Implementation Plan +# Stromo Implementation Plan **Working plan — SU-2026-WP-001** · Last updated: July 26, 2026 @@ -25,13 +25,13 @@ and the full QUIC loopback lifecycle. Three Network.framework platform findings recorded in [spike-results.md](spike-results.md) (inbound-stream readiness, inbound-connection retention, two-step FIN). -Integration proof: `remote-shutter` branch `feat/peermesh-mpccompat` (compiles + full unit suite green against MPCCompat; functional once Step 4 lands). +Integration proof: `remote-shutter` branch `feat/Stromo-mpccompat` (compiles + full unit suite green against MPCCompat; functional once Step 4 lands). --- ## Step 0 — Scaffolding ✅ -Landed: SPM package (`PeerMeshProtocol` / `PeerMesh` / `MPCCompat` / `PeerMeshUI` / `PeerMeshTestKit`); sans-I/O `ProtocolEngine` (DD-6) with invitation/roster/messaging transitions; disciplined zero-copy FlatBuffers signaling (`signal.fbs`, `stream_header.fbs`, `SignalCodec` with verifier caps, DD-5); flake.nix pinning `flatc` = SPM runtime (exact 25.2.10); CI workflow (macOS native + iOS Simulator + Mac Catalyst + codegen-drift job); MPCCompat compile surface validated against a real app (remote-shutter). +Landed: SPM package (`StromoProtocol` / `Stromo` / `MPCCompat` / `StromoUI` / `StromoTestKit`); sans-I/O `ProtocolEngine` (DD-6) with invitation/roster/messaging transitions; disciplined zero-copy FlatBuffers signaling (`signal.fbs`, `stream_header.fbs`, `SignalCodec` with verifier caps, DD-5); flake.nix pinning `flatc` = SPM runtime (exact 25.2.10); CI workflow (macOS native + iOS Simulator + Mac Catalyst + codegen-drift job); MPCCompat compile surface validated against a real app (remote-shutter). ## Step 1 — Runtime shell + InMemoryTransport ✅ @@ -55,7 +55,7 @@ Everything QUIC needs before a TLS handshake can happen. The real driver, validated entirely on `127.0.0.1` in CI — no radios. -1. **Listener/advertiser:** `NWListener` with QUIC parameters (ALPN `"peermesh"`, local identity from Step 2) + Bonjour `.service(type:)` with `NWTXTRecord` metadata; `includePeerToPeer = true`; Local Network permission surfacing (FR-4). +1. **Listener/advertiser:** `NWListener` with QUIC parameters (ALPN `"Stromo"`, local identity from Step 2) + Bonjour `.service(type:)` with `NWTXTRecord` metadata; `includePeerToPeer = true`; Local Network permission surfacing (FR-4). 2. **Browser:** `NWBrowser(.bonjourWithTXTRecord)`, cross-interface dedup by peer id (FR-2). 3. **Connection = QUIC multiplex:** resolve **Spike S-3** (`NWMultiplexGroup` vs per-stream `NWConnection`s) empirically on loopback; control stream (signals, size-prefixed) + stream-per-message with `StreamHeader` (DD-7) + datagram flow for `.datagram` (RFC 9221). 4. **Benchmark = Spike S-6:** stream-churn rate (target ≥1,000 msg/s loopback, flat memory) wired as the nightly CI job stub in `ci.yml`. @@ -69,9 +69,9 @@ The real driver, validated entirely on `127.0.0.1` in CI — no radios. 2. **Resource transfer (FR-17):** `TransferOffer` signal + `transferChunk` stream; disk-to-disk, `Progress`, cancellation via stream reset; memory cap per QA-3. 3. **App byte streams (FR-18):** `StreamOpen` + `PeerByteStream` over dedicated streams. 4. **MPCCompat bridge:** pump `PeerSession` membership/messages/resources into `MultipeerSessionDelegate` callbacks (serial delegate queue, MCSession semantics); `NearbyServiceAdvertiser/Browser` bridging; `MultipeerSession.send/sendResource` wired (`.reliable`→`.reliableOrdered`, `.unreliable`→`.datagram`). -5. **Validation:** remote-shutter `feat/peermesh-mpccompat` runs camera↔monitor over LAN (simulator loopback first, then two Macs/devices on one network); its loopback session tests pass against the bridge over InMemoryTransport. +5. **Validation:** remote-shutter `feat/Stromo-mpccompat` runs camera↔monitor over LAN (simulator loopback first, then two Macs/devices on one network); its loopback session tests pass against the bridge over InMemoryTransport. -**Exit criteria:** remote-shutter takes a photo through PeerMesh on LAN; MPCCompat E2E tests in PeerMesh repo green over both transports. +**Exit criteria:** remote-shutter takes a photo through Stromo on LAN; MPCCompat E2E tests in Stromo repo green over both transports. ## Step 5 — Hardware spikes ⬜ (requires operator + ≥2 physical devices) @@ -84,13 +84,13 @@ The real driver, validated entirely on `127.0.0.1` in CI — no radios. ## Step 6 — Release engineering ⬜ -Initial public commit + tag `0.1.0`; license decision (MIT vs Apache-2.0 — study §8 leans Apache for the patent grant); repo description/topics + Swift Package Index submission; hosted DocC + `llms.txt`; "Migrating from MultipeerConnectivity to PeerMesh" guide (the TN3213-echo landing page); name availability final check (`PeerMesh` verified clear Jul 26); threat-model document (security-consultancy differentiator, study §8.3). +Initial public commit + tag `0.1.0`; license decision (MIT vs Apache-2.0 — study §8 leans Apache for the patent grant); repo description/topics + Swift Package Index submission; hosted DocC + `llms.txt`; "Migrating from MultipeerConnectivity to Stromo" guide (the TN3213-echo landing page); name availability final check (`Stromo` verified clear Jul 26); threat-model document (security-consultancy differentiator, study §8.3). --- ## Standing constraints (from design doc) -- Engine stays sans-I/O: no sockets/clocks/async in `PeerMeshProtocol` (DD-6). +- Engine stays sans-I/O: no sockets/clocks/async in `StromoProtocol` (DD-6). - FlatBuffers discipline: schema evolution rules + verifier caps + pinned-together flatc/runtime (DD-5). - One suite, N transports/platforms: new features land with engine tests first, driver parity second (QA-8). - No Bluetooth claims, ever (C-2); no private API (C-6). diff --git a/docs/design-mpc-successor.md b/docs/design-mpc-successor.md index 7e0a82e..399646d 100644 --- a/docs/design-mpc-successor.md +++ b/docs/design-mpc-successor.md @@ -1,4 +1,4 @@ -# Design Document: PeerMesh — A Modern Peer-to-Peer Session Framework for Apple Platforms +# Design Document: Stromo — A Modern Peer-to-Peer Session Framework for Apple Platforms **Design Document — SU-2026-DD-001 (Draft 1)** @@ -9,13 +9,13 @@ | **Status** | Draft — requirements baselined; architecture in progress | | **Companion document** | *Feasibility Study: A Third-Party Replacement for Apple's MultipeerConnectivity Framework* (SU-2026-TR-001) | -*Working name `PeerMesh` is a placeholder pending a trademark/Swift Package Index availability check. Quality attribute requirements use the SEI six-part scenario form (source – stimulus – artifact – environment – response – response measure).* +*Working name `Stromo` is a placeholder pending a trademark/Swift Package Index availability check. Quality attribute requirements use the SEI six-part scenario form (source – stimulus – artifact – environment – response – response measure).* --- ## 1. Purpose and Scope -PeerMesh is an open-source Swift package providing discovery, secure session establishment, and data exchange among nearby Apple devices, replacing the deprecated MultipeerConnectivity (MPC) framework. It consists of: +Stromo is an open-source Swift package providing discovery, secure session establishment, and data exchange among nearby Apple devices, replacing the deprecated MultipeerConnectivity (MPC) framework. It consists of: 1. a **modern core** built on Network.framework with Swift 6 structured concurrency (`async`/`await`, `AsyncSequence`), QUIC as the primary transport; and 2. an **`MPCCompat` shim** offering a near-drop-in migration surface for existing MPC code. @@ -80,7 +80,7 @@ Priority: **M** = must (1.0), **S** = should (1.x), **C** = could (post-1.0). ### 3.6 UI Components and Compatibility -- **FR-23 (S)** SwiftUI components: a peer browser/picker view and an invitation-consent sheet (replacing `MCBrowserViewController` / `MCAdvertiserAssistant`), fully restyleable, built solely on public PeerMesh APIs. +- **FR-23 (S)** SwiftUI components: a peer browser/picker view and an invitation-consent sheet (replacing `MCBrowserViewController` / `MCAdvertiserAssistant`), fully restyleable, built solely on public Stromo APIs. - **FR-24 (S)** **`MPCCompat` module:** near-drop-in analogs of `MCSession`, `MCPeerID`, `MCNearbyServiceAdvertiser/Browser` delegate semantics (original type names, no `MC` prefix; migration = mechanical rename), including `NSStream`-bridged streams and 8-peer-limit emulation flag for behavioral parity testing. - **FR-25 (C)** Pluggable transport backends behind a `PeerTransport` protocol: QUIC/Bonjour (1.0), TCP+TLS fallback profile (contingency), Wi-Fi Aware backend for paired-device scenarios on iOS 26+ hardware (post-1.0). @@ -99,15 +99,15 @@ Priority: **M** = must (1.0), **S** = should (1.x), **C** = could (post-1.0). | **QA-7** | Modifiability (transport evolution) | Maintainer adds a Wi-Fi Aware backend → change confined to a new `PeerTransport` conformance + capability gating → **zero changes to session, security, messaging, or UI modules; core compiles without the new backend**. | | **QA-8** | Testability | CI runs on every commit without physical devices → handshake, mesh membership, topology election, timers, and transfer signaling execute as deterministic sans-I/O engine simulations (DD-6), plus real-QUIC loopback integration tests → **≥ 85% line coverage of non-radio code; full 32-peer mesh simulation in < 60 s on a CI runner; any recorded device-session input log replays deterministically into the engine**. | | **QA-9** | Usability (migration) | An MPC app of moderate complexity (advertise+browse+session+send) migrates to `MPCCompat` → **≤ 1 day of work, no architectural changes; diff limited to imports/type renames + Info.plist keys**, validated by a published ported sample app. | -| **QA-10** | Compatibility (OS floor) | App integrates PeerMesh 1.0 → runs on **iOS 15+/iPadOS 15+/macOS 12+/tvOS 15+/visionOS 1+** (QUIC floor); Wi-Fi Aware features degrade gracefully with runtime capability checks. | -| **QA-11** | Interoperability (protocol evolution) | A device running an app built against PeerMesh 1.0 joins a session whose other peers run PeerMesh 1.x (newer signaling schema) → signaling interoperates via FlatBuffers evolution rules (DD-5): unknown fields skipped, unknown union variants ignored-and-logged → **session forms and all 1.0-era features function; zero connection failures attributable to schema version skew, proven by golden-file cross-version tests in CI**. | +| **QA-10** | Compatibility (OS floor) | App integrates Stromo 1.0 → runs on **iOS 15+/iPadOS 15+/macOS 12+/tvOS 15+/visionOS 1+** (QUIC floor); Wi-Fi Aware features degrade gracefully with runtime capability checks. | +| **QA-11** | Interoperability (protocol evolution) | A device running an app built against Stromo 1.0 joins a session whose other peers run Stromo 1.x (newer signaling schema) → signaling interoperates via FlatBuffers evolution rules (DD-5): unknown fields skipped, unknown union variants ignored-and-logged → **session forms and all 1.0-era features function; zero connection failures attributable to schema version skew, proven by golden-file cross-version tests in CI**. | --- ## 5. Constraints (platform-imposed) - **C-1** AWDL is reachable only via `NWParameters.includePeerToPeer`; path selection is Happy-Eyeballs-driven and cannot be forced to prefer the P2P path. -- **C-2** No Bluetooth data transport exists for third-party apps; PeerMesh makes no Bluetooth claims. +- **C-2** No Bluetooth data transport exists for third-party apps; Stromo makes no Bluetooth claims. - **C-3** QUIC in Network.framework supports **PKI only, not TLS-PSK** (TN3213) → drives FR-21's identity design. - **C-4** `NSLocalNetworkUsageDescription` + `NSBonjourServices` required; standard Bonjour needs no multicast entitlement; Wi-Fi Aware backend would additionally need its entitlement + plist declarations. - **C-5** iOS backgrounding suspends advertising/browsing and connections (except future Wi-Fi Aware paired scenarios); the API must make suspension/resume explicit. @@ -137,7 +137,7 @@ One QUIC connection per peer pair carries everything: Because of C-3, the WWDC19 passcode/PSK pattern is unavailable on QUIC. Instead: every peer has a persistent P-256 identity (FR-20); TLS always terminates against the peer's self-signed certificate; *authentication* is a policy layered on *encryption* (FR-21). -**The governing principle is MPC-ergonomics parity (FR-21):** MPC's celebrated simplicity was, concretely, *encrypted-but-unauthenticated by default* — `encryptionPreference = .required` auto-generated keys, and the de facto authorization was the user tapping Accept on the invitation (`didReceiveCertificate` accepted everyone unless overridden, and rarely was). PeerMesh's `.automatic` default reproduces exactly that developer and user experience on QUIC: the identity is generated and persisted silently, the self-signed certificate exists only to satisfy the QUIC handshake, the verify block accepts and records the peer key hash, and invitation consent is the authorization step. Zero configuration, no pairing ceremony — and two silent upgrades over MPC: unforgeable key-derived peer IDs, and TOFU continuity warnings if a known peer's key ever changes. +**The governing principle is MPC-ergonomics parity (FR-21):** MPC's celebrated simplicity was, concretely, *encrypted-but-unauthenticated by default* — `encryptionPreference = .required` auto-generated keys, and the de facto authorization was the user tapping Accept on the invitation (`didReceiveCertificate` accepted everyone unless overridden, and rarely was). Stromo's `.automatic` default reproduces exactly that developer and user experience on QUIC: the identity is generated and persisted silently, the self-signed certificate exists only to satisfy the QUIC handshake, the verify block accepts and records the peer key hash, and invitation consent is the authorization step. Zero configuration, no pairing ceremony — and two silent upgrades over MPC: unforgeable key-derived peer IDs, and TOFU continuity warnings if a known peer's key ever changes. The opt-in `.pairingCode` mode binds a short user-visible code to the TLS transcript/exporter secret covering both certificates — a MITM cannot present matching codes on both sides. This is strictly stronger than MPC (which authorized on cleartext hostname) and stronger than bare PSK (identity continuity persists across sessions). It exists for apps that genuinely need active-MITM resistance; it is deliberately not the default, because MPC never had it either and imposing a ceremony on every session would betray the framework's reason to exist. @@ -177,7 +177,7 @@ All event delivery via `AsyncSequence`; all operations `async` with cancellation ### DD-5: Disciplined FlatBuffers for the signaling plane — **adopted** -All control-plane messages (identity bootstrap/PeerHello, invitation, accept/decline, roster gossip, keepalive, topology election, transfer/stream announcements) are FlatBuffers, generated from `.fbs` schemas that are the single source of truth in the repository. Version negotiation is semver carried once per connection in `PeerHello` (same-major interop; a mismatch surfaces as a typed "upgrade required" error naming both versions) — which is why the QUIC ALPN is frozen: bumping it would fail the handshake before hello and hide the diagnosis. `peer_hello.fbs` is therefore a frozen bootstrap contract every major version must parse. Application payloads (FR-15..FR-18) remain opaque bytes — apps serialize however they like; FlatBuffers governs only PeerMesh's own protocol. +All control-plane messages (identity bootstrap/PeerHello, invitation, accept/decline, roster gossip, keepalive, topology election, transfer/stream announcements) are FlatBuffers, generated from `.fbs` schemas that are the single source of truth in the repository. Version negotiation is semver carried once per connection in `PeerHello` (same-major interop; a mismatch surfaces as a typed "upgrade required" error naming both versions) — which is why the QUIC ALPN is frozen: bumping it would fail the handshake before hello and hide the diagnosis. `peer_hello.fbs` is therefore a frozen bootstrap contract every major version must parse. Application payloads (FR-15..FR-18) remain opaque bytes — apps serialize however they like; FlatBuffers governs only Stromo's own protocol. **Rationale:** zero-copy access suits a hot control path on many concurrent connections; the schema compiler enforces cross-version wire compatibility mechanically (vs. convention-only discipline in CBOR/JSON); the built-in **verifier** gives structural validation of bytes received from *not-yet-authenticated* peers during the handshake — a security property the signaling path specifically needs; official Swift support ships via SPM (`google/flatbuffers`, `flatc --swift`). @@ -221,7 +221,7 @@ ProtocolEngine.handle(Input) -> [Effect] // synchronous, deterministic > **Mesh reliability — validated in CI (2026-07).** The QA-2/QA-8 mesh > targets are now asserted on every commit by -> `Tests/PeerMeshTests/MeshReliabilityTests.swift`: a monotonic N = 3…32 +> `Tests/StromoTests/MeshReliabilityTests.swift`: a monotonic N = 3…32 > full-mesh sweep over `InMemoryTransport` (formation convergence, exactly-once > delivery, payload integrity, sender attribution, per-sender ordering under a > reordering transport, clean drain) plus a 3-peer **real-QUIC loopback** @@ -233,9 +233,9 @@ ProtocolEngine.handle(Input) -> [Effect] // synchronous, deterministic > does NOT cover: real radios/AWDL (tier 3) and relayed topologies (QA-2's > 128-peer target). -**Platform execution matrix (tiers 1–2):** the same test bundles run on every shipping target — macOS native via `swift test` (fast loop + coverage), and per-destination via `xcodebuild test -scheme PeerMesh-Package` against **iOS Simulator** and **Mac Catalyst** (tvOS/visionOS simulators when installed). This is the pattern used by swift-nio, Apple's own packages, and the major Swift OSS projects: one suite, N destinations, no per-platform test code. Network.framework QUIC works over loopback in the iOS Simulator and Catalyst, so tier 2 runs on all of them without radios. Aspirational tier-1 extension (FoundationDB-style deterministic simulation testing): seeded random command/interleaving exploration against engine invariants — the seed arrives as engine input, so any nightly failure reproduces exactly from its seed. +**Platform execution matrix (tiers 1–2):** the same test bundles run on every shipping target — macOS native via `swift test` (fast loop + coverage), and per-destination via `xcodebuild test -scheme Stromo-Package` against **iOS Simulator** and **Mac Catalyst** (tvOS/visionOS simulators when installed). This is the pattern used by swift-nio, Apple's own packages, and the major Swift OSS projects: one suite, N destinations, no per-platform test code. Network.framework QUIC works over loopback in the iOS Simulator and Catalyst, so tier 2 runs on all of them without radios. Aspirational tier-1 extension (FoundationDB-style deterministic simulation testing): seeded random command/interleaving exploration against engine invariants — the seed arrives as engine input, so any nightly failure reproduces exactly from its seed. -Module consequence: the engine, typed `Signal` model, and codec live in a dedicated **`PeerMeshProtocol`** target whose only dependency is FlatBuffers; `PeerMesh` (runtime shell + drivers) depends on it — **the shipped `PeerMesh` product includes the QUIC integration**; consumers get working transport out of the box, and the protocol target stays importable on its own for tests and tooling. Under the AI-driven development model (study §4.4) this is the highest-leverage structural decision in the project: it moves the majority of correctness into tier 1, which is agent-buildable and agent-testable with zero device time. +Module consequence: the engine, typed `Signal` model, and codec live in a dedicated **`StromoProtocol`** target whose only dependency is FlatBuffers; `Stromo` (runtime shell + drivers) depends on it — **the shipped `Stromo` product includes the QUIC integration**; consumers get working transport out of the box, and the protocol target stays importable on its own for tests and tooling. Under the AI-driven development model (study §4.4) this is the highest-leverage structural decision in the project: it moves the majority of correctness into tier 1, which is agent-buildable and agent-testable with zero device time. ### DD-7: Stream-per-message data plane (MoQ-inspired) — **adopted** @@ -275,13 +275,13 @@ iOS 16+/macOS 13+) recover loss-independence where latency matters. ### DD-8: Custom session protocol over raw QUIC, not libp2p — **adopted** -The obvious alternative to a bespoke protocol is libp2p (the IPFS-lineage modular P2P stack), especially since PeerMesh convergently shares several of its ideas (key-derived peer IDs, transport abstraction, QUIC). Rejected as the foundation, for recorded reasons: +The obvious alternative to a bespoke protocol is libp2p (the IPFS-lineage modular P2P stack), especially since Stromo convergently shares several of its ideas (key-derived peer IDs, transport abstraction, QUIC). Rejected as the foundation, for recorded reasons: -1. **No help where help is needed.** PeerMesh's hard problems are Apple-platform-specific — Bonjour over peer-to-peer Wi-Fi, `includePeerToPeer`, Local Network permission UX, AWDL lifecycle, future Wi-Fi Aware. No libp2p transport addresses any of it; adopting libp2p means building that custom transport anyway, inside someone else's abstraction stack. -2. **Wrong-scale machinery.** DHT discovery, NAT traversal, relays, and gossipsub target internet-scale adversarial networks; PeerMesh sessions are 2–32 co-located consenting devices (§1 scope). The machinery costs dependency surface, binary size, and audit scope without serving a requirement. On QUIC, libp2p's Noise + muxer + multistream-select layering is additionally redundant — QUIC provides encryption, muxing, and streams natively. +1. **No help where help is needed.** Stromo's hard problems are Apple-platform-specific — Bonjour over peer-to-peer Wi-Fi, `includePeerToPeer`, Local Network permission UX, AWDL lifecycle, future Wi-Fi Aware. No libp2p transport addresses any of it; adopting libp2p means building that custom transport anyway, inside someone else's abstraction stack. +2. **Wrong-scale machinery.** DHT discovery, NAT traversal, relays, and gossipsub target internet-scale adversarial networks; Stromo sessions are 2–32 co-located consenting devices (§1 scope). The machinery costs dependency surface, binary size, and audit scope without serving a requirement. On QUIC, libp2p's Noise + muxer + multistream-select layering is additionally redundant — QUIC provides encryption, muxing, and streams natively. 3. **swift-libp2p is not production-viable** (feasibility study §3.1): experimental by its own declaration, pre-1.0, minimal maintainer base, TCP/WebSocket-centric — the Network.framework QUIC transport would still be ours to write. 4. **The product layer doesn't exist in libp2p:** invitation/consent semantics, rosters, `Progress` resource transfer, and `MPCCompat` are application protocol either way. -5. **Interop parity:** cross-platform reach via libp2p requires a mutually supported transport, which today means infrastructure LAN — where PeerMesh's QUIC + FlatBuffers wire protocol is equally portable (quiche/Cronet, FlatBuffers Kotlin). The infrastructure-less cross-platform path is blocked by radio availability (C-1/C-2, Wi-Fi Aware version gap), not by protocol choice. +5. **Interop parity:** cross-platform reach via libp2p requires a mutually supported transport, which today means infrastructure LAN — where Stromo's QUIC + FlatBuffers wire protocol is equally portable (quiche/Cronet, FlatBuffers Kotlin). The infrastructure-less cross-platform path is blocked by radio availability (C-1/C-2, Wi-Fi Aware version gap), not by protocol choice. **Adopted from libp2p instead:** the PeerID identity encoding (multihash of the encoded public key, CIDv1 text representation) SHALL replace the ad-hoc SHA-256-of-raw-key format before the wire protocol freezes — near-zero cost now, and it keeps a future libp2p bridge (post-1.0 internet reach via relays) identity-compatible. Revisit trigger: if post-1.0 scope expands to internet-wide P2P (NAT traversal/relays), evaluate bridging to libp2p protocols rather than reinventing that tier. @@ -291,11 +291,11 @@ The obvious alternative to a bespoke protocol is libp2p (the IPFS-lineage modula ```mermaid flowchart TD - UI["PeerMeshUI
SwiftUI picker, consent sheet
[FR-23]"] + UI["StromoUI
SwiftUI picker, consent sheet
[FR-23]"] COMPAT["MPCCompat
MCSession-shaped shim, NSStream bridge
[FR-24]"] - API["PeerMesh public API
PeerSession · Advertiser · Browser ·
Message · ResourceTransfer · PeerStream"] + API["Stromo public API
PeerSession · Advertiser · Browser ·
Message · ResourceTransfer · PeerStream"] - subgraph PROTO["PeerMeshProtocol — sans-I/O engine [DD-6] (no sockets, no clocks, no async)"] + subgraph PROTO["StromoProtocol — sans-I/O engine [DD-6] (no sockets, no clocks, no async)"] ENGINE["ProtocolEngine
handle(Input) → [Effect]
invitation · roster · tie-break ·
timers · topology [DD-3]"] SIGNAL["Signal model +
FlatBuffers codec + verifier [DD-5]"] end @@ -466,7 +466,7 @@ each self-identified by its first byte (the stream tag): |---|---|---| | **App messages** — video frames, game state, anything via `send(_:delivery:)`, up to 1 MiB, all delivery modes | the sender's **message channel**: one long-lived stream per direction, framed `StreamHeader` + payload units | `0x02` | | **App messages over 1 MiB** (FR-15 allows 16 MB) | a **dedicated stream** per message, retired when spent | `0x01` | -| **PeerMesh's own protocol** — `PeerHello`, invitations, accept/decline, roster gossip, keepalives, transfer offers, stream-open announcements | the **control stream**: one bidirectional stream, dialer-opened first, total order (DD-5) | `0x00` | +| **Stromo's own protocol** — `PeerHello`, invitations, accept/decline, roster gossip, keepalives, transfer offers, stream-open announcements | the **control stream**: one bidirectional stream, dialer-opened first, total order (DD-5) | `0x00` | | **File transfers** (`sendResource`, FR-17) | offer/accept signals on the control stream; the bytes on a **dedicated stream** per transfer (disk-to-disk, own flow control) | `0x00` + `0x01` | | **App byte streams** (`openStream`, FR-18) | a **dedicated stream** each, duplex, app-controlled lifetime | `0x01` | @@ -512,7 +512,7 @@ directions. sequenceDiagram participant B as Inviter — QUIC/TLS client participant A as Advertiser — QUIC/TLS server (NWListener) - B->>A: QUIC ClientHello (ALPN peermesh/1) + B->>A: QUIC ClientHello (ALPN Stromo/1) A->>B: server cert: self-signed P-256 leaf + CertificateRequest (mTLS) B->>A: client cert: self-signed P-256 leaf Note over B,A: Each side runs the SAME verify block — PKI ignored,
leaf DER → TrustEvaluator with the session TrustPolicy
(.automatic TOFU / .pairingCode / .pinned) diff --git a/flake.nix b/flake.nix index 0780057..60e7a39 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "PeerMesh — dev shell (auxiliary tooling; Swift toolchain comes from Xcode)"; + description = "Stromo — dev shell (auxiliary tooling; Swift toolchain comes from Xcode)"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; @@ -17,7 +17,7 @@ # `google/flatbuffers` runtime pinned `exact:` in Package.swift — the # generated code and the runtime that reads it are one unit; bump both # together. Regenerate the committed sources with: - # flatc --swift -o Sources/PeerMeshProtocol/Generated Schemas/*.fbs + # flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs pkgs.flatbuffers ]; }; From 0443a1d1a55a4eba5b0ea44b91205e2fc34c539c Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Tue, 28 Jul 2026 00:19:38 -0700 Subject: [PATCH 2/3] Fix the rename: Stromo -> Stormo, and finish the mechanical moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rebased rename carried a transposition typo ("Stromo") through every identifier, and left the Sources/Tests directories and the CLI file names unrenamed (SPM infers target paths from target names, so the package could not build). Also: - STORMO_NO_P2P restores the all-caps env-var convention (sed had produced Stromo_NO_P2P); CI matches. - stormo-cli lowercased (binary-name convention). - Reverse-DNS strings lowercased: dev.securityunion.stormo.{identity, tls,quic} — keychain tags and dispatch-queue labels. - signal.fbs regen comment re-fixed: pointed at a pre-move Generated path already stale on main. - Generated FlatBuffers sources verified byte-identical to a fresh regen with the pinned flatc 25.2.10 (namespace Stormo.Wire). Verified: swift build clean; 72/72 tests in 17 suites; Scripts/e2e-cli.sh cross-process exchange + heartbeat death detection PASS. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 10 +- CLAUDE.md | 30 ++--- Package.swift | 42 +++--- README.md | 18 +-- Schemas/common.fbs | 4 +- Schemas/peer_hello.fbs | 8 +- Schemas/signal.fbs | 6 +- Schemas/stream_header.fbs | 6 +- Scripts/e2e-cli.sh | 4 +- Sources/MPCCompat/CompatCore.swift | 8 +- Sources/MPCCompat/MultipeerSession.swift | 20 +-- Sources/MPCCompat/NearbyService.swift | 2 +- Sources/PeerMesh/Reexport.swift | 5 - .../{PeerMesh => Stormo}/Configuration.swift | 4 +- Sources/{PeerMesh => Stormo}/Events.swift | 2 +- .../{PeerMesh => Stormo}/PeerIdentity.swift | 0 .../{PeerMesh => Stormo}/PeerSession.swift | 28 ++-- .../{PeerMesh => Stormo}/PeerTransport.swift | 2 +- Sources/Stormo/Reexport.swift | 5 + .../Security/IdentityCertificate.swift | 2 +- .../Security/IdentityStore.swift | 4 +- .../Security/TrustEvaluator.swift | 0 .../Transports/QUICByteStream.swift | 0 .../Transports/QUICConnection.swift | 2 +- .../Transports/QUICSupport.swift | 30 ++--- .../Transports/QUICTLS.swift | 10 +- .../Transports/QUICTransport.swift | 16 +-- .../StormoCLI.swift} | 18 +-- .../Generated/common_generated.swift | 6 +- .../Generated/peer_hello_generated.swift | 18 +-- .../Generated/signal_generated.swift | 126 +++++++++--------- .../Generated/stream_header_generated.swift | 38 +++--- .../LibP2PIdentity.swift | 6 +- .../Locked.swift | 2 +- .../PeerID.swift | 0 .../ProtocolEngine.swift | 0 .../ProtocolVersion.swift | 2 +- .../Signal.swift | 52 ++++---- .../SignalCodec.swift | 4 +- .../InMemoryTransport.swift | 12 +- .../PeerBrowserView.swift | 4 +- Tests/MPCCompatTests/MPCCompatE2ETests.swift | 6 +- Tests/MPCCompatTests/MPCCompatTests.swift | 4 +- .../ProtocolEngineTests.swift | 2 +- .../ProtocolVersionTests.swift | 2 +- .../SignalCodecTests.swift | 8 +- .../DisconnectionTests.swift | 4 +- .../EndToEndSessionTests.swift | 16 +-- .../IdentitySecurityTests.swift | 4 +- .../MeshHarness.swift | 2 +- .../MeshReliabilityTests.swift | 4 +- .../PeerHelloCodecTests.swift | 4 +- .../QUICSmokeTests.swift | 4 +- .../QUICStreamChurnTests.swift | 2 +- .../QUICWirePrimitiveTests.swift | 2 +- .../StormoTests.swift} | 12 +- docs/IMPLEMENTATION-PLAN.md | 16 +-- docs/design-mpc-successor.md | 42 +++--- flake.nix | 4 +- 59 files changed, 347 insertions(+), 347 deletions(-) delete mode 100644 Sources/PeerMesh/Reexport.swift rename Sources/{PeerMesh => Stormo}/Configuration.swift (93%) rename Sources/{PeerMesh => Stormo}/Events.swift (97%) rename Sources/{PeerMesh => Stormo}/PeerIdentity.swift (100%) rename Sources/{PeerMesh => Stormo}/PeerSession.swift (97%) rename Sources/{PeerMesh => Stormo}/PeerTransport.swift (99%) create mode 100644 Sources/Stormo/Reexport.swift rename Sources/{PeerMesh => Stormo}/Security/IdentityCertificate.swift (99%) rename Sources/{PeerMesh => Stormo}/Security/IdentityStore.swift (98%) rename Sources/{PeerMesh => Stormo}/Security/TrustEvaluator.swift (100%) rename Sources/{PeerMesh => Stormo}/Transports/QUICByteStream.swift (100%) rename Sources/{PeerMesh => Stormo}/Transports/QUICConnection.swift (99%) rename Sources/{PeerMesh => Stormo}/Transports/QUICSupport.swift (95%) rename Sources/{PeerMesh => Stormo}/Transports/QUICTLS.swift (97%) rename Sources/{PeerMesh => Stormo}/Transports/QUICTransport.swift (98%) rename Sources/{PeerMeshCLI/PeerMeshCLI.swift => StormoCLI/StormoCLI.swift} (94%) rename Sources/{PeerMeshProtocol => StormoProtocol}/Generated/common_generated.swift (89%) rename Sources/{PeerMeshProtocol => StormoProtocol}/Generated/peer_hello_generated.swift (83%) rename Sources/{PeerMeshProtocol => StormoProtocol}/Generated/signal_generated.swift (79%) rename Sources/{PeerMeshProtocol => StormoProtocol}/Generated/stream_header_generated.swift (74%) rename Sources/{PeerMeshProtocol => StormoProtocol}/LibP2PIdentity.swift (96%) rename Sources/{PeerMeshProtocol => StormoProtocol}/Locked.swift (95%) rename Sources/{PeerMeshProtocol => StormoProtocol}/PeerID.swift (100%) rename Sources/{PeerMeshProtocol => StormoProtocol}/ProtocolEngine.swift (100%) rename Sources/{PeerMeshProtocol => StormoProtocol}/ProtocolVersion.swift (95%) rename Sources/{PeerMeshProtocol => StormoProtocol}/Signal.swift (88%) rename Sources/{PeerMeshProtocol => StormoProtocol}/SignalCodec.swift (95%) rename Sources/{PeerMeshTestKit => StormoTestKit}/InMemoryTransport.swift (97%) rename Sources/{PeerMeshUI => StormoUI}/PeerBrowserView.swift (98%) rename Tests/{PeerMeshProtocolTests => StormoProtocolTests}/ProtocolEngineTests.swift (99%) rename Tests/{PeerMeshProtocolTests => StormoProtocolTests}/ProtocolVersionTests.swift (97%) rename Tests/{PeerMeshProtocolTests => StormoProtocolTests}/SignalCodecTests.swift (91%) rename Tests/{PeerMeshTests => StormoTests}/DisconnectionTests.swift (99%) rename Tests/{PeerMeshTests => StormoTests}/EndToEndSessionTests.swift (97%) rename Tests/{PeerMeshTests => StormoTests}/IdentitySecurityTests.swift (98%) rename Tests/{PeerMeshTests => StormoTests}/MeshHarness.swift (99%) rename Tests/{PeerMeshTests => StormoTests}/MeshReliabilityTests.swift (99%) rename Tests/{PeerMeshTests => StormoTests}/PeerHelloCodecTests.swift (98%) rename Tests/{PeerMeshTests => StormoTests}/QUICSmokeTests.swift (98%) rename Tests/{PeerMeshTests => StormoTests}/QUICStreamChurnTests.swift (99%) rename Tests/{PeerMeshTests => StormoTests}/QUICWirePrimitiveTests.swift (98%) rename Tests/{PeerMeshTests/PeerMeshTests.swift => StormoTests/StormoTests.swift} (94%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f172e7e..936d50d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: QUIC_DEBUG_LOG: /tmp/quic-debug.log # Same-machine runs need p2p off (failure mode 10). Peer-to-peer # Wi-Fi coverage is tier 3 (hardware lab) by design. - Stromo_NO_P2P: "1" + STORMO_NO_P2P: "1" - name: Diagnostics on failure (QUIC log + crash reports) if: failure() run: | @@ -70,7 +70,7 @@ jobs: run: ./Scripts/e2e-cli.sh env: QUIC_DEBUG: "1" - Stromo_NO_P2P: "1" + STORMO_NO_P2P: "1" # Same test bundle per Apple destination. iOS Simulator runs on PRs (the # availability-floor check); Mac Catalyst only on pushes to main. @@ -106,7 +106,7 @@ jobs: # xcbeautify streams progress live — a hung xcodebuild shows where # it stalled instead of buffering silently until the timeout. xcodebuild test \ - -scheme Stromo-Package \ + -scheme Stormo-Package \ -destination '${{ steps.dest.outputs.destination }}' \ | xcbeautify --renderer github-actions @@ -122,7 +122,7 @@ jobs: run: | set -o pipefail xcodebuild test \ - -scheme Stromo-Package \ + -scheme Stormo-Package \ -destination 'platform=macOS,variant=Mac Catalyst' \ | xcbeautify --renderer github-actions @@ -145,7 +145,7 @@ jobs: - name: Regenerate and diff run: | /tmp/flatc-bin/flatc --swift -o /tmp/generated Schemas/*.fbs - diff -r /tmp/generated Sources/StromoProtocol/Generated \ + diff -r /tmp/generated Sources/StormoProtocol/Generated \ || { echo '::error::Generated sources drifted from schemas (DD-5 rule 4)'; exit 1; } # TODO(S-6): nightly workflow_dispatch job — stream-churn benchmark and 32-peer diff --git a/CLAUDE.md b/CLAUDE.md index 3a65767..305fe1a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ -# CLAUDE.md — Stromo contributor guide (AI & human) +# CLAUDE.md — Stormo contributor guide (AI & human) -Stromo is an open-source replacement for Apple's deprecated +Stormo is an open-source replacement for Apple's deprecated MultipeerConnectivity, built on Network.framework + QUIC. The codebase is split by the sans-I/O discipline (DD-6): a pure, deterministic protocol engine that decides, and thin async drivers that move bytes. Read @@ -12,29 +12,29 @@ the hard-won platform-findings log and the source of the failure modes below. | Module | Kind | What lives here | |---|---|---| -| **StromoProtocol** | library (sans-I/O) | `ProtocolEngine` state machine, `Signal` model + `SignalCodec` (FlatBuffers verifier), `PeerID`/`LibP2PIdentity`, `Delivery`/`Recipients`/`StromoError`, `Locked`. Only dependency: FlatBuffers. | -| **Stromo** | library (runtime) | `PeerSession` actor (effect executor), `QUICTransport` + QUIC driver, Security (identity, certificate, keychain stores, `TrustEvaluator`), public event/config types. Re-exports StromoProtocol so apps write only `import Stromo`. | +| **StormoProtocol** | library (sans-I/O) | `ProtocolEngine` state machine, `Signal` model + `SignalCodec` (FlatBuffers verifier), `PeerID`/`LibP2PIdentity`, `Delivery`/`Recipients`/`StormoError`, `Locked`. Only dependency: FlatBuffers. | +| **Stormo** | library (runtime) | `PeerSession` actor (effect executor), `QUICTransport` + QUIC driver, Security (identity, certificate, keychain stores, `TrustEvaluator`), public event/config types. Re-exports StormoProtocol so apps write only `import Stormo`. | | **MPCCompat** | library | Near-drop-in `MCSession`/`MCPeerID`/advertiser/browser analogs over `PeerSession` (FR-24). | -| **StromoUI** | library | SwiftUI peer picker + invitation consent (FR-23). **Experimental preview, API unstable.** | -| **StromoTestKit** | library | `InMemoryTransport` (+ mesh sim) and `ReorderingTransport` — CI without radios (QA-8). | -| **StromoCLI** (`Stromo-cli`) | executable | Diagnostic advertise/browse/host/join over real Bonjour+QUIC between processes. | +| **StormoUI** | library | SwiftUI peer picker + invitation consent (FR-23). **Experimental preview, API unstable.** | +| **StormoTestKit** | library | `InMemoryTransport` (+ mesh sim) and `ReorderingTransport` — CI without radios (QA-8). | +| **StormoCLI** (`stormo-cli`) | executable | Diagnostic advertise/browse/host/join over real Bonjour+QUIC between processes. | ## Build & test - `swift test` — tiers 1 & 2 (engine + loopback QUIC), fast macOS loop. 54 tests today. - `./Scripts/e2e-cli.sh` — cross-process Bonjour+QUIC exchange (host + joiner as separate processes). Caught the FIN and inbound-retention bugs. -- `xcodebuild test -scheme Stromo-Package -destination ''` for other Apple targets (same suite, N destinations): +- `xcodebuild test -scheme Stormo-Package -destination ''` for other Apple targets (same suite, N destinations): - iOS simulator: `-destination 'platform=iOS Simulator,id='` (resolve a UDID via `xcrun simctl list devices available`). - Mac Catalyst: `-destination 'platform=macOS,variant=Mac Catalyst'`. ### Environment flags - `QUIC_DEBUG=1` — enable the QUIC driver's diagnostic log to stdout. - `QUIC_DEBUG_LOG=` — also append that log to a file (survives sandboxing). -- `Stromo_NO_P2P=1` — disable `includePeerToPeer`. Required for **in-process** entitled E2E tests: `includePeerToPeer` breaks same-machine self-dials (failure mode 10). Never set it for real cross-device runs. +- `STORMO_NO_P2P=1` — disable `includePeerToPeer`. Required for **in-process** entitled E2E tests: `includePeerToPeer` breaks same-machine self-dials (failure mode 10). Never set it for real cross-device runs. ### FlatBuffers regeneration (DD-5 rule 4) ``` -flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs +flatc --swift -o Sources/StormoProtocol/Generated Schemas/*.fbs ``` Generated sources are committed and CI fails on drift. **Exact-version pin rule:** the `flatc` in `flake.nix` MUST equal the `google/flatbuffers` runtime @@ -43,18 +43,18 @@ the runtime that reads it are one unit. Bump both together. ## Hard architectural rules -- **StromoProtocol stays sans-I/O.** No sockets, no clocks, no async, no +- **StormoProtocol stays sans-I/O.** No sockets, no clocks, no async, no Foundation I/O. Its only dependency is FlatBuffers. Time enters the engine as an `Input`, never from a clock. (`Locked`/`NSLock` is a synchronization primitive, not I/O, and is allowed.) -- **Engine changes land with tier-1 tests first** (`Tests/StromoProtocolTests`). +- **Engine changes land with tier-1 tests first** (`Tests/StormoProtocolTests`). The engine is `handle(Input) -> [Effect]`, pure and deterministic — assert on the effect list. - **FlatBuffers schema evolution:** field ids are append-only; never renumber, retype, or remove (only `(deprecated)`); no new `(required)` after 1.0; enum/union values append-only with `UNKNOWN = 0`. Every inbound buffer goes through the verifier (`getCheckedRoot`, hard caps). Generated code is - committed. Do NOT hand-edit `Schemas/` or `Sources/StromoProtocol/Generated/`. + committed. Do NOT hand-edit `Schemas/` or `Sources/StormoProtocol/Generated/`. - **Platform floor: iOS 15 / macOS 12** (QUIC floor). No `Duration` (use `TimeInterval`). RFC 9221 datagrams are iOS 16/macOS 13, below the floor — see failure mode 1 and the `StreamKind.Datagram` channel mapping. @@ -106,7 +106,7 @@ code that guards them without understanding why it exists. interface assertion MUST be validated in the mesh-hardware spike before radio release. 10. **`includePeerToPeer` breaks same-machine self-dials.** Use - `Stromo_NO_P2P=1` for in-process tests; CI sets it for every + `STORMO_NO_P2P=1` for in-process tests; CI sets it for every same-machine job. Note `ProcessInfo.environment` caches at first access; the driver reads this flag via `getenv` so a test's `setenv` actually lands. @@ -153,7 +153,7 @@ In-code TODOs reference these by name: `// TODO(ledger-name): one line`. - **churn-benchmark** — S-6 formal stream-churn benchmark (nightly CI stub in `ci.yml`). - **pairing-code** — `.pairingCode` transcript binding (S-4, DD-2). - **compat-nsstream-bridge** — `MPCCompat.startStream` `NSStream` bridge over `PeerByteStream`. -- **ui-completion** — StromoUI beyond the current skeleton. +- **ui-completion** — StormoUI beyond the current skeleton. - **mesh-join** — join via endpoint exchange for gossiped roster members (roster names peers we haven't discovered). - **send-ack** — send-acknowledgement API (`PeerSession.send` returns before transport handoff). - **mesh-hardware** — S-2 mesh-ceiling and S-5 backgrounding hardware spikes. diff --git a/Package.swift b/Package.swift index 1a2dd74..dee77bc 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,7 @@ import PackageDescription let package = Package( - name: "Stromo", + name: "Stormo", platforms: [ .iOS(.v15), .macOS(.v12), @@ -12,21 +12,21 @@ let package = Package( products: [ // Sans-I/O protocol engine: deterministic state machine + signal model, // fully testable without any transport (DD-6). - .library(name: "StromoProtocol", targets: ["StromoProtocol"]), + .library(name: "StormoProtocol", targets: ["StormoProtocol"]), // Modern core: discovery, sessions, messaging, streams, resources over QUIC (DD-1). - .library(name: "Stromo", targets: ["Stromo"]), + .library(name: "Stormo", targets: ["Stormo"]), // Near-drop-in migration surface for MultipeerConnectivity codebases (FR-24). .library(name: "MPCCompat", targets: ["MPCCompat"]), // SwiftUI peer picker and invitation consent components (FR-23). - .library(name: "StromoUI", targets: ["StromoUI"]), + .library(name: "StormoUI", targets: ["StormoUI"]), // In-memory transport and mesh simulation for CI without radios (QA-8). - .library(name: "StromoTestKit", targets: ["StromoTestKit"]), + .library(name: "StormoTestKit", targets: ["StormoTestKit"]), // Diagnostic CLI: advertise/browse/chat between real processes over // Bonjour + QUIC (the production discovery path). - // Named "Stromo-cli", not "Stromo": a product differing from the - // Stromo library only by case collides on case-insensitive APFS when + // Named "stormo-cli", not "Stormo": a product differing from the + // Stormo library only by case collides on case-insensitive APFS when // xcodebuild materializes package products as modules. - .executable(name: "Stromo-cli", targets: ["StromoCLI"]), + .executable(name: "stormo-cli", targets: ["StormoCLI"]), ], dependencies: [ // Signaling plane serialization (DD-5). Pinned EXACTLY to match the @@ -35,42 +35,42 @@ let package = Package( // doesn't import). Bump both together (DD-5 rule 4). .package(url: "https://github.com/google/flatbuffers.git", exact: "25.2.10"), // Pure-Swift X.509 for self-signed identity certificates (DD-2). Used by - // the Stromo target only — StromoProtocol stays FlatBuffers-only. + // the Stormo target only — StormoProtocol stays FlatBuffers-only. .package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.0"), ], targets: [ // Sans-I/O (DD-6): the ONLY dependency is FlatBuffers (pure CPU). // No Network.framework, no clocks, no async — keep it that way. .target( - name: "StromoProtocol", + name: "StormoProtocol", dependencies: [ .product(name: "FlatBuffers", package: "flatbuffers") ] ), // Runtime shell: executes engine Effects against real transports. .target( - name: "Stromo", + name: "Stormo", dependencies: [ - "StromoProtocol", + "StormoProtocol", .product(name: "X509", package: "swift-certificates"), // The QUIC driver serializes the DD-7 `StreamHeader` prologue of // every dedicated stream (the generated FlatBuffers type lives in - // StromoProtocol). Moving bytes onto the wire is driver work + // StormoProtocol). Moving bytes onto the wire is driver work // (DD-6); no protocol decisions live here. .product(name: "FlatBuffers", package: "flatbuffers"), ] ), - .target(name: "MPCCompat", dependencies: ["Stromo"]), - .target(name: "StromoUI", dependencies: ["Stromo"]), - .target(name: "StromoTestKit", dependencies: ["Stromo"]), - .executableTarget(name: "StromoCLI", dependencies: ["Stromo"]), + .target(name: "MPCCompat", dependencies: ["Stormo"]), + .target(name: "StormoUI", dependencies: ["Stormo"]), + .target(name: "StormoTestKit", dependencies: ["Stormo"]), + .executableTarget(name: "StormoCLI", dependencies: ["Stormo"]), // Tier 1 (DD-6): engine tests — no transport, no radios, deterministic. - .testTarget(name: "StromoProtocolTests", dependencies: ["StromoProtocol"]), + .testTarget(name: "StormoProtocolTests", dependencies: ["StormoProtocol"]), .testTarget( - name: "StromoTests", - dependencies: ["Stromo", "StromoTestKit"] + name: "StormoTests", + dependencies: ["Stormo", "StormoTestKit"] ), - .testTarget(name: "MPCCompatTests", dependencies: ["MPCCompat", "StromoTestKit"]), + .testTarget(name: "MPCCompatTests", dependencies: ["MPCCompat", "StormoTestKit"]), ], swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md index 47a908a..c7ba19f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Stromo +# Stormo **The open-source replacement for Apple's deprecated MultipeerConnectivity framework** — peer-to-peer sessions over Network.framework + QUIC. @@ -10,7 +10,7 @@ camera app streaming live preview at 33 fps device-to-device with no infrastructure. ```swift -import Stromo +import Stormo let session = PeerSession(name: "Dario's iPhone", service: "_myapp._udp") try await session.startAdvertising() @@ -35,7 +35,7 @@ typealias file makes it a near-drop-in swap — see ## Install ```swift -.package(url: "https://github.com/security-union/Stromo.git", from: "1.0.0") +.package(url: "https://github.com/security-union/Stormo.git", from: "1.0.0") ``` Platforms: iOS 15+ · macOS 12+ · tvOS 15+ · visionOS 1+. Apps need @@ -49,29 +49,29 @@ The recipe below is exactly how [Remote Shutter](https://github.com/security-uni (a shipping App Store camera app) migrated off MPC — the app diff was a few imports, one new file, and its peer-ID cache. -**1. Add the package** to your app target, linking the `Stromo` and +**1. Add the package** to your app target, linking the `Stormo` and `MPCCompat` products: ```swift -.package(url: "https://github.com/security-union/Stromo.git", from: "1.0.0") +.package(url: "https://github.com/security-union/Stormo.git", from: "1.0.0") ``` **2. Check your `Info.plist`.** Same requirements as MPC: `NSLocalNetworkUsageDescription`, and your service type under -`NSBonjourServices` — Stromo uses the `_yourservice._udp` variant (Apple's +`NSBonjourServices` — Stormo uses the `_yourservice._udp` variant (Apple's guidance for MPC apps was to declare both `._tcp` and `._udp`, so most apps already have it). **3. Add one app-local typealias file** — this is the key to a thin diff. Your -app keeps MPC's type names; the implementations come from MPCCompat. Stromo +app keeps MPC's type names; the implementations come from MPCCompat. Stormo deliberately does not publish `MC`-prefixed names, so the mapping lives in *your* app: ```swift -// MultipeerCompatAliases.swift — the entire MPC → Stromo mapping. +// MultipeerCompatAliases.swift — the entire MPC → Stormo mapping. import MPCCompat -import Stromo +import Stormo public typealias MCPeerID = PeerID public typealias MCSession = MultipeerSession diff --git a/Schemas/common.fbs b/Schemas/common.fbs index e480e91..8e28cda 100644 --- a/Schemas/common.fbs +++ b/Schemas/common.fbs @@ -1,9 +1,9 @@ // Shared wire scalars (DD-5), included by signal.fbs and stream_header.fbs. // // Codegen (flatc version pinned in flake.nix, matched exactly by Package.swift): -// flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs +// flatc --swift -o Sources/StormoProtocol/Generated Schemas/*.fbs -namespace Stromo.Wire; +namespace Stormo.Wire; /// A 128-bit transfer identifier: the two 8-byte halves of a `uuid_t`, in /// memory order. A fixed-size struct — no vector length word, and the diff --git a/Schemas/peer_hello.fbs b/Schemas/peer_hello.fbs index 1ba4c40..3575bac 100644 --- a/Schemas/peer_hello.fbs +++ b/Schemas/peer_hello.fbs @@ -1,4 +1,4 @@ -// Stromo transport-level identity bootstrap (PeerHello). +// Stormo transport-level identity bootstrap (PeerHello). // // The first frame in each direction on the control stream, sent immediately // after the QUIC/mTLS handshake. TLS authenticates the peer's KEY; PeerHello @@ -10,15 +10,15 @@ // this table — it is how incompatible peers discover the mismatch and tell // the user to upgrade, instead of dying in an opaque handshake failure. // Breaking changes to this file are forbidden, forever: append-only field -// ids with no pre-1.0 exceptions. The QUIC ALPN ("Stromo/1") is frozen +// ids with no pre-1.0 exceptions. The QUIC ALPN ("Stormo/1") is frozen // for the same reason — bumping it would fail the handshake before hello. // // Codegen (flatc version pinned in flake.nix, matched exactly by Package.swift): -// flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs +// flatc --swift -o Sources/StormoProtocol/Generated Schemas/*.fbs include "signal.fbs"; -namespace Stromo.Wire; +namespace Stormo.Wire; table PeerHello { /// The sender's identity. key_hash must match the TLS certificate's key diff --git a/Schemas/signal.fbs b/Schemas/signal.fbs index 754e773..9cb96d6 100644 --- a/Schemas/signal.fbs +++ b/Schemas/signal.fbs @@ -1,4 +1,4 @@ -// Stromo signaling schema (DD-5: disciplined FlatBuffers). +// Stormo signaling schema (DD-5: disciplined FlatBuffers). // // DISCIPLINE (normative — see design doc DD-5; enforced by CI): // * Field ids are append-only. Never renumber, retype, or remove a field — @@ -12,11 +12,11 @@ // (64 KB control-message size) — see SignalCodec. // // Codegen (flatc version pinned in flake.nix): -// flatc --swift -o Sources/Stromo/Signaling/Generated Schemas/signal.fbs +// flatc --swift -o Sources/StormoProtocol/Generated Schemas/*.fbs include "common.fbs"; -namespace Stromo.Wire; +namespace Stormo.Wire; /// A session member, as carried in rosters (FR-13). table PeerInfo { diff --git a/Schemas/stream_header.fbs b/Schemas/stream_header.fbs index 33e98f5..b9db72e 100644 --- a/Schemas/stream_header.fbs +++ b/Schemas/stream_header.fbs @@ -1,4 +1,4 @@ -// Stromo data-plane stream header (DD-7, incl. the message-channel +// Stormo data-plane stream header (DD-7, incl. the message-channel // amendment). // // On the message channel (stream tag 0x02) every message is a size-prefixed @@ -11,11 +11,11 @@ // append-only enum values, verifier + caps on every inbound read. // // Codegen (flatc version pinned in flake.nix, matched exactly by Package.swift): -// flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs +// flatc --swift -o Sources/StormoProtocol/Generated Schemas/*.fbs include "common.fbs"; -namespace Stromo.Wire; +namespace Stormo.Wire; enum StreamKind: ubyte { Unknown = 0, diff --git a/Scripts/e2e-cli.sh b/Scripts/e2e-cli.sh index 14a3d64..52831da 100755 --- a/Scripts/e2e-cli.sh +++ b/Scripts/e2e-cli.sh @@ -1,6 +1,6 @@ #!/bin/bash # Cross-process E2E over the production Bonjour + QUIC path (tier 2.5): -# builds the Stromo CLI and runs host + joiner as SEPARATE processes. +# builds the Stormo CLI and runs host + joiner as SEPARATE processes. # Scenario 1: full discovery → invite → message → echo → departure cycle # (caught the QUIC FIN bug and the inbound-retention bug). # Scenario 2: silent peer death — kill -9 a lingering joiner; the host must @@ -16,7 +16,7 @@ HOST_PID=""; HOST2_PID=""; JOIN2_PID="" trap 'kill $HOST_PID $HOST2_PID $JOIN2_PID 2>/dev/null || true; rm -rf "$LOGDIR"' EXIT swift build > /dev/null -BIN="$(swift build --show-bin-path)/Stromo-cli" +BIN="$(swift build --show-bin-path)/stormo-cli" # GNU timeout is absent on stock macOS (incl. GitHub runners); perl is not. with_timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; } diff --git a/Sources/MPCCompat/CompatCore.swift b/Sources/MPCCompat/CompatCore.swift index d6026ae..ca23930 100644 --- a/Sources/MPCCompat/CompatCore.swift +++ b/Sources/MPCCompat/CompatCore.swift @@ -1,12 +1,12 @@ import Foundation -import Stromo +import Stormo /// The shared runtime backing an `MPCCompat` device. /// /// MultipeerConnectivity splits one logical peer across three objects — an /// `MCSession`, an `MCNearbyServiceAdvertiser`, and an `MCNearbyServiceBrowser` /// — that an app constructs with the *same* `MCPeerID` and expects to cooperate. -/// Stromo unifies discovery, advertising, invitation, membership, and +/// Stormo unifies discovery, advertising, invitation, membership, and /// messaging into a single ``PeerSession`` actor. `CompatCore` is the bridge: /// exactly one instance exists per `(PeerID, serviceType)` pair (see /// ``CompatRegistry``), owns the underlying `PeerSession`, runs long-lived pump @@ -57,7 +57,7 @@ final class CompatCore: @unchecked Sendable { self.serviceType = serviceType self.transport = transport // We only hold the app's PeerID (public-key hash + display name), never - // its private key, so the underlying session runs a Stromo identity + // its private key, so the underlying session runs a Stormo identity // keyed on the display name — persisted (FR-20), so every screen visit // and relaunch presents the SAME peer. Without persistence each core // minted a fresh key, and browsers piled up ghost entries of one @@ -72,7 +72,7 @@ final class CompatCore: @unchecked Sendable { /// MPC-style service types are bare identifiers (`"remotecam"`); /// `MCNearbyServiceAdvertiser` translated them to Bonjour registration - /// types internally. Stromo's Bonjour discovery needs the full form, so + /// types internally. Stormo's Bonjour discovery needs the full form, so /// the bridge performs the same translation: `"remotecam"` → /// `"_remotecam._udp"` (UDP — the QUIC transport). Apps must declare that /// type under `NSBonjourServices` (MPC apps already declare both `._tcp` diff --git a/Sources/MPCCompat/MultipeerSession.swift b/Sources/MPCCompat/MultipeerSession.swift index 7db56b9..a3f2bb8 100644 --- a/Sources/MPCCompat/MultipeerSession.swift +++ b/Sources/MPCCompat/MultipeerSession.swift @@ -1,5 +1,5 @@ import Foundation -import Stromo +import Stormo /// Near-drop-in replacement for `MCSession` (FR-24). /// @@ -8,7 +8,7 @@ import Stromo /// | MultipeerConnectivity | MPCCompat | /// |------------------------------|-------------------------------| /// | `MCSession` | `MultipeerSession` | -/// | `MCPeerID` | `Stromo.PeerID` | +/// | `MCPeerID` | `Stormo.PeerID` | /// | `MCSessionDelegate` | `MultipeerSessionDelegate` | /// | `MCSessionSendDataMode` | `MultipeerSession.SendDataMode` | /// | `MCSessionState` | `MultipeerSession.PeerState` | @@ -51,7 +51,7 @@ public final class MultipeerSession: @unchecked Sendable { case connected = 2 } - /// Source-compatibility analog of `MCEncryptionPreference`. Stromo is + /// Source-compatibility analog of `MCEncryptionPreference`. Stormo is /// ALWAYS encrypted (FR-19): `.optional` and `.none` are accepted for /// mechanical migration but behave as `.required`. public enum EncryptionPreference: Sendable { @@ -96,7 +96,7 @@ public final class MultipeerSession: @unchecked Sendable { /// `MCSession(peer:securityIdentity:encryptionPreference:)` analog. /// /// - `securityIdentity` is accepted for source compatibility and ignored: - /// Stromo identities are key-derived (FR-20) and managed automatically. + /// Stormo identities are key-derived (FR-20) and managed automatically. /// - `encryptionPreference` is accepted for source compatibility; traffic /// is always encrypted regardless (FR-19). /// - MCSession carries no service type (its advertiser/browser do); the @@ -105,7 +105,7 @@ public final class MultipeerSession: @unchecked Sendable { peer peerID: PeerID, securityIdentity: [Any]? = nil, encryptionPreference: EncryptionPreference = .required, - service: String = "_Stromo._udp" + service: String = "_Stormo._udp" ) { self.init(myPeerID: peerID, service: service, transport: nil) } @@ -114,7 +114,7 @@ public final class MultipeerSession: @unchecked Sendable { /// through the shared ``CompatCore`` to the underlying `PeerSession`. convenience init( peer peerID: PeerID, - service: String = "_Stromo._udp", + service: String = "_Stormo._udp", transport: any PeerTransport ) { self.init(myPeerID: peerID, service: service, transport: transport) @@ -166,9 +166,9 @@ public final class MultipeerSession: @unchecked Sendable { /// (no route exists); transient delivery failures surface on the delegate /// path, as they do in MCSession. public func send(_ data: Data, toPeers peerIDs: [PeerID], with mode: SendDataMode) throws { - guard let core else { throw StromoError.peerUnreachable(myPeerID) } + guard let core else { throw StormoError.peerUnreachable(myPeerID) } // MPC's .unreliable allowed large payloads (raw UDP + IP - // fragmentation); Stromo datagrams are honest about the MTU and + // fragmentation); Stormo datagrams are honest about the MTU and // refuse them. Oversized unreliable sends degrade to .reliable — // unordered, guaranteed: a superset of MPC's "may be dropped" // promise, and the closest semantics that still deliver. @@ -195,7 +195,7 @@ public final class MultipeerSession: @unchecked Sendable { withCompletionHandler completionHandler: ((Error?) -> Void)? = nil ) -> Progress? { guard let core else { - completionHandler?(StromoError.peerUnreachable(peerID)) + completionHandler?(StormoError.peerUnreachable(peerID)) return nil } return core.sendResource( @@ -208,7 +208,7 @@ public final class MultipeerSession: @unchecked Sendable { /// Modern callers should use `PeerSession.openStream` (byte streams /// themselves are implemented; only the `NSStream` shim is not). public func startStream(withName streamName: String, toPeer peerID: PeerID) throws -> OutputStream { - throw StromoError.unimplemented("MultipeerSession.startStream") + throw StormoError.unimplemented("MultipeerSession.startStream") } public func disconnect() { diff --git a/Sources/MPCCompat/NearbyService.swift b/Sources/MPCCompat/NearbyService.swift index 08fdadd..dcd537a 100644 --- a/Sources/MPCCompat/NearbyService.swift +++ b/Sources/MPCCompat/NearbyService.swift @@ -1,5 +1,5 @@ import Foundation -import Stromo +import Stormo /// Near-drop-in replacement for `MCNearbyServiceAdvertiser` (FR-24). /// diff --git a/Sources/PeerMesh/Reexport.swift b/Sources/PeerMesh/Reexport.swift deleted file mode 100644 index 1e018ac..0000000 --- a/Sources/PeerMesh/Reexport.swift +++ /dev/null @@ -1,5 +0,0 @@ -/// Stromo presents one unified namespace: the sans-I/O protocol layer -/// (DD-6: `ProtocolEngine`, `Signal`, `PeerID`, `Delivery`, `Recipients`, -/// `StromoError`, `SignalCodec`) is re-exported so app code only ever -/// writes `import Stromo`. -@_exported import StromoProtocol diff --git a/Sources/PeerMesh/Configuration.swift b/Sources/Stormo/Configuration.swift similarity index 93% rename from Sources/PeerMesh/Configuration.swift rename to Sources/Stormo/Configuration.swift index b207093..72bf7c4 100644 --- a/Sources/PeerMesh/Configuration.swift +++ b/Sources/Stormo/Configuration.swift @@ -52,5 +52,5 @@ public enum SessionTopology: Sendable { public static let `default`: SessionTopology = .fullMesh(maxPeers: 32) } -// Delivery, Recipients, StromoError, Signal, and ProtocolEngine live in the -// sans-I/O StromoProtocol target (DD-6) and are re-exported by this module. +// Delivery, Recipients, StormoError, Signal, and ProtocolEngine live in the +// sans-I/O StormoProtocol target (DD-6) and are re-exported by this module. diff --git a/Sources/PeerMesh/Events.swift b/Sources/Stormo/Events.swift similarity index 97% rename from Sources/PeerMesh/Events.swift rename to Sources/Stormo/Events.swift index 9c6599d..43ac505 100644 --- a/Sources/PeerMesh/Events.swift +++ b/Sources/Stormo/Events.swift @@ -82,7 +82,7 @@ public struct Invitation: Sendable { /// `.automatic` trust; it is a distinct field because an app running /// `.pinned`/`.pairingCode` verifies against this authenticated value rather /// than the self-reported identity. Part of the public FR-22 surface even - /// though Stromo's own consumers (MPCCompat/UI) do not yet read it. + /// though Stormo's own consumers (MPCCompat/UI) do not yet read it. public let inviterKeyHash: Data public let accept: @Sendable () async -> Void diff --git a/Sources/PeerMesh/PeerIdentity.swift b/Sources/Stormo/PeerIdentity.swift similarity index 100% rename from Sources/PeerMesh/PeerIdentity.swift rename to Sources/Stormo/PeerIdentity.swift diff --git a/Sources/PeerMesh/PeerSession.swift b/Sources/Stormo/PeerSession.swift similarity index 97% rename from Sources/PeerMesh/PeerSession.swift rename to Sources/Stormo/PeerSession.swift index 4875a89..b366e14 100644 --- a/Sources/PeerMesh/PeerSession.swift +++ b/Sources/Stormo/PeerSession.swift @@ -1,6 +1,6 @@ import Foundation -/// The primary Stromo API: discovery, invitation, membership, and data +/// The primary Stormo API: discovery, invitation, membership, and data /// exchange with nearby peers (design document §7). /// /// MPC-simple by default: one line to construct, encryption always on, @@ -96,7 +96,7 @@ public actor PeerSession { } /// Full-control entry point (custom identity or transport injection; the - /// latter is how StromoTestKit runs sessions without radios, QA-8). + /// latter is how StormoTestKit runs sessions without radios, QA-8). public init( identity: PeerIdentity, service: ServiceDescriptor, @@ -166,7 +166,7 @@ public actor PeerSession { knownEndpoints[peer.id] = peer return try await withCheckedThrowingContinuation { continuation in guard inviteWaiters[peer.id] == nil else { - continuation.resume(throwing: StromoError.invitationAlreadyPending(peer.id)) + continuation.resume(throwing: StormoError.invitationAlreadyPending(peer.id)) return } inviteWaiters[peer.id] = continuation @@ -183,10 +183,10 @@ public actor PeerSession { ) async throws { // FR-16: datagrams never fragment, so the cap is part of the contract. if delivery == .datagram, payload.count > Delivery.maxDatagramPayload { - throw StromoError.datagramTooLarge( + throw StormoError.datagramTooLarge( bytes: payload.count, limit: Delivery.maxDatagramPayload) } - guard !engine.members.isEmpty else { throw StromoError.peerUnreachable(identity.id) } + guard !engine.members.isEmpty else { throw StormoError.peerUnreachable(identity.id) } run(.command(.send(payload, to: recipients, delivery: delivery))) } @@ -196,7 +196,7 @@ public actor PeerSession { /// dedicated `transferChunk` stream. Cancel via ``ResourceTransfer/progress``. public func sendResource(at url: URL, to peer: PeerID, name resourceName: String? = nil) async throws -> ResourceTransfer { guard engine.members.contains(peer), let connection = connections[peer] else { - throw StromoError.peerUnreachable(peer) + throw StormoError.peerUnreachable(peer) } let attributes = try FileManager.default.attributesOfItem(atPath: url.path) let totalBytes = (attributes[.size] as? NSNumber)?.uint64Value ?? 0 @@ -225,7 +225,7 @@ public actor PeerSession { /// on the control stream (`StreamOpen`), then opens the dedicated `appStream`. public func openStream(_ label: String, with peer: PeerID) async throws -> any PeerByteStream { guard engine.members.contains(peer), let connection = connections[peer] else { - throw StromoError.peerUnreachable(peer) + throw StormoError.peerUnreachable(peer) } try await connection.sendSignal(SignalCodec.encode(.streamOpen(label: label))) return try await connection.openOutgoingStream( @@ -246,7 +246,7 @@ public actor PeerSession { run(.command(.leave)) for timer in timers.values { timer.cancel() } timers.removeAll() - inviteWaiters.forEach { $0.value.resume(throwing: StromoError.peerUnreachable($0.key)) } + inviteWaiters.forEach { $0.value.resume(throwing: StormoError.peerUnreachable($0.key)) } inviteWaiters.removeAll() } @@ -366,11 +366,11 @@ public actor PeerSession { membershipContinuation.yield(.left(peer)) case .invitationFailed(let peer, let reason): - let error: StromoError + let error: StormoError switch reason { - case .declined: error = StromoError.invitationDeclined - case .timedOut: error = StromoError.invitationTimedOut - case .connectionLost: error = StromoError.peerUnreachable(peer) + case .declined: error = StormoError.invitationDeclined + case .timedOut: error = StormoError.invitationTimedOut + case .connectionLost: error = StormoError.peerUnreachable(peer) } inviteWaiters.removeValue(forKey: peer)?.resume(throwing: error) @@ -470,7 +470,7 @@ public actor PeerSession { .appendingPathComponent("\(UUID().uuidString)-\(name)") FileManager.default.createFile(atPath: tempURL.path, contents: nil) guard let handle = try? FileHandle(forWritingTo: tempURL) else { - continuation.yield(.failed(name: name, from: from, error: StromoError.resourceTransferIncomplete)) + continuation.yield(.failed(name: name, from: from, error: StormoError.resourceTransferIncomplete)) return } @@ -494,7 +494,7 @@ public actor PeerSession { try? FileManager.default.removeItem(at: tempURL) continuation.yield(.failed( name: name, from: from, - error: failure ?? StromoError.resourceTransferIncomplete)) + error: failure ?? StormoError.resourceTransferIncomplete)) } } diff --git a/Sources/PeerMesh/PeerTransport.swift b/Sources/Stormo/PeerTransport.swift similarity index 99% rename from Sources/PeerMesh/PeerTransport.swift rename to Sources/Stormo/PeerTransport.swift index 08a9b2a..737e223 100644 --- a/Sources/PeerMesh/PeerTransport.swift +++ b/Sources/Stormo/PeerTransport.swift @@ -6,7 +6,7 @@ import Foundation /// connections, and report lifecycle. Implementations: `QUICTransport` /// (primary, DD-1), `TCPTLSTransport` (contingency, pending Spike S-1), /// `WiFiAwareTransport` (post-1.0), and `InMemoryTransport` in -/// StromoTestKit (QA-8). +/// StormoTestKit (QA-8). public protocol PeerTransport: Sendable { /// Advertise the service with metadata (Bonjour TXT record) on all /// eligible paths, including peer-to-peer Wi-Fi (FR-1, FR-3). diff --git a/Sources/Stormo/Reexport.swift b/Sources/Stormo/Reexport.swift new file mode 100644 index 0000000..3ab7524 --- /dev/null +++ b/Sources/Stormo/Reexport.swift @@ -0,0 +1,5 @@ +/// Stormo presents one unified namespace: the sans-I/O protocol layer +/// (DD-6: `ProtocolEngine`, `Signal`, `PeerID`, `Delivery`, `Recipients`, +/// `StormoError`, `SignalCodec`) is re-exported so app code only ever +/// writes `import Stormo`. +@_exported import StormoProtocol diff --git a/Sources/PeerMesh/Security/IdentityCertificate.swift b/Sources/Stormo/Security/IdentityCertificate.swift similarity index 99% rename from Sources/PeerMesh/Security/IdentityCertificate.swift rename to Sources/Stormo/Security/IdentityCertificate.swift index ebdec77..7b364bc 100644 --- a/Sources/PeerMesh/Security/IdentityCertificate.swift +++ b/Sources/Stormo/Security/IdentityCertificate.swift @@ -129,7 +129,7 @@ extension IdentityCertificate { let addKey: [String: Any] = [ kSecClass as String: kSecClassKey, kSecValueRef as String: secKey, - kSecAttrApplicationTag as String: Data("dev.securityunion.Stromo.tls".utf8), + kSecAttrApplicationTag as String: Data("dev.securityunion.stormo.tls".utf8), kSecUseDataProtectionKeychain as String: true, ] let keyStatus = SecItemAdd(addKey as CFDictionary, nil) diff --git a/Sources/PeerMesh/Security/IdentityStore.swift b/Sources/Stormo/Security/IdentityStore.swift similarity index 98% rename from Sources/PeerMesh/Security/IdentityStore.swift rename to Sources/Stormo/Security/IdentityStore.swift index aac20cc..511646d 100644 --- a/Sources/PeerMesh/Security/IdentityStore.swift +++ b/Sources/Stormo/Security/IdentityStore.swift @@ -100,14 +100,14 @@ public struct FileIdentityStore: IdentityStore { /// `SecureEnclave.isAvailable`; otherwise a software P-256 key is used /// (simulator/CI/Intel Macs without an enclave). Either way the serialized key /// material lives as a `kSecClassKey` item tagged -/// `dev.securityunion.Stromo.identity`, in the data-protection keychain. +/// `dev.securityunion.stormo.identity`, in the data-protection keychain. /// /// Note: Secure Enclave keys can never export a private-key `derRepresentation` /// (only the public key can) — that is fine; we persist the enclave key's opaque /// `dataRepresentation` reference blob and the key signs in place. public struct KeychainIdentityStore: IdentityStore { /// Application-tag prefix for identity items (per-name suffix appended). - public static let applicationTag = "dev.securityunion.Stromo.identity" + public static let applicationTag = "dev.securityunion.stormo.identity" public init() {} diff --git a/Sources/PeerMesh/Security/TrustEvaluator.swift b/Sources/Stormo/Security/TrustEvaluator.swift similarity index 100% rename from Sources/PeerMesh/Security/TrustEvaluator.swift rename to Sources/Stormo/Security/TrustEvaluator.swift diff --git a/Sources/PeerMesh/Transports/QUICByteStream.swift b/Sources/Stormo/Transports/QUICByteStream.swift similarity index 100% rename from Sources/PeerMesh/Transports/QUICByteStream.swift rename to Sources/Stormo/Transports/QUICByteStream.swift diff --git a/Sources/PeerMesh/Transports/QUICConnection.swift b/Sources/Stormo/Transports/QUICConnection.swift similarity index 99% rename from Sources/PeerMesh/Transports/QUICConnection.swift rename to Sources/Stormo/Transports/QUICConnection.swift index eda2207..bf3df7b 100644 --- a/Sources/PeerMesh/Transports/QUICConnection.swift +++ b/Sources/Stormo/Transports/QUICConnection.swift @@ -1,5 +1,5 @@ import Foundation -import StromoProtocol +import StormoProtocol #if canImport(Network) import Network diff --git a/Sources/PeerMesh/Transports/QUICSupport.swift b/Sources/Stormo/Transports/QUICSupport.swift similarity index 95% rename from Sources/PeerMesh/Transports/QUICSupport.swift rename to Sources/Stormo/Transports/QUICSupport.swift index 469190b..e8aa6ac 100644 --- a/Sources/PeerMesh/Transports/QUICSupport.swift +++ b/Sources/Stormo/Transports/QUICSupport.swift @@ -1,6 +1,6 @@ import Foundation import FlatBuffers -import StromoProtocol +import StormoProtocol #if canImport(Network) import Network @@ -39,20 +39,20 @@ enum QUICError: Error, Sendable, LocalizedError { // LocalizedError puts the actual reason in localizedDescription. var errorDescription: String? { switch self { - case .connectionClosed: return "Stromo QUIC: connection closed" - case .shortRead: return "Stromo QUIC: stream ended mid-frame" - case .identityMismatch: return "Stromo QUIC: peer key hash does not match its certificate" + case .connectionClosed: return "Stormo QUIC: connection closed" + case .shortRead: return "Stormo QUIC: stream ended mid-frame" + case .identityMismatch: return "Stormo QUIC: peer key hash does not match its certificate" case .tlsIdentityUnavailable(let reason): - return "Stromo QUIC: no local TLS identity — \(reason)" - case .malformedStreamHeader: return "Stromo QUIC: malformed stream header" - case .listenerFailed(let reason): return "Stromo QUIC: listener failed — \(reason)" + return "Stormo QUIC: no local TLS identity — \(reason)" + case .malformedStreamHeader: return "Stormo QUIC: malformed stream header" + case .listenerFailed(let reason): return "Stormo QUIC: listener failed — \(reason)" case .serviceResolutionFailed: - return "Stromo QUIC: Bonjour service endpoint did not resolve" + return "Stormo QUIC: Bonjour service endpoint did not resolve" case .protocolVersionMismatch(let local, let remote): let hint = remote.major > local.major ? "this device needs an app upgrade" : "the peer needs an app upgrade" - return "Stromo QUIC: protocol version mismatch — local \(local), peer \(remote); \(hint)" + return "Stormo QUIC: protocol version mismatch — local \(local), peer \(remote); \(hint)" } } } @@ -366,7 +366,7 @@ enum PeerHello { let name = fbb.create(string: peer.displayName) let info = WirePeerInfo.createPeerInfo( &fbb, keyHashVectorOffset: keyHash, displayNameOffset: name) - let root = Stromo_Wire_PeerHello.createPeerHello( + let root = Stormo_Wire_PeerHello.createPeerHello( &fbb, peerOffset: info, protocolMajor: version.major, @@ -379,7 +379,7 @@ enum PeerHello { static func decode(_ data: Data) -> Decoded? { var buffer = ByteBuffer(data: data) guard - let root: Stromo_Wire_PeerHello = try? getCheckedRoot( + let root: Stormo_Wire_PeerHello = try? getCheckedRoot( byteBuffer: &buffer, options: VerifierOptions(maxDepth: 16, maxTableCount: 64, maxApparentSize: 1 << 16)) else { return nil } @@ -415,7 +415,7 @@ enum QUICStreamHeaderCodec { static func encode(_ info: StreamHeaderInfo) -> Data { var fbb = FlatBufferBuilder(initialSize: 128) let labelOffset = info.label.map { fbb.create(string: $0) } ?? Offset() - let root = Stromo_Wire_StreamHeader.createStreamHeader( + let root = Stormo_Wire_StreamHeader.createStreamHeader( &fbb, kind: wireKind(info.kind), sequence: info.sequence ?? 0, @@ -427,7 +427,7 @@ enum QUICStreamHeaderCodec { static func decode(_ data: Data) throws -> StreamHeaderInfo { var buffer = ByteBuffer(data: data) - let root: Stromo_Wire_StreamHeader + let root: Stormo_Wire_StreamHeader do { root = try getCheckedRoot( byteBuffer: &buffer, @@ -442,7 +442,7 @@ enum QUICStreamHeaderCodec { transferID: root.transferId?.uuidValue, label: root.label) } - private static func wireKind(_ kind: StreamHeaderInfo.Kind) -> Stromo_Wire_StreamKind { + private static func wireKind(_ kind: StreamHeaderInfo.Kind) -> Stormo_Wire_StreamKind { switch kind { case .message: return .message case .orderedMessage: return .orderedmessage @@ -452,7 +452,7 @@ enum QUICStreamHeaderCodec { } } - private static func infoKind(_ kind: Stromo_Wire_StreamKind) -> StreamHeaderInfo.Kind { + private static func infoKind(_ kind: Stormo_Wire_StreamKind) -> StreamHeaderInfo.Kind { switch kind { case .message, .unknown: return .message case .orderedmessage: return .orderedMessage diff --git a/Sources/PeerMesh/Transports/QUICTLS.swift b/Sources/Stormo/Transports/QUICTLS.swift similarity index 97% rename from Sources/PeerMesh/Transports/QUICTLS.swift rename to Sources/Stormo/Transports/QUICTLS.swift index d389ce6..6d54b95 100644 --- a/Sources/PeerMesh/Transports/QUICTLS.swift +++ b/Sources/Stormo/Transports/QUICTLS.swift @@ -1,6 +1,6 @@ import CryptoKit import Foundation -import StromoProtocol +import StormoProtocol #if canImport(Network) import Network @@ -60,7 +60,7 @@ enum QUICTLS { /// FROZEN. Version gating is the PeerHello semver (same-major interop) — /// bumping the ALPN would fail the handshake before hello, turning a /// diagnosable "upgrade required" into an opaque connect failure. - static let alpn = "Stromo/1" + static let alpn = "Stormo/1" /// Forms the local `sec_identity_t`. /// @@ -106,7 +106,7 @@ enum QUICTLS { #if os(macOS) private static func createFileKeychain() throws -> (SecKeychain, URL) { let dir = FileManager.default.temporaryDirectory - .appendingPathComponent("Stromo-kc-\(UUID().uuidString)", isDirectory: true) + .appendingPathComponent("Stormo-kc-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) let url = dir.appendingPathComponent("id.keychain") let pass = UUID().uuidString @@ -188,7 +188,7 @@ enum QUICTLS { } #endif - /// Builds `NWParameters` for a QUIC endpoint: ALPN `Stromo/1`, local + /// Builds `NWParameters` for a QUIC endpoint: ALPN `Stormo/1`, local /// identity, mutual authentication (so both ends recover the peer's /// authenticated key hash — FR-22), a ``TrustPolicy`` verify block, and /// generous stream/flow limits for the stream-per-message data plane (DD-7, @@ -235,7 +235,7 @@ enum QUICTLS { /// apply the policy. `displayName`/key-hash bookkeeping happens later from the /// connection's authenticated metadata. private static func installVerifyBlock(_ sec: sec_protocol_options_t, trust: TrustPolicy) { - let queue = DispatchQueue(label: "dev.securityunion.Stromo.quic.verify") + let queue = DispatchQueue(label: "dev.securityunion.stormo.quic.verify") sec_protocol_options_set_verify_block(sec, { _, secTrust, complete in quicDebug("verify block called") let trustRef = sec_trust_copy_ref(secTrust).takeRetainedValue() diff --git a/Sources/PeerMesh/Transports/QUICTransport.swift b/Sources/Stormo/Transports/QUICTransport.swift similarity index 98% rename from Sources/PeerMesh/Transports/QUICTransport.swift rename to Sources/Stormo/Transports/QUICTransport.swift index 12b2b80..f940317 100644 --- a/Sources/PeerMesh/Transports/QUICTransport.swift +++ b/Sources/Stormo/Transports/QUICTransport.swift @@ -1,5 +1,5 @@ import Foundation -import StromoProtocol +import StormoProtocol #if canImport(Network) import Network @@ -72,7 +72,7 @@ public final class QUICTransport: PeerTransport, @unchecked Sendable { /// every stream's callbacks — runs here, never on main. `.userInitiated`: /// the frame path is interactive; default QoS gets deprioritized under /// system load. - private let queue = DispatchQueue(label: "dev.securityunion.Stromo.quic", qos: .userInitiated) + private let queue = DispatchQueue(label: "dev.securityunion.stormo.quic", qos: .userInitiated) // Advertiser state. private let listenerBox = Locked(nil) @@ -98,13 +98,13 @@ public final class QUICTransport: PeerTransport, @unchecked Sendable { /// so this is `true`. On platforms with no entitlement-free route it is /// `false` — QUIC tests should skip cleanly and hosts should inject a /// `SecIdentity` via ``Configuration/tlsProvider``. - /// Peer-to-peer Wi-Fi opt-out (`Stromo_NO_P2P=1`): same-machine + /// Peer-to-peer Wi-Fi opt-out (`STORMO_NO_P2P=1`): same-machine /// self-dials fail with `includePeerToPeer` enabled /// (docs/spike-results.md), so same-machine test runs disable it. /// `getenv`, not `ProcessInfo`: the latter caches the environment at /// first access, which silently ignores a test's later `setenv`. static var peerToPeerEnabled: Bool { - getenv("Stromo_NO_P2P") == nil + getenv("STORMO_NO_P2P") == nil } /// Bonjour registration types are `_name._tcp|_udp` with a 1–15 char name. @@ -363,7 +363,7 @@ public final class QUICTransport: PeerTransport, @unchecked Sendable { case .bonjour: endpoint = endpoints.value[peer.id] } - guard let endpoint else { throw StromoError.peerUnreachable(peer.id) } + guard let endpoint else { throw StormoError.peerUnreachable(peer.id) } let localIdentity = try makeLocalIdentity(for: identity) let isBonjour: Bool @@ -511,15 +511,15 @@ public struct QUICTransport: PeerTransport { public let inboundConnections: AsyncStream = AsyncStream { _ in } public init() {} public func startAdvertising(service: ServiceDescriptor, metadata: [String: String], identity: PeerIdentity) async throws { - throw StromoError.unimplemented("QUICTransport requires Network.framework") + throw StormoError.unimplemented("QUICTransport requires Network.framework") } public func stopAdvertising() async {} public func discoveries(service: ServiceDescriptor) async throws -> AsyncStream { - throw StromoError.unimplemented("QUICTransport requires Network.framework") + throw StormoError.unimplemented("QUICTransport requires Network.framework") } public func stopBrowsing() async {} public func connect(to peer: DiscoveredPeer, identity: PeerIdentity, trust: TrustPolicy) async throws -> any PeerConnection { - throw StromoError.unimplemented("QUICTransport requires Network.framework") + throw StormoError.unimplemented("QUICTransport requires Network.framework") } } diff --git a/Sources/PeerMeshCLI/PeerMeshCLI.swift b/Sources/StormoCLI/StormoCLI.swift similarity index 94% rename from Sources/PeerMeshCLI/PeerMeshCLI.swift rename to Sources/StormoCLI/StormoCLI.swift index 438dab5..2841362 100644 --- a/Sources/PeerMeshCLI/PeerMeshCLI.swift +++ b/Sources/StormoCLI/StormoCLI.swift @@ -1,27 +1,27 @@ import Foundation -import Stromo +import Stormo /// Diagnostic CLI: exercises the production discovery + transport path /// (Bonjour + QUIC) between real processes. The debugging ladder for /// "devices can't see each other": two processes on one Mac → two Macs on a /// LAN → two iPhones (implementation plan Step 5). /// -/// Stromo advertise [--service _pmdemo._udp] [--name A] [--meta k=v]... -/// Stromo browse [--service _pmdemo._udp] [--timeout 30] -/// Stromo host [--service ...] [--name A] [--once] -/// Stromo join [--service ...] [--name B] [--peer A] [--send ping] [--linger] +/// Stormo advertise [--service _pmdemo._udp] [--name A] [--meta k=v]... +/// Stormo browse [--service _pmdemo._udp] [--timeout 30] +/// Stormo host [--service ...] [--name A] [--once] +/// Stormo join [--service ...] [--name B] [--peer A] [--send ping] [--linger] /// /// `host` advertises, auto-accepts invitations, echoes every message back /// ("pong: "). `join` browses, invites the first (or --peer named) peer, /// sends --send, waits for the echo, prints SUCCESS, exits 0. Both exit 2 on /// --timeout (default 30 s). Set QUIC_DEBUG=1 for driver logs. @main -struct StromoCLI { +struct StormoCLI { static func main() async { var arguments = Array(CommandLine.arguments.dropFirst()) guard let command = arguments.first, ["advertise", "browse", "host", "join"].contains(command) else { - print("usage: Stromo advertise|browse|host|join [--service TYPE] [--name NAME] [--peer NAME] [--send TEXT] [--meta k=v] [--timeout SECS] [--once]") + print("usage: Stormo advertise|browse|host|join [--service TYPE] [--name NAME] [--peer NAME] [--send TEXT] [--meta k=v] [--timeout SECS] [--once]") exit(64) } arguments.removeFirst() @@ -51,7 +51,7 @@ struct StromoCLI { let name = options["name"] ?? "\(ProcessInfo.processInfo.hostName)-\(getpid())" let timeout = TimeInterval(options["timeout"] ?? "30") ?? 30 - log("Stromo \(command) — name=\(name) service=\(service) pid=\(getpid())") + log("Stormo \(command) — name=\(name) service=\(service) pid=\(getpid())") // Global watchdog: a diagnostic tool must never hang silently. Task { @@ -146,7 +146,7 @@ struct StromoCLI { log("inviting \(target.id.displayName)…") let member = try await session.invite( - target, context: Data("Stromo-cli".utf8), timeout: min(timeout, 15)) + target, context: Data("stormo-cli".utf8), timeout: min(timeout, 15)) log("JOINED session with \(member.id.displayName)") let text = options["send"] ?? "ping" diff --git a/Sources/PeerMeshProtocol/Generated/common_generated.swift b/Sources/StormoProtocol/Generated/common_generated.swift similarity index 89% rename from Sources/PeerMeshProtocol/Generated/common_generated.swift rename to Sources/StormoProtocol/Generated/common_generated.swift index 3e77df0..1ae53ce 100644 --- a/Sources/PeerMeshProtocol/Generated/common_generated.swift +++ b/Sources/StormoProtocol/Generated/common_generated.swift @@ -7,7 +7,7 @@ import FlatBuffers /// A 128-bit transfer identifier: the two 8-byte halves of a `uuid_t`, in /// memory order. A fixed-size struct — no vector length word, and the /// 16-byte contract is enforced by the schema instead of a runtime check. -public struct Stromo_Wire_TransferId: NativeStruct, Verifiable, FlatbuffersInitializable { +public struct Stormo_Wire_TransferId: NativeStruct, Verifiable, FlatbuffersInitializable { static func validateVersion() { FlatBuffersVersion_25_2_10() } @@ -34,14 +34,14 @@ public struct Stromo_Wire_TransferId: NativeStruct, Verifiable, FlatbuffersIniti public var lo: UInt64 { _lo } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { - try verifier.inBuffer(position: position, of: Stromo_Wire_TransferId.self) + try verifier.inBuffer(position: position, of: Stormo_Wire_TransferId.self) } } /// A 128-bit transfer identifier: the two 8-byte halves of a `uuid_t`, in /// memory order. A fixed-size struct — no vector length word, and the /// 16-byte contract is enforced by the schema instead of a runtime check. -public struct Stromo_Wire_TransferId_Mutable: FlatBufferObject { +public struct Stormo_Wire_TransferId_Mutable: FlatBufferObject { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } diff --git a/Sources/PeerMeshProtocol/Generated/peer_hello_generated.swift b/Sources/StormoProtocol/Generated/peer_hello_generated.swift similarity index 83% rename from Sources/PeerMeshProtocol/Generated/peer_hello_generated.swift rename to Sources/StormoProtocol/Generated/peer_hello_generated.swift index 176ef61..1c82cd7 100644 --- a/Sources/PeerMeshProtocol/Generated/peer_hello_generated.swift +++ b/Sources/StormoProtocol/Generated/peer_hello_generated.swift @@ -4,7 +4,7 @@ import FlatBuffers -public struct Stromo_Wire_PeerHello: FlatBufferObject, Verifiable { +public struct Stormo_Wire_PeerHello: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -24,7 +24,7 @@ public struct Stromo_Wire_PeerHello: FlatBufferObject, Verifiable { /// The sender's identity. key_hash must match the TLS certificate's key /// hash — divergence is identityMismatch and kills the connection. - public var peer: Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.peer.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } + public var peer: Stormo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.peer.v); return o == 0 ? nil : Stormo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } /// Protocol semver: peers interoperate iff `major` matches. `minor` is /// additive (append-only schema evolution; the newer side adapts to the /// older); `patch` never changes the wire — diagnostics only. @@ -44,17 +44,17 @@ public struct Stromo_Wire_PeerHello: FlatBufferObject, Verifiable { protocolMinor: UInt16 = 0, protocolPatch: UInt16 = 0 ) -> Offset { - let __start = Stromo_Wire_PeerHello.startPeerHello(&fbb) - Stromo_Wire_PeerHello.add(peer: peer, &fbb) - Stromo_Wire_PeerHello.add(protocolMajor: protocolMajor, &fbb) - Stromo_Wire_PeerHello.add(protocolMinor: protocolMinor, &fbb) - Stromo_Wire_PeerHello.add(protocolPatch: protocolPatch, &fbb) - return Stromo_Wire_PeerHello.endPeerHello(&fbb, start: __start) + let __start = Stormo_Wire_PeerHello.startPeerHello(&fbb) + Stormo_Wire_PeerHello.add(peer: peer, &fbb) + Stormo_Wire_PeerHello.add(protocolMajor: protocolMajor, &fbb) + Stormo_Wire_PeerHello.add(protocolMinor: protocolMinor, &fbb) + Stormo_Wire_PeerHello.add(protocolPatch: protocolPatch, &fbb) + return Stormo_Wire_PeerHello.endPeerHello(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.peer.p, fieldName: "peer", required: false, type: ForwardOffset.self) + try _v.visit(field: VTOFFSET.peer.p, fieldName: "peer", required: false, type: ForwardOffset.self) try _v.visit(field: VTOFFSET.protocolMajor.p, fieldName: "protocolMajor", required: false, type: UInt16.self) try _v.visit(field: VTOFFSET.protocolMinor.p, fieldName: "protocolMinor", required: false, type: UInt16.self) try _v.visit(field: VTOFFSET.protocolPatch.p, fieldName: "protocolPatch", required: false, type: UInt16.self) diff --git a/Sources/PeerMeshProtocol/Generated/signal_generated.swift b/Sources/StormoProtocol/Generated/signal_generated.swift similarity index 79% rename from Sources/PeerMeshProtocol/Generated/signal_generated.swift rename to Sources/StormoProtocol/Generated/signal_generated.swift index 5f90d09..b7f2342 100644 --- a/Sources/PeerMeshProtocol/Generated/signal_generated.swift +++ b/Sources/StormoProtocol/Generated/signal_generated.swift @@ -4,7 +4,7 @@ import FlatBuffers -public enum Stromo_Wire_SignalBody: UInt8, UnionEnum { +public enum Stormo_Wire_SignalBody: UInt8, UnionEnum { public typealias T = UInt8 public init?(value: T) { @@ -21,13 +21,13 @@ public enum Stromo_Wire_SignalBody: UInt8, UnionEnum { case transferoffer = 5 case streamopen = 6 - public static var max: Stromo_Wire_SignalBody { return .streamopen } - public static var min: Stromo_Wire_SignalBody { return .none_ } + public static var max: Stormo_Wire_SignalBody { return .streamopen } + public static var min: Stormo_Wire_SignalBody { return .none_ } } /// A session member, as carried in rosters (FR-13). -public struct Stromo_Wire_PeerInfo: FlatBufferObject, Verifiable { +public struct Stormo_Wire_PeerInfo: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -59,10 +59,10 @@ public struct Stromo_Wire_PeerInfo: FlatBufferObject, Verifiable { keyHashVectorOffset keyHash: Offset = Offset(), displayNameOffset displayName: Offset = Offset() ) -> Offset { - let __start = Stromo_Wire_PeerInfo.startPeerInfo(&fbb) - Stromo_Wire_PeerInfo.addVectorOf(keyHash: keyHash, &fbb) - Stromo_Wire_PeerInfo.add(displayName: displayName, &fbb) - return Stromo_Wire_PeerInfo.endPeerInfo(&fbb, start: __start) + let __start = Stormo_Wire_PeerInfo.startPeerInfo(&fbb) + Stormo_Wire_PeerInfo.addVectorOf(keyHash: keyHash, &fbb) + Stormo_Wire_PeerInfo.add(displayName: displayName, &fbb) + return Stormo_Wire_PeerInfo.endPeerInfo(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { @@ -75,7 +75,7 @@ public struct Stromo_Wire_PeerInfo: FlatBufferObject, Verifiable { /// Invitation to join a session (FR-6). First message on the control stream /// after the QUIC handshake (encrypted, not yet authorized). -public struct Stromo_Wire_Invite: FlatBufferObject, Verifiable { +public struct Stormo_Wire_Invite: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -91,7 +91,7 @@ public struct Stromo_Wire_Invite: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var inviter: Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.inviter.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } + public var inviter: Stormo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.inviter.v); return o == 0 ? nil : Stormo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(o + _accessor.position)) } /// Opaque application context (FR-6). Capped by SignalCodec's control-message /// limit; larger contexts are chunked (DD-5 rule 3). public var hasContext: Bool { let o = _accessor.offset(VTOFFSET.context.v); return o == 0 ? false : true } @@ -107,22 +107,22 @@ public struct Stromo_Wire_Invite: FlatBufferObject, Verifiable { inviterOffset inviter: Offset = Offset(), contextVectorOffset context: Offset = Offset() ) -> Offset { - let __start = Stromo_Wire_Invite.startInvite(&fbb) - Stromo_Wire_Invite.add(inviter: inviter, &fbb) - Stromo_Wire_Invite.addVectorOf(context: context, &fbb) - return Stromo_Wire_Invite.endInvite(&fbb, start: __start) + let __start = Stormo_Wire_Invite.startInvite(&fbb) + Stormo_Wire_Invite.add(inviter: inviter, &fbb) + Stormo_Wire_Invite.addVectorOf(context: context, &fbb) + return Stormo_Wire_Invite.endInvite(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.inviter.p, fieldName: "inviter", required: false, type: ForwardOffset.self) + try _v.visit(field: VTOFFSET.inviter.p, fieldName: "inviter", required: false, type: ForwardOffset.self) try _v.visit(field: VTOFFSET.context.p, fieldName: "context", required: false, type: ForwardOffset>.self) _v.finish() } } /// Response to an Invite (FR-7). -public struct Stromo_Wire_InviteResponse: FlatBufferObject, Verifiable { +public struct Stormo_Wire_InviteResponse: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -143,7 +143,7 @@ public struct Stromo_Wire_InviteResponse: FlatBufferObject, Verifiable { /// one round-trip (FR-13). public var hasRoster: Bool { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? false : true } public var rosterCount: Int32 { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? 0 : _accessor.vector(count: o) } - public func roster(at index: Int32) -> Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } + public func roster(at index: Int32) -> Stormo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.roster.v); return o == 0 ? nil : Stormo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } public static func startInviteResponse(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 2) } public static func add(accepted: Bool, _ fbb: inout FlatBufferBuilder) { fbb.add(element: accepted, def: false, at: VTOFFSET.accepted.p) } @@ -154,22 +154,22 @@ public struct Stromo_Wire_InviteResponse: FlatBufferObject, Verifiable { accepted: Bool = false, rosterVectorOffset roster: Offset = Offset() ) -> Offset { - let __start = Stromo_Wire_InviteResponse.startInviteResponse(&fbb) - Stromo_Wire_InviteResponse.add(accepted: accepted, &fbb) - Stromo_Wire_InviteResponse.addVectorOf(roster: roster, &fbb) - return Stromo_Wire_InviteResponse.endInviteResponse(&fbb, start: __start) + let __start = Stormo_Wire_InviteResponse.startInviteResponse(&fbb) + Stormo_Wire_InviteResponse.add(accepted: accepted, &fbb) + Stormo_Wire_InviteResponse.addVectorOf(roster: roster, &fbb) + return Stormo_Wire_InviteResponse.endInviteResponse(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) try _v.visit(field: VTOFFSET.accepted.p, fieldName: "accepted", required: false, type: Bool.self) - try _v.visit(field: VTOFFSET.roster.p, fieldName: "roster", required: false, type: ForwardOffset, Stromo_Wire_PeerInfo>>.self) + try _v.visit(field: VTOFFSET.roster.p, fieldName: "roster", required: false, type: ForwardOffset, Stormo_Wire_PeerInfo>>.self) _v.finish() } } /// Pairing-code transcript confirmation (DD-2, `.pairingCode` mode only). -public struct Stromo_Wire_CodeConfirm: FlatBufferObject, Verifiable { +public struct Stormo_Wire_CodeConfirm: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -196,9 +196,9 @@ public struct Stromo_Wire_CodeConfirm: FlatBufferObject, Verifiable { _ fbb: inout FlatBufferBuilder, transcriptMacVectorOffset transcriptMac: Offset = Offset() ) -> Offset { - let __start = Stromo_Wire_CodeConfirm.startCodeConfirm(&fbb) - Stromo_Wire_CodeConfirm.addVectorOf(transcriptMac: transcriptMac, &fbb) - return Stromo_Wire_CodeConfirm.endCodeConfirm(&fbb, start: __start) + let __start = Stormo_Wire_CodeConfirm.startCodeConfirm(&fbb) + Stormo_Wire_CodeConfirm.addVectorOf(transcriptMac: transcriptMac, &fbb) + return Stormo_Wire_CodeConfirm.endCodeConfirm(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { @@ -209,7 +209,7 @@ public struct Stromo_Wire_CodeConfirm: FlatBufferObject, Verifiable { } /// Membership gossip (FR-13, FR-14). -public struct Stromo_Wire_RosterUpdate: FlatBufferObject, Verifiable { +public struct Stormo_Wire_RosterUpdate: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -227,7 +227,7 @@ public struct Stromo_Wire_RosterUpdate: FlatBufferObject, Verifiable { public var hasMembers: Bool { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? false : true } public var membersCount: Int32 { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? 0 : _accessor.vector(count: o) } - public func members(at index: Int32) -> Stromo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? nil : Stromo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } + public func members(at index: Int32) -> Stormo_Wire_PeerInfo? { let o = _accessor.offset(VTOFFSET.members.v); return o == 0 ? nil : Stormo_Wire_PeerInfo(_accessor.bb, o: _accessor.indirect(_accessor.vector(at: o) + index * 4)) } /// Monotonic epoch for last-writer-wins convergence. public var epoch: UInt64 { let o = _accessor.offset(VTOFFSET.epoch.v); return o == 0 ? 0 : _accessor.readBuffer(of: UInt64.self, at: o) } public static func startRosterUpdate(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 2) } @@ -239,22 +239,22 @@ public struct Stromo_Wire_RosterUpdate: FlatBufferObject, Verifiable { membersVectorOffset members: Offset = Offset(), epoch: UInt64 = 0 ) -> Offset { - let __start = Stromo_Wire_RosterUpdate.startRosterUpdate(&fbb) - Stromo_Wire_RosterUpdate.addVectorOf(members: members, &fbb) - Stromo_Wire_RosterUpdate.add(epoch: epoch, &fbb) - return Stromo_Wire_RosterUpdate.endRosterUpdate(&fbb, start: __start) + let __start = Stormo_Wire_RosterUpdate.startRosterUpdate(&fbb) + Stormo_Wire_RosterUpdate.addVectorOf(members: members, &fbb) + Stormo_Wire_RosterUpdate.add(epoch: epoch, &fbb) + return Stormo_Wire_RosterUpdate.endRosterUpdate(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.members.p, fieldName: "members", required: false, type: ForwardOffset, Stromo_Wire_PeerInfo>>.self) + try _v.visit(field: VTOFFSET.members.p, fieldName: "members", required: false, type: ForwardOffset, Stormo_Wire_PeerInfo>>.self) try _v.visit(field: VTOFFSET.epoch.p, fieldName: "epoch", required: false, type: UInt64.self) _v.finish() } } /// Announces an incoming resource transfer on a dedicated stream (FR-17). -public struct Stromo_Wire_TransferOffer: FlatBufferObject, Verifiable { +public struct Stormo_Wire_TransferOffer: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -271,32 +271,32 @@ public struct Stromo_Wire_TransferOffer: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var transferId: Stromo_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: Stromo_Wire_TransferId.self, at: o) } - public var mutableTransferId: Stromo_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : Stromo_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } + public var transferId: Stormo_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: Stormo_Wire_TransferId.self, at: o) } + public var mutableTransferId: Stormo_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : Stormo_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } public var name: String? { let o = _accessor.offset(VTOFFSET.name.v); return o == 0 ? nil : _accessor.string(at: o) } public var nameSegmentArray: [UInt8]? { return _accessor.getVector(at: VTOFFSET.name.v) } public var totalBytes: UInt64 { let o = _accessor.offset(VTOFFSET.totalBytes.v); return o == 0 ? 0 : _accessor.readBuffer(of: UInt64.self, at: o) } public static func startTransferOffer(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 3) } - public static func add(transferId: Stromo_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } + public static func add(transferId: Stormo_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } public static func add(name: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: name, at: VTOFFSET.name.p) } public static func add(totalBytes: UInt64, _ fbb: inout FlatBufferBuilder) { fbb.add(element: totalBytes, def: 0, at: VTOFFSET.totalBytes.p) } public static func endTransferOffer(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } public static func createTransferOffer( _ fbb: inout FlatBufferBuilder, - transferId: Stromo_Wire_TransferId? = nil, + transferId: Stormo_Wire_TransferId? = nil, nameOffset name: Offset = Offset(), totalBytes: UInt64 = 0 ) -> Offset { - let __start = Stromo_Wire_TransferOffer.startTransferOffer(&fbb) - Stromo_Wire_TransferOffer.add(transferId: transferId, &fbb) - Stromo_Wire_TransferOffer.add(name: name, &fbb) - Stromo_Wire_TransferOffer.add(totalBytes: totalBytes, &fbb) - return Stromo_Wire_TransferOffer.endTransferOffer(&fbb, start: __start) + let __start = Stormo_Wire_TransferOffer.startTransferOffer(&fbb) + Stormo_Wire_TransferOffer.add(transferId: transferId, &fbb) + Stormo_Wire_TransferOffer.add(name: name, &fbb) + Stormo_Wire_TransferOffer.add(totalBytes: totalBytes, &fbb) + return Stormo_Wire_TransferOffer.endTransferOffer(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: Stromo_Wire_TransferId.self) + try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: Stormo_Wire_TransferId.self) try _v.visit(field: VTOFFSET.name.p, fieldName: "name", required: false, type: ForwardOffset.self) try _v.visit(field: VTOFFSET.totalBytes.p, fieldName: "totalBytes", required: false, type: UInt64.self) _v.finish() @@ -304,7 +304,7 @@ public struct Stromo_Wire_TransferOffer: FlatBufferObject, Verifiable { } /// Announces an application byte stream opening on a dedicated stream (FR-18). -public struct Stromo_Wire_StreamOpen: FlatBufferObject, Verifiable { +public struct Stormo_Wire_StreamOpen: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -328,9 +328,9 @@ public struct Stromo_Wire_StreamOpen: FlatBufferObject, Verifiable { _ fbb: inout FlatBufferBuilder, labelOffset label: Offset = Offset() ) -> Offset { - let __start = Stromo_Wire_StreamOpen.startStreamOpen(&fbb) - Stromo_Wire_StreamOpen.add(label: label, &fbb) - return Stromo_Wire_StreamOpen.endStreamOpen(&fbb, start: __start) + let __start = Stormo_Wire_StreamOpen.startStreamOpen(&fbb) + Stormo_Wire_StreamOpen.add(label: label, &fbb) + return Stormo_Wire_StreamOpen.endStreamOpen(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { @@ -342,7 +342,7 @@ public struct Stromo_Wire_StreamOpen: FlatBufferObject, Verifiable { /// Envelope for every control-plane message (DD-5 rule 1). Size-prefixed on /// the wire. Unknown body variants are ignored-and-logged, never fatal (QA-11). -public struct Stromo_Wire_Signal: FlatBufferObject, Verifiable { +public struct Stormo_Wire_Signal: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -358,41 +358,41 @@ public struct Stromo_Wire_Signal: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var bodyType: Stromo_Wire_SignalBody { let o = _accessor.offset(VTOFFSET.bodyType.v); return o == 0 ? .none_ : Stromo_Wire_SignalBody(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .none_ } + public var bodyType: Stormo_Wire_SignalBody { let o = _accessor.offset(VTOFFSET.bodyType.v); return o == 0 ? .none_ : Stormo_Wire_SignalBody(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .none_ } public func body(type: T.Type) -> T? { let o = _accessor.offset(VTOFFSET.body.v); return o == 0 ? nil : _accessor.union(o) } public static func startSignal(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 2) } - public static func add(bodyType: Stromo_Wire_SignalBody, _ fbb: inout FlatBufferBuilder) { fbb.add(element: bodyType.rawValue, def: 0, at: VTOFFSET.bodyType.p) } + public static func add(bodyType: Stormo_Wire_SignalBody, _ fbb: inout FlatBufferBuilder) { fbb.add(element: bodyType.rawValue, def: 0, at: VTOFFSET.bodyType.p) } public static func add(body: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: body, at: VTOFFSET.body.p) } public static func endSignal(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } public static func createSignal( _ fbb: inout FlatBufferBuilder, - bodyType: Stromo_Wire_SignalBody = .none_, + bodyType: Stormo_Wire_SignalBody = .none_, bodyOffset body: Offset = Offset() ) -> Offset { - let __start = Stromo_Wire_Signal.startSignal(&fbb) - Stromo_Wire_Signal.add(bodyType: bodyType, &fbb) - Stromo_Wire_Signal.add(body: body, &fbb) - return Stromo_Wire_Signal.endSignal(&fbb, start: __start) + let __start = Stormo_Wire_Signal.startSignal(&fbb) + Stormo_Wire_Signal.add(bodyType: bodyType, &fbb) + Stormo_Wire_Signal.add(body: body, &fbb) + return Stormo_Wire_Signal.endSignal(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(unionKey: VTOFFSET.bodyType.p, unionField: VTOFFSET.body.p, unionKeyName: "bodyType", fieldName: "body", required: false, completion: { (verifier, key: Stromo_Wire_SignalBody, pos) in + try _v.visit(unionKey: VTOFFSET.bodyType.p, unionField: VTOFFSET.body.p, unionKeyName: "bodyType", fieldName: "body", required: false, completion: { (verifier, key: Stormo_Wire_SignalBody, pos) in switch key { case .none_: break // NOTE - SWIFT doesnt support none case .invite: - try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_Invite.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stormo_Wire_Invite.self) case .inviteresponse: - try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_InviteResponse.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stormo_Wire_InviteResponse.self) case .codeconfirm: - try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_CodeConfirm.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stormo_Wire_CodeConfirm.self) case .rosterupdate: - try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_RosterUpdate.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stormo_Wire_RosterUpdate.self) case .transferoffer: - try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_TransferOffer.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stormo_Wire_TransferOffer.self) case .streamopen: - try ForwardOffset.verify(&verifier, at: pos, of: Stromo_Wire_StreamOpen.self) + try ForwardOffset.verify(&verifier, at: pos, of: Stormo_Wire_StreamOpen.self) } }) _v.finish() diff --git a/Sources/PeerMeshProtocol/Generated/stream_header_generated.swift b/Sources/StormoProtocol/Generated/stream_header_generated.swift similarity index 74% rename from Sources/PeerMeshProtocol/Generated/stream_header_generated.swift rename to Sources/StormoProtocol/Generated/stream_header_generated.swift index f1c8308..9900f73 100644 --- a/Sources/PeerMeshProtocol/Generated/stream_header_generated.swift +++ b/Sources/StormoProtocol/Generated/stream_header_generated.swift @@ -4,7 +4,7 @@ import FlatBuffers -public enum Stromo_Wire_StreamKind: UInt8, Enum, Verifiable { +public enum Stormo_Wire_StreamKind: UInt8, Enum, Verifiable { public typealias T = UInt8 public static var byteSize: Int { return MemoryLayout.size } public var value: UInt8 { return self.rawValue } @@ -23,12 +23,12 @@ public enum Stromo_Wire_StreamKind: UInt8, Enum, Verifiable { /// (iOS 16+/macOS 13+) are the latency refinement (TODO datagrams). case datagram = 5 - public static var max: Stromo_Wire_StreamKind { return .datagram } - public static var min: Stromo_Wire_StreamKind { return .unknown } + public static var max: Stormo_Wire_StreamKind { return .datagram } + public static var min: Stormo_Wire_StreamKind { return .unknown } } -public struct Stromo_Wire_StreamHeader: FlatBufferObject, Verifiable { +public struct Stormo_Wire_StreamHeader: FlatBufferObject, Verifiable { static func validateVersion() { FlatBuffersVersion_25_2_10() } public var __buffer: ByteBuffer! { return _accessor.bb } @@ -46,41 +46,41 @@ public struct Stromo_Wire_StreamHeader: FlatBufferObject, Verifiable { var p: VOffset { self.rawValue } } - public var kind: Stromo_Wire_StreamKind { let o = _accessor.offset(VTOFFSET.kind.v); return o == 0 ? .unknown : Stromo_Wire_StreamKind(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .unknown } + public var kind: Stormo_Wire_StreamKind { let o = _accessor.offset(VTOFFSET.kind.v); return o == 0 ? .unknown : Stormo_Wire_StreamKind(rawValue: _accessor.readBuffer(of: UInt8.self, at: o)) ?? .unknown } /// Sender-local FIFO sequence, per peer pair. Meaningful for OrderedMessage. public var sequence: UInt64 { let o = _accessor.offset(VTOFFSET.sequence.v); return o == 0 ? 0 : _accessor.readBuffer(of: UInt64.self, at: o) } /// Meaningful for TransferChunk. - public var transferId: Stromo_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: Stromo_Wire_TransferId.self, at: o) } - public var mutableTransferId: Stromo_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : Stromo_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } + public var transferId: Stormo_Wire_TransferId? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : _accessor.readBuffer(of: Stormo_Wire_TransferId.self, at: o) } + public var mutableTransferId: Stormo_Wire_TransferId_Mutable? { let o = _accessor.offset(VTOFFSET.transferId.v); return o == 0 ? nil : Stormo_Wire_TransferId_Mutable(_accessor.bb, o: o + _accessor.position) } /// The app-chosen stream name. Meaningful for AppStream. public var label: String? { let o = _accessor.offset(VTOFFSET.label.v); return o == 0 ? nil : _accessor.string(at: o) } public var labelSegmentArray: [UInt8]? { return _accessor.getVector(at: VTOFFSET.label.v) } public static func startStreamHeader(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 4) } - public static func add(kind: Stromo_Wire_StreamKind, _ fbb: inout FlatBufferBuilder) { fbb.add(element: kind.rawValue, def: 0, at: VTOFFSET.kind.p) } + public static func add(kind: Stormo_Wire_StreamKind, _ fbb: inout FlatBufferBuilder) { fbb.add(element: kind.rawValue, def: 0, at: VTOFFSET.kind.p) } public static func add(sequence: UInt64, _ fbb: inout FlatBufferBuilder) { fbb.add(element: sequence, def: 0, at: VTOFFSET.sequence.p) } - public static func add(transferId: Stromo_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } + public static func add(transferId: Stormo_Wire_TransferId?, _ fbb: inout FlatBufferBuilder) { guard let transferId = transferId else { return }; fbb.create(struct: transferId, position: VTOFFSET.transferId.p) } public static func add(label: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: label, at: VTOFFSET.label.p) } public static func endStreamHeader(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } public static func createStreamHeader( _ fbb: inout FlatBufferBuilder, - kind: Stromo_Wire_StreamKind = .unknown, + kind: Stormo_Wire_StreamKind = .unknown, sequence: UInt64 = 0, - transferId: Stromo_Wire_TransferId? = nil, + transferId: Stormo_Wire_TransferId? = nil, labelOffset label: Offset = Offset() ) -> Offset { - let __start = Stromo_Wire_StreamHeader.startStreamHeader(&fbb) - Stromo_Wire_StreamHeader.add(kind: kind, &fbb) - Stromo_Wire_StreamHeader.add(sequence: sequence, &fbb) - Stromo_Wire_StreamHeader.add(transferId: transferId, &fbb) - Stromo_Wire_StreamHeader.add(label: label, &fbb) - return Stromo_Wire_StreamHeader.endStreamHeader(&fbb, start: __start) + let __start = Stormo_Wire_StreamHeader.startStreamHeader(&fbb) + Stormo_Wire_StreamHeader.add(kind: kind, &fbb) + Stormo_Wire_StreamHeader.add(sequence: sequence, &fbb) + Stormo_Wire_StreamHeader.add(transferId: transferId, &fbb) + Stormo_Wire_StreamHeader.add(label: label, &fbb) + return Stormo_Wire_StreamHeader.endStreamHeader(&fbb, start: __start) } public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { var _v = try verifier.visitTable(at: position) - try _v.visit(field: VTOFFSET.kind.p, fieldName: "kind", required: false, type: Stromo_Wire_StreamKind.self) + try _v.visit(field: VTOFFSET.kind.p, fieldName: "kind", required: false, type: Stormo_Wire_StreamKind.self) try _v.visit(field: VTOFFSET.sequence.p, fieldName: "sequence", required: false, type: UInt64.self) - try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: Stromo_Wire_TransferId.self) + try _v.visit(field: VTOFFSET.transferId.p, fieldName: "transferId", required: false, type: Stormo_Wire_TransferId.self) try _v.visit(field: VTOFFSET.label.p, fieldName: "label", required: false, type: ForwardOffset.self) _v.finish() } diff --git a/Sources/PeerMeshProtocol/LibP2PIdentity.swift b/Sources/StormoProtocol/LibP2PIdentity.swift similarity index 96% rename from Sources/PeerMeshProtocol/LibP2PIdentity.swift rename to Sources/StormoProtocol/LibP2PIdentity.swift index 2c8a824..719299b 100644 --- a/Sources/PeerMeshProtocol/LibP2PIdentity.swift +++ b/Sources/StormoProtocol/LibP2PIdentity.swift @@ -3,11 +3,11 @@ import Foundation /// libp2p-compatible identity encoding (DD-8). /// -/// Stromo derives its ``PeerID`` from the peer's public key exactly the way +/// Stormo derives its ``PeerID`` from the peer's public key exactly the way /// libp2p does, so a future libp2p bridge stays identity-compatible. This helper /// is deliberately dependency-free (only CryptoKit's SHA-256, a pure CPU hash — -/// no I/O, consistent with the sans-I/O `StromoProtocol` target, DD-6) and is -/// shared by both `StromoProtocol` and `Stromo` so the two never disagree on +/// no I/O, consistent with the sans-I/O `StormoProtocol` target, DD-6) and is +/// shared by both `StormoProtocol` and `Stormo` so the two never disagree on /// the byte layout. /// /// ## PeerID multihash byte layout (34 bytes) diff --git a/Sources/PeerMeshProtocol/Locked.swift b/Sources/StormoProtocol/Locked.swift similarity index 95% rename from Sources/PeerMeshProtocol/Locked.swift rename to Sources/StormoProtocol/Locked.swift index ea4d44d..dccf51f 100644 --- a/Sources/PeerMeshProtocol/Locked.swift +++ b/Sources/StormoProtocol/Locked.swift @@ -2,7 +2,7 @@ import Foundation /// A minimal mutex-guarded box for shared mutable state. /// -/// This lives in the sans-I/O `StromoProtocol` target (its only dependency is +/// This lives in the sans-I/O `StormoProtocol` target (its only dependency is /// FlatBuffers, DD-6) purely so the runtime shell and its transports — which /// each need the same primitive — share one canonical implementation rather /// than re-declaring it per module. `NSLock` is a synchronization primitive, not diff --git a/Sources/PeerMeshProtocol/PeerID.swift b/Sources/StormoProtocol/PeerID.swift similarity index 100% rename from Sources/PeerMeshProtocol/PeerID.swift rename to Sources/StormoProtocol/PeerID.swift diff --git a/Sources/PeerMeshProtocol/ProtocolEngine.swift b/Sources/StormoProtocol/ProtocolEngine.swift similarity index 100% rename from Sources/PeerMeshProtocol/ProtocolEngine.swift rename to Sources/StormoProtocol/ProtocolEngine.swift diff --git a/Sources/PeerMeshProtocol/ProtocolVersion.swift b/Sources/StormoProtocol/ProtocolVersion.swift similarity index 95% rename from Sources/PeerMeshProtocol/ProtocolVersion.swift rename to Sources/StormoProtocol/ProtocolVersion.swift index 1ca61c4..2219575 100644 --- a/Sources/PeerMeshProtocol/ProtocolVersion.swift +++ b/Sources/StormoProtocol/ProtocolVersion.swift @@ -1,4 +1,4 @@ -/// Stromo wire-protocol version, negotiated once per connection via the +/// Stormo wire-protocol version, negotiated once per connection via the /// `PeerHello` bootstrap (peer_hello.fbs) — never per-signal. /// /// Semver interop rule: peers talk iff `major` matches. `minor` is additive diff --git a/Sources/PeerMeshProtocol/Signal.swift b/Sources/StormoProtocol/Signal.swift similarity index 88% rename from Sources/PeerMeshProtocol/Signal.swift rename to Sources/StormoProtocol/Signal.swift index 5796728..b1680c7 100644 --- a/Sources/PeerMeshProtocol/Signal.swift +++ b/Sources/StormoProtocol/Signal.swift @@ -1,17 +1,17 @@ import FlatBuffers import Foundation -// Ergonomic aliases for the flatc-generated types (namespace Stromo.Wire). -public typealias WireSignal = Stromo_Wire_Signal -public typealias WireSignalBody = Stromo_Wire_SignalBody -public typealias WirePeerInfo = Stromo_Wire_PeerInfo -public typealias WireInvite = Stromo_Wire_Invite -public typealias WireInviteResponse = Stromo_Wire_InviteResponse -public typealias WireCodeConfirm = Stromo_Wire_CodeConfirm -public typealias WireRosterUpdate = Stromo_Wire_RosterUpdate -public typealias WireTransferOffer = Stromo_Wire_TransferOffer -public typealias WireStreamOpen = Stromo_Wire_StreamOpen -public typealias WireTransferId = Stromo_Wire_TransferId +// Ergonomic aliases for the flatc-generated types (namespace Stormo.Wire). +public typealias WireSignal = Stormo_Wire_Signal +public typealias WireSignalBody = Stormo_Wire_SignalBody +public typealias WirePeerInfo = Stormo_Wire_PeerInfo +public typealias WireInvite = Stormo_Wire_Invite +public typealias WireInviteResponse = Stormo_Wire_InviteResponse +public typealias WireCodeConfirm = Stormo_Wire_CodeConfirm +public typealias WireRosterUpdate = Stormo_Wire_RosterUpdate +public typealias WireTransferOffer = Stormo_Wire_TransferOffer +public typealias WireStreamOpen = Stormo_Wire_StreamOpen +public typealias WireTransferId = Stormo_Wire_TransferId /// A control-plane message: a verified FlatBuffers buffer read **in place** /// (DD-5, DD-6). Zero-copy discipline: @@ -197,8 +197,8 @@ extension WireTransferId { // Data-plane stream header (DD-7). Every non-control QUIC stream starts with // a size-prefixed StreamHeader; FIN delimits the payload. -public typealias WireStreamHeader = Stromo_Wire_StreamHeader -public typealias WireStreamKind = Stromo_Wire_StreamKind +public typealias WireStreamHeader = Stormo_Wire_StreamHeader +public typealias WireStreamKind = Stormo_Wire_StreamKind /// Delivery semantics for data (FR-15, FR-16, DD-7). public enum Delivery: Sendable, Equatable { @@ -217,7 +217,7 @@ public enum Delivery: Sendable, Equatable { case reliableOrdered /// Droppable, unordered, low-latency — true datagram semantics, so the /// payload must fit one datagram: sends over ``maxDatagramPayload`` throw - /// ``StromoError/datagramTooLarge(bytes:limit:)``. For larger droppable + /// ``StormoError/datagramTooLarge(bytes:limit:)``. For larger droppable /// data, send `.reliable` and supersede at the application layer. case datagram } @@ -229,7 +229,7 @@ public enum Recipients: Sendable, Equatable { case peers([PeerID]) } -public enum StromoError: Error, Sendable, Equatable { +public enum StormoError: Error, Sendable, Equatable { /// A surface that is deliberately not wired up yet on this platform or build. /// The associated string names it; each call site carries a comment pointing /// at the relevant TODO ledger entry (see CLAUDE.md). Current uses: the @@ -259,29 +259,29 @@ public enum StromoError: Error, Sendable, Equatable { // NSError bridging renumbers payload cases (failure mode 11) — LocalizedError // keeps the diagnostic in localizedDescription. -extension StromoError: LocalizedError { +extension StormoError: LocalizedError { public var errorDescription: String? { switch self { case .unimplemented(let surface): - return "Stromo: \(surface) is not implemented" + return "Stormo: \(surface) is not implemented" case .localNetworkPermissionDenied: - return "Stromo: Local Network permission denied" + return "Stormo: Local Network permission denied" case .invitationTimedOut: - return "Stromo: invitation timed out" + return "Stormo: invitation timed out" case .invitationDeclined: - return "Stromo: invitation declined" + return "Stormo: invitation declined" case .invitationAlreadyPending(let peer): - return "Stromo: an invitation to \(peer.displayName) is already pending" + return "Stormo: an invitation to \(peer.displayName) is already pending" case .peerUnreachable(let peer): - return "Stromo: peer \(peer.displayName) is unreachable" + return "Stormo: peer \(peer.displayName) is unreachable" case .datagramTooLarge(let bytes, let limit): - return "Stromo: .datagram payload is \(bytes) bytes; datagrams cannot exceed \(limit) bytes (they never fragment) — use .reliable for payloads this size" + return "Stormo: .datagram payload is \(bytes) bytes; datagrams cannot exceed \(limit) bytes (they never fragment) — use .reliable for payloads this size" case .malformedSignal: - return "Stromo: inbound signal failed verification" + return "Stormo: inbound signal failed verification" case .resourceTransferIncomplete: - return "Stromo: resource transfer ended before all bytes arrived" + return "Stormo: resource transfer ended before all bytes arrived" case .reorderBufferOverflow: - return "Stromo: ordered-message reorder buffer overflowed" + return "Stormo: ordered-message reorder buffer overflowed" } } } diff --git a/Sources/PeerMeshProtocol/SignalCodec.swift b/Sources/StormoProtocol/SignalCodec.swift similarity index 95% rename from Sources/PeerMeshProtocol/SignalCodec.swift rename to Sources/StormoProtocol/SignalCodec.swift index 571d712..15a7796 100644 --- a/Sources/PeerMeshProtocol/SignalCodec.swift +++ b/Sources/StormoProtocol/SignalCodec.swift @@ -27,7 +27,7 @@ public enum SignalCodec { /// genuine structural malformation throws, and that is connection-fatal. public static func decode(_ data: Data) throws -> Signal { guard data.count <= maxControlMessageSize, !data.isEmpty else { - throw StromoError.malformedSignal + throw StormoError.malformedSignal } var buffer = ByteBuffer(data: data) do { @@ -39,7 +39,7 @@ public enum SignalCodec { maxApparentSize: UInt32(maxControlMessageSize))) return Signal(verified: data, root: root) } catch { - throw StromoError.malformedSignal + throw StormoError.malformedSignal } } } diff --git a/Sources/PeerMeshTestKit/InMemoryTransport.swift b/Sources/StormoTestKit/InMemoryTransport.swift similarity index 97% rename from Sources/PeerMeshTestKit/InMemoryTransport.swift rename to Sources/StormoTestKit/InMemoryTransport.swift index 19136bb..0cffbfc 100644 --- a/Sources/PeerMeshTestKit/InMemoryTransport.swift +++ b/Sources/StormoTestKit/InMemoryTransport.swift @@ -1,5 +1,5 @@ import Foundation -import Stromo +import Stormo /// In-process transport for tests and mesh simulation (QA-8): exercises the /// full runtime — discovery, invitation, membership, messaging — with no @@ -68,7 +68,7 @@ public final class InMemoryTransport: PeerTransport, @unchecked Sendable { /// inbound stream receives the other. func connect(to peerID: PeerID, from identity: PeerIdentity) throws -> any PeerConnection { guard let ad = advertisements[peerID] else { - throw StromoError.peerUnreachable(peerID) + throw StormoError.peerUnreachable(peerID) } let (dialerEnd, listenerEnd) = InMemoryConnection.pair( dialer: identity, listener: ad.identity) @@ -156,21 +156,21 @@ final class InMemoryConnection: PeerConnection, @unchecked Sendable { func sendSignal(_ bytes: Data) async throws { guard !closed.value, let partner = partnerBox.value else { - throw StromoError.peerUnreachable(remotePeer) + throw StormoError.peerUnreachable(remotePeer) } partner.ownContinuation.yield(.signal(bytes)) } func sendData(_ payload: Data, delivery: Delivery, sequence: UInt64?) async throws { guard !closed.value, let partner = partnerBox.value else { - throw StromoError.peerUnreachable(remotePeer) + throw StormoError.peerUnreachable(remotePeer) } partner.ownContinuation.yield(.data(payload, delivery, sequence: sequence)) } func openOutgoingStream(header: StreamHeaderInfo) async throws -> any PeerByteStream { guard !closed.value, let partner = partnerBox.value else { - throw StromoError.peerUnreachable(remotePeer) + throw StormoError.peerUnreachable(remotePeer) } // The dedicated stream is a paired byte pipe; the writer stays local, // the reader surfaces on the partner's incomingStreams (with the header). @@ -221,7 +221,7 @@ final class InMemoryByteStream: PeerByteStream, @unchecked Sendable { func write(_ data: Data) async throws { guard !finished.value, let partner = partnerBox.value else { - throw StromoError.resourceTransferIncomplete + throw StormoError.resourceTransferIncomplete } partner.incomingContinuation.yield(data) } diff --git a/Sources/PeerMeshUI/PeerBrowserView.swift b/Sources/StormoUI/PeerBrowserView.swift similarity index 98% rename from Sources/PeerMeshUI/PeerBrowserView.swift rename to Sources/StormoUI/PeerBrowserView.swift index 64a8c01..99261f9 100644 --- a/Sources/PeerMeshUI/PeerBrowserView.swift +++ b/Sources/StormoUI/PeerBrowserView.swift @@ -1,10 +1,10 @@ #if canImport(SwiftUI) -import Stromo +import Stormo import SwiftUI /// SwiftUI replacement for `MCBrowserViewController` (FR-23): lists discovered /// peers and lets the user invite them. Fully restyleable; built solely on -/// public Stromo APIs. +/// public Stormo APIs. /// /// - Warning: Experimental preview — the API is unstable and this view is a /// minimal skeleton. TODO(ui-completion): invite action with progress/error diff --git a/Tests/MPCCompatTests/MPCCompatE2ETests.swift b/Tests/MPCCompatTests/MPCCompatE2ETests.swift index f742eb9..1cdfd8d 100644 --- a/Tests/MPCCompatTests/MPCCompatE2ETests.swift +++ b/Tests/MPCCompatTests/MPCCompatE2ETests.swift @@ -2,8 +2,8 @@ import Foundation import Testing @testable import MPCCompat -import Stromo -import StromoTestKit +import Stormo +import StormoTestKit /// End-to-end bridge tests over `InMemoryTransport` (a shared `Hub`), no radios. /// @@ -304,7 +304,7 @@ extension MPCCompatE2ETests { /// retry fail instantly ("browser has already been cancelled"). @Test("Disconnect then re-invite over real QUIC (MCSession retry semantics)") func reinviteAfterDisconnectOverQUIC() async throws { - setenv("Stromo_NO_P2P", "1", 1) + setenv("STORMO_NO_P2P", "1", 1) let probe = PeerIdentity(name: "compat-retry-probe") guard QUICTransport.isTLSIdentityAvailable(for: probe) else { print("[skip] no TLS identity in this environment"); return diff --git a/Tests/MPCCompatTests/MPCCompatTests.swift b/Tests/MPCCompatTests/MPCCompatTests.swift index d528ace..c57853d 100644 --- a/Tests/MPCCompatTests/MPCCompatTests.swift +++ b/Tests/MPCCompatTests/MPCCompatTests.swift @@ -2,7 +2,7 @@ import Foundation import Testing @testable import MPCCompat -import Stromo +import Stormo @Suite("MPCCompat shim") struct MPCCompatTests { @@ -19,7 +19,7 @@ struct MPCCompatTests { // A session with no advertiser/browser attached has no CompatCore and // thus no route; send must surface that as a throw, not a trap. let session = MultipeerSession(peer: "T", service: "_compat._udp") - #expect(throws: StromoError.self) { + #expect(throws: StormoError.self) { try session.send(Data([0x01]), toPeers: [], with: .reliable) } } diff --git a/Tests/PeerMeshProtocolTests/ProtocolEngineTests.swift b/Tests/StormoProtocolTests/ProtocolEngineTests.swift similarity index 99% rename from Tests/PeerMeshProtocolTests/ProtocolEngineTests.swift rename to Tests/StormoProtocolTests/ProtocolEngineTests.swift index 1f1bdeb..ac7c481 100644 --- a/Tests/PeerMeshProtocolTests/ProtocolEngineTests.swift +++ b/Tests/StormoProtocolTests/ProtocolEngineTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import StromoProtocol +@testable import StormoProtocol /// Tier-1 tests (DD-6): the complete invitation protocol exercised with NO /// transport — no QUIC, no sockets, no async, no clocks. Two engines wired diff --git a/Tests/PeerMeshProtocolTests/ProtocolVersionTests.swift b/Tests/StormoProtocolTests/ProtocolVersionTests.swift similarity index 97% rename from Tests/PeerMeshProtocolTests/ProtocolVersionTests.swift rename to Tests/StormoProtocolTests/ProtocolVersionTests.swift index d0d6e7c..c761fe9 100644 --- a/Tests/PeerMeshProtocolTests/ProtocolVersionTests.swift +++ b/Tests/StormoProtocolTests/ProtocolVersionTests.swift @@ -1,6 +1,6 @@ import Testing -@testable import StromoProtocol +@testable import StormoProtocol @Suite("ProtocolVersion — semver interop rule") struct ProtocolVersionTests { diff --git a/Tests/PeerMeshProtocolTests/SignalCodecTests.swift b/Tests/StormoProtocolTests/SignalCodecTests.swift similarity index 91% rename from Tests/PeerMeshProtocolTests/SignalCodecTests.swift rename to Tests/StormoProtocolTests/SignalCodecTests.swift index 9965cb1..86609ee 100644 --- a/Tests/PeerMeshProtocolTests/SignalCodecTests.swift +++ b/Tests/StormoProtocolTests/SignalCodecTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import StromoProtocol +@testable import StormoProtocol @Suite("SignalCodec — verified zero-copy boundary (DD-5)") struct SignalCodecTests { @@ -32,10 +32,10 @@ struct SignalCodecTests { @Test("Verifier rejects malformed bytes as malformedSignal, never traps") func rejectsGarbage() { let garbage = Data((0..<64).map { _ in UInt8.random(in: .min ... .max) }) - #expect(throws: StromoError.malformedSignal) { + #expect(throws: StormoError.malformedSignal) { _ = try SignalCodec.decode(garbage) } - #expect(throws: StromoError.malformedSignal) { + #expect(throws: StormoError.malformedSignal) { _ = try SignalCodec.decode(Data()) } } @@ -43,7 +43,7 @@ struct SignalCodecTests { @Test("Size cap enforced before any parsing (DD-5 rule 3)") func sizeCap() { let oversized = Data(count: SignalCodec.maxControlMessageSize + 1) - #expect(throws: StromoError.malformedSignal) { + #expect(throws: StormoError.malformedSignal) { _ = try SignalCodec.decode(oversized) } } diff --git a/Tests/PeerMeshTests/DisconnectionTests.swift b/Tests/StormoTests/DisconnectionTests.swift similarity index 99% rename from Tests/PeerMeshTests/DisconnectionTests.swift rename to Tests/StormoTests/DisconnectionTests.swift index 7222b05..f61041a 100644 --- a/Tests/PeerMeshTests/DisconnectionTests.swift +++ b/Tests/StormoTests/DisconnectionTests.swift @@ -1,9 +1,9 @@ import Foundation import Testing -import StromoTestKit +import StormoTestKit -@testable import Stromo +@testable import Stormo // ============================================================================= // Disconnection detection (FR-14 bound) — the three departure shapes diff --git a/Tests/PeerMeshTests/EndToEndSessionTests.swift b/Tests/StormoTests/EndToEndSessionTests.swift similarity index 97% rename from Tests/PeerMeshTests/EndToEndSessionTests.swift rename to Tests/StormoTests/EndToEndSessionTests.swift index 6ad17b7..6f62751 100644 --- a/Tests/PeerMeshTests/EndToEndSessionTests.swift +++ b/Tests/StormoTests/EndToEndSessionTests.swift @@ -2,8 +2,8 @@ import CryptoKit import Foundation import Testing -import Stromo -import StromoTestKit +import Stormo +import StormoTestKit /// Tier-1.5 tests: the COMPLETE runtime — PeerSession effect executor, engine, /// codec, timers — over the in-memory transport. Real wire bytes (FlatBuffers @@ -103,7 +103,7 @@ struct EndToEndSessionTests { } } - await #expect(throws: StromoError.invitationDeclined) { + await #expect(throws: StormoError.invitationDeclined) { try await monitor.invite(try #require(discovered)) } await declineTask.value @@ -129,7 +129,7 @@ struct EndToEndSessionTests { } } - await #expect(throws: StromoError.invitationTimedOut) { + await #expect(throws: StormoError.invitationTimedOut) { try await monitor.invite(try #require(discovered), timeout: 0.2) } @@ -147,7 +147,7 @@ struct EndToEndSessionTests { transport: InMemoryTransport(hub: hub)) let oversized = Data(repeating: 0xD8, count: Delivery.maxDatagramPayload + 1) - await #expect(throws: StromoError.datagramTooLarge( + await #expect(throws: StormoError.datagramTooLarge( bytes: Delivery.maxDatagramPayload + 1, limit: Delivery.maxDatagramPayload) ) { try await session.send(oversized, delivery: .datagram) @@ -155,7 +155,7 @@ struct EndToEndSessionTests { // Exactly at the cap: passes the size guard (then fails on membership, // proving the guard, not the payload, was the gate above). - await #expect(throws: StromoError.peerUnreachable(identity.id)) { + await #expect(throws: StormoError.peerUnreachable(identity.id)) { try await session.send( Data(repeating: 0xD8, count: Delivery.maxDatagramPayload), delivery: .datagram) } @@ -169,7 +169,7 @@ struct EndToEndSessionTests { identity: PeerIdentity(name: "Lonely"), service: "_e2e._udp", transport: InMemoryTransport(hub: hub)) - await #expect(throws: StromoError.self) { + await #expect(throws: StormoError.self) { try await session.send(Data([1])) } await session.disconnect() @@ -396,7 +396,7 @@ struct EndToEndSessionTests { var data = Data(capacity: bytes) while data.count < bytes { data.append(block) } let url = FileManager.default.temporaryDirectory - .appendingPathComponent("Stromo-src-\(UUID().uuidString).bin") + .appendingPathComponent("Stormo-src-\(UUID().uuidString).bin") try data.prefix(bytes).write(to: url) return url } diff --git a/Tests/PeerMeshTests/IdentitySecurityTests.swift b/Tests/StormoTests/IdentitySecurityTests.swift similarity index 98% rename from Tests/PeerMeshTests/IdentitySecurityTests.swift rename to Tests/StormoTests/IdentitySecurityTests.swift index 048a1cf..5a0ba11 100644 --- a/Tests/PeerMeshTests/IdentitySecurityTests.swift +++ b/Tests/StormoTests/IdentitySecurityTests.swift @@ -2,7 +2,7 @@ import CryptoKit import Foundation import Testing -@testable import Stromo +@testable import Stormo #if canImport(Security) import Security @@ -200,7 +200,7 @@ struct IdentitySecurityTests { private static func tempDir() -> URL { let base = FileManager.default.temporaryDirectory - .appendingPathComponent("Stromo-tests-\(UUID().uuidString)") + .appendingPathComponent("Stormo-tests-\(UUID().uuidString)") try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true) return base } diff --git a/Tests/PeerMeshTests/MeshHarness.swift b/Tests/StormoTests/MeshHarness.swift similarity index 99% rename from Tests/PeerMeshTests/MeshHarness.swift rename to Tests/StormoTests/MeshHarness.swift index 869a2e3..a9deaf2 100644 --- a/Tests/PeerMeshTests/MeshHarness.swift +++ b/Tests/StormoTests/MeshHarness.swift @@ -1,7 +1,7 @@ import Foundation import Testing -import Stromo +import Stormo /// N-peer mesh test harness (QA-2 / QA-8): full-mesh formation over any /// `PeerTransport`, a traffic pump with self-verifying payloads, and the diff --git a/Tests/PeerMeshTests/MeshReliabilityTests.swift b/Tests/StormoTests/MeshReliabilityTests.swift similarity index 99% rename from Tests/PeerMeshTests/MeshReliabilityTests.swift rename to Tests/StormoTests/MeshReliabilityTests.swift index cd97429..6cb840a 100644 --- a/Tests/PeerMeshTests/MeshReliabilityTests.swift +++ b/Tests/StormoTests/MeshReliabilityTests.swift @@ -1,9 +1,9 @@ import Foundation import Testing -import StromoTestKit +import StormoTestKit -@testable import Stromo +@testable import Stormo // ============================================================================= // N-peer mesh reliability (QA-2 / QA-8) — what these suites prove and how diff --git a/Tests/PeerMeshTests/PeerHelloCodecTests.swift b/Tests/StormoTests/PeerHelloCodecTests.swift similarity index 98% rename from Tests/PeerMeshTests/PeerHelloCodecTests.swift rename to Tests/StormoTests/PeerHelloCodecTests.swift index 3a12cbd..58b752d 100644 --- a/Tests/PeerMeshTests/PeerHelloCodecTests.swift +++ b/Tests/StormoTests/PeerHelloCodecTests.swift @@ -1,8 +1,8 @@ import Foundation import Testing -@testable import Stromo -import StromoProtocol +@testable import Stormo +import StormoProtocol #if canImport(Network) && canImport(Security) diff --git a/Tests/PeerMeshTests/QUICSmokeTests.swift b/Tests/StormoTests/QUICSmokeTests.swift similarity index 98% rename from Tests/PeerMeshTests/QUICSmokeTests.swift rename to Tests/StormoTests/QUICSmokeTests.swift index 26bfc9b..f8d3315 100644 --- a/Tests/PeerMeshTests/QUICSmokeTests.swift +++ b/Tests/StormoTests/QUICSmokeTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -import Stromo +import Stormo @Suite("QUIC smoke", .serialized) struct QUICSmokeTests { @@ -77,7 +77,7 @@ extension QUICSmokeTests { guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { print("[skip] QUIC: no TLS identity in this environment"); return } - setenv("Stromo_NO_P2P", "1", 1) + setenv("STORMO_NO_P2P", "1", 1) let service = ServiceDescriptor(type: "_pmbj\(UInt16.random(in: 1000...9999))._udp") func transport() -> QUICTransport { QUICTransport(configuration: .init(discovery: .bonjour)) } let camera = PeerSession( diff --git a/Tests/PeerMeshTests/QUICStreamChurnTests.swift b/Tests/StormoTests/QUICStreamChurnTests.swift similarity index 99% rename from Tests/PeerMeshTests/QUICStreamChurnTests.swift rename to Tests/StormoTests/QUICStreamChurnTests.swift index ce65f18..26909ec 100644 --- a/Tests/PeerMeshTests/QUICStreamChurnTests.swift +++ b/Tests/StormoTests/QUICStreamChurnTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import Stromo +@testable import Stormo #if canImport(Network) && canImport(Security) diff --git a/Tests/PeerMeshTests/QUICWirePrimitiveTests.swift b/Tests/StormoTests/QUICWirePrimitiveTests.swift similarity index 98% rename from Tests/PeerMeshTests/QUICWirePrimitiveTests.swift rename to Tests/StormoTests/QUICWirePrimitiveTests.swift index 7d9f391..ceacc32 100644 --- a/Tests/PeerMeshTests/QUICWirePrimitiveTests.swift +++ b/Tests/StormoTests/QUICWirePrimitiveTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import Stromo +@testable import Stormo #if canImport(Network) && canImport(Security) diff --git a/Tests/PeerMeshTests/PeerMeshTests.swift b/Tests/StormoTests/StormoTests.swift similarity index 94% rename from Tests/PeerMeshTests/PeerMeshTests.swift rename to Tests/StormoTests/StormoTests.swift index 622dc70..f2a5aa2 100644 --- a/Tests/PeerMeshTests/PeerMeshTests.swift +++ b/Tests/StormoTests/StormoTests.swift @@ -1,10 +1,10 @@ import Foundation import Testing -@testable import Stromo +@testable import Stormo -@Suite("Stromo core") -struct StromoTests { +@Suite("Stormo core") +struct StormoTests { @Test("Identity derives PeerID from public key hash") func identityDerivesPeerID() { @@ -46,7 +46,7 @@ struct StromoTests { service: "_t._udp", transport: InMemoryTransport(hub: hub) ) - await #expect(throws: StromoError.self) { + await #expect(throws: StormoError.self) { try await session.send(Data([0x01])) } await session.disconnect() @@ -58,10 +58,10 @@ struct StromoTests { } } -import StromoTestKit +import StormoTestKit #if canImport(Network) && canImport(Security) -extension StromoTests { +extension StormoTests { @Test("Invalid Bonjour service types are rejected loudly, not silently") func bonjourTypeValidation() throws { // The class of bug that reached physical devices: a bare MPC-style diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md index 4e6021b..1e25c6f 100644 --- a/docs/IMPLEMENTATION-PLAN.md +++ b/docs/IMPLEMENTATION-PLAN.md @@ -1,4 +1,4 @@ -# Stromo Implementation Plan +# Stormo Implementation Plan **Working plan — SU-2026-WP-001** · Last updated: July 26, 2026 @@ -25,13 +25,13 @@ and the full QUIC loopback lifecycle. Three Network.framework platform findings recorded in [spike-results.md](spike-results.md) (inbound-stream readiness, inbound-connection retention, two-step FIN). -Integration proof: `remote-shutter` branch `feat/Stromo-mpccompat` (compiles + full unit suite green against MPCCompat; functional once Step 4 lands). +Integration proof: `remote-shutter` branch `feat/Stormo-mpccompat` (compiles + full unit suite green against MPCCompat; functional once Step 4 lands). --- ## Step 0 — Scaffolding ✅ -Landed: SPM package (`StromoProtocol` / `Stromo` / `MPCCompat` / `StromoUI` / `StromoTestKit`); sans-I/O `ProtocolEngine` (DD-6) with invitation/roster/messaging transitions; disciplined zero-copy FlatBuffers signaling (`signal.fbs`, `stream_header.fbs`, `SignalCodec` with verifier caps, DD-5); flake.nix pinning `flatc` = SPM runtime (exact 25.2.10); CI workflow (macOS native + iOS Simulator + Mac Catalyst + codegen-drift job); MPCCompat compile surface validated against a real app (remote-shutter). +Landed: SPM package (`StormoProtocol` / `Stormo` / `MPCCompat` / `StormoUI` / `StormoTestKit`); sans-I/O `ProtocolEngine` (DD-6) with invitation/roster/messaging transitions; disciplined zero-copy FlatBuffers signaling (`signal.fbs`, `stream_header.fbs`, `SignalCodec` with verifier caps, DD-5); flake.nix pinning `flatc` = SPM runtime (exact 25.2.10); CI workflow (macOS native + iOS Simulator + Mac Catalyst + codegen-drift job); MPCCompat compile surface validated against a real app (remote-shutter). ## Step 1 — Runtime shell + InMemoryTransport ✅ @@ -55,7 +55,7 @@ Everything QUIC needs before a TLS handshake can happen. The real driver, validated entirely on `127.0.0.1` in CI — no radios. -1. **Listener/advertiser:** `NWListener` with QUIC parameters (ALPN `"Stromo"`, local identity from Step 2) + Bonjour `.service(type:)` with `NWTXTRecord` metadata; `includePeerToPeer = true`; Local Network permission surfacing (FR-4). +1. **Listener/advertiser:** `NWListener` with QUIC parameters (ALPN `"Stormo"`, local identity from Step 2) + Bonjour `.service(type:)` with `NWTXTRecord` metadata; `includePeerToPeer = true`; Local Network permission surfacing (FR-4). 2. **Browser:** `NWBrowser(.bonjourWithTXTRecord)`, cross-interface dedup by peer id (FR-2). 3. **Connection = QUIC multiplex:** resolve **Spike S-3** (`NWMultiplexGroup` vs per-stream `NWConnection`s) empirically on loopback; control stream (signals, size-prefixed) + stream-per-message with `StreamHeader` (DD-7) + datagram flow for `.datagram` (RFC 9221). 4. **Benchmark = Spike S-6:** stream-churn rate (target ≥1,000 msg/s loopback, flat memory) wired as the nightly CI job stub in `ci.yml`. @@ -69,9 +69,9 @@ The real driver, validated entirely on `127.0.0.1` in CI — no radios. 2. **Resource transfer (FR-17):** `TransferOffer` signal + `transferChunk` stream; disk-to-disk, `Progress`, cancellation via stream reset; memory cap per QA-3. 3. **App byte streams (FR-18):** `StreamOpen` + `PeerByteStream` over dedicated streams. 4. **MPCCompat bridge:** pump `PeerSession` membership/messages/resources into `MultipeerSessionDelegate` callbacks (serial delegate queue, MCSession semantics); `NearbyServiceAdvertiser/Browser` bridging; `MultipeerSession.send/sendResource` wired (`.reliable`→`.reliableOrdered`, `.unreliable`→`.datagram`). -5. **Validation:** remote-shutter `feat/Stromo-mpccompat` runs camera↔monitor over LAN (simulator loopback first, then two Macs/devices on one network); its loopback session tests pass against the bridge over InMemoryTransport. +5. **Validation:** remote-shutter `feat/Stormo-mpccompat` runs camera↔monitor over LAN (simulator loopback first, then two Macs/devices on one network); its loopback session tests pass against the bridge over InMemoryTransport. -**Exit criteria:** remote-shutter takes a photo through Stromo on LAN; MPCCompat E2E tests in Stromo repo green over both transports. +**Exit criteria:** remote-shutter takes a photo through Stormo on LAN; MPCCompat E2E tests in Stormo repo green over both transports. ## Step 5 — Hardware spikes ⬜ (requires operator + ≥2 physical devices) @@ -84,13 +84,13 @@ The real driver, validated entirely on `127.0.0.1` in CI — no radios. ## Step 6 — Release engineering ⬜ -Initial public commit + tag `0.1.0`; license decision (MIT vs Apache-2.0 — study §8 leans Apache for the patent grant); repo description/topics + Swift Package Index submission; hosted DocC + `llms.txt`; "Migrating from MultipeerConnectivity to Stromo" guide (the TN3213-echo landing page); name availability final check (`Stromo` verified clear Jul 26); threat-model document (security-consultancy differentiator, study §8.3). +Initial public commit + tag `0.1.0`; license decision (MIT vs Apache-2.0 — study §8 leans Apache for the patent grant); repo description/topics + Swift Package Index submission; hosted DocC + `llms.txt`; "Migrating from MultipeerConnectivity to Stormo" guide (the TN3213-echo landing page); name availability final check (`Stormo` verified clear Jul 26); threat-model document (security-consultancy differentiator, study §8.3). --- ## Standing constraints (from design doc) -- Engine stays sans-I/O: no sockets/clocks/async in `StromoProtocol` (DD-6). +- Engine stays sans-I/O: no sockets/clocks/async in `StormoProtocol` (DD-6). - FlatBuffers discipline: schema evolution rules + verifier caps + pinned-together flatc/runtime (DD-5). - One suite, N transports/platforms: new features land with engine tests first, driver parity second (QA-8). - No Bluetooth claims, ever (C-2); no private API (C-6). diff --git a/docs/design-mpc-successor.md b/docs/design-mpc-successor.md index 399646d..6b4a93d 100644 --- a/docs/design-mpc-successor.md +++ b/docs/design-mpc-successor.md @@ -1,4 +1,4 @@ -# Design Document: Stromo — A Modern Peer-to-Peer Session Framework for Apple Platforms +# Design Document: Stormo — A Modern Peer-to-Peer Session Framework for Apple Platforms **Design Document — SU-2026-DD-001 (Draft 1)** @@ -9,13 +9,13 @@ | **Status** | Draft — requirements baselined; architecture in progress | | **Companion document** | *Feasibility Study: A Third-Party Replacement for Apple's MultipeerConnectivity Framework* (SU-2026-TR-001) | -*Working name `Stromo` is a placeholder pending a trademark/Swift Package Index availability check. Quality attribute requirements use the SEI six-part scenario form (source – stimulus – artifact – environment – response – response measure).* +*Working name `Stormo` is a placeholder pending a trademark/Swift Package Index availability check. Quality attribute requirements use the SEI six-part scenario form (source – stimulus – artifact – environment – response – response measure).* --- ## 1. Purpose and Scope -Stromo is an open-source Swift package providing discovery, secure session establishment, and data exchange among nearby Apple devices, replacing the deprecated MultipeerConnectivity (MPC) framework. It consists of: +Stormo is an open-source Swift package providing discovery, secure session establishment, and data exchange among nearby Apple devices, replacing the deprecated MultipeerConnectivity (MPC) framework. It consists of: 1. a **modern core** built on Network.framework with Swift 6 structured concurrency (`async`/`await`, `AsyncSequence`), QUIC as the primary transport; and 2. an **`MPCCompat` shim** offering a near-drop-in migration surface for existing MPC code. @@ -80,7 +80,7 @@ Priority: **M** = must (1.0), **S** = should (1.x), **C** = could (post-1.0). ### 3.6 UI Components and Compatibility -- **FR-23 (S)** SwiftUI components: a peer browser/picker view and an invitation-consent sheet (replacing `MCBrowserViewController` / `MCAdvertiserAssistant`), fully restyleable, built solely on public Stromo APIs. +- **FR-23 (S)** SwiftUI components: a peer browser/picker view and an invitation-consent sheet (replacing `MCBrowserViewController` / `MCAdvertiserAssistant`), fully restyleable, built solely on public Stormo APIs. - **FR-24 (S)** **`MPCCompat` module:** near-drop-in analogs of `MCSession`, `MCPeerID`, `MCNearbyServiceAdvertiser/Browser` delegate semantics (original type names, no `MC` prefix; migration = mechanical rename), including `NSStream`-bridged streams and 8-peer-limit emulation flag for behavioral parity testing. - **FR-25 (C)** Pluggable transport backends behind a `PeerTransport` protocol: QUIC/Bonjour (1.0), TCP+TLS fallback profile (contingency), Wi-Fi Aware backend for paired-device scenarios on iOS 26+ hardware (post-1.0). @@ -99,15 +99,15 @@ Priority: **M** = must (1.0), **S** = should (1.x), **C** = could (post-1.0). | **QA-7** | Modifiability (transport evolution) | Maintainer adds a Wi-Fi Aware backend → change confined to a new `PeerTransport` conformance + capability gating → **zero changes to session, security, messaging, or UI modules; core compiles without the new backend**. | | **QA-8** | Testability | CI runs on every commit without physical devices → handshake, mesh membership, topology election, timers, and transfer signaling execute as deterministic sans-I/O engine simulations (DD-6), plus real-QUIC loopback integration tests → **≥ 85% line coverage of non-radio code; full 32-peer mesh simulation in < 60 s on a CI runner; any recorded device-session input log replays deterministically into the engine**. | | **QA-9** | Usability (migration) | An MPC app of moderate complexity (advertise+browse+session+send) migrates to `MPCCompat` → **≤ 1 day of work, no architectural changes; diff limited to imports/type renames + Info.plist keys**, validated by a published ported sample app. | -| **QA-10** | Compatibility (OS floor) | App integrates Stromo 1.0 → runs on **iOS 15+/iPadOS 15+/macOS 12+/tvOS 15+/visionOS 1+** (QUIC floor); Wi-Fi Aware features degrade gracefully with runtime capability checks. | -| **QA-11** | Interoperability (protocol evolution) | A device running an app built against Stromo 1.0 joins a session whose other peers run Stromo 1.x (newer signaling schema) → signaling interoperates via FlatBuffers evolution rules (DD-5): unknown fields skipped, unknown union variants ignored-and-logged → **session forms and all 1.0-era features function; zero connection failures attributable to schema version skew, proven by golden-file cross-version tests in CI**. | +| **QA-10** | Compatibility (OS floor) | App integrates Stormo 1.0 → runs on **iOS 15+/iPadOS 15+/macOS 12+/tvOS 15+/visionOS 1+** (QUIC floor); Wi-Fi Aware features degrade gracefully with runtime capability checks. | +| **QA-11** | Interoperability (protocol evolution) | A device running an app built against Stormo 1.0 joins a session whose other peers run Stormo 1.x (newer signaling schema) → signaling interoperates via FlatBuffers evolution rules (DD-5): unknown fields skipped, unknown union variants ignored-and-logged → **session forms and all 1.0-era features function; zero connection failures attributable to schema version skew, proven by golden-file cross-version tests in CI**. | --- ## 5. Constraints (platform-imposed) - **C-1** AWDL is reachable only via `NWParameters.includePeerToPeer`; path selection is Happy-Eyeballs-driven and cannot be forced to prefer the P2P path. -- **C-2** No Bluetooth data transport exists for third-party apps; Stromo makes no Bluetooth claims. +- **C-2** No Bluetooth data transport exists for third-party apps; Stormo makes no Bluetooth claims. - **C-3** QUIC in Network.framework supports **PKI only, not TLS-PSK** (TN3213) → drives FR-21's identity design. - **C-4** `NSLocalNetworkUsageDescription` + `NSBonjourServices` required; standard Bonjour needs no multicast entitlement; Wi-Fi Aware backend would additionally need its entitlement + plist declarations. - **C-5** iOS backgrounding suspends advertising/browsing and connections (except future Wi-Fi Aware paired scenarios); the API must make suspension/resume explicit. @@ -137,7 +137,7 @@ One QUIC connection per peer pair carries everything: Because of C-3, the WWDC19 passcode/PSK pattern is unavailable on QUIC. Instead: every peer has a persistent P-256 identity (FR-20); TLS always terminates against the peer's self-signed certificate; *authentication* is a policy layered on *encryption* (FR-21). -**The governing principle is MPC-ergonomics parity (FR-21):** MPC's celebrated simplicity was, concretely, *encrypted-but-unauthenticated by default* — `encryptionPreference = .required` auto-generated keys, and the de facto authorization was the user tapping Accept on the invitation (`didReceiveCertificate` accepted everyone unless overridden, and rarely was). Stromo's `.automatic` default reproduces exactly that developer and user experience on QUIC: the identity is generated and persisted silently, the self-signed certificate exists only to satisfy the QUIC handshake, the verify block accepts and records the peer key hash, and invitation consent is the authorization step. Zero configuration, no pairing ceremony — and two silent upgrades over MPC: unforgeable key-derived peer IDs, and TOFU continuity warnings if a known peer's key ever changes. +**The governing principle is MPC-ergonomics parity (FR-21):** MPC's celebrated simplicity was, concretely, *encrypted-but-unauthenticated by default* — `encryptionPreference = .required` auto-generated keys, and the de facto authorization was the user tapping Accept on the invitation (`didReceiveCertificate` accepted everyone unless overridden, and rarely was). Stormo's `.automatic` default reproduces exactly that developer and user experience on QUIC: the identity is generated and persisted silently, the self-signed certificate exists only to satisfy the QUIC handshake, the verify block accepts and records the peer key hash, and invitation consent is the authorization step. Zero configuration, no pairing ceremony — and two silent upgrades over MPC: unforgeable key-derived peer IDs, and TOFU continuity warnings if a known peer's key ever changes. The opt-in `.pairingCode` mode binds a short user-visible code to the TLS transcript/exporter secret covering both certificates — a MITM cannot present matching codes on both sides. This is strictly stronger than MPC (which authorized on cleartext hostname) and stronger than bare PSK (identity continuity persists across sessions). It exists for apps that genuinely need active-MITM resistance; it is deliberately not the default, because MPC never had it either and imposing a ceremony on every session would betray the framework's reason to exist. @@ -177,7 +177,7 @@ All event delivery via `AsyncSequence`; all operations `async` with cancellation ### DD-5: Disciplined FlatBuffers for the signaling plane — **adopted** -All control-plane messages (identity bootstrap/PeerHello, invitation, accept/decline, roster gossip, keepalive, topology election, transfer/stream announcements) are FlatBuffers, generated from `.fbs` schemas that are the single source of truth in the repository. Version negotiation is semver carried once per connection in `PeerHello` (same-major interop; a mismatch surfaces as a typed "upgrade required" error naming both versions) — which is why the QUIC ALPN is frozen: bumping it would fail the handshake before hello and hide the diagnosis. `peer_hello.fbs` is therefore a frozen bootstrap contract every major version must parse. Application payloads (FR-15..FR-18) remain opaque bytes — apps serialize however they like; FlatBuffers governs only Stromo's own protocol. +All control-plane messages (identity bootstrap/PeerHello, invitation, accept/decline, roster gossip, keepalive, topology election, transfer/stream announcements) are FlatBuffers, generated from `.fbs` schemas that are the single source of truth in the repository. Version negotiation is semver carried once per connection in `PeerHello` (same-major interop; a mismatch surfaces as a typed "upgrade required" error naming both versions) — which is why the QUIC ALPN is frozen: bumping it would fail the handshake before hello and hide the diagnosis. `peer_hello.fbs` is therefore a frozen bootstrap contract every major version must parse. Application payloads (FR-15..FR-18) remain opaque bytes — apps serialize however they like; FlatBuffers governs only Stormo's own protocol. **Rationale:** zero-copy access suits a hot control path on many concurrent connections; the schema compiler enforces cross-version wire compatibility mechanically (vs. convention-only discipline in CBOR/JSON); the built-in **verifier** gives structural validation of bytes received from *not-yet-authenticated* peers during the handshake — a security property the signaling path specifically needs; official Swift support ships via SPM (`google/flatbuffers`, `flatc --swift`). @@ -221,7 +221,7 @@ ProtocolEngine.handle(Input) -> [Effect] // synchronous, deterministic > **Mesh reliability — validated in CI (2026-07).** The QA-2/QA-8 mesh > targets are now asserted on every commit by -> `Tests/StromoTests/MeshReliabilityTests.swift`: a monotonic N = 3…32 +> `Tests/StormoTests/MeshReliabilityTests.swift`: a monotonic N = 3…32 > full-mesh sweep over `InMemoryTransport` (formation convergence, exactly-once > delivery, payload integrity, sender attribution, per-sender ordering under a > reordering transport, clean drain) plus a 3-peer **real-QUIC loopback** @@ -233,9 +233,9 @@ ProtocolEngine.handle(Input) -> [Effect] // synchronous, deterministic > does NOT cover: real radios/AWDL (tier 3) and relayed topologies (QA-2's > 128-peer target). -**Platform execution matrix (tiers 1–2):** the same test bundles run on every shipping target — macOS native via `swift test` (fast loop + coverage), and per-destination via `xcodebuild test -scheme Stromo-Package` against **iOS Simulator** and **Mac Catalyst** (tvOS/visionOS simulators when installed). This is the pattern used by swift-nio, Apple's own packages, and the major Swift OSS projects: one suite, N destinations, no per-platform test code. Network.framework QUIC works over loopback in the iOS Simulator and Catalyst, so tier 2 runs on all of them without radios. Aspirational tier-1 extension (FoundationDB-style deterministic simulation testing): seeded random command/interleaving exploration against engine invariants — the seed arrives as engine input, so any nightly failure reproduces exactly from its seed. +**Platform execution matrix (tiers 1–2):** the same test bundles run on every shipping target — macOS native via `swift test` (fast loop + coverage), and per-destination via `xcodebuild test -scheme Stormo-Package` against **iOS Simulator** and **Mac Catalyst** (tvOS/visionOS simulators when installed). This is the pattern used by swift-nio, Apple's own packages, and the major Swift OSS projects: one suite, N destinations, no per-platform test code. Network.framework QUIC works over loopback in the iOS Simulator and Catalyst, so tier 2 runs on all of them without radios. Aspirational tier-1 extension (FoundationDB-style deterministic simulation testing): seeded random command/interleaving exploration against engine invariants — the seed arrives as engine input, so any nightly failure reproduces exactly from its seed. -Module consequence: the engine, typed `Signal` model, and codec live in a dedicated **`StromoProtocol`** target whose only dependency is FlatBuffers; `Stromo` (runtime shell + drivers) depends on it — **the shipped `Stromo` product includes the QUIC integration**; consumers get working transport out of the box, and the protocol target stays importable on its own for tests and tooling. Under the AI-driven development model (study §4.4) this is the highest-leverage structural decision in the project: it moves the majority of correctness into tier 1, which is agent-buildable and agent-testable with zero device time. +Module consequence: the engine, typed `Signal` model, and codec live in a dedicated **`StormoProtocol`** target whose only dependency is FlatBuffers; `Stormo` (runtime shell + drivers) depends on it — **the shipped `Stormo` product includes the QUIC integration**; consumers get working transport out of the box, and the protocol target stays importable on its own for tests and tooling. Under the AI-driven development model (study §4.4) this is the highest-leverage structural decision in the project: it moves the majority of correctness into tier 1, which is agent-buildable and agent-testable with zero device time. ### DD-7: Stream-per-message data plane (MoQ-inspired) — **adopted** @@ -275,13 +275,13 @@ iOS 16+/macOS 13+) recover loss-independence where latency matters. ### DD-8: Custom session protocol over raw QUIC, not libp2p — **adopted** -The obvious alternative to a bespoke protocol is libp2p (the IPFS-lineage modular P2P stack), especially since Stromo convergently shares several of its ideas (key-derived peer IDs, transport abstraction, QUIC). Rejected as the foundation, for recorded reasons: +The obvious alternative to a bespoke protocol is libp2p (the IPFS-lineage modular P2P stack), especially since Stormo convergently shares several of its ideas (key-derived peer IDs, transport abstraction, QUIC). Rejected as the foundation, for recorded reasons: -1. **No help where help is needed.** Stromo's hard problems are Apple-platform-specific — Bonjour over peer-to-peer Wi-Fi, `includePeerToPeer`, Local Network permission UX, AWDL lifecycle, future Wi-Fi Aware. No libp2p transport addresses any of it; adopting libp2p means building that custom transport anyway, inside someone else's abstraction stack. -2. **Wrong-scale machinery.** DHT discovery, NAT traversal, relays, and gossipsub target internet-scale adversarial networks; Stromo sessions are 2–32 co-located consenting devices (§1 scope). The machinery costs dependency surface, binary size, and audit scope without serving a requirement. On QUIC, libp2p's Noise + muxer + multistream-select layering is additionally redundant — QUIC provides encryption, muxing, and streams natively. +1. **No help where help is needed.** Stormo's hard problems are Apple-platform-specific — Bonjour over peer-to-peer Wi-Fi, `includePeerToPeer`, Local Network permission UX, AWDL lifecycle, future Wi-Fi Aware. No libp2p transport addresses any of it; adopting libp2p means building that custom transport anyway, inside someone else's abstraction stack. +2. **Wrong-scale machinery.** DHT discovery, NAT traversal, relays, and gossipsub target internet-scale adversarial networks; Stormo sessions are 2–32 co-located consenting devices (§1 scope). The machinery costs dependency surface, binary size, and audit scope without serving a requirement. On QUIC, libp2p's Noise + muxer + multistream-select layering is additionally redundant — QUIC provides encryption, muxing, and streams natively. 3. **swift-libp2p is not production-viable** (feasibility study §3.1): experimental by its own declaration, pre-1.0, minimal maintainer base, TCP/WebSocket-centric — the Network.framework QUIC transport would still be ours to write. 4. **The product layer doesn't exist in libp2p:** invitation/consent semantics, rosters, `Progress` resource transfer, and `MPCCompat` are application protocol either way. -5. **Interop parity:** cross-platform reach via libp2p requires a mutually supported transport, which today means infrastructure LAN — where Stromo's QUIC + FlatBuffers wire protocol is equally portable (quiche/Cronet, FlatBuffers Kotlin). The infrastructure-less cross-platform path is blocked by radio availability (C-1/C-2, Wi-Fi Aware version gap), not by protocol choice. +5. **Interop parity:** cross-platform reach via libp2p requires a mutually supported transport, which today means infrastructure LAN — where Stormo's QUIC + FlatBuffers wire protocol is equally portable (quiche/Cronet, FlatBuffers Kotlin). The infrastructure-less cross-platform path is blocked by radio availability (C-1/C-2, Wi-Fi Aware version gap), not by protocol choice. **Adopted from libp2p instead:** the PeerID identity encoding (multihash of the encoded public key, CIDv1 text representation) SHALL replace the ad-hoc SHA-256-of-raw-key format before the wire protocol freezes — near-zero cost now, and it keeps a future libp2p bridge (post-1.0 internet reach via relays) identity-compatible. Revisit trigger: if post-1.0 scope expands to internet-wide P2P (NAT traversal/relays), evaluate bridging to libp2p protocols rather than reinventing that tier. @@ -291,11 +291,11 @@ The obvious alternative to a bespoke protocol is libp2p (the IPFS-lineage modula ```mermaid flowchart TD - UI["StromoUI
SwiftUI picker, consent sheet
[FR-23]"] + UI["StormoUI
SwiftUI picker, consent sheet
[FR-23]"] COMPAT["MPCCompat
MCSession-shaped shim, NSStream bridge
[FR-24]"] - API["Stromo public API
PeerSession · Advertiser · Browser ·
Message · ResourceTransfer · PeerStream"] + API["Stormo public API
PeerSession · Advertiser · Browser ·
Message · ResourceTransfer · PeerStream"] - subgraph PROTO["StromoProtocol — sans-I/O engine [DD-6] (no sockets, no clocks, no async)"] + subgraph PROTO["StormoProtocol — sans-I/O engine [DD-6] (no sockets, no clocks, no async)"] ENGINE["ProtocolEngine
handle(Input) → [Effect]
invitation · roster · tie-break ·
timers · topology [DD-3]"] SIGNAL["Signal model +
FlatBuffers codec + verifier [DD-5]"] end @@ -466,7 +466,7 @@ each self-identified by its first byte (the stream tag): |---|---|---| | **App messages** — video frames, game state, anything via `send(_:delivery:)`, up to 1 MiB, all delivery modes | the sender's **message channel**: one long-lived stream per direction, framed `StreamHeader` + payload units | `0x02` | | **App messages over 1 MiB** (FR-15 allows 16 MB) | a **dedicated stream** per message, retired when spent | `0x01` | -| **Stromo's own protocol** — `PeerHello`, invitations, accept/decline, roster gossip, keepalives, transfer offers, stream-open announcements | the **control stream**: one bidirectional stream, dialer-opened first, total order (DD-5) | `0x00` | +| **Stormo's own protocol** — `PeerHello`, invitations, accept/decline, roster gossip, keepalives, transfer offers, stream-open announcements | the **control stream**: one bidirectional stream, dialer-opened first, total order (DD-5) | `0x00` | | **File transfers** (`sendResource`, FR-17) | offer/accept signals on the control stream; the bytes on a **dedicated stream** per transfer (disk-to-disk, own flow control) | `0x00` + `0x01` | | **App byte streams** (`openStream`, FR-18) | a **dedicated stream** each, duplex, app-controlled lifetime | `0x01` | @@ -512,7 +512,7 @@ directions. sequenceDiagram participant B as Inviter — QUIC/TLS client participant A as Advertiser — QUIC/TLS server (NWListener) - B->>A: QUIC ClientHello (ALPN Stromo/1) + B->>A: QUIC ClientHello (ALPN Stormo/1) A->>B: server cert: self-signed P-256 leaf + CertificateRequest (mTLS) B->>A: client cert: self-signed P-256 leaf Note over B,A: Each side runs the SAME verify block — PKI ignored,
leaf DER → TrustEvaluator with the session TrustPolicy
(.automatic TOFU / .pairingCode / .pinned) diff --git a/flake.nix b/flake.nix index 60e7a39..7a665a6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Stromo — dev shell (auxiliary tooling; Swift toolchain comes from Xcode)"; + description = "Stormo — dev shell (auxiliary tooling; Swift toolchain comes from Xcode)"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; @@ -17,7 +17,7 @@ # `google/flatbuffers` runtime pinned `exact:` in Package.swift — the # generated code and the runtime that reads it are one unit; bump both # together. Regenerate the committed sources with: - # flatc --swift -o Sources/StromoProtocol/Generated Schemas/*.fbs + # flatc --swift -o Sources/StormoProtocol/Generated Schemas/*.fbs pkgs.flatbuffers ]; }; From 746639a84d143fdc04f082fd15c76cc1941d27f4 Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Tue, 28 Jul 2026 00:31:19 -0700 Subject: [PATCH 3/3] Fix post-rename CI cache poisoning and a dedicated-counter suite race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent failures surfaced after the repo rename: 1. CI macOS: "PCH was compiled with module cache path .../PeerMesh/PeerMesh". The SwiftPM cache restore-keys prefix matched pre-rename caches, whose PCHs embed the old workspace path — clang rejects them at the new /Stormo/Stormo checkout. The cache key now carries a 'stormo' salt so no pre-rename cache can restore. 2. macOS xcodebuild: "Sub-cap messages" expected dedicatedOpened == openedBefore+2 but read 16. The churn suite and the loopback-QUIC mesh suite both move the process-global dedicated-stream counters; each was .serialized internally but they ran concurrently with each other, so the mesh triangle's ladder (which crosses channelMaxPayload) polluted the churn suite's exact-delta assertions. Both suites now nest under a shared .serialized umbrella (DedicatedStreamCounterSuites) — the trait applies recursively, so counter-moving suites can never overlap. Verified: swift test 72/72 (18 suites); xcodebuild test -scheme Stormo-Package -destination platform=macOS SUCCEEDED (was the repro). Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 6 +- Tests/StormoTests/MeshHarness.swift | 11 + Tests/StormoTests/MeshReliabilityTests.swift | 131 ++++----- Tests/StormoTests/QUICStreamChurnTests.swift | 287 ++++++++++--------- 4 files changed, 227 insertions(+), 208 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 936d50d..f0a0bd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,10 @@ jobs: uses: actions/cache@v4 with: path: .build - key: spm-${{ runner.os }}-${{ hashFiles('Package.swift') }} - restore-keys: spm-${{ runner.os }}- + # 'stormo' salt: pre-rename caches embed the old /PeerMesh/ workspace + # path in their PCHs; restoring one poisons every clang module build. + key: spm-stormo-${{ runner.os }}-${{ hashFiles('Package.swift') }} + restore-keys: spm-stormo-${{ runner.os }}- - name: Run tests with coverage run: swift test --enable-code-coverage env: diff --git a/Tests/StormoTests/MeshHarness.swift b/Tests/StormoTests/MeshHarness.swift index a9deaf2..0e4eddb 100644 --- a/Tests/StormoTests/MeshHarness.swift +++ b/Tests/StormoTests/MeshHarness.swift @@ -324,3 +324,14 @@ func meshDrained(_ sessions: [PeerSession], timeout: TimeInterval = 10) async -> } return true } + +// MARK: - Dedicated-stream counter serialization + +/// Umbrella for every suite that asserts on the process-global dedicated- +/// stream counters (`QUICConnection.dedicatedOpened`/`.dedicatedRetired`). +/// The churn suite's exact-delta expectations are only sound while no other +/// suite opens oversized-payload streams, and `.serialized` applies +/// recursively, so member suites never overlap. Any new suite that sends +/// payloads above `channelMaxPayload` over real QUIC must live here. +@Suite(.serialized) +enum DedicatedStreamCounterSuites {} diff --git a/Tests/StormoTests/MeshReliabilityTests.swift b/Tests/StormoTests/MeshReliabilityTests.swift index 6cb840a..3875ed8 100644 --- a/Tests/StormoTests/MeshReliabilityTests.swift +++ b/Tests/StormoTests/MeshReliabilityTests.swift @@ -130,71 +130,74 @@ struct InMemoryMeshReliabilityTests { #if canImport(Network) && canImport(Security) -/// Tier 2: the same contract over REAL loopback QUIC (Rendezvous discovery — -/// no mDNS). What the in-memory tier is blind to lives here: the per-direction -/// message channel vs dedicated-stream routing at `channelMaxPayload` -/// (failure mode 13), the FIN two-step, and stream retirement. Serialized: -/// binds real sockets and reads the global dedicated-stream counters. -@Suite("Mesh reliability over loopback QUIC", .serialized) -struct QUICMeshReliabilityTests { - - @Test("3-peer triangle: full size ladder across both stream paths") - func quicTriangle() async throws { - guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { - print("[skip] QUIC: no TLS identity in this environment") - return +extension DedicatedStreamCounterSuites { + + /// Tier 2: the same contract over REAL loopback QUIC (Rendezvous discovery — + /// no mDNS). What the in-memory tier is blind to lives here: the per-direction + /// message channel vs dedicated-stream routing at `channelMaxPayload` + /// (failure mode 13), the FIN two-step, and stream retirement. Lives in the + /// umbrella: the ladder crosses `channelMaxPayload`, moving the global + /// dedicated-stream counters the churn suite asserts exact deltas on. + @Suite("Mesh reliability over loopback QUIC", .serialized) + struct QUICMeshReliabilityTests { + + @Test("3-peer triangle: full size ladder across both stream paths") + func quicTriangle() async throws { + guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { + print("[skip] QUIC: no TLS identity in this environment") + return + } + let rendezvous = Rendezvous() + let sessions = (0 ..< 3).map { i in + PeerSession( + identity: PeerIdentity(name: "peer-\(i)"), + service: "_mesh._udp", + transport: QUICTransport(configuration: .init(discovery: .rendezvous(rendezvous)))) + } + + let formation = try await formMesh(sessions) + print("MESH quic n=3 formed in \(String(format: "%.2f", formation))s") + + // The ladder pinned to the transport's seams: channel rungs, the exact + // routing boundary (last-on-channel / first-dedicated), and a payload + // comfortably inside the dedicated-stream path. Cycling it makes + // consecutive seqs of ONE sender cross the boundary, so for + // `.reliableOrdered` a small channel message physically beats the big + // dedicated-stream message preceding it in sequence — the reorder + // buffer must hold and release (DD-7). Nothing else forces that. + let ladder = [ + 64, 1_200, 4_500, + QUICConnection.channelMaxPayload, + QUICConnection.channelMaxPayload + 1, + 2 << 20, + ] + let messagesPerSender = 12 // two full ladder cycles per sender + + for delivery in [Delivery.reliable, .reliableOrdered] { + let (inboxes, corrupt, metrics) = try await pump( + sessions, delivery: delivery, messagesPerSender: messagesPerSender, + size: { seq in ladder[seq % ladder.count] }, timeout: 120) + assertMeshDelivery( + inboxes: inboxes, corrupt: corrupt, delivery: delivery, + messagesPerSender: messagesPerSender, ordered: delivery == .reliableOrdered) + print("MESH quic n=3 \(delivery): \(metrics.summary)") + } + + for session in sessions { await session.disconnect() } + #expect(await meshDrained(sessions), "membership did not drain after disconnect") + + // No zombie dedicated streams (failure mode 13). Retirement is + // asynchronous, so assert eventual convergence, never deltas. + let deadline = Date().addingTimeInterval(30) + while QUICConnection.dedicatedOpened.value != QUICConnection.dedicatedRetired.value, + Date() < deadline + { + try await Task.sleep(nanoseconds: 100_000_000) + } + let opened = QUICConnection.dedicatedOpened.value + let retired = QUICConnection.dedicatedRetired.value + #expect(retired == opened, "zombie streams: \(opened - retired) opened, never retired") } - let rendezvous = Rendezvous() - let sessions = (0 ..< 3).map { i in - PeerSession( - identity: PeerIdentity(name: "peer-\(i)"), - service: "_mesh._udp", - transport: QUICTransport(configuration: .init(discovery: .rendezvous(rendezvous)))) - } - - let formation = try await formMesh(sessions) - print("MESH quic n=3 formed in \(String(format: "%.2f", formation))s") - - // The ladder pinned to the transport's seams: channel rungs, the exact - // routing boundary (last-on-channel / first-dedicated), and a payload - // comfortably inside the dedicated-stream path. Cycling it makes - // consecutive seqs of ONE sender cross the boundary, so for - // `.reliableOrdered` a small channel message physically beats the big - // dedicated-stream message preceding it in sequence — the reorder - // buffer must hold and release (DD-7). Nothing else forces that. - let ladder = [ - 64, 1_200, 4_500, - QUICConnection.channelMaxPayload, - QUICConnection.channelMaxPayload + 1, - 2 << 20, - ] - let messagesPerSender = 12 // two full ladder cycles per sender - - for delivery in [Delivery.reliable, .reliableOrdered] { - let (inboxes, corrupt, metrics) = try await pump( - sessions, delivery: delivery, messagesPerSender: messagesPerSender, - size: { seq in ladder[seq % ladder.count] }, timeout: 120) - assertMeshDelivery( - inboxes: inboxes, corrupt: corrupt, delivery: delivery, - messagesPerSender: messagesPerSender, ordered: delivery == .reliableOrdered) - print("MESH quic n=3 \(delivery): \(metrics.summary)") - } - - for session in sessions { await session.disconnect() } - #expect(await meshDrained(sessions), "membership did not drain after disconnect") - - // No zombie dedicated streams (failure mode 13). Retirement is - // asynchronous and the churn suite moves the same global counters, so - // assert eventual convergence, never deltas. - let deadline = Date().addingTimeInterval(30) - while QUICConnection.dedicatedOpened.value != QUICConnection.dedicatedRetired.value, - Date() < deadline - { - try await Task.sleep(nanoseconds: 100_000_000) - } - let opened = QUICConnection.dedicatedOpened.value - let retired = QUICConnection.dedicatedRetired.value - #expect(retired == opened, "zombie streams: \(opened - retired) opened, never retired") } } diff --git a/Tests/StormoTests/QUICStreamChurnTests.swift b/Tests/StormoTests/QUICStreamChurnTests.swift index 26909ec..71c5413 100644 --- a/Tests/StormoTests/QUICStreamChurnTests.swift +++ b/Tests/StormoTests/QUICStreamChurnTests.swift @@ -5,165 +5,168 @@ import Testing #if canImport(Network) && canImport(Security) -/// Message-churn soak (failure mode 13). Messages ride the persistent -/// channel, so churn no longer consumes the connection's lifetime stream -/// budget; oversized payloads take a dedicated stream, which must be retired -/// (no zombie handles). Related: TODO(churn-benchmark) measures the ceiling; -/// this asserts correctness at volume. -@Suite("QUIC stream churn", .serialized) -struct QUICStreamChurnTests { - - // No .timeLimit trait: it is iOS 16+ API and the floor is iOS 15 (the CI - // job timeout bounds a hang instead). - @Test("A session survives message churn far past the old per-stream budget") - func churnPastInitialCredit() async throws { - guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { - print("[skip] QUIC: no TLS identity in this environment"); return - } - let smallCount = 2_200 // would exceed the old 2048 stream budget - let bigPayload = Data(repeating: 0xB1, count: QUICConnection.channelMaxPayload + 1) - - let rv = Rendezvous() - func t() -> QUICTransport { QUICTransport(configuration: .init(discovery: .rendezvous(rv))) } - let camera = PeerSession(identity: PeerIdentity(name: "Camera"), service: "_churn._udp", transport: t()) - let monitor = PeerSession(identity: PeerIdentity(name: "Monitor"), service: "_churn._udp", transport: t()) - - try await camera.startAdvertising(metadata: [:]) - let accept = Task { for await inv in camera.invitations { await inv.accept(); break } } - try await monitor.startBrowsing() - var discovered: DiscoveredPeer? - for await event in monitor.discoveries { - if case .found(let peer) = event { discovered = peer; break } - } - _ = try await monitor.invite(try #require(discovered), context: Data()) - await accept.value - - let received = Task<(small: Int, big: Int), Never> { - var small = 0 - var big = 0 - for await message in camera.messages { - if message.payload.count > QUICConnection.channelMaxPayload { big += 1 } else { small += 1 } - if small == smallCount && big == 1 { break } +extension DedicatedStreamCounterSuites { + + /// Message-churn soak (failure mode 13). Messages ride the persistent + /// channel, so churn no longer consumes the connection's lifetime stream + /// budget; oversized payloads take a dedicated stream, which must be retired + /// (no zombie handles). Related: TODO(churn-benchmark) measures the ceiling; + /// this asserts correctness at volume. + @Suite("QUIC stream churn", .serialized) + struct QUICStreamChurnTests { + + // No .timeLimit trait: it is iOS 16+ API and the floor is iOS 15 (the CI + // job timeout bounds a hang instead). + @Test("A session survives message churn far past the old per-stream budget") + func churnPastInitialCredit() async throws { + guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { + print("[skip] QUIC: no TLS identity in this environment"); return + } + let smallCount = 2_200 // would exceed the old 2048 stream budget + let bigPayload = Data(repeating: 0xB1, count: QUICConnection.channelMaxPayload + 1) + + let rv = Rendezvous() + func t() -> QUICTransport { QUICTransport(configuration: .init(discovery: .rendezvous(rv))) } + let camera = PeerSession(identity: PeerIdentity(name: "Camera"), service: "_churn._udp", transport: t()) + let monitor = PeerSession(identity: PeerIdentity(name: "Monitor"), service: "_churn._udp", transport: t()) + + try await camera.startAdvertising(metadata: [:]) + let accept = Task { for await inv in camera.invitations { await inv.accept(); break } } + try await monitor.startBrowsing() + var discovered: DiscoveredPeer? + for await event in monitor.discoveries { + if case .found(let peer) = event { discovered = peer; break } + } + _ = try await monitor.invite(try #require(discovered), context: Data()) + await accept.value + + let received = Task<(small: Int, big: Int), Never> { + var small = 0 + var big = 0 + for await message in camera.messages { + if message.payload.count > QUICConnection.channelMaxPayload { big += 1 } else { small += 1 } + if small == smallCount && big == 1 { break } + } + return (small, big) } - return (small, big) - } - // Bounded concurrency: the shape of a real frame-streaming sender. - let width = 32 - var sendFailure: (index: Int, error: any Error)? - await withTaskGroup(of: (Int, (any Error)?).self) { group in - var next = 1 - var inFlight = 0 - while next <= smallCount || inFlight > 0 { - while next <= smallCount && inFlight < width { - let n = next - next += 1 - inFlight += 1 - group.addTask { - do { - try await monitor.send(Data([UInt8(n % 256)]), delivery: .reliable) - return (n, nil) - } catch { - return (n, error) + // Bounded concurrency: the shape of a real frame-streaming sender. + let width = 32 + var sendFailure: (index: Int, error: any Error)? + await withTaskGroup(of: (Int, (any Error)?).self) { group in + var next = 1 + var inFlight = 0 + while next <= smallCount || inFlight > 0 { + while next <= smallCount && inFlight < width { + let n = next + next += 1 + inFlight += 1 + group.addTask { + do { + try await monitor.send(Data([UInt8(n % 256)]), delivery: .reliable) + return (n, nil) + } catch { + return (n, error) + } } } - } - if let (n, error) = await group.next() { - inFlight -= 1 - if let error, sendFailure == nil { sendFailure = (n, error) } + if let (n, error) = await group.next() { + inFlight -= 1 + if let error, sendFailure == nil { sendFailure = (n, error) } + } } } - } - // Oversized message: dedicated-stream fallback (FR-15 16 MB support). - try await monitor.send(bigPayload, delivery: .reliable) + // Oversized message: dedicated-stream fallback (FR-15 16 MB support). + try await monitor.send(bigPayload, delivery: .reliable) - if let sendFailure { - Issue.record("send #\(sendFailure.index) failed: \(sendFailure.error)") - received.cancel() - } + if let sendFailure { + Issue.record("send #\(sendFailure.index) failed: \(sendFailure.error)") + received.cancel() + } - let counts = await received.value - #expect(counts.small == smallCount) - #expect(counts.big == 1) - await monitor.disconnect() - await camera.disconnect() - - // No zombie dedicated streams (failure mode 13): the oversized message - // opened one handle per side; every opened handle must retire. - // Retirement is asynchronous, so poll briefly before judging. - let deadline = Date().addingTimeInterval(10) - while QUICConnection.dedicatedOpened.value != QUICConnection.dedicatedRetired.value, - Date() < deadline - { - try await Task.sleep(nanoseconds: 100_000_000) + let counts = await received.value + #expect(counts.small == smallCount) + #expect(counts.big == 1) + await monitor.disconnect() + await camera.disconnect() + + // No zombie dedicated streams (failure mode 13): the oversized message + // opened one handle per side; every opened handle must retire. + // Retirement is asynchronous, so poll briefly before judging. + let deadline = Date().addingTimeInterval(10) + while QUICConnection.dedicatedOpened.value != QUICConnection.dedicatedRetired.value, + Date() < deadline + { + try await Task.sleep(nanoseconds: 100_000_000) + } + let opened = QUICConnection.dedicatedOpened.value + let retired = QUICConnection.dedicatedRetired.value + #expect(opened >= 2) // big message: sender + receiver handles + #expect(retired == opened, "zombie streams: \(opened - retired) opened, never retired") } - let opened = QUICConnection.dedicatedOpened.value - let retired = QUICConnection.dedicatedRetired.value - #expect(opened >= 2) // big message: sender + receiver handles - #expect(retired == opened, "zombie streams: \(opened - retired) opened, never retired") - } - // Lives in this serialized suite: it reads the global dedicated-stream - // counters, which the churn test also moves — parallel suites would race. - @Test("Sub-cap messages of every mode ride the channel; only oversized opens dedicated streams") - func primitiveSelection() async throws { - guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { - print("[skip] QUIC: no TLS identity in this environment"); return - } - let rv = Rendezvous() - func t() -> QUICTransport { QUICTransport(configuration: .init(discovery: .rendezvous(rv))) } - let camera = PeerSession(identity: PeerIdentity(name: "Camera"), service: "_wire._udp", transport: t()) - let monitor = PeerSession(identity: PeerIdentity(name: "Monitor"), service: "_wire._udp", transport: t()) - - try await camera.startAdvertising(metadata: [:]) - let accept = Task { for await inv in camera.invitations { await inv.accept(); break } } - try await monitor.startBrowsing() - var discovered: DiscoveredPeer? - for await event in monitor.discoveries { - if case .found(let peer) = event { discovered = peer; break } - } - _ = try await monitor.invite(try #require(discovered), context: Data()) - await accept.value - - let perMode = 20 - let receiver = Task<[Delivery: Int], Never> { - var counts: [Delivery: Int] = [:] - var oversized = 0 - for await message in camera.messages { - if message.payload.count > QUICConnection.channelMaxPayload { - oversized += 1 - } else { - counts[message.delivery, default: 0] += 1 + // Exact-delta counter assertions: sound only because the umbrella suite + // serializes this against every other oversized-payload sender. + @Test("Sub-cap messages of every mode ride the channel; only oversized opens dedicated streams") + func primitiveSelection() async throws { + guard QUICTransport.isTLSIdentityAvailable(for: PeerIdentity(name: "probe")) else { + print("[skip] QUIC: no TLS identity in this environment"); return + } + let rv = Rendezvous() + func t() -> QUICTransport { QUICTransport(configuration: .init(discovery: .rendezvous(rv))) } + let camera = PeerSession(identity: PeerIdentity(name: "Camera"), service: "_wire._udp", transport: t()) + let monitor = PeerSession(identity: PeerIdentity(name: "Monitor"), service: "_wire._udp", transport: t()) + + try await camera.startAdvertising(metadata: [:]) + let accept = Task { for await inv in camera.invitations { await inv.accept(); break } } + try await monitor.startBrowsing() + var discovered: DiscoveredPeer? + for await event in monitor.discoveries { + if case .found(let peer) = event { discovered = peer; break } + } + _ = try await monitor.invite(try #require(discovered), context: Data()) + await accept.value + + let perMode = 20 + let receiver = Task<[Delivery: Int], Never> { + var counts: [Delivery: Int] = [:] + var oversized = 0 + for await message in camera.messages { + if message.payload.count > QUICConnection.channelMaxPayload { + oversized += 1 + } else { + counts[message.delivery, default: 0] += 1 + } + if oversized == 1, counts[.reliable] == perMode, + counts[.reliableOrdered] == perMode, counts[.datagram] == perMode + { break } } - if oversized == 1, counts[.reliable] == perMode, - counts[.reliableOrdered] == perMode, counts[.datagram] == perMode - { break } + return counts } - return counts - } - let openedBefore = QUICConnection.dedicatedOpened.value + let openedBefore = QUICConnection.dedicatedOpened.value - for n in 0 ..< perMode { - try await monitor.send(Data([UInt8(n)]), delivery: .reliable) - try await monitor.send(Data([UInt8(n)]), delivery: .reliableOrdered) - try await monitor.send(Data(repeating: UInt8(n), count: 600), delivery: .datagram) - } - // All three modes above must not have opened a single dedicated stream. - #expect(QUICConnection.dedicatedOpened.value == openedBefore) + for n in 0 ..< perMode { + try await monitor.send(Data([UInt8(n)]), delivery: .reliable) + try await monitor.send(Data([UInt8(n)]), delivery: .reliableOrdered) + try await monitor.send(Data(repeating: UInt8(n), count: 600), delivery: .datagram) + } + // All three modes above must not have opened a single dedicated stream. + #expect(QUICConnection.dedicatedOpened.value == openedBefore) - // Oversized .reliable is the ONLY route to a dedicated message stream. - try await monitor.send( - Data(repeating: 0xE0, count: QUICConnection.channelMaxPayload + 1), delivery: .reliable) + // Oversized .reliable is the ONLY route to a dedicated message stream. + try await monitor.send( + Data(repeating: 0xE0, count: QUICConnection.channelMaxPayload + 1), delivery: .reliable) - let counts = await receiver.value - #expect(counts[.reliable] == perMode) - #expect(counts[.reliableOrdered] == perMode) - #expect(counts[.datagram] == perMode) - #expect(QUICConnection.dedicatedOpened.value == openedBefore + 2) // sender + receiver handles + let counts = await receiver.value + #expect(counts[.reliable] == perMode) + #expect(counts[.reliableOrdered] == perMode) + #expect(counts[.datagram] == perMode) + #expect(QUICConnection.dedicatedOpened.value == openedBefore + 2) // sender + receiver handles - await monitor.disconnect() - await camera.disconnect() + await monitor.disconnect() + await camera.disconnect() + } } }