From fdc596854ecdad5af3a6d56dc76212f0f2b611ba Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 7 Oct 2026 01:46:58 -0400 Subject: [PATCH] Match legacy installed image digests to verified native media policy --- .../discover/__tests__/nodeCatalog.test.ts | 25 +++++++++++++++++++ neode-ui/src/views/discover/curatedApps.ts | 18 +++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/neode-ui/src/views/discover/__tests__/nodeCatalog.test.ts b/neode-ui/src/views/discover/__tests__/nodeCatalog.test.ts index de9e6049..aaa29142 100644 --- a/neode-ui/src/views/discover/__tests__/nodeCatalog.test.ts +++ b/neode-ui/src/views/discover/__tests__/nodeCatalog.test.ts @@ -27,6 +27,31 @@ describe('node-scoped demo catalog', () => { expect(await ensureNodeAppAvailable('node-demo-v4v')).toBe(true) expect(fetcher).toHaveBeenCalledTimes(1) }) + it('admits the exact digest-version reported by the installed private demo', async () => { + vi.resetModules() + const digest = '4f28702e4f85368e4ad886f06d58b38f869c560c90ca40487bfaebe69090a870' + const image = `localhost/node-demo:0.6.7-alpha-private@sha256:${digest}` + const payload = { ...demo(), apps: { 'node-demo-v4v': { + version: '0.6.7-alpha-private', digest: `sha256:${digest}`, image, + manifest: { app: { id: 'node-demo-v4v', version: '0.6.7-alpha-private', + container: { image }, metadata: { launch: { media_controls: 'archipelago-v1', requires_host_frame: true } } } }, + } } } + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, json: async () => payload }))) + const { ensureNodeAppAvailable, appHasMediaBridge, appRequiresHostFrame } = await import('../curatedApps') + expect(await ensureNodeAppAvailable('node-demo-v4v')).toBe(true) + expect(appHasMediaBridge('node-demo-v4v', digest)).toBe(true) + expect(appRequiresHostFrame('node-demo-v4v', digest)).toBe(true) + expect(appHasMediaBridge('node-demo-v4v', 'a'.repeat(64))).toBe(false) + expect(appHasMediaBridge('node-demo-v4v', digest.slice(0, 12))).toBe(false) + payload.apps['node-demo-v4v'].digest = `sha256:${'b'.repeat(64)}` + expect(appHasMediaBridge('node-demo-v4v', digest)).toBe(false) + payload.apps['node-demo-v4v'].digest = `sha256:${digest}` + payload.apps['node-demo-v4v'].manifest.app.container.image = `${image.slice(0, image.indexOf('@'))}@sha256:${'c'.repeat(64)}` + expect(appHasMediaBridge('node-demo-v4v', digest)).toBe(false) + payload.apps['node-demo-v4v'].manifest.app.container.image = image + payload.apps['node-demo-v4v'].image = 'localhost/node-demo:mutable' + expect(appHasMediaBridge('node-demo-v4v', digest)).toBe(false) + }) it('fails closed for unavailable and expired launch policy', async () => { vi.resetModules() const fetcher = vi.fn(async () => ({ ok: true, json: async () => ({ ...demo(), expires_at: '2000-01-01T00:00:00Z' }) })) diff --git a/neode-ui/src/views/discover/curatedApps.ts b/neode-ui/src/views/discover/curatedApps.ts index e2f58c3f..fb341624 100644 --- a/neode-ui/src/views/discover/curatedApps.ts +++ b/neode-ui/src/views/discover/curatedApps.ts @@ -59,6 +59,7 @@ export interface SignedAppCatalog { export interface SignedAppEntry { version: string + digest?: string image?: string manifest?: { app?: { @@ -117,15 +118,28 @@ function verifiedLaunchEntry(id: string): SignedAppEntry | undefined { : signedCatalogCache?.apps[id] return entry?.manifest?.app?.id === id ? entry : undefined } +/** Older package scanners expose the inspected image digest as manifest.version. + * Accept that identity only when it matches the verified immutable image pin; + * a tag, abbreviated digest or conflicting catalog field cannot substitute. */ +function installedEntryMatches(entry: SignedAppEntry, installedVersion?: string): boolean { + if (installedVersion === undefined || entry.version === installedVersion) return true + if (!/^[a-f0-9]{64}$/i.test(installedVersion)) return false + const image = entry.image ?? entry.manifest?.app?.container?.image + const pin = image?.match(/@sha256:([a-f0-9]{64})$/i)?.[1]?.toLowerCase() + if (!pin || pin !== installedVersion.toLowerCase()) return false + if (entry.digest !== undefined && entry.digest.toLowerCase() !== `sha256:${pin}`) return false + const manifestImage = entry.manifest?.app?.container?.image + return manifestImage === undefined || manifestImage.match(/@sha256:([a-f0-9]{64})$/i)?.[1]?.toLowerCase() === pin +} export function appHasMediaBridge(id: string, installedVersion?: string): boolean { const entry = verifiedLaunchEntry(id) - if (!entry || (installedVersion !== undefined && entry.version !== installedVersion)) return false + if (!entry || !installedEntryMatches(entry, installedVersion)) return false const launch = entry.manifest?.app?.metadata?.launch return launch?.media_controls === 'archipelago-v1' && launch.open_in_new_tab !== true } export function appRequiresHostFrame(id: string, installedVersion?: string): boolean { const entry = verifiedLaunchEntry(id) - return Boolean(entry && (installedVersion === undefined || entry.version === installedVersion) + return Boolean(entry && installedEntryMatches(entry, installedVersion) && entry.manifest?.app?.metadata?.launch?.requires_host_frame === true && entry.manifest.app.metadata.launch.open_in_new_tab !== true) }