@firststep-studio/sdk 0.1.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/README.md +40 -0
- package/dist/index.d.mts +299 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +204 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +77 -0
- package/dist/server.d.ts +77 -0
- package/dist/server.js +312 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +287 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types-Bm98aHcd.d.mts +438 -0
- package/dist/types-Bm98aHcd.d.ts +438 -0
- package/llms.txt +597 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
UCPClient: () => UCPClient,
|
|
24
|
+
UCPError: () => UCPError,
|
|
25
|
+
classifyWithUCP: () => classifyWithUCP,
|
|
26
|
+
createAuthHeader: () => createAuthHeader,
|
|
27
|
+
createRequestSignature: () => createRequestSignature,
|
|
28
|
+
createUCPClient: () => createUCPClient,
|
|
29
|
+
isValidToken: () => isValidToken,
|
|
30
|
+
verifyRequestSignature: () => verifyRequestSignature
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/ucp/client.ts
|
|
35
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
36
|
+
var UCPClient = class _UCPClient {
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
39
|
+
this.classifierId = config.classifierId;
|
|
40
|
+
this.apiKey = config.apiKey;
|
|
41
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
42
|
+
this.headers = config.headers ?? {};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create client from a full endpoint URL
|
|
46
|
+
*
|
|
47
|
+
* URL format: {baseUrl}/classifiers/{classifierId}
|
|
48
|
+
* Example: https://api.example.com/ucp/v1/classifiers/abc123
|
|
49
|
+
*/
|
|
50
|
+
static fromEndpoint(endpoint, options) {
|
|
51
|
+
const url = new URL(endpoint);
|
|
52
|
+
const pathParts = url.pathname.split("/");
|
|
53
|
+
const classifiersIndex = pathParts.indexOf("classifiers");
|
|
54
|
+
if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const classifierId = pathParts[classifiersIndex + 1];
|
|
60
|
+
const baseUrlPath = pathParts.slice(0, classifiersIndex).join("/");
|
|
61
|
+
const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;
|
|
62
|
+
return new _UCPClient({
|
|
63
|
+
baseUrl,
|
|
64
|
+
classifierId,
|
|
65
|
+
apiKey: options?.apiKey,
|
|
66
|
+
timeout: options?.timeout
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the classify endpoint URL
|
|
71
|
+
*/
|
|
72
|
+
get classifyUrl() {
|
|
73
|
+
return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get the info endpoint URL
|
|
77
|
+
*/
|
|
78
|
+
get infoUrl() {
|
|
79
|
+
return `${this.baseUrl}/classifiers/${this.classifierId}/info`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Classify messages using UCP protocol
|
|
83
|
+
*/
|
|
84
|
+
async classify(messages) {
|
|
85
|
+
const request = { messages };
|
|
86
|
+
const response = await this.fetch(
|
|
87
|
+
this.classifyUrl,
|
|
88
|
+
{
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: JSON.stringify(request)
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
classifierId: this.classifierId,
|
|
95
|
+
category: response.category,
|
|
96
|
+
level: response.level,
|
|
97
|
+
confidence: response.score / 100,
|
|
98
|
+
// Normalize to 0-1
|
|
99
|
+
reasoning: response.rationale,
|
|
100
|
+
raw: response
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Classify ChatMessage array (convenience method)
|
|
105
|
+
* Converts ChatMessage to UCPMessage format
|
|
106
|
+
*/
|
|
107
|
+
async classifyChat(messages) {
|
|
108
|
+
const ucpMessages = messages.filter((m) => m.role === "user" || m.role === "assistant").map((m, index) => ({
|
|
109
|
+
id: `msg-${index}`,
|
|
110
|
+
role: m.role,
|
|
111
|
+
content: m.content,
|
|
112
|
+
timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),
|
|
113
|
+
metadata: m.metadata
|
|
114
|
+
}));
|
|
115
|
+
return this.classify(ucpMessages);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get classifier info
|
|
119
|
+
*/
|
|
120
|
+
async getInfo() {
|
|
121
|
+
return this.fetch(this.infoUrl, {
|
|
122
|
+
method: "GET"
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check if the classifier endpoint is reachable
|
|
127
|
+
*/
|
|
128
|
+
async healthCheck() {
|
|
129
|
+
try {
|
|
130
|
+
const healthUrl = `${this.baseUrl}/health`;
|
|
131
|
+
const response = await fetch(healthUrl, {
|
|
132
|
+
method: "GET",
|
|
133
|
+
signal: AbortSignal.timeout(5e3)
|
|
134
|
+
});
|
|
135
|
+
return response.ok;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Internal fetch helper with error handling
|
|
142
|
+
*/
|
|
143
|
+
async fetch(url, options) {
|
|
144
|
+
const headers = {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
...this.headers
|
|
147
|
+
};
|
|
148
|
+
if (this.apiKey) {
|
|
149
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
150
|
+
}
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(url, {
|
|
155
|
+
...options,
|
|
156
|
+
headers,
|
|
157
|
+
signal: controller.signal
|
|
158
|
+
});
|
|
159
|
+
clearTimeout(timeoutId);
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const errorData = await response.json().catch(() => ({}));
|
|
162
|
+
const ucpError = errorData;
|
|
163
|
+
throw new UCPError(
|
|
164
|
+
ucpError.error?.code || `HTTP_${response.status}`,
|
|
165
|
+
ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
166
|
+
ucpError.error?.details
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return response.json();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
clearTimeout(timeoutId);
|
|
172
|
+
if (error instanceof UCPError) {
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
if (error instanceof Error) {
|
|
176
|
+
if (error.name === "AbortError") {
|
|
177
|
+
throw new UCPError("TIMEOUT", `Request timed out after ${this.timeout}ms`);
|
|
178
|
+
}
|
|
179
|
+
throw new UCPError("NETWORK_ERROR", error.message);
|
|
180
|
+
}
|
|
181
|
+
throw new UCPError("UNKNOWN_ERROR", "An unknown error occurred");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var UCPError = class extends Error {
|
|
186
|
+
constructor(code, message, details) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.code = code;
|
|
189
|
+
this.details = details;
|
|
190
|
+
this.name = "UCPError";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function createUCPClient(endpoint, options) {
|
|
194
|
+
return UCPClient.fromEndpoint(endpoint, options);
|
|
195
|
+
}
|
|
196
|
+
async function classifyWithUCP(endpoint, messages, options) {
|
|
197
|
+
const client = createUCPClient(endpoint, options);
|
|
198
|
+
return client.classifyChat(messages);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/auth.ts
|
|
202
|
+
var import_crypto = require("crypto");
|
|
203
|
+
var TOKEN_PREFIX = "fst_";
|
|
204
|
+
var TOKEN_LENGTH = 44;
|
|
205
|
+
function verifyRequestSignature(token, payload, signature) {
|
|
206
|
+
try {
|
|
207
|
+
const expected = (0, import_crypto.createHmac)("sha256", token).update(payload).digest("hex");
|
|
208
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
209
|
+
const signatureBuffer = Buffer.from(signature, "hex");
|
|
210
|
+
if (expectedBuffer.length !== signatureBuffer.length) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return (0, import_crypto.timingSafeEqual)(expectedBuffer, signatureBuffer);
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function createRequestSignature(token, payload) {
|
|
219
|
+
return (0, import_crypto.createHmac)("sha256", token).update(payload).digest("hex");
|
|
220
|
+
}
|
|
221
|
+
function createAuthHeader(token) {
|
|
222
|
+
return `Bearer ${token}`;
|
|
223
|
+
}
|
|
224
|
+
function isValidToken(token) {
|
|
225
|
+
return typeof token === "string" && token.startsWith(TOKEN_PREFIX) && token.length === TOKEN_LENGTH;
|
|
226
|
+
}
|
|
227
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
228
|
+
0 && (module.exports = {
|
|
229
|
+
UCPClient,
|
|
230
|
+
UCPError,
|
|
231
|
+
classifyWithUCP,
|
|
232
|
+
createAuthHeader,
|
|
233
|
+
createRequestSignature,
|
|
234
|
+
createUCPClient,
|
|
235
|
+
isValidToken,
|
|
236
|
+
verifyRequestSignature
|
|
237
|
+
});
|
|
238
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ucp/client.ts","../src/auth.ts"],"sourcesContent":["/**\n * FirstStep SDK\n *\n * SDK for building protocol handlers that integrate with FirstStep Studio.\n *\n * @example\n * ```typescript\n * import { ProtocolHandler, ProtocolRequest, ProtocolResponse, ProtocolContext } from '@firststep-studio/sdk';\n *\n * class MyHandler implements ProtocolHandler {\n * async handleMessage(request: ProtocolRequest, context: ProtocolContext): Promise<ProtocolResponse> {\n * // Your implementation\n * }\n *\n * getCapabilities() {\n * return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };\n * }\n * }\n * ```\n */\n\n// Core types\nexport type {\n ProtocolRequest,\n ProtocolResponse,\n SessionStatus,\n} from './types';\n\n// Form types\nexport type {\n ProtocolForm,\n ProtocolFormField,\n ProtocolFormOption,\n ProtocolFieldValidation,\n} from './types';\n\n// Streaming types\nexport type {\n ProtocolStreamChunk,\n ProtocolError,\n} from './types';\n\n// Handler interface\nexport type {\n ProtocolHandler,\n ProtocolCapabilities,\n HandlerInfo,\n} from './types';\n\n// Context types\nexport type {\n ProtocolContext,\n SessionContext,\n SessionState,\n ChatMessage,\n KnowledgeContext,\n KnowledgeResult,\n IntegrationContext,\n IntegrationResult,\n HelplineSearchOptions,\n HelplineResult,\n Helpline,\n LoggerContext,\n RoutingDecision,\n DeploymentInfo,\n ChatbotInfo,\n ClassifierConfig,\n} from './types';\n\n// ============================================\n// Platform Standard Data Types\n// ============================================\n\nexport type {\n // Form Data\n FormData,\n FormFieldValue,\n\n // Routing Logs\n RoutingLog,\n\n // Session Metadata\n SessionMetadata,\n\n // Analytics\n AnalyticsContext,\n InteractionEvent,\n InteractionEventType,\n\n // Form Schema (Protocol Registration)\n FormSchema,\n FormFieldDefinition,\n FormFieldType,\n\n // Protocol Registration\n ProtocolRegistration,\n} from './types';\n\n// ============================================\n// UCP (Universal Classification Protocol)\n// ============================================\n\n// UCP Types\nexport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './ucp';\n\n// UCP Client\nexport {\n UCPClient,\n UCPError,\n createUCPClient,\n classifyWithUCP,\n} from './ucp';\n\n// ============================================\n// Auth Utilities\n// ============================================\n\nexport {\n verifyRequestSignature,\n createRequestSignature,\n createAuthHeader,\n isValidToken,\n} from './auth';\n","/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","import { createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const expected = createHmac('sha256', token)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n return createHmac('sha256', token).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// src/ucp/client.ts
|
|
2
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
3
|
+
var UCPClient = class _UCPClient {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
6
|
+
this.classifierId = config.classifierId;
|
|
7
|
+
this.apiKey = config.apiKey;
|
|
8
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
9
|
+
this.headers = config.headers ?? {};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create client from a full endpoint URL
|
|
13
|
+
*
|
|
14
|
+
* URL format: {baseUrl}/classifiers/{classifierId}
|
|
15
|
+
* Example: https://api.example.com/ucp/v1/classifiers/abc123
|
|
16
|
+
*/
|
|
17
|
+
static fromEndpoint(endpoint, options) {
|
|
18
|
+
const url = new URL(endpoint);
|
|
19
|
+
const pathParts = url.pathname.split("/");
|
|
20
|
+
const classifiersIndex = pathParts.indexOf("classifiers");
|
|
21
|
+
if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const classifierId = pathParts[classifiersIndex + 1];
|
|
27
|
+
const baseUrlPath = pathParts.slice(0, classifiersIndex).join("/");
|
|
28
|
+
const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;
|
|
29
|
+
return new _UCPClient({
|
|
30
|
+
baseUrl,
|
|
31
|
+
classifierId,
|
|
32
|
+
apiKey: options?.apiKey,
|
|
33
|
+
timeout: options?.timeout
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the classify endpoint URL
|
|
38
|
+
*/
|
|
39
|
+
get classifyUrl() {
|
|
40
|
+
return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the info endpoint URL
|
|
44
|
+
*/
|
|
45
|
+
get infoUrl() {
|
|
46
|
+
return `${this.baseUrl}/classifiers/${this.classifierId}/info`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Classify messages using UCP protocol
|
|
50
|
+
*/
|
|
51
|
+
async classify(messages) {
|
|
52
|
+
const request = { messages };
|
|
53
|
+
const response = await this.fetch(
|
|
54
|
+
this.classifyUrl,
|
|
55
|
+
{
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify(request)
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
return {
|
|
61
|
+
classifierId: this.classifierId,
|
|
62
|
+
category: response.category,
|
|
63
|
+
level: response.level,
|
|
64
|
+
confidence: response.score / 100,
|
|
65
|
+
// Normalize to 0-1
|
|
66
|
+
reasoning: response.rationale,
|
|
67
|
+
raw: response
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Classify ChatMessage array (convenience method)
|
|
72
|
+
* Converts ChatMessage to UCPMessage format
|
|
73
|
+
*/
|
|
74
|
+
async classifyChat(messages) {
|
|
75
|
+
const ucpMessages = messages.filter((m) => m.role === "user" || m.role === "assistant").map((m, index) => ({
|
|
76
|
+
id: `msg-${index}`,
|
|
77
|
+
role: m.role,
|
|
78
|
+
content: m.content,
|
|
79
|
+
timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),
|
|
80
|
+
metadata: m.metadata
|
|
81
|
+
}));
|
|
82
|
+
return this.classify(ucpMessages);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get classifier info
|
|
86
|
+
*/
|
|
87
|
+
async getInfo() {
|
|
88
|
+
return this.fetch(this.infoUrl, {
|
|
89
|
+
method: "GET"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if the classifier endpoint is reachable
|
|
94
|
+
*/
|
|
95
|
+
async healthCheck() {
|
|
96
|
+
try {
|
|
97
|
+
const healthUrl = `${this.baseUrl}/health`;
|
|
98
|
+
const response = await fetch(healthUrl, {
|
|
99
|
+
method: "GET",
|
|
100
|
+
signal: AbortSignal.timeout(5e3)
|
|
101
|
+
});
|
|
102
|
+
return response.ok;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Internal fetch helper with error handling
|
|
109
|
+
*/
|
|
110
|
+
async fetch(url, options) {
|
|
111
|
+
const headers = {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
...this.headers
|
|
114
|
+
};
|
|
115
|
+
if (this.apiKey) {
|
|
116
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
117
|
+
}
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(url, {
|
|
122
|
+
...options,
|
|
123
|
+
headers,
|
|
124
|
+
signal: controller.signal
|
|
125
|
+
});
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const errorData = await response.json().catch(() => ({}));
|
|
129
|
+
const ucpError = errorData;
|
|
130
|
+
throw new UCPError(
|
|
131
|
+
ucpError.error?.code || `HTTP_${response.status}`,
|
|
132
|
+
ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
133
|
+
ucpError.error?.details
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return response.json();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
clearTimeout(timeoutId);
|
|
139
|
+
if (error instanceof UCPError) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
if (error instanceof Error) {
|
|
143
|
+
if (error.name === "AbortError") {
|
|
144
|
+
throw new UCPError("TIMEOUT", `Request timed out after ${this.timeout}ms`);
|
|
145
|
+
}
|
|
146
|
+
throw new UCPError("NETWORK_ERROR", error.message);
|
|
147
|
+
}
|
|
148
|
+
throw new UCPError("UNKNOWN_ERROR", "An unknown error occurred");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var UCPError = class extends Error {
|
|
153
|
+
constructor(code, message, details) {
|
|
154
|
+
super(message);
|
|
155
|
+
this.code = code;
|
|
156
|
+
this.details = details;
|
|
157
|
+
this.name = "UCPError";
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function createUCPClient(endpoint, options) {
|
|
161
|
+
return UCPClient.fromEndpoint(endpoint, options);
|
|
162
|
+
}
|
|
163
|
+
async function classifyWithUCP(endpoint, messages, options) {
|
|
164
|
+
const client = createUCPClient(endpoint, options);
|
|
165
|
+
return client.classifyChat(messages);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/auth.ts
|
|
169
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
170
|
+
var TOKEN_PREFIX = "fst_";
|
|
171
|
+
var TOKEN_LENGTH = 44;
|
|
172
|
+
function verifyRequestSignature(token, payload, signature) {
|
|
173
|
+
try {
|
|
174
|
+
const expected = createHmac("sha256", token).update(payload).digest("hex");
|
|
175
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
176
|
+
const signatureBuffer = Buffer.from(signature, "hex");
|
|
177
|
+
if (expectedBuffer.length !== signatureBuffer.length) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return timingSafeEqual(expectedBuffer, signatureBuffer);
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function createRequestSignature(token, payload) {
|
|
186
|
+
return createHmac("sha256", token).update(payload).digest("hex");
|
|
187
|
+
}
|
|
188
|
+
function createAuthHeader(token) {
|
|
189
|
+
return `Bearer ${token}`;
|
|
190
|
+
}
|
|
191
|
+
function isValidToken(token) {
|
|
192
|
+
return typeof token === "string" && token.startsWith(TOKEN_PREFIX) && token.length === TOKEN_LENGTH;
|
|
193
|
+
}
|
|
194
|
+
export {
|
|
195
|
+
UCPClient,
|
|
196
|
+
UCPError,
|
|
197
|
+
classifyWithUCP,
|
|
198
|
+
createAuthHeader,
|
|
199
|
+
createRequestSignature,
|
|
200
|
+
createUCPClient,
|
|
201
|
+
isValidToken,
|
|
202
|
+
verifyRequestSignature
|
|
203
|
+
};
|
|
204
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ucp/client.ts","../src/auth.ts"],"sourcesContent":["/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","import { createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const expected = createHmac('sha256', token)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n return createHmac('sha256', token).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import { h as ProtocolHandler } from './types-Bm98aHcd.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FirstStep SDK Server
|
|
6
|
+
*
|
|
7
|
+
* Standalone HTTP server for protocol handlers.
|
|
8
|
+
* Zero external dependencies, uses Node.js built-in `http` module.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createServer } from '@firststep-studio/sdk/server';
|
|
13
|
+
* import type { ProtocolHandler } from '@firststep-studio/sdk';
|
|
14
|
+
*
|
|
15
|
+
* const handler: ProtocolHandler = {
|
|
16
|
+
* async handleMessage(request, context) {
|
|
17
|
+
* return {
|
|
18
|
+
* message: 'Hello from my handler!',
|
|
19
|
+
* sessionId: request.sessionId || 'new',
|
|
20
|
+
* agentId: 'main',
|
|
21
|
+
* sessionStatus: 'active',
|
|
22
|
+
* };
|
|
23
|
+
* },
|
|
24
|
+
* getCapabilities() {
|
|
25
|
+
* return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };
|
|
26
|
+
* },
|
|
27
|
+
* };
|
|
28
|
+
*
|
|
29
|
+
* const server = createServer(handler, {
|
|
30
|
+
* token: process.env.FIRSTSTEP_TOKEN!,
|
|
31
|
+
* port: 3001,
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* server.start();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
interface ServerConfig {
|
|
39
|
+
/** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */
|
|
40
|
+
token: string;
|
|
41
|
+
/** Port to listen on. Defaults to 3001, or the PORT env variable. */
|
|
42
|
+
port?: number;
|
|
43
|
+
/** Host to bind to. Defaults to '0.0.0.0'. */
|
|
44
|
+
host?: string;
|
|
45
|
+
/** Skip signature verification (for local development only). */
|
|
46
|
+
skipSignatureVerification?: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface FirstStepServer {
|
|
49
|
+
/** Start the server */
|
|
50
|
+
start(): Promise<void>;
|
|
51
|
+
/** Stop the server gracefully */
|
|
52
|
+
stop(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Get the request handler function.
|
|
55
|
+
* Use this to integrate with Express, Fastify, or other frameworks.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // Express
|
|
60
|
+
* const server = createServer(handler, { token: '...' });
|
|
61
|
+
* app.post('/handshake', server.getRequestHandler());
|
|
62
|
+
* app.post('/message', server.getRequestHandler());
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a standalone HTTP server for a protocol handler.
|
|
69
|
+
*
|
|
70
|
+
* The server exposes three endpoints:
|
|
71
|
+
* - `GET /health` - Health check (always 200)
|
|
72
|
+
* - `POST /handshake` - Returns handler capabilities (signature verified)
|
|
73
|
+
* - `POST /message` - Handles a chat message (signature verified)
|
|
74
|
+
*/
|
|
75
|
+
declare function createServer(handler: ProtocolHandler, config: ServerConfig): FirstStepServer;
|
|
76
|
+
|
|
77
|
+
export { type FirstStepServer, type ServerConfig, createServer };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import { h as ProtocolHandler } from './types-Bm98aHcd.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FirstStep SDK Server
|
|
6
|
+
*
|
|
7
|
+
* Standalone HTTP server for protocol handlers.
|
|
8
|
+
* Zero external dependencies, uses Node.js built-in `http` module.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createServer } from '@firststep-studio/sdk/server';
|
|
13
|
+
* import type { ProtocolHandler } from '@firststep-studio/sdk';
|
|
14
|
+
*
|
|
15
|
+
* const handler: ProtocolHandler = {
|
|
16
|
+
* async handleMessage(request, context) {
|
|
17
|
+
* return {
|
|
18
|
+
* message: 'Hello from my handler!',
|
|
19
|
+
* sessionId: request.sessionId || 'new',
|
|
20
|
+
* agentId: 'main',
|
|
21
|
+
* sessionStatus: 'active',
|
|
22
|
+
* };
|
|
23
|
+
* },
|
|
24
|
+
* getCapabilities() {
|
|
25
|
+
* return { streaming: false, formQuestions: false, knowledgeActions: false, integrations: false };
|
|
26
|
+
* },
|
|
27
|
+
* };
|
|
28
|
+
*
|
|
29
|
+
* const server = createServer(handler, {
|
|
30
|
+
* token: process.env.FIRSTSTEP_TOKEN!,
|
|
31
|
+
* port: 3001,
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* server.start();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
interface ServerConfig {
|
|
39
|
+
/** API token (FIRSTSTEP_TOKEN). Used for request signature verification. */
|
|
40
|
+
token: string;
|
|
41
|
+
/** Port to listen on. Defaults to 3001, or the PORT env variable. */
|
|
42
|
+
port?: number;
|
|
43
|
+
/** Host to bind to. Defaults to '0.0.0.0'. */
|
|
44
|
+
host?: string;
|
|
45
|
+
/** Skip signature verification (for local development only). */
|
|
46
|
+
skipSignatureVerification?: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface FirstStepServer {
|
|
49
|
+
/** Start the server */
|
|
50
|
+
start(): Promise<void>;
|
|
51
|
+
/** Stop the server gracefully */
|
|
52
|
+
stop(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Get the request handler function.
|
|
55
|
+
* Use this to integrate with Express, Fastify, or other frameworks.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // Express
|
|
60
|
+
* const server = createServer(handler, { token: '...' });
|
|
61
|
+
* app.post('/handshake', server.getRequestHandler());
|
|
62
|
+
* app.post('/message', server.getRequestHandler());
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a standalone HTTP server for a protocol handler.
|
|
69
|
+
*
|
|
70
|
+
* The server exposes three endpoints:
|
|
71
|
+
* - `GET /health` - Health check (always 200)
|
|
72
|
+
* - `POST /handshake` - Returns handler capabilities (signature verified)
|
|
73
|
+
* - `POST /message` - Handles a chat message (signature verified)
|
|
74
|
+
*/
|
|
75
|
+
declare function createServer(handler: ProtocolHandler, config: ServerConfig): FirstStepServer;
|
|
76
|
+
|
|
77
|
+
export { type FirstStepServer, type ServerConfig, createServer };
|