Skip to content

feat(group-messaging): add @bsv/group-messaging - #546

Draft
kjartan221 wants to merge 2 commits into
bsv-blockchain:mainfrom
kjartan221:feat/group-messaging
Draft

kjartan221 wants to merge 2 commits into
bsv-blockchain:mainfrom
kjartan221:feat/group-messaging

Conversation

@kjartan221

Copy link
Copy Markdown
Collaborator

End-to-end encrypted group messaging for BRC-100 wallets, with MLS (RFC 9420) group state by way of ts-mls. Each member's MLS credential carries a wallet attestation derived against the "anyone" root, so every member verifies every other independently and a peer cannot join under a name that is not theirs.

The library opens no database and no network connection of its own: it takes the storage and transport the host already has. One socket is opened, on the host's own MessageBoxClient, and only when a caller sets live: true.

Storage backends for Map, SQL and IndexedDB; transport backends for an in-process hub and @bsv/message-box-client. Live socket delivery is additive rather than an alternative, because joinRoom replays no backlog: a slower poll always runs underneath as the backstop that makes a silent socket recoverable.

Private KeyPackage material is never stored. keyPackages.create() returns the private half once and keeps only the public one; a successful join or create retires the spent ref and emits keyPackageConsumed so the host knows which secret to destroy.

Migrated from a standalone repository. History was not carried across: the reasoning lives in code comments and the README rather than in commit messages, and the source history is interleaved with a demo application that did not move. That demo, which exercises this package end to end over a live MessageBox socket and against an adversary suite, stays there.

413 tests, typecheck, lint and format:check all pass.

LICENSE.txt, LICENSES/ and THIRD_PARTY_NOTICES.md are not included and are the one remaining repository-health finding.

Program and scope

  • Tracker or issue: (none — new package; add one if the program requires it)
  • Program gate(s) advanced: new public package onboarding for the messaging area
  • Why this change is needed: @bsv/sdk gives wallets identity and payment but no
    group messaging primitive. This adds one: MLS (RFC 9420) group state via
    ts-mls, with each member's MLS credential carrying a BRC-100 wallet
    attestation derived against the "anyone" root, so every member verifies every
    other independently and nobody can join under a name that is not theirs.
    Developed in a standalone repository; this is the migration, a file copy rather
    than a history graft.
  • Explicitly out of scope:
    • The demo application that exercised this end to end (live MessageBox harness,
      adversary suite, simulation scenarios) stays in the standalone repository.
    • Licence artefacts. LICENSE.txt, the LICENSES/ entry and the
      THIRD_PARTY_NOTICES.md row are deliberately absent and deliberately not
      baselined away: scripts/repository-health.mjs reports exactly one finding,
      missing-license-file, and it is meant to stay visible. It needs a maintainer
      decision on which Open BSV variant applies, not a guess.
    • The browser-library profile. Registered as node-library with node-cjs
      and node-esm consumer profiles. The code has no Node-only dependency and an
      IndexedDB backend ships in it, but that profile carries a
      browser-budget.json and there is no measured number to put in it yet.
      Follow-up, not a fabricated budget.
  • Exact head SHA reviewed: 83f052faac8d420be0c9a8933802dc25ed794ba3

Impact

  • Public package source or manifest changed; affected packages are listed below
  • Public API, exports, types, runtime targets, or browser/mobile behavior changed
  • Security-sensitive boundary changed
  • Documentation or examples changed

Affected packages/services and intended patch versions:

  • @bsv/group-messaging — new, 0.1.0, unpublished. No existing package's
    source or manifest is touched. The non-package files in the diff are
    docs/packages/messaging/group-messaging.md,
    governance/repository-health/projects.json,
    governance/repository-health/baselines.json,
    governance/package-release-notes.json and pnpm-lock.yaml.

Shipped deliberately caveated. The README opens with a Security status section
rather than burying one at the bottom, and the docs page is registered
status: experimental, not beta: 0.x, ts-mls carries its own
no-formal-audit disclaimer and that inherits, the attestation layer on top has
been reviewed by reading rather than audited, and there has been no interop
testing against another MLS implementation. If the program would rather that
caveat be worded differently or live elsewhere, say so and it moves.

