UNPKG

@microsoft/teams.apps

Version:

<p> <a href="https://www.npmjs.com/package/@microsoft/teams.apps" target="_blank"> <img src="https://img.shields.io/npm/v/@microsoft/teams.apps/latest" /> </a> <a href="https://www.npmjs.com/package/@microsoft/teams.apps?activeTab=code

333 lines • 26.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildHtmlWidgetMarkdown = buildHtmlWidgetMarkdown; exports.buildHtmlWidgetMessage = buildHtmlWidgetMessage; exports.validateSecurityPolicy = validateSecurityPolicy; exports.tryGetWidgetModelContext = tryGetWidgetModelContext; /** * Validates an HTML widget payload, throwing if required fields are * missing or empty. This catches issues that would otherwise result * in a silent client-side render failure ("Couldn't load widget"). * * @experimental This API is in preview and may change in the future. * Diagnostic: ExperimentalTeamsHtmlWidget */ function validateHtmlWidgetPayload(payload) { if (!payload.name.trim()) { throw new Error('HTML widget payload requires a non-empty "name" field.'); } if (!payload.html.trim()) { throw new Error('HTML widget payload requires a non-empty "html" field.'); } const domain = payload.domain.trim(); let domainUrl; try { domainUrl = new URL(domain); } catch { domainUrl = undefined; } if (!domainUrl || domainUrl.protocol !== 'https:' || !domainUrl.hostname) { throw new Error('HTML widget payload requires "domain" to be a valid URL starting with "https://".'); } } /** * Default security policy applied when none is specified. * Follows the MCP Apps spec recommendation of restrictive defaults: * no external network access, self + data URIs for resources only. */ const DEFAULT_SECURITY_POLICY = { connectDomains: [], resourceDomains: ['\'self\'', 'data:'], frameDomains: [], baseUriDomains: [], }; /** * Wraps an HTML widget payload in the ` ```html-widget ` markdown code fence * format required by Teams to render the widget in a message. * The widget HTML is sent as authored. * The widget owns the `ui/initialize` handshake and the `ui/notifications/size-changed` reporting. * Without a size reporter, Teams renders the widget at about 150px tall. * * @param payload - The widget payload to serialize. * @param options - Optional text to include before/after the widget block. * @returns The markdown string containing the widget code block. * * @experimental This API is in preview and may change in the future. * Diagnostic: ExperimentalTeamsHtmlWidget */ function buildHtmlWidgetMarkdown(payload, options) { const resolved = { type: 'widget/mcp-ui', ...payload }; validateHtmlWidgetPayload(resolved); const json = JSON.stringify({ ...resolved, securityPolicy: resolved.securityPolicy ?? DEFAULT_SECURITY_POLICY, }); const parts = []; if (options?.before) { parts.push(options.before, ''); } parts.push('```html-widget', json, '```'); if (options?.after) { parts.push('', options.after); } return parts.join('\n'); } /** * Builds a message activity containing an HTML widget, ready to be sent. * The widget HTML is sent as authored, so the widget owns the `ui/initialize` handshake and the `ui/notifications/size-changed` reporting. * * @param payload - The widget payload to include in the message. * @param options - Optional text to include before/after the widget block. * @returns An activity object with textFormat set to 'extendedmarkdown'. * * @experimental This API is in preview and may change in the future. * Diagnostic: ExperimentalTeamsHtmlWidget */ function buildHtmlWidgetMessage(payload, options) { return { type: 'message', text: buildHtmlWidgetMarkdown(payload, options), textFormat: 'extendedmarkdown', }; } /** * Extracts the origin (scheme + host) from a URL string. * Returns null if the URL is relative, a data URI, or unparseable. */ function extractOrigin(url) { const trimmed = url.trim(); if (!trimmed || trimmed.startsWith('data:') || trimmed.startsWith('#') || trimmed.startsWith('blob:')) { return null; } // Relative URLs are fine (they resolve to the iframe origin) if (!trimmed.includes('://') && !trimmed.startsWith('//')) { return null; } try { const parsed = new URL(trimmed.startsWith('//') ? `https:${trimmed}` : trimmed); return `${parsed.protocol}//${parsed.host}`; } catch { return null; } } /** * Checks whether an origin is covered by a list of allowed domains/origins. * Handles special CSP values like `'self'` and `*`. */ function isOriginAllowed(origin, allowedDomains) { if (allowedDomains.includes('*')) return true; return allowedDomains.some((domain) => { const cleaned = domain.replace(/^['"]|['"]$/g, ''); if (cleaned === '*') return true; if (origin === cleaned) return true; // If the policy entry pins a scheme (e.g. `https://example.com`), the // origin must use that same scheme; otherwise fall back to host-only match. const schemeMatch = /^(https?):\/\//.exec(cleaned); if (schemeMatch && !origin.startsWith(`${schemeMatch[1]}://`)) return false; const host = cleaned.replace(/^https?:\/\//, ''); return origin.endsWith(`.${host}`); }); } /** * Validates that external references in widget HTML are covered by the * declared security policy. Returns an array of warnings for any * references to origins not present in the appropriate policy field. * * This is a static analysis tool - it cannot catch dynamically constructed * URLs (e.g. `fetch('https://' + domain)`). A `securitypolicyviolation` listener in the widget * reports those at runtime. * * Note: The CSP keyword `'self'` cannot be validated statically because it * resolves to the iframe's parent origin at runtime. References that would * be allowed by `'self'` may still produce warnings. * * @param html - The raw HTML content of the widget. * @param policy - The security policy to validate against. * @returns An array of warnings. Empty array means no issues found. * * @experimental This API is in preview and may change in the future. * Diagnostic: ExperimentalTeamsHtmlWidget */ function validateSecurityPolicy(html, policy) { const warnings = []; // Helper: find all opening tags by name using indexOf (O(n), no regex backtracking). // Returns the substring of each tag (from '<tagName' to the next '>'). function findTags(tagName) { const tags = []; const needle = '<' + tagName; const lower = html.toLowerCase(); let pos = 0; while (pos < lower.length) { const start = lower.indexOf(needle, pos); if (start === -1) break; // Ensure tag name is followed by whitespace or '>' (not a prefix of another tag) const afterTag = start + needle.length; if (afterTag < lower.length && lower[afterTag] !== ' ' && lower[afterTag] !== '\t' && lower[afterTag] !== '\n' && lower[afterTag] !== '\r' && lower[afterTag] !== '>' && lower[afterTag] !== '/') { pos = afterTag; continue; } const end = html.indexOf('>', start); if (end === -1) break; tags.push(html.substring(start, end + 1)); pos = end + 1; } return tags; } // resourceDomains: <script src>, <link href>, <img src>, <source src>, // <audio src>, <video src>, CSS url(), @import const tagAttrChecks = [ { tag: 'script', attrRegex: /src=["']([^"']+)["']/i, source: '<script src>' }, { tag: 'link', attrRegex: /href=["']([^"']+)["']/i, source: '<link href>' }, { tag: 'img', attrRegex: /src=["']([^"']+)["']/i, source: '<img src>' }, { tag: 'source', attrRegex: /src=["']([^"']+)["']/i, source: '<source src>' }, { tag: 'audio', attrRegex: /src=["']([^"']+)["']/i, source: '<audio src>' }, { tag: 'video', attrRegex: /src=["']([^"']+)["']/i, source: '<video src>' }, ]; for (const { tag, attrRegex, source } of tagAttrChecks) { for (const tagStr of findTags(tag)) { const attrMatch = tagStr.match(attrRegex); if (attrMatch) { const origin = extractOrigin(attrMatch[1]); if (origin && !isOriginAllowed(origin, policy.resourceDomains ?? [])) { warnings.push({ url: attrMatch[1], source, policyField: 'resourceDomains', message: `${source} references "${attrMatch[1]}" but origin "${origin}" is not in resourceDomains.`, }); } } } } // CSS url() and @import (simple patterns, no tag-level backtracking risk) const cssPatterns = [ { regex: /url\(\s*["']([^"')]+)["']\s*\)/gi, source: 'CSS url()' }, { regex: /@import\s+["']([^"']+)["']/gi, source: 'CSS @import' }, ]; for (const { regex, source } of cssPatterns) { let match; while ((match = regex.exec(html)) !== null) { const origin = extractOrigin(match[1]); if (origin && !isOriginAllowed(origin, policy.resourceDomains ?? [])) { warnings.push({ url: match[1], source, policyField: 'resourceDomains', message: `${source} references "${match[1]}" but origin "${origin}" is not in resourceDomains.`, }); } } } // connectDomains: fetch(), XMLHttpRequest.open(), new WebSocket(), new EventSource() const connectPatterns = [ { regex: /fetch\(\s*["']([^"']+)["']/gi, source: 'fetch()' }, { regex: /\.open\(\s*["'][A-Za-z]+["']\s*,\s*["']([^"']+)["']/gi, source: 'XMLHttpRequest.open()' }, { regex: /new\s+WebSocket\(\s*["']([^"']+)["']/gi, source: 'new WebSocket()' }, { regex: /new\s+EventSource\(\s*["']([^"']+)["']/gi, source: 'new EventSource()' }, ]; for (const { regex, source } of connectPatterns) { let match; while ((match = regex.exec(html)) !== null) { const origin = extractOrigin(match[1]); if (origin && !isOriginAllowed(origin, policy.connectDomains ?? [])) { warnings.push({ url: match[1], source, policyField: 'connectDomains', message: `${source} references "${match[1]}" but origin "${origin}" is not in connectDomains.`, }); } } } // frameDomains: <iframe src> for (const tagStr of findTags('iframe')) { const attrMatch = tagStr.match(/src=["']([^"']+)["']/i); if (attrMatch) { const origin = extractOrigin(attrMatch[1]); if (origin && !isOriginAllowed(origin, policy.frameDomains ?? [])) { warnings.push({ url: attrMatch[1], source: '<iframe src>', policyField: 'frameDomains', message: `<iframe src> references "${attrMatch[1]}" but origin "${origin}" is not in frameDomains.`, }); } } } // connectDomains: <form action> (form submissions can exfiltrate data) for (const tagStr of findTags('form')) { const attrMatch = tagStr.match(/action=["']([^"']+)["']/i); if (attrMatch) { const origin = extractOrigin(attrMatch[1]); if (origin && !isOriginAllowed(origin, policy.connectDomains ?? [])) { warnings.push({ url: attrMatch[1], source: '<form action>', policyField: 'connectDomains', message: `<form action> references "${attrMatch[1]}" but origin "${origin}" is not in connectDomains.`, }); } } } // baseUriDomains: <base href> for (const tagStr of findTags('base')) { const attrMatch = tagStr.match(/href=["']([^"']+)["']/i); if (attrMatch) { const origin = extractOrigin(attrMatch[1]); if (origin && !isOriginAllowed(origin, policy.baseUriDomains ?? [])) { warnings.push({ url: attrMatch[1], source: '<base href>', policyField: 'baseUriDomains', message: `<base href> references "${attrMatch[1]}" but origin "${origin}" is not in baseUriDomains.`, }); } } } return warnings; } /** * Attempts to extract an MCP UI update-model-context request from a message * activity's `value`. A widget can request that content be added to the model * context by reusing the messageBack mechanism (like `Action.Submit` for * adaptive cards). Such a request arrives as a normal message activity whose * `value` carries the {@link IMcpUiUpdateModelContextRequest} payload. This is * fire-and-forget: the bot does not respond. * * This helper is tolerant of two wire shapes: * 1. The raw request object (`{ method: 'ui/update-model-context', params }`). * 2. An envelope of the form `{ type: 'widgetModelContext', data: <request> }`. * * @param activity - A message activity (or any object with a `value` field). * @returns The parsed request, or `undefined` if `value` is not a valid * update-model-context request. * * @experimental This API is in preview and may change in the future. * Diagnostic: ExperimentalTeamsHtmlWidget */ function tryGetWidgetModelContext(activity) { const value = activity?.value; if (!value || typeof value !== 'object') { return undefined; } // Unwrap the `{ type: 'widgetModelContext', data }` envelope if present. const candidate = value.type === 'widgetModelContext' && value.data && typeof value.data === 'object' ? value.data : value; if (candidate.method !== 'ui/update-model-context') { return undefined; } if (!candidate.params || typeof candidate.params !== 'object') { return undefined; } return candidate; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHRtbC13aWRnZXQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdXRpbHMvaHRtbC13aWRnZXQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUEwRkEsMERBd0JDO0FBYUQsd0RBU0M7QUFzRkQsd0RBeUpDO0FBcUJELDREQXFCQztBQWhaRDs7Ozs7OztHQU9HO0FBQ0gsU0FBUyx5QkFBeUIsQ0FBQyxPQUEyQjtJQUM1RCxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDO1FBQ3pCLE1BQU0sSUFBSSxLQUFLLENBQUMsd0RBQXdELENBQUMsQ0FBQztJQUM1RSxDQUFDO0lBRUQsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQztRQUN6QixNQUFNLElBQUksS0FBSyxDQUFDLHdEQUF3RCxDQUFDLENBQUM7SUFDNUUsQ0FBQztJQUVELE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDckMsSUFBSSxTQUEwQixDQUFDO0lBQy9CLElBQUksQ0FBQztRQUNILFNBQVMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM5QixDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsU0FBUyxHQUFHLFNBQVMsQ0FBQztJQUN4QixDQUFDO0lBQ0QsSUFBSSxDQUFDLFNBQVMsSUFBSSxTQUFTLENBQUMsUUFBUSxLQUFLLFFBQVEsSUFBSSxDQUFDLFNBQVMsQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUN6RSxNQUFNLElBQUksS0FBSyxDQUFDLG1GQUFtRixDQUFDLENBQUM7SUFDdkcsQ0FBQztBQUNILENBQUM7QUFvQkQ7Ozs7R0FJRztBQUNILE1BQU0sdUJBQXVCLEdBQW1EO0lBQzlFLGNBQWMsRUFBRSxFQUFFO0lBQ2xCLGVBQWUsRUFBRSxDQUFDLFVBQVUsRUFBRSxPQUFPLENBQUM7SUFDdEMsWUFBWSxFQUFF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