// Distribution features: plugin permissions on X's API, accurate headers on every bundled plugin, // the install screen's helpers as the app binds them (framework storeModel), the store catalog on // disk (X's part of the shared catalog), and the signing script round trip (framework CLI). import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { createPluginContext, parsePermissions, PluginPermissionError } from '../../../waybetter/packages/runtime/src/permissions.ts'; import { createPluginApi, X_PERMISSION_GATES } from '../src/inject/pluginApi.ts'; import { supersededFiles } from '../../../waybetter/packages/runtime/src/featureTable.ts'; import { X_TABLE } from '../src/areas/table.ts'; import { filterItems, freeThemeId, installedThemeId, itemState, kindForFile, newerVersion, permissionBadges, previewLines, type StoreItem } from '../src/shell/installModel.ts'; const ROOT = new URL('../../../', import.meta.url); const PLUGINS = new URL('../src-tauri/bundled-plugins/', import.meta.url); const fakeWindow = { document: { documentElement: {} }, location: { href: 'https://x.com/home' } } as unknown as Window & typeof globalThis; function header(source: string): string[] | null { const body = /^\s*\/\*\*([\s\S]*?)\*\//.exec(source)?.[1] ?? ''; const line = body.split(/\r?\n/).map(l => l.trim().replace(/^\*+/, '').trim()).find(l => l.startsWith('@permissions')); if (!line) return null; const parsed = parsePermissions(line.slice('@permissions'.length)); assert.ok('permissions' in parsed, `invalid @permissions: ${line}`); return (parsed as { permissions: string[] }).permissions; } /** Gated API members a plugin's source uses (the same members X_PERMISSION_GATES lists). */ function used(source: string): string[] { const out = new Set(); if (/\.storage\(/.test(source)) out.add('storage'); if (/\.ui\b|\.toast\(/.test(source)) out.add('ui'); if (/\.copyText\(/.test(source)) out.add('clipboard'); if (/\.account\(/.test(source)) out.add('account'); if (/\.users\./.test(source)) out.add('users'); if (/\.commands\./.test(source)) out.add('commands'); return [...out].sort(); } test("X's plugin API gates account() and users on top of the core's storage/ui/clipboard", () => { assert.deepEqual({ ...X_PERMISSION_GATES }, { storage: 'storage', ui: 'ui', toast: 'ui', copyText: 'clipboard', commands: 'commands', account: 'account', users: 'users' }); const full = createPluginApi(fakeWindow); const context = createPluginContext(full, X_PERMISSION_GATES); const view = context.run('notes.plugin.js', ['storage'], () => context.current()); assert.throws(() => view.account(), (error: unknown) => error instanceof PluginPermissionError && /notes\.plugin\.js: api\.account needs the "account" permission/.test(error.message)); assert.throws(() => view.users.get('someone'), /api\.users\.get needs the "users" permission/); assert.throws(() => view.copyText('x'), /"clipboard"/); assert.equal(typeof view.tweet, 'function', 'reading a rendered post needs no permission'); // A legacy plugin (no header line) keeps the full API. const legacy = context.run('old.plugin.js', null, () => context.current()); assert.equal(legacy, full); }); test('every bundled plugin declares exactly the gated API it uses, plus network:none', () => { const files = readdirSync(PLUGINS).filter(file => file.endsWith('.plugin.js')); assert.ok(files.length >= 14); for (const file of files) { const source = readFileSync(new URL(file, PLUGINS), 'utf8'); const declared = header(source); // The Emoji Picker is being redesigned in parallel; its header is added with that work. if (declared === null && file === 'emoji-picker.plugin.js') continue; assert.ok(declared, `${file} has @permissions`); assert.deepEqual(declared!.filter(p => !p.startsWith('network')).sort(), used(source), `${file}: declared vs used`); assert.ok(declared!.includes('network:none'), `${file} promises no network`); assert.doesNotMatch(source, /\bfetch\(|XMLHttpRequest|WebSocket|sendBeacon|EventSource/, `${file} really makes no requests`); } }); const item = (patch: Partial): StoreItem => ({ apps: ['x'], kind: 'plugin', id: 'tool', name: 'Tool', author: 'me', version: '1.1.0', description: 'Does things', category: 'tools', tags: ['x'], url: 'https://waybetter.fyi/store/plugins/tool.plugin.js', file: 'tool.plugin.js', sha256: 'a'.repeat(64), size: 10, permissions: ['ui'], ...patch, }); test('store item state: install, installed, update, changed locally, built in', () => { const base = { plugins: {}, records: {}, builtinThemes: ['nord'], userThemes: [] as string[] }; assert.equal(itemState(item({}), base).state, 'install'); assert.equal(itemState(item({}), { ...base, plugins: { 'tool.plugin.js': { version: '1.1.0', sha256: 'a'.repeat(64), enabled: true } } }).state, 'installed'); assert.equal(itemState(item({}), { ...base, plugins: { 'tool.plugin.js': { version: '1.0.0', sha256: 'b'.repeat(64), enabled: false } } }).state, 'update'); assert.equal(itemState(item({}), { ...base, plugins: { 'tool.plugin.js': { version: '1.1.0', sha256: 'b'.repeat(64), enabled: false } } }).state, 'modified'); const theme = item({ kind: 'theme', id: 'nord', file: undefined, permissions: undefined }); assert.equal(itemState(theme, base).state, 'builtin'); const own = item({ kind: 'theme', id: 'sunset', file: undefined, version: '1.2.0' }); const records = { 'theme:sunset-2': { kind: 'theme' as const, source: 'store' as const, storeId: 'sunset', version: '1.0.0', sha256: 'c'.repeat(64), installedAt: '' } }; assert.equal(itemState(own, { ...base, records, userThemes: ['sunset-2'] }).state, 'update'); assert.equal(itemState(own, { ...base, records, userThemes: [] }).state, 'install', 'a theme the owner deleted is not installed'); assert.equal(installedThemeId(own, records, ['sunset-2']), 'sunset-2'); }); test('install helpers: file kinds, theme ids, versions, badges, search, previews', () => { assert.equal(kindForFile('Focus.plugin.js'), 'plugin'); assert.equal(kindForFile('nord.bttheme.JSON'), 'theme'); assert.equal(kindForFile('photo.png'), null); assert.equal(freeThemeId('nord', ['x', 'paper']), 'nord'); assert.equal(freeThemeId('nord', ['nord', 'nord-2']), 'nord-3'); assert.ok(newerVersion('1.10.0', '1.9.0') && !newerVersion('1.0.0', '1.0.0') && !newerVersion('0.9', '1.0')); assert.equal(permissionBadges(null)[0]!.label, 'Legacy ยท full page access'); assert.equal(permissionBadges([])[0]!.id, 'none'); assert.deepEqual(permissionBadges(['ui', 'network:none']).map(b => b.tone), ['plain', 'ok']); const items = [item({}), item({ id: 'nord', kind: 'theme', name: 'Nord', category: 'dark', description: 'Arctic', permissions: undefined })]; assert.deepEqual(filterItems(items, 'arctic', 'all', 'all').map(i => i.id), ['nord']); assert.deepEqual(filterItems(items, '', 'plugin', 'all').map(i => i.id), ['tool']); assert.deepEqual(filterItems(items, 'ui', 'all', 'tools').map(i => i.id), ['tool'], 'permissions are searchable'); assert.deepEqual(previewLines('a\r\nb\nc', 2), { lines: ['a', 'b'], hidden: 1 }); }); test('the committed store catalog lists every bundled plugin with its header version and permissions', () => { const catalog = JSON.parse(readFileSync(new URL('apps/web/public/store/catalog.json', ROOT), 'utf8')) as { format: number; items: StoreItem[] }; assert.equal(catalog.format, 2); const mine = catalog.items.filter(entry => entry.apps?.includes('x')); assert.ok(mine.length > 0, "X's items are in the shared catalog"); const plugins = mine.filter(entry => entry.kind === 'plugin'); // A bundled file a built-in feature replaced stays in the app for the one-time move of an install, // and is not a Store item any more (its area has a page on the site instead). const superseded = new Set(supersededFiles(X_TABLE).map(entry => entry.file)); const bundled = readdirSync(PLUGINS).filter(file => file.endsWith('.plugin.js') && !superseded.has(`bt-${file}`)); assert.ok(bundled.length > 0 && superseded.size === 13, 'the Emoji Picker stays a plugin; the other thirteen are features'); assert.deepEqual(plugins.map(entry => entry.file).sort(), bundled.map(file => `bt-${file}`).sort(), 'every bundled plugin that is not a feature is in the store'); for (const entry of plugins) { assert.match(entry.file!, /^bt-[a-z-]+\.plugin\.js$/, 'installed under the bundled file name'); assert.ok(entry.url.startsWith('https://waybetter.fyi/store/')); const copy = readFileSync(new URL(`apps/web/public/store/plugins/${entry.file}`, ROOT)); assert.equal(copy.length, entry.size); } assert.ok(mine.some(entry => entry.kind === 'theme' && entry.id === 'nord')); assert.ok(!mine.some(entry => entry.kind === 'theme' && entry.id === 'x'), "X's own look is not in the store"); }); test('sign-store.mjs: a fresh key signs, the signature verifies, a changed byte does not', () => { const dir = mkdtempSync(join(tmpdir(), 'wb-store-')); const key = join(dir, 'test-store.key'); const pub = join(dir, 'store.pub'); const catalog = join(dir, 'catalog.json'); writeFileSync(catalog, JSON.stringify({ format: 2, name: 'Test', generatedAt: '2026-09-28T00:00:00Z', items: [] })); const script = new URL('scripts/sign-store.mjs', ROOT).pathname.replace(/^\/([A-Za-z]:)/, '$1'); const run = (...args: string[]) => execFileSync(process.execPath, [script, ...args], { encoding: 'utf8' }); assert.match(run('--init', '--key', key, '--pub', pub), /Created the store key/); assert.doesNotMatch(run('--init', '--key', key, '--pub', pub), /PRIVATE/, 'never prints key material'); assert.match(run(catalog, '--key', key, '--pub', pub), /Signed and verified/); assert.equal(Buffer.from(readFileSync(`${catalog}.sig`, 'utf8').trim(), 'base64').length, 64); assert.equal(Buffer.from(readFileSync(pub, 'utf8').trim(), 'base64').length, 32); writeFileSync(catalog, readFileSync(catalog, 'utf8').replace('Test', 'Tesu')); assert.throws(() => execFileSync(process.execPath, [script, '--verify', catalog, '--pub', pub], { stdio: 'pipe' }), /does NOT verify|Command failed/); });