@ours.network/cowork 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.js CHANGED
@@ -4767,6 +4767,615 @@ var init_config = __esm({
4767
4767
  }
4768
4768
  });
4769
4769
 
4770
+ // src/contracts.ts
4771
+ import { createHash } from "node:crypto";
4772
+ function utf8Bounded(label, maximumBytes) {
4773
+ return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
4774
+ (value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
4775
+ `${label} must be at most ${maximumBytes} UTF-8 bytes`
4776
+ );
4777
+ }
4778
+ function isStrictRfc3339(value) {
4779
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
4780
+ if (!match) return false;
4781
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText] = match;
4782
+ const year = Number(yearText);
4783
+ const month = Number(monthText);
4784
+ const day = Number(dayText);
4785
+ const hour = Number(hourText);
4786
+ const minute = Number(minuteText);
4787
+ const second = Number(secondText);
4788
+ const offsetHour = offsetHourText === void 0 ? 0 : Number(offsetHourText);
4789
+ const offsetMinute = offsetMinuteText === void 0 ? 0 : Number(offsetMinuteText);
4790
+ if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
4791
+ if (offsetHour > 23 || offsetMinute > 59) return false;
4792
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
4793
+ const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
4794
+ return day >= 1 && day <= days[month - 1];
4795
+ }
4796
+ function refineRoomLineage(room, context) {
4797
+ const pendingIdentityName = `cowork-room-${room.room_id}`;
4798
+ const exactPacketPending = room.state === "provisioning" && room.status === "packet_pending" && room.identity_name === pendingIdentityName && room.invites.length === 0 && room.seats.length === 0 && room.activated_at === void 0 && room.closed_at === void 0;
4799
+ if (room.identity_cid === "" && !exactPacketPending) {
4800
+ context.addIssue({
4801
+ code: external_exports.ZodIssueCode.custom,
4802
+ path: ["identity_cid"],
4803
+ message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
4804
+ });
4805
+ }
4806
+ if (room.identity_cid !== "" && room.status === "packet_pending") {
4807
+ context.addIssue({
4808
+ code: external_exports.ZodIssueCode.custom,
4809
+ path: ["status"],
4810
+ message: "packet_pending status requires an empty identity_cid"
4811
+ });
4812
+ }
4813
+ const pendingByRecovery = /* @__PURE__ */ new Map();
4814
+ for (const [index, invite] of room.invites.entries()) {
4815
+ if (invite.recovery_of === void 0) continue;
4816
+ const recoveryOf = invite.recovery_of;
4817
+ const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
4818
+ const validSourceState = invite.state === "receipt_pending" ? source?.state === "replacement_required" : invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required" ? source?.state === "revoked" : invite.state === "revoked" ? invite.recovery_confirmed === true ? source?.state === "revoked" : source?.state === "replacement_required" || source?.state === "revoked" : false;
4819
+ if (!source || source.invite_id === invite.invite_id || !validSourceState) {
4820
+ context.addIssue({
4821
+ code: external_exports.ZodIssueCode.custom,
4822
+ path: ["invites", index, "recovery_of"],
4823
+ message: "recovery_of must point to a source invite in the state required by this recovery lineage"
4824
+ });
4825
+ } else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
4826
+ context.addIssue({
4827
+ code: external_exports.ZodIssueCode.custom,
4828
+ path: ["invites", index],
4829
+ message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
4830
+ });
4831
+ }
4832
+ if (invite.state === "receipt_pending") {
4833
+ const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
4834
+ pendingByRecovery.set(recoveryOf, count);
4835
+ if (count > 1) {
4836
+ context.addIssue({
4837
+ code: external_exports.ZodIssueCode.custom,
4838
+ path: ["invites", index, "recovery_of"],
4839
+ message: "only one receipt_pending invite may exist per recovery_of pointer"
4840
+ });
4841
+ }
4842
+ }
4843
+ }
4844
+ }
4845
+ function migrateRoomV1(room, mintParticipantId) {
4846
+ return RoomSchema.parse({
4847
+ ...room,
4848
+ version: 2,
4849
+ mission: { ...room.mission, briefing_version: 1 },
4850
+ role_briefings: {},
4851
+ anonymous: false,
4852
+ quiet_membership: false,
4853
+ membership_epoch: 0,
4854
+ seats: room.seats.map((seat) => ({
4855
+ ...seat,
4856
+ participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
4857
+ state: "active"
4858
+ }))
4859
+ });
4860
+ }
4861
+ function refineRelaySubject(record, context) {
4862
+ if (record.kind !== "relay_intent" && record.kind !== "relay_result") return;
4863
+ if (record.message_id === void 0 === (record.file_id === void 0)) {
4864
+ context.addIssue({
4865
+ code: external_exports.ZodIssueCode.custom,
4866
+ path: ["message_id"],
4867
+ message: "relay records require exactly one of message_id or file_id"
4868
+ });
4869
+ }
4870
+ }
4871
+ function refineFileRecord(record, context) {
4872
+ if (record.kind !== "file" || record.data_base64 === void 0) return;
4873
+ const bytes = Buffer.from(record.data_base64, "base64");
4874
+ if (bytes.toString("base64") !== record.data_base64) {
4875
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["data_base64"], message: "file bytes must use canonical base64" });
4876
+ }
4877
+ if (bytes.length !== record.size) {
4878
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["size"], message: "file size must match decoded bytes" });
4879
+ }
4880
+ const digest = createHash("sha256").update(bytes).digest("hex");
4881
+ if (digest !== record.sha256) {
4882
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["sha256"], message: "file sha256 must match decoded bytes" });
4883
+ }
4884
+ }
4885
+ function refineMessageCategory(message, context) {
4886
+ const requires = (field, present) => {
4887
+ if (present && message[field] === void 0) {
4888
+ context.addIssue({
4889
+ code: external_exports.ZodIssueCode.custom,
4890
+ path: [field],
4891
+ message: `${message.category} messages require ${field}`
4892
+ });
4893
+ }
4894
+ if (!present && message[field] !== void 0) {
4895
+ context.addIssue({
4896
+ code: external_exports.ZodIssueCode.custom,
4897
+ path: [field],
4898
+ message: `${field} is forbidden on ${message.category} messages`
4899
+ });
4900
+ }
4901
+ };
4902
+ requires("briefing_role", message.category === "role_briefing");
4903
+ requires("membership", message.category === "membership");
4904
+ if (message.category === "role_briefing" && message.briefing_version === void 0) {
4905
+ context.addIssue({
4906
+ code: external_exports.ZodIssueCode.custom,
4907
+ path: ["briefing_version"],
4908
+ message: "role_briefing messages require briefing_version"
4909
+ });
4910
+ }
4911
+ if (message.category === "chat" || message.category === "membership") {
4912
+ if (message.briefing_version !== void 0) {
4913
+ context.addIssue({
4914
+ code: external_exports.ZodIssueCode.custom,
4915
+ path: ["briefing_version"],
4916
+ message: `briefing_version is forbidden on ${message.category} messages`
4917
+ });
4918
+ }
4919
+ }
4920
+ }
4921
+ var MAX_TEXT_BYTES, MAX_FILE_BYTES, MAX_HISTORY_PAGE_BYTES, MAX_MANAGEMENT_RESPONSE_BYTES, MAX_FILE_NAME_BYTES, MAX_MIME_BYTES, MAX_ROLE_BYTES, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoleSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
4922
+ var init_contracts = __esm({
4923
+ "src/contracts.ts"() {
4924
+ "use strict";
4925
+ init_zod();
4926
+ MAX_TEXT_BYTES = 262144;
4927
+ MAX_FILE_BYTES = 2 * 1024 * 1024;
4928
+ MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
4929
+ MAX_MANAGEMENT_RESPONSE_BYTES = MAX_HISTORY_PAGE_BYTES + 1024 * 1024;
4930
+ MAX_FILE_NAME_BYTES = 255;
4931
+ MAX_MIME_BYTES = 255;
4932
+ MAX_ROLE_BYTES = 256;
4933
+ NonEmptyStringSchema = external_exports.string().min(1);
4934
+ PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
4935
+ LowerCrockfordUlidSchema = external_exports.string().regex(
4936
+ /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4937
+ "must be a 26-character lowercase Crockford ULID"
4938
+ );
4939
+ Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
4940
+ RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4941
+ MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
4942
+ MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
4943
+ FileNameSchema = utf8Bounded("file name", MAX_FILE_NAME_BYTES).refine((value) => value !== "." && value !== "..", "file name must not be a relative path token").refine((value) => !/[\x00/\\]/.test(value), "file name must be a single path-free name");
4944
+ FileMimeSchema = external_exports.string().refine(
4945
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_MIME_BYTES,
4946
+ `file MIME metadata must be at most ${MAX_MIME_BYTES} UTF-8 bytes`
4947
+ );
4948
+ RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
4949
+ SeatStateSchema = external_exports.enum(["active", "removed"]);
4950
+ InviteModeSchema = external_exports.enum(["one_time", "public"]);
4951
+ DEFAULT_ROLE = "Participant";
4952
+ InviteStateSchema = external_exports.enum([
4953
+ "live",
4954
+ "consumed",
4955
+ "revoked",
4956
+ "replacement_required",
4957
+ "receipt_pending"
4958
+ ]);
4959
+ RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
4960
+ SeatV1Schema = external_exports.object({
4961
+ identity: NonEmptyStringSchema,
4962
+ display_name: NonEmptyStringSchema,
4963
+ role: RoleSchema,
4964
+ invite_id: NonEmptyStringSchema,
4965
+ accepted_at: Rfc3339Schema
4966
+ }).strict();
4967
+ SeatSchema = external_exports.object({
4968
+ identity: NonEmptyStringSchema,
4969
+ display_name: NonEmptyStringSchema,
4970
+ role: RoleSchema,
4971
+ invite_id: NonEmptyStringSchema,
4972
+ accepted_at: Rfc3339Schema,
4973
+ participant_id: LowerCrockfordUlidSchema,
4974
+ state: SeatStateSchema,
4975
+ alias: NonEmptyStringSchema.optional(),
4976
+ removed_at: Rfc3339Schema.optional(),
4977
+ removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
4978
+ replaces_seat: LowerCrockfordUlidSchema.optional(),
4979
+ bounced_at: Rfc3339Schema.optional()
4980
+ }).strict().superRefine((seat, context) => {
4981
+ if (seat.state === "removed") {
4982
+ if (seat.removed_at === void 0) {
4983
+ context.addIssue({
4984
+ code: external_exports.ZodIssueCode.custom,
4985
+ path: ["removed_at"],
4986
+ message: "removed seats require removed_at"
4987
+ });
4988
+ }
4989
+ if (seat.removed_epoch === void 0) {
4990
+ context.addIssue({
4991
+ code: external_exports.ZodIssueCode.custom,
4992
+ path: ["removed_epoch"],
4993
+ message: "removed seats require removed_epoch"
4994
+ });
4995
+ }
4996
+ } else {
4997
+ for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
4998
+ if (seat[field] !== void 0) {
4999
+ context.addIssue({
5000
+ code: external_exports.ZodIssueCode.custom,
5001
+ path: [field],
5002
+ message: `${field} is reserved for removed seats`
5003
+ });
5004
+ }
5005
+ }
5006
+ }
5007
+ });
5008
+ RoomInviteSchema = external_exports.object({
5009
+ invite_id: NonEmptyStringSchema,
5010
+ mode: InviteModeSchema,
5011
+ role: RoleSchema,
5012
+ min_accepts: PositiveSafeIntegerSchema,
5013
+ accepted_cids: external_exports.array(NonEmptyStringSchema),
5014
+ state: InviteStateSchema,
5015
+ recovery_of: NonEmptyStringSchema.optional(),
5016
+ recovery_confirmed: external_exports.boolean().optional(),
5017
+ created_at: Rfc3339Schema,
5018
+ replaces_seat: LowerCrockfordUlidSchema.optional()
5019
+ }).strict().superRefine((invite, context) => {
5020
+ if (invite.mode === "one_time" && invite.min_accepts !== 1) {
5021
+ context.addIssue({
5022
+ code: external_exports.ZodIssueCode.custom,
5023
+ path: ["min_accepts"],
5024
+ message: "one_time invites require min_accepts === 1"
5025
+ });
5026
+ }
5027
+ if (invite.state === "receipt_pending" && invite.recovery_of === void 0) {
5028
+ context.addIssue({
5029
+ code: external_exports.ZodIssueCode.custom,
5030
+ path: ["recovery_of"],
5031
+ message: "receipt_pending invites require recovery_of"
5032
+ });
5033
+ }
5034
+ if (invite.recovery_of === void 0 && invite.recovery_confirmed !== void 0) {
5035
+ context.addIssue({
5036
+ code: external_exports.ZodIssueCode.custom,
5037
+ path: ["recovery_confirmed"],
5038
+ message: "recovery_confirmed is forbidden without recovery_of"
5039
+ });
5040
+ }
5041
+ if (invite.recovery_of !== void 0 && invite.recovery_confirmed === void 0) {
5042
+ context.addIssue({
5043
+ code: external_exports.ZodIssueCode.custom,
5044
+ path: ["recovery_confirmed"],
5045
+ message: "recovery_confirmed is required with recovery_of"
5046
+ });
5047
+ }
5048
+ if (invite.state === "receipt_pending" && invite.recovery_confirmed !== false) {
5049
+ context.addIssue({
5050
+ code: external_exports.ZodIssueCode.custom,
5051
+ path: ["recovery_confirmed"],
5052
+ message: "receipt_pending recovery lineage must be unconfirmed"
5053
+ });
5054
+ }
5055
+ if (invite.recovery_of !== void 0 && (invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required") && invite.recovery_confirmed !== true) {
5056
+ context.addIssue({
5057
+ code: external_exports.ZodIssueCode.custom,
5058
+ path: ["recovery_confirmed"],
5059
+ message: "live, consumed, and replacement_required recovery lineage must be confirmed"
5060
+ });
5061
+ }
5062
+ if (invite.state === "receipt_pending" && invite.accepted_cids.length > 0) {
5063
+ context.addIssue({
5064
+ code: external_exports.ZodIssueCode.custom,
5065
+ path: ["accepted_cids"],
5066
+ message: "receipt_pending invites cannot have accepted CIDs"
5067
+ });
5068
+ }
5069
+ });
5070
+ MissionV1Schema = external_exports.object({
5071
+ goal: MissionTextSchema,
5072
+ briefing: MissionTextSchema
5073
+ }).strict();
5074
+ MissionSchema = external_exports.object({
5075
+ goal: MissionTextSchema,
5076
+ briefing: MissionTextSchema,
5077
+ briefing_version: PositiveSafeIntegerSchema
5078
+ }).strict();
5079
+ RoleBriefingSchema = external_exports.object({
5080
+ text: MissionTextSchema,
5081
+ version: PositiveSafeIntegerSchema,
5082
+ updated_at: Rfc3339Schema
5083
+ }).strict();
5084
+ RoomCommonShape = {
5085
+ room_id: LowerCrockfordUlidSchema,
5086
+ identity_name: NonEmptyStringSchema,
5087
+ identity_cid: external_exports.string(),
5088
+ state: RoomStateSchema,
5089
+ status: NonEmptyStringSchema.optional(),
5090
+ invites: external_exports.array(RoomInviteSchema),
5091
+ created_at: Rfc3339Schema,
5092
+ activated_at: Rfc3339Schema.optional(),
5093
+ closed_at: Rfc3339Schema.optional()
5094
+ };
5095
+ RoomV1Schema = external_exports.object({
5096
+ ...RoomCommonShape,
5097
+ version: external_exports.literal(1),
5098
+ mission: MissionV1Schema,
5099
+ seats: external_exports.array(SeatV1Schema)
5100
+ }).strict().superRefine(refineRoomLineage);
5101
+ RoomSchema = external_exports.object({
5102
+ ...RoomCommonShape,
5103
+ version: external_exports.literal(2),
5104
+ mission: MissionSchema,
5105
+ role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
5106
+ anonymous: external_exports.boolean(),
5107
+ quiet_membership: external_exports.boolean(),
5108
+ membership_epoch: external_exports.number().int().nonnegative().safe(),
5109
+ seats: external_exports.array(SeatSchema)
5110
+ }).strict().superRefine((room, context) => {
5111
+ refineRoomLineage(room, context);
5112
+ const byParticipant = /* @__PURE__ */ new Map();
5113
+ const activeAliases = /* @__PURE__ */ new Set();
5114
+ for (const [index, seat] of room.seats.entries()) {
5115
+ if (byParticipant.has(seat.participant_id)) {
5116
+ context.addIssue({
5117
+ code: external_exports.ZodIssueCode.custom,
5118
+ path: ["seats", index, "participant_id"],
5119
+ message: "participant_id must be unique within the room"
5120
+ });
5121
+ }
5122
+ byParticipant.set(seat.participant_id, seat);
5123
+ if (room.anonymous && seat.alias === void 0) {
5124
+ context.addIssue({
5125
+ code: external_exports.ZodIssueCode.custom,
5126
+ path: ["seats", index, "alias"],
5127
+ message: "anonymous rooms require an alias on every seat"
5128
+ });
5129
+ }
5130
+ if (!room.anonymous && seat.alias !== void 0) {
5131
+ context.addIssue({
5132
+ code: external_exports.ZodIssueCode.custom,
5133
+ path: ["seats", index, "alias"],
5134
+ message: "aliases are reserved for anonymous rooms"
5135
+ });
5136
+ }
5137
+ if (seat.state === "active" && seat.alias !== void 0) {
5138
+ if (activeAliases.has(seat.alias)) {
5139
+ context.addIssue({
5140
+ code: external_exports.ZodIssueCode.custom,
5141
+ path: ["seats", index, "alias"],
5142
+ message: "active seats must hold distinct aliases"
5143
+ });
5144
+ }
5145
+ activeAliases.add(seat.alias);
5146
+ }
5147
+ if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
5148
+ context.addIssue({
5149
+ code: external_exports.ZodIssueCode.custom,
5150
+ path: ["seats", index, "removed_epoch"],
5151
+ message: "removed_epoch cannot exceed the room membership_epoch"
5152
+ });
5153
+ }
5154
+ }
5155
+ for (const [index, seat] of room.seats.entries()) {
5156
+ if (seat.replaces_seat === void 0) continue;
5157
+ const predecessor = byParticipant.get(seat.replaces_seat);
5158
+ if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
5159
+ context.addIssue({
5160
+ code: external_exports.ZodIssueCode.custom,
5161
+ path: ["seats", index, "replaces_seat"],
5162
+ message: "replaces_seat must reference a removed seat in this room"
5163
+ });
5164
+ continue;
5165
+ }
5166
+ if (predecessor.role !== seat.role) {
5167
+ context.addIssue({
5168
+ code: external_exports.ZodIssueCode.custom,
5169
+ path: ["seats", index, "role"],
5170
+ message: "a replacement seat must inherit the predecessor role"
5171
+ });
5172
+ }
5173
+ if (room.anonymous && seat.alias !== predecessor.alias) {
5174
+ context.addIssue({
5175
+ code: external_exports.ZodIssueCode.custom,
5176
+ path: ["seats", index, "alias"],
5177
+ message: "an anonymous replacement seat must inherit the predecessor alias"
5178
+ });
5179
+ }
5180
+ }
5181
+ });
5182
+ CreateRoomInputSchema = external_exports.object({
5183
+ goal: MissionTextSchema,
5184
+ briefing: MissionTextSchema,
5185
+ anonymous: external_exports.boolean().optional(),
5186
+ quiet_membership: external_exports.boolean().optional()
5187
+ }).strict();
5188
+ UpdateRoomInputSchema = external_exports.object({
5189
+ goal: MissionTextSchema.optional(),
5190
+ briefing: MissionTextSchema.optional(),
5191
+ status: NonEmptyStringSchema.optional(),
5192
+ quiet_membership: external_exports.boolean().optional()
5193
+ }).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
5194
+ RoleBriefingSetInputSchema = external_exports.object({
5195
+ role: RoleSchema,
5196
+ text: MissionTextSchema
5197
+ }).strict();
5198
+ RoleBriefingDeleteInputSchema = external_exports.object({
5199
+ role: RoleSchema
5200
+ }).strict();
5201
+ PostMessageInputSchema = external_exports.object({
5202
+ text: MessageTextSchema
5203
+ }).strict();
5204
+ AuthorSnapshotSchema = external_exports.object({
5205
+ identity: NonEmptyStringSchema,
5206
+ display_name: NonEmptyStringSchema,
5207
+ role: RoleSchema
5208
+ }).strict();
5209
+ RecordCommonShape = {
5210
+ version: external_exports.literal(1),
5211
+ room_id: LowerCrockfordUlidSchema,
5212
+ seq: PositiveSafeIntegerSchema,
5213
+ record_id: NonEmptyStringSchema,
5214
+ at: Rfc3339Schema
5215
+ };
5216
+ AppendCommonShape = {
5217
+ version: external_exports.literal(1),
5218
+ room_id: LowerCrockfordUlidSchema,
5219
+ at: Rfc3339Schema
5220
+ };
5221
+ MembershipNoticeSchema = external_exports.object({
5222
+ action: external_exports.enum(["remove"]),
5223
+ alias: NonEmptyStringSchema.optional(),
5224
+ role: RoleSchema.optional(),
5225
+ epoch: external_exports.number().int().nonnegative().safe()
5226
+ }).strict();
5227
+ AuthorAliasSchema = external_exports.object({
5228
+ participant_id: LowerCrockfordUlidSchema,
5229
+ alias: NonEmptyStringSchema
5230
+ }).strict();
5231
+ MessageShape = {
5232
+ kind: external_exports.literal("message"),
5233
+ message_id: LowerCrockfordUlidSchema,
5234
+ author: AuthorSnapshotSchema,
5235
+ author_alias: AuthorAliasSchema.optional(),
5236
+ category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
5237
+ briefing_role: RoleSchema.optional(),
5238
+ briefing_version: PositiveSafeIntegerSchema.optional(),
5239
+ membership: MembershipNoticeSchema.optional(),
5240
+ text: MessageTextSchema,
5241
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5242
+ const seen = /* @__PURE__ */ new Set();
5243
+ for (const [index, identity] of identities.entries()) {
5244
+ if (seen.has(identity)) {
5245
+ context.addIssue({
5246
+ code: external_exports.ZodIssueCode.custom,
5247
+ path: [index],
5248
+ message: "recipient identities must be unique"
5249
+ });
5250
+ }
5251
+ seen.add(identity);
5252
+ }
5253
+ }),
5254
+ source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5255
+ source_wire_id: NonEmptyStringSchema.optional()
5256
+ };
5257
+ RelayIntentShape = {
5258
+ kind: external_exports.literal("relay_intent"),
5259
+ message_id: LowerCrockfordUlidSchema.optional(),
5260
+ file_id: LowerCrockfordUlidSchema.optional(),
5261
+ recipient_identity: NonEmptyStringSchema
5262
+ };
5263
+ RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
5264
+ RelayResultShape = {
5265
+ kind: external_exports.literal("relay_result"),
5266
+ intent_record_id: NonEmptyStringSchema,
5267
+ message_id: LowerCrockfordUlidSchema.optional(),
5268
+ file_id: LowerCrockfordUlidSchema.optional(),
5269
+ recipient_identity: NonEmptyStringSchema,
5270
+ status: RelayResultStatusSchema,
5271
+ wire_id: NonEmptyStringSchema.optional(),
5272
+ metadata_wire_id: NonEmptyStringSchema.optional()
5273
+ };
5274
+ FileShape = {
5275
+ kind: external_exports.literal("file"),
5276
+ file_id: LowerCrockfordUlidSchema,
5277
+ author: AuthorSnapshotSchema,
5278
+ author_alias: AuthorAliasSchema.optional(),
5279
+ filename: FileNameSchema,
5280
+ mime: FileMimeSchema,
5281
+ size: external_exports.number().int().nonnegative().max(MAX_FILE_BYTES),
5282
+ sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
5283
+ data_base64: external_exports.string(),
5284
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5285
+ const seen = /* @__PURE__ */ new Set();
5286
+ for (const [index, identity] of identities.entries()) {
5287
+ if (seen.has(identity)) {
5288
+ context.addIssue({
5289
+ code: external_exports.ZodIssueCode.custom,
5290
+ path: [index],
5291
+ message: "recipient identities must be unique"
5292
+ });
5293
+ }
5294
+ seen.add(identity);
5295
+ }
5296
+ }),
5297
+ source_file_id: external_exports.number().int().nonnegative().safe(),
5298
+ source_wire_id: NonEmptyStringSchema.optional()
5299
+ };
5300
+ MembershipIntentShape = {
5301
+ kind: external_exports.literal("membership_intent"),
5302
+ action: external_exports.enum(["remove"]),
5303
+ participant_id: LowerCrockfordUlidSchema,
5304
+ recipient_identity: NonEmptyStringSchema,
5305
+ role: RoleSchema,
5306
+ alias: NonEmptyStringSchema.optional(),
5307
+ epoch: PositiveSafeIntegerSchema,
5308
+ notify: external_exports.boolean()
5309
+ };
5310
+ MembershipResultShape = {
5311
+ kind: external_exports.literal("membership_result"),
5312
+ intent_record_id: NonEmptyStringSchema,
5313
+ participant_id: LowerCrockfordUlidSchema,
5314
+ status: RelayStatusSchema,
5315
+ notified: external_exports.boolean(),
5316
+ key_material_retained: external_exports.literal(true),
5317
+ uncertain_after_restart: external_exports.literal(true).optional()
5318
+ };
5319
+ CloseNoticeIntentShape = {
5320
+ kind: external_exports.literal("close_notice_intent"),
5321
+ recipient_identity: NonEmptyStringSchema
5322
+ };
5323
+ CloseNoticeResultShape = {
5324
+ kind: external_exports.literal("close_notice_result"),
5325
+ intent_record_id: NonEmptyStringSchema,
5326
+ recipient_identity: NonEmptyStringSchema,
5327
+ status: RelayStatusSchema,
5328
+ notified: external_exports.boolean(),
5329
+ key_material_retained: external_exports.literal(true),
5330
+ uncertain_after_restart: external_exports.literal(true).optional()
5331
+ };
5332
+ MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
5333
+ FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
5334
+ RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5335
+ RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5336
+ MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
5337
+ MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
5338
+ CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
5339
+ CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
5340
+ RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5341
+ MessageRecordSchema,
5342
+ FileRecordSchema,
5343
+ RelayIntentRecordSchema,
5344
+ RelayResultRecordSchema,
5345
+ MembershipIntentRecordSchema,
5346
+ MembershipResultRecordSchema,
5347
+ CloseNoticeIntentRecordSchema,
5348
+ CloseNoticeResultRecordSchema
5349
+ ]);
5350
+ CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5351
+ if (record.record_id !== `${record.room_id}:${record.seq}`) {
5352
+ context.addIssue({
5353
+ code: external_exports.ZodIssueCode.custom,
5354
+ path: ["record_id"],
5355
+ message: 'record_id must equal room_id + ":" + seq'
5356
+ });
5357
+ }
5358
+ if (record.kind === "message") refineMessageCategory(record, context);
5359
+ refineRelaySubject(record, context);
5360
+ refineFileRecord(record, context);
5361
+ });
5362
+ AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5363
+ external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
5364
+ external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
5365
+ external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5366
+ external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5367
+ external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
5368
+ external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
5369
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
5370
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
5371
+ ]).superRefine((record, context) => {
5372
+ if (record.kind === "message") refineMessageCategory(record, context);
5373
+ refineRelaySubject(record, context);
5374
+ refineFileRecord(record, context);
5375
+ });
5376
+ }
5377
+ });
5378
+
4770
5379
  // src/packets.ts
