Match legacy installed image digests to verified native media policy
This commit is contained in:
@@ -27,6 +27,31 @@ describe('node-scoped demo catalog', () => {
|
|||||||
expect(await ensureNodeAppAvailable('node-demo-v4v')).toBe(true)
|
expect(await ensureNodeAppAvailable('node-demo-v4v')).toBe(true)
|
||||||
expect(fetcher).toHaveBeenCalledTimes(1)
|
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 () => {
|
it('fails closed for unavailable and expired launch policy', async () => {
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
const fetcher = vi.fn(async () => ({ ok: true, json: async () => ({ ...demo(), expires_at: '2000-01-01T00:00:00Z' }) }))
|
const fetcher = vi.fn(async () => ({ ok: true, json: async () => ({ ...demo(), expires_at: '2000-01-01T00:00:00Z' }) }))
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface SignedAppCatalog {
|
|||||||
|
|
||||||
export interface SignedAppEntry {
|
export interface SignedAppEntry {
|
||||||
version: string
|
version: string
|
||||||
|
digest?: string
|
||||||
image?: string
|
image?: string
|
||||||
manifest?: {
|
manifest?: {
|
||||||
app?: {
|
app?: {
|
||||||
@@ -117,15 +118,28 @@ function verifiedLaunchEntry(id: string): SignedAppEntry | undefined {
|
|||||||
: signedCatalogCache?.apps[id]
|
: signedCatalogCache?.apps[id]
|
||||||
return entry?.manifest?.app?.id === id ? entry : undefined
|
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 {
|
export function appHasMediaBridge(id: string, installedVersion?: string): boolean {
|
||||||
const entry = verifiedLaunchEntry(id)
|
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
|
const launch = entry.manifest?.app?.metadata?.launch
|
||||||
return launch?.media_controls === 'archipelago-v1' && launch.open_in_new_tab !== true
|
return launch?.media_controls === 'archipelago-v1' && launch.open_in_new_tab !== true
|
||||||
}
|
}
|
||||||
export function appRequiresHostFrame(id: string, installedVersion?: string): boolean {
|
export function appRequiresHostFrame(id: string, installedVersion?: string): boolean {
|
||||||
const entry = verifiedLaunchEntry(id)
|
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?.requires_host_frame === true
|
||||||
&& entry.manifest.app.metadata.launch.open_in_new_tab !== true)
|
&& entry.manifest.app.metadata.launch.open_in_new_tab !== true)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user