@firststep-studio/sdk 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +122 -2
- package/dist/index.d.ts +122 -2
- package/dist/index.js +47 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +47 -12
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +55 -0
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +55 -0
- package/dist/server.mjs.map +1 -1
- package/dist/{types-fhi9K2il.d.mts → types-GoTI_c14.d.mts} +117 -1
- package/dist/{types-fhi9K2il.d.ts → types-GoTI_c14.d.ts} +117 -1
- package/package.json +1 -1
package/dist/server.mjs.map
CHANGED
|
@@ -1 +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 * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\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 prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\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 // Strip prefix to get the remaining path\n const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Extract optional config slug from path: /{slug}/message -> slug = \"slug\", route = \"/message\"\n // If no slug segment, route is the full remaining path (backward compatible)\n let configSlug: string | undefined;\n let route: string;\n const slugMatch = afterPrefix.match(/^\\/([^/]+)(\\/(?:health|handshake|message(?:\\/stream)?))?$/);\n if (slugMatch && slugMatch[2]) {\n // Path has a slug segment: /{slug}/{endpoint}\n configSlug = slugMatch[1];\n route = slugMatch[2];\n } else {\n // No slug: /{endpoint} (backward compatible)\n route = afterPrefix;\n }\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/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 (route === '/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 // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\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 (route === '/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 // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\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 ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/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 { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\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 * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\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 signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\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 const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).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"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/resources/storageClient.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 StorageContext,\n} from './types';\nimport { StorageClient } from './resources/storageClient.js';\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 * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\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?: any): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n // Detect _resources injected by Studio proxy for HTTP-backed resource clients\n const resources = data?._resources as { baseUrl: string; token: string; capabilities?: Record<string, boolean> } | undefined;\n let storage: StorageContext | undefined;\n\n if (resources?.baseUrl && resources?.token && resources.capabilities?.storage !== false) {\n storage = new StorageClient(resources.baseUrl, resources.token);\n }\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 storage,\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 prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\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 // Strip prefix to get the remaining path\n const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Extract optional config slug from path: /{slug}/message -> slug = \"slug\", route = \"/message\"\n // If no slug segment, route is the full remaining path (backward compatible)\n let configSlug: string | undefined;\n let route: string;\n const slugMatch = afterPrefix.match(/^\\/([^/]+)(\\/(?:health|handshake|message(?:\\/stream)?))?$/);\n if (slugMatch && slugMatch[2]) {\n // Path has a slug segment: /{slug}/{endpoint}\n configSlug = slugMatch[1];\n route = slugMatch[2];\n } else {\n // No slug: /{endpoint} (backward compatible)\n route = afterPrefix;\n }\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/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 (route === '/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 // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\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 (route === '/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 // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\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 ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/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 type { StorageContext, StorageSetOptions } from '../types.js';\n\n/**\n * HTTP-backed implementation of StorageContext.\n * All operations call back to Studio's protocol-resources API.\n * Data is scoped to the deployment via the JWT token.\n */\nexport class StorageClient implements StorageContext {\n private baseUrl: string;\n private token: string;\n\n constructor(baseUrl: string, token: string) {\n this.baseUrl = baseUrl;\n this.token = token;\n }\n\n async get<T = unknown>(key: string): Promise<T | null> {\n const res = await this._post('/storage/get', { key });\n if (!res.success) return null;\n return (res.value ?? null) as T;\n }\n\n async set(key: string, value: unknown, options?: StorageSetOptions): Promise<void> {\n await this._post('/storage/set', { key, value, ttl: options?.ttl });\n }\n\n async delete(key: string): Promise<void> {\n await this._post('/storage/delete', { key });\n }\n\n async has(key: string): Promise<boolean> {\n const res = await this._post('/storage/has', { key });\n return res.exists === true;\n }\n\n async keys(prefix?: string): Promise<string[]> {\n const res = await this._post('/storage/keys', { prefix });\n return res.keys ?? [];\n }\n\n async merge(key: string, partial: Record<string, unknown>): Promise<void> {\n await this._post('/storage/merge', { key, partial });\n }\n\n async append(key: string, item: unknown): Promise<void> {\n await this._post('/storage/append', { key, item });\n }\n\n private async _post(path: string, body: unknown): Promise<any> {\n const res = await fetch(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.token}`,\n },\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n console.error(`[StorageClient] ${path} failed: ${res.status} ${text}`);\n return { success: false, error: `HTTP ${res.status}` };\n }\n return res.json();\n }\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\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 * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\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 signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\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 const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).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"]}
|
|
@@ -154,6 +154,8 @@ interface ProtocolContext {
|
|
|
154
154
|
knowledge: KnowledgeContext;
|
|
155
155
|
/** Integration access */
|
|
156
156
|
integrations: IntegrationContext;
|
|
157
|
+
/** Handler document store (backed by Studio MongoDB) */
|
|
158
|
+
storage?: StorageContext;
|
|
157
159
|
/** Analytics tracking */
|
|
158
160
|
analytics: AnalyticsContext;
|
|
159
161
|
/** Logging */
|
|
@@ -313,9 +315,28 @@ interface SchemaQuestion {
|
|
|
313
315
|
negative: string;
|
|
314
316
|
};
|
|
315
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Optional context about the chatbot for persona generation and testing.
|
|
320
|
+
* Handlers can provide as much or as little as they have available.
|
|
321
|
+
*/
|
|
322
|
+
interface SchemaBrief {
|
|
323
|
+
/** What the chatbot does */
|
|
324
|
+
description?: string;
|
|
325
|
+
/** Target audience description (e.g. "Teens and young adults aged 13-25") */
|
|
326
|
+
audience?: string;
|
|
327
|
+
/** Chatbot goals/objectives */
|
|
328
|
+
goals?: string[];
|
|
329
|
+
/** Stage/agent objectives for richer persona context */
|
|
330
|
+
stageDescriptions?: Array<{
|
|
331
|
+
id: string;
|
|
332
|
+
objective: string;
|
|
333
|
+
}>;
|
|
334
|
+
}
|
|
316
335
|
interface SchemaDeclarationPayload {
|
|
317
336
|
agents: SchemaAgent[];
|
|
318
337
|
questions: SchemaQuestion[];
|
|
338
|
+
/** Optional context for virtual persona testing and simulation */
|
|
339
|
+
brief?: SchemaBrief;
|
|
319
340
|
}
|
|
320
341
|
/**
|
|
321
342
|
* Agent transition payload - sent when the conversation moves to a new stage.
|
|
@@ -346,12 +367,75 @@ interface KnowledgeContext {
|
|
|
346
367
|
queryDatabase(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
347
368
|
/** Search page knowledge */
|
|
348
369
|
searchPages(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
370
|
+
/**
|
|
371
|
+
* Fetch knowledge metadata (name, type, column shape) without records.
|
|
372
|
+
*
|
|
373
|
+
* Used by handlers when generating system prompts: the LLM needs to know
|
|
374
|
+
* a knowledge base's columns/types to write valid queries against it,
|
|
375
|
+
* but loading the actual records would be wasteful (and big). Platform
|
|
376
|
+
* implementations of this method return a pre-resolved projection — the
|
|
377
|
+
* "main sheet" columns for database knowledge, just `name`+`type` for
|
|
378
|
+
* text knowledge — so handlers don't have to know about sheet selection
|
|
379
|
+
* or storage internals.
|
|
380
|
+
*
|
|
381
|
+
* Returns null when the knowledge id is not found.
|
|
382
|
+
*/
|
|
383
|
+
getMetadata?(knowledgeId: string): Promise<KnowledgeMetadata | null>;
|
|
349
384
|
}
|
|
350
385
|
interface KnowledgeResult {
|
|
351
386
|
success: boolean;
|
|
352
387
|
records?: Record<string, unknown>[];
|
|
353
388
|
error?: string;
|
|
354
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Knowledge metadata projection returned by KnowledgeContext.getMetadata.
|
|
392
|
+
*
|
|
393
|
+
* Intentionally minimal: only the fields a handler needs to describe the
|
|
394
|
+
* knowledge to an LLM (or branch on type). Storage shape, sheet selection,
|
|
395
|
+
* record contents, etc. stay platform-internal.
|
|
396
|
+
*/
|
|
397
|
+
interface KnowledgeMetadata {
|
|
398
|
+
/** Knowledge id (echoed back so callers can correlate). */
|
|
399
|
+
id: string;
|
|
400
|
+
/** Display name. */
|
|
401
|
+
name: string;
|
|
402
|
+
/**
|
|
403
|
+
* Knowledge kind. `text` = unstructured pages searched semantically;
|
|
404
|
+
* `database` = tabular rows queried with structured filters. Other
|
|
405
|
+
* platform-specific kinds may be added; handlers should treat unknown
|
|
406
|
+
* values as opaque.
|
|
407
|
+
*/
|
|
408
|
+
type: 'text' | 'database' | string;
|
|
409
|
+
/**
|
|
410
|
+
* For database knowledge: the canonical (main-sheet) column shape that
|
|
411
|
+
* the LLM should be told about. Empty/undefined for non-database kinds.
|
|
412
|
+
*/
|
|
413
|
+
columns?: KnowledgeColumnMetadata[];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Column metadata as exposed to handlers. Platform implementations
|
|
417
|
+
* project from their internal column model down to this shape — the LLM-
|
|
418
|
+
* facing prompt-builder only needs name, type, and any descriptive hints.
|
|
419
|
+
*/
|
|
420
|
+
interface KnowledgeColumnMetadata {
|
|
421
|
+
id: string;
|
|
422
|
+
name: string;
|
|
423
|
+
/** Column data type (e.g. 'text', 'number', 'url', 'email', 'date'). */
|
|
424
|
+
type: string;
|
|
425
|
+
description?: string;
|
|
426
|
+
/**
|
|
427
|
+
* Bag of analysis hints the platform may have produced (unique value
|
|
428
|
+
* counts, sample values, ranges). Optional and platform-defined; the
|
|
429
|
+
* SDK reserves the keys it documents and tolerates extras.
|
|
430
|
+
*/
|
|
431
|
+
metadata?: {
|
|
432
|
+
description?: string;
|
|
433
|
+
analysis?: {
|
|
434
|
+
uniqueCount?: number;
|
|
435
|
+
uniqueValues?: string[];
|
|
436
|
+
};
|
|
437
|
+
} & Record<string, unknown>;
|
|
438
|
+
}
|
|
355
439
|
interface IntegrationContext {
|
|
356
440
|
/** Execute integration */
|
|
357
441
|
execute(integrationId: string, params: Record<string, unknown>): Promise<IntegrationResult>;
|
|
@@ -379,6 +463,38 @@ interface Helpline {
|
|
|
379
463
|
website?: string;
|
|
380
464
|
description?: string;
|
|
381
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Handler document store backed by Studio MongoDB.
|
|
468
|
+
* Opaque to Studio: stores data but never reads or interprets it.
|
|
469
|
+
* Key naming, data structure, and lifecycle are the handler's responsibility.
|
|
470
|
+
* All operations are scoped to the deployment (isolated by JWT token).
|
|
471
|
+
*/
|
|
472
|
+
interface StorageContext {
|
|
473
|
+
/** Get a value by key. Returns null if not found. */
|
|
474
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
475
|
+
/** Set a value by key. Overwrites if exists. */
|
|
476
|
+
set(key: string, value: unknown, options?: StorageSetOptions): Promise<void>;
|
|
477
|
+
/** Delete a key. */
|
|
478
|
+
delete(key: string): Promise<void>;
|
|
479
|
+
/** Check if key exists. */
|
|
480
|
+
has(key: string): Promise<boolean>;
|
|
481
|
+
/** List keys matching a prefix. Returns all keys if no prefix given. */
|
|
482
|
+
keys(prefix?: string): Promise<string[]>;
|
|
483
|
+
/**
|
|
484
|
+
* Shallow merge a partial object into an existing value.
|
|
485
|
+
* Creates the key if it doesn't exist.
|
|
486
|
+
*/
|
|
487
|
+
merge(key: string, partial: Record<string, unknown>): Promise<void>;
|
|
488
|
+
/**
|
|
489
|
+
* Append an item to an array stored at key.
|
|
490
|
+
* Creates the array if the key doesn't exist.
|
|
491
|
+
*/
|
|
492
|
+
append(key: string, item: unknown): Promise<void>;
|
|
493
|
+
}
|
|
494
|
+
interface StorageSetOptions {
|
|
495
|
+
/** TTL in seconds. Key auto-deletes after expiry. */
|
|
496
|
+
ttl?: number;
|
|
497
|
+
}
|
|
382
498
|
interface LoggerContext {
|
|
383
499
|
debug(message: string, data?: Record<string, unknown>): void;
|
|
384
500
|
info(message: string, data?: Record<string, unknown>): void;
|
|
@@ -598,4 +714,4 @@ interface HandoffInboundContext {
|
|
|
598
714
|
returnResult?: HandoffReturnPayload;
|
|
599
715
|
}
|
|
600
716
|
|
|
601
|
-
export type { AgentTransitionPayload as A,
|
|
717
|
+
export type { HandoffOptions as $, AgentTransitionPayload as A, RoutingDecision as B, ChatMessage as C, DeploymentInfo as D, ChatbotInfo as E, ClassifierConfig as F, FormData as G, HandoffRequestPayload as H, IntegrationContext as I, FormFieldValue as J, KnowledgeContext as K, LoggerContext as L, RoutingLog as M, SessionMetadata as N, AnalyticsContext as O, ProtocolStreamChunk as P, InteractionEvent as Q, RoutingClassificationPayload as R, SchemaDeclarationPayload as S, InteractionEventType as T, FormSchema as U, FormFieldDefinition as V, FormFieldType as W, ProtocolRegistration as X, SchemaAgent as Y, SchemaQuestion as Z, HandoffContext as _, HandoffReturnPayload as a, HandoffOfferPayload as b, ProtocolRequest as c, ProtocolResponse as d, SessionStatus as e, HandoffInboundContext as f, ProtocolForm as g, ProtocolFormField as h, ProtocolFormOption as i, ProtocolFieldValidation as j, ProtocolError as k, ProtocolHandler as l, ProtocolCapabilities as m, HandlerInfo as n, ProtocolContext as o, SessionContext as p, SessionState as q, KnowledgeResult as r, KnowledgeMetadata as s, KnowledgeColumnMetadata as t, IntegrationResult as u, HelplineSearchOptions as v, HelplineResult as w, Helpline as x, StorageContext as y, StorageSetOptions as z };
|
|
@@ -154,6 +154,8 @@ interface ProtocolContext {
|
|
|
154
154
|
knowledge: KnowledgeContext;
|
|
155
155
|
/** Integration access */
|
|
156
156
|
integrations: IntegrationContext;
|
|
157
|
+
/** Handler document store (backed by Studio MongoDB) */
|
|
158
|
+
storage?: StorageContext;
|
|
157
159
|
/** Analytics tracking */
|
|
158
160
|
analytics: AnalyticsContext;
|
|
159
161
|
/** Logging */
|
|
@@ -313,9 +315,28 @@ interface SchemaQuestion {
|
|
|
313
315
|
negative: string;
|
|
314
316
|
};
|
|
315
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Optional context about the chatbot for persona generation and testing.
|
|
320
|
+
* Handlers can provide as much or as little as they have available.
|
|
321
|
+
*/
|
|
322
|
+
interface SchemaBrief {
|
|
323
|
+
/** What the chatbot does */
|
|
324
|
+
description?: string;
|
|
325
|
+
/** Target audience description (e.g. "Teens and young adults aged 13-25") */
|
|
326
|
+
audience?: string;
|
|
327
|
+
/** Chatbot goals/objectives */
|
|
328
|
+
goals?: string[];
|
|
329
|
+
/** Stage/agent objectives for richer persona context */
|
|
330
|
+
stageDescriptions?: Array<{
|
|
331
|
+
id: string;
|
|
332
|
+
objective: string;
|
|
333
|
+
}>;
|
|
334
|
+
}
|
|
316
335
|
interface SchemaDeclarationPayload {
|
|
317
336
|
agents: SchemaAgent[];
|
|
318
337
|
questions: SchemaQuestion[];
|
|
338
|
+
/** Optional context for virtual persona testing and simulation */
|
|
339
|
+
brief?: SchemaBrief;
|
|
319
340
|
}
|
|
320
341
|
/**
|
|
321
342
|
* Agent transition payload - sent when the conversation moves to a new stage.
|
|
@@ -346,12 +367,75 @@ interface KnowledgeContext {
|
|
|
346
367
|
queryDatabase(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
347
368
|
/** Search page knowledge */
|
|
348
369
|
searchPages(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
370
|
+
/**
|
|
371
|
+
* Fetch knowledge metadata (name, type, column shape) without records.
|
|
372
|
+
*
|
|
373
|
+
* Used by handlers when generating system prompts: the LLM needs to know
|
|
374
|
+
* a knowledge base's columns/types to write valid queries against it,
|
|
375
|
+
* but loading the actual records would be wasteful (and big). Platform
|
|
376
|
+
* implementations of this method return a pre-resolved projection — the
|
|
377
|
+
* "main sheet" columns for database knowledge, just `name`+`type` for
|
|
378
|
+
* text knowledge — so handlers don't have to know about sheet selection
|
|
379
|
+
* or storage internals.
|
|
380
|
+
*
|
|
381
|
+
* Returns null when the knowledge id is not found.
|
|
382
|
+
*/
|
|
383
|
+
getMetadata?(knowledgeId: string): Promise<KnowledgeMetadata | null>;
|
|
349
384
|
}
|
|
350
385
|
interface KnowledgeResult {
|
|
351
386
|
success: boolean;
|
|
352
387
|
records?: Record<string, unknown>[];
|
|
353
388
|
error?: string;
|
|
354
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Knowledge metadata projection returned by KnowledgeContext.getMetadata.
|
|
392
|
+
*
|
|
393
|
+
* Intentionally minimal: only the fields a handler needs to describe the
|
|
394
|
+
* knowledge to an LLM (or branch on type). Storage shape, sheet selection,
|
|
395
|
+
* record contents, etc. stay platform-internal.
|
|
396
|
+
*/
|
|
397
|
+
interface KnowledgeMetadata {
|
|
398
|
+
/** Knowledge id (echoed back so callers can correlate). */
|
|
399
|
+
id: string;
|
|
400
|
+
/** Display name. */
|
|
401
|
+
name: string;
|
|
402
|
+
/**
|
|
403
|
+
* Knowledge kind. `text` = unstructured pages searched semantically;
|
|
404
|
+
* `database` = tabular rows queried with structured filters. Other
|
|
405
|
+
* platform-specific kinds may be added; handlers should treat unknown
|
|
406
|
+
* values as opaque.
|
|
407
|
+
*/
|
|
408
|
+
type: 'text' | 'database' | string;
|
|
409
|
+
/**
|
|
410
|
+
* For database knowledge: the canonical (main-sheet) column shape that
|
|
411
|
+
* the LLM should be told about. Empty/undefined for non-database kinds.
|
|
412
|
+
*/
|
|
413
|
+
columns?: KnowledgeColumnMetadata[];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Column metadata as exposed to handlers. Platform implementations
|
|
417
|
+
* project from their internal column model down to this shape — the LLM-
|
|
418
|
+
* facing prompt-builder only needs name, type, and any descriptive hints.
|
|
419
|
+
*/
|
|
420
|
+
interface KnowledgeColumnMetadata {
|
|
421
|
+
id: string;
|
|
422
|
+
name: string;
|
|
423
|
+
/** Column data type (e.g. 'text', 'number', 'url', 'email', 'date'). */
|
|
424
|
+
type: string;
|
|
425
|
+
description?: string;
|
|
426
|
+
/**
|
|
427
|
+
* Bag of analysis hints the platform may have produced (unique value
|
|
428
|
+
* counts, sample values, ranges). Optional and platform-defined; the
|
|
429
|
+
* SDK reserves the keys it documents and tolerates extras.
|
|
430
|
+
*/
|
|
431
|
+
metadata?: {
|
|
432
|
+
description?: string;
|
|
433
|
+
analysis?: {
|
|
434
|
+
uniqueCount?: number;
|
|
435
|
+
uniqueValues?: string[];
|
|
436
|
+
};
|
|
437
|
+
} & Record<string, unknown>;
|
|
438
|
+
}
|
|
355
439
|
interface IntegrationContext {
|
|
356
440
|
/** Execute integration */
|
|
357
441
|
execute(integrationId: string, params: Record<string, unknown>): Promise<IntegrationResult>;
|
|
@@ -379,6 +463,38 @@ interface Helpline {
|
|
|
379
463
|
website?: string;
|
|
380
464
|
description?: string;
|
|
381
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Handler document store backed by Studio MongoDB.
|
|
468
|
+
* Opaque to Studio: stores data but never reads or interprets it.
|
|
469
|
+
* Key naming, data structure, and lifecycle are the handler's responsibility.
|
|
470
|
+
* All operations are scoped to the deployment (isolated by JWT token).
|
|
471
|
+
*/
|
|
472
|
+
interface StorageContext {
|
|
473
|
+
/** Get a value by key. Returns null if not found. */
|
|
474
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
475
|
+
/** Set a value by key. Overwrites if exists. */
|
|
476
|
+
set(key: string, value: unknown, options?: StorageSetOptions): Promise<void>;
|
|
477
|
+
/** Delete a key. */
|
|
478
|
+
delete(key: string): Promise<void>;
|
|
479
|
+
/** Check if key exists. */
|
|
480
|
+
has(key: string): Promise<boolean>;
|
|
481
|
+
/** List keys matching a prefix. Returns all keys if no prefix given. */
|
|
482
|
+
keys(prefix?: string): Promise<string[]>;
|
|
483
|
+
/**
|
|
484
|
+
* Shallow merge a partial object into an existing value.
|
|
485
|
+
* Creates the key if it doesn't exist.
|
|
486
|
+
*/
|
|
487
|
+
merge(key: string, partial: Record<string, unknown>): Promise<void>;
|
|
488
|
+
/**
|
|
489
|
+
* Append an item to an array stored at key.
|
|
490
|
+
* Creates the array if the key doesn't exist.
|
|
491
|
+
*/
|
|
492
|
+
append(key: string, item: unknown): Promise<void>;
|
|
493
|
+
}
|
|
494
|
+
interface StorageSetOptions {
|
|
495
|
+
/** TTL in seconds. Key auto-deletes after expiry. */
|
|
496
|
+
ttl?: number;
|
|
497
|
+
}
|
|
382
498
|
interface LoggerContext {
|
|
383
499
|
debug(message: string, data?: Record<string, unknown>): void;
|
|
384
500
|
info(message: string, data?: Record<string, unknown>): void;
|
|
@@ -598,4 +714,4 @@ interface HandoffInboundContext {
|
|
|
598
714
|
returnResult?: HandoffReturnPayload;
|
|
599
715
|
}
|
|
600
716
|
|
|
601
|
-
export type { AgentTransitionPayload as A,
|
|
717
|
+
export type { HandoffOptions as $, AgentTransitionPayload as A, RoutingDecision as B, ChatMessage as C, DeploymentInfo as D, ChatbotInfo as E, ClassifierConfig as F, FormData as G, HandoffRequestPayload as H, IntegrationContext as I, FormFieldValue as J, KnowledgeContext as K, LoggerContext as L, RoutingLog as M, SessionMetadata as N, AnalyticsContext as O, ProtocolStreamChunk as P, InteractionEvent as Q, RoutingClassificationPayload as R, SchemaDeclarationPayload as S, InteractionEventType as T, FormSchema as U, FormFieldDefinition as V, FormFieldType as W, ProtocolRegistration as X, SchemaAgent as Y, SchemaQuestion as Z, HandoffContext as _, HandoffReturnPayload as a, HandoffOfferPayload as b, ProtocolRequest as c, ProtocolResponse as d, SessionStatus as e, HandoffInboundContext as f, ProtocolForm as g, ProtocolFormField as h, ProtocolFormOption as i, ProtocolFieldValidation as j, ProtocolError as k, ProtocolHandler as l, ProtocolCapabilities as m, HandlerInfo as n, ProtocolContext as o, SessionContext as p, SessionState as q, KnowledgeResult as r, KnowledgeMetadata as s, KnowledgeColumnMetadata as t, IntegrationResult as u, HelplineSearchOptions as v, HelplineResult as w, Helpline as x, StorageContext as y, StorageSetOptions as z };
|