diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 27070ca61..711dcd91c 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -180,7 +180,23 @@ ${Object.entries(VIDEO_LIBRARY) ); return { contents: [ - { uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html }, + { + uri: RESOURCE_URI, + mimeType: RESOURCE_MIME_TYPE, + text: html, + _meta: { + ui: { + // Media permissions a video player requests; the native controls + // use fullscreen and picture-in-picture + permissions: { + fullscreen: {}, + "picture-in-picture": {}, + autoplay: {}, + "encrypted-media": {}, + }, + }, + }, + }, ], }; }, diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index f2523f25e..b3ce82dfe 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -165,7 +165,11 @@ interface UIResourceMeta { * Sandbox permissions requested by the UI * * Servers declare which browser capabilities their UI needs. + * Keys are Permission Policy feature names. + * * Hosts MAY honor these by setting appropriate iframe `allow` attributes. + * Hosts SHOULD recognize the named keys, MAY honor other feature names, and + * MUST NOT apply keys that are not valid Permission Policy feature names. * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. */ permissions?: { @@ -192,7 +196,41 @@ interface UIResourceMeta { * * Maps to Permission Policy `clipboard-write` feature */ + "clipboard-write"?: {}, + /** + * Legacy alias for `clipboard-write` + * + * @deprecated Use `"clipboard-write"` instead + */ clipboardWrite?: {}, + /** + * Request Fullscreen API access + * + * Maps to Permission Policy `fullscreen` feature + */ + fullscreen?: {}, + /** + * Request Picture-in-Picture API access + * + * Maps to Permission Policy `picture-in-picture` feature + */ + "picture-in-picture"?: {}, + /** + * Request media autoplay with sound + * + * Maps to Permission Policy `autoplay` feature + */ + autoplay?: {}, + /** + * Request Encrypted Media Extensions access for DRM-protected media playback + * + * Maps to Permission Policy `encrypted-media` feature + */ + "encrypted-media"?: {}, + /** + * Any other Permission Policy feature, keyed by its feature name + */ + [feature: string]: {}, }, /** * Dedicated origin for view @@ -247,10 +285,16 @@ The resource content is returned via `resources/read`: baseUriDomains?: string[]; // Allowed base URIs for the document (base-uri directive). }; permissions?: { - camera?: {}; // Request camera access - microphone?: {}; // Request microphone access - geolocation?: {}; // Request geolocation access - clipboardWrite?: {}; // Request clipboard write access + camera?: {}; // Request camera access + microphone?: {}; // Request microphone access + geolocation?: {}; // Request geolocation access + "clipboard-write"?: {}; // Request clipboard write access + clipboardWrite?: {}; // Legacy alias for clipboard-write + fullscreen?: {}; // Request Fullscreen API access + "picture-in-picture"?: {}; // Request Picture-in-Picture API access + autoplay?: {}; // Request media autoplay with sound + "encrypted-media"?: {}; // Request Encrypted Media Extensions access for DRM-protected media playback + [feature: string]: {}; // Any other Permission Policy feature, by name }; domain?: string; prefersBorder?: boolean; @@ -500,7 +544,7 @@ If the Host is a web page, it MUST wrap the View and communicate with it through - If `baseUriDomains` is provided, allow base URIs from declared origins; otherwise use `base-uri 'self'` - Block dangerous features (`object-src 'none'`) - Apply restrictive defaults if no CSP metadata is provided - - If `permissions` is declared, the Sandbox MAY set the inner iframe's `allow` attribute accordingly + - If `permissions` is declared, the Sandbox MAY set the inner iframe's `allow` attribute accordingly, applying only keys that are valid Permission Policy feature names 6. The Sandbox MUST forward messages sent by the Host to the View, and vice versa, for any method that doesn't start with `ui/notifications/sandbox-`. This includes lifecycle messages, e.g., `ui/initialize` request & `ui/notifications/initialized` notification both sent by the View. The Host MUST NOT send any request or notification to the View before it receives an `initialized` notification. 7. The Sandbox SHOULD NOT create/send any requests to the Host or to the View (this would require synthesizing new request ids). 8. The Host MAY forward any message from the View (coming via the Sandbox) to the MCP Apps server, for any method that doesn't start with `ui/`. While the Host SHOULD ensure the View's MCP connection is spec-compliant, it MAY decide to block some messages or subject them to further user approval. @@ -697,12 +741,18 @@ interface HostCapabilities { logging?: {}; /** Sandbox configuration applied by the host. */ sandbox?: { - /** Permissions granted by the host (camera, microphone, geolocation, clipboard-write). */ + /** Permission Policy features granted by the host. */ permissions?: { camera?: {}; microphone?: {}; geolocation?: {}; + "clipboard-write"?: {}; clipboardWrite?: {}; + fullscreen?: {}; + "picture-in-picture"?: {}; + autoplay?: {}; + "encrypted-media"?: {}; + [feature: string]: {}; }; /** CSP domains approved by the host. */ csp?: { @@ -1503,7 +1553,13 @@ These messages are reserved for web-based hosts that implement the recommended d camera?: {}, microphone?: {}, geolocation?: {}, + "clipboard-write"?: {}, clipboardWrite?: {}, + fullscreen?: {}, + "picture-in-picture"?: {}, + autoplay?: {}, + "encrypted-media"?: {}, + [feature: string]: {}, } } } diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index a87b0e7db..876621c18 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -3109,6 +3109,63 @@ describe("buildAllowAttribute", () => { "clipboard-write", ); }); + + it("when only clipboard-write is set", () => { + expect(buildAllowAttribute({ "clipboard-write": {} })).toBe( + "clipboard-write", + ); + }); + + it("when clipboard-write is requested under both its key and its alias, emits it once", () => { + expect( + buildAllowAttribute({ "clipboard-write": {}, clipboardWrite: {} }), + ).toBe("clipboard-write"); + }); + + it("when only fullscreen is set", () => { + expect(buildAllowAttribute({ fullscreen: {} })).toBe("fullscreen"); + }); + + it("when only picture-in-picture is set", () => { + expect(buildAllowAttribute({ "picture-in-picture": {} })).toBe( + "picture-in-picture", + ); + }); + + it("when only autoplay is set", () => { + expect(buildAllowAttribute({ autoplay: {} })).toBe("autoplay"); + }); + + it("when only encrypted-media is set", () => { + expect(buildAllowAttribute({ "encrypted-media": {} })).toBe( + "encrypted-media", + ); + }); + + it("when the key is a feature without a named property, passes it through verbatim", () => { + expect(buildAllowAttribute({ "xr-spatial-tracking": {} })).toBe( + "xr-spatial-tracking", + ); + }); + }); + + describe("drops entries that cannot be emitted", () => { + it("when the value is undefined", () => { + expect(buildAllowAttribute({ camera: undefined, fullscreen: {} })).toBe( + "fullscreen", + ); + }); + + it("when the key is not a bare Permission Policy feature name", () => { + expect( + buildAllowAttribute({ + "camera 'self'": {}, + Fullscreen: {}, + "camera;": {}, + toString: {}, + }), + ).toBe(""); + }); }); describe("returns multiple directives joined with '; '", () => { @@ -3124,9 +3181,16 @@ describe("buildAllowAttribute", () => { camera: {}, microphone: {}, geolocation: {}, + "clipboard-write": {}, clipboardWrite: {}, + fullscreen: {}, + "picture-in-picture": {}, + autoplay: {}, + "encrypted-media": {}, }), - ).toBe("camera; microphone; geolocation; clipboard-write"); + ).toBe( + "camera; microphone; geolocation; clipboard-write; fullscreen; picture-in-picture; autoplay; encrypted-media", + ); }); }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 1ac4db485..464982a42 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -170,14 +170,27 @@ export function isToolVisibilityAppOnly(tool: Partial): boolean { return false; } +/** + * Permission Policy feature names for the {@link McpUiResourcePermissions `McpUiResourcePermissions`} + * keys that are not spelled as their feature name. Every other key is its own feature name. + */ +const PERMISSION_POLICY_FEATURE_ALIASES: Record = { + clipboardWrite: "clipboard-write", +}; + +/** Bare Permission Policy feature tokens: lowercase letters, digits and hyphens. */ +const PERMISSION_POLICY_FEATURE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/; + /** * Build iframe `allow` attribute string from permissions. * - * Maps McpUiResourcePermissions to the Permission Policy allow attribute - * format used by iframes (e.g., "microphone; clipboard-write"). + * Emits each requested key as its Permission Policy feature name in the + * allow attribute format used by iframes (e.g., "microphone; clipboard-write"). + * Keys that are not valid feature names are dropped, so the result can be + * assigned to the attribute as is. * * @param permissions - Permissions requested by the UI resource - * @returns Space-separated permission directives, or empty string if none + * @returns Semicolon-separated permission directives, or empty string if none * * @example * ```typescript @@ -192,10 +205,19 @@ export function buildAllowAttribute( if (!permissions) return ""; const allowList: string[] = []; - if (permissions.camera) allowList.push("camera"); - if (permissions.microphone) allowList.push("microphone"); - if (permissions.geolocation) allowList.push("geolocation"); - if (permissions.clipboardWrite) allowList.push("clipboard-write"); + + for (const [key, value] of Object.entries(permissions)) { + if (!value) continue; + + const feature = PERMISSION_POLICY_FEATURE_ALIASES[key] ?? key; + + if ( + PERMISSION_POLICY_FEATURE_NAME_PATTERN.test(feature) && + !allowList.includes(feature) + ) { + allowList.push(feature); + } + } return allowList.join("; "); } diff --git a/src/generated/schema.json b/src/generated/schema.json index 246ed7d9b..62a7c4d86 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -366,7 +366,7 @@ "type": "object", "properties": { "permissions": { - "description": "Permissions granted by the host (camera, microphone, geolocation).", + "description": "Permission Policy features granted by the host.", "type": "object", "properties": { "camera": { @@ -387,14 +387,44 @@ "properties": {}, "additionalProperties": false }, - "clipboardWrite": { + "clipboard-write": { "description": "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", "type": "object", "properties": {}, "additionalProperties": false + }, + "clipboardWrite": { + "description": "Legacy alias for `clipboard-write`.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "fullscreen": { + "description": "Request Fullscreen API access.\n\nMaps to Permission Policy `fullscreen` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "picture-in-picture": { + "description": "Request Picture-in-Picture API access.\n\nMaps to Permission Policy `picture-in-picture` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "autoplay": { + "description": "Request media autoplay with sound.\n\nMaps to Permission Policy `autoplay` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "encrypted-media": { + "description": "Request Encrypted Media Extensions access for DRM-protected media playback.\n\nMaps to Permission Policy `encrypted-media` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} }, "csp": { "description": "CSP domains approved by the host.", @@ -2777,7 +2807,7 @@ "type": "object", "properties": { "permissions": { - "description": "Permissions granted by the host (camera, microphone, geolocation).", + "description": "Permission Policy features granted by the host.", "type": "object", "properties": { "camera": { @@ -2798,14 +2828,44 @@ "properties": {}, "additionalProperties": false }, - "clipboardWrite": { + "clipboard-write": { "description": "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", "type": "object", "properties": {}, "additionalProperties": false + }, + "clipboardWrite": { + "description": "Legacy alias for `clipboard-write`.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "fullscreen": { + "description": "Request Fullscreen API access.\n\nMaps to Permission Policy `fullscreen` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "picture-in-picture": { + "description": "Request Picture-in-Picture API access.\n\nMaps to Permission Policy `picture-in-picture` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "autoplay": { + "description": "Request media autoplay with sound.\n\nMaps to Permission Policy `autoplay` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "encrypted-media": { + "description": "Request Encrypted Media Extensions access for DRM-protected media playback.\n\nMaps to Permission Policy `encrypted-media` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} }, "csp": { "description": "CSP domains approved by the host.", @@ -4315,14 +4375,44 @@ "properties": {}, "additionalProperties": false }, - "clipboardWrite": { + "clipboard-write": { "description": "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", "type": "object", "properties": {}, "additionalProperties": false + }, + "clipboardWrite": { + "description": "Legacy alias for `clipboard-write`.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "fullscreen": { + "description": "Request Fullscreen API access.\n\nMaps to Permission Policy `fullscreen` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "picture-in-picture": { + "description": "Request Picture-in-Picture API access.\n\nMaps to Permission Policy `picture-in-picture` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "autoplay": { + "description": "Request media autoplay with sound.\n\nMaps to Permission Policy `autoplay` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "encrypted-media": { + "description": "Request Encrypted Media Extensions access for DRM-protected media playback.\n\nMaps to Permission Policy `encrypted-media` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} }, "domain": { "description": "Dedicated origin for view sandbox.\n\nUseful when views need stable, dedicated origins for OAuth callbacks, CORS policies, or API key allowlists.\n\n**Host-dependent:** The format and validation rules for this field are determined by each host. Servers MUST consult host-specific documentation for the expected domain format. Common patterns include:\n- Hash-based subdomains (e.g., `{hash}.claudemcpcontent.com`)\n- URL-derived subdomains (e.g., `www-example-com.oaiusercontent.com`)\n\nIf omitted, host uses default sandbox origin (typically per-conversation).", @@ -4357,14 +4447,44 @@ "properties": {}, "additionalProperties": false }, - "clipboardWrite": { + "clipboard-write": { "description": "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", "type": "object", "properties": {}, "additionalProperties": false + }, + "clipboardWrite": { + "description": "Legacy alias for `clipboard-write`.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "fullscreen": { + "description": "Request Fullscreen API access.\n\nMaps to Permission Policy `fullscreen` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "picture-in-picture": { + "description": "Request Picture-in-Picture API access.\n\nMaps to Permission Policy `picture-in-picture` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "autoplay": { + "description": "Request media autoplay with sound.\n\nMaps to Permission Policy `autoplay` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "encrypted-media": { + "description": "Request Encrypted Media Extensions access for DRM-protected media playback.\n\nMaps to Permission Policy `encrypted-media` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} }, "McpUiResourceTeardownRequest": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -4484,14 +4604,44 @@ "properties": {}, "additionalProperties": false }, - "clipboardWrite": { + "clipboard-write": { "description": "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", "type": "object", "properties": {}, "additionalProperties": false + }, + "clipboardWrite": { + "description": "Legacy alias for `clipboard-write`.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "fullscreen": { + "description": "Request Fullscreen API access.\n\nMaps to Permission Policy `fullscreen` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "picture-in-picture": { + "description": "Request Picture-in-Picture API access.\n\nMaps to Permission Policy `picture-in-picture` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "autoplay": { + "description": "Request media autoplay with sound.\n\nMaps to Permission Policy `autoplay` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "encrypted-media": { + "description": "Request Encrypted Media Extensions access for DRM-protected media playback.\n\nMaps to Permission Policy `encrypted-media` feature.", + "type": "object", + "properties": {}, + "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": {} } }, "required": ["html"], diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 214b227c2..0152f59d6 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -288,55 +288,114 @@ export const McpUiResourceCspSchema = z.object({ * @description Sandbox permissions requested by the UI resource. * * Servers declare which browser capabilities their UI needs. + * Keys are Permission Policy feature names. + * * Hosts MAY honor these by setting appropriate iframe `allow` attributes. + * Hosts SHOULD recognize the named keys, MAY honor other feature names, and + * MUST NOT apply keys that are not valid Permission Policy feature names. * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. */ -export const McpUiResourcePermissionsSchema = z.object({ - /** - * @description Request camera access. - * - * Maps to Permission Policy `camera` feature. - */ - camera: z - .object({}) - .optional() - .describe( - "Request camera access.\n\nMaps to Permission Policy `camera` feature.", - ), - /** - * @description Request microphone access. - * - * Maps to Permission Policy `microphone` feature. - */ - microphone: z - .object({}) - .optional() - .describe( - "Request microphone access.\n\nMaps to Permission Policy `microphone` feature.", - ), - /** - * @description Request geolocation access. - * - * Maps to Permission Policy `geolocation` feature. - */ - geolocation: z - .object({}) - .optional() - .describe( - "Request geolocation access.\n\nMaps to Permission Policy `geolocation` feature.", - ), - /** - * @description Request clipboard write access. - * - * Maps to Permission Policy `clipboard-write` feature. - */ - clipboardWrite: z - .object({}) - .optional() - .describe( - "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", - ), -}); +export const McpUiResourcePermissionsSchema = z + .object({ + /** + * @description Request camera access. + * + * Maps to Permission Policy `camera` feature. + */ + camera: z + .object({}) + .optional() + .describe( + "Request camera access.\n\nMaps to Permission Policy `camera` feature.", + ), + /** + * @description Request microphone access. + * + * Maps to Permission Policy `microphone` feature. + */ + microphone: z + .object({}) + .optional() + .describe( + "Request microphone access.\n\nMaps to Permission Policy `microphone` feature.", + ), + /** + * @description Request geolocation access. + * + * Maps to Permission Policy `geolocation` feature. + */ + geolocation: z + .object({}) + .optional() + .describe( + "Request geolocation access.\n\nMaps to Permission Policy `geolocation` feature.", + ), + /** + * @description Request clipboard write access. + * + * Maps to Permission Policy `clipboard-write` feature. + */ + "clipboard-write": z + .object({}) + .optional() + .describe( + "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", + ), + /** + * @description Legacy alias for `clipboard-write`. + * + * @deprecated Use `"clipboard-write"` instead. + */ + clipboardWrite: z + .object({}) + .optional() + .describe("Legacy alias for `clipboard-write`."), + /** + * @description Request Fullscreen API access. + * + * Maps to Permission Policy `fullscreen` feature. + */ + fullscreen: z + .object({}) + .optional() + .describe( + "Request Fullscreen API access.\n\nMaps to Permission Policy `fullscreen` feature.", + ), + /** + * @description Request Picture-in-Picture API access. + * + * Maps to Permission Policy `picture-in-picture` feature. + */ + "picture-in-picture": z + .object({}) + .optional() + .describe( + "Request Picture-in-Picture API access.\n\nMaps to Permission Policy `picture-in-picture` feature.", + ), + /** + * @description Request media autoplay with sound. + * + * Maps to Permission Policy `autoplay` feature. + */ + autoplay: z + .object({}) + .optional() + .describe( + "Request media autoplay with sound.\n\nMaps to Permission Policy `autoplay` feature.", + ), + /** + * @description Request Encrypted Media Extensions access for DRM-protected media playback. + * + * Maps to Permission Policy `encrypted-media` feature. + */ + "encrypted-media": z + .object({}) + .optional() + .describe( + "Request Encrypted Media Extensions access for DRM-protected media playback.\n\nMaps to Permission Policy `encrypted-media` feature.", + ), + }) + .passthrough(); /** * @description Notification of UI size changes (View -> Host). @@ -543,9 +602,9 @@ export const McpUiHostCapabilitiesSchema = z.object({ /** @description Sandbox configuration applied by the host. */ sandbox: z .object({ - /** @description Permissions granted by the host (camera, microphone, geolocation). */ + /** @description Permission Policy features granted by the host. */ permissions: McpUiResourcePermissionsSchema.optional().describe( - "Permissions granted by the host (camera, microphone, geolocation).", + "Permission Policy features granted by the host.", ), /** @description CSP domains approved by the host. */ csp: McpUiResourceCspSchema.optional().describe( diff --git a/src/spec.types.ts b/src/spec.types.ts index 0c4539637..10d26d298 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -512,7 +512,7 @@ export interface McpUiHostCapabilities { logging?: {}; /** @description Sandbox configuration applied by the host. */ sandbox?: { - /** @description Permissions granted by the host (camera, microphone, geolocation). */ + /** @description Permission Policy features granted by the host. */ permissions?: McpUiResourcePermissions; /** @description CSP domains approved by the host. */ csp?: McpUiResourceCsp; @@ -658,7 +658,11 @@ export interface McpUiResourceCsp { * @description Sandbox permissions requested by the UI resource. * * Servers declare which browser capabilities their UI needs. + * Keys are Permission Policy feature names. + * * Hosts MAY honor these by setting appropriate iframe `allow` attributes. + * Hosts SHOULD recognize the named keys, MAY honor other feature names, and + * MUST NOT apply keys that are not valid Permission Policy feature names. * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. */ export interface McpUiResourcePermissions { @@ -685,7 +689,45 @@ export interface McpUiResourcePermissions { * * Maps to Permission Policy `clipboard-write` feature. */ + "clipboard-write"?: {}; + /** + * @description Legacy alias for `clipboard-write`. + * + * @deprecated Use `"clipboard-write"` instead. + */ clipboardWrite?: {}; + /** + * @description Request Fullscreen API access. + * + * Maps to Permission Policy `fullscreen` feature. + */ + fullscreen?: {}; + /** + * @description Request Picture-in-Picture API access. + * + * Maps to Permission Policy `picture-in-picture` feature. + */ + "picture-in-picture"?: {}; + /** + * @description Request media autoplay with sound. + * + * Maps to Permission Policy `autoplay` feature. + */ + autoplay?: {}; + /** + * @description Request Encrypted Media Extensions access for DRM-protected media playback. + * + * Maps to Permission Policy `encrypted-media` feature. + */ + "encrypted-media"?: {}; + /** + * Any other Permission Policy feature, keyed by its feature name. + * + * Typed `unknown` because the schema generator turns only that form into a + * passthrough object, so parsing keeps keys not declared above instead of + * stripping them. + */ + [feature: string]: unknown; } /**