Draft generic audio manifest admission and public media adapter
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user