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

224 lines (223 loc) • 10.7 kB
import { Credentials, InvokeResponse } from '@microsoft/teams.api'; import { Client as HttpClient, IEventEmitter, ILogger } from '@microsoft/teams.common'; import { IActivityEvent } from '../events'; import { HttpMethod, HttpRouteHandler, IHttpServerAdapter } from '../http/adapter'; import { IAppTokenProvider } from '../token-provider'; import { SocketReadyFrame, SocketModeOptions, SocketModeStatus } from './types'; /** * Lifecycle events emitted by {@link SocketModeAdapter.events}. Subscribe via * `app.socketMode?.events.on('ready', ...)` to observe the socket lifecycle. * Each payload carries the `geo` it relates to, since a bot maintains one * connection per geo. * * @experimental This API is in preview and may change in the future. */ export type SocketModeEvents = { /** A geo's socket connected and Teams backend service confirmed readiness. */ ready: { geo: string; frame: SocketReadyFrame; }; /** * A geo's socket dropped unexpectedly; a reconnect for that geo may be in * progress. Not emitted for a successful planned proactive token rotation, * which renegotiates transparently without surfacing a drop. When a * reconnect or planned rotation is rejected with HTTP 401/403, this is * emitted with `terminal: true` and that {@link NegotiateError}, and the geo * stops reconnecting — so after a network drop, listeners see one event for * the drop and a second for the auth rejection. */ disconnected: { geo: string; error?: Error; /** * `true` when this geo has stopped for good and will not reconnect until the * app is restarted (negotiate was rejected with HTTP 401/403). `false` for * an ordinary drop, after which the SDK keeps reconnecting. */ terminal: boolean; }; /** * A geo's socket reconnected after an unexpected drop and re-established its * Teams backend service group. Not emitted for a planned token rotation. */ reconnected: { geo: string; }; }; /** * Dependencies the owning {@link App} supplies to the socket adapter, wired * internally rather than by the developer. */ export type SocketModeAdapterDeps = { /** App credentials used to identify the bot on reply frames. */ readonly credentials?: Credentials; /** The app's shared HTTP client used for Socket Mode negotiation. */ readonly client: HttpClient; /** * Token source for the Bot Framework token that authenticates the Teams backend service * negotiate call, reusing the app's credentials. */ readonly tokenProvider: IAppTokenProvider; /** * Dispatches a connection-authenticated activity into the owning app's shared * activity pipeline. Socket Mode creates the normalized token internally. */ readonly processActivity: (event: IActivityEvent) => Promise<InvokeResponse>; /** * The messaging endpoint path the owning {@link HttpServer} registers its * inbound-activity route on. Inbound socket frames are dispatched to the * handler registered for this path; {@link SocketModeAdapter.registerRoute} * calls for any other path (e.g. `app.function()` routes) are no-ops, since a * socket transport serves only messaging. */ readonly messagingEndpoint: string; /** * Surface an unexpected inbound-processing error to the app pipeline. May be * async — the transport awaits it (behind an error boundary) so an * `App.onError` that performs async work (logging, telemetry flush) completes * before the reply frame is returned. */ readonly onError?: (error: Error) => void | Promise<void>; /** Logger to use; defaults to a `SocketModeAdapter`-tagged console logger. */ readonly logger?: ILogger; }; /** * Inbound Socket Mode transport, implemented as an {@link IHttpServerAdapter}. * * With `new App({ socketMode: true })` the app receives activities over a * Teams backend service-negotiated Azure SignalR socket. Rather than being a * separate server, Socket Mode plugs in as the adapter *inside* the app's single * {@link HttpServer}. Inbound frames enter the same app activity pipeline as * HTTP after the connection-authenticated transport creates their normalized * token. Only inbound delivery moves to the socket; outbound sends stay on HTTP * and handlers stay transport-agnostic. * * No inbound HTTP adapter, listener, route, or public endpoint is started. The * socket is authenticated once at negotiate, so the adapter synthesizes the * {@link IToken} the pipeline expects without exposing transport trust through * the public HTTP request interface. Invoke activities return a real invoke * response over SignalR client results; one-way activities return a post-handler * ack. Not usable until Teams backend service's `SocketReady` frame arrives. * * Browser-oriented calls are no-ops: tabs, remote functions, OAuth callbacks, * and other non-messaging routes require an HTTP transport. * WebSocket is only recommended for use when developing agents. * * @experimental This API is in preview and may change in the future. */ export declare class SocketModeAdapter implements IHttpServerAdapter { readonly options: SocketModeOptions; private readonly deps; /** * Lifecycle event surface for observing the socket without reaching into the * transport. Emits `ready`, `disconnected`, and `reconnected`. */ readonly events: IEventEmitter<SocketModeEvents>; private readonly log; private _lifecycle; private readonly transportSettings; /** One independent supervised connection per geo. */ private geoSockets; /** Aborts all in-flight negotiate/connect/back-off waits when stopping. */ private abort?; private stopped; constructor(options: SocketModeOptions | undefined, deps: SocketModeAdapterDeps); /** * Aggregate lifecycle status across all geo connections. `ready` means every * geo is ready; `connecting` while any geo is still (re)connecting and none is * mid-drop; `disconnected` when at least one geo has dropped and is recovering, * or has stopped reconnecting after a 401/403 negotiate rejection. */ get status(): SocketModeStatus; /** Per-geo status snapshot, for diagnostics/observability. */ get geoStatuses(): ReadonlyArray<{ geo: string; status: SocketModeStatus; }>; /** The geos this server connects to (resolved from options/defaults). */ get geoList(): readonly string[]; /** * {@link IHttpServerAdapter} route registration. The owning {@link HttpServer} * registers its inbound-activity route here as part of normal server * initialization. Socket Mode dispatches through its internal app callback, so * the messaging route is accepted as a no-op. Any other path (e.g. * `app.function()` POST routes) is also a no-op, but is warned because the * corresponding HTTP-only feature is unavailable. */ registerRoute(method: HttpMethod, path: string, _handler: HttpRouteHandler): void; /** * {@link IHttpServerAdapter} static-file serving. No-op for Socket Mode — tabs * and other static assets require an HTTP transport, so the ignored mount is * warned. */ serveStatic(path: string, _directory: string): void; /** * {@link IHttpServerAdapter} lifecycle: open one socket per geo, resolving only * once every geo's readiness contract is satisfied. Called by {@link App.start} * via {@link HttpServer.start}. The `port` argument is unused — Socket Mode * dials out rather than listening. * * The initial connect for each geo is retried for up to 30 seconds before * `App.start()` rejects; once all geos are ready, each geo's supervisor keeps * it alive across drops and token expiry independently until {@link stop}. */ start(_port?: number | string): Promise<void>; /** * {@link IHttpServerAdapter} lifecycle: stop admitting activities, abort any * reconnect/back-off in progress, then close every geo's socket. In-flight * handlers are not awaited: any activity whose reply doesn't make it out before * shutdown is redelivered by the Teams backend to another connection/instance. */ stop(): Promise<void>; /** Build one geo supervisor from the adapter's narrow internal callbacks. */ private createGeoSocket; /** Build a connection for a geo. */ private createConnection; /** Back-off delay (ms) for a reconnect attempt (shared policy across geos). */ private backoffDelay; /** Abortable delay; resolves `true` when it completed, `false` if aborted. */ private sleep; private retryAfterOf; /** Emit a lifecycle event without letting a throwing listener break state. */ private emit; /** * Dispatch one inbound envelope after the geo supervisor applies its * generation/readiness/admission fence. There is no concurrency cap or * admission queue: SignalR already invokes the handler per activity, the Teams * backend load-balances delivery across connections/instances, and each * activity carries a reply deadline, so an in-SDK cap would mostly produce * work whose reply is discarded. */ private dispatch; /** The configured geos, defaulting to amer/emea/apac. */ private resolveGeos; /** * Handle one inbound envelope: feed the embedded activity and an internally * synthesized token into the app pipeline, then return the reply frame to send * back over client results. * * Invoke activities return the pipeline's status/body; one-way activities * return a post-handler acknowledgement once the pipeline has run. */ private handleEnvelope; /** * Invoke the optional {@link SocketModeAdapterDeps.onError} hook, awaiting it * (behind an error boundary) so an async handler completes before we return * the reply frame, and a throwing/rejecting hook can't crash the transport. */ private reportError; /** The bot's client id, echoed on reply frames for Teams backend service routing. */ private get botId(); /** * Acquire the Bot Framework negotiate token (string form) via the app's token * provider, so Socket Mode reuses the same credential flow as the rest of the SDK. */ private acquireBotToken; /** * Build the normalized token expected by the shared activity pipeline. Socket * authentication is established at negotiation, so there is no per-activity * bearer token to validate or forward. */ private inboundToken; }