@firststep-studio/sdk 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.
package/dist/server.js ADDED
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ createServer: () => createServer
24
+ });
25
+ module.exports = __toCommonJS(server_exports);
26
+ var import_http = require("http");
27
+
28
+ // src/auth.ts
29
+ var import_crypto = require("crypto");
30
+ function verifyRequestSignature(token, payload, signature) {
31
+ try {
32
+ const expected = (0, import_crypto.createHmac)("sha256", token).update(payload).digest("hex");
33
+ const expectedBuffer = Buffer.from(expected, "hex");
34
+ const signatureBuffer = Buffer.from(signature, "hex");
35
+ if (expectedBuffer.length !== signatureBuffer.length) {
36
+ return false;
37
+ }
38
+ return (0, import_crypto.timingSafeEqual)(expectedBuffer, signatureBuffer);
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ // src/server.ts
45
+ function readBody(req) {
46
+ return new Promise((resolve, reject) => {
47
+ const chunks = [];
48
+ req.on("data", (chunk) => chunks.push(chunk));
49
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
50
+ req.on("error", reject);
51
+ });
52
+ }
53
+ function sendJson(res, status, body) {
54
+ const json = JSON.stringify(body);
55
+ res.writeHead(status, {
56
+ "Content-Type": "application/json",
57
+ "Content-Length": Buffer.byteLength(json),
58
+ "Access-Control-Allow-Origin": "*"
59
+ });
60
+ res.end(json);
61
+ }
62
+ function parsePath(url) {
63
+ if (!url) return "/";
64
+ const idx = url.indexOf("?");
65
+ return idx >= 0 ? url.substring(0, idx) : url;
66
+ }
67
+ function buildStubContext(data) {
68
+ const noopAsync = async () => ({});
69
+ const noop = () => {
70
+ };
71
+ return {
72
+ session: {
73
+ sessionId: "",
74
+ getState: noopAsync,
75
+ updateState: noopAsync,
76
+ getHistory: async () => [],
77
+ saveMessage: noopAsync,
78
+ complete: noopAsync,
79
+ getFormData: async () => ({}),
80
+ updateFormField: noopAsync,
81
+ updateFormData: noopAsync,
82
+ ...data?.session
83
+ },
84
+ knowledge: {
85
+ queryDatabase: noopAsync,
86
+ searchPages: noopAsync,
87
+ ...data?.knowledge
88
+ },
89
+ integrations: {
90
+ execute: noopAsync,
91
+ ...data?.integrations
92
+ },
93
+ analytics: {
94
+ logActionExecuted: noop,
95
+ logInteraction: noop,
96
+ logCustomEvent: noop,
97
+ ...data?.analytics
98
+ },
99
+ logger: {
100
+ debug: noop,
101
+ info: noop,
102
+ warn: noop,
103
+ error: noop,
104
+ logRouting: noop,
105
+ logToolUse: noop,
106
+ ...data?.logger
107
+ },
108
+ deployment: {
109
+ id: "",
110
+ slug: "",
111
+ name: "",
112
+ protocolType: "external",
113
+ ...data?.deployment
114
+ },
115
+ chatbot: data?.chatbot
116
+ };
117
+ }
118
+ function createServer(handler, config) {
119
+ const {
120
+ token,
121
+ port = parseInt(process.env.PORT || "3001", 10),
122
+ host = "0.0.0.0",
123
+ skipSignatureVerification = false
124
+ } = config;
125
+ if (!token && !skipSignatureVerification) {
126
+ throw new Error(
127
+ "FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev."
128
+ );
129
+ }
130
+ async function handleRequest(req, res) {
131
+ const path = parsePath(req.url);
132
+ const method = (req.method || "GET").toUpperCase();
133
+ if (method === "OPTIONS") {
134
+ res.writeHead(204, {
135
+ "Access-Control-Allow-Origin": "*",
136
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
137
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-FirstStep-Signature",
138
+ "Access-Control-Max-Age": "86400"
139
+ });
140
+ res.end();
141
+ return;
142
+ }
143
+ if (path === "/health" && method === "GET") {
144
+ sendJson(res, 200, { status: "ok", timestamp: Date.now() });
145
+ return;
146
+ }
147
+ if (path === "/handshake" && method === "POST") {
148
+ const body = await readBody(req);
149
+ if (!skipSignatureVerification) {
150
+ const signature = req.headers["x-firststep-signature"];
151
+ if (!signature || !verifyRequestSignature(token, body, signature)) {
152
+ sendJson(res, 401, { error: "Invalid signature" });
153
+ return;
154
+ }
155
+ }
156
+ const capabilities = handler.getCapabilities();
157
+ const handlerInfo = handler.getHandlerInfo?.();
158
+ sendJson(res, 200, { capabilities, handler: handlerInfo || void 0 });
159
+ return;
160
+ }
161
+ if (path === "/message" && method === "POST") {
162
+ const body = await readBody(req);
163
+ if (!skipSignatureVerification) {
164
+ const signature = req.headers["x-firststep-signature"];
165
+ if (!signature || !verifyRequestSignature(token, body, signature)) {
166
+ sendJson(res, 401, { error: "Invalid signature" });
167
+ return;
168
+ }
169
+ }
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(body);
173
+ } catch {
174
+ sendJson(res, 400, { error: "Invalid JSON body" });
175
+ return;
176
+ }
177
+ const context = buildStubContext(parsed.context);
178
+ try {
179
+ const response = await handler.handleMessage(parsed.request, context);
180
+ sendJson(res, 200, response);
181
+ } catch (err) {
182
+ console.error("[firststep] Handler error:", err);
183
+ sendJson(res, 500, {
184
+ error: "Handler error",
185
+ message: err?.message || "Unknown error"
186
+ });
187
+ }
188
+ return;
189
+ }
190
+ if (path === "/message/stream" && method === "POST") {
191
+ const body = await readBody(req);
192
+ if (!skipSignatureVerification) {
193
+ const signature = req.headers["x-firststep-signature"];
194
+ if (!signature || !verifyRequestSignature(token, body, signature)) {
195
+ sendJson(res, 401, { error: "Invalid signature" });
196
+ return;
197
+ }
198
+ }
199
+ let parsed;
200
+ try {
201
+ parsed = JSON.parse(body);
202
+ } catch {
203
+ sendJson(res, 400, { error: "Invalid JSON body" });
204
+ return;
205
+ }
206
+ if (!handler.handleStream) {
207
+ const context2 = buildStubContext(parsed.context);
208
+ try {
209
+ const response = await handler.handleMessage(parsed.request, context2);
210
+ res.writeHead(200, {
211
+ "Content-Type": "text/event-stream",
212
+ "Cache-Control": "no-cache",
213
+ "Connection": "keep-alive",
214
+ "Access-Control-Allow-Origin": "*"
215
+ });
216
+ const sendSSE2 = (event, data) => {
217
+ res.write(`event: ${event}
218
+ data: ${JSON.stringify(data)}
219
+
220
+ `);
221
+ };
222
+ sendSSE2("connected", { sessionId: response.sessionId });
223
+ sendSSE2("text", { type: "text", content: response.message, sessionId: response.sessionId });
224
+ if (response.metadata) {
225
+ sendSSE2("metadata", { type: "metadata", content: response.metadata, sessionId: response.sessionId });
226
+ }
227
+ if (response.form) {
228
+ sendSSE2("form", { type: "form", content: response.form, sessionId: response.sessionId });
229
+ }
230
+ sendSSE2("status", { type: "status", content: response.sessionStatus, sessionId: response.sessionId });
231
+ sendSSE2("done", { sessionId: response.sessionId });
232
+ res.end();
233
+ } catch (err) {
234
+ console.error("[firststep] Handler error:", err);
235
+ sendJson(res, 500, { error: "Handler error", message: err?.message || "Unknown error" });
236
+ }
237
+ return;
238
+ }
239
+ const context = buildStubContext(parsed.context);
240
+ res.writeHead(200, {
241
+ "Content-Type": "text/event-stream",
242
+ "Cache-Control": "no-cache",
243
+ "Connection": "keep-alive",
244
+ "Access-Control-Allow-Origin": "*"
245
+ });
246
+ const sendSSE = (event, data) => {
247
+ res.write(`event: ${event}
248
+ data: ${JSON.stringify(data)}
249
+
250
+ `);
251
+ };
252
+ const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;
253
+ sendSSE("connected", { sessionId });
254
+ try {
255
+ for await (const chunk of handler.handleStream(parsed.request, context)) {
256
+ sendSSE(chunk.type, {
257
+ type: chunk.type,
258
+ content: chunk.content,
259
+ sessionId
260
+ });
261
+ }
262
+ sendSSE("done", { sessionId });
263
+ } catch (err) {
264
+ console.error("[firststep] Stream error:", err);
265
+ sendSSE("error", { code: "STREAM_ERROR", message: err?.message || "Unknown error" });
266
+ }
267
+ res.end();
268
+ return;
269
+ }
270
+ sendJson(res, 404, { error: "Not found" });
271
+ }
272
+ function requestListener(req, res) {
273
+ handleRequest(req, res).catch((err) => {
274
+ console.error("[firststep] Unexpected error:", err);
275
+ if (!res.headersSent) {
276
+ sendJson(res, 500, { error: "Internal server error" });
277
+ }
278
+ });
279
+ }
280
+ const httpServer = (0, import_http.createServer)(requestListener);
281
+ return {
282
+ start() {
283
+ return new Promise((resolve) => {
284
+ httpServer.listen(port, host, () => {
285
+ console.log(`[firststep] Handler server listening on ${host}:${port}`);
286
+ console.log(`[firststep] Endpoints:`);
287
+ console.log(` GET /health - Health check`);
288
+ console.log(` POST /handshake - Capability exchange`);
289
+ console.log(` POST /message - Handle chat message`);
290
+ console.log(` POST /message/stream - Handle chat message (SSE stream)`);
291
+ resolve();
292
+ });
293
+ });
294
+ },
295
+ stop() {
296
+ return new Promise((resolve, reject) => {
297
+ httpServer.close((err) => {
298
+ if (err) reject(err);
299
+ else resolve();
300
+ });
301
+ });
302
+ },
303
+ getRequestHandler() {
304
+ return requestListener;
305
+ }
306
+ };
307
+ }
308
+ // Annotate the CommonJS export names for ESM import in node:
309
+ 0 && (module.exports = {
310
+ createServer
311
+ });
312
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK Server\n *\n * Standalone HTTP server for protocol handlers.\n * Zero external dependencies, uses Node.js built-in `http` module.\n *\n * @example\n * ```typescript\n * import { createServer } from '@firststep-studio/sdk/server';\n * import type { ProtocolHandler } from '@firststep-studio/sdk';\n *\n * const handler: ProtocolHandler = {\n * async handleMessage(request, context) {\n * return {\n * message: 'Hello from my handler!',\n * sessionId: request.sessionId || 'new',\n * agentId: 'main',\n * sessionStatus: 'active',\n * };\n * },\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * },\n * };\n *\n * const server = createServer(handler, {\n * token: process.env.FIRSTSTEP_TOKEN!,\n * port: 3001,\n * });\n *\n * server.start();\n * ```\n */\n\nimport { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http';\nimport type {\n ProtocolHandler,\n ProtocolRequest,\n ProtocolContext,\n ProtocolCapabilities,\n ProtocolStreamChunk,\n} from './types';\nimport { verifyRequestSignature } from './auth';\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface ServerConfig {\n /** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */\n token: string;\n\n /** Port to listen on. Defaults to 3001, or the PORT env variable. */\n port?: number;\n\n /** Host to bind to. Defaults to '0.0.0.0'. */\n host?: string;\n\n /** Skip signature verification (for local development only). */\n skipSignatureVerification?: boolean;\n}\n\n// ============================================\n// Server Instance\n// ============================================\n\nexport interface FirstStepServer {\n /** Start the server */\n start(): Promise<void>;\n\n /** Stop the server gracefully */\n stop(): Promise<void>;\n\n /**\n * Get the request handler function.\n * Use this to integrate with Express, Fastify, or other frameworks.\n *\n * @example\n * ```typescript\n * // Express\n * const server = createServer(handler, { token: '...' });\n * app.post('/handshake', server.getRequestHandler());\n * app.post('/message', server.getRequestHandler());\n * ```\n */\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;\n}\n\n// ============================================\n// Internal Helpers\n// ============================================\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json),\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(json);\n}\n\nfunction parsePath(url: string | undefined): string {\n if (!url) return '/';\n const idx = url.indexOf('?');\n return idx >= 0 ? url.substring(0, idx) : url;\n}\n\n/**\n * Build a stub ProtocolContext for external handlers.\n *\n * External handlers receive context data as plain JSON from the platform.\n * The stub provides no-op implementations for methods like session.getState()\n * since those will be handled by callback APIs to the FirstStep backend.\n *\n * When the platform sends a /message request, the request body includes\n * a `context` field with serialized context data. This function merges\n * that data with no-op method stubs.\n */\nfunction buildStubContext(data?: Partial<ProtocolContext>): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n return {\n session: {\n sessionId: '',\n getState: noopAsync,\n updateState: noopAsync,\n getHistory: async () => [],\n saveMessage: noopAsync,\n complete: noopAsync,\n getFormData: async () => ({}),\n updateFormField: noopAsync,\n updateFormData: noopAsync,\n ...data?.session,\n },\n knowledge: {\n queryDatabase: noopAsync,\n searchPages: noopAsync,\n ...data?.knowledge,\n },\n integrations: {\n execute: noopAsync,\n ...data?.integrations,\n },\n analytics: {\n logActionExecuted: noop,\n logInteraction: noop,\n logCustomEvent: noop,\n ...data?.analytics,\n },\n logger: {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n logRouting: noop,\n logToolUse: noop,\n ...data?.logger,\n },\n deployment: {\n id: '',\n slug: '',\n name: '',\n protocolType: 'external',\n ...data?.deployment,\n },\n chatbot: data?.chatbot,\n };\n}\n\n// ============================================\n// createServer\n// ============================================\n\n/**\n * Create a standalone HTTP server for a protocol handler.\n *\n * The server exposes three endpoints:\n * - `GET /health` - Health check (always 200)\n * - `POST /handshake` - Returns handler capabilities (signature verified)\n * - `POST /message` - Handles a chat message (signature verified)\n */\nexport function createServer(\n handler: ProtocolHandler,\n config: ServerConfig\n): FirstStepServer {\n const {\n token,\n port = parseInt(process.env.PORT || '3001', 10),\n host = '0.0.0.0',\n skipSignatureVerification = false,\n } = config;\n\n // Validate config\n if (!token && !skipSignatureVerification) {\n throw new Error(\n 'FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev.'\n );\n }\n\n /**\n * Core request router\n */\n async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const path = parsePath(req.url);\n const method = (req.method || 'GET').toUpperCase();\n\n // CORS preflight\n if (method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-FirstStep-Signature',\n 'Access-Control-Max-Age': '86400',\n });\n res.end();\n return;\n }\n\n // Health check\n if (path === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n if (path === '/handshake' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (!signature || !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n const capabilities: ProtocolCapabilities = handler.getCapabilities();\n const handlerInfo = handler.getHandlerInfo?.();\n sendJson(res, 200, { capabilities, handler: handlerInfo || undefined });\n return;\n }\n\n // Message\n if (path === '/message' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (!signature || !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n try {\n const response = await handler.handleMessage(parsed.request, context);\n sendJson(res, 200, response);\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, {\n error: 'Handler error',\n message: err?.message || 'Unknown error',\n });\n }\n return;\n }\n\n // Message (streaming via SSE)\n if (path === '/message/stream' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (!signature || !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n if (!handler.handleStream) {\n // Fallback: use non-streaming handleMessage, send as single SSE burst\n const context = buildStubContext(parsed.context);\n try {\n const response = await handler.handleMessage(parsed.request, context);\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n sendSSE('connected', { sessionId: response.sessionId });\n sendSSE('text', { type: 'text', content: response.message, sessionId: response.sessionId });\n if (response.metadata) {\n sendSSE('metadata', { type: 'metadata', content: response.metadata, sessionId: response.sessionId });\n }\n if (response.form) {\n sendSSE('form', { type: 'form', content: response.form, sessionId: response.sessionId });\n }\n sendSSE('status', { type: 'status', content: response.sessionStatus, sessionId: response.sessionId });\n sendSSE('done', { sessionId: response.sessionId });\n res.end();\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, { error: 'Handler error', message: err?.message || 'Unknown error' });\n }\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n\n const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;\n sendSSE('connected', { sessionId });\n\n try {\n for await (const chunk of handler.handleStream(parsed.request, context)) {\n sendSSE(chunk.type, {\n type: chunk.type,\n content: chunk.content,\n sessionId,\n });\n }\n sendSSE('done', { sessionId });\n } catch (err: any) {\n console.error('[firststep] Stream error:', err);\n sendSSE('error', { code: 'STREAM_ERROR', message: err?.message || 'Unknown error' });\n }\n\n res.end();\n return;\n }\n\n // 404\n sendJson(res, 404, { error: 'Not found' });\n }\n\n // Wrap in error boundary\n function requestListener(req: IncomingMessage, res: ServerResponse): void {\n handleRequest(req, res).catch((err) => {\n console.error('[firststep] Unexpected error:', err);\n if (!res.headersSent) {\n sendJson(res, 500, { error: 'Internal server error' });\n }\n });\n }\n\n const httpServer = createHttpServer(requestListener);\n\n return {\n start() {\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n console.log(`[firststep] Handler server listening on ${host}:${port}`);\n console.log(`[firststep] Endpoints:`);\n console.log(` GET /health - Health check`);\n console.log(` POST /handshake - Capability exchange`);\n console.log(` POST /message - Handle chat message`);\n console.log(` POST /message/stream - Handle chat message (SSE stream)`);\n resolve();\n });\n });\n },\n\n stop() {\n return new Promise<void>((resolve, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n getRequestHandler() {\n return requestListener;\n },\n };\n}\n","import { createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const expected = createHmac('sha256', token)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n return createHmac('sha256', token).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":["context","sendSSE","createHttpServer"]}
@@ -0,0 +1,287 @@
1
+ // src/server.ts
2
+ import { createServer as createHttpServer } from "http";
3
+
4
+ // src/auth.ts
5
+ import { createHmac, timingSafeEqual } from "crypto";
6
+ function verifyRequestSignature(token, payload, signature) {
7
+ try {
8
+ const expected = createHmac("sha256", token).update(payload).digest("hex");
9
+ const expectedBuffer = Buffer.from(expected, "hex");
10
+ const signatureBuffer = Buffer.from(signature, "hex");
11
+ if (expectedBuffer.length !== signatureBuffer.length) {
12
+ return false;
13
+ }
14
+ return timingSafeEqual(expectedBuffer, signatureBuffer);
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ // src/server.ts
21
+ function readBody(req) {
22
+ return new Promise((resolve, reject) => {
23
+ const chunks = [];
24
+ req.on("data", (chunk) => chunks.push(chunk));
25
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
26
+ req.on("error", reject);
27
+ });
28
+ }
29
+ function sendJson(res, status, body) {
30
+ const json = JSON.stringify(body);
31
+ res.writeHead(status, {
32
+ "Content-Type": "application/json",
33
+ "Content-Length": Buffer.byteLength(json),
34
+ "Access-Control-Allow-Origin": "*"
35
+ });
36
+ res.end(json);
37
+ }
38
+ function parsePath(url) {
39
+ if (!url) return "/";
40
+ const idx = url.indexOf("?");
41
+ return idx >= 0 ? url.substring(0, idx) : url;
42
+ }
43
+ function buildStubContext(data) {
44
+ const noopAsync = async () => ({});
45
+ const noop = () => {
46
+ };
47
+ return {
48
+ session: {
49
+ sessionId: "",
50
+ getState: noopAsync,
51
+ updateState: noopAsync,
52
+ getHistory: async () => [],
53
+ saveMessage: noopAsync,
54
+ complete: noopAsync,
55
+ getFormData: async () => ({}),
56
+ updateFormField: noopAsync,
57
+ updateFormData: noopAsync,
58
+ ...data?.session
59
+ },
60
+ knowledge: {
61
+ queryDatabase: noopAsync,
62
+ searchPages: noopAsync,
63
+ ...data?.knowledge
64
+ },
65
+ integrations: {
66
+ execute: noopAsync,
67
+ ...data?.integrations
68
+ },
69
+ analytics: {
70
+ logActionExecuted: noop,
71
+ logInteraction: noop,
72
+ logCustomEvent: noop,
73
+ ...data?.analytics
74
+ },
75
+ logger: {
76
+ debug: noop,
77
+ info: noop,
78
+ warn: noop,
79
+ error: noop,
80
+ logRouting: noop,
81
+ logToolUse: noop,
82
+ ...data?.logger
83
+ },
84
+ deployment: {
85
+ id: "",
86
+ slug: "",
87
+ name: "",
88
+ protocolType: "external",
89
+ ...data?.deployment
90
+ },
91
+ chatbot: data?.chatbot
92
+ };
93
+ }
94
+ function createServer(handler, config) {
95
+ const {
96
+ token,
97
+ port = parseInt(process.env.PORT || "3001", 10),
98
+ host = "0.0.0.0",
99
+ skipSignatureVerification = false
100
+ } = config;
101
+ if (!token && !skipSignatureVerification) {
102
+ throw new Error(
103
+ "FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev."
104
+ );
105
+ }
106
+ async function handleRequest(req, res) {
107
+ const path = parsePath(req.url);
108
+ const method = (req.method || "GET").toUpperCase();
109
+ if (method === "OPTIONS") {
110
+ res.writeHead(204, {
111
+ "Access-Control-Allow-Origin": "*",
112
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
113
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-FirstStep-Signature",
114
+ "Access-Control-Max-Age": "86400"
115
+ });
116
+ res.end();
117
+ return;
118
+ }
119
+ if (path === "/health" && method === "GET") {
120
+ sendJson(res, 200, { status: "ok", timestamp: Date.now() });
121
+ return;
122
+ }
123
+ if (path === "/handshake" && method === "POST") {
124
+ const body = await readBody(req);
125
+ if (!skipSignatureVerification) {
126
+ const signature = req.headers["x-firststep-signature"];
127
+ if (!signature || !verifyRequestSignature(token, body, signature)) {
128
+ sendJson(res, 401, { error: "Invalid signature" });
129
+ return;
130
+ }
131
+ }
132
+ const capabilities = handler.getCapabilities();
133
+ const handlerInfo = handler.getHandlerInfo?.();
134
+ sendJson(res, 200, { capabilities, handler: handlerInfo || void 0 });
135
+ return;
136
+ }
137
+ if (path === "/message" && method === "POST") {
138
+ const body = await readBody(req);
139
+ if (!skipSignatureVerification) {
140
+ const signature = req.headers["x-firststep-signature"];
141
+ if (!signature || !verifyRequestSignature(token, body, signature)) {
142
+ sendJson(res, 401, { error: "Invalid signature" });
143
+ return;
144
+ }
145
+ }
146
+ let parsed;
147
+ try {
148
+ parsed = JSON.parse(body);
149
+ } catch {
150
+ sendJson(res, 400, { error: "Invalid JSON body" });
151
+ return;
152
+ }
153
+ const context = buildStubContext(parsed.context);
154
+ try {
155
+ const response = await handler.handleMessage(parsed.request, context);
156
+ sendJson(res, 200, response);
157
+ } catch (err) {
158
+ console.error("[firststep] Handler error:", err);
159
+ sendJson(res, 500, {
160
+ error: "Handler error",
161
+ message: err?.message || "Unknown error"
162
+ });
163
+ }
164
+ return;
165
+ }
166
+ if (path === "/message/stream" && method === "POST") {
167
+ const body = await readBody(req);
168
+ if (!skipSignatureVerification) {
169
+ const signature = req.headers["x-firststep-signature"];
170
+ if (!signature || !verifyRequestSignature(token, body, signature)) {
171
+ sendJson(res, 401, { error: "Invalid signature" });
172
+ return;
173
+ }
174
+ }
175
+ let parsed;
176
+ try {
177
+ parsed = JSON.parse(body);
178
+ } catch {
179
+ sendJson(res, 400, { error: "Invalid JSON body" });
180
+ return;
181
+ }
182
+ if (!handler.handleStream) {
183
+ const context2 = buildStubContext(parsed.context);
184
+ try {
185
+ const response = await handler.handleMessage(parsed.request, context2);
186
+ res.writeHead(200, {
187
+ "Content-Type": "text/event-stream",
188
+ "Cache-Control": "no-cache",
189
+ "Connection": "keep-alive",
190
+ "Access-Control-Allow-Origin": "*"
191
+ });
192
+ const sendSSE2 = (event, data) => {
193
+ res.write(`event: ${event}
194
+ data: ${JSON.stringify(data)}
195
+
196
+ `);
197
+ };
198
+ sendSSE2("connected", { sessionId: response.sessionId });
199
+ sendSSE2("text", { type: "text", content: response.message, sessionId: response.sessionId });
200
+ if (response.metadata) {
201
+ sendSSE2("metadata", { type: "metadata", content: response.metadata, sessionId: response.sessionId });
202
+ }
203
+ if (response.form) {
204
+ sendSSE2("form", { type: "form", content: response.form, sessionId: response.sessionId });
205
+ }
206
+ sendSSE2("status", { type: "status", content: response.sessionStatus, sessionId: response.sessionId });
207
+ sendSSE2("done", { sessionId: response.sessionId });
208
+ res.end();
209
+ } catch (err) {
210
+ console.error("[firststep] Handler error:", err);
211
+ sendJson(res, 500, { error: "Handler error", message: err?.message || "Unknown error" });
212
+ }
213
+ return;
214
+ }
215
+ const context = buildStubContext(parsed.context);
216
+ res.writeHead(200, {
217
+ "Content-Type": "text/event-stream",
218
+ "Cache-Control": "no-cache",
219
+ "Connection": "keep-alive",
220
+ "Access-Control-Allow-Origin": "*"
221
+ });
222
+ const sendSSE = (event, data) => {
223
+ res.write(`event: ${event}
224
+ data: ${JSON.stringify(data)}
225
+
226
+ `);
227
+ };
228
+ const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;
229
+ sendSSE("connected", { sessionId });
230
+ try {
231
+ for await (const chunk of handler.handleStream(parsed.request, context)) {
232
+ sendSSE(chunk.type, {
233
+ type: chunk.type,
234
+ content: chunk.content,
235
+ sessionId
236
+ });
237
+ }
238
+ sendSSE("done", { sessionId });
239
+ } catch (err) {
240
+ console.error("[firststep] Stream error:", err);
241
+ sendSSE("error", { code: "STREAM_ERROR", message: err?.message || "Unknown error" });
242
+ }
243
+ res.end();
244
+ return;
245
+ }
246
+ sendJson(res, 404, { error: "Not found" });
247
+ }
248
+ function requestListener(req, res) {
249
+ handleRequest(req, res).catch((err) => {
250
+ console.error("[firststep] Unexpected error:", err);
251
+ if (!res.headersSent) {
252
+ sendJson(res, 500, { error: "Internal server error" });
253
+ }
254
+ });
255
+ }
256
+ const httpServer = createHttpServer(requestListener);
257
+ return {
258
+ start() {
259
+ return new Promise((resolve) => {
260
+ httpServer.listen(port, host, () => {
261
+ console.log(`[firststep] Handler server listening on ${host}:${port}`);
262
+ console.log(`[firststep] Endpoints:`);
263
+ console.log(` GET /health - Health check`);
264
+ console.log(` POST /handshake - Capability exchange`);
265
+ console.log(` POST /message - Handle chat message`);
266
+ console.log(` POST /message/stream - Handle chat message (SSE stream)`);
267
+ resolve();
268
+ });
269
+ });
270
+ },
271
+ stop() {
272
+ return new Promise((resolve, reject) => {
273
+ httpServer.close((err) => {
274
+ if (err) reject(err);
275
+ else resolve();
276
+ });
277
+ });
278
+ },
279
+ getRequestHandler() {
280
+ return requestListener;
281
+ }
282
+ };
283
+ }
284
+ export {
285
+ createServer
286
+ };
287
+ //# sourceMappingURL=server.mjs.map