- apps/cli/ is now the canonical CLI (was apps/cli-v2/). - apps/cli/ legacy v0 archived as branch 'legacy-cli-archive' and tag 'cli-v0-legacy-final' before deletion; git history preserves it too. - .github/workflows/release-cli.yml paths updated. - pnpm-lock.yaml regenerated. Broker-side peer-grant enforcement (spec: 2026-04-15-per-peer-capabilities): - 0020_peer-grants.sql adds peer_grants jsonb + GIN index on mesh.member. - handleSend in broker fetches recipient grant maps once per send, drops messages silently when sender lacks the required capability. - POST /cli/mesh/:slug/grants to update from CLI; broker_messages_dropped_by_grant_total metric. - CLI grant/revoke/block now mirror to broker via syncToBroker. Auto-migrate on broker startup: - apps/broker/src/migrate.ts runs drizzle migrate with pg_advisory_lock before the HTTP server binds. Exits non-zero on failure so Coolify healthcheck fails closed. - Dockerfile copies packages/db/migrations into /app/migrations. - postgres 3.4.5 added as direct broker dep. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
generateKeypair, sign, verify, encrypt, decrypt, boxSeal, boxOpen,
|
|
encryptDirect, decryptDirect, randomBytes, randomHex,
|
|
} from "~/services/crypto/facade.js";
|
|
|
|
describe("crypto", () => {
|
|
it("generates valid Ed25519 keypair", async () => {
|
|
const kp = await generateKeypair();
|
|
expect(kp.publicKey).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(kp.secretKey).toMatch(/^[0-9a-f]{128}$/);
|
|
});
|
|
|
|
it("sign + verify round-trip", async () => {
|
|
const kp = await generateKeypair();
|
|
const msg = "hello world";
|
|
const sig = await sign(msg, kp.secretKey);
|
|
expect(await verify(msg, sig, kp.publicKey)).toBe(true);
|
|
expect(await verify("tampered", sig, kp.publicKey)).toBe(false);
|
|
});
|
|
|
|
it("file encrypt + decrypt round-trip", async () => {
|
|
const data = new TextEncoder().encode("secret document");
|
|
const encrypted = await encrypt(data);
|
|
expect(encrypted.ciphertext.length).toBeGreaterThan(0);
|
|
expect(encrypted.nonce).toBeTruthy();
|
|
|
|
const decrypted = await decrypt(encrypted.ciphertext, encrypted.nonce, encrypted.key);
|
|
expect(decrypted).not.toBeNull();
|
|
expect(new TextDecoder().decode(decrypted!)).toBe("secret document");
|
|
});
|
|
|
|
it("file encrypt + decrypt fails with wrong key", async () => {
|
|
const data = new TextEncoder().encode("secret");
|
|
const encrypted = await encrypt(data);
|
|
const wrongKey = await randomBytes(32);
|
|
const result = await decrypt(encrypted.ciphertext, encrypted.nonce, wrongKey);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("boxSeal + boxOpen round-trip", async () => {
|
|
const kp = await generateKeypair();
|
|
const secret = await randomBytes(32);
|
|
const sealed = await boxSeal(secret, kp.publicKey);
|
|
expect(sealed).toBeTruthy();
|
|
|
|
const opened = await boxOpen(sealed, kp.publicKey, kp.secretKey);
|
|
expect(opened).not.toBeNull();
|
|
expect(Buffer.from(opened!).toString("hex")).toBe(Buffer.from(secret).toString("hex"));
|
|
});
|
|
|
|
it("crypto_box direct message round-trip", async () => {
|
|
const alice = await generateKeypair();
|
|
const bob = await generateKeypair();
|
|
const msg = "hello bob";
|
|
|
|
const envelope = await encryptDirect(msg, bob.publicKey, alice.secretKey);
|
|
expect(envelope.nonce).toBeTruthy();
|
|
expect(envelope.ciphertext).toBeTruthy();
|
|
|
|
const decrypted = await decryptDirect(envelope, alice.publicKey, bob.secretKey);
|
|
expect(decrypted).toBe("hello bob");
|
|
});
|
|
|
|
it("crypto_box decrypt fails with wrong keys", async () => {
|
|
const alice = await generateKeypair();
|
|
const bob = await generateKeypair();
|
|
const eve = await generateKeypair();
|
|
|
|
const envelope = await encryptDirect("secret", bob.publicKey, alice.secretKey);
|
|
const result = await decryptDirect(envelope, alice.publicKey, eve.secretKey);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("randomBytes returns correct length", async () => {
|
|
const bytes = await randomBytes(16);
|
|
expect(bytes.length).toBe(16);
|
|
});
|
|
|
|
it("randomHex returns correct length", async () => {
|
|
const hex = await randomHex(8);
|
|
expect(hex).toMatch(/^[0-9a-f]{16}$/);
|
|
});
|
|
});
|