@personaai/runtime 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -159,7 +159,7 @@ function evictStaleRuns(runs, now, graceMs = DEFAULT_RUN_GRACE_MS, maxTracked =
159
159
  }
160
160
 
161
161
  // src/version.ts
162
- var RUNTIME_VERSION = "0.16.1";
162
+ var RUNTIME_VERSION = "0.17.0";
163
163
 
164
164
  // src/routes/health.ts
165
165
  var alwaysOnCapabilities = {
@@ -1681,6 +1681,203 @@ var workflowStreamRoute = async (request, ctx) => {
1681
1681
  };
1682
1682
  };
1683
1683
 
1684
+ // src/routes/channels.ts
1685
+ var SIGNATURE_HEADER = "x-twilio-signature";
1686
+ var DEFAULT_VOICE_FALLBACK = "Sorry, this assistant is unavailable right now. Please try again later.";
1687
+ var DEFAULT_MESSAGE_FALLBACK = "Sorry, something went wrong while handling your message. Please try again.";
1688
+ function firstHeader(value) {
1689
+ return value?.split(",")[0]?.trim() || void 0;
1690
+ }
1691
+ function resolveSignedUrl(request, publicUrl) {
1692
+ const search = new URLSearchParams();
1693
+ for (const [key, value] of Object.entries(request.query)) {
1694
+ if (value !== void 0) search.set(key, value);
1695
+ }
1696
+ const query = search.toString();
1697
+ const pathWithQuery = request.path + (query ? `?${query}` : "");
1698
+ if (publicUrl) {
1699
+ return `${publicUrl.replace(/\/+$/, "")}${pathWithQuery}`;
1700
+ }
1701
+ const host = firstHeader(request.headers["x-forwarded-host"]) ?? firstHeader(request.headers.host);
1702
+ const proto = firstHeader(request.headers["x-forwarded-proto"]) ?? "https";
1703
+ if (!host) return null;
1704
+ return `${proto}://${host}${pathWithQuery}`;
1705
+ }
1706
+ async function computeTwilioSignature(authToken, url, params) {
1707
+ const payload = url + Object.keys(params).sort().map((key2) => `${key2}${params[key2]}`).join("");
1708
+ const encoder = new TextEncoder();
1709
+ const key = await crypto.subtle.importKey(
1710
+ "raw",
1711
+ encoder.encode(authToken),
1712
+ { name: "HMAC", hash: "SHA-1" },
1713
+ false,
1714
+ ["sign"]
1715
+ );
1716
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
1717
+ const bytes = new Uint8Array(signature);
1718
+ let binary = "";
1719
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1720
+ return btoa(binary);
1721
+ }
1722
+ function constantTimeEqual(a, b) {
1723
+ if (a.length !== b.length) return false;
1724
+ let diff = 0;
1725
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1726
+ return diff === 0;
1727
+ }
1728
+ async function verifyTwilioSignature(request, options) {
1729
+ const provided = request.headers[SIGNATURE_HEADER];
1730
+ if (!provided || !options.authToken) return false;
1731
+ const url = resolveSignedUrl(request, options.publicUrl);
1732
+ if (!url) return false;
1733
+ const expected = await computeTwilioSignature(options.authToken, url, formParams(request));
1734
+ return constantTimeEqual(expected, provided);
1735
+ }
1736
+ function formParams(request) {
1737
+ const params = {};
1738
+ const body = request.body;
1739
+ if (!body || typeof body !== "object" || Array.isArray(body)) return params;
1740
+ for (const [key, value] of Object.entries(body)) {
1741
+ if (typeof value === "string") params[key] = value;
1742
+ else if (typeof value === "number" || typeof value === "boolean") params[key] = String(value);
1743
+ }
1744
+ return params;
1745
+ }
1746
+ function escapeXml(value) {
1747
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1748
+ }
1749
+ function twiml(body) {
1750
+ return {
1751
+ kind: "buffered",
1752
+ status: 200,
1753
+ headers: { "content-type": "text/xml; charset=utf-8" },
1754
+ body: `<?xml version="1.0" encoding="UTF-8"?>${body}`
1755
+ };
1756
+ }
1757
+ function forbidden(message) {
1758
+ return {
1759
+ kind: "buffered",
1760
+ status: 403,
1761
+ headers: { "content-type": "text/plain; charset=utf-8" },
1762
+ body: message
1763
+ };
1764
+ }
1765
+ function channelKind(from) {
1766
+ return from.trim().toLowerCase().startsWith("whatsapp:") ? "whatsapp" : "sms";
1767
+ }
1768
+ function toUser(result) {
1769
+ if (!result) return null;
1770
+ if (typeof result === "string") return result ? { externalUserId: result } : null;
1771
+ if (typeof result.externalUserId !== "string" || !result.externalUserId) return null;
1772
+ return result;
1773
+ }
1774
+ async function authenticate(request, options, channelRequest) {
1775
+ if (!await verifyTwilioSignature(request, options)) {
1776
+ return { response: forbidden("Invalid Twilio signature") };
1777
+ }
1778
+ let resolved = null;
1779
+ try {
1780
+ resolved = await options.resolveUser(channelRequest);
1781
+ } catch {
1782
+ resolved = null;
1783
+ }
1784
+ const user = toUser(resolved);
1785
+ if (!user) {
1786
+ return {
1787
+ response: channelRequest.channel === "voice" ? twiml("<Response><Reject/></Response>") : twiml("<Response></Response>")
1788
+ };
1789
+ }
1790
+ return { user };
1791
+ }
1792
+ var twilioVoiceChannel = async (request, ctx) => {
1793
+ const options = ctx.channels?.twilio;
1794
+ if (!options) {
1795
+ throw new RuntimeHttpError(404, "NOT_FOUND", "Twilio voice channel is not configured.");
1796
+ }
1797
+ const params = formParams(request);
1798
+ const channelRequest = {
1799
+ channel: "voice",
1800
+ from: params.From ?? "",
1801
+ to: params.To ?? "",
1802
+ ...params.CallSid ? { callSid: params.CallSid } : {}
1803
+ };
1804
+ const auth = await authenticate(request, options, channelRequest);
1805
+ if ("response" in auth) return auth.response;
1806
+ const agentId = auth.user.agentId ?? options.agentId;
1807
+ const fallback = options.voiceFallback ?? DEFAULT_VOICE_FALLBACK;
1808
+ if (!agentId) {
1809
+ ctx.logger.warn("twilio voice channel has no agent configured", {
1810
+ hasDefault: false,
1811
+ resolverProvided: false
1812
+ });
1813
+ return twiml(`<Response><Say>${escapeXml(fallback)}</Say><Hangup/></Response>`);
1814
+ }
1815
+ try {
1816
+ const session = await ctx.client.voice.createTelephonySession({
1817
+ agentId,
1818
+ externalUserId: auth.user.externalUserId,
1819
+ ...auth.user.threadId ? { threadId: auth.user.threadId } : {},
1820
+ ...channelRequest.from ? { from: channelRequest.from } : {},
1821
+ ...channelRequest.to ? { to: channelRequest.to } : {},
1822
+ ...channelRequest.callSid ? { callSid: channelRequest.callSid } : {}
1823
+ });
1824
+ await ctx.hooks?.onVoiceSessionCreate?.({
1825
+ userId: auth.user.externalUserId,
1826
+ agentId
1827
+ });
1828
+ return twiml(
1829
+ `<Response><Connect><Stream url="${escapeXml(session.streamUrl)}"/></Connect></Response>`
1830
+ );
1831
+ } catch (err) {
1832
+ ctx.logger.error("twilio voice channel failed to start a session", {
1833
+ error: err instanceof Error ? err.message : String(err)
1834
+ });
1835
+ return twiml(`<Response><Say>${escapeXml(fallback)}</Say><Hangup/></Response>`);
1836
+ }
1837
+ };
1838
+ var twilioMessageChannel = async (request, ctx) => {
1839
+ const options = ctx.channels?.twilio;
1840
+ if (!options) {
1841
+ throw new RuntimeHttpError(404, "NOT_FOUND", "Twilio messaging channel is not configured.");
1842
+ }
1843
+ const params = formParams(request);
1844
+ const channelRequest = {
1845
+ channel: channelKind(params.From ?? ""),
1846
+ from: params.From ?? "",
1847
+ to: params.To ?? "",
1848
+ ...params.MessageSid ? { messageSid: params.MessageSid } : {},
1849
+ ...params.Body ? { body: params.Body } : {},
1850
+ ...params.ProfileName ? { profileName: params.ProfileName } : {}
1851
+ };
1852
+ const auth = await authenticate(request, options, channelRequest);
1853
+ if ("response" in auth) return auth.response;
1854
+ const agentId = auth.user.agentId ?? options.agentId;
1855
+ const fallback = options.messageFallback === void 0 ? DEFAULT_MESSAGE_FALLBACK : options.messageFallback;
1856
+ if (!agentId) {
1857
+ ctx.logger.warn("twilio message channel has no agent configured");
1858
+ return fallback ? twiml(`<Response><Message>${escapeXml(fallback)}</Message></Response>`) : twiml("<Response></Response>");
1859
+ }
1860
+ const text = channelRequest.body ?? "";
1861
+ if (!text.trim()) {
1862
+ return twiml("<Response></Response>");
1863
+ }
1864
+ try {
1865
+ const result = await ctx.client.messages.generateReply({
1866
+ agentId,
1867
+ externalUserId: auth.user.externalUserId,
1868
+ text,
1869
+ ...auth.user.threadId ? { threadId: auth.user.threadId } : {},
1870
+ ...channelRequest.profileName ? { displayName: channelRequest.profileName } : {}
1871
+ });
1872
+ return twiml(`<Response><Message>${escapeXml(result.reply)}</Message></Response>`);
1873
+ } catch (err) {
1874
+ ctx.logger.error("twilio message channel failed to generate a reply", {
1875
+ error: err instanceof Error ? err.message : String(err)
1876
+ });
1877
+ return fallback ? twiml(`<Response><Message>${escapeXml(fallback)}</Message></Response>`) : twiml("<Response></Response>");
1878
+ }
1879
+ };
1880
+
1684
1881
  // src/runtime.ts
