@ours.network/cowork 0.3.0 → 0.3.2

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,1241 +4767,1402 @@ var init_config = __esm({
4767
4767
  }
4768
4768
  });
4769
4769
 
4770
- // src/packets.ts
4771
- import { randomBytes } from "node:crypto";
4772
- import * as nodeFs2 from "node:fs";
4773
- import { dirname as dirname3, join as join3 } from "node:path";
4774
- function atomicWriteFileSync(target, bytes, ops = nodeFs2) {
4775
- const temp = `${target}.tmp-${process.pid}-${temporarySequence++}`;
4776
- let fileFd;
4777
- let directoryFd;
4778
- try {
4779
- fileFd = ops.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_TRUNC | nodeFs2.constants.O_WRONLY, 384);
4780
- ops.fchmodSync(fileFd, 384);
4781
- let offset = 0;
4782
- while (offset < bytes.byteLength) {
4783
- const written = ops.writeSync(fileFd, bytes, offset, bytes.byteLength - offset, null);
4784
- if (written <= 0) throw new Error(`short write while persisting ${target}`);
4785
- offset += written;
4786
- }
4787
- ops.fsyncSync(fileFd);
4788
- ops.closeSync(fileFd);
4789
- fileFd = void 0;
4790
- ops.renameSync(temp, target);
4791
- ops.chmodSync(target, 384);
4792
- directoryFd = ops.openSync(dirname3(target), nodeFs2.constants.O_RDONLY);
4793
- ops.fsyncSync(directoryFd);
4794
- ops.closeSync(directoryFd);
4795
- directoryFd = void 0;
4796
- } catch (error) {
4797
- if (fileFd !== void 0) {
4798
- try {
4799
- ops.closeSync(fileFd);
4800
- } catch {
4801
- }
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 normalizeRoomName(value) {
4797
+ return value.trim().normalize("NFC");
4798
+ }
4799
+ function refineRoomLineage(room, context) {
4800
+ const pendingIdentityName = `cowork-room-${room.room_id}`;
4801
+ 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;
4802
+ if (room.identity_cid === "" && !exactPacketPending) {
4803
+ context.addIssue({
4804
+ code: external_exports.ZodIssueCode.custom,
4805
+ path: ["identity_cid"],
4806
+ message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
4807
+ });
4808
+ }
4809
+ if (room.identity_cid !== "" && room.status === "packet_pending") {
4810
+ context.addIssue({
4811
+ code: external_exports.ZodIssueCode.custom,
4812
+ path: ["status"],
4813
+ message: "packet_pending status requires an empty identity_cid"
4814
+ });
4815
+ }
4816
+ const pendingByRecovery = /* @__PURE__ */ new Map();
4817
+ for (const [index, invite] of room.invites.entries()) {
4818
+ if (invite.recovery_of === void 0) continue;
4819
+ const recoveryOf = invite.recovery_of;
4820
+ const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
4821
+ 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;
4822
+ if (!source || source.invite_id === invite.invite_id || !validSourceState) {
4823
+ context.addIssue({
4824
+ code: external_exports.ZodIssueCode.custom,
4825
+ path: ["invites", index, "recovery_of"],
4826
+ message: "recovery_of must point to a source invite in the state required by this recovery lineage"
4827
+ });
4828
+ } else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
4829
+ context.addIssue({
4830
+ code: external_exports.ZodIssueCode.custom,
4831
+ path: ["invites", index],
4832
+ message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
4833
+ });
4802
4834
  }
4803
- if (directoryFd !== void 0) {
4804
- try {
4805
- ops.closeSync(directoryFd);
4806
- } catch {
4835
+ if (invite.state === "receipt_pending") {
4836
+ const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
4837
+ pendingByRecovery.set(recoveryOf, count);
4838
+ if (count > 1) {
4839
+ context.addIssue({
4840
+ code: external_exports.ZodIssueCode.custom,
4841
+ path: ["invites", index, "recovery_of"],
4842
+ message: "only one receipt_pending invite may exist per recovery_of pointer"
4843
+ });
4807
4844
  }
4808
4845
  }
4809
- try {
4810
- ops.rmSync(temp, { force: true });
4811
- } catch {
4812
- }
4813
- throw new PacketPersistenceError(`failed to durably persist ${target}`, { cause: error });
4814
4846
  }
4815
4847
  }
4816
- function renderInbox(value) {
4817
- const output = [];
4818
- if (value.IsNil()) return output;
4819
- for (let index = 0; ; index += 1) {
4820
- const message = value.Reduce(index);
4821
- if (message.IsNil()) break;
4822
- output.push({
4823
- msg_id: Number(message.Reduce("msg_id").Visualize()),
4824
- sender_id: message.Reduce("sender_id").Visualize(),
4825
- sender_name: message.Reduce("sender_name").Visualize(),
4826
- text: message.Reduce("text").Visualize(),
4827
- date: adaptTimeToRfc3339(message.Reduce("date").Visualize()),
4828
- status: message.Reduce("status").Visualize(),
4829
- wire_id: message.Reduce("wire_id").Visualize()
4848
+ function defaultRoomName(roomId) {
4849
+ return `Room ${LowerCrockfordUlidSchema.parse(roomId).slice(0, 8)}`;
4850
+ }
4851
+ function migrateRoomV1(room, mintParticipantId) {
4852
+ return RoomSchema.parse({
4853
+ ...room,
4854
+ version: 2,
4855
+ mission: { ...room.mission, briefing_version: 1 },
4856
+ role_briefings: {},
4857
+ anonymous: false,
4858
+ quiet_membership: false,
4859
+ membership_epoch: 0,
4860
+ seats: room.seats.map((seat) => ({
4861
+ ...seat,
4862
+ participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
4863
+ state: "active"
4864
+ }))
4865
+ });
4866
+ }
4867
+ function refineRelaySubject(record, context) {
4868
+ if (record.kind !== "relay_intent" && record.kind !== "relay_result") return;
4869
+ if (record.message_id === void 0 === (record.file_id === void 0)) {
4870
+ context.addIssue({
4871
+ code: external_exports.ZodIssueCode.custom,
4872
+ path: ["message_id"],
4873
+ message: "relay records require exactly one of message_id or file_id"
4830
4874
  });
4831
4875
  }
4832
- return output;
4833
4876
  }
4834
- function renderIntegerArray(value) {
4835
- const output = [];
4836
- if (value.IsNil()) return output;
4837
- for (let index = 0; ; index += 1) {
4838
- const item = value.Reduce(index);
4839
- if (item.IsNil()) break;
4840
- output.push(Number(item.Visualize()));
4877
+ function refineFileRecord(record, context) {
4878
+ if (record.kind !== "file" || record.data_base64 === void 0) return;
4879
+ const bytes = Buffer.from(record.data_base64, "base64");
4880
+ if (bytes.toString("base64") !== record.data_base64) {
4881
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["data_base64"], message: "file bytes must use canonical base64" });
4841
4882
  }
4842
- return output;
4843
- }
4844
- function dictionaryEntries(value) {
4845
- if (value.IsNil()) return [];
4846
- return value.GetKeys().map((key) => [key.Visualize(), value.Reduce(key)]);
4847
- }
4848
- function booleanValue(value) {
4849
- if (value.IsNil()) return false;
4850
- try {
4851
- return value.GetBoolean();
4852
- } catch {
4853
- return /true/i.test(value.Visualize());
4883
+ if (bytes.length !== record.size) {
4884
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["size"], message: "file size must match decoded bytes" });
4854
4885
  }
4855
- }
4856
- function strictBooleanValue(value, label) {
4857
- if (value.IsNil()) throw new Error(`${label} must be a boolean`);
4858
- let decoded;
4859
- try {
4860
- decoded = value.GetBoolean();
4861
- } catch (error) {
4862
- throw new Error(`${label} must be a boolean`, { cause: error });
4886
+ const digest = createHash("sha256").update(bytes).digest("hex");
4887
+ if (digest !== record.sha256) {
4888
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["sha256"], message: "file sha256 must match decoded bytes" });
4863
4889
  }
4864
- if (typeof decoded !== "boolean") throw new Error(`${label} must be a boolean`);
4865
- return decoded;
4866
- }
4867
- function nilString(value) {
4868
- return value.IsNil() ? "" : value.Visualize();
4869
4890
  }
4870
- function adaptTimeToRfc3339(value) {
4871
- const rfc3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
4872
- if (rfc3339) {
4873
- const [, year2, month2, day2, hour2, minute2, second2, fraction2 = "", sign2, offsetHour2 = "0", offsetMinute = "0"] = rfc3339;
4874
- if (sign2 === "-" && offsetHour2 === "00" && offsetMinute === "00") return invalidAdaptTime(value);
4875
- return canonicalUtcTime(value, year2, month2, day2, hour2, minute2, second2, fraction2, sign2, offsetHour2, offsetMinute);
4891
+ function refineMessageCategory(message, context) {
4892
+ const requires = (field, present) => {
4893
+ if (present && message[field] === void 0) {
4894
+ context.addIssue({
4895
+ code: external_exports.ZodIssueCode.custom,
4896
+ path: [field],
4897
+ message: `${message.category} messages require ${field}`
4898
+ });
4899
+ }
4900
+ if (!present && message[field] !== void 0) {
4901
+ context.addIssue({
4902
+ code: external_exports.ZodIssueCode.custom,
4903
+ path: [field],
4904
+ message: `${field} is forbidden on ${message.category} messages`
4905
+ });
4906
+ }
4907
+ };
4908
+ requires("briefing_role", message.category === "role_briefing");
4909
+ requires("membership", message.category === "membership");
4910
+ if (message.category === "role_briefing" && message.briefing_version === void 0) {
4911
+ context.addIssue({
4912
+ code: external_exports.ZodIssueCode.custom,
4913
+ path: ["briefing_version"],
4914
+ message: "role_briefing messages require briefing_version"
4915
+ });
4876
4916
  }
4877
- const native = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))? \(UTC(?:([+-])(0|[1-9]|1\d|2[0-3]))?\)$/.exec(value);
4878
- if (!native) return invalidAdaptTime(value);
4879
- const [, year, month, day, hour, minute, second, fraction = "", sign, offsetHour = "0"] = native;
4880
- if (sign === "-" && offsetHour === "0") return invalidAdaptTime(value);
4881
- return canonicalUtcTime(value, year, month, day, hour, minute, second, fraction, sign, offsetHour, "0");
4882
- }
4883
- function canonicalUtcTime(source, yearText, monthText, dayText, hourText, minuteText, secondText, fraction, offsetSign, offsetHourText, offsetMinuteText) {
4884
- const year = Number(yearText);
4885
- const month = Number(monthText);
4886
- const day = Number(dayText);
4887
- const hour = Number(hourText);
4888
- const minute = Number(minuteText);
4889
- const second = Number(secondText);
4890
- const offsetHour = Number(offsetHourText);
4891
- const offsetMinute = Number(offsetMinuteText);
4892
- const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
4893
- const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
4894
- if (month < 1 || month > 12 || day < 1 || day > monthDays[month - 1] || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) return invalidAdaptTime(source);
4895
- const milliseconds = Number(fraction.slice(0, 3).padEnd(3, "0"));
4896
- const local = /* @__PURE__ */ new Date(0);
4897
- local.setUTCFullYear(year, month - 1, day);
4898
- local.setUTCHours(hour, minute, second, milliseconds);
4899
- if (local.getUTCFullYear() !== year || local.getUTCMonth() !== month - 1 || local.getUTCDate() !== day || local.getUTCHours() !== hour || local.getUTCMinutes() !== minute || local.getUTCSeconds() !== second) {
4900
- return invalidAdaptTime(source);
4917
+ if (message.category === "chat" || message.category === "membership") {
4918
+ if (message.briefing_version !== void 0) {
4919
+ context.addIssue({
4920
+ code: external_exports.ZodIssueCode.custom,
4921
+ path: ["briefing_version"],
4922
+ message: `briefing_version is forbidden on ${message.category} messages`
4923
+ });
4924
+ }
4901
4925
  }
4902
- const direction = offsetSign === "-" ? -1 : 1;
4903
- const offsetMilliseconds = direction * (offsetHour * 60 + offsetMinute) * 6e4;
4904
- const canonical = new Date(local.getTime() - offsetMilliseconds).toISOString();
4905
- if (!/^\d{4}-/.test(canonical)) return invalidAdaptTime(source);
4906
- return canonical;
4907
- }
4908
- function invalidAdaptTime(value) {
4909
- throw new Error(`unexpected ADAPT time visualization: ${value}`);
4910
- }
4911
- function inviteMode(value) {
4912
- const normalized = value.replace(/^\$/, "");
4913
- if (normalized === "one_time" || normalized === "public") return normalized;
4914
- throw new Error(`unexpected invite mode: ${value}`);
4915
- }
4916
- function exportSigningSecret(packet) {
4917
- return withScope((lifetime) => Buffer.from(packet.readonlyTx("::actor::export_signing_secret", lifetime).Serialize()).toString("hex"));
4918
4926
  }
4919
- function validateRoomId(roomId) {
4920
- if (!/^[A-Za-z0-9_-]{1,128}$/.test(roomId)) throw new Error(`invalid room id: ${roomId}`);
4921
- }
4922
- var PacketPersistenceError, temporarySequence, PacketRegistry, HostedRoomPacket;
4923
- var init_packets = __esm({
4924
- async "src/packets.ts"() {
4927
+ 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, MAX_ROOM_NAME_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoomNameSchema, RoleSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, CurrentRoomSchema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, 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;
4928
+ var init_contracts = __esm({
4929
+ "src/contracts.ts"() {
4925
4930
  "use strict";
4926
- await init_adapt();
4927
- PacketPersistenceError = class extends Error {
4928
- constructor(message, options) {
4929
- super(message, options);
4930
- this.name = "PacketPersistenceError";
4931
+ init_zod();
4932
+ MAX_TEXT_BYTES = 262144;
4933
+ MAX_FILE_BYTES = 2 * 1024 * 1024;
4934
+ MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
4935
+ MAX_MANAGEMENT_RESPONSE_BYTES = MAX_HISTORY_PAGE_BYTES + 1024 * 1024;
4936
+ MAX_FILE_NAME_BYTES = 255;
4937
+ MAX_MIME_BYTES = 255;
4938
+ MAX_ROLE_BYTES = 256;
4939
+ MAX_ROOM_NAME_CHARACTERS = 64;
4940
+ NonEmptyStringSchema = external_exports.string().min(1);
4941
+ PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
4942
+ LowerCrockfordUlidSchema = external_exports.string().regex(
4943
+ /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4944
+ "must be a 26-character lowercase Crockford ULID"
4945
+ );
4946
+ Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
4947
+ RoomNameSchema = external_exports.string().refine(
4948
+ (value) => !/[\p{Cc}\p{Cf}]/u.test(value),
4949
+ "room name must not contain Unicode control or format characters"
4950
+ ).transform(normalizeRoomName).superRefine((value, context) => {
4951
+ const length = Array.from(value).length;
4952
+ if (length < 1 || length > MAX_ROOM_NAME_CHARACTERS) {
4953
+ context.addIssue({
4954
+ code: external_exports.ZodIssueCode.custom,
4955
+ message: `room name must contain 1-${MAX_ROOM_NAME_CHARACTERS} Unicode characters after normalization`
4956
+ });
4931
4957
  }
4932
- };
4933
- temporarySequence = 0;
4934
- PacketRegistry = class {
4935
- packets = /* @__PURE__ */ new Map();
4936
- host;
4937
- stateDir;
4938
- fs;
4939
- persistence;
4940
- log;
4941
- seed;
4942
- beforeExpose;
4943
- onNotify;
4944
- provisioningCheckpoint;
4945
- stagingName;
4946
- constructor(host, stateDir, options = {}) {
4947
- this.host = host;
4948
- this.stateDir = stateDir;
4949
- this.fs = options.fs ?? nodeFs2;
4950
- this.persistence = options.persistence ?? this.fs;
4951
- this.log = options.log ?? (() => {
4958
+ });
4959
+ RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4960
+ MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
4961
+ MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
4962
+ 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");
4963
+ FileMimeSchema = external_exports.string().refine(
4964
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_MIME_BYTES,
4965
+ `file MIME metadata must be at most ${MAX_MIME_BYTES} UTF-8 bytes`
4966
+ );
4967
+ RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
4968
+ SeatStateSchema = external_exports.enum(["active", "removed"]);
4969
+ InviteModeSchema = external_exports.enum(["one_time", "public"]);
4970
+ DEFAULT_ROLE = "Participant";
4971
+ InviteStateSchema = external_exports.enum([
4972
+ "live",
4973
+ "consumed",
4974
+ "revoked",
4975
+ "replacement_required",
4976
+ "receipt_pending"
4977
+ ]);
4978
+ RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
4979
+ SeatV1Schema = external_exports.object({
4980
+ identity: NonEmptyStringSchema,
4981
+ display_name: NonEmptyStringSchema,
4982
+ role: RoleSchema,
4983
+ invite_id: NonEmptyStringSchema,
4984
+ accepted_at: Rfc3339Schema
4985
+ }).strict();
4986
+ SeatSchema = external_exports.object({
4987
+ identity: NonEmptyStringSchema,
4988
+ display_name: NonEmptyStringSchema,
4989
+ role: RoleSchema,
4990
+ invite_id: NonEmptyStringSchema,
4991
+ accepted_at: Rfc3339Schema,
4992
+ participant_id: LowerCrockfordUlidSchema,
4993
+ state: SeatStateSchema,
4994
+ alias: NonEmptyStringSchema.optional(),
4995
+ removed_at: Rfc3339Schema.optional(),
4996
+ removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
4997
+ replaces_seat: LowerCrockfordUlidSchema.optional(),
4998
+ bounced_at: Rfc3339Schema.optional()
4999
+ }).strict().superRefine((seat, context) => {
5000
+ if (seat.state === "removed") {
5001
+ if (seat.removed_at === void 0) {
5002
+ context.addIssue({
5003
+ code: external_exports.ZodIssueCode.custom,
5004
+ path: ["removed_at"],
5005
+ message: "removed seats require removed_at"
5006
+ });
5007
+ }
5008
+ if (seat.removed_epoch === void 0) {
5009
+ context.addIssue({
5010
+ code: external_exports.ZodIssueCode.custom,
5011
+ path: ["removed_epoch"],
5012
+ message: "removed seats require removed_epoch"
5013
+ });
5014
+ }
5015
+ } else {
5016
+ for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
5017
+ if (seat[field] !== void 0) {
5018
+ context.addIssue({
5019
+ code: external_exports.ZodIssueCode.custom,
5020
+ path: [field],
5021
+ message: `${field} is reserved for removed seats`
5022
+ });
5023
+ }
5024
+ }
5025
+ }
5026
+ });
5027
+ RoomInviteSchema = external_exports.object({
5028
+ invite_id: NonEmptyStringSchema,
5029
+ mode: InviteModeSchema,
5030
+ role: RoleSchema,
5031
+ min_accepts: PositiveSafeIntegerSchema,
5032
+ accepted_cids: external_exports.array(NonEmptyStringSchema),
5033
+ state: InviteStateSchema,
5034
+ recovery_of: NonEmptyStringSchema.optional(),
5035
+ recovery_confirmed: external_exports.boolean().optional(),
5036
+ created_at: Rfc3339Schema,
5037
+ replaces_seat: LowerCrockfordUlidSchema.optional()
5038
+ }).strict().superRefine((invite, context) => {
5039
+ if (invite.mode === "one_time" && invite.min_accepts !== 1) {
5040
+ context.addIssue({
5041
+ code: external_exports.ZodIssueCode.custom,
5042
+ path: ["min_accepts"],
5043
+ message: "one_time invites require min_accepts === 1"
4952
5044
  });
4953
- this.seed = options.seed ?? (() => randomBytes(24).toString("hex"));
4954
- this.beforeExpose = options.beforeExpose ?? (() => {
5045
+ }
5046
+ if (invite.state === "receipt_pending" && invite.recovery_of === void 0) {
5047
+ context.addIssue({
5048
+ code: external_exports.ZodIssueCode.custom,
5049
+ path: ["recovery_of"],
5050
+ message: "receipt_pending invites require recovery_of"
4955
5051
  });
4956
- this.onNotify = options.onNotify ?? (() => {
5052
+ }
5053
+ if (invite.recovery_of === void 0 && invite.recovery_confirmed !== void 0) {
5054
+ context.addIssue({
5055
+ code: external_exports.ZodIssueCode.custom,
5056
+ path: ["recovery_confirmed"],
5057
+ message: "recovery_confirmed is forbidden without recovery_of"
4957
5058
  });
4958
- this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
5059
+ }
5060
+ if (invite.recovery_of !== void 0 && invite.recovery_confirmed === void 0) {
5061
+ context.addIssue({
5062
+ code: external_exports.ZodIssueCode.custom,
5063
+ path: ["recovery_confirmed"],
5064
+ message: "recovery_confirmed is required with recovery_of"
4959
5065
  });
4960
- this.stagingName = options.stagingName ?? (() => `live.staging-${randomBytes(16).toString("hex")}`);
4961
5066
  }
4962
- get size() {
4963
- return this.packets.size;
5067
+ if (invite.state === "receipt_pending" && invite.recovery_confirmed !== false) {
5068
+ context.addIssue({
5069
+ code: external_exports.ZodIssueCode.custom,
5070
+ path: ["recovery_confirmed"],
5071
+ message: "receipt_pending recovery lineage must be unconfirmed"
5072
+ });
4964
5073
  }
4965
- get(roomId) {
4966
- return this.packets.get(roomId);
5074
+ if (invite.recovery_of !== void 0 && (invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required") && invite.recovery_confirmed !== true) {
5075
+ context.addIssue({
5076
+ code: external_exports.ZodIssueCode.custom,
5077
+ path: ["recovery_confirmed"],
5078
+ message: "live, consumed, and replacement_required recovery lineage must be confirmed"
5079
+ });
4967
5080
  }
4968
- async create(roomId, identityName = `cowork-room-${roomId}`, bio = `ours-cowork mission room ${roomId}`) {
4969
- validateRoomId(roomId);
4970
- if (this.packets.has(roomId)) throw new Error(`room packet "${roomId}" is already hosted`);
4971
- const liveDir = this.liveDir(roomId);
4972
- if (this.hasRestorableState(roomId)) {
4973
- try {
4974
- return await this.restore(roomId, void 0, identityName, bio);
4975
- } catch (error) {
4976
- this.log(`[${this.packetName(roomId)}] pending packet restore failed; reprovisioning:`, error);
4977
- }
4978
- }
4979
- this.prepareProvisioningDirectory(roomId);
4980
- let native;
4981
- try {
4982
- native = await this.host.createPacket(this.packetName(roomId), this.seed());
4983
- let room;
4984
- room = new HostedRoomPacket(
4985
- native,
4986
- () => this.saveState(native, liveDir),
4987
- this.log,
4988
- (event) => this.onNotify(roomId, event),
4989
- () => {
4990
- if (this.packets.get(roomId) === room) this.packets.delete(roomId);
4991
- }
4992
- );
4993
- atomicWriteFileSync(this.identityPath(roomId), Buffer.from(exportSigningSecret(native), "utf8"), this.persistence);
4994
- this.provisioningCheckpoint("identity");
4995
- this.saveState(native, liveDir);
4996
- this.provisioningCheckpoint("state");
4997
- this.packets.set(roomId, room);
4998
- await room.setIdentity(identityName, bio);
4999
- this.provisioningCheckpoint("identity_applied");
5000
- return room;
5001
- } catch (error) {
5002
- if (native) {
5003
- try {
5004
- this.host.removePacket(native.cid);
5005
- } catch {
5006
- }
5007
- }
5008
- this.packets.delete(roomId);
5009
- throw error;
5010
- }
5081
+ if (invite.state === "receipt_pending" && invite.accepted_cids.length > 0) {
5082
+ context.addIssue({
5083
+ code: external_exports.ZodIssueCode.custom,
5084
+ path: ["accepted_cids"],
5085
+ message: "receipt_pending invites cannot have accepted CIDs"
5086
+ });
5011
5087
  }
5012
- async restore(roomId, expectedCid, identityName, bio) {
5013
- validateRoomId(roomId);
5014
- if (this.packets.has(roomId)) throw new Error(`room packet "${roomId}" is already hosted`);
5015
- const liveDir = this.liveDir(roomId);
5016
- const secret = this.fs.readFileSync(this.identityPath(roomId), "utf8").trim();
5017
- if (!/^[0-9a-f]+$/i.test(secret) || secret.length % 2 !== 0) {
5018
- throw new Error(`invalid signing secret for room "${roomId}"`);
5019
- }
5020
- const stateBytes = this.fs.readFileSync(this.statePath(roomId));
5021
- if (stateBytes.length === 0) throw new Error(`empty packet state for room "${roomId}"`);
5022
- const native = await this.host.createPacket(
5023
- this.packetName(roomId),
5024
- this.seed(),
5025
- secret,
5026
- { deferredExposure: true }
5027
- );
5028
- if (expectedCid !== void 0 && native.cid !== expectedCid) {
5029
- try {
5030
- this.host.removePacket(native.cid);
5031
- } catch {
5032
- }
5033
- throw new Error(
5034
- `restored room packet CID mismatch for "${roomId}": expected "${expectedCid}", found "${native.cid}"`
5035
- );
5036
- }
5037
- let room;
5038
- room = new HostedRoomPacket(
5039
- native,
5040
- () => this.saveState(native, liveDir),
5041
- this.log,
5042
- (event) => this.onNotify(roomId, event),
5043
- () => {
5044
- if (this.packets.get(roomId) === room) this.packets.delete(roomId);
5045
- }
5046
- );
5047
- try {
5048
- await withScopeAsync(async (lifetime) => {
5049
- const state = native.pw.packet.ParseValue(new Uint8Array(stateBytes)).Attach(lifetime);
5050
- await native.mutatingTx("::actor::import_state", state, lifetime);
5051
- });
5052
- native.pw.refresh_identity_proof_document();
5053
- if (identityName !== void 0 && bio !== void 0) {
5054
- await room.setIdentity(identityName, bio);
5055
- this.provisioningCheckpoint("identity_applied");
5056
- }
5057
- atomicWriteFileSync(
5058
- this.ownershipPath(roomId),
5059
- Buffer.from(`${roomId}
5060
- `, "utf8"),
5061
- this.persistence
5062
- );
5063
- await this.beforeExpose(room);
5064
- this.host.exposePacket(native.cid);
5065
- this.packets.set(roomId, room);
5066
- return room;
5067
- } catch (error) {
5068
- try {
5069
- this.host.removePacket(native.cid);
5070
- } catch {
5071
- }
5072
- throw error;
5073
- }
5074
- }
5075
- async destroy(roomId) {
5076
- validateRoomId(roomId);
5077
- const room = this.packets.get(roomId);
5078
- if (room) {
5079
- this.host.removePacket(room.cid);
5080
- this.packets.delete(roomId);
5088
+ });
5089
+ MissionV1Schema = external_exports.object({
5090
+ goal: MissionTextSchema,
5091
+ briefing: MissionTextSchema
5092
+ }).strict();
5093
+ MissionSchema = external_exports.object({
5094
+ goal: MissionTextSchema,
5095
+ briefing: MissionTextSchema,
5096
+ briefing_version: PositiveSafeIntegerSchema
5097
+ }).strict();
5098
+ RoleBriefingSchema = external_exports.object({
5099
+ text: MissionTextSchema,
5100
+ version: PositiveSafeIntegerSchema,
5101
+ updated_at: Rfc3339Schema
5102
+ }).strict();
5103
+ RoomCommonShape = {
5104
+ room_id: LowerCrockfordUlidSchema,
5105
+ identity_name: NonEmptyStringSchema,
5106
+ identity_cid: external_exports.string(),
5107
+ state: RoomStateSchema,
5108
+ status: NonEmptyStringSchema.optional(),
5109
+ invites: external_exports.array(RoomInviteSchema),
5110
+ created_at: Rfc3339Schema,
5111
+ activated_at: Rfc3339Schema.optional(),
5112
+ closed_at: Rfc3339Schema.optional()
5113
+ };
5114
+ RoomV1Schema = external_exports.object({
5115
+ ...RoomCommonShape,
5116
+ version: external_exports.literal(1),
5117
+ mission: MissionV1Schema,
5118
+ seats: external_exports.array(SeatV1Schema)
5119
+ }).strict().superRefine(refineRoomLineage);
5120
+ CurrentRoomSchema = external_exports.object({
5121
+ ...RoomCommonShape,
5122
+ room_name: RoomNameSchema,
5123
+ version: external_exports.literal(2),
5124
+ mission: MissionSchema,
5125
+ role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
5126
+ anonymous: external_exports.boolean(),
5127
+ quiet_membership: external_exports.boolean(),
5128
+ membership_epoch: external_exports.number().int().nonnegative().safe(),
5129
+ seats: external_exports.array(SeatSchema)
5130
+ }).strict().superRefine((room, context) => {
5131
+ refineRoomLineage(room, context);
5132
+ const byParticipant = /* @__PURE__ */ new Map();
5133
+ const activeAliases = /* @__PURE__ */ new Set();
5134
+ for (const [index, seat] of room.seats.entries()) {
5135
+ if (byParticipant.has(seat.participant_id)) {
5136
+ context.addIssue({
5137
+ code: external_exports.ZodIssueCode.custom,
5138
+ path: ["seats", index, "participant_id"],
5139
+ message: "participant_id must be unique within the room"
5140
+ });
5081
5141
  }
5082
- const liveDir = this.liveDir(roomId);
5083
- let removalFailure;
5084
- try {
5085
- this.assertSafeRoomDirectory(roomId);
5086
- this.fs.rmSync(liveDir, { recursive: true, force: true });
5087
- this.fsyncDirectory(this.roomDir(roomId));
5088
- } catch (error) {
5089
- this.log(`[${room?.name ?? this.packetName(roomId)}] live-state removal failed:`, error);
5090
- removalFailure = error;
5142
+ byParticipant.set(seat.participant_id, seat);
5143
+ if (room.anonymous && seat.alias === void 0) {
5144
+ context.addIssue({
5145
+ code: external_exports.ZodIssueCode.custom,
5146
+ path: ["seats", index, "alias"],
5147
+ message: "anonymous rooms require an alias on every seat"
5148
+ });
5091
5149
  }
5092
- const residue = this.residue(roomId);
5093
- if (removalFailure !== void 0 && residue.length === 0) {
5094
- throw new PacketPersistenceError(
5095
- `live-state removal durability is uncertain for room "${roomId}"`,
5096
- { cause: removalFailure }
5097
- );
5150
+ if (!room.anonymous && seat.alias !== void 0) {
5151
+ context.addIssue({
5152
+ code: external_exports.ZodIssueCode.custom,
5153
+ path: ["seats", index, "alias"],
5154
+ message: "aliases are reserved for anonymous rooms"
5155
+ });
5098
5156
  }
5099
- return residue;
5100
- }
5101
- /** Unhost runtime packets while retaining every byte required for restart. */
5102
- async unhostAll() {
5103
- const errors = [];
5104
- for (const [roomId, room] of [...this.packets]) {
5105
- try {
5106
- this.host.removePacket(room.cid, new Error("cowork daemon is shutting down"));
5107
- this.packets.delete(roomId);
5108
- } catch (error) {
5109
- errors.push(error);
5157
+ if (seat.state === "active" && seat.alias !== void 0) {
5158
+ if (activeAliases.has(seat.alias)) {
5159
+ context.addIssue({
5160
+ code: external_exports.ZodIssueCode.custom,
5161
+ path: ["seats", index, "alias"],
5162
+ message: "active seats must hold distinct aliases"
5163
+ });
5110
5164
  }
5165
+ activeAliases.add(seat.alias);
5111
5166
  }
5112
- if (errors.length > 0) throw new AggregateError(errors, "failed to unhost room packets");
5113
- }
5114
- saveState(packet, liveDir) {
5115
- try {
5116
- const bytes = withScope((lifetime) => Buffer.from(packet.readonlyTx("::actor::export_state", lifetime).Serialize()));
5117
- atomicWriteFileSync(join3(liveDir, "state_data.bin"), bytes, this.persistence);
5118
- } catch (error) {
5119
- if (error instanceof PacketPersistenceError) throw error;
5120
- throw new PacketPersistenceError(`failed to export state for packet "${packet.name}"`, { cause: error });
5121
- }
5122
- }
5123
- residue(roomId) {
5124
- const liveDir = this.liveDir(roomId);
5125
- try {
5126
- this.fs.lstatSync(liveDir);
5127
- return [liveDir];
5128
- } catch (error) {
5129
- if (error.code === "ENOENT") return [];
5130
- throw error;
5131
- }
5132
- }
5133
- packetName(roomId) {
5134
- return `cowork-room-${roomId}`;
5135
- }
5136
- roomDir(roomId) {
5137
- return join3(this.stateDir, "rooms", roomId);
5138
- }
5139
- liveDir(roomId) {
5140
- return join3(this.roomDir(roomId), "live");
5141
- }
5142
- identityPath(roomId) {
5143
- return join3(this.liveDir(roomId), "identity.key");
5144
- }
5145
- statePath(roomId) {
5146
- return join3(this.liveDir(roomId), "state_data.bin");
5147
- }
5148
- ownershipPath(roomId) {
5149
- return join3(this.liveDir(roomId), ".cowork-provisioning-v1");
5150
- }
5151
- stagingJournalPath(roomId) {
5152
- return join3(this.roomDir(roomId), ".cowork-provisioning-stage");
5153
- }
5154
- residuePath(roomId) {
5155
- return join3(this.roomDir(roomId), "provisioning-residue");
5156
- }
5157
- hasRestorableState(roomId) {
5158
- try {
5159
- const secret = this.fs.readFileSync(this.identityPath(roomId), "utf8").trim();
5160
- return /^[0-9a-f]+$/i.test(secret) && secret.length % 2 === 0 && this.fs.readFileSync(this.statePath(roomId)).length > 0;
5161
- } catch {
5162
- return false;
5167
+ if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
5168
+ context.addIssue({
5169
+ code: external_exports.ZodIssueCode.custom,
5170
+ path: ["seats", index, "removed_epoch"],
5171
+ message: "removed_epoch cannot exceed the room membership_epoch"
5172
+ });
5163
5173
  }
5164
5174
  }
5165
- prepareProvisioningDirectory(roomId) {
5166
- const roomDir = this.roomDir(roomId);
5167
- const liveDir = this.liveDir(roomId);
5168
- if (!this.fs.existsSync(roomDir)) {
5169
- this.fs.mkdirSync(roomDir, { recursive: true, mode: 448 });
5170
- this.fs.chmodSync(roomDir, 448);
5171
- }
5172
- if (this.fs.existsSync(liveDir)) {
5173
- let owned = false;
5174
- try {
5175
- owned = this.fs.readFileSync(this.ownershipPath(roomId), "utf8") === `${roomId}
5176
- `;
5177
- } catch {
5178
- }
5179
- if (owned) {
5180
- this.assertSafeRoomDirectory(roomId);
5181
- this.fs.rmSync(liveDir, { recursive: true, force: true });
5182
- this.fsyncDirectory(roomDir);
5183
- } else {
5184
- this.assertSafeRoomDirectory(roomId);
5185
- const residue = this.residuePath(roomId);
5186
- if (this.fs.existsSync(residue)) {
5187
- throw new PacketPersistenceError(
5188
- `room "${roomId}" has unknown live state and an existing provisioning residue; inspect both before retrying`
5189
- );
5190
- }
5191
- this.fs.renameSync(liveDir, residue);
5192
- this.fs.chmodSync(residue, 448);
5193
- this.fsyncDirectory(roomDir);
5194
- }
5175
+ for (const [index, seat] of room.seats.entries()) {
5176
+ if (seat.replaces_seat === void 0) continue;
5177
+ const predecessor = byParticipant.get(seat.replaces_seat);
5178
+ if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
5179
+ context.addIssue({
5180
+ code: external_exports.ZodIssueCode.custom,
5181
+ path: ["seats", index, "replaces_seat"],
5182
+ message: "replaces_seat must reference a removed seat in this room"
5183
+ });
5184
+ continue;
5195
5185
  }
5196
- this.cleanupOwnedStaging(roomId);
5197
- const stagingName = this.stagingName();
5198
- if (!/^live\.staging-[0-9a-f]{32}$/.test(stagingName)) {
5199
- throw new PacketPersistenceError(`invalid provisioning staging name for room "${roomId}"`);
5186
+ if (predecessor.role !== seat.role) {
5187
+ context.addIssue({
5188
+ code: external_exports.ZodIssueCode.custom,
5189
+ path: ["seats", index, "role"],
5190
+ message: "a replacement seat must inherit the predecessor role"
5191
+ });
5200
5192
  }
5201
- this.createStagingJournal(roomId, stagingName);
5202
- const stagingDir = join3(roomDir, stagingName);
5203
- try {
5204
- this.fs.mkdirSync(stagingDir, { recursive: false, mode: 448 });
5205
- } catch (error) {
5206
- try {
5207
- this.fs.unlinkSync(this.stagingJournalPath(roomId));
5208
- this.fsyncDirectory(roomDir);
5209
- } catch (cleanupError) {
5210
- throw new AggregateError([error, cleanupError], `provisioning staging collision cleanup failed for room "${roomId}"`);
5211
- }
5212
- throw new PacketPersistenceError(`provisioning staging collision for room "${roomId}"`, { cause: error });
5193
+ if (room.anonymous && seat.alias !== predecessor.alias) {
5194
+ context.addIssue({
5195
+ code: external_exports.ZodIssueCode.custom,
5196
+ path: ["seats", index, "alias"],
5197
+ message: "an anonymous replacement seat must inherit the predecessor alias"
5198
+ });
5213
5199
  }
5214
- this.fs.chmodSync(stagingDir, 448);
5215
- atomicWriteFileSync(
5216
- join3(stagingDir, ".cowork-provisioning-v1"),
5217
- Buffer.from(`${roomId}
5218
- `, "utf8"),
5219
- this.persistence
5220
- );
5221
- this.provisioningCheckpoint("mkdir");
5222
- this.fs.renameSync(stagingDir, liveDir);
5223
- this.fsyncDirectory(roomDir);
5224
- this.fs.unlinkSync(this.stagingJournalPath(roomId));
5225
- this.fsyncDirectory(roomDir);
5226
5200
  }
5227
- cleanupOwnedStaging(roomId) {
5228
- const journal = this.stagingJournalPath(roomId);
5229
- let journalFd;
5230
- try {
5231
- try {
5232
- journalFd = this.fs.openSync(
5233
- journal,
5234
- nodeFs2.constants.O_RDONLY | (nodeFs2.constants.O_NOFOLLOW ?? 0)
5235
- );
5236
- } catch (error) {
5237
- if (error.code === "ENOENT") return;
5238
- throw error;
5239
- }
5240
- const journalStat = this.fs.fstatSync(journalFd);
5241
- if (!journalStat.isFile() || journalStat.size > 128) {
5242
- throw new PacketPersistenceError(`unsafe provisioning staging journal for room "${roomId}"`);
5243
- }
5244
- const stagingName = this.fs.readFileSync(journalFd, "utf8").trim();
5245
- if (!/^live\.staging-[0-9a-f]{32}$/.test(stagingName)) {
5246
- throw new PacketPersistenceError(`invalid provisioning staging journal for room "${roomId}"`);
5247
- }
5248
- const roomDir = this.roomDir(roomId);
5249
- const stagingDir = join3(roomDir, stagingName);
5250
- if (this.fs.existsSync(stagingDir)) {
5251
- const stat = this.fs.lstatSync(stagingDir);
5252
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
5253
- throw new PacketPersistenceError(`unsafe provisioning staging path for room "${roomId}"`);
5254
- }
5255
- let owned = false;
5256
- try {
5257
- owned = this.fs.readFileSync(join3(stagingDir, ".cowork-provisioning-v1"), "utf8") === `${roomId}
5258
- `;
5259
- } catch {
5260
- }
5261
- if (owned) {
5262
- this.fs.rmSync(stagingDir, { recursive: true, force: true });
5263
- this.fsyncDirectory(roomDir);
5264
- } else {
5265
- const residue = this.residuePath(roomId);
5266
- if (this.fs.existsSync(residue)) {
5267
- throw new PacketPersistenceError(
5268
- `provisioning staging recovery for room "${roomId}" found an existing provisioning residue; preserving both`
5269
- );
5270
- }
5271
- this.fs.renameSync(stagingDir, residue);
5272
- this.fs.chmodSync(residue, 448);
5273
- this.fsyncDirectory(roomDir);
5274
- }
5275
- }
5276
- const currentJournal = this.fs.lstatSync(journal);
5277
- if (!currentJournal.isFile() || currentJournal.dev !== journalStat.dev || currentJournal.ino !== journalStat.ino) {
5278
- throw new PacketPersistenceError(
5279
- `provisioning staging journal changed during recovery for room "${roomId}"; replacement preserved`
5280
- );
5201
+ });
5202
+ RoomSchema = external_exports.preprocess((value) => {
5203
+ if (typeof value !== "object" || value === null || Object.hasOwn(value, "room_name")) return value;
5204
+ const roomId = value.room_id;
5205
+ if (typeof roomId !== "string" || !LowerCrockfordUlidSchema.safeParse(roomId).success) return value;
5206
+ return { ...value, room_name: defaultRoomName(roomId) };
5207
+ }, CurrentRoomSchema);
5208
+ CreateRoomInputSchema = external_exports.object({
5209
+ name: RoomNameSchema.optional(),
5210
+ goal: MissionTextSchema,
5211
+ briefing: MissionTextSchema,
5212
+ anonymous: external_exports.boolean().optional(),
5213
+ quiet_membership: external_exports.boolean().optional()
5214
+ }).strict();
5215
+ UpdateRoomInputSchema = external_exports.object({
5216
+ name: RoomNameSchema.optional(),
5217
+ goal: MissionTextSchema.optional(),
5218
+ briefing: MissionTextSchema.optional(),
5219
+ status: NonEmptyStringSchema.optional(),
5220
+ quiet_membership: external_exports.boolean().optional()
5221
+ }).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
5222
+ RoleBriefingSetInputSchema = external_exports.object({
5223
+ role: RoleSchema,
5224
+ text: MissionTextSchema
5225
+ }).strict();
5226
+ RoleBriefingDeleteInputSchema = external_exports.object({
5227
+ role: RoleSchema
5228
+ }).strict();
5229
+ PostMessageInputSchema = external_exports.object({
5230
+ text: MessageTextSchema
5231
+ }).strict();
5232
+ AuthorSnapshotSchema = external_exports.object({
5233
+ identity: NonEmptyStringSchema,
5234
+ display_name: NonEmptyStringSchema,
5235
+ role: RoleSchema
5236
+ }).strict();
5237
+ RecordCommonShape = {
5238
+ version: external_exports.literal(1),
5239
+ room_id: LowerCrockfordUlidSchema,
5240
+ seq: PositiveSafeIntegerSchema,
5241
+ record_id: NonEmptyStringSchema,
5242
+ at: Rfc3339Schema
5243
+ };
5244
+ AppendCommonShape = {
5245
+ version: external_exports.literal(1),
5246
+ room_id: LowerCrockfordUlidSchema,
5247
+ at: Rfc3339Schema
5248
+ };
5249
+ MembershipNoticeSchema = external_exports.object({
5250
+ action: external_exports.enum(["remove"]),
5251
+ alias: NonEmptyStringSchema.optional(),
5252
+ role: RoleSchema.optional(),
5253
+ epoch: external_exports.number().int().nonnegative().safe()
5254
+ }).strict();
5255
+ AuthorAliasSchema = external_exports.object({
5256
+ participant_id: LowerCrockfordUlidSchema,
5257
+ alias: NonEmptyStringSchema
5258
+ }).strict();
5259
+ MessageShape = {
5260
+ kind: external_exports.literal("message"),
5261
+ message_id: LowerCrockfordUlidSchema,
5262
+ author: AuthorSnapshotSchema,
5263
+ author_alias: AuthorAliasSchema.optional(),
5264
+ category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
5265
+ briefing_role: RoleSchema.optional(),
5266
+ briefing_version: PositiveSafeIntegerSchema.optional(),
5267
+ membership: MembershipNoticeSchema.optional(),
5268
+ text: MessageTextSchema,
5269
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5270
+ const seen = /* @__PURE__ */ new Set();
5271
+ for (const [index, identity] of identities.entries()) {
5272
+ if (seen.has(identity)) {
5273
+ context.addIssue({
5274
+ code: external_exports.ZodIssueCode.custom,
5275
+ path: [index],
5276
+ message: "recipient identities must be unique"
5277
+ });
5281
5278
  }
5282
- this.fs.unlinkSync(journal);
5283
- this.fsyncDirectory(roomDir);
5284
- } finally {
5285
- if (journalFd !== void 0) this.fs.closeSync(journalFd);
5279
+ seen.add(identity);
5286
5280
  }
5287
- }
5288
- createStagingJournal(roomId, stagingName) {
5289
- const journal = this.stagingJournalPath(roomId);
5290
- const bytes = Buffer.from(`${stagingName}
5291
- `, "utf8");
5292
- let fd;
5293
- let created = false;
5294
- try {
5295
- fd = this.persistence.openSync(
5296
- journal,
5297
- nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | (nodeFs2.constants.O_NOFOLLOW ?? 0),
5298
- 384
5299
- );
5300
- created = true;
5301
- this.persistence.fchmodSync(fd, 384);
5302
- let offset = 0;
5303
- while (offset < bytes.byteLength) {
5304
- const written = this.persistence.writeSync(fd, bytes, offset, bytes.byteLength - offset, null);
5305
- if (written <= 0) throw new Error(`short write while persisting ${journal}`);
5306
- offset += written;
5307
- }
5308
- this.persistence.fsyncSync(fd);
5309
- this.persistence.closeSync(fd);
5310
- fd = void 0;
5311
- this.fsyncDirectory(this.roomDir(roomId));
5312
- } catch (error) {
5313
- if (fd !== void 0) {
5314
- try {
5315
- this.persistence.closeSync(fd);
5316
- } catch {
5317
- }
5318
- }
5319
- if (created) {
5320
- try {
5321
- this.fs.unlinkSync(journal);
5322
- this.fsyncDirectory(this.roomDir(roomId));
5323
- } catch {
5324
- }
5281
+ }),
5282
+ source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5283
+ source_wire_id: NonEmptyStringSchema.optional()
5284
+ };
5285
+ RelayIntentShape = {
5286
+ kind: external_exports.literal("relay_intent"),
5287
+ message_id: LowerCrockfordUlidSchema.optional(),
5288
+ file_id: LowerCrockfordUlidSchema.optional(),
5289
+ recipient_identity: NonEmptyStringSchema
5290
+ };
5291
+ RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
5292
+ RelayResultShape = {
5293
+ kind: external_exports.literal("relay_result"),
5294
+ intent_record_id: NonEmptyStringSchema,
5295
+ message_id: LowerCrockfordUlidSchema.optional(),
5296
+ file_id: LowerCrockfordUlidSchema.optional(),
5297
+ recipient_identity: NonEmptyStringSchema,
5298
+ status: RelayResultStatusSchema,
5299
+ wire_id: NonEmptyStringSchema.optional(),
5300
+ metadata_wire_id: NonEmptyStringSchema.optional()
5301
+ };
5302
+ FileShape = {
5303
+ kind: external_exports.literal("file"),
5304
+ file_id: LowerCrockfordUlidSchema,
5305
+ author: AuthorSnapshotSchema,
5306
+ author_alias: AuthorAliasSchema.optional(),
5307
+ filename: FileNameSchema,
5308
+ mime: FileMimeSchema,
5309
+ size: external_exports.number().int().nonnegative().max(MAX_FILE_BYTES),
5310
+ sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
5311
+ data_base64: external_exports.string(),
5312
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5313
+ const seen = /* @__PURE__ */ new Set();
5314
+ for (const [index, identity] of identities.entries()) {
5315
+ if (seen.has(identity)) {
5316
+ context.addIssue({
5317
+ code: external_exports.ZodIssueCode.custom,
5318
+ path: [index],
5319
+ message: "recipient identities must be unique"
5320
+ });
5325
5321
  }
5326
- throw new PacketPersistenceError(`failed to establish staging ownership for room "${roomId}"`, { cause: error });
5327
- }
5328
- }
5329
- assertSafeRoomDirectory(roomId) {
5330
- const roomDir = this.roomDir(roomId);
5331
- const stat = this.fs.lstatSync(roomDir);
5332
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
5333
- throw new Error(`room directory for "${roomId}" is not a safe directory`);
5334
- }
5335
- }
5336
- fsyncDirectory(path) {
5337
- let fd;
5338
- try {
5339
- fd = this.fs.openSync(path, nodeFs2.constants.O_RDONLY | (nodeFs2.constants.O_NOFOLLOW ?? 0));
5340
- this.fs.fsyncSync(fd);
5341
- } finally {
5342
- if (fd !== void 0) this.fs.closeSync(fd);
5322
+ seen.add(identity);
5343
5323
  }
5344
- }
5324
+ }),
5325
+ source_file_id: external_exports.number().int().nonnegative().safe(),
5326
+ source_wire_id: NonEmptyStringSchema.optional()
5345
5327
  };
5346
- HostedRoomPacket = class {
5347
- name;
5348
- cid;
5349
- packet;
5350
- constructor(packet, saveState, log, onNotify = () => {
5351
- }, onTerminal = () => {
5352
- }) {
5353
- this.packet = packet;
5354
- this.name = packet.name;
5355
- this.cid = packet.cid;
5356
- wireHandlers(packet, { onSaveState: saveState, onNotify: (event) => onNotify(event) }, log);
5357
- packet.onTerminalClose?.(onTerminal);
5358
- }
5359
- async setIdentity(identityName, bio) {
5360
- await withScopeAsync((lifetime) => this.packet.mutatingTx("::a2a_messaging::set_my_name", { name: identityName }, lifetime));
5361
- await withScopeAsync((lifetime) => this.packet.mutatingTx("::a2a_messaging::set_my_bio", { bio }, lifetime));
5362
- }
5363
- async mintInvite(mode) {
5364
- return withScopeAsync(async (lifetime) => {
5365
- const result = await this.packet.mutatingTx("::a2a_messaging::generate_invite", { mode }, lifetime);
5366
- return {
5367
- blob: packInvite(Buffer.from(result.Reduce("invite").GetBinary())),
5368
- invite_id: result.Reduce("invite_id").Visualize(),
5369
- reusable: booleanValue(result.Reduce("reusable"))
5370
- };
5371
- });
5372
- }
5373
- async revokeInvite(inviteId) {
5374
- return withScopeAsync(async (lifetime) => {
5375
- const result = await this.packet.mutatingTx("::a2a_messaging::revoke_invite", { invite_id: inviteId }, lifetime);
5376
- return { revoked: booleanValue(result.Reduce("revoked")) };
5377
- });
5378
- }
5379
- listInvites() {
5380
- return withScope((lifetime) => {
5381
- const value = this.packet.readonlyTx("::a2a_messaging::list_invites", lifetime);
5382
- return dictionaryEntries(value).map(([inviteId, invite]) => ({
5383
- invite_id: inviteId,
5384
- mode: inviteMode(invite.Reduce("mode").Visualize())
5385
- }));
5386
- });
5387
- }
5388
- listContacts() {
5389
- return withScope((lifetime) => {
5390
- const value = this.packet.readonlyTx("::a2a_messaging::list_contacts", lifetime);
5391
- return dictionaryEntries(value).map(([, contact]) => ({
5392
- name: contact.Reduce("name").Visualize(),
5393
- container_id: contact.Reduce("container_id").Visualize()
5394
- }));
5395
- });
5396
- }
5397
- listContactOrigins() {
5398
- return withScope((lifetime) => {
5399
- const value = this.packet.readonlyTx("::a2a_messaging::list_contact_origins", lifetime);
5400
- return Object.fromEntries(dictionaryEntries(value).map(([cid, origin]) => [cid, {
5401
- via: origin.Reduce("via").Visualize(),
5402
- invite_id: nilString(origin.Reduce("invite_id")),
5403
- at: adaptTimeToRfc3339(origin.Reduce("at").Visualize())
5404
- }]));
5405
- });
5406
- }
5407
- peekInbox() {
5408
- return withScope((lifetime) => renderInbox(this.packet.readonlyTx("::actor::list_incoming_messages", lifetime)).filter((message) => message.status === "unread").map(({ status: _status, ...message }) => message));
5409
- }
5410
- async consumeInbox(expectedIds) {
5411
- return withScopeAsync(async (lifetime) => {
5412
- const result = await this.packet.mutatingTx(
5413
- "::actor::consume_messages",
5414
- { expected_ids: expectedIds },
5415
- lifetime
5416
- );
5417
- return {
5418
- consumed: renderIntegerArray(result.Reduce("consumed")),
5419
- deferred: renderIntegerArray(result.Reduce("deferred"))
5420
- };
5421
- });
5422
- }
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
- });
5433
- }
5434
- async removeContact(contactCid) {
5435
- return withScopeAsync(async (lifetime) => {
5436
- const result = await this.packet.mutatingTx(
5437
- "::a2a_messaging::remove_contact",
5438
- { contact: contactCid },
5439
- lifetime
5440
- );
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
- return {
5450
- status: notified ? "queued" : "send_failed",
5451
- notified,
5452
- key_material_retained: true
5453
- };
5454
- });
5455
- }
5456
- async sign(canonicalJson2) {
5457
- return withScopeAsync(async (lifetime) => {
5458
- 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");
5328
+ MembershipIntentShape = {
5329
+ kind: external_exports.literal("membership_intent"),
5330
+ action: external_exports.enum(["remove"]),
5331
+ participant_id: LowerCrockfordUlidSchema,
5332
+ recipient_identity: NonEmptyStringSchema,
5333
+ role: RoleSchema,
5334
+ alias: NonEmptyStringSchema.optional(),
5335
+ epoch: PositiveSafeIntegerSchema,
5336
+ notify: external_exports.boolean()
5337
+ };
5338
+ MembershipResultShape = {
5339
+ kind: external_exports.literal("membership_result"),
5340
+ intent_record_id: NonEmptyStringSchema,
5341
+ participant_id: LowerCrockfordUlidSchema,
5342
+ status: RelayStatusSchema,
5343
+ notified: external_exports.boolean(),
5344
+ key_material_retained: external_exports.literal(true),
5345
+ uncertain_after_restart: external_exports.literal(true).optional()
5346
+ };
5347
+ CloseNoticeIntentShape = {
5348
+ kind: external_exports.literal("close_notice_intent"),
5349
+ recipient_identity: NonEmptyStringSchema
5350
+ };
5351
+ CloseNoticeResultShape = {
5352
+ kind: external_exports.literal("close_notice_result"),
5353
+ intent_record_id: NonEmptyStringSchema,
5354
+ recipient_identity: NonEmptyStringSchema,
5355
+ status: RelayStatusSchema,
5356
+ notified: external_exports.boolean(),
5357
+ key_material_retained: external_exports.literal(true),
5358
+ uncertain_after_restart: external_exports.literal(true).optional()
5359
+ };
5360
+ MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
5361
+ FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
5362
+ RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5363
+ RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5364
+ MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
5365
+ MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
5366
+ CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
5367
+ CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
5368
+ RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5369
+ MessageRecordSchema,
5370
+ FileRecordSchema,
5371
+ RelayIntentRecordSchema,
5372
+ RelayResultRecordSchema,
5373
+ MembershipIntentRecordSchema,
5374
+ MembershipResultRecordSchema,
5375
+ CloseNoticeIntentRecordSchema,
5376
+ CloseNoticeResultRecordSchema
5377
+ ]);
5378
+ CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5379
+ if (record.record_id !== `${record.room_id}:${record.seq}`) {
5380
+ context.addIssue({
5381
+ code: external_exports.ZodIssueCode.custom,
5382
+ path: ["record_id"],
5383
+ message: 'record_id must equal room_id + ":" + seq'
5464
5384
  });
5465
5385
  }
5466
- };
5386
+ if (record.kind === "message") refineMessageCategory(record, context);
5387
+ refineRelaySubject(record, context);
5388
+ refineFileRecord(record, context);
5389
+ });
5390
+ AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5391
+ external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
5392
+ external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
5393
+ external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5394
+ external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5395
+ external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
5396
+ external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
5397
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
5398
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
5399
+ ]).superRefine((record, context) => {
5400
+ if (record.kind === "message") refineMessageCategory(record, context);
5401
+ refineRelaySubject(record, context);
5402
+ refineFileRecord(record, context);
5403
+ });
5467
5404
  }
5468
5405
  });
5469
5406
 
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];
5407
+ // src/packets.ts
5408
+ import { randomBytes } from "node:crypto";
5409
+ import * as nodeFs2 from "node:fs";
5410
+ import { dirname as dirname3, join as join3 } from "node:path";
5411
+ function atomicWriteFileSync(target, bytes, ops = nodeFs2) {
5412
+ const temp = `${target}.tmp-${process.pid}-${temporarySequence++}`;
5413
+ let fileFd;
5414
+ let directoryFd;
5415
+ try {
5416
+ fileFd = ops.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_TRUNC | nodeFs2.constants.O_WRONLY, 384);
5417
+ ops.fchmodSync(fileFd, 384);
5418
+ let offset = 0;
5419
+ while (offset < bytes.byteLength) {
5420
+ const written = ops.writeSync(fileFd, bytes, offset, bytes.byteLength - offset, null);
5421
+ if (written <= 0) throw new Error(`short write while persisting ${target}`);
5422
+ offset += written;
5423
+ }
5424
+ ops.fsyncSync(fileFd);
5425
+ ops.closeSync(fileFd);
5426
+ fileFd = void 0;
5427
+ ops.renameSync(temp, target);
5428
+ ops.chmodSync(target, 384);
5429
+ directoryFd = ops.openSync(dirname3(target), nodeFs2.constants.O_RDONLY);
5430
+ ops.fsyncSync(directoryFd);
5431
+ ops.closeSync(directoryFd);
5432
+ directoryFd = void 0;
5433
+ } catch (error) {
5434
+ if (fileFd !== void 0) {
5435
+ try {
5436
+ ops.closeSync(fileFd);
5437
+ } catch {
5438
+ }
5439
+ }
5440
+ if (directoryFd !== void 0) {
5441
+ try {
5442
+ ops.closeSync(directoryFd);
5443
+ } catch {
5444
+ }
5445
+ }
5446
+ try {
5447
+ ops.rmSync(temp, { force: true });
5448
+ } catch {
5449
+ }
5450
+ throw new PacketPersistenceError(`failed to durably persist ${target}`, { cause: error });
5451
+ }
5494
5452
  }
5495
- function refineRoomLineage(room, context) {
5496
- const pendingIdentityName = `cowork-room-${room.room_id}`;
5497
- 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;
5498
- if (room.identity_cid === "" && !exactPacketPending) {
5499
- context.addIssue({
5500
- code: external_exports.ZodIssueCode.custom,
5501
- path: ["identity_cid"],
5502
- message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
5453
+ function renderInbox(value) {
5454
+ const output = [];
5455
+ if (value.IsNil()) return output;
5456
+ for (let index = 0; ; index += 1) {
5457
+ const message = value.Reduce(index);
5458
+ if (message.IsNil()) break;
5459
+ output.push({
5460
+ msg_id: Number(message.Reduce("msg_id").Visualize()),
5461
+ sender_id: message.Reduce("sender_id").Visualize(),
5462
+ sender_name: message.Reduce("sender_name").Visualize(),
5463
+ text: message.Reduce("text").Visualize(),
5464
+ date: adaptTimeToRfc3339(message.Reduce("date").Visualize()),
5465
+ status: message.Reduce("status").Visualize(),
5466
+ wire_id: message.Reduce("wire_id").Visualize()
5503
5467
  });
5504
5468
  }
5505
- if (room.identity_cid !== "" && room.status === "packet_pending") {
5506
- context.addIssue({
5507
- code: external_exports.ZodIssueCode.custom,
5508
- path: ["status"],
5509
- message: "packet_pending status requires an empty identity_cid"
5469
+ return output;
5470
+ }
5471
+ function renderFileInbox(value) {
5472
+ const output = [];
5473
+ if (value.IsNil()) return output;
5474
+ for (let index = 0; ; index += 1) {
5475
+ const file = value.Reduce(index);
5476
+ if (file.IsNil()) break;
5477
+ output.push({
5478
+ file_id: Number(file.Reduce("file_id").Visualize()),
5479
+ sender_id: file.Reduce("sender_id").Visualize(),
5480
+ sender_name: file.Reduce("sender_name").Visualize(),
5481
+ filename: file.Reduce("filename").Visualize(),
5482
+ mime: file.Reduce("mime").Visualize(),
5483
+ data: Buffer.from(file.Reduce("data").GetBinary()),
5484
+ date: adaptTimeToRfc3339(file.Reduce("date").Visualize()),
5485
+ status: file.Reduce("status").Visualize(),
5486
+ wire_id: file.Reduce("wire_id").Visualize()
5510
5487
  });
5511
5488
  }
5512
- const pendingByRecovery = /* @__PURE__ */ new Map();
5513
- for (const [index, invite] of room.invites.entries()) {
5514
- if (invite.recovery_of === void 0) continue;
5515
- const recoveryOf = invite.recovery_of;
5516
- const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
5517
- 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;
5518
- if (!source || source.invite_id === invite.invite_id || !validSourceState) {
5519
- context.addIssue({
5520
- code: external_exports.ZodIssueCode.custom,
5521
- path: ["invites", index, "recovery_of"],
5522
- message: "recovery_of must point to a source invite in the state required by this recovery lineage"
5523
- });
5524
- } else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
5525
- context.addIssue({
5526
- code: external_exports.ZodIssueCode.custom,
5527
- path: ["invites", index],
5528
- message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
5529
- });
5530
- }
5531
- if (invite.state === "receipt_pending") {
5532
- const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
5533
- pendingByRecovery.set(recoveryOf, count);
5534
- if (count > 1) {
5535
- context.addIssue({
5536
- code: external_exports.ZodIssueCode.custom,
5537
- path: ["invites", index, "recovery_of"],
5538
- message: "only one receipt_pending invite may exist per recovery_of pointer"
5539
- });
5540
- }
5541
- }
5489
+ return output;
5490
+ }
5491
+ function renderIntegerArray(value) {
5492
+ const output = [];
5493
+ if (value.IsNil()) return output;
5494
+ for (let index = 0; ; index += 1) {
5495
+ const item = value.Reduce(index);
5496
+ if (item.IsNil()) break;
5497
+ output.push(Number(item.Visualize()));
5542
5498
  }
5499
+ return output;
5543
5500
  }
5544
- function migrateRoomV1(room, mintParticipantId) {
5545
- return RoomSchema.parse({
5546
- ...room,
5547
- version: 2,
5548
- mission: { ...room.mission, briefing_version: 1 },
5549
- role_briefings: {},
5550
- anonymous: false,
5551
- quiet_membership: false,
5552
- membership_epoch: 0,
5553
- seats: room.seats.map((seat) => ({
5554
- ...seat,
5555
- participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
5556
- state: "active"
5557
- }))
5558
- });
5501
+ function dictionaryEntries(value) {
5502
+ if (value.IsNil()) return [];
5503
+ return value.GetKeys().map((key) => [key.Visualize(), value.Reduce(key)]);
5559
5504
  }
5560
- function refineMessageCategory(message, context) {
5561
- const requires = (field, present) => {
5562
- if (present && message[field] === void 0) {
5563
- context.addIssue({
5564
- code: external_exports.ZodIssueCode.custom,
5565
- path: [field],
5566
- message: `${message.category} messages require ${field}`
5567
- });
5568
- }
5569
- if (!present && message[field] !== void 0) {
5570
- context.addIssue({
5571
- code: external_exports.ZodIssueCode.custom,
5572
- path: [field],
5573
- message: `${field} is forbidden on ${message.category} messages`
5574
- });
5575
- }
5576
- };
5577
- requires("briefing_role", message.category === "role_briefing");
5578
- requires("membership", message.category === "membership");
5579
- if (message.category === "role_briefing" && message.briefing_version === void 0) {
5580
- context.addIssue({
5581
- code: external_exports.ZodIssueCode.custom,
5582
- path: ["briefing_version"],
5583
- message: "role_briefing messages require briefing_version"
5584
- });
5505
+ function booleanValue(value) {
5506
+ if (value.IsNil()) return false;
5507
+ try {
5508
+ return value.GetBoolean();
5509
+ } catch {
5510
+ return /true/i.test(value.Visualize());
5585
5511
  }
5586
- if (message.category === "chat" || message.category === "membership") {
5587
- if (message.briefing_version !== void 0) {
5588
- context.addIssue({
5589
- code: external_exports.ZodIssueCode.custom,
5590
- path: ["briefing_version"],
5591
- message: `briefing_version is forbidden on ${message.category} messages`
5592
- });
5593
- }
5512
+ }
5513
+ function strictBooleanValue(value, label) {
5514
+ if (value.IsNil()) throw new Error(`${label} must be a boolean`);
5515
+ let decoded;
5516
+ try {
5517
+ decoded = value.GetBoolean();
5518
+ } catch (error) {
5519
+ throw new Error(`${label} must be a boolean`, { cause: error });
5594
5520
  }
5521
+ if (typeof decoded !== "boolean") throw new Error(`${label} must be a boolean`);
5522
+ return decoded;
5595
5523
  }
5596
- var MAX_TEXT_BYTES, MAX_ROLE_BYTES, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoleSchema, MissionTextSchema, MessageTextSchema, 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, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
5597
- var init_contracts = __esm({
5598
- "src/contracts.ts"() {
5524
+ function nilString(value) {
5525
+ return value.IsNil() ? "" : value.Visualize();
5526
+ }
5527
+ function adaptTimeToRfc3339(value) {
5528
+ const rfc3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
5529
+ if (rfc3339) {
5530
+ const [, year2, month2, day2, hour2, minute2, second2, fraction2 = "", sign2, offsetHour2 = "0", offsetMinute = "0"] = rfc3339;
5531
+ if (sign2 === "-" && offsetHour2 === "00" && offsetMinute === "00") return invalidAdaptTime(value);
5532
+ return canonicalUtcTime(value, year2, month2, day2, hour2, minute2, second2, fraction2, sign2, offsetHour2, offsetMinute);
5533
+ }
5534
+ const native = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))? \(UTC(?:([+-])(0|[1-9]|1\d|2[0-3]))?\)$/.exec(value);
5535
+ if (!native) return invalidAdaptTime(value);
5536
+ const [, year, month, day, hour, minute, second, fraction = "", sign, offsetHour = "0"] = native;
5537
+ if (sign === "-" && offsetHour === "0") return invalidAdaptTime(value);
5538
+ return canonicalUtcTime(value, year, month, day, hour, minute, second, fraction, sign, offsetHour, "0");
5539
+ }
5540
+ function canonicalUtcTime(source, yearText, monthText, dayText, hourText, minuteText, secondText, fraction, offsetSign, offsetHourText, offsetMinuteText) {
5541
+ const year = Number(yearText);
5542
+ const month = Number(monthText);
5543
+ const day = Number(dayText);
5544
+ const hour = Number(hourText);
5545
+ const minute = Number(minuteText);
5546
+ const second = Number(secondText);
5547
+ const offsetHour = Number(offsetHourText);
5548
+ const offsetMinute = Number(offsetMinuteText);
5549
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
5550
+ const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
5551
+ if (month < 1 || month > 12 || day < 1 || day > monthDays[month - 1] || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) return invalidAdaptTime(source);
5552
+ const milliseconds = Number(fraction.slice(0, 3).padEnd(3, "0"));
5553
+ const local = /* @__PURE__ */ new Date(0);
5554
+ local.setUTCFullYear(year, month - 1, day);
5555
+ local.setUTCHours(hour, minute, second, milliseconds);
5556
+ if (local.getUTCFullYear() !== year || local.getUTCMonth() !== month - 1 || local.getUTCDate() !== day || local.getUTCHours() !== hour || local.getUTCMinutes() !== minute || local.getUTCSeconds() !== second) {
5557
+ return invalidAdaptTime(source);
5558
+ }
5559
+ const direction = offsetSign === "-" ? -1 : 1;
5560
+ const offsetMilliseconds = direction * (offsetHour * 60 + offsetMinute) * 6e4;
5561
+ const canonical = new Date(local.getTime() - offsetMilliseconds).toISOString();
5562
+ if (!/^\d{4}-/.test(canonical)) return invalidAdaptTime(source);
5563
+ return canonical;
5564
+ }
5565
+ function invalidAdaptTime(value) {
5566
+ throw new Error(`unexpected ADAPT time visualization: ${value}`);
5567
+ }
5568
+ function inviteMode(value) {
5569
+ const normalized = value.replace(/^\$/, "");
5570
+ if (normalized === "one_time" || normalized === "public") return normalized;
5571
+ throw new Error(`unexpected invite mode: ${value}`);
5572
+ }
5573
+ function exportSigningSecret(packet) {
5574
+ return withScope((lifetime) => Buffer.from(packet.readonlyTx("::actor::export_signing_secret", lifetime).Serialize()).toString("hex"));
5575
+ }
5576
+ function validateRoomId(roomId) {
5577
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(roomId)) throw new Error(`invalid room id: ${roomId}`);
5578
+ }
5579
+ var PacketPersistenceError, temporarySequence, PacketRegistry, HostedRoomPacket;
5580
+ var init_packets = __esm({
5581
+ async "src/packets.ts"() {
5599
5582
  "use strict";
5600
- init_zod();
5601
- MAX_TEXT_BYTES = 262144;
5602
- MAX_ROLE_BYTES = 256;
5603
- NonEmptyStringSchema = external_exports.string().min(1);
5604
- PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
5605
- LowerCrockfordUlidSchema = external_exports.string().regex(
5606
- /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
5607
- "must be a 26-character lowercase Crockford ULID"
5608
- );
5609
- Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
5610
- RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
5611
- MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
5612
- MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
5613
- RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
5614
- SeatStateSchema = external_exports.enum(["active", "removed"]);
5615
- InviteModeSchema = external_exports.enum(["one_time", "public"]);
5616
- DEFAULT_ROLE = "Participant";
5617
- InviteStateSchema = external_exports.enum([
5618
- "live",
5619
- "consumed",
5620
- "revoked",
5621
- "replacement_required",
5622
- "receipt_pending"
5623
- ]);
5624
- RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
5625
- SeatV1Schema = external_exports.object({
5626
- identity: NonEmptyStringSchema,
5627
- display_name: NonEmptyStringSchema,
5628
- role: RoleSchema,
5629
- invite_id: NonEmptyStringSchema,
5630
- accepted_at: Rfc3339Schema
5631
- }).strict();
5632
- SeatSchema = external_exports.object({
5633
- identity: NonEmptyStringSchema,
5634
- display_name: NonEmptyStringSchema,
5635
- role: RoleSchema,
5636
- invite_id: NonEmptyStringSchema,
5637
- accepted_at: Rfc3339Schema,
5638
- participant_id: LowerCrockfordUlidSchema,
5639
- state: SeatStateSchema,
5640
- alias: NonEmptyStringSchema.optional(),
5641
- removed_at: Rfc3339Schema.optional(),
5642
- removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
5643
- replaces_seat: LowerCrockfordUlidSchema.optional(),
5644
- bounced_at: Rfc3339Schema.optional()
5645
- }).strict().superRefine((seat, context) => {
5646
- if (seat.state === "removed") {
5647
- if (seat.removed_at === void 0) {
5648
- context.addIssue({
5649
- code: external_exports.ZodIssueCode.custom,
5650
- path: ["removed_at"],
5651
- message: "removed seats require removed_at"
5652
- });
5653
- }
5654
- if (seat.removed_epoch === void 0) {
5655
- context.addIssue({
5656
- code: external_exports.ZodIssueCode.custom,
5657
- path: ["removed_epoch"],
5658
- message: "removed seats require removed_epoch"
5659
- });
5660
- }
5661
- } else {
5662
- for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
5663
- if (seat[field] !== void 0) {
5664
- context.addIssue({
5665
- code: external_exports.ZodIssueCode.custom,
5666
- path: [field],
5667
- message: `${field} is reserved for removed seats`
5668
- });
5669
- }
5583
+ await init_adapt();
5584
+ init_contracts();
5585
+ PacketPersistenceError = class extends Error {
5586
+ constructor(message, options) {
5587
+ super(message, options);
5588
+ this.name = "PacketPersistenceError";
5589
+ }
5590
+ };
5591
+ temporarySequence = 0;
5592
+ PacketRegistry = class {
5593
+ packets = /* @__PURE__ */ new Map();
5594
+ host;
5595
+ stateDir;
5596
+ fs;
5597
+ persistence;
5598
+ log;
5599
+ seed;
5600
+ beforeExpose;
5601
+ onNotify;
5602
+ provisioningCheckpoint;
5603
+ stagingName;
5604
+ constructor(host, stateDir, options = {}) {
5605
+ this.host = host;
5606
+ this.stateDir = stateDir;
5607
+ this.fs = options.fs ?? nodeFs2;
5608
+ this.persistence = options.persistence ?? this.fs;
5609
+ this.log = options.log ?? (() => {
5610
+ });
5611
+ this.seed = options.seed ?? (() => randomBytes(24).toString("hex"));
5612
+ this.beforeExpose = options.beforeExpose ?? (() => {
5613
+ });
5614
+ this.onNotify = options.onNotify ?? (() => {
5615
+ });
5616
+ this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
5617
+ });
5618
+ this.stagingName = options.stagingName ?? (() => `live.staging-${randomBytes(16).toString("hex")}`);
5619
+ }
5620
+ get size() {
5621
+ return this.packets.size;
5622
+ }
5623
+ get(roomId) {
5624
+ return this.packets.get(roomId);
5625
+ }
5626
+ async create(roomId, identityName = `cowork-room-${roomId}`, bio = `ours-cowork mission room ${roomId}`) {
5627
+ validateRoomId(roomId);
5628
+ if (this.packets.has(roomId)) throw new Error(`room packet "${roomId}" is already hosted`);
5629
+ const liveDir = this.liveDir(roomId);
5630
+ if (this.hasRestorableState(roomId)) {
5631
+ try {
5632
+ return await this.restore(roomId, void 0, identityName, bio);
5633
+ } catch (error) {
5634
+ this.log(`[${this.packetName(roomId)}] pending packet restore failed; reprovisioning:`, error);
5635
+ }
5636
+ }
5637
+ this.prepareProvisioningDirectory(roomId);
5638
+ let native;
5639
+ try {
5640
+ native = await this.host.createPacket(this.packetName(roomId), this.seed());
5641
+ let room;
5642
+ room = new HostedRoomPacket(
5643
+ native,
5644
+ () => this.saveState(native, liveDir),
5645
+ this.log,
5646
+ (event) => this.onNotify(roomId, event),
5647
+ () => {
5648
+ if (this.packets.get(roomId) === room) this.packets.delete(roomId);
5649
+ }
5650
+ );
5651
+ atomicWriteFileSync(this.identityPath(roomId), Buffer.from(exportSigningSecret(native), "utf8"), this.persistence);
5652
+ this.provisioningCheckpoint("identity");
5653
+ this.saveState(native, liveDir);
5654
+ this.provisioningCheckpoint("state");
5655
+ this.packets.set(roomId, room);
5656
+ await room.setIdentity(identityName, bio);
5657
+ this.provisioningCheckpoint("identity_applied");
5658
+ return room;
5659
+ } catch (error) {
5660
+ if (native) {
5661
+ try {
5662
+ this.host.removePacket(native.cid);
5663
+ } catch {
5664
+ }
5665
+ }
5666
+ this.packets.delete(roomId);
5667
+ throw error;
5668
+ }
5669
+ }
5670
+ async restore(roomId, expectedCid, identityName, bio) {
5671
+ validateRoomId(roomId);
5672
+ if (this.packets.has(roomId)) throw new Error(`room packet "${roomId}" is already hosted`);
5673
+ const liveDir = this.liveDir(roomId);
5674
+ const secret = this.fs.readFileSync(this.identityPath(roomId), "utf8").trim();
5675
+ if (!/^[0-9a-f]+$/i.test(secret) || secret.length % 2 !== 0) {
5676
+ throw new Error(`invalid signing secret for room "${roomId}"`);
5677
+ }
5678
+ const stateBytes = this.fs.readFileSync(this.statePath(roomId));
5679
+ if (stateBytes.length === 0) throw new Error(`empty packet state for room "${roomId}"`);
5680
+ const native = await this.host.createPacket(
5681
+ this.packetName(roomId),
5682
+ this.seed(),
5683
+ secret,
5684
+ { deferredExposure: true }
5685
+ );
5686
+ if (expectedCid !== void 0 && native.cid !== expectedCid) {
5687
+ try {
5688
+ this.host.removePacket(native.cid);
5689
+ } catch {
5690
+ }
5691
+ throw new Error(
5692
+ `restored room packet CID mismatch for "${roomId}": expected "${expectedCid}", found "${native.cid}"`
5693
+ );
5694
+ }
5695
+ let room;
5696
+ room = new HostedRoomPacket(
5697
+ native,
5698
+ () => this.saveState(native, liveDir),
5699
+ this.log,
5700
+ (event) => this.onNotify(roomId, event),
5701
+ () => {
5702
+ if (this.packets.get(roomId) === room) this.packets.delete(roomId);
5703
+ }
5704
+ );
5705
+ try {
5706
+ await withScopeAsync(async (lifetime) => {
5707
+ const state = native.pw.packet.ParseValue(new Uint8Array(stateBytes)).Attach(lifetime);
5708
+ await native.mutatingTx("::actor::import_state", state, lifetime);
5709
+ });
5710
+ native.pw.refresh_identity_proof_document();
5711
+ if (identityName !== void 0 && bio !== void 0) {
5712
+ await room.setIdentity(identityName, bio);
5713
+ this.provisioningCheckpoint("identity_applied");
5714
+ }
5715
+ atomicWriteFileSync(
5716
+ this.ownershipPath(roomId),
5717
+ Buffer.from(`${roomId}
5718
+ `, "utf8"),
5719
+ this.persistence
5720
+ );
5721
+ await this.beforeExpose(room);
5722
+ this.host.exposePacket(native.cid);
5723
+ this.packets.set(roomId, room);
5724
+ return room;
5725
+ } catch (error) {
5726
+ try {
5727
+ this.host.removePacket(native.cid);
5728
+ } catch {
5729
+ }
5730
+ throw error;
5731
+ }
5732
+ }
5733
+ async destroy(roomId) {
5734
+ validateRoomId(roomId);
5735
+ const room = this.packets.get(roomId);
5736
+ if (room) {
5737
+ this.host.removePacket(room.cid);
5738
+ this.packets.delete(roomId);
5739
+ }
5740
+ const liveDir = this.liveDir(roomId);
5741
+ let removalFailure;
5742
+ try {
5743
+ this.assertSafeRoomDirectory(roomId);
5744
+ this.fs.rmSync(liveDir, { recursive: true, force: true });
5745
+ this.fsyncDirectory(this.roomDir(roomId));
5746
+ } catch (error) {
5747
+ this.log(`[${room?.name ?? this.packetName(roomId)}] live-state removal failed:`, error);
5748
+ removalFailure = error;
5749
+ }
5750
+ const residue = this.residue(roomId);
5751
+ if (removalFailure !== void 0 && residue.length === 0) {
5752
+ throw new PacketPersistenceError(
5753
+ `live-state removal durability is uncertain for room "${roomId}"`,
5754
+ { cause: removalFailure }
5755
+ );
5756
+ }
5757
+ return residue;
5758
+ }
5759
+ /** Unhost runtime packets while retaining every byte required for restart. */
5760
+ async unhostAll() {
5761
+ const errors = [];
5762
+ for (const [roomId, room] of [...this.packets]) {
5763
+ try {
5764
+ this.host.removePacket(room.cid, new Error("cowork daemon is shutting down"));
5765
+ this.packets.delete(roomId);
5766
+ } catch (error) {
5767
+ errors.push(error);
5768
+ }
5769
+ }
5770
+ if (errors.length > 0) throw new AggregateError(errors, "failed to unhost room packets");
5771
+ }
5772
+ saveState(packet, liveDir) {
5773
+ try {
5774
+ const bytes = withScope((lifetime) => Buffer.from(packet.readonlyTx("::actor::export_state", lifetime).Serialize()));
5775
+ atomicWriteFileSync(join3(liveDir, "state_data.bin"), bytes, this.persistence);
5776
+ } catch (error) {
5777
+ if (error instanceof PacketPersistenceError) throw error;
5778
+ throw new PacketPersistenceError(`failed to export state for packet "${packet.name}"`, { cause: error });
5779
+ }
5780
+ }
5781
+ residue(roomId) {
5782
+ const liveDir = this.liveDir(roomId);
5783
+ try {
5784
+ this.fs.lstatSync(liveDir);
5785
+ return [liveDir];
5786
+ } catch (error) {
5787
+ if (error.code === "ENOENT") return [];
5788
+ throw error;
5789
+ }
5790
+ }
5791
+ packetName(roomId) {
5792
+ return `cowork-room-${roomId}`;
5793
+ }
5794
+ roomDir(roomId) {
5795
+ return join3(this.stateDir, "rooms", roomId);
5796
+ }
5797
+ liveDir(roomId) {
5798
+ return join3(this.roomDir(roomId), "live");
5799
+ }
5800
+ identityPath(roomId) {
5801
+ return join3(this.liveDir(roomId), "identity.key");
5802
+ }
5803
+ statePath(roomId) {
5804
+ return join3(this.liveDir(roomId), "state_data.bin");
5805
+ }
5806
+ ownershipPath(roomId) {
5807
+ return join3(this.liveDir(roomId), ".cowork-provisioning-v1");
5808
+ }
5809
+ stagingJournalPath(roomId) {
5810
+ return join3(this.roomDir(roomId), ".cowork-provisioning-stage");
5811
+ }
5812
+ residuePath(roomId) {
5813
+ return join3(this.roomDir(roomId), "provisioning-residue");
5814
+ }
5815
+ hasRestorableState(roomId) {
5816
+ try {
5817
+ const secret = this.fs.readFileSync(this.identityPath(roomId), "utf8").trim();
5818
+ return /^[0-9a-f]+$/i.test(secret) && secret.length % 2 === 0 && this.fs.readFileSync(this.statePath(roomId)).length > 0;
5819
+ } catch {
5820
+ return false;
5821
+ }
5822
+ }
5823
+ prepareProvisioningDirectory(roomId) {
5824
+ const roomDir = this.roomDir(roomId);
5825
+ const liveDir = this.liveDir(roomId);
5826
+ if (!this.fs.existsSync(roomDir)) {
5827
+ this.fs.mkdirSync(roomDir, { recursive: true, mode: 448 });
5828
+ this.fs.chmodSync(roomDir, 448);
5829
+ }
5830
+ if (this.fs.existsSync(liveDir)) {
5831
+ let owned = false;
5832
+ try {
5833
+ owned = this.fs.readFileSync(this.ownershipPath(roomId), "utf8") === `${roomId}
5834
+ `;
5835
+ } catch {
5836
+ }
5837
+ if (owned) {
5838
+ this.assertSafeRoomDirectory(roomId);
5839
+ this.fs.rmSync(liveDir, { recursive: true, force: true });
5840
+ this.fsyncDirectory(roomDir);
5841
+ } else {
5842
+ this.assertSafeRoomDirectory(roomId);
5843
+ const residue = this.residuePath(roomId);
5844
+ if (this.fs.existsSync(residue)) {
5845
+ throw new PacketPersistenceError(
5846
+ `room "${roomId}" has unknown live state and an existing provisioning residue; inspect both before retrying`
5847
+ );
5848
+ }
5849
+ this.fs.renameSync(liveDir, residue);
5850
+ this.fs.chmodSync(residue, 448);
5851
+ this.fsyncDirectory(roomDir);
5852
+ }
5853
+ }
5854
+ this.cleanupOwnedStaging(roomId);
5855
+ const stagingName = this.stagingName();
5856
+ if (!/^live\.staging-[0-9a-f]{32}$/.test(stagingName)) {
5857
+ throw new PacketPersistenceError(`invalid provisioning staging name for room "${roomId}"`);
5858
+ }
5859
+ this.createStagingJournal(roomId, stagingName);
5860
+ const stagingDir = join3(roomDir, stagingName);
5861
+ try {
5862
+ this.fs.mkdirSync(stagingDir, { recursive: false, mode: 448 });
5863
+ } catch (error) {
5864
+ try {
5865
+ this.fs.unlinkSync(this.stagingJournalPath(roomId));
5866
+ this.fsyncDirectory(roomDir);
5867
+ } catch (cleanupError) {
5868
+ throw new AggregateError([error, cleanupError], `provisioning staging collision cleanup failed for room "${roomId}"`);
5869
+ }
5870
+ throw new PacketPersistenceError(`provisioning staging collision for room "${roomId}"`, { cause: error });
5871
+ }
5872
+ this.fs.chmodSync(stagingDir, 448);
5873
+ atomicWriteFileSync(
5874
+ join3(stagingDir, ".cowork-provisioning-v1"),
5875
+ Buffer.from(`${roomId}
5876
+ `, "utf8"),
5877
+ this.persistence
5878
+ );
5879
+ this.provisioningCheckpoint("mkdir");
5880
+ this.fs.renameSync(stagingDir, liveDir);
5881
+ this.fsyncDirectory(roomDir);
5882
+ this.fs.unlinkSync(this.stagingJournalPath(roomId));
5883
+ this.fsyncDirectory(roomDir);
5884
+ }
5885
+ cleanupOwnedStaging(roomId) {
5886
+ const journal = this.stagingJournalPath(roomId);
5887
+ let journalFd;
5888
+ try {
5889
+ try {
5890
+ journalFd = this.fs.openSync(
5891
+ journal,
5892
+ nodeFs2.constants.O_RDONLY | (nodeFs2.constants.O_NOFOLLOW ?? 0)
5893
+ );
5894
+ } catch (error) {
5895
+ if (error.code === "ENOENT") return;
5896
+ throw error;
5897
+ }
5898
+ const journalStat = this.fs.fstatSync(journalFd);
5899
+ if (!journalStat.isFile() || journalStat.size > 128) {
5900
+ throw new PacketPersistenceError(`unsafe provisioning staging journal for room "${roomId}"`);
5901
+ }
5902
+ const stagingName = this.fs.readFileSync(journalFd, "utf8").trim();
5903
+ if (!/^live\.staging-[0-9a-f]{32}$/.test(stagingName)) {
5904
+ throw new PacketPersistenceError(`invalid provisioning staging journal for room "${roomId}"`);
5905
+ }
5906
+ const roomDir = this.roomDir(roomId);
5907
+ const stagingDir = join3(roomDir, stagingName);
5908
+ if (this.fs.existsSync(stagingDir)) {
5909
+ const stat = this.fs.lstatSync(stagingDir);
5910
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
5911
+ throw new PacketPersistenceError(`unsafe provisioning staging path for room "${roomId}"`);
5912
+ }
5913
+ let owned = false;
5914
+ try {
5915
+ owned = this.fs.readFileSync(join3(stagingDir, ".cowork-provisioning-v1"), "utf8") === `${roomId}
5916
+ `;
5917
+ } catch {
5918
+ }
5919
+ if (owned) {
5920
+ this.fs.rmSync(stagingDir, { recursive: true, force: true });
5921
+ this.fsyncDirectory(roomDir);
5922
+ } else {
5923
+ const residue = this.residuePath(roomId);
5924
+ if (this.fs.existsSync(residue)) {
5925
+ throw new PacketPersistenceError(
5926
+ `provisioning staging recovery for room "${roomId}" found an existing provisioning residue; preserving both`
5927
+ );
5928
+ }
5929
+ this.fs.renameSync(stagingDir, residue);
5930
+ this.fs.chmodSync(residue, 448);
5931
+ this.fsyncDirectory(roomDir);
5932
+ }
5933
+ }
5934
+ const currentJournal = this.fs.lstatSync(journal);
5935
+ if (!currentJournal.isFile() || currentJournal.dev !== journalStat.dev || currentJournal.ino !== journalStat.ino) {
5936
+ throw new PacketPersistenceError(
5937
+ `provisioning staging journal changed during recovery for room "${roomId}"; replacement preserved`
5938
+ );
5939
+ }
5940
+ this.fs.unlinkSync(journal);
5941
+ this.fsyncDirectory(roomDir);
5942
+ } finally {
5943
+ if (journalFd !== void 0) this.fs.closeSync(journalFd);
5944
+ }
5945
+ }
5946
+ createStagingJournal(roomId, stagingName) {
5947
+ const journal = this.stagingJournalPath(roomId);
5948
+ const bytes = Buffer.from(`${stagingName}
5949
+ `, "utf8");
5950
+ let fd;
5951
+ let created = false;
5952
+ try {
5953
+ fd = this.persistence.openSync(
5954
+ journal,
5955
+ nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | (nodeFs2.constants.O_NOFOLLOW ?? 0),
5956
+ 384
5957
+ );
5958
+ created = true;
5959
+ this.persistence.fchmodSync(fd, 384);
5960
+ let offset = 0;
5961
+ while (offset < bytes.byteLength) {
5962
+ const written = this.persistence.writeSync(fd, bytes, offset, bytes.byteLength - offset, null);
5963
+ if (written <= 0) throw new Error(`short write while persisting ${journal}`);
5964
+ offset += written;
5965
+ }
5966
+ this.persistence.fsyncSync(fd);
5967
+ this.persistence.closeSync(fd);
5968
+ fd = void 0;
5969
+ this.fsyncDirectory(this.roomDir(roomId));
5970
+ } catch (error) {
5971
+ if (fd !== void 0) {
5972
+ try {
5973
+ this.persistence.closeSync(fd);
5974
+ } catch {
5975
+ }
5976
+ }
5977
+ if (created) {
5978
+ try {
5979
+ this.fs.unlinkSync(journal);
5980
+ this.fsyncDirectory(this.roomDir(roomId));
5981
+ } catch {
5982
+ }
5983
+ }
5984
+ throw new PacketPersistenceError(`failed to establish staging ownership for room "${roomId}"`, { cause: error });
5985
+ }
5986
+ }
5987
+ assertSafeRoomDirectory(roomId) {
5988
+ const roomDir = this.roomDir(roomId);
5989
+ const stat = this.fs.lstatSync(roomDir);
5990
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
5991
+ throw new Error(`room directory for "${roomId}" is not a safe directory`);
5670
5992
  }
5671
5993
  }
5672
- });
5673
- RoomInviteSchema = external_exports.object({
5674
- invite_id: NonEmptyStringSchema,
5675
- mode: InviteModeSchema,
5676
- role: RoleSchema,
5677
- min_accepts: PositiveSafeIntegerSchema,
5678
- accepted_cids: external_exports.array(NonEmptyStringSchema),
5679
- state: InviteStateSchema,
5680
- recovery_of: NonEmptyStringSchema.optional(),
5681
- recovery_confirmed: external_exports.boolean().optional(),
5682
- created_at: Rfc3339Schema,
5683
- replaces_seat: LowerCrockfordUlidSchema.optional()
5684
- }).strict().superRefine((invite, context) => {
5685
- if (invite.mode === "one_time" && invite.min_accepts !== 1) {
5686
- context.addIssue({
5687
- code: external_exports.ZodIssueCode.custom,
5688
- path: ["min_accepts"],
5689
- message: "one_time invites require min_accepts === 1"
5994
+ fsyncDirectory(path) {
5995
+ let fd;
5996
+ try {
5997
+ fd = this.fs.openSync(path, nodeFs2.constants.O_RDONLY | (nodeFs2.constants.O_NOFOLLOW ?? 0));
5998
+ this.fs.fsyncSync(fd);
5999
+ } finally {
6000
+ if (fd !== void 0) this.fs.closeSync(fd);
6001
+ }
6002
+ }
6003
+ };
6004
+ HostedRoomPacket = class {
6005
+ name;
6006
+ cid;
6007
+ packet;
6008
+ constructor(packet, saveState, log, onNotify = () => {
6009
+ }, onTerminal = () => {
6010
+ }) {
6011
+ this.packet = packet;
6012
+ this.name = packet.name;
6013
+ this.cid = packet.cid;
6014
+ wireHandlers(packet, { onSaveState: saveState, onNotify: (event) => onNotify(event) }, log);
6015
+ packet.onTerminalClose?.(onTerminal);
6016
+ }
6017
+ async setIdentity(identityName, bio) {
6018
+ await withScopeAsync((lifetime) => this.packet.mutatingTx("::a2a_messaging::set_my_name", { name: identityName }, lifetime));
6019
+ await withScopeAsync((lifetime) => this.packet.mutatingTx("::a2a_messaging::set_my_bio", { bio }, lifetime));
6020
+ }
6021
+ async mintInvite(mode) {
6022
+ return withScopeAsync(async (lifetime) => {
6023
+ const result = await this.packet.mutatingTx("::a2a_messaging::generate_invite", { mode }, lifetime);
6024
+ return {
6025
+ blob: packInvite(Buffer.from(result.Reduce("invite").GetBinary())),
6026
+ invite_id: result.Reduce("invite_id").Visualize(),
6027
+ reusable: booleanValue(result.Reduce("reusable"))
6028
+ };
5690
6029
  });
5691
6030
  }
5692
- if (invite.state === "receipt_pending" && invite.recovery_of === void 0) {
5693
- context.addIssue({
5694
- code: external_exports.ZodIssueCode.custom,
5695
- path: ["recovery_of"],
5696
- message: "receipt_pending invites require recovery_of"
6031
+ async revokeInvite(inviteId) {
6032
+ return withScopeAsync(async (lifetime) => {
6033
+ const result = await this.packet.mutatingTx("::a2a_messaging::revoke_invite", { invite_id: inviteId }, lifetime);
6034
+ return { revoked: booleanValue(result.Reduce("revoked")) };
5697
6035
  });
5698
6036
  }
5699
- if (invite.recovery_of === void 0 && invite.recovery_confirmed !== void 0) {
5700
- context.addIssue({
5701
- code: external_exports.ZodIssueCode.custom,
5702
- path: ["recovery_confirmed"],
5703
- message: "recovery_confirmed is forbidden without recovery_of"
6037
+ listInvites() {
6038
+ return withScope((lifetime) => {
6039
+ const value = this.packet.readonlyTx("::a2a_messaging::list_invites", lifetime);
6040
+ return dictionaryEntries(value).map(([inviteId, invite]) => ({
6041
+ invite_id: inviteId,
6042
+ mode: inviteMode(invite.Reduce("mode").Visualize())
6043
+ }));
5704
6044
  });
5705
6045
  }
5706
- if (invite.recovery_of !== void 0 && invite.recovery_confirmed === void 0) {
5707
- context.addIssue({
5708
- code: external_exports.ZodIssueCode.custom,
5709
- path: ["recovery_confirmed"],
5710
- message: "recovery_confirmed is required with recovery_of"
6046
+ listContacts() {
6047
+ return withScope((lifetime) => {
6048
+ const value = this.packet.readonlyTx("::a2a_messaging::list_contacts", lifetime);
6049
+ return dictionaryEntries(value).map(([, contact]) => ({
6050
+ name: contact.Reduce("name").Visualize(),
6051
+ container_id: contact.Reduce("container_id").Visualize()
6052
+ }));
5711
6053
  });
5712
6054
  }
5713
- if (invite.state === "receipt_pending" && invite.recovery_confirmed !== false) {
5714
- context.addIssue({
5715
- code: external_exports.ZodIssueCode.custom,
5716
- path: ["recovery_confirmed"],
5717
- message: "receipt_pending recovery lineage must be unconfirmed"
6055
+ listContactOrigins() {
6056
+ return withScope((lifetime) => {
6057
+ const value = this.packet.readonlyTx("::a2a_messaging::list_contact_origins", lifetime);
6058
+ return Object.fromEntries(dictionaryEntries(value).map(([cid, origin]) => [cid, {
6059
+ via: origin.Reduce("via").Visualize(),
6060
+ invite_id: nilString(origin.Reduce("invite_id")),
6061
+ at: adaptTimeToRfc3339(origin.Reduce("at").Visualize())
6062
+ }]));
5718
6063
  });
5719
6064
  }
5720
- if (invite.recovery_of !== void 0 && (invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required") && invite.recovery_confirmed !== true) {
5721
- context.addIssue({
5722
- code: external_exports.ZodIssueCode.custom,
5723
- path: ["recovery_confirmed"],
5724
- message: "live, consumed, and replacement_required recovery lineage must be confirmed"
6065
+ peekInbox() {
6066
+ return withScope((lifetime) => renderInbox(this.packet.readonlyTx("::actor::list_incoming_messages", lifetime)).filter((message) => message.status === "unread").map(({ status: _status, ...message }) => message));
6067
+ }
6068
+ async consumeInbox(expectedIds) {
6069
+ return withScopeAsync(async (lifetime) => {
6070
+ const result = await this.packet.mutatingTx(
6071
+ "::actor::consume_messages",
6072
+ { expected_ids: expectedIds },
6073
+ lifetime
6074
+ );
6075
+ return {
6076
+ consumed: renderIntegerArray(result.Reduce("consumed")),
6077
+ deferred: renderIntegerArray(result.Reduce("deferred"))
6078
+ };
5725
6079
  });
5726
6080
  }
5727
- if (invite.state === "receipt_pending" && invite.accepted_cids.length > 0) {
5728
- context.addIssue({
5729
- code: external_exports.ZodIssueCode.custom,
5730
- path: ["accepted_cids"],
5731
- message: "receipt_pending invites cannot have accepted CIDs"
6081
+ peekFileInbox() {
6082
+ return withScope((lifetime) => renderFileInbox(
6083
+ this.packet.readonlyTx("::actor::list_incoming_files", lifetime)
6084
+ ).filter((file) => file.status === "unread").map(({ status: _status, ...file }) => file));
6085
+ }
6086
+ async consumeFileInbox(expectedIds) {
6087
+ return withScopeAsync(async (lifetime) => {
6088
+ const result = await this.packet.mutatingTx(
6089
+ "::actor::consume_files",
6090
+ { expected_ids: expectedIds },
6091
+ lifetime
6092
+ );
6093
+ return {
6094
+ consumed: renderIntegerArray(result.Reduce("consumed")),
6095
+ deferred: renderIntegerArray(result.Reduce("deferred"))
6096
+ };
5732
6097
  });
5733
6098
  }
5734
- });
5735
- MissionV1Schema = external_exports.object({
5736
- goal: MissionTextSchema,
5737
- briefing: MissionTextSchema
5738
- }).strict();
5739
- MissionSchema = external_exports.object({
5740
- goal: MissionTextSchema,
5741
- briefing: MissionTextSchema,
5742
- briefing_version: PositiveSafeIntegerSchema
5743
- }).strict();
5744
- RoleBriefingSchema = external_exports.object({
5745
- text: MissionTextSchema,
5746
- version: PositiveSafeIntegerSchema,
5747
- updated_at: Rfc3339Schema
5748
- }).strict();
5749
- RoomCommonShape = {
5750
- room_id: LowerCrockfordUlidSchema,
5751
- identity_name: NonEmptyStringSchema,
5752
- identity_cid: external_exports.string(),
5753
- state: RoomStateSchema,
5754
- status: NonEmptyStringSchema.optional(),
5755
- invites: external_exports.array(RoomInviteSchema),
5756
- created_at: Rfc3339Schema,
5757
- activated_at: Rfc3339Schema.optional(),
5758
- closed_at: Rfc3339Schema.optional()
5759
- };
5760
- RoomV1Schema = external_exports.object({
5761
- ...RoomCommonShape,
5762
- version: external_exports.literal(1),
5763
- mission: MissionV1Schema,
5764
- seats: external_exports.array(SeatV1Schema)
5765
- }).strict().superRefine(refineRoomLineage);
5766
- RoomSchema = external_exports.object({
5767
- ...RoomCommonShape,
5768
- version: external_exports.literal(2),
5769
- mission: MissionSchema,
5770
- role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
5771
- anonymous: external_exports.boolean(),
5772
- quiet_membership: external_exports.boolean(),
5773
- membership_epoch: external_exports.number().int().nonnegative().safe(),
5774
- seats: external_exports.array(SeatSchema)
5775
- }).strict().superRefine((room, context) => {
5776
- refineRoomLineage(room, context);
5777
- const byParticipant = /* @__PURE__ */ new Map();
5778
- const activeAliases = /* @__PURE__ */ new Set();
5779
- for (const [index, seat] of room.seats.entries()) {
5780
- if (byParticipant.has(seat.participant_id)) {
5781
- context.addIssue({
5782
- code: external_exports.ZodIssueCode.custom,
5783
- path: ["seats", index, "participant_id"],
5784
- message: "participant_id must be unique within the room"
5785
- });
5786
- }
5787
- byParticipant.set(seat.participant_id, seat);
5788
- if (room.anonymous && seat.alias === void 0) {
5789
- context.addIssue({
5790
- code: external_exports.ZodIssueCode.custom,
5791
- path: ["seats", index, "alias"],
5792
- message: "anonymous rooms require an alias on every seat"
5793
- });
5794
- }
5795
- if (!room.anonymous && seat.alias !== void 0) {
5796
- context.addIssue({
5797
- code: external_exports.ZodIssueCode.custom,
5798
- path: ["seats", index, "alias"],
5799
- message: "aliases are reserved for anonymous rooms"
5800
- });
5801
- }
5802
- if (seat.state === "active" && seat.alias !== void 0) {
5803
- if (activeAliases.has(seat.alias)) {
5804
- context.addIssue({
5805
- code: external_exports.ZodIssueCode.custom,
5806
- path: ["seats", index, "alias"],
5807
- message: "active seats must hold distinct aliases"
5808
- });
5809
- }
5810
- activeAliases.add(seat.alias);
5811
- }
5812
- if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
5813
- context.addIssue({
5814
- code: external_exports.ZodIssueCode.custom,
5815
- path: ["seats", index, "removed_epoch"],
5816
- message: "removed_epoch cannot exceed the room membership_epoch"
5817
- });
5818
- }
6099
+ async send(contactCid, body) {
6100
+ return withScopeAsync(async (lifetime) => {
6101
+ const result = await this.packet.mutatingTx(
6102
+ "::a2a_messaging::send_message",
6103
+ { contact: contactCid, text: body },
6104
+ lifetime
6105
+ );
6106
+ const refused = !result.Reduce("downgrade_refused").IsNil();
6107
+ return refused ? { status: "send_failed" } : { status: "queued", wire_id: nilString(result.Reduce("wire_id")) || void 0 };
6108
+ });
5819
6109
  }
5820
- for (const [index, seat] of room.seats.entries()) {
5821
- if (seat.replaces_seat === void 0) continue;
5822
- const predecessor = byParticipant.get(seat.replaces_seat);
5823
- if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
5824
- context.addIssue({
5825
- code: external_exports.ZodIssueCode.custom,
5826
- path: ["seats", index, "replaces_seat"],
5827
- message: "replaces_seat must reference a removed seat in this room"
5828
- });
5829
- continue;
5830
- }
5831
- if (predecessor.role !== seat.role) {
5832
- context.addIssue({
5833
- code: external_exports.ZodIssueCode.custom,
5834
- path: ["seats", index, "role"],
5835
- message: "a replacement seat must inherit the predecessor role"
5836
- });
5837
- }
5838
- if (room.anonymous && seat.alias !== predecessor.alias) {
5839
- context.addIssue({
5840
- code: external_exports.ZodIssueCode.custom,
5841
- path: ["seats", index, "alias"],
5842
- message: "an anonymous replacement seat must inherit the predecessor alias"
5843
- });
6110
+ async sendFile(contactCid, filename, mime, data) {
6111
+ const validName = FileNameSchema.parse(filename);
6112
+ const validMime = FileMimeSchema.parse(mime);
6113
+ if (data.length > MAX_FILE_BYTES) {
6114
+ throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
5844
6115
  }
6116
+ return withScopeAsync(async (lifetime) => {
6117
+ const result = await this.packet.mutatingTx(
6118
+ "::a2a_messaging::send_file",
6119
+ {
6120
+ contact: contactCid,
6121
+ filename: validName,
6122
+ mime: validMime,
6123
+ // The core contract takes bytes. A filesystem path here would make
6124
+ // recovery depend on staging ownership and is deliberately forbidden.
6125
+ data: this.packet.newBinary(data, lifetime)
6126
+ },
6127
+ lifetime
6128
+ );
6129
+ const refused = !result.Reduce("downgrade_refused").IsNil() || !result.Reduce("migrating").IsNil();
6130
+ return refused ? { status: "send_failed" } : { status: "queued", wire_id: nilString(result.Reduce("wire_id")) || void 0 };
6131
+ });
5845
6132
  }
5846
- });
5847
- CreateRoomInputSchema = external_exports.object({
5848
- goal: MissionTextSchema,
5849
- briefing: MissionTextSchema,
5850
- anonymous: external_exports.boolean().optional(),
5851
- quiet_membership: external_exports.boolean().optional()
5852
- }).strict();
5853
- UpdateRoomInputSchema = external_exports.object({
5854
- goal: MissionTextSchema.optional(),
5855
- briefing: MissionTextSchema.optional(),
5856
- status: NonEmptyStringSchema.optional(),
5857
- quiet_membership: external_exports.boolean().optional()
5858
- }).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
5859
- RoleBriefingSetInputSchema = external_exports.object({
5860
- role: RoleSchema,
5861
- text: MissionTextSchema
5862
- }).strict();
5863
- RoleBriefingDeleteInputSchema = external_exports.object({
5864
- role: RoleSchema
5865
- }).strict();
5866
- PostMessageInputSchema = external_exports.object({
5867
- text: MessageTextSchema
5868
- }).strict();
5869
- AuthorSnapshotSchema = external_exports.object({
5870
- identity: NonEmptyStringSchema,
5871
- display_name: NonEmptyStringSchema,
5872
- role: RoleSchema
5873
- }).strict();
5874
- RecordCommonShape = {
5875
- version: external_exports.literal(1),
5876
- room_id: LowerCrockfordUlidSchema,
5877
- seq: PositiveSafeIntegerSchema,
5878
- record_id: NonEmptyStringSchema,
5879
- at: Rfc3339Schema
5880
- };
5881
- AppendCommonShape = {
5882
- version: external_exports.literal(1),
5883
- room_id: LowerCrockfordUlidSchema,
5884
- at: Rfc3339Schema
5885
- };
5886
- MembershipNoticeSchema = external_exports.object({
5887
- action: external_exports.enum(["remove"]),
5888
- alias: NonEmptyStringSchema.optional(),
5889
- role: RoleSchema.optional(),
5890
- epoch: external_exports.number().int().nonnegative().safe()
5891
- }).strict();
5892
- AuthorAliasSchema = external_exports.object({
5893
- participant_id: LowerCrockfordUlidSchema,
5894
- alias: NonEmptyStringSchema
5895
- }).strict();
5896
- MessageShape = {
5897
- kind: external_exports.literal("message"),
5898
- message_id: LowerCrockfordUlidSchema,
5899
- author: AuthorSnapshotSchema,
5900
- author_alias: AuthorAliasSchema.optional(),
5901
- category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
5902
- briefing_role: RoleSchema.optional(),
5903
- briefing_version: PositiveSafeIntegerSchema.optional(),
5904
- membership: MembershipNoticeSchema.optional(),
5905
- text: MessageTextSchema,
5906
- recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5907
- const seen = /* @__PURE__ */ new Set();
5908
- for (const [index, identity] of identities.entries()) {
5909
- if (seen.has(identity)) {
5910
- context.addIssue({
5911
- code: external_exports.ZodIssueCode.custom,
5912
- path: [index],
5913
- message: "recipient identities must be unique"
5914
- });
6133
+ async removeContact(contactCid) {
6134
+ return withScopeAsync(async (lifetime) => {
6135
+ const result = await this.packet.mutatingTx(
6136
+ "::a2a_messaging::remove_contact",
6137
+ { contact: contactCid },
6138
+ lifetime
6139
+ );
6140
+ const notified = strictBooleanValue(result.Reduce("notified"), "remove_contact notified");
6141
+ const keyMaterialRetained = strictBooleanValue(
6142
+ result.Reduce("key_material_retained"),
6143
+ "remove_contact key_material_retained"
6144
+ );
6145
+ if (!keyMaterialRetained) {
6146
+ throw new Error("remove_contact key_material_retained must be true");
5915
6147
  }
5916
- seen.add(identity);
5917
- }
5918
- }),
5919
- source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5920
- source_wire_id: NonEmptyStringSchema.optional()
5921
- };
5922
- RelayIntentShape = {
5923
- kind: external_exports.literal("relay_intent"),
5924
- message_id: LowerCrockfordUlidSchema,
5925
- recipient_identity: NonEmptyStringSchema
5926
- };
5927
- RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
5928
- RelayResultShape = {
5929
- kind: external_exports.literal("relay_result"),
5930
- intent_record_id: NonEmptyStringSchema,
5931
- message_id: LowerCrockfordUlidSchema,
5932
- recipient_identity: NonEmptyStringSchema,
5933
- status: RelayResultStatusSchema,
5934
- wire_id: NonEmptyStringSchema.optional()
5935
- };
5936
- MembershipIntentShape = {
5937
- kind: external_exports.literal("membership_intent"),
5938
- action: external_exports.enum(["remove"]),
5939
- participant_id: LowerCrockfordUlidSchema,
5940
- recipient_identity: NonEmptyStringSchema,
5941
- role: RoleSchema,
5942
- alias: NonEmptyStringSchema.optional(),
5943
- epoch: PositiveSafeIntegerSchema,
5944
- notify: external_exports.boolean()
5945
- };
5946
- MembershipResultShape = {
5947
- kind: external_exports.literal("membership_result"),
5948
- intent_record_id: NonEmptyStringSchema,
5949
- participant_id: LowerCrockfordUlidSchema,
5950
- status: RelayStatusSchema,
5951
- notified: external_exports.boolean(),
5952
- key_material_retained: external_exports.literal(true),
5953
- uncertain_after_restart: external_exports.literal(true).optional()
5954
- };
5955
- CloseNoticeIntentShape = {
5956
- kind: external_exports.literal("close_notice_intent"),
5957
- recipient_identity: NonEmptyStringSchema
5958
- };
5959
- CloseNoticeResultShape = {
5960
- kind: external_exports.literal("close_notice_result"),
5961
- intent_record_id: NonEmptyStringSchema,
5962
- recipient_identity: NonEmptyStringSchema,
5963
- status: RelayStatusSchema,
5964
- notified: external_exports.boolean(),
5965
- key_material_retained: external_exports.literal(true),
5966
- uncertain_after_restart: external_exports.literal(true).optional()
5967
- };
5968
- MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
5969
- RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5970
- RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5971
- MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
5972
- MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
5973
- CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
5974
- CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
5975
- RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5976
- MessageRecordSchema,
5977
- RelayIntentRecordSchema,
5978
- RelayResultRecordSchema,
5979
- MembershipIntentRecordSchema,
5980
- MembershipResultRecordSchema,
5981
- CloseNoticeIntentRecordSchema,
5982
- CloseNoticeResultRecordSchema
5983
- ]);
5984
- CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5985
- if (record.record_id !== `${record.room_id}:${record.seq}`) {
5986
- context.addIssue({
5987
- code: external_exports.ZodIssueCode.custom,
5988
- path: ["record_id"],
5989
- message: 'record_id must equal room_id + ":" + seq'
6148
+ return {
6149
+ status: notified ? "queued" : "send_failed",
6150
+ notified,
6151
+ key_material_retained: true
6152
+ };
5990
6153
  });
5991
6154
  }
5992
- if (record.kind === "message") refineMessageCategory(record, context);
5993
- });
5994
- AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5995
- external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
5996
- external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5997
- external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5998
- external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
5999
- external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
6000
- external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
6001
- external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
6002
- ]).superRefine((record, context) => {
6003
- if (record.kind === "message") refineMessageCategory(record, context);
6004
- });
6155
+ async sign(canonicalJson2) {
6156
+ return withScopeAsync(async (lifetime) => {
6157
+ const result = await this.packet.mutatingTx(
6158
+ "::actor::sign_app_envelope",
6159
+ { canonical_json: canonicalJson2 },
6160
+ lifetime
6161
+ );
6162
+ return Buffer.from(result.Reduce("signature").GetBinary()).toString("base64url");
6163
+ });
6164
+ }
6165
+ };
6005
6166
  }
6006
6167
  });
6007
6168
 
@@ -6037,11 +6198,16 @@ var init_ulid = __esm({
6037
6198
  });
6038
6199
 
6039
6200
  // src/intake.ts
6201
+ import { createHash as createHash2 } from "node:crypto";
6040
6202
  function canonicalJson(value) {
6041
6203
  const encoded = JSON.stringify(canonicalValue(value));
6042
6204
  if (encoded === void 0) throw new TypeError("canonical JSON value is not serializable");
6043
6205
  return encoded;
6044
6206
  }
6207
+ async function sendSignedBody(packet, recipientIdentity, unsigned) {
6208
+ const signature = await packet.sign(canonicalJson(unsigned));
6209
+ return packet.send(recipientIdentity, canonicalJson({ ...unsigned, signature }));
6210
+ }
6045
6211
  function canonicalValue(value) {
6046
6212
  if (Array.isArray(value)) return value.map(canonicalValue);
6047
6213
  if (value !== null && typeof value === "object") {
@@ -6155,9 +6321,58 @@ var init_intake = __esm({
6155
6321
  async processAndRelayUnlocked(roomId, packet) {
6156
6322
  const snapshot = packet.peekInbox();
6157
6323
  for (const item of snapshot) await this.processInboxItem(roomId, packet, item);
6324
+ const fileSnapshot = packet.peekFileInbox();
6325
+ for (const item of fileSnapshot) await this.processFileInboxItem(roomId, packet, item);
6158
6326
  await this.completeSnapshotIntents(roomId);
6159
6327
  await this.relayPendingUnlocked(roomId, packet);
6160
6328
  }
6329
+ async processFileInboxItem(roomId, packet, item) {
6330
+ const parsedName = FileNameSchema.safeParse(item.filename);
6331
+ const parsedMime = FileMimeSchema.safeParse(item.mime);
6332
+ if (!parsedName.success || !parsedMime.success) {
6333
+ await packet.consumeFileInbox([item.file_id]);
6334
+ return;
6335
+ }
6336
+ if (item.data.length > MAX_FILE_BYTES) {
6337
+ throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
6338
+ }
6339
+ const room = await this.store.load(roomId);
6340
+ const seat = room.seats.find(
6341
+ (candidate) => candidate.identity === item.sender_id && candidate.state === "active"
6342
+ );
6343
+ if (room.state !== "active" || !seat) {
6344
+ if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
6345
+ await packet.consumeFileInbox([item.file_id]);
6346
+ return;
6347
+ }
6348
+ const records = await this.store.read(roomId);
6349
+ let file = this.findSourceFile(records, item);
6350
+ if (!file) {
6351
+ const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
6352
+ const bytes = Buffer.from(item.data);
6353
+ const appended = await this.store.append(roomId, {
6354
+ version: 1,
6355
+ kind: "file",
6356
+ room_id: roomId,
6357
+ at: Rfc3339Schema.parse(item.date),
6358
+ file_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
6359
+ author: { identity: seat.identity, display_name: seat.display_name, role: seat.role },
6360
+ ...room.anonymous && seat.alias !== void 0 ? { author_alias: { participant_id: seat.participant_id, alias: seat.alias } } : {},
6361
+ filename: parsedName.data,
6362
+ mime: parsedMime.data,
6363
+ size: bytes.length,
6364
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
6365
+ data_base64: bytes.toString("base64"),
6366
+ recipient_identities: recipientIdentities,
6367
+ source_file_id: item.file_id,
6368
+ ...item.wire_id === "" ? {} : { source_wire_id: item.wire_id }
6369
+ });
6370
+ if (appended.kind !== "file") throw new Error("storage returned the wrong participant file kind");
6371
+ file = appended;
6372
+ }
6373
+ await this.completeFileIntents(roomId, file);
6374
+ await packet.consumeFileInbox([item.file_id]);
6375
+ }
6161
6376
  async processInboxItem(roomId, packet, item) {
6162
6377
  const room = await this.store.load(roomId);
6163
6378
  const seat = room.seats.find(
@@ -6215,8 +6430,7 @@ var init_intake = __esm({
6215
6430
  await this.store.save(RoomSchema.parse({ ...room, seats }));
6216
6431
  try {
6217
6432
  const unsigned = { version: 1, kind: "room_not_member", room_id: roomId };
6218
- const signature = await packet.sign(canonicalJson(unsigned));
6219
- await packet.send(item.sender_id, canonicalJson({ ...unsigned, signature }));
6433
+ await sendSignedBody(packet, item.sender_id, unsigned);
6220
6434
  } catch {
6221
6435
  }
6222
6436
  }
@@ -6225,6 +6439,25 @@ var init_intake = __esm({
6225
6439
  for (const message of records.filter(
6226
6440
  (record) => record.kind === "message"
6227
6441
  )) await this.completeMessageIntents(roomId, message);
6442
+ for (const file of records.filter(
6443
+ (record) => record.kind === "file"
6444
+ )) await this.completeFileIntents(roomId, file);
6445
+ }
6446
+ async completeFileIntents(roomId, file) {
6447
+ const records = await this.store.read(roomId);
6448
+ const intended = new Set(records.filter((record) => record.kind === "relay_intent" && record.file_id === file.file_id).map((intent) => intent.recipient_identity));
6449
+ for (const recipientIdentity of file.recipient_identities) {
6450
+ if (intended.has(recipientIdentity)) continue;
6451
+ await this.store.append(roomId, {
6452
+ version: 1,
6453
+ kind: "relay_intent",
6454
+ room_id: roomId,
6455
+ at: this.now(),
6456
+ file_id: file.file_id,
6457
+ recipient_identity: recipientIdentity
6458
+ });
6459
+ intended.add(recipientIdentity);
6460
+ }
6228
6461
  }
6229
6462
  async completeMessageIntents(roomId, message) {
6230
6463
  const records = await this.store.read(roomId);
@@ -6248,14 +6481,17 @@ var init_intake = __esm({
6248
6481
  const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
6249
6482
  const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
6250
6483
  const messages = new Map(records.filter((record) => record.kind === "message").map((message) => [message.message_id, message]));
6484
+ const files = new Map(records.filter((record) => record.kind === "file").map((file) => [file.file_id, file]));
6251
6485
  const completed = new Set(records.filter((record) => record.kind === "relay_result").map((result) => result.kind === "relay_result" ? result.intent_record_id : ""));
6252
6486
  for (const intent of records.filter(
6253
6487
  (record) => record.kind === "relay_intent"
6254
6488
  )) {
6255
6489
  if (completed.has(intent.record_id)) continue;
6256
- const message = messages.get(intent.message_id);
6257
- if (!message) continue;
6258
- if (!message.recipient_identities.includes(intent.recipient_identity)) continue;
6490
+ const message = intent.message_id === void 0 ? void 0 : messages.get(intent.message_id);
6491
+ const file = intent.file_id === void 0 ? void 0 : files.get(intent.file_id);
6492
+ if (message === void 0 === (file === void 0)) continue;
6493
+ const recipients = message?.recipient_identities ?? file.recipient_identities;
6494
+ if (!recipients.includes(intent.recipient_identity)) continue;
6259
6495
  if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
6260
6496
  const skipped = await this.store.append(roomId, {
6261
6497
  version: 1,
@@ -6263,7 +6499,8 @@ var init_intake = __esm({
6263
6499
  room_id: roomId,
6264
6500
  at: this.now(),
6265
6501
  intent_record_id: intent.record_id,
6266
- message_id: intent.message_id,
6502
+ ...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
6503
+ ...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
6267
6504
  recipient_identity: intent.recipient_identity,
6268
6505
  status: "skipped_removed"
6269
6506
  });
@@ -6271,6 +6508,61 @@ var init_intake = __esm({
6271
6508
  completed.add(intent.record_id);
6272
6509
  continue;
6273
6510
  }
6511
+ if (file !== void 0) {
6512
+ const author = file.author_alias === void 0 ? file.author : {
6513
+ identity: file.author_alias.participant_id,
6514
+ display_name: file.author_alias.alias,
6515
+ role: file.author.role
6516
+ };
6517
+ const metadata = await sendSignedBody(packet, intent.recipient_identity, {
6518
+ version: 1,
6519
+ kind: "room_file",
6520
+ room_id: roomId,
6521
+ file_id: file.file_id,
6522
+ author,
6523
+ filename: file.filename,
6524
+ mime: file.mime,
6525
+ size: file.size,
6526
+ sha256: file.sha256,
6527
+ at: file.at
6528
+ });
6529
+ if (metadata.status === "send_failed") {
6530
+ const failed = await this.store.append(roomId, {
6531
+ version: 1,
6532
+ kind: "relay_result",
6533
+ room_id: roomId,
6534
+ at: this.now(),
6535
+ intent_record_id: intent.record_id,
6536
+ file_id: file.file_id,
6537
+ recipient_identity: intent.recipient_identity,
6538
+ status: "send_failed"
6539
+ });
6540
+ if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6541
+ completed.add(intent.record_id);
6542
+ continue;
6543
+ }
6544
+ const outcome2 = await packet.sendFile(
6545
+ intent.recipient_identity,
6546
+ file.filename,
6547
+ file.mime,
6548
+ Buffer.from(file.data_base64, "base64")
6549
+ );
6550
+ const appended2 = await this.store.append(roomId, {
6551
+ version: 1,
6552
+ kind: "relay_result",
6553
+ room_id: roomId,
6554
+ at: this.now(),
6555
+ intent_record_id: intent.record_id,
6556
+ file_id: file.file_id,
6557
+ recipient_identity: intent.recipient_identity,
6558
+ status: outcome2.status,
6559
+ ...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
6560
+ ...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
6561
+ });
6562
+ if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6563
+ completed.add(intent.record_id);
6564
+ continue;
6565
+ }
6274
6566
  const unsigned = {
6275
6567
  version: 1,
6276
6568
  kind: wireKind(message.category),
@@ -6288,9 +6580,7 @@ var init_intake = __esm({
6288
6580
  ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
6289
6581
  ...message.membership === void 0 ? {} : { membership: message.membership }
6290
6582
  };
6291
- const signature = await packet.sign(canonicalJson(unsigned));
6292
- const body = canonicalJson({ ...unsigned, signature });
6293
- const outcome = await packet.send(intent.recipient_identity, body);
6583
+ const outcome = await sendSignedBody(packet, intent.recipient_identity, unsigned);
6294
6584
  const appended = await this.store.append(roomId, {
6295
6585
  version: 1,
6296
6586
  kind: "relay_result",
@@ -6315,6 +6605,16 @@ var init_intake = __esm({
6315
6605
  }
6316
6606
  return message;
6317
6607
  }
6608
+ findSourceFile(records, item) {
6609
+ const file = records.find((record) => record.kind === "file" && record.source_file_id === item.file_id);
6610
+ if (!file) return void 0;
6611
+ const observedWireId = item.wire_id === "" ? void 0 : item.wire_id;
6612
+ const bytes = Buffer.from(item.data);
6613
+ 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")) {
6614
+ throw new Error(`file inbox source ${item.file_id} does not match its durable room file`);
6615
+ }
6616
+ return file;
6617
+ }
6318
6618
  lock(roomId, work) {
6319
6619
  return this.store.mutex(roomId).runExclusive(work);
6320
6620
  }
@@ -6331,6 +6631,23 @@ var init_intake = __esm({
6331
6631
  });
6332
6632
 
6333
6633
  // src/service.ts
6634
+ function byteBoundedHistoryPage(records) {
6635
+ const page = [];
6636
+ let bytes = 2;
6637
+ for (const record of records) {
6638
+ const encoded = JSON.stringify(record);
6639
+ const nextBytes = Buffer.byteLength(encoded, "utf8") + (page.length === 0 ? 0 : 1);
6640
+ if (bytes + nextBytes > MAX_HISTORY_PAGE_BYTES) {
6641
+ if (page.length === 0) {
6642
+ throw new RangeError(`one history record exceeds the ${MAX_HISTORY_PAGE_BYTES}-byte page contract`);
6643
+ }
6644
+ break;
6645
+ }
6646
+ page.push(record);
6647
+ bytes += nextBytes;
6648
+ }
6649
+ return page;
6650
+ }
6334
6651
  function activeSeats(room) {
6335
6652
  return room.seats.filter((seat) => seat.state === "active");
6336
6653
  }
@@ -6418,6 +6735,7 @@ var init_service = __esm({
6418
6735
  const provisional = RoomSchema.parse({
6419
6736
  version: 2,
6420
6737
  room_id: roomId,
6738
+ room_name: settings.name ?? defaultRoomName(roomId),
6421
6739
  identity_name: identityName,
6422
6740
  // PacketRegistry needs the durable room directory to exist first. A
6423
6741
  // valid, explicitly provisional value lets startup resume this exact
@@ -6529,6 +6847,7 @@ var init_service = __esm({
6529
6847
  };
6530
6848
  const next = await this.store.save(RoomSchema.parse({
6531
6849
  ...room,
6850
+ room_name: settings.name ?? room.room_name,
6532
6851
  mission,
6533
6852
  ...settings.quiet_membership === void 0 ? {} : { quiet_membership: settings.quiet_membership },
6534
6853
  ...settings.status === void 0 ? {} : { status: settings.status }
@@ -6982,9 +7301,9 @@ var init_service = __esm({
6982
7301
  async history(roomId, options = {}) {
6983
7302
  const id = LowerCrockfordUlidSchema.parse(roomId);
6984
7303
  const { view, ...page } = HistoryOptionsSchema.parse(options);
6985
- const records = await this.store.read(id, page);
6986
- if (view !== "participant") return records;
6987
- return records.filter((record) => record.kind === "message").map((record) => {
7304
+ const records = await this.store.read(id, view === "participant" ? { after: page.after } : page);
7305
+ if (view !== "participant") return byteBoundedHistoryPage(records);
7306
+ const projected = records.filter((record) => record.kind === "message").map((record) => {
6988
7307
  const {
6989
7308
  author_alias,
6990
7309
  recipient_identities: _recipients,
@@ -7001,6 +7320,7 @@ var init_service = __esm({
7001
7320
  }
7002
7321
  };
7003
7322
  });
7323
+ return byteBoundedHistoryPage(projected.slice(0, page.limit ?? Number.MAX_SAFE_INTEGER));
7004
7324
  }
7005
7325
  /**
7006
7326
  * Forward-only close. Every external contact mutation is preceded by a
@@ -7304,7 +7624,7 @@ var init_service = __esm({
7304
7624
  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);
7305
7625
  const intentsByMessage = /* @__PURE__ */ new Map();
7306
7626
  for (const record of records) {
7307
- if (record.kind !== "relay_intent") continue;
7627
+ if (record.kind !== "relay_intent" || record.message_id === void 0) continue;
7308
7628
  const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
7309
7629
  intents.add(record.recipient_identity);
7310
7630
  intentsByMessage.set(record.message_id, intents);
@@ -7697,12 +8017,18 @@ var init_storage = __esm({
7697
8017
  throw this.wrap(`malformed metadata for room "${roomId}"`, error);
7698
8018
  }
7699
8019
  const room = this.isVersion1(decoded) ? this.migrateUnlocked(roomId, decoded, bytes) : RoomSchema.parse(decoded);
8020
+ if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name) {
8021
+ this.atomicMetadataWrite(this.metadataPath(roomId), room);
8022
+ }
7700
8023
  if (room.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
7701
8024
  return room;
7702
8025
  }
7703
8026
  isVersion1(decoded) {
7704
8027
  return typeof decoded === "object" && decoded !== null && decoded.version === 1;
7705
8028
  }
8029
+ persistedRoomName(decoded) {
8030
+ return typeof decoded === "object" && decoded !== null ? decoded.room_name : void 0;
8031
+ }
7706
8032
  /**
7707
8033
  * Lazy additive v1 → v2 migration (spec §7): preserve the exact pre-migration
7708
8034
  * bytes once as room.json.v1.bak, then atomically persist the v2 metadata.
@@ -8211,6 +8537,7 @@ function createServiceRoutes(service) {
8211
8537
  "room.settings": { auth: true, run: (params) => {
8212
8538
  const { room_id, ...input } = external_exports.object({
8213
8539
  room_id: external_exports.string(),
8540
+ name: external_exports.unknown().optional(),
8214
8541
  goal: external_exports.unknown().optional(),
8215
8542
  briefing: external_exports.unknown().optional(),
8216
8543
  status: external_exports.unknown().optional(),
@@ -8915,6 +9242,7 @@ __export(daemon_runtime_exports, {
8915
9242
  DaemonShutdownError: () => DaemonShutdownError,
8916
9243
  acquireDaemonLock: () => acquireDaemonLock,
8917
9244
  createDaemonControlRoutes: () => createDaemonControlRoutes,
9245
+ isIntakeNotification: () => isIntakeNotification,
8918
9246
  loadConfig: () => loadConfig,
8919
9247
  removeDaemonPid: () => removeDaemonPid,
8920
9248
  writeDaemonPid: () => writeDaemonPid
@@ -8922,6 +9250,9 @@ __export(daemon_runtime_exports, {
8922
9250
  import * as nodeFs6 from "node:fs";
8923
9251
  import { join as join7 } from "node:path";
8924
9252
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9253
+ function isIntakeNotification(event) {
9254
+ return event === "message_received" || event === "file_received" || event === "contact_accepted";
9255
+ }
8925
9256
  function createDaemonControlRoutes(control) {
8926
9257
  if (!/^[0-9a-f]{32}$/.test(control.session)) throw new TypeError("invalid daemon control session");
8927
9258
  const requireExact = (params, keys) => {
@@ -9204,7 +9535,7 @@ var init_daemon_runtime = __esm({
9204
9535
  {
9205
9536
  log: this.options.log,
9206
9537
  onNotify: (roomId, event) => {
9207
- if (event === "message_received" || event === "contact_accepted") {
9538
+ if (isIntakeNotification(event)) {
9208
9539
  this.handleNotification(roomId, event, serviceRef);
9209
9540
  }
9210
9541
  }