release(v4.6.0): broadcast channels — Signal sender-keys for one-to-many fan-out
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Lands the broadcast-channel primitive Prism asked for in Docs/shade-feature-request-sender-keys.md. The crypto in @shade/core/sender-keys.ts was already in place; this release wires it up as a first-class app-facing API, adds the persistence schema across all six storage backends (memory, sqlite, indexeddb + encrypted variants), introduces wire type 0x21 in @shade/proto, and ships Prism's three acceptance tests verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -168,6 +168,82 @@ describe('EncryptedSQLiteStorage', () => {
|
||||
expect(await store.getStreamState('stream-1')).not.toBeNull(); // active rows untouched
|
||||
});
|
||||
|
||||
test('broadcast channel + member round-trip (V4.6)', async () => {
|
||||
const channel = {
|
||||
channelId: 'c-1',
|
||||
ownerRole: 'sender' as const,
|
||||
ownerAddress: 'pc',
|
||||
label: 'output',
|
||||
generation: 0,
|
||||
chainKey: randBytes(32),
|
||||
iteration: 0,
|
||||
signingPublicKey: randBytes(32),
|
||||
signingPrivateKey: randBytes(32),
|
||||
createdAt: 1_700_000_000_000,
|
||||
updatedAt: 1_700_000_000_000,
|
||||
};
|
||||
await store.saveBroadcastChannel(channel);
|
||||
const fetched = await store.getBroadcastChannel('c-1');
|
||||
expect(fetched).not.toBeNull();
|
||||
expect(fetched!.ownerRole).toBe('sender');
|
||||
expect(fetched!.label).toBe('output');
|
||||
expect(fetched!.chainKey).toEqual(channel.chainKey);
|
||||
expect(fetched!.signingPrivateKey).toEqual(channel.signingPrivateKey);
|
||||
|
||||
// Add two members + verify list + remove.
|
||||
await store.saveBroadcastMember({
|
||||
channelId: 'c-1',
|
||||
peerAddress: 'mobile-a',
|
||||
joinedAt: 1_700_000_000_001,
|
||||
removedAt: null,
|
||||
});
|
||||
await store.saveBroadcastMember({
|
||||
channelId: 'c-1',
|
||||
peerAddress: 'mobile-b',
|
||||
joinedAt: 1_700_000_000_002,
|
||||
removedAt: null,
|
||||
});
|
||||
let members = await store.getBroadcastMembers('c-1');
|
||||
expect(members.map((m) => m.peerAddress)).toEqual(['mobile-a', 'mobile-b']);
|
||||
|
||||
// Mark one removed.
|
||||
await store.saveBroadcastMember({
|
||||
channelId: 'c-1',
|
||||
peerAddress: 'mobile-a',
|
||||
joinedAt: 1_700_000_000_001,
|
||||
removedAt: 1_700_000_000_500,
|
||||
});
|
||||
members = await store.getBroadcastMembers('c-1');
|
||||
expect(members.find((m) => m.peerAddress === 'mobile-a')?.removedAt).toBe(1_700_000_000_500);
|
||||
|
||||
// List + delete cascade.
|
||||
expect((await store.listBroadcastChannels())).toHaveLength(1);
|
||||
await store.removeBroadcastChannel('c-1');
|
||||
expect(await store.getBroadcastChannel('c-1')).toBeNull();
|
||||
expect((await store.getBroadcastMembers('c-1'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('broadcast channel sealed: receiver-side row has no private key', async () => {
|
||||
await store.saveBroadcastChannel({
|
||||
channelId: 'c-2',
|
||||
ownerRole: 'receiver',
|
||||
ownerAddress: 'pc',
|
||||
generation: 1,
|
||||
chainKey: randBytes(32),
|
||||
iteration: 5,
|
||||
signingPublicKey: randBytes(32),
|
||||
// no signingPrivateKey
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
});
|
||||
const r = await store.getBroadcastChannel('c-2');
|
||||
expect(r).not.toBeNull();
|
||||
expect(r!.ownerRole).toBe('receiver');
|
||||
expect(r!.signingPrivateKey).toBeUndefined();
|
||||
expect(r!.iteration).toBe(5);
|
||||
expect(r!.generation).toBe(1);
|
||||
});
|
||||
|
||||
test('rejects open with wrong key (fingerprint mismatch)', async () => {
|
||||
await store.saveIdentityKeyPair(dummyIdentity());
|
||||
store.close();
|
||||
|
||||
Reference in New Issue
Block a user