Draft generic audio manifest admission and public media adapter

This commit is contained in:
archipelago
2026-10-07 01:05:33 -04:00
parent 30b5975534
commit f1fb388ded
9 changed files with 251 additions and 14 deletions
@@ -0,0 +1,19 @@
import {afterEach,describe,expect,it,vi} from 'vitest'
import {__setSignedCatalogForTests,appHasMediaBridge,appRequiresHostFrame,ensureAppLaunchPolicy} from '../curatedApps'
afterEach(()=>{__setSignedCatalogForTests(null);vi.unstubAllGlobals()})
describe('generic manifest media eligibility',()=>{
it('accepts a verified ordinary catalog app at its installed version',()=>{
__setSignedCatalogForTests({apps:{'public-audio':{version:'2',manifest:{app:{id:'public-audio',metadata:{launch:{media_controls:'archipelago-v1',requires_host_frame:true}}}}}}})
expect(appHasMediaBridge('public-audio','2')).toBe(true);expect(appRequiresHostFrame('public-audio','2')).toBe(true)
expect(appHasMediaBridge('public-audio','1')).toBe(false)
})
it('does not grant a public catalog authority over node-only demos or conflicting launch policy',()=>{
__setSignedCatalogForTests({apps:{'node-demo-injected':{version:'1',manifest:{app:{id:'node-demo-injected',metadata:{launch:{media_controls:'archipelago-v1'}}}}},bad:{version:'1',manifest:{app:{id:'bad',metadata:{launch:{media_controls:'archipelago-v1',open_in_new_tab:true}}}}}}})
expect(appHasMediaBridge('node-demo-injected','1')).toBe(false);expect(appHasMediaBridge('bad','1')).toBe(false)
})
it('loads only the authenticated verified endpoint before first launch',async()=>{
const fetcher=vi.fn(async(_url:string,_init?:RequestInit)=>({ok:true,json:async()=>({apps:{music:{version:'1',manifest:{app:{id:'music',metadata:{launch:{media_controls:'archipelago-v1'}}}}}}})}))
vi.stubGlobal('fetch',fetcher);await Promise.all([ensureAppLaunchPolicy(),ensureAppLaunchPolicy()])
expect(fetcher).toHaveBeenCalledTimes(1);expect(fetcher.mock.calls[0]?.[0]).toBe('/api/app-catalog');expect(appHasMediaBridge('music','1')).toBe(true)
})
})
+36 -3
View File
@@ -68,7 +68,7 @@ export interface SignedAppEntry {
description?: string
category?: string
container?: { image?: string }
metadata?: { icon?: string; author?: string; repo?: string; launch?: { media_controls?: string } }
metadata?: { icon?: string; author?: string; repo?: string; launch?: { media_controls?: string; requires_host_frame?: boolean; open_in_new_tab?: boolean } }
ports?: { host?: number | string; container?: number | string; auth?: string }[]
}
}
@@ -110,8 +110,41 @@ let nodeCatalogRequest: Promise<SignedAppCatalog | null> | null = null
export function nodeAppIsAvailable(id: string): boolean {
return nodeCatalogExpires > Date.now() && Boolean(nodeCatalogCache?.apps[id])
}
export function appHasMediaBridge(id: string): boolean {
return nodeAppIsAvailable(id) && nodeCatalogCache?.apps[id]?.manifest?.app?.metadata?.launch?.media_controls === 'archipelago-v1'
function verifiedLaunchEntry(id: string): SignedAppEntry | undefined {
// Node-only app names never acquire authority from the public catalog.
const entry = id.startsWith('node-demo-')
? (nodeAppIsAvailable(id) ? nodeCatalogCache?.apps[id] : undefined)
: signedCatalogCache?.apps[id]
return entry?.manifest?.app?.id === id ? entry : undefined
}
export function appHasMediaBridge(id: string, installedVersion?: string): boolean {
const entry = verifiedLaunchEntry(id)
if (!entry || (installedVersion !== undefined && entry.version !== 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)
&& entry.manifest?.app?.metadata?.launch?.requires_host_frame === true
&& entry.manifest.app.metadata.launch.open_in_new_tab !== true)
}
let launchPolicyRequest: Promise<void> | null = null
export function appLaunchPolicyLoaded(): boolean { return signedCatalogCache !== null }
/** Read only the authenticated daemon-verified catalog; community fallback
* metadata must never grant a native integration. */
export async function ensureAppLaunchPolicy(): Promise<void> {
if (signedCatalogCache) return
if (!launchPolicyRequest) launchPolicyRequest = (async () => {
try {
const response = await fetch('/api/app-catalog', {credentials:'include',signal:AbortSignal.timeout(5000)})
if (!response.ok) return
const value = await response.json() as SignedAppCatalog
if (value.apps && !Array.isArray(value.apps)) signedCatalogCache = value
} catch { /* Unavailable policy cannot authorize a new integration. */ }
finally { launchPolicyRequest = null }
})()
await launchPolicyRequest
}
/** Resolve node-owned launch policy independently of the public storefront.