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.
54 lines
2.3 KiB
TypeScript
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);
|
|
});
|