Security-sensitive boundary, named so review can find it: this library accepts
KeyPackages, Welcomes and Commits from unauthenticated peers. The check that
stops a peer joining under someone else's identity is in
src/mls/authentication.ts, and src/mls/state.ts is what re-attaches it to
restored group state — without that line the check silently stops running.
src/bootstrap/envelope.ts is the wire parser.

Verification

  • Local commands and results:
    • pnpm test (package) — 27 files, 421 tests, all green
    • pnpm typecheck — clean
    • oxlint src — clean
    • prettier --check — clean
    • node scripts/repository-health.mjs — one finding,
      packages/messaging/group-messaging::missing-license-file, deliberate (see
      scope), not baselined
    • attw --pack — ESM and CJS resolution clean in both directions
  • Hosted CI run: none yet. Opening as draft; this line gets the run link once
    checks are terminal on this head.
  • Conformance evidence: RFC 9420 conformance is ts-mls's and is not re-derived
    here. What this package's own suite proves is the layer above it — the wallet
    attestation is refused at all three seams ts-mls exposes: createCommit,
    joinGroup, and processMessage's applyTreeMutations
    (src/mls/__tests__/engine-membership-forgery.test.ts). All three cases were
    watched to fail with validateCredential stubbed to return true.
  • Coverage delta: new package, no prior baseline.
  • Lint/typecheck delta: zero new findings.
  • Browser/mobile/packed-consumer evidence: scripts/check-package-artifact.mjs
    and scripts/check-browser-package.mjs both die on this Windows workstation
    with spawn pnpm ENOENT before reaching any check. attw --pack stood in for
    the artifact half. Neither is real evidence until CI runs them.
  • Performance or bundle-size delta: not measured. See the browser-library note
    above.
  • I self-reviewed the complete diff for correctness, security,
    compatibility, public API, artifacts, dependencies, docs, and operations
  • All applicable checks are terminal and successful on the exact head
    (draft — nothing hosted has run yet)

Security and dependencies

  • No dependency or lockfile change — does not apply; see below
  • Changelog, runtime relevance, peer compatibility, transitive graph, and
    audit results were reviewed
  • CodeQL/negative tests cover any changed trust boundary — negative tests do;
    CodeQL has not run on this head
  • The exact-head CodeQL analysis has no new alert (not run)
  • The exact-head repository quality gate reports zero new Sonar findings
    (not run)
  • No new override, advisory dismissal, quality suppression, or skipped test
  • Any temporary exception is registered with owner, evidence, review date and
    removal condition — none added; the one open finding is reported rather
    than excepted
  • Workflow permissions and lifecycle-script behavior remain least privilege —
    no workflow and no lifecycle script changed

Trust-boundary coverage, since that boundary is the reason the package exists.
Negative tests cover forged credentials at all three MLS seams; a KeyPackage
declaring an unsupported or mismatched ciphersuite; malformed and truncated wire
envelopes; oversized and null-typed content fields; unbounded writes by an
unauthenticated peer, bounded per group by both count and bytes; and handler
isolation, so one subscriber throwing cannot abort a delivery loop.

Dependency evidence

  • Release notes and necessity: three new runtime dependencies, all pinned exactly.
    ts-mls@1.6.4 is the MLS implementation and the reason the package can exist;
    @noble/ciphers@2.1.1 and @noble/curves@2.0.1 are its peer requirements.
    1.6.4 over 1.6.2 is not cosmetic: needsUpdatePath now forces a path update
    for a single Remove where 1.6.2 did not — a post-compromise-security fix — and
    getCommitSecret replaces a deriveSecret(secret, "path", kdf) call in the key
    schedule, so 1.6.2 and 1.6.4 peers are not expected to interoperate. Pinned
    exactly rather than caretted for that reason. The credential-binding seams were
    re-read against 1.6.4 and the authService call sites are unchanged from 1.6.2.
  • Runtime, build, and peer compatibility: Node >= 22. @bsv/sdk is a peer at
    ^2.4.1, not a dependency. @bsv/message-box-client is a devDependency only —
    the transport backend types against it; the host supplies the instance.
  • Deduplicated lockfile: pnpm-lock.yaml regenerated in-repo, ts-mls@1.6.4
    resolves once.
  • Audit and CodeQL: not run on this head.
  • Package and consumer tests: 421 in-package, plus attw --pack for the two
    consumer profiles.
  • Bundle and performance impact: not measured.
  • Affected public package versions: none — nothing existing depends on this yet.

