@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/README.md +106 -0
- package/dist/index.cjs +621 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +120 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +589 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/src/flows/components.ts +58 -0
- package/src/flows/conversational.ts +26 -0
- package/src/flows/index.ts +11 -0
- package/src/flows/scheduling.ts +64 -0
- package/src/flows/shopping-assistant.ts +171 -0
- package/src/index.ts +331 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/stripe.ts +115 -0
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,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
|
+
}
|