signalwire-agents 0.1.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.
Files changed (245) hide show
  1. package/CLAUDE.md +112 -0
  2. package/README.md +417 -0
  3. package/dist/AgentBase.d.ts +481 -0
  4. package/dist/AgentBase.js +1155 -0
  5. package/dist/AgentBase.js.map +1 -0
  6. package/dist/AgentServer.d.ts +60 -0
  7. package/dist/AgentServer.js +128 -0
  8. package/dist/AgentServer.js.map +1 -0
  9. package/dist/AuthHandler.d.ts +56 -0
  10. package/dist/AuthHandler.js +119 -0
  11. package/dist/AuthHandler.js.map +1 -0
  12. package/dist/ConfigLoader.d.ts +68 -0
  13. package/dist/ConfigLoader.js +150 -0
  14. package/dist/ConfigLoader.js.map +1 -0
  15. package/dist/ContextBuilder.d.ts +431 -0
  16. package/dist/ContextBuilder.js +751 -0
  17. package/dist/ContextBuilder.js.map +1 -0
  18. package/dist/DataMap.d.ts +179 -0
  19. package/dist/DataMap.js +321 -0
  20. package/dist/DataMap.js.map +1 -0
  21. package/dist/Logger.d.ts +91 -0
  22. package/dist/Logger.js +191 -0
  23. package/dist/Logger.js.map +1 -0
  24. package/dist/PomBuilder.d.ts +144 -0
  25. package/dist/PomBuilder.js +249 -0
  26. package/dist/PomBuilder.js.map +1 -0
  27. package/dist/PromptManager.d.ts +85 -0
  28. package/dist/PromptManager.js +108 -0
  29. package/dist/PromptManager.js.map +1 -0
  30. package/dist/SchemaUtils.d.ts +42 -0
  31. package/dist/SchemaUtils.js +148 -0
  32. package/dist/SchemaUtils.js.map +1 -0
  33. package/dist/ServerlessAdapter.d.ts +100 -0
  34. package/dist/ServerlessAdapter.js +166 -0
  35. package/dist/ServerlessAdapter.js.map +1 -0
  36. package/dist/SessionManager.d.ts +86 -0
  37. package/dist/SessionManager.js +155 -0
  38. package/dist/SessionManager.js.map +1 -0
  39. package/dist/SslConfig.d.ts +77 -0
  40. package/dist/SslConfig.js +100 -0
  41. package/dist/SslConfig.js.map +1 -0
  42. package/dist/SwaigFunction.d.ts +88 -0
  43. package/dist/SwaigFunction.js +125 -0
  44. package/dist/SwaigFunction.js.map +1 -0
  45. package/dist/SwaigFunctionResult.d.ts +442 -0
  46. package/dist/SwaigFunctionResult.js +735 -0
  47. package/dist/SwaigFunctionResult.js.map +1 -0
  48. package/dist/SwmlBuilder.d.ts +37 -0
  49. package/dist/SwmlBuilder.js +55 -0
  50. package/dist/SwmlBuilder.js.map +1 -0
  51. package/dist/cli/agent-loader.d.ts +22 -0
  52. package/dist/cli/agent-loader.js +106 -0
  53. package/dist/cli/agent-loader.js.map +1 -0
  54. package/dist/cli/mock-data.d.ts +37 -0
  55. package/dist/cli/mock-data.js +79 -0
  56. package/dist/cli/mock-data.js.map +1 -0
  57. package/dist/cli/swaig-test.d.ts +32 -0
  58. package/dist/cli/swaig-test.js +405 -0
  59. package/dist/cli/swaig-test.js.map +1 -0
  60. package/dist/index.d.ts +36 -0
  61. package/dist/index.js +44 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/prefabs/ConciergeAgent.d.ts +64 -0
  64. package/dist/prefabs/ConciergeAgent.js +188 -0
  65. package/dist/prefabs/ConciergeAgent.js.map +1 -0
  66. package/dist/prefabs/FAQBotAgent.d.ts +68 -0
  67. package/dist/prefabs/FAQBotAgent.js +206 -0
  68. package/dist/prefabs/FAQBotAgent.js.map +1 -0
  69. package/dist/prefabs/InfoGathererAgent.d.ts +73 -0
  70. package/dist/prefabs/InfoGathererAgent.js +202 -0
  71. package/dist/prefabs/InfoGathererAgent.js.map +1 -0
  72. package/dist/prefabs/ReceptionistAgent.d.ts +65 -0
  73. package/dist/prefabs/ReceptionistAgent.js +205 -0
  74. package/dist/prefabs/ReceptionistAgent.js.map +1 -0
  75. package/dist/prefabs/SurveyAgent.d.ts +84 -0
  76. package/dist/prefabs/SurveyAgent.js +324 -0
  77. package/dist/prefabs/SurveyAgent.js.map +1 -0
  78. package/dist/prefabs/index.d.ts +13 -0
  79. package/dist/prefabs/index.js +14 -0
  80. package/dist/prefabs/index.js.map +1 -0
  81. package/dist/skills/SkillBase.d.ts +134 -0
  82. package/dist/skills/SkillBase.js +105 -0
  83. package/dist/skills/SkillBase.js.map +1 -0
  84. package/dist/skills/SkillManager.d.ts +83 -0
  85. package/dist/skills/SkillManager.js +159 -0
  86. package/dist/skills/SkillManager.js.map +1 -0
  87. package/dist/skills/SkillRegistry.d.ts +105 -0
  88. package/dist/skills/SkillRegistry.js +195 -0
  89. package/dist/skills/SkillRegistry.js.map +1 -0
  90. package/dist/skills/builtin/api_ninjas_trivia.d.ts +35 -0
  91. package/dist/skills/builtin/api_ninjas_trivia.js +161 -0
  92. package/dist/skills/builtin/api_ninjas_trivia.js.map +1 -0
  93. package/dist/skills/builtin/claude_skills.d.ts +34 -0
  94. package/dist/skills/builtin/claude_skills.js +156 -0
  95. package/dist/skills/builtin/claude_skills.js.map +1 -0
  96. package/dist/skills/builtin/custom_skills.d.ts +59 -0
  97. package/dist/skills/builtin/custom_skills.js +231 -0
  98. package/dist/skills/builtin/custom_skills.js.map +1 -0
  99. package/dist/skills/builtin/datasphere.d.ts +34 -0
  100. package/dist/skills/builtin/datasphere.js +163 -0
  101. package/dist/skills/builtin/datasphere.js.map +1 -0
  102. package/dist/skills/builtin/datasphere_serverless.d.ts +54 -0
  103. package/dist/skills/builtin/datasphere_serverless.js +155 -0
  104. package/dist/skills/builtin/datasphere_serverless.js.map +1 -0
  105. package/dist/skills/builtin/datetime.d.ts +31 -0
  106. package/dist/skills/builtin/datetime.js +95 -0
  107. package/dist/skills/builtin/datetime.js.map +1 -0
  108. package/dist/skills/builtin/google_maps.d.ts +33 -0
  109. package/dist/skills/builtin/google_maps.js +233 -0
  110. package/dist/skills/builtin/google_maps.js.map +1 -0
  111. package/dist/skills/builtin/index.d.ts +28 -0
  112. package/dist/skills/builtin/index.js +75 -0
  113. package/dist/skills/builtin/index.js.map +1 -0
  114. package/dist/skills/builtin/info_gatherer.d.ts +52 -0
  115. package/dist/skills/builtin/info_gatherer.js +237 -0
  116. package/dist/skills/builtin/info_gatherer.js.map +1 -0
  117. package/dist/skills/builtin/joke.d.ts +32 -0
  118. package/dist/skills/builtin/joke.js +142 -0
  119. package/dist/skills/builtin/joke.js.map +1 -0
  120. package/dist/skills/builtin/math.d.ts +33 -0
  121. package/dist/skills/builtin/math.js +120 -0
  122. package/dist/skills/builtin/math.js.map +1 -0
  123. package/dist/skills/builtin/mcp_gateway.d.ts +35 -0
  124. package/dist/skills/builtin/mcp_gateway.js +85 -0
  125. package/dist/skills/builtin/mcp_gateway.js.map +1 -0
  126. package/dist/skills/builtin/native_vector_search.d.ts +44 -0
  127. package/dist/skills/builtin/native_vector_search.js +248 -0
  128. package/dist/skills/builtin/native_vector_search.js.map +1 -0
  129. package/dist/skills/builtin/play_background_file.d.ts +34 -0
  130. package/dist/skills/builtin/play_background_file.js +135 -0
  131. package/dist/skills/builtin/play_background_file.js.map +1 -0
  132. package/dist/skills/builtin/spider.d.ts +34 -0
  133. package/dist/skills/builtin/spider.js +164 -0
  134. package/dist/skills/builtin/spider.js.map +1 -0
  135. package/dist/skills/builtin/swml_transfer.d.ts +44 -0
  136. package/dist/skills/builtin/swml_transfer.js +204 -0
  137. package/dist/skills/builtin/swml_transfer.js.map +1 -0
  138. package/dist/skills/builtin/weather_api.d.ts +34 -0
  139. package/dist/skills/builtin/weather_api.js +136 -0
  140. package/dist/skills/builtin/weather_api.js.map +1 -0
  141. package/dist/skills/builtin/web_search.d.ts +34 -0
  142. package/dist/skills/builtin/web_search.js +141 -0
  143. package/dist/skills/builtin/web_search.js.map +1 -0
  144. package/dist/skills/builtin/wikipedia_search.d.ts +34 -0
  145. package/dist/skills/builtin/wikipedia_search.js +166 -0
  146. package/dist/skills/builtin/wikipedia_search.js.map +1 -0
  147. package/dist/skills/index.d.ts +11 -0
  148. package/dist/skills/index.js +10 -0
  149. package/dist/skills/index.js.map +1 -0
  150. package/dist/types.d.ts +85 -0
  151. package/dist/types.js +5 -0
  152. package/dist/types.js.map +1 -0
  153. package/docs/agent-guide.md +869 -0
  154. package/docs/api-reference.md +2419 -0
  155. package/docs/architecture.md +843 -0
  156. package/docs/cli-guide.md +579 -0
  157. package/docs/configuration.md +422 -0
  158. package/docs/contexts-guide.md +787 -0
  159. package/docs/datamap-guide.md +1141 -0
  160. package/docs/prefabs-guide.md +830 -0
  161. package/docs/security.md +436 -0
  162. package/docs/serverless-guide.md +513 -0
  163. package/docs/skills-guide.md +845 -0
  164. package/docs/swaig-reference.md +1627 -0
  165. package/examples/call-flow.ts +60 -0
  166. package/examples/contexts-steps.ts +62 -0
  167. package/examples/datamap-tools.ts +52 -0
  168. package/examples/dynamic-config.ts +51 -0
  169. package/examples/multi-agent.ts +67 -0
  170. package/examples/pom-prompt.ts +68 -0
  171. package/examples/simple-agent.ts +33 -0
  172. package/package.json +35 -0
  173. package/src/AgentBase.ts +1279 -0
  174. package/src/AgentServer.ts +149 -0
  175. package/src/AuthHandler.ts +141 -0
  176. package/src/ConfigLoader.ts +165 -0
  177. package/src/ContextBuilder.ts +796 -0
  178. package/src/DataMap.ts +365 -0
  179. package/src/Logger.ts +197 -0
  180. package/src/PomBuilder.ts +298 -0
  181. package/src/PromptManager.ts +133 -0
  182. package/src/SchemaUtils.ts +170 -0
  183. package/src/ServerlessAdapter.ts +218 -0
  184. package/src/SessionManager.ts +161 -0
  185. package/src/SslConfig.ts +118 -0
  186. package/src/SwaigFunction.ts +168 -0
  187. package/src/SwaigFunctionResult.ts +864 -0
  188. package/src/SwmlBuilder.ts +62 -0
  189. package/src/cli/agent-loader.ts +122 -0
  190. package/src/cli/mock-data.ts +110 -0
  191. package/src/cli/swaig-test.ts +440 -0
  192. package/src/index.ts +99 -0
  193. package/src/prefabs/ConciergeAgent.ts +251 -0
  194. package/src/prefabs/FAQBotAgent.ts +263 -0
  195. package/src/prefabs/InfoGathererAgent.ts +276 -0
  196. package/src/prefabs/ReceptionistAgent.ts +277 -0
  197. package/src/prefabs/SurveyAgent.ts +413 -0
  198. package/src/prefabs/index.ts +23 -0
  199. package/src/skills/SkillBase.ts +184 -0
  200. package/src/skills/SkillManager.ts +177 -0
  201. package/src/skills/SkillRegistry.ts +231 -0
  202. package/src/skills/builtin/api_ninjas_trivia.ts +230 -0
  203. package/src/skills/builtin/claude_skills.ts +239 -0
  204. package/src/skills/builtin/custom_skills.ts +326 -0
  205. package/src/skills/builtin/datasphere.ts +225 -0
  206. package/src/skills/builtin/datasphere_serverless.ts +194 -0
  207. package/src/skills/builtin/datetime.ts +115 -0
  208. package/src/skills/builtin/google_maps.ts +347 -0
  209. package/src/skills/builtin/index.ts +79 -0
  210. package/src/skills/builtin/info_gatherer.ts +331 -0
  211. package/src/skills/builtin/joke.ts +180 -0
  212. package/src/skills/builtin/math.ts +150 -0
  213. package/src/skills/builtin/mcp_gateway.ts +99 -0
  214. package/src/skills/builtin/native_vector_search.ts +327 -0
  215. package/src/skills/builtin/play_background_file.ts +189 -0
  216. package/src/skills/builtin/spider.ts +232 -0
  217. package/src/skills/builtin/swml_transfer.ts +285 -0
  218. package/src/skills/builtin/weather_api.ts +193 -0
  219. package/src/skills/builtin/web_search.ts +212 -0
  220. package/src/skills/builtin/wikipedia_search.ts +248 -0
  221. package/src/skills/index.ts +12 -0
  222. package/src/types.ts +99 -0
  223. package/tests/AgentBase.test.ts +693 -0
  224. package/tests/AgentServer.test.ts +102 -0
  225. package/tests/AuthHandler.test.ts +121 -0
  226. package/tests/ConfigLoader.test.ts +121 -0
  227. package/tests/DataMap.test.ts +286 -0
  228. package/tests/Logger.test.ts +218 -0
  229. package/tests/PomBuilder.test.ts +164 -0
  230. package/tests/ProxyDetection.test.ts +185 -0
  231. package/tests/SchemaUtils.test.ts +106 -0
  232. package/tests/ServerlessAdapter.test.ts +200 -0
  233. package/tests/SessionManager.test.ts +139 -0
  234. package/tests/SslConfig.test.ts +97 -0
  235. package/tests/SwaigFunctionResult.test.ts +298 -0
  236. package/tests/cli/mock-data.test.ts +94 -0
  237. package/tests/cli/swaig-test.test.ts +96 -0
  238. package/tests/prefabs/prefabs.test.ts +854 -0
  239. package/tests/setup.ts +6 -0
  240. package/tests/skills/SkillBase.test.ts +118 -0
  241. package/tests/skills/SkillManager.test.ts +164 -0
  242. package/tests/skills/SkillRegistry.test.ts +124 -0
  243. package/tests/skills/builtin.test.ts +924 -0
  244. package/tsconfig.json +18 -0
  245. package/vitest.config.ts +9 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * ServerlessAdapter - Adapts AgentBase for serverless platforms.
