@kilnai/runtime 0.1.1
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/a2a/a2a-client.d.ts +6 -0
- package/dist/a2a/a2a-client.d.ts.map +1 -0
- package/dist/a2a/a2a-client.js +112 -0
- package/dist/a2a/a2a-client.js.map +1 -0
- package/dist/a2a/index.d.ts +2 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +3 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/channels/api-channel.d.ts +43 -0
- package/dist/channels/api-channel.d.ts.map +1 -0
- package/dist/channels/api-channel.js +95 -0
- package/dist/channels/api-channel.js.map +1 -0
- package/dist/channels/channel-registry.d.ts +25 -0
- package/dist/channels/channel-registry.d.ts.map +1 -0
- package/dist/channels/channel-registry.js +49 -0
- package/dist/channels/channel-registry.js.map +1 -0
- package/dist/channels/channel-router.d.ts +43 -0
- package/dist/channels/channel-router.d.ts.map +1 -0
- package/dist/channels/channel-router.js +72 -0
- package/dist/channels/channel-router.js.map +1 -0
- package/dist/channels/cli-channel.d.ts +19 -0
- package/dist/channels/cli-channel.d.ts.map +1 -0
- package/dist/channels/cli-channel.js +37 -0
- package/dist/channels/cli-channel.js.map +1 -0
- package/dist/channels/event-bridge.d.ts +27 -0
- package/dist/channels/event-bridge.d.ts.map +1 -0
- package/dist/channels/event-bridge.js +83 -0
- package/dist/channels/event-bridge.js.map +1 -0
- package/dist/channels/index.d.ts +17 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +12 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/message-formatter.d.ts +4 -0
- package/dist/channels/message-formatter.d.ts.map +1 -0
- package/dist/channels/message-formatter.js +32 -0
- package/dist/channels/message-formatter.js.map +1 -0
- package/dist/channels/slack-channel.d.ts +32 -0
- package/dist/channels/slack-channel.d.ts.map +1 -0
- package/dist/channels/slack-channel.js +71 -0
- package/dist/channels/slack-channel.js.map +1 -0
- package/dist/channels/types.d.ts +27 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +12 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/web-channel.d.ts +37 -0
- package/dist/channels/web-channel.d.ts.map +1 -0
- package/dist/channels/web-channel.js +112 -0
- package/dist/channels/web-channel.js.map +1 -0
- package/dist/channels/whatsapp-api.d.ts +20 -0
- package/dist/channels/whatsapp-api.d.ts.map +1 -0
- package/dist/channels/whatsapp-api.js +44 -0
- package/dist/channels/whatsapp-api.js.map +1 -0
- package/dist/channels/whatsapp-channel.d.ts +32 -0
- package/dist/channels/whatsapp-channel.d.ts.map +1 -0
- package/dist/channels/whatsapp-channel.js +85 -0
- package/dist/channels/whatsapp-channel.js.map +1 -0
- package/dist/gateway/app-resolver.d.ts +11 -0
- package/dist/gateway/app-resolver.d.ts.map +1 -0
- package/dist/gateway/app-resolver.js +36 -0
- package/dist/gateway/app-resolver.js.map +1 -0
- package/dist/gateway/approval-registry.d.ts +19 -0
- package/dist/gateway/approval-registry.d.ts.map +1 -0
- package/dist/gateway/approval-registry.js +49 -0
- package/dist/gateway/approval-registry.js.map +1 -0
- package/dist/gateway/budget-middleware.d.ts +47 -0
- package/dist/gateway/budget-middleware.d.ts.map +1 -0
- package/dist/gateway/budget-middleware.js +88 -0
- package/dist/gateway/budget-middleware.js.map +1 -0
- package/dist/gateway/config-validator.d.ts +31 -0
- package/dist/gateway/config-validator.d.ts.map +1 -0
- package/dist/gateway/config-validator.js +68 -0
- package/dist/gateway/config-validator.js.map +1 -0
- package/dist/gateway/delegation-handler.d.ts +53 -0
- package/dist/gateway/delegation-handler.d.ts.map +1 -0
- package/dist/gateway/delegation-handler.js +257 -0
- package/dist/gateway/delegation-handler.js.map +1 -0
- package/dist/gateway/delegation-routes.d.ts +7 -0
- package/dist/gateway/delegation-routes.d.ts.map +1 -0
- package/dist/gateway/delegation-routes.js +48 -0
- package/dist/gateway/delegation-routes.js.map +1 -0
- package/dist/gateway/dev-inspector.d.ts +2 -0
- package/dist/gateway/dev-inspector.d.ts.map +1 -0
- package/dist/gateway/dev-inspector.js +355 -0
- package/dist/gateway/dev-inspector.js.map +1 -0
- package/dist/gateway/dev-orchestrator.d.ts +24 -0
- package/dist/gateway/dev-orchestrator.d.ts.map +1 -0
- package/dist/gateway/dev-orchestrator.js +71 -0
- package/dist/gateway/dev-orchestrator.js.map +1 -0
- package/dist/gateway/dev-routes-types.d.ts +39 -0
- package/dist/gateway/dev-routes-types.d.ts.map +1 -0
- package/dist/gateway/dev-routes-types.js +3 -0
- package/dist/gateway/dev-routes-types.js.map +1 -0
- package/dist/gateway/dev-routes.d.ts +53 -0
- package/dist/gateway/dev-routes.d.ts.map +1 -0
- package/dist/gateway/dev-routes.js +217 -0
- package/dist/gateway/dev-routes.js.map +1 -0
- package/dist/gateway/dev-token-store.d.ts +19 -0
- package/dist/gateway/dev-token-store.d.ts.map +1 -0
- package/dist/gateway/dev-token-store.js +40 -0
- package/dist/gateway/dev-token-store.js.map +1 -0
- package/dist/gateway/gateway-routes.d.ts +46 -0
- package/dist/gateway/gateway-routes.d.ts.map +1 -0
- package/dist/gateway/gateway-routes.js +143 -0
- package/dist/gateway/gateway-routes.js.map +1 -0
- package/dist/gateway/gateway-server.d.ts +25 -0
- package/dist/gateway/gateway-server.d.ts.map +1 -0
- package/dist/gateway/gateway-server.js +736 -0
- package/dist/gateway/gateway-server.js.map +1 -0
- package/dist/gateway/health-registry.d.ts +18 -0
- package/dist/gateway/health-registry.d.ts.map +1 -0
- package/dist/gateway/health-registry.js +40 -0
- package/dist/gateway/health-registry.js.map +1 -0
- package/dist/gateway/memory-routes.d.ts +12 -0
- package/dist/gateway/memory-routes.d.ts.map +1 -0
- package/dist/gateway/memory-routes.js +32 -0
- package/dist/gateway/memory-routes.js.map +1 -0
- package/dist/gateway/mode-b-routes.d.ts +14 -0
- package/dist/gateway/mode-b-routes.d.ts.map +1 -0
- package/dist/gateway/mode-b-routes.js +96 -0
- package/dist/gateway/mode-b-routes.js.map +1 -0
- package/dist/gateway/safety-middleware.d.ts +21 -0
- package/dist/gateway/safety-middleware.d.ts.map +1 -0
- package/dist/gateway/safety-middleware.js +175 -0
- package/dist/gateway/safety-middleware.js.map +1 -0
- package/dist/gateway/security-middleware.d.ts +12 -0
- package/dist/gateway/security-middleware.d.ts.map +1 -0
- package/dist/gateway/security-middleware.js +62 -0
- package/dist/gateway/security-middleware.js.map +1 -0
- package/dist/gateway/tenant-admin-routes.d.ts +15 -0
- package/dist/gateway/tenant-admin-routes.d.ts.map +1 -0
- package/dist/gateway/tenant-admin-routes.js +148 -0
- package/dist/gateway/tenant-admin-routes.js.map +1 -0
- package/dist/gateway/tenant-routes.d.ts +15 -0
- package/dist/gateway/tenant-routes.d.ts.map +1 -0
- package/dist/gateway/tenant-routes.js +107 -0
- package/dist/gateway/tenant-routes.js.map +1 -0
- package/dist/gateway/whatsapp-webhook-routes.d.ts +15 -0
- package/dist/gateway/whatsapp-webhook-routes.d.ts.map +1 -0
- package/dist/gateway/whatsapp-webhook-routes.js +217 -0
- package/dist/gateway/whatsapp-webhook-routes.js.map +1 -0
- package/dist/gateway/ws-routes.d.ts +19 -0
- package/dist/gateway/ws-routes.d.ts.map +1 -0
- package/dist/gateway/ws-routes.js +79 -0
- package/dist/gateway/ws-routes.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/session/index.d.ts +6 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +4 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/mode-b-orchestrator.d.ts +43 -0
- package/dist/session/mode-b-orchestrator.d.ts.map +1 -0
- package/dist/session/mode-b-orchestrator.js +224 -0
- package/dist/session/mode-b-orchestrator.js.map +1 -0
- package/dist/session/mode-b-session.d.ts +29 -0
- package/dist/session/mode-b-session.d.ts.map +1 -0
- package/dist/session/mode-b-session.js +50 -0
- package/dist/session/mode-b-session.js.map +1 -0
- package/dist/session/session-registry.d.ts +15 -0
- package/dist/session/session-registry.d.ts.map +1 -0
- package/dist/session/session-registry.js +60 -0
- package/dist/session/session-registry.js.map +1 -0
- package/dist/tenant/index.d.ts +3 -0
- package/dist/tenant/index.d.ts.map +1 -0
- package/dist/tenant/index.js +3 -0
- package/dist/tenant/index.js.map +1 -0
- package/dist/tenant/system-prompt-builder.d.ts +3 -0
- package/dist/tenant/system-prompt-builder.d.ts.map +1 -0
- package/dist/tenant/system-prompt-builder.js +76 -0
- package/dist/tenant/system-prompt-builder.js.map +1 -0
- package/dist/tenant/tenant-registry.d.ts +33 -0
- package/dist/tenant/tenant-registry.d.ts.map +1 -0
- package/dist/tenant/tenant-registry.js +156 -0
- package/dist/tenant/tenant-registry.js.map +1 -0
- package/dist/trigger/event-listener.d.ts +22 -0
- package/dist/trigger/event-listener.d.ts.map +1 -0
- package/dist/trigger/event-listener.js +64 -0
- package/dist/trigger/event-listener.js.map +1 -0
- package/dist/trigger/index.d.ts +10 -0
- package/dist/trigger/index.d.ts.map +1 -0
- package/dist/trigger/index.js +7 -0
- package/dist/trigger/index.js.map +1 -0
- package/dist/trigger/scheduler.d.ts +22 -0
- package/dist/trigger/scheduler.d.ts.map +1 -0
- package/dist/trigger/scheduler.js +77 -0
- package/dist/trigger/scheduler.js.map +1 -0
- package/dist/trigger/trigger-executor.d.ts +14 -0
- package/dist/trigger/trigger-executor.d.ts.map +1 -0
- package/dist/trigger/trigger-executor.js +47 -0
- package/dist/trigger/trigger-executor.js.map +1 -0
- package/dist/trigger/trigger-registry.d.ts +28 -0
- package/dist/trigger/trigger-registry.d.ts.map +1 -0
- package/dist/trigger/trigger-registry.js +118 -0
- package/dist/trigger/trigger-registry.js.map +1 -0
- package/dist/trigger/webhook-handler.d.ts +11 -0
- package/dist/trigger/webhook-handler.d.ts.map +1 -0
- package/dist/trigger/webhook-handler.js +71 -0
- package/dist/trigger/webhook-handler.js.map +1 -0
- package/dist/utils/hmac.d.ts +15 -0
- package/dist/utils/hmac.d.ts.map +1 -0
- package/dist/utils/hmac.js +27 -0
- package/dist/utils/hmac.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Gateway: Tenant admin CRUD routes -- Hono sub-app for managing tenant configs
|
|
2
|
+
// Handles tenant listing, creation, update, and deletion with optional auth
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { TenantNotFoundError, TenantValidationFailedError } from "../tenant/tenant-registry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Mutable fields allowed in tenant create/update requests.
|
|
7
|
+
* Immutable fields (tenantId, appName, createdAt, updatedAt) are set server-side.
|
|
8
|
+
* This prevents prototype pollution and field injection from untrusted request bodies.
|
|
9
|
+
*/
|
|
10
|
+
const MUTABLE_TENANT_FIELDS = [
|
|
11
|
+
"name",
|
|
12
|
+
"description",
|
|
13
|
+
"services",
|
|
14
|
+
"hours",
|
|
15
|
+
"faqEntries",
|
|
16
|
+
"escalationContact",
|
|
17
|
+
"tone",
|
|
18
|
+
"language",
|
|
19
|
+
"whatsappPhoneNumberId",
|
|
20
|
+
"whatsappAccessToken",
|
|
21
|
+
"whatsappVerifyToken",
|
|
22
|
+
"billing",
|
|
23
|
+
"idleTimeoutMs",
|
|
24
|
+
"enabled",
|
|
25
|
+
];
|
|
26
|
+
/** Pick only safe mutable fields from an untrusted body */
|
|
27
|
+
function pickMutableFields(body) {
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const field of MUTABLE_TENANT_FIELDS) {
|
|
30
|
+
if (field in body) {
|
|
31
|
+
result[field] = body[field];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generate a tenant ID from a display name.
|
|
38
|
+
* Lowercases, removes diacritics (NFD + strip combining chars),
|
|
39
|
+
* replaces non-alphanumeric with hyphens, trims hyphens, max 64 chars.
|
|
40
|
+
*/
|
|
41
|
+
export function generateTenantId(name) {
|
|
42
|
+
return name
|
|
43
|
+
.normalize("NFD")
|
|
44
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
47
|
+
.replace(/^-+|-+$/g, "")
|
|
48
|
+
.slice(0, 64);
|
|
49
|
+
}
|
|
50
|
+
export function createTenantAdminRoutes(config) {
|
|
51
|
+
const app = new Hono();
|
|
52
|
+
// Auth middleware: if adminToken is configured, require Bearer token
|
|
53
|
+
if (config.adminToken) {
|
|
54
|
+
app.use("*", async (c, next) => {
|
|
55
|
+
const auth = c.req.header("Authorization");
|
|
56
|
+
if (!auth || auth !== `Bearer ${config.adminToken}`) {
|
|
57
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
58
|
+
}
|
|
59
|
+
await next();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// GET /tenants -- list tenants for this App
|
|
63
|
+
app.get("/tenants", (c) => {
|
|
64
|
+
const tenants = config.tenantRegistry.list(config.appName);
|
|
65
|
+
return c.json({ tenants });
|
|
66
|
+
});
|
|
67
|
+
// GET /tenants/:tenantId -- get single tenant
|
|
68
|
+
app.get("/tenants/:tenantId", (c) => {
|
|
69
|
+
const tenantId = c.req.param("tenantId");
|
|
70
|
+
const tenant = config.tenantRegistry.get(tenantId);
|
|
71
|
+
if (!tenant || tenant.appName !== config.appName) {
|
|
72
|
+
return c.json({ error: "Tenant not found" }, 404);
|
|
73
|
+
}
|
|
74
|
+
return c.json(tenant);
|
|
75
|
+
});
|
|
76
|
+
// POST /tenants -- create tenant
|
|
77
|
+
app.post("/tenants", async (c) => {
|
|
78
|
+
let body;
|
|
79
|
+
try {
|
|
80
|
+
body = await c.req.json();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
84
|
+
}
|
|
85
|
+
const now = new Date().toISOString();
|
|
86
|
+
const tenantId = body.tenantId || generateTenantId(body.name ?? "");
|
|
87
|
+
const safeFields = pickMutableFields(body);
|
|
88
|
+
const tenantConfig = {
|
|
89
|
+
...safeFields,
|
|
90
|
+
tenantId,
|
|
91
|
+
appName: config.appName,
|
|
92
|
+
enabled: body.enabled !== undefined ? Boolean(body.enabled) : true,
|
|
93
|
+
createdAt: now,
|
|
94
|
+
updatedAt: now,
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const created = config.tenantRegistry.create(tenantConfig);
|
|
98
|
+
return c.json(created, 201);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err instanceof TenantValidationFailedError) {
|
|
102
|
+
const isDuplicate = err.errors.some((e) => e.message.includes("duplicate"));
|
|
103
|
+
if (isDuplicate) {
|
|
104
|
+
return c.json({ error: "Tenant already exists", details: err.errors }, 409);
|
|
105
|
+
}
|
|
106
|
+
return c.json({ error: "Validation failed", details: err.errors }, 422);
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// PATCH /tenants/:tenantId -- partial update
|
|
112
|
+
app.patch("/tenants/:tenantId", async (c) => {
|
|
113
|
+
const tenantId = c.req.param("tenantId");
|
|
114
|
+
let body;
|
|
115
|
+
try {
|
|
116
|
+
body = await c.req.json();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const safeUpdate = pickMutableFields(body);
|
|
123
|
+
const updated = config.tenantRegistry.update(tenantId, safeUpdate);
|
|
124
|
+
return c.json(updated);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (err instanceof TenantNotFoundError) {
|
|
128
|
+
return c.json({ error: "Tenant not found" }, 404);
|
|
129
|
+
}
|
|
130
|
+
if (err instanceof TenantValidationFailedError) {
|
|
131
|
+
return c.json({ error: "Validation failed", details: err.errors }, 422);
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// DELETE /tenants/:tenantId -- remove tenant
|
|
137
|
+
app.delete("/tenants/:tenantId", (c) => {
|
|
138
|
+
const tenantId = c.req.param("tenantId");
|
|
139
|
+
const tenant = config.tenantRegistry.get(tenantId);
|
|
140
|
+
if (!tenant || tenant.appName !== config.appName) {
|
|
141
|
+
return c.json({ error: "Tenant not found" }, 404);
|
|
142
|
+
}
|
|
143
|
+
config.tenantRegistry.remove(tenantId);
|
|
144
|
+
return c.json({ removed: true });
|
|
145
|
+
});
|
|
146
|
+
return app;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=tenant-admin-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-admin-routes.js","sourceRoot":"","sources":["../../src/gateway/tenant-admin-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { ModeBOrchestrator } from "../session/mode-b-orchestrator.js";
|
|
3
|
+
import type { SessionRegistry } from "../session/session-registry.js";
|
|
4
|
+
import type { TenantRegistry } from "../tenant/tenant-registry.js";
|
|
5
|
+
import type { BillingConfig } from "./budget-middleware.js";
|
|
6
|
+
/** Runtime configuration for a multi-tenant App */
|
|
7
|
+
export interface TenantAppRuntime {
|
|
8
|
+
readonly appName: string;
|
|
9
|
+
readonly orchestrator: ModeBOrchestrator;
|
|
10
|
+
readonly sessionRegistry: SessionRegistry;
|
|
11
|
+
readonly tenantRegistry: TenantRegistry;
|
|
12
|
+
readonly billing?: BillingConfig;
|
|
13
|
+
}
|
|
14
|
+
export declare function createTenantRoutes(runtime: TenantAppRuntime): Hono;
|
|
15
|
+
//# sourceMappingURL=tenant-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-routes.d.ts","sourceRoot":"","sources":["../../src/gateway/tenant-routes.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IACzC,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;CAClC;AAWD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA8GlE"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Gateway: Tenant routes -- Hono sub-app for multi-tenant message processing
|
|
2
|
+
// Handles tenant-scoped message processing, session listing, and session removal
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { textParts, extractText } from "@kilnai/core";
|
|
5
|
+
import { buildTenantSystemPrompt } from "../tenant/system-prompt-builder.js";
|
|
6
|
+
import { checkBudget, reportUsage } from "./budget-middleware.js";
|
|
7
|
+
export function createTenantRoutes(runtime) {
|
|
8
|
+
const app = new Hono();
|
|
9
|
+
app.post("/message", async (c) => {
|
|
10
|
+
let body;
|
|
11
|
+
try {
|
|
12
|
+
body = await c.req.json();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
16
|
+
}
|
|
17
|
+
// Accept either { message: string } or { parts: ContentPart[] }
|
|
18
|
+
const userParts = body.parts && Array.isArray(body.parts)
|
|
19
|
+
? body.parts
|
|
20
|
+
: (body.message && typeof body.message === "string" ? textParts(body.message) : []);
|
|
21
|
+
if (userParts.length === 0) {
|
|
22
|
+
return c.json({ error: "message or parts is required" }, 400);
|
|
23
|
+
}
|
|
24
|
+
if (!body.userId || typeof body.userId !== "string") {
|
|
25
|
+
return c.json({ error: "userId is required" }, 400);
|
|
26
|
+
}
|
|
27
|
+
if (!body.tenantId || typeof body.tenantId !== "string") {
|
|
28
|
+
return c.json({ error: "tenantId is required" }, 400);
|
|
29
|
+
}
|
|
30
|
+
// Resolve tenant
|
|
31
|
+
const tenant = runtime.tenantRegistry.get(body.tenantId);
|
|
32
|
+
if (!tenant || tenant.appName !== runtime.appName) {
|
|
33
|
+
return c.json({ error: "Tenant not found" }, 404);
|
|
34
|
+
}
|
|
35
|
+
if (!tenant.enabled) {
|
|
36
|
+
return c.json({ error: "Tenant is disabled" }, 403);
|
|
37
|
+
}
|
|
38
|
+
// Budget check (use tenantId:userId as billing key)
|
|
39
|
+
const billingConfig = tenant.billing?.budgetEndpoint
|
|
40
|
+
? tenant.billing
|
|
41
|
+
: runtime.billing;
|
|
42
|
+
if (billingConfig) {
|
|
43
|
+
const billingUserId = `${body.tenantId}:${body.userId}`;
|
|
44
|
+
const budgetResult = await checkBudget(billingConfig, billingUserId);
|
|
45
|
+
if (!budgetResult.allowed) {
|
|
46
|
+
return c.json({
|
|
47
|
+
content: billingConfig.overBudgetMessage ?? "Budget exhausted.",
|
|
48
|
+
budgetExhausted: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Build system prompt from tenant config
|
|
53
|
+
const systemPrompt = buildTenantSystemPrompt(tenant);
|
|
54
|
+
// Get or create session with tenantId
|
|
55
|
+
const session = runtime.sessionRegistry.getOrCreate({
|
|
56
|
+
appName: runtime.appName,
|
|
57
|
+
tenantId: body.tenantId,
|
|
58
|
+
userId: body.userId,
|
|
59
|
+
systemPrompt,
|
|
60
|
+
idleTimeoutMs: tenant.idleTimeoutMs,
|
|
61
|
+
});
|
|
62
|
+
// Process message
|
|
63
|
+
const result = await runtime.orchestrator.processMessage(session, userParts);
|
|
64
|
+
// Report token usage to billing (fire-and-forget)
|
|
65
|
+
if (billingConfig) {
|
|
66
|
+
const billingUserId = `${body.tenantId}:${body.userId}`;
|
|
67
|
+
reportUsage(billingConfig, billingUserId, {
|
|
68
|
+
tokens: result.inputTokens + result.outputTokens,
|
|
69
|
+
model: runtime.orchestrator.model ?? "unknown",
|
|
70
|
+
role: "assistant",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return c.json({
|
|
74
|
+
content: extractText(result.parts),
|
|
75
|
+
parts: result.parts,
|
|
76
|
+
inputTokens: result.inputTokens,
|
|
77
|
+
outputTokens: result.outputTokens,
|
|
78
|
+
sessionId: session.id,
|
|
79
|
+
tenantId: body.tenantId,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
app.get("/sessions", (c) => {
|
|
83
|
+
const tenantIdFilter = c.req.query("tenantId");
|
|
84
|
+
let sessions = runtime.sessionRegistry.activeSessions();
|
|
85
|
+
if (tenantIdFilter) {
|
|
86
|
+
sessions = sessions.filter((s) => s.tenantId === tenantIdFilter);
|
|
87
|
+
}
|
|
88
|
+
return c.json({
|
|
89
|
+
sessions: sessions.map((s) => ({
|
|
90
|
+
id: s.id,
|
|
91
|
+
userId: s.userId,
|
|
92
|
+
tenantId: s.tenantId,
|
|
93
|
+
messageCount: s.messageCount,
|
|
94
|
+
createdAt: s.createdAt.toISOString(),
|
|
95
|
+
lastActivityAt: s.lastActivityAt.toISOString(),
|
|
96
|
+
})),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
app.delete("/sessions/:tenantId/:userId", (c) => {
|
|
100
|
+
const tenantId = c.req.param("tenantId");
|
|
101
|
+
const userId = c.req.param("userId");
|
|
102
|
+
const removed = runtime.sessionRegistry.remove(runtime.appName, userId, tenantId);
|
|
103
|
+
return c.json({ removed });
|
|
104
|
+
});
|
|
105
|
+
return app;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=tenant-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-routes.js","sourceRoot":"","sources":["../../src/gateway/tenant-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { ModeBOrchestrator } from "../session/mode-b-orchestrator.js";
|
|
3
|
+
import type { SessionRegistry } from "../session/session-registry.js";
|
|
4
|
+
import type { TenantRegistry } from "../tenant/tenant-registry.js";
|
|
5
|
+
export interface WhatsAppWebhookConfig {
|
|
6
|
+
readonly appName: string;
|
|
7
|
+
readonly orchestrator: ModeBOrchestrator;
|
|
8
|
+
readonly sessionRegistry: SessionRegistry;
|
|
9
|
+
readonly tenantRegistry: TenantRegistry;
|
|
10
|
+
readonly verifyToken: string;
|
|
11
|
+
/** Base path for per-tenant data (e.g. ~/.kiln/gateway/bonitas). Memory DBs stored under <basePath>/memory/ */
|
|
12
|
+
readonly memoryBasePath?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function createWhatsAppWebhookRoutes(config: WhatsAppWebhookConfig): Hono;
|
|
15
|
+
//# sourceMappingURL=whatsapp-webhook-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp-webhook-routes.d.ts","sourceRoot":"","sources":["../../src/gateway/whatsapp-webhook-routes.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IACzC,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,+GAA+G;IAC/G,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AA2FD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAuF/E"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Gateway: WhatsApp webhook routes -- Hono sub-app for Meta webhook verification and incoming messages
|
|
2
|
+
// Resolves tenant by phone number, processes messages via Mode B orchestrator, replies via Cloud API
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { mkdirSync } from "node:fs";
|
|
6
|
+
import { textParts, extractText, SqliteMemoryStore } from "@kilnai/core";
|
|
7
|
+
import { buildTenantSystemPrompt } from "../tenant/system-prompt-builder.js";
|
|
8
|
+
import { sendWhatsAppMessage, whatsappMediaUrl } from "../channels/whatsapp-api.js";
|
|
9
|
+
/** Lazily-opened per-tenant memory stores. Keyed by tenantId. */
|
|
10
|
+
const memoryStores = new Map();
|
|
11
|
+
function getMemoryStore(memoryBasePath, tenantId) {
|
|
12
|
+
let store = memoryStores.get(tenantId);
|
|
13
|
+
if (store)
|
|
14
|
+
return store;
|
|
15
|
+
const dir = join(memoryBasePath, "memory");
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
store = new SqliteMemoryStore({
|
|
18
|
+
dbPath: join(dir, `${tenantId}.db`),
|
|
19
|
+
layer: "user",
|
|
20
|
+
tenantId,
|
|
21
|
+
});
|
|
22
|
+
memoryStores.set(tenantId, store);
|
|
23
|
+
return store;
|
|
24
|
+
}
|
|
25
|
+
/** Tool definition for notify_owner -- injected when tenant has escalationContact */
|
|
26
|
+
const NOTIFY_OWNER_TOOL = {
|
|
27
|
+
name: "notify_owner",
|
|
28
|
+
description: "Send a WhatsApp notification to the business owner. Use this when a customer wants to schedule an appointment, needs escalation, or when the owner needs to be informed about something. Include a clear summary of what the customer needs.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
message: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "The message to send to the owner. Include customer name (if known), requested service, date/time, and phone number.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["message"],
|
|
38
|
+
},
|
|
39
|
+
tags: new Set(["builtin"]),
|
|
40
|
+
};
|
|
41
|
+
/** Parse a WhatsApp message into ContentPart[] */
|
|
42
|
+
function parseWhatsAppMessageParts(msg) {
|
|
43
|
+
switch (msg.type) {
|
|
44
|
+
case "text":
|
|
45
|
+
return msg.text?.body ? textParts(msg.text.body) : null;
|
|
46
|
+
case "image": {
|
|
47
|
+
if (!msg.image)
|
|
48
|
+
return null;
|
|
49
|
+
const parts = [
|
|
50
|
+
{ type: "image", mimeType: msg.image.mime_type, url: whatsappMediaUrl(msg.image.id) },
|
|
51
|
+
];
|
|
52
|
+
if (msg.image.caption)
|
|
53
|
+
parts.push({ type: "text", text: msg.image.caption });
|
|
54
|
+
return parts;
|
|
55
|
+
}
|
|
56
|
+
case "audio":
|
|
57
|
+
if (!msg.audio)
|
|
58
|
+
return null;
|
|
59
|
+
return [{ type: "audio", mimeType: msg.audio.mime_type, url: whatsappMediaUrl(msg.audio.id) }];
|
|
60
|
+
case "document": {
|
|
61
|
+
if (!msg.document)
|
|
62
|
+
return null;
|
|
63
|
+
const parts = [
|
|
64
|
+
{ type: "file", mimeType: msg.document.mime_type, url: whatsappMediaUrl(msg.document.id), filename: msg.document.filename },
|
|
65
|
+
];
|
|
66
|
+
if (msg.document.caption)
|
|
67
|
+
parts.push({ type: "text", text: msg.document.caption });
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function createWhatsAppWebhookRoutes(config) {
|
|
75
|
+
const app = new Hono();
|
|
76
|
+
// GET /webhook -- Meta verification handshake
|
|
77
|
+
app.get("/webhook", (c) => {
|
|
78
|
+
const mode = c.req.query("hub.mode");
|
|
79
|
+
const token = c.req.query("hub.verify_token");
|
|
80
|
+
const challenge = c.req.query("hub.challenge");
|
|
81
|
+
if (mode === "subscribe" && token === config.verifyToken) {
|
|
82
|
+
return c.text(challenge ?? "", 200);
|
|
83
|
+
}
|
|
84
|
+
return c.text("Forbidden", 403);
|
|
85
|
+
});
|
|
86
|
+
// POST /webhook -- Incoming messages from Meta
|
|
87
|
+
app.post("/webhook", async (c) => {
|
|
88
|
+
let payload;
|
|
89
|
+
try {
|
|
90
|
+
payload = await c.req.json();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return c.text("OK", 200);
|
|
94
|
+
}
|
|
95
|
+
if (!payload.entry) {
|
|
96
|
+
return c.text("OK", 200);
|
|
97
|
+
}
|
|
98
|
+
// Process each entry in the background
|
|
99
|
+
const processPromises = [];
|
|
100
|
+
for (const entry of payload.entry) {
|
|
101
|
+
for (const change of entry.changes) {
|
|
102
|
+
const phoneNumberId = change.value.metadata?.phone_number_id;
|
|
103
|
+
if (!phoneNumberId)
|
|
104
|
+
continue;
|
|
105
|
+
const messages = change.value.messages;
|
|
106
|
+
if (!messages)
|
|
107
|
+
continue;
|
|
108
|
+
// Resolve tenant by phone number
|
|
109
|
+
const tenant = config.tenantRegistry.resolveByPhone(phoneNumberId, config.appName);
|
|
110
|
+
if (!tenant) {
|
|
111
|
+
console.warn(`[whatsapp] No tenant found for phone_number_id=${phoneNumberId} app=${config.appName}`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const contacts = change.value.contacts ?? [];
|
|
115
|
+
for (const msg of messages) {
|
|
116
|
+
const msgParts = parseWhatsAppMessageParts(msg);
|
|
117
|
+
if (!msgParts) {
|
|
118
|
+
console.warn(`[whatsapp] Unsupported message type=${msg.type} from=${msg.from}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Resolve canonical reply address from contacts; fall back to msg.from
|
|
122
|
+
const contact = contacts.find((c) => c.wa_id === msg.from);
|
|
123
|
+
const replyTo = contact?.wa_id ?? msg.from;
|
|
124
|
+
console.log(`[whatsapp] Received message from=${replyTo} tenant=${tenant.tenantId} type=${msg.type}`);
|
|
125
|
+
const promise = processWhatsAppMessage(config, tenant.tenantId, replyTo, msgParts, phoneNumberId, tenant.whatsappAccessToken);
|
|
126
|
+
processPromises.push(promise);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Fire and forget -- log any failures from settled promises
|
|
131
|
+
Promise.allSettled(processPromises).then((results) => {
|
|
132
|
+
for (const result of results) {
|
|
133
|
+
if (result.status === "rejected") {
|
|
134
|
+
console.warn("[whatsapp] Message processing failed:", result.reason);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return c.text("OK", 200);
|
|
139
|
+
});
|
|
140
|
+
return app;
|
|
141
|
+
}
|
|
142
|
+
async function processWhatsAppMessage(config, tenantId, senderPhone, messageParts, phoneNumberId, accessTokenEnv) {
|
|
143
|
+
const tenant = config.tenantRegistry.get(tenantId);
|
|
144
|
+
if (!tenant)
|
|
145
|
+
return;
|
|
146
|
+
const systemPrompt = buildTenantSystemPrompt(tenant);
|
|
147
|
+
const accessToken = accessTokenEnv ? process.env[accessTokenEnv] ?? "" : "";
|
|
148
|
+
const messageText = extractText(messageParts);
|
|
149
|
+
const session = config.sessionRegistry.getOrCreate({
|
|
150
|
+
appName: config.appName,
|
|
151
|
+
tenantId,
|
|
152
|
+
userId: senderPhone,
|
|
153
|
+
systemPrompt,
|
|
154
|
+
idleTimeoutMs: tenant.idleTimeoutMs,
|
|
155
|
+
});
|
|
156
|
+
// --- Memory: recall past context about this user ---
|
|
157
|
+
let recalledMemory;
|
|
158
|
+
if (config.memoryBasePath) {
|
|
159
|
+
try {
|
|
160
|
+
const store = getMemoryStore(config.memoryBasePath, tenantId);
|
|
161
|
+
const query = `${senderPhone} ${messageText}`;
|
|
162
|
+
recalledMemory = await store.recall(query, 500) || undefined;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.warn(`[whatsapp] Memory recall failed for tenant=${tenantId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// --- Tools: build per-call builtin tools ---
|
|
169
|
+
const callTools = new Map();
|
|
170
|
+
if (tenant.escalationContact?.phone) {
|
|
171
|
+
const ownerPhone = tenant.escalationContact.phone.replace(/\+/g, "");
|
|
172
|
+
callTools.set("notify_owner", async (input) => {
|
|
173
|
+
const msg = String(input.message ?? "");
|
|
174
|
+
const fullMessage = `[Ale - Notificación automática]\n\nCliente: ${senderPhone}\n${msg}`;
|
|
175
|
+
await sendWhatsAppMessage(phoneNumberId, accessToken, ownerPhone, {
|
|
176
|
+
type: "text",
|
|
177
|
+
text: { body: fullMessage },
|
|
178
|
+
});
|
|
179
|
+
console.log(`[whatsapp] Owner notified for tenant=${tenantId} owner=${ownerPhone}`);
|
|
180
|
+
return { success: true, message: "Owner has been notified." };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Register notify_owner tool definition on the orchestrator if not already present
|
|
184
|
+
if (callTools.size > 0 && config.orchestrator.tools) {
|
|
185
|
+
const hasNotifyTool = config.orchestrator.tools.some((t) => t.name === "notify_owner");
|
|
186
|
+
if (!hasNotifyTool) {
|
|
187
|
+
config.orchestrator.registerTools([NOTIFY_OWNER_TOOL]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const result = await config.orchestrator.processMessage(session, messageParts, recalledMemory, callTools.size > 0 ? callTools : undefined);
|
|
191
|
+
// Reply via WhatsApp Cloud API
|
|
192
|
+
const replyText = extractText(result.parts);
|
|
193
|
+
try {
|
|
194
|
+
await sendWhatsAppMessage(phoneNumberId, accessToken, senderPhone, {
|
|
195
|
+
type: "text",
|
|
196
|
+
text: { body: replyText },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
console.warn(`[whatsapp] Failed to send reply -- phoneNumberId=${phoneNumberId} recipient=${senderPhone} error=${err instanceof Error ? err.message : String(err)}`);
|
|
201
|
+
}
|
|
202
|
+
// --- Memory: save what was learned from this exchange ---
|
|
203
|
+
if (config.memoryBasePath && messageText.length > 5) {
|
|
204
|
+
try {
|
|
205
|
+
const store = getMemoryStore(config.memoryBasePath, tenantId);
|
|
206
|
+
await store.save({
|
|
207
|
+
layer: "user",
|
|
208
|
+
content: `[${senderPhone}] User: ${messageText}\nAssistant: ${replyText}`,
|
|
209
|
+
tags: [senderPhone],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.warn(`[whatsapp] Memory save failed for tenant=${tenantId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=whatsapp-webhook-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp-webhook-routes.js","sourceRoot":"","sources":["../../src/gateway/whatsapp-webhook-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { UpgradeWebSocket } from "hono/ws";
|
|
3
|
+
import type { WebChannel } from "../channels/web-channel.js";
|
|
4
|
+
import type { ContentPart } from "@kilnai/core";
|
|
5
|
+
export interface WsRoutesConfig {
|
|
6
|
+
readonly webChannel: WebChannel;
|
|
7
|
+
readonly upgradeWebSocket: UpgradeWebSocket;
|
|
8
|
+
readonly validateToken?: (token: string) => {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
userId?: string;
|
|
11
|
+
};
|
|
12
|
+
readonly processMessage?: (userId: string, parts: readonly ContentPart[]) => Promise<{
|
|
13
|
+
parts: readonly ContentPart[];
|
|
14
|
+
inputTokens: number;
|
|
15
|
+
outputTokens: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createWsRoutes(config: WsRoutesConfig): Hono;
|
|
19
|
+
//# sourceMappingURL=ws-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-routes.d.ts","sourceRoot":"","sources":["../../src/gateway/ws-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,SAAS,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,cAAc,CAAC;AAGjE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,WAAW,EAAE,KAAK,OAAO,CAAC;QACnF,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAmF3D"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { textParts, extractText } from "@kilnai/core";
|
|
3
|
+
export function createWsRoutes(config) {
|
|
4
|
+
const app = new Hono();
|
|
5
|
+
/**
|
|
6
|
+
* Per-request validated userId, scoped by token to avoid module-level mutable state.
|
|
7
|
+
* Entries are consumed (deleted) immediately in the upgrade handler, so concurrent
|
|
8
|
+
* requests with different tokens never collide.
|
|
9
|
+
*/
|
|
10
|
+
const validatedUserIds = new Map();
|
|
11
|
+
app.get("/ws", async (c, next) => {
|
|
12
|
+
if (config.validateToken) {
|
|
13
|
+
const token = c.req.query("token");
|
|
14
|
+
if (!token)
|
|
15
|
+
return c.text("Unauthorized", 401);
|
|
16
|
+
const result = config.validateToken(token);
|
|
17
|
+
if (!result.valid)
|
|
18
|
+
return c.text("Unauthorized", 401);
|
|
19
|
+
if (result.userId) {
|
|
20
|
+
validatedUserIds.set(token, result.userId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
await next();
|
|
24
|
+
}, config.upgradeWebSocket((c) => {
|
|
25
|
+
const token = c.req.query("token");
|
|
26
|
+
const validatedUserId = token ? validatedUserIds.get(token) : undefined;
|
|
27
|
+
if (token)
|
|
28
|
+
validatedUserIds.delete(token);
|
|
29
|
+
const userId = validatedUserId ??
|
|
30
|
+
c.req.query("sessionId") ??
|
|
31
|
+
c.req.query("userId") ??
|
|
32
|
+
crypto.randomUUID();
|
|
33
|
+
return {
|
|
34
|
+
onOpen(_event, ws) {
|
|
35
|
+
config.webChannel.addClient(ws, userId);
|
|
36
|
+
},
|
|
37
|
+
onClose(_event, ws) {
|
|
38
|
+
config.webChannel.removeClient(ws);
|
|
39
|
+
},
|
|
40
|
+
async onMessage(event, ws) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = event.data;
|
|
43
|
+
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
44
|
+
const parsed = JSON.parse(text);
|
|
45
|
+
// Handle chat message frames via orchestrator
|
|
46
|
+
if (parsed.type === "message" && config.processMessage) {
|
|
47
|
+
const userParts = Array.isArray(parsed.parts)
|
|
48
|
+
? parsed.parts
|
|
49
|
+
: textParts(String(parsed.content ?? ""));
|
|
50
|
+
try {
|
|
51
|
+
const result = await config.processMessage(userId, userParts);
|
|
52
|
+
ws.send(JSON.stringify({
|
|
53
|
+
type: "done",
|
|
54
|
+
content: extractText(result.parts),
|
|
55
|
+
parts: result.parts,
|
|
56
|
+
inputTokens: result.inputTokens,
|
|
57
|
+
outputTokens: result.outputTokens,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
ws.send(JSON.stringify({
|
|
62
|
+
type: "error",
|
|
63
|
+
message: err instanceof Error ? err.message : String(err),
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Fall back to webChannel.receive for non-message frames
|
|
69
|
+
await config.webChannel.receive(parsed);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Discard malformed messages
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}));
|
|
77
|
+
return app;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=ws-routes.js.map
|