storebound/tests/desktop-dist.test.ts
CRT 710c3a53d0 README, docs and site: one story (the whole job, no avoidable rejections, the desktop app, your AI, install)
README is a short front page (about 120 lines) with one real visual per section; every former section moved
unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth,
studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one
page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight
screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools:
tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and
WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
2026-10-13 03:29:59 +08:00

136 lines
9.4 KiB
TypeScript

// `storebound desktop`: minisign checks (Tauri's updater signatures), latest.json, the installer for each system,
// finding the installed app and the deep link it opens.
import { describe, expect, test } from 'bun:test';
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
import { mkdtempSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { download, pick } from '../cli/desktop';
import { downloadsFor, findInstalled, type Manifest, manifestProblems, openCommand, openFolderLink, PUBLIC_KEY } from '../cli/lib/desktop-dist';
import { parsePublicKey, parseSignature, verify } from '../cli/lib/minisign';
/** A throwaway minisign key pair in Tauri's base64 form, and a signer like `tauri signer sign`. */
function keyPair() {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const raw = Buffer.from(publicKey.export({ format: 'jwk' }).x!, 'base64url');
const id = Buffer.from('0102030405060708', 'hex');
const pubText = `untrusted comment: minisign public key: ${Buffer.from(id).reverse().toString('hex').toUpperCase()}\n${Buffer.concat([Buffer.from('Ed'), id, raw]).toString('base64')}\n`;
const signer = (data: Uint8Array, alg: 'Ed' | 'ED' = 'ED', comment = 'timestamp:1\tfile:x') => {
const msg = alg === 'ED' ? createHash('blake2b512').update(data).digest() : Buffer.from(data);
const sig = sign(null, msg, privateKey);
const global = sign(null, Buffer.concat([sig, Buffer.from(comment)]), privateKey);
const text = `untrusted comment: signature from tauri secret key\n${Buffer.concat([Buffer.from(alg), id, sig]).toString('base64')}\ntrusted comment: ${comment}\n${global.toString('base64')}\n`;
return Buffer.from(text).toString('base64');
};
return { pub: Buffer.from(pubText).toString('base64'), pubText, signer };
}
describe('minisign', () => {
const k = keyPair();
const data = new Uint8Array(Buffer.from('storebound installer bytes'));
test('prehashed (Tauri) and plain signatures verify, in both wrappings', () => {
expect(verify(data, k.signer(data, 'ED'), k.pub)).toEqual({ ok: true, trustedComment: 'timestamp:1\tfile:x' });
expect(verify(data, k.signer(data, 'Ed'), k.pubText).ok).toBe(true);
expect(parsePublicKey(k.pub).keyId).toBe('0807060504030201');
expect(parseSignature(k.signer(data)).alg).toBe('ED');
});
test('a changed file, comment, key or garbage fails', () => {
const changed = new Uint8Array(data); changed[0] = changed[0]! ^ 1;
expect(verify(changed, k.signer(data), k.pub)).toEqual({ ok: false, reason: 'the signature does not match the file' });
const sig = Buffer.from(k.signer(data), 'base64').toString().replace('file:x', 'file:y');
expect(verify(data, Buffer.from(sig).toString('base64'), k.pub)).toEqual({ ok: false, reason: 'the trusted comment was changed' });
const other = keyPair();
expect(verify(data, k.signer(data), other.pub).ok).toBe(false);
expect(verify(data, 'not a signature', k.pub).ok).toBe(false);
expect(verify(data, k.signer(data), 'AAAA').ok).toBe(false);
});
test('the CLI carries the desktop app\'s updater key', () => {
const conf = JSON.parse(readFileSync(join(import.meta.dir, '../apps/desktop/src-tauri/tauri.conf.json'), 'utf8'));
expect(PUBLIC_KEY).toBe(conf.plugins.updater.pubkey);
expect(parsePublicKey(PUBLIC_KEY).keyId).toMatch(/^[0-9A-F]{16}$/);
// the home page's download section prints the minisign key line for people who verify by hand
const line = Buffer.from(PUBLIC_KEY, 'base64').toString().split('\n')[1]!;
expect(readFileSync(join(import.meta.dir, '../site/index.html'), 'utf8')).toContain(`<code class="dl-key">${line}</code>`);
});
});
function manifest(k: ReturnType<typeof keyPair>, data: Uint8Array): Manifest {
const sha256 = createHash('sha256').update(data).digest('hex');
const file = 'storebound_0.1.0_universal.dmg';
return {
version: '0.1.0', pub_date: '2026-10-11T00:00:00Z',
platforms: { 'darwin-aarch64': { signature: k.signer(data), url: 'https://storebound.dev/desktop/dl/0.1.0/storebound_0.1.0_universal.app.tar.gz' } },
downloads: {
'macos-universal-dmg': { file, url: `https://storebound.dev/desktop/dl/0.1.0/${file}`, sha256, size: data.length, signature: k.signer(data), codeSigned: false },
'linux-x86_64-deb': { file: 'storebound_0.1.0_amd64.deb', url: 'https://storebound.dev/desktop/dl/0.1.0/storebound_0.1.0_amd64.deb', sha256, size: data.length, signature: k.signer(data), codeSigned: false },
},
};
}
describe('latest.json and downloads', () => {
const k = keyPair();
const data = new Uint8Array(Buffer.from('a'.repeat(1000)));
test('installers per system', () => {
expect(downloadsFor('darwin', 'arm64')).toEqual(['macos-universal-dmg']);
expect(downloadsFor('darwin', 'x64')).toEqual(['macos-universal-dmg']);
expect(downloadsFor('win32', 'x64')).toEqual(['windows-x86_64-nsis']);
expect(downloadsFor('linux', 'x64')).toEqual(['linux-x86_64-appimage', 'linux-x86_64-deb']);
expect(downloadsFor('linux', 'arm64')).toEqual([]);
const m = manifest(k, data);
expect(pick(m, downloadsFor('darwin', 'arm64'))?.[0]).toBe('macos-universal-dmg');
expect(pick(m, downloadsFor('linux', 'x64'))?.[0]).toBe('linux-x86_64-deb');
expect(pick(m, downloadsFor('linux', 'x64'), 'appimage')).toBeNull();
expect(pick(m, downloadsFor('win32', 'x64'))).toBeNull();
});
test('only storebound.dev URLs, hashes and signatures pass', () => {
const m = manifest(k, data);
expect(manifestProblems(m)).toEqual([]);
const bad = structuredClone(m) as Manifest;
bad.downloads!['macos-universal-dmg']!.url = 'https://evil.test/desktop/storebound_0.1.0_universal.dmg';
bad.platforms['darwin-aarch64']!.url = 'http://storebound.dev/desktop/x.tar.gz';
bad.downloads!['linux-x86_64-deb']!.sha256 = 'abc';
expect(manifestProblems(bad)).toHaveLength(3);
expect(manifestProblems({ version: 'latest', platforms: {} })).toEqual(['version is not a version number']);
expect(manifestProblems([])).toEqual(['latest.json is not an object']);
});
test('download keeps the file only when size, SHA-256 and signature match', async () => {
const m = manifest(k, data);
const d = m.downloads!['macos-universal-dmg']!;
const dir = mkdtempSync(join(tmpdir(), 'sb-dl-'));
const serve = (body: Uint8Array) => async () => new Response(Buffer.from(body));
const out = await download(d, dir, serve(data), k.pub);
expect(readFileSync(out)).toEqual(Buffer.from(data));
const evil = new Uint8Array(data); evil[3] = evil[3]! ^ 1;
await expect(download({ ...d, file: 'b.dmg' }, dir, serve(evil), k.pub)).rejects.toThrow(/SHA-256/);
const sha = createHash('sha256').update(evil).digest('hex');
await expect(download({ ...d, file: 'c.dmg', sha256: sha }, dir, serve(evil), k.pub)).rejects.toThrow(/signature/);
await expect(download({ ...d, file: 'e.dmg' }, dir, serve(data.subarray(1)), k.pub)).rejects.toThrow(/bytes/);
await expect(download({ ...d, file: 'f.dmg' }, dir, async () => new Response('', { status: 404 }), k.pub)).rejects.toThrow(/404/);
for (const f of ['b.dmg', 'c.dmg', 'e.dmg', 'f.dmg']) expect(existsSync(join(dir, f))).toBe(false);
});
});
describe('the installed app', () => {
const at = (paths: string[]) => (p: string) => paths.includes(p);
test('found per system', () => {
expect(findInstalled('darwin', {}, at(['/Applications/storebound.app/Contents/MacOS/storebound-desktop']), '/Users/you'))
.toEqual({ path: '/Applications/storebound.app', how: 'bundle' });
expect(findInstalled('darwin', {}, at(['/Users/you/Applications/storebound.app/Contents/MacOS/storebound-desktop']), '/Users/you')?.path)
.toBe('/Users/you/Applications/storebound.app');
expect(findInstalled('darwin', {}, at([]), '/Users/you')).toBeNull();
expect(findInstalled('win32', { LOCALAPPDATA: 'C:\\Users\\you\\AppData\\Local' }, at(['C:\\Users\\you\\AppData\\Local\\storebound\\storebound-desktop.exe']), 'C:\\Users\\you'))
.toEqual({ path: 'C:\\Users\\you\\AppData\\Local\\storebound\\storebound-desktop.exe', how: 'exe' });
expect(findInstalled('linux', {}, at(['/usr/bin/storebound-desktop']), '/home/you')).toEqual({ path: '/usr/bin/storebound-desktop', how: 'deb' });
const list = (d: string) => (d === '/home/you/Applications' ? ['notes.txt', 'storebound_0.1.0_amd64.AppImage', 'storebound_0.2.0_amd64.AppImage'] : []);
expect(findInstalled('linux', {}, at([]), '/home/you', list)).toEqual({ path: '/home/you/Applications/storebound_0.2.0_amd64.AppImage', how: 'appimage' });
expect(findInstalled('linux', {}, at([]), '/home/you', () => ['storeboundish.AppImage'])).toBeNull();
});
test('opened with the folder as a deep link, never through a shell', () => {
const link = openFolderLink('/Users/you/games/Demo App & co');
expect(link).toBe('storebound://open?path=%2FUsers%2Fyou%2Fgames%2FDemo App%20%26%20co');
expect(new URL(link).searchParams.get('path')).toBe('/Users/you/games/Demo App & co');
expect(openCommand('darwin', { path: '/Applications/storebound.app', how: 'bundle' }, link)).toEqual(['open', '-b', 'dev.storebound.desktop', link]);
expect(openCommand('darwin', { path: '/Applications/storebound.app', how: 'bundle' }, null)).toEqual(['open', '-b', 'dev.storebound.desktop']);
expect(openCommand('win32', { path: 'C:\\x\\storebound-desktop.exe', how: 'exe' }, link)).toEqual(['C:\\x\\storebound-desktop.exe', link]);
});
});