@runtypelabs/persona-proxy 1.36.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/src/index.ts ADDED
@@ -0,0 +1,331 @@
1
+ import { Hono } from "hono";
2
+ import type { Context } from "hono";
3
+ import { handle } from "hono/vercel";
4
+
5
+ export type TravrseFlowStep = {
6
+ id: string;
7
+ name: string;
8
+ type: string;
9
+ enabled: boolean;
10
+ config: Record<string, unknown>;
11
+ };
12
+
13
+ export type TravrseFlowConfig = {
14
+ name: string;
15
+ description: string;
16
+ steps: TravrseFlowStep[];
17
+ };
18
+
19
+ /**
20
+ * Payload for message feedback (upvote/downvote)
21
+ */
22
+ export type FeedbackPayload = {
23
+ type: "upvote" | "downvote";
24
+ messageId: string;
25
+ content?: string;
26
+ timestamp?: string;
27
+ sessionId?: string;
28
+ metadata?: Record<string, unknown>;
29
+ };
30
+
31
+ /**
32
+ * Handler function for processing feedback
33
+ */
34
+ export type FeedbackHandler = (feedback: FeedbackPayload) => Promise<void> | void;
35
+
36
+ export type ChatProxyOptions = {
37
+ upstreamUrl?: string;
38
+ apiKey?: string;
39
+ path?: string;
40
+ allowedOrigins?: string[];
41
+ flowId?: string;
42
+ flowConfig?: TravrseFlowConfig;
43
+ /**
44
+ * Path for the feedback endpoint (default: "/api/feedback")
45
+ */
46
+ feedbackPath?: string;
47
+ /**
48
+ * Custom handler for processing feedback.
49
+ * Use this to store feedback in a database or send to analytics.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * onFeedback: async (feedback) => {
54
+ * await db.feedback.create({ data: feedback });
55
+ * }
56
+ * ```
57
+ */
58
+ onFeedback?: FeedbackHandler;
59
+ };
60
+
61
+ const DEFAULT_ENDPOINT = "https://api.travrse.ai/v1/dispatch";
62
+ const DEFAULT_PATH = "/api/chat/dispatch";
63
+
64
+ const DEFAULT_FLOW: TravrseFlowConfig = {
65
+ name: "Streaming Prompt Flow",
66
+ description: "Streaming chat generated by the widget",
67
+ steps: [
68
+ {
69
+ id: "widget_prompt",
70
+ name: "Prompt",
71
+ type: "prompt",
72
+ enabled: true,
73
+ config: {
74
+ model: "gemini-2.5-flash",
75
+ // model: "gpt-4o",
76
+ response_format: "markdown",
77
+ output_variable: "prompt_result",
78
+ user_prompt: "{{user_message}}",
79
+ system_prompt: "you are a helpful assistant, chatting with a user",
80
+ // tools: {
81
+ // tool_ids: [
82
+ // "builtin:dalle"
83
+ // ]
84
+ // },
85
+ previous_messages: "{{messages}}"
86
+ }
87
+ }
88
+ ]
89
+ };
90
+
91
+ const withCors =
92
+ (allowedOrigins: string[] | undefined) =>
93
+ async (c: Context, next: () => Promise<void>) => {
94
+ const origin = c.req.header("origin");
95
+ const isDevelopment = process.env.NODE_ENV === "development" || !process.env.NODE_ENV;
96
+
97
+ // Determine the CORS origin to allow
98
+ let corsOrigin: string;
99
+ if (!allowedOrigins || allowedOrigins.length === 0) {
100
+ // No restrictions - allow any origin (or use the request origin)
101
+ corsOrigin = origin || "*";
102
+ } else if (allowedOrigins.includes(origin || "")) {
103
+ // Origin is in the allowed list
104
+ corsOrigin = origin || "*";
105
+ } else if (isDevelopment && origin) {
106
+ // In development, allow the actual origin even if not in the list
107
+ // This helps with local development where ports might vary
108
+ corsOrigin = origin;
109
+ } else {
110
+ // Production: origin not allowed - reject by not setting CORS headers
111
+ // Return error for preflight, or continue without CORS headers
112
+ if (c.req.method === "OPTIONS") {
113
+ return c.json({ error: "CORS policy violation: origin not allowed" }, 403);
114
+ }
115
+ // For non-preflight requests, continue but browser will block due to missing CORS headers
116
+ await next();
117
+ return;
118
+ }
119
+
120
+ const headers: Record<string, string> = {
121
+ "Access-Control-Allow-Origin": corsOrigin,
122
+ "Access-Control-Allow-Headers":
123
+ c.req.header("access-control-request-headers") ??
124
+ "Content-Type, Authorization",
125
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
126
+ Vary: "Origin"
127
+ };
128
+
129
+ if (c.req.method === "OPTIONS") {
130
+ return new Response(null, { status: 204, headers });
131
+ }
132
+
133
+ await next();
134
+ Object.entries(headers).forEach(([key, value]) =>
135
+ c.header(key, value, { append: false })
136
+ );
137
+ };
138
+
139
+ export const createChatProxyApp = (options: ChatProxyOptions = {}) => {
140
+ const app = new Hono();
141
+ const path = options.path ?? DEFAULT_PATH;
142
+ const feedbackPath = options.feedbackPath ?? "/api/feedback";
143
+ const upstream = options.upstreamUrl ?? DEFAULT_ENDPOINT;
144
+
145
+ app.use("*", withCors(options.allowedOrigins));
146
+
147
+ // Feedback endpoint for collecting upvote/downvote data
148
+ app.post(feedbackPath, async (c) => {
149
+ let payload: FeedbackPayload;
150
+ try {
151
+ payload = await c.req.json();
152
+ } catch (error) {
153
+ return c.json({ error: "Invalid JSON body" }, 400);
154
+ }
155
+
156
+ // Validate payload
157
+ if (!payload.type || !["upvote", "downvote"].includes(payload.type)) {
158
+ return c.json(
159
+ { error: "Invalid feedback type. Must be 'upvote' or 'downvote'" },
160
+ 400
161
+ );
162
+ }
163
+ if (!payload.messageId) {
164
+ return c.json({ error: "Missing messageId" }, 400);
165
+ }
166
+
167
+ // Add timestamp if not provided
168
+ payload.timestamp = payload.timestamp ?? new Date().toISOString();
169
+
170
+ const isDevelopment =
171
+ process.env.NODE_ENV === "development" || !process.env.NODE_ENV;
172
+
173
+ if (isDevelopment) {
174
+ console.log("\n=== Feedback Received ===");
175
+ console.log("Type:", payload.type);
176
+ console.log("Message ID:", payload.messageId);
177
+ console.log(
178
+ "Content Preview:",
179
+ payload.content?.substring(0, 100) ?? "(none)"
180
+ );
181
+ console.log("Timestamp:", payload.timestamp);
182
+ console.log("=== End Feedback ===\n");
183
+ }
184
+
185
+ // Call custom handler if provided
186
+ if (options.onFeedback) {
187
+ try {
188
+ await options.onFeedback(payload);
189
+ } catch (error) {
190
+ console.error("[Feedback] Handler error:", error);
191
+ return c.json({ error: "Feedback handler failed" }, 500);
192
+ }
193
+ }
194
+
195
+ return c.json({
196
+ success: true,
197
+ message: "Feedback recorded",
198
+ feedback: {
199
+ type: payload.type,
200
+ messageId: payload.messageId,
201
+ timestamp: payload.timestamp
202
+ }
203
+ });
204
+ });
205
+
206
+ // Chat dispatch endpoint
207
+ app.post(path, async (c) => {
208
+ const apiKey = options.apiKey ?? process.env.TRAVRSE_API_KEY;
209
+ if (!apiKey) {
210
+ return c.json(
211
+ { error: "Missing API key. Set TRAVRSE_API_KEY." },
212
+ 401
213
+ );
214
+ }
215
+
216
+ let clientPayload: {
217
+ messages?: Array<{ role: string; content: string; createdAt?: string }>;
218
+ flowId?: string;
219
+ metadata?: Record<string, unknown>;
220
+ };
221
+ try {
222
+ clientPayload = await c.req.json();
223
+ } catch (error) {
224
+ return c.json(
225
+ { error: "Invalid JSON body", details: error },
226
+ 400
227
+ );
228
+ }
229
+
230
+ // Build the Travrse payload
231
+ const messages = clientPayload.messages ?? [];
232
+ // Sort messages by timestamp to ensure correct order
233
+ const sortedMessages = [...messages].sort((a, b) => {
234
+ const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
235
+ const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
236
+ return timeA - timeB;
237
+ });
238
+ const formattedMessages = sortedMessages.map((message) => ({
239
+ role: message.role,
240
+ content: message.content
241
+ }));
242
+
243
+ // Determine which flow to use
244
+ const flowId = clientPayload.flowId ?? options.flowId;
245
+ const flowConfig = options.flowConfig ?? DEFAULT_FLOW;
246
+
247
+ const travrsePayload: Record<string, unknown> = {
248
+ record: {
249
+ name: "Streaming Chat Widget",
250
+ type: "standalone",
251
+ metadata: clientPayload.metadata || {}
252
+ },
253
+ messages: formattedMessages,
254
+ options: {
255
+ stream_response: true,
256
+ record_mode: "virtual",
257
+ flow_mode: flowId ? "existing" : "virtual",
258
+ auto_append_metadata: false
259
+ }
260
+ };
261
+
262
+ // Use flow ID if provided, otherwise use flow config
263
+ if (flowId) {
264
+ travrsePayload.flow = {
265
+ id: flowId
266
+ }
267
+ } else {
268
+ travrsePayload.flow = flowConfig;
269
+ }
270
+
271
+ // Development logging
272
+ const isDevelopment = process.env.NODE_ENV === "development" || !process.env.NODE_ENV;
273
+
274
+ if (isDevelopment) {
275
+ console.log("\n=== Travrse Proxy Request ===");
276
+ console.log("URL:", upstream);
277
+ console.log("API Key Used:", apiKey ? "Yes" : "No");
278
+ console.log("API Key (first 12 chars):", apiKey ? apiKey.substring(0, 12) : "N/A");
279
+ console.log("Request Payload:", JSON.stringify(travrsePayload, null, 2));
280
+ }
281
+
282
+ const response = await fetch(upstream, {
283
+ method: "POST",
284
+ headers: {
285
+ Authorization: `Bearer ${apiKey}`,
286
+ "Content-Type": "application/json"
287
+ },
288
+ body: JSON.stringify(travrsePayload)
289
+ });
290
+
291
+ if (isDevelopment) {
292
+ console.log("Response Status:", response.status);
293
+ console.log("Response Status Text:", response.statusText);
294
+
295
+ // If there's an error, try to read and log the response body
296
+ if (!response.ok) {
297
+ const clonedResponse = response.clone();
298
+ try {
299
+ const errorBody = await clonedResponse.text();
300
+ console.log("Error Response Body:", errorBody);
301
+ } catch (e) {
302
+ console.log("Could not read error response body:", e);
303
+ }
304
+ }
305
+ console.log("=== End Travrse Proxy Request ===\n");
306
+ }
307
+
308
+ return new Response(response.body, {
309
+ status: response.status,
310
+ headers: {
311
+ "Content-Type":
312
+ response.headers.get("content-type") ?? "application/json",
313
+ "Cache-Control": "no-store"
314
+ }
315
+ });
316
+ });
317
+
318
+ return app;
319
+ };
320
+
321
+ export const createVercelHandler = (options?: ChatProxyOptions) =>
322
+ handle(createChatProxyApp(options));
323
+
324
+ // Export pre-configured flows
325
+ export * from "./flows/index.js";
326
+
327
+ // Export utility functions
328
+ export * from "./utils/index.js";
329
+
330
+ export default createChatProxyApp;
331
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Utility functions for proxy implementations
3
+ */
4
+
5
+ export {
6
+ createCheckoutSession,
7
+ type CheckoutItem,
8
+ type CreateCheckoutSessionOptions,
9
+ type CheckoutSessionResponse
10
+ } from "./stripe.js";
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Stripe checkout helpers using the REST API
3
+ * This approach works on all platforms including Cloudflare Workers, Vercel Edge, etc.
4
+ */
5
+
6
+ export interface CheckoutItem {
7
+ name: string;
8
+ price: number; // Price in cents
9
+ quantity: number;
10
+ }
11
+
12
+ export interface CreateCheckoutSessionOptions {
13
+ secretKey: string;
14
+ items: CheckoutItem[];
15
+ successUrl: string;
16
+ cancelUrl: string;
17
+ }
18
+
19
+ export interface CheckoutSessionResponse {
20
+ success: boolean;
21
+ checkoutUrl?: string;
22
+ sessionId?: string;
23
+ error?: string;
24
+ }
25
+
26
+ /**
27
+ * Creates a Stripe checkout session using the REST API
28
+ * @param options - Checkout session configuration
29
+ * @returns Checkout session response with URL and session ID
30
+ */
31
+ export async function createCheckoutSession(
32
+ options: CreateCheckoutSessionOptions
33
+ ): Promise<CheckoutSessionResponse> {
34
+ const { secretKey, items, successUrl, cancelUrl } = options;
35
+
36
+ try {
37
+ // Validate items
38
+ if (!items || !Array.isArray(items) || items.length === 0) {
39
+ return {
40
+ success: false,
41
+ error: "Items array is required"
42
+ };
43
+ }
44
+
45
+ for (const item of items) {
46
+ if (!item.name || typeof item.price !== "number" || typeof item.quantity !== "number") {
47
+ return {
48
+ success: false,
49
+ error: "Each item must have name (string), price (number in cents), and quantity (number)"
50
+ };
51
+ }
52
+ }
53
+
54
+ // Build line items for URL encoding
55
+ const lineItems = items.map((item) => ({
56
+ price_data: {
57
+ currency: "usd",
58
+ product_data: {
59
+ name: item.name,
60
+ },
61
+ unit_amount: item.price,
62
+ },
63
+ quantity: item.quantity,
64
+ }));
65
+
66
+ // Convert line items to URL-encoded format
67
+ const params = new URLSearchParams({
68
+ "payment_method_types[0]": "card",
69
+ "mode": "payment",
70
+ "success_url": successUrl,
71
+ "cancel_url": cancelUrl,
72
+ });
73
+
74
+ // Add line items to params
75
+ lineItems.forEach((item, index) => {
76
+ params.append(`line_items[${index}][price_data][currency]`, item.price_data.currency);
77
+ params.append(`line_items[${index}][price_data][product_data][name]`, item.price_data.product_data.name);
78
+ params.append(`line_items[${index}][price_data][unit_amount]`, item.price_data.unit_amount.toString());
79
+ params.append(`line_items[${index}][quantity]`, item.quantity.toString());
80
+ });
81
+
82
+ // Create Stripe checkout session using REST API
83
+ const stripeResponse = await fetch("https://api.stripe.com/v1/checkout/sessions", {
84
+ method: "POST",
85
+ headers: {
86
+ "Authorization": `Bearer ${secretKey}`,
87
+ "Content-Type": "application/x-www-form-urlencoded",
88
+ },
89
+ body: params,
90
+ });
91
+
92
+ if (!stripeResponse.ok) {
93
+ const errorData = await stripeResponse.text();
94
+ console.error("Stripe API error:", errorData);
95
+ return {
96
+ success: false,
97
+ error: "Failed to create checkout session"
98
+ };
99
+ }
100
+
101
+ const session = await stripeResponse.json() as { url: string; id: string };
102
+
103
+ return {
104
+ success: true,
105
+ checkoutUrl: session.url,
106
+ sessionId: session.id,
107
+ };
108
+ } catch (error) {
109
+ console.error("Stripe checkout error:", error);
110
+ return {
111
+ success: false,
112
+ error: error instanceof Error ? error.message : "Failed to create checkout session"
113
+ };
114
+ }
115
+ }