3
+ *
4
+ * Supports AWS Lambda, Google Cloud Functions, Azure Functions, and CGI mode.
5
+ * Auto-detects platform from environment variables.
6
+ */
7
+
8
+ import { getLogger } from './Logger.js';
9
+
10
+ const log = getLogger('ServerlessAdapter');
11
+
12
+ /** Supported serverless platform identifiers, or 'auto' for environment-based detection. */
13
+ export type ServerlessPlatform = 'lambda' | 'gcf' | 'azure' | 'cgi' | 'auto';
14
+
15
+ /** Normalized incoming event from a serverless platform. */
16
+ export interface ServerlessEvent {
17
+ /** HTTP method (AWS Lambda style). */
18
+ httpMethod?: string;
19
+ /** HTTP method (GCF/Azure style). */
20
+ method?: string;
21
+ /** Request headers as key-value pairs. */
22
+ headers?: Record<string, string>;
23
+ /** Request body, either raw JSON string or parsed object. */
24
+ body?: string | Record<string, unknown>;
25
+ /** Request path. */
26
+ path?: string;
27
+ /** Raw request path (AWS API Gateway v2). */
28
+ rawPath?: string;
29
+ /** Query string parameters as key-value pairs. */
30
+ queryStringParameters?: Record<string, string>;
31
+ /** Platform-specific request context metadata. */
32
+ requestContext?: Record<string, unknown>;
33
+ }
34
+
35
+ /** Normalized outgoing response returned to a serverless platform. */
36
+ export interface ServerlessResponse {
37
+ /** HTTP status code. */
38
+ statusCode: number;
39
+ /** Response headers as key-value pairs. */
40
+ headers: Record<string, string>;
41
+ /** Response body as a string. */
42
+ body: string;
43
+ }
44
+
45
+ /** Adapts a Hono application for deployment on AWS Lambda, Google Cloud Functions, Azure Functions, or CGI. */
46
+ export class ServerlessAdapter {
47
+ private platform: ServerlessPlatform;
48
+
49
+ /**
50
+ * Create a ServerlessAdapter for the given platform.
51
+ * @param platform - Target platform; defaults to 'auto' which detects from environment variables.
52
+ */
53
+ constructor(platform: ServerlessPlatform = 'auto') {
54
+ this.platform = platform === 'auto' ? this.detectPlatform() : platform;
55
+ }
56
+
57
+ /**
58
+ * Detect the serverless platform by inspecting well-known environment variables.
59
+ * @returns The detected platform identifier; defaults to 'lambda' if no match is found.
60
+ */
61
+ detectPlatform(): ServerlessPlatform {
62
+ if (process.env['AWS_LAMBDA_FUNCTION_NAME'] || process.env['_HANDLER']) return 'lambda';
63
+ if (process.env['FUNCTION_TARGET'] || process.env['K_SERVICE']) return 'gcf';
64
+ if (process.env['FUNCTIONS_WORKER_RUNTIME'] || process.env['AZURE_FUNCTIONS_ENVIRONMENT']) return 'azure';
65
+ if (process.env['GATEWAY_INTERFACE']) return 'cgi';
66
+ return 'lambda'; // default fallback
67
+ }
68
+
69
+ /**
70
+ * Get the resolved platform identifier.
71
+ * @returns The serverless platform this adapter is configured for.
72
+ */
73
+ getPlatform(): ServerlessPlatform {
74
+ return this.platform;
75
+ }
76
+
77
+ /**
78
+ * Convert a serverless event into a standard Request, route it through the Hono app, and return a normalized response.
79
+ * @param app - A Hono-compatible application with a `fetch` method.
80
+ * @param event - The incoming serverless event to process.
81
+ * @returns The normalized serverless response.
82
+ */
83
+ async handleRequest(app: { fetch: (req: Request) => Promise<Response> }, event: ServerlessEvent): Promise<ServerlessResponse> {
84
+ const method = event.httpMethod ?? event.method ?? 'POST';
85
+ const path = event.rawPath ?? event.path ?? '/';
86
+ const headers = event.headers ?? {};
87
+
88
+ // Build URL
89
+ const host = headers['host'] ?? 'localhost';
90
+ const proto = headers['x-forwarded-proto'] ?? 'https';
91
+ let url = `${proto}://${host}${path}`;
92
+ if (event.queryStringParameters) {
93
+ const qs = new URLSearchParams(event.queryStringParameters).toString();
94
+ if (qs) url += `?${qs}`;
95
+ }
96
+
97
+ // Build body
98
+ let body: string | undefined;
99
+ if (event.body) {
100
+ body = typeof event.body === 'string' ? event.body : JSON.stringify(event.body);
101
+ }
102
+
103
+ // Create Request
104
+ const request = new Request(url, {
105
+ method,
106
+ headers: new Headers(headers),
107
+ body: method !== 'GET' && method !== 'HEAD' ? body : undefined,
108
+ });
109
+
110
+ log.debug(`Handling ${method} ${path} on ${this.platform}`);
111
+
112
+ // Route through Hono
113
+ const response = await app.fetch(request);
114
+
115
+ // Convert back to serverless response
116
+ const responseHeaders: Record<string, string> = {};
117
+ response.headers.forEach((v, k) => { responseHeaders[k] = v; });
118
+
119
+ return {
120
+ statusCode: response.status,
121
+ headers: responseHeaders,
122
+ body: await response.text(),
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Generate the platform-specific invocation URL for a deployed function.
128
+ * @param opts - Optional overrides for region, project, function name, stage, or API ID.
129
+ * @returns The constructed URL string.
130
+ */
131
+ generateUrl(opts?: {
132
+ region?: string;
133
+ projectId?: string;
134
+ functionName?: string;
135
+ stage?: string;
136
+ apiId?: string;
137
+ }): string {
138
+ const functionName = opts?.functionName ?? process.env['AWS_LAMBDA_FUNCTION_NAME'] ?? 'agent';
139
+
140
+ switch (this.platform) {
141
+ case 'lambda': {
142
+ const region = opts?.region ?? process.env['AWS_REGION'] ?? 'us-east-1';
143
+ const apiId = opts?.apiId ?? 'API_ID';
144
+ const stage = opts?.stage ?? 'prod';
145
+ return `https://${apiId}.execute-api.${region}.amazonaws.com/${stage}`;
146
+ }
147
+ case 'gcf': {
148
+ const project = opts?.projectId ?? process.env['GCLOUD_PROJECT'] ?? 'PROJECT';
149
+ const region = opts?.region ?? process.env['FUNCTION_REGION'] ?? 'us-central1';
150
+ return `https://${region}-${project}.cloudfunctions.net/${functionName}`;
151
+ }
152
+ case 'azure': {
153
+ return `https://${functionName}.azurewebsites.net/api/${functionName}`;
154
+ }
155
+ case 'cgi': {
156
+ return `http://localhost/cgi-bin/${functionName}`;
157
+ }
158
+ default:
159
+ return `https://localhost/${functionName}`;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Create an AWS Lambda-compatible handler function from a Hono app.
165
+ * @param app - A Hono-compatible application with a `fetch` method.
166
+ * @returns A function that accepts a Lambda event and returns a promise of a serverless response.
167
+ */
168
+ static createLambdaHandler(app: { fetch: (req: Request) => Promise<Response> }): (event: ServerlessEvent) => Promise<ServerlessResponse> {
169
+ const adapter = new ServerlessAdapter('lambda');
170
+ return (event: ServerlessEvent) => adapter.handleRequest(app, event);
171
+ }
172
+
173
+ /**
174
+ * Create a Google Cloud Functions-compatible handler from a Hono app.
175
+ * @param app - A Hono-compatible application with a `fetch` method.
176
+ * @returns A function that accepts GCF request/response objects.
177
+ */
178
+ static createGcfHandler(app: { fetch: (req: Request) => Promise<Response> }): (req: any, res: any) => Promise<void> {
179
+ const adapter = new ServerlessAdapter('gcf');
180
+ return async (req: any, res: any) => {
181
+ const event: ServerlessEvent = {
182
+ method: req.method,
183
+ headers: req.headers,
184
+ body: req.body,
185
+ path: req.path ?? req.url,
186
+ };
187
+ const response = await adapter.handleRequest(app, event);
188
+ res.status(response.statusCode);
189
+ for (const [k, v] of Object.entries(response.headers)) {
190
+ res.set(k, v);
191
+ }
192
+ res.send(response.body);
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Create an Azure Functions-compatible handler from a Hono app.
198
+ * @param app - A Hono-compatible application with a `fetch` method.
199
+ * @returns A function that accepts an Azure context and request object.
200
+ */
201
+ static createAzureHandler(app: { fetch: (req: Request) => Promise<Response> }): (context: any, req: any) => Promise<void> {
202
+ const adapter = new ServerlessAdapter('azure');
203
+ return async (context: any, req: any) => {
204
+ const event: ServerlessEvent = {
205
+ method: req.method,
206
+ headers: req.headers,
207
+ body: req.body,
208
+ path: req.url,
209
+ };
210
+ const response = await adapter.handleRequest(app, event);
211
+ context.res = {
212
+ status: response.statusCode,
213
+ headers: response.headers,
214
+ body: response.body,
215
+ };
216
+ };
217
+ }
218
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * SessionManager - Stateless HMAC-SHA256 token generation and validation.
3
+ *
4
+ * Tokens encode call_id, function name, expiry and a nonce, signed with a
5
+ * shared secret. No server-side state is stored.
6
+ */
7
+
8
+ import { randomBytes, createHmac } from 'node:crypto';
9
+
10
+ /** Stateless HMAC-SHA256 token manager for SWAIG function call authentication and per-session metadata storage. */
11
+ export class SessionManager {
12
+ /** Token validity duration in seconds. */
13
+ tokenExpirySecs: number;
14
+ private secretKey: string;
15
+ private sessionMetadata: Map<string, Record<string, unknown>> = new Map();
16
+
17
+ /**
18
+ * Create a new SessionManager.
19
+ * @param tokenExpirySecs - Token validity duration in seconds (default 3600).
20
+ * @param secretKey - HMAC signing secret; a random key is generated if omitted.
21
+ */
22
+ constructor(tokenExpirySecs = 3600, secretKey?: string) {
23
+ this.tokenExpirySecs = tokenExpirySecs;
24
+ this.secretKey = secretKey ?? randomBytes(32).toString('hex');
25
+ }
26
+
27
+ /**
28
+ * Return the given callId or generate a new random session identifier.
29
+ * @param callId - Existing call ID to reuse.
30
+ * @returns The call ID string.
31
+ */
32
+ createSession(callId?: string): string {
33
+ if (callId) return callId;
34
+ return randomBytes(16).toString('base64url');
35
+ }
36
+
37
+ /**
38
+ * Generate a signed, base64url-encoded token binding a function name to a call ID.
39
+ * @param functionName - The SWAIG function name to bind.
40
+ * @param callId - The call ID to bind.
41
+ * @returns A base64url-encoded token string.
42
+ */
43
+ generateToken(functionName: string, callId: string): string {
44
+ const expiry = Math.floor(Date.now() / 1000) + this.tokenExpirySecs;
45
+ const nonce = randomBytes(4).toString('hex');
46
+ const message = `${callId}:${functionName}:${expiry}:${nonce}`;
47
+ const signature = createHmac('sha256', this.secretKey)
48
+ .update(message)
49
+ .digest('hex')
50
+ .slice(0, 16);
51
+ const token = `${callId}.${functionName}.${expiry}.${nonce}.${signature}`;
52
+ return Buffer.from(token).toString('base64url');
53
+ }
54
+
55
+ /**
56
+ * Alias for {@link generateToken}.
57
+ * @param functionName - The SWAIG function name to bind.
58
+ * @param callId - The call ID to bind.
59
+ * @returns A base64url-encoded token string.
60
+ */
61
+ createToolToken(functionName: string, callId: string): string {
62
+ return this.generateToken(functionName, callId);
63
+ }
64
+
65
+ /**
66
+ * Validate a token against the expected call ID and function name.
67
+ * @param callId - The expected call ID.
68
+ * @param functionName - The expected function name.
69
+ * @param token - The base64url-encoded token to validate.
70
+ * @returns True if the token is valid and not expired.
71
+ */
72
+ validateToken(callId: string, functionName: string, token: string): boolean {
73
+ try {
74
+ const decoded = Buffer.from(token, 'base64url').toString();
75
+ const parts = decoded.split('.');
76
+ if (parts.length !== 5) return false;
77
+ const [tokenCallId, tokenFunction, tokenExpiry, tokenNonce, tokenSignature] = parts;
78
+
79
+ const effectiveCallId = callId || tokenCallId;
80
+ if (tokenFunction !== functionName) return false;
81
+
82
+ const expiry = parseInt(tokenExpiry, 10);
83
+ if (expiry < Date.now() / 1000) return false;
84
+
85
+ const message = `${tokenCallId}:${tokenFunction}:${tokenExpiry}:${tokenNonce}`;
86
+ const expectedSig = createHmac('sha256', this.secretKey)
87
+ .update(message)
88
+ .digest('hex')
89
+ .slice(0, 16);
90
+ if (tokenSignature !== expectedSig) return false;
91
+ if (tokenCallId !== effectiveCallId) return false;
92
+
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Alias for {@link validateToken} with reordered parameters.
101
+ * @param functionName - The expected function name.
102
+ * @param token - The base64url-encoded token to validate.
103
+ * @param callId - The expected call ID.
104
+ * @returns True if the token is valid and not expired.
105
+ */
106
+ validateToolToken(functionName: string, token: string, callId: string): boolean {
107
+ return this.validateToken(callId, functionName, token);
108
+ }
109
+
110
+ /**
111
+ * Decode token components for debugging without validating the signature.
112
+ * @param token - The base64url-encoded token to decode.
113
+ * @returns The decoded token fields and expiration status, or null if malformed.
114
+ */
115
+ debugToken(token: string): { callId: string; functionName: string; expiry: number; nonce: string; signature: string; expired: boolean } | null {
116
+ try {
117
+ const decoded = Buffer.from(token, 'base64url').toString();
118
+ const parts = decoded.split('.');
119
+ if (parts.length !== 5) return null;
120
+ const [callId, functionName, expiryStr, nonce, signature] = parts;
121
+ const expiry = parseInt(expiryStr, 10);
122
+ return {
123
+ callId,
124
+ functionName,
125
+ expiry,
126
+ nonce,
127
+ signature,
128
+ expired: expiry < Date.now() / 1000,
129
+ };
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Retrieve metadata associated with a session.
137
+ * @param sessionId - The session identifier.
138
+ * @returns The metadata record, or undefined if no metadata exists.
139
+ */
140
+ getSessionMetadata(sessionId: string): Record<string, unknown> | undefined {
141
+ return this.sessionMetadata.get(sessionId);
142
+ }
143
+
144
+ /**
145
+ * Merge metadata into a session, creating the entry if it does not exist.
146
+ * @param sessionId - The session identifier.
147
+ * @param metadata - Key-value pairs to merge into the session metadata.
148
+ */
149
+ setSessionMetadata(sessionId: string, metadata: Record<string, unknown>): void {
150
+ this.sessionMetadata.set(sessionId, { ...this.sessionMetadata.get(sessionId), ...metadata });
151
+ }
152
+
153
+ /**
154
+ * Delete all metadata for a session.
155
+ * @param sessionId - The session identifier.
156
+ * @returns True if the session existed and was deleted.
157
+ */
158
+ deleteSessionMetadata(sessionId: string): boolean {
159
+ return this.sessionMetadata.delete(sessionId);
160
+ }
161
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * SslConfig - SSL/TLS configuration for HTTPS serving.
3
+ *
4
+ * Env vars:
5
+ * SWML_SSL_ENABLED: true|false
6
+ * SWML_SSL_CERT_PATH: path to PEM cert
7
+ * SWML_SSL_KEY_PATH: path to PEM key
8
+ * SWML_SSL_DOMAIN: domain for HSTS
9
+ */
10
+
11
+ import { readFileSync, existsSync } from 'node:fs';
12
+
13
+ /** Configuration options for SSL/TLS setup. */
14
+ export interface SslOptions {
15
+ /** Whether SSL is enabled. */
16
+ enabled?: boolean;
17
+ /** Filesystem path to the PEM-encoded certificate. */
18
+ certPath?: string;
19
+ /** Filesystem path to the PEM-encoded private key. */
20
+ keyPath?: string;
21
+ /** Domain name used for HSTS headers. */
22
+ domain?: string;
23
+ /** Whether to send HSTS headers; defaults to true. */
24
+ hsts?: boolean;
25
+ /** HSTS max-age in seconds; defaults to 31536000 (1 year). */
26
+ hstsMaxAge?: number;
27
+ }
28
+
29
+ /** Manages SSL/TLS configuration sourced from explicit options or environment variables. */
30
+ export class SslConfig {
31
+ /** Whether SSL is enabled. */
32
+ enabled: boolean;
33
+ /** Filesystem path to the PEM certificate, or null if unset. */
34
+ certPath: string | null;
35
+ /** Filesystem path to the PEM private key, or null if unset. */
36
+ keyPath: string | null;
37
+ /** Domain name for HSTS, or null if unset. */
38
+ domain: string | null;
39
+ /** Whether HSTS headers should be emitted. */
40
+ hsts: boolean;
41
+ /** HSTS max-age value in seconds. */
42
+ hstsMaxAge: number;
43
+
44
+ /**
45
+ * Create an SslConfig, falling back to environment variables for any unset options.
46
+ * @param opts - Optional SSL configuration overrides.
47
+ */
48
+ constructor(opts?: SslOptions) {
49
+ this.enabled = opts?.enabled ?? (process.env['SWML_SSL_ENABLED'] === 'true');
50
+ this.certPath = opts?.certPath ?? process.env['SWML_SSL_CERT_PATH'] ?? null;
51
+ this.keyPath = opts?.keyPath ?? process.env['SWML_SSL_KEY_PATH'] ?? null;
52
+ this.domain = opts?.domain ?? process.env['SWML_SSL_DOMAIN'] ?? null;
53
+ this.hsts = opts?.hsts ?? true;
54
+ this.hstsMaxAge = opts?.hstsMaxAge ?? 31536000; // 1 year
55
+ }
56
+
57
+ /**
58
+ * Check whether SSL is fully configured: enabled with both cert and key files present on disk.
59
+ * @returns True if SSL is enabled and both certificate and key files exist.
60
+ */
61
+ isConfigured(): boolean {
62
+ if (!this.enabled) return false;
63
+ if (!this.certPath || !this.keyPath) return false;
64
+ return existsSync(this.certPath) && existsSync(this.keyPath);
65
+ }
66
+
67
+ /**
68
+ * Read the PEM certificate file from disk.
69
+ * @returns The certificate contents, or null if the file is missing.
70
+ */
71
+ getCert(): string | null {
72
+ if (!this.certPath || !existsSync(this.certPath)) return null;
73
+ return readFileSync(this.certPath, 'utf-8');
74
+ }
75
+
76
+ /**
77
+ * Read the PEM private key file from disk.
78
+ * @returns The key contents, or null if the file is missing.
79
+ */
80
+ getKey(): string | null {
81
+ if (!this.keyPath || !existsSync(this.keyPath)) return null;
82
+ return readFileSync(this.keyPath, 'utf-8');
83
+ }
84
+
85
+ /**
86
+ * Build the Strict-Transport-Security header value.
87
+ * @returns The HSTS header string, or null if HSTS is disabled or SSL is off.
88
+ */
89
+ getHstsHeader(): string | null {
90
+ if (!this.hsts || !this.enabled) return null;
91
+ return `max-age=${this.hstsMaxAge}; includeSubDomains`;
92
+ }
93
+
94
+ /**
95
+ * Create the options object needed by Node.js `https.createServer()`.
96
+ * @returns An object with `cert` and `key` strings, or null if either file is missing.
97
+ */
98
+ getServerOptions(): { cert: string; key: string } | null {
99
+ const cert = this.getCert();
100
+ const key = this.getKey();
101
+ if (!cert || !key) return null;
102
+ return { cert, key };
103
+ }
104
+
105
+ /**
106
+ * Return a Hono middleware that appends the HSTS header to every response.
107
+ * @returns A Hono-compatible middleware function.
108
+ */
109
+ hstsMiddleware(): (c: any, next: () => Promise<void>) => Promise<void> {
110
+ return async (c: any, next: () => Promise<void>) => {
111
+ await next();
112
+ const hsts = this.getHstsHeader();
113
+ if (hsts) {
114
+ c.res.headers.set('Strict-Transport-Security', hsts);
115
+ }
116
+ };
117
+ }
118
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * SwaigFunction - Wraps a handler function with metadata for SWAIG registration.
3
+ */
4
+
5
+ import { SwaigFunctionResult } from './SwaigFunctionResult.js';
6
+ import { getLogger } from './Logger.js';
7
+
8
+ const log = getLogger('SwaigFunction');
9
+
10
+ /**
11
+ * Handler function for a SWAIG tool invocation.
12
+ * @param args - Parsed arguments extracted by the AI from user speech.
13
+ * @param rawData - The full raw request payload from SignalWire.
14
+ * @returns A SwaigFunctionResult, a plain object with a response key, a string, or a Promise of any of these.
15
+ */
16
+ export type SwaigHandler = (
17
+ args: Record<string, unknown>,
18
+ rawData: Record<string, unknown>,
19
+ ) => SwaigFunctionResult | Record<string, unknown> | string | Promise<SwaigFunctionResult | Record<string, unknown> | string>;
20
+
21
+ /** Configuration options for creating a SwaigFunction. */
22
+ export interface SwaigFunctionOptions {
23
+ /** Unique name used to register and invoke this tool. */
24
+ name: string;
25
+ /** The handler function called when the tool is invoked. */
26
+ handler: SwaigHandler;
27
+ /** Human-readable description of what the tool does, shown to the AI. */
28
+ description: string;
29
+ /** JSON Schema properties describing the tool's parameters. */
30
+ parameters?: Record<string, unknown>;
31
+ /** Whether this tool requires session token authentication. */
32
+ secure?: boolean;
33
+ /** Language-keyed filler phrases spoken while the tool executes. */
34
+ fillers?: Record<string, string[]>;
35
+ /** Audio file URL to play while waiting for the tool to complete. */
36
+ waitFile?: string;
37
+ /** Number of times to loop the wait file. */
38
+ waitFileLoops?: number;
39
+ /** External webhook URL; makes this an externally-hosted tool. */
40
+ webhookUrl?: string;
41
+ /** List of required parameter names. */
42
+ required?: string[];
43
+ /** Additional fields to include in the SWAIG definition output. */
44
+ extraFields?: Record<string, unknown>;
45
+ }
46
+
47
+ /**
48
+ * Wraps a tool handler function with metadata for SWAIG registration.
49
+ *
50
+ * Manages the tool's name, description, parameter schema, and handler,
51
+ * and serializes the definition into the SWAIG wire format.
52
+ */
53
+ export class SwaigFunction {
54
+ /** Unique name used to register and invoke this tool. */
55
+ name: string;
56
+ /** The handler function called when the tool is invoked. */
57
+ handler: SwaigHandler;
58
+ /** Human-readable description shown to the AI. */
59
+ description: string;
60
+ /** JSON Schema properties describing the tool's parameters. */
61
+ parameters: Record<string, unknown>;
62
+ /** Whether this tool requires session token authentication. */
63
+ secure: boolean;
64
+ /** Language-keyed filler phrases spoken while the tool executes. */
65
+ fillers?: Record<string, string[]>;
66
+ /** Audio file URL to play while waiting for the tool to complete. */
67
+ waitFile?: string;
68
+ /** Number of times to loop the wait file. */
69
+ waitFileLoops?: number;
70
+ /** External webhook URL; set when the tool is externally hosted. */
71
+ webhookUrl?: string;
72
+ /** List of required parameter names. */
73
+ required: string[];
74
+ /** Additional fields included in the SWAIG definition output. */
75
+ extraFields: Record<string, unknown>;
76
+ /** Whether this tool is externally hosted (has a webhookUrl). */
77
+ isExternal: boolean;
78
+
79
+ /**
80
+ * @param opts - Configuration options for the SWAIG function.
81
+ */
82
+ constructor(opts: SwaigFunctionOptions) {
83
+ this.name = opts.name;
84
+ this.handler = opts.handler;
85
+ this.description = opts.description;
86
+ this.parameters = opts.parameters ?? {};
87
+ this.secure = opts.secure ?? false;
88
+ this.fillers = opts.fillers;
89
+ this.waitFile = opts.waitFile;
90
+ this.waitFileLoops = opts.waitFileLoops;
91
+ this.webhookUrl = opts.webhookUrl;
92
+ this.required = opts.required ?? [];
93
+ this.extraFields = opts.extraFields ?? {};
94
+ this.isExternal = opts.webhookUrl !== undefined;
95
+ }
96
+
97
+ private ensureParameterStructure(): Record<string, unknown> {
98
+ if (!this.parameters || Object.keys(this.parameters).length === 0) {
99
+ return { type: 'object', properties: {} };
100
+ }
101
+ if ('type' in this.parameters && 'properties' in this.parameters) {
102
+ return this.parameters;
103
+ }
104
+ const result: Record<string, unknown> = {
105
+ type: 'object',
106
+ properties: this.parameters,
107
+ };
108
+ if (this.required.length) result['required'] = this.required;
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Invoke the handler with the given arguments and return a serialized result.
114
+ * @param args - Parsed arguments from the AI.
115
+ * @param rawData - The full raw request payload.
116
+ * @returns A serialized result dictionary suitable for the SWAIG response.
117
+ */
118
+ async execute(
119
+ args: Record<string, unknown>,
120
+ rawData?: Record<string, unknown>,
121
+ ): Promise<Record<string, unknown>> {
122
+ try {
123
+ const result = await this.handler(args, rawData ?? {});
124
+ if (result instanceof SwaigFunctionResult) {
125
+ return result.toDict();
126
+ }
127
+ if (typeof result === 'object' && result !== null && 'response' in result) {
128
+ return result as Record<string, unknown>;
129
+ }
130
+ if (typeof result === 'object' && result !== null) {
131
+ return new SwaigFunctionResult('Function completed successfully').toDict();
132
+ }
133
+ return new SwaigFunctionResult(String(result)).toDict();
134
+ } catch (err) {
135
+ log.error(`Error executing SWAIG function ${this.name}: ${err}`);
136
+ return new SwaigFunctionResult(
137
+ "Sorry, I couldn't complete that action. Please try again or contact support if the issue persists.",
138
+ ).toDict();
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Serialize this function to the SWAIG wire format for inclusion in SWML.
144
+ * @param baseUrl - The base URL of the agent server.
145
+ * @param token - Optional session token for secure functions.
146
+ * @param callId - Optional call ID for secure function URLs.
147
+ * @returns A SWAIG function definition object.
148
+ */
149
+ toSwaig(baseUrl: string, token?: string, callId?: string): Record<string, unknown> {
150
+ let url = `${baseUrl}/swaig`;
151
+ if (token && callId) {
152
+ url = `${url}?token=${token}&call_id=${callId}`;
153
+ }
154
+ const def: Record<string, unknown> = {
155
+ function: this.name,
156
+ description: this.description,
157
+ parameters: this.ensureParameterStructure(),
158
+ };
159
+ if (url) def['web_hook_url'] = url;
160
+ if (this.fillers && Object.keys(this.fillers).length > 0) {
161
+ def['fillers'] = this.fillers;
162
+ }
163
+ if (this.waitFile) def['wait_file'] = this.waitFile;
164
+ if (this.waitFileLoops !== undefined) def['wait_file_loops'] = this.waitFileLoops;
165
+ Object.assign(def, this.extraFields);
166
+ return def;
167
+ }
168
+ }