@firststep-studio/sdk 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -300,29 +300,64 @@ var PlatformClient = class {
300
300
  this.token = config.token;
301
301
  this.timeout = config.timeout || 1e4;
302
302
  }
303
- /**
304
- * List chatbots accessible to this handler's organization.
305
- * Studio resolves the organization from the API token.
306
- */
307
- async listChatbots() {
308
- const res = await fetch(`${this.studioUrl}/api/protocol/chatbots`, {
309
- method: "GET",
303
+ async request(path, errorPrefix, init = {}) {
304
+ const res = await fetch(`${this.studioUrl}${path}`, {
305
+ method: init.method || "GET",
310
306
  headers: {
311
307
  "Authorization": `Bearer ${this.token}`,
312
308
  "Content-Type": "application/json"
313
309
  },
310
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
314
311
  signal: AbortSignal.timeout(this.timeout)
315
312
  });
316
313
  if (!res.ok) {
317
314
  const body = await res.text().catch(() => "");
318
- throw new PlatformError(
319
- `Failed to list chatbots: ${res.status} ${body}`,
320
- res.status
321
- );
315
+ throw new PlatformError(`${errorPrefix}: ${res.status} ${body}`, res.status);
322
316
  }
323
- const data = await res.json();
317
+ if (res.status === 204) return void 0;
318
+ return res.json();
319
+ }
320
+ /**
321
+ * Resolve the organization and token metadata for the configured token.
322
+ * Useful for connection state UI ("Connected to {orgName}") and audit.
323
+ */
324
+ async getMe() {
325
+ return this.request("/api/protocol/me", "Failed to fetch identity");
326
+ }
327
+ /**
328
+ * List chatbots accessible to this handler's organization.
329
+ * Studio resolves the organization from the API token.
330
+ */
331
+ async listChatbots() {
332
+ const data = await this.request(
333
+ "/api/protocol/chatbots",
334
+ "Failed to list chatbots"
335
+ );
324
336
  return data.chatbots;
325
337
  }
338
+ /**
339
+ * Register or update an external-handler chatbot under the token's
340
+ * organization. Idempotent: identified by `externalId`, so handlers
341
+ * can call this on every config save without creating duplicates.
342
+ */
343
+ async registerChatbot(input) {
344
+ return this.request(
345
+ "/api/protocol/chatbots",
346
+ "Failed to register chatbot",
347
+ { method: "POST", body: input }
348
+ );
349
+ }
350
+ /**
351
+ * Mark the chatbot identified by `externalId` as disconnected.
352
+ * The Studio record is preserved (history, analytics survive).
353
+ */
354
+ async unregisterChatbot(externalId) {
355
+ return this.request(
356
+ `/api/protocol/chatbots/${encodeURIComponent(externalId)}`,
357
+ "Failed to unregister chatbot",
358
+ { method: "DELETE" }
359
+ );
360
+ }
326
361
  };
327
362
  function createPlatformClient(config) {
328
363
  return new PlatformClient(config);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.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 HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\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 // Handoff Protocol Builders\n // ============================================\n\n /**\n * Request a handoff to another chatbot.\n *\n * The Studio proxy pushes a handoff stack entry and routes subsequent\n * user messages to the target handler. If `options.consent` is set,\n * Studio stores the request as a pending offer instead.\n *\n * @param payload - Handoff target, reason, context, and options\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffRequest(payload: HandoffRequestPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffRequest: payload } };\n },\n\n /**\n * Return from a handoff back to the parent handler.\n *\n * The Studio proxy pops the handoff stack and delivers the return\n * result to the parent handler on the next user message.\n *\n * @param payload - Summary and artifacts from the completed handoff\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffReturn(payload: HandoffReturnPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffReturn: payload } };\n },\n\n /**\n * Offer a handoff with user consent required.\n *\n * Studio stores this as a pending offer, sends an SSE event to the\n * frontend, and waits for the user to accept or decline before\n * executing the handoff.\n *\n * @param payload - Offer display info and the pending handoff request\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffOffer: payload } };\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 HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n};\n","/**\n * Platform Client\n *\n * SDK client for handlers to call back to FirstStep Studio.\n * Enables handlers to query platform data (chatbot list, etc.)\n * using the same API token used for request signature verification.\n *\n * @example\n * ```typescript\n * import { createPlatformClient } from '@firststep-studio/sdk';\n *\n * const client = createPlatformClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN,\n * });\n *\n * const chatbots = await client.listChatbots();\n * // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]\n * ```\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PlatformClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (same as FIRSTSTEP_TOKEN used for signature verification) */\n token: string;\n /** Request timeout in ms (default: 10000) */\n timeout?: number;\n}\n\nexport interface PlatformChatbot {\n /** Studio chatbot MongoDB ID */\n id: string;\n /** Chatbot display name */\n name: string;\n /** Handler URL (if external handler configured) */\n handlerUrl?: string;\n /** Config slug for multi-config routing */\n configSlug?: string;\n /** Handler type */\n handlerType?: 'builtin' | 'external';\n}\n\nexport class PlatformError extends Error {\n status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'PlatformError';\n this.status = status;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class PlatformClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: PlatformClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout || 10000;\n }\n\n /**\n * List chatbots accessible to this handler's organization.\n * Studio resolves the organization from the API token.\n */\n async listChatbots(): Promise<PlatformChatbot[]> {\n const res = await fetch(`${this.studioUrl}/api/protocol/chatbots`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new PlatformError(\n `Failed to list chatbots: ${res.status} ${body}`,\n res.status\n );\n }\n\n const data = await res.json() as { chatbots: PlatformChatbot[] };\n return data.chatbots;\n }\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create a PlatformClient for calling back to FirstStep Studio.\n */\nexport function createPlatformClient(config: PlatformClientConfig): PlatformClient {\n return new PlatformClient(config);\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":[]}
1
+ {"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.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 HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\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 // Handoff Protocol Builders\n // ============================================\n\n /**\n * Request a handoff to another chatbot.\n *\n * The Studio proxy pushes a handoff stack entry and routes subsequent\n * user messages to the target handler. If `options.consent` is set,\n * Studio stores the request as a pending offer instead.\n *\n * @param payload - Handoff target, reason, context, and options\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffRequest(payload: HandoffRequestPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffRequest: payload } };\n },\n\n /**\n * Return from a handoff back to the parent handler.\n *\n * The Studio proxy pops the handoff stack and delivers the return\n * result to the parent handler on the next user message.\n *\n * @param payload - Summary and artifacts from the completed handoff\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffReturn(payload: HandoffReturnPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffReturn: payload } };\n },\n\n /**\n * Offer a handoff with user consent required.\n *\n * Studio stores this as a pending offer, sends an SSE event to the\n * frontend, and waits for the user to accept or decline before\n * executing the handoff.\n *\n * @param payload - Offer display info and the pending handoff request\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffOffer: payload } };\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 HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n};\n","/**\n * Platform Client\n *\n * SDK client for handlers to call back to FirstStep Studio.\n * Enables handlers to query platform data (chatbot list, etc.)\n * using the same API token used for request signature verification.\n *\n * @example\n * ```typescript\n * import { createPlatformClient } from '@firststep-studio/sdk';\n *\n * const client = createPlatformClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN,\n * });\n *\n * const chatbots = await client.listChatbots();\n * // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]\n * ```\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PlatformClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (same as FIRSTSTEP_TOKEN used for signature verification) */\n token: string;\n /** Request timeout in ms (default: 10000) */\n timeout?: number;\n}\n\nexport interface PlatformOrganization {\n /** Studio organization ID */\n id: string;\n /** Display name (null if the organization record has been deleted) */\n name: string | null;\n /** URL-safe slug */\n slug: string | null;\n}\n\nexport interface PlatformTokenInfo {\n /** API token ID */\n id: string;\n /** Human-friendly token label */\n name: string;\n /** Authorized scopes */\n scopes: string[];\n /** Project IDs this token is scoped to (null = full org access) */\n projectIds: string[] | null;\n}\n\nexport interface PlatformIdentity {\n organization: PlatformOrganization;\n token: PlatformTokenInfo;\n}\n\nexport interface PlatformChatbotRegisterInput {\n /** Stable handler-side identifier used to upsert (e.g. handler config ID). */\n externalId: string;\n /** Display name shown in Studio's chatbot list. */\n name: string;\n /** Optional human description. */\n description?: string;\n /** Public URL where Studio should send protocol requests. */\n handlerUrl: string;\n /** Slug for multi-config routing on the handler side. */\n configSlug?: string;\n /** Optional capability flags advertised to Studio. */\n capabilities?: {\n streaming?: boolean;\n formQuestions?: boolean;\n knowledgeActions?: boolean;\n integrations?: boolean;\n [key: string]: unknown;\n };\n}\n\nexport interface PlatformChatbotRegistration {\n /** Studio chatbot MongoDB ID (immutable across re-registrations). */\n id: string;\n /** Echoes back the externalId used as upsert key. */\n externalId: string;\n /** Resolved name as stored in Studio. */\n name: string;\n protocolConfig: Record<string, unknown>;\n}\n\nexport interface PlatformChatbot {\n /** Studio chatbot MongoDB ID */\n id: string;\n /** Chatbot display name */\n name: string;\n /** Handler URL (if external handler configured) */\n handlerUrl?: string;\n /** Config slug for multi-config routing */\n configSlug?: string;\n /** Handler type */\n handlerType?: 'builtin' | 'external';\n}\n\nexport class PlatformError extends Error {\n status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'PlatformError';\n this.status = status;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class PlatformClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: PlatformClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout || 10000;\n }\n\n private async request<T>(\n path: string,\n errorPrefix: string,\n init: { method?: string; body?: unknown } = {}\n ): Promise<T> {\n const res = await fetch(`${this.studioUrl}${path}`, {\n method: init.method || 'GET',\n headers: {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n body: init.body !== undefined ? JSON.stringify(init.body) : undefined,\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new PlatformError(`${errorPrefix}: ${res.status} ${body}`, res.status);\n }\n if (res.status === 204) return undefined as T;\n return res.json() as Promise<T>;\n }\n\n /**\n * Resolve the organization and token metadata for the configured token.\n * Useful for connection state UI (\"Connected to {orgName}\") and audit.\n */\n async getMe(): Promise<PlatformIdentity> {\n return this.request<PlatformIdentity>('/api/protocol/me', 'Failed to fetch identity');\n }\n\n /**\n * List chatbots accessible to this handler's organization.\n * Studio resolves the organization from the API token.\n */\n async listChatbots(): Promise<PlatformChatbot[]> {\n const data = await this.request<{ chatbots: PlatformChatbot[] }>(\n '/api/protocol/chatbots',\n 'Failed to list chatbots'\n );\n return data.chatbots;\n }\n\n /**\n * Register or update an external-handler chatbot under the token's\n * organization. Idempotent: identified by `externalId`, so handlers\n * can call this on every config save without creating duplicates.\n */\n async registerChatbot(input: PlatformChatbotRegisterInput): Promise<PlatformChatbotRegistration> {\n return this.request<PlatformChatbotRegistration>(\n '/api/protocol/chatbots',\n 'Failed to register chatbot',\n { method: 'POST', body: input }\n );\n }\n\n /**\n * Mark the chatbot identified by `externalId` as disconnected.\n * The Studio record is preserved (history, analytics survive).\n */\n async unregisterChatbot(externalId: string): Promise<{ ok: boolean; modified: number }> {\n return this.request<{ ok: boolean; modified: number }>(\n `/api/protocol/chatbots/${encodeURIComponent(externalId)}`,\n 'Failed to unregister chatbot',\n { method: 'DELETE' }\n );\n }\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create a PlatformClient for calling back to FirstStep Studio.\n */\nexport function createPlatformClient(config: PlatformClientConfig): PlatformClient {\n return new PlatformClient(config);\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 { l as ProtocolHandler } from './types-fhi9K2il.mjs';
2
+ import { l as ProtocolHandler } from './types-GoTI_c14.mjs';
3
3
 
4
4
  /**
5
5
  * FirstStep SDK Server
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from 'http';
2
- import { l as ProtocolHandler } from './types-fhi9K2il.js';
2
+ import { l as ProtocolHandler } from './types-GoTI_c14.js';
3
3
 
4
4
  /**
5
5
  * FirstStep SDK Server
package/dist/server.js CHANGED
@@ -25,6 +25,55 @@ __export(server_exports, {
25
25
  module.exports = __toCommonJS(server_exports);
26
26
  var import_http = require("http");
27
27
 
28
+ // src/resources/storageClient.ts
29
+ var StorageClient = class {
30
+ constructor(baseUrl, token) {
31
+ this.baseUrl = baseUrl;
32
+ this.token = token;
33
+ }
34
+ async get(key) {
35
+ const res = await this._post("/storage/get", { key });
36
+ if (!res.success) return null;
37
+ return res.value ?? null;
38
+ }
39
+ async set(key, value, options) {
40
+ await this._post("/storage/set", { key, value, ttl: options?.ttl });
41
+ }
42
+ async delete(key) {
43
+ await this._post("/storage/delete", { key });
44
+ }
45
+ async has(key) {
46
+ const res = await this._post("/storage/has", { key });
47
+ return res.exists === true;
48
+ }
49
+ async keys(prefix) {
50
+ const res = await this._post("/storage/keys", { prefix });
51
+ return res.keys ?? [];
52
+ }
53
+ async merge(key, partial) {
54
+ await this._post("/storage/merge", { key, partial });
55
+ }
56
+ async append(key, item) {
57
+ await this._post("/storage/append", { key, item });
58
+ }
59
+ async _post(path, body) {
60
+ const res = await fetch(`${this.baseUrl}${path}`, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ Authorization: `Bearer ${this.token}`
65
+ },
66
+ body: JSON.stringify(body)
67
+ });
68
+ if (!res.ok) {
69
+ const text = await res.text().catch(() => "");
70
+ console.error(`[StorageClient] ${path} failed: ${res.status} ${text}`);
71
+ return { success: false, error: `HTTP ${res.status}` };
72
+ }
73
+ return res.json();
74
+ }
75
+ };
76
+
28
77
  // src/auth.ts
29
78
  var import_crypto = require("crypto");
30
79
  function deriveSigningKey(token) {
@@ -72,6 +121,11 @@ function buildStubContext(data) {
72
121
  const noopAsync = async () => ({});
73
122
  const noop = () => {
74
123
  };
124
+ const resources = data?._resources;
125
+ let storage;
126
+ if (resources?.baseUrl && resources?.token && resources.capabilities?.storage !== false) {
127
+ storage = new StorageClient(resources.baseUrl, resources.token);
128
+ }
75
129
  return {
76
130
  session: {
77
131
  sessionId: "",
@@ -94,6 +148,7 @@ function buildStubContext(data) {
94
148
  execute: noopAsync,
95
149
  ...data?.integrations
96
150
  },
151
+ storage,
97
152
  analytics: {
98
153
  logActionExecuted: noop,
99
154
  logInteraction: noop,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK Server\n *\n * Standalone HTTP server for protocol handlers.\n * Zero external dependencies, uses Node.js built-in `http` module.\n *\n * @example\n * ```typescript\n * import { createServer } from '@firststep-studio/sdk/server';\n * import type { ProtocolHandler } from '@firststep-studio/sdk';\n *\n * const handler: ProtocolHandler = {\n * async handleMessage(request, context) {\n * return {\n * message: 'Hello from my handler!',\n * sessionId: request.sessionId || 'new',\n * agentId: 'main',\n * sessionStatus: 'active',\n * };\n * },\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * },\n * };\n *\n * const server = createServer(handler, {\n * token: process.env.FIRSTSTEP_TOKEN!,\n * port: 3001,\n * });\n *\n * server.start();\n * ```\n */\n\nimport { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http';\nimport type {\n ProtocolHandler,\n ProtocolRequest,\n ProtocolContext,\n ProtocolCapabilities,\n ProtocolStreamChunk,\n} from './types';\nimport { verifyRequestSignature } from './auth';\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface ServerConfig {\n /** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */\n token: string;\n\n /** Port to listen on. Defaults to 3001, or the PORT env variable. */\n port?: number;\n\n /** Host to bind to. Defaults to '0.0.0.0'. */\n host?: string;\n\n /** Skip signature verification (for local development only). */\n skipSignatureVerification?: boolean;\n\n /**\n * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\n}\n\n// ============================================\n// Server Instance\n// ============================================\n\nexport interface FirstStepServer {\n /** Start the server */\n start(): Promise<void>;\n\n /** Stop the server gracefully */\n stop(): Promise<void>;\n\n /**\n * Get the request handler function.\n * Use this to integrate with Express, Fastify, or other frameworks.\n *\n * @example\n * ```typescript\n * // Express\n * const server = createServer(handler, { token: '...' });\n * app.post('/handshake', server.getRequestHandler());\n * app.post('/message', server.getRequestHandler());\n * ```\n */\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;\n}\n\n// ============================================\n// Internal Helpers\n// ============================================\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json),\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(json);\n}\n\nfunction parsePath(url: string | undefined): string {\n if (!url) return '/';\n const idx = url.indexOf('?');\n return idx >= 0 ? url.substring(0, idx) : url;\n}\n\n/**\n * Build a stub ProtocolContext for external handlers.\n *\n * External handlers receive context data as plain JSON from the platform.\n * The stub provides no-op implementations for methods like session.getState()\n * since those will be handled by callback APIs to the FirstStep backend.\n *\n * When the platform sends a /message request, the request body includes\n * a `context` field with serialized context data. This function merges\n * that data with no-op method stubs.\n */\nfunction buildStubContext(data?: Partial<ProtocolContext>): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n return {\n session: {\n sessionId: '',\n getState: noopAsync,\n updateState: noopAsync,\n getHistory: async () => [],\n saveMessage: noopAsync,\n complete: noopAsync,\n getFormData: async () => ({}),\n updateFormField: noopAsync,\n updateFormData: noopAsync,\n ...data?.session,\n },\n knowledge: {\n queryDatabase: noopAsync,\n searchPages: noopAsync,\n ...data?.knowledge,\n },\n integrations: {\n execute: noopAsync,\n ...data?.integrations,\n },\n analytics: {\n logActionExecuted: noop,\n logInteraction: noop,\n logCustomEvent: noop,\n ...data?.analytics,\n },\n logger: {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n logRouting: noop,\n logToolUse: noop,\n ...data?.logger,\n },\n deployment: {\n id: '',\n slug: '',\n name: '',\n protocolType: 'external',\n ...data?.deployment,\n },\n chatbot: data?.chatbot,\n };\n}\n\n// ============================================\n// createServer\n// ============================================\n\n/**\n * Create a standalone HTTP server for a protocol handler.\n *\n * The server exposes three endpoints:\n * - `GET /health` - Health check (always 200)\n * - `POST /handshake` - Returns handler capabilities (signature verified)\n * - `POST /message` - Handles a chat message (signature verified)\n */\nexport function createServer(\n handler: ProtocolHandler,\n config: ServerConfig\n): FirstStepServer {\n const {\n token,\n port = parseInt(process.env.PORT || '3001', 10),\n host = '0.0.0.0',\n skipSignatureVerification = false,\n prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\n\n // Validate config\n if (!token && !skipSignatureVerification) {\n throw new Error(\n 'FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev.'\n );\n }\n\n /**\n * Core request router\n */\n async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const path = parsePath(req.url);\n const method = (req.method || 'GET').toUpperCase();\n\n // CORS preflight\n if (method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-FirstStep-Signature',\n 'Access-Control-Max-Age': '86400',\n });\n res.end();\n return;\n }\n\n // Strip prefix to get the remaining path\n const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Extract optional config slug from path: /{slug}/message -> slug = \"slug\", route = \"/message\"\n // If no slug segment, route is the full remaining path (backward compatible)\n let configSlug: string | undefined;\n let route: string;\n const slugMatch = afterPrefix.match(/^\\/([^/]+)(\\/(?:health|handshake|message(?:\\/stream)?))?$/);\n if (slugMatch && slugMatch[2]) {\n // Path has a slug segment: /{slug}/{endpoint}\n configSlug = slugMatch[1];\n route = slugMatch[2];\n } else {\n // No slug: /{endpoint} (backward compatible)\n route = afterPrefix;\n }\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/handshake' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n const capabilities: ProtocolCapabilities = handler.getCapabilities();\n const handlerInfo = handler.getHandlerInfo?.();\n sendJson(res, 200, { capabilities, handler: handlerInfo || undefined });\n return;\n }\n\n // Message\n if (route === '/message' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n const context = buildStubContext(parsed.context);\n\n try {\n const response = await handler.handleMessage(parsed.request, context);\n sendJson(res, 200, response);\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, {\n error: 'Handler error',\n message: err?.message || 'Unknown error',\n });\n }\n return;\n }\n\n // Message (streaming via SSE)\n if (route === '/message/stream' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n if (!handler.handleStream) {\n // Fallback: use non-streaming handleMessage, send as single SSE burst\n const context = buildStubContext(parsed.context);\n try {\n const response = await handler.handleMessage(parsed.request, context);\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n sendSSE('connected', { sessionId: response.sessionId });\n sendSSE('text', { type: 'text', content: response.message, sessionId: response.sessionId });\n if (response.metadata) {\n sendSSE('metadata', { type: 'metadata', content: response.metadata, sessionId: response.sessionId });\n }\n if (response.form) {\n sendSSE('form', { type: 'form', content: response.form, sessionId: response.sessionId });\n }\n sendSSE('status', { type: 'status', content: response.sessionStatus, sessionId: response.sessionId });\n sendSSE('done', { sessionId: response.sessionId });\n res.end();\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, { error: 'Handler error', message: err?.message || 'Unknown error' });\n }\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n\n const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;\n sendSSE('connected', { sessionId });\n\n try {\n for await (const chunk of handler.handleStream(parsed.request, context)) {\n sendSSE(chunk.type, {\n type: chunk.type,\n content: chunk.content,\n sessionId,\n });\n }\n sendSSE('done', { sessionId });\n } catch (err: any) {\n console.error('[firststep] Stream error:', err);\n sendSSE('error', { code: 'STREAM_ERROR', message: err?.message || 'Unknown error' });\n }\n\n res.end();\n return;\n }\n\n // 404\n sendJson(res, 404, { error: 'Not found' });\n }\n\n // Wrap in error boundary\n function requestListener(req: IncomingMessage, res: ServerResponse): void {\n handleRequest(req, res).catch((err) => {\n console.error('[firststep] Unexpected error:', err);\n if (!res.headersSent) {\n sendJson(res, 500, { error: 'Internal server error' });\n }\n });\n }\n\n const httpServer = createHttpServer(requestListener);\n\n return {\n start() {\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n console.log(`[firststep] Handler server listening on ${host}:${port}`);\n console.log(`[firststep] Endpoints:`);\n console.log(` GET ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/message/stream - Handle chat message (SSE stream)`);\n resolve();\n });\n });\n },\n\n stop() {\n return new Promise<void>((resolve, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n getRequestHandler() {\n return requestListener;\n },\n };\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":["context","sendSSE","createHttpServer"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/resources/storageClient.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK Server\n *\n * Standalone HTTP server for protocol handlers.\n * Zero external dependencies, uses Node.js built-in `http` module.\n *\n * @example\n * ```typescript\n * import { createServer } from '@firststep-studio/sdk/server';\n * import type { ProtocolHandler } from '@firststep-studio/sdk';\n *\n * const handler: ProtocolHandler = {\n * async handleMessage(request, context) {\n * return {\n * message: 'Hello from my handler!',\n * sessionId: request.sessionId || 'new',\n * agentId: 'main',\n * sessionStatus: 'active',\n * };\n * },\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * },\n * };\n *\n * const server = createServer(handler, {\n * token: process.env.FIRSTSTEP_TOKEN!,\n * port: 3001,\n * });\n *\n * server.start();\n * ```\n */\n\nimport { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http';\nimport type {\n ProtocolHandler,\n ProtocolRequest,\n ProtocolContext,\n ProtocolCapabilities,\n ProtocolStreamChunk,\n StorageContext,\n} from './types';\nimport { StorageClient } from './resources/storageClient.js';\nimport { verifyRequestSignature } from './auth';\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface ServerConfig {\n /** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */\n token: string;\n\n /** Port to listen on. Defaults to 3001, or the PORT env variable. */\n port?: number;\n\n /** Host to bind to. Defaults to '0.0.0.0'. */\n host?: string;\n\n /** Skip signature verification (for local development only). */\n skipSignatureVerification?: boolean;\n\n /**\n * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\n}\n\n// ============================================\n// Server Instance\n// ============================================\n\nexport interface FirstStepServer {\n /** Start the server */\n start(): Promise<void>;\n\n /** Stop the server gracefully */\n stop(): Promise<void>;\n\n /**\n * Get the request handler function.\n * Use this to integrate with Express, Fastify, or other frameworks.\n *\n * @example\n * ```typescript\n * // Express\n * const server = createServer(handler, { token: '...' });\n * app.post('/handshake', server.getRequestHandler());\n * app.post('/message', server.getRequestHandler());\n * ```\n */\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;\n}\n\n// ============================================\n// Internal Helpers\n// ============================================\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json),\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(json);\n}\n\nfunction parsePath(url: string | undefined): string {\n if (!url) return '/';\n const idx = url.indexOf('?');\n return idx >= 0 ? url.substring(0, idx) : url;\n}\n\n/**\n * Build a stub ProtocolContext for external handlers.\n *\n * External handlers receive context data as plain JSON from the platform.\n * The stub provides no-op implementations for methods like session.getState()\n * since those will be handled by callback APIs to the FirstStep backend.\n *\n * When the platform sends a /message request, the request body includes\n * a `context` field with serialized context data. This function merges\n * that data with no-op method stubs.\n */\nfunction buildStubContext(data?: any): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n // Detect _resources injected by Studio proxy for HTTP-backed resource clients\n const resources = data?._resources as { baseUrl: string; token: string; capabilities?: Record<string, boolean> } | undefined;\n let storage: StorageContext | undefined;\n\n if (resources?.baseUrl && resources?.token && resources.capabilities?.storage !== false) {\n storage = new StorageClient(resources.baseUrl, resources.token);\n }\n\n return {\n session: {\n sessionId: '',\n getState: noopAsync,\n updateState: noopAsync,\n getHistory: async () => [],\n saveMessage: noopAsync,\n complete: noopAsync,\n getFormData: async () => ({}),\n updateFormField: noopAsync,\n updateFormData: noopAsync,\n ...data?.session,\n },\n knowledge: {\n queryDatabase: noopAsync,\n searchPages: noopAsync,\n ...data?.knowledge,\n },\n integrations: {\n execute: noopAsync,\n ...data?.integrations,\n },\n storage,\n analytics: {\n logActionExecuted: noop,\n logInteraction: noop,\n logCustomEvent: noop,\n ...data?.analytics,\n },\n logger: {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n logRouting: noop,\n logToolUse: noop,\n ...data?.logger,\n },\n deployment: {\n id: '',\n slug: '',\n name: '',\n protocolType: 'external',\n ...data?.deployment,\n },\n chatbot: data?.chatbot,\n };\n}\n\n// ============================================\n// createServer\n// ============================================\n\n/**\n * Create a standalone HTTP server for a protocol handler.\n *\n * The server exposes three endpoints:\n * - `GET /health` - Health check (always 200)\n * - `POST /handshake` - Returns handler capabilities (signature verified)\n * - `POST /message` - Handles a chat message (signature verified)\n */\nexport function createServer(\n handler: ProtocolHandler,\n config: ServerConfig\n): FirstStepServer {\n const {\n token,\n port = parseInt(process.env.PORT || '3001', 10),\n host = '0.0.0.0',\n skipSignatureVerification = false,\n prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\n\n // Validate config\n if (!token && !skipSignatureVerification) {\n throw new Error(\n 'FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev.'\n );\n }\n\n /**\n * Core request router\n */\n async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const path = parsePath(req.url);\n const method = (req.method || 'GET').toUpperCase();\n\n // CORS preflight\n if (method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-FirstStep-Signature',\n 'Access-Control-Max-Age': '86400',\n });\n res.end();\n return;\n }\n\n // Strip prefix to get the remaining path\n const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Extract optional config slug from path: /{slug}/message -> slug = \"slug\", route = \"/message\"\n // If no slug segment, route is the full remaining path (backward compatible)\n let configSlug: string | undefined;\n let route: string;\n const slugMatch = afterPrefix.match(/^\\/([^/]+)(\\/(?:health|handshake|message(?:\\/stream)?))?$/);\n if (slugMatch && slugMatch[2]) {\n // Path has a slug segment: /{slug}/{endpoint}\n configSlug = slugMatch[1];\n route = slugMatch[2];\n } else {\n // No slug: /{endpoint} (backward compatible)\n route = afterPrefix;\n }\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/handshake' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n const capabilities: ProtocolCapabilities = handler.getCapabilities();\n const handlerInfo = handler.getHandlerInfo?.();\n sendJson(res, 200, { capabilities, handler: handlerInfo || undefined });\n return;\n }\n\n // Message\n if (route === '/message' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n const context = buildStubContext(parsed.context);\n\n try {\n const response = await handler.handleMessage(parsed.request, context);\n sendJson(res, 200, response);\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, {\n error: 'Handler error',\n message: err?.message || 'Unknown error',\n });\n }\n return;\n }\n\n // Message (streaming via SSE)\n if (route === '/message/stream' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n if (!handler.handleStream) {\n // Fallback: use non-streaming handleMessage, send as single SSE burst\n const context = buildStubContext(parsed.context);\n try {\n const response = await handler.handleMessage(parsed.request, context);\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n sendSSE('connected', { sessionId: response.sessionId });\n sendSSE('text', { type: 'text', content: response.message, sessionId: response.sessionId });\n if (response.metadata) {\n sendSSE('metadata', { type: 'metadata', content: response.metadata, sessionId: response.sessionId });\n }\n if (response.form) {\n sendSSE('form', { type: 'form', content: response.form, sessionId: response.sessionId });\n }\n sendSSE('status', { type: 'status', content: response.sessionStatus, sessionId: response.sessionId });\n sendSSE('done', { sessionId: response.sessionId });\n res.end();\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, { error: 'Handler error', message: err?.message || 'Unknown error' });\n }\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n\n const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;\n sendSSE('connected', { sessionId });\n\n try {\n for await (const chunk of handler.handleStream(parsed.request, context)) {\n sendSSE(chunk.type, {\n type: chunk.type,\n content: chunk.content,\n sessionId,\n });\n }\n sendSSE('done', { sessionId });\n } catch (err: any) {\n console.error('[firststep] Stream error:', err);\n sendSSE('error', { code: 'STREAM_ERROR', message: err?.message || 'Unknown error' });\n }\n\n res.end();\n return;\n }\n\n // 404\n sendJson(res, 404, { error: 'Not found' });\n }\n\n // Wrap in error boundary\n function requestListener(req: IncomingMessage, res: ServerResponse): void {\n handleRequest(req, res).catch((err) => {\n console.error('[firststep] Unexpected error:', err);\n if (!res.headersSent) {\n sendJson(res, 500, { error: 'Internal server error' });\n }\n });\n }\n\n const httpServer = createHttpServer(requestListener);\n\n return {\n start() {\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n console.log(`[firststep] Handler server listening on ${host}:${port}`);\n console.log(`[firststep] Endpoints:`);\n console.log(` GET ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/message/stream - Handle chat message (SSE stream)`);\n resolve();\n });\n });\n },\n\n stop() {\n return new Promise<void>((resolve, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n getRequestHandler() {\n return requestListener;\n },\n };\n}\n","import type { StorageContext, StorageSetOptions } from '../types.js';\n\n/**\n * HTTP-backed implementation of StorageContext.\n * All operations call back to Studio's protocol-resources API.\n * Data is scoped to the deployment via the JWT token.\n */\nexport class StorageClient implements StorageContext {\n private baseUrl: string;\n private token: string;\n\n constructor(baseUrl: string, token: string) {\n this.baseUrl = baseUrl;\n this.token = token;\n }\n\n async get<T = unknown>(key: string): Promise<T | null> {\n const res = await this._post('/storage/get', { key });\n if (!res.success) return null;\n return (res.value ?? null) as T;\n }\n\n async set(key: string, value: unknown, options?: StorageSetOptions): Promise<void> {\n await this._post('/storage/set', { key, value, ttl: options?.ttl });\n }\n\n async delete(key: string): Promise<void> {\n await this._post('/storage/delete', { key });\n }\n\n async has(key: string): Promise<boolean> {\n const res = await this._post('/storage/has', { key });\n return res.exists === true;\n }\n\n async keys(prefix?: string): Promise<string[]> {\n const res = await this._post('/storage/keys', { prefix });\n return res.keys ?? [];\n }\n\n async merge(key: string, partial: Record<string, unknown>): Promise<void> {\n await this._post('/storage/merge', { key, partial });\n }\n\n async append(key: string, item: unknown): Promise<void> {\n await this._post('/storage/append', { key, item });\n }\n\n private async _post(path: string, body: unknown): Promise<any> {\n const res = await fetch(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.token}`,\n },\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n console.error(`[StorageClient] ${path} failed: ${res.status} ${text}`);\n return { success: false, error: `HTTP ${res.status}` };\n }\n return res.json();\n }\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":["context","sendSSE","createHttpServer"]}
package/dist/server.mjs CHANGED
@@ -1,6 +1,55 @@
1
1
  // src/server.ts
2
2
  import { createServer as createHttpServer } from "http";
3
3
 
4
+ // src/resources/storageClient.ts
5
+ var StorageClient = class {
6
+ constructor(baseUrl, token) {
7
+ this.baseUrl = baseUrl;
8
+ this.token = token;
9
+ }
10
+ async get(key) {
11
+ const res = await this._post("/storage/get", { key });
12
+ if (!res.success) return null;
13
+ return res.value ?? null;
14
+ }
15
+ async set(key, value, options) {
16
+ await this._post("/storage/set", { key, value, ttl: options?.ttl });
17
+ }
18
+ async delete(key) {
19
+ await this._post("/storage/delete", { key });
20
+ }
21
+ async has(key) {
22
+ const res = await this._post("/storage/has", { key });
23
+ return res.exists === true;
24
+ }
25
+ async keys(prefix) {
26
+ const res = await this._post("/storage/keys", { prefix });
27
+ return res.keys ?? [];
28
+ }
29
+ async merge(key, partial) {
30
+ await this._post("/storage/merge", { key, partial });
31
+ }
32
+ async append(key, item) {
33
+ await this._post("/storage/append", { key, item });
34
+ }
35
+ async _post(path, body) {
36
+ const res = await fetch(`${this.baseUrl}${path}`, {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ Authorization: `Bearer ${this.token}`
41
+ },
42
+ body: JSON.stringify(body)
43
+ });
44
+ if (!res.ok) {
45
+ const text = await res.text().catch(() => "");
46
+ console.error(`[StorageClient] ${path} failed: ${res.status} ${text}`);
47
+ return { success: false, error: `HTTP ${res.status}` };
48
+ }
49
+ return res.json();
50
+ }
51
+ };
52
+
4
53
  // src/auth.ts
5
54
  import { createHash, createHmac, timingSafeEqual } from "crypto";
6
55
  function deriveSigningKey(token) {
@@ -48,6 +97,11 @@ function buildStubContext(data) {
48
97
  const noopAsync = async () => ({});
49
98
  const noop = () => {
50
99
  };
100
+ const resources = data?._resources;
101
+ let storage;
102
+ if (resources?.baseUrl && resources?.token && resources.capabilities?.storage !== false) {
103
+ storage = new StorageClient(resources.baseUrl, resources.token);
104
+ }
51
105
  return {
52
106
  session: {
53
107
  sessionId: "",
@@ -70,6 +124,7 @@ function buildStubContext(data) {
70
124
  execute: noopAsync,
71
125
  ...data?.integrations
72
126
  },
127
+ storage,
73
128
  analytics: {
74
129
  logActionExecuted: noop,
75
130
  logInteraction: noop,