@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
TypeScript
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;
}