import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { createProfileNamespace, profilePlaintextToString, deriveBlobSlotId, deriveBlobKey, deriveBlobSigningSeed, ed25519PublicKeyFromSeed, slotIdToHex, } from '../src/index.js'; import { createInboxServer, MemoryInboxStore, MemoryBlobStore, } from '@shade/inbox-server'; import { SubtleCryptoProvider } from '@shade/crypto-web'; import { ShadeError } from '@shade/core'; const crypto = new SubtleCryptoProvider(); function randBytes(n: number): Uint8Array { const buf = new Uint8Array(n); globalThis.crypto.getRandomValues(buf); return buf; } interface ServerHandle { url: string; stop: () => void; } async function startServer(): Promise { const app = createInboxServer({ crypto, store: new MemoryInboxStore(), blobStore: new MemoryBlobStore(), disableRateLimit: true, }); const port = 19000 + Math.floor(Math.random() * 500); const handle = Bun.serve({ port, fetch: app.fetch }); return { url: `http://localhost:${port}`, stop: () => handle.stop(true), }; } describe('SDK Profile namespace (V4.9)', () => { let server: ServerHandle; let masterKey: Uint8Array; beforeEach(async () => { server = await startServer(); masterKey = randBytes(32); }); afterEach(() => { server.stop(); }); test('credential-only round trip: create, read, update, delete', async () => { const profile = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'test-profile-v1', }); // Empty slot. expect(await profile.get()).toBeNull(); // Create. const payload = JSON.stringify({ hosts: ['device:abc'], v: 1 }); const created = await profile.put(payload); expect(created.created).toBe(true); // Read back. const got1 = await profile.get(); expect(got1).not.toBeNull(); expect(profilePlaintextToString(got1!)).toBe(payload); expect(got1!.etag).toBe(created.etag); // CAS update with the etag we just read. const next = JSON.stringify({ hosts: ['device:abc', 'device:def'], v: 2 }); const updated = await profile.put(next, { ifMatch: got1!.etag }); expect(updated.created).toBe(false); expect(Number(updated.etag)).toBeGreaterThan(Number(created.etag)); // Stale CAS fails. await expect( profile.put(JSON.stringify({ hosts: [] }), { ifMatch: created.etag }), ).rejects.toThrow(ShadeError); // Delete. const removed = await profile.delete(); expect(removed).toBe(true); expect(await profile.get()).toBeNull(); }); test('different app namespaces map to different slots', async () => { const a = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'app-a', }); const b = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'app-b', }); expect(a.slotIdHex).not.toBe(b.slotIdHex); }); test('different master keys map to different slots', async () => { const km2 = randBytes(32); const a = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'shared', }); const b = createProfileNamespace({ baseUrl: server.url, crypto, masterKey: km2, app: 'shared', }); expect(a.slotIdHex).not.toBe(b.slotIdHex); }); test('a fresh client with the same master + app reads the existing blob', async () => { const writer = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'shared', }); await writer.put('hello world'); // Brand-new namespace instance — simulates "log in from a new // device". Uses *only* the master key + app namespace; nothing // else carried over. const reader = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'shared', }); const got = await reader.get(); expect(got).not.toBeNull(); expect(profilePlaintextToString(got!)).toBe('hello world'); }); test('without ifMatch on populated slot is a SHADE_CONFLICT error', async () => { const profile = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'conflict-test', }); await profile.put('first'); try { await profile.put('second'); throw new Error('expected put to throw'); } catch (err) { expect(err).toBeInstanceOf(ShadeError); expect((err as ShadeError).code).toBe('SHADE_CONFLICT'); } }); test('stale ifMatch is a SHADE_PRECONDITION_FAILED error', async () => { const profile = createProfileNamespace({ baseUrl: server.url, crypto, masterKey, app: 'precondition-test', }); const first = await profile.put('first'); await profile.put('second', { ifMatch: first.etag }); try { await profile.put('third', { ifMatch: first.etag }); throw new Error('expected put to throw'); } catch (err) { expect(err).toBeInstanceOf(ShadeError); expect((err as ShadeError).code).toBe('SHADE_PRECONDITION_FAILED'); } }); }); describe('KDF helpers (V4.9)', () => { test('derivations are deterministic per (masterKey, app)', () => { const km = randBytes(32); const a1 = deriveBlobSlotId(km, 'x'); const a2 = deriveBlobSlotId(km, 'x'); expect(a1).toEqual(a2); expect(deriveBlobSlotId(km, 'y')).not.toEqual(a1); expect(deriveBlobKey(km, 'x')).not.toEqual(a1); expect(deriveBlobSigningSeed(km, 'x')).not.toEqual(deriveBlobKey(km, 'x')); }); test('signing seed → pubkey is deterministic and 32 bytes', () => { const km = randBytes(32); const seed = deriveBlobSigningSeed(km, 'p'); const pk1 = ed25519PublicKeyFromSeed(seed); const pk2 = ed25519PublicKeyFromSeed(seed); expect(pk1).toEqual(pk2); expect(pk1.length).toBe(32); }); test('slotIdToHex round-trips through hex form', () => { const km = randBytes(32); const id = deriveBlobSlotId(km, 'rt'); const hex = slotIdToHex(id); expect(hex.length).toBe(64); expect(/^[0-9a-f]{64}$/.test(hex)).toBe(true); }); });