@personaai/runtime 0.16.1 → 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 +216 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -2
- package/dist/index.d.ts +72 -2
- package/dist/index.js +216 -2
- package/dist/index.js.map +1 -1
- package/package.json +67 -67
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.
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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", {
|