@khoralabs/chat 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 +7 -0
- package/dist/domain.d.ts +10 -0
- package/dist/domain.d.ts.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/events.d.ts +59 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/hash.d.ts +47 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/http/client.d.ts +21 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +367 -0
- package/dist/http/config.d.ts +5 -0
- package/dist/http/config.d.ts.map +1 -0
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +1084 -0
- package/dist/http/routes.d.ts +15 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +645 -0
- package/dist/http/server.d.ts +19 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +973 -0
- package/dist/http/service.d.ts +34 -0
- package/dist/http/service.d.ts.map +1 -0
- package/dist/http/service.js +501 -0
- package/dist/ids.d.ts +8 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +453 -0
- package/dist/lineage.d.ts +5 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/persistence/core/index.d.ts +2 -0
- package/dist/persistence/core/index.d.ts.map +1 -0
- package/dist/persistence/core/persistence/base-persistence.d.ts +12 -0
- package/dist/persistence/core/persistence/base-persistence.d.ts.map +1 -0
- package/dist/persistence/core/persistence/helpers.d.ts +30 -0
- package/dist/persistence/core/persistence/helpers.d.ts.map +1 -0
- package/dist/persistence/core/persistence/index.d.ts +6 -0
- package/dist/persistence/core/persistence/index.d.ts.map +1 -0
- package/dist/persistence/core/persistence/index.js +1276 -0
- package/dist/persistence/core/persistence/memory-persistence.d.ts +55 -0
- package/dist/persistence/core/persistence/memory-persistence.d.ts.map +1 -0
- package/dist/persistence/core/persistence/signed-persistence.d.ts +18 -0
- package/dist/persistence/core/persistence/signed-persistence.d.ts.map +1 -0
- package/dist/persistence/core/persistence/types.d.ts +210 -0
- package/dist/persistence/core/persistence/types.d.ts.map +1 -0
- package/dist/persistence/sqlite/index.d.ts +3 -0
- package/dist/persistence/sqlite/index.d.ts.map +1 -0
- package/dist/persistence/sqlite/index.js +1544 -0
- package/dist/persistence/sqlite/persistence/persistence.d.ts +49 -0
- package/dist/persistence/sqlite/persistence/persistence.d.ts.map +1 -0
- package/dist/persistence/sqlite/persistence/schema.d.ts +4 -0
- package/dist/persistence/sqlite/persistence/schema.d.ts.map +1 -0
- package/dist/persistence/sqlite/persistence/streaming.d.ts +27 -0
- package/dist/persistence/sqlite/persistence/streaming.d.ts.map +1 -0
- package/dist/persistence/testing/index.d.ts +2 -0
- package/dist/persistence/testing/index.d.ts.map +1 -0
- package/dist/persistence/testing/index.js +689 -0
- package/dist/persistence/testing/persistence/contract.d.ts +3 -0
- package/dist/persistence/testing/persistence/contract.d.ts.map +1 -0
- package/dist/persistence/turso-serverless/index.d.ts +10 -0
- package/dist/persistence/turso-serverless/index.d.ts.map +1 -0
- package/dist/persistence/turso-serverless/index.js +1812 -0
- package/dist/persistence/turso-serverless/persistence/local-sqlite.d.ts +5 -0
- package/dist/persistence/turso-serverless/persistence/local-sqlite.d.ts.map +1 -0
- package/dist/persistence/turso-serverless/persistence/persistence.d.ts +49 -0
- package/dist/persistence/turso-serverless/persistence/persistence.d.ts.map +1 -0
- package/dist/persistence/turso-serverless/persistence/schema.d.ts +4 -0
- package/dist/persistence/turso-serverless/persistence/schema.d.ts.map +1 -0
- package/dist/persistence/turso-serverless/persistence/sql.d.ts +20 -0
- package/dist/persistence/turso-serverless/persistence/sql.d.ts.map +1 -0
- package/dist/persistence/turso-serverless/persistence/sql.js +53 -0
- package/dist/persistence/turso-serverless/persistence/streaming.d.ts +27 -0
- package/dist/persistence/turso-serverless/persistence/streaming.d.ts.map +1 -0
- package/dist/service.d.ts +49 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/stream.d.ts +32 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/validation.d.ts +4 -0
- package/dist/validation.d.ts.map +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChatService } from "../service.js";
|
|
2
|
+
export type AuthorizeRequest = (req: Request) => Response | null | Promise<Response | null>;
|
|
3
|
+
export declare function requireInternalToken(req: Request, expected: string): Response | null;
|
|
4
|
+
export declare function createTokenAuthorizer(token: string): AuthorizeRequest;
|
|
5
|
+
export declare function readJson<T>(req: Request): Promise<T | null>;
|
|
6
|
+
export type RouteHandler = (req: Request) => Response | Promise<Response>;
|
|
7
|
+
export type CreateChatRoutesOptions = {
|
|
8
|
+
/** Default: shared-secret Bearer via `token`. */
|
|
9
|
+
authorize?: AuthorizeRequest;
|
|
10
|
+
token?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function createChatRoutes(chat: ChatService, tokenOrOptions: string | CreateChatRoutesOptions): Record<string, RouteHandler>;
|
|
13
|
+
export declare function createChatRoutesWithParams(chat: ChatService, tokenOrOptions: string | CreateChatRoutesOptions): Record<string, RouteHandler>;
|
|
14
|
+
export declare function dispatchChatRoute(routes: Record<string, RouteHandler>, req: Request): Promise<Response>;
|
|
15
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/http/routes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAejD,MAAM,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAE5F,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAMpF;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAErE;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAMjE;AAeD,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE1E,MAAM,MAAM,uBAAuB,GAAG;IACpC,iDAAiD;IACjD,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAUF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EACjB,cAAc,EAAE,MAAM,GAAG,uBAAuB,GAC/C,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CA8S9B;AAcD,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,WAAW,EACjB,cAAc,EAAE,MAAM,GAAG,uBAAuB,GAC/C,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CA0C9B;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EACpC,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,QAAQ,CAAC,CAwBnB"}
|
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
class ChatError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ChatError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ChatNotFoundError extends ChatError {
|
|
15
|
+
resource;
|
|
16
|
+
resourceId;
|
|
17
|
+
constructor(resource, resourceId) {
|
|
18
|
+
super("not_found", `${resource} not found: ${resourceId}`);
|
|
19
|
+
this.name = "ChatNotFoundError";
|
|
20
|
+
this.resource = resource;
|
|
21
|
+
this.resourceId = resourceId;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class ChatConflictError extends ChatError {
|
|
26
|
+
reason;
|
|
27
|
+
constructor(reason, message) {
|
|
28
|
+
super("conflict", message);
|
|
29
|
+
this.name = "ChatConflictError";
|
|
30
|
+
this.reason = reason;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class ChatValidationError extends ChatError {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super("validation", message);
|
|
37
|
+
this.name = "ChatValidationError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class ChatPermissionError extends ChatError {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super("permission_denied", message);
|
|
44
|
+
this.name = "ChatPermissionError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// src/hash.ts
|
|
48
|
+
function sha256Hex(input) {
|
|
49
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
50
|
+
hasher.update(input);
|
|
51
|
+
return hasher.digest("hex");
|
|
52
|
+
}
|
|
53
|
+
function stableStringify(value) {
|
|
54
|
+
if (value === undefined) {
|
|
55
|
+
return "null";
|
|
56
|
+
}
|
|
57
|
+
if (value === null || typeof value !== "object") {
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
62
|
+
}
|
|
63
|
+
const record = value;
|
|
64
|
+
const keys = Object.keys(record).sort();
|
|
65
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
66
|
+
}
|
|
67
|
+
function canonicalPostVersionPayload(input) {
|
|
68
|
+
return stableStringify({
|
|
69
|
+
postId: input.postId,
|
|
70
|
+
versionId: input.versionId,
|
|
71
|
+
threadId: input.threadId,
|
|
72
|
+
author: input.author,
|
|
73
|
+
role: input.role,
|
|
74
|
+
parts: input.parts,
|
|
75
|
+
metadata: input.metadata ?? null,
|
|
76
|
+
mentions: input.mentions ?? null,
|
|
77
|
+
model: input.model ?? null,
|
|
78
|
+
usage: input.usage ?? null,
|
|
79
|
+
parentVersionId: input.parentVersionId ?? null,
|
|
80
|
+
previousPostVersionId: input.previousPostVersionId ?? null
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function computeContentHash(payload) {
|
|
84
|
+
return sha256Hex(canonicalPostVersionPayload(payload));
|
|
85
|
+
}
|
|
86
|
+
function computeLineageHash(input) {
|
|
87
|
+
return sha256Hex(stableStringify({
|
|
88
|
+
previousLineageHash: input.previousLineageHash,
|
|
89
|
+
contentHash: input.contentHash,
|
|
90
|
+
postId: input.postId,
|
|
91
|
+
versionId: input.versionId
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
function canonicalAclEventPayload(input) {
|
|
95
|
+
return stableStringify({
|
|
96
|
+
type: input.type,
|
|
97
|
+
targetType: input.targetType,
|
|
98
|
+
targetId: input.targetId,
|
|
99
|
+
scope: input.scope ?? null,
|
|
100
|
+
role: input.role ?? null,
|
|
101
|
+
actor: input.actor,
|
|
102
|
+
previousAclEventId: input.previousAclEventId ?? null
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function computeAclEventContentHash(payload) {
|
|
106
|
+
return sha256Hex(canonicalAclEventPayload(payload));
|
|
107
|
+
}
|
|
108
|
+
function signedPayloadBytes(payload) {
|
|
109
|
+
return new TextEncoder().encode(payload);
|
|
110
|
+
}
|
|
111
|
+
function canonicalSignedPostVersionPayload(input) {
|
|
112
|
+
return stableStringify({
|
|
113
|
+
contentHash: input.contentHash,
|
|
114
|
+
lineageHash: input.lineageHash,
|
|
115
|
+
payload: JSON.parse(canonicalPostVersionPayload(input))
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function canonicalSignedAclEventPayload(input) {
|
|
119
|
+
return stableStringify({
|
|
120
|
+
contentHash: input.contentHash,
|
|
121
|
+
payload: JSON.parse(canonicalAclEventPayload(input.payload))
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function maybeSignPayload(payload, signer, sign) {
|
|
125
|
+
return sign(signedPayloadBytes(payload), signer);
|
|
126
|
+
}
|
|
127
|
+
// src/ids.ts
|
|
128
|
+
function createId() {
|
|
129
|
+
return crypto.randomUUID();
|
|
130
|
+
}
|
|
131
|
+
function scopeKey(scope) {
|
|
132
|
+
return `${scope.type}:${scope.id}`;
|
|
133
|
+
}
|
|
134
|
+
function scopeRefFromKey(key) {
|
|
135
|
+
const separatorIndex = key.indexOf(":");
|
|
136
|
+
if (separatorIndex === -1) {
|
|
137
|
+
throw new Error(`Invalid scope key: ${key}`);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
type: key.slice(0, separatorIndex),
|
|
141
|
+
id: key.slice(separatorIndex + 1)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// src/lineage.ts
|
|
145
|
+
function walkLineageFromHead(headVersionId, versionsById) {
|
|
146
|
+
const lineage = [];
|
|
147
|
+
let currentId = headVersionId;
|
|
148
|
+
const seenPostIds = new Set;
|
|
149
|
+
while (currentId) {
|
|
150
|
+
const version = versionsById.get(currentId);
|
|
151
|
+
if (!version)
|
|
152
|
+
break;
|
|
153
|
+
if (!seenPostIds.has(version.postId)) {
|
|
154
|
+
lineage.push(version);
|
|
155
|
+
seenPostIds.add(version.postId);
|
|
156
|
+
}
|
|
157
|
+
currentId = version.previousPostVersionId;
|
|
158
|
+
}
|
|
159
|
+
return lineage.reverse();
|
|
160
|
+
}
|
|
161
|
+
function lineageBetween(headVersionId, ancestorVersionId, versionsById) {
|
|
162
|
+
const lineage = walkLineageFromHead(headVersionId, versionsById);
|
|
163
|
+
const startIndex = lineage.findIndex((version) => version.id === ancestorVersionId);
|
|
164
|
+
if (startIndex === -1)
|
|
165
|
+
return null;
|
|
166
|
+
return lineage.slice(startIndex);
|
|
167
|
+
}
|
|
168
|
+
function postFromVersion(version, index, deletedAtMs) {
|
|
169
|
+
return {
|
|
170
|
+
...version,
|
|
171
|
+
id: version.postId,
|
|
172
|
+
status: "complete",
|
|
173
|
+
versionId: version.id,
|
|
174
|
+
previousVersionId: version.parentVersionId ?? null,
|
|
175
|
+
index,
|
|
176
|
+
updatedAtMs: version.parentVersionId ? version.createdAtMs : null,
|
|
177
|
+
deletedAtMs: deletedAtMs ?? null
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// src/stream.ts
|
|
181
|
+
function rebuildStreamCacheFromEvents(events) {
|
|
182
|
+
const sorted = [...events].sort((a, b) => a.revision - b.revision);
|
|
183
|
+
let message = null;
|
|
184
|
+
let mentions;
|
|
185
|
+
let model;
|
|
186
|
+
let usage;
|
|
187
|
+
let revision = 0;
|
|
188
|
+
for (const event of sorted) {
|
|
189
|
+
if (event.eventType === "stream.started") {
|
|
190
|
+
if (!event.message) {
|
|
191
|
+
throw new Error(`stream.started event ${event.id} missing message`);
|
|
192
|
+
}
|
|
193
|
+
message = event.message;
|
|
194
|
+
mentions = event.mentions;
|
|
195
|
+
model = event.model;
|
|
196
|
+
usage = event.usage;
|
|
197
|
+
revision = event.revision;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (event.eventType === "stream.delta") {
|
|
201
|
+
if (!event.message) {
|
|
202
|
+
throw new Error(`stream.delta event ${event.id} missing message`);
|
|
203
|
+
}
|
|
204
|
+
message = event.message;
|
|
205
|
+
if (event.mentions !== undefined)
|
|
206
|
+
mentions = event.mentions;
|
|
207
|
+
if (event.model !== undefined)
|
|
208
|
+
model = event.model;
|
|
209
|
+
if (event.usage !== undefined)
|
|
210
|
+
usage = event.usage;
|
|
211
|
+
revision = event.revision;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (event.eventType === "stream.completed" || event.eventType === "stream.aborted") {
|
|
215
|
+
revision = event.revision;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!message) {
|
|
219
|
+
throw new Error("stream events did not produce a message");
|
|
220
|
+
}
|
|
221
|
+
return { message, mentions, model, usage, revision };
|
|
222
|
+
}
|
|
223
|
+
function streamingPostFromCache(input) {
|
|
224
|
+
return {
|
|
225
|
+
...input.message,
|
|
226
|
+
id: input.postId,
|
|
227
|
+
status: "streaming",
|
|
228
|
+
threadId: input.threadId,
|
|
229
|
+
author: input.author,
|
|
230
|
+
mentions: input.mentions,
|
|
231
|
+
model: input.model,
|
|
232
|
+
usage: input.usage,
|
|
233
|
+
index: input.index,
|
|
234
|
+
streamRevision: input.streamRevision,
|
|
235
|
+
createdAtMs: input.createdAtMs,
|
|
236
|
+
updatedAtMs: input.updatedAtMs ?? null,
|
|
237
|
+
deletedAtMs: input.deletedAtMs ?? null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function mergeThreadPostsForList(lineagePosts, activePosts) {
|
|
241
|
+
const byIndex = new Map;
|
|
242
|
+
for (const item of lineagePosts) {
|
|
243
|
+
byIndex.set(item.index, item.post);
|
|
244
|
+
}
|
|
245
|
+
for (const post of activePosts) {
|
|
246
|
+
if (post.status === "streaming" || post.status === "aborted") {
|
|
247
|
+
byIndex.set(post.index, post);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [...byIndex.entries()].sort(([a], [b]) => a - b).map(([, post]) => post);
|
|
251
|
+
}
|
|
252
|
+
// src/validation.ts
|
|
253
|
+
function assertThreadRoot(root) {
|
|
254
|
+
if (root.type === "channel") {
|
|
255
|
+
if (!root.channelId.trim()) {
|
|
256
|
+
throw new ChatValidationError("channel root requires channelId");
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!root.postId.trim()) {
|
|
261
|
+
throw new ChatValidationError("post root requires postId");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function assertNonEmptyString(value, label) {
|
|
265
|
+
if (!value.trim()) {
|
|
266
|
+
throw new ChatValidationError(`${label} is required`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// src/http/routes.ts
|
|
270
|
+
function json(value, init) {
|
|
271
|
+
return Response.json(value, init);
|
|
272
|
+
}
|
|
273
|
+
function extractBearerToken(req) {
|
|
274
|
+
const auth = req.headers.get("authorization")?.trim() ?? "";
|
|
275
|
+
const match = /^Bearer\s+(.+)$/i.exec(auth);
|
|
276
|
+
const headerToken = match?.[1]?.trim() ?? "";
|
|
277
|
+
if (headerToken.length > 0)
|
|
278
|
+
return headerToken;
|
|
279
|
+
const url = new URL(req.url);
|
|
280
|
+
return url.searchParams.get("token")?.trim() ?? "";
|
|
281
|
+
}
|
|
282
|
+
function requireInternalToken(req, expected) {
|
|
283
|
+
const token = extractBearerToken(req);
|
|
284
|
+
if (token.length === 0 || token !== expected) {
|
|
285
|
+
return json({ error: "Unauthorized" }, { status: 401 });
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function createTokenAuthorizer(token) {
|
|
290
|
+
return (req) => requireInternalToken(req, token);
|
|
291
|
+
}
|
|
292
|
+
async function readJson(req) {
|
|
293
|
+
try {
|
|
294
|
+
return await req.json();
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function stringField(body, key) {
|
|
300
|
+
const value = body[key];
|
|
301
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
302
|
+
}
|
|
303
|
+
function errorResponse(error) {
|
|
304
|
+
if (error instanceof ChatNotFoundError) {
|
|
305
|
+
return json({ error: error.message }, { status: 404 });
|
|
306
|
+
}
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
return json({ error: message }, { status: 500 });
|
|
309
|
+
}
|
|
310
|
+
function resolveAuthorize(options) {
|
|
311
|
+
if (options.authorize !== undefined)
|
|
312
|
+
return options.authorize;
|
|
313
|
+
if (options.token !== undefined && options.token.length > 0) {
|
|
314
|
+
return createTokenAuthorizer(options.token);
|
|
315
|
+
}
|
|
316
|
+
throw new Error("createChatRoutes requires token or authorize");
|
|
317
|
+
}
|
|
318
|
+
function createChatRoutes(chat, tokenOrOptions) {
|
|
319
|
+
const options = typeof tokenOrOptions === "string" ? { token: tokenOrOptions } : tokenOrOptions;
|
|
320
|
+
const authorized = resolveAuthorize(options);
|
|
321
|
+
return {
|
|
322
|
+
"GET /health": async () => json({ ok: true }),
|
|
323
|
+
"POST /channels/get": async (req) => {
|
|
324
|
+
const error = await authorized(req);
|
|
325
|
+
if (error !== null)
|
|
326
|
+
return error;
|
|
327
|
+
const body = await readJson(req);
|
|
328
|
+
const channelId = body === null ? null : stringField(body, "channelId");
|
|
329
|
+
if (channelId === null)
|
|
330
|
+
return json({ error: "channelId is required" }, { status: 400 });
|
|
331
|
+
try {
|
|
332
|
+
return json(await chat.getChannel(channelId));
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return errorResponse(err);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
"POST /channels/create": async (req) => {
|
|
338
|
+
const error = await authorized(req);
|
|
339
|
+
if (error !== null)
|
|
340
|
+
return error;
|
|
341
|
+
const body = await readJson(req);
|
|
342
|
+
const id = body === null ? null : stringField(body, "id");
|
|
343
|
+
if (id === null)
|
|
344
|
+
return json({ error: "id is required" }, { status: 400 });
|
|
345
|
+
try {
|
|
346
|
+
return json(await chat.createChannel({
|
|
347
|
+
id,
|
|
348
|
+
metadata: body?.metadata
|
|
349
|
+
}));
|
|
350
|
+
} catch (err) {
|
|
351
|
+
return errorResponse(err);
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
"POST /threads/get": async (req) => {
|
|
355
|
+
const error = await authorized(req);
|
|
356
|
+
if (error !== null)
|
|
357
|
+
return error;
|
|
358
|
+
const body = await readJson(req);
|
|
359
|
+
const threadId = body === null ? null : stringField(body, "threadId");
|
|
360
|
+
if (threadId === null)
|
|
361
|
+
return json({ error: "threadId is required" }, { status: 400 });
|
|
362
|
+
try {
|
|
363
|
+
return json(await chat.getThread(threadId));
|
|
364
|
+
} catch (err) {
|
|
365
|
+
return errorResponse(err);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
"POST /threads/create": async (req) => {
|
|
369
|
+
const error = await authorized(req);
|
|
370
|
+
if (error !== null)
|
|
371
|
+
return error;
|
|
372
|
+
const body = await readJson(req);
|
|
373
|
+
if (body?.id === undefined || body.root === undefined) {
|
|
374
|
+
return json({ error: "id and root are required" }, { status: 400 });
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
return json(await chat.createThread({
|
|
378
|
+
id: body.id,
|
|
379
|
+
root: body.root,
|
|
380
|
+
metadata: body.metadata
|
|
381
|
+
}));
|
|
382
|
+
} catch (err) {
|
|
383
|
+
return errorResponse(err);
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
"POST /threads/list": async (req) => {
|
|
387
|
+
const error = await authorized(req);
|
|
388
|
+
if (error !== null)
|
|
389
|
+
return error;
|
|
390
|
+
const body = await readJson(req);
|
|
391
|
+
if (body?.channelId === undefined) {
|
|
392
|
+
return json({ error: "channelId is required" }, { status: 400 });
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
return json(await chat.listThreads({
|
|
396
|
+
channelId: body.channelId,
|
|
397
|
+
participant: body.participant,
|
|
398
|
+
limit: body.limit,
|
|
399
|
+
cursor: body.cursor
|
|
400
|
+
}));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return errorResponse(err);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
"POST /threads/list-posts": async (req) => {
|
|
406
|
+
const error = await authorized(req);
|
|
407
|
+
if (error !== null)
|
|
408
|
+
return error;
|
|
409
|
+
const body = await readJson(req);
|
|
410
|
+
if (body?.threadId === undefined) {
|
|
411
|
+
return json({ error: "threadId is required" }, { status: 400 });
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
return json(await chat.listPosts({ threadId: body.threadId, limit: body.limit, cursor: body.cursor }));
|
|
415
|
+
} catch (err) {
|
|
416
|
+
return errorResponse(err);
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
"POST /threads/tip": async (req) => {
|
|
420
|
+
const error = await authorized(req);
|
|
421
|
+
if (error !== null)
|
|
422
|
+
return error;
|
|
423
|
+
const body = await readJson(req);
|
|
424
|
+
const threadId = body === null ? null : stringField(body, "threadId");
|
|
425
|
+
if (threadId === null)
|
|
426
|
+
return json({ error: "threadId is required" }, { status: 400 });
|
|
427
|
+
try {
|
|
428
|
+
return json({ tip: await chat.getThreadTip(threadId) });
|
|
429
|
+
} catch (err) {
|
|
430
|
+
return errorResponse(err);
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
"POST /threads/list-participants": async (req) => {
|
|
434
|
+
const error = await authorized(req);
|
|
435
|
+
if (error !== null)
|
|
436
|
+
return error;
|
|
437
|
+
const body = await readJson(req);
|
|
438
|
+
const threadId = body === null ? null : stringField(body, "threadId");
|
|
439
|
+
if (threadId === null)
|
|
440
|
+
return json({ error: "threadId is required" }, { status: 400 });
|
|
441
|
+
try {
|
|
442
|
+
return json({ participants: await chat.listThreadParticipants(threadId) });
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return errorResponse(err);
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
"POST /threads/add-participant": async (req) => {
|
|
448
|
+
const error = await authorized(req);
|
|
449
|
+
if (error !== null)
|
|
450
|
+
return error;
|
|
451
|
+
const body = await readJson(req);
|
|
452
|
+
if (body?.threadId === undefined || body.scope === undefined || body.actor === undefined) {
|
|
453
|
+
return json({ error: "threadId, scope, and actor are required" }, { status: 400 });
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
return json(await chat.addThreadParticipant({
|
|
457
|
+
threadId: body.threadId,
|
|
458
|
+
scope: body.scope,
|
|
459
|
+
role: body.role ?? "participant",
|
|
460
|
+
actor: body.actor
|
|
461
|
+
}));
|
|
462
|
+
} catch (err) {
|
|
463
|
+
return errorResponse(err);
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
"POST /threads/append-post": async (req) => {
|
|
467
|
+
const error = await authorized(req);
|
|
468
|
+
if (error !== null)
|
|
469
|
+
return error;
|
|
470
|
+
const body = await readJson(req);
|
|
471
|
+
if (body?.threadId === undefined || body.author === undefined || body.message === undefined) {
|
|
472
|
+
return json({ error: "threadId, author, and message are required" }, { status: 400 });
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
return json(await chat.appendPost({
|
|
476
|
+
threadId: body.threadId,
|
|
477
|
+
author: body.author,
|
|
478
|
+
message: body.message,
|
|
479
|
+
expectedHeadPostVersionId: body.expectedHeadPostVersionId,
|
|
480
|
+
versionId: body.versionId,
|
|
481
|
+
createdAtMs: body.createdAtMs,
|
|
482
|
+
signature: body.signature
|
|
483
|
+
}));
|
|
484
|
+
} catch (err) {
|
|
485
|
+
return errorResponse(err);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
"POST /posts/set-signature": async (req) => {
|
|
489
|
+
const error = await authorized(req);
|
|
490
|
+
if (error !== null)
|
|
491
|
+
return error;
|
|
492
|
+
const body = await readJson(req);
|
|
493
|
+
if (body?.versionId === undefined || body.signature === undefined) {
|
|
494
|
+
return json({ error: "versionId and signature are required" }, { status: 400 });
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
await chat.setPostVersionSignature(body.versionId, body.signature);
|
|
498
|
+
return json({ ok: true });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
return errorResponse(err);
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
"POST /internal/chat/streamed-posts": async (req) => {
|
|
504
|
+
const error = await authorized(req);
|
|
505
|
+
if (error !== null)
|
|
506
|
+
return error;
|
|
507
|
+
const body = await readJson(req);
|
|
508
|
+
if (body?.author === undefined || body.message === undefined || body.threadId === undefined) {
|
|
509
|
+
return json({ error: "threadId, author, and message are required" }, { status: 400 });
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
return json(await chat.startStreamedPost({
|
|
513
|
+
threadId: body.threadId,
|
|
514
|
+
author: body.author,
|
|
515
|
+
idempotencyKey: body.idempotencyKey,
|
|
516
|
+
message: body.message
|
|
517
|
+
}));
|
|
518
|
+
} catch (err) {
|
|
519
|
+
return errorResponse(err);
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
"POST /internal/chat/posts/:postId/deltas": async (req) => {
|
|
523
|
+
const error = await authorized(req);
|
|
524
|
+
if (error !== null)
|
|
525
|
+
return error;
|
|
526
|
+
const url = new URL(req.url);
|
|
527
|
+
const postId = url.pathname.split("/").at(-2);
|
|
528
|
+
if (postId === undefined)
|
|
529
|
+
return json({ error: "postId is required" }, { status: 400 });
|
|
530
|
+
const body = await readJson(req);
|
|
531
|
+
if (body?.message === undefined) {
|
|
532
|
+
return json({ error: "message is required" }, { status: 400 });
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
return json(await chat.applyPostDelta({
|
|
536
|
+
postId,
|
|
537
|
+
expectedRevision: body.expectedRevision,
|
|
538
|
+
message: body.message,
|
|
539
|
+
model: body.model,
|
|
540
|
+
usage: body.usage
|
|
541
|
+
}));
|
|
542
|
+
} catch (err) {
|
|
543
|
+
return errorResponse(err);
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
"POST /internal/chat/posts/:postId/complete": async (req) => {
|
|
547
|
+
const error = await authorized(req);
|
|
548
|
+
if (error !== null)
|
|
549
|
+
return error;
|
|
550
|
+
const url = new URL(req.url);
|
|
551
|
+
const postId = url.pathname.split("/").at(-2);
|
|
552
|
+
if (postId === undefined)
|
|
553
|
+
return json({ error: "postId is required" }, { status: 400 });
|
|
554
|
+
const body = await readJson(req);
|
|
555
|
+
try {
|
|
556
|
+
return json(await chat.completeStreamedPost({
|
|
557
|
+
postId,
|
|
558
|
+
expectedRevision: body?.expectedRevision,
|
|
559
|
+
idempotencyKey: body?.idempotencyKey
|
|
560
|
+
}));
|
|
561
|
+
} catch (err) {
|
|
562
|
+
return errorResponse(err);
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
"POST /internal/chat/posts/:postId/abort": async (req) => {
|
|
566
|
+
const error = await authorized(req);
|
|
567
|
+
if (error !== null)
|
|
568
|
+
return error;
|
|
569
|
+
const url = new URL(req.url);
|
|
570
|
+
const postId = url.pathname.split("/").at(-2);
|
|
571
|
+
if (postId === undefined)
|
|
572
|
+
return json({ error: "postId is required" }, { status: 400 });
|
|
573
|
+
try {
|
|
574
|
+
const post = await chat.abortStreamedPost({ postId });
|
|
575
|
+
return json({ post });
|
|
576
|
+
} catch (err) {
|
|
577
|
+
return errorResponse(err);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function createChatRoutesWithParams(chat, tokenOrOptions) {
|
|
583
|
+
const options = typeof tokenOrOptions === "string" ? { token: tokenOrOptions } : tokenOrOptions;
|
|
584
|
+
const base = createChatRoutes(chat, options);
|
|
585
|
+
const authorized = resolveAuthorize(options);
|
|
586
|
+
return {
|
|
587
|
+
...base,
|
|
588
|
+
"POST /internal/chat/threads/:threadId/streamed-posts": async (req) => {
|
|
589
|
+
const error = await authorized(req);
|
|
590
|
+
if (error !== null)
|
|
591
|
+
return error;
|
|
592
|
+
const url = new URL(req.url);
|
|
593
|
+
const parts = url.pathname.split("/");
|
|
594
|
+
const threadIdx = parts.indexOf("threads");
|
|
595
|
+
const threadId = threadIdx >= 0 ? decodeURIComponent(parts[threadIdx + 1] ?? "") : undefined;
|
|
596
|
+
const body = await readJson(req);
|
|
597
|
+
if (threadId === undefined || threadId.length === 0 || body?.author === undefined || body.message === undefined) {
|
|
598
|
+
return json({ error: "threadId, author, and message are required" }, { status: 400 });
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
return json(await chat.startStreamedPost({
|
|
602
|
+
threadId,
|
|
603
|
+
author: body.author,
|
|
604
|
+
idempotencyKey: body.idempotencyKey,
|
|
605
|
+
message: body.message
|
|
606
|
+
}));
|
|
607
|
+
} catch (err) {
|
|
608
|
+
return errorResponse(err);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function dispatchChatRoute(routes, req) {
|
|
614
|
+
const url = new URL(req.url);
|
|
615
|
+
const paramPaths = [
|
|
616
|
+
"POST /internal/chat/threads/:threadId/streamed-posts",
|
|
617
|
+
"POST /internal/chat/posts/:postId/deltas",
|
|
618
|
+
"POST /internal/chat/posts/:postId/complete",
|
|
619
|
+
"POST /internal/chat/posts/:postId/abort"
|
|
620
|
+
];
|
|
621
|
+
for (const pattern of paramPaths) {
|
|
622
|
+
const [method, pathPattern] = pattern.split(" ");
|
|
623
|
+
if (req.method !== method || pathPattern === undefined)
|
|
624
|
+
continue;
|
|
625
|
+
const regex = new RegExp(`^${pathPattern.replace(/:[^/]+/g, "[^/]+")}$`);
|
|
626
|
+
if (regex.test(url.pathname)) {
|
|
627
|
+
const handler2 = routes[pattern];
|
|
628
|
+
if (handler2 !== undefined)
|
|
629
|
+
return handler2(req);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const key = `${req.method} ${url.pathname}`;
|
|
633
|
+
const handler = routes[key];
|
|
634
|
+
if (handler === undefined)
|
|
635
|
+
return json({ error: "Not found" }, { status: 404 });
|
|
636
|
+
return handler(req);
|
|
637
|
+
}
|
|
638
|
+
export {
|
|
639
|
+
requireInternalToken,
|
|
640
|
+
readJson,
|
|
641
|
+
dispatchChatRoute,
|
|
642
|
+
createTokenAuthorizer,
|
|
643
|
+
createChatRoutesWithParams,
|
|
644
|
+
createChatRoutes
|
|
645
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChatEvent } from "../domain.js";
|
|
2
|
+
import type { ChatService } from "../service.js";
|
|
3
|
+
import { type ChatStorageConfig } from "./service.js";
|
|
4
|
+
export type ChatHttpServerHandle = {
|
|
5
|
+
port: number;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
token: string;
|
|
8
|
+
service: ChatService;
|
|
9
|
+
stop(): void;
|
|
10
|
+
};
|
|
11
|
+
export type StartChatHttpServerOptions = {
|
|
12
|
+
storage: ChatStorageConfig;
|
|
13
|
+
token: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
onEvent?: (event: ChatEvent) => void;
|
|
16
|
+
};
|
|
17
|
+
/** Start chat HTTP (routes + /health + thread WebSocket fanout) with host-provided storage. */
|
|
18
|
+
export declare function startChatHttpServer(opts: StartChatHttpServerOptions): Promise<ChatHttpServerHandle>;
|
|
19
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/http/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAIjD,OAAO,EAAE,KAAK,iBAAiB,EAA4C,MAAM,cAAc,CAAC;AAEhG,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,IAAI,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC,CAAC;AAOF,+FAA+F;AAC/F,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,oBAAoB,CAAC,CAsD/B"}
|