Release and operations

  • No npm publication was performed from a workstation or from this PR
  • Required npm patch bumps are included or intentionally deferred — none
    required, no existing package is touched
  • Image/SBOM/provenance/deployment/rollback impact is documented — does not
    apply, library only
  • Documentation, changelog, migration, and operational guidance are current —
    README.md, CHANGELOG.md, AGENTS.md,
    docs/packages/messaging/group-messaging.md and a
    governance/package-release-notes.json entry

Completion evidence

  • The linked tracker is updated — no tracker linked yet
  • Review conversations are resolved — none opened yet
  • Documentation, changelog, migration notes, release notes and operator
    guidance are current or concretely not applicable
  • No pending, failed, stale, cancelled or unexpectedly skipped check is being
    handed to another contributor as "complete" — the two gaps are named above
    rather than left for a reviewer to find: the licence artefacts and the
    browser-library profile
  • One qualified maintainer approval is sufficient; no last-pusher restriction
    is assumed

End-to-end encrypted group messaging for BRC-100 wallets, with MLS (RFC 9420)
group state by way of ts-mls. Each member's MLS credential carries a wallet
attestation derived against the "anyone" root, so every member verifies every
other independently and a peer cannot join under a name that is not theirs.

The library opens no database and no network connection of its own: it takes
the storage and transport the host already has. One socket is opened, on the
host's own MessageBoxClient, and only when a caller sets `live: true`.

Storage backends for Map, SQL and IndexedDB; transport backends for an
in-process hub and @bsv/message-box-client. Live socket delivery is additive
rather than an alternative, because `joinRoom` replays no backlog: a slower
poll always runs underneath as the backstop that makes a silent socket
recoverable.

Private KeyPackage material is never stored. `keyPackages.create()` returns the
private half once and keeps only the public one; a successful join or create
retires the spent ref and emits `keyPackageConsumed` so the host knows which
secret to destroy.

Migrated from a standalone repository. History was not carried across: the
reasoning lives in code comments and the README rather than in commit
messages, and the source history is interleaved with a demo application that
did not move. That demo, which exercises this package end to end over a live
MessageBox socket and against an adversary suite, stays there.

413 tests, typecheck, lint and format:check all pass.

Registered as node-library rather than browser-library: the package runs in the
browser and imports no node builtins, but certifying that profile needs a
measured browser-budget.json, and check-browser-package.mjs cannot run on
Windows.

LICENSE.txt, LICENSES/ and THIRD_PARTY_NOTICES.md are not included and are the
one remaining repository-health finding.
@socket-security

socket-security Bot commented Sep 17, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​@​noble/​curves@​2.2.0 ⏵ 2.0.110010010088100
Updatednpm/​@​noble/​ciphers@​2.2.0 ⏵ 2.1.110010010088100
Addednpm/​ts-mls@​1.6.49210010094100

View full report

Three groups.

The blockers were three tests with no runtime assertion in wire-payload.test.ts.
They were compile-fail fixtures — each `@ts-expect-error` breaks the build when
WirePayload's brand is relaxed — but the brand is erased once compiled, so spec
§4.1 held only for TypeScript callers and one of those tests asserted that the
private KeyPackage was in fact sent. Every payload the library emits comes from
encodeEnvelope, so send and broadcast now refuse anything that is not a
well-formed envelope: a private KeyPackage reads as envelope kind 0, a bare
KeyPackage as version 0, and one rule covers both without sniffing for key
material. The tests assert the rejection and were watched to fail without it.

The three cognitive-complexity findings are decomposed by responsibility, not
split to satisfy a counter. MessageBoxTransport.#deliver gives up #settled,
#decode and #fanOut while keeping its own control flow, because the group
vanishing mid-drain has to abandon the batch rather than write it back.
InviteService.handle becomes one method per message type with the six identical
refusals behind #refuse.

The rest are cosmetic: a useless constructor, a nested ternary, \w, code-point
string methods, one readonly, and five length comparisons as toHaveLength.

The six redundant-type-alias findings are left alone. IdentityKey, ChatId and
their siblings are documentation aliases, and branding them is a public API
decision to take deliberately rather than in response to a linter.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant