@ours.network/cowork 1.1.4 → 1.3.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 +232 -14
- package/dist/daemon.js +1053 -177
- package/dist/web/assets/app.js +9 -9
- package/docs/05-room-workflow.md +80 -2
- 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
|
@@ -11625,6 +11625,9 @@ var init_ours_runtime = __esm({
|
|
|
11625
11625
|
});
|
|
11626
11626
|
|
|
11627
11627
|
// src/command-names.ts
|
|
11628
|
+
function commandGrantMatches(pattern, command) {
|
|
11629
|
+
return pattern === command || pattern === "*" || pattern.endsWith(".*") && command.startsWith(pattern.slice(0, -1)) && command.length > pattern.length - 1;
|
|
11630
|
+
}
|
|
11628
11631
|
var SHARED_ROOM_COMMANDS, RUNTIME_COMMAND_NAMES;
|
|
11629
11632
|
var init_command_names = __esm({
|
|
11630
11633
|
"src/command-names.ts"() {
|
|
@@ -11656,6 +11659,7 @@ var init_command_names = __esm({
|
|
|
11656
11659
|
"room.role.rest.remove"
|
|
11657
11660
|
];
|
|
11658
11661
|
RUNTIME_COMMAND_NAMES = [
|
|
11662
|
+
"start_thread",
|
|
11659
11663
|
"list-members",
|
|
11660
11664
|
"remove-member",
|
|
11661
11665
|
...SHARED_ROOM_COMMANDS
|
|
@@ -11663,6 +11667,93 @@ var init_command_names = __esm({
|
|
|
11663
11667
|
}
|
|
11664
11668
|
});
|
|
11665
11669
|
|
|
11670
|
+
// src/thread-contracts.ts
|
|
11671
|
+
var ParticipantIdSchema, ContainerIdSchema, TopicTextSchema, InputTopicSchema, StoredTopicSchema, IdempotencyKeySchema, ThreadMemberSchema, ThreadRootSchema, ThreadScopeSchema, StartThreadInputSchema, ThreadFailure;
|
|
11672
|
+
var init_thread_contracts = __esm({
|
|
11673
|
+
"src/thread-contracts.ts"() {
|
|
11674
|
+
"use strict";
|
|
11675
|
+
init_zod();
|
|
11676
|
+
ParticipantIdSchema = external_exports.string().regex(
|
|
11677
|
+
/^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
|
|
11678
|
+
"must be a 26-character lowercase Crockford ULID"
|
|
11679
|
+
);
|
|
11680
|
+
ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
|
|
11681
|
+
TopicTextSchema = external_exports.string().refine(
|
|
11682
|
+
(value) => Array.from(value).length >= 1 && Array.from(value).length <= 120 && value.trim().length > 0 && !/[\p{Cc}\p{Cf}]/u.test(value),
|
|
11683
|
+
"topic must contain 1-120 Unicode characters without control or format characters"
|
|
11684
|
+
);
|
|
11685
|
+
InputTopicSchema = TopicTextSchema.transform((value) => value.trim());
|
|
11686
|
+
StoredTopicSchema = TopicTextSchema.refine(
|
|
11687
|
+
(value) => value === value.trim(),
|
|
11688
|
+
"stored topic must already be trimmed"
|
|
11689
|
+
);
|
|
11690
|
+
IdempotencyKeySchema = external_exports.string().regex(
|
|
11691
|
+
/^[A-Za-z0-9._:-]{1,128}$/,
|
|
11692
|
+
"must contain 1-128 portable idempotency-key characters"
|
|
11693
|
+
);
|
|
11694
|
+
ThreadMemberSchema = external_exports.object({
|
|
11695
|
+
participant_id: ParticipantIdSchema,
|
|
11696
|
+
identity: ContainerIdSchema
|
|
11697
|
+
}).strict();
|
|
11698
|
+
ThreadRootSchema = external_exports.object({
|
|
11699
|
+
schema_version: external_exports.literal(1),
|
|
11700
|
+
thread_id: ParticipantIdSchema,
|
|
11701
|
+
topic: StoredTopicSchema,
|
|
11702
|
+
creator_participant_id: ParticipantIdSchema,
|
|
11703
|
+
members: external_exports.array(ThreadMemberSchema).min(1),
|
|
11704
|
+
idempotency_key: IdempotencyKeySchema,
|
|
11705
|
+
fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase SHA-256 digest")
|
|
11706
|
+
}).strict().superRefine((root, context) => {
|
|
11707
|
+
const participantIds = /* @__PURE__ */ new Set();
|
|
11708
|
+
const identities = /* @__PURE__ */ new Set();
|
|
11709
|
+
for (const [index, member] of root.members.entries()) {
|
|
11710
|
+
if (participantIds.has(member.participant_id)) {
|
|
11711
|
+
context.addIssue({
|
|
11712
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11713
|
+
path: ["members", index, "participant_id"],
|
|
11714
|
+
message: "thread member participant IDs must be unique"
|
|
11715
|
+
});
|
|
11716
|
+
}
|
|
11717
|
+
participantIds.add(member.participant_id);
|
|
11718
|
+
if (identities.has(member.identity)) {
|
|
11719
|
+
context.addIssue({
|
|
11720
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11721
|
+
path: ["members", index, "identity"],
|
|
11722
|
+
message: "thread member identities must be unique"
|
|
11723
|
+
});
|
|
11724
|
+
}
|
|
11725
|
+
identities.add(member.identity);
|
|
11726
|
+
}
|
|
11727
|
+
if (!participantIds.has(root.creator_participant_id)) {
|
|
11728
|
+
context.addIssue({
|
|
11729
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11730
|
+
path: ["creator_participant_id"],
|
|
11731
|
+
message: "thread creator must be a member"
|
|
11732
|
+
});
|
|
11733
|
+
}
|
|
11734
|
+
});
|
|
11735
|
+
ThreadScopeSchema = external_exports.object({
|
|
11736
|
+
thread_id: ParticipantIdSchema,
|
|
11737
|
+
parent_key: external_exports.string().regex(
|
|
11738
|
+
/^(?:message|file):[0-7][0-9a-hjkmnp-tv-z]{25}$/,
|
|
11739
|
+
"must identify an immediate message or file parent"
|
|
11740
|
+
).optional()
|
|
11741
|
+
}).strict();
|
|
11742
|
+
StartThreadInputSchema = external_exports.object({
|
|
11743
|
+
topic: InputTopicSchema,
|
|
11744
|
+
participant_ids: external_exports.array(ParticipantIdSchema).min(1),
|
|
11745
|
+
idempotency_key: IdempotencyKeySchema
|
|
11746
|
+
}).strict();
|
|
11747
|
+
ThreadFailure = class extends Error {
|
|
11748
|
+
constructor(code) {
|
|
11749
|
+
super(code);
|
|
11750
|
+
this.code = code;
|
|
11751
|
+
this.name = "ThreadFailure";
|
|
11752
|
+
}
|
|
11753
|
+
};
|
|
11754
|
+
}
|
|
11755
|
+
});
|
|
11756
|
+
|
|
11666
11757
|
// src/contracts.ts
|
|
11667
11758
|
import { createHash } from "node:crypto";
|
|
11668
11759
|
function utf8Bounded(label, maximumBytes) {
|
|
@@ -11804,6 +11895,18 @@ function refineRelaySubject(record, context) {
|
|
|
11804
11895
|
});
|
|
11805
11896
|
}
|
|
11806
11897
|
}
|
|
11898
|
+
function refineIntakeRejection(record, context) {
|
|
11899
|
+
if (record.kind !== "intake_rejection") return;
|
|
11900
|
+
const hasMessage = record.source_msg_id !== void 0;
|
|
11901
|
+
const hasFile = record.source_file_id !== void 0;
|
|
11902
|
+
if (hasMessage === hasFile || (record.source_kind === "message" ? !hasMessage : !hasFile)) {
|
|
11903
|
+
context.addIssue({
|
|
11904
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11905
|
+
path: ["source_kind"],
|
|
11906
|
+
message: "intake rejections require exactly one numeric source field matching source_kind"
|
|
11907
|
+
});
|
|
11908
|
+
}
|
|
11909
|
+
}
|
|
11807
11910
|
function refineFileRecord(record, context) {
|
|
11808
11911
|
if (record.kind !== "file" || record.data_base64 === void 0) return;
|
|
11809
11912
|
const bytes = Buffer.from(record.data_base64, "base64");
|
|
@@ -11854,13 +11957,110 @@ function refineMessageCategory(message, context) {
|
|
|
11854
11957
|
}
|
|
11855
11958
|
}
|
|
11856
11959
|
}
|
|
11857
|
-
|
|
11960
|
+
function refineMessageThread(message, context) {
|
|
11961
|
+
if (message.thread_root !== void 0) {
|
|
11962
|
+
if (message.scope === void 0) {
|
|
11963
|
+
context.addIssue({
|
|
11964
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11965
|
+
path: ["scope"],
|
|
11966
|
+
message: "thread root messages require scope"
|
|
11967
|
+
});
|
|
11968
|
+
return;
|
|
11969
|
+
}
|
|
11970
|
+
if (message.scope.thread_id !== message.message_id || message.thread_root.thread_id !== message.message_id) {
|
|
11971
|
+
context.addIssue({
|
|
11972
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11973
|
+
path: ["thread_root", "thread_id"],
|
|
11974
|
+
message: "thread root thread_id and scope thread_id must equal message_id"
|
|
11975
|
+
});
|
|
11976
|
+
}
|
|
11977
|
+
if (message.scope.parent_key !== void 0) {
|
|
11978
|
+
context.addIssue({
|
|
11979
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11980
|
+
path: ["scope", "parent_key"],
|
|
11981
|
+
message: "parent_key is forbidden on thread root messages"
|
|
11982
|
+
});
|
|
11983
|
+
}
|
|
11984
|
+
if (message.category !== "chat") {
|
|
11985
|
+
context.addIssue({
|
|
11986
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11987
|
+
path: ["category"],
|
|
11988
|
+
message: "thread root messages must be chat messages"
|
|
11989
|
+
});
|
|
11990
|
+
}
|
|
11991
|
+
if (message.text !== `Thread: ${message.thread_root.topic}`) {
|
|
11992
|
+
context.addIssue({
|
|
11993
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11994
|
+
path: ["text"],
|
|
11995
|
+
message: "thread root message text must identify its topic"
|
|
11996
|
+
});
|
|
11997
|
+
}
|
|
11998
|
+
const creator = message.thread_root.members.find(
|
|
11999
|
+
(member) => member.participant_id === message.thread_root?.creator_participant_id
|
|
12000
|
+
);
|
|
12001
|
+
if (creator?.identity !== message.author.identity) {
|
|
12002
|
+
context.addIssue({
|
|
12003
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12004
|
+
path: ["thread_root", "creator_participant_id"],
|
|
12005
|
+
message: "thread root creator must match the message author"
|
|
12006
|
+
});
|
|
12007
|
+
}
|
|
12008
|
+
if (message.author_alias !== void 0 && message.author_alias.participant_id !== message.thread_root.creator_participant_id) {
|
|
12009
|
+
context.addIssue({
|
|
12010
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12011
|
+
path: ["author_alias", "participant_id"],
|
|
12012
|
+
message: "thread root author alias must identify the creator"
|
|
12013
|
+
});
|
|
12014
|
+
}
|
|
12015
|
+
for (const field of ["source_msg_id", "source_wire_id", "source_reply_to"]) {
|
|
12016
|
+
if (message[field] !== void 0) {
|
|
12017
|
+
context.addIssue({
|
|
12018
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12019
|
+
path: [field],
|
|
12020
|
+
message: `${field} is forbidden on thread root messages`
|
|
12021
|
+
});
|
|
12022
|
+
}
|
|
12023
|
+
}
|
|
12024
|
+
return;
|
|
12025
|
+
}
|
|
12026
|
+
if (message.scope === void 0) return;
|
|
12027
|
+
if (message.scope.parent_key === void 0) {
|
|
12028
|
+
context.addIssue({
|
|
12029
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12030
|
+
path: ["scope", "parent_key"],
|
|
12031
|
+
message: "thread descendants require an immediate parent_key"
|
|
12032
|
+
});
|
|
12033
|
+
}
|
|
12034
|
+
if (message.scope.thread_id === message.message_id) {
|
|
12035
|
+
context.addIssue({
|
|
12036
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12037
|
+
path: ["scope", "thread_id"],
|
|
12038
|
+
message: "thread descendant thread_id must identify a distinct root message"
|
|
12039
|
+
});
|
|
12040
|
+
}
|
|
12041
|
+
if (message.source_reply_to === void 0) {
|
|
12042
|
+
context.addIssue({
|
|
12043
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12044
|
+
path: ["source_reply_to"],
|
|
12045
|
+
message: "thread descendants require source_reply_to"
|
|
12046
|
+
});
|
|
12047
|
+
}
|
|
12048
|
+
if (message.category !== "chat") {
|
|
12049
|
+
context.addIssue({
|
|
12050
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12051
|
+
path: ["category"],
|
|
12052
|
+
message: "thread descendants must be chat messages"
|
|
12053
|
+
});
|
|
12054
|
+
}
|
|
12055
|
+
}
|
|
12056
|
+
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, RuntimeCommandNamespacePatternSchema, RuntimeCommandGrantPatternSchema, 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
12057
|
var init_contracts = __esm({
|
|
11859
12058
|
"src/contracts.ts"() {
|
|
11860
12059
|
"use strict";
|
|
11861
12060
|
init_zod();
|
|
11862
12061
|
init_consumer_commands();
|
|
11863
12062
|
init_command_names();
|
|
12063
|
+
init_thread_contracts();
|
|
11864
12064
|
MAX_TEXT_BYTES = 262144;
|
|
11865
12065
|
MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11866
12066
|
MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
|
|
@@ -11877,11 +12077,17 @@ var init_contracts = __esm({
|
|
|
11877
12077
|
/^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
|
|
11878
12078
|
"must be a 26-character lowercase Crockford ULID"
|
|
11879
12079
|
);
|
|
11880
|
-
|
|
12080
|
+
ContainerIdSchema2 = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
|
|
11881
12081
|
RuntimeCommandNameSchema = external_exports.union([external_exports.enum(RUNTIME_COMMAND_NAMES), ConsumerCommandNameSchema]);
|
|
12082
|
+
RuntimeCommandNamespacePatternSchema = external_exports.string().max(128).regex(/^(?:[a-z0-9][a-z0-9-]*\.)+\*$/);
|
|
12083
|
+
RuntimeCommandGrantPatternSchema = external_exports.union([
|
|
12084
|
+
RuntimeCommandNameSchema,
|
|
12085
|
+
external_exports.literal("*"),
|
|
12086
|
+
RuntimeCommandNamespacePatternSchema
|
|
12087
|
+
]);
|
|
11882
12088
|
RuntimeCommandGrantSchema = external_exports.object({
|
|
11883
|
-
caller_cid:
|
|
11884
|
-
command:
|
|
12089
|
+
caller_cid: ContainerIdSchema2,
|
|
12090
|
+
command: RuntimeCommandGrantPatternSchema
|
|
11885
12091
|
}).strict();
|
|
11886
12092
|
Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
|
|
11887
12093
|
RoomNameSchema = external_exports.string().refine(
|
|
@@ -11911,7 +12117,7 @@ var init_contracts = __esm({
|
|
|
11911
12117
|
ROOM_ROLE = "room";
|
|
11912
12118
|
RuntimeRoleCommandGrantSchema = external_exports.object({
|
|
11913
12119
|
role: RoleSchema,
|
|
11914
|
-
commands: external_exports.array(
|
|
12120
|
+
commands: external_exports.array(RuntimeCommandGrantPatternSchema).superRefine((commands, context) => {
|
|
11915
12121
|
const seen = /* @__PURE__ */ new Set();
|
|
11916
12122
|
for (const [index, command] of commands.entries()) {
|
|
11917
12123
|
if (seen.has(command)) {
|
|
@@ -12159,7 +12365,7 @@ var init_contracts = __esm({
|
|
|
12159
12365
|
lifecycle_request: external_exports.object({
|
|
12160
12366
|
request_id: external_exports.string().min(1).max(256),
|
|
12161
12367
|
command: external_exports.enum(["room.close", "room.delete"]),
|
|
12162
|
-
caller_cid:
|
|
12368
|
+
caller_cid: ContainerIdSchema2,
|
|
12163
12369
|
accepted_at: Rfc3339Schema,
|
|
12164
12370
|
state: external_exports.enum(["pending", "failed", "completed"]),
|
|
12165
12371
|
error: external_exports.literal("lifecycle_failed").optional()
|
|
@@ -12310,7 +12516,7 @@ var init_contracts = __esm({
|
|
|
12310
12516
|
(value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
|
|
12311
12517
|
`invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
|
|
12312
12518
|
),
|
|
12313
|
-
expected_cid:
|
|
12519
|
+
expected_cid: ContainerIdSchema2.optional()
|
|
12314
12520
|
}).strict();
|
|
12315
12521
|
ListMembersCommandInputSchema = external_exports.object({}).strict();
|
|
12316
12522
|
CommandIdempotencyKeySchema = external_exports.string().regex(
|
|
@@ -12387,7 +12593,9 @@ var init_contracts = __esm({
|
|
|
12387
12593
|
}),
|
|
12388
12594
|
source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
12389
12595
|
source_wire_id: NonEmptyStringSchema.optional(),
|
|
12390
|
-
source_reply_to: ReplyReferenceSchema.optional()
|
|
12596
|
+
source_reply_to: ReplyReferenceSchema.optional(),
|
|
12597
|
+
scope: ThreadScopeSchema.optional(),
|
|
12598
|
+
thread_root: ThreadRootSchema.optional()
|
|
12391
12599
|
};
|
|
12392
12600
|
RelayIntentShape = {
|
|
12393
12601
|
kind: external_exports.literal("relay_intent"),
|
|
@@ -12395,7 +12603,12 @@ var init_contracts = __esm({
|
|
|
12395
12603
|
file_id: LowerCrockfordUlidSchema.optional(),
|
|
12396
12604
|
recipient_identity: NonEmptyStringSchema
|
|
12397
12605
|
};
|
|
12398
|
-
RelayResultStatusSchema = external_exports.enum([
|
|
12606
|
+
RelayResultStatusSchema = external_exports.enum([
|
|
12607
|
+
"queued",
|
|
12608
|
+
"send_failed",
|
|
12609
|
+
"skipped_removed",
|
|
12610
|
+
"skipped_reply_unavailable"
|
|
12611
|
+
]);
|
|
12399
12612
|
RelayResultShape = {
|
|
12400
12613
|
kind: external_exports.literal("relay_result"),
|
|
12401
12614
|
intent_record_id: NonEmptyStringSchema,
|
|
@@ -12433,6 +12646,18 @@ var init_contracts = __esm({
|
|
|
12433
12646
|
source_wire_id: NonEmptyStringSchema.optional(),
|
|
12434
12647
|
source_reply_to: ReplyReferenceSchema.optional()
|
|
12435
12648
|
};
|
|
12649
|
+
IntakeRejectionShape = {
|
|
12650
|
+
kind: external_exports.literal("intake_rejection"),
|
|
12651
|
+
source_kind: external_exports.enum(["message", "file"]),
|
|
12652
|
+
source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
12653
|
+
source_file_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
12654
|
+
source_wire_id: NonEmptyStringSchema,
|
|
12655
|
+
sender_identity: NonEmptyStringSchema,
|
|
12656
|
+
sender_participant_id: LowerCrockfordUlidSchema,
|
|
12657
|
+
fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
12658
|
+
error: external_exports.enum(["reply_target_unavailable", "thread_files_unsupported"]),
|
|
12659
|
+
notification_attempt_claimed: external_exports.literal(true)
|
|
12660
|
+
};
|
|
12436
12661
|
MembershipIntentShape = {
|
|
12437
12662
|
kind: external_exports.literal("membership_intent"),
|
|
12438
12663
|
action: external_exports.enum(["remove"]),
|
|
@@ -12470,6 +12695,7 @@ var init_contracts = __esm({
|
|
|
12470
12695
|
FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
|
|
12471
12696
|
RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
|
|
12472
12697
|
RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
|
|
12698
|
+
IntakeRejectionRecordSchema = external_exports.object({ ...RecordCommonShape, ...IntakeRejectionShape }).strict();
|
|
12473
12699
|
MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
|
|
12474
12700
|
MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
|
|
12475
12701
|
CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
|
|
@@ -12479,6 +12705,7 @@ var init_contracts = __esm({
|
|
|
12479
12705
|
FileRecordSchema,
|
|
12480
12706
|
RelayIntentRecordSchema,
|
|
12481
12707
|
RelayResultRecordSchema,
|
|
12708
|
+
IntakeRejectionRecordSchema,
|
|
12482
12709
|
MembershipIntentRecordSchema,
|
|
12483
12710
|
MembershipResultRecordSchema,
|
|
12484
12711
|
CloseNoticeIntentRecordSchema,
|
|
@@ -12492,8 +12719,12 @@ var init_contracts = __esm({
|
|
|
12492
12719
|
message: 'record_id must equal room_id + ":" + seq'
|
|
12493
12720
|
});
|
|
12494
12721
|
}
|
|
12495
|
-
if (record.kind === "message")
|
|
12722
|
+
if (record.kind === "message") {
|
|
12723
|
+
refineMessageCategory(record, context);
|
|
12724
|
+
refineMessageThread(record, context);
|
|
12725
|
+
}
|
|
12496
12726
|
refineRelaySubject(record, context);
|
|
12727
|
+
refineIntakeRejection(record, context);
|
|
12497
12728
|
refineFileRecord(record, context);
|
|
12498
12729
|
});
|
|
12499
12730
|
AppendRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
@@ -12501,11 +12732,16 @@ var init_contracts = __esm({
|
|
|
12501
12732
|
external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
|
|
12502
12733
|
external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
|
|
12503
12734
|
external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
|
|
12735
|
+
external_exports.object({ ...AppendCommonShape, ...IntakeRejectionShape }).strict(),
|
|
12504
12736
|
external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
|
|
12505
12737
|
external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
|
|
12506
12738
|
]).superRefine((record, context) => {
|
|
12507
|
-
if (record.kind === "message")
|
|
12739
|
+
if (record.kind === "message") {
|
|
12740
|
+
refineMessageCategory(record, context);
|
|
12741
|
+
refineMessageThread(record, context);
|
|
12742
|
+
}
|
|
12508
12743
|
refineRelaySubject(record, context);
|
|
12744
|
+
refineIntakeRejection(record, context);
|
|
12509
12745
|
refineFileRecord(record, context);
|
|
12510
12746
|
});
|
|
12511
12747
|
}
|
|
@@ -12746,7 +12982,7 @@ function apiDocsAsset(pathname) {
|
|
|
12746
12982
|
return void 0;
|
|
12747
12983
|
}
|
|
12748
12984
|
}
|
|
12749
|
-
var RPC_PATH, OPENAPI_DOCUMENT_PATH, API_DOCS_PATH, API_DOCS_SCRIPT_PATH, API_DOCS_STYLESHEET_PATH, API_VERSION, EXAMPLE_ROOM_ID, roomIdProperty, roleProperty, restRoleProperty, missionTextProperty, roomNameProperty, inviteModeProperty, notifyProperty, PRIVATE_ROOM_RPC_METHODS, ROOM_RPC_METHODS, RPC_ERROR_CODES, DOCS_PAGE, DOCS_STYLESHEET, DOCS_SCRIPT;
|
|
12985
|
+
var RPC_PATH, OPENAPI_DOCUMENT_PATH, API_DOCS_PATH, API_DOCS_SCRIPT_PATH, API_DOCS_STYLESHEET_PATH, API_VERSION, commandGrantPatternProperty, EXAMPLE_ROOM_ID, roomIdProperty, roleProperty, restRoleProperty, missionTextProperty, roomNameProperty, inviteModeProperty, notifyProperty, PRIVATE_ROOM_RPC_METHODS, ROOM_RPC_METHODS, RPC_ERROR_CODES, DOCS_PAGE, DOCS_STYLESHEET, DOCS_SCRIPT;
|
|
12750
12986
|
var init_openapi = __esm({
|
|
12751
12987
|
"src/openapi.ts"() {
|
|
12752
12988
|
"use strict";
|
|
@@ -12757,6 +12993,15 @@ var init_openapi = __esm({
|
|
|
12757
12993
|
API_DOCS_SCRIPT_PATH = "/docs/ui.js";
|
|
12758
12994
|
API_DOCS_STYLESHEET_PATH = "/docs/ui.css";
|
|
12759
12995
|
API_VERSION = "1";
|
|
12996
|
+
commandGrantPatternProperty = {
|
|
12997
|
+
anyOf: [
|
|
12998
|
+
{ type: "string", enum: [...RUNTIME_COMMAND_NAMES] },
|
|
12999
|
+
{ type: "string", maxLength: 128, pattern: "^consumer\\.[a-z0-9][a-z0-9.-]*[a-z0-9]$" },
|
|
13000
|
+
{ type: "string", enum: ["*"] },
|
|
13001
|
+
{ type: "string", maxLength: 128, pattern: "^(?:[a-z0-9][a-z0-9-]*\\.)+\\*$" }
|
|
13002
|
+
],
|
|
13003
|
+
description: "Exact command, * for all runtime commands, or terminal namespace.*. Patterns also cover future matching commands."
|
|
13004
|
+
};
|
|
12760
13005
|
EXAMPLE_ROOM_ID = "01jd7q4h9m2v8xk3znbc5regty";
|
|
12761
13006
|
roomIdProperty = {
|
|
12762
13007
|
type: "string",
|
|
@@ -12821,7 +13066,7 @@ var init_openapi = __esm({
|
|
|
12821
13066
|
{
|
|
12822
13067
|
method: "room.command.definition.put",
|
|
12823
13068
|
summary: "Register or replace a consumer command",
|
|
12824
|
-
description: "Registers a consumer.* command for the ours catalog using an exact host-configured handler reference. Replacement clears grants for that name. Local definitions cannot be replaced here. A published:false result means desired state is committed; use reload to retry publication.",
|
|
13069
|
+
description: "Registers a consumer.* command for the ours catalog using an exact host-configured handler reference. Replacement clears exact grants for that name; wildcard grants remain effective. Local definitions cannot be replaced here. A published:false result means desired state is committed; use reload to retry publication.",
|
|
12825
13070
|
params: params({ room_id: roomIdProperty, expected_revision: { type: "integer", minimum: 0 }, definition: {
|
|
12826
13071
|
type: "object",
|
|
12827
13072
|
additionalProperties: false,
|
|
@@ -12834,7 +13079,7 @@ var init_openapi = __esm({
|
|
|
12834
13079
|
{
|
|
12835
13080
|
method: "room.command.definition.delete",
|
|
12836
13081
|
summary: "Delete a REST consumer command",
|
|
12837
|
-
description: "Deletes the desired definition and its CID/role grants. Requires the current registry revision. Local commands must be removed from their file and reloaded.",
|
|
13082
|
+
description: "Deletes the desired definition and its exact CID/role grants; wildcard grants remain stored. Requires the current registry revision. Local commands must be removed from their file and reloaded.",
|
|
12838
13083
|
params: params({ room_id: roomIdProperty, expected_revision: { type: "integer", minimum: 0 }, name: { type: "string" } }, ["room_id", "expected_revision", "name"]),
|
|
12839
13084
|
result: "Updated revision, published and definitions.",
|
|
12840
13085
|
example: { room_id: EXAMPLE_ROOM_ID, expected_revision: 1, name: "consumer.orders" }
|
|
@@ -13004,7 +13249,7 @@ var init_openapi = __esm({
|
|
|
13004
13249
|
{
|
|
13005
13250
|
method: "room.command.grants",
|
|
13006
13251
|
summary: "List runtime-command grants",
|
|
13007
|
-
description: "Lists the default-deny grants that bind one active participant CID to one room runtime command.",
|
|
13252
|
+
description: "Lists the default-deny grants that bind one active participant CID to one room runtime command name or stored wildcard pattern.",
|
|
13008
13253
|
params: params({ room_id: roomIdProperty }, ["room_id"]),
|
|
13009
13254
|
result: "An array of `{caller_cid, command}` grants.",
|
|
13010
13255
|
example: { room_id: EXAMPLE_ROOM_ID }
|
|
@@ -13020,13 +13265,11 @@ var init_openapi = __esm({
|
|
|
13020
13265
|
{
|
|
13021
13266
|
method: "room.command.role.set",
|
|
13022
13267
|
summary: "Set a role runtime-command policy",
|
|
13023
|
-
description: "Atomically replaces the command policy for one exact role. An empty command list removes the policy without removing independent per-CID grants.",
|
|
13268
|
+
description: "Atomically replaces the command policy for one exact role. An empty command list removes the policy without removing independent per-CID grants. Accepts exact names, * and namespace.* patterns.",
|
|
13024
13269
|
params: params({
|
|
13025
13270
|
room_id: roomIdProperty,
|
|
13026
13271
|
role: { type: "string", minLength: 1, description: "Exact admitted seat role." },
|
|
13027
|
-
commands: { type: "array", uniqueItems: true, items:
|
|
13028
|
-
anyOf: [{ type: "string", enum: [...RUNTIME_COMMAND_NAMES] }, { type: "string", maxLength: 128, pattern: "^consumer\\.[a-z0-9][a-z0-9.-]*[a-z0-9]$" }]
|
|
13029
|
-
} }
|
|
13272
|
+
commands: { type: "array", uniqueItems: true, items: commandGrantPatternProperty }
|
|
13030
13273
|
}, ["room_id", "role", "commands"]),
|
|
13031
13274
|
result: "The complete sorted role-policy list after the idempotent update.",
|
|
13032
13275
|
example: { room_id: EXAMPLE_ROOM_ID, role: "Owner", commands: ["list-members", "remove-member"] }
|
|
@@ -13034,11 +13277,11 @@ var init_openapi = __esm({
|
|
|
13034
13277
|
{
|
|
13035
13278
|
method: "room.command.grant",
|
|
13036
13279
|
summary: "Authorize a runtime command caller",
|
|
13037
|
-
description: "Idempotently grants one command to one exact active participant CID. Display names and roles are never authority.",
|
|
13280
|
+
description: "Idempotently grants one command name or dynamic pattern to one exact active participant CID. Display names and caller-supplied roles are never authority. * and room.* include permission administration and room lifecycle commands; * also covers future or replaced consumer commands.",
|
|
13038
13281
|
params: params({
|
|
13039
13282
|
room_id: roomIdProperty,
|
|
13040
13283
|
caller_cid: { type: "string", pattern: "^[0-9A-Fa-f]{64}$", description: "Authenticated caller CID." },
|
|
13041
|
-
command:
|
|
13284
|
+
command: commandGrantPatternProperty
|
|
13042
13285
|
}, ["room_id", "caller_cid", "command"]),
|
|
13043
13286
|
result: "The complete sorted grant list after the idempotent update.",
|
|
13044
13287
|
example: { room_id: EXAMPLE_ROOM_ID, caller_cid: "A".repeat(64), command: "list-members" }
|
|
@@ -13046,11 +13289,11 @@ var init_openapi = __esm({
|
|
|
13046
13289
|
{
|
|
13047
13290
|
method: "room.command.revoke",
|
|
13048
13291
|
summary: "Revoke a runtime command caller",
|
|
13049
|
-
description: "Idempotently removes
|
|
13292
|
+
description: "Idempotently removes only the exact stored CID/name or CID/pattern entry; overlapping grants remain effective. Revocation is persisted before subsequent command dispatch can acquire the room mutex.",
|
|
13050
13293
|
params: params({
|
|
13051
13294
|
room_id: roomIdProperty,
|
|
13052
13295
|
caller_cid: { type: "string", pattern: "^[0-9A-Fa-f]{64}$", description: "Authenticated caller CID." },
|
|
13053
|
-
command:
|
|
13296
|
+
command: commandGrantPatternProperty
|
|
13054
13297
|
}, ["room_id", "caller_cid", "command"]),
|
|
13055
13298
|
result: "The complete sorted grant list after the idempotent update.",
|
|
13056
13299
|
example: { room_id: EXAMPLE_ROOM_ID, caller_cid: "A".repeat(64), command: "remove-member" }
|
|
@@ -13842,6 +14085,34 @@ var init_packets = __esm({
|
|
|
13842
14085
|
this.runtimeHandlers = handlers;
|
|
13843
14086
|
await this.runBound(() => this.client.registerCommands([
|
|
13844
14087
|
...handlers.consumerCommands ?? [],
|
|
14088
|
+
...handlers.startThread === void 0 ? [] : [{
|
|
14089
|
+
name: "start_thread",
|
|
14090
|
+
description: "Create a scoped reply thread for an explicit set of room participant IDs.",
|
|
14091
|
+
input_schema: {
|
|
14092
|
+
type: "object",
|
|
14093
|
+
additionalProperties: false,
|
|
14094
|
+
required: ["topic", "participant_ids", "idempotency_key"],
|
|
14095
|
+
properties: {
|
|
14096
|
+
topic: {
|
|
14097
|
+
type: "string",
|
|
14098
|
+
minLength: 1,
|
|
14099
|
+
maxLength: 120,
|
|
14100
|
+
pattern: "^(?![\\s\\S]*[\\p{Cc}\\p{Cf}])[\\s\\S]*\\S[\\s\\S]*$"
|
|
14101
|
+
},
|
|
14102
|
+
participant_ids: {
|
|
14103
|
+
type: "array",
|
|
14104
|
+
minItems: 1,
|
|
14105
|
+
uniqueItems: true,
|
|
14106
|
+
items: { type: "string", pattern: "^[0-7][0-9a-hjkmnp-tv-z]{25}$" }
|
|
14107
|
+
},
|
|
14108
|
+
idempotency_key: {
|
|
14109
|
+
type: "string",
|
|
14110
|
+
pattern: "^[A-Za-z0-9._:-]{1,128}$"
|
|
14111
|
+
}
|
|
14112
|
+
}
|
|
14113
|
+
},
|
|
14114
|
+
handler: handlers.startThread
|
|
14115
|
+
}],
|
|
13845
14116
|
{
|
|
13846
14117
|
name: "list-members",
|
|
13847
14118
|
description: "List the room roster using contact-safe member fields.",
|
|
@@ -13866,9 +14137,10 @@ var init_packets = __esm({
|
|
|
13866
14137
|
...handlers.sharedCommand === void 0 ? [] : SHARED_ROOM_COMMANDS.map((name) => {
|
|
13867
14138
|
const doc = [...ROOM_RPC_METHODS, ...PRIVATE_ROOM_RPC_METHODS].find((method) => method.method === name);
|
|
13868
14139
|
const { room_id: _roomId, ...properties } = doc.params.properties;
|
|
14140
|
+
if (name === "room.history") properties.view = { const: "participant" };
|
|
13869
14141
|
return {
|
|
13870
14142
|
name,
|
|
13871
|
-
description: `${doc.description} Requires an explicit grant; applies only to this room.`,
|
|
14143
|
+
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
14144
|
input_schema: {
|
|
13873
14145
|
...doc.params,
|
|
13874
14146
|
properties,
|
|
@@ -14011,6 +14283,22 @@ var init_ulid = __esm({
|
|
|
14011
14283
|
});
|
|
14012
14284
|
|
|
14013
14285
|
// src/reply-threading.ts
|
|
14286
|
+
function buildAliasIndex(rows, roomId) {
|
|
14287
|
+
const local = rows.filter((r) => r.room_id === roomId);
|
|
14288
|
+
const items = local.filter((r) => r.kind === "message" || r.kind === "file");
|
|
14289
|
+
const intents = local.filter((r) => r.kind === "relay_intent");
|
|
14290
|
+
const results = local.filter((r) => r.kind === "relay_result");
|
|
14291
|
+
const copies = (parent, cid) => results.filter((result) => {
|
|
14292
|
+
if (result.status !== "queued" || result.recipient_identity !== cid || key(result) !== key(parent) || !parent.recipient_identities.includes(cid)) return false;
|
|
14293
|
+
const matches = intents.filter((intent2) => intent2.record_id === result.intent_record_id);
|
|
14294
|
+
if (matches.length !== 1) return false;
|
|
14295
|
+
const intent = matches[0];
|
|
14296
|
+
return key(intent) === key(parent) && intent.recipient_identity === cid && parent.seq < intent.seq && intent.seq < result.seq;
|
|
14297
|
+
}).sort((a, b) => a.seq - b.seq);
|
|
14298
|
+
const wires = (result, parent) => [result.wire_id, ...parent.kind === "file" ? [result.metadata_wire_id] : []].filter(nonempty);
|
|
14299
|
+
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)));
|
|
14300
|
+
return { items, copies, wires, ownersFor };
|
|
14301
|
+
}
|
|
14014
14302
|
async function readReplyRows(store, roomId) {
|
|
14015
14303
|
const rows = [];
|
|
14016
14304
|
let after = 0;
|
|
@@ -14028,42 +14316,53 @@ async function readReplyRows(store, roomId) {
|
|
|
14028
14316
|
after = last.seq;
|
|
14029
14317
|
}
|
|
14030
14318
|
}
|
|
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)));
|
|
14319
|
+
function resolveReplyParent(rows, roomId, senderCid, wireId, beforeSeq) {
|
|
14320
|
+
if (wireId === void 0) return { state: "none" };
|
|
14321
|
+
if (!nonempty(wireId)) return { state: "unknown_parent" };
|
|
14322
|
+
const index = buildAliasIndex(rows, roomId);
|
|
14323
|
+
const candidates = index.ownersFor(wireId, senderCid);
|
|
14049
14324
|
if (candidates.length === 0) return { state: "unknown_parent" };
|
|
14050
14325
|
if (candidates.length !== 1) return { state: "ambiguous_parent" };
|
|
14051
14326
|
const parent = candidates[0];
|
|
14052
|
-
if (parent.seq >=
|
|
14327
|
+
if (parent.seq >= beforeSeq) return { state: "unknown_parent" };
|
|
14053
14328
|
const parentKey = key(parent);
|
|
14054
|
-
if (items.filter((item) => key(item) === parentKey).length !== 1) {
|
|
14329
|
+
if (index.items.filter((item) => key(item) === parentKey).length !== 1) {
|
|
14055
14330
|
return { state: "ambiguous_parent" };
|
|
14056
14331
|
}
|
|
14057
|
-
|
|
14332
|
+
return { state: "resolved", parent: { key: parentKey, item: parent } };
|
|
14333
|
+
}
|
|
14334
|
+
function mapReplyParent(rows, roomId, logicalParent, recipientCid) {
|
|
14335
|
+
const index = buildAliasIndex(rows, roomId);
|
|
14336
|
+
if (logicalParent.item.room_id !== roomId || key(logicalParent.item) !== logicalParent.key) {
|
|
14337
|
+
return { state: "ambiguous_parent", parentKey: logicalParent.key };
|
|
14338
|
+
}
|
|
14339
|
+
const matches = index.items.filter((item) => key(item) === logicalParent.key);
|
|
14340
|
+
if (matches.length !== 1) {
|
|
14341
|
+
return { state: "ambiguous_parent", parentKey: logicalParent.key };
|
|
14342
|
+
}
|
|
14343
|
+
const parent = matches[0];
|
|
14344
|
+
const recipientCopies = index.copies(parent, recipientCid);
|
|
14058
14345
|
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 };
|
|
14346
|
+
const sourceOwners = sourceWire === void 0 ? [] : index.ownersFor(sourceWire, recipientCid);
|
|
14347
|
+
const wireId = sourceOwners.length === 1 && key(sourceOwners[0]) === logicalParent.key ? sourceWire : recipientCopies.map((copy) => index.wires(copy, parent)[0]).find(nonempty);
|
|
14348
|
+
if (!nonempty(wireId)) return { state: "missing_copy", parentKey: logicalParent.key };
|
|
14349
|
+
const owners = index.ownersFor(wireId, recipientCid);
|
|
14350
|
+
if (owners.length !== 1 || key(owners[0]) !== logicalParent.key) {
|
|
14351
|
+
return { state: "ambiguous_parent", parentKey: logicalParent.key };
|
|
14065
14352
|
}
|
|
14066
|
-
return { state: "linked", parentKey, replyTo: { wire_id: wireId } };
|
|
14353
|
+
return { state: "linked", parentKey: logicalParent.key, replyTo: { wire_id: wireId } };
|
|
14354
|
+
}
|
|
14355
|
+
function selectReply(rows, roomId, child, recipientCid) {
|
|
14356
|
+
if (child.room_id !== roomId) throw new Error("reply child room mismatch");
|
|
14357
|
+
const resolution = resolveReplyParent(
|
|
14358
|
+
rows,
|
|
14359
|
+
roomId,
|
|
14360
|
+
child.author.identity,
|
|
14361
|
+
child.source_reply_to?.wire_id,
|
|
14362
|
+
child.seq
|
|
14363
|
+
);
|
|
14364
|
+
if (resolution.state !== "resolved") return resolution;
|
|
14365
|
+
return mapReplyParent(rows, roomId, resolution.parent, recipientCid);
|
|
14067
14366
|
}
|
|
14068
14367
|
var key, nonempty;
|
|
14069
14368
|
var init_reply_threading = __esm({
|
|
@@ -14074,9 +14373,159 @@ var init_reply_threading = __esm({
|
|
|
14074
14373
|
}
|
|
14075
14374
|
});
|
|
14076
14375
|
|
|
14376
|
+
// src/threads.ts
|
|
14377
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
14378
|
+
function publicThreadMetadata(root, room) {
|
|
14379
|
+
let creator;
|
|
14380
|
+
if (room.anonymous) {
|
|
14381
|
+
const alias = AuthorAliasSchema.safeParse(root.author_alias);
|
|
14382
|
+
if (!alias.success || alias.data.participant_id !== root.thread_root.creator_participant_id) {
|
|
14383
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14384
|
+
}
|
|
14385
|
+
creator = {
|
|
14386
|
+
identity: alias.data.participant_id,
|
|
14387
|
+
display_name: alias.data.alias,
|
|
14388
|
+
role: root.author.role
|
|
14389
|
+
};
|
|
14390
|
+
} else {
|
|
14391
|
+
creator = {
|
|
14392
|
+
identity: root.author.identity,
|
|
14393
|
+
display_name: root.author.display_name,
|
|
14394
|
+
role: root.author.role
|
|
14395
|
+
};
|
|
14396
|
+
}
|
|
14397
|
+
return {
|
|
14398
|
+
schema_version: 1,
|
|
14399
|
+
thread_id: root.thread_root.thread_id,
|
|
14400
|
+
topic: root.thread_root.topic,
|
|
14401
|
+
creator,
|
|
14402
|
+
participant_ids: root.thread_root.members.map((member) => member.participant_id),
|
|
14403
|
+
created_at: root.at
|
|
14404
|
+
};
|
|
14405
|
+
}
|
|
14406
|
+
function selectThreadMembers(room, cid, input) {
|
|
14407
|
+
const selected = new Set(input.participant_ids);
|
|
14408
|
+
const active = room.seats.filter((seat) => seat.state === "active");
|
|
14409
|
+
const creator = active.find((seat) => seat.identity === cid);
|
|
14410
|
+
if (creator === void 0 || selected.size === 0 || selected.size !== input.participant_ids.length || selected.size > active.length || !selected.has(creator.participant_id)) {
|
|
14411
|
+
throw new ThreadFailure("invalid_members");
|
|
14412
|
+
}
|
|
14413
|
+
const members = active.filter((seat) => selected.has(seat.participant_id));
|
|
14414
|
+
if (members.length !== selected.size) throw new ThreadFailure("invalid_members");
|
|
14415
|
+
return members.map(({ participant_id, identity }) => ({ participant_id, identity })).sort((left, right) => left.participant_id.localeCompare(right.participant_id));
|
|
14416
|
+
}
|
|
14417
|
+
function activeThreadSeat(room, root, cid) {
|
|
14418
|
+
return room.seats.find((seat) => seat.state === "active" && seat.identity === cid && root.members.some((member) => member.identity === cid && member.participant_id === seat.participant_id));
|
|
14419
|
+
}
|
|
14420
|
+
function threadRelayEligible(room, root, cid) {
|
|
14421
|
+
return activeThreadSeat(room, root, cid) !== void 0;
|
|
14422
|
+
}
|
|
14423
|
+
function publicThreadAuthor(message, root, room) {
|
|
14424
|
+
const member = root.members.find((member2) => member2.identity === message.author.identity);
|
|
14425
|
+
if (!member) throw new ThreadFailure("reply_target_unavailable");
|
|
14426
|
+
if (!room.anonymous) return message.author;
|
|
14427
|
+
const alias = AuthorAliasSchema.safeParse(message.author_alias);
|
|
14428
|
+
if (!alias.success || alias.data.participant_id !== member.participant_id) {
|
|
14429
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14430
|
+
}
|
|
14431
|
+
return { identity: alias.data.participant_id, display_name: alias.data.alias, role: message.author.role };
|
|
14432
|
+
}
|
|
14433
|
+
function threadFingerprint(input) {
|
|
14434
|
+
return createHash3("sha256").update(JSON.stringify({
|
|
14435
|
+
topic: input.topic,
|
|
14436
|
+
participant_ids: [...input.participant_ids].sort()
|
|
14437
|
+
})).digest("hex");
|
|
14438
|
+
}
|
|
14439
|
+
function findThreadRoot(rows, id) {
|
|
14440
|
+
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));
|
|
14441
|
+
if (candidates.length === 0) return void 0;
|
|
14442
|
+
if (candidates.length !== 1) throw new ThreadFailure("reply_target_unavailable");
|
|
14443
|
+
const root = candidates[0];
|
|
14444
|
+
const parsedRoot = ThreadRootSchema.safeParse(root.thread_root);
|
|
14445
|
+
const parsedScope = ThreadScopeSchema.safeParse(root.scope);
|
|
14446
|
+
const parsedAlias = AuthorAliasSchema.safeParse(root.author_alias);
|
|
14447
|
+
const creator = parsedRoot.success ? parsedRoot.data.members.find((member) => member.participant_id === parsedRoot.data.creator_participant_id) : void 0;
|
|
14448
|
+
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) {
|
|
14449
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14450
|
+
}
|
|
14451
|
+
return root;
|
|
14452
|
+
}
|
|
14453
|
+
function classifyThreadAssociation(room, rows, source) {
|
|
14454
|
+
const chain = [];
|
|
14455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14456
|
+
let item = source;
|
|
14457
|
+
for (; ; ) {
|
|
14458
|
+
const key2 = item.kind === "message" ? `message:${item.message_id}` : `file:${item.file_id}`;
|
|
14459
|
+
if (item.room_id !== room.room_id || seen.has(key2) || chain.length > rows.length) {
|
|
14460
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14461
|
+
}
|
|
14462
|
+
seen.add(key2);
|
|
14463
|
+
const resolved = resolveReplyParent(
|
|
14464
|
+
rows,
|
|
14465
|
+
room.room_id,
|
|
14466
|
+
item.author.identity,
|
|
14467
|
+
item.source_reply_to?.wire_id,
|
|
14468
|
+
item.seq
|
|
14469
|
+
);
|
|
14470
|
+
chain.push({ item, resolved });
|
|
14471
|
+
if (resolved.state !== "resolved") break;
|
|
14472
|
+
if (resolved.parent.item.seq >= item.seq) throw new ThreadFailure("reply_target_unavailable");
|
|
14473
|
+
item = resolved.parent.item;
|
|
14474
|
+
}
|
|
14475
|
+
let association = { state: "ordinary" };
|
|
14476
|
+
for (const { item: item2, resolved } of chain.reverse()) {
|
|
14477
|
+
const declared = item2.kind === "message" && (item2.scope !== void 0 || item2.thread_root !== void 0);
|
|
14478
|
+
if (!declared && association.state === "ordinary") continue;
|
|
14479
|
+
const scope = ThreadScopeSchema.safeParse(item2.kind === "message" ? item2.scope : void 0);
|
|
14480
|
+
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) {
|
|
14481
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14482
|
+
}
|
|
14483
|
+
const root = findThreadRoot(rows.filter((row) => row.room_id === room.room_id), scope.data.thread_id);
|
|
14484
|
+
if (!root?.thread_root) throw new ThreadFailure("reply_target_unavailable");
|
|
14485
|
+
publicThreadMetadata({ ...root, thread_root: root.thread_root }, room);
|
|
14486
|
+
publicThreadAuthor(item2, root.thread_root, room);
|
|
14487
|
+
if (item2.message_id !== root.message_id) {
|
|
14488
|
+
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) {
|
|
14489
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14490
|
+
}
|
|
14491
|
+
}
|
|
14492
|
+
association = { state: "scoped", root: { ...root, thread_root: root.thread_root }, scope: scope.data };
|
|
14493
|
+
}
|
|
14494
|
+
return association;
|
|
14495
|
+
}
|
|
14496
|
+
function resolveIntakeScope(room, rows, item, beforeSeq) {
|
|
14497
|
+
const ordinary = () => ({ recipients: [...new Set(room.seats.filter((seat) => seat.state === "active" && seat.identity !== item.sender_id).map((seat) => seat.identity))] });
|
|
14498
|
+
if (item.reply_to == null) return ordinary();
|
|
14499
|
+
const reply = item.reply_to;
|
|
14500
|
+
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")) {
|
|
14501
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14502
|
+
}
|
|
14503
|
+
const resolved = resolveReplyParent(rows, room.room_id, item.sender_id, reply.wire_id, beforeSeq);
|
|
14504
|
+
if (resolved.state !== "resolved") throw new ThreadFailure("reply_target_unavailable");
|
|
14505
|
+
const association = classifyThreadAssociation(room, rows, resolved.parent.item);
|
|
14506
|
+
if (association.state === "ordinary") return ordinary();
|
|
14507
|
+
const { root } = association;
|
|
14508
|
+
if (!activeThreadSeat(room, root.thread_root, item.sender_id)) {
|
|
14509
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
14510
|
+
}
|
|
14511
|
+
if ("file_id" in item) throw new ThreadFailure("thread_files_unsupported");
|
|
14512
|
+
return {
|
|
14513
|
+
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),
|
|
14514
|
+
scope: { thread_id: root.message_id, parent_key: resolved.parent.key }
|
|
14515
|
+
};
|
|
14516
|
+
}
|
|
14517
|
+
var init_threads = __esm({
|
|
14518
|
+
"src/threads.ts"() {
|
|
14519
|
+
"use strict";
|
|
14520
|
+
init_reply_threading();
|
|
14521
|
+
init_contracts();
|
|
14522
|
+
init_thread_contracts();
|
|
14523
|
+
}
|
|
14524
|
+
});
|
|
14525
|
+
|
|
14077
14526
|
// src/intake.ts
|
|
14078
14527
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14079
|
-
import { createHash as
|
|
14528
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
14080
14529
|
function canonicalJson(value) {
|
|
14081
14530
|
const encoded = JSON.stringify(canonicalValue(value));
|
|
14082
14531
|
if (encoded === void 0) throw new TypeError("canonical JSON value is not serializable");
|
|
@@ -14091,13 +14540,25 @@ function sameReply2(stored, observed) {
|
|
|
14091
14540
|
}
|
|
14092
14541
|
async function queryStore(store, roomId, options) {
|
|
14093
14542
|
if (store.query) return store.query(roomId, options);
|
|
14094
|
-
|
|
14095
|
-
|
|
14543
|
+
const archive = [];
|
|
14544
|
+
let after = 0;
|
|
14545
|
+
for (; ; ) {
|
|
14546
|
+
const page = await store.read(roomId, { after, limit: JOURNAL_WORK_BATCH_SIZE });
|
|
14547
|
+
if (page.length === 0) break;
|
|
14548
|
+
for (const row of page) {
|
|
14549
|
+
if (row.room_id !== roomId || !Number.isSafeInteger(row.seq) || row.seq <= after) {
|
|
14550
|
+
throw new Error("intake archive cursor did not advance");
|
|
14551
|
+
}
|
|
14552
|
+
archive.push(row);
|
|
14553
|
+
after = row.seq;
|
|
14554
|
+
}
|
|
14555
|
+
}
|
|
14556
|
+
let records = archive.filter((record) => {
|
|
14096
14557
|
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);
|
|
14558
|
+
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
14559
|
});
|
|
14099
14560
|
if (options.unresolvedResultKind) {
|
|
14100
|
-
const completed = new Set(
|
|
14561
|
+
const completed = new Set(archive.filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
|
|
14101
14562
|
records = records.filter((record) => !completed.has(record.record_id));
|
|
14102
14563
|
}
|
|
14103
14564
|
if (options.descending) records.reverse();
|
|
@@ -14115,8 +14576,20 @@ function canonicalValue(value) {
|
|
|
14115
14576
|
}
|
|
14116
14577
|
return value;
|
|
14117
14578
|
}
|
|
14118
|
-
function
|
|
14119
|
-
return
|
|
14579
|
+
function inputFingerprint(item) {
|
|
14580
|
+
return createHash4("sha256").update(canonicalJson({
|
|
14581
|
+
sender: item.sender_id,
|
|
14582
|
+
wire: item.wire_id,
|
|
14583
|
+
date: item.date,
|
|
14584
|
+
reply: item.reply_to ?? null,
|
|
14585
|
+
..."file_id" in item ? {
|
|
14586
|
+
kind: "file",
|
|
14587
|
+
id: item.file_id,
|
|
14588
|
+
filename: item.filename,
|
|
14589
|
+
mime: item.mime,
|
|
14590
|
+
sha256: createHash4("sha256").update(item.data).digest("hex")
|
|
14591
|
+
} : { kind: "message", id: item.msg_id, text: item.text }
|
|
14592
|
+
})).digest("hex");
|
|
14120
14593
|
}
|
|
14121
14594
|
function wireKind(category) {
|
|
14122
14595
|
switch (category) {
|
|
@@ -14138,6 +14611,8 @@ var init_intake = __esm({
|
|
|
14138
14611
|
init_contracts();
|
|
14139
14612
|
init_ulid();
|
|
14140
14613
|
init_reply_threading();
|
|
14614
|
+
init_threads();
|
|
14615
|
+
init_thread_contracts();
|
|
14141
14616
|
JOURNAL_WORK_BATCH_SIZE = 64;
|
|
14142
14617
|
INTAKE_BATCH_SIZE = 32;
|
|
14143
14618
|
IntakePump = class {
|
|
@@ -14282,28 +14757,36 @@ var init_intake = __esm({
|
|
|
14282
14757
|
});
|
|
14283
14758
|
}
|
|
14284
14759
|
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
14760
|
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);
|
|
14761
|
+
const [stored] = await queryStore(this.store, roomId, { sourceFileId: item.file_id, limit: 1 });
|
|
14762
|
+
if (this.isRejectedReplay(stored, item)) {
|
|
14300
14763
|
await packet.acknowledgeFile(item);
|
|
14301
14764
|
return;
|
|
14302
14765
|
}
|
|
14303
|
-
|
|
14304
|
-
let file = this.findSourceFile(storedFile === void 0 ? [] : [storedFile], item);
|
|
14766
|
+
let file = this.findSourceFile(stored === void 0 ? [] : [stored], item);
|
|
14305
14767
|
if (!file) {
|
|
14306
|
-
const
|
|
14768
|
+
const seat = room.seats.find((candidate) => candidate.identity === item.sender_id && candidate.state === "active");
|
|
14769
|
+
const known = room.seats.some((candidate) => candidate.identity === item.sender_id);
|
|
14770
|
+
if (!known || item.reply_to == null && (room.state !== "active" || !seat)) {
|
|
14771
|
+
if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
|
|
14772
|
+
await packet.acknowledgeFile(item);
|
|
14773
|
+
return;
|
|
14774
|
+
}
|
|
14775
|
+
const disposition = await this.freshScopeUnlocked(room, item);
|
|
14776
|
+
if (!disposition) {
|
|
14777
|
+
await packet.acknowledgeFile(item);
|
|
14778
|
+
return;
|
|
14779
|
+
}
|
|
14780
|
+
if (!seat) throw new Error("authorized file sender has no active seat");
|
|
14781
|
+
const parsedName = FileNameSchema.safeParse(item.filename);
|
|
14782
|
+
const parsedMime = FileMimeSchema.safeParse(item.mime);
|
|
14783
|
+
if (!parsedName.success || !parsedMime.success) {
|
|
14784
|
+
await packet.acknowledgeFile(item);
|
|
14785
|
+
return;
|
|
14786
|
+
}
|
|
14787
|
+
if (item.data.length > MAX_FILE_BYTES) {
|
|
14788
|
+
throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
|
|
14789
|
+
}
|
|
14307
14790
|
const bytes = Buffer.from(item.data);
|
|
14308
14791
|
const appended = await this.store.append(roomId, {
|
|
14309
14792
|
version: 1,
|
|
@@ -14316,9 +14799,9 @@ var init_intake = __esm({
|
|
|
14316
14799
|
filename: parsedName.data,
|
|
14317
14800
|
mime: parsedMime.data,
|
|
14318
14801
|
size: bytes.length,
|
|
14319
|
-
sha256:
|
|
14802
|
+
sha256: createHash4("sha256").update(bytes).digest("hex"),
|
|
14320
14803
|
data_base64: bytes.toString("base64"),
|
|
14321
|
-
recipient_identities:
|
|
14804
|
+
recipient_identities: disposition.recipients,
|
|
14322
14805
|
source_file_id: item.file_id,
|
|
14323
14806
|
...item.wire_id === "" ? {} : { source_wire_id: item.wire_id },
|
|
14324
14807
|
...item.reply_to == null ? {} : { source_reply_to: item.reply_to }
|
|
@@ -14331,18 +14814,26 @@ var init_intake = __esm({
|
|
|
14331
14814
|
}
|
|
14332
14815
|
async processInboxItem(roomId, packet, item, acknowledge = true) {
|
|
14333
14816
|
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);
|
|
14817
|
+
const [stored] = await queryStore(this.store, roomId, { sourceMsgId: item.msg_id, limit: 1 });
|
|
14818
|
+
if (this.isRejectedReplay(stored, item)) {
|
|
14339
14819
|
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14340
14820
|
return;
|
|
14341
14821
|
}
|
|
14342
|
-
|
|
14343
|
-
let message = this.findSourceMessage(storedMessage === void 0 ? [] : [storedMessage], item);
|
|
14822
|
+
let message = this.findSourceMessage(stored === void 0 ? [] : [stored], item);
|
|
14344
14823
|
if (!message) {
|
|
14345
|
-
const
|
|
14824
|
+
const seat = room.seats.find((candidate) => candidate.identity === item.sender_id && candidate.state === "active");
|
|
14825
|
+
const known = room.seats.some((candidate) => candidate.identity === item.sender_id);
|
|
14826
|
+
if (!known || item.reply_to == null && (room.state !== "active" || !seat)) {
|
|
14827
|
+
if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
|
|
14828
|
+
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14829
|
+
return;
|
|
14830
|
+
}
|
|
14831
|
+
const disposition = await this.freshScopeUnlocked(room, item);
|
|
14832
|
+
if (!disposition) {
|
|
14833
|
+
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14834
|
+
return;
|
|
14835
|
+
}
|
|
14836
|
+
if (!seat) throw new Error("authorized message sender has no active seat");
|
|
14346
14837
|
const appended = await this.store.append(roomId, {
|
|
14347
14838
|
version: 1,
|
|
14348
14839
|
kind: "message",
|
|
@@ -14359,7 +14850,8 @@ var init_intake = __esm({
|
|
|
14359
14850
|
...room.anonymous && seat.alias !== void 0 ? { author_alias: { participant_id: seat.participant_id, alias: seat.alias } } : {},
|
|
14360
14851
|
category: "chat",
|
|
14361
14852
|
text: item.text,
|
|
14362
|
-
recipient_identities:
|
|
14853
|
+
recipient_identities: disposition.recipients,
|
|
14854
|
+
...disposition.scope === void 0 ? {} : { scope: disposition.scope },
|
|
14363
14855
|
source_msg_id: item.msg_id,
|
|
14364
14856
|
...item.wire_id === "" ? {} : { source_wire_id: item.wire_id },
|
|
14365
14857
|
...item.reply_to == null ? {} : { source_reply_to: item.reply_to }
|
|
@@ -14370,6 +14862,78 @@ var init_intake = __esm({
|
|
|
14370
14862
|
await this.completeMessageIntents(roomId, message);
|
|
14371
14863
|
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
14372
14864
|
}
|
|
14865
|
+
async freshScopeUnlocked(room, item) {
|
|
14866
|
+
try {
|
|
14867
|
+
const rows = item.reply_to == null ? [] : await readReplyRows(this.store, room.room_id);
|
|
14868
|
+
const beforeSeq = item.reply_to == null ? 1 : await this.nextRecordSeq(room.room_id);
|
|
14869
|
+
return resolveIntakeScope(room, rows, item, beforeSeq);
|
|
14870
|
+
} catch (error) {
|
|
14871
|
+
if (!(error instanceof ThreadFailure)) throw error;
|
|
14872
|
+
await this.recordRejectionUnlocked(room, item, error);
|
|
14873
|
+
return void 0;
|
|
14874
|
+
}
|
|
14875
|
+
}
|
|
14876
|
+
async nextRecordSeq(roomId) {
|
|
14877
|
+
if (this.store.query) {
|
|
14878
|
+
const [last] = await this.store.query(roomId, { descending: true, limit: 1 });
|
|
14879
|
+
if (last && (last.room_id !== roomId || !Number.isSafeInteger(last.seq) || last.seq < 1)) {
|
|
14880
|
+
throw new Error("intake archive tail is invalid");
|
|
14881
|
+
}
|
|
14882
|
+
return (last?.seq ?? 0) + 1;
|
|
14883
|
+
}
|
|
14884
|
+
let after = 0;
|
|
14885
|
+
for (; ; ) {
|
|
14886
|
+
const page = await this.store.read(roomId, { after, limit: JOURNAL_WORK_BATCH_SIZE });
|
|
14887
|
+
if (page.length === 0) return after + 1;
|
|
14888
|
+
for (const row of page) {
|
|
14889
|
+
if (row.room_id !== roomId || !Number.isSafeInteger(row.seq) || row.seq <= after) {
|
|
14890
|
+
throw new Error("intake archive cursor did not advance");
|
|
14891
|
+
}
|
|
14892
|
+
after = row.seq;
|
|
14893
|
+
}
|
|
14894
|
+
}
|
|
14895
|
+
}
|
|
14896
|
+
isRejectedReplay(record, item) {
|
|
14897
|
+
if (record?.kind !== "intake_rejection") return false;
|
|
14898
|
+
const file = "file_id" in item;
|
|
14899
|
+
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)) {
|
|
14900
|
+
throw new Error("inbox source does not match its durable intake rejection");
|
|
14901
|
+
}
|
|
14902
|
+
return true;
|
|
14903
|
+
}
|
|
14904
|
+
async recordRejectionUnlocked(room, item, error) {
|
|
14905
|
+
const seat = room.seats.find((seat2) => seat2.identity === item.sender_id && seat2.state === "active") ?? room.seats.find((seat2) => seat2.identity === item.sender_id);
|
|
14906
|
+
if (!seat || typeof item.wire_id !== "string" || item.wire_id.length === 0) {
|
|
14907
|
+
throw new Error("intake rejection requires a known seat and source wire");
|
|
14908
|
+
}
|
|
14909
|
+
if (error.code !== "reply_target_unavailable" && error.code !== "thread_files_unsupported") throw error;
|
|
14910
|
+
await this.store.append(room.room_id, {
|
|
14911
|
+
version: 1,
|
|
14912
|
+
kind: "intake_rejection",
|
|
14913
|
+
room_id: room.room_id,
|
|
14914
|
+
at: this.now(),
|
|
14915
|
+
..."file_id" in item ? { source_kind: "file", source_file_id: item.file_id } : { source_kind: "message", source_msg_id: item.msg_id },
|
|
14916
|
+
source_wire_id: item.wire_id,
|
|
14917
|
+
sender_identity: item.sender_id,
|
|
14918
|
+
sender_participant_id: seat.participant_id,
|
|
14919
|
+
fingerprint: inputFingerprint(item),
|
|
14920
|
+
error: error.code,
|
|
14921
|
+
notification_attempt_claimed: true
|
|
14922
|
+
});
|
|
14923
|
+
try {
|
|
14924
|
+
await sendRoomBody(this.packet(room.room_id), item.sender_id, {
|
|
14925
|
+
version: 1,
|
|
14926
|
+
kind: "room_msg",
|
|
14927
|
+
room_id: room.room_id,
|
|
14928
|
+
room_name: room.room_name,
|
|
14929
|
+
message_id: this.nextMessageId(),
|
|
14930
|
+
at: this.now(),
|
|
14931
|
+
text: error.code,
|
|
14932
|
+
author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE }
|
|
14933
|
+
});
|
|
14934
|
+
} catch {
|
|
14935
|
+
}
|
|
14936
|
+
}
|
|
14373
14937
|
acknowledgeMessage(roomId, packet, expected) {
|
|
14374
14938
|
return packet.acknowledgeMessage(
|
|
14375
14939
|
expected,
|
|
@@ -14493,26 +15057,44 @@ var init_intake = __esm({
|
|
|
14493
15057
|
const [message] = intent.message_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "message", messageId: intent.message_id, limit: 1 });
|
|
14494
15058
|
const [file] = intent.file_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "file", fileId: intent.file_id, limit: 1 });
|
|
14495
15059
|
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
15060
|
const source = message ?? file;
|
|
14514
|
-
const replyRows = source.source_reply_to === void 0 ? [] : await readReplyRows(this.store, roomId);
|
|
15061
|
+
const replyRows = source.source_reply_to === void 0 && message?.scope === void 0 && message?.thread_root === void 0 ? [] : await readReplyRows(this.store, roomId);
|
|
14515
15062
|
const decision = selectReply(replyRows, roomId, source, intent.recipient_identity);
|
|
15063
|
+
let publicThread = {};
|
|
15064
|
+
let scopedAuthor;
|
|
15065
|
+
try {
|
|
15066
|
+
const association = classifyThreadAssociation(room, replyRows, source);
|
|
15067
|
+
if (association.state === "scoped") {
|
|
15068
|
+
const { root, scope } = association;
|
|
15069
|
+
if (!message || !message.recipient_identities.includes(intent.recipient_identity) || message.seq >= intent.seq || !root.thread_root.members.some((member) => member.identity === intent.recipient_identity)) {
|
|
15070
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
15071
|
+
}
|
|
15072
|
+
if (!threadRelayEligible(room, root.thread_root, intent.recipient_identity)) {
|
|
15073
|
+
await this.skipRelay(roomId, intent, "skipped_removed");
|
|
15074
|
+
continue;
|
|
15075
|
+
}
|
|
15076
|
+
const metadata = publicThreadMetadata({ ...root, thread_root: root.thread_root }, room);
|
|
15077
|
+
scopedAuthor = publicThreadAuthor(message, root.thread_root, room);
|
|
15078
|
+
if (message.message_id === root.message_id) {
|
|
15079
|
+
publicThread = { thread: { schema_version: 1, thread_id: root.message_id }, thread_root: metadata };
|
|
15080
|
+
} else {
|
|
15081
|
+
if (decision.state !== "linked" || decision.parentKey !== scope.parent_key) {
|
|
15082
|
+
throw new ThreadFailure("reply_target_unavailable");
|
|
15083
|
+
}
|
|
15084
|
+
publicThread = { thread: { schema_version: 1, thread_id: root.message_id } };
|
|
15085
|
+
}
|
|
15086
|
+
} else {
|
|
15087
|
+
if (!source.recipient_identities.includes(intent.recipient_identity)) continue;
|
|
15088
|
+
if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
|
|
15089
|
+
await this.skipRelay(roomId, intent, "skipped_removed");
|
|
15090
|
+
continue;
|
|
15091
|
+
}
|
|
15092
|
+
}
|
|
15093
|
+
} catch (error) {
|
|
15094
|
+
if (!(error instanceof ThreadFailure)) throw error;
|
|
15095
|
+
await this.skipRelay(roomId, intent, "skipped_reply_unavailable");
|
|
15096
|
+
continue;
|
|
15097
|
+
}
|
|
14516
15098
|
const replyTo = decision.replyTo;
|
|
14517
15099
|
if (file !== void 0) {
|
|
14518
15100
|
const uploader = file.author_alias?.alias ?? file.author.display_name;
|
|
@@ -14573,11 +15155,12 @@ var init_intake = __esm({
|
|
|
14573
15155
|
room_name: room.room_name,
|
|
14574
15156
|
message_id: message.message_id,
|
|
14575
15157
|
// An anonymous author leaves the archive only in alias form.
|
|
14576
|
-
author: message.author_alias === void 0 ? message.author : {
|
|
15158
|
+
author: scopedAuthor ?? (message.author_alias === void 0 ? message.author : {
|
|
14577
15159
|
identity: message.author_alias.participant_id,
|
|
14578
15160
|
display_name: message.author_alias.alias,
|
|
14579
15161
|
role: message.author.role
|
|
14580
|
-
},
|
|
15162
|
+
}),
|
|
15163
|
+
...publicThread,
|
|
14581
15164
|
text: message.text,
|
|
14582
15165
|
at: message.at,
|
|
14583
15166
|
...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
|
|
@@ -14600,6 +15183,20 @@ var init_intake = __esm({
|
|
|
14600
15183
|
}
|
|
14601
15184
|
}
|
|
14602
15185
|
}
|
|
15186
|
+
async skipRelay(roomId, intent, status) {
|
|
15187
|
+
const result = await this.store.append(roomId, {
|
|
15188
|
+
version: 1,
|
|
15189
|
+
kind: "relay_result",
|
|
15190
|
+
room_id: roomId,
|
|
15191
|
+
at: this.now(),
|
|
15192
|
+
intent_record_id: intent.record_id,
|
|
15193
|
+
...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
|
|
15194
|
+
...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
|
|
15195
|
+
recipient_identity: intent.recipient_identity,
|
|
15196
|
+
status
|
|
15197
|
+
});
|
|
15198
|
+
if (result.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
15199
|
+
}
|
|
14603
15200
|
findSourceMessage(records, item) {
|
|
14604
15201
|
const message = records.find((record) => record.kind === "message" && record.source_msg_id === item.msg_id);
|
|
14605
15202
|
if (!message) return void 0;
|
|
@@ -14793,13 +15390,13 @@ var init_command_routes = __esm({
|
|
|
14793
15390
|
}).strict();
|
|
14794
15391
|
RuntimeCommandGrantParams = external_exports.object({
|
|
14795
15392
|
room_id: external_exports.string(),
|
|
14796
|
-
caller_cid:
|
|
14797
|
-
command:
|
|
15393
|
+
caller_cid: ContainerIdSchema2,
|
|
15394
|
+
command: RuntimeCommandGrantPatternSchema
|
|
14798
15395
|
}).strict();
|
|
14799
15396
|
RuntimeRoleCommandGrantParams = external_exports.object({
|
|
14800
15397
|
room_id: external_exports.string(),
|
|
14801
15398
|
role: external_exports.string(),
|
|
14802
|
-
commands: external_exports.array(
|
|
15399
|
+
commands: external_exports.array(RuntimeCommandGrantPatternSchema)
|
|
14803
15400
|
}).strict();
|
|
14804
15401
|
ParticipantRemoveParams = external_exports.object({
|
|
14805
15402
|
room_id: external_exports.string(),
|
|
@@ -14815,9 +15412,132 @@ var init_command_routes = __esm({
|
|
|
14815
15412
|
}
|
|
14816
15413
|
});
|
|
14817
15414
|
|
|
15415
|
+
// src/thread-history.ts
|
|
15416
|
+
function publicHistoryMessage(record, author = record.author) {
|
|
15417
|
+
return {
|
|
15418
|
+
version: 1,
|
|
15419
|
+
room_id: record.room_id,
|
|
15420
|
+
seq: record.seq,
|
|
15421
|
+
record_id: record.record_id,
|
|
15422
|
+
at: record.at,
|
|
15423
|
+
kind: "message",
|
|
15424
|
+
message_id: record.message_id,
|
|
15425
|
+
author: { identity: author.identity, display_name: author.display_name, role: author.role },
|
|
15426
|
+
category: record.category,
|
|
15427
|
+
text: record.text,
|
|
15428
|
+
...record.category === "role_briefing" ? { briefing_role: record.briefing_role } : {},
|
|
15429
|
+
...(record.category === "briefing" || record.category === "role_briefing") && record.briefing_version !== void 0 ? { briefing_version: record.briefing_version } : {},
|
|
15430
|
+
...record.category === "membership" && record.membership ? { membership: {
|
|
15431
|
+
action: record.membership.action,
|
|
15432
|
+
epoch: record.membership.epoch,
|
|
15433
|
+
...record.membership.alias === void 0 ? {} : { alias: record.membership.alias },
|
|
15434
|
+
...record.membership.role === void 0 ? {} : { role: record.membership.role }
|
|
15435
|
+
} } : {}
|
|
15436
|
+
};
|
|
15437
|
+
}
|
|
15438
|
+
function projectParticipantHistory(room, records, viewerCid, page) {
|
|
15439
|
+
const viewer = room.seats.find((seat) => seat.state === "active" && seat.identity === viewerCid);
|
|
15440
|
+
if (!viewer || room.state !== "active") throw new ThreadFailure("unauthorized");
|
|
15441
|
+
const { after = 0, limit = 200 } = ParticipantHistoryPageSchema.parse(page);
|
|
15442
|
+
const replyRows = records.filter((row) => ["message", "file", "relay_intent", "relay_result"].includes(row.kind));
|
|
15443
|
+
const output = [];
|
|
15444
|
+
let ordinal = 0, bytes = 2;
|
|
15445
|
+
for (const record of records) {
|
|
15446
|
+
if (record.kind !== "message" || record.room_id !== room.room_id) continue;
|
|
15447
|
+
let author = record.author;
|
|
15448
|
+
let thread;
|
|
15449
|
+
let thread_root;
|
|
15450
|
+
try {
|
|
15451
|
+
const association = classifyThreadAssociation(room, replyRows, record);
|
|
15452
|
+
if (association.state === "scoped") {
|
|
15453
|
+
const { root } = association;
|
|
15454
|
+
if (!activeThreadSeat(room, root.thread_root, viewerCid)) continue;
|
|
15455
|
+
const metadata = publicThreadMetadata(root, room);
|
|
15456
|
+
author = publicThreadAuthor(record, root.thread_root, room);
|
|
15457
|
+
if (record.message_id === root.message_id) thread_root = metadata;
|
|
15458
|
+
thread = { schema_version: 1, thread_id: root.message_id };
|
|
15459
|
+
} else if (record.author_alias !== void 0) {
|
|
15460
|
+
const alias = AuthorAliasSchema.parse(record.author_alias);
|
|
15461
|
+
author = { identity: alias.participant_id, display_name: alias.alias, role: record.author.role };
|
|
15462
|
+
}
|
|
15463
|
+
} catch (error) {
|
|
15464
|
+
if (error instanceof ThreadFailure || error instanceof external_exports.ZodError) continue;
|
|
15465
|
+
throw error;
|
|
15466
|
+
}
|
|
15467
|
+
ordinal += 1;
|
|
15468
|
+
if (ordinal <= after) continue;
|
|
15469
|
+
const projected = {
|
|
15470
|
+
...publicHistoryMessage(record, author),
|
|
15471
|
+
seq: ordinal,
|
|
15472
|
+
record_id: `${room.room_id}:participant:${viewer.participant_id}:${ordinal}`,
|
|
15473
|
+
...thread ? { thread } : {},
|
|
15474
|
+
...thread_root ? { thread_root } : {}
|
|
15475
|
+
};
|
|
15476
|
+
const size = Buffer.byteLength(JSON.stringify(projected), "utf8") + (output.length ? 1 : 0);
|
|
15477
|
+
if (bytes + size > MAX_HISTORY_PAGE_BYTES) {
|
|
15478
|
+
if (output.length === 0) throw new RangeError("one participant history record exceeds the page byte contract");
|
|
15479
|
+
break;
|
|
15480
|
+
}
|
|
15481
|
+
output.push(projected);
|
|
15482
|
+
bytes += size;
|
|
15483
|
+
if (output.length >= limit) break;
|
|
15484
|
+
}
|
|
15485
|
+
return output;
|
|
15486
|
+
}
|
|
15487
|
+
var PublicThreadSchema, PublicThreadMetadataSchema, PublicMessageShape, ParticipantHistoryRecordSchema, ParticipantHistoryPageSchema;
|
|
15488
|
+
var init_thread_history = __esm({
|
|
15489
|
+
"src/thread-history.ts"() {
|
|
15490
|
+
"use strict";
|
|
15491
|
+
init_zod();
|
|
15492
|
+
init_contracts();
|
|
15493
|
+
init_thread_contracts();
|
|
15494
|
+
init_threads();
|
|
15495
|
+
PublicThreadSchema = external_exports.object({ schema_version: external_exports.literal(1), thread_id: LowerCrockfordUlidSchema }).strict();
|
|
15496
|
+
PublicThreadMetadataSchema = PublicThreadSchema.extend({
|
|
15497
|
+
topic: external_exports.string(),
|
|
15498
|
+
creator: AuthorSnapshotSchema,
|
|
15499
|
+
participant_ids: external_exports.array(LowerCrockfordUlidSchema),
|
|
15500
|
+
created_at: Rfc3339Schema
|
|
15501
|
+
}).strict();
|
|
15502
|
+
PublicMessageShape = {
|
|
15503
|
+
version: external_exports.literal(1),
|
|
15504
|
+
room_id: LowerCrockfordUlidSchema,
|
|
15505
|
+
seq: external_exports.number().int().positive().safe(),
|
|
15506
|
+
record_id: external_exports.string(),
|
|
15507
|
+
at: Rfc3339Schema,
|
|
15508
|
+
kind: external_exports.literal("message"),
|
|
15509
|
+
message_id: LowerCrockfordUlidSchema,
|
|
15510
|
+
author: AuthorSnapshotSchema,
|
|
15511
|
+
category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
|
|
15512
|
+
briefing_role: RoleSchema.optional(),
|
|
15513
|
+
briefing_version: external_exports.number().int().positive().safe().optional(),
|
|
15514
|
+
membership: MembershipNoticeSchema.optional(),
|
|
15515
|
+
text: MessageTextSchema
|
|
15516
|
+
};
|
|
15517
|
+
ParticipantHistoryRecordSchema = external_exports.object({
|
|
15518
|
+
...PublicMessageShape,
|
|
15519
|
+
thread: PublicThreadSchema.optional(),
|
|
15520
|
+
thread_root: PublicThreadMetadataSchema.optional()
|
|
15521
|
+
}).strict().superRefine((row, ctx) => {
|
|
15522
|
+
const prefix = `${row.room_id}:participant:`;
|
|
15523
|
+
const participantId = row.record_id.slice(prefix.length, -(String(row.seq).length + 1));
|
|
15524
|
+
if (!row.record_id.startsWith(prefix) || !LowerCrockfordUlidSchema.safeParse(participantId).success || row.record_id !== `${prefix}${participantId}:${row.seq}`) {
|
|
15525
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["record_id"], message: "must identify the viewer and visible ordinal" });
|
|
15526
|
+
}
|
|
15527
|
+
if (row.thread_root && (!row.thread || row.thread.thread_id !== row.thread_root.thread_id || row.message_id !== row.thread.thread_id)) {
|
|
15528
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["thread_root"], message: "must identify this thread root" });
|
|
15529
|
+
}
|
|
15530
|
+
});
|
|
15531
|
+
ParticipantHistoryPageSchema = external_exports.object({
|
|
15532
|
+
after: external_exports.number().int().nonnegative().safe().optional(),
|
|
15533
|
+
limit: external_exports.number().int().positive().safe().optional()
|
|
15534
|
+
}).strict();
|
|
15535
|
+
}
|
|
15536
|
+
});
|
|
15537
|
+
|
|
14818
15538
|
// src/service.ts
|
|
14819
15539
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
14820
|
-
import { createHash as
|
|
15540
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
14821
15541
|
function byteBoundedHistoryPage(records) {
|
|
14822
15542
|
const page = [];
|
|
14823
15543
|
let bytes = 2;
|
|
@@ -14874,7 +15594,7 @@ function uniqueIdentities(identities) {
|
|
|
14874
15594
|
function currentContactIdentities(packet) {
|
|
14875
15595
|
return new Set(packet.listContacts().map((contact) => contact.container_id));
|
|
14876
15596
|
}
|
|
14877
|
-
var CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, RoomServiceError, RoomService;
|
|
15597
|
+
var MAX_ROOM_MESSAGE_BYTES, CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, RoomServiceError, RoomService;
|
|
14878
15598
|
var init_service = __esm({
|
|
14879
15599
|
"src/service.ts"() {
|
|
14880
15600
|
"use strict";
|
|
@@ -14886,6 +15606,11 @@ var init_service = __esm({
|
|
|
14886
15606
|
init_consumer_commands();
|
|
14887
15607
|
init_command_names();
|
|
14888
15608
|
init_ulid();
|
|
15609
|
+
init_reply_threading();
|
|
15610
|
+
init_thread_contracts();
|
|
15611
|
+
init_thread_history();
|
|
15612
|
+
init_threads();
|
|
15613
|
+
MAX_ROOM_MESSAGE_BYTES = 262144;
|
|
14889
15614
|
CreateInviteInputSchema = external_exports.object({
|
|
14890
15615
|
mode: InviteModeSchema,
|
|
14891
15616
|
role: RoleSchema.optional(),
|
|
@@ -15105,11 +15830,106 @@ var init_service = __esm({
|
|
|
15105
15830
|
handler: (input, context) => this.invokeConsumerCommand(roomId, definition.name, input, context)
|
|
15106
15831
|
})),
|
|
15107
15832
|
sharedCommand: (name, input, context) => this.lock(roomId, () => this.sharedCommandUnlocked(roomId, name, input, context)),
|
|
15833
|
+
startThread: async (input, context) => {
|
|
15834
|
+
const result = await this.lock(roomId, () => this.startThreadCommandUnlocked(roomId, input, context));
|
|
15835
|
+
if (result.ok === true) await this.intake.resumePending(roomId);
|
|
15836
|
+
return result;
|
|
15837
|
+
},
|
|
15108
15838
|
listMembers: (input, context) => this.lock(roomId, () => this.listMembersCommandUnlocked(roomId, input, context)),
|
|
15109
15839
|
removeMember: (input, context) => this.lock(roomId, () => this.removeMemberCommandUnlocked(roomId, input, context))
|
|
15110
15840
|
});
|
|
15111
15841
|
this.publishedConsumerRevisions.set(roomId, registered.consumer_commands_revision ?? 0);
|
|
15112
15842
|
}
|
|
15843
|
+
/** Called by the registered adapter while it owns the room mutex. */
|
|
15844
|
+
async startThreadCommandUnlocked(roomId, input, context) {
|
|
15845
|
+
const room = await this.store.load(roomId);
|
|
15846
|
+
if (room.state !== "active" || room.lifecycle_request?.state === "pending") {
|
|
15847
|
+
return { ok: false, error: "room_unavailable" };
|
|
15848
|
+
}
|
|
15849
|
+
const creator = room.seats.find((seat) => seat.state === "active" && seat.identity === context.sender_cid);
|
|
15850
|
+
if (creator === void 0 || !this.hasRuntimeCommandGrant(room, context.sender_cid, "start_thread")) {
|
|
15851
|
+
return { ok: false, error: "unauthorized" };
|
|
15852
|
+
}
|
|
15853
|
+
const parsed = StartThreadInputSchema.safeParse(input);
|
|
15854
|
+
if (!parsed.success) return { ok: false, error: "invalid_request" };
|
|
15855
|
+
const request = parsed.data;
|
|
15856
|
+
const rows = await readReplyRows(this.store, roomId);
|
|
15857
|
+
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);
|
|
15858
|
+
if (prior.length > 1) throw new Error("duplicate thread roots for creator idempotency key");
|
|
15859
|
+
if (prior.length === 1) {
|
|
15860
|
+
const root2 = prior[0];
|
|
15861
|
+
if (root2.thread_root.creator_participant_id !== creator.participant_id) {
|
|
15862
|
+
return { ok: false, error: "unauthorized" };
|
|
15863
|
+
}
|
|
15864
|
+
if (root2.thread_root.fingerprint !== threadFingerprint(request)) {
|
|
15865
|
+
return { ok: false, error: "idempotency_conflict" };
|
|
15866
|
+
}
|
|
15867
|
+
return { ok: true, thread_id: root2.message_id, status: "accepted" };
|
|
15868
|
+
}
|
|
15869
|
+
let members;
|
|
15870
|
+
try {
|
|
15871
|
+
members = selectThreadMembers(room, context.sender_cid, request);
|
|
15872
|
+
} catch (error) {
|
|
15873
|
+
if (error instanceof ThreadFailure) return { ok: false, error: error.code };
|
|
15874
|
+
throw error;
|
|
15875
|
+
}
|
|
15876
|
+
const threadId = LowerCrockfordUlidSchema.parse(this.nextMessageId());
|
|
15877
|
+
const at = this.now();
|
|
15878
|
+
const threadRoot = {
|
|
15879
|
+
schema_version: 1,
|
|
15880
|
+
thread_id: threadId,
|
|
15881
|
+
topic: request.topic,
|
|
15882
|
+
creator_participant_id: creator.participant_id,
|
|
15883
|
+
members,
|
|
15884
|
+
idempotency_key: request.idempotency_key,
|
|
15885
|
+
fingerprint: threadFingerprint(request)
|
|
15886
|
+
};
|
|
15887
|
+
if (room.anonymous && creator.alias === void 0) {
|
|
15888
|
+
throw new Error("anonymous thread creator is missing its room alias");
|
|
15889
|
+
}
|
|
15890
|
+
const root = {
|
|
15891
|
+
version: 1,
|
|
15892
|
+
kind: "message",
|
|
15893
|
+
room_id: roomId,
|
|
15894
|
+
at,
|
|
15895
|
+
message_id: threadId,
|
|
15896
|
+
author: {
|
|
15897
|
+
identity: creator.identity,
|
|
15898
|
+
display_name: creator.display_name,
|
|
15899
|
+
role: creator.role
|
|
15900
|
+
},
|
|
15901
|
+
...room.anonymous ? {
|
|
15902
|
+
author_alias: { participant_id: creator.participant_id, alias: creator.alias }
|
|
15903
|
+
} : {},
|
|
15904
|
+
category: "chat",
|
|
15905
|
+
text: `Thread: ${request.topic}`,
|
|
15906
|
+
recipient_identities: members.map((member) => member.identity),
|
|
15907
|
+
scope: { thread_id: threadId },
|
|
15908
|
+
thread_root: threadRoot
|
|
15909
|
+
};
|
|
15910
|
+
const projected = {
|
|
15911
|
+
version: 1,
|
|
15912
|
+
kind: "room_msg",
|
|
15913
|
+
room_id: roomId,
|
|
15914
|
+
room_name: room.room_name,
|
|
15915
|
+
message_id: threadId,
|
|
15916
|
+
author: room.anonymous ? {
|
|
15917
|
+
identity: creator.participant_id,
|
|
15918
|
+
display_name: creator.alias,
|
|
15919
|
+
role: creator.role
|
|
15920
|
+
} : root.author,
|
|
15921
|
+
text: root.text,
|
|
15922
|
+
at,
|
|
15923
|
+
thread: { schema_version: 1, thread_id: threadId },
|
|
15924
|
+
thread_root: publicThreadMetadata(root, room)
|
|
15925
|
+
};
|
|
15926
|
+
if (Buffer.byteLength(JSON.stringify(projected), "utf8") > MAX_ROOM_MESSAGE_BYTES) {
|
|
15927
|
+
return { ok: false, error: "invalid_request" };
|
|
15928
|
+
}
|
|
15929
|
+
const appended = await this.store.append(roomId, root);
|
|
15930
|
+
if (appended.kind !== "message") throw new Error("storage returned the wrong thread root kind");
|
|
15931
|
+
return { ok: true, thread_id: appended.message_id, status: "accepted" };
|
|
15932
|
+
}
|
|
15113
15933
|
/** The SDK supplies authenticated context; arguments never select another room. */
|
|
15114
15934
|
async sharedCommandUnlocked(roomId, name, input, context) {
|
|
15115
15935
|
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 +15970,30 @@ var init_service = __esm({
|
|
|
15150
15970
|
try {
|
|
15151
15971
|
return await this.commandScope.run(scope, async () => {
|
|
15152
15972
|
try {
|
|
15973
|
+
if (name === "room.history") {
|
|
15974
|
+
const request = ParticipantHistoryPageSchema.extend({ view: external_exports.literal("participant").optional() }).safeParse(input);
|
|
15975
|
+
if (!request.success) return { ok: false, error: "invalid_request" };
|
|
15976
|
+
const { view: _view, ...page } = request.data;
|
|
15977
|
+
return { ok: true, result: JSON.parse(JSON.stringify(await this.participantHistory(roomId, context.sender_cid, page))) };
|
|
15978
|
+
}
|
|
15979
|
+
if (name === "room.show" || name === "room.participants") {
|
|
15980
|
+
external_exports.object({}).strict().parse(input);
|
|
15981
|
+
const result2 = name === "room.show" ? {
|
|
15982
|
+
room_id: room.room_id,
|
|
15983
|
+
room_name: room.room_name,
|
|
15984
|
+
state: room.state,
|
|
15985
|
+
mission: { goal: room.mission.goal, briefing: room.mission.briefing, briefing_version: room.mission.briefing_version },
|
|
15986
|
+
anonymous: room.anonymous,
|
|
15987
|
+
quiet_membership: room.quiet_membership,
|
|
15988
|
+
membership_epoch: room.membership_epoch
|
|
15989
|
+
} : room.seats.map(({ participant_id, role, state }) => ({ participant_id, role, state }));
|
|
15990
|
+
return { ok: true, result: JSON.parse(JSON.stringify(result2)) };
|
|
15991
|
+
}
|
|
15153
15992
|
const routes = name === "room.accept" ? createPrivateServiceRoutes(this) : createServiceRoutes(this);
|
|
15154
15993
|
const result = await routes[name].run({ ...input, room_id: roomId });
|
|
15994
|
+
if (name === "room.message" || name === "room.say") {
|
|
15995
|
+
return { ok: true, result: { message_id: result.message_id, accepted: true } };
|
|
15996
|
+
}
|
|
15155
15997
|
return { ok: true, result: JSON.parse(JSON.stringify(result)) };
|
|
15156
15998
|
} catch (error) {
|
|
15157
15999
|
return { ok: false, error: classifyServiceError(error) };
|
|
@@ -15322,7 +16164,7 @@ var init_service = __esm({
|
|
|
15322
16164
|
} catch {
|
|
15323
16165
|
throw new RoomServiceError("external invite is invalid or exceeds the 48 KiB decoded limit");
|
|
15324
16166
|
}
|
|
15325
|
-
const digest =
|
|
16167
|
+
const digest = createHash5("sha256").update(decoded).digest("hex");
|
|
15326
16168
|
const receipt = await this.lock(id, async () => {
|
|
15327
16169
|
const room = await this.store.load(id);
|
|
15328
16170
|
this.assertMutable(room, "accept an external invite for");
|
|
@@ -15335,7 +16177,7 @@ var init_service = __esm({
|
|
|
15335
16177
|
} catch {
|
|
15336
16178
|
throw new RoomServiceError("external invite was rejected");
|
|
15337
16179
|
}
|
|
15338
|
-
const cid =
|
|
16180
|
+
const cid = ContainerIdSchema2.parse(added.container_id);
|
|
15339
16181
|
if (request.expected_cid !== void 0 && cid !== request.expected_cid) {
|
|
15340
16182
|
throw new RoomServiceError("external invite inviter CID did not match --expected-cid");
|
|
15341
16183
|
}
|
|
@@ -15439,9 +16281,9 @@ var init_service = __esm({
|
|
|
15439
16281
|
return seat;
|
|
15440
16282
|
}
|
|
15441
16283
|
hasRuntimeCommandGrant(room, callerCid, command) {
|
|
15442
|
-
if (room.command_grants.some((grant) => grant.caller_cid === callerCid && grant.command
|
|
16284
|
+
if (room.command_grants.some((grant) => grant.caller_cid === callerCid && commandGrantMatches(grant.command, command))) return true;
|
|
15443
16285
|
const caller = room.seats.find((seat) => seat.state === "active" && seat.identity === callerCid);
|
|
15444
|
-
return caller !== void 0 && room.role_command_grants.some((grant) => grant.role === caller.role && grant.commands.
|
|
16286
|
+
return caller !== void 0 && room.role_command_grants.some((grant) => grant.role === caller.role && grant.commands.some((pattern) => commandGrantMatches(pattern, command)));
|
|
15445
16287
|
}
|
|
15446
16288
|
async beginRemovalUnlocked(room, seat, notify) {
|
|
15447
16289
|
if (seat.state === "pending" || isCancelledExternalSeat(seat)) {
|
|
@@ -15749,7 +16591,7 @@ var init_service = __esm({
|
|
|
15749
16591
|
}
|
|
15750
16592
|
/** List the operator-managed runtime-command grants for one room. */
|
|
15751
16593
|
assertRegisteredConsumerName(room, name) {
|
|
15752
|
-
if (name.startsWith("consumer.") && !(room.consumer_commands ?? []).some((definition) => definition.name === name)) {
|
|
16594
|
+
if (name.startsWith("consumer.") && !name.endsWith(".*") && !(room.consumer_commands ?? []).some((definition) => definition.name === name)) {
|
|
15753
16595
|
throw new RoomServiceError("consumer command is not registered");
|
|
15754
16596
|
}
|
|
15755
16597
|
}
|
|
@@ -15893,7 +16735,7 @@ var init_service = __esm({
|
|
|
15893
16735
|
return saved.role_command_grants.map((grant) => ({ role: grant.role, commands: [...grant.commands] }));
|
|
15894
16736
|
});
|
|
15895
16737
|
}
|
|
15896
|
-
/** Grant one
|
|
16738
|
+
/** Grant one command selector to one active authenticated room identity. Idempotent. */
|
|
15897
16739
|
async grantRuntimeCommand(roomId, input) {
|
|
15898
16740
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
15899
16741
|
const request = RuntimeCommandGrantInputSchema.parse(input);
|
|
@@ -15912,7 +16754,7 @@ var init_service = __esm({
|
|
|
15912
16754
|
return saved.command_grants.map((grant) => ({ ...grant }));
|
|
15913
16755
|
});
|
|
15914
16756
|
}
|
|
15915
|
-
/**
|
|
16757
|
+
/** Remove only the exact stored selector; overlapping grants remain effective. */
|
|
15916
16758
|
async revokeRuntimeCommand(roomId, input) {
|
|
15917
16759
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
15918
16760
|
const request = RuntimeCommandGrantInputSchema.parse(input);
|
|
@@ -15927,29 +16769,38 @@ var init_service = __esm({
|
|
|
15927
16769
|
return saved.command_grants.map((grant) => ({ ...grant }));
|
|
15928
16770
|
});
|
|
15929
16771
|
}
|
|
16772
|
+
/** Internal authenticated API; numeric cursors count only this viewer's visible messages. */
|
|
16773
|
+
async participantHistory(roomId, viewerCid, page = {}) {
|
|
16774
|
+
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
16775
|
+
const request = ParticipantHistoryPageSchema.parse(page);
|
|
16776
|
+
return this.lock(id, async () => {
|
|
16777
|
+
const room = await this.store.load(id);
|
|
16778
|
+
if (room.state !== "active" || !room.seats.some((seat) => seat.state === "active" && seat.identity === viewerCid)) {
|
|
16779
|
+
throw new ThreadFailure("unauthorized");
|
|
16780
|
+
}
|
|
16781
|
+
const records = [];
|
|
16782
|
+
let after = 0;
|
|
16783
|
+
for (; ; ) {
|
|
16784
|
+
const batch = await this.store.read(id, { after, limit: JOURNAL_WORK_BATCH_SIZE2 });
|
|
16785
|
+
if (batch.length === 0) break;
|
|
16786
|
+
const last = batch[batch.length - 1];
|
|
16787
|
+
if (last.seq <= after || batch.some((row) => row.room_id !== id)) throw new Error("invalid participant history archive page");
|
|
16788
|
+
records.push(...batch);
|
|
16789
|
+
after = last.seq;
|
|
16790
|
+
}
|
|
16791
|
+
return projectParticipantHistory(room, records, viewerCid, request);
|
|
16792
|
+
});
|
|
16793
|
+
}
|
|
15930
16794
|
async history(roomId, options = {}) {
|
|
15931
16795
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
15932
16796
|
const { view, ...page } = HistoryOptionsSchema.parse(options);
|
|
15933
16797
|
const records = view === "participant" ? await queryStore2(this.store, id, { kind: "message", after: page.after, limit: page.limit }) : await this.store.read(id, page);
|
|
15934
16798
|
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
|
-
});
|
|
16799
|
+
const projected = records.filter((record) => record.kind === "message").map((record) => publicHistoryMessage(record, record.author_alias === void 0 ? record.author : {
|
|
16800
|
+
identity: record.author_alias.participant_id,
|
|
16801
|
+
display_name: record.author_alias.alias,
|
|
16802
|
+
role: record.author.role
|
|
16803
|
+
}));
|
|
15953
16804
|
return byteBoundedHistoryPage(projected.slice(0, page.limit ?? Number.MAX_SAFE_INTEGER));
|
|
15954
16805
|
}
|
|
15955
16806
|
/**
|
|
@@ -16509,7 +17360,7 @@ import * as nodeFs2 from "node:fs";
|
|
|
16509
17360
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
16510
17361
|
import { basename, dirname as dirname3, join as join4 } from "node:path";
|
|
16511
17362
|
import Database from "better-sqlite3";
|
|
16512
|
-
var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
|
|
17363
|
+
var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, SQLITE_V2_EXTENSION_DDL, CoworkStorageError, RoomQueue, CoworkStore;
|
|
16513
17364
|
var init_storage = __esm({
|
|
16514
17365
|
"src/storage.ts"() {
|
|
16515
17366
|
"use strict";
|
|
@@ -16518,9 +17369,24 @@ var init_storage = __esm({
|
|
|
16518
17369
|
DIRECTORY_MODE2 = 448;
|
|
16519
17370
|
FILE_MODE2 = 384;
|
|
16520
17371
|
NO_FOLLOW2 = nodeFs2.constants.O_NOFOLLOW ?? 0;
|
|
16521
|
-
SQLITE_SCHEMA_VERSION =
|
|
17372
|
+
SQLITE_SCHEMA_VERSION = 2;
|
|
16522
17373
|
DEFAULT_WORK_BATCH_SIZE = 64;
|
|
16523
17374
|
utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
17375
|
+
SQLITE_V2_EXTENSION_DDL = `
|
|
17376
|
+
CREATE UNIQUE INDEX IF NOT EXISTS records_thread_creation_key
|
|
17377
|
+
ON records(json_extract(payload_json,'$.author.identity'),
|
|
17378
|
+
json_extract(payload_json,'$.thread_root.idempotency_key'))
|
|
17379
|
+
WHERE kind='message' AND json_type(payload_json,'$.thread_root')='object';
|
|
17380
|
+
CREATE INDEX IF NOT EXISTS records_thread_id
|
|
17381
|
+
ON records(json_extract(payload_json,'$.scope.thread_id'))
|
|
17382
|
+
WHERE kind='message' AND json_type(payload_json,'$.scope')='object';
|
|
17383
|
+
DROP INDEX IF EXISTS records_source_message;
|
|
17384
|
+
DROP INDEX IF EXISTS records_source_file;
|
|
17385
|
+
CREATE UNIQUE INDEX records_source_message ON records(source_msg_id)
|
|
17386
|
+
WHERE source_msg_id IS NOT NULL;
|
|
17387
|
+
CREATE UNIQUE INDEX records_source_file ON records(source_file_id)
|
|
17388
|
+
WHERE source_file_id IS NOT NULL;
|
|
17389
|
+
`;
|
|
16524
17390
|
CoworkStorageError = class extends Error {
|
|
16525
17391
|
constructor(message, options) {
|
|
16526
17392
|
super(message, options);
|
|
@@ -16924,55 +17790,65 @@ var init_storage = __esm({
|
|
|
16924
17790
|
try {
|
|
16925
17791
|
this.secureSqliteFiles(path);
|
|
16926
17792
|
db = new Database(path, { fileMustExist: !create });
|
|
17793
|
+
const activeDb = db;
|
|
16927
17794
|
if (guardFd !== void 0) this.validateOpenPath(guardFd, path, "room archive database", "file", true);
|
|
16928
17795
|
this.fs.chmodSync(path, FILE_MODE2);
|
|
16929
|
-
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
17796
|
+
const existingVersion = create ? void 0 : activeDb.pragma("user_version", { simple: true });
|
|
17797
|
+
if (existingVersion !== void 0 && existingVersion !== 1 && existingVersion !== SQLITE_SCHEMA_VERSION) {
|
|
17798
|
+
throw new CoworkStorageError(`unsupported room archive schema version ${existingVersion}`);
|
|
17799
|
+
}
|
|
17800
|
+
activeDb.pragma("journal_mode = WAL");
|
|
17801
|
+
activeDb.pragma("synchronous = FULL");
|
|
17802
|
+
activeDb.pragma("foreign_keys = ON");
|
|
17803
|
+
activeDb.pragma("busy_timeout = 5000");
|
|
16933
17804
|
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
|
-
|
|
17805
|
+
activeDb.transaction(() => {
|
|
17806
|
+
activeDb.exec(`CREATE TABLE records (
|
|
17807
|
+
seq INTEGER PRIMARY KEY, record_id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, at TEXT NOT NULL,
|
|
17808
|
+
payload_json TEXT NOT NULL, blob_path TEXT, message_id TEXT, file_id TEXT, intent_record_id TEXT,
|
|
17809
|
+
recipient_identity TEXT, source_msg_id INTEGER, source_file_id INTEGER, category TEXT,
|
|
17810
|
+
briefing_role TEXT, briefing_version INTEGER, membership_epoch INTEGER
|
|
17811
|
+
);
|
|
17812
|
+
CREATE TABLE record_recipients (
|
|
17813
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
17814
|
+
recipient_identity TEXT NOT NULL, category TEXT, briefing_role TEXT,
|
|
17815
|
+
briefing_version INTEGER, PRIMARY KEY(record_seq, recipient_identity)
|
|
17816
|
+
);
|
|
17817
|
+
CREATE TABLE relay_intent_work (
|
|
17818
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
17819
|
+
recipient_identity TEXT NOT NULL, PRIMARY KEY(record_seq, recipient_identity)
|
|
17820
|
+
);
|
|
17821
|
+
CREATE INDEX relay_work_source ON relay_intent_work(record_seq, recipient_identity);
|
|
17822
|
+
CREATE INDEX records_kind_seq ON records(kind, seq);
|
|
17823
|
+
CREATE INDEX records_message ON records(message_id, kind, seq);
|
|
17824
|
+
CREATE INDEX records_file ON records(file_id, kind, seq);
|
|
17825
|
+
CREATE INDEX records_intent_result ON records(intent_record_id, kind);
|
|
17826
|
+
CREATE INDEX records_relay_recipient ON records(kind, recipient_identity, seq);
|
|
17827
|
+
CREATE UNIQUE INDEX records_source_message ON records(source_msg_id) WHERE kind='message' AND source_msg_id IS NOT NULL;
|
|
17828
|
+
CREATE UNIQUE INDEX records_source_file ON records(source_file_id) WHERE kind='file' AND source_file_id IS NOT NULL;
|
|
17829
|
+
CREATE INDEX records_briefing ON records(category, briefing_role, briefing_version, seq);
|
|
17830
|
+
CREATE INDEX records_membership_epoch ON records(category, membership_epoch);
|
|
17831
|
+
CREATE INDEX recipients_identity ON record_recipients(recipient_identity, record_seq);
|
|
17832
|
+
CREATE INDEX recipients_briefing_delivery ON record_recipients
|
|
17833
|
+
(recipient_identity, category, briefing_role, briefing_version, record_seq);`);
|
|
17834
|
+
activeDb.exec(SQLITE_V2_EXTENSION_DDL);
|
|
17835
|
+
activeDb.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
17836
|
+
}).immediate();
|
|
16963
17837
|
this.reconciledBlobRooms.add(roomId);
|
|
16964
17838
|
} else {
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
17839
|
+
if (existingVersion === 1) {
|
|
17840
|
+
activeDb.transaction(() => {
|
|
17841
|
+
activeDb.exec(SQLITE_V2_EXTENSION_DDL);
|
|
17842
|
+
activeDb.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
17843
|
+
}).immediate();
|
|
16968
17844
|
}
|
|
16969
17845
|
if (!this.reconciledBlobRooms.has(roomId)) {
|
|
16970
|
-
this.reconcileBlobDirectory(roomId,
|
|
17846
|
+
this.reconcileBlobDirectory(roomId, activeDb);
|
|
16971
17847
|
this.reconciledBlobRooms.add(roomId);
|
|
16972
17848
|
}
|
|
16973
17849
|
}
|
|
16974
17850
|
this.secureSqliteFiles(path);
|
|
16975
|
-
const result = work(
|
|
17851
|
+
const result = work(activeDb);
|
|
16976
17852
|
this.secureSqliteFiles(path);
|
|
16977
17853
|
return result;
|
|
16978
17854
|
} catch (error) {
|