4771
5380
  import { randomBytes } from "node:crypto";
4772
5381
  import * as nodeFs2 from "node:fs";
@@ -4831,6 +5440,26 @@ function renderInbox(value) {
4831
5440
  }
4832
5441
  return output;
4833
5442
  }
5443
+ function renderFileInbox(value) {
5444
+ const output = [];
5445
+ if (value.IsNil()) return output;
5446
+ for (let index = 0; ; index += 1) {
5447
+ const file = value.Reduce(index);
5448
+ if (file.IsNil()) break;
5449
+ output.push({
5450
+ file_id: Number(file.Reduce("file_id").Visualize()),
5451
+ sender_id: file.Reduce("sender_id").Visualize(),
5452
+ sender_name: file.Reduce("sender_name").Visualize(),
5453
+ filename: file.Reduce("filename").Visualize(),
5454
+ mime: file.Reduce("mime").Visualize(),
5455
+ data: Buffer.from(file.Reduce("data").GetBinary()),
5456
+ date: adaptTimeToRfc3339(file.Reduce("date").Visualize()),
5457
+ status: file.Reduce("status").Visualize(),
5458
+ wire_id: file.Reduce("wire_id").Visualize()
5459
+ });
5460
+ }
5461
+ return output;
5462
+ }
4834
5463
  function renderIntegerArray(value) {
4835
5464
  const output = [];
4836
5465
  if (value.IsNil()) return output;
@@ -4924,6 +5553,7 @@ var init_packets = __esm({
4924
5553
  async "src/packets.ts"() {
4925
5554
  "use strict";
4926
5555
  await init_adapt();
5556
+ init_contracts();
4927
5557
  PacketPersistenceError = class extends Error {
4928
5558
  constructor(message, options) {
4929
5559
  super(message, options);
@@ -5420,354 +6050,136 @@ var init_packets = __esm({
5420
6050
  };
5421
6051
  });
5422
6052
  }
5423
- async send(contactCid, body) {
5424
- return withScopeAsync(async (lifetime) => {
5425
- const result = await this.packet.mutatingTx(
5426
- "::a2a_messaging::send_message",
5427
- { contact: contactCid, text: body },
5428
- lifetime
5429
- );
5430
- const refused = !result.Reduce("downgrade_refused").IsNil();
5431
- return refused ? { status: "send_failed" } : { status: "queued", wire_id: nilString(result.Reduce("wire_id")) || void 0 };
5432
- });
6053
+ peekFileInbox() {
6054
+ return withScope((lifetime) => renderFileInbox(
6055
+ this.packet.readonlyTx("::actor::list_incoming_files", lifetime)
6056
+ ).filter((file) => file.status === "unread").map(({ status: _status, ...file }) => file));
5433
6057
  }
5434
- async removeContact(contactCid) {
6058
+ async consumeFileInbox(expectedIds) {
5435
6059
  return withScopeAsync(async (lifetime) => {
5436
6060
  const result = await this.packet.mutatingTx(
5437
- "::a2a_messaging::remove_contact",
5438
- { contact: contactCid },
6061
+ "::actor::consume_files",
6062
+ { expected_ids: expectedIds },
5439
6063
  lifetime
5440
6064
  );
5441
- const notified = strictBooleanValue(result.Reduce("notified"), "remove_contact notified");
5442
- const keyMaterialRetained = strictBooleanValue(
5443
- result.Reduce("key_material_retained"),
5444
- "remove_contact key_material_retained"
5445
- );
5446
- if (!keyMaterialRetained) {
5447
- throw new Error("remove_contact key_material_retained must be true");
5448
- }
5449
6065
  return {
5450
- status: notified ? "queued" : "send_failed",
5451
- notified,
5452
- key_material_retained: true
6066
+ consumed: renderIntegerArray(result.Reduce("consumed")),
6067
+ deferred: renderIntegerArray(result.Reduce("deferred"))
5453
6068
  };
5454
6069
  });
5455
6070
  }
5456
- async sign(canonicalJson2) {
6071
+ async send(contactCid, body) {
5457
6072
  return withScopeAsync(async (lifetime) => {
5458
6073
  const result = await this.packet.mutatingTx(
5459
- "::actor::sign_app_envelope",
5460
- { canonical_json: canonicalJson2 },
5461
- lifetime
5462
- );
5463
- return Buffer.from(result.Reduce("signature").GetBinary()).toString("base64url");
5464
- });
5465
- }
5466
- };
5467
- }
5468
- });
5469
-
5470
- // src/contracts.ts
5471
- function utf8Bounded(label, maximumBytes) {
5472
- return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
5473
- (value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
5474
- `${label} must be at most ${maximumBytes} UTF-8 bytes`
5475
- );
5476
- }
5477
- function isStrictRfc3339(value) {
5478
- const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
5479
- if (!match) return false;
5480
- const [, yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText] = match;
5481
- const year = Number(yearText);
5482
- const month = Number(monthText);
5483
- const day = Number(dayText);
5484
- const hour = Number(hourText);
5485
- const minute = Number(minuteText);
5486
- const second = Number(secondText);
5487
- const offsetHour = offsetHourText === void 0 ? 0 : Number(offsetHourText);
5488
- const offsetMinute = offsetMinuteText === void 0 ? 0 : Number(offsetMinuteText);
5489
- if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
5490
- if (offsetHour > 23 || offsetMinute > 59) return false;
5491
- const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
5492
- const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
5493
- return day >= 1 && day <= days[month - 1];
5494
- }
5495
- var MAX_TEXT_BYTES, MAX_ROLE_BYTES, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoleSchema, MissionTextSchema, MessageTextSchema, RoomStateSchema, InviteModeSchema, InviteStateSchema, RelayStatusSchema, SeatSchema, RoomInviteSchema, MissionSchema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, PostMessageInputSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MessageShape, RelayIntentShape, RelayResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
5496
- var init_contracts = __esm({
5497
- "src/contracts.ts"() {
5498
- "use strict";
5499
- init_zod();
5500
- MAX_TEXT_BYTES = 262144;
5501
- MAX_ROLE_BYTES = 256;
5502
- NonEmptyStringSchema = external_exports.string().min(1);
5503
- PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
5504
- LowerCrockfordUlidSchema = external_exports.string().regex(
5505
- /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
5506
- "must be a 26-character lowercase Crockford ULID"
5507
- );
5508
- Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
5509
- RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
5510
- MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
5511
- MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
5512
- RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
5513
- InviteModeSchema = external_exports.enum(["one_time", "public"]);
5514
- InviteStateSchema = external_exports.enum([
5515
- "live",
5516
- "consumed",
5517
- "revoked",
5518
- "replacement_required",
5519
- "receipt_pending"
5520
- ]);
5521
- RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
5522
- SeatSchema = external_exports.object({
5523
- identity: NonEmptyStringSchema,
5524
- display_name: NonEmptyStringSchema,
5525
- role: RoleSchema,
5526
- invite_id: NonEmptyStringSchema,
5527
- accepted_at: Rfc3339Schema
5528
- }).strict();
5529
- RoomInviteSchema = external_exports.object({
5530
- invite_id: NonEmptyStringSchema,
5531
- mode: InviteModeSchema,
5532
- role: RoleSchema,
5533
- min_accepts: PositiveSafeIntegerSchema,
5534
- accepted_cids: external_exports.array(NonEmptyStringSchema),
5535
- state: InviteStateSchema,
5536
- recovery_of: NonEmptyStringSchema.optional(),
5537
- recovery_confirmed: external_exports.boolean().optional(),
5538
- created_at: Rfc3339Schema
5539
- }).strict().superRefine((invite, context) => {
5540
- if (invite.mode === "one_time" && invite.min_accepts !== 1) {
5541
- context.addIssue({
5542
- code: external_exports.ZodIssueCode.custom,
5543
- path: ["min_accepts"],
5544
- message: "one_time invites require min_accepts === 1"
5545
- });
5546
- }
5547
- if (invite.state === "receipt_pending" && invite.recovery_of === void 0) {
5548
- context.addIssue({
5549
- code: external_exports.ZodIssueCode.custom,
5550
- path: ["recovery_of"],
5551
- message: "receipt_pending invites require recovery_of"
5552
- });
5553
- }
5554
- if (invite.recovery_of === void 0 && invite.recovery_confirmed !== void 0) {
5555
- context.addIssue({
5556
- code: external_exports.ZodIssueCode.custom,
5557
- path: ["recovery_confirmed"],
5558
- message: "recovery_confirmed is forbidden without recovery_of"
5559
- });
5560
- }
5561
- if (invite.recovery_of !== void 0 && invite.recovery_confirmed === void 0) {
5562
- context.addIssue({
5563
- code: external_exports.ZodIssueCode.custom,
5564
- path: ["recovery_confirmed"],
5565
- message: "recovery_confirmed is required with recovery_of"
5566
- });
5567
- }
5568
- if (invite.state === "receipt_pending" && invite.recovery_confirmed !== false) {
5569
- context.addIssue({
5570
- code: external_exports.ZodIssueCode.custom,
5571
- path: ["recovery_confirmed"],
5572
- message: "receipt_pending recovery lineage must be unconfirmed"
5573
- });
5574
- }
5575
- if (invite.recovery_of !== void 0 && (invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required") && invite.recovery_confirmed !== true) {
5576
- context.addIssue({
5577
- code: external_exports.ZodIssueCode.custom,
5578
- path: ["recovery_confirmed"],
5579
- message: "live, consumed, and replacement_required recovery lineage must be confirmed"
5580
- });
5581
- }
5582
- if (invite.state === "receipt_pending" && invite.accepted_cids.length > 0) {
5583
- context.addIssue({
5584
- code: external_exports.ZodIssueCode.custom,
5585
- path: ["accepted_cids"],
5586
- message: "receipt_pending invites cannot have accepted CIDs"
5587
- });
5588
- }
5589
- });
5590
- MissionSchema = external_exports.object({
5591
- goal: MissionTextSchema,
5592
- briefing: MissionTextSchema
5593
- }).strict();
5594
- RoomSchema = external_exports.object({
5595
- version: external_exports.literal(1),
5596
- room_id: LowerCrockfordUlidSchema,
5597
- identity_name: NonEmptyStringSchema,
5598
- identity_cid: external_exports.string(),
5599
- mission: MissionSchema,
5600
- state: RoomStateSchema,
5601
- status: NonEmptyStringSchema.optional(),
5602
- invites: external_exports.array(RoomInviteSchema),
5603
- seats: external_exports.array(SeatSchema),
5604
- created_at: Rfc3339Schema,
5605
- activated_at: Rfc3339Schema.optional(),
5606
- closed_at: Rfc3339Schema.optional()
5607
- }).strict().superRefine((room, context) => {
5608
- const pendingIdentityName = `cowork-room-${room.room_id}`;
5609
- const exactPacketPending = room.state === "provisioning" && room.status === "packet_pending" && room.identity_name === pendingIdentityName && room.invites.length === 0 && room.seats.length === 0 && room.activated_at === void 0 && room.closed_at === void 0;
5610
- if (room.identity_cid === "" && !exactPacketPending) {
5611
- context.addIssue({
5612
- code: external_exports.ZodIssueCode.custom,
5613
- path: ["identity_cid"],
5614
- message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
6074
+ "::a2a_messaging::send_message",
6075
+ { contact: contactCid, text: body },
6076
+ lifetime
6077
+ );
6078
+ const refused = !result.Reduce("downgrade_refused").IsNil();
6079
+ return refused ? { status: "send_failed" } : { status: "queued", wire_id: nilString(result.Reduce("wire_id")) || void 0 };
5615
6080
  });
5616
6081
  }
5617
- if (room.identity_cid !== "" && room.status === "packet_pending") {
5618
- context.addIssue({
5619
- code: external_exports.ZodIssueCode.custom,
5620
- path: ["status"],
5621
- message: "packet_pending status requires an empty identity_cid"
6082
+ async sendFile(contactCid, filename, mime, data) {
6083
+ const validName = FileNameSchema.parse(filename);
6084
+ const validMime = FileMimeSchema.parse(mime);
6085
+ if (data.length > MAX_FILE_BYTES) {
6086
+ throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
6087
+ }
6088
+ return withScopeAsync(async (lifetime) => {
6089
+ const result = await this.packet.mutatingTx(
6090
+ "::a2a_messaging::send_file",
6091
+ {
6092
+ contact: contactCid,
6093
+ filename: validName,
6094
+ mime: validMime,
6095
+ // The core contract takes bytes. A filesystem path here would make
6096
+ // recovery depend on staging ownership and is deliberately forbidden.
6097
+ data: this.packet.newBinary(data, lifetime)
6098
+ },
6099
+ lifetime
6100
+ );
6101
+ const refused = !result.Reduce("downgrade_refused").IsNil() || !result.Reduce("migrating").IsNil();
6102
+ return refused ? { status: "send_failed" } : { status: "queued", wire_id: nilString(result.Reduce("wire_id")) || void 0 };
5622
6103
  });
5623
6104
  }
5624
- const pendingByRecovery = /* @__PURE__ */ new Map();
5625
- for (const [index, invite] of room.invites.entries()) {
5626
- if (invite.recovery_of === void 0) continue;
5627
- const recoveryOf = invite.recovery_of;
5628
- const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
5629
- const validSourceState = invite.state === "receipt_pending" ? source?.state === "replacement_required" : invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required" ? source?.state === "revoked" : invite.state === "revoked" ? invite.recovery_confirmed === true ? source?.state === "revoked" : source?.state === "replacement_required" || source?.state === "revoked" : false;
5630
- if (!source || source.invite_id === invite.invite_id || !validSourceState) {
5631
- context.addIssue({
5632
- code: external_exports.ZodIssueCode.custom,
5633
- path: ["invites", index, "recovery_of"],
5634
- message: "recovery_of must point to a source invite in the state required by this recovery lineage"
5635
- });
5636
- } else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
5637
- context.addIssue({
5638
- code: external_exports.ZodIssueCode.custom,
5639
- path: ["invites", index],
5640
- message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
5641
- });
5642
- }
5643
- if (invite.state === "receipt_pending") {
5644
- const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
5645
- pendingByRecovery.set(recoveryOf, count);
5646
- if (count > 1) {
5647
- context.addIssue({
5648
- code: external_exports.ZodIssueCode.custom,
5649
- path: ["invites", index, "recovery_of"],
5650
- message: "only one receipt_pending invite may exist per recovery_of pointer"
5651
- });
6105
+ async removeContact(contactCid) {
6106
+ return withScopeAsync(async (lifetime) => {
6107
+ const result = await this.packet.mutatingTx(
6108
+ "::a2a_messaging::remove_contact",
6109
+ { contact: contactCid },
6110
+ lifetime
6111
+ );
6112
+ const notified = strictBooleanValue(result.Reduce("notified"), "remove_contact notified");
6113
+ const keyMaterialRetained = strictBooleanValue(
6114
+ result.Reduce("key_material_retained"),
6115
+ "remove_contact key_material_retained"
6116
+ );
6117
+ if (!keyMaterialRetained) {
6118
+ throw new Error("remove_contact key_material_retained must be true");
5652
6119
  }
5653
- }
6120
+ return {
6121
+ status: notified ? "queued" : "send_failed",
6122
+ notified,
6123
+ key_material_retained: true
6124
+ };
6125
+ });
5654
6126
  }
5655
- });
5656
- CreateRoomInputSchema = external_exports.object({
5657
- goal: MissionTextSchema,
5658
- briefing: MissionTextSchema
5659
- }).strict();
5660
- UpdateRoomInputSchema = external_exports.object({
5661
- goal: MissionTextSchema.optional(),
5662
- briefing: MissionTextSchema.optional(),
5663
- status: NonEmptyStringSchema.optional()
5664
- }).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
5665
- PostMessageInputSchema = external_exports.object({
5666
- text: MessageTextSchema
5667
- }).strict();
5668
- AuthorSnapshotSchema = external_exports.object({
5669
- identity: NonEmptyStringSchema,
5670
- display_name: NonEmptyStringSchema,
5671
- role: RoleSchema
5672
- }).strict();
5673
- RecordCommonShape = {
5674
- version: external_exports.literal(1),
5675
- room_id: LowerCrockfordUlidSchema,
5676
- seq: PositiveSafeIntegerSchema,
5677
- record_id: NonEmptyStringSchema,
5678
- at: Rfc3339Schema
5679
- };
5680
- AppendCommonShape = {
5681
- version: external_exports.literal(1),
5682
- room_id: LowerCrockfordUlidSchema,
5683
- at: Rfc3339Schema
5684
- };
5685
- MessageShape = {
5686
- kind: external_exports.literal("message"),
5687
- message_id: LowerCrockfordUlidSchema,
5688
- author: AuthorSnapshotSchema,
5689
- category: external_exports.enum(["briefing", "chat"]),
5690
- text: MessageTextSchema,
5691
- recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5692
- const seen = /* @__PURE__ */ new Set();
5693
- for (const [index, identity] of identities.entries()) {
5694
- if (seen.has(identity)) {
5695
- context.addIssue({
5696
- code: external_exports.ZodIssueCode.custom,
5697
- path: [index],
5698
- message: "recipient identities must be unique"
5699
- });
5700
- }
5701
- seen.add(identity);
5702
- }
5703
- }),
5704
- source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5705
- source_wire_id: NonEmptyStringSchema.optional()
5706
- };
5707
- RelayIntentShape = {
5708
- kind: external_exports.literal("relay_intent"),
5709
- message_id: LowerCrockfordUlidSchema,
5710
- recipient_identity: NonEmptyStringSchema
5711
- };
5712
- RelayResultShape = {
5713
- kind: external_exports.literal("relay_result"),
5714
- intent_record_id: NonEmptyStringSchema,
5715
- message_id: LowerCrockfordUlidSchema,
5716
- recipient_identity: NonEmptyStringSchema,
5717
- status: RelayStatusSchema,
5718
- wire_id: NonEmptyStringSchema.optional()
5719
- };
5720
- CloseNoticeIntentShape = {
5721
- kind: external_exports.literal("close_notice_intent"),
5722
- recipient_identity: NonEmptyStringSchema
5723
- };
5724
- CloseNoticeResultShape = {
5725
- kind: external_exports.literal("close_notice_result"),
5726
- intent_record_id: NonEmptyStringSchema,
5727
- recipient_identity: NonEmptyStringSchema,
5728
- status: RelayStatusSchema,
5729
- notified: external_exports.boolean(),
5730
- key_material_retained: external_exports.literal(true),
5731
- uncertain_after_restart: external_exports.literal(true).optional()
5732
- };
5733
- MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
5734
- RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5735
- RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5736
- CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
5737
- CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
5738
- RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5739
- MessageRecordSchema,
5740
- RelayIntentRecordSchema,
5741
- RelayResultRecordSchema,
5742
- CloseNoticeIntentRecordSchema,
5743
- CloseNoticeResultRecordSchema
5744
- ]);
5745
- CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5746
- if (record.record_id !== `${record.room_id}:${record.seq}`) {
5747
- context.addIssue({
5748
- code: external_exports.ZodIssueCode.custom,
5749
- path: ["record_id"],
5750
- message: 'record_id must equal room_id + ":" + seq'
6127
+ async sign(canonicalJson2) {
6128
+ return withScopeAsync(async (lifetime) => {
6129
+ const result = await this.packet.mutatingTx(
6130
+ "::actor::sign_app_envelope",
6131
+ { canonical_json: canonicalJson2 },
6132
+ lifetime
6133
+ );
6134
+ return Buffer.from(result.Reduce("signature").GetBinary()).toString("base64url");
5751
6135
  });
5752
6136
  }
5753
- });
5754
- AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5755
- external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
5756
- external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5757
- external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5758
- external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
5759
- external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
5760
- ]);
6137
+ };
5761
6138
  }
5762
6139
  });
5763
6140
 
5764
- // src/intake.ts
6141
+ // src/ulid.ts
5765
6142
  import { randomBytes as randomBytes2 } from "node:crypto";
6143
+ function generateUlid() {
6144
+ let time = Date.now();
6145
+ const output = new Array(26);
6146
+ for (let index = 9; index >= 0; index -= 1) {
6147
+ output[index] = CROCKFORD[time % 32];
6148
+ time = Math.floor(time / 32);
6149
+ }
6150
+ const entropy = randomBytes2(10);
6151
+ let bits = 0;
6152
+ let value = 0;
6153
+ let byteIndex = 0;
6154
+ for (let index = 10; index < 26; index += 1) {
6155
+ while (bits < 5) {
6156
+ value = value << 8 | entropy[byteIndex++];
6157
+ bits += 8;
6158
+ }
6159
+ bits -= 5;
6160
+ output[index] = CROCKFORD[value >>> bits & 31];
6161
+ }
6162
+ return output.join("");
6163
+ }
6164
+ var CROCKFORD;
6165
+ var init_ulid = __esm({
6166
+ "src/ulid.ts"() {
6167
+ "use strict";
6168
+ CROCKFORD = "0123456789abcdefghjkmnpqrstvwxyz";
6169
+ }
6170
+ });
6171
+
6172
+ // src/intake.ts
6173
+ import { createHash as createHash2 } from "node:crypto";
5766
6174
  function canonicalJson(value) {
5767
6175
  const encoded = JSON.stringify(canonicalValue(value));
5768
6176
  if (encoded === void 0) throw new TypeError("canonical JSON value is not serializable");
5769
6177
  return encoded;
5770
6178
  }
6179
+ async function sendSignedBody(packet, recipientIdentity, unsigned) {
6180
+ const signature = await packet.sign(canonicalJson(unsigned));
6181
+ return packet.send(recipientIdentity, canonicalJson({ ...unsigned, signature }));
6182
+ }
5771
6183
  function canonicalValue(value) {
5772
6184
  if (Array.isArray(value)) return value.map(canonicalValue);
5773
6185
  if (value !== null && typeof value === "object") {
@@ -5783,34 +6195,25 @@ function canonicalValue(value) {
5783
6195
  function unique(values) {
5784
6196
  return [...new Set(values)];
5785
6197
  }
5786
- function generateUlid() {
5787
- let time = Date.now();
5788
- const output = new Array(26);
5789
- for (let index = 9; index >= 0; index -= 1) {
5790
- output[index] = CROCKFORD[time % 32];
5791
- time = Math.floor(time / 32);
5792
- }
5793
- const entropy = randomBytes2(10);
5794
- let bits = 0;
5795
- let value = 0;
5796
- let byteIndex = 0;
5797
- for (let index = 10; index < 26; index += 1) {
5798
- while (bits < 5) {
5799
- value = value << 8 | entropy[byteIndex++];
5800
- bits += 8;
5801
- }
5802
- bits -= 5;
5803
- output[index] = CROCKFORD[value >>> bits & 31];
6198
+ function wireKind(category) {
6199
+ switch (category) {
6200
+ case "briefing":
6201
+ return "room_briefing";
6202
+ case "role_briefing":
6203
+ return "room_role_briefing";
6204
+ case "membership":
6205
+ return "room_membership";
6206
+ default:
6207
+ return "room_msg";
5804
6208
  }
5805
- return output.join("");
5806
6209
  }
5807
- var CROCKFORD, IntakePump;
6210
+ var IntakePump;
5808
6211
  var init_intake = __esm({
5809
6212
  "src/intake.ts"() {
5810
6213
  "use strict";
5811
6214
  init_zod();
5812
6215
  init_contracts();
5813
- CROCKFORD = "0123456789abcdefghjkmnpqrstvwxyz";
6216
+ init_ulid();
5814
6217
  IntakePump = class {
5815
6218
  store;
5816
6219
  packets;
@@ -5890,20 +6293,72 @@ var init_intake = __esm({
5890
6293
  async processAndRelayUnlocked(roomId, packet) {
5891
6294
  const snapshot = packet.peekInbox();
5892
6295
  for (const item of snapshot) await this.processInboxItem(roomId, packet, item);
6296
+ const fileSnapshot = packet.peekFileInbox();
6297
+ for (const item of fileSnapshot) await this.processFileInboxItem(roomId, packet, item);
5893
6298
  await this.completeSnapshotIntents(roomId);
5894
6299
  await this.relayPendingUnlocked(roomId, packet);
5895
6300
  }
6301
+ async processFileInboxItem(roomId, packet, item) {
6302
+ const parsedName = FileNameSchema.safeParse(item.filename);
6303
+ const parsedMime = FileMimeSchema.safeParse(item.mime);
6304
+ if (!parsedName.success || !parsedMime.success) {
6305
+ await packet.consumeFileInbox([item.file_id]);
6306
+ return;
6307
+ }
6308
+ if (item.data.length > MAX_FILE_BYTES) {
6309
+ throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
6310
+ }
6311
+ const room = await this.store.load(roomId);
6312
+ const seat = room.seats.find(
6313
+ (candidate) => candidate.identity === item.sender_id && candidate.state === "active"
6314
+ );
6315
+ if (room.state !== "active" || !seat) {
6316
+ if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
6317
+ await packet.consumeFileInbox([item.file_id]);
6318
+ return;
6319
+ }
6320
+ const records = await this.store.read(roomId);
6321
+ let file = this.findSourceFile(records, item);
6322
+ if (!file) {
6323
+ const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
6324
+ const bytes = Buffer.from(item.data);
6325
+ const appended = await this.store.append(roomId, {
6326
+ version: 1,
6327
+ kind: "file",
6328
+ room_id: roomId,
6329
+ at: Rfc3339Schema.parse(item.date),
6330
+ file_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
6331
+ author: { identity: seat.identity, display_name: seat.display_name, role: seat.role },
6332
+ ...room.anonymous && seat.alias !== void 0 ? { author_alias: { participant_id: seat.participant_id, alias: seat.alias } } : {},
6333
+ filename: parsedName.data,
6334
+ mime: parsedMime.data,
6335
+ size: bytes.length,
6336
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
6337
+ data_base64: bytes.toString("base64"),
6338
+ recipient_identities: recipientIdentities,
6339
+ source_file_id: item.file_id,
6340
+ ...item.wire_id === "" ? {} : { source_wire_id: item.wire_id }
6341
+ });
6342
+ if (appended.kind !== "file") throw new Error("storage returned the wrong participant file kind");
6343
+ file = appended;
6344
+ }
6345
+ await this.completeFileIntents(roomId, file);
6346
+ await packet.consumeFileInbox([item.file_id]);
6347
+ }
5896
6348
  async processInboxItem(roomId, packet, item) {
5897
6349
  const room = await this.store.load(roomId);
5898
- const seat = room.seats.find((candidate) => candidate.identity === item.sender_id);
6350
+ const seat = room.seats.find(
6351
+ (candidate) => candidate.identity === item.sender_id && candidate.state === "active"
6352
+ );
5899
6353
  if (room.state !== "active" || !seat) {
6354
+ if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
5900
6355
  await packet.consumeInbox([item.msg_id]);
5901
6356
  return;
5902
6357
  }
5903
6358
  const before = await this.store.read(roomId);
5904
6359
  let message = this.findSourceMessage(before, item);
5905
6360
  if (!message) {
5906
- const recipientIdentities = unique(room.seats.map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
6361
+ const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
5907
6362
  const appended = await this.store.append(roomId, {
5908
6363
  version: 1,
5909
6364
  kind: "message",
@@ -5915,6 +6370,9 @@ var init_intake = __esm({
5915
6370
  display_name: seat.display_name,
5916
6371
  role: seat.role
5917
6372
  },
6373
+ // In an anonymous room the archive keeps both identities (INV-R4);
6374
+ // the relay pump substitutes the alias into every outbound body.
6375
+ ...room.anonymous && seat.alias !== void 0 ? { author_alias: { participant_id: seat.participant_id, alias: seat.alias } } : {},
5918
6376
  category: "chat",
5919
6377
  text: item.text,
5920
6378
  recipient_identities: recipientIdentities,
@@ -5927,11 +6385,51 @@ var init_intake = __esm({
5927
6385
  await this.completeMessageIntents(roomId, message);
5928
6386
  await packet.consumeInbox([item.msg_id]);
5929
6387
  }
6388
+ /**
6389
+ * One content-free self-assertion per removed seat (spec §5.2, OC-8), so a
6390
+ * healthy ex-client stops sending. The durable bounced_at mark precedes the
6391
+ * best-effort send: at-most-once, and a hostile peer gets nothing further.
6392
+ */
6393
+ async bounceRemovedSender(roomId, room, packet, item) {
6394
+ const removed = room.seats.find(
6395
+ (candidate) => candidate.identity === item.sender_id && candidate.state === "removed"
6396
+ );
6397
+ if (!removed || removed.bounced_at !== void 0) return;
6398
+ if (room.seats.some(
6399
+ (candidate) => candidate.identity === item.sender_id && candidate.state === "active"
6400
+ )) return;
6401
+ const seats = room.seats.map((candidate) => candidate.participant_id === removed.participant_id ? { ...candidate, bounced_at: this.now() } : candidate);
6402
+ await this.store.save(RoomSchema.parse({ ...room, seats }));
6403
+ try {
6404
+ const unsigned = { version: 1, kind: "room_not_member", room_id: roomId };
6405
+ await sendSignedBody(packet, item.sender_id, unsigned);
6406
+ } catch {
6407
+ }
6408
+ }
5930
6409
  async completeSnapshotIntents(roomId) {
5931
6410
  const records = await this.store.read(roomId);
5932
6411
  for (const message of records.filter(
5933
6412
  (record) => record.kind === "message"
5934
6413
  )) await this.completeMessageIntents(roomId, message);
6414
+ for (const file of records.filter(
6415
+ (record) => record.kind === "file"
6416
+ )) await this.completeFileIntents(roomId, file);
6417
+ }
6418
+ async completeFileIntents(roomId, file) {
6419
+ const records = await this.store.read(roomId);
6420
+ const intended = new Set(records.filter((record) => record.kind === "relay_intent" && record.file_id === file.file_id).map((intent) => intent.recipient_identity));
6421
+ for (const recipientIdentity of file.recipient_identities) {
6422
+ if (intended.has(recipientIdentity)) continue;
6423
+ await this.store.append(roomId, {
6424
+ version: 1,
6425
+ kind: "relay_intent",
6426
+ room_id: roomId,
6427
+ at: this.now(),
6428
+ file_id: file.file_id,
6429
+ recipient_identity: recipientIdentity
6430
+ });
6431
+ intended.add(recipientIdentity);
6432
+ }
5935
6433
  }
5936
6434
  async completeMessageIntents(roomId, message) {
5937
6435
  const records = await this.store.read(roomId);
@@ -5951,27 +6449,110 @@ var init_intake = __esm({
5951
6449
  }
5952
6450
  async relayPendingUnlocked(roomId, packet) {
5953
6451
  const records = await this.store.read(roomId);
6452
+ const room = await this.store.load(roomId);
6453
+ const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
6454
+ const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
5954
6455
  const messages = new Map(records.filter((record) => record.kind === "message").map((message) => [message.message_id, message]));
6456
+ const files = new Map(records.filter((record) => record.kind === "file").map((file) => [file.file_id, file]));
5955
6457
  const completed = new Set(records.filter((record) => record.kind === "relay_result").map((result) => result.kind === "relay_result" ? result.intent_record_id : ""));
5956
6458
  for (const intent of records.filter(
5957
6459
  (record) => record.kind === "relay_intent"
5958
6460
  )) {
5959
6461
  if (completed.has(intent.record_id)) continue;
5960
- const message = messages.get(intent.message_id);
5961
- if (!message) continue;
5962
- if (!message.recipient_identities.includes(intent.recipient_identity)) continue;
6462
+ const message = intent.message_id === void 0 ? void 0 : messages.get(intent.message_id);
6463
+ const file = intent.file_id === void 0 ? void 0 : files.get(intent.file_id);
6464
+ if (message === void 0 === (file === void 0)) continue;
6465
+ const recipients = message?.recipient_identities ?? file.recipient_identities;
6466
+ if (!recipients.includes(intent.recipient_identity)) continue;
6467
+ if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
6468
+ const skipped = await this.store.append(roomId, {
6469
+ version: 1,
6470
+ kind: "relay_result",
6471
+ room_id: roomId,
6472
+ at: this.now(),
6473
+ intent_record_id: intent.record_id,
6474
+ ...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
6475
+ ...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
6476
+ recipient_identity: intent.recipient_identity,
6477
+ status: "skipped_removed"
6478
+ });
6479
+ if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6480
+ completed.add(intent.record_id);
6481
+ continue;
6482
+ }
6483
+ if (file !== void 0) {
6484
+ const author = file.author_alias === void 0 ? file.author : {
6485
+ identity: file.author_alias.participant_id,
6486
+ display_name: file.author_alias.alias,
6487
+ role: file.author.role
6488
+ };
6489
+ const metadata = await sendSignedBody(packet, intent.recipient_identity, {
6490
+ version: 1,
6491
+ kind: "room_file",
6492
+ room_id: roomId,
6493
+ file_id: file.file_id,
6494
+ author,
6495
+ filename: file.filename,
6496
+ mime: file.mime,
6497
+ size: file.size,
6498
+ sha256: file.sha256,
6499
+ at: file.at
6500
+ });
6501
+ if (metadata.status === "send_failed") {
6502
+ const failed = await this.store.append(roomId, {
6503
+ version: 1,
6504
+ kind: "relay_result",
6505
+ room_id: roomId,
6506
+ at: this.now(),
6507
+ intent_record_id: intent.record_id,
6508
+ file_id: file.file_id,
6509
+ recipient_identity: intent.recipient_identity,
6510
+ status: "send_failed"
6511
+ });
6512
+ if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6513
+ completed.add(intent.record_id);
6514
+ continue;
6515
+ }
6516
+ const outcome2 = await packet.sendFile(
6517
+ intent.recipient_identity,
6518
+ file.filename,
6519
+ file.mime,
6520
+ Buffer.from(file.data_base64, "base64")
6521
+ );
6522
+ const appended2 = await this.store.append(roomId, {
6523
+ version: 1,
6524
+ kind: "relay_result",
6525
+ room_id: roomId,
6526
+ at: this.now(),
6527
+ intent_record_id: intent.record_id,
6528
+ file_id: file.file_id,
6529
+ recipient_identity: intent.recipient_identity,
6530
+ status: outcome2.status,
6531
+ ...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
6532
+ ...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
6533
+ });
6534
+ if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6535
+ completed.add(intent.record_id);
6536
+ continue;
6537
+ }
5963
6538
  const unsigned = {
5964
6539
  version: 1,
5965
- kind: message.category === "briefing" ? "room_briefing" : "room_msg",
6540
+ kind: wireKind(message.category),
5966
6541
  room_id: roomId,
5967
6542
  message_id: message.message_id,
5968
- author: message.author,
6543
+ // INV-R3: an anonymous author leaves the archive only in alias form.
6544
+ author: message.author_alias === void 0 ? message.author : {
6545
+ identity: message.author_alias.participant_id,
6546
+ display_name: message.author_alias.alias,
6547
+ role: message.author.role
6548
+ },
5969
6549
  text: message.text,
5970
- at: message.at
6550
+ at: message.at,
6551
+ ...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
6552
+ ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
6553
+ ...message.membership === void 0 ? {} : { membership: message.membership }
5971
6554
  };
5972
- const signature = await packet.sign(canonicalJson(unsigned));
5973
- const body = canonicalJson({ ...unsigned, signature });
5974
- const outcome = await packet.send(intent.recipient_identity, body);
6555
+ const outcome = await sendSignedBody(packet, intent.recipient_identity, unsigned);
5975
6556
  const appended = await this.store.append(roomId, {
5976
6557
  version: 1,
5977
6558
  kind: "relay_result",
@@ -5996,6 +6577,16 @@ var init_intake = __esm({
5996
6577
  }
5997
6578
  return message;
5998
6579
  }
6580
+ findSourceFile(records, item) {
6581
+ const file = records.find((record) => record.kind === "file" && record.source_file_id === item.file_id);
6582
+ if (!file) return void 0;
6583
+ const observedWireId = item.wire_id === "" ? void 0 : item.wire_id;
6584
+ const bytes = Buffer.from(item.data);
6585
+ if (file.source_wire_id !== observedWireId || file.author.identity !== item.sender_id || file.filename !== item.filename || file.mime !== item.mime || file.at !== item.date || file.size !== bytes.length || file.data_base64 !== bytes.toString("base64")) {
6586
+ throw new Error(`file inbox source ${item.file_id} does not match its durable room file`);
6587
+ }
6588
+ return file;
6589
+ }
5999
6590
  lock(roomId, work) {
6000
6591
  return this.store.mutex(roomId).runExclusive(work);
6001
6592
  }
@@ -6012,27 +6603,28 @@ var init_intake = __esm({
6012
6603
  });
6013
6604
 
6014
6605
  // src/service.ts
6015
- import { randomBytes as randomBytes3 } from "node:crypto";
6016
- function generateUlid2() {
6017
- let time = Date.now();
6018
- const output = new Array(26);
6019
- for (let index = 9; index >= 0; index -= 1) {
6020
- output[index] = CROCKFORD2[time % 32];
6021
- time = Math.floor(time / 32);
6022
- }
6023
- const entropy = randomBytes3(10);
6024
- let bits = 0;
6025
- let value = 0;
6026
- let byteIndex = 0;
6027
- for (let index = 10; index < 26; index += 1) {
6028
- while (bits < 5) {
6029
- value = value << 8 | entropy[byteIndex++];
6030
- bits += 8;
6606
+ function byteBoundedHistoryPage(records) {
6607
+ const page = [];
6608
+ let bytes = 2;
6609
+ for (const record of records) {
6610
+ const encoded = JSON.stringify(record);
6611
+ const nextBytes = Buffer.byteLength(encoded, "utf8") + (page.length === 0 ? 0 : 1);
6612
+ if (bytes + nextBytes > MAX_HISTORY_PAGE_BYTES) {
6613
+ if (page.length === 0) {
6614
+ throw new RangeError(`one history record exceeds the ${MAX_HISTORY_PAGE_BYTES}-byte page contract`);
6615
+ }
6616
+ break;
6031
6617
  }
6032
- bits -= 5;
6033
- output[index] = CROCKFORD2[value >>> bits & 31];
6618
+ page.push(record);
6619
+ bytes += nextBytes;
6034
6620
  }
6035
- return output.join("");
6621
+ return page;
6622
+ }
6623
+ function activeSeats(room) {
6624
+ return room.seats.filter((seat) => seat.state === "active");
6625
+ }
6626
+ function mintAlias(seated, role) {
6627
+ return `${role} #${seated.filter((seat) => seat.role === role).length + 1}`;
6036
6628
  }
6037
6629
  function uniqueIdentities(identities) {
6038
6630
  return [...new Set(identities)];
@@ -6040,18 +6632,18 @@ function uniqueIdentities(identities) {
6040
6632
  function currentContactIdentities(packet) {
6041
6633
  return new Set(packet.listContacts().map((contact) => contact.container_id));
6042
6634
  }
6043
- var CROCKFORD2, ROOM_ROLE, CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RoomServiceError, RoomService;
6635
+ var ROOM_ROLE, CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
6044
6636
  var init_service = __esm({
6045
6637
  "src/service.ts"() {
6046
6638
  "use strict";
6047
6639
  init_zod();
6048
6640
  init_contracts();
6049
6641
  init_intake();
6050
- CROCKFORD2 = "0123456789abcdefghjkmnpqrstvwxyz";
6642
+ init_ulid();
6051
6643
  ROOM_ROLE = "room";
6052
6644
  CreateInviteInputSchema = external_exports.object({
6053
6645
  mode: InviteModeSchema,
6054
- role: RoleSchema,
6646
+ role: RoleSchema.optional(),
6055
6647
  min_accepts: external_exports.number().int().positive().safe()
6056
6648
  }).strict().superRefine((input, context) => {
6057
6649
  if (input.mode === "one_time" && input.min_accepts !== 1) {
@@ -6064,11 +6656,22 @@ var init_service = __esm({
6064
6656
  });
6065
6657
  HistoryOptionsSchema = external_exports.object({
6066
6658
  after: external_exports.number().int().nonnegative().safe().optional(),
6067
- limit: external_exports.number().int().positive().safe().optional()
6659
+ limit: external_exports.number().int().positive().safe().optional(),
6660
+ view: external_exports.enum(["operator", "participant"]).optional()
6068
6661
  }).strict();
6069
6662
  DeleteRoomInputSchema = external_exports.object({
6070
6663
  confirm: external_exports.literal(true)
6071
6664
  }).strict();
6665
+ RemoveParticipantInputSchema = external_exports.object({
6666
+ participant: external_exports.string().min(1),
6667
+ notify: external_exports.boolean().optional()
6668
+ }).strict();
6669
+ ReplaceParticipantInputSchema = external_exports.object({
6670
+ participant: external_exports.string().min(1),
6671
+ notify: external_exports.boolean().optional(),
6672
+ mode: InviteModeSchema.optional(),
6673
+ min_accepts: external_exports.number().int().positive().safe().optional()
6674
+ }).strict();
6072
6675
  RoomServiceError = class extends Error {
6073
6676
  constructor(message, options) {
6074
6677
  super(message, options);
@@ -6087,8 +6690,8 @@ var init_service = __esm({
6087
6690
  this.store = store;
6088
6691
  this.packets = packets;
6089
6692
  this.nowValue = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
6090
- this.nextRoomId = options.roomId ?? generateUlid2;
6091
- this.nextMessageId = options.messageId ?? generateUlid2;
6693
+ this.nextRoomId = options.roomId ?? generateUlid;
6694
+ this.nextMessageId = options.messageId ?? generateUlid;
6092
6695
  this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
6093
6696
  });
6094
6697
  this.intake = new IntakePump(store, packets, {
@@ -6102,14 +6705,18 @@ var init_service = __esm({
6102
6705
  const identityName = `cowork-room-${roomId}`;
6103
6706
  return this.lock(roomId, async () => {
6104
6707
  const provisional = RoomSchema.parse({
6105
- version: 1,
6708
+ version: 2,
6106
6709
  room_id: roomId,
6107
6710
  identity_name: identityName,
6108
6711
  // PacketRegistry needs the durable room directory to exist first. A
6109
6712
  // valid, explicitly provisional value lets startup resume this exact
6110
6713
  // two-resource boundary without claiming a packet CID yet.
6111
6714
  identity_cid: "",
6112
- mission: { goal: settings.goal, briefing: settings.briefing },
6715
+ mission: { goal: settings.goal, briefing: settings.briefing, briefing_version: 1 },
6716
+ role_briefings: {},
6717
+ anonymous: settings.anonymous ?? false,
6718
+ quiet_membership: settings.quiet_membership ?? false,
6719
+ membership_epoch: 0,
6113
6720
  state: "provisioning",
6114
6721
  status: "packet_pending",
6115
6722
  invites: [],
@@ -6200,18 +6807,73 @@ var init_service = __esm({
6200
6807
  async updateRoom(roomId, input) {
6201
6808
  const id = LowerCrockfordUlidSchema.parse(roomId);
6202
6809
  const settings = UpdateRoomInputSchema.parse(input);
6203
- return this.lock(id, async () => {
6810
+ const { room: updated, redelivered } = await this.lock(id, async () => {
6204
6811
  const room = await this.store.load(id);
6205
6812
  this.assertMutable(room, "update");
6813
+ const briefingChanged = settings.briefing !== void 0 && settings.briefing !== room.mission.briefing;
6206
6814
  const mission = {
6207
6815
  goal: settings.goal ?? room.mission.goal,
6208
- briefing: settings.briefing ?? room.mission.briefing
6816
+ briefing: settings.briefing ?? room.mission.briefing,
6817
+ briefing_version: briefingChanged ? room.mission.briefing_version + 1 : room.mission.briefing_version
6209
6818
  };
6210
- return this.store.save(RoomSchema.parse({
6819
+ const next = await this.store.save(RoomSchema.parse({
6211
6820
  ...room,
6212
6821
  mission,
6822
+ ...settings.quiet_membership === void 0 ? {} : { quiet_membership: settings.quiet_membership },
6213
6823
  ...settings.status === void 0 ? {} : { status: settings.status }
6214
6824
  }));
6825
+ if (briefingChanged && next.state === "active") {
6826
+ await this.redeliverCommonBriefing(next);
6827
+ return { room: next, redelivered: true };
6828
+ }
6829
+ return { room: next, redelivered: false };
6830
+ });
6831
+ if (redelivered) await this.intake.resumePending(id);
6832
+ return updated;
6833
+ }
6834
+ /** Author or edit one role's briefing; an edit bumps its version and re-delivers to seats of that role only (spec §3.3). */
6835
+ async setRoleBriefing(roomId, input) {
6836
+ const id = LowerCrockfordUlidSchema.parse(roomId);
6837
+ const request = RoleBriefingSetInputSchema.parse(input);
6838
+ const { room: updated, redelivered } = await this.lock(id, async () => {
6839
+ const room = await this.store.load(id);
6840
+ this.assertMutable(room, "set a role briefing for");
6841
+ const existing = room.role_briefings[request.role];
6842
+ if (existing && existing.text === request.text) return { room, redelivered: false };
6843
+ const briefing = {
6844
+ text: request.text,
6845
+ version: existing === void 0 ? 1 : existing.version + 1,
6846
+ updated_at: this.now()
6847
+ };
6848
+ const next = await this.store.save(RoomSchema.parse({
6849
+ ...room,
6850
+ role_briefings: { ...room.role_briefings, [request.role]: briefing }
6851
+ }));
6852
+ if (next.state !== "active") return { room: next, redelivered: false };
6853
+ const holders = activeSeats(next).filter((seat) => seat.role === request.role);
6854
+ if (holders.length === 0) return { room: next, redelivered: false };
6855
+ await this.ensureBriefingKind(next, holders, {
6856
+ category: "role_briefing",
6857
+ briefing_role: request.role,
6858
+ text: briefing.text,
6859
+ briefing_version: briefing.version
6860
+ });
6861
+ return { room: next, redelivered: true };
6862
+ });
6863
+ if (redelivered) await this.intake.resumePending(id);
6864
+ return updated;
6865
+ }
6866
+ async deleteRoleBriefing(roomId, input) {
6867
+ const id = LowerCrockfordUlidSchema.parse(roomId);
6868
+ const request = RoleBriefingDeleteInputSchema.parse(input);
6869
+ return this.lock(id, async () => {
6870
+ const room = await this.store.load(id);
6871
+ this.assertMutable(room, "delete a role briefing for");
6872
+ if (room.role_briefings[request.role] === void 0) {
6873
+ throw new RoomServiceError(`no role briefing exists for "${request.role}" in room "${id}"`);
6874
+ }
6875
+ const { [request.role]: _removed, ...rest } = room.role_briefings;
6876
+ return this.store.save(RoomSchema.parse({ ...room, role_briefings: rest }));
6215
6877
  });
6216
6878
  }
6217
6879
  async createInvite(roomId, input) {
@@ -6220,33 +6882,215 @@ var init_service = __esm({
6220
6882
  return this.lock(id, async () => {
6221
6883
  const room = await this.store.load(id);
6222
6884
  this.assertMutable(room, "create an invite for");
6223
- const packet = this.packet(id);
6224
- const minted = await packet.mintInvite(request.mode);
6225
- const invite = RoomInviteSchema.parse({
6226
- invite_id: minted.invite_id,
6885
+ return this.mintInviteUnlocked(room, {
6227
6886
  mode: request.mode,
6228
- role: request.role,
6229
- min_accepts: request.min_accepts,
6230
- accepted_cids: [],
6231
- state: "live",
6232
- created_at: this.now()
6887
+ role: request.role ?? DEFAULT_ROLE,
6888
+ min_accepts: request.min_accepts
6233
6889
  });
6890
+ });
6891
+ }
6892
+ async mintInviteUnlocked(room, request) {
6893
+ const packet = this.packet(room.room_id);
6894
+ const minted = await packet.mintInvite(request.mode);
6895
+ const invite = RoomInviteSchema.parse({
6896
+ invite_id: minted.invite_id,
6897
+ mode: request.mode,
6898
+ role: request.role,
6899
+ min_accepts: request.min_accepts,
6900
+ accepted_cids: [],
6901
+ state: "live",
6902
+ created_at: this.now(),
6903
+ ...request.replaces_seat === void 0 ? {} : { replaces_seat: request.replaces_seat }
6904
+ });
6905
+ try {
6906
+ await this.store.save(RoomSchema.parse({ ...room, invites: [...room.invites, invite] }));
6907
+ } catch (error) {
6234
6908
  try {
6235
- await this.store.save(RoomSchema.parse({ ...room, invites: [...room.invites, invite] }));
6236
- } catch (error) {
6237
- try {
6238
- await packet.revokeInvite(minted.invite_id);
6239
- } catch {
6240
- }
6241
- throw error;
6909
+ await packet.revokeInvite(minted.invite_id);
6910
+ } catch {
6242
6911
  }
6243
- return {
6244
- room_id: id,
6245
- invite,
6246
- blob: minted.blob,
6247
- reusable: minted.reusable
6912
+ throw error;
6913
+ }
6914
+ return {
6915
+ room_id: room.room_id,
6916
+ invite,
6917
+ blob: minted.blob,
6918
+ reusable: minted.reusable
6919
+ };
6920
+ }
6921
+ /**
6922
+ * Operator-only removal (spec §5.2): archive-before-act membership intent,
6923
+ * seat state flip + epoch bump, core 0.13 bilateral sever with an honest
6924
+ * receipt, and an alias-form announcement unless the room or call is quiet.
6925
+ */
6926
+ async removeParticipant(roomId, input) {
6927
+ const id = LowerCrockfordUlidSchema.parse(roomId);
6928
+ const request = RemoveParticipantInputSchema.parse(input);
6929
+ const receipt = await this.lock(id, async () => {
6930
+ const room = await this.store.load(id);
6931
+ this.assertMutable(room, "remove a participant from");
6932
+ const seat = this.findActiveSeat(room, request.participant);
6933
+ const notify = (request.notify ?? true) && !room.quiet_membership;
6934
+ return this.beginRemovalUnlocked(room, seat, notify);
6935
+ });
6936
+ await this.intake.resumePending(id);
6937
+ return receipt;
6938
+ }
6939
+ /**
6940
+ * Removal plus a same-role invite stamped with the seat lineage (spec §5.3).
6941
+ * Owner override OC-2/OC-6: in an anonymous room the flow is unconditionally
6942
+ * silent — the successor inherits the alias and other members see nothing.
6943
+ */
6944
+ async replaceParticipant(roomId, input) {
6945
+ const id = LowerCrockfordUlidSchema.parse(roomId);
6946
+ const request = ReplaceParticipantInputSchema.parse(input);
6947
+ const receipt = await this.lock(id, async () => {
6948
+ const room = await this.store.load(id);
6949
+ this.assertMutable(room, "replace a participant in");
6950
+ const seat = this.findActiveSeat(room, request.participant);
6951
+ const notify = room.anonymous ? false : (request.notify ?? true) && !room.quiet_membership;
6952
+ const removal = await this.beginRemovalUnlocked(room, seat, notify);
6953
+ const current = await this.store.load(id);
6954
+ const invite = await this.mintInviteUnlocked(current, {
6955
+ mode: request.mode ?? "one_time",
6956
+ role: seat.role,
6957
+ min_accepts: request.min_accepts ?? 1,
6958
+ replaces_seat: seat.participant_id
6959
+ });
6960
+ return { ...invite, removal };
6961
+ });
6962
+ await this.intake.resumePending(id);
6963
+ return receipt;
6964
+ }
6965
+ findActiveSeat(room, participant) {
6966
+ const seat = room.seats.find((candidate) => candidate.state === "active" && (candidate.identity === participant || candidate.participant_id === participant));
6967
+ if (!seat) {
6968
+ throw new RoomServiceError(`"${participant}" is not an active participant of room "${room.room_id}"`);
6969
+ }
6970
+ return seat;
6971
+ }
6972
+ async beginRemovalUnlocked(room, seat, notify) {
6973
+ const intent = await this.store.append(room.room_id, {
6974
+ version: 1,
6975
+ kind: "membership_intent",
6976
+ room_id: room.room_id,
6977
+ at: this.now(),
6978
+ action: "remove",
6979
+ participant_id: seat.participant_id,
6980
+ recipient_identity: seat.identity,
6981
+ role: seat.role,
6982
+ // The participant-visible label: the alias in anonymous rooms, the
6983
+ // contact display name otherwise (INV-R3 holds either way).
6984
+ alias: seat.alias ?? seat.display_name,
6985
+ epoch: room.membership_epoch + 1,
6986
+ notify
6987
+ });
6988
+ if (intent.kind !== "membership_intent") {
6989
+ throw new RoomServiceError("storage returned the wrong membership intent kind");
6990
+ }
6991
+ const { receipt } = await this.completeRemovalUnlocked(room, intent);
6992
+ return receipt;
6993
+ }
6994
+ /**
6995
+ * Idempotent completion of a durable removal intent: each step re-checks the
6996
+ * archive/state it would produce, so a crash anywhere re-drives cleanly
6997
+ * (INV-R5; the 0.13 sever is replay-safe by design).
6998
+ */
6999
+ async completeRemovalUnlocked(room, intent) {
7000
+ let current = room;
7001
+ const index = current.seats.findIndex(
7002
+ (candidate) => candidate.participant_id === intent.participant_id
7003
+ );
7004
+ if (index < 0) {
7005
+ throw new RoomServiceError(
7006
+ `membership intent ${intent.record_id} references an unknown seat in room "${current.room_id}"`
7007
+ );
7008
+ }
7009
+ if (current.seats[index].state !== "removed") {
7010
+ const seats = [...current.seats];
7011
+ seats[index] = {
7012
+ ...seats[index],
7013
+ state: "removed",
7014
+ removed_at: intent.at,
7015
+ removed_epoch: intent.epoch
7016
+ };
7017
+ current = await this.store.save(RoomSchema.parse({
7018
+ ...current,
7019
+ seats,
7020
+ membership_epoch: Math.max(current.membership_epoch, intent.epoch)
7021
+ }));
7022
+ }
7023
+ const records = await this.store.read(current.room_id);
7024
+ const existing = records.find((record) => record.kind === "membership_result" && record.intent_record_id === intent.record_id);
7025
+ let outcome;
7026
+ if (existing !== void 0 && existing.kind === "membership_result") {
7027
+ outcome = {
7028
+ status: existing.status,
7029
+ notified: existing.notified,
7030
+ key_material_retained: true
6248
7031
  };
7032
+ } else {
7033
+ outcome = await this.packet(current.room_id).removeContact(intent.recipient_identity);
7034
+ const result = await this.store.append(current.room_id, {
7035
+ version: 1,
7036
+ kind: "membership_result",
7037
+ room_id: current.room_id,
7038
+ at: this.now(),
7039
+ intent_record_id: intent.record_id,
7040
+ participant_id: intent.participant_id,
7041
+ status: outcome.status,
7042
+ notified: outcome.notified,
7043
+ key_material_retained: true
7044
+ });
7045
+ if (result.kind !== "membership_result") {
7046
+ throw new RoomServiceError("storage returned the wrong membership result kind");
7047
+ }
7048
+ }
7049
+ if (intent.notify) await this.ensureMembershipNotice(current, intent);
7050
+ return {
7051
+ room: current,
7052
+ receipt: {
7053
+ room_id: current.room_id,
7054
+ participant_id: intent.participant_id,
7055
+ epoch: intent.epoch,
7056
+ status: outcome.status,
7057
+ notified: outcome.notified,
7058
+ key_material_retained: true
7059
+ }
7060
+ };
7061
+ }
7062
+ async ensureMembershipNotice(room, intent) {
7063
+ const records = await this.store.read(room.room_id);
7064
+ const already = records.some((record) => record.kind === "message" && record.category === "membership" && record.membership?.action === "remove" && record.membership.epoch === intent.epoch);
7065
+ if (already) return;
7066
+ const remaining = activeSeats(room);
7067
+ if (remaining.length === 0) return;
7068
+ const label = intent.alias ?? intent.role;
7069
+ const appended = await this.store.append(room.room_id, {
7070
+ version: 1,
7071
+ kind: "message",
7072
+ room_id: room.room_id,
7073
+ at: this.now(),
7074
+ message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
7075
+ author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
7076
+ category: "membership",
7077
+ text: `${label} left the room \xB7 epoch ${intent.epoch}`,
7078
+ membership: { action: "remove", alias: label, role: intent.role, epoch: intent.epoch },
7079
+ recipient_identities: uniqueIdentities(remaining.map((seat) => seat.identity))
6249
7080
  });
7081
+ if (appended.kind !== "message") {
7082
+ throw new RoomServiceError("storage returned the wrong membership notice kind");
7083
+ }
7084
+ for (const recipientIdentity of appended.recipient_identities) {
7085
+ await this.store.append(room.room_id, {
7086
+ version: 1,
7087
+ kind: "relay_intent",
7088
+ room_id: room.room_id,
7089
+ at: this.now(),
7090
+ message_id: appended.message_id,
7091
+ recipient_identity: recipientIdentity
7092
+ });
7093
+ }
6250
7094
  }
6251
7095
  async revokeInvite(roomId, inviteId) {
6252
7096
  const id = LowerCrockfordUlidSchema.parse(roomId);
@@ -6426,8 +7270,27 @@ var init_service = __esm({
6426
7270
  }
6427
7271
  async history(roomId, options = {}) {
6428
7272
  const id = LowerCrockfordUlidSchema.parse(roomId);
6429
- const page = HistoryOptionsSchema.parse(options);
6430
- return this.store.read(id, page);
7273
+ const { view, ...page } = HistoryOptionsSchema.parse(options);
7274
+ const records = await this.store.read(id, view === "participant" ? { after: page.after } : page);
7275
+ if (view !== "participant") return byteBoundedHistoryPage(records);
7276
+ const projected = records.filter((record) => record.kind === "message").map((record) => {
7277
+ const {
7278
+ author_alias,
7279
+ recipient_identities: _recipients,
7280
+ source_msg_id: _sourceMsg,
7281
+ source_wire_id: _sourceWire,
7282
+ ...rest
7283
+ } = record;
7284
+ return {
7285
+ ...rest,
7286
+ author: author_alias === void 0 ? record.author : {
7287
+ identity: author_alias.participant_id,
7288
+ display_name: author_alias.alias,
7289
+ role: record.author.role
7290
+ }
7291
+ };
7292
+ });
7293
+ return byteBoundedHistoryPage(projected.slice(0, page.limit ?? Number.MAX_SAFE_INTEGER));
6431
7294
  }
6432
7295
  /**
6433
7296
  * Forward-only close. Every external contact mutation is preceded by a
@@ -6492,7 +7355,7 @@ var init_service = __esm({
6492
7355
  },
6493
7356
  category: "chat",
6494
7357
  text: request.text,
6495
- recipient_identities: uniqueIdentities(room.seats.map((seat) => seat.identity))
7358
+ recipient_identities: uniqueIdentities(activeSeats(room).map((seat) => seat.identity))
6496
7359
  });
6497
7360
  if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong room message kind");
6498
7361
  for (const recipientIdentity of appended.recipient_identities) {
@@ -6517,7 +7380,15 @@ var init_service = __esm({
6517
7380
  }
6518
7381
  const origins = packet.listContactOrigins();
6519
7382
  const inviteById = new Map(room.invites.map((invite) => [invite.invite_id, invite]));
6520
- const existingCids = new Set(room.seats.map((seat) => seat.identity));
7383
+ const existingCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
7384
+ const lastRemovedAt = /* @__PURE__ */ new Map();
7385
+ for (const seat of room.seats) {
7386
+ if (seat.state !== "removed" || seat.removed_at === void 0) continue;
7387
+ const previous = lastRemovedAt.get(seat.identity);
7388
+ if (previous === void 0 || seat.removed_at > previous) {
7389
+ lastRemovedAt.set(seat.identity, seat.removed_at);
7390
+ }
7391
+ }
6521
7392
  const newSeats = [];
6522
7393
  for (const [cid, displayName] of contactsByCid) {
6523
7394
  if (existingCids.has(cid)) continue;
@@ -6525,12 +7396,23 @@ var init_service = __esm({
6525
7396
  if (!origin || origin.via !== "invite_one_time" && origin.via !== "invite_public") continue;
6526
7397
  const invite = inviteById.get(origin.invite_id);
6527
7398
  if (!invite || invite.state === "receipt_pending" || invite.recovery_of !== void 0 && invite.recovery_confirmed !== true) continue;
7399
+ const removedAt = lastRemovedAt.get(cid);
7400
+ if (removedAt !== void 0 && invite.created_at <= removedAt) continue;
7401
+ const seated = [...room.seats, ...newSeats];
7402
+ const predecessor = invite.replaces_seat === void 0 ? void 0 : seated.find((seat) => seat.participant_id === invite.replaces_seat && seat.state === "removed");
7403
+ if (invite.replaces_seat !== void 0 && !predecessor) continue;
6528
7404
  newSeats.push({
6529
7405
  identity: cid,
6530
7406
  display_name: displayName,
6531
7407
  role: invite.role,
6532
7408
  invite_id: invite.invite_id,
6533
- accepted_at: origin.at
7409
+ accepted_at: origin.at,
7410
+ participant_id: LowerCrockfordUlidSchema.parse(generateUlid()),
7411
+ state: "active",
7412
+ ...predecessor === void 0 ? {} : { replaces_seat: predecessor.participant_id },
7413
+ // OC-6 (owner override): a replacement into a role inherits the
7414
+ // predecessor's alias — the alias binds to the seat/role lineage.
7415
+ ...room.anonymous ? { alias: predecessor?.alias ?? mintAlias(seated, invite.role) } : {}
6534
7416
  });
6535
7417
  existingCids.add(cid);
6536
7418
  }
@@ -6556,13 +7438,26 @@ var init_service = __esm({
6556
7438
  }
6557
7439
  return { ...invite, accepted_cids, state };
6558
7440
  });
6559
- let next = RoomSchema.parse({ ...room, seats, invites });
7441
+ let next = RoomSchema.parse({
7442
+ ...room,
7443
+ seats,
7444
+ invites,
7445
+ membership_epoch: room.membership_epoch + newSeats.length
7446
+ });
7447
+ const journal = await this.store.read(next.room_id);
7448
+ const completedIntents = new Set(journal.filter((record) => record.kind === "membership_result").map((record) => record.kind === "membership_result" ? record.intent_record_id : ""));
7449
+ for (const intent of journal.filter(
7450
+ (record) => record.kind === "membership_intent"
7451
+ )) {
7452
+ if (completedIntents.has(intent.record_id)) continue;
7453
+ ({ room: next } = await this.completeRemovalUnlocked(next, intent));
7454
+ }
6560
7455
  const requirementsMet = invites.filter((invite) => invite.state !== "revoked").every((invite) => invite.accepted_cids.length >= invite.min_accepts);
6561
7456
  if (next.state === "provisioning" && seats.length > 0 && requirementsMet) {
6562
- const activationAt = await this.ensureActivationBriefing(next, seats);
7457
+ const activationAt = await this.ensureActivationBriefing(next, activeSeats(next));
6563
7458
  next = RoomSchema.parse({ ...next, state: "active", activated_at: activationAt });
6564
- } else if (next.state === "active") {
6565
- for (const seat of newSeats) await this.ensureLateBriefing(next, seat);
7459
+ } else if (next.state === "active" && newSeats.length > 0) {
7460
+ await this.ensureActivationBriefing(next, newSeats);
6566
7461
  }
6567
7462
  return this.store.save(next);
6568
7463
  }
@@ -6659,62 +7554,98 @@ var init_service = __esm({
6659
7554
  deleteReceipt(roomId) {
6660
7555
  return { version: 1, room_id: roomId, deleted: true, scope: "this_host" };
6661
7556
  }
7557
+ /**
7558
+ * Deliver the common briefing followed by the seat's role briefing (spec
7559
+ * §3.3): exactly once per (seat, briefing kind, version) via the message +
7560
+ * relay-intent ledger. Returns the timestamp of the common briefing message
7561
+ * that admitted the earliest of the given recipients (activation time).
7562
+ */
6662
7563
  async ensureActivationBriefing(room, recipients) {
7564
+ const at = await this.ensureBriefingKind(room, recipients, {
7565
+ category: "briefing",
7566
+ text: room.mission.briefing,
7567
+ briefing_version: room.mission.briefing_version
7568
+ });
7569
+ await this.ensureRoleBriefings(room, recipients);
7570
+ return at;
7571
+ }
7572
+ async ensureRoleBriefings(room, recipients) {
7573
+ for (const seat of recipients) {
7574
+ const briefing = room.role_briefings[seat.role];
7575
+ if (!briefing) continue;
7576
+ await this.ensureBriefingKind(room, [seat], {
7577
+ category: "role_briefing",
7578
+ briefing_role: seat.role,
7579
+ text: briefing.text,
7580
+ briefing_version: briefing.version
7581
+ });
7582
+ }
7583
+ }
7584
+ /** Re-deliver the (just bumped) common briefing to every active seat. */
7585
+ async redeliverCommonBriefing(room) {
7586
+ await this.ensureBriefingKind(room, activeSeats(room), {
7587
+ category: "briefing",
7588
+ text: room.mission.briefing,
7589
+ briefing_version: room.mission.briefing_version
7590
+ });
7591
+ }
7592
+ async ensureBriefingKind(room, recipients, briefing) {
6663
7593
  const records = await this.store.read(room.room_id);
6664
- let message = records.find(
6665
- (record) => record.kind === "message" && record.category === "briefing"
6666
- );
6667
- if (!message) message = await this.appendBriefing(room, recipients);
6668
- const intents = new Set(records.filter((record) => record.kind === "relay_intent" && record.message_id === message.message_id).map((record) => record.kind === "relay_intent" ? record.recipient_identity : ""));
6669
- for (const recipientIdentity of message.recipient_identities) {
6670
- if (!intents.has(recipientIdentity)) {
7594
+ const matching = records.filter((record) => record.kind === "message" && record.category === briefing.category && record.briefing_role === briefing.briefing_role && (record.briefing_version ?? 1) === briefing.briefing_version);
7595
+ const intentsByMessage = /* @__PURE__ */ new Map();
7596
+ for (const record of records) {
7597
+ if (record.kind !== "relay_intent" || record.message_id === void 0) continue;
7598
+ const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
7599
+ intents.add(record.recipient_identity);
7600
+ intentsByMessage.set(record.message_id, intents);
7601
+ }
7602
+ const covered = /* @__PURE__ */ new Set();
7603
+ for (const message of matching) {
7604
+ const intents = intentsByMessage.get(message.message_id) ?? /* @__PURE__ */ new Set();
7605
+ for (const recipientIdentity of message.recipient_identities) {
7606
+ covered.add(recipientIdentity);
7607
+ if (!intents.has(recipientIdentity)) {
7608
+ await this.store.append(room.room_id, {
7609
+ version: 1,
7610
+ kind: "relay_intent",
7611
+ room_id: room.room_id,
7612
+ at: this.now(),
7613
+ message_id: message.message_id,
7614
+ recipient_identity: recipientIdentity
7615
+ });
7616
+ }
7617
+ }
7618
+ }
7619
+ const missing = recipients.filter((seat) => !covered.has(seat.identity));
7620
+ let appendedAt;
7621
+ if (missing.length > 0) {
7622
+ const appended = await this.store.append(room.room_id, {
7623
+ version: 1,
7624
+ kind: "message",
7625
+ room_id: room.room_id,
7626
+ at: this.now(),
7627
+ message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
7628
+ author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
7629
+ category: briefing.category,
7630
+ ...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
7631
+ briefing_version: briefing.briefing_version,
7632
+ text: briefing.text,
7633
+ recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
7634
+ });
7635
+ if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
7636
+ appendedAt = appended.at;
7637
+ for (const recipientIdentity of appended.recipient_identities) {
6671
7638
  await this.store.append(room.room_id, {
6672
7639
  version: 1,
6673
7640
  kind: "relay_intent",
6674
7641
  room_id: room.room_id,
6675
7642
  at: this.now(),
6676
- message_id: message.message_id,
7643
+ message_id: appended.message_id,
6677
7644
  recipient_identity: recipientIdentity
6678
7645
  });
6679
7646
  }
6680
7647
  }
6681
- const originalAudience = new Set(message.recipient_identities);
6682
- for (const recipient of recipients) {
6683
- if (!originalAudience.has(recipient.identity)) await this.ensureLateBriefing(room, recipient);
6684
- }
6685
- return message.at;
6686
- }
6687
- async ensureLateBriefing(room, seat) {
6688
- const records = await this.store.read(room.room_id);
6689
- let message = [...records].reverse().find(
6690
- (record) => record.kind === "message" && record.category === "briefing" && record.recipient_identities.includes(seat.identity)
6691
- );
6692
- if (!message) message = await this.appendBriefing(room, [seat]);
6693
- const alreadyBriefed = records.some((record) => record.kind === "relay_intent" && record.message_id === message.message_id && record.recipient_identity === seat.identity);
6694
- if (alreadyBriefed) return;
6695
- await this.store.append(room.room_id, {
6696
- version: 1,
6697
- kind: "relay_intent",
6698
- room_id: room.room_id,
6699
- at: this.now(),
6700
- message_id: message.message_id,
6701
- recipient_identity: seat.identity
6702
- });
6703
- }
6704
- async appendBriefing(room, recipients) {
6705
- const record = await this.store.append(room.room_id, {
6706
- version: 1,
6707
- kind: "message",
6708
- room_id: room.room_id,
6709
- at: this.now(),
6710
- message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
6711
- author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
6712
- category: "briefing",
6713
- text: room.mission.briefing,
6714
- recipient_identities: uniqueIdentities(recipients.map((seat) => seat.identity))
6715
- });
6716
- if (record.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
6717
- return record;
7648
+ return matching[0]?.at ?? appendedAt ?? this.now();
6718
7649
  }
6719
7650
  lock(roomId, work) {
6720
7651
  return this.store.mutex(roomId).runExclusive(work);
@@ -6740,7 +7671,7 @@ var init_service = __esm({
6740
7671
  });
6741
7672
 
6742
7673
  // src/storage.ts
6743
- import { randomBytes as randomBytes4 } from "node:crypto";
7674
+ import { randomBytes as randomBytes3 } from "node:crypto";
6744
7675
  import * as nodeFs3 from "node:fs";
6745
7676
  import { AsyncLocalStorage } from "node:async_hooks";
6746
7677
  import { dirname as dirname4, join as join4 } from "node:path";
@@ -6749,6 +7680,7 @@ var init_storage = __esm({
6749
7680
  "src/storage.ts"() {
6750
7681
  "use strict";
6751
7682
  init_contracts();
7683
+ init_ulid();
6752
7684
  DIRECTORY_MODE2 = 448;
6753
7685
  FILE_MODE2 = 384;
6754
7686
  NO_FOLLOW2 = nodeFs3.constants.O_NOFOLLOW ?? 0;
@@ -6989,11 +7921,17 @@ var init_storage = __esm({
6989
7921
  `room "${validRoomId}" has archive residue without deletion metadata`
6990
7922
  );
6991
7923
  }
6992
- const expected = /* @__PURE__ */ new Set(["archive.jsonl", "room.json"]);
7924
+ const expected = /* @__PURE__ */ new Set(["archive.jsonl", "room.json", "room.json.v1.bak"]);
6993
7925
  const unexpected = this.fs.readdirSync(roomDir).filter((name) => !expected.has(name));
6994
7926
  if (unexpected.length > 0) {
6995
7927
  throw new CoworkStorageError(`room "${validRoomId}" contains live or unexpected residue: ${unexpected.join(", ")}`);
6996
7928
  }
7929
+ const backupPath = `${metadataPath}.v1.bak`;
7930
+ if (this.lstatIfPresent(backupPath)) {
7931
+ this.assertRegularFile(backupPath, "room metadata v1 backup");
7932
+ this.fs.unlinkSync(backupPath);
7933
+ this.fsyncDirectory(roomDir);
7934
+ }
6997
7935
  if (archivePresent) {
6998
7936
  this.assertRegularFile(archivePath, "room archive");
6999
7937
  this.fs.unlinkSync(archivePath);
@@ -7048,10 +7986,106 @@ var init_storage = __esm({
7048
7986
  } catch (error) {
7049
7987
  throw this.wrap(`malformed metadata for room "${roomId}"`, error);
7050
7988
  }
7051
- const room = RoomSchema.parse(decoded);
7989
+ const room = this.isVersion1(decoded) ? this.migrateUnlocked(roomId, decoded, bytes) : RoomSchema.parse(decoded);
7052
7990
  if (room.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
7053
7991
  return room;
7054
7992
  }
7993
+ isVersion1(decoded) {
7994
+ return typeof decoded === "object" && decoded !== null && decoded.version === 1;
7995
+ }
7996
+ /**
7997
+ * Lazy additive v1 → v2 migration (spec §7): preserve the exact pre-migration
7998
+ * bytes once as room.json.v1.bak, then atomically persist the v2 metadata.
7999
+ *
8000
+ * THE BACKUP IS WRITTEN TEMP → FSYNC → RENAME, not opened in place, and the
8001
+ * reason is a crash window that an existence check cannot see. The earlier
8002
+ * version guarded with `if (!lstatIfPresent(backupPath))` and wrote straight
8003
+ * into the final path under O_CREAT|O_EXCL. A crash inside that write leaves a
8004
+ * PARTIAL file that nonetheless EXISTS, so the next load's existence check
8005
+ * skips the backup, writes v2, and the pre-migration bytes are gone — no
8006
+ * error, no warning, and the one artefact that exists to undo a bad migration
8007
+ * is a truncated fragment. Measured before the fix: a 40-byte prefix of a
8008
+ * 604-byte room, JSON.parse false, room.json already v2.
8009
+ *
8010
+ * A rename is atomic, so the final path now only ever appears complete. The
8011
+ * temp file is created with O_EXCL under a pid+random name and removed on
8012
+ * failure, so a crashed attempt leaves at most an orphan temp, never a
8013
+ * plausible-looking backup.
8014
+ *
8015
+ * AND AN EXISTING BACKUP IS PARSED BEFORE IT IS TRUSTED, which repairs the
8016
+ * case where a partial file is ALREADY on disk from a build without this fix —
8017
+ * exactly the state a host that ran the previous code could be in right now.
8018
+ * A backup that does not parse as v1 is replaced by the bytes we hold, because
8019
+ * those are the real pre-migration bytes and the fragment is worthless.
8020
+ *
8021
+ * WHAT THE BACKUP IS NOT: restoring it is NOT a rollback. See the note on
8022
+ * `restoreV1Backup` — re-migrating mints fresh participant ids.
8023
+ */
8024
+ migrateUnlocked(roomId, decoded, originalBytes) {
8025
+ const v1 = RoomV1Schema.parse(decoded);
8026
+ if (v1.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
8027
+ const migrated = migrateRoomV1(v1, generateUlid);
8028
+ const backupPath = `${this.metadataPath(roomId)}.v1.bak`;
8029
+ if (!this.hasIntactV1Backup(backupPath)) {
8030
+ this.atomicBytesWrite(backupPath, originalBytes, "room metadata v1 backup");
8031
+ }
8032
+ this.atomicMetadataWrite(this.metadataPath(roomId), migrated);
8033
+ return migrated;
8034
+ }
8035
+ /**
8036
+ * Is there already a backup we would be willing to hand back to an operator?
8037
+ *
8038
+ * Existence is not the question — a partial file exists. It must parse and
8039
+ * still claim to be the v1 metadata for this room; anything else is a fragment
8040
+ * and is better overwritten with the bytes we are holding right now.
8041
+ */
8042
+ hasIntactV1Backup(backupPath) {
8043
+ if (!this.lstatIfPresent(backupPath)) return false;
8044
+ try {
8045
+ const decoded = JSON.parse(utf8Decoder.decode(this.readFileNoFollow(backupPath, "room metadata v1 backup")));
8046
+ return this.isVersion1(decoded);
8047
+ } catch {
8048
+ return false;
8049
+ }
8050
+ }
8051
+ /**
8052
+ * Durably place exact bytes at `path`: temp under O_EXCL, fsync, rename, fsync
8053
+ * the directory. The same dance as atomicMetadataWrite, which serialises a Room
8054
+ * rather than taking bytes verbatim — and taking them verbatim is the whole
8055
+ * point for a backup, whose value is being byte-identical to what was there.
8056
+ */
8057
+ atomicBytesWrite(path, bytes, label) {
8058
+ if (this.lstatIfPresent(path)) this.assertRegularFile(path, label);
8059
+ const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
8060
+ let fd;
8061
+ try {
8062
+ fd = this.fs.openSync(
8063
+ temp,
8064
+ nodeFs3.constants.O_CREAT | nodeFs3.constants.O_EXCL | nodeFs3.constants.O_WRONLY | NO_FOLLOW2,
8065
+ FILE_MODE2
8066
+ );
8067
+ this.validateOpenPath(fd, temp, `temporary ${label}`, "file", true);
8068
+ this.fs.fchmodSync(fd, FILE_MODE2);
8069
+ this.writeAll(fd, bytes);
8070
+ this.fs.fsyncSync(fd);
8071
+ this.fs.closeSync(fd);
8072
+ fd = void 0;
8073
+ this.fs.renameSync(temp, path);
8074
+ this.fsyncDirectory(dirname4(path));
8075
+ } catch (error) {
8076
+ if (fd !== void 0) {
8077
+ try {
8078
+ this.fs.closeSync(fd);
8079
+ } catch {
8080
+ }
8081
+ }
8082
+ try {
8083
+ this.fs.rmSync(temp, { force: true });
8084
+ } catch {
8085
+ }
8086
+ throw this.wrap(`failed to write ${label} at ${path}`, error);
8087
+ }
8088
+ }
7055
8089
  scanArchive(roomId) {
7056
8090
  const path = this.archivePath(roomId);
7057
8091
  this.assertRegularFile(path, "room archive");
@@ -7213,7 +8247,7 @@ var init_storage = __esm({
7213
8247
  }
7214
8248
  atomicMetadataWrite(path, room) {
7215
8249
  if (this.lstatIfPresent(path)) this.assertRegularFile(path, "room metadata");
7216
- const temp = `${path}.tmp-${process.pid}-${randomBytes4(8).toString("hex")}`;
8250
+ const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
7217
8251
  const bytes = Buffer.from(`${JSON.stringify(room)}
7218
8252
  `, "utf8");
7219
8253
  let fd;
@@ -7456,7 +8490,7 @@ var init_web = __esm({
7456
8490
  });
7457
8491
 
7458
8492
  // src/transports.ts
7459
- import { randomBytes as randomBytes5 } from "node:crypto";
8493
+ import { randomBytes as randomBytes4 } from "node:crypto";
7460
8494
  import * as http from "node:http";
7461
8495
  import * as net from "node:net";
7462
8496
  import * as nodeFs5 from "node:fs";
@@ -7469,19 +8503,36 @@ function createServiceRoutes(service) {
7469
8503
  room_id: external_exports.string(),
7470
8504
  goal: external_exports.unknown().optional(),
7471
8505
  briefing: external_exports.unknown().optional(),
7472
- status: external_exports.unknown().optional()
8506
+ status: external_exports.unknown().optional(),
8507
+ quiet_membership: external_exports.unknown().optional()
7473
8508
  }).strict().parse(params);
7474
8509
  return service.updateRoom(room_id, input);
7475
8510
  } },
8511
+ "room.briefing.role.set": { auth: true, run: (params) => {
8512
+ const { room_id, ...input } = RoleBriefingSetParams.parse(params);
8513
+ return service.setRoleBriefing(room_id, input);
8514
+ } },
8515
+ "room.briefing.role.delete": { auth: true, run: (params) => {
8516
+ const { room_id, ...input } = RoleBriefingDeleteParams.parse(params);
8517
+ return service.deleteRoleBriefing(room_id, input);
8518
+ } },
7476
8519
  "room.invite": { auth: true, run: (params) => {
7477
8520
  const { room_id, ...input } = external_exports.object({
7478
8521
  room_id: external_exports.string(),
7479
8522
  mode: external_exports.unknown(),
7480
- role: external_exports.unknown(),
8523
+ role: external_exports.unknown().optional(),
7481
8524
  min_accepts: external_exports.unknown()
7482
8525
  }).strict().parse(params);
7483
8526
  return service.createInvite(room_id, input);
7484
8527
  } },
8528
+ "room.participant.remove": { auth: true, run: (params) => {
8529
+ const { room_id, ...input } = ParticipantRemoveParams.parse(params);
8530
+ return service.removeParticipant(room_id, input);
8531
+ } },
8532
+ "room.participant.replace": { auth: true, run: (params) => {
8533
+ const { room_id, ...input } = ParticipantReplaceParams.parse(params);
8534
+ return service.replaceParticipant(room_id, input);
8535
+ } },
7485
8536
  "room.revoke": { auth: true, run: (params) => {
7486
8537
  const value = InviteRevokeParams.parse(params);
7487
8538
  return service.revokeInvite(value.room_id, value.invite_id);
@@ -7639,7 +8690,7 @@ function responseFinished2(response) {
7639
8690
  response.once("error", rejectResponse);
7640
8691
  });
7641
8692
  }
7642
- var MAX_REQUEST_BYTES, HTTP_HEADERS_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, HTTP_BODY_IDLE_TIMEOUT_MS, HTTP_KEEP_ALIVE_TIMEOUT_MS, MAX_STALE_PRIVATE_SOCKET_CLEANUP, RpcIdSchema, RpcRequestSchema, RpcDispatcher, RoomIdParams, InviteRevokeParams, RecoverConfirmParams, HistoryParams, TransportServer;
8693
+ var MAX_REQUEST_BYTES, HTTP_HEADERS_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, HTTP_BODY_IDLE_TIMEOUT_MS, HTTP_KEEP_ALIVE_TIMEOUT_MS, MAX_STALE_PRIVATE_SOCKET_CLEANUP, RpcIdSchema, RpcRequestSchema, RpcDispatcher, RoomIdParams, InviteRevokeParams, RecoverConfirmParams, HistoryParams, RoleBriefingSetParams, RoleBriefingDeleteParams, ParticipantRemoveParams, ParticipantReplaceParams, TransportServer;
7643
8694
  var init_transports = __esm({
7644
8695
  "src/transports.ts"() {
7645
8696
  "use strict";
@@ -7714,7 +8765,29 @@ var init_transports = __esm({
7714
8765
  HistoryParams = external_exports.object({
7715
8766
  room_id: external_exports.string(),
7716
8767
  after: external_exports.number().optional(),
7717
- limit: external_exports.number().optional()
8768
+ limit: external_exports.number().optional(),
8769
+ view: external_exports.enum(["operator", "participant"]).optional()
8770
+ }).strict();
8771
+ RoleBriefingSetParams = external_exports.object({
8772
+ room_id: external_exports.string(),
8773
+ role: external_exports.string(),
8774
+ text: external_exports.string()
8775
+ }).strict();
8776
+ RoleBriefingDeleteParams = external_exports.object({
8777
+ room_id: external_exports.string(),
8778
+ role: external_exports.string()
8779
+ }).strict();
8780
+ ParticipantRemoveParams = external_exports.object({
8781
+ room_id: external_exports.string(),
8782
+ participant: external_exports.string(),
8783
+ notify: external_exports.boolean().optional()
8784
+ }).strict();
8785
+ ParticipantReplaceParams = external_exports.object({
8786
+ room_id: external_exports.string(),
8787
+ participant: external_exports.string(),
8788
+ notify: external_exports.boolean().optional(),
8789
+ mode: external_exports.enum(["one_time", "public"]).optional(),
8790
+ min_accepts: external_exports.number().optional()
7718
8791
  }).strict();
7719
8792
  TransportServer = class {
7720
8793
  options;
@@ -7751,7 +8824,7 @@ var init_transports = __esm({
7751
8824
  await this.cleanupStalePrivateSockets();
7752
8825
  const unix = net.createServer((socket) => this.handleUnix(socket));
7753
8826
  this.unixServer = unix;
7754
- const privateSocketPath = `${this.options.socketPath}.private-${process.pid}-${randomBytes5(6).toString("hex")}`;
8827
+ const privateSocketPath = `${this.options.socketPath}.private-${process.pid}-${randomBytes4(6).toString("hex")}`;
7755
8828
  this.privateSocketPath = privateSocketPath;
7756
8829
  try {
7757
8830
  await listen(unix, privateSocketPath);
@@ -8060,7 +9133,7 @@ var init_transports = __esm({
8060
9133
  }
8061
9134
  }
8062
9135
  quarantinePath(target, label) {
8063
- const path = `${this.options.socketPath}.safe-residue-${label}-${process.pid}-${randomBytes5(6).toString("hex")}`;
9136
+ const path = `${this.options.socketPath}.safe-residue-${label}-${process.pid}-${randomBytes4(6).toString("hex")}`;
8064
9137
  this.fs.renameSync(target, path);
8065
9138
  const moved = this.fs.lstatSync(path);
8066
9139
  return { target, path, dev: moved.dev, ino: moved.ino };
@@ -8084,7 +9157,7 @@ var init_transports = __esm({
8084
9157
  }
8085
9158
  }
8086
9159
  quarantinePublicPath() {
8087
- const quarantinePath = `${this.options.socketPath}.replacement-${process.pid}-${randomBytes5(6).toString("hex")}`;
9160
+ const quarantinePath = `${this.options.socketPath}.replacement-${process.pid}-${randomBytes4(6).toString("hex")}`;
8088
9161
  try {
8089
9162
  this.fs.renameSync(this.options.socketPath, quarantinePath);
8090
9163
  } catch (error) {
@@ -8132,6 +9205,7 @@ __export(daemon_runtime_exports, {
8132
9205
  DaemonShutdownError: () => DaemonShutdownError,
8133
9206
  acquireDaemonLock: () => acquireDaemonLock,
8134
9207
  createDaemonControlRoutes: () => createDaemonControlRoutes,
9208
+ isIntakeNotification: () => isIntakeNotification,
8135
9209
  loadConfig: () => loadConfig,
8136
9210
  removeDaemonPid: () => removeDaemonPid,
8137
9211
  writeDaemonPid: () => writeDaemonPid
@@ -8139,6 +9213,9 @@ __export(daemon_runtime_exports, {
8139
9213
  import * as nodeFs6 from "node:fs";
8140
9214
  import { join as join7 } from "node:path";
8141
9215
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9216
+ function isIntakeNotification(event) {
9217
+ return event === "message_received" || event === "file_received" || event === "contact_accepted";
9218
+ }
8142
9219
  function createDaemonControlRoutes(control) {
8143
9220
  if (!/^[0-9a-f]{32}$/.test(control.session)) throw new TypeError("invalid daemon control session");
8144
9221
  const requireExact = (params, keys) => {
@@ -8421,7 +9498,7 @@ var init_daemon_runtime = __esm({
8421
9498
  {
8422
9499
  log: this.options.log,
8423
9500
  onNotify: (roomId, event) => {
8424
- if (event === "message_received" || event === "contact_accepted") {
9501
+ if (isIntakeNotification(event)) {
8425
9502
  this.handleNotification(roomId, event, serviceRef);
8426
9503
  }
8427
9504
  }
@@ -8608,7 +9685,7 @@ var daemon_process_exports = {};
8608
9685
  __export(daemon_process_exports, {
8609
9686
  runDaemonProcess: () => runDaemonProcess
8610
9687
  });
8611
- import { randomBytes as randomBytes6 } from "node:crypto";
9688
+ import { randomBytes as randomBytes5 } from "node:crypto";
8612
9689
  async function runDaemonProcess() {
8613
9690
  try {
8614
9691
  const code = process.env.OURS_COWORK_DAEMON_WORKER === "1" ? await runWorker() : await runSupervisor();
@@ -8721,7 +9798,7 @@ async function runWorker() {
8721
9798
  },
8722
9799
  control: {
8723
9800
  // Created only after the supervisor capability handshake completed.
8724
- session: randomBytes6(16).toString("hex"),
9801
+ session: randomBytes5(16).toString("hex"),
8725
9802
  async requestSupervisorShutdown() {
8726
9803
  if (!capability || disconnected || process.connected === false) return false;
8727
9804
  return sendIpc({ type: "shutdown_request", capability });
@@ -8799,7 +9876,7 @@ var init_daemon_process = __esm({
8799
9876
 
8800
9877
  // src/daemon.ts
8801
9878
  import { fork } from "node:child_process";
8802
- import { randomBytes as randomBytes7 } from "node:crypto";
9879
+ import { randomBytes as randomBytes6 } from "node:crypto";
8803
9880
  import { resolve as resolve3 } from "node:path";
8804
9881
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8805
9882
  async function runSupervisor(options = {}) {
@@ -8906,7 +9983,7 @@ var init_daemon = __esm({
8906
9983
  this.signals = options.signals ?? process;
8907
9984
  this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? DAEMON_SHUTDOWN_TIMEOUT_MS;
8908
9985
  this.onStageCallback = options.onStage;
8909
- this.capability = options.capability ?? randomBytes7(32).toString("hex");
9986
+ this.capability = options.capability ?? randomBytes6(32).toString("hex");
8910
9987
  if (!/^[0-9a-f]{64}$/.test(this.capability)) throw new Error("invalid daemon worker capability");
8911
9988
  this.done = new Promise((resolveDone) => {
8912
9989
  this.resolveDone = resolveDone;