@firststep-studio/sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ MARKER_TYPES: () => MARKER_TYPES,
23
24
  UCPClient: () => UCPClient,
24
25
  UCPError: () => UCPError,
25
26
  classifyWithUCP: () => classifyWithUCP,
@@ -27,6 +28,8 @@ __export(index_exports, {
27
28
  createRequestSignature: () => createRequestSignature,
28
29
  createUCPClient: () => createUCPClient,
29
30
  isValidToken: () => isValidToken,
31
+ renderMarkers: () => renderMarkers,
32
+ streamMetadata: () => streamMetadata,
30
33
  verifyRequestSignature: () => verifyRequestSignature
31
34
  });
32
35
  module.exports = __toCommonJS(index_exports);
@@ -198,13 +201,97 @@ async function classifyWithUCP(endpoint, messages, options) {
198
201
  return client.classifyChat(messages);
199
202
  }
200
203
 
204
+ // src/renderMarkers.ts
205
+ var MARKER_TYPES = {
206
+ HELPLINE_CARD: "RENDER_HELPLINE_CARD",
207
+ EMERGENCY: "RENDER_EMERGENCY",
208
+ RESOURCE_CARD: "RENDER_RESOURCE_CARD",
209
+ PROVIDER_CARD: "RENDER_PROVIDER_CARD",
210
+ SAFETY_PLAN: "RENDER_SAFETY_PLAN",
211
+ REPORT_CARD: "RENDER_REPORT_CARD"
212
+ };
213
+ function wrapMarker(type, payload) {
214
+ return `[${type}]${JSON.stringify(payload)}[/${type}]`;
215
+ }
216
+ var renderMarkers = {
217
+ /** Build a helpline card carousel marker */
218
+ helplineCard: (payload) => wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),
219
+ /** Build an emergency number alert marker */
220
+ emergency: (payload) => wrapMarker(MARKER_TYPES.EMERGENCY, payload),
221
+ /** Build a resource card carousel marker */
222
+ resourceCard: (payload) => wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),
223
+ /** Build a provider card carousel marker */
224
+ providerCard: (payload) => wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),
225
+ /** Build a safety plan artifact marker */
226
+ safetyPlan: (payload) => wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),
227
+ /** Build a report draft card marker */
228
+ reportCard: (payload) => wrapMarker(MARKER_TYPES.REPORT_CARD, payload)
229
+ };
230
+
231
+ // src/streamMetadata.ts
232
+ var streamMetadata = {
233
+ /**
234
+ * Declare the form schema (agents + questions) for Dashboard Form Insights.
235
+ *
236
+ * Yield this once during session initialization (welcome message).
237
+ * The Studio proxy stores it in `chatbot.externalSchema` so that
238
+ * Dashboard Form Insights can display question-level analytics.
239
+ *
240
+ * @param schema - Agents and questions your handler uses
241
+ * @returns A metadata stream chunk ready to yield or queue.push
242
+ */
243
+ declareSchema(schema) {
244
+ return { type: "metadata", content: { schema } };
245
+ },
246
+ /**
247
+ * Send collected form field values for Dashboard persistence.
248
+ *
249
+ * Call this after each turn when new fields are captured.
250
+ * Values are incrementally merged into `ChatSession.formData`.
251
+ *
252
+ * @param fields - Key-value pairs of field IDs to their collected values
253
+ * @returns A metadata stream chunk ready to yield or queue.push
254
+ */
255
+ formDataUpdate(fields) {
256
+ return { type: "metadata", content: { formData: fields } };
257
+ },
258
+ /**
259
+ * Signal an agent/stage transition for routing logs.
260
+ *
261
+ * The Studio proxy records this in `ChatSession.routingLogs` so
262
+ * the Dashboard can show the conversation's agent flow.
263
+ *
264
+ * @param agent - The agent/stage being transitioned to
265
+ * @returns A metadata stream chunk ready to yield or queue.push
266
+ */
267
+ agentTransition(agent) {
268
+ return { type: "metadata", content: { currentAgent: agent } };
269
+ },
270
+ /**
271
+ * Send a routing/classification result for routing logs.
272
+ *
273
+ * The Studio proxy records this in `ChatSession.routingLogs` as a
274
+ * classification event with category, level, and confidence score.
275
+ *
276
+ * @param result - Classification/routing decision details
277
+ * @returns A metadata stream chunk ready to yield or queue.push
278
+ */
279
+ routingResult(result) {
280
+ return { type: "metadata", content: { routing: result } };
281
+ }
282
+ };
283
+
201
284
  // src/auth.ts
202
285
  var import_crypto = require("crypto");
203
286
  var TOKEN_PREFIX = "fst_";
204
287
  var TOKEN_LENGTH = 44;
288
+ function deriveSigningKey(token) {
289
+ return (0, import_crypto.createHash)("sha256").update(token).digest("hex");
290
+ }
205
291
  function verifyRequestSignature(token, payload, signature) {
206
292
  try {
207
- const expected = (0, import_crypto.createHmac)("sha256", token).update(payload).digest("hex");
293
+ const signingKey = deriveSigningKey(token);
294
+ const expected = (0, import_crypto.createHmac)("sha256", signingKey).update(payload).digest("hex");
208
295
  const expectedBuffer = Buffer.from(expected, "hex");
209
296
  const signatureBuffer = Buffer.from(signature, "hex");
210
297
  if (expectedBuffer.length !== signatureBuffer.length) {
@@ -216,7 +303,8 @@ function verifyRequestSignature(token, payload, signature) {
216
303
  }
217
304
  }
218
305
  function createRequestSignature(token, payload) {
219
- return (0, import_crypto.createHmac)("sha256", token).update(payload).digest("hex");
306
+ const signingKey = deriveSigningKey(token);
307
+ return (0, import_crypto.createHmac)("sha256", signingKey).update(payload).digest("hex");
220
308
  }
221
309
  function createAuthHeader(token) {
222
310
  return `Bearer ${token}`;
@@ -226,6 +314,7 @@ function isValidToken(token) {
226
314
  }
227
315
  // Annotate the CommonJS export names for ESM import in node:
228
316
  0 && (module.exports = {
317
+ MARKER_TYPES,
229
318
  UCPClient,
230
319
  UCPError,
231
320
  classifyWithUCP,
@@ -233,6 +322,8 @@ function isValidToken(token) {
233
322
  createRequestSignature,
234
323
  createUCPClient,
235
324
  isValidToken,
325
+ renderMarkers,
326
+ streamMetadata,
236
327
  verifyRequestSignature
237
328
  });
238
329
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/ucp/client.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK\n *\n * SDK for building protocol handlers that integrate with FirstStep Studio.\n *\n * @example\n * ```typescript\n * import { ProtocolHandler, ProtocolRequest, ProtocolResponse, ProtocolContext } from '@firststep-studio/sdk';\n *\n * class MyHandler implements ProtocolHandler {\n * async handleMessage(request: ProtocolRequest, context: ProtocolContext): Promise<ProtocolResponse> {\n * // Your implementation\n * }\n *\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * }\n * }\n * ```\n */\n\n// Core types\nexport type {\n ProtocolRequest,\n ProtocolResponse,\n SessionStatus,\n} from './types';\n\n// Form types\nexport type {\n ProtocolForm,\n ProtocolFormField,\n ProtocolFormOption,\n ProtocolFieldValidation,\n} from './types';\n\n// Streaming types\nexport type {\n ProtocolStreamChunk,\n ProtocolError,\n} from './types';\n\n// Handler interface\nexport type {\n ProtocolHandler,\n ProtocolCapabilities,\n HandlerInfo,\n} from './types';\n\n// Context types\nexport type {\n ProtocolContext,\n SessionContext,\n SessionState,\n ChatMessage,\n KnowledgeContext,\n KnowledgeResult,\n IntegrationContext,\n IntegrationResult,\n HelplineSearchOptions,\n HelplineResult,\n Helpline,\n LoggerContext,\n RoutingDecision,\n DeploymentInfo,\n ChatbotInfo,\n ClassifierConfig,\n} from './types';\n\n// ============================================\n// Platform Standard Data Types\n// ============================================\n\nexport type {\n // Form Data\n FormData,\n FormFieldValue,\n\n // Routing Logs\n RoutingLog,\n\n // Session Metadata\n SessionMetadata,\n\n // Analytics\n AnalyticsContext,\n InteractionEvent,\n InteractionEventType,\n\n // Form Schema (Protocol Registration)\n FormSchema,\n FormFieldDefinition,\n FormFieldType,\n\n // Protocol Registration\n ProtocolRegistration,\n} from './types';\n\n// ============================================\n// UCP (Universal Classification Protocol)\n// ============================================\n\n// UCP Types\nexport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './ucp';\n\n// UCP Client\nexport {\n UCPClient,\n UCPError,\n createUCPClient,\n classifyWithUCP,\n} from './ucp';\n\n// ============================================\n// Auth Utilities\n// ============================================\n\nexport {\n verifyRequestSignature,\n createRequestSignature,\n createAuthHeader,\n isValidToken,\n} from './auth';\n","/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\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":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK\n *\n * SDK for building protocol handlers that integrate with FirstStep Studio.\n *\n * @example\n * ```typescript\n * import { ProtocolHandler, ProtocolRequest, ProtocolResponse, ProtocolContext } from '@firststep-studio/sdk';\n *\n * class MyHandler implements ProtocolHandler {\n * async handleMessage(request: ProtocolRequest, context: ProtocolContext): Promise<ProtocolResponse> {\n * // Your implementation\n * }\n *\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * }\n * }\n * ```\n */\n\n// Core types\nexport type {\n ProtocolRequest,\n ProtocolResponse,\n SessionStatus,\n} from './types';\n\n// Form types\nexport type {\n ProtocolForm,\n ProtocolFormField,\n ProtocolFormOption,\n ProtocolFieldValidation,\n} from './types';\n\n// Streaming types\nexport type {\n ProtocolStreamChunk,\n ProtocolError,\n} from './types';\n\n// Handler interface\nexport type {\n ProtocolHandler,\n ProtocolCapabilities,\n HandlerInfo,\n} from './types';\n\n// Context types\nexport type {\n ProtocolContext,\n SessionContext,\n SessionState,\n ChatMessage,\n KnowledgeContext,\n KnowledgeResult,\n IntegrationContext,\n IntegrationResult,\n HelplineSearchOptions,\n HelplineResult,\n Helpline,\n LoggerContext,\n RoutingDecision,\n DeploymentInfo,\n ChatbotInfo,\n ClassifierConfig,\n} from './types';\n\n// ============================================\n// Platform Standard Data Types\n// ============================================\n\nexport type {\n // Form Data\n FormData,\n FormFieldValue,\n\n // Routing Logs\n RoutingLog,\n\n // Session Metadata\n SessionMetadata,\n\n // Analytics\n AnalyticsContext,\n InteractionEvent,\n InteractionEventType,\n\n // Form Schema (Protocol Registration)\n FormSchema,\n FormFieldDefinition,\n FormFieldType,\n\n // Protocol Registration\n ProtocolRegistration,\n} from './types';\n\n// ============================================\n// UCP (Universal Classification Protocol)\n// ============================================\n\n// UCP Types\nexport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './ucp';\n\n// UCP Client\nexport {\n UCPClient,\n UCPError,\n createUCPClient,\n classifyWithUCP,\n} from './ucp';\n\n// ============================================\n// Render Markers (Rich Content)\n// ============================================\n\nexport {\n renderMarkers,\n MARKER_TYPES,\n} from './renderMarkers';\n\nexport type {\n MarkerType,\n HelplineCardPayload,\n HelplineCardItem,\n EmergencyPayload,\n ResourceCardPayload,\n ResourceCardItem,\n ProviderCardPayload,\n ProviderCardItem,\n SafetyPlanPayload,\n SafetyPlanSection,\n ReportCardPayload,\n} from './renderMarkers';\n\n// ============================================\n// Stream Metadata (Dashboard Persistence)\n// ============================================\n\nexport {\n streamMetadata,\n} from './streamMetadata';\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n} from './streamMetadata';\n\n// ============================================\n// Auth Utilities\n// ============================================\n\nexport {\n verifyRequestSignature,\n createRequestSignature,\n createAuthHeader,\n isValidToken,\n} from './auth';\n","/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n} from './types';\n\n// ============================================\n// Builder Functions\n// ============================================\n\nexport const streamMetadata = {\n /**\n * Declare the form schema (agents + questions) for Dashboard Form Insights.\n *\n * Yield this once during session initialization (welcome message).\n * The Studio proxy stores it in `chatbot.externalSchema` so that\n * Dashboard Form Insights can display question-level analytics.\n *\n * @param schema - Agents and questions your handler uses\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n declareSchema(schema: SchemaDeclarationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { schema } };\n },\n\n /**\n * Send collected form field values for Dashboard persistence.\n *\n * Call this after each turn when new fields are captured.\n * Values are incrementally merged into `ChatSession.formData`.\n *\n * @param fields - Key-value pairs of field IDs to their collected values\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n formDataUpdate(fields: Record<string, string | number>): ProtocolStreamChunk {\n return { type: 'metadata', content: { formData: fields } };\n },\n\n /**\n * Signal an agent/stage transition for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` so\n * the Dashboard can show the conversation's agent flow.\n *\n * @param agent - The agent/stage being transitioned to\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n agentTransition(agent: AgentTransitionPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { currentAgent: agent } };\n },\n\n /**\n * Send a routing/classification result for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` as a\n * classification event with category, level, and confidence score.\n *\n * @param result - Classification/routing decision details\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n routingResult(result: RoutingClassificationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { routing: result } };\n },\n};\n\n// ============================================\n// Re-export payload types for convenience\n// ============================================\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\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":[]}
package/dist/index.mjs CHANGED
@@ -165,13 +165,97 @@ async function classifyWithUCP(endpoint, messages, options) {
165
165
  return client.classifyChat(messages);
166
166
  }
167
167
 
168
+ // src/renderMarkers.ts
169
+ var MARKER_TYPES = {
170
+ HELPLINE_CARD: "RENDER_HELPLINE_CARD",
171
+ EMERGENCY: "RENDER_EMERGENCY",
172
+ RESOURCE_CARD: "RENDER_RESOURCE_CARD",
173
+ PROVIDER_CARD: "RENDER_PROVIDER_CARD",
174
+ SAFETY_PLAN: "RENDER_SAFETY_PLAN",
175
+ REPORT_CARD: "RENDER_REPORT_CARD"
176
+ };
177
+ function wrapMarker(type, payload) {
178
+ return `[${type}]${JSON.stringify(payload)}[/${type}]`;
179
+ }
180
+ var renderMarkers = {
181
+ /** Build a helpline card carousel marker */
182
+ helplineCard: (payload) => wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),
183
+ /** Build an emergency number alert marker */
184
+ emergency: (payload) => wrapMarker(MARKER_TYPES.EMERGENCY, payload),
185
+ /** Build a resource card carousel marker */
186
+ resourceCard: (payload) => wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),
187
+ /** Build a provider card carousel marker */
188
+ providerCard: (payload) => wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),
189
+ /** Build a safety plan artifact marker */
190
+ safetyPlan: (payload) => wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),
191
+ /** Build a report draft card marker */
192
+ reportCard: (payload) => wrapMarker(MARKER_TYPES.REPORT_CARD, payload)
193
+ };
194
+
195
+ // src/streamMetadata.ts
196
+ var streamMetadata = {
197
+ /**
198
+ * Declare the form schema (agents + questions) for Dashboard Form Insights.
199
+ *
200
+ * Yield this once during session initialization (welcome message).
201
+ * The Studio proxy stores it in `chatbot.externalSchema` so that
202
+ * Dashboard Form Insights can display question-level analytics.
203
+ *
204
+ * @param schema - Agents and questions your handler uses
205
+ * @returns A metadata stream chunk ready to yield or queue.push
206
+ */
207
+ declareSchema(schema) {
208
+ return { type: "metadata", content: { schema } };
209
+ },
210
+ /**
211
+ * Send collected form field values for Dashboard persistence.
212
+ *
213
+ * Call this after each turn when new fields are captured.
214
+ * Values are incrementally merged into `ChatSession.formData`.
215
+ *
216
+ * @param fields - Key-value pairs of field IDs to their collected values
217
+ * @returns A metadata stream chunk ready to yield or queue.push
218
+ */
219
+ formDataUpdate(fields) {
220
+ return { type: "metadata", content: { formData: fields } };
221
+ },
222
+ /**
223
+ * Signal an agent/stage transition for routing logs.
224
+ *
225
+ * The Studio proxy records this in `ChatSession.routingLogs` so
226
+ * the Dashboard can show the conversation's agent flow.
227
+ *
228
+ * @param agent - The agent/stage being transitioned to
229
+ * @returns A metadata stream chunk ready to yield or queue.push
230
+ */
231
+ agentTransition(agent) {
232
+ return { type: "metadata", content: { currentAgent: agent } };
233
+ },
234
+ /**
235
+ * Send a routing/classification result for routing logs.
236
+ *
237
+ * The Studio proxy records this in `ChatSession.routingLogs` as a
238
+ * classification event with category, level, and confidence score.
239
+ *
240
+ * @param result - Classification/routing decision details
241
+ * @returns A metadata stream chunk ready to yield or queue.push
242
+ */
243
+ routingResult(result) {
244
+ return { type: "metadata", content: { routing: result } };
245
+ }
246
+ };
247
+
168
248
  // src/auth.ts
169
- import { createHmac, timingSafeEqual } from "crypto";
249
+ import { createHash, createHmac, timingSafeEqual } from "crypto";
170
250
  var TOKEN_PREFIX = "fst_";
171
251
  var TOKEN_LENGTH = 44;
252
+ function deriveSigningKey(token) {
253
+ return createHash("sha256").update(token).digest("hex");
254
+ }
172
255
  function verifyRequestSignature(token, payload, signature) {
173
256
  try {
174
- const expected = createHmac("sha256", token).update(payload).digest("hex");
257
+ const signingKey = deriveSigningKey(token);
258
+ const expected = createHmac("sha256", signingKey).update(payload).digest("hex");
175
259
  const expectedBuffer = Buffer.from(expected, "hex");
176
260
  const signatureBuffer = Buffer.from(signature, "hex");
177
261
  if (expectedBuffer.length !== signatureBuffer.length) {
@@ -183,7 +267,8 @@ function verifyRequestSignature(token, payload, signature) {
183
267
  }
184
268
  }
185
269
  function createRequestSignature(token, payload) {
186
- return createHmac("sha256", token).update(payload).digest("hex");
270
+ const signingKey = deriveSigningKey(token);
271
+ return createHmac("sha256", signingKey).update(payload).digest("hex");
187
272
  }
188
273
  function createAuthHeader(token) {
189
274
  return `Bearer ${token}`;
@@ -192,6 +277,7 @@ function isValidToken(token) {
192
277
  return typeof token === "string" && token.startsWith(TOKEN_PREFIX) && token.length === TOKEN_LENGTH;
193
278
  }
194
279
  export {
280
+ MARKER_TYPES,
195
281
  UCPClient,
196
282
  UCPError,
197
283
  classifyWithUCP,
@@ -199,6 +285,8 @@ export {
199
285
  createRequestSignature,
200
286
  createUCPClient,
201
287
  isValidToken,
288
+ renderMarkers,
289
+ streamMetadata,
202
290
  verifyRequestSignature
203
291
  };
204
292
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ucp/client.ts","../src/auth.ts"],"sourcesContent":["/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\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":[]}
1
+ {"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/auth.ts"],"sourcesContent":["/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n} from './types';\n\n// ============================================\n// Builder Functions\n// ============================================\n\nexport const streamMetadata = {\n /**\n * Declare the form schema (agents + questions) for Dashboard Form Insights.\n *\n * Yield this once during session initialization (welcome message).\n * The Studio proxy stores it in `chatbot.externalSchema` so that\n * Dashboard Form Insights can display question-level analytics.\n *\n * @param schema - Agents and questions your handler uses\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n declareSchema(schema: SchemaDeclarationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { schema } };\n },\n\n /**\n * Send collected form field values for Dashboard persistence.\n *\n * Call this after each turn when new fields are captured.\n * Values are incrementally merged into `ChatSession.formData`.\n *\n * @param fields - Key-value pairs of field IDs to their collected values\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n formDataUpdate(fields: Record<string, string | number>): ProtocolStreamChunk {\n return { type: 'metadata', content: { formData: fields } };\n },\n\n /**\n * Signal an agent/stage transition for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` so\n * the Dashboard can show the conversation's agent flow.\n *\n * @param agent - The agent/stage being transitioned to\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n agentTransition(agent: AgentTransitionPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { currentAgent: agent } };\n },\n\n /**\n * Send a routing/classification result for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` as a\n * classification event with category, level, and confidence score.\n *\n * @param result - Classification/routing decision details\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n routingResult(result: RoutingClassificationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { routing: result } };\n },\n};\n\n// ============================================\n// Re-export payload types for convenience\n// ============================================\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\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":[]}
package/dist/server.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from 'http';
2
- import { h as ProtocolHandler } from './types-Bm98aHcd.mjs';
2
+ import { i as ProtocolHandler } from './types-B71xClvf.mjs';
3
3
 
4
4
  /**
5
5
  * FirstStep SDK Server
@@ -44,6 +44,11 @@ interface ServerConfig {
44
44
  host?: string;
45
45
  /** Skip signature verification (for local development only). */
46
46
  skipSignatureVerification?: boolean;
47
+ /**
48
+ * URL prefix for all endpoints.
49
+ * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.
50
+ */
51
+ prefix?: string;
47
52
  }
48
53
  interface FirstStepServer {
49
54
  /** Start the server */
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from 'http';
2
- import { h as ProtocolHandler } from './types-Bm98aHcd.js';
2
+ import { i as ProtocolHandler } from './types-B71xClvf.js';
3
3
 
4
4
  /**
5
5
  * FirstStep SDK Server
@@ -44,6 +44,11 @@ interface ServerConfig {
44
44
  host?: string;
45
45
  /** Skip signature verification (for local development only). */
46
46
  skipSignatureVerification?: boolean;
47
+ /**
48
+ * URL prefix for all endpoints.
49
+ * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.
50
+ */
51
+ prefix?: string;
47
52
  }
48
53
  interface FirstStepServer {
49
54
  /** Start the server */
package/dist/server.js CHANGED
@@ -27,9 +27,13 @@ var import_http = require("http");
27
27
 
28
28
  // src/auth.ts
29
29
  var import_crypto = require("crypto");
30
+ function deriveSigningKey(token) {
31
+ return (0, import_crypto.createHash)("sha256").update(token).digest("hex");
32
+ }
30
33
  function verifyRequestSignature(token, payload, signature) {
31
34
  try {
32
- const expected = (0, import_crypto.createHmac)("sha256", token).update(payload).digest("hex");
35
+ const signingKey = deriveSigningKey(token);
36
+ const expected = (0, import_crypto.createHmac)("sha256", signingKey).update(payload).digest("hex");
33
37
  const expectedBuffer = Buffer.from(expected, "hex");
34
38
  const signatureBuffer = Buffer.from(signature, "hex");
35
39
  if (expectedBuffer.length !== signatureBuffer.length) {
@@ -120,8 +124,10 @@ function createServer(handler, config) {
120
124
  token,
121
125
  port = parseInt(process.env.PORT || "3001", 10),
122
126
  host = "0.0.0.0",
123
- skipSignatureVerification = false
127
+ skipSignatureVerification = false,
128
+ prefix: rawPrefix = ""
124
129
  } = config;
130
+ const prefix = rawPrefix ? "/" + rawPrefix.replace(/^\/|\/$/g, "") : "";
125
131
  if (!token && !skipSignatureVerification) {
126
132
  throw new Error(
127
133
  "FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev."
@@ -140,15 +146,16 @@ function createServer(handler, config) {
140
146
  res.end();
141
147
  return;
142
148
  }
143
- if (path === "/health" && method === "GET") {
149
+ const route = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
150
+ if (route === "/health" && method === "GET") {
144
151
  sendJson(res, 200, { status: "ok", timestamp: Date.now() });
145
152
  return;
146
153
  }
147
- if (path === "/handshake" && method === "POST") {
154
+ if (route === "/handshake" && method === "POST") {
148
155
  const body = await readBody(req);
149
156
  if (!skipSignatureVerification) {
150
157
  const signature = req.headers["x-firststep-signature"];
151
- if (!signature || !verifyRequestSignature(token, body, signature)) {
158
+ if (signature && !verifyRequestSignature(token, body, signature)) {
152
159
  sendJson(res, 401, { error: "Invalid signature" });
153
160
  return;
154
161
  }
@@ -158,11 +165,11 @@ function createServer(handler, config) {
158
165
  sendJson(res, 200, { capabilities, handler: handlerInfo || void 0 });
159
166
  return;
160
167
  }
161
- if (path === "/message" && method === "POST") {
168
+ if (route === "/message" && method === "POST") {
162
169
  const body = await readBody(req);
163
170
  if (!skipSignatureVerification) {
164
171
  const signature = req.headers["x-firststep-signature"];
165
- if (!signature || !verifyRequestSignature(token, body, signature)) {
172
+ if (signature && !verifyRequestSignature(token, body, signature)) {
166
173
  sendJson(res, 401, { error: "Invalid signature" });
167
174
  return;
168
175
  }
@@ -187,11 +194,11 @@ function createServer(handler, config) {
187
194
  }
188
195
  return;
189
196
  }
190
- if (path === "/message/stream" && method === "POST") {
197
+ if (route === "/message/stream" && method === "POST") {
191
198
  const body = await readBody(req);
192
199
  if (!skipSignatureVerification) {
193
200
  const signature = req.headers["x-firststep-signature"];
194
- if (!signature || !verifyRequestSignature(token, body, signature)) {
201
+ if (signature && !verifyRequestSignature(token, body, signature)) {
195
202
  sendJson(res, 401, { error: "Invalid signature" });
196
203
  return;
197
204
  }
@@ -284,10 +291,10 @@ data: ${JSON.stringify(data)}
284
291
  httpServer.listen(port, host, () => {
285
292
  console.log(`[firststep] Handler server listening on ${host}:${port}`);
286
293
  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)`);
294
+ console.log(` GET ${prefix}/health - Health check`);
295
+ console.log(` POST ${prefix}/handshake - Capability exchange`);
296
+ console.log(` POST ${prefix}/message - Handle chat message`);
297
+ console.log(` POST ${prefix}/message/stream - Handle chat message (SSE stream)`);
291
298
  resolve();
292
299
  });
293
300
  });