@firststep-studio/sdk 0.1.0 → 0.2.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.d.mts +268 -3
- package/dist/index.d.ts +268 -3
- package/dist/index.js +93 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +91 -3
- 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 +8 -4
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +9 -5
- package/dist/server.mjs.map +1 -1
- package/dist/{types-Bm98aHcd.d.mts → types-B71xClvf.d.mts} +65 -1
- package/dist/{types-Bm98aHcd.d.ts → types-B71xClvf.d.ts} +65 -1
- package/llms.txt +464 -39
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -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":";AAmCA,IAAM,kBAAkB;AAKjB,IAAM,YAAN,MAAM,WAAU;AAAA,EAOrB,YAAY,QAAyB;AAEnC,SAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAC/C,SAAK,eAAe,OAAO;AAC3B,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU,OAAO,WAAW;AACjC,SAAK,UAAU,OAAO,WAAW,CAAC;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,aACL,UACA,SACW;AAEX,UAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,UAAM,YAAY,IAAI,SAAS,MAAM,GAAG;AAGxC,UAAM,mBAAmB,UAAU,QAAQ,aAAa;AACxD,QAAI,qBAAqB,MAAM,oBAAoB,UAAU,SAAS,GAAG;AACvE,YAAM,IAAI;AAAA,QACR,6BAA6B,QAAQ;AAAA,MACvC;AAAA,IACF;AAEA,UAAM,eAAe,UAAU,mBAAmB,CAAC;AACnD,UAAM,cAAc,UAAU,MAAM,GAAG,gBAAgB,EAAE,KAAK,GAAG;AACjE,UAAM,UAAU,GAAG,IAAI,QAAQ,KAAK,IAAI,IAAI,GAAG,WAAW;AAE1D,WAAO,IAAI,WAAU;AAAA,MACnB;AAAA,MACA;AAAA,MACA,QAAQ,SAAS;AAAA,MACjB,SAAS,SAAS;AAAA,IACpB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,cAAsB;AACxB,WAAO,GAAG,KAAK,OAAO,gBAAgB,KAAK,YAAY;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,UAAkB;AACpB,WAAO,GAAG,KAAK,OAAO,gBAAgB,KAAK,YAAY;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,UAAuD;AACpE,UAAM,UAA8B,EAAE,SAAS;AAE/C,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B,KAAK;AAAA,MACL;AAAA,QACE,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU,OAAO;AAAA,MAC9B;AAAA,IACF;AAGA,WAAO;AAAA,MACL,cAAc,KAAK;AAAA,MACnB,UAAU,SAAS;AAAA,MACnB,OAAO,SAAS;AAAA,MAChB,YAAY,SAAS,QAAQ;AAAA;AAAA,MAC7B,WAAW,SAAS;AAAA,MACpB,KAAK;AAAA,IACP;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aAAa,UAAwD;AACzE,UAAM,cAA4B,SAC/B,OAAO,OAAK,EAAE,SAAS,UAAU,EAAE,SAAS,WAAW,EACvD,IAAI,CAAC,GAAG,WAAW;AAAA,MAClB,IAAI,OAAO,KAAK;AAAA,MAChB,MAAM,EAAE;AAAA,MACR,SAAS,EAAE;AAAA,MACX,WAAW,EAAE,YAAY,EAAE,UAAU,QAAQ,IAAI,KAAK,IAAI;AAAA,MAC1D,UAAU,EAAE;AAAA,IACd,EAAE;AAEJ,WAAO,KAAK,SAAS,WAAW;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAoC;AACxC,WAAO,KAAK,MAAuB,KAAK,SAAS;AAAA,MAC/C,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAgC;AACpC,QAAI;AACF,YAAM,YAAY,GAAG,KAAK,OAAO;AACjC,YAAM,WAAW,MAAM,MAAM,WAAW;AAAA,QACtC,QAAQ;AAAA,QACR,QAAQ,YAAY,QAAQ,GAAI;AAAA,MAClC,CAAC;AACD,aAAO,SAAS;AAAA,IAClB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,MAAS,KAAa,SAAkC;AACpE,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,MAChB,GAAG,KAAK;AAAA,IACV;AAEA,QAAI,KAAK,QAAQ;AACf,cAAQ,eAAe,IAAI,UAAU,KAAK,MAAM;AAAA,IAClD;AAEA,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAEnE,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK;AAAA,QAChC,GAAG;AAAA,QACH;AAAA,QACA,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,mBAAa,SAAS;AAEtB,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACxD,cAAM,WAAW;AAEjB,cAAM,IAAI;AAAA,UACR,SAAS,OAAO,QAAQ,QAAQ,SAAS,MAAM;AAAA,UAC/C,SAAS,OAAO,WAAW,QAAQ,SAAS,MAAM,KAAK,SAAS,UAAU;AAAA,UAC1E,SAAS,OAAO;AAAA,QAClB;AAAA,MACF;AAEA,aAAO,SAAS,KAAK;AAAA,IACvB,SAAS,OAAO;AACd,mBAAa,SAAS;AAEtB,UAAI,iBAAiB,UAAU;AAC7B,cAAM;AAAA,MACR;AAEA,UAAI,iBAAiB,OAAO;AAC1B,YAAI,MAAM,SAAS,cAAc;AAC/B,gBAAM,IAAI,SAAS,WAAW,2BAA2B,KAAK,OAAO,IAAI;AAAA,QAC3E;AACA,cAAM,IAAI,SAAS,iBAAiB,MAAM,OAAO;AAAA,MACnD;AAEA,YAAM,IAAI,SAAS,iBAAiB,2BAA2B;AAAA,IACjE;AAAA,EACF;AACF;AAKO,IAAM,WAAN,cAAuB,MAAM;AAAA,EAClC,YACS,MACP,SACO,SACP;AACA,UAAM,OAAO;AAJN;AAEA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AASO,SAAS,gBACd,UACA,SACW;AACX,SAAO,UAAU,aAAa,UAAU,OAAO;AACjD;AAMA,eAAsB,gBACpB,UACA,UACA,SAC+B;AAC/B,QAAM,SAAS,gBAAgB,UAAU,OAAO;AAChD,SAAO,OAAO,aAAa,QAAQ;AACrC;;;AC/OO,IAAM,eAAe;AAAA,EAC1B,eAAe;AAAA,EACf,WAAW;AAAA,EACX,eAAe;AAAA,EACf,eAAe;AAAA,EACf,aAAa;AAAA,EACb,aAAa;AACf;AAwIA,SAAS,WAAW,MAAc,SAA0B;AAC1D,SAAO,IAAI,IAAI,IAAI,KAAK,UAAU,OAAO,CAAC,KAAK,IAAI;AACrD;AAiBO,IAAM,gBAAgB;AAAA;AAAA,EAE3B,cAAc,CAAC,YACb,WAAW,aAAa,eAAe,OAAO;AAAA;AAAA,EAGhD,WAAW,CAAC,YACV,WAAW,aAAa,WAAW,OAAO;AAAA;AAAA,EAG5C,cAAc,CAAC,YACb,WAAW,aAAa,eAAe,OAAO;AAAA;AAAA,EAGhD,cAAc,CAAC,YACb,WAAW,aAAa,eAAe,OAAO;AAAA;AAAA,EAGhD,YAAY,CAAC,YACX,WAAW,aAAa,aAAa,OAAO;AAAA;AAAA,EAG9C,YAAY,CAAC,YACX,WAAW,aAAa,aAAa,OAAO;AAChD;;;AClKO,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAW5B,cAAc,QAAuD;AACnE,WAAO,EAAE,MAAM,YAAY,SAAS,EAAE,OAAO,EAAE;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,eAAe,QAA8D;AAC3E,WAAO,EAAE,MAAM,YAAY,SAAS,EAAE,UAAU,OAAO,EAAE;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,gBAAgB,OAAoD;AAClE,WAAO,EAAE,MAAM,YAAY,SAAS,EAAE,cAAc,MAAM,EAAE;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,cAAc,QAA2D;AACvE,WAAO,EAAE,MAAM,YAAY,SAAS,EAAE,SAAS,OAAO,EAAE;AAAA,EAC1D;AACF;;;ACzGA,SAAS,YAAY,YAAY,uBAAuB;AAExD,IAAM,eAAe;AACrB,IAAM,eAAe;AAOrB,SAAS,iBAAiB,OAAuB;AAC/C,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;AA6BO,SAAS,uBACd,OACA,SACA,WACS;AACT,MAAI;AACF,UAAM,aAAa,iBAAiB,KAAK;AACzC,UAAM,WAAW,WAAW,UAAU,UAAU,EAC7C,OAAO,OAAO,EACd,OAAO,KAAK;AACf,UAAM,iBAAiB,OAAO,KAAK,UAAU,KAAK;AAClD,UAAM,kBAAkB,OAAO,KAAK,WAAW,KAAK;AAEpD,QAAI,eAAe,WAAW,gBAAgB,QAAQ;AACpD,aAAO;AAAA,IACT;AAEA,WAAO,gBAAgB,gBAAgB,eAAe;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,uBACd,OACA,SACQ;AACR,QAAM,aAAa,iBAAiB,KAAK;AACzC,SAAO,WAAW,UAAU,UAAU,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AACtE;AAmBO,SAAS,iBAAiB,OAAuB;AACtD,SAAO,UAAU,KAAK;AACxB;AAQO,SAAS,aAAa,OAAwB;AACnD,SACE,OAAO,UAAU,YACjB,MAAM,WAAW,YAAY,KAC7B,MAAM,WAAW;AAErB;","names":[]}
|
package/dist/server.d.mts
CHANGED
package/dist/server.d.ts
CHANGED
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
|
|
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) {
|
|
@@ -148,7 +152,7 @@ function createServer(handler, config) {
|
|
|
148
152
|
const body = await readBody(req);
|
|
149
153
|
if (!skipSignatureVerification) {
|
|
150
154
|
const signature = req.headers["x-firststep-signature"];
|
|
151
|
-
if (
|
|
155
|
+
if (signature && !verifyRequestSignature(token, body, signature)) {
|
|
152
156
|
sendJson(res, 401, { error: "Invalid signature" });
|
|
153
157
|
return;
|
|
154
158
|
}
|
|
@@ -162,7 +166,7 @@ function createServer(handler, config) {
|
|
|
162
166
|
const body = await readBody(req);
|
|
163
167
|
if (!skipSignatureVerification) {
|
|
164
168
|
const signature = req.headers["x-firststep-signature"];
|
|
165
|
-
if (
|
|
169
|
+
if (signature && !verifyRequestSignature(token, body, signature)) {
|
|
166
170
|
sendJson(res, 401, { error: "Invalid signature" });
|
|
167
171
|
return;
|
|
168
172
|
}
|
|
@@ -191,7 +195,7 @@ function createServer(handler, config) {
|
|
|
191
195
|
const body = await readBody(req);
|
|
192
196
|
if (!skipSignatureVerification) {
|
|
193
197
|
const signature = req.headers["x-firststep-signature"];
|
|
194
|
-
if (
|
|
198
|
+
if (signature && !verifyRequestSignature(token, body, signature)) {
|
|
195
199
|
sendJson(res, 401, { error: "Invalid signature" });
|
|
196
200
|
return;
|
|
197
201
|
}
|