1685
1882
  function resolveCapabilities(capabilities) {
1686
1883
  return {
@@ -1952,6 +2149,22 @@ function createRuntime(options) {
1952
2149
  requiresAuth: false
1953
2150
  });
1954
2151
  }
2152
+ if (options.channels?.twilio) {
2153
+ routes.push(
2154
+ {
2155
+ method: "POST",
2156
+ pattern: ["channels", "twilio", "voice"],
2157
+ handler: twilioVoiceChannel,
2158
+ requiresAuth: false
2159
+ },
2160
+ {
2161
+ method: "POST",
2162
+ pattern: ["channels", "twilio", "message"],
2163
+ handler: twilioMessageChannel,
2164
+ requiresAuth: false
2165
+ }
2166
+ );
2167
+ }
1955
2168
  const mode = resolveMode(options);
1956
2169
  const heartbeatIntervalMs = options.heartbeatIntervalMs ?? 15e3;
1957
2170
  const runGraceMs = options.runGraceMs ?? DEFAULT_RUN_GRACE_MS;
@@ -2119,7 +2332,8 @@ function createRuntime(options) {
2119
2332
  capabilities,
2120
2333
  logger: routeLogger,
2121
2334
  restToolsManifest: options.restToolsManifest,
2122
- rcpManifest: options.rcpManifest
2335
+ rcpManifest: options.rcpManifest,
2336
+ channels: options.channels
2123
2337
  });
2124
2338
  const durationMs = Date.now() - startMs;
2125
2339
  logger.info("handle succeeded", {