@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
JavaScript
;
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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