SkateSpots/src/lib/cordn/native/native.live.test.ts
SkateSpots 1dcaf8cea3 Cordn native: real MLS client (Level 2 foundation)
Add src/lib/cordn/native/ — a browser-safe Cordn client doing real RFC 9420
MLS over a ContextVM coordinator, mirroring Cordn's wire contract (reimplemented,
not copied from @cordn/cli).

- mls.ts: identity, key packages, group create/add/join, application messages,
  the ChaCha20-Poly1305 coordinator wrapper, causal prev tags.
- coordinator.ts: ContextVM/MCP-over-Nostr client on @contextvm/sdk, validated
  with @cordn/core schemas.
- session.ts: CordnSession (key packages, createGroup, addMember->Welcome,
  joinGroup, send, fetch) with persistence behind an injectable CordnStore
  (MemoryCordnStore today; IndexedDB adapter TODO).
- Verified end-to-end against our own coordinator: a two-member round trip
  (CORDN_LIVE=1 vitest run native.live.test.ts) — create, add, join, and
  bidirectional encrypted messages with proven senders.

Deps: @cordn/core, @contextvm/mcp-sdk, @noble/ciphers pinned 2.2.0 (fixes the
previously invalid tree: ts-mls wants 2.2.0, a stale root copy was 0.5.3).
src/test/setup.ts is now guarded for node-environment test files. Not wired to
any UI yet; nothing imports the module, so the web bundle is unchanged.
2026-10-12 22:14:20 +07:00

54 lines
2.3 KiB
TypeScript

// @vitest-environment node
/**
* Live Cordn-native smoke test — runs ONLY with `CORDN_LIVE=1` (it talks to the
* real coordinator at `wss://relay.skate-spots.org`). Kept out of the default
* `npm test` run so CI never depends on the network.
*
* CORDN_LIVE=1 npx vitest run src/lib/cordn/native/native.live.test.ts
*
* Runs in the Node environment (not jsdom): the ContextVM transport's nested
* nostr-tools/@noble/hashes reject a Uint8Array from another realm.
*/
import { describe, expect, it } from 'vitest';
import { CordnSession, MemoryCordnStore } from './session';
import { SKATESPOTS_COORDINATOR } from '../../coordinator';
const live = process.env.CORDN_LIVE === '1';
describe.skipIf(!live)('cordn native (live)', () => {
it('creates a group, adds a member, and exchanges MLS messages', async () => {
const coordinator = {
serverPubkey: SKATESPOTS_COORDINATOR.pubkey,
relays: SKATESPOTS_COORDINATOR.relays,
};
const a = await CordnSession.open({ coordinator, store: new MemoryCordnStore() });
const b = await CordnSession.open({ coordinator, store: new MemoryCordnStore() });
await a.generateKeyPackage('a');
const bKeyPackage = await b.generateKeyPackage('b');
await a.publishKeyPackage('a');
await b.publishKeyPackage('b');
const groupId = `skate-native-${Date.now()}`;
const group = await a.createGroup({ groupId, name: 'Native test' });
const { welcomeBase64 } = await a.addMember(group.gid, bKeyPackage.keyPackage);
const bGroup = await b.joinGroup({ welcomeBase64, keyPackage: bKeyPackage, name: 'Native test' });
expect(bGroup.members.map((m) => m.pubkey)).toContain(a.pubkey);
expect(bGroup.members.map((m) => m.pubkey)).toContain(b.pubkey);
await a.send(group.gid, 'hello from A');
const receivedByB = await b.fetch(bGroup.gid);
expect(receivedByB.map((m) => m.content)).toContain('hello from A');
expect(receivedByB.find((m) => m.content === 'hello from A')?.sender).toBe(a.pubkey);
await b.send(bGroup.gid, 'hi from B');
const receivedByA = await a.fetch(group.gid);
expect(receivedByA.map((m) => m.content)).toContain('hi from B');
expect(receivedByA.find((m) => m.content === 'hi from B')?.sender).toBe(b.pubkey);
await a.disconnect();
await b.disconnect();
}, 180_000);
});