@ours.network/cowork 1.1.4 → 1.2.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 +3 -0
- package/dist/cli.js +220 -8
- package/dist/daemon.js +1016 -156
- package/dist/web/assets/app.js +9 -9
- package/docs/05-room-workflow.md +37 -1
- package/docs/07-messaging-history.md +9 -3
- package/docs/08-backup-restore.md +1 -1
- package/docs/10-limitations.md +1 -0
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -11656,6 +11656,7 @@ var init_command_names = __esm({
|
|
|
11656
11656
|
"room.role.rest.remove"
|
|
11657
11657
|
];
|
|
11658
11658
|
RUNTIME_COMMAND_NAMES = [
|
|
11659
|
+
"start_thread",
|
|
11659
11660
|
"list-members",
|
|
11660
11661
|
"remove-member",
|
|
11661
11662
|
...SHARED_ROOM_COMMANDS
|
|
@@ -11663,6 +11664,93 @@ var init_command_names = __esm({
|
|
|
11663
11664
|
}
|
|
11664
11665
|
});
|
|
11665
11666
|
|
|
11667
|
+
// src/thread-contracts.ts
|
|
11668
|
+
var ParticipantIdSchema, ContainerIdSchema, TopicTextSchema, InputTopicSchema, StoredTopicSchema, IdempotencyKeySchema, ThreadMemberSchema, ThreadRootSchema, ThreadScopeSchema, StartThreadInputSchema, ThreadFailure;
|
|
11669
|
+
var init_thread_contracts = __esm({
|
|
11670
|
+
"src/thread-contracts.ts"() {
|
|
11671
|
+
"use strict";
|
|
11672
|
+
init_zod();
|
|
11673
|
+
ParticipantIdSchema = external_exports.string().regex(
|
|
11674
|
+
/^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
|
|
11675
|
+
"must be a 26-character lowercase Crockford ULID"
|
|
11676
|
+
);
|
|
11677
|
+
ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
|
|
11678
|
+
TopicTextSchema = external_exports.string().refine(
|
|
11679
|
+
(value) => Array.from(value).length >= 1 && Array.from(value).length <= 120 && value.trim().length > 0 && !/[\p{Cc}\p{Cf}]/u.test(value),
|
|
11680
|
+
"topic must contain 1-120 Unicode characters without control or format characters"
|
|
11681
|
+
);
|
|
11682
|
+
InputTopicSchema = TopicTextSchema.transform((value) => value.trim());
|
|
11683
|
+
StoredTopicSchema = TopicTextSchema.refine(
|
|
11684
|
+
(value) => value === value.trim(),
|
|
11685
|
+
"stored topic must already be trimmed"
|
|
11686
|
+
);
|
|
11687
|
+
IdempotencyKeySchema = external_exports.string().regex(
|
|
11688
|
+
/^[A-Za-z0-9._:-]{1,128}$/,
|
|
11689
|
+
"must contain 1-128 portable idempotency-key characters"
|
|
11690
|
+
);
|
|
11691
|
+
ThreadMemberSchema = external_exports.object({
|
|
11692
|
+
participant_id: ParticipantIdSchema,
|
|
11693
|
+
identity: ContainerIdSchema
|
|
11694
|
+
}).strict();
|
|
11695
|
+
ThreadRootSchema = external_exports.object({
|
|
11696
|
+
schema_version: external_exports.literal(1),
|
|
11697
|
+
thread_id: ParticipantIdSchema,
|
|
11698
|
+
topic: StoredTopicSchema,
|
|
11699
|
+
creator_participant_id: ParticipantIdSchema,
|
|
11700
|
+
members: external_exports.array(ThreadMemberSchema).min(1),
|
|
11701
|
+
idempotency_key: IdempotencyKeySchema,
|
|
11702
|
+
fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase SHA-256 digest")
|
|
11703
|
+
}).strict().superRefine((root, context) => {
|
|
11704
|
+
const participantIds = /* @__PURE__ */ new Set();
|
|
11705
|
+
const identities = /* @__PURE__ */ new Set();
|
|
11706
|
+
for (const [index, member] of root.members.entries()) {
|
|
11707
|
+
if (participantIds.has(member.participant_id)) {
|
|
11708
|
+
context.addIssue({
|
|
11709
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11710
|
+
path: ["members", index, "participant_id"],
|
|
11711
|
+
message: "thread member participant IDs must be unique"
|
|
11712
|
+
});
|
|
11713
|
+
}
|
|
11714
|
+
participantIds.add(member.participant_id);
|
|
11715
|
+
if (identities.has(member.identity)) {
|
|
11716
|
+
context.addIssue({
|
|
11717
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11718
|
+
path: ["members", index, "identity"],
|
|
11719
|
+
message: "thread member identities must be unique"
|
|
11720
|
+
});
|
|
11721
|
+
}
|
|
11722
|
+
identities.add(member.identity);
|
|
11723
|
+
}
|
|
11724
|
+
if (!participantIds.has(root.creator_participant_id)) {
|
|
11725
|
+
context.addIssue({
|
|
11726
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11727
|
+
path: ["creator_participant_id"],
|
|
11728
|
+
message: "thread creator must be a member"
|
|
11729
|
+
});
|
|
11730
|
+
}
|
|
11731
|
+
});
|
|
11732
|
+
ThreadScopeSchema = external_exports.object({
|
|
11733
|
+
thread_id: ParticipantIdSchema,
|
|
11734
|
+
parent_key: external_exports.string().regex(
|
|
11735
|
+
/^(?:message|file):[0-7][0-9a-hjkmnp-tv-z]{25}$/,
|
|
11736
|
+
"must identify an immediate message or file parent"
|
|
11737
|
+
).optional()
|
|
11738
|
+
}).strict();
|
|
11739
|
+
StartThreadInputSchema = external_exports.object({
|
|
11740
|
+
topic: InputTopicSchema,
|
|
11741
|
+
participant_ids: external_exports.array(ParticipantIdSchema).min(1),
|
|
11742
|
+
idempotency_key: IdempotencyKeySchema
|
|
11743
|
+
}).strict();
|
|
11744
|
+
ThreadFailure = class extends Error {
|
|
11745
|
+
constructor(code) {
|
|
11746
|
+
super(code);
|
|
11747
|
+
this.code = code;
|
|
11748
|
+
this.name = "ThreadFailure";
|
|
11749
|
+
}
|
|
11750
|
+
};
|
|
11751
|
+
}
|
|
11752
|
+
});
|
|
11753
|
+
|
|
11666
11754
|
// src/contracts.ts
|
|
11667
11755
|
import { createHash } from "node:crypto";
|
|
11668
11756
|
function utf8Bounded(label, maximumBytes) {
|
|
@@ -11804,6 +11892,18 @@ function refineRelaySubject(record, context) {
|
|
|
11804
11892
|
});
|
|
11805
11893
|
}
|
|
11806
11894
|
}
|
|
11895
|
+
function refineIntakeRejection(record, context) {
|
|
11896
|
+
if (record.kind !== "intake_rejection") return;
|
|
11897
|
+
const hasMessage = record.source_msg_id !== void 0;
|
|
11898
|
+
const hasFile = record.source_file_id !== void 0;
|
|
11899
|
+
if (hasMessage === hasFile || (record.source_kind === "message" ? !hasMessage : !hasFile)) {
|
|
11900
|
+
context.addIssue({
|
|
11901
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11902
|
+
path: ["source_kind"],
|
|
11903
|
+
message: "intake rejections require exactly one numeric source field matching source_kind"
|
|
11904
|
+
});
|
|
11905
|
+
}
|
|
11906
|
+
}
|
|
11807
11907
|
function refineFileRecord(record, context) {
|
|
11808
11908
|
if (record.kind !== "file" || record.data_base64 === void 0) return;
|
|
11809
11909
|
const bytes = Buffer.from(record.data_base64, "base64");
|
|
@@ -11854,13 +11954,110 @@ function refineMessageCategory(message, context) {
|
|
|
11854
11954
|
}
|
|
11855
11955
|
}
|
|
11856
11956
|
}
|
|
11857
|
-
|
|
11957
|
+
function refineMessageThread(message, context) {
|
|
11958
|
+
if (message.thread_root !== void 0) {
|
|
11959
|
+
if (message.scope === void 0) {
|
|
11960
|
+
context.addIssue({
|
|
11961
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11962
|
+
path: ["scope"],
|
|
11963
|
+
message: "thread root messages require scope"
|
|
11964
|
+
});
|
|
11965
|
+
return;
|
|
11966
|
+
}
|
|
11967
|
+
if (message.scope.thread_id !== message.message_id || message.thread_root.thread_id !== message.message_id) {
|
|
11968
|
+
context.addIssue({
|
|
11969
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11970
|
+
path: ["thread_root", "thread_id"],
|
|
11971
|
+
message: "thread root thread_id and scope thread_id must equal message_id"
|
|
11972
|
+
});
|
|
11973
|
+
}
|
|
11974
|
+
if (message.scope.parent_key !== void 0) {
|
|
11975
|
+
context.addIssue({
|
|
11976
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11977
|
+
path: ["scope", "parent_key"],
|
|
11978
|
+
message: "parent_key is forbidden on thread root messages"
|
|
11979
|
+
});
|
|
11980
|
+
}
|
|
11981
|
+
if (message.category !== "chat") {
|
|
11982
|
+
context.addIssue({
|
|
11983
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11984
|
+
path: ["category"],
|
|
11985
|
+
message: "thread root messages must be chat messages"
|
|
11986
|
+
});
|
|
11987
|
+
}
|
|
11988
|
+
if (message.text !== `Thread: ${message.thread_root.topic}`) {
|
|
11989
|
+
context.addIssue({
|
|
11990
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11991
|
+
path: ["text"],
|
|
11992
|
+
message: "thread root message text must identify its topic"
|
|
11993
|
+
});
|
|
11994
|
+
}
|
|
11995
|
+
const creator = message.thread_root.members.find(
|
|
11996
|
+
(member) => member.participant_id === message.thread_root?.creator_participant_id
|
|
11997
|
+
);
|
|
11998
|
+
if (creator?.identity !== message.author.identity) {
|
|
11999
|
+
context.addIssue({
|
|
12000
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12001
|
+
path: ["thread_root", "creator_participant_id"],
|
|
12002
|
+
message: "thread root creator must match the message author"
|
|
12003
|
+
});
|
|
12004
|
+
}
|
|
12005
|
+
if (message.author_alias !== void 0 && message.author_alias.participant_id !== message.thread_root.creator_participant_id) {
|
|
12006
|
+
context.addIssue({
|
|
12007
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12008
|
+
path: ["author_alias", "participant_id"],
|
|
12009
|
+
message: "thread root author alias must identify the creator"
|
|
12010
|
+
});
|
|
12011
|
+
}
|
|
12012
|
+
for (const field of ["source_msg_id", "source_wire_id", "source_reply_to"]) {
|
|
12013
|
+
if (message[field] !== void 0) {
|
|
12014
|
+
context.addIssue({
|
|
12015
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12016
|
+
path: [field],
|
|
12017
|
+
message: `${field} is forbidden on thread root messages`
|
|
12018
|
+
});
|
|
12019
|
+
}
|
|
12020
|
+
}
|
|
12021
|
+
return;
|
|
12022
|
+
}
|
|
12023
|
+
if (message.scope === void 0) return;
|
|
12024
|
+
if (message.scope.parent_key === void 0) {
|
|
12025
|
+
context.addIssue({
|
|
12026
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12027
|
+
path: ["scope", "parent_key"],
|
|
12028
|
+
message: "thread descendants require an immediate parent_key"
|
|
12029
|
+
});
|
|
12030
|
+
}
|
|
12031
|
+
if (message.scope.thread_id === message.message_id) {
|
|
12032
|
+
context.addIssue({
|
|
12033
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12034
|
+
path: ["scope", "thread_id"],
|
|
12035
|
+
message: "thread descendant thread_id must identify a distinct root message"
|
|
12036
|
+
});
|
|
12037
|
+
}
|
|
12038
|
+
if (message.source_reply_to === void 0) {
|
|
12039
|
+
context.addIssue({
|
|
12040
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12041
|
+
path: ["source_reply_to"],
|
|
12042
|
+
message: "thread descendants require source_reply_to"
|
|
12043
|
+
});
|
|
12044
|
+
}
|
|
12045
|
+
if (message.category !== "chat") {
|
|
12046
|
+
context.addIssue({
|
|
12047
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12048
|
+
path: ["category"],
|
|
12049
|
+
message: "thread descendants must be chat messages"
|
|
12050
|
+
});
|
|
12051
|
+
}
|
|
12052
|
+
}
|
|
12053
|
+
var MAX_TEXT_BYTES, MAX_FILE_BYTES, MAX_HISTORY_PAGE_BYTES, MAX_MANAGEMENT_RESPONSE_BYTES, MAX_EXTERNAL_INVITE_BYTES, MAX_FILE_NAME_BYTES, MAX_MIME_BYTES, MAX_ROLE_BYTES, MAX_ROOM_NAME_CHARACTERS, MAX_ROOM_IDENTITY_NAME_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, ContainerIdSchema2, RuntimeCommandNameSchema, RuntimeCommandGrantSchema, Rfc3339Schema, RoomNameSchema, ROOM_IDENTITY_PREFIX, MAX_ROOM_IDENTITY_TITLE_CHARACTERS, SDK_IDENTITY_NAME_FORBIDDEN, SDK_IDENTITY_NAME_RESERVED, CoworkIdentityNameError, RoleSchema, ROOM_ROLE, RuntimeRoleCommandGrantSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, CurrentRoomSchema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, PostAsRoleInputSchema, RestRoleInputSchema, AcceptExternalInviteInputSchema, ListMembersCommandInputSchema, CommandIdempotencyKeySchema, RemoveMemberCommandInputSchema, RuntimeCommandGrantInputSchema, RuntimeRoleCommandGrantInputSchema, RuntimeCommandAuditSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, ReplyReferenceSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, IntakeRejectionShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, IntakeRejectionRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
|
|
11858
12054
|
var init_contracts = __esm({
|
|
11859
12055
|
"src/contracts.ts"() {
|
|
11860
12056
|
"use strict";
|
|
11861
12057
|
init_zod();
|
|
11862
12058
|
init_consumer_commands();
|
|
11863
12059
|
init_command_names();
|
|
12060
|
+
init_thread_contracts();
|
|
11864
12061
|
MAX_TEXT_BYTES = 262144;
|
|
11865
12062
|
MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11866
12063
|
MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
|
|
@@ -11877,10 +12074,10 @@ var init_contracts = __esm({
|
|
|
11877
12074
|
/^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
|
|
11878
12075
|
"must be a 26-character lowercase Crockford ULID"
|
|
11879
12076
|
);
|
|
11880
|
-
|
|
12077
|
+
ContainerIdSchema2 = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
|
|
11881
12078
|
RuntimeCommandNameSchema = external_exports.union([external_exports.enum(RUNTIME_COMMAND_NAMES), ConsumerCommandNameSchema]);
|
|
11882
12079
|
RuntimeCommandGrantSchema = external_exports.object({
|
|
11883
|
-
caller_cid:
|
|
12080
|
+
caller_cid: ContainerIdSchema2,
|
|
11884
12081
|
command: RuntimeCommandNameSchema
|
|
11885
12082
|
}).strict();
|
|
11886
12083
|
Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
|
|
@@ -12159,7 +12356,7 @@ var init_contracts = __esm({
|
|
|
12159
12356
|
lifecycle_request: external_exports.object({
|
|
12160
12357
|
request_id: external_exports.string().min(1).max(256),
|
|
12161
12358
|
command: external_exports.enum(["room.close", "room.delete"]),
|
|
12162
|
-
caller_cid:
|
|
12359
|
+
caller_cid: ContainerIdSchema2,
|
|
12163
12360
|
accepted_at: Rfc3339Schema,
|
|
12164
12361
|
state: external_exports.enum(["pending", "failed", "completed"]),
|
|
12165
12362
|
error: external_exports.literal("lifecycle_failed").optional()
|
|
@@ -12310,7 +12507,7 @@ var init_contracts = __esm({
|
|
|
12310
12507
|
(value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
|
|
12311
12508
|
`invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
|
|
12312
12509
|
),
|
|
12313
|
-
expected_cid:
|
|
12510
|
+
expected_cid: ContainerIdSchema2.optional()
|
|
12314
12511
|
}).strict();
|
|
12315
12512
|
ListMembersCommandInputSchema = external_exports.object({}).strict();
|
|
12316
12513
|
CommandIdempotencyKeySchema = external_exports.string().regex(
|
|
@@ -12387,7 +12584,9 @@ var init_contracts = __esm({
|
|
|
12387
12584
|
}),
|
|
12388
12585
|
source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
12389
12586
|
source_wire_id: NonEmptyStringSchema.optional(),
|
|
12390
|
-
source_reply_to: ReplyReferenceSchema.optional()
|
|
12587
|
+
source_reply_to: ReplyReferenceSchema.optional(),
|
|
12588
|
+
scope: ThreadScopeSchema.optional(),
|
|
12589
|
+
thread_root: ThreadRootSchema.optional()
|
|
12391
12590
|
};
|
|
12392
12591
|
RelayIntentShape = {
|
|
12393
12592
|
kind: external_exports.literal("relay_intent"),
|
|
@@ -12395,7 +12594,12 @@ var init_contracts = __esm({
|
|
|
12395
12594
|
file_id: LowerCrockfordUlidSchema.optional(),
|
|
12396
12595
|
recipient_identity: NonEmptyStringSchema
|
|
12397
12596
|
};
|
|
12398
|
-
RelayResultStatusSchema = external_exports.enum([
|
|
12597
|
+
RelayResultStatusSchema = external_exports.enum([
|
|
12598
|
+
"queued",
|
|
12599
|
+
"send_failed",
|
|
12600
|
+
"skipped_removed",
|
|
12601
|
+
"skipped_reply_unavailable"
|
|
12602
|
+
]);
|
|
12399
12603
|
RelayResultShape = {
|
|
12400
12604
|
kind: external_exports.literal("relay_result"),
|
|
12401
12605
|
intent_record_id: NonEmptyStringSchema,
|
|
@@ -12433,6 +12637,18 @@ var init_contracts = __esm({
|
|
|
12433
12637
|
source_wire_id: NonEmptyStringSchema.optional(),
|
|
12434
12638
|
source_reply_to: ReplyReferenceSchema.optional()
|
|
12435
12639
|
};
|
|
12640
|
+
IntakeRejectionShape = {
|
|
12641
|
+
kind: external_exports.literal("intake_rejection"),
|
|
12642
|
+
source_kind: external_exports.enum(["message", "file"]),
|
|
12643
|
+
source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
12644
|
+
source_file_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
12645
|
+
source_wire_id: NonEmptyStringSchema,
|
|
12646
|
+
sender_identity: NonEmptyStringSchema,
|
|
12647
|
+
sender_participant_id: LowerCrockfordUlidSchema,
|
|
12648
|
+
fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
12649
|
+
error: external_exports.enum(["reply_target_unavailable", "thread_files_unsupported"]),
|
|
12650
|
+
notification_attempt_claimed: external_exports.literal(true)
|
|
12651
|
+
};
|
|
12436
12652
|
MembershipIntentShape = {
|
|
12437
12653
|
kind: external_exports.literal("membership_intent"),
|
|
12438
12654
|
action: external_exports.enum(["remove"]),
|
|
@@ -12470,6 +12686,7 @@ var init_contracts = __esm({
|
|
|
12470
12686
|
FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
|
|
12471
12687
|
RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
|
|
12472
12688
|
RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
|
|
12689
|
+
IntakeRejectionRecordSchema = external_exports.object({ ...RecordCommonShape, ...IntakeRejectionShape }).strict();
|
|
12473
12690
|
MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
|
|
12474
12691
|
MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
|
|
12475
12692
|
CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
|
|
@@ -12479,6 +12696,7 @@ var init_contracts = __esm({
|
|
|
12479
12696
|
FileRecordSchema,
|
|
12480
12697
|
RelayIntentRecordSchema,
|
|
12481
12698
|
RelayResultRecordSchema,
|
|
12699
|
+
IntakeRejectionRecordSchema,
|
|
12482
12700
|
MembershipIntentRecordSchema,
|
|
12483
12701
|
MembershipResultRecordSchema,
|
|
12484
12702
|
CloseNoticeIntentRecordSchema,
|
|
@@ -12492,8 +12710,12 @@ var init_contracts = __esm({
|
|
|
12492
12710
|
message: 'record_id must equal room_id + ":" + seq'
|
|
12493
12711
|
});
|
|
12494
12712
|
}
|
|
12495
|
-
if (record.kind === "message")
|
|
12713
|
+
if (record.kind === "message") {
|
|
12714
|
+
refineMessageCategory(record, context);
|
|
12715
|
+
refineMessageThread(record, context);
|
|
12716
|
+
}
|
|
12496
12717
|
refineRelaySubject(record, context);
|
|
12718
|
+
refineIntakeRejection(record, context);
|
|
12497
12719
|
refineFileRecord(record, context);
|
|
12498
12720
|
});
|
|
12499
12721
|
AppendRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
@@ -12501,11 +12723,16 @@ var init_contracts = __esm({
|
|
|
12501
12723
|
external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
|
|
12502
12724
|
external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
|
|
12503
12725
|
external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
|
|
12726
|
+
external_exports.object({ ...AppendCommonShape, ...IntakeRejectionShape }).strict(),
|
|
12504
12727
|
external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
|
|
12505
12728
|
external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
|
|
12506
12729
|
]).superRefine((record, context) => {
|
|
12507
|
-
if (record.kind === "message")
|
|
12730
|
+
if (record.kind === "message") {
|
|
12731
|
+
refineMessageCategory(record, context);
|
|
12732
|
+
refineMessageThread(record, context);
|
|
12733
|
+
}
|
|
12508
12734
|
refineRelaySubject(record, context);
|
|
12735
|
+
refineIntakeRejection(record, context);
|
|
12509
12736
|
refineFileRecord(record, context);
|
|
12510
12737
|
});
|
|
12511
12738
|
}
|
|
@@ -13842,6 +14069,34 @@ var init_packets = __esm({
|
|
|
13842
14069
|
this.runtimeHandlers = handlers;
|
|
13843
14070
|
await this.runBound(() => this.client.registerCommands([
|
|
13844
14071
|
...handlers.consumerCommands ?? [],
|
|
14072
|
+
...handlers.startThread === void 0 ? [] : [{
|
|
14073
|
+
name: "start_thread",
|
|
14074
|
+
description: "Create a scoped reply thread for an explicit set of room participant IDs.",
|
|
14075
|
+
input_schema: {
|
|
14076
|
+
type: "object",
|
|
14077
|
+
additionalProperties: false,
|
|
14078
|
+
required: ["topic", "participant_ids", "idempotency_key"],
|
|
14079
|
+
properties: {
|
|
14080
|
+
topic: {
|
|
14081
|
+
type: "string",
|
|
14082
|
+
minLength: 1,
|
|
14083
|
+
maxLength: 120,
|
|
14084
|
+
pattern: "^(?![\\s\\S]*[\\p{Cc}\\p{Cf}])[\\s\\S]*\\S[\\s\\S]*$"
|
|
14085
|
+
},
|
|
14086
|
+
participant_ids: {
|
|
14087
|
+
type: "array",
|
|
14088
|
+
minItems: 1,
|
|
14089
|
+
uniqueItems: true,
|
|
14090
|
+
items: { type: "string", pattern: "^[0-7][0-9a-hjkmnp-tv-z]{25}$" }
|
|
14091
|
+
},
|
|
14092
|
+
idempotency_key: {
|
|
14093
|
+
type: "string",
|
|
14094
|
+
pattern: "^[A-Za-z0-9._:-]{1,128}$"
|
|
14095
|
+
}
|
|
14096
|
+
}
|
|
14097
|
+
},
|
|
14098
|
+
handler: handlers.startThread
|
|
14099
|
+
}],
|
|
13845
14100
|
{
|
|
13846
14101
|
name: "list-members",
|
|
13847
14102
|
description: "List the room roster using contact-safe member fields.",
|
|
@@ -13866,9 +14121,10 @@ var init_packets = __esm({
|
|
|
13866
14121
|
...handlers.sharedCommand === void 0 ? [] : SHARED_ROOM_COMMANDS.map((name) => {
|
|
13867
14122
|
const doc = [...ROOM_RPC_METHODS, ...PRIVATE_ROOM_RPC_METHODS].find((method) => method.method === name);
|
|
13868
14123
|
const { room_id: _roomId, ...properties } = doc.params.properties;
|
|
14124
|
+
if (name === "room.history") properties.view = { const: "participant" };
|
|
13869
14125
|
return {
|
|
13870
14126
|
name,
|
|
13871
|
-
description: `${doc.description} Requires an explicit grant; applies only to this room.`,
|
|
14127
|
+
description: `${name === "room.history" ? "Read your visible messages using viewer-local after cursors." : doc.description} Requires an explicit grant; applies only to this room.`,
|
|
13872
14128
|
input_schema: {
|
|
13873
14129
|
...doc.params,
|
|
13874
14130
|
properties,
|
|
@@ -14011,6 +14267,22 @@ var init_ulid = __esm({
|
|
|
14011
14267
|
});
|
|
14012
14268
|
|
|
14013
14269
|
// src/reply-threading.ts
|
|
14270
|
+
function buildAliasIndex(rows, roomId) {
|
|
14271
|
+
const local = rows.filter((r) => r.room_id === roomId);
|
|
14272
|
+
const items = local.filter((r) => r.kind === "message" || r.kind === "file");
|
|
14273
|
+
const intents = local.filter((r) => r.kind === "relay_intent");
|
|
14274
|
+
const results = local.filter((r) => r.kind === "relay_result");
|
|
14275
|
+
const copies = (parent, cid) => results.filter((result) => {
|
|
14276
|
+
if (result.status !== "queued" || result.recipient_identity !== cid || key(result) !== key(parent) || !parent.recipient_identities.includes(cid)) return false;
|
|
14277
|
+
const matches = intents.filter((intent2) => intent2.record_id === result.intent_record_id);
|
|
14278
|
+
if (matches.length !== 1) return false;
|
|
14279
|
+
const intent = matches[0];
|
|
14280
|
+
return key(intent) === key(parent) && intent.recipient_identity === cid && parent.seq < intent.seq && intent.seq < result.seq;
|
|
14281
|
+
}).sort((a, b) => a.seq - b.seq);
|
|
14282
|
+
const wires = (result, parent) => [result.wire_id, ...parent.kind === "file" ? [result.metadata_wire_id] : []].filter(nonempty);
|
|
14283
|
+
const ownersFor = (wireId, cid) => items.filter((item) => item.author.identity === cid && item.source_wire_id === wireId || copies(item, cid).some((result) => wires(result, item).includes(wireId)));
|
|
14284
|
+
return { items, copies, wires, ownersFor };
|
|
14285
|
+
}
|
|
14014
14286
|
async function readReplyRows(store, roomId) {
|
|
14015
14287
|
const rows = [];
|
|
14016
14288
|
let after = 0;
|
|
@@ -14028,42 +14300,53 @@ async function readReplyRows(store, roomId) {
|
|
|
14028
14300
|
after = last.seq;
|
|
14029
14301
|
}
|
|
14030
14302
|
}
|
|
14031
|
-
function
|
|
14032
|
-
if (
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
const
|
|
14036
|
-
const items = local.filter((r) => r.kind === "message" || r.kind === "file");
|
|
14037
|
-
const intents = local.filter((r) => r.kind === "relay_intent");
|
|
14038
|
-
const results = local.filter((r) => r.kind === "relay_result");
|
|
14039
|
-
const copies = (parent2, cid) => results.filter((result) => {
|
|
14040
|
-
if (result.status !== "queued" || result.recipient_identity !== cid || key(result) !== key(parent2) || !parent2.recipient_identities.includes(cid)) return false;
|
|
14041
|
-
const matches = intents.filter((intent2) => intent2.record_id === result.intent_record_id);
|
|
14042
|
-
if (matches.length !== 1) return false;
|
|
14043
|
-
const intent = matches[0];
|
|
14044
|
-
return key(intent) === key(parent2) && intent.recipient_identity === cid && parent2.seq < intent.seq && intent.seq < result.seq;
|
|
14045
|
-
}).sort((a, b) => a.seq - b.seq);
|
|
14046
|
-
const wires = (r, parent2) => [r.wire_id, ...parent2.kind === "file" ? [r.metadata_wire_id] : []].filter(nonempty);
|
|
14047
|
-
const ownersFor = (wireId2, cid) => items.filter((item) => item.author.identity === cid && item.source_wire_id === wireId2 || copies(item, cid).some((result) => wires(result, item).includes(wireId2)));
|
|
14048
|
-
const candidates = items.filter((parent2) => parent2.author.identity === child.author.identity && parent2.source_wire_id === incoming || copies(parent2, child.author.identity).some((result) => wires(result, parent2).includes(incoming)));
|
|
14303
|
+
function resolveReplyParent(rows, roomId, senderCid, wireId, beforeSeq) {
|
|
14304
|
+
if (wireId === void 0) return { state: "none" };
|
|
14305
|
+
if (!nonempty(wireId)) return { state: "unknown_parent" };
|
|
14306
|
+
const index = buildAliasIndex(rows, roomId);
|
|
14307
|
+
const candidates = index.ownersFor(wireId, senderCid);
|
|
14049
14308
|
if (candidates.length === 0) return { state: "unknown_parent" };
|
|
14050
14309
|
if (candidates.length !== 1) return { state: "ambiguous_parent" };
|
|
14051
14310
|
const parent = candidates[0];
|
|
14052
|
-
if (parent.seq >=
|
|
14311
|
+
if (parent.seq >= beforeSeq) return { state: "unknown_parent" };
|
|
14053
14312
|
const parentKey = key(parent);
|
|
14054
|
-
if (items.filter((item) => key(item) === parentKey).length !== 1) {
|
|
14313
|
+
if (index.items.filter((item) => key(item) === parentKey).length !== 1) {
|
|
14055
14314
|
return { state: "ambiguous_parent" };
|
|
14056
14315
|
}
|
|
14057
|
-
|
|
14316
|
+
return { state: "resolved", parent: { key: parentKey, item: parent } };
|
|
14317
|
+
}
|
|
14318
|
+
function mapReplyParent(rows, roomId, logicalParent, recipientCid) {
|
|
14319
|
+
const index = buildAliasIndex(rows, roomId);
|
|
14320
|
+
if (logicalParent.item.room_id !== roomId || key(logicalParent.item) !== logicalParent.key) {
|
|
14321
|
+
return { state: "ambiguous_parent", parentKey: logicalParent.key };
|
|
14322
|
+
}
|
|
14323
|
+
const matches = index.items.filter((item) => key(item) === logicalParent.key);
|
|
14324
|
+
if (matches.length !== 1) {
|
|
14325
|
+
return { state: "ambiguous_parent", parentKey: logicalParent.key };
|
|
14326
|
+
}
|
|
14327
|
+
const parent = matches[0];
|
|
14328
|
+
const recipientCopies = index.copies(parent, recipientCid);
|
|
14058
14329
|
const sourceWire = parent.author.identity === recipientCid && nonempty(parent.source_wire_id) ? parent.source_wire_id : void 0;
|
|
14059
|
-
const sourceOwners = sourceWire === void 0 ? [] : ownersFor(sourceWire, recipientCid);
|
|
14060
|
-
const wireId = sourceOwners.length === 1 && key(sourceOwners[0]) ===
|
|
14061
|
-
if (!nonempty(wireId)) return { state: "missing_copy", parentKey };
|
|
14062
|
-
const owners = ownersFor(wireId, recipientCid);
|
|
14063
|
-
if (owners.length !== 1 || key(owners[0]) !==
|
|
14064
|
-
return { state: "ambiguous_parent", parentKey };
|
|
14330
|
+
const sourceOwners = sourceWire === void 0 ? [] : index.ownersFor(sourceWire, recipientCid);
|
|
14331
|
+
const wireId = sourceOwners.length === 1 && key(sourceOwners[0]) === logicalParent.key ? sourceWire : recipientCopies.map((copy) => index.wires(copy, parent)[0]).find(nonempty);
|
|
14332
|
+
if (!nonempty(wireId)) return { state: "missing_copy", parentKey: logicalParent.key };
|
|
14333
|
+
const owners = index.ownersFor(wireId, recipientCid);
|
|
14334
|
+
if (owners.length !== 1 || key(owners[0]) !== logicalParent.key) {
|
|
14335
|
+
return { state: "ambiguous_parent", parentKey: logicalParent.key };
|
|
14065
14336
|
}
|
|
14066
|
-
return { state: "linked", parentKey, replyTo: { wire_id: wireId } };
|
|
14337
|
+
return { state: "linked", parentKey: logicalParent.key, replyTo: { wire_id: wireId } };
|
|
14338
|
+
}
|
|
14339
|
+
function selectReply(rows, roomId, child, recipientCid) {
|
|
14340
|
+
if (child.room_id !== roomId) throw new Error("reply child room mismatch");
|
|
14341
|
+
const resolution = resolveReplyParent(
|
|
14342
|
+
rows,
|
|
14343
|
+
roomId,
|
|
14344
|
+
child.author.identity,
|
|
14345
|
+
child.source_reply_to?.wire_id,
|
|
14346
|
+
child.seq
|
|
14347
|
+
);
|
|
14348
|
+
if (resolution.state !== "resolved") return resolution;
|
|
14349
|
+
return mapReplyParent(rows, roomId, resolution.parent, recipientCid);
|
|
14067
14350
|
}
|
|
14068
14351
|
var key, nonempty;
|
|
14069
14352
|
var init_reply_threading = __esm({
|
|
@@ -14074,9 +14357,159 @@ var init_reply_threading = __esm({
|
|
|
14074
14357
|
}
|
|
14075
14358
|
});
|
|
14076
14359
|
|
|
14360
|
+
// src/threads.ts
|
|
14361
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
14362
|
+
function publicThreadMetadata(root, room) {
|
|
14363
|
+
let creator;
|
|
14364
|
+
if (room.anonymous) {
|
|
14365
|
+
const alias = AuthorAliasSchema.safeParse(root.author_alias);
|
|
14366
|
+
if (!alias.success || alias.data.participant_id !== root.thread_root.creator_participant_id) {
|
|
14367
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14368
|
+
}
|
|
14369
|
+
creator = {
|
|
14370
|
+
identity: alias.data.participant_id,
|
|
14371
|
+
display_name: alias.data.alias,
|
|
14372
|
+
role: root.author.role
|
|
14373
|
+
};
|
|
14374
|
+
} else {
|
|
14375
|
+
creator = {
|
|
14376
|
+
identity: root.author.identity,
|
|
14377
|
+
display_name: root.author.display_name,
|
|
14378
|
+
role: root.author.role
|
|
14379
|
+
};
|
|
14380
|
+
}
|
|
14381
|
+
return {
|
|
14382
|
+
schema_version: 1,
|
|
14383
|
+
thread_id: root.thread_root.thread_id,
|
|
14384
|
+
topic: root.thread_root.topic,
|
|
14385
|
+
creator,
|
|
14386
|
+
participant_ids: root.thread_root.members.map((member) => member.participant_id),
|
|
14387
|
+
created_at: root.at
|
|
14388
|
+
};
|
|
14389
|
+
}
|
|
14390
|
+
function selectThreadMembers(room, cid, input) {
|
|
14391
|
+
const selected = new Set(input.participant_ids);
|
|
14392
|
+
const active = room.seats.filter((seat) => seat.state === "active");
|
|
14393
|
+
const creator = active.find((seat) => seat.identity === cid);
|
|
14394
|
+
if (creator === void 0 || selected.size === 0 || selected.size !== input.participant_ids.length || selected.size > active.length || !selected.has(creator.participant_id)) {
|
|
14395
|
+
throw new ThreadFailure("invalid_members");
|
|
14396
|
+
}
|
|
14397
|
+
const members = active.filter((seat) => selected.has(seat.participant_id));
|
|
14398
|
+
if (members.length !== selected.size) throw new ThreadFailure("invalid_members");
|
|
14399
|
+
return members.map(({ participant_id, identity }) => ({ participant_id, identity })).sort((left, right) => left.participant_id.localeCompare(right.participant_id));
|
|
14400
|
+
}
|
|
14401
|
+
function activeThreadSeat(room, root, cid) {
|
|
14402
|
+
return room.seats.find((seat) => seat.state === "active" && seat.identity === cid && root.members.some((member) => member.identity === cid && member.participant_id === seat.participant_id));
|
|
14403
|
+
}
|
|
14404
|
+
function threadRelayEligible(room, root, cid) {
|
|
14405
|
+
return activeThreadSeat(room, root, cid) !== void 0;
|
|
14406
|
+
}
|
|
14407
|
+
function publicThreadAuthor(message, root, room) {
|
|
14408
|
+
const member = root.members.find((member2) => member2.identity === message.author.identity);
|
|
14409
|
+
if (!member) throw new ThreadFailure("reply_target_unavailable");
|
|
14410
|
+
if (!room.anonymous) return message.author;
|
|
14411
|
+
const alias = AuthorAliasSchema.safeParse(message.author_alias);
|
|
14412
|
+
if (!alias.success || alias.data.participant_id !== member.participant_id) {
|
|
14413
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14414
|
+
}
|
|
14415
|
+
return { identity: alias.data.participant_id, display_name: alias.data.alias, role: message.author.role };
|
|
14416
|
+
}
|
|
14417
|
+
function threadFingerprint(input) {
|
|
14418
|
+
return createHash3("sha256").update(JSON.stringify({
|
|
14419
|
+
topic: input.topic,
|
|
14420
|
+
participant_ids: [...input.participant_ids].sort()
|
|
14421
|
+
})).digest("hex");
|
|
14422
|
+
}
|
|
14423
|
+
function findThreadRoot(rows, id) {
|
|
14424
|
+
const candidates = rows.filter((row) => row.kind === "message" && (row.message_id === id || row.thread_root?.thread_id === id || row.scope?.thread_id === id && row.scope.parent_key === void 0));
|
|
14425
|
+
if (candidates.length === 0) return void 0;
|
|
14426
|
+
if (candidates.length !== 1) throw new ThreadFailure("reply_target_unavailable");
|
|
14427
|
+
const root = candidates[0];
|
|
14428
|
+
const parsedRoot = ThreadRootSchema.safeParse(root.thread_root);
|
|
14429
|
+
const parsedScope = ThreadScopeSchema.safeParse(root.scope);
|
|
14430
|
+
const parsedAlias = AuthorAliasSchema.safeParse(root.author_alias);
|
|
14431
|
+
const creator = parsedRoot.success ? parsedRoot.data.members.find((member) => member.participant_id === parsedRoot.data.creator_participant_id) : void 0;
|
|
14432
|
+
if (!parsedRoot.success || !parsedScope.success || root.message_id !== id || parsedRoot.data.thread_id !== id || parsedScope.data.thread_id !== id || parsedScope.data.parent_key !== void 0 || root.category !== "chat" || creator?.identity !== root.author.identity || root.author_alias !== void 0 && (!parsedAlias.success || parsedAlias.data.participant_id !== parsedRoot.data.creator_participant_id) || root.source_msg_id !== void 0 || root.source_wire_id !== void 0 || root.source_reply_to !== void 0) {
|
|
14433
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14434
|
+
}
|
|
14435
|
+
return root;
|
|
14436
|
+
}
|
|
14437
|
+
function classifyThreadAssociation(room, rows, source) {
|
|
14438
|
+
const chain = [];
|
|
14439
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14440
|
+
let item = source;
|
|
14441
|
+
for (; ; ) {
|
|
14442
|
+
const key2 = item.kind === "message" ? `message:${item.message_id}` : `file:${item.file_id}`;
|
|
14443
|
+
if (item.room_id !== room.room_id || seen.has(key2) || chain.length > rows.length) {
|
|
14444
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14445
|
+
}
|
|
14446
|
+
seen.add(key2);
|
|
14447
|
+
const resolved = resolveReplyParent(
|
|
14448
|
+
rows,
|
|
14449
|
+
room.room_id,
|
|
14450
|
+
item.author.identity,
|
|
14451
|
+
item.source_reply_to?.wire_id,
|
|
14452
|
+
item.seq
|
|
14453
|
+
);
|
|
14454
|
+
chain.push({ item, resolved });
|
|
14455
|
+
if (resolved.state !== "resolved") break;
|
|
14456
|
+
if (resolved.parent.item.seq >= item.seq) throw new ThreadFailure("reply_target_unavailable");
|
|
14457
|
+
item = resolved.parent.item;
|
|
14458
|
+
}
|
|
14459
|
+
let association = { state: "ordinary" };
|
|
14460
|
+
for (const { item: item2, resolved } of chain.reverse()) {
|
|
14461
|
+
const declared = item2.kind === "message" && (item2.scope !== void 0 || item2.thread_root !== void 0);
|
|
14462
|
+
if (!declared && association.state === "ordinary") continue;
|
|
14463
|
+
const scope = ThreadScopeSchema.safeParse(item2.kind === "message" ? item2.scope : void 0);
|
|
14464
|
+
if (item2.kind !== "message" || !scope.success || item2.category !== "chat" || rows.filter((row) => row.room_id === room.room_id && row.kind === "message" && row.message_id === item2.message_id).length !== 1) {
|
|
14465
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14466
|
+
}
|
|
14467
|
+
const root = findThreadRoot(rows.filter((row) => row.room_id === room.room_id), scope.data.thread_id);
|
|
14468
|
+
if (!root?.thread_root) throw new ThreadFailure("reply_target_unavailable");
|
|
14469
|
+
publicThreadMetadata({ ...root, thread_root: root.thread_root }, room);
|
|
14470
|
+
publicThreadAuthor(item2, root.thread_root, room);
|
|
14471
|
+
if (item2.message_id !== root.message_id) {
|
|
14472
|
+
if (item2.thread_root !== void 0 || root.seq >= item2.seq || resolved.state !== "resolved" || scope.data.parent_key !== resolved.parent.key || association.state !== "scoped" || association.root.message_id !== root.message_id) {
|
|
14473
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14474
|
+
}
|
|
14475
|
+
}
|
|
14476
|
+
association = { state: "scoped", root: { ...root, thread_root: root.thread_root }, scope: scope.data };
|
|
14477
|
+
}
|
|
14478
|
+
return association;
|
|
14479
|
+
}
|
|
14480
|
+
function resolveIntakeScope(room, rows, item, beforeSeq) {
|
|
14481
|
+
const ordinary = () => ({ recipients: [...new Set(room.seats.filter((seat) => seat.state === "active" && seat.identity !== item.sender_id).map((seat) => seat.identity))] });
|
|
14482
|
+
if (item.reply_to == null) return ordinary();
|
|
14483
|
+
const reply = item.reply_to;
|
|
14484
|
+
if (typeof reply.wire_id !== "string" || reply.wire_id.length === 0 || reply.wire_id.length > 256 || reply.sentence !== void 0 && (!Number.isSafeInteger(reply.sentence) || reply.sentence < 1) || room.state !== "active" || room.lifecycle_request?.state === "pending" || !room.seats.some((seat) => seat.identity === item.sender_id && seat.state === "active")) {
|
|
14485
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14486
|
+
}
|
|
14487
|
+
const resolved = resolveReplyParent(rows, room.room_id, item.sender_id, reply.wire_id, beforeSeq);
|
|
14488
|
+
if (resolved.state !== "resolved") throw new ThreadFailure("reply_target_unavailable");
|
|
14489
|
+
const association = classifyThreadAssociation(room, rows, resolved.parent.item);
|
|
14490
|
+
if (association.state === "ordinary") return ordinary();
|
|
14491
|
+
const { root } = association;
|
|
14492
|
+
if (!activeThreadSeat(room, root.thread_root, item.sender_id)) {
|
|
14493
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14494
|
+
}
|
|
14495
|
+
if ("file_id" in item) throw new ThreadFailure("thread_files_unsupported");
|
|
14496
|
+
return {
|
|
14497
|
+
recipients: root.thread_root.members.filter((member) => member.identity !== item.sender_id && activeThreadSeat(room, root.thread_root, member.identity) !== void 0).map((member) => member.identity),
|
|
14498
|
+
scope: { thread_id: root.message_id, parent_key: resolved.parent.key }
|
|
14499
|
+
};
|
|
14500
|
+
}
|
|
14501
|
+
var init_threads = __esm({
|
|
14502
|
+
"src/threads.ts"() {
|
|
14503
|
+
"use strict";
|
|
14504
|
+
init_reply_threading();
|
|
14505
|
+
init_contracts();
|
|
14506
|
+
init_thread_contracts();
|
|
14507
|
+
}
|
|
14508
|
+
});
|
|
14509
|
+
|
|
14077
14510
|
// src/intake.ts
|
|
14078
14511
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14079
|
-
import { createHash as
|
|
14512
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
14080
14513
|
function canonicalJson(value) {
|
|
14081
14514
|
const encoded = JSON.stringify(canonicalValue(value));
|
|
14082
14515
|
if (encoded === void 0) throw new TypeError("canonical JSON value is not serializable");
|
|
@@ -14091,13 +14524,25 @@ function sameReply2(stored, observed) {
|
|
|
14091
14524
|
}
|
|
14092
14525
|
async function queryStore(store, roomId, options) {
|
|
14093
14526
|
if (store.query) return store.query(roomId, options);
|
|
14094
|
-
|
|
14095
|
-
|
|
14527
|
+
const archive = [];
|
|
14528
|
+
let after = 0;
|
|
14529
|
+
for (; ; ) {
|
|
14530
|
+
const page = await store.read(roomId, { after, limit: JOURNAL_WORK_BATCH_SIZE });
|
|
14531
|
+
if (page.length === 0) break;
|
|
14532
|
+
for (const row of page) {
|
|
14533
|
+
if (row.room_id !== roomId || !Number.isSafeInteger(row.seq) || row.seq <= after) {
|
|
14534
|
+
throw new Error("intake archive cursor did not advance");
|
|
14535
|
+
}
|
|
14536
|
+
archive.push(row);
|
|
14537
|
+
after = row.seq;
|
|
14538
|
+
}
|
|
14539
|
+
}
|
|
14540
|
+
let records = archive.filter((record) => {
|
|
14096
14541
|
const value = record;
|
|
14097
|
-
return (options.kind === void 0 || record.kind === options.kind) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.sourceMsgId === void 0 || value.source_msg_id === options.sourceMsgId) && (options.sourceFileId === void 0 || value.source_file_id === options.sourceFileId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity);
|
|
14542
|
+
return (options.after === void 0 || record.seq > options.after) && (options.kind === void 0 || record.kind === options.kind) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.sourceMsgId === void 0 || value.source_msg_id === options.sourceMsgId) && (options.sourceFileId === void 0 || value.source_file_id === options.sourceFileId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity);
|
|
14098
14543
|
});
|
|
14099
14544
|
if (options.unresolvedResultKind) {
|
|
14100
|
-
const completed = new Set(
|
|
14545
|
+
const completed = new Set(archive.filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
|
|
14101
14546
|
records = records.filter((record) => !completed.has(record.record_id));
|
|
14102
14547
|
}
|
|
14103
14548
|
if (options.descending) records.reverse();
|
|
@@ -14115,8 +14560,20 @@ function canonicalValue(value) {
|
|
|
14115
14560
|
}
|
|
14116
14561
|
return value;
|
|
14117
14562
|
}
|
|
14118
|
-
function
|
|
14119
|
-
return
|
|
14563
|
+
function inputFingerprint(item) {
|
|
14564
|
+
return createHash4("sha256").update(canonicalJson({
|
|
14565
|
+
sender: item.sender_id,
|
|
14566
|
+
wire: item.wire_id,
|
|
14567
|
+
date: item.date,
|
|
14568
|
+
reply: item.reply_to ?? null,
|
|
14569
|
+
..."file_id" in item ? {
|
|
14570
|
+
kind: "file",
|
|
14571
|
+
id: item.file_id,
|
|
14572
|
+
filename: item.filename,
|
|
14573
|
+
mime: item.mime,
|
|
14574
|
+
sha256: createHash4("sha256").update(item.data).digest("hex")
|
|
14575
|
+
} : { kind: "message", id: item.msg_id, text: item.text }
|
|
14576
|
+
})).digest("hex");
|
|
14120
14577
|
}
|
|
14121
14578
|
function wireKind(category) {
|
|
14122
14579
|
switch (category) {
|
|
@@ -14138,6 +14595,8 @@ var init_intake = __esm({
|
|
|
14138
14595
|
init_contracts();
|
|
14139
14596
|
init_ulid();
|
|
14140
14597
|
init_reply_threading();
|
|
14598
|
+
init_threads();
|
|
14599
|
+
init_thread_contracts();
|
|
14141
14600
|
JOURNAL_WORK_BATCH_SIZE = 64;
|
|
14142
14601
|
INTAKE_BATCH_SIZE = 32;
|
|
14143
14602
|
IntakePump = class {
|
|
@@ -14282,28 +14741,36 @@ var init_intake = __esm({
|
|
|
14282
14741
|
});
|
|
14283
14742
|
}
|
|
14284
14743
|
async processFileInboxItem(roomId, packet, item) {
|
|
14285
|
-
const parsedName = FileNameSchema.safeParse(item.filename);
|
|
14286
|
-
const parsedMime = FileMimeSchema.safeParse(item.mime);
|
|
14287
|
-
if (!parsedName.success || !parsedMime.success) {
|
|
14288
|
-
await packet.acknowledgeFile(item);
|
|
14289
|
-
return;
|
|
14290
|
-
}
|
|
14291
|
-
if (item.data.length > MAX_FILE_BYTES) {
|
|
14292
|
-
throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
|
|
14293
|
-
}
|
|
14294
14744
|
const room = await this.store.load(roomId);
|
|
14295
|
-
const
|
|
14296
|
-
|
|
14297
|
-
);
|
|
14298
|
-
if (room.state !== "active" || !seat) {
|
|
14299
|
-
if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
|
|
14745
|
+
const [stored] = await queryStore(this.store, roomId, { sourceFileId: item.file_id, limit: 1 });
|
|
14746
|
+
if (this.isRejectedReplay(stored, item)) {
|
|
14300
14747
|
await packet.acknowledgeFile(item);
|
|
14301
14748
|
return;
|
|
14302
14749
|
}
|
|
14303
|
-
|
|
14304
|
-
let file = this.findSourceFile(storedFile === void 0 ? [] : [storedFile], item);
|
|
14750
|
+
let file = this.findSourceFile(stored === void 0 ? [] : [stored], item);
|
|
14305
14751
|
if (!file) {
|
|
14306
|
-
const
|
|
14752
|
+
const seat = room.seats.find((candidate) => candidate.identity === item.sender_id && candidate.state === "active");
|
|
14753
|
+
const known = room.seats.some((candidate) => candidate.identity === item.sender_id);
|
|
14754
|
+
if (!known || item.reply_to == null && (room.state !== "active" || !seat)) {
|
|
14755
|
+
if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
|
|
14756
|
+
await packet.acknowledgeFile(item);
|
|
14757
|
+
return;
|
|
14758
|
+
}
|
|
14759
|
+
const disposition = await this.freshScopeUnlocked(room, item);
|
|
14760
|
+
if (!disposition) {
|
|
14761
|
+
await packet.acknowledgeFile(item);
|
|
14762
|
+
return;
|
|
14763
|
+
}
|
|
14764
|
+
if (!seat) throw new Error("authorized file sender has no active seat");
|
|
14765
|
+
const parsedName = FileNameSchema.safeParse(item.filename);
|
|
14766
|
+
const parsedMime = FileMimeSchema.safeParse(item.mime);
|
|
14767
|
+
if (!parsedName.success || !parsedMime.success) {
|
|
14768
|
+
await packet.acknowledgeFile(item);
|
|
14769
|
+
return;
|
|
14770
|
+
}
|
|
14771
|
+
if (item.data.length > MAX_FILE_BYTES) {
|
|
14772
|
+
throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
|
|
14773
|
+
}
|
|
14307
14774
|
const bytes = Buffer.from(item.data);
|
|
14308
14775
|
const appended = await this.store.append(roomId, {
|
|
14309
14776
|
version: 1,
|
|
@@ -14316,9 +14783,9 @@ var init_intake = __esm({
|
|
|
14316
14783
|
filename: parsedName.data,
|
|
14317
14784
|
mime: parsedMime.data,
|
|
14318
14785
|
size: bytes.length,
|
|
14319
|
-
sha256:
|
|
14786
|
+
sha256: createHash4("sha256").update(bytes).digest("hex"),
|
|
14320
14787
|
data_base64: bytes.toString("base64"),
|
|
14321
|
-
recipient_identities:
|
|
14788
|
+
recipient_identities: disposition.recipients,
|
|
14322
14789
|
source_file_id: item.file_id,
|
|
14323
14790
|
...item.wire_id === "" ? {} : { source_wire_id: item.wire_id },
|
|
14324
14791
|
...item.reply_to == null ? {} : { source_reply_to: item.reply_to }
|
|
@@ -14331,18 +14798,26 @@ var init_intake = __esm({
|
|
|
14331
14798
|
}
|
|
14332
14799
|
async processInboxItem(roomId, packet, item, acknowledge = true) {
|
|
14333
14800
|
const room = await this.store.load(roomId);
|
|
14334
|
-
const
|
|
14335
|
-
|
|
14336
|
-
);
|
|
14337
|
-
if (room.state !== "active" || !seat) {
|
|
14338
|
-
if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
|
|
14801
|
+
const [stored] = await queryStore(this.store, roomId, { sourceMsgId: item.msg_id, limit: 1 });
|
|
14802
|
+
if (this.isRejectedReplay(stored, item)) {
|
|
14339
14803
|
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14340
14804
|
return;
|
|
14341
14805
|
}
|
|
14342
|
-
|
|
14343
|
-
let message = this.findSourceMessage(storedMessage === void 0 ? [] : [storedMessage], item);
|
|
14806
|
+
let message = this.findSourceMessage(stored === void 0 ? [] : [stored], item);
|
|
14344
14807
|
if (!message) {
|
|
14345
|
-
const
|
|
14808
|
+
const seat = room.seats.find((candidate) => candidate.identity === item.sender_id && candidate.state === "active");
|
|
14809
|
+
const known = room.seats.some((candidate) => candidate.identity === item.sender_id);
|
|
14810
|
+
if (!known || item.reply_to == null && (room.state !== "active" || !seat)) {
|
|
14811
|
+
if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
|
|
14812
|
+
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14813
|
+
return;
|
|
14814
|
+
}
|
|
14815
|
+
const disposition = await this.freshScopeUnlocked(room, item);
|
|
14816
|
+
if (!disposition) {
|
|
14817
|
+
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14818
|
+
return;
|
|
14819
|
+
}
|
|
14820
|
+
if (!seat) throw new Error("authorized message sender has no active seat");
|
|
14346
14821
|
const appended = await this.store.append(roomId, {
|
|
14347
14822
|
version: 1,
|
|
14348
14823
|
kind: "message",
|
|
@@ -14359,7 +14834,8 @@ var init_intake = __esm({
|
|
|
14359
14834
|
...room.anonymous && seat.alias !== void 0 ? { author_alias: { participant_id: seat.participant_id, alias: seat.alias } } : {},
|
|
14360
14835
|
category: "chat",
|
|
14361
14836
|
text: item.text,
|
|
14362
|
-
recipient_identities:
|
|
14837
|
+
recipient_identities: disposition.recipients,
|
|
14838
|
+
...disposition.scope === void 0 ? {} : { scope: disposition.scope },
|
|
14363
14839
|
source_msg_id: item.msg_id,
|
|
14364
14840
|
...item.wire_id === "" ? {} : { source_wire_id: item.wire_id },
|
|
14365
14841
|
...item.reply_to == null ? {} : { source_reply_to: item.reply_to }
|
|
@@ -14370,6 +14846,78 @@ var init_intake = __esm({
|
|
|
14370
14846
|
await this.completeMessageIntents(roomId, message);
|
|
14371
14847
|
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14372
14848
|
}
|
|
14849
|
+
async freshScopeUnlocked(room, item) {
|
|
14850
|
+
try {
|
|
14851
|
+
const rows = item.reply_to == null ? [] : await readReplyRows(this.store, room.room_id);
|
|
14852
|
+
const beforeSeq = item.reply_to == null ? 1 : await this.nextRecordSeq(room.room_id);
|
|
14853
|
+
return resolveIntakeScope(room, rows, item, beforeSeq);
|
|
14854
|
+
} catch (error) {
|
|
14855
|
+
if (!(error instanceof ThreadFailure)) throw error;
|
|
14856
|
+
await this.recordRejectionUnlocked(room, item, error);
|
|
14857
|
+
return void 0;
|
|
14858
|
+
}
|
|
14859
|
+
}
|
|
14860
|
+
async nextRecordSeq(roomId) {
|
|
14861
|
+
if (this.store.query) {
|
|
14862
|
+
const [last] = await this.store.query(roomId, { descending: true, limit: 1 });
|
|
14863
|
+
if (last && (last.room_id !== roomId || !Number.isSafeInteger(last.seq) || last.seq < 1)) {
|
|
14864
|
+
throw new Error("intake archive tail is invalid");
|
|
14865
|
+
}
|
|
14866
|
+
return (last?.seq ?? 0) + 1;
|
|
14867
|
+
}
|
|
14868
|
+
let after = 0;
|
|
14869
|
+
for (; ; ) {
|
|
14870
|
+
const page = await this.store.read(roomId, { after, limit: JOURNAL_WORK_BATCH_SIZE });
|
|
14871
|
+
if (page.length === 0) return after + 1;
|
|
14872
|
+
for (const row of page) {
|
|
14873
|
+
if (row.room_id !== roomId || !Number.isSafeInteger(row.seq) || row.seq <= after) {
|
|
14874
|
+
throw new Error("intake archive cursor did not advance");
|
|
14875
|
+
}
|
|
14876
|
+
after = row.seq;
|
|
14877
|
+
}
|
|
14878
|
+
}
|
|
14879
|
+
}
|
|
14880
|
+
isRejectedReplay(record, item) {
|
|
14881
|
+
if (record?.kind !== "intake_rejection") return false;
|
|
14882
|
+
const file = "file_id" in item;
|
|
14883
|
+
if (record.source_kind !== (file ? "file" : "message") || (file ? record.source_file_id !== item.file_id : record.source_msg_id !== item.msg_id) || record.source_wire_id !== item.wire_id || record.sender_identity !== item.sender_id || record.fingerprint !== inputFingerprint(item)) {
|
|
14884
|
+
throw new Error("inbox source does not match its durable intake rejection");
|
|
14885
|
+
}
|
|
14886
|
+
return true;
|
|
14887
|
+
}
|
|
14888
|
+
async recordRejectionUnlocked(room, item, error) {
|
|
14889
|
+
const seat = room.seats.find((seat2) => seat2.identity === item.sender_id && seat2.state === "active") ?? room.seats.find((seat2) => seat2.identity === item.sender_id);
|
|
14890
|
+
if (!seat || typeof item.wire_id !== "string" || item.wire_id.length === 0) {
|
|
14891
|
+
throw new Error("intake rejection requires a known seat and source wire");
|
|
14892
|
+
}
|
|
14893
|
+
if (error.code !== "reply_target_unavailable" && error.code !== "thread_files_unsupported") throw error;
|
|
14894
|
+
await this.store.append(room.room_id, {
|
|
14895
|
+
version: 1,
|
|
14896
|
+
kind: "intake_rejection",
|
|
14897
|
+
room_id: room.room_id,
|
|
14898
|
+
at: this.now(),
|
|
14899
|
+
..."file_id" in item ? { source_kind: "file", source_file_id: item.file_id } : { source_kind: "message", source_msg_id: item.msg_id },
|
|
14900
|
+
source_wire_id: item.wire_id,
|
|
14901
|
+
sender_identity: item.sender_id,
|
|
14902
|
+
sender_participant_id: seat.participant_id,
|
|
14903
|
+
fingerprint: inputFingerprint(item),
|
|
14904
|
+
error: error.code,
|
|
14905
|
+
notification_attempt_claimed: true
|
|
14906
|
+
});
|
|
14907
|
+
try {
|
|
14908
|
+
await sendRoomBody(this.packet(room.room_id), item.sender_id, {
|
|
14909
|
+
version: 1,
|
|
14910
|
+
kind: "room_msg",
|
|
14911
|
+
room_id: room.room_id,
|
|
14912
|
+
room_name: room.room_name,
|
|
14913
|
+
message_id: this.nextMessageId(),
|
|
14914
|
+
at: this.now(),
|
|
14915
|
+
text: error.code,
|
|
14916
|
+
author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE }
|
|
14917
|
+
});
|
|
14918
|
+
} catch {
|
|
14919
|
+
}
|
|
14920
|
+
}
|
|
14373
14921
|
acknowledgeMessage(roomId, packet, expected) {
|
|
14374
14922
|
return packet.acknowledgeMessage(
|
|
14375
14923
|
expected,
|
|
@@ -14493,26 +15041,44 @@ var init_intake = __esm({
|
|
|
14493
15041
|
const [message] = intent.message_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "message", messageId: intent.message_id, limit: 1 });
|
|
14494
15042
|
const [file] = intent.file_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "file", fileId: intent.file_id, limit: 1 });
|
|
14495
15043
|
if (message === void 0 === (file === void 0)) continue;
|
|
14496
|
-
const recipients = message?.recipient_identities ?? file.recipient_identities;
|
|
14497
|
-
if (!recipients.includes(intent.recipient_identity)) continue;
|
|
14498
|
-
if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
|
|
14499
|
-
const skipped = await this.store.append(roomId, {
|
|
14500
|
-
version: 1,
|
|
14501
|
-
kind: "relay_result",
|
|
14502
|
-
room_id: roomId,
|
|
14503
|
-
at: this.now(),
|
|
14504
|
-
intent_record_id: intent.record_id,
|
|
14505
|
-
...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
|
|
14506
|
-
...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
|
|
14507
|
-
recipient_identity: intent.recipient_identity,
|
|
14508
|
-
status: "skipped_removed"
|
|
14509
|
-
});
|
|
14510
|
-
if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
14511
|
-
continue;
|
|
14512
|
-
}
|
|
14513
15044
|
const source = message ?? file;
|
|
14514
|
-
const replyRows = source.source_reply_to === void 0 ? [] : await readReplyRows(this.store, roomId);
|
|
15045
|
+
const replyRows = source.source_reply_to === void 0 && message?.scope === void 0 && message?.thread_root === void 0 ? [] : await readReplyRows(this.store, roomId);
|
|
14515
15046
|
const decision = selectReply(replyRows, roomId, source, intent.recipient_identity);
|
|
15047
|
+
let publicThread = {};
|
|
15048
|
+
let scopedAuthor;
|
|
15049
|
+
try {
|
|
15050
|
+
const association = classifyThreadAssociation(room, replyRows, source);
|
|
15051
|
+
if (association.state === "scoped") {
|
|
15052
|
+
const { root, scope } = association;
|
|
15053
|
+
if (!message || !message.recipient_identities.includes(intent.recipient_identity) || message.seq >= intent.seq || !root.thread_root.members.some((member) => member.identity === intent.recipient_identity)) {
|
|
15054
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
15055
|
+
}
|
|
15056
|
+
if (!threadRelayEligible(room, root.thread_root, intent.recipient_identity)) {
|
|
15057
|
+
await this.skipRelay(roomId, intent, "skipped_removed");
|
|
15058
|
+
continue;
|
|
15059
|
+
}
|
|
15060
|
+
const metadata = publicThreadMetadata({ ...root, thread_root: root.thread_root }, room);
|
|
15061
|
+
scopedAuthor = publicThreadAuthor(message, root.thread_root, room);
|
|
15062
|
+
if (message.message_id === root.message_id) {
|
|
15063
|
+
publicThread = { thread: { schema_version: 1, thread_id: root.message_id }, thread_root: metadata };
|
|
15064
|
+
} else {
|
|
15065
|
+
if (decision.state !== "linked" || decision.parentKey !== scope.parent_key) {
|
|
15066
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
15067
|
+
}
|
|
15068
|
+
publicThread = { thread: { schema_version: 1, thread_id: root.message_id } };
|
|
15069
|
+
}
|
|
15070
|
+
} else {
|
|
15071
|
+
if (!source.recipient_identities.includes(intent.recipient_identity)) continue;
|
|
15072
|
+
if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
|
|
15073
|
+
await this.skipRelay(roomId, intent, "skipped_removed");
|
|
15074
|
+
continue;
|
|
15075
|
+
}
|
|
15076
|
+
}
|
|
15077
|
+
} catch (error) {
|
|
15078
|
+
if (!(error instanceof ThreadFailure)) throw error;
|
|
15079
|
+
await this.skipRelay(roomId, intent, "skipped_reply_unavailable");
|
|
15080
|
+
continue;
|
|
15081
|
+
}
|
|
14516
15082
|
const replyTo = decision.replyTo;
|
|
14517
15083
|
if (file !== void 0) {
|
|
14518
15084
|
const uploader = file.author_alias?.alias ?? file.author.display_name;
|
|
@@ -14573,11 +15139,12 @@ var init_intake = __esm({
|
|
|
14573
15139
|
room_name: room.room_name,
|
|
14574
15140
|
message_id: message.message_id,
|
|
14575
15141
|
// An anonymous author leaves the archive only in alias form.
|
|
14576
|
-
author: message.author_alias === void 0 ? message.author : {
|
|
15142
|
+
author: scopedAuthor ?? (message.author_alias === void 0 ? message.author : {
|
|
14577
15143
|
identity: message.author_alias.participant_id,
|
|
14578
15144
|
display_name: message.author_alias.alias,
|
|
14579
15145
|
role: message.author.role
|
|
14580
|
-
},
|
|
15146
|
+
}),
|
|
15147
|
+
...publicThread,
|
|
14581
15148
|
text: message.text,
|
|
14582
15149
|
at: message.at,
|
|
14583
15150
|
...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
|
|
@@ -14600,6 +15167,20 @@ var init_intake = __esm({
|
|
|
14600
15167
|
}
|
|
14601
15168
|
}
|
|
14602
15169
|
}
|
|
15170
|
+
async skipRelay(roomId, intent, status) {
|
|
15171
|
+
const result = await this.store.append(roomId, {
|
|
15172
|
+
version: 1,
|
|
15173
|
+
kind: "relay_result",
|
|
15174
|
+
room_id: roomId,
|
|
15175
|
+
at: this.now(),
|
|
15176
|
+
intent_record_id: intent.record_id,
|
|
15177
|
+
...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
|
|
15178
|
+
...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
|
|
15179
|
+
recipient_identity: intent.recipient_identity,
|
|
15180
|
+
status
|
|
15181
|
+
});
|
|
15182
|
+
if (result.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
15183
|
+
}
|
|
14603
15184
|
findSourceMessage(records, item) {
|
|
14604
15185
|
const message = records.find((record) => record.kind === "message" && record.source_msg_id === item.msg_id);
|
|
14605
15186
|
if (!message) return void 0;
|
|
@@ -14793,7 +15374,7 @@ var init_command_routes = __esm({
|
|
|
14793
15374
|
}).strict();
|
|
14794
15375
|
RuntimeCommandGrantParams = external_exports.object({
|
|
14795
15376
|
room_id: external_exports.string(),
|
|
14796
|
-
caller_cid:
|
|
15377
|
+
caller_cid: ContainerIdSchema2,
|
|
14797
15378
|
command: RuntimeCommandNameSchema
|
|
14798
15379
|
}).strict();
|
|
14799
15380
|
RuntimeRoleCommandGrantParams = external_exports.object({
|
|
@@ -14815,9 +15396,132 @@ var init_command_routes = __esm({
|
|
|
14815
15396
|
}
|
|
14816
15397
|
});
|
|
14817
15398
|
|
|
15399
|
+
// src/thread-history.ts
|
|
15400
|
+
function publicHistoryMessage(record, author = record.author) {
|
|
15401
|
+
return {
|
|
15402
|
+
version: 1,
|
|
15403
|
+
room_id: record.room_id,
|
|
15404
|
+
seq: record.seq,
|
|
15405
|
+
record_id: record.record_id,
|
|
15406
|
+
at: record.at,
|
|
15407
|
+
kind: "message",
|
|
15408
|
+
message_id: record.message_id,
|
|
15409
|
+
author: { identity: author.identity, display_name: author.display_name, role: author.role },
|
|
15410
|
+
category: record.category,
|
|
15411
|
+
text: record.text,
|
|
15412
|
+
...record.category === "role_briefing" ? { briefing_role: record.briefing_role } : {},
|
|
15413
|
+
...(record.category === "briefing" || record.category === "role_briefing") && record.briefing_version !== void 0 ? { briefing_version: record.briefing_version } : {},
|
|
15414
|
+
...record.category === "membership" && record.membership ? { membership: {
|
|
15415
|
+
action: record.membership.action,
|
|
15416
|
+
epoch: record.membership.epoch,
|
|
15417
|
+
...record.membership.alias === void 0 ? {} : { alias: record.membership.alias },
|
|
15418
|
+
...record.membership.role === void 0 ? {} : { role: record.membership.role }
|
|
15419
|
+
} } : {}
|
|
15420
|
+
};
|
|
15421
|
+
}
|
|
15422
|
+
function projectParticipantHistory(room, records, viewerCid, page) {
|
|
15423
|
+
const viewer = room.seats.find((seat) => seat.state === "active" && seat.identity === viewerCid);
|
|
15424
|
+
if (!viewer || room.state !== "active") throw new ThreadFailure("unauthorized");
|
|
15425
|
+
const { after = 0, limit = 200 } = ParticipantHistoryPageSchema.parse(page);
|
|
15426
|
+
const replyRows = records.filter((row) => ["message", "file", "relay_intent", "relay_result"].includes(row.kind));
|
|
15427
|
+
const output = [];
|
|
15428
|
+
let ordinal = 0, bytes = 2;
|
|
15429
|
+
for (const record of records) {
|
|
15430
|
+
if (record.kind !== "message" || record.room_id !== room.room_id) continue;
|
|
15431
|
+
let author = record.author;
|
|
15432
|
+
let thread;
|
|
15433
|
+
let thread_root;
|
|
15434
|
+
try {
|
|
15435
|
+
const association = classifyThreadAssociation(room, replyRows, record);
|
|
15436
|
+
if (association.state === "scoped") {
|
|
15437
|
+
const { root } = association;
|
|
15438
|
+
if (!activeThreadSeat(room, root.thread_root, viewerCid)) continue;
|
|
15439
|
+
const metadata = publicThreadMetadata(root, room);
|
|
15440
|
+
author = publicThreadAuthor(record, root.thread_root, room);
|
|
15441
|
+
if (record.message_id === root.message_id) thread_root = metadata;
|
|
15442
|
+
thread = { schema_version: 1, thread_id: root.message_id };
|
|
15443
|
+
} else if (record.author_alias !== void 0) {
|
|
15444
|
+
const alias = AuthorAliasSchema.parse(record.author_alias);
|
|
15445
|
+
author = { identity: alias.participant_id, display_name: alias.alias, role: record.author.role };
|
|
15446
|
+
}
|
|
15447
|
+
} catch (error) {
|
|
15448
|
+
if (error instanceof ThreadFailure || error instanceof external_exports.ZodError) continue;
|
|
15449
|
+
throw error;
|
|
15450
|
+
}
|
|
15451
|
+
ordinal += 1;
|
|
15452
|
+
if (ordinal <= after) continue;
|
|
15453
|
+
const projected = {
|
|
15454
|
+
...publicHistoryMessage(record, author),
|
|
15455
|
+
seq: ordinal,
|
|
15456
|
+
record_id: `${room.room_id}:participant:${viewer.participant_id}:${ordinal}`,
|
|
15457
|
+
...thread ? { thread } : {},
|
|
15458
|
+
...thread_root ? { thread_root } : {}
|
|
15459
|
+
};
|
|
15460
|
+
const size = Buffer.byteLength(JSON.stringify(projected), "utf8") + (output.length ? 1 : 0);
|
|
15461
|
+
if (bytes + size > MAX_HISTORY_PAGE_BYTES) {
|
|
15462
|
+
if (output.length === 0) throw new RangeError("one participant history record exceeds the page byte contract");
|
|
15463
|
+
break;
|
|
15464
|
+
}
|
|
15465
|
+
output.push(projected);
|
|
15466
|
+
bytes += size;
|
|
15467
|
+
if (output.length >= limit) break;
|
|
15468
|
+
}
|
|
15469
|
+
return output;
|
|
15470
|
+
}
|
|
15471
|
+
var PublicThreadSchema, PublicThreadMetadataSchema, PublicMessageShape, ParticipantHistoryRecordSchema, ParticipantHistoryPageSchema;
|
|
15472
|
+
var init_thread_history = __esm({
|
|
15473
|
+
"src/thread-history.ts"() {
|
|
15474
|
+
"use strict";
|
|
15475
|
+
init_zod();
|
|
15476
|
+
init_contracts();
|
|
15477
|
+
init_thread_contracts();
|
|
15478
|
+
init_threads();
|
|
15479
|
+
PublicThreadSchema = external_exports.object({ schema_version: external_exports.literal(1), thread_id: LowerCrockfordUlidSchema }).strict();
|
|
15480
|
+
PublicThreadMetadataSchema = PublicThreadSchema.extend({
|
|
15481
|
+
topic: external_exports.string(),
|
|
15482
|
+
creator: AuthorSnapshotSchema,
|
|
15483
|
+
participant_ids: external_exports.array(LowerCrockfordUlidSchema),
|
|
15484
|
+
created_at: Rfc3339Schema
|
|
15485
|
+
}).strict();
|
|
15486
|
+
PublicMessageShape = {
|
|
15487
|
+
version: external_exports.literal(1),
|
|
15488
|
+
room_id: LowerCrockfordUlidSchema,
|
|
15489
|
+
seq: external_exports.number().int().positive().safe(),
|
|
15490
|
+
record_id: external_exports.string(),
|
|
15491
|
+
at: Rfc3339Schema,
|
|
15492
|
+
kind: external_exports.literal("message"),
|
|
15493
|
+
message_id: LowerCrockfordUlidSchema,
|
|
15494
|
+
author: AuthorSnapshotSchema,
|
|
15495
|
+
category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
|
|
15496
|
+
briefing_role: RoleSchema.optional(),
|
|
15497
|
+
briefing_version: external_exports.number().int().positive().safe().optional(),
|
|
15498
|
+
membership: MembershipNoticeSchema.optional(),
|
|
15499
|
+
text: MessageTextSchema
|
|
15500
|
+
};
|
|
15501
|
+
ParticipantHistoryRecordSchema = external_exports.object({
|
|
15502
|
+
...PublicMessageShape,
|
|
15503
|
+
thread: PublicThreadSchema.optional(),
|
|
15504
|
+
thread_root: PublicThreadMetadataSchema.optional()
|
|
15505
|
+
}).strict().superRefine((row, ctx) => {
|
|
15506
|
+
const prefix = `${row.room_id}:participant:`;
|
|
15507
|
+
const participantId = row.record_id.slice(prefix.length, -(String(row.seq).length + 1));
|
|
15508
|
+
if (!row.record_id.startsWith(prefix) || !LowerCrockfordUlidSchema.safeParse(participantId).success || row.record_id !== `${prefix}${participantId}:${row.seq}`) {
|
|
15509
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["record_id"], message: "must identify the viewer and visible ordinal" });
|
|
15510
|
+
}
|
|
15511
|
+
if (row.thread_root && (!row.thread || row.thread.thread_id !== row.thread_root.thread_id || row.message_id !== row.thread.thread_id)) {
|
|
15512
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["thread_root"], message: "must identify this thread root" });
|
|
15513
|
+
}
|
|
15514
|
+
});
|
|
15515
|
+
ParticipantHistoryPageSchema = external_exports.object({
|
|
15516
|
+
after: external_exports.number().int().nonnegative().safe().optional(),
|
|
15517
|
+
limit: external_exports.number().int().positive().safe().optional()
|
|
15518
|
+
}).strict();
|
|
15519
|
+
}
|
|
15520
|
+
});
|
|
15521
|
+
|
|
14818
15522
|
// src/service.ts
|
|
14819
15523
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
14820
|
-
import { createHash as
|
|
15524
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
14821
15525
|
function byteBoundedHistoryPage(records) {
|
|
14822
15526
|
const page = [];
|
|
14823
15527
|
let bytes = 2;
|
|
@@ -14874,7 +15578,7 @@ function uniqueIdentities(identities) {
|
|
|
14874
15578
|
function currentContactIdentities(packet) {
|
|
14875
15579
|
return new Set(packet.listContacts().map((contact) => contact.container_id));
|
|
14876
15580
|
}
|
|
14877
|
-
var CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, RoomServiceError, RoomService;
|
|
15581
|
+
var MAX_ROOM_MESSAGE_BYTES, CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, RoomServiceError, RoomService;
|
|
14878
15582
|
var init_service = __esm({
|
|
14879
15583
|
"src/service.ts"() {
|
|
14880
15584
|
"use strict";
|
|
@@ -14886,6 +15590,11 @@ var init_service = __esm({
|
|
|
14886
15590
|
init_consumer_commands();
|
|
14887
15591
|
init_command_names();
|
|
14888
15592
|
init_ulid();
|
|
15593
|
+
init_reply_threading();
|
|
15594
|
+
init_thread_contracts();
|
|
15595
|
+
init_thread_history();
|
|
15596
|
+
init_threads();
|
|
15597
|
+
MAX_ROOM_MESSAGE_BYTES = 262144;
|
|
14889
15598
|
CreateInviteInputSchema = external_exports.object({
|
|
14890
15599
|
mode: InviteModeSchema,
|
|
14891
15600
|
role: RoleSchema.optional(),
|
|
@@ -15105,11 +15814,106 @@ var init_service = __esm({
|
|
|
15105
15814
|
handler: (input, context) => this.invokeConsumerCommand(roomId, definition.name, input, context)
|
|
15106
15815
|
})),
|
|
15107
15816
|
sharedCommand: (name, input, context) => this.lock(roomId, () => this.sharedCommandUnlocked(roomId, name, input, context)),
|
|
15817
|
+
startThread: async (input, context) => {
|
|
15818
|
+
const result = await this.lock(roomId, () => this.startThreadCommandUnlocked(roomId, input, context));
|
|
15819
|
+
if (result.ok === true) await this.intake.resumePending(roomId);
|
|
15820
|
+
return result;
|
|
15821
|
+
},
|
|
15108
15822
|
listMembers: (input, context) => this.lock(roomId, () => this.listMembersCommandUnlocked(roomId, input, context)),
|
|
15109
15823
|
removeMember: (input, context) => this.lock(roomId, () => this.removeMemberCommandUnlocked(roomId, input, context))
|
|
15110
15824
|
});
|
|
15111
15825
|
this.publishedConsumerRevisions.set(roomId, registered.consumer_commands_revision ?? 0);
|
|
15112
15826
|
}
|
|
15827
|
+
/** Called by the registered adapter while it owns the room mutex. */
|
|
15828
|
+
async startThreadCommandUnlocked(roomId, input, context) {
|
|
15829
|
+
const room = await this.store.load(roomId);
|
|
15830
|
+
if (room.state !== "active" || room.lifecycle_request?.state === "pending") {
|
|
15831
|
+
return { ok: false, error: "room_unavailable" };
|
|
15832
|
+
}
|
|
15833
|
+
const creator = room.seats.find((seat) => seat.state === "active" && seat.identity === context.sender_cid);
|
|
15834
|
+
if (creator === void 0 || !this.hasRuntimeCommandGrant(room, context.sender_cid, "start_thread")) {
|
|
15835
|
+
return { ok: false, error: "unauthorized" };
|
|
15836
|
+
}
|
|
15837
|
+
const parsed = StartThreadInputSchema.safeParse(input);
|
|
15838
|
+
if (!parsed.success) return { ok: false, error: "invalid_request" };
|
|
15839
|
+
const request = parsed.data;
|
|
15840
|
+
const rows = await readReplyRows(this.store, roomId);
|
|
15841
|
+
const prior = rows.filter((row) => row.kind === "message" && row.thread_root !== void 0 && row.author.identity === context.sender_cid && row.thread_root.idempotency_key === request.idempotency_key);
|
|
15842
|
+
if (prior.length > 1) throw new Error("duplicate thread roots for creator idempotency key");
|
|
15843
|
+
if (prior.length === 1) {
|
|
15844
|
+
const root2 = prior[0];
|
|
15845
|
+
if (root2.thread_root.creator_participant_id !== creator.participant_id) {
|
|
15846
|
+
return { ok: false, error: "unauthorized" };
|
|
15847
|
+
}
|
|
15848
|
+
if (root2.thread_root.fingerprint !== threadFingerprint(request)) {
|
|
15849
|
+
return { ok: false, error: "idempotency_conflict" };
|
|
15850
|
+
}
|
|
15851
|
+
return { ok: true, thread_id: root2.message_id, status: "accepted" };
|
|
15852
|
+
}
|
|
15853
|
+
let members;
|
|
15854
|
+
try {
|
|
15855
|
+
members = selectThreadMembers(room, context.sender_cid, request);
|
|
15856
|
+
} catch (error) {
|
|
15857
|
+
if (error instanceof ThreadFailure) return { ok: false, error: error.code };
|
|
15858
|
+
throw error;
|
|
15859
|
+
}
|
|
15860
|
+
const threadId = LowerCrockfordUlidSchema.parse(this.nextMessageId());
|
|
15861
|
+
const at = this.now();
|
|
15862
|
+
const threadRoot = {
|
|
15863
|
+
schema_version: 1,
|
|
15864
|
+
thread_id: threadId,
|
|
15865
|
+
topic: request.topic,
|
|
15866
|
+
creator_participant_id: creator.participant_id,
|
|
15867
|
+
members,
|
|
15868
|
+
idempotency_key: request.idempotency_key,
|
|
15869
|
+
fingerprint: threadFingerprint(request)
|
|
15870
|
+
};
|
|
15871
|
+
if (room.anonymous && creator.alias === void 0) {
|
|
15872
|
+
throw new Error("anonymous thread creator is missing its room alias");
|
|
15873
|
+
}
|
|
15874
|
+
const root = {
|
|
15875
|
+
version: 1,
|
|
15876
|
+
kind: "message",
|
|
15877
|
+
room_id: roomId,
|
|
15878
|
+
at,
|
|
15879
|
+
message_id: threadId,
|
|
15880
|
+
author: {
|
|
15881
|
+
identity: creator.identity,
|
|
15882
|
+
display_name: creator.display_name,
|
|
15883
|
+
role: creator.role
|
|
15884
|
+
},
|
|
15885
|
+
...room.anonymous ? {
|
|
15886
|
+
author_alias: { participant_id: creator.participant_id, alias: creator.alias }
|
|
15887
|
+
} : {},
|
|
15888
|
+
category: "chat",
|
|
15889
|
+
text: `Thread: ${request.topic}`,
|
|
15890
|
+
recipient_identities: members.map((member) => member.identity),
|
|
15891
|
+
scope: { thread_id: threadId },
|
|
15892
|
+
thread_root: threadRoot
|
|
15893
|
+
};
|
|
15894
|
+
const projected = {
|
|
15895
|
+
version: 1,
|
|
15896
|
+
kind: "room_msg",
|
|
15897
|
+
room_id: roomId,
|
|
15898
|
+
room_name: room.room_name,
|
|
15899
|
+
message_id: threadId,
|
|
15900
|
+
author: room.anonymous ? {
|
|
15901
|
+
identity: creator.participant_id,
|
|
15902
|
+
display_name: creator.alias,
|
|
15903
|
+
role: creator.role
|
|
15904
|
+
} : root.author,
|
|
15905
|
+
text: root.text,
|
|
15906
|
+
at,
|
|
15907
|
+
thread: { schema_version: 1, thread_id: threadId },
|
|
15908
|
+
thread_root: publicThreadMetadata(root, room)
|
|
15909
|
+
};
|
|
15910
|
+
if (Buffer.byteLength(JSON.stringify(projected), "utf8") > MAX_ROOM_MESSAGE_BYTES) {
|
|
15911
|
+
return { ok: false, error: "invalid_request" };
|
|
15912
|
+
}
|
|
15913
|
+
const appended = await this.store.append(roomId, root);
|
|
15914
|
+
if (appended.kind !== "message") throw new Error("storage returned the wrong thread root kind");
|
|
15915
|
+
return { ok: true, thread_id: appended.message_id, status: "accepted" };
|
|
15916
|
+
}
|
|
15113
15917
|
/** The SDK supplies authenticated context; arguments never select another room. */
|
|
15114
15918
|
async sharedCommandUnlocked(roomId, name, input, context) {
|
|
15115
15919
|
if (!SHARED_ROOM_COMMANDS.includes(name) || input === null || typeof input !== "object" || Array.isArray(input) || Object.hasOwn(input, "room_id")) return { ok: false, error: "invalid_request" };
|
|
@@ -15150,8 +15954,30 @@ var init_service = __esm({
|
|
|
15150
15954
|
try {
|
|
15151
15955
|
return await this.commandScope.run(scope, async () => {
|
|
15152
15956
|
try {
|
|
15957
|
+
if (name === "room.history") {
|
|
15958
|
+
const request = ParticipantHistoryPageSchema.extend({ view: external_exports.literal("participant").optional() }).safeParse(input);
|
|
15959
|
+
if (!request.success) return { ok: false, error: "invalid_request" };
|
|
15960
|
+
const { view: _view, ...page } = request.data;
|
|
15961
|
+
return { ok: true, result: JSON.parse(JSON.stringify(await this.participantHistory(roomId, context.sender_cid, page))) };
|
|
15962
|
+
}
|
|
15963
|
+
if (name === "room.show" || name === "room.participants") {
|
|
15964
|
+
external_exports.object({}).strict().parse(input);
|
|
15965
|
+
const result2 = name === "room.show" ? {
|
|
15966
|
+
room_id: room.room_id,
|
|
15967
|
+
room_name: room.room_name,
|
|
15968
|
+
state: room.state,
|
|
15969
|
+
mission: { goal: room.mission.goal, briefing: room.mission.briefing, briefing_version: room.mission.briefing_version },
|
|
15970
|
+
anonymous: room.anonymous,
|
|
15971
|
+
quiet_membership: room.quiet_membership,
|
|
15972
|
+
membership_epoch: room.membership_epoch
|
|
15973
|
+
} : room.seats.map(({ participant_id, role, state }) => ({ participant_id, role, state }));
|
|
15974
|
+
return { ok: true, result: JSON.parse(JSON.stringify(result2)) };
|
|
15975
|
+
}
|
|
15153
15976
|
const routes = name === "room.accept" ? createPrivateServiceRoutes(this) : createServiceRoutes(this);
|
|
15154
15977
|
const result = await routes[name].run({ ...input, room_id: roomId });
|
|
15978
|
+
if (name === "room.message" || name === "room.say") {
|
|
15979
|
+
return { ok: true, result: { message_id: result.message_id, accepted: true } };
|
|
15980
|
+
}
|
|
15155
15981
|
return { ok: true, result: JSON.parse(JSON.stringify(result)) };
|
|
15156
15982
|
} catch (error) {
|
|
15157
15983
|
return { ok: false, error: classifyServiceError(error) };
|
|
@@ -15322,7 +16148,7 @@ var init_service = __esm({
|
|
|
15322
16148
|
} catch {
|
|
15323
16149
|
throw new RoomServiceError("external invite is invalid or exceeds the 48 KiB decoded limit");
|
|
15324
16150
|
}
|
|
15325
|
-
const digest =
|
|
16151
|
+
const digest = createHash5("sha256").update(decoded).digest("hex");
|
|
15326
16152
|
const receipt = await this.lock(id, async () => {
|
|
15327
16153
|
const room = await this.store.load(id);
|
|
15328
16154
|
this.assertMutable(room, "accept an external invite for");
|
|
@@ -15335,7 +16161,7 @@ var init_service = __esm({
|
|
|
15335
16161
|
} catch {
|
|
15336
16162
|
throw new RoomServiceError("external invite was rejected");
|
|
15337
16163
|
}
|
|
15338
|
-
const cid =
|
|
16164
|
+
const cid = ContainerIdSchema2.parse(added.container_id);
|
|
15339
16165
|
if (request.expected_cid !== void 0 && cid !== request.expected_cid) {
|
|
15340
16166
|
throw new RoomServiceError("external invite inviter CID did not match --expected-cid");
|
|
15341
16167
|
}
|
|
@@ -15927,29 +16753,38 @@ var init_service = __esm({
|
|
|
15927
16753
|
return saved.command_grants.map((grant) => ({ ...grant }));
|
|
15928
16754
|
});
|
|
15929
16755
|
}
|
|
16756
|
+
/** Internal authenticated API; numeric cursors count only this viewer's visible messages. */
|
|
16757
|
+
async participantHistory(roomId, viewerCid, page = {}) {
|
|
16758
|
+
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
16759
|
+
const request = ParticipantHistoryPageSchema.parse(page);
|
|
16760
|
+
return this.lock(id, async () => {
|
|
16761
|
+
const room = await this.store.load(id);
|
|
16762
|
+
if (room.state !== "active" || !room.seats.some((seat) => seat.state === "active" && seat.identity === viewerCid)) {
|
|
16763
|
+
throw new ThreadFailure("unauthorized");
|
|
16764
|
+
}
|
|
16765
|
+
const records = [];
|
|
16766
|
+
let after = 0;
|
|
16767
|
+
for (; ; ) {
|
|
16768
|
+
const batch = await this.store.read(id, { after, limit: JOURNAL_WORK_BATCH_SIZE2 });
|
|
16769
|
+
if (batch.length === 0) break;
|
|
16770
|
+
const last = batch[batch.length - 1];
|
|
16771
|
+
if (last.seq <= after || batch.some((row) => row.room_id !== id)) throw new Error("invalid participant history archive page");
|
|
16772
|
+
records.push(...batch);
|
|
16773
|
+
after = last.seq;
|
|
16774
|
+
}
|
|
16775
|
+
return projectParticipantHistory(room, records, viewerCid, request);
|
|
16776
|
+
});
|
|
16777
|
+
}
|
|
15930
16778
|
async history(roomId, options = {}) {
|
|
15931
16779
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
15932
16780
|
const { view, ...page } = HistoryOptionsSchema.parse(options);
|
|
15933
16781
|
const records = view === "participant" ? await queryStore2(this.store, id, { kind: "message", after: page.after, limit: page.limit }) : await this.store.read(id, page);
|
|
15934
16782
|
if (view !== "participant") return byteBoundedHistoryPage(records);
|
|
15935
|
-
const projected = records.filter((record) => record.kind === "message").map((record) => {
|
|
15936
|
-
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
source_wire_id: _sourceWire,
|
|
15941
|
-
source_reply_to: _sourceReplyTo,
|
|
15942
|
-
...rest
|
|
15943
|
-
} = record;
|
|
15944
|
-
return {
|
|
15945
|
-
...rest,
|
|
15946
|
-
author: author_alias === void 0 ? record.author : {
|
|
15947
|
-
identity: author_alias.participant_id,
|
|
15948
|
-
display_name: author_alias.alias,
|
|
15949
|
-
role: record.author.role
|
|
15950
|
-
}
|
|
15951
|
-
};
|
|
15952
|
-
});
|
|
16783
|
+
const projected = records.filter((record) => record.kind === "message").map((record) => publicHistoryMessage(record, record.author_alias === void 0 ? record.author : {
|
|
16784
|
+
identity: record.author_alias.participant_id,
|
|
16785
|
+
display_name: record.author_alias.alias,
|
|
16786
|
+
role: record.author.role
|
|
16787
|
+
}));
|
|
15953
16788
|
return byteBoundedHistoryPage(projected.slice(0, page.limit ?? Number.MAX_SAFE_INTEGER));
|
|
15954
16789
|
}
|
|
15955
16790
|
/**
|
|
@@ -16509,7 +17344,7 @@ import * as nodeFs2 from "node:fs";
|
|
|
16509
17344
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
16510
17345
|
import { basename, dirname as dirname3, join as join4 } from "node:path";
|
|
16511
17346
|
import Database from "better-sqlite3";
|
|
16512
|
-
var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
|
|
17347
|
+
var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, SQLITE_V2_EXTENSION_DDL, CoworkStorageError, RoomQueue, CoworkStore;
|
|
16513
17348
|
var init_storage = __esm({
|
|
16514
17349
|
"src/storage.ts"() {
|
|
16515
17350
|
"use strict";
|
|
@@ -16518,9 +17353,24 @@ var init_storage = __esm({
|
|
|
16518
17353
|
DIRECTORY_MODE2 = 448;
|
|
16519
17354
|
FILE_MODE2 = 384;
|
|
16520
17355
|
NO_FOLLOW2 = nodeFs2.constants.O_NOFOLLOW ?? 0;
|
|
16521
|
-
SQLITE_SCHEMA_VERSION =
|
|
17356
|
+
SQLITE_SCHEMA_VERSION = 2;
|
|
16522
17357
|
DEFAULT_WORK_BATCH_SIZE = 64;
|
|
16523
17358
|
utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
17359
|
+
SQLITE_V2_EXTENSION_DDL = `
|
|
17360
|
+
CREATE UNIQUE INDEX IF NOT EXISTS records_thread_creation_key
|
|
17361
|
+
ON records(json_extract(payload_json,'$.author.identity'),
|
|
17362
|
+
json_extract(payload_json,'$.thread_root.idempotency_key'))
|
|
17363
|
+
WHERE kind='message' AND json_type(payload_json,'$.thread_root')='object';
|
|
17364
|
+
CREATE INDEX IF NOT EXISTS records_thread_id
|
|
17365
|
+
ON records(json_extract(payload_json,'$.scope.thread_id'))
|
|
17366
|
+
WHERE kind='message' AND json_type(payload_json,'$.scope')='object';
|
|
17367
|
+
DROP INDEX IF EXISTS records_source_message;
|
|
17368
|
+
DROP INDEX IF EXISTS records_source_file;
|
|
17369
|
+
CREATE UNIQUE INDEX records_source_message ON records(source_msg_id)
|
|
17370
|
+
WHERE source_msg_id IS NOT NULL;
|
|
17371
|
+
CREATE UNIQUE INDEX records_source_file ON records(source_file_id)
|
|
17372
|
+
WHERE source_file_id IS NOT NULL;
|
|
17373
|
+
`;
|
|
16524
17374
|
CoworkStorageError = class extends Error {
|
|
16525
17375
|
constructor(message, options) {
|
|
16526
17376
|
super(message, options);
|
|
@@ -16924,55 +17774,65 @@ var init_storage = __esm({
|
|
|
16924
17774
|
try {
|
|
16925
17775
|
this.secureSqliteFiles(path);
|
|
16926
17776
|
db = new Database(path, { fileMustExist: !create });
|
|
17777
|
+
const activeDb = db;
|
|
16927
17778
|
if (guardFd !== void 0) this.validateOpenPath(guardFd, path, "room archive database", "file", true);
|
|
16928
17779
|
this.fs.chmodSync(path, FILE_MODE2);
|
|
16929
|
-
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
17780
|
+
const existingVersion = create ? void 0 : activeDb.pragma("user_version", { simple: true });
|
|
17781
|
+
if (existingVersion !== void 0 && existingVersion !== 1 && existingVersion !== SQLITE_SCHEMA_VERSION) {
|
|
17782
|
+
throw new CoworkStorageError(`unsupported room archive schema version ${existingVersion}`);
|
|
17783
|
+
}
|
|
17784
|
+
activeDb.pragma("journal_mode = WAL");
|
|
17785
|
+
activeDb.pragma("synchronous = FULL");
|
|
17786
|
+
activeDb.pragma("foreign_keys = ON");
|
|
17787
|
+
activeDb.pragma("busy_timeout = 5000");
|
|
16933
17788
|
if (create) {
|
|
16934
|
-
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
16951
|
-
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
16960
|
-
|
|
16961
|
-
|
|
16962
|
-
|
|
17789
|
+
activeDb.transaction(() => {
|
|
17790
|
+
activeDb.exec(`CREATE TABLE records (
|
|
17791
|
+
seq INTEGER PRIMARY KEY, record_id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, at TEXT NOT NULL,
|
|
17792
|
+
payload_json TEXT NOT NULL, blob_path TEXT, message_id TEXT, file_id TEXT, intent_record_id TEXT,
|
|
17793
|
+
recipient_identity TEXT, source_msg_id INTEGER, source_file_id INTEGER, category TEXT,
|
|
17794
|
+
briefing_role TEXT, briefing_version INTEGER, membership_epoch INTEGER
|
|
17795
|
+
);
|
|
17796
|
+
CREATE TABLE record_recipients (
|
|
17797
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
17798
|
+
recipient_identity TEXT NOT NULL, category TEXT, briefing_role TEXT,
|
|
17799
|
+
briefing_version INTEGER, PRIMARY KEY(record_seq, recipient_identity)
|
|
17800
|
+
);
|
|
17801
|
+
CREATE TABLE relay_intent_work (
|
|
17802
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
17803
|
+
recipient_identity TEXT NOT NULL, PRIMARY KEY(record_seq, recipient_identity)
|
|
17804
|
+
);
|
|
17805
|
+
CREATE INDEX relay_work_source ON relay_intent_work(record_seq, recipient_identity);
|
|
17806
|
+
CREATE INDEX records_kind_seq ON records(kind, seq);
|
|
17807
|
+
CREATE INDEX records_message ON records(message_id, kind, seq);
|
|
17808
|
+
CREATE INDEX records_file ON records(file_id, kind, seq);
|
|
17809
|
+
CREATE INDEX records_intent_result ON records(intent_record_id, kind);
|
|
17810
|
+
CREATE INDEX records_relay_recipient ON records(kind, recipient_identity, seq);
|
|
17811
|
+
CREATE UNIQUE INDEX records_source_message ON records(source_msg_id) WHERE kind='message' AND source_msg_id IS NOT NULL;
|
|
17812
|
+
CREATE UNIQUE INDEX records_source_file ON records(source_file_id) WHERE kind='file' AND source_file_id IS NOT NULL;
|
|
17813
|
+
CREATE INDEX records_briefing ON records(category, briefing_role, briefing_version, seq);
|
|
17814
|
+
CREATE INDEX records_membership_epoch ON records(category, membership_epoch);
|
|
17815
|
+
CREATE INDEX recipients_identity ON record_recipients(recipient_identity, record_seq);
|
|
17816
|
+
CREATE INDEX recipients_briefing_delivery ON record_recipients
|
|
17817
|
+
(recipient_identity, category, briefing_role, briefing_version, record_seq);`);
|
|
17818
|
+
activeDb.exec(SQLITE_V2_EXTENSION_DDL);
|
|
17819
|
+
activeDb.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
17820
|
+
}).immediate();
|
|
16963
17821
|
this.reconciledBlobRooms.add(roomId);
|
|
16964
17822
|
} else {
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
17823
|
+
if (existingVersion === 1) {
|
|
17824
|
+
activeDb.transaction(() => {
|
|
17825
|
+
activeDb.exec(SQLITE_V2_EXTENSION_DDL);
|
|
17826
|
+
activeDb.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
17827
|
+
}).immediate();
|
|
16968
17828
|
}
|
|
16969
17829
|
if (!this.reconciledBlobRooms.has(roomId)) {
|
|
16970
|
-
this.reconcileBlobDirectory(roomId,
|
|
17830
|
+
this.reconcileBlobDirectory(roomId, activeDb);
|
|
16971
17831
|
this.reconciledBlobRooms.add(roomId);
|
|
16972
17832
|
}
|
|
16973
17833
|
}
|
|
16974
17834
|
this.secureSqliteFiles(path);
|
|
16975
|
-
const result = work(
|
|
17835
|
+
const result = work(activeDb);
|
|
16976
17836
|
this.secureSqliteFiles(path);
|
|
16977
17837
|
return result;
|
|
16978
17838
|
} catch (error) {
|