@firststep-studio/sdk 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +52 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -669,6 +669,36 @@ interface PlatformChatbot {
|
|
|
669
669
|
/** Handler type */
|
|
670
670
|
handlerType?: 'builtin' | 'external';
|
|
671
671
|
}
|
|
672
|
+
interface PlatformKnowledgeSummary {
|
|
673
|
+
/** Studio knowledge MongoDB ID */
|
|
674
|
+
id: string;
|
|
675
|
+
/** Knowledge name */
|
|
676
|
+
name: string;
|
|
677
|
+
/** Optional description */
|
|
678
|
+
description?: string;
|
|
679
|
+
/** Knowledge type (e.g. 'text', 'database') */
|
|
680
|
+
type?: string;
|
|
681
|
+
/** Owning organization ID */
|
|
682
|
+
organizationId: string;
|
|
683
|
+
/** Timestamps */
|
|
684
|
+
createdAt?: number | string;
|
|
685
|
+
updatedAt?: number | string;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Full knowledge document (shape mirrors the underlying mongoose doc).
|
|
689
|
+
* Kept loose because the schema is large and varies by knowledge `type`;
|
|
690
|
+
* narrow types can be added incrementally as callers need them.
|
|
691
|
+
*/
|
|
692
|
+
type PlatformKnowledge = PlatformKnowledgeSummary & Record<string, unknown>;
|
|
693
|
+
interface ListKnowledgeOptions {
|
|
694
|
+
/**
|
|
695
|
+
* Target organization to list knowledge from. If omitted, defaults to
|
|
696
|
+
* the org associated with the SDK token. Use this to read from a
|
|
697
|
+
* different org (e.g. a platform-operated shared library marked
|
|
698
|
+
* `publicReadable` in Studio).
|
|
699
|
+
*/
|
|
700
|
+
orgId?: string;
|
|
701
|
+
}
|
|
672
702
|
declare class PlatformError extends Error {
|
|
673
703
|
status: number;
|
|
674
704
|
constructor(message: string, status: number);
|
|
@@ -703,6 +733,27 @@ declare class PlatformClient {
|
|
|
703
733
|
ok: boolean;
|
|
704
734
|
modified: number;
|
|
705
735
|
}>;
|
|
736
|
+
/**
|
|
737
|
+
* Knowledge / resource surface.
|
|
738
|
+
*
|
|
739
|
+
* Reads are allowed when the SDK token's org matches the target org,
|
|
740
|
+
* OR when the target org has `publicReadable: true` on the Studio side.
|
|
741
|
+
* Writes are not yet exposed here — those still go through the user-
|
|
742
|
+
* authenticated /api/knowledge routes from the Studio UI.
|
|
743
|
+
*/
|
|
744
|
+
knowledge: {
|
|
745
|
+
/**
|
|
746
|
+
* List knowledge items for an organization.
|
|
747
|
+
* Defaults to the SDK token's own org if `orgId` is omitted.
|
|
748
|
+
*/
|
|
749
|
+
list: (options?: ListKnowledgeOptions) => Promise<PlatformKnowledgeSummary[]>;
|
|
750
|
+
/**
|
|
751
|
+
* Fetch a single knowledge item by its Studio ID. The caller must
|
|
752
|
+
* have read access to the knowledge's owning org (same-org or
|
|
753
|
+
* publicReadable).
|
|
754
|
+
*/
|
|
755
|
+
get: (id: string) => Promise<PlatformKnowledge>;
|
|
756
|
+
};
|
|
706
757
|
}
|
|
707
758
|
/**
|
|
708
759
|
* Create a PlatformClient for calling back to FirstStep Studio.
|
|
@@ -976,4 +1027,4 @@ declare class LLMClient {
|
|
|
976
1027
|
*/
|
|
977
1028
|
declare function createLLMClient(config: LLMClientConfig): LLMClient;
|
|
978
1029
|
|
|
979
|
-
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, type LLMCacheCreateRequest, type LLMCachedContent, type LLMCandidate, LLMClient, type LLMClientConfig, type LLMContent, LLMError, type LLMGenerateRequest, type LLMGenerateResponse, type LLMGenerationConfig, type LLMPart, type LLMSafetySetting, type LLMUsageMetadata, MARKER_TYPES, type MarkerType, type PlatformChatbot, PlatformClient, type PlatformClientConfig, PlatformError, ProtocolStreamChunk, type ProviderCardItem, type ProviderCardPayload, type ReportCardPayload, type ResourceCardItem, type ResourceCardPayload, RoutingClassificationPayload, type SafetyPlanPayload, type SafetyPlanSection, SchemaDeclarationPayload, type UCPClassifyRequest, type UCPClassifyResponse, UCPClient, type UCPClientConfig, type UCPEndpointConfig, UCPError, type UCPErrorResponse, type UCPInfoResponse, type UCPMessage, classifyWithUCP, createAuthHeader, createLLMClient, createPlatformClient, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
|
1030
|
+
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, type LLMCacheCreateRequest, type LLMCachedContent, type LLMCandidate, LLMClient, type LLMClientConfig, type LLMContent, LLMError, type LLMGenerateRequest, type LLMGenerateResponse, type LLMGenerationConfig, type LLMPart, type LLMSafetySetting, type LLMUsageMetadata, type ListKnowledgeOptions, MARKER_TYPES, type MarkerType, type PlatformChatbot, PlatformClient, type PlatformClientConfig, PlatformError, type PlatformKnowledge, type PlatformKnowledgeSummary, ProtocolStreamChunk, type ProviderCardItem, type ProviderCardPayload, type ReportCardPayload, type ResourceCardItem, type ResourceCardPayload, RoutingClassificationPayload, type SafetyPlanPayload, type SafetyPlanSection, SchemaDeclarationPayload, type UCPClassifyRequest, type UCPClassifyResponse, UCPClient, type UCPClientConfig, type UCPEndpointConfig, UCPError, type UCPErrorResponse, type UCPInfoResponse, type UCPMessage, classifyWithUCP, createAuthHeader, createLLMClient, createPlatformClient, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -669,6 +669,36 @@ interface PlatformChatbot {
|
|
|
669
669
|
/** Handler type */
|
|
670
670
|
handlerType?: 'builtin' | 'external';
|
|
671
671
|
}
|
|
672
|
+
interface PlatformKnowledgeSummary {
|
|
673
|
+
/** Studio knowledge MongoDB ID */
|
|
674
|
+
id: string;
|
|
675
|
+
/** Knowledge name */
|
|
676
|
+
name: string;
|
|
677
|
+
/** Optional description */
|
|
678
|
+
description?: string;
|
|
679
|
+
/** Knowledge type (e.g. 'text', 'database') */
|
|
680
|
+
type?: string;
|
|
681
|
+
/** Owning organization ID */
|
|
682
|
+
organizationId: string;
|
|
683
|
+
/** Timestamps */
|
|
684
|
+
createdAt?: number | string;
|
|
685
|
+
updatedAt?: number | string;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Full knowledge document (shape mirrors the underlying mongoose doc).
|
|
689
|
+
* Kept loose because the schema is large and varies by knowledge `type`;
|
|
690
|
+
* narrow types can be added incrementally as callers need them.
|
|
691
|
+
*/
|
|
692
|
+
type PlatformKnowledge = PlatformKnowledgeSummary & Record<string, unknown>;
|
|
693
|
+
interface ListKnowledgeOptions {
|
|
694
|
+
/**
|
|
695
|
+
* Target organization to list knowledge from. If omitted, defaults to
|
|
696
|
+
* the org associated with the SDK token. Use this to read from a
|
|
697
|
+
* different org (e.g. a platform-operated shared library marked
|
|
698
|
+
* `publicReadable` in Studio).
|
|
699
|
+
*/
|
|
700
|
+
orgId?: string;
|
|
701
|
+
}
|
|
672
702
|
declare class PlatformError extends Error {
|
|
673
703
|
status: number;
|
|
674
704
|
constructor(message: string, status: number);
|
|
@@ -703,6 +733,27 @@ declare class PlatformClient {
|
|
|
703
733
|
ok: boolean;
|
|
704
734
|
modified: number;
|
|
705
735
|
}>;
|
|
736
|
+
/**
|
|
737
|
+
* Knowledge / resource surface.
|
|
738
|
+
*
|
|
739
|
+
* Reads are allowed when the SDK token's org matches the target org,
|
|
740
|
+
* OR when the target org has `publicReadable: true` on the Studio side.
|
|
741
|
+
* Writes are not yet exposed here — those still go through the user-
|
|
742
|
+
* authenticated /api/knowledge routes from the Studio UI.
|
|
743
|
+
*/
|
|
744
|
+
knowledge: {
|
|
745
|
+
/**
|
|
746
|
+
* List knowledge items for an organization.
|
|
747
|
+
* Defaults to the SDK token's own org if `orgId` is omitted.
|
|
748
|
+
*/
|
|
749
|
+
list: (options?: ListKnowledgeOptions) => Promise<PlatformKnowledgeSummary[]>;
|
|
750
|
+
/**
|
|
751
|
+
* Fetch a single knowledge item by its Studio ID. The caller must
|
|
752
|
+
* have read access to the knowledge's owning org (same-org or
|
|
753
|
+
* publicReadable).
|
|
754
|
+
*/
|
|
755
|
+
get: (id: string) => Promise<PlatformKnowledge>;
|
|
756
|
+
};
|
|
706
757
|
}
|
|
707
758
|
/**
|
|
708
759
|
* Create a PlatformClient for calling back to FirstStep Studio.
|
|
@@ -976,4 +1027,4 @@ declare class LLMClient {
|
|
|
976
1027
|
*/
|
|
977
1028
|
declare function createLLMClient(config: LLMClientConfig): LLMClient;
|
|
978
1029
|
|
|
979
|
-
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, type LLMCacheCreateRequest, type LLMCachedContent, type LLMCandidate, LLMClient, type LLMClientConfig, type LLMContent, LLMError, type LLMGenerateRequest, type LLMGenerateResponse, type LLMGenerationConfig, type LLMPart, type LLMSafetySetting, type LLMUsageMetadata, MARKER_TYPES, type MarkerType, type PlatformChatbot, PlatformClient, type PlatformClientConfig, PlatformError, ProtocolStreamChunk, type ProviderCardItem, type ProviderCardPayload, type ReportCardPayload, type ResourceCardItem, type ResourceCardPayload, RoutingClassificationPayload, type SafetyPlanPayload, type SafetyPlanSection, SchemaDeclarationPayload, type UCPClassifyRequest, type UCPClassifyResponse, UCPClient, type UCPClientConfig, type UCPEndpointConfig, UCPError, type UCPErrorResponse, type UCPInfoResponse, type UCPMessage, classifyWithUCP, createAuthHeader, createLLMClient, createPlatformClient, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
|
1030
|
+
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, type LLMCacheCreateRequest, type LLMCachedContent, type LLMCandidate, LLMClient, type LLMClientConfig, type LLMContent, LLMError, type LLMGenerateRequest, type LLMGenerateResponse, type LLMGenerationConfig, type LLMPart, type LLMSafetySetting, type LLMUsageMetadata, type ListKnowledgeOptions, MARKER_TYPES, type MarkerType, type PlatformChatbot, PlatformClient, type PlatformClientConfig, PlatformError, type PlatformKnowledge, type PlatformKnowledgeSummary, ProtocolStreamChunk, type ProviderCardItem, type ProviderCardPayload, type ReportCardPayload, type ResourceCardItem, type ResourceCardPayload, RoutingClassificationPayload, type SafetyPlanPayload, type SafetyPlanSection, SchemaDeclarationPayload, type UCPClassifyRequest, type UCPClassifyResponse, UCPClient, type UCPClientConfig, type UCPEndpointConfig, UCPError, type UCPErrorResponse, type UCPInfoResponse, type UCPMessage, classifyWithUCP, createAuthHeader, createLLMClient, createPlatformClient, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
package/dist/index.js
CHANGED
|
@@ -338,6 +338,40 @@ var PlatformError = class extends Error {
|
|
|
338
338
|
};
|
|
339
339
|
var PlatformClient = class {
|
|
340
340
|
constructor(config) {
|
|
341
|
+
/**
|
|
342
|
+
* Knowledge / resource surface.
|
|
343
|
+
*
|
|
344
|
+
* Reads are allowed when the SDK token's org matches the target org,
|
|
345
|
+
* OR when the target org has `publicReadable: true` on the Studio side.
|
|
346
|
+
* Writes are not yet exposed here — those still go through the user-
|
|
347
|
+
* authenticated /api/knowledge routes from the Studio UI.
|
|
348
|
+
*/
|
|
349
|
+
this.knowledge = {
|
|
350
|
+
/**
|
|
351
|
+
* List knowledge items for an organization.
|
|
352
|
+
* Defaults to the SDK token's own org if `orgId` is omitted.
|
|
353
|
+
*/
|
|
354
|
+
list: async (options = {}) => {
|
|
355
|
+
const qs = options.orgId ? `?organizationId=${encodeURIComponent(options.orgId)}` : "";
|
|
356
|
+
const data = await this.request(
|
|
357
|
+
`/api/protocol/knowledge${qs}`,
|
|
358
|
+
"Failed to list knowledge"
|
|
359
|
+
);
|
|
360
|
+
return data.knowledge;
|
|
361
|
+
},
|
|
362
|
+
/**
|
|
363
|
+
* Fetch a single knowledge item by its Studio ID. The caller must
|
|
364
|
+
* have read access to the knowledge's owning org (same-org or
|
|
365
|
+
* publicReadable).
|
|
366
|
+
*/
|
|
367
|
+
get: async (id) => {
|
|
368
|
+
const data = await this.request(
|
|
369
|
+
`/api/protocol/knowledge/${encodeURIComponent(id)}`,
|
|
370
|
+
"Failed to fetch knowledge"
|
|
371
|
+
);
|
|
372
|
+
return data.knowledge;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
341
375
|
this.studioUrl = config.studioUrl.replace(/\/+$/, "");
|
|
342
376
|
this.token = config.token;
|
|
343
377
|
this.timeout = config.timeout || 1e4;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts","../src/llm.ts"],"sourcesContent":["/**\n * FirstStep SDK\n *\n * SDK for building protocol handlers that integrate with FirstStep Studio.\n *\n * @example\n * ```typescript\n * import { ProtocolHandler, ProtocolRequest, ProtocolResponse, ProtocolContext } from '@firststep-studio/sdk';\n *\n * class MyHandler implements ProtocolHandler {\n * async handleMessage(request: ProtocolRequest, context: ProtocolContext): Promise<ProtocolResponse> {\n * // Your implementation\n * }\n *\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * }\n * }\n * ```\n */\n\n// Core types\nexport type {\n ProtocolRequest,\n ProtocolResponse,\n SessionStatus,\n HandoffInboundContext,\n} from './types';\n\n// Form types\nexport type {\n ProtocolForm,\n ProtocolFormField,\n ProtocolFormOption,\n ProtocolFieldValidation,\n} from './types';\n\n// Streaming types\nexport type {\n ProtocolStreamChunk,\n ProtocolError,\n} from './types';\n\n// Handler interface\nexport type {\n ProtocolHandler,\n ProtocolCapabilities,\n HandlerInfo,\n} from './types';\n\n// Context types\nexport type {\n ProtocolContext,\n SessionContext,\n SessionState,\n ChatMessage,\n KnowledgeContext,\n KnowledgeResult,\n KnowledgeMetadata,\n KnowledgeColumnMetadata,\n IntegrationContext,\n IntegrationResult,\n HelplineSearchOptions,\n HelplineResult,\n Helpline,\n StorageContext,\n StorageSetOptions,\n LoggerContext,\n RoutingDecision,\n DeploymentInfo,\n ChatbotInfo,\n ClassifierConfig,\n} from './types';\n\n// ============================================\n// Platform Standard Data Types\n// ============================================\n\nexport type {\n // Form Data\n FormData,\n FormFieldValue,\n\n // Routing Logs\n RoutingLog,\n\n // Session Metadata\n SessionMetadata,\n\n // Analytics\n AnalyticsContext,\n InteractionEvent,\n InteractionEventType,\n\n // Form Schema (Protocol Registration)\n FormSchema,\n FormFieldDefinition,\n FormFieldType,\n\n // Protocol Registration\n ProtocolRegistration,\n} from './types';\n\n// ============================================\n// UCP (Universal Classification Protocol)\n// ============================================\n\n// UCP Types\nexport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './ucp';\n\n// UCP Client\nexport {\n UCPClient,\n UCPError,\n createUCPClient,\n classifyWithUCP,\n} from './ucp';\n\n// ============================================\n// Render Markers (Rich Content)\n// ============================================\n\nexport {\n renderMarkers,\n MARKER_TYPES,\n} from './renderMarkers';\n\nexport type {\n MarkerType,\n HelplineCardPayload,\n HelplineCardItem,\n EmergencyPayload,\n ResourceCardPayload,\n ResourceCardItem,\n ProviderCardPayload,\n ProviderCardItem,\n SafetyPlanPayload,\n SafetyPlanSection,\n ReportCardPayload,\n} from './renderMarkers';\n\n// ============================================\n// Stream Metadata (Dashboard Persistence)\n// ============================================\n\nexport {\n streamMetadata,\n} from './streamMetadata';\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n} from './streamMetadata';\n\n// Handoff types (from types.ts)\nexport type {\n HandoffContext,\n HandoffOptions,\n} from './types';\n\n// ============================================\n// Platform Client (Handler -> Studio)\n// ============================================\n\nexport {\n PlatformClient,\n PlatformError,\n createPlatformClient,\n} from './platform';\n\nexport type {\n PlatformClientConfig,\n PlatformChatbot,\n} from './platform';\n\n// ============================================\n// Auth Utilities\n// ============================================\n\nexport {\n verifyRequestSignature,\n createRequestSignature,\n createAuthHeader,\n isValidToken,\n} from './auth';\n\n// ============================================\n// LLM Client (Handler -> Studio /api/llm/*)\n// ============================================\n\nexport {\n LLMClient,\n LLMError,\n createLLMClient,\n} from './llm';\n\nexport type {\n LLMClientConfig,\n LLMGenerateRequest,\n LLMGenerateResponse,\n LLMContent,\n LLMPart,\n LLMGenerationConfig,\n LLMSafetySetting,\n LLMUsageMetadata,\n LLMCandidate,\n LLMCacheCreateRequest,\n LLMCachedContent,\n} from './llm';\n","/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n 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","/**\n * LLM Client\n *\n * SDK client for calling Studio's `/api/llm/*` proxy routes. Studio forwards\n * the request body 1:1 to Google's Generative Language REST API, so the\n * shapes here mirror Google's REST shape (NOT the @google/genai SDK shape).\n *\n * Reference: https://ai.google.dev/api/generate-content\n *\n * @example\n * ```typescript\n * import { createLLMClient } from '@firststep-studio/sdk';\n *\n * const llm = createLLMClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN!,\n * });\n *\n * // Single-shot\n * const res = await llm.generate({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],\n * generationConfig: { temperature: 0.7, maxOutputTokens: 500 },\n * });\n * console.log(res.candidates[0].content.parts[0].text);\n *\n * // Streaming\n * for await (const chunk of llm.generateStream({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Count to 5' }] }],\n * })) {\n * process.stdout.write(chunk.candidates?.[0]?.content?.parts?.[0]?.text ?? '');\n * }\n *\n * // Cache\n * const cache = await llm.cache.create({\n * model: 'models/gemini-2.5-flash',\n * systemInstruction: { parts: [{ text: 'long system prompt...' }] },\n * ttl: '3600s',\n * });\n * await llm.cache.get(cache.name);\n * await llm.cache.delete(cache.name);\n * ```\n */\n\n// ============================================\n// Types — Google REST API shape (kept loose for forward-compat)\n// ============================================\n\nexport interface LLMClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (Bearer fst_<...>) */\n token: string;\n /** Request timeout in ms for non-streaming calls (default: 60000) */\n timeout?: number;\n}\n\nexport interface LLMPart {\n text?: string;\n inlineData?: { mimeType: string; data: string };\n [k: string]: unknown;\n}\n\nexport interface LLMContent {\n role?: 'user' | 'model';\n parts: LLMPart[];\n}\n\nexport interface LLMGenerationConfig {\n temperature?: number;\n maxOutputTokens?: number;\n topP?: number;\n topK?: number;\n responseMimeType?: string;\n responseSchema?: Record<string, unknown>;\n thinkingConfig?: {\n thinkingBudget?: number;\n thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';\n includeThoughts?: boolean;\n };\n stopSequences?: string[];\n [k: string]: unknown;\n}\n\nexport interface LLMSafetySetting {\n category: string;\n threshold: string;\n}\n\n/**\n * Request body for /api/llm/generate and /api/llm/generate/stream.\n * Mirrors Google's `:generateContent` REST request, plus `model` at the top level\n * so Studio can build the URL path.\n */\nexport interface LLMGenerateRequest {\n model: string;\n contents: LLMContent[];\n systemInstruction?: { parts: LLMPart[] };\n generationConfig?: LLMGenerationConfig;\n safetySettings?: LLMSafetySetting[];\n /** Resource name of an explicit cache, e.g. \"cachedContents/abc123\" */\n cachedContent?: string;\n /** Function-calling tools (forwarded as-is) */\n tools?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMUsageMetadata {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n cachedContentTokenCount?: number;\n thoughtsTokenCount?: number;\n totalTokenCount?: number;\n [k: string]: unknown;\n}\n\nexport interface LLMCandidate {\n content?: LLMContent;\n finishReason?: string;\n index?: number;\n safetyRatings?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMGenerateResponse {\n candidates?: LLMCandidate[];\n usageMetadata?: LLMUsageMetadata;\n promptFeedback?: { blockReason?: string; [k: string]: unknown };\n modelVersion?: string;\n responseId?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCacheCreateRequest {\n model: string;\n displayName?: string;\n systemInstruction?: { parts: LLMPart[] };\n contents?: LLMContent[];\n /** Time-to-live, e.g. \"3600s\" */\n ttl?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCachedContent {\n name: string;\n model: string;\n createTime?: string;\n updateTime?: string;\n expireTime?: string;\n displayName?: string;\n usageMetadata?: { totalTokenCount?: number };\n [k: string]: unknown;\n}\n\nexport class LLMError extends Error {\n status: number;\n body: unknown;\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = 'LLMError';\n this.status = status;\n this.body = body;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class LLMClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: LLMClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout ?? 60000;\n }\n\n private headers(): Record<string, string> {\n return {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Single-shot generation. Body is forwarded to Google's :generateContent\n * unchanged. Response is Google's response shape unchanged.\n */\n async generate(req: LLMGenerateRequest): Promise<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n\n if (!res.ok) {\n throw new LLMError(`LLM generate failed: ${res.status}`, res.status, body);\n }\n return body as LLMGenerateResponse;\n }\n\n /**\n * Streaming generation as an AsyncGenerator of partial responses.\n * Each yielded chunk has the same shape as a non-streaming response (a\n * `candidates[]` slice with the next text fragment, plus usageMetadata\n * on the final chunk).\n *\n * Note: timeout option is not enforced for streams; consumer should\n * abort externally if needed.\n */\n async *generateStream(req: LLMGenerateRequest): AsyncGenerator<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate/stream`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n });\n\n if (!res.ok || !res.body) {\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM generateStream failed: ${res.status}`, res.status, body);\n }\n\n // Consume SSE: lines like `data: {json}\\n\\n`. We pop full lines off a\n // rolling buffer and yield each parsed `data:` payload.\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n const t = line.trim();\n if (!t.startsWith('data: ')) continue;\n const payload = t.slice(6);\n if (payload === '[DONE]') continue;\n try {\n yield JSON.parse(payload) as LLMGenerateResponse;\n } catch {\n // Skip unparsable chunk; SSE may emit stray comments / heartbeats.\n }\n }\n }\n\n // Flush any final buffered line that didn't end in \\n.\n const t = buffer.trim();\n if (t.startsWith('data: ')) {\n const payload = t.slice(6);\n if (payload && payload !== '[DONE]') {\n try { yield JSON.parse(payload) as LLMGenerateResponse; } catch { /* noop */ }\n }\n }\n } finally {\n try { reader.releaseLock(); } catch { /* noop */ }\n }\n }\n\n /**\n * Explicit prompt cache surface. Maps to Google's `/cachedContents` REST API.\n */\n cache = {\n /**\n * Create an explicit cache. Returns the resource (use `cache.name` as the\n * `cachedContent` field on a future generate() request).\n */\n create: async (req: LLMCacheCreateRequest): Promise<LLMCachedContent> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.create failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Fetch a cache by resource name (e.g. \"cachedContents/abc123\").\n * Returns null when the cache is not found (404), which is the common\n * \"expired\" signal — callers can simply recreate.\n */\n get: async (name: string): Promise<LLMCachedContent | null> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'GET',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.status === 404) return null;\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.get failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Delete a cache by resource name. Idempotent: 404 is treated as success.\n */\n delete: async (name: string): Promise<void> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'DELETE',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.ok || res.status === 404) return;\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM cache.delete failed: ${res.status}`, res.status, body);\n },\n };\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create an LLMClient that talks to Studio's /api/llm/* proxy routes.\n */\nexport function createLLMClient(config: LLMClientConfig): LLMClient {\n return new LLMClient(config);\n}\n"],"mappings":"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","names":["t"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts","../src/llm.ts"],"sourcesContent":["/**\n * FirstStep SDK\n *\n * SDK for building protocol handlers that integrate with FirstStep Studio.\n *\n * @example\n * ```typescript\n * import { ProtocolHandler, ProtocolRequest, ProtocolResponse, ProtocolContext } from '@firststep-studio/sdk';\n *\n * class MyHandler implements ProtocolHandler {\n * async handleMessage(request: ProtocolRequest, context: ProtocolContext): Promise<ProtocolResponse> {\n * // Your implementation\n * }\n *\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * }\n * }\n * ```\n */\n\n// Core types\nexport type {\n ProtocolRequest,\n ProtocolResponse,\n SessionStatus,\n HandoffInboundContext,\n} from './types';\n\n// Form types\nexport type {\n ProtocolForm,\n ProtocolFormField,\n ProtocolFormOption,\n ProtocolFieldValidation,\n} from './types';\n\n// Streaming types\nexport type {\n ProtocolStreamChunk,\n ProtocolError,\n} from './types';\n\n// Handler interface\nexport type {\n ProtocolHandler,\n ProtocolCapabilities,\n HandlerInfo,\n} from './types';\n\n// Context types\nexport type {\n ProtocolContext,\n SessionContext,\n SessionState,\n ChatMessage,\n KnowledgeContext,\n KnowledgeResult,\n KnowledgeMetadata,\n KnowledgeColumnMetadata,\n IntegrationContext,\n IntegrationResult,\n HelplineSearchOptions,\n HelplineResult,\n Helpline,\n StorageContext,\n StorageSetOptions,\n LoggerContext,\n RoutingDecision,\n DeploymentInfo,\n ChatbotInfo,\n ClassifierConfig,\n} from './types';\n\n// ============================================\n// Platform Standard Data Types\n// ============================================\n\nexport type {\n // Form Data\n FormData,\n FormFieldValue,\n\n // Routing Logs\n RoutingLog,\n\n // Session Metadata\n SessionMetadata,\n\n // Analytics\n AnalyticsContext,\n InteractionEvent,\n InteractionEventType,\n\n // Form Schema (Protocol Registration)\n FormSchema,\n FormFieldDefinition,\n FormFieldType,\n\n // Protocol Registration\n ProtocolRegistration,\n} from './types';\n\n// ============================================\n// UCP (Universal Classification Protocol)\n// ============================================\n\n// UCP Types\nexport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './ucp';\n\n// UCP Client\nexport {\n UCPClient,\n UCPError,\n createUCPClient,\n classifyWithUCP,\n} from './ucp';\n\n// ============================================\n// Render Markers (Rich Content)\n// ============================================\n\nexport {\n renderMarkers,\n MARKER_TYPES,\n} from './renderMarkers';\n\nexport type {\n MarkerType,\n HelplineCardPayload,\n HelplineCardItem,\n EmergencyPayload,\n ResourceCardPayload,\n ResourceCardItem,\n ProviderCardPayload,\n ProviderCardItem,\n SafetyPlanPayload,\n SafetyPlanSection,\n ReportCardPayload,\n} from './renderMarkers';\n\n// ============================================\n// Stream Metadata (Dashboard Persistence)\n// ============================================\n\nexport {\n streamMetadata,\n} from './streamMetadata';\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n} from './streamMetadata';\n\n// Handoff types (from types.ts)\nexport type {\n HandoffContext,\n HandoffOptions,\n} from './types';\n\n// ============================================\n// Platform Client (Handler -> Studio)\n// ============================================\n\nexport {\n PlatformClient,\n PlatformError,\n createPlatformClient,\n} from './platform';\n\nexport type {\n PlatformClientConfig,\n PlatformChatbot,\n PlatformKnowledge,\n PlatformKnowledgeSummary,\n ListKnowledgeOptions,\n} from './platform';\n\n// ============================================\n// Auth Utilities\n// ============================================\n\nexport {\n verifyRequestSignature,\n createRequestSignature,\n createAuthHeader,\n isValidToken,\n} from './auth';\n\n// ============================================\n// LLM Client (Handler -> Studio /api/llm/*)\n// ============================================\n\nexport {\n LLMClient,\n LLMError,\n createLLMClient,\n} from './llm';\n\nexport type {\n LLMClientConfig,\n LLMGenerateRequest,\n LLMGenerateResponse,\n LLMContent,\n LLMPart,\n LLMGenerationConfig,\n LLMSafetySetting,\n LLMUsageMetadata,\n LLMCandidate,\n LLMCacheCreateRequest,\n LLMCachedContent,\n} from './llm';\n","/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n 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 interface PlatformKnowledgeSummary {\n /** Studio knowledge MongoDB ID */\n id: string;\n /** Knowledge name */\n name: string;\n /** Optional description */\n description?: string;\n /** Knowledge type (e.g. 'text', 'database') */\n type?: string;\n /** Owning organization ID */\n organizationId: string;\n /** Timestamps */\n createdAt?: number | string;\n updatedAt?: number | string;\n}\n\n/**\n * Full knowledge document (shape mirrors the underlying mongoose doc).\n * Kept loose because the schema is large and varies by knowledge `type`;\n * narrow types can be added incrementally as callers need them.\n */\nexport type PlatformKnowledge = PlatformKnowledgeSummary & Record<string, unknown>;\n\nexport interface ListKnowledgeOptions {\n /**\n * Target organization to list knowledge from. If omitted, defaults to\n * the org associated with the SDK token. Use this to read from a\n * different org (e.g. a platform-operated shared library marked\n * `publicReadable` in Studio).\n */\n orgId?: string;\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 * Knowledge / resource surface.\n *\n * Reads are allowed when the SDK token's org matches the target org,\n * OR when the target org has `publicReadable: true` on the Studio side.\n * Writes are not yet exposed here — those still go through the user-\n * authenticated /api/knowledge routes from the Studio UI.\n */\n knowledge = {\n /**\n * List knowledge items for an organization.\n * Defaults to the SDK token's own org if `orgId` is omitted.\n */\n list: async (options: ListKnowledgeOptions = {}): Promise<PlatformKnowledgeSummary[]> => {\n const qs = options.orgId ? `?organizationId=${encodeURIComponent(options.orgId)}` : '';\n const data = await this.request<{ knowledge: PlatformKnowledgeSummary[] }>(\n `/api/protocol/knowledge${qs}`,\n 'Failed to list knowledge'\n );\n return data.knowledge;\n },\n\n /**\n * Fetch a single knowledge item by its Studio ID. The caller must\n * have read access to the knowledge's owning org (same-org or\n * publicReadable).\n */\n get: async (id: string): Promise<PlatformKnowledge> => {\n const data = await this.request<{ knowledge: PlatformKnowledge }>(\n `/api/protocol/knowledge/${encodeURIComponent(id)}`,\n 'Failed to fetch knowledge'\n );\n return data.knowledge;\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","/**\n * LLM Client\n *\n * SDK client for calling Studio's `/api/llm/*` proxy routes. Studio forwards\n * the request body 1:1 to Google's Generative Language REST API, so the\n * shapes here mirror Google's REST shape (NOT the @google/genai SDK shape).\n *\n * Reference: https://ai.google.dev/api/generate-content\n *\n * @example\n * ```typescript\n * import { createLLMClient } from '@firststep-studio/sdk';\n *\n * const llm = createLLMClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN!,\n * });\n *\n * // Single-shot\n * const res = await llm.generate({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],\n * generationConfig: { temperature: 0.7, maxOutputTokens: 500 },\n * });\n * console.log(res.candidates[0].content.parts[0].text);\n *\n * // Streaming\n * for await (const chunk of llm.generateStream({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Count to 5' }] }],\n * })) {\n * process.stdout.write(chunk.candidates?.[0]?.content?.parts?.[0]?.text ?? '');\n * }\n *\n * // Cache\n * const cache = await llm.cache.create({\n * model: 'models/gemini-2.5-flash',\n * systemInstruction: { parts: [{ text: 'long system prompt...' }] },\n * ttl: '3600s',\n * });\n * await llm.cache.get(cache.name);\n * await llm.cache.delete(cache.name);\n * ```\n */\n\n// ============================================\n// Types — Google REST API shape (kept loose for forward-compat)\n// ============================================\n\nexport interface LLMClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (Bearer fst_<...>) */\n token: string;\n /** Request timeout in ms for non-streaming calls (default: 60000) */\n timeout?: number;\n}\n\nexport interface LLMPart {\n text?: string;\n inlineData?: { mimeType: string; data: string };\n [k: string]: unknown;\n}\n\nexport interface LLMContent {\n role?: 'user' | 'model';\n parts: LLMPart[];\n}\n\nexport interface LLMGenerationConfig {\n temperature?: number;\n maxOutputTokens?: number;\n topP?: number;\n topK?: number;\n responseMimeType?: string;\n responseSchema?: Record<string, unknown>;\n thinkingConfig?: {\n thinkingBudget?: number;\n thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';\n includeThoughts?: boolean;\n };\n stopSequences?: string[];\n [k: string]: unknown;\n}\n\nexport interface LLMSafetySetting {\n category: string;\n threshold: string;\n}\n\n/**\n * Request body for /api/llm/generate and /api/llm/generate/stream.\n * Mirrors Google's `:generateContent` REST request, plus `model` at the top level\n * so Studio can build the URL path.\n */\nexport interface LLMGenerateRequest {\n model: string;\n contents: LLMContent[];\n systemInstruction?: { parts: LLMPart[] };\n generationConfig?: LLMGenerationConfig;\n safetySettings?: LLMSafetySetting[];\n /** Resource name of an explicit cache, e.g. \"cachedContents/abc123\" */\n cachedContent?: string;\n /** Function-calling tools (forwarded as-is) */\n tools?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMUsageMetadata {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n cachedContentTokenCount?: number;\n thoughtsTokenCount?: number;\n totalTokenCount?: number;\n [k: string]: unknown;\n}\n\nexport interface LLMCandidate {\n content?: LLMContent;\n finishReason?: string;\n index?: number;\n safetyRatings?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMGenerateResponse {\n candidates?: LLMCandidate[];\n usageMetadata?: LLMUsageMetadata;\n promptFeedback?: { blockReason?: string; [k: string]: unknown };\n modelVersion?: string;\n responseId?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCacheCreateRequest {\n model: string;\n displayName?: string;\n systemInstruction?: { parts: LLMPart[] };\n contents?: LLMContent[];\n /** Time-to-live, e.g. \"3600s\" */\n ttl?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCachedContent {\n name: string;\n model: string;\n createTime?: string;\n updateTime?: string;\n expireTime?: string;\n displayName?: string;\n usageMetadata?: { totalTokenCount?: number };\n [k: string]: unknown;\n}\n\nexport class LLMError extends Error {\n status: number;\n body: unknown;\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = 'LLMError';\n this.status = status;\n this.body = body;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class LLMClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: LLMClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout ?? 60000;\n }\n\n private headers(): Record<string, string> {\n return {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Single-shot generation. Body is forwarded to Google's :generateContent\n * unchanged. Response is Google's response shape unchanged.\n */\n async generate(req: LLMGenerateRequest): Promise<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n\n if (!res.ok) {\n throw new LLMError(`LLM generate failed: ${res.status}`, res.status, body);\n }\n return body as LLMGenerateResponse;\n }\n\n /**\n * Streaming generation as an AsyncGenerator of partial responses.\n * Each yielded chunk has the same shape as a non-streaming response (a\n * `candidates[]` slice with the next text fragment, plus usageMetadata\n * on the final chunk).\n *\n * Note: timeout option is not enforced for streams; consumer should\n * abort externally if needed.\n */\n async *generateStream(req: LLMGenerateRequest): AsyncGenerator<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate/stream`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n });\n\n if (!res.ok || !res.body) {\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM generateStream failed: ${res.status}`, res.status, body);\n }\n\n // Consume SSE: lines like `data: {json}\\n\\n`. We pop full lines off a\n // rolling buffer and yield each parsed `data:` payload.\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n const t = line.trim();\n if (!t.startsWith('data: ')) continue;\n const payload = t.slice(6);\n if (payload === '[DONE]') continue;\n try {\n yield JSON.parse(payload) as LLMGenerateResponse;\n } catch {\n // Skip unparsable chunk; SSE may emit stray comments / heartbeats.\n }\n }\n }\n\n // Flush any final buffered line that didn't end in \\n.\n const t = buffer.trim();\n if (t.startsWith('data: ')) {\n const payload = t.slice(6);\n if (payload && payload !== '[DONE]') {\n try { yield JSON.parse(payload) as LLMGenerateResponse; } catch { /* noop */ }\n }\n }\n } finally {\n try { reader.releaseLock(); } catch { /* noop */ }\n }\n }\n\n /**\n * Explicit prompt cache surface. Maps to Google's `/cachedContents` REST API.\n */\n cache = {\n /**\n * Create an explicit cache. Returns the resource (use `cache.name` as the\n * `cachedContent` field on a future generate() request).\n */\n create: async (req: LLMCacheCreateRequest): Promise<LLMCachedContent> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.create failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Fetch a cache by resource name (e.g. \"cachedContents/abc123\").\n * Returns null when the cache is not found (404), which is the common\n * \"expired\" signal — callers can simply recreate.\n */\n get: async (name: string): Promise<LLMCachedContent | null> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'GET',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.status === 404) return null;\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.get failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Delete a cache by resource name. Idempotent: 404 is treated as success.\n */\n delete: async (name: string): Promise<void> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'DELETE',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.ok || res.status === 404) return;\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM cache.delete failed: ${res.status}`, res.status, body);\n },\n };\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create an LLMClient that talks to Studio's /api/llm/* proxy routes.\n */\nexport function createLLMClient(config: LLMClientConfig): LLMClient {\n return new LLMClient(config);\n}\n"],"mappings":"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","names":["t"]}
|
package/dist/index.mjs
CHANGED
|
@@ -296,6 +296,40 @@ var PlatformError = class extends Error {
|
|
|
296
296
|
};
|
|
297
297
|
var PlatformClient = class {
|
|
298
298
|
constructor(config) {
|
|
299
|
+
/**
|
|
300
|
+
* Knowledge / resource surface.
|
|
301
|
+
*
|
|
302
|
+
* Reads are allowed when the SDK token's org matches the target org,
|
|
303
|
+
* OR when the target org has `publicReadable: true` on the Studio side.
|
|
304
|
+
* Writes are not yet exposed here — those still go through the user-
|
|
305
|
+
* authenticated /api/knowledge routes from the Studio UI.
|
|
306
|
+
*/
|
|
307
|
+
this.knowledge = {
|
|
308
|
+
/**
|
|
309
|
+
* List knowledge items for an organization.
|
|
310
|
+
* Defaults to the SDK token's own org if `orgId` is omitted.
|
|
311
|
+
*/
|
|
312
|
+
list: async (options = {}) => {
|
|
313
|
+
const qs = options.orgId ? `?organizationId=${encodeURIComponent(options.orgId)}` : "";
|
|
314
|
+
const data = await this.request(
|
|
315
|
+
`/api/protocol/knowledge${qs}`,
|
|
316
|
+
"Failed to list knowledge"
|
|
317
|
+
);
|
|
318
|
+
return data.knowledge;
|
|
319
|
+
},
|
|
320
|
+
/**
|
|
321
|
+
* Fetch a single knowledge item by its Studio ID. The caller must
|
|
322
|
+
* have read access to the knowledge's owning org (same-org or
|
|
323
|
+
* publicReadable).
|
|
324
|
+
*/
|
|
325
|
+
get: async (id) => {
|
|
326
|
+
const data = await this.request(
|
|
327
|
+
`/api/protocol/knowledge/${encodeURIComponent(id)}`,
|
|
328
|
+
"Failed to fetch knowledge"
|
|
329
|
+
);
|
|
330
|
+
return data.knowledge;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
299
333
|
this.studioUrl = config.studioUrl.replace(/\/+$/, "");
|
|
300
334
|
this.token = config.token;
|
|
301
335
|
this.timeout = config.timeout || 1e4;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts","../src/llm.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","/**\n * LLM Client\n *\n * SDK client for calling Studio's `/api/llm/*` proxy routes. Studio forwards\n * the request body 1:1 to Google's Generative Language REST API, so the\n * shapes here mirror Google's REST shape (NOT the @google/genai SDK shape).\n *\n * Reference: https://ai.google.dev/api/generate-content\n *\n * @example\n * ```typescript\n * import { createLLMClient } from '@firststep-studio/sdk';\n *\n * const llm = createLLMClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN!,\n * });\n *\n * // Single-shot\n * const res = await llm.generate({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],\n * generationConfig: { temperature: 0.7, maxOutputTokens: 500 },\n * });\n * console.log(res.candidates[0].content.parts[0].text);\n *\n * // Streaming\n * for await (const chunk of llm.generateStream({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Count to 5' }] }],\n * })) {\n * process.stdout.write(chunk.candidates?.[0]?.content?.parts?.[0]?.text ?? '');\n * }\n *\n * // Cache\n * const cache = await llm.cache.create({\n * model: 'models/gemini-2.5-flash',\n * systemInstruction: { parts: [{ text: 'long system prompt...' }] },\n * ttl: '3600s',\n * });\n * await llm.cache.get(cache.name);\n * await llm.cache.delete(cache.name);\n * ```\n */\n\n// ============================================\n// Types — Google REST API shape (kept loose for forward-compat)\n// ============================================\n\nexport interface LLMClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (Bearer fst_<...>) */\n token: string;\n /** Request timeout in ms for non-streaming calls (default: 60000) */\n timeout?: number;\n}\n\nexport interface LLMPart {\n text?: string;\n inlineData?: { mimeType: string; data: string };\n [k: string]: unknown;\n}\n\nexport interface LLMContent {\n role?: 'user' | 'model';\n parts: LLMPart[];\n}\n\nexport interface LLMGenerationConfig {\n temperature?: number;\n maxOutputTokens?: number;\n topP?: number;\n topK?: number;\n responseMimeType?: string;\n responseSchema?: Record<string, unknown>;\n thinkingConfig?: {\n thinkingBudget?: number;\n thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';\n includeThoughts?: boolean;\n };\n stopSequences?: string[];\n [k: string]: unknown;\n}\n\nexport interface LLMSafetySetting {\n category: string;\n threshold: string;\n}\n\n/**\n * Request body for /api/llm/generate and /api/llm/generate/stream.\n * Mirrors Google's `:generateContent` REST request, plus `model` at the top level\n * so Studio can build the URL path.\n */\nexport interface LLMGenerateRequest {\n model: string;\n contents: LLMContent[];\n systemInstruction?: { parts: LLMPart[] };\n generationConfig?: LLMGenerationConfig;\n safetySettings?: LLMSafetySetting[];\n /** Resource name of an explicit cache, e.g. \"cachedContents/abc123\" */\n cachedContent?: string;\n /** Function-calling tools (forwarded as-is) */\n tools?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMUsageMetadata {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n cachedContentTokenCount?: number;\n thoughtsTokenCount?: number;\n totalTokenCount?: number;\n [k: string]: unknown;\n}\n\nexport interface LLMCandidate {\n content?: LLMContent;\n finishReason?: string;\n index?: number;\n safetyRatings?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMGenerateResponse {\n candidates?: LLMCandidate[];\n usageMetadata?: LLMUsageMetadata;\n promptFeedback?: { blockReason?: string; [k: string]: unknown };\n modelVersion?: string;\n responseId?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCacheCreateRequest {\n model: string;\n displayName?: string;\n systemInstruction?: { parts: LLMPart[] };\n contents?: LLMContent[];\n /** Time-to-live, e.g. \"3600s\" */\n ttl?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCachedContent {\n name: string;\n model: string;\n createTime?: string;\n updateTime?: string;\n expireTime?: string;\n displayName?: string;\n usageMetadata?: { totalTokenCount?: number };\n [k: string]: unknown;\n}\n\nexport class LLMError extends Error {\n status: number;\n body: unknown;\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = 'LLMError';\n this.status = status;\n this.body = body;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class LLMClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: LLMClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout ?? 60000;\n }\n\n private headers(): Record<string, string> {\n return {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Single-shot generation. Body is forwarded to Google's :generateContent\n * unchanged. Response is Google's response shape unchanged.\n */\n async generate(req: LLMGenerateRequest): Promise<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n\n if (!res.ok) {\n throw new LLMError(`LLM generate failed: ${res.status}`, res.status, body);\n }\n return body as LLMGenerateResponse;\n }\n\n /**\n * Streaming generation as an AsyncGenerator of partial responses.\n * Each yielded chunk has the same shape as a non-streaming response (a\n * `candidates[]` slice with the next text fragment, plus usageMetadata\n * on the final chunk).\n *\n * Note: timeout option is not enforced for streams; consumer should\n * abort externally if needed.\n */\n async *generateStream(req: LLMGenerateRequest): AsyncGenerator<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate/stream`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n });\n\n if (!res.ok || !res.body) {\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM generateStream failed: ${res.status}`, res.status, body);\n }\n\n // Consume SSE: lines like `data: {json}\\n\\n`. We pop full lines off a\n // rolling buffer and yield each parsed `data:` payload.\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n const t = line.trim();\n if (!t.startsWith('data: ')) continue;\n const payload = t.slice(6);\n if (payload === '[DONE]') continue;\n try {\n yield JSON.parse(payload) as LLMGenerateResponse;\n } catch {\n // Skip unparsable chunk; SSE may emit stray comments / heartbeats.\n }\n }\n }\n\n // Flush any final buffered line that didn't end in \\n.\n const t = buffer.trim();\n if (t.startsWith('data: ')) {\n const payload = t.slice(6);\n if (payload && payload !== '[DONE]') {\n try { yield JSON.parse(payload) as LLMGenerateResponse; } catch { /* noop */ }\n }\n }\n } finally {\n try { reader.releaseLock(); } catch { /* noop */ }\n }\n }\n\n /**\n * Explicit prompt cache surface. Maps to Google's `/cachedContents` REST API.\n */\n cache = {\n /**\n * Create an explicit cache. Returns the resource (use `cache.name` as the\n * `cachedContent` field on a future generate() request).\n */\n create: async (req: LLMCacheCreateRequest): Promise<LLMCachedContent> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.create failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Fetch a cache by resource name (e.g. \"cachedContents/abc123\").\n * Returns null when the cache is not found (404), which is the common\n * \"expired\" signal — callers can simply recreate.\n */\n get: async (name: string): Promise<LLMCachedContent | null> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'GET',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.status === 404) return null;\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.get failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Delete a cache by resource name. Idempotent: 404 is treated as success.\n */\n delete: async (name: string): Promise<void> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'DELETE',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.ok || res.status === 404) return;\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM cache.delete failed: ${res.status}`, res.status, body);\n },\n };\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create an LLMClient that talks to Studio's /api/llm/* proxy routes.\n */\nexport function createLLMClient(config: LLMClientConfig): LLMClient {\n return new LLMClient(config);\n}\n"],"mappings":"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","names":["t"]}
|
|
1
|
+
{"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts","../src/llm.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 interface PlatformKnowledgeSummary {\n /** Studio knowledge MongoDB ID */\n id: string;\n /** Knowledge name */\n name: string;\n /** Optional description */\n description?: string;\n /** Knowledge type (e.g. 'text', 'database') */\n type?: string;\n /** Owning organization ID */\n organizationId: string;\n /** Timestamps */\n createdAt?: number | string;\n updatedAt?: number | string;\n}\n\n/**\n * Full knowledge document (shape mirrors the underlying mongoose doc).\n * Kept loose because the schema is large and varies by knowledge `type`;\n * narrow types can be added incrementally as callers need them.\n */\nexport type PlatformKnowledge = PlatformKnowledgeSummary & Record<string, unknown>;\n\nexport interface ListKnowledgeOptions {\n /**\n * Target organization to list knowledge from. If omitted, defaults to\n * the org associated with the SDK token. Use this to read from a\n * different org (e.g. a platform-operated shared library marked\n * `publicReadable` in Studio).\n */\n orgId?: string;\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 * Knowledge / resource surface.\n *\n * Reads are allowed when the SDK token's org matches the target org,\n * OR when the target org has `publicReadable: true` on the Studio side.\n * Writes are not yet exposed here — those still go through the user-\n * authenticated /api/knowledge routes from the Studio UI.\n */\n knowledge = {\n /**\n * List knowledge items for an organization.\n * Defaults to the SDK token's own org if `orgId` is omitted.\n */\n list: async (options: ListKnowledgeOptions = {}): Promise<PlatformKnowledgeSummary[]> => {\n const qs = options.orgId ? `?organizationId=${encodeURIComponent(options.orgId)}` : '';\n const data = await this.request<{ knowledge: PlatformKnowledgeSummary[] }>(\n `/api/protocol/knowledge${qs}`,\n 'Failed to list knowledge'\n );\n return data.knowledge;\n },\n\n /**\n * Fetch a single knowledge item by its Studio ID. The caller must\n * have read access to the knowledge's owning org (same-org or\n * publicReadable).\n */\n get: async (id: string): Promise<PlatformKnowledge> => {\n const data = await this.request<{ knowledge: PlatformKnowledge }>(\n `/api/protocol/knowledge/${encodeURIComponent(id)}`,\n 'Failed to fetch knowledge'\n );\n return data.knowledge;\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","/**\n * LLM Client\n *\n * SDK client for calling Studio's `/api/llm/*` proxy routes. Studio forwards\n * the request body 1:1 to Google's Generative Language REST API, so the\n * shapes here mirror Google's REST shape (NOT the @google/genai SDK shape).\n *\n * Reference: https://ai.google.dev/api/generate-content\n *\n * @example\n * ```typescript\n * import { createLLMClient } from '@firststep-studio/sdk';\n *\n * const llm = createLLMClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN!,\n * });\n *\n * // Single-shot\n * const res = await llm.generate({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],\n * generationConfig: { temperature: 0.7, maxOutputTokens: 500 },\n * });\n * console.log(res.candidates[0].content.parts[0].text);\n *\n * // Streaming\n * for await (const chunk of llm.generateStream({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Count to 5' }] }],\n * })) {\n * process.stdout.write(chunk.candidates?.[0]?.content?.parts?.[0]?.text ?? '');\n * }\n *\n * // Cache\n * const cache = await llm.cache.create({\n * model: 'models/gemini-2.5-flash',\n * systemInstruction: { parts: [{ text: 'long system prompt...' }] },\n * ttl: '3600s',\n * });\n * await llm.cache.get(cache.name);\n * await llm.cache.delete(cache.name);\n * ```\n */\n\n// ============================================\n// Types — Google REST API shape (kept loose for forward-compat)\n// ============================================\n\nexport interface LLMClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (Bearer fst_<...>) */\n token: string;\n /** Request timeout in ms for non-streaming calls (default: 60000) */\n timeout?: number;\n}\n\nexport interface LLMPart {\n text?: string;\n inlineData?: { mimeType: string; data: string };\n [k: string]: unknown;\n}\n\nexport interface LLMContent {\n role?: 'user' | 'model';\n parts: LLMPart[];\n}\n\nexport interface LLMGenerationConfig {\n temperature?: number;\n maxOutputTokens?: number;\n topP?: number;\n topK?: number;\n responseMimeType?: string;\n responseSchema?: Record<string, unknown>;\n thinkingConfig?: {\n thinkingBudget?: number;\n thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';\n includeThoughts?: boolean;\n };\n stopSequences?: string[];\n [k: string]: unknown;\n}\n\nexport interface LLMSafetySetting {\n category: string;\n threshold: string;\n}\n\n/**\n * Request body for /api/llm/generate and /api/llm/generate/stream.\n * Mirrors Google's `:generateContent` REST request, plus `model` at the top level\n * so Studio can build the URL path.\n */\nexport interface LLMGenerateRequest {\n model: string;\n contents: LLMContent[];\n systemInstruction?: { parts: LLMPart[] };\n generationConfig?: LLMGenerationConfig;\n safetySettings?: LLMSafetySetting[];\n /** Resource name of an explicit cache, e.g. \"cachedContents/abc123\" */\n cachedContent?: string;\n /** Function-calling tools (forwarded as-is) */\n tools?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMUsageMetadata {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n cachedContentTokenCount?: number;\n thoughtsTokenCount?: number;\n totalTokenCount?: number;\n [k: string]: unknown;\n}\n\nexport interface LLMCandidate {\n content?: LLMContent;\n finishReason?: string;\n index?: number;\n safetyRatings?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMGenerateResponse {\n candidates?: LLMCandidate[];\n usageMetadata?: LLMUsageMetadata;\n promptFeedback?: { blockReason?: string; [k: string]: unknown };\n modelVersion?: string;\n responseId?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCacheCreateRequest {\n model: string;\n displayName?: string;\n systemInstruction?: { parts: LLMPart[] };\n contents?: LLMContent[];\n /** Time-to-live, e.g. \"3600s\" */\n ttl?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCachedContent {\n name: string;\n model: string;\n createTime?: string;\n updateTime?: string;\n expireTime?: string;\n displayName?: string;\n usageMetadata?: { totalTokenCount?: number };\n [k: string]: unknown;\n}\n\nexport class LLMError extends Error {\n status: number;\n body: unknown;\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = 'LLMError';\n this.status = status;\n this.body = body;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class LLMClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: LLMClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout ?? 60000;\n }\n\n private headers(): Record<string, string> {\n return {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Single-shot generation. Body is forwarded to Google's :generateContent\n * unchanged. Response is Google's response shape unchanged.\n */\n async generate(req: LLMGenerateRequest): Promise<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n\n if (!res.ok) {\n throw new LLMError(`LLM generate failed: ${res.status}`, res.status, body);\n }\n return body as LLMGenerateResponse;\n }\n\n /**\n * Streaming generation as an AsyncGenerator of partial responses.\n * Each yielded chunk has the same shape as a non-streaming response (a\n * `candidates[]` slice with the next text fragment, plus usageMetadata\n * on the final chunk).\n *\n * Note: timeout option is not enforced for streams; consumer should\n * abort externally if needed.\n */\n async *generateStream(req: LLMGenerateRequest): AsyncGenerator<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate/stream`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n });\n\n if (!res.ok || !res.body) {\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM generateStream failed: ${res.status}`, res.status, body);\n }\n\n // Consume SSE: lines like `data: {json}\\n\\n`. We pop full lines off a\n // rolling buffer and yield each parsed `data:` payload.\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n const t = line.trim();\n if (!t.startsWith('data: ')) continue;\n const payload = t.slice(6);\n if (payload === '[DONE]') continue;\n try {\n yield JSON.parse(payload) as LLMGenerateResponse;\n } catch {\n // Skip unparsable chunk; SSE may emit stray comments / heartbeats.\n }\n }\n }\n\n // Flush any final buffered line that didn't end in \\n.\n const t = buffer.trim();\n if (t.startsWith('data: ')) {\n const payload = t.slice(6);\n if (payload && payload !== '[DONE]') {\n try { yield JSON.parse(payload) as LLMGenerateResponse; } catch { /* noop */ }\n }\n }\n } finally {\n try { reader.releaseLock(); } catch { /* noop */ }\n }\n }\n\n /**\n * Explicit prompt cache surface. Maps to Google's `/cachedContents` REST API.\n */\n cache = {\n /**\n * Create an explicit cache. Returns the resource (use `cache.name` as the\n * `cachedContent` field on a future generate() request).\n */\n create: async (req: LLMCacheCreateRequest): Promise<LLMCachedContent> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.create failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Fetch a cache by resource name (e.g. \"cachedContents/abc123\").\n * Returns null when the cache is not found (404), which is the common\n * \"expired\" signal — callers can simply recreate.\n */\n get: async (name: string): Promise<LLMCachedContent | null> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'GET',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.status === 404) return null;\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.get failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Delete a cache by resource name. Idempotent: 404 is treated as success.\n */\n delete: async (name: string): Promise<void> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'DELETE',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.ok || res.status === 404) return;\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM cache.delete failed: ${res.status}`, res.status, body);\n },\n };\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create an LLMClient that talks to Studio's /api/llm/* proxy routes.\n */\nexport function createLLMClient(config: LLMClientConfig): LLMClient {\n return new LLMClient(config);\n}\n"],"mappings":"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","names":["t"]}
|