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.
136 lines
9.4 KiB
TypeScript
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]);
|
|
});
|
|
});
|