@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.
Files changed (85) hide show
  1. package/README.md +7 -0
  2. package/dist/domain.d.ts +10 -0
  3. package/dist/domain.d.ts.map +1 -0
  4. package/dist/errors.d.ts +20 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/events.d.ts +59 -0
  7. package/dist/events.d.ts.map +1 -0
  8. package/dist/hash.d.ts +47 -0
  9. package/dist/hash.d.ts.map +1 -0
  10. package/dist/http/client.d.ts +21 -0
  11. package/dist/http/client.d.ts.map +1 -0
  12. package/dist/http/client.js +367 -0
  13. package/dist/http/config.d.ts +5 -0
  14. package/dist/http/config.d.ts.map +1 -0
  15. package/dist/http/index.d.ts +6 -0
  16. package/dist/http/index.d.ts.map +1 -0
  17. package/dist/http/index.js +1084 -0
  18. package/dist/http/routes.d.ts +15 -0
  19. package/dist/http/routes.d.ts.map +1 -0
  20. package/dist/http/routes.js +645 -0
  21. package/dist/http/server.d.ts +19 -0
  22. package/dist/http/server.d.ts.map +1 -0
  23. package/dist/http/server.js +973 -0
  24. package/dist/http/service.d.ts +34 -0
  25. package/dist/http/service.d.ts.map +1 -0
  26. package/dist/http/service.js +501 -0
  27. package/dist/ids.d.ts +8 -0
  28. package/dist/ids.d.ts.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +453 -0
  32. package/dist/lineage.d.ts +5 -0
  33. package/dist/lineage.d.ts.map +1 -0
  34. package/dist/persistence/core/index.d.ts +2 -0
  35. package/dist/persistence/core/index.d.ts.map +1 -0
  36. package/dist/persistence/core/persistence/base-persistence.d.ts +12 -0
  37. package/dist/persistence/core/persistence/base-persistence.d.ts.map +1 -0
  38. package/dist/persistence/core/persistence/helpers.d.ts +30 -0
  39. package/dist/persistence/core/persistence/helpers.d.ts.map +1 -0
  40. package/dist/persistence/core/persistence/index.d.ts +6 -0
  41. package/dist/persistence/core/persistence/index.d.ts.map +1 -0
  42. package/dist/persistence/core/persistence/index.js +1276 -0
  43. package/dist/persistence/core/persistence/memory-persistence.d.ts +55 -0
  44. package/dist/persistence/core/persistence/memory-persistence.d.ts.map +1 -0
  45. package/dist/persistence/core/persistence/signed-persistence.d.ts +18 -0
  46. package/dist/persistence/core/persistence/signed-persistence.d.ts.map +1 -0
  47. package/dist/persistence/core/persistence/types.d.ts +210 -0
  48. package/dist/persistence/core/persistence/types.d.ts.map +1 -0
  49. package/dist/persistence/sqlite/index.d.ts +3 -0
  50. package/dist/persistence/sqlite/index.d.ts.map +1 -0
  51. package/dist/persistence/sqlite/index.js +1544 -0
  52. package/dist/persistence/sqlite/persistence/persistence.d.ts +49 -0
  53. package/dist/persistence/sqlite/persistence/persistence.d.ts.map +1 -0
  54. package/dist/persistence/sqlite/persistence/schema.d.ts +4 -0
  55. package/dist/persistence/sqlite/persistence/schema.d.ts.map +1 -0
  56. package/dist/persistence/sqlite/persistence/streaming.d.ts +27 -0
  57. package/dist/persistence/sqlite/persistence/streaming.d.ts.map +1 -0
  58. package/dist/persistence/testing/index.d.ts +2 -0
  59. package/dist/persistence/testing/index.d.ts.map +1 -0
  60. package/dist/persistence/testing/index.js +689 -0
  61. package/dist/persistence/testing/persistence/contract.d.ts +3 -0
  62. package/dist/persistence/testing/persistence/contract.d.ts.map +1 -0
  63. package/dist/persistence/turso-serverless/index.d.ts +10 -0
  64. package/dist/persistence/turso-serverless/index.d.ts.map +1 -0
  65. package/dist/persistence/turso-serverless/index.js +1812 -0
  66. package/dist/persistence/turso-serverless/persistence/local-sqlite.d.ts +5 -0
  67. package/dist/persistence/turso-serverless/persistence/local-sqlite.d.ts.map +1 -0
  68. package/dist/persistence/turso-serverless/persistence/persistence.d.ts +49 -0
  69. package/dist/persistence/turso-serverless/persistence/persistence.d.ts.map +1 -0
  70. package/dist/persistence/turso-serverless/persistence/schema.d.ts +4 -0
  71. package/dist/persistence/turso-serverless/persistence/schema.d.ts.map +1 -0
  72. package/dist/persistence/turso-serverless/persistence/sql.d.ts +20 -0
  73. package/dist/persistence/turso-serverless/persistence/sql.d.ts.map +1 -0
  74. package/dist/persistence/turso-serverless/persistence/sql.js +53 -0
  75. package/dist/persistence/turso-serverless/persistence/streaming.d.ts +27 -0
  76. package/dist/persistence/turso-serverless/persistence/streaming.d.ts.map +1 -0
  77. package/dist/service.d.ts +49 -0
  78. package/dist/service.d.ts.map +1 -0
  79. package/dist/stream.d.ts +32 -0
  80. package/dist/stream.d.ts.map +1 -0
  81. package/dist/types.d.ts +201 -0
  82. package/dist/types.d.ts.map +1 -0
  83. package/dist/validation.d.ts +4 -0
  84. package/dist/validation.d.ts.map +1 -0
  85. package/package.json +104 -0
@@ -0,0 +1,1276 @@
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/persistence/core/persistence/base-persistence.ts
270
+ class BaseChatPersistence {
271
+ assertThreadRootExists(root) {
272
+ if (root.type === "channel" && !root.channelId.trim()) {
273
+ throw new ChatValidationError("channel root requires channelId");
274
+ }
275
+ if (root.type === "post" && !root.postId.trim()) {
276
+ throw new ChatValidationError("post root requires postId");
277
+ }
278
+ }
279
+ async requireChannel(id) {
280
+ const channel = await this.getChannel(id);
281
+ if (!channel)
282
+ throw new ChatNotFoundError("channel", id);
283
+ return channel;
284
+ }
285
+ async requireThread(id) {
286
+ const thread = await this.getThread(id);
287
+ if (!thread)
288
+ throw new ChatNotFoundError("thread", id);
289
+ return thread;
290
+ }
291
+ async requirePost(id) {
292
+ const post = await this.getPost(id);
293
+ if (!post)
294
+ throw new ChatNotFoundError("post", id);
295
+ return post;
296
+ }
297
+ validateCreateThreadInput(input) {
298
+ this.assertThreadRootExists(input.root);
299
+ }
300
+ }
301
+ // src/persistence/core/persistence/helpers.ts
302
+ function prepareAppendPost(input, options = {}) {
303
+ const generate = options.generateId ?? createId;
304
+ const now = options.now ?? (() => Date.now());
305
+ const postId = input.message.id || generate();
306
+ const versionId = input.versionId ?? generate();
307
+ const createdAtMs = input.createdAtMs ?? now();
308
+ const message = { ...input.message, id: postId };
309
+ const contentHash = computeContentHash({
310
+ postId,
311
+ versionId,
312
+ threadId: input.threadId,
313
+ author: input.author,
314
+ role: message.role,
315
+ parts: message.parts,
316
+ metadata: message.metadata,
317
+ mentions: input.mentions,
318
+ model: input.model,
319
+ usage: input.usage,
320
+ parentVersionId: null,
321
+ previousPostVersionId: input.previousPostVersionId
322
+ });
323
+ const lineageHash = computeLineageHash({
324
+ previousLineageHash: input.previousLineageHash,
325
+ contentHash,
326
+ postId,
327
+ versionId
328
+ });
329
+ return {
330
+ postId,
331
+ versionId,
332
+ threadId: input.threadId,
333
+ author: input.author,
334
+ message,
335
+ mentions: input.mentions,
336
+ model: input.model,
337
+ usage: input.usage,
338
+ previousPostVersionId: input.previousPostVersionId,
339
+ previousLineageHash: input.previousLineageHash,
340
+ parentVersionId: null,
341
+ createdAtMs,
342
+ contentHash,
343
+ lineageHash
344
+ };
345
+ }
346
+ function prepareEditPost(input, options = {}) {
347
+ const generate = options.generateId ?? createId;
348
+ const now = options.now ?? (() => Date.now());
349
+ const postId = input.postId;
350
+ const versionId = generate();
351
+ const createdAtMs = now();
352
+ const message = { ...input.message, id: postId };
353
+ const contentHash = computeContentHash({
354
+ postId,
355
+ versionId,
356
+ threadId: input.threadId,
357
+ author: input.author,
358
+ role: message.role,
359
+ parts: message.parts,
360
+ metadata: message.metadata,
361
+ mentions: input.mentions,
362
+ model: input.model,
363
+ usage: input.usage,
364
+ parentVersionId: input.parentVersionId,
365
+ previousPostVersionId: input.previousPostVersionId
366
+ });
367
+ const lineageHash = computeLineageHash({
368
+ previousLineageHash: input.previousLineageHash,
369
+ contentHash,
370
+ postId,
371
+ versionId
372
+ });
373
+ return {
374
+ postId,
375
+ versionId,
376
+ threadId: input.threadId,
377
+ author: input.author,
378
+ message,
379
+ mentions: input.mentions,
380
+ model: input.model,
381
+ usage: input.usage,
382
+ previousPostVersionId: input.previousPostVersionId,
383
+ previousLineageHash: input.previousLineageHash,
384
+ parentVersionId: input.parentVersionId,
385
+ createdAtMs,
386
+ contentHash,
387
+ lineageHash
388
+ };
389
+ }
390
+ function buildAclEventContentHash(input) {
391
+ return computeAclEventContentHash(input);
392
+ }
393
+ // src/persistence/core/persistence/memory-persistence.ts
394
+ var DEFAULT_HEAD_NAME = "default";
395
+
396
+ class MemoryChatPersistence extends BaseChatPersistence {
397
+ channels = new Map;
398
+ threads = new Map;
399
+ posts = new Map;
400
+ versions = new Map;
401
+ streamEvents = [];
402
+ streamIdempotency = new Map;
403
+ heads = new Map;
404
+ channelMembers = new Map;
405
+ threadParticipants = new Map;
406
+ aclEvents = [];
407
+ idempotency = new Map;
408
+ threadIndexes = new Map;
409
+ async createChannel(input) {
410
+ const channel = {
411
+ id: input.id ?? createId(),
412
+ metadata: input.metadata,
413
+ createdAtMs: Date.now()
414
+ };
415
+ this.channels.set(channel.id, channel);
416
+ return channel;
417
+ }
418
+ async createThread(input) {
419
+ this.validateCreateThreadInput(input);
420
+ if (input.root.type === "channel") {
421
+ await this.requireChannel(input.root.channelId);
422
+ } else {
423
+ await this.requirePost(input.root.postId);
424
+ }
425
+ const thread = {
426
+ id: input.id ?? createId(),
427
+ root: input.root,
428
+ defaultHeadId: null,
429
+ metadata: input.metadata,
430
+ createdAtMs: Date.now()
431
+ };
432
+ this.threads.set(thread.id, thread);
433
+ this.threadIndexes.set(thread.id, 0);
434
+ return thread;
435
+ }
436
+ async getChannel(id) {
437
+ return this.channels.get(id) ?? null;
438
+ }
439
+ async getThread(id) {
440
+ return this.threads.get(id) ?? null;
441
+ }
442
+ postFromRecord(postId, record) {
443
+ if (record.status === "streaming") {
444
+ if (!record.message)
445
+ return null;
446
+ return streamingPostFromCache({
447
+ postId,
448
+ threadId: record.threadId,
449
+ author: record.author,
450
+ message: record.message,
451
+ mentions: record.mentions,
452
+ model: record.model,
453
+ usage: record.usage,
454
+ index: record.index,
455
+ streamRevision: record.streamRevision,
456
+ createdAtMs: record.createdAtMs,
457
+ updatedAtMs: record.updatedAtMs,
458
+ deletedAtMs: record.deletedAtMs
459
+ });
460
+ }
461
+ if (record.status === "aborted") {
462
+ if (!record.message)
463
+ return null;
464
+ return {
465
+ ...record.message,
466
+ id: postId,
467
+ status: "aborted",
468
+ threadId: record.threadId,
469
+ author: record.author,
470
+ mentions: record.mentions,
471
+ model: record.model,
472
+ usage: record.usage,
473
+ index: record.index,
474
+ streamRevision: record.streamRevision,
475
+ createdAtMs: record.createdAtMs,
476
+ updatedAtMs: record.updatedAtMs,
477
+ deletedAtMs: record.deletedAtMs
478
+ };
479
+ }
480
+ const version = record.completedVersionId ? this.versions.get(record.completedVersionId) : [...this.versions.values()].filter((item) => item.postId === postId).sort((a, b) => b.createdAtMs - a.createdAtMs)[0];
481
+ if (!version)
482
+ return null;
483
+ return postFromVersion(version, record.index, record.deletedAtMs);
484
+ }
485
+ async getPost(id) {
486
+ const record = this.posts.get(id);
487
+ if (!record)
488
+ return null;
489
+ return this.postFromRecord(id, record);
490
+ }
491
+ async getPostVersion(id) {
492
+ return this.versions.get(id) ?? null;
493
+ }
494
+ async getThreadHead(threadId, headId) {
495
+ const thread = await this.requireThread(threadId);
496
+ const resolvedHeadId = headId ?? thread.defaultHeadId;
497
+ if (!resolvedHeadId)
498
+ return null;
499
+ const head = this.heads.get(resolvedHeadId);
500
+ return head?.threadId === threadId ? head : null;
501
+ }
502
+ async getThreadTip(threadId) {
503
+ const head = await this.getThreadHead(threadId);
504
+ if (!head)
505
+ return null;
506
+ const version = await this.getPostVersion(head.headPostVersionId);
507
+ if (!version)
508
+ return null;
509
+ return { id: version.id, lineageHash: version.lineageHash };
510
+ }
511
+ async listThreads(input) {
512
+ const limit = input.limit ?? 50;
513
+ let items = [...this.threads.values()];
514
+ if (input.channelId) {
515
+ items = items.filter((thread) => thread.root.type === "channel" && thread.root.channelId === input.channelId);
516
+ }
517
+ if (input.postId) {
518
+ items = items.filter((thread) => thread.root.type === "post" && thread.root.postId === input.postId);
519
+ }
520
+ if (input.participant) {
521
+ const key = scopeKey(input.participant);
522
+ items = items.filter((thread) => this.threadParticipants.get(thread.id)?.has(key));
523
+ }
524
+ items.sort((a, b) => a.createdAtMs - b.createdAtMs);
525
+ const start = input.cursor ? Number.parseInt(input.cursor, 10) : 0;
526
+ const page = items.slice(start, start + limit);
527
+ const nextCursor = start + limit < items.length ? String(start + limit) : null;
528
+ return { items: page, nextCursor };
529
+ }
530
+ async listPosts(input) {
531
+ const thread = await this.requireThread(input.threadId);
532
+ const head = input.headPostVersionId ? {
533
+ id: "inline",
534
+ threadId: input.threadId,
535
+ name: "inline",
536
+ headPostVersionId: input.headPostVersionId,
537
+ createdAtMs: 0
538
+ } : await this.getThreadHead(input.threadId, input.headId ?? thread.defaultHeadId ?? undefined);
539
+ const lineagePosts = head ? walkLineageFromHead(head.headPostVersionId, this.versions).flatMap((version) => {
540
+ const record = this.posts.get(version.postId);
541
+ if (!record)
542
+ return [];
543
+ const post = postFromVersion(version, record.index, record.deletedAtMs);
544
+ return [{ index: record.index, post }];
545
+ }) : [];
546
+ const activePosts = [...this.posts.entries()].filter(([, record]) => record.threadId === input.threadId && record.status !== "complete").map(([postId, record]) => this.postFromRecord(postId, record)).filter((post) => post !== null && (post.status === "streaming" || post.status === "aborted"));
547
+ const merged = mergeThreadPostsForList(lineagePosts, activePosts);
548
+ const limit = input.limit ?? 50;
549
+ const start = input.cursor ? Number.parseInt(input.cursor, 10) : 0;
550
+ const items = merged.slice(start, start + limit);
551
+ const nextCursor = start + limit < merged.length ? String(start + limit) : null;
552
+ return { items, nextCursor };
553
+ }
554
+ async appendPost(input) {
555
+ if (input.idempotencyKey) {
556
+ const existing = this.idempotency.get(input.idempotencyKey);
557
+ if (existing?.kind === "append") {
558
+ return { ok: true, post: existing.post, head: existing.head };
559
+ }
560
+ }
561
+ const thread = await this.requireThread(input.threadId);
562
+ const currentHead = thread.defaultHeadId ? this.heads.get(thread.defaultHeadId) : null;
563
+ if (input.expectedHeadPostVersionId !== undefined && (currentHead?.headPostVersionId ?? null) !== (input.expectedHeadPostVersionId ?? null)) {
564
+ if (!currentHead) {
565
+ throw new ChatConflictError("head_conflict", "expected head but thread is empty");
566
+ }
567
+ return { ok: false, reason: "head_conflict", currentHead };
568
+ }
569
+ const previousVersion = currentHead ? this.versions.get(currentHead.headPostVersionId) : null;
570
+ const prepared = prepareAppendPost({
571
+ ...input,
572
+ previousPostVersionId: previousVersion?.id ?? null,
573
+ previousLineageHash: previousVersion?.lineageHash ?? null
574
+ });
575
+ const nextIndex = (this.threadIndexes.get(input.threadId) ?? 0) + 1;
576
+ this.threadIndexes.set(input.threadId, nextIndex);
577
+ const version = {
578
+ ...prepared.message,
579
+ id: prepared.versionId,
580
+ postId: prepared.postId,
581
+ threadId: prepared.threadId,
582
+ parentVersionId: null,
583
+ previousPostVersionId: prepared.previousPostVersionId,
584
+ author: prepared.author,
585
+ contentHash: prepared.contentHash,
586
+ lineageHash: prepared.lineageHash,
587
+ mentions: prepared.mentions,
588
+ model: prepared.model,
589
+ usage: prepared.usage,
590
+ signature: input.signature,
591
+ createdAtMs: prepared.createdAtMs
592
+ };
593
+ this.posts.set(prepared.postId, {
594
+ threadId: input.threadId,
595
+ index: nextIndex,
596
+ status: "complete",
597
+ author: prepared.author,
598
+ streamRevision: 0,
599
+ completedVersionId: version.id,
600
+ createdAtMs: prepared.createdAtMs,
601
+ updatedAtMs: null,
602
+ deletedAtMs: null
603
+ });
604
+ this.versions.set(version.id, version);
605
+ const head = {
606
+ id: currentHead?.id ?? createId(),
607
+ threadId: input.threadId,
608
+ name: currentHead?.name ?? DEFAULT_HEAD_NAME,
609
+ headPostVersionId: version.id,
610
+ createdAtMs: currentHead?.createdAtMs ?? prepared.createdAtMs
611
+ };
612
+ this.heads.set(head.id, head);
613
+ this.threads.set(input.threadId, {
614
+ ...thread,
615
+ defaultHeadId: head.id
616
+ });
617
+ const post = postFromVersion(version, nextIndex);
618
+ if (input.idempotencyKey) {
619
+ this.idempotency.set(input.idempotencyKey, {
620
+ kind: "append",
621
+ post,
622
+ head
623
+ });
624
+ }
625
+ return { ok: true, post, head };
626
+ }
627
+ async editPost(input) {
628
+ if (input.idempotencyKey) {
629
+ const existing = this.idempotency.get(input.idempotencyKey);
630
+ if (existing?.kind === "edit") {
631
+ return { ok: true, post: existing.post, head: existing.head };
632
+ }
633
+ }
634
+ const parentVersion = await this.getPostVersion(input.parentVersionId);
635
+ if (!parentVersion) {
636
+ throw new ChatNotFoundError("post_version", input.parentVersionId);
637
+ }
638
+ const thread = await this.requireThread(parentVersion.threadId);
639
+ const currentHead = thread.defaultHeadId ? this.heads.get(thread.defaultHeadId) : null;
640
+ if (input.expectedHeadPostVersionId !== undefined && (currentHead?.headPostVersionId ?? null) !== (input.expectedHeadPostVersionId ?? null)) {
641
+ if (!currentHead) {
642
+ throw new ChatConflictError("head_conflict", "expected head but thread is empty");
643
+ }
644
+ return { ok: false, reason: "head_conflict", currentHead };
645
+ }
646
+ const previousInChain = parentVersion.previousPostVersionId ? this.versions.get(parentVersion.previousPostVersionId) ?? null : null;
647
+ const prepared = prepareEditPost({
648
+ ...input,
649
+ threadId: parentVersion.threadId,
650
+ previousPostVersionId: parentVersion.previousPostVersionId ?? null,
651
+ previousLineageHash: previousInChain?.lineageHash ?? null
652
+ });
653
+ const record = this.posts.get(input.postId);
654
+ if (!record)
655
+ throw new ChatNotFoundError("post", input.postId);
656
+ const version = {
657
+ ...prepared.message,
658
+ id: prepared.versionId,
659
+ postId: prepared.postId,
660
+ threadId: prepared.threadId,
661
+ parentVersionId: prepared.parentVersionId,
662
+ previousPostVersionId: prepared.previousPostVersionId,
663
+ author: prepared.author,
664
+ contentHash: prepared.contentHash,
665
+ lineageHash: prepared.lineageHash,
666
+ mentions: prepared.mentions,
667
+ model: prepared.model,
668
+ usage: prepared.usage,
669
+ createdAtMs: prepared.createdAtMs
670
+ };
671
+ this.versions.set(version.id, version);
672
+ let head;
673
+ if (currentHead?.headPostVersionId === input.parentVersionId) {
674
+ head = {
675
+ ...currentHead,
676
+ headPostVersionId: version.id
677
+ };
678
+ this.heads.set(head.id, head);
679
+ } else if (currentHead) {
680
+ head = currentHead;
681
+ } else {
682
+ head = {
683
+ id: createId(),
684
+ threadId: parentVersion.threadId,
685
+ name: DEFAULT_HEAD_NAME,
686
+ headPostVersionId: version.id,
687
+ createdAtMs: prepared.createdAtMs
688
+ };
689
+ this.heads.set(head.id, head);
690
+ this.threads.set(parentVersion.threadId, {
691
+ ...thread,
692
+ defaultHeadId: head.id
693
+ });
694
+ }
695
+ const post = postFromVersion(version, record.index, record.deletedAtMs);
696
+ if (input.idempotencyKey) {
697
+ this.idempotency.set(input.idempotencyKey, {
698
+ kind: "edit",
699
+ post,
700
+ head
701
+ });
702
+ }
703
+ return { ok: true, post, head };
704
+ }
705
+ async deletePost(input) {
706
+ const post = await this.requirePost(input.postId);
707
+ const record = this.posts.get(input.postId);
708
+ if (!record)
709
+ throw new ChatNotFoundError("post", input.postId);
710
+ const deletedAtMs = input.deletedAtMs ?? Date.now();
711
+ this.posts.set(input.postId, { ...record, deletedAtMs });
712
+ return { ...post, deletedAtMs };
713
+ }
714
+ async addChannelMember(input) {
715
+ await this.requireChannel(input.channelId);
716
+ const members = this.channelMembers.get(input.channelId) ?? new Map;
717
+ members.set(scopeKey(input.scope), {
718
+ role: input.role,
719
+ createdAtMs: Date.now()
720
+ });
721
+ this.channelMembers.set(input.channelId, members);
722
+ const previousAclEventId = this.aclEvents.length > 0 ? this.aclEvents[this.aclEvents.length - 1]?.id : null;
723
+ const event = {
724
+ id: createId(),
725
+ type: "channel.member.added",
726
+ channelId: input.channelId,
727
+ scope: input.scope,
728
+ role: input.role,
729
+ actor: input.actor,
730
+ previousAclEventId,
731
+ contentHash: buildAclEventContentHash({
732
+ type: "channel.member.added",
733
+ targetType: "channel",
734
+ targetId: input.channelId,
735
+ scope: input.scope,
736
+ role: input.role,
737
+ actor: input.actor,
738
+ previousAclEventId
739
+ }),
740
+ signature: input.signature,
741
+ createdAtMs: Date.now()
742
+ };
743
+ this.aclEvents.push(event);
744
+ return event;
745
+ }
746
+ async removeChannelMember(input) {
747
+ await this.requireChannel(input.channelId);
748
+ const members = this.channelMembers.get(input.channelId);
749
+ members?.delete(scopeKey(input.scope));
750
+ const previousAclEventId = this.aclEvents.length > 0 ? this.aclEvents[this.aclEvents.length - 1]?.id : null;
751
+ const event = {
752
+ id: createId(),
753
+ type: "channel.member.removed",
754
+ channelId: input.channelId,
755
+ scope: input.scope,
756
+ actor: input.actor,
757
+ previousAclEventId,
758
+ contentHash: buildAclEventContentHash({
759
+ type: "channel.member.removed",
760
+ targetType: "channel",
761
+ targetId: input.channelId,
762
+ scope: input.scope,
763
+ actor: input.actor,
764
+ previousAclEventId
765
+ }),
766
+ signature: input.signature,
767
+ createdAtMs: Date.now()
768
+ };
769
+ this.aclEvents.push(event);
770
+ return event;
771
+ }
772
+ async addThreadParticipant(input) {
773
+ await this.requireThread(input.threadId);
774
+ const participants = this.threadParticipants.get(input.threadId) ?? new Map;
775
+ participants.set(scopeKey(input.scope), {
776
+ role: input.role,
777
+ createdAtMs: Date.now()
778
+ });
779
+ this.threadParticipants.set(input.threadId, participants);
780
+ const previousAclEventId = this.aclEvents.length > 0 ? this.aclEvents[this.aclEvents.length - 1]?.id : null;
781
+ const event = {
782
+ id: createId(),
783
+ type: "thread.participant.added",
784
+ threadId: input.threadId,
785
+ scope: input.scope,
786
+ role: input.role,
787
+ actor: input.actor,
788
+ previousAclEventId,
789
+ contentHash: buildAclEventContentHash({
790
+ type: "thread.participant.added",
791
+ targetType: "thread",
792
+ targetId: input.threadId,
793
+ scope: input.scope,
794
+ role: input.role,
795
+ actor: input.actor,
796
+ previousAclEventId
797
+ }),
798
+ signature: input.signature,
799
+ createdAtMs: Date.now()
800
+ };
801
+ this.aclEvents.push(event);
802
+ return event;
803
+ }
804
+ async removeThreadParticipant(input) {
805
+ await this.requireThread(input.threadId);
806
+ const participants = this.threadParticipants.get(input.threadId);
807
+ participants?.delete(scopeKey(input.scope));
808
+ const previousAclEventId = this.aclEvents.length > 0 ? this.aclEvents[this.aclEvents.length - 1]?.id : null;
809
+ const event = {
810
+ id: createId(),
811
+ type: "thread.participant.removed",
812
+ threadId: input.threadId,
813
+ scope: input.scope,
814
+ actor: input.actor,
815
+ previousAclEventId,
816
+ contentHash: buildAclEventContentHash({
817
+ type: "thread.participant.removed",
818
+ targetType: "thread",
819
+ targetId: input.threadId,
820
+ scope: input.scope,
821
+ actor: input.actor,
822
+ previousAclEventId
823
+ }),
824
+ signature: input.signature,
825
+ createdAtMs: Date.now()
826
+ };
827
+ this.aclEvents.push(event);
828
+ return event;
829
+ }
830
+ async createThreadHead(input) {
831
+ await this.requireThread(input.threadId);
832
+ const version = await this.getPostVersion(input.headPostVersionId);
833
+ if (!version) {
834
+ throw new ChatNotFoundError("post_version", input.headPostVersionId);
835
+ }
836
+ const head = {
837
+ id: createId(),
838
+ threadId: input.threadId,
839
+ name: input.name,
840
+ headPostVersionId: input.headPostVersionId,
841
+ createdAtMs: Date.now()
842
+ };
843
+ this.heads.set(head.id, head);
844
+ return head;
845
+ }
846
+ async listChannelMembers(channelId) {
847
+ const members = this.channelMembers.get(channelId);
848
+ if (!members)
849
+ return [];
850
+ return [...members.keys()].map(scopeRefFromKey);
851
+ }
852
+ async listThreadParticipants(threadId) {
853
+ const participants = this.threadParticipants.get(threadId);
854
+ if (!participants)
855
+ return [];
856
+ return [...participants.keys()].map(scopeRefFromKey);
857
+ }
858
+ async listAclEvents(input) {
859
+ let events2 = [...this.aclEvents];
860
+ if (input.channelId) {
861
+ events2 = events2.filter((event) => event.type.startsWith("channel.") && ("channelId" in event) && event.channelId === input.channelId);
862
+ }
863
+ if (input.threadId) {
864
+ events2 = events2.filter((event) => event.type.startsWith("thread.") && ("threadId" in event) && event.threadId === input.threadId);
865
+ }
866
+ return events2.slice(0, input.limit ?? events2.length);
867
+ }
868
+ async listPostStreamEvents(postId) {
869
+ return this.streamEvents.filter((event) => event.postId === postId).sort((a, b) => a.revision - b.revision);
870
+ }
871
+ async startStreamedPost(input) {
872
+ if (input.idempotencyKey) {
873
+ const existing = this.streamIdempotency.get(input.idempotencyKey);
874
+ if (existing) {
875
+ const post2 = await this.getPost(existing.postId);
876
+ if (post2?.status !== "streaming") {
877
+ throw new ChatConflictError("idempotency_mismatch", "idempotency key reused");
878
+ }
879
+ return { post: post2, revision: existing.revision };
880
+ }
881
+ }
882
+ await this.requireThread(input.threadId);
883
+ const postId = input.message.id || createId();
884
+ const nextIndex = (this.threadIndexes.get(input.threadId) ?? 0) + 1;
885
+ this.threadIndexes.set(input.threadId, nextIndex);
886
+ const createdAtMs = Date.now();
887
+ const message = { ...input.message, id: postId };
888
+ const revision = 1;
889
+ const event = {
890
+ id: createId(),
891
+ postId,
892
+ threadId: input.threadId,
893
+ eventType: "stream.started",
894
+ revision,
895
+ message,
896
+ mentions: input.mentions,
897
+ model: input.model,
898
+ usage: input.usage,
899
+ idempotencyKey: input.idempotencyKey,
900
+ createdAtMs
901
+ };
902
+ this.streamEvents.push(event);
903
+ this.posts.set(postId, {
904
+ threadId: input.threadId,
905
+ index: nextIndex,
906
+ status: "streaming",
907
+ author: input.author,
908
+ message,
909
+ mentions: input.mentions,
910
+ model: input.model,
911
+ usage: input.usage,
912
+ streamRevision: revision,
913
+ createdAtMs,
914
+ updatedAtMs: null,
915
+ deletedAtMs: null
916
+ });
917
+ if (input.idempotencyKey) {
918
+ this.streamIdempotency.set(input.idempotencyKey, {
919
+ postId,
920
+ revision,
921
+ eventId: event.id
922
+ });
923
+ }
924
+ const post = streamingPostFromCache({
925
+ postId,
926
+ threadId: input.threadId,
927
+ author: input.author,
928
+ message,
929
+ mentions: input.mentions,
930
+ model: input.model,
931
+ usage: input.usage,
932
+ index: nextIndex,
933
+ streamRevision: revision,
934
+ createdAtMs
935
+ });
936
+ return { post, revision };
937
+ }
938
+ async applyPostDelta(input) {
939
+ if (input.idempotencyKey) {
940
+ const existing = this.streamIdempotency.get(input.idempotencyKey);
941
+ if (existing && existing.postId === input.postId) {
942
+ const post2 = await this.getPost(input.postId);
943
+ if (post2?.status !== "streaming") {
944
+ throw new ChatConflictError("idempotency_mismatch", "idempotency key reused");
945
+ }
946
+ return { post: post2, revision: existing.revision };
947
+ }
948
+ }
949
+ const record = this.posts.get(input.postId);
950
+ if (!record)
951
+ throw new ChatNotFoundError("post", input.postId);
952
+ if (record.status !== "streaming") {
953
+ throw new ChatValidationError(`post ${input.postId} is not streaming`);
954
+ }
955
+ if (input.expectedRevision !== undefined && record.streamRevision !== input.expectedRevision) {
956
+ throw new ChatConflictError("stream_revision_conflict", `expected revision ${input.expectedRevision}, current ${record.streamRevision}`);
957
+ }
958
+ const revision = record.streamRevision + 1;
959
+ const updatedAtMs = Date.now();
960
+ const message = { ...input.message, id: input.postId };
961
+ const event = {
962
+ id: createId(),
963
+ postId: input.postId,
964
+ threadId: record.threadId,
965
+ eventType: "stream.delta",
966
+ revision,
967
+ message,
968
+ delta: input.delta,
969
+ mentions: input.mentions ?? record.mentions,
970
+ model: input.model ?? record.model,
971
+ usage: input.usage ?? record.usage,
972
+ idempotencyKey: input.idempotencyKey,
973
+ createdAtMs: updatedAtMs
974
+ };
975
+ this.streamEvents.push(event);
976
+ this.posts.set(input.postId, {
977
+ ...record,
978
+ message,
979
+ mentions: input.mentions ?? record.mentions,
980
+ model: input.model ?? record.model,
981
+ usage: input.usage ?? record.usage,
982
+ streamRevision: revision,
983
+ updatedAtMs
984
+ });
985
+ if (input.idempotencyKey) {
986
+ this.streamIdempotency.set(input.idempotencyKey, {
987
+ postId: input.postId,
988
+ revision,
989
+ eventId: event.id
990
+ });
991
+ }
992
+ const post = streamingPostFromCache({
993
+ postId: input.postId,
994
+ threadId: record.threadId,
995
+ author: record.author,
996
+ message,
997
+ mentions: input.mentions ?? record.mentions,
998
+ model: input.model ?? record.model,
999
+ usage: input.usage ?? record.usage,
1000
+ index: record.index,
1001
+ streamRevision: revision,
1002
+ createdAtMs: record.createdAtMs,
1003
+ updatedAtMs,
1004
+ deletedAtMs: record.deletedAtMs
1005
+ });
1006
+ return { post, revision };
1007
+ }
1008
+ async completeStreamedPost(input) {
1009
+ const record = this.posts.get(input.postId);
1010
+ if (!record)
1011
+ throw new ChatNotFoundError("post", input.postId);
1012
+ if (record.status !== "streaming") {
1013
+ throw new ChatValidationError(`post ${input.postId} is not streaming`);
1014
+ }
1015
+ if (input.expectedRevision !== undefined && record.streamRevision !== input.expectedRevision) {
1016
+ throw new ChatConflictError("stream_revision_conflict", `expected revision ${input.expectedRevision}, current ${record.streamRevision}`);
1017
+ }
1018
+ if (!record.message) {
1019
+ throw new ChatValidationError(`post ${input.postId} has no streamed message`);
1020
+ }
1021
+ const thread = await this.requireThread(record.threadId);
1022
+ const currentHead = thread.defaultHeadId ? this.heads.get(thread.defaultHeadId) : null;
1023
+ if (input.expectedHeadPostVersionId !== undefined && (currentHead?.headPostVersionId ?? null) !== (input.expectedHeadPostVersionId ?? null)) {
1024
+ if (!currentHead) {
1025
+ throw new ChatConflictError("head_conflict", "expected head but thread is empty");
1026
+ }
1027
+ return { ok: false, reason: "head_conflict", currentHead };
1028
+ }
1029
+ const previousVersion = currentHead ? this.versions.get(currentHead.headPostVersionId) : null;
1030
+ const prepared = prepareAppendPost({
1031
+ threadId: record.threadId,
1032
+ author: record.author,
1033
+ message: record.message,
1034
+ mentions: record.mentions,
1035
+ model: record.model,
1036
+ usage: record.usage,
1037
+ previousPostVersionId: previousVersion?.id ?? null,
1038
+ previousLineageHash: previousVersion?.lineageHash ?? null
1039
+ });
1040
+ const version = {
1041
+ ...prepared.message,
1042
+ id: prepared.versionId,
1043
+ postId: prepared.postId,
1044
+ threadId: prepared.threadId,
1045
+ parentVersionId: null,
1046
+ previousPostVersionId: prepared.previousPostVersionId,
1047
+ author: prepared.author,
1048
+ contentHash: prepared.contentHash,
1049
+ lineageHash: prepared.lineageHash,
1050
+ mentions: prepared.mentions,
1051
+ model: prepared.model,
1052
+ usage: prepared.usage,
1053
+ createdAtMs: prepared.createdAtMs
1054
+ };
1055
+ this.versions.set(version.id, version);
1056
+ const head = {
1057
+ id: currentHead?.id ?? createId(),
1058
+ threadId: record.threadId,
1059
+ name: currentHead?.name ?? DEFAULT_HEAD_NAME,
1060
+ headPostVersionId: version.id,
1061
+ createdAtMs: currentHead?.createdAtMs ?? prepared.createdAtMs
1062
+ };
1063
+ this.heads.set(head.id, head);
1064
+ this.threads.set(record.threadId, { ...thread, defaultHeadId: head.id });
1065
+ const completedRevision = record.streamRevision + 1;
1066
+ const completedAtMs = Date.now();
1067
+ this.streamEvents.push({
1068
+ id: createId(),
1069
+ postId: input.postId,
1070
+ threadId: record.threadId,
1071
+ eventType: "stream.completed",
1072
+ revision: completedRevision,
1073
+ message: record.message,
1074
+ mentions: record.mentions,
1075
+ model: record.model,
1076
+ usage: record.usage,
1077
+ createdAtMs: completedAtMs
1078
+ });
1079
+ this.posts.set(input.postId, {
1080
+ ...record,
1081
+ status: "complete",
1082
+ streamRevision: completedRevision,
1083
+ completedVersionId: version.id,
1084
+ updatedAtMs: completedAtMs,
1085
+ message: undefined,
1086
+ mentions: undefined
1087
+ });
1088
+ const post = postFromVersion(version, record.index, record.deletedAtMs);
1089
+ return { ok: true, post, head };
1090
+ }
1091
+ async abortStreamedPost(input) {
1092
+ const record = this.posts.get(input.postId);
1093
+ if (!record)
1094
+ throw new ChatNotFoundError("post", input.postId);
1095
+ if (record.status !== "streaming") {
1096
+ throw new ChatValidationError(`post ${input.postId} is not streaming`);
1097
+ }
1098
+ const deletedAtMs = input.deletedAtMs ?? Date.now();
1099
+ const revision = record.streamRevision + 1;
1100
+ this.streamEvents.push({
1101
+ id: createId(),
1102
+ postId: input.postId,
1103
+ threadId: record.threadId,
1104
+ eventType: "stream.aborted",
1105
+ revision,
1106
+ message: record.message,
1107
+ mentions: record.mentions,
1108
+ model: record.model,
1109
+ usage: record.usage,
1110
+ createdAtMs: deletedAtMs
1111
+ });
1112
+ this.posts.set(input.postId, {
1113
+ ...record,
1114
+ status: "aborted",
1115
+ streamRevision: revision,
1116
+ deletedAtMs,
1117
+ updatedAtMs: deletedAtMs
1118
+ });
1119
+ const post = await this.getPost(input.postId);
1120
+ if (post?.status !== "aborted") {
1121
+ throw new ChatNotFoundError("post", input.postId);
1122
+ }
1123
+ return { post };
1124
+ }
1125
+ async rebuildStreamedPostCache(postId) {
1126
+ const record = this.posts.get(postId);
1127
+ if (!record)
1128
+ throw new ChatNotFoundError("post", postId);
1129
+ if (record.status !== "streaming") {
1130
+ throw new ChatValidationError(`post ${postId} is not streaming`);
1131
+ }
1132
+ const events2 = await this.listPostStreamEvents(postId);
1133
+ const rebuilt = rebuildStreamCacheFromEvents(events2);
1134
+ this.posts.set(postId, {
1135
+ ...record,
1136
+ message: rebuilt.message,
1137
+ mentions: rebuilt.mentions,
1138
+ streamRevision: rebuilt.revision,
1139
+ updatedAtMs: Date.now()
1140
+ });
1141
+ return streamingPostFromCache({
1142
+ postId,
1143
+ threadId: record.threadId,
1144
+ author: record.author,
1145
+ message: rebuilt.message,
1146
+ mentions: rebuilt.mentions,
1147
+ index: record.index,
1148
+ streamRevision: rebuilt.revision,
1149
+ createdAtMs: record.createdAtMs,
1150
+ updatedAtMs: Date.now(),
1151
+ deletedAtMs: record.deletedAtMs
1152
+ });
1153
+ }
1154
+ async setPostVersionSignature(versionId, signature) {
1155
+ const version = this.versions.get(versionId);
1156
+ if (!version)
1157
+ throw new ChatNotFoundError("post_version", versionId);
1158
+ this.versions.set(versionId, { ...version, signature });
1159
+ }
1160
+ }
1161
+ function createMemoryChatPersistence() {
1162
+ return new MemoryChatPersistence;
1163
+ }
1164
+ // src/persistence/core/persistence/signed-persistence.ts
1165
+ function scopesEqual(a, b) {
1166
+ return a.type === b.type && a.id === b.id;
1167
+ }
1168
+ function preparedToSignable(prepared) {
1169
+ return {
1170
+ postId: prepared.postId,
1171
+ versionId: prepared.versionId,
1172
+ threadId: prepared.threadId,
1173
+ author: prepared.author,
1174
+ role: prepared.message.role,
1175
+ parts: prepared.message.parts,
1176
+ metadata: prepared.message.metadata,
1177
+ mentions: prepared.mentions,
1178
+ model: prepared.model,
1179
+ usage: prepared.usage,
1180
+ parentVersionId: prepared.parentVersionId ?? null,
1181
+ previousPostVersionId: prepared.previousPostVersionId,
1182
+ contentHash: prepared.contentHash,
1183
+ lineageHash: prepared.lineageHash
1184
+ };
1185
+ }
1186
+ async function prepareAppendForSigning(persistence, input) {
1187
+ const tip = await persistence.getThreadTip(input.threadId);
1188
+ return prepareAppendPost({
1189
+ ...input,
1190
+ previousPostVersionId: tip?.id ?? null,
1191
+ previousLineageHash: tip?.lineageHash ?? null
1192
+ });
1193
+ }
1194
+ async function signPreparedAppendPost(signer, author, prepared) {
1195
+ const payload = canonicalSignedPostVersionPayload(preparedToSignable(prepared));
1196
+ return signer.sign(signedPayloadBytes(payload), author);
1197
+ }
1198
+ async function assertValidAppendSignature(prepared, author, envelope, verifier) {
1199
+ if (!scopesEqual(envelope.signer, author)) {
1200
+ throw new ChatValidationError("appendPost signature signer must match author");
1201
+ }
1202
+ const payload = canonicalSignedPostVersionPayload(preparedToSignable(prepared));
1203
+ const ok = await verifier.verify(signedPayloadBytes(payload), envelope);
1204
+ if (!ok) {
1205
+ throw new ChatValidationError("invalid post signature");
1206
+ }
1207
+ }
1208
+ function signableFromCommittedPost(post) {
1209
+ return {
1210
+ postId: post.id,
1211
+ versionId: post.versionId,
1212
+ threadId: post.threadId,
1213
+ author: post.author,
1214
+ role: post.role,
1215
+ parts: post.parts,
1216
+ metadata: post.metadata,
1217
+ mentions: post.mentions,
1218
+ model: post.model,
1219
+ usage: post.usage,
1220
+ parentVersionId: post.previousVersionId ?? null,
1221
+ previousPostVersionId: post.previousPostVersionId ?? null,
1222
+ contentHash: post.contentHash,
1223
+ lineageHash: post.lineageHash
1224
+ };
1225
+ }
1226
+ function withSignedChatPersistence(persistence, options) {
1227
+ return new Proxy(persistence, {
1228
+ get(target, prop, receiver) {
1229
+ if (prop === "appendPost") {
1230
+ return async (input) => {
1231
+ if (!input.signature) {
1232
+ throw new ChatValidationError("appendPost requires a signed envelope");
1233
+ }
1234
+ const prepared = await prepareAppendForSigning(target, input);
1235
+ await assertValidAppendSignature(prepared, input.author, input.signature, options.verifier);
1236
+ return target.appendPost({
1237
+ ...input,
1238
+ message: prepared.message,
1239
+ versionId: prepared.versionId,
1240
+ createdAtMs: prepared.createdAtMs,
1241
+ signature: input.signature
1242
+ });
1243
+ };
1244
+ }
1245
+ if (prop === "completeStreamedPost") {
1246
+ return async (input) => {
1247
+ const result = await target.completeStreamedPost(input);
1248
+ if (!result.ok)
1249
+ return result;
1250
+ const signable = signableFromCommittedPost(result.post);
1251
+ const payload = canonicalSignedPostVersionPayload(signable);
1252
+ const envelope = await options.signer.sign(signedPayloadBytes(payload), result.post.author);
1253
+ await target.setPostVersionSignature(result.post.versionId, envelope);
1254
+ return {
1255
+ ok: true,
1256
+ post: { ...result.post, signature: envelope },
1257
+ head: result.head
1258
+ };
1259
+ };
1260
+ }
1261
+ const value = Reflect.get(target, prop, receiver);
1262
+ return typeof value === "function" ? value.bind(target) : value;
1263
+ }
1264
+ });
1265
+ }
1266
+ export {
1267
+ withSignedChatPersistence,
1268
+ signPreparedAppendPost,
1269
+ prepareEditPost,
1270
+ prepareAppendPost,
1271
+ prepareAppendForSigning,
1272
+ createMemoryChatPersistence,
1273
+ buildAclEventContentHash,
1274
+ MemoryChatPersistence,
1275
+ BaseChatPersistence
1276
+ };