From 750f3114c1952b334afa0b78169d81507df27cd5 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:58:33 +0100 Subject: [PATCH] fix: keep an old plugin's bundle hash when the shared store hiccups while it is fixed #1066 --- .../plugin-registry-legacy-hash.test.ts | 56 +++++++++++++++++++ lib/admin/plugin-registry.ts | 4 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 lib/admin/__tests__/plugin-registry-legacy-hash.test.ts diff --git a/lib/admin/__tests__/plugin-registry-legacy-hash.test.ts b/lib/admin/__tests__/plugin-registry-legacy-hash.test.ts new file mode 100644 index 000000000..9eca88c7a --- /dev/null +++ b/lib/admin/__tests__/plugin-registry-legacy-hash.test.ts @@ -0,0 +1,56 @@ +// @vitest-environment node +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { setStoreForTests, StoreUnavailableError, type CollectionOptions } from '@/lib/shared-store'; +import { resetLocalStoreForTests } from '@/lib/shared-store/local'; +import { separateFakeStore } from './shared-store-fake'; + +// Registry entries from old builds carry a 16-character bundle hash, which +// is replaced by the full hash of the stored bundle (or cleared when the +// bundle is gone). A shared-store hiccup while reading the bundle (#1066) +// is not "the bundle is gone": clearing the hash then made clients skip the +// integrity check and dropped the hash from the stored registry for good. + +let dir: string; + +beforeEach(() => { + dir = mkdtempSync(path.join(tmpdir(), 'bw-legacy-hash-')); + vi.stubEnv('ADMIN_CONFIG_DIR', path.join(dir, 'config')); + resetLocalStoreForTests(); + vi.resetModules(); + delete (globalThis as Record)[Symbol.for('bulwark.admin.pluginRegistry')]; +}); + +afterEach(() => { + setStoreForTests(null); + resetLocalStoreForTests(); + vi.unstubAllEnvs(); + rmSync(dir, { recursive: true, force: true }); +}); + +describe('legacy short bundle hashes', () => { + it('keeps the hash when the bundle read hits a store outage', async () => { + const store = separateFakeStore(path.join(dir, 'store')); + const collection = store.collection.bind(store); + let blip = true; + store.collection = (name: string, options: CollectionOptions) => { + const docs = collection(name, options); + return { ...docs, get: (id: string) => (blip && id === 'demo.js' ? Promise.reject(new StoreUnavailableError('blip')) : docs.get(id)) }; + }; + setStoreForTests(store); + const { pluginDocs } = await import('../extension-store'); + const { getPluginRegistry } = await import('../plugin-registry'); + await pluginDocs().put('registry.json', JSON.stringify({ plugins: [{ id: 'demo', bundleHash: 'abcdef0123456789', enabled: true }] })); + await pluginDocs().put('demo.js', 'code()'); + + const registry = await getPluginRegistry(); + expect(registry.plugins[0].bundleHash).toBe('abcdef0123456789'); + blip = false; + expect(JSON.parse((await pluginDocs().get('registry.json'))!.data.toString()).plugins[0].bundleHash).toBe('abcdef0123456789'); + + // Once the store answers, the hash is fixed. + expect((await getPluginRegistry()).plugins[0].bundleHash).toMatch(/^[0-9a-f]{64}$/); + }); +}); diff --git a/lib/admin/plugin-registry.ts b/lib/admin/plugin-registry.ts index 90d45a588..05d0ee170 100644 --- a/lib/admin/plugin-registry.ts +++ b/lib/admin/plugin-registry.ts @@ -296,7 +296,9 @@ async function bundleHashFixes(registry: PluginRegistry): Promise