@firststep-studio/sdk 0.4.0 → 0.6.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 +63 -3
- package/dist/index.d.ts +63 -3
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -0
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +16 -1
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +16 -1
- package/dist/server.mjs.map +1 -1
- package/dist/{types-DRgb_0ga.d.mts → types-fhi9K2il.d.mts} +7 -0
- package/dist/{types-DRgb_0ga.d.ts → types-fhi9K2il.d.ts} +7 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ChatMessage, S as SchemaDeclarationPayload, P as ProtocolStreamChunk, A as AgentTransitionPayload, R as RoutingClassificationPayload, H as HandoffRequestPayload, a as HandoffReturnPayload, b as HandoffOfferPayload } from './types-
|
|
2
|
-
export { G as AnalyticsContext, x as ChatbotInfo, y as ClassifierConfig, D as DeploymentInfo, F as FormData, O as FormFieldDefinition, Q as FormFieldType, z as FormFieldValue, N as FormSchema, n as HandlerInfo, W as HandoffContext, f as HandoffInboundContext, X as HandoffOptions, v as Helpline, u as HelplineResult, t as HelplineSearchOptions, I as IntegrationContext, s as IntegrationResult, J as InteractionEvent, M as InteractionEventType, K as KnowledgeContext, r as KnowledgeResult, L as LoggerContext, m as ProtocolCapabilities, o as ProtocolContext, k as ProtocolError, j as ProtocolFieldValidation, g as ProtocolForm, h as ProtocolFormField, i as ProtocolFormOption, l as ProtocolHandler, T as ProtocolRegistration, c as ProtocolRequest, d as ProtocolResponse, w as RoutingDecision, B as RoutingLog, U as SchemaAgent, V as SchemaQuestion, p as SessionContext, E as SessionMetadata, q as SessionState, e as SessionStatus } from './types-
|
|
1
|
+
import { C as ChatMessage, S as SchemaDeclarationPayload, P as ProtocolStreamChunk, A as AgentTransitionPayload, R as RoutingClassificationPayload, H as HandoffRequestPayload, a as HandoffReturnPayload, b as HandoffOfferPayload } from './types-fhi9K2il.mjs';
|
|
2
|
+
export { G as AnalyticsContext, x as ChatbotInfo, y as ClassifierConfig, D as DeploymentInfo, F as FormData, O as FormFieldDefinition, Q as FormFieldType, z as FormFieldValue, N as FormSchema, n as HandlerInfo, W as HandoffContext, f as HandoffInboundContext, X as HandoffOptions, v as Helpline, u as HelplineResult, t as HelplineSearchOptions, I as IntegrationContext, s as IntegrationResult, J as InteractionEvent, M as InteractionEventType, K as KnowledgeContext, r as KnowledgeResult, L as LoggerContext, m as ProtocolCapabilities, o as ProtocolContext, k as ProtocolError, j as ProtocolFieldValidation, g as ProtocolForm, h as ProtocolFormField, i as ProtocolFormOption, l as ProtocolHandler, T as ProtocolRegistration, c as ProtocolRequest, d as ProtocolResponse, w as RoutingDecision, B as RoutingLog, U as SchemaAgent, V as SchemaQuestion, p as SessionContext, E as SessionMetadata, q as SessionState, e as SessionStatus } from './types-fhi9K2il.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* UCP (Universal Classification Protocol) Types
|
|
@@ -529,6 +529,66 @@ declare const streamMetadata: {
|
|
|
529
529
|
handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk;
|
|
530
530
|
};
|
|
531
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Platform Client
|
|
534
|
+
*
|
|
535
|
+
* SDK client for handlers to call back to FirstStep Studio.
|
|
536
|
+
* Enables handlers to query platform data (chatbot list, etc.)
|
|
537
|
+
* using the same API token used for request signature verification.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* import { createPlatformClient } from '@firststep-studio/sdk';
|
|
542
|
+
*
|
|
543
|
+
* const client = createPlatformClient({
|
|
544
|
+
* studioUrl: 'https://studio-api.example.com',
|
|
545
|
+
* token: process.env.FIRSTSTEP_TOKEN,
|
|
546
|
+
* });
|
|
547
|
+
*
|
|
548
|
+
* const chatbots = await client.listChatbots();
|
|
549
|
+
* // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]
|
|
550
|
+
* ```
|
|
551
|
+
*/
|
|
552
|
+
interface PlatformClientConfig {
|
|
553
|
+
/** Studio backend API URL (e.g. https://studio-api.example.com) */
|
|
554
|
+
studioUrl: string;
|
|
555
|
+
/** API token (same as FIRSTSTEP_TOKEN used for signature verification) */
|
|
556
|
+
token: string;
|
|
557
|
+
/** Request timeout in ms (default: 10000) */
|
|
558
|
+
timeout?: number;
|
|
559
|
+
}
|
|
560
|
+
interface PlatformChatbot {
|
|
561
|
+
/** Studio chatbot MongoDB ID */
|
|
562
|
+
id: string;
|
|
563
|
+
/** Chatbot display name */
|
|
564
|
+
name: string;
|
|
565
|
+
/** Handler URL (if external handler configured) */
|
|
566
|
+
handlerUrl?: string;
|
|
567
|
+
/** Config slug for multi-config routing */
|
|
568
|
+
configSlug?: string;
|
|
569
|
+
/** Handler type */
|
|
570
|
+
handlerType?: 'builtin' | 'external';
|
|
571
|
+
}
|
|
572
|
+
declare class PlatformError extends Error {
|
|
573
|
+
status: number;
|
|
574
|
+
constructor(message: string, status: number);
|
|
575
|
+
}
|
|
576
|
+
declare class PlatformClient {
|
|
577
|
+
private studioUrl;
|
|
578
|
+
private token;
|
|
579
|
+
private timeout;
|
|
580
|
+
constructor(config: PlatformClientConfig);
|
|
581
|
+
/**
|
|
582
|
+
* List chatbots accessible to this handler's organization.
|
|
583
|
+
* Studio resolves the organization from the API token.
|
|
584
|
+
*/
|
|
585
|
+
listChatbots(): Promise<PlatformChatbot[]>;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Create a PlatformClient for calling back to FirstStep Studio.
|
|
589
|
+
*/
|
|
590
|
+
declare function createPlatformClient(config: PlatformClientConfig): PlatformClient;
|
|
591
|
+
|
|
532
592
|
/**
|
|
533
593
|
* Verify an HMAC-SHA256 request signature.
|
|
534
594
|
*
|
|
@@ -593,4 +653,4 @@ declare function createAuthHeader(token: string): string;
|
|
|
593
653
|
*/
|
|
594
654
|
declare function isValidToken(token: string): boolean;
|
|
595
655
|
|
|
596
|
-
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, MARKER_TYPES, type MarkerType, 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, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
|
656
|
+
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, 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, createPlatformClient, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ChatMessage, S as SchemaDeclarationPayload, P as ProtocolStreamChunk, A as AgentTransitionPayload, R as RoutingClassificationPayload, H as HandoffRequestPayload, a as HandoffReturnPayload, b as HandoffOfferPayload } from './types-
|
|
2
|
-
export { G as AnalyticsContext, x as ChatbotInfo, y as ClassifierConfig, D as DeploymentInfo, F as FormData, O as FormFieldDefinition, Q as FormFieldType, z as FormFieldValue, N as FormSchema, n as HandlerInfo, W as HandoffContext, f as HandoffInboundContext, X as HandoffOptions, v as Helpline, u as HelplineResult, t as HelplineSearchOptions, I as IntegrationContext, s as IntegrationResult, J as InteractionEvent, M as InteractionEventType, K as KnowledgeContext, r as KnowledgeResult, L as LoggerContext, m as ProtocolCapabilities, o as ProtocolContext, k as ProtocolError, j as ProtocolFieldValidation, g as ProtocolForm, h as ProtocolFormField, i as ProtocolFormOption, l as ProtocolHandler, T as ProtocolRegistration, c as ProtocolRequest, d as ProtocolResponse, w as RoutingDecision, B as RoutingLog, U as SchemaAgent, V as SchemaQuestion, p as SessionContext, E as SessionMetadata, q as SessionState, e as SessionStatus } from './types-
|
|
1
|
+
import { C as ChatMessage, S as SchemaDeclarationPayload, P as ProtocolStreamChunk, A as AgentTransitionPayload, R as RoutingClassificationPayload, H as HandoffRequestPayload, a as HandoffReturnPayload, b as HandoffOfferPayload } from './types-fhi9K2il.js';
|
|
2
|
+
export { G as AnalyticsContext, x as ChatbotInfo, y as ClassifierConfig, D as DeploymentInfo, F as FormData, O as FormFieldDefinition, Q as FormFieldType, z as FormFieldValue, N as FormSchema, n as HandlerInfo, W as HandoffContext, f as HandoffInboundContext, X as HandoffOptions, v as Helpline, u as HelplineResult, t as HelplineSearchOptions, I as IntegrationContext, s as IntegrationResult, J as InteractionEvent, M as InteractionEventType, K as KnowledgeContext, r as KnowledgeResult, L as LoggerContext, m as ProtocolCapabilities, o as ProtocolContext, k as ProtocolError, j as ProtocolFieldValidation, g as ProtocolForm, h as ProtocolFormField, i as ProtocolFormOption, l as ProtocolHandler, T as ProtocolRegistration, c as ProtocolRequest, d as ProtocolResponse, w as RoutingDecision, B as RoutingLog, U as SchemaAgent, V as SchemaQuestion, p as SessionContext, E as SessionMetadata, q as SessionState, e as SessionStatus } from './types-fhi9K2il.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* UCP (Universal Classification Protocol) Types
|
|
@@ -529,6 +529,66 @@ declare const streamMetadata: {
|
|
|
529
529
|
handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk;
|
|
530
530
|
};
|
|
531
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Platform Client
|
|
534
|
+
*
|
|
535
|
+
* SDK client for handlers to call back to FirstStep Studio.
|
|
536
|
+
* Enables handlers to query platform data (chatbot list, etc.)
|
|
537
|
+
* using the same API token used for request signature verification.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* import { createPlatformClient } from '@firststep-studio/sdk';
|
|
542
|
+
*
|
|
543
|
+
* const client = createPlatformClient({
|
|
544
|
+
* studioUrl: 'https://studio-api.example.com',
|
|
545
|
+
* token: process.env.FIRSTSTEP_TOKEN,
|
|
546
|
+
* });
|
|
547
|
+
*
|
|
548
|
+
* const chatbots = await client.listChatbots();
|
|
549
|
+
* // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]
|
|
550
|
+
* ```
|
|
551
|
+
*/
|
|
552
|
+
interface PlatformClientConfig {
|
|
553
|
+
/** Studio backend API URL (e.g. https://studio-api.example.com) */
|
|
554
|
+
studioUrl: string;
|
|
555
|
+
/** API token (same as FIRSTSTEP_TOKEN used for signature verification) */
|
|
556
|
+
token: string;
|
|
557
|
+
/** Request timeout in ms (default: 10000) */
|
|
558
|
+
timeout?: number;
|
|
559
|
+
}
|
|
560
|
+
interface PlatformChatbot {
|
|
561
|
+
/** Studio chatbot MongoDB ID */
|
|
562
|
+
id: string;
|
|
563
|
+
/** Chatbot display name */
|
|
564
|
+
name: string;
|
|
565
|
+
/** Handler URL (if external handler configured) */
|
|
566
|
+
handlerUrl?: string;
|
|
567
|
+
/** Config slug for multi-config routing */
|
|
568
|
+
configSlug?: string;
|
|
569
|
+
/** Handler type */
|
|
570
|
+
handlerType?: 'builtin' | 'external';
|
|
571
|
+
}
|
|
572
|
+
declare class PlatformError extends Error {
|
|
573
|
+
status: number;
|
|
574
|
+
constructor(message: string, status: number);
|
|
575
|
+
}
|
|
576
|
+
declare class PlatformClient {
|
|
577
|
+
private studioUrl;
|
|
578
|
+
private token;
|
|
579
|
+
private timeout;
|
|
580
|
+
constructor(config: PlatformClientConfig);
|
|
581
|
+
/**
|
|
582
|
+
* List chatbots accessible to this handler's organization.
|
|
583
|
+
* Studio resolves the organization from the API token.
|
|
584
|
+
*/
|
|
585
|
+
listChatbots(): Promise<PlatformChatbot[]>;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Create a PlatformClient for calling back to FirstStep Studio.
|
|
589
|
+
*/
|
|
590
|
+
declare function createPlatformClient(config: PlatformClientConfig): PlatformClient;
|
|
591
|
+
|
|
532
592
|
/**
|
|
533
593
|
* Verify an HMAC-SHA256 request signature.
|
|
534
594
|
*
|
|
@@ -593,4 +653,4 @@ declare function createAuthHeader(token: string): string;
|
|
|
593
653
|
*/
|
|
594
654
|
declare function isValidToken(token: string): boolean;
|
|
595
655
|
|
|
596
|
-
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, MARKER_TYPES, type MarkerType, 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, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
|
656
|
+
export { AgentTransitionPayload, ChatMessage, type ClassificationResult, type EmergencyPayload, HandoffOfferPayload, HandoffRequestPayload, HandoffReturnPayload, type HelplineCardItem, type HelplineCardPayload, 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, createPlatformClient, createRequestSignature, createUCPClient, isValidToken, renderMarkers, streamMetadata, verifyRequestSignature };
|
package/dist/index.js
CHANGED
|
@@ -21,10 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
MARKER_TYPES: () => MARKER_TYPES,
|
|
24
|
+
PlatformClient: () => PlatformClient,
|
|
25
|
+
PlatformError: () => PlatformError,
|
|
24
26
|
UCPClient: () => UCPClient,
|
|
25
27
|
UCPError: () => UCPError,
|
|
26
28
|
classifyWithUCP: () => classifyWithUCP,
|
|
27
29
|
createAuthHeader: () => createAuthHeader,
|
|
30
|
+
createPlatformClient: () => createPlatformClient,
|
|
28
31
|
createRequestSignature: () => createRequestSignature,
|
|
29
32
|
createUCPClient: () => createUCPClient,
|
|
30
33
|
isValidToken: () => isValidToken,
|
|
@@ -322,6 +325,48 @@ var streamMetadata = {
|
|
|
322
325
|
}
|
|
323
326
|
};
|
|
324
327
|
|
|
328
|
+
// src/platform.ts
|
|
329
|
+
var PlatformError = class extends Error {
|
|
330
|
+
constructor(message, status) {
|
|
331
|
+
super(message);
|
|
332
|
+
this.name = "PlatformError";
|
|
333
|
+
this.status = status;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var PlatformClient = class {
|
|
337
|
+
constructor(config) {
|
|
338
|
+
this.studioUrl = config.studioUrl.replace(/\/+$/, "");
|
|
339
|
+
this.token = config.token;
|
|
340
|
+
this.timeout = config.timeout || 1e4;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* List chatbots accessible to this handler's organization.
|
|
344
|
+
* Studio resolves the organization from the API token.
|
|
345
|
+
*/
|
|
346
|
+
async listChatbots() {
|
|
347
|
+
const res = await fetch(`${this.studioUrl}/api/protocol/chatbots`, {
|
|
348
|
+
method: "GET",
|
|
349
|
+
headers: {
|
|
350
|
+
"Authorization": `Bearer ${this.token}`,
|
|
351
|
+
"Content-Type": "application/json"
|
|
352
|
+
},
|
|
353
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
354
|
+
});
|
|
355
|
+
if (!res.ok) {
|
|
356
|
+
const body = await res.text().catch(() => "");
|
|
357
|
+
throw new PlatformError(
|
|
358
|
+
`Failed to list chatbots: ${res.status} ${body}`,
|
|
359
|
+
res.status
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const data = await res.json();
|
|
363
|
+
return data.chatbots;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function createPlatformClient(config) {
|
|
367
|
+
return new PlatformClient(config);
|
|
368
|
+
}
|
|
369
|
+
|
|
325
370
|
// src/auth.ts
|
|
326
371
|
var import_crypto = require("crypto");
|
|
327
372
|
var TOKEN_PREFIX = "fst_";
|
|
@@ -356,10 +401,13 @@ function isValidToken(token) {
|
|
|
356
401
|
// Annotate the CommonJS export names for ESM import in node:
|
|
357
402
|
0 && (module.exports = {
|
|
358
403
|
MARKER_TYPES,
|
|
404
|
+
PlatformClient,
|
|
405
|
+
PlatformError,
|
|
359
406
|
UCPClient,
|
|
360
407
|
UCPError,
|
|
361
408
|
classifyWithUCP,
|
|
362
409
|
createAuthHeader,
|
|
410
|
+
createPlatformClient,
|
|
363
411
|
createRequestSignature,
|
|
364
412
|
createUCPClient,
|
|
365
413
|
isValidToken,
|
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/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK\n *\n * SDK for building protocol handlers that integrate with FirstStep Studio.\n *\n * @example\n * ```typescript\n * import { ProtocolHandler, ProtocolRequest, ProtocolResponse, ProtocolContext } from '@firststep-studio/sdk';\n *\n * class MyHandler implements ProtocolHandler {\n * async handleMessage(request: ProtocolRequest, context: ProtocolContext): Promise<ProtocolResponse> {\n * // Your implementation\n * }\n *\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * }\n * }\n * ```\n */\n\n// Core types\nexport type {\n ProtocolRequest,\n ProtocolResponse,\n SessionStatus,\n 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 IntegrationContext,\n IntegrationResult,\n HelplineSearchOptions,\n HelplineResult,\n Helpline,\n LoggerContext,\n RoutingDecision,\n DeploymentInfo,\n ChatbotInfo,\n ClassifierConfig,\n} from './types';\n\n// ============================================\n// Platform Standard Data Types\n// ============================================\n\nexport type {\n // Form Data\n FormData,\n FormFieldValue,\n\n // Routing Logs\n RoutingLog,\n\n // Session Metadata\n SessionMetadata,\n\n // Analytics\n AnalyticsContext,\n InteractionEvent,\n InteractionEventType,\n\n // Form Schema (Protocol Registration)\n FormSchema,\n FormFieldDefinition,\n FormFieldType,\n\n // Protocol Registration\n ProtocolRegistration,\n} from './types';\n\n// ============================================\n// UCP (Universal Classification Protocol)\n// ============================================\n\n// UCP Types\nexport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './ucp';\n\n// UCP Client\nexport {\n UCPClient,\n UCPError,\n createUCPClient,\n classifyWithUCP,\n} from './ucp';\n\n// ============================================\n// Render Markers (Rich Content)\n// ============================================\n\nexport {\n renderMarkers,\n MARKER_TYPES,\n} from './renderMarkers';\n\nexport type {\n MarkerType,\n HelplineCardPayload,\n HelplineCardItem,\n EmergencyPayload,\n ResourceCardPayload,\n ResourceCardItem,\n ProviderCardPayload,\n ProviderCardItem,\n SafetyPlanPayload,\n SafetyPlanSection,\n ReportCardPayload,\n} from './renderMarkers';\n\n// ============================================\n// Stream Metadata (Dashboard Persistence)\n// ============================================\n\nexport {\n streamMetadata,\n} from './streamMetadata';\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n 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// Auth Utilities\n// ============================================\n\nexport {\n verifyRequestSignature,\n createRequestSignature,\n createAuthHeader,\n isValidToken,\n} from './auth';\n","/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n 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","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK\n *\n * SDK for building protocol handlers that integrate with FirstStep Studio.\n *\n * @example\n * ```typescript\n * import { ProtocolHandler, ProtocolRequest, ProtocolResponse, ProtocolContext } from '@firststep-studio/sdk';\n *\n * class MyHandler implements ProtocolHandler {\n * async handleMessage(request: ProtocolRequest, context: ProtocolContext): Promise<ProtocolResponse> {\n * // Your implementation\n * }\n *\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * }\n * }\n * ```\n */\n\n// Core types\nexport type {\n ProtocolRequest,\n ProtocolResponse,\n SessionStatus,\n 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 IntegrationContext,\n IntegrationResult,\n HelplineSearchOptions,\n HelplineResult,\n Helpline,\n LoggerContext,\n RoutingDecision,\n DeploymentInfo,\n ChatbotInfo,\n ClassifierConfig,\n} from './types';\n\n// ============================================\n// Platform Standard Data Types\n// ============================================\n\nexport type {\n // Form Data\n FormData,\n FormFieldValue,\n\n // Routing Logs\n RoutingLog,\n\n // Session Metadata\n SessionMetadata,\n\n // Analytics\n AnalyticsContext,\n InteractionEvent,\n InteractionEventType,\n\n // Form Schema (Protocol Registration)\n FormSchema,\n FormFieldDefinition,\n FormFieldType,\n\n // Protocol Registration\n ProtocolRegistration,\n} from './types';\n\n// ============================================\n// UCP (Universal Classification Protocol)\n// ============================================\n\n// UCP Types\nexport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './ucp';\n\n// UCP Client\nexport {\n UCPClient,\n UCPError,\n createUCPClient,\n classifyWithUCP,\n} from './ucp';\n\n// ============================================\n// Render Markers (Rich Content)\n// ============================================\n\nexport {\n renderMarkers,\n MARKER_TYPES,\n} from './renderMarkers';\n\nexport type {\n MarkerType,\n HelplineCardPayload,\n HelplineCardItem,\n EmergencyPayload,\n ResourceCardPayload,\n ResourceCardItem,\n ProviderCardPayload,\n ProviderCardItem,\n SafetyPlanPayload,\n SafetyPlanSection,\n ReportCardPayload,\n} from './renderMarkers';\n\n// ============================================\n// Stream Metadata (Dashboard Persistence)\n// ============================================\n\nexport {\n streamMetadata,\n} from './streamMetadata';\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n 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 * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n} from './types';\n\n// ============================================\n// Builder Functions\n// ============================================\n\nexport const streamMetadata = {\n /**\n * Declare the form schema (agents + questions) for Dashboard Form Insights.\n *\n * Yield this once during session initialization (welcome message).\n * The Studio proxy stores it in `chatbot.externalSchema` so that\n * Dashboard Form Insights can display question-level analytics.\n *\n * @param schema - Agents and questions your handler uses\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n declareSchema(schema: SchemaDeclarationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { schema } };\n },\n\n /**\n * Send collected form field values for Dashboard persistence.\n *\n * Call this after each turn when new fields are captured.\n * Values are incrementally merged into `ChatSession.formData`.\n *\n * @param fields - Key-value pairs of field IDs to their collected values\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n formDataUpdate(fields: Record<string, string | number>): ProtocolStreamChunk {\n return { type: 'metadata', content: { formData: fields } };\n },\n\n /**\n * Signal an agent/stage transition for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` so\n * the Dashboard can show the conversation's agent flow.\n *\n * @param agent - The agent/stage being transitioned to\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n agentTransition(agent: AgentTransitionPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { currentAgent: agent } };\n },\n\n /**\n * Send a routing/classification result for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` as a\n * classification event with category, level, and confidence score.\n *\n * @param result - Classification/routing decision details\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n routingResult(result: RoutingClassificationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { routing: result } };\n },\n\n // ============================================\n // Handoff Protocol Builders\n // ============================================\n\n /**\n * Request a handoff to another chatbot.\n *\n * The Studio proxy pushes a handoff stack entry and routes subsequent\n * user messages to the target handler. If `options.consent` is set,\n * Studio stores the request as a pending offer instead.\n *\n * @param payload - Handoff target, reason, context, and options\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffRequest(payload: HandoffRequestPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffRequest: payload } };\n },\n\n /**\n * Return from a handoff back to the parent handler.\n *\n * The Studio proxy pops the handoff stack and delivers the return\n * result to the parent handler on the next user message.\n *\n * @param payload - Summary and artifacts from the completed handoff\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffReturn(payload: HandoffReturnPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffReturn: payload } };\n },\n\n /**\n * Offer a handoff with user consent required.\n *\n * Studio stores this as a pending offer, sends an SSE event to the\n * frontend, and waits for the user to accept or decline before\n * executing the handoff.\n *\n * @param payload - Offer display info and the pending handoff request\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffOffer: payload } };\n },\n};\n\n// ============================================\n// Re-export payload types for convenience\n// ============================================\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n};\n","/**\n * Platform Client\n *\n * SDK client for handlers to call back to FirstStep Studio.\n * Enables handlers to query platform data (chatbot list, etc.)\n * using the same API token used for request signature verification.\n *\n * @example\n * ```typescript\n * import { createPlatformClient } from '@firststep-studio/sdk';\n *\n * const client = createPlatformClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN,\n * });\n *\n * const chatbots = await client.listChatbots();\n * // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]\n * ```\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PlatformClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (same as FIRSTSTEP_TOKEN used for signature verification) */\n token: string;\n /** Request timeout in ms (default: 10000) */\n timeout?: number;\n}\n\nexport interface PlatformChatbot {\n /** Studio chatbot MongoDB ID */\n id: string;\n /** Chatbot display name */\n name: string;\n /** Handler URL (if external handler configured) */\n handlerUrl?: string;\n /** Config slug for multi-config routing */\n configSlug?: string;\n /** Handler type */\n handlerType?: 'builtin' | 'external';\n}\n\nexport class PlatformError extends Error {\n status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'PlatformError';\n this.status = status;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class PlatformClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: PlatformClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout || 10000;\n }\n\n /**\n * List chatbots accessible to this handler's organization.\n * Studio resolves the organization from the API token.\n */\n async listChatbots(): Promise<PlatformChatbot[]> {\n const res = await fetch(`${this.studioUrl}/api/protocol/chatbots`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new PlatformError(\n `Failed to list chatbots: ${res.status} ${body}`,\n res.status\n );\n }\n\n const data = await res.json() as { chatbots: PlatformChatbot[] };\n return data.chatbots;\n }\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create a PlatformClient for calling back to FirstStep Studio.\n */\nexport function createPlatformClient(config: PlatformClientConfig): PlatformClient {\n return new PlatformClient(config);\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -286,6 +286,48 @@ var streamMetadata = {
|
|
|
286
286
|
}
|
|
287
287
|
};
|
|
288
288
|
|
|
289
|
+
// src/platform.ts
|
|
290
|
+
var PlatformError = class extends Error {
|
|
291
|
+
constructor(message, status) {
|
|
292
|
+
super(message);
|
|
293
|
+
this.name = "PlatformError";
|
|
294
|
+
this.status = status;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var PlatformClient = class {
|
|
298
|
+
constructor(config) {
|
|
299
|
+
this.studioUrl = config.studioUrl.replace(/\/+$/, "");
|
|
300
|
+
this.token = config.token;
|
|
301
|
+
this.timeout = config.timeout || 1e4;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* List chatbots accessible to this handler's organization.
|
|
305
|
+
* Studio resolves the organization from the API token.
|
|
306
|
+
*/
|
|
307
|
+
async listChatbots() {
|
|
308
|
+
const res = await fetch(`${this.studioUrl}/api/protocol/chatbots`, {
|
|
309
|
+
method: "GET",
|
|
310
|
+
headers: {
|
|
311
|
+
"Authorization": `Bearer ${this.token}`,
|
|
312
|
+
"Content-Type": "application/json"
|
|
313
|
+
},
|
|
314
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
315
|
+
});
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
const body = await res.text().catch(() => "");
|
|
318
|
+
throw new PlatformError(
|
|
319
|
+
`Failed to list chatbots: ${res.status} ${body}`,
|
|
320
|
+
res.status
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const data = await res.json();
|
|
324
|
+
return data.chatbots;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
function createPlatformClient(config) {
|
|
328
|
+
return new PlatformClient(config);
|
|
329
|
+
}
|
|
330
|
+
|
|
289
331
|
// src/auth.ts
|
|
290
332
|
import { createHash, createHmac, timingSafeEqual } from "crypto";
|
|
291
333
|
var TOKEN_PREFIX = "fst_";
|
|
@@ -319,10 +361,13 @@ function isValidToken(token) {
|
|
|
319
361
|
}
|
|
320
362
|
export {
|
|
321
363
|
MARKER_TYPES,
|
|
364
|
+
PlatformClient,
|
|
365
|
+
PlatformError,
|
|
322
366
|
UCPClient,
|
|
323
367
|
UCPError,
|
|
324
368
|
classifyWithUCP,
|
|
325
369
|
createAuthHeader,
|
|
370
|
+
createPlatformClient,
|
|
326
371
|
createRequestSignature,
|
|
327
372
|
createUCPClient,
|
|
328
373
|
isValidToken,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/auth.ts"],"sourcesContent":["/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n 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","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts"],"sourcesContent":["/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n} from './types';\n\n// ============================================\n// Builder Functions\n// ============================================\n\nexport const streamMetadata = {\n /**\n * Declare the form schema (agents + questions) for Dashboard Form Insights.\n *\n * Yield this once during session initialization (welcome message).\n * The Studio proxy stores it in `chatbot.externalSchema` so that\n * Dashboard Form Insights can display question-level analytics.\n *\n * @param schema - Agents and questions your handler uses\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n declareSchema(schema: SchemaDeclarationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { schema } };\n },\n\n /**\n * Send collected form field values for Dashboard persistence.\n *\n * Call this after each turn when new fields are captured.\n * Values are incrementally merged into `ChatSession.formData`.\n *\n * @param fields - Key-value pairs of field IDs to their collected values\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n formDataUpdate(fields: Record<string, string | number>): ProtocolStreamChunk {\n return { type: 'metadata', content: { formData: fields } };\n },\n\n /**\n * Signal an agent/stage transition for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` so\n * the Dashboard can show the conversation's agent flow.\n *\n * @param agent - The agent/stage being transitioned to\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n agentTransition(agent: AgentTransitionPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { currentAgent: agent } };\n },\n\n /**\n * Send a routing/classification result for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` as a\n * classification event with category, level, and confidence score.\n *\n * @param result - Classification/routing decision details\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n routingResult(result: RoutingClassificationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { routing: result } };\n },\n\n // ============================================\n // Handoff Protocol Builders\n // ============================================\n\n /**\n * Request a handoff to another chatbot.\n *\n * The Studio proxy pushes a handoff stack entry and routes subsequent\n * user messages to the target handler. If `options.consent` is set,\n * Studio stores the request as a pending offer instead.\n *\n * @param payload - Handoff target, reason, context, and options\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffRequest(payload: HandoffRequestPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffRequest: payload } };\n },\n\n /**\n * Return from a handoff back to the parent handler.\n *\n * The Studio proxy pops the handoff stack and delivers the return\n * result to the parent handler on the next user message.\n *\n * @param payload - Summary and artifacts from the completed handoff\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffReturn(payload: HandoffReturnPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffReturn: payload } };\n },\n\n /**\n * Offer a handoff with user consent required.\n *\n * Studio stores this as a pending offer, sends an SSE event to the\n * frontend, and waits for the user to accept or decline before\n * executing the handoff.\n *\n * @param payload - Offer display info and the pending handoff request\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffOffer: payload } };\n },\n};\n\n// ============================================\n// Re-export payload types for convenience\n// ============================================\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n};\n","/**\n * Platform Client\n *\n * SDK client for handlers to call back to FirstStep Studio.\n * Enables handlers to query platform data (chatbot list, etc.)\n * using the same API token used for request signature verification.\n *\n * @example\n * ```typescript\n * import { createPlatformClient } from '@firststep-studio/sdk';\n *\n * const client = createPlatformClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN,\n * });\n *\n * const chatbots = await client.listChatbots();\n * // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]\n * ```\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PlatformClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (same as FIRSTSTEP_TOKEN used for signature verification) */\n token: string;\n /** Request timeout in ms (default: 10000) */\n timeout?: number;\n}\n\nexport interface PlatformChatbot {\n /** Studio chatbot MongoDB ID */\n id: string;\n /** Chatbot display name */\n name: string;\n /** Handler URL (if external handler configured) */\n handlerUrl?: string;\n /** Config slug for multi-config routing */\n configSlug?: string;\n /** Handler type */\n handlerType?: 'builtin' | 'external';\n}\n\nexport class PlatformError extends Error {\n status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'PlatformError';\n this.status = status;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class PlatformClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: PlatformClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout || 10000;\n }\n\n /**\n * List chatbots accessible to this handler's organization.\n * Studio resolves the organization from the API token.\n */\n async listChatbots(): Promise<PlatformChatbot[]> {\n const res = await fetch(`${this.studioUrl}/api/protocol/chatbots`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new PlatformError(\n `Failed to list chatbots: ${res.status} ${body}`,\n res.status\n );\n }\n\n const data = await res.json() as { chatbots: PlatformChatbot[] };\n return data.chatbots;\n }\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create a PlatformClient for calling back to FirstStep Studio.\n */\nexport function createPlatformClient(config: PlatformClientConfig): PlatformClient {\n return new PlatformClient(config);\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/server.d.mts
CHANGED
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -146,7 +146,16 @@ function createServer(handler, config) {
|
|
|
146
146
|
res.end();
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
|
-
const
|
|
149
|
+
const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
150
|
+
let configSlug;
|
|
151
|
+
let route;
|
|
152
|
+
const slugMatch = afterPrefix.match(/^\/([^/]+)(\/(?:health|handshake|message(?:\/stream)?))?$/);
|
|
153
|
+
if (slugMatch && slugMatch[2]) {
|
|
154
|
+
configSlug = slugMatch[1];
|
|
155
|
+
route = slugMatch[2];
|
|
156
|
+
} else {
|
|
157
|
+
route = afterPrefix;
|
|
158
|
+
}
|
|
150
159
|
if (route === "/health" && method === "GET") {
|
|
151
160
|
sendJson(res, 200, { status: "ok", timestamp: Date.now() });
|
|
152
161
|
return;
|
|
@@ -181,6 +190,9 @@ function createServer(handler, config) {
|
|
|
181
190
|
sendJson(res, 400, { error: "Invalid JSON body" });
|
|
182
191
|
return;
|
|
183
192
|
}
|
|
193
|
+
if (configSlug) {
|
|
194
|
+
parsed.request.configSlug = configSlug;
|
|
195
|
+
}
|
|
184
196
|
const context = buildStubContext(parsed.context);
|
|
185
197
|
try {
|
|
186
198
|
const response = await handler.handleMessage(parsed.request, context);
|
|
@@ -210,6 +222,9 @@ function createServer(handler, config) {
|
|
|
210
222
|
sendJson(res, 400, { error: "Invalid JSON body" });
|
|
211
223
|
return;
|
|
212
224
|
}
|
|
225
|
+
if (configSlug) {
|
|
226
|
+
parsed.request.configSlug = configSlug;
|
|
227
|
+
}
|
|
213
228
|
if (!handler.handleStream) {
|
|
214
229
|
const context2 = buildStubContext(parsed.context);
|
|
215
230
|
try {
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK Server\n *\n * Standalone HTTP server for protocol handlers.\n * Zero external dependencies, uses Node.js built-in `http` module.\n *\n * @example\n * ```typescript\n * import { createServer } from '@firststep-studio/sdk/server';\n * import type { ProtocolHandler } from '@firststep-studio/sdk';\n *\n * const handler: ProtocolHandler = {\n * async handleMessage(request, context) {\n * return {\n * message: 'Hello from my handler!',\n * sessionId: request.sessionId || 'new',\n * agentId: 'main',\n * sessionStatus: 'active',\n * };\n * },\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * },\n * };\n *\n * const server = createServer(handler, {\n * token: process.env.FIRSTSTEP_TOKEN!,\n * port: 3001,\n * });\n *\n * server.start();\n * ```\n */\n\nimport { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http';\nimport type {\n ProtocolHandler,\n ProtocolRequest,\n ProtocolContext,\n ProtocolCapabilities,\n ProtocolStreamChunk,\n} from './types';\nimport { verifyRequestSignature } from './auth';\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface ServerConfig {\n /** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */\n token: string;\n\n /** Port to listen on. Defaults to 3001, or the PORT env variable. */\n port?: number;\n\n /** Host to bind to. Defaults to '0.0.0.0'. */\n host?: string;\n\n /** Skip signature verification (for local development only). */\n skipSignatureVerification?: boolean;\n\n /**\n * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\n}\n\n// ============================================\n// Server Instance\n// ============================================\n\nexport interface FirstStepServer {\n /** Start the server */\n start(): Promise<void>;\n\n /** Stop the server gracefully */\n stop(): Promise<void>;\n\n /**\n * Get the request handler function.\n * Use this to integrate with Express, Fastify, or other frameworks.\n *\n * @example\n * ```typescript\n * // Express\n * const server = createServer(handler, { token: '...' });\n * app.post('/handshake', server.getRequestHandler());\n * app.post('/message', server.getRequestHandler());\n * ```\n */\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;\n}\n\n// ============================================\n// Internal Helpers\n// ============================================\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json),\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(json);\n}\n\nfunction parsePath(url: string | undefined): string {\n if (!url) return '/';\n const idx = url.indexOf('?');\n return idx >= 0 ? url.substring(0, idx) : url;\n}\n\n/**\n * Build a stub ProtocolContext for external handlers.\n *\n * External handlers receive context data as plain JSON from the platform.\n * The stub provides no-op implementations for methods like session.getState()\n * since those will be handled by callback APIs to the FirstStep backend.\n *\n * When the platform sends a /message request, the request body includes\n * a `context` field with serialized context data. This function merges\n * that data with no-op method stubs.\n */\nfunction buildStubContext(data?: Partial<ProtocolContext>): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n return {\n session: {\n sessionId: '',\n getState: noopAsync,\n updateState: noopAsync,\n getHistory: async () => [],\n saveMessage: noopAsync,\n complete: noopAsync,\n getFormData: async () => ({}),\n updateFormField: noopAsync,\n updateFormData: noopAsync,\n ...data?.session,\n },\n knowledge: {\n queryDatabase: noopAsync,\n searchPages: noopAsync,\n ...data?.knowledge,\n },\n integrations: {\n execute: noopAsync,\n ...data?.integrations,\n },\n analytics: {\n logActionExecuted: noop,\n logInteraction: noop,\n logCustomEvent: noop,\n ...data?.analytics,\n },\n logger: {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n logRouting: noop,\n logToolUse: noop,\n ...data?.logger,\n },\n deployment: {\n id: '',\n slug: '',\n name: '',\n protocolType: 'external',\n ...data?.deployment,\n },\n chatbot: data?.chatbot,\n };\n}\n\n// ============================================\n// createServer\n// ============================================\n\n/**\n * Create a standalone HTTP server for a protocol handler.\n *\n * The server exposes three endpoints:\n * - `GET /health` - Health check (always 200)\n * - `POST /handshake` - Returns handler capabilities (signature verified)\n * - `POST /message` - Handles a chat message (signature verified)\n */\nexport function createServer(\n handler: ProtocolHandler,\n config: ServerConfig\n): FirstStepServer {\n const {\n token,\n port = parseInt(process.env.PORT || '3001', 10),\n host = '0.0.0.0',\n skipSignatureVerification = false,\n prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\n\n // Validate config\n if (!token && !skipSignatureVerification) {\n throw new Error(\n 'FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev.'\n );\n }\n\n /**\n * Core request router\n */\n async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const path = parsePath(req.url);\n const method = (req.method || 'GET').toUpperCase();\n\n // CORS preflight\n if (method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-FirstStep-Signature',\n 'Access-Control-Max-Age': '86400',\n });\n res.end();\n return;\n }\n\n // Strip prefix to match routes\n const route = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/handshake' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n const capabilities: ProtocolCapabilities = handler.getCapabilities();\n const handlerInfo = handler.getHandlerInfo?.();\n sendJson(res, 200, { capabilities, handler: handlerInfo || undefined });\n return;\n }\n\n // Message\n if (route === '/message' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n try {\n const response = await handler.handleMessage(parsed.request, context);\n sendJson(res, 200, response);\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, {\n error: 'Handler error',\n message: err?.message || 'Unknown error',\n });\n }\n return;\n }\n\n // Message (streaming via SSE)\n if (route === '/message/stream' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n if (!handler.handleStream) {\n // Fallback: use non-streaming handleMessage, send as single SSE burst\n const context = buildStubContext(parsed.context);\n try {\n const response = await handler.handleMessage(parsed.request, context);\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n sendSSE('connected', { sessionId: response.sessionId });\n sendSSE('text', { type: 'text', content: response.message, sessionId: response.sessionId });\n if (response.metadata) {\n sendSSE('metadata', { type: 'metadata', content: response.metadata, sessionId: response.sessionId });\n }\n if (response.form) {\n sendSSE('form', { type: 'form', content: response.form, sessionId: response.sessionId });\n }\n sendSSE('status', { type: 'status', content: response.sessionStatus, sessionId: response.sessionId });\n sendSSE('done', { sessionId: response.sessionId });\n res.end();\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, { error: 'Handler error', message: err?.message || 'Unknown error' });\n }\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n\n const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;\n sendSSE('connected', { sessionId });\n\n try {\n for await (const chunk of handler.handleStream(parsed.request, context)) {\n sendSSE(chunk.type, {\n type: chunk.type,\n content: chunk.content,\n sessionId,\n });\n }\n sendSSE('done', { sessionId });\n } catch (err: any) {\n console.error('[firststep] Stream error:', err);\n sendSSE('error', { code: 'STREAM_ERROR', message: err?.message || 'Unknown error' });\n }\n\n res.end();\n return;\n }\n\n // 404\n sendJson(res, 404, { error: 'Not found' });\n }\n\n // Wrap in error boundary\n function requestListener(req: IncomingMessage, res: ServerResponse): void {\n handleRequest(req, res).catch((err) => {\n console.error('[firststep] Unexpected error:', err);\n if (!res.headersSent) {\n sendJson(res, 500, { error: 'Internal server error' });\n }\n });\n }\n\n const httpServer = createHttpServer(requestListener);\n\n return {\n start() {\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n console.log(`[firststep] Handler server listening on ${host}:${port}`);\n console.log(`[firststep] Endpoints:`);\n console.log(` GET ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/message/stream - Handle chat message (SSE stream)`);\n resolve();\n });\n });\n },\n\n stop() {\n return new Promise<void>((resolve, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n getRequestHandler() {\n return requestListener;\n },\n };\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCA,kBAAkF;;;AClClF,oBAAwD;AAUxD,SAAS,iBAAiB,OAAuB;AAC/C,aAAO,0BAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;AA6BO,SAAS,uBACd,OACA,SACA,WACS;AACT,MAAI;AACF,UAAM,aAAa,iBAAiB,KAAK;AACzC,UAAM,eAAW,0BAAW,UAAU,UAAU,EAC7C,OAAO,OAAO,EACd,OAAO,KAAK;AACf,UAAM,iBAAiB,OAAO,KAAK,UAAU,KAAK;AAClD,UAAM,kBAAkB,OAAO,KAAK,WAAW,KAAK;AAEpD,QAAI,eAAe,WAAW,gBAAgB,QAAQ;AACpD,aAAO;AAAA,IACT;AAEA,eAAO,+BAAgB,gBAAgB,eAAe;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADoCA,SAAS,SAAS,KAAuC;AACvD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAmB,CAAC;AAC1B,QAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,QAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO,CAAC,CAAC;AACpE,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAEA,SAAS,SAAS,KAAqB,QAAgB,MAAqB;AAC1E,QAAM,OAAO,KAAK,UAAU,IAAI;AAChC,MAAI,UAAU,QAAQ;AAAA,IACpB,gBAAgB;AAAA,IAChB,kBAAkB,OAAO,WAAW,IAAI;AAAA,IACxC,+BAA+B;AAAA,EACjC,CAAC;AACD,MAAI,IAAI,IAAI;AACd;AAEA,SAAS,UAAU,KAAiC;AAClD,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,IAAI,QAAQ,GAAG;AAC3B,SAAO,OAAO,IAAI,IAAI,UAAU,GAAG,GAAG,IAAI;AAC5C;AAaA,SAAS,iBAAiB,MAAkD;AAC1E,QAAM,YAAY,aAAa,CAAC;AAChC,QAAM,OAAO,MAAM;AAAA,EAAC;AAEpB,SAAO;AAAA,IACL,SAAS;AAAA,MACP,WAAW;AAAA,MACX,UAAU;AAAA,MACV,aAAa;AAAA,MACb,YAAY,YAAY,CAAC;AAAA,MACzB,aAAa;AAAA,MACb,UAAU;AAAA,MACV,aAAa,aAAa,CAAC;AAAA,MAC3B,iBAAiB;AAAA,MACjB,gBAAgB;AAAA,MAChB,GAAG,MAAM;AAAA,IACX;AAAA,IACA,WAAW;AAAA,MACT,eAAe;AAAA,MACf,aAAa;AAAA,MACb,GAAG,MAAM;AAAA,IACX;AAAA,IACA,cAAc;AAAA,MACZ,SAAS;AAAA,MACT,GAAG,MAAM;AAAA,IACX;AAAA,IACA,WAAW;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,GAAG,MAAM;AAAA,IACX;AAAA,IACA,QAAQ;AAAA,MACN,OAAO;AAAA,MACP,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,GAAG,MAAM;AAAA,IACX;AAAA,IACA,YAAY;AAAA,MACV,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,cAAc;AAAA,MACd,GAAG,MAAM;AAAA,IACX;AAAA,IACA,SAAS,MAAM;AAAA,EACjB;AACF;AAcO,SAAS,aACd,SACA,QACiB;AACjB,QAAM;AAAA,IACJ;AAAA,IACA,OAAO,SAAS,QAAQ,IAAI,QAAQ,QAAQ,EAAE;AAAA,IAC9C,OAAO;AAAA,IACP,4BAA4B;AAAA,IAC5B,QAAQ,YAAY;AAAA,EACtB,IAAI;AAGJ,QAAM,SAAS,YAAa,MAAM,UAAU,QAAQ,YAAY,EAAE,IAAK;AAGvE,MAAI,CAAC,SAAS,CAAC,2BAA2B;AACxC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAKA,iBAAe,cAAc,KAAsB,KAAoC;AACrF,UAAM,OAAO,UAAU,IAAI,GAAG;AAC9B,UAAM,UAAU,IAAI,UAAU,OAAO,YAAY;AAGjD,QAAI,WAAW,WAAW;AACxB,UAAI,UAAU,KAAK;AAAA,QACjB,+BAA+B;AAAA,QAC/B,gCAAgC;AAAA,QAChC,gCAAgC;AAAA,QAChC,0BAA0B;AAAA,MAC5B,CAAC;AACD,UAAI,IAAI;AACR;AAAA,IACF;AAGA,UAAM,QAAQ,UAAU,KAAK,WAAW,MAAM,IAAI,KAAK,MAAM,OAAO,MAAM,KAAK,MAAM;AAGrF,QAAI,UAAU,aAAa,WAAW,OAAO;AAC3C,eAAS,KAAK,KAAK,EAAE,QAAQ,MAAM,WAAW,KAAK,IAAI,EAAE,CAAC;AAC1D;AAAA,IACF;AAMA,QAAI,UAAU,gBAAgB,WAAW,QAAQ;AAC/C,YAAM,OAAO,MAAM,SAAS,GAAG;AAE/B,UAAI,CAAC,2BAA2B;AAC9B,cAAM,YAAY,IAAI,QAAQ,uBAAuB;AACrD,YAAI,aAAa,CAAC,uBAAuB,OAAO,MAAM,SAAS,GAAG;AAChE,mBAAS,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AACjD;AAAA,QACF;AAAA,MACF;AAEA,YAAM,eAAqC,QAAQ,gBAAgB;AACnE,YAAM,cAAc,QAAQ,iBAAiB;AAC7C,eAAS,KAAK,KAAK,EAAE,cAAc,SAAS,eAAe,OAAU,CAAC;AACtE;AAAA,IACF;AAGA,QAAI,UAAU,cAAc,WAAW,QAAQ;AAC7C,YAAM,OAAO,MAAM,SAAS,GAAG;AAE/B,UAAI,CAAC,2BAA2B;AAC9B,cAAM,YAAY,IAAI,QAAQ,uBAAuB;AACrD,YAAI,aAAa,CAAC,uBAAuB,OAAO,MAAM,SAAS,GAAG;AAChE,mBAAS,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AACjD;AAAA,QACF;AAAA,MACF;AAEA,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC1B,QAAQ;AACN,iBAAS,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AACjD;AAAA,MACF;AAEA,YAAM,UAAU,iBAAiB,OAAO,OAAO;AAE/C,UAAI;AACF,cAAM,WAAW,MAAM,QAAQ,cAAc,OAAO,SAAS,OAAO;AACpE,iBAAS,KAAK,KAAK,QAAQ;AAAA,MAC7B,SAAS,KAAU;AACjB,gBAAQ,MAAM,8BAA8B,GAAG;AAC/C,iBAAS,KAAK,KAAK;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,KAAK,WAAW;AAAA,QAC3B,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAGA,QAAI,UAAU,qBAAqB,WAAW,QAAQ;AACpD,YAAM,OAAO,MAAM,SAAS,GAAG;AAE/B,UAAI,CAAC,2BAA2B;AAC9B,cAAM,YAAY,IAAI,QAAQ,uBAAuB;AACrD,YAAI,aAAa,CAAC,uBAAuB,OAAO,MAAM,SAAS,GAAG;AAChE,mBAAS,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AACjD;AAAA,QACF;AAAA,MACF;AAEA,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC1B,QAAQ;AACN,iBAAS,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AACjD;AAAA,MACF;AAEA,UAAI,CAAC,QAAQ,cAAc;AAEzB,cAAMA,WAAU,iBAAiB,OAAO,OAAO;AAC/C,YAAI;AACF,gBAAM,WAAW,MAAM,QAAQ,cAAc,OAAO,SAASA,QAAO;AACpE,cAAI,UAAU,KAAK;AAAA,YACjB,gBAAgB;AAAA,YAChB,iBAAiB;AAAA,YACjB,cAAc;AAAA,YACd,+BAA+B;AAAA,UACjC,CAAC;AACD,gBAAMC,WAAU,CAAC,OAAe,SAAkB;AAChD,gBAAI,MAAM,UAAU,KAAK;AAAA,QAAW,KAAK,UAAU,IAAI,CAAC;AAAA;AAAA,CAAM;AAAA,UAChE;AACA,UAAAA,SAAQ,aAAa,EAAE,WAAW,SAAS,UAAU,CAAC;AACtD,UAAAA,SAAQ,QAAQ,EAAE,MAAM,QAAQ,SAAS,SAAS,SAAS,WAAW,SAAS,UAAU,CAAC;AAC1F,cAAI,SAAS,UAAU;AACrB,YAAAA,SAAQ,YAAY,EAAE,MAAM,YAAY,SAAS,SAAS,UAAU,WAAW,SAAS,UAAU,CAAC;AAAA,UACrG;AACA,cAAI,SAAS,MAAM;AACjB,YAAAA,SAAQ,QAAQ,EAAE,MAAM,QAAQ,SAAS,SAAS,MAAM,WAAW,SAAS,UAAU,CAAC;AAAA,UACzF;AACA,UAAAA,SAAQ,UAAU,EAAE,MAAM,UAAU,SAAS,SAAS,eAAe,WAAW,SAAS,UAAU,CAAC;AACpG,UAAAA,SAAQ,QAAQ,EAAE,WAAW,SAAS,UAAU,CAAC;AACjD,cAAI,IAAI;AAAA,QACV,SAAS,KAAU;AACjB,kBAAQ,MAAM,8BAA8B,GAAG;AAC/C,mBAAS,KAAK,KAAK,EAAE,OAAO,iBAAiB,SAAS,KAAK,WAAW,gBAAgB,CAAC;AAAA,QACzF;AACA;AAAA,MACF;AAEA,YAAM,UAAU,iBAAiB,OAAO,OAAO;AAE/C,UAAI,UAAU,KAAK;AAAA,QACjB,gBAAgB;AAAA,QAChB,iBAAiB;AAAA,QACjB,cAAc;AAAA,QACd,+BAA+B;AAAA,MACjC,CAAC;AAED,YAAM,UAAU,CAAC,OAAe,SAAkB;AAChD,YAAI,MAAM,UAAU,KAAK;AAAA,QAAW,KAAK,UAAU,IAAI,CAAC;AAAA;AAAA,CAAM;AAAA,MAChE;AAEA,YAAM,YAAY,OAAO,QAAQ,aAAa,OAAO,KAAK,IAAI,CAAC;AAC/D,cAAQ,aAAa,EAAE,UAAU,CAAC;AAElC,UAAI;AACF,yBAAiB,SAAS,QAAQ,aAAa,OAAO,SAAS,OAAO,GAAG;AACvE,kBAAQ,MAAM,MAAM;AAAA,YAClB,MAAM,MAAM;AAAA,YACZ,SAAS,MAAM;AAAA,YACf;AAAA,UACF,CAAC;AAAA,QACH;AACA,gBAAQ,QAAQ,EAAE,UAAU,CAAC;AAAA,MAC/B,SAAS,KAAU;AACjB,gBAAQ,MAAM,6BAA6B,GAAG;AAC9C,gBAAQ,SAAS,EAAE,MAAM,gBAAgB,SAAS,KAAK,WAAW,gBAAgB,CAAC;AAAA,MACrF;AAEA,UAAI,IAAI;AACR;AAAA,IACF;AAGA,aAAS,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,EAC3C;AAGA,WAAS,gBAAgB,KAAsB,KAA2B;AACxE,kBAAc,KAAK,GAAG,EAAE,MAAM,CAAC,QAAQ;AACrC,cAAQ,MAAM,iCAAiC,GAAG;AAClD,UAAI,CAAC,IAAI,aAAa;AACpB,iBAAS,KAAK,KAAK,EAAE,OAAO,wBAAwB,CAAC;AAAA,MACvD;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,iBAAa,YAAAC,cAAiB,eAAe;AAEnD,SAAO;AAAA,IACL,QAAQ;AACN,aAAO,IAAI,QAAc,CAAC,YAAY;AACpC,mBAAW,OAAO,MAAM,MAAM,MAAM;AAClC,kBAAQ,IAAI,2CAA2C,IAAI,IAAI,IAAI,EAAE;AACrE,kBAAQ,IAAI,wBAAwB;AACpC,kBAAQ,IAAI,UAAU,MAAM,gCAAgC;AAC5D,kBAAQ,IAAI,UAAU,MAAM,uCAAuC;AACnE,kBAAQ,IAAI,UAAU,MAAM,uCAAuC;AACnE,kBAAQ,IAAI,UAAU,MAAM,oDAAoD;AAChF,kBAAQ;AAAA,QACV,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,IAEA,OAAO;AACL,aAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,mBAAW,MAAM,CAAC,QAAQ;AACxB,cAAI,IAAK,QAAO,GAAG;AAAA,cACd,SAAQ;AAAA,QACf,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,IAEA,oBAAoB;AAClB,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":["context","sendSSE","createHttpServer"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK Server\n *\n * Standalone HTTP server for protocol handlers.\n * Zero external dependencies, uses Node.js built-in `http` module.\n *\n * @example\n * ```typescript\n * import { createServer } from '@firststep-studio/sdk/server';\n * import type { ProtocolHandler } from '@firststep-studio/sdk';\n *\n * const handler: ProtocolHandler = {\n * async handleMessage(request, context) {\n * return {\n * message: 'Hello from my handler!',\n * sessionId: request.sessionId || 'new',\n * agentId: 'main',\n * sessionStatus: 'active',\n * };\n * },\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * },\n * };\n *\n * const server = createServer(handler, {\n * token: process.env.FIRSTSTEP_TOKEN!,\n * port: 3001,\n * });\n *\n * server.start();\n * ```\n */\n\nimport { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http';\nimport type {\n ProtocolHandler,\n ProtocolRequest,\n ProtocolContext,\n ProtocolCapabilities,\n ProtocolStreamChunk,\n} from './types';\nimport { verifyRequestSignature } from './auth';\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface ServerConfig {\n /** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */\n token: string;\n\n /** Port to listen on. Defaults to 3001, or the PORT env variable. */\n port?: number;\n\n /** Host to bind to. Defaults to '0.0.0.0'. */\n host?: string;\n\n /** Skip signature verification (for local development only). */\n skipSignatureVerification?: boolean;\n\n /**\n * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\n}\n\n// ============================================\n// Server Instance\n// ============================================\n\nexport interface FirstStepServer {\n /** Start the server */\n start(): Promise<void>;\n\n /** Stop the server gracefully */\n stop(): Promise<void>;\n\n /**\n * Get the request handler function.\n * Use this to integrate with Express, Fastify, or other frameworks.\n *\n * @example\n * ```typescript\n * // Express\n * const server = createServer(handler, { token: '...' });\n * app.post('/handshake', server.getRequestHandler());\n * app.post('/message', server.getRequestHandler());\n * ```\n */\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;\n}\n\n// ============================================\n// Internal Helpers\n// ============================================\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json),\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(json);\n}\n\nfunction parsePath(url: string | undefined): string {\n if (!url) return '/';\n const idx = url.indexOf('?');\n return idx >= 0 ? url.substring(0, idx) : url;\n}\n\n/**\n * Build a stub ProtocolContext for external handlers.\n *\n * External handlers receive context data as plain JSON from the platform.\n * The stub provides no-op implementations for methods like session.getState()\n * since those will be handled by callback APIs to the FirstStep backend.\n *\n * When the platform sends a /message request, the request body includes\n * a `context` field with serialized context data. This function merges\n * that data with no-op method stubs.\n */\nfunction buildStubContext(data?: Partial<ProtocolContext>): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n return {\n session: {\n sessionId: '',\n getState: noopAsync,\n updateState: noopAsync,\n getHistory: async () => [],\n saveMessage: noopAsync,\n complete: noopAsync,\n getFormData: async () => ({}),\n updateFormField: noopAsync,\n updateFormData: noopAsync,\n ...data?.session,\n },\n knowledge: {\n queryDatabase: noopAsync,\n searchPages: noopAsync,\n ...data?.knowledge,\n },\n integrations: {\n execute: noopAsync,\n ...data?.integrations,\n },\n analytics: {\n logActionExecuted: noop,\n logInteraction: noop,\n logCustomEvent: noop,\n ...data?.analytics,\n },\n logger: {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n logRouting: noop,\n logToolUse: noop,\n ...data?.logger,\n },\n deployment: {\n id: '',\n slug: '',\n name: '',\n protocolType: 'external',\n ...data?.deployment,\n },\n chatbot: data?.chatbot,\n };\n}\n\n// ============================================\n// createServer\n// ============================================\n\n/**\n * Create a standalone HTTP server for a protocol handler.\n *\n * The server exposes three endpoints:\n * - `GET /health` - Health check (always 200)\n * - `POST /handshake` - Returns handler capabilities (signature verified)\n * - `POST /message` - Handles a chat message (signature verified)\n */\nexport function createServer(\n handler: ProtocolHandler,\n config: ServerConfig\n): FirstStepServer {\n const {\n token,\n port = parseInt(process.env.PORT || '3001', 10),\n host = '0.0.0.0',\n skipSignatureVerification = false,\n prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\n\n // Validate config\n if (!token && !skipSignatureVerification) {\n throw new Error(\n 'FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev.'\n );\n }\n\n /**\n * Core request router\n */\n async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const path = parsePath(req.url);\n const method = (req.method || 'GET').toUpperCase();\n\n // CORS preflight\n if (method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-FirstStep-Signature',\n 'Access-Control-Max-Age': '86400',\n });\n res.end();\n return;\n }\n\n // Strip prefix to get the remaining path\n const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Extract optional config slug from path: /{slug}/message -> slug = \"slug\", route = \"/message\"\n // If no slug segment, route is the full remaining path (backward compatible)\n let configSlug: string | undefined;\n let route: string;\n const slugMatch = afterPrefix.match(/^\\/([^/]+)(\\/(?:health|handshake|message(?:\\/stream)?))?$/);\n if (slugMatch && slugMatch[2]) {\n // Path has a slug segment: /{slug}/{endpoint}\n configSlug = slugMatch[1];\n route = slugMatch[2];\n } else {\n // No slug: /{endpoint} (backward compatible)\n route = afterPrefix;\n }\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/handshake' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n const capabilities: ProtocolCapabilities = handler.getCapabilities();\n const handlerInfo = handler.getHandlerInfo?.();\n sendJson(res, 200, { capabilities, handler: handlerInfo || undefined });\n return;\n }\n\n // Message\n if (route === '/message' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n const context = buildStubContext(parsed.context);\n\n try {\n const response = await handler.handleMessage(parsed.request, context);\n sendJson(res, 200, response);\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, {\n error: 'Handler error',\n message: err?.message || 'Unknown error',\n });\n }\n return;\n }\n\n // Message (streaming via SSE)\n if (route === '/message/stream' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n if (!handler.handleStream) {\n // Fallback: use non-streaming handleMessage, send as single SSE burst\n const context = buildStubContext(parsed.context);\n try {\n const response = await handler.handleMessage(parsed.request, context);\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n sendSSE('connected', { sessionId: response.sessionId });\n sendSSE('text', { type: 'text', content: response.message, sessionId: response.sessionId });\n if (response.metadata) {\n sendSSE('metadata', { type: 'metadata', content: response.metadata, sessionId: response.sessionId });\n }\n if (response.form) {\n sendSSE('form', { type: 'form', content: response.form, sessionId: response.sessionId });\n }\n sendSSE('status', { type: 'status', content: response.sessionStatus, sessionId: response.sessionId });\n sendSSE('done', { sessionId: response.sessionId });\n res.end();\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, { error: 'Handler error', message: err?.message || 'Unknown error' });\n }\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n\n const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;\n sendSSE('connected', { sessionId });\n\n try {\n for await (const chunk of handler.handleStream(parsed.request, context)) {\n sendSSE(chunk.type, {\n type: chunk.type,\n content: chunk.content,\n sessionId,\n });\n }\n sendSSE('done', { sessionId });\n } catch (err: any) {\n console.error('[firststep] Stream error:', err);\n sendSSE('error', { code: 'STREAM_ERROR', message: err?.message || 'Unknown error' });\n }\n\n res.end();\n return;\n }\n\n // 404\n sendJson(res, 404, { error: 'Not found' });\n }\n\n // Wrap in error boundary\n function requestListener(req: IncomingMessage, res: ServerResponse): void {\n handleRequest(req, res).catch((err) => {\n console.error('[firststep] Unexpected error:', err);\n if (!res.headersSent) {\n sendJson(res, 500, { error: 'Internal server error' });\n }\n });\n }\n\n const httpServer = createHttpServer(requestListener);\n\n return {\n start() {\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n console.log(`[firststep] Handler server listening on ${host}:${port}`);\n console.log(`[firststep] Endpoints:`);\n console.log(` GET ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/message/stream - Handle chat message (SSE stream)`);\n resolve();\n });\n });\n },\n\n stop() {\n return new Promise<void>((resolve, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n getRequestHandler() {\n return requestListener;\n },\n };\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":["context","sendSSE","createHttpServer"]}
|
package/dist/server.mjs
CHANGED
|
@@ -122,7 +122,16 @@ function createServer(handler, config) {
|
|
|
122
122
|
res.end();
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
|
-
const
|
|
125
|
+
const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
126
|
+
let configSlug;
|
|
127
|
+
let route;
|
|
128
|
+
const slugMatch = afterPrefix.match(/^\/([^/]+)(\/(?:health|handshake|message(?:\/stream)?))?$/);
|
|
129
|
+
if (slugMatch && slugMatch[2]) {
|
|
130
|
+
configSlug = slugMatch[1];
|
|
131
|
+
route = slugMatch[2];
|
|
132
|
+
} else {
|
|
133
|
+
route = afterPrefix;
|
|
134
|
+
}
|
|
126
135
|
if (route === "/health" && method === "GET") {
|
|
127
136
|
sendJson(res, 200, { status: "ok", timestamp: Date.now() });
|
|
128
137
|
return;
|
|
@@ -157,6 +166,9 @@ function createServer(handler, config) {
|
|
|
157
166
|
sendJson(res, 400, { error: "Invalid JSON body" });
|
|
158
167
|
return;
|
|
159
168
|
}
|
|
169
|
+
if (configSlug) {
|
|
170
|
+
parsed.request.configSlug = configSlug;
|
|
171
|
+
}
|
|
160
172
|
const context = buildStubContext(parsed.context);
|
|
161
173
|
try {
|
|
162
174
|
const response = await handler.handleMessage(parsed.request, context);
|
|
@@ -186,6 +198,9 @@ function createServer(handler, config) {
|
|
|
186
198
|
sendJson(res, 400, { error: "Invalid JSON body" });
|
|
187
199
|
return;
|
|
188
200
|
}
|
|
201
|
+
if (configSlug) {
|
|
202
|
+
parsed.request.configSlug = configSlug;
|
|
203
|
+
}
|
|
189
204
|
if (!handler.handleStream) {
|
|
190
205
|
const context2 = buildStubContext(parsed.context);
|
|
191
206
|
try {
|
package/dist/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK Server\n *\n * Standalone HTTP server for protocol handlers.\n * Zero external dependencies, uses Node.js built-in `http` module.\n *\n * @example\n * ```typescript\n * import { createServer } from '@firststep-studio/sdk/server';\n * import type { ProtocolHandler } from '@firststep-studio/sdk';\n *\n * const handler: ProtocolHandler = {\n * async handleMessage(request, context) {\n * return {\n * message: 'Hello from my handler!',\n * sessionId: request.sessionId || 'new',\n * agentId: 'main',\n * sessionStatus: 'active',\n * };\n * },\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * },\n * };\n *\n * const server = createServer(handler, {\n * token: process.env.FIRSTSTEP_TOKEN!,\n * port: 3001,\n * });\n *\n * server.start();\n * ```\n */\n\nimport { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http';\nimport type {\n ProtocolHandler,\n ProtocolRequest,\n ProtocolContext,\n ProtocolCapabilities,\n ProtocolStreamChunk,\n} from './types';\nimport { verifyRequestSignature } from './auth';\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface ServerConfig {\n /** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */\n token: string;\n\n /** Port to listen on. Defaults to 3001, or the PORT env variable. */\n port?: number;\n\n /** Host to bind to. Defaults to '0.0.0.0'. */\n host?: string;\n\n /** Skip signature verification (for local development only). */\n skipSignatureVerification?: boolean;\n\n /**\n * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\n}\n\n// ============================================\n// Server Instance\n// ============================================\n\nexport interface FirstStepServer {\n /** Start the server */\n start(): Promise<void>;\n\n /** Stop the server gracefully */\n stop(): Promise<void>;\n\n /**\n * Get the request handler function.\n * Use this to integrate with Express, Fastify, or other frameworks.\n *\n * @example\n * ```typescript\n * // Express\n * const server = createServer(handler, { token: '...' });\n * app.post('/handshake', server.getRequestHandler());\n * app.post('/message', server.getRequestHandler());\n * ```\n */\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;\n}\n\n// ============================================\n// Internal Helpers\n// ============================================\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json),\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(json);\n}\n\nfunction parsePath(url: string | undefined): string {\n if (!url) return '/';\n const idx = url.indexOf('?');\n return idx >= 0 ? url.substring(0, idx) : url;\n}\n\n/**\n * Build a stub ProtocolContext for external handlers.\n *\n * External handlers receive context data as plain JSON from the platform.\n * The stub provides no-op implementations for methods like session.getState()\n * since those will be handled by callback APIs to the FirstStep backend.\n *\n * When the platform sends a /message request, the request body includes\n * a `context` field with serialized context data. This function merges\n * that data with no-op method stubs.\n */\nfunction buildStubContext(data?: Partial<ProtocolContext>): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n return {\n session: {\n sessionId: '',\n getState: noopAsync,\n updateState: noopAsync,\n getHistory: async () => [],\n saveMessage: noopAsync,\n complete: noopAsync,\n getFormData: async () => ({}),\n updateFormField: noopAsync,\n updateFormData: noopAsync,\n ...data?.session,\n },\n knowledge: {\n queryDatabase: noopAsync,\n searchPages: noopAsync,\n ...data?.knowledge,\n },\n integrations: {\n execute: noopAsync,\n ...data?.integrations,\n },\n analytics: {\n logActionExecuted: noop,\n logInteraction: noop,\n logCustomEvent: noop,\n ...data?.analytics,\n },\n logger: {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n logRouting: noop,\n logToolUse: noop,\n ...data?.logger,\n },\n deployment: {\n id: '',\n slug: '',\n name: '',\n protocolType: 'external',\n ...data?.deployment,\n },\n chatbot: data?.chatbot,\n };\n}\n\n// ============================================\n// createServer\n// ============================================\n\n/**\n * Create a standalone HTTP server for a protocol handler.\n *\n * The server exposes three endpoints:\n * - `GET /health` - Health check (always 200)\n * - `POST /handshake` - Returns handler capabilities (signature verified)\n * - `POST /message` - Handles a chat message (signature verified)\n */\nexport function createServer(\n handler: ProtocolHandler,\n config: ServerConfig\n): FirstStepServer {\n const {\n token,\n port = parseInt(process.env.PORT || '3001', 10),\n host = '0.0.0.0',\n skipSignatureVerification = false,\n prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\n\n // Validate config\n if (!token && !skipSignatureVerification) {\n throw new Error(\n 'FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev.'\n );\n }\n\n /**\n * Core request router\n */\n async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const path = parsePath(req.url);\n const method = (req.method || 'GET').toUpperCase();\n\n // CORS preflight\n if (method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-FirstStep-Signature',\n 'Access-Control-Max-Age': '86400',\n });\n res.end();\n return;\n }\n\n // Strip prefix to match routes\n const route = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/handshake' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n const capabilities: ProtocolCapabilities = handler.getCapabilities();\n const handlerInfo = handler.getHandlerInfo?.();\n sendJson(res, 200, { capabilities, handler: handlerInfo || undefined });\n return;\n }\n\n // Message\n if (route === '/message' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n try {\n const response = await handler.handleMessage(parsed.request, context);\n sendJson(res, 200, response);\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, {\n error: 'Handler error',\n message: err?.message || 'Unknown error',\n });\n }\n return;\n }\n\n // Message (streaming via SSE)\n if (route === '/message/stream' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n if (!handler.handleStream) {\n // Fallback: use non-streaming handleMessage, send as single SSE burst\n const context = buildStubContext(parsed.context);\n try {\n const response = await handler.handleMessage(parsed.request, context);\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n sendSSE('connected', { sessionId: response.sessionId });\n sendSSE('text', { type: 'text', content: response.message, sessionId: response.sessionId });\n if (response.metadata) {\n sendSSE('metadata', { type: 'metadata', content: response.metadata, sessionId: response.sessionId });\n }\n if (response.form) {\n sendSSE('form', { type: 'form', content: response.form, sessionId: response.sessionId });\n }\n sendSSE('status', { type: 'status', content: response.sessionStatus, sessionId: response.sessionId });\n sendSSE('done', { sessionId: response.sessionId });\n res.end();\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, { error: 'Handler error', message: err?.message || 'Unknown error' });\n }\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n\n const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;\n sendSSE('connected', { sessionId });\n\n try {\n for await (const chunk of handler.handleStream(parsed.request, context)) {\n sendSSE(chunk.type, {\n type: chunk.type,\n content: chunk.content,\n sessionId,\n });\n }\n sendSSE('done', { sessionId });\n } catch (err: any) {\n console.error('[firststep] Stream error:', err);\n sendSSE('error', { code: 'STREAM_ERROR', message: err?.message || 'Unknown error' });\n }\n\n res.end();\n return;\n }\n\n // 404\n sendJson(res, 404, { error: 'Not found' });\n }\n\n // Wrap in error boundary\n function requestListener(req: IncomingMessage, res: ServerResponse): void {\n handleRequest(req, res).catch((err) => {\n console.error('[firststep] Unexpected error:', err);\n if (!res.headersSent) {\n sendJson(res, 500, { error: 'Internal server error' });\n }\n });\n }\n\n const httpServer = createHttpServer(requestListener);\n\n return {\n start() {\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n console.log(`[firststep] Handler server listening on ${host}:${port}`);\n console.log(`[firststep] Endpoints:`);\n console.log(` GET ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/message/stream - Handle chat message (SSE stream)`);\n resolve();\n });\n });\n },\n\n stop() {\n return new Promise<void>((resolve, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n getRequestHandler() {\n return requestListener;\n },\n };\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":["context","sendSSE"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK Server\n *\n * Standalone HTTP server for protocol handlers.\n * Zero external dependencies, uses Node.js built-in `http` module.\n *\n * @example\n * ```typescript\n * import { createServer } from '@firststep-studio/sdk/server';\n * import type { ProtocolHandler } from '@firststep-studio/sdk';\n *\n * const handler: ProtocolHandler = {\n * async handleMessage(request, context) {\n * return {\n * message: 'Hello from my handler!',\n * sessionId: request.sessionId || 'new',\n * agentId: 'main',\n * sessionStatus: 'active',\n * };\n * },\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * },\n * };\n *\n * const server = createServer(handler, {\n * token: process.env.FIRSTSTEP_TOKEN!,\n * port: 3001,\n * });\n *\n * server.start();\n * ```\n */\n\nimport { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'http';\nimport type {\n ProtocolHandler,\n ProtocolRequest,\n ProtocolContext,\n ProtocolCapabilities,\n ProtocolStreamChunk,\n} from './types';\nimport { verifyRequestSignature } from './auth';\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface ServerConfig {\n /** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */\n token: string;\n\n /** Port to listen on. Defaults to 3001, or the PORT env variable. */\n port?: number;\n\n /** Host to bind to. Defaults to '0.0.0.0'. */\n host?: string;\n\n /** Skip signature verification (for local development only). */\n skipSignatureVerification?: boolean;\n\n /**\n * URL prefix for all endpoints.\n * Example: '/ucp/v1' makes endpoints available at /ucp/v1/handshake, /ucp/v1/message, etc.\n */\n prefix?: string;\n}\n\n// ============================================\n// Server Instance\n// ============================================\n\nexport interface FirstStepServer {\n /** Start the server */\n start(): Promise<void>;\n\n /** Stop the server gracefully */\n stop(): Promise<void>;\n\n /**\n * Get the request handler function.\n * Use this to integrate with Express, Fastify, or other frameworks.\n *\n * @example\n * ```typescript\n * // Express\n * const server = createServer(handler, { token: '...' });\n * app.post('/handshake', server.getRequestHandler());\n * app.post('/message', server.getRequestHandler());\n * ```\n */\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;\n}\n\n// ============================================\n// Internal Helpers\n// ============================================\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json),\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(json);\n}\n\nfunction parsePath(url: string | undefined): string {\n if (!url) return '/';\n const idx = url.indexOf('?');\n return idx >= 0 ? url.substring(0, idx) : url;\n}\n\n/**\n * Build a stub ProtocolContext for external handlers.\n *\n * External handlers receive context data as plain JSON from the platform.\n * The stub provides no-op implementations for methods like session.getState()\n * since those will be handled by callback APIs to the FirstStep backend.\n *\n * When the platform sends a /message request, the request body includes\n * a `context` field with serialized context data. This function merges\n * that data with no-op method stubs.\n */\nfunction buildStubContext(data?: Partial<ProtocolContext>): ProtocolContext {\n const noopAsync = async () => ({} as any);\n const noop = () => {};\n\n return {\n session: {\n sessionId: '',\n getState: noopAsync,\n updateState: noopAsync,\n getHistory: async () => [],\n saveMessage: noopAsync,\n complete: noopAsync,\n getFormData: async () => ({}),\n updateFormField: noopAsync,\n updateFormData: noopAsync,\n ...data?.session,\n },\n knowledge: {\n queryDatabase: noopAsync,\n searchPages: noopAsync,\n ...data?.knowledge,\n },\n integrations: {\n execute: noopAsync,\n ...data?.integrations,\n },\n analytics: {\n logActionExecuted: noop,\n logInteraction: noop,\n logCustomEvent: noop,\n ...data?.analytics,\n },\n logger: {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n logRouting: noop,\n logToolUse: noop,\n ...data?.logger,\n },\n deployment: {\n id: '',\n slug: '',\n name: '',\n protocolType: 'external',\n ...data?.deployment,\n },\n chatbot: data?.chatbot,\n };\n}\n\n// ============================================\n// createServer\n// ============================================\n\n/**\n * Create a standalone HTTP server for a protocol handler.\n *\n * The server exposes three endpoints:\n * - `GET /health` - Health check (always 200)\n * - `POST /handshake` - Returns handler capabilities (signature verified)\n * - `POST /message` - Handles a chat message (signature verified)\n */\nexport function createServer(\n handler: ProtocolHandler,\n config: ServerConfig\n): FirstStepServer {\n const {\n token,\n port = parseInt(process.env.PORT || '3001', 10),\n host = '0.0.0.0',\n skipSignatureVerification = false,\n prefix: rawPrefix = '',\n } = config;\n\n // Normalize prefix: ensure leading slash, no trailing slash\n const prefix = rawPrefix ? ('/' + rawPrefix.replace(/^\\/|\\/$/g, '')) : '';\n\n // Validate config\n if (!token && !skipSignatureVerification) {\n throw new Error(\n 'FIRSTSTEP_TOKEN is required. Pass it via config.token or set skipSignatureVerification for local dev.'\n );\n }\n\n /**\n * Core request router\n */\n async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const path = parsePath(req.url);\n const method = (req.method || 'GET').toUpperCase();\n\n // CORS preflight\n if (method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-FirstStep-Signature',\n 'Access-Control-Max-Age': '86400',\n });\n res.end();\n return;\n }\n\n // Strip prefix to get the remaining path\n const afterPrefix = prefix && path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;\n\n // Extract optional config slug from path: /{slug}/message -> slug = \"slug\", route = \"/message\"\n // If no slug segment, route is the full remaining path (backward compatible)\n let configSlug: string | undefined;\n let route: string;\n const slugMatch = afterPrefix.match(/^\\/([^/]+)(\\/(?:health|handshake|message(?:\\/stream)?))?$/);\n if (slugMatch && slugMatch[2]) {\n // Path has a slug segment: /{slug}/{endpoint}\n configSlug = slugMatch[1];\n route = slugMatch[2];\n } else {\n // No slug: /{endpoint} (backward compatible)\n route = afterPrefix;\n }\n\n // Health check\n if (route === '/health' && method === 'GET') {\n sendJson(res, 200, { status: 'ok', timestamp: Date.now() });\n return;\n }\n\n // Handshake\n // Signature is verified when present, but not required.\n // This allows the frontend to probe handler capabilities before\n // a project (and its API token binding) exists.\n if (route === '/handshake' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n const capabilities: ProtocolCapabilities = handler.getCapabilities();\n const handlerInfo = handler.getHandlerInfo?.();\n sendJson(res, 200, { capabilities, handler: handlerInfo || undefined });\n return;\n }\n\n // Message\n if (route === '/message' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n const context = buildStubContext(parsed.context);\n\n try {\n const response = await handler.handleMessage(parsed.request, context);\n sendJson(res, 200, response);\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, {\n error: 'Handler error',\n message: err?.message || 'Unknown error',\n });\n }\n return;\n }\n\n // Message (streaming via SSE)\n if (route === '/message/stream' && method === 'POST') {\n const body = await readBody(req);\n\n if (!skipSignatureVerification) {\n const signature = req.headers['x-firststep-signature'] as string;\n if (signature && !verifyRequestSignature(token, body, signature)) {\n sendJson(res, 401, { error: 'Invalid signature' });\n return;\n }\n }\n\n let parsed: { request: ProtocolRequest; context?: Partial<ProtocolContext> };\n try {\n parsed = JSON.parse(body);\n } catch {\n sendJson(res, 400, { error: 'Invalid JSON body' });\n return;\n }\n\n // Inject config slug from URL path into request\n if (configSlug) {\n parsed.request.configSlug = configSlug;\n }\n\n if (!handler.handleStream) {\n // Fallback: use non-streaming handleMessage, send as single SSE burst\n const context = buildStubContext(parsed.context);\n try {\n const response = await handler.handleMessage(parsed.request, context);\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n sendSSE('connected', { sessionId: response.sessionId });\n sendSSE('text', { type: 'text', content: response.message, sessionId: response.sessionId });\n if (response.metadata) {\n sendSSE('metadata', { type: 'metadata', content: response.metadata, sessionId: response.sessionId });\n }\n if (response.form) {\n sendSSE('form', { type: 'form', content: response.form, sessionId: response.sessionId });\n }\n sendSSE('status', { type: 'status', content: response.sessionStatus, sessionId: response.sessionId });\n sendSSE('done', { sessionId: response.sessionId });\n res.end();\n } catch (err: any) {\n console.error('[firststep] Handler error:', err);\n sendJson(res, 500, { error: 'Handler error', message: err?.message || 'Unknown error' });\n }\n return;\n }\n\n const context = buildStubContext(parsed.context);\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const sendSSE = (event: string, data: unknown) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n\n const sessionId = parsed.request.sessionId || `ext-${Date.now()}`;\n sendSSE('connected', { sessionId });\n\n try {\n for await (const chunk of handler.handleStream(parsed.request, context)) {\n sendSSE(chunk.type, {\n type: chunk.type,\n content: chunk.content,\n sessionId,\n });\n }\n sendSSE('done', { sessionId });\n } catch (err: any) {\n console.error('[firststep] Stream error:', err);\n sendSSE('error', { code: 'STREAM_ERROR', message: err?.message || 'Unknown error' });\n }\n\n res.end();\n return;\n }\n\n // 404\n sendJson(res, 404, { error: 'Not found' });\n }\n\n // Wrap in error boundary\n function requestListener(req: IncomingMessage, res: ServerResponse): void {\n handleRequest(req, res).catch((err) => {\n console.error('[firststep] Unexpected error:', err);\n if (!res.headersSent) {\n sendJson(res, 500, { error: 'Internal server error' });\n }\n });\n }\n\n const httpServer = createHttpServer(requestListener);\n\n return {\n start() {\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n console.log(`[firststep] Handler server listening on ${host}:${port}`);\n console.log(`[firststep] Endpoints:`);\n console.log(` GET ${prefix}/health - Health check`);\n console.log(` POST ${prefix}/handshake - Capability exchange`);\n console.log(` POST ${prefix}/message - Handle chat message`);\n console.log(` POST ${prefix}/message/stream - Handle chat message (SSE stream)`);\n resolve();\n });\n });\n },\n\n stop() {\n return new Promise<void>((resolve, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n getRequestHandler() {\n return requestListener;\n },\n };\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":["context","sendSSE"]}
|
|
@@ -17,6 +17,13 @@ interface ProtocolRequest {
|
|
|
17
17
|
deploymentSlug: string;
|
|
18
18
|
/** Additional metadata */
|
|
19
19
|
metadata?: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Handler-side config slug extracted from the URL path.
|
|
22
|
+
* When the handler URL includes a slug (e.g. /ucp/v1/safety-navigator/message),
|
|
23
|
+
* the SDK server extracts it and sets this field.
|
|
24
|
+
* Used by multi-config handlers to route to the correct config.
|
|
25
|
+
*/
|
|
26
|
+
configSlug?: string;
|
|
20
27
|
/**
|
|
21
28
|
* Handoff context injected by Studio when a handoff is in progress.
|
|
22
29
|
* Present on the first message after a handoff_start or handoff_return.
|
|
@@ -17,6 +17,13 @@ interface ProtocolRequest {
|
|
|
17
17
|
deploymentSlug: string;
|
|
18
18
|
/** Additional metadata */
|
|
19
19
|
metadata?: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Handler-side config slug extracted from the URL path.
|
|
22
|
+
* When the handler URL includes a slug (e.g. /ucp/v1/safety-navigator/message),
|
|
23
|
+
* the SDK server extracts it and sets this field.
|
|
24
|
+
* Used by multi-config handlers to route to the correct config.
|
|
25
|
+
*/
|
|
26
|
+
configSlug?: string;
|
|
20
27
|
/**
|
|
21
28
|
* Handoff context injected by Studio when a handoff is in progress.
|
|
22
29
|
* Present on the first message after a handoff_start or handoff_return.
|