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,149 @@
1
+ /**
2
+ * AgentServer - Hosts multiple AgentBase instances under a single HTTP server.
3
+ *
4
+ * Each agent is mounted at its own route prefix. The root `/` lists all
5
+ * registered agents, and `/health` + `/ready` are global health checks.
6
+ */
7
+
8
+ import { Hono } from 'hono';
9
+ import { cors } from 'hono/cors';
10
+ import { AgentBase } from './AgentBase.js';
11
+ import { getLogger } from './Logger.js';
12
+
13
+ /** Multi-agent HTTP server that hosts multiple AgentBase instances on distinct route prefixes. */
14
+ export class AgentServer {
15
+ /** Hostname the server binds to. */
16
+ host: string;
17
+ /** Port the server listens on. */
18
+ port: number;
19
+ private agents: Map<string, AgentBase> = new Map();
20
+ private _app: Hono;
21
+ private log = getLogger('AgentServer');
22
+
23
+ /**
24
+ * Create an AgentServer.
25
+ * @param opts - Optional host and port overrides; defaults to 0.0.0.0:3000.
26
+ */
27
+ constructor(opts?: { host?: string; port?: number }) {
28
+ this.host = opts?.host ?? '0.0.0.0';
29
+ this.port = opts?.port ?? parseInt(process.env['PORT'] ?? '3000', 10);
30
+ this._app = new Hono();
31
+
32
+ // Security headers
33
+ this._app.use('*', async (c, next) => {
34
+ await next();
35
+ c.res.headers.set('X-Content-Type-Options', 'nosniff');
36
+ c.res.headers.set('X-Frame-Options', 'DENY');
37
+ c.res.headers.set('X-XSS-Protection', '1; mode=block');
38
+ c.res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
39
+ });
40
+
41
+ // CORS
42
+ this._app.use('*', cors({ origin: '*', credentials: true }));
43
+
44
+ // Global health endpoints
45
+ this._app.get('/health', (c) => c.json({ status: 'ok' }));
46
+ this._app.get('/ready', (c) => c.json({ status: 'ready' }));
47
+ }
48
+
49
+ /**
50
+ * Register an agent at the given route prefix.
51
+ * @param agent - The AgentBase instance to mount.
52
+ * @param route - Route prefix; defaults to the agent's own route or '/'.
53
+ * @throws If the route is already occupied by another agent.
54
+ */
55
+ register(agent: AgentBase, route?: string): void {
56
+ let r = route ?? (agent as any).route ?? '/';
57
+ if (!r.startsWith('/')) r = `/${r}`;
58
+ r = r.replace(/\/+$/, '') || '/';
59
+
60
+ if (this.agents.has(r)) {
61
+ throw new Error(`Route '${r}' is already in use`);
62
+ }
63
+
64
+ this.agents.set(r, agent);
65
+
66
+ // Mount the agent's Hono app at the route prefix
67
+ const agentApp = agent.getApp();
68
+ if (r === '/') {
69
+ this._app.route('/', agentApp);
70
+ } else {
71
+ this._app.route(r, agentApp);
72
+ }
73
+
74
+ this.log.info(`Registered '${agent.name}' at ${r}`);
75
+ }
76
+
77
+ /**
78
+ * Remove an agent registration by route.
79
+ * @param route - The route prefix to unregister.
80
+ */
81
+ unregister(route: string): void {
82
+ this.agents.delete(route);
83
+ // Note: Hono doesn't support dynamic route removal,
84
+ // but the agent won't be listed anymore
85
+ }
86
+
87
+ /**
88
+ * Get all registered agents keyed by their route prefix.
89
+ * @returns A map of route prefixes to AgentBase instances.
90
+ */
91
+ getAgents(): Map<string, AgentBase> {
92
+ return this.agents;
93
+ }
94
+
95
+ /**
96
+ * Look up a registered agent by its route prefix.
97
+ * @param route - The route prefix to look up.
98
+ * @returns The agent at that route, or undefined if none is registered.
99
+ */
100
+ getAgent(route: string): AgentBase | undefined {
101
+ return this.agents.get(route);
102
+ }
103
+
104
+ /**
105
+ * Build and return the Hono application with all registered agents and a root listing endpoint.
106
+ * @returns The fully configured Hono app.
107
+ */
108
+ getApp(): Hono {
109
+ // Add root listing (registered after agents so it doesn't shadow them)
110
+ const listing = [...this.agents.entries()].map(([route, agent]) => ({
111
+ name: agent.name,
112
+ route,
113
+ }));
114
+
115
+ // We create a response for the root that lists agents
116
+ // Only if no agent is mounted at /
117
+ if (!this.agents.has('/')) {
118
+ this._app.get('/', (c) =>
119
+ c.json({
120
+ service: 'SignalWire AI Agents',
121
+ agents: listing,
122
+ }),
123
+ );
124
+ }
125
+
126
+ return this._app;
127
+ }
128
+
129
+ /**
130
+ * Start the HTTP server and begin listening for requests.
131
+ * @param host - Override the configured hostname.
132
+ * @param port - Override the configured port.
133
+ */
134
+ async run(host?: string, port?: number): Promise<void> {
135
+ const { serve } = await import('@hono/node-server');
136
+ const h = host ?? this.host;
137
+ const p = port ?? this.port;
138
+
139
+ const app = this.getApp();
140
+
141
+ this.log.info(`Starting on http://${h}:${p}`);
142
+ for (const [route, agent] of this.agents) {
143
+ const [user] = agent.getBasicAuthCredentials();
144
+ this.log.info(` ${route} -> ${agent.name} (auth: ${user}:****)`);
145
+ }
146
+
147
+ serve({ fetch: app.fetch, port: p, hostname: h });
148
+ }
149
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * AuthHandler - Multi-method authentication handler.
3
+ *
4
+ * Supports Bearer token, API key, and Basic auth with constant-time comparison.
5
+ * Can be used as Hono middleware or standalone validator.
6
+ */
7
+
8
+ import { timingSafeEqual, randomBytes } from 'node:crypto';
9
+
10
+ /** Configuration for one or more authentication methods checked by {@link AuthHandler}. */
11
+ export interface AuthConfig {
12
+ /** Bearer token matched against the Authorization header. */
13
+ bearerToken?: string;
14
+ /** API key matched against the X-Api-Key header. */
15
+ apiKey?: string;
16
+ /** Basic auth credentials as a [username, password] tuple. */
17
+ basicAuth?: [string, string];
18
+ /** Custom validator function; return true to allow the request. */
19
+ customValidator?: (request: { headers: Record<string, string>; method: string; url: string }) => boolean | Promise<boolean>;
20
+ }
21
+
22
+ /**
23
+ * Constant-time string comparison to prevent timing attacks.
24
+ */
25
+ function safeCompare(a: string, b: string): boolean {
26
+ if (a.length !== b.length) {
27
+ // Compare against a dummy to prevent length-based timing leaks
28
+ const dummy = randomBytes(a.length).toString('hex');
29
+ timingSafeEqual(Buffer.from(dummy), Buffer.from(dummy));
30
+ return false;
31
+ }
32
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b));
33
+ }
34
+
35
+ /** Multi-method authentication handler with timing-safe credential comparison. */
36
+ export class AuthHandler {
37
+ private config: AuthConfig;
38
+
39
+ /**
40
+ * Create a new AuthHandler.
41
+ * @param config - Authentication configuration specifying one or more auth methods.
42
+ */
43
+ constructor(config: AuthConfig) {
44
+ this.config = config;
45
+ }
46
+
47
+ /**
48
+ * Validate request headers against configured auth methods (Bearer, API Key, Basic, Custom) in order.
49
+ * @param headers - The request headers as a string-keyed record.
50
+ * @returns True if any configured method accepts the request, or if no methods are configured.
51
+ */
52
+ async validate(headers: Record<string, string>): Promise<boolean> {
53
+ // 1. Bearer token
54
+ if (this.config.bearerToken) {
55
+ const authHeader = headers['authorization'] || headers['Authorization'] || '';
56
+ if (authHeader.startsWith('Bearer ')) {
57
+ const token = authHeader.slice(7);
58
+ if (safeCompare(token, this.config.bearerToken)) return true;
59
+ }
60
+ }
61
+
62
+ // 2. API Key
63
+ if (this.config.apiKey) {
64
+ const key = headers['x-api-key'] || headers['X-Api-Key'] || '';
65
+ if (key && safeCompare(key, this.config.apiKey)) return true;
66
+ }
67
+
68
+ // 3. Basic auth
69
+ if (this.config.basicAuth) {
70
+ const authHeader = headers['authorization'] || headers['Authorization'] || '';
71
+ if (authHeader.startsWith('Basic ')) {
72
+ const decoded = Buffer.from(authHeader.slice(6), 'base64').toString();
73
+ const colonIdx = decoded.indexOf(':');
74
+ if (colonIdx > 0) {
75
+ const user = decoded.slice(0, colonIdx);
76
+ const pass = decoded.slice(colonIdx + 1);
77
+ const [expectedUser, expectedPass] = this.config.basicAuth;
78
+ if (safeCompare(user, expectedUser) && safeCompare(pass, expectedPass)) return true;
79
+ }
80
+ }
81
+ }
82
+
83
+ // 4. Custom validator
84
+ if (this.config.customValidator) {
85
+ const result = await this.config.customValidator({
86
+ headers,
87
+ method: '',
88
+ url: '',
89
+ });
90
+ if (result) return true;
91
+ }
92
+
93
+ // If no methods configured, allow (backwards compat)
94
+ if (!this.config.bearerToken && !this.config.apiKey && !this.config.basicAuth && !this.config.customValidator) {
95
+ return true;
96
+ }
97
+
98
+ return false;
99
+ }
100
+
101
+ /**
102
+ * Create a Hono-compatible middleware that rejects unauthorized requests with a 401 response.
103
+ * @returns A middleware function suitable for use with Hono's `app.use()`.
104
+ */
105
+ middleware(): (c: any, next: () => Promise<void>) => Promise<Response | void> {
106
+ return async (c: any, next: () => Promise<void>) => {
107
+ const headers: Record<string, string> = {};
108
+ c.req.raw.headers.forEach((v: string, k: string) => { headers[k] = v; });
109
+
110
+ const valid = await this.validate(headers);
111
+ if (!valid) {
112
+ return c.json({ error: 'Unauthorized' }, 401);
113
+ }
114
+ await next();
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Check whether Bearer token authentication is configured.
120
+ * @returns True if a bearer token has been set.
121
+ */
122
+ hasBearerAuth(): boolean {
123
+ return !!this.config.bearerToken;
124
+ }
125
+
126
+ /**
127
+ * Check whether API key authentication is configured.
128
+ * @returns True if an API key has been set.
129
+ */
130
+ hasApiKeyAuth(): boolean {
131
+ return !!this.config.apiKey;
132
+ }
133
+
134
+ /**
135
+ * Check whether Basic authentication is configured.
136
+ * @returns True if basic auth credentials have been set.
137
+ */
138
+ hasBasicAuth(): boolean {
139
+ return !!this.config.basicAuth;
140
+ }
141
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * ConfigLoader - JSON config file loader with env var interpolation.
3
+ *
4
+ * Supports ${VAR|default} syntax for env var substitution and
5
+ * dot-notation access for nested values.
6
+ */
7
+
8
+ import { readFileSync, existsSync } from 'node:fs';
9
+ import { resolve, join } from 'node:path';
10
+
11
+ const ENV_VAR_PATTERN = /\$\{([^}|]+)(?:\|([^}]*))?\}/g;
12
+
13
+ /** JSON configuration file loader with environment variable interpolation and dot-notation access. */
14
+ export class ConfigLoader {
15
+ private data: Record<string, unknown> = {};
16
+ private filePath: string | null = null;
17
+
18
+ /**
19
+ * Create a new ConfigLoader, optionally loading a JSON file immediately.
20
+ * @param filePath - Path to a JSON config file to load on construction.
21
+ */
22
+ constructor(filePath?: string) {
23
+ if (filePath) {
24
+ this.load(filePath);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Load configuration from a JSON file, performing `${VAR|default}` env var interpolation on the raw text.
30
+ * @param filePath - Path to the JSON config file.
31
+ * @returns This instance for chaining.
32
+ */
33
+ load(filePath: string): this {
34
+ const absPath = resolve(filePath);
35
+ if (!existsSync(absPath)) {
36
+ throw new Error(`Config file not found: ${absPath}`);
37
+ }
38
+ const raw = readFileSync(absPath, 'utf-8');
39
+ const interpolated = this.interpolateEnvVars(raw);
40
+ this.data = JSON.parse(interpolated);
41
+ this.filePath = absPath;
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * Search for a config file in standard locations (CWD, `./config`, `$HOME/.signalwire`).
47
+ * @param filename - The config file name to search for.
48
+ * @returns A loaded ConfigLoader if found, or null if the file does not exist in any search path.
49
+ */
50
+ static search(filename: string): ConfigLoader | null {
51
+ const searchPaths = [
52
+ process.cwd(),
53
+ join(process.cwd(), 'config'),
54
+ join(process.env['HOME'] ?? '', '.signalwire'),
55
+ ];
56
+
57
+ for (const dir of searchPaths) {
58
+ const filePath = join(dir, filename);
59
+ if (existsSync(filePath)) {
60
+ return new ConfigLoader(filePath);
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Retrieve a configuration value using a dot-notation path (e.g. `'server.port'`).
68
+ * @param path - Dot-separated key path into the config object.
69
+ * @param defaultValue - Value returned when the path does not exist.
70
+ * @returns The resolved value, or defaultValue if not found.
71
+ */
72
+ get<T = unknown>(path: string, defaultValue?: T): T {
73
+ const parts = path.split('.');
74
+ let current: unknown = this.data;
75
+
76
+ for (const part of parts) {
77
+ if (current === null || current === undefined || typeof current !== 'object') {
78
+ return defaultValue as T;
79
+ }
80
+ current = (current as Record<string, unknown>)[part];
81
+ }
82
+
83
+ return (current !== undefined ? current : defaultValue) as T;
84
+ }
85
+
86
+ /**
87
+ * Set a configuration value at the given dot-notation path, creating intermediate objects as needed.
88
+ * @param path - Dot-separated key path into the config object.
89
+ * @param value - The value to store.
90
+ * @returns This instance for chaining.
91
+ */
92
+ set(path: string, value: unknown): this {
93
+ const parts = path.split('.');
94
+ let current: Record<string, unknown> = this.data;
95
+
96
+ for (let i = 0; i < parts.length - 1; i++) {
97
+ const part = parts[i];
98
+ if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
99
+ current[part] = {};
100
+ }
101
+ current = current[part] as Record<string, unknown>;
102
+ }
103
+
104
+ current[parts[parts.length - 1]] = value;
105
+ return this;
106
+ }
107
+
108
+ /**
109
+ * Check whether a dot-notation path exists in the loaded configuration.
110
+ * @param path - Dot-separated key path to check.
111
+ * @returns True if the path resolves to a defined value.
112
+ */
113
+ has(path: string): boolean {
114
+ const parts = path.split('.');
115
+ let current: unknown = this.data;
116
+
117
+ for (const part of parts) {
118
+ if (current === null || current === undefined || typeof current !== 'object') {
119
+ return false;
120
+ }
121
+ if (!(part in (current as Record<string, unknown>))) return false;
122
+ current = (current as Record<string, unknown>)[part];
123
+ }
124
+ return true;
125
+ }
126
+
127
+ /**
128
+ * Return a shallow copy of the entire configuration object.
129
+ * @returns A copy of the top-level config data.
130
+ */
131
+ getAll(): Record<string, unknown> {
132
+ return { ...this.data };
133
+ }
134
+
135
+ /**
136
+ * Return the absolute path of the loaded config file, if any.
137
+ * @returns The file path, or null if config was loaded from an object.
138
+ */
139
+ getFilePath(): string | null {
140
+ return this.filePath;
141
+ }
142
+
143
+ /**
144
+ * Load configuration from a plain object instead of a file, useful for testing or programmatic setup.
145
+ * @param obj - The configuration object to use.
146
+ * @returns This instance for chaining.
147
+ */
148
+ loadFromObject(obj: Record<string, unknown>): this {
149
+ this.data = { ...obj };
150
+ this.filePath = null;
151
+ return this;
152
+ }
153
+
154
+ /**
155
+ * Interpolate ${VAR|default} patterns in a string.
156
+ */
157
+ private interpolateEnvVars(input: string): string {
158
+ return input.replace(ENV_VAR_PATTERN, (_match, varName: string, defaultValue?: string) => {
159
+ const envVal = process.env[varName.trim()];
160
+ if (envVal !== undefined) return envVal;
161
+ if (defaultValue !== undefined) return defaultValue;
162
+ return '';
163
+ });
164
+ }
165
+ }