@ours.network/cowork 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.js CHANGED
@@ -4767,377 +4767,1007 @@ 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 refineRoomLineage(room, context) {
4797
+ const pendingIdentityName = `cowork-room-${room.room_id}`;
4798
+ const exactPacketPending = room.state === "provisioning" && room.status === "packet_pending" && room.identity_name === pendingIdentityName && room.invites.length === 0 && room.seats.length === 0 && room.activated_at === void 0 && room.closed_at === void 0;
4799
+ if (room.identity_cid === "" && !exactPacketPending) {
4800
+ context.addIssue({
4801
+ code: external_exports.ZodIssueCode.custom,
4802
+ path: ["identity_cid"],
4803
+ message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
4804
+ });
4805
+ }
4806
+ if (room.identity_cid !== "" && room.status === "packet_pending") {
4807
+ context.addIssue({
4808
+ code: external_exports.ZodIssueCode.custom,
4809
+ path: ["status"],
4810
+ message: "packet_pending status requires an empty identity_cid"
4811
+ });
4812
+ }
4813
+ const pendingByRecovery = /* @__PURE__ */ new Map();
4814
+ for (const [index, invite] of room.invites.entries()) {
4815
+ if (invite.recovery_of === void 0) continue;
4816
+ const recoveryOf = invite.recovery_of;
4817
+ const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
4818
+ const validSourceState = invite.state === "receipt_pending" ? source?.state === "replacement_required" : invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required" ? source?.state === "revoked" : invite.state === "revoked" ? invite.recovery_confirmed === true ? source?.state === "revoked" : source?.state === "replacement_required" || source?.state === "revoked" : false;
4819
+ if (!source || source.invite_id === invite.invite_id || !validSourceState) {
4820
+ context.addIssue({
4821
+ code: external_exports.ZodIssueCode.custom,
4822
+ path: ["invites", index, "recovery_of"],
4823
+ message: "recovery_of must point to a source invite in the state required by this recovery lineage"
4824
+ });
4825
+ } else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
4826
+ context.addIssue({
4827
+ code: external_exports.ZodIssueCode.custom,
4828
+ path: ["invites", index],
4829
+ message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
4830
+ });
4802
4831
  }
4803
- if (directoryFd !== void 0) {
4804
- try {
4805
- ops.closeSync(directoryFd);
4806
- } catch {
4832
+ if (invite.state === "receipt_pending") {
4833
+ const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
4834
+ pendingByRecovery.set(recoveryOf, count);
4835
+ if (count > 1) {
4836
+ context.addIssue({
4837
+ code: external_exports.ZodIssueCode.custom,
4838
+ path: ["invites", index, "recovery_of"],
4839
+ message: "only one receipt_pending invite may exist per recovery_of pointer"
4840
+ });
4807
4841
  }
4808
4842
  }
4809
- try {
4810
- ops.rmSync(temp, { force: true });
4811
- } catch {
4812
- }
4813
- throw new PacketPersistenceError(`failed to durably persist ${target}`, { cause: error });
4814
4843
  }
4815
4844
  }
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()
4845
+ function migrateRoomV1(room, mintParticipantId) {
4846
+ return RoomSchema.parse({
4847
+ ...room,
4848
+ version: 2,
4849
+ mission: { ...room.mission, briefing_version: 1 },
4850
+ role_briefings: {},
4851
+ anonymous: false,
4852
+ quiet_membership: false,
4853
+ membership_epoch: 0,
4854
+ seats: room.seats.map((seat) => ({
4855
+ ...seat,
4856
+ participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
4857
+ state: "active"
4858
+ }))
4859
+ });
4860
+ }
4861
+ function refineRelaySubject(record, context) {
4862
+ if (record.kind !== "relay_intent" && record.kind !== "relay_result") return;
4863
+ if (record.message_id === void 0 === (record.file_id === void 0)) {
4864
+ context.addIssue({
4865
+ code: external_exports.ZodIssueCode.custom,
4866
+ path: ["message_id"],
4867
+ message: "relay records require exactly one of message_id or file_id"
4830
4868
  });
4831
4869
  }
4832
- return output;
4833
4870
  }
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()));
4871
+ function refineFileRecord(record, context) {
4872
+ if (record.kind !== "file" || record.data_base64 === void 0) return;
4873
+ const bytes = Buffer.from(record.data_base64, "base64");
4874
+ if (bytes.toString("base64") !== record.data_base64) {
4875
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["data_base64"], message: "file bytes must use canonical base64" });
4841
4876
  }
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());
4877
+ if (bytes.length !== record.size) {
4878
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["size"], message: "file size must match decoded bytes" });
4854
4879
  }
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 });
4880
+ const digest = createHash("sha256").update(bytes).digest("hex");
4881
+ if (digest !== record.sha256) {
4882
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["sha256"], message: "file sha256 must match decoded bytes" });
4863
4883
  }
4864
- if (typeof decoded !== "boolean") throw new Error(`${label} must be a boolean`);
4865
- return decoded;
4866
4884
  }
4867
- function nilString(value) {
4868
- return value.IsNil() ? "" : value.Visualize();
4869
- }
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);
4885
+ function refineMessageCategory(message, context) {
4886
+ const requires = (field, present) => {
4887
+ if (present && message[field] === void 0) {
4888
+ context.addIssue({
4889
+ code: external_exports.ZodIssueCode.custom,
4890
+ path: [field],
4891
+ message: `${message.category} messages require ${field}`
4892
+ });
4893
+ }
4894
+ if (!present && message[field] !== void 0) {
4895
+ context.addIssue({
4896
+ code: external_exports.ZodIssueCode.custom,
4897
+ path: [field],
4898
+ message: `${field} is forbidden on ${message.category} messages`
4899
+ });
4900
+ }
4901
+ };
4902
+ requires("briefing_role", message.category === "role_briefing");
4903
+ requires("membership", message.category === "membership");
4904
+ if (message.category === "role_briefing" && message.briefing_version === void 0) {
4905
+ context.addIssue({
4906
+ code: external_exports.ZodIssueCode.custom,
4907
+ path: ["briefing_version"],
4908
+ message: "role_briefing messages require briefing_version"
4909
+ });
4876
4910
  }
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);
4911
+ if (message.category === "chat" || message.category === "membership") {
4912
+ if (message.briefing_version !== void 0) {
4913
+ context.addIssue({
4914
+ code: external_exports.ZodIssueCode.custom,
4915
+ path: ["briefing_version"],
4916
+ message: `briefing_version is forbidden on ${message.category} messages`
4917
+ });
4918
+ }
4901
4919
  }
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
4920
  }
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"() {
4921
+ var MAX_TEXT_BYTES, MAX_FILE_BYTES, MAX_HISTORY_PAGE_BYTES, MAX_MANAGEMENT_RESPONSE_BYTES, MAX_FILE_NAME_BYTES, MAX_MIME_BYTES, MAX_ROLE_BYTES, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoleSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
4922
+ var init_contracts = __esm({
4923
+ "src/contracts.ts"() {
4925
4924
  "use strict";
4926
- await init_adapt();
4927
- PacketPersistenceError = class extends Error {
4928
- constructor(message, options) {
4929
- super(message, options);
4930
- this.name = "PacketPersistenceError";
4925
+ init_zod();
4926
+ MAX_TEXT_BYTES = 262144;
4927
+ MAX_FILE_BYTES = 2 * 1024 * 1024;
4928
+ MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
4929
+ MAX_MANAGEMENT_RESPONSE_BYTES = MAX_HISTORY_PAGE_BYTES + 1024 * 1024;
4930
+ MAX_FILE_NAME_BYTES = 255;
4931
+ MAX_MIME_BYTES = 255;
4932
+ MAX_ROLE_BYTES = 256;
4933
+ NonEmptyStringSchema = external_exports.string().min(1);
4934
+ PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
4935
+ LowerCrockfordUlidSchema = external_exports.string().regex(
4936
+ /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4937
+ "must be a 26-character lowercase Crockford ULID"
4938
+ );
4939
+ Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
4940
+ RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4941
+ MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
4942
+ MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
4943
+ FileNameSchema = utf8Bounded("file name", MAX_FILE_NAME_BYTES).refine((value) => value !== "." && value !== "..", "file name must not be a relative path token").refine((value) => !/[\x00/\\]/.test(value), "file name must be a single path-free name");
4944
+ FileMimeSchema = external_exports.string().refine(
4945
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_MIME_BYTES,
4946
+ `file MIME metadata must be at most ${MAX_MIME_BYTES} UTF-8 bytes`
4947
+ );
4948
+ RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
4949
+ SeatStateSchema = external_exports.enum(["active", "removed"]);
4950
+ InviteModeSchema = external_exports.enum(["one_time", "public"]);
4951
+ DEFAULT_ROLE = "Participant";
4952
+ InviteStateSchema = external_exports.enum([
4953
+ "live",
4954
+ "consumed",
4955
+ "revoked",
4956
+ "replacement_required",
4957
+ "receipt_pending"
4958
+ ]);
4959
+ RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
4960
+ SeatV1Schema = external_exports.object({
4961
+ identity: NonEmptyStringSchema,
4962
+ display_name: NonEmptyStringSchema,
4963
+ role: RoleSchema,
4964
+ invite_id: NonEmptyStringSchema,
4965
+ accepted_at: Rfc3339Schema
4966
+ }).strict();
4967
+ SeatSchema = external_exports.object({
4968
+ identity: NonEmptyStringSchema,
4969
+ display_name: NonEmptyStringSchema,
4970
+ role: RoleSchema,
4971
+ invite_id: NonEmptyStringSchema,
4972
+ accepted_at: Rfc3339Schema,
4973
+ participant_id: LowerCrockfordUlidSchema,
4974
+ state: SeatStateSchema,
4975
+ alias: NonEmptyStringSchema.optional(),
4976
+ removed_at: Rfc3339Schema.optional(),
4977
+ removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
4978
+ replaces_seat: LowerCrockfordUlidSchema.optional(),
4979
+ bounced_at: Rfc3339Schema.optional()
4980
+ }).strict().superRefine((seat, context) => {
4981
+ if (seat.state === "removed") {
4982
+ if (seat.removed_at === void 0) {
4983
+ context.addIssue({
4984
+ code: external_exports.ZodIssueCode.custom,
4985
+ path: ["removed_at"],
4986
+ message: "removed seats require removed_at"
4987
+ });
4988
+ }
4989
+ if (seat.removed_epoch === void 0) {
4990
+ context.addIssue({
4991
+ code: external_exports.ZodIssueCode.custom,
4992
+ path: ["removed_epoch"],
4993
+ message: "removed seats require removed_epoch"
4994
+ });
4995
+ }
4996
+ } else {
4997
+ for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
4998
+ if (seat[field] !== void 0) {
4999
+ context.addIssue({
5000
+ code: external_exports.ZodIssueCode.custom,
5001
+ path: [field],
5002
+ message: `${field} is reserved for removed seats`
5003
+ });
5004
+ }
5005
+ }
4931
5006
  }
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 ?? (() => {
5007
+ });
5008
+ RoomInviteSchema = external_exports.object({
5009
+ invite_id: NonEmptyStringSchema,
5010
+ mode: InviteModeSchema,
5011
+ role: RoleSchema,
5012
+ min_accepts: PositiveSafeIntegerSchema,
5013
+ accepted_cids: external_exports.array(NonEmptyStringSchema),
5014
+ state: InviteStateSchema,
5015
+ recovery_of: NonEmptyStringSchema.optional(),
5016
+ recovery_confirmed: external_exports.boolean().optional(),
5017
+ created_at: Rfc3339Schema,
5018
+ replaces_seat: LowerCrockfordUlidSchema.optional()
5019
+ }).strict().superRefine((invite, context) => {
5020
+ if (invite.mode === "one_time" && invite.min_accepts !== 1) {
5021
+ context.addIssue({
5022
+ code: external_exports.ZodIssueCode.custom,
5023
+ path: ["min_accepts"],
5024
+ message: "one_time invites require min_accepts === 1"
4952
5025
  });
4953
- this.seed = options.seed ?? (() => randomBytes(24).toString("hex"));
4954
- this.beforeExpose = options.beforeExpose ?? (() => {
5026
+ }
5027
+ if (invite.state === "receipt_pending" && invite.recovery_of === void 0) {
5028
+ context.addIssue({
5029
+ code: external_exports.ZodIssueCode.custom,
5030
+ path: ["recovery_of"],
5031
+ message: "receipt_pending invites require recovery_of"
4955
5032
  });
4956
- this.onNotify = options.onNotify ?? (() => {
5033
+ }
5034
+ if (invite.recovery_of === void 0 && invite.recovery_confirmed !== void 0) {
5035
+ context.addIssue({
5036
+ code: external_exports.ZodIssueCode.custom,
5037
+ path: ["recovery_confirmed"],
5038
+ message: "recovery_confirmed is forbidden without recovery_of"
4957
5039
  });
4958
- this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
5040
+ }
5041
+ if (invite.recovery_of !== void 0 && invite.recovery_confirmed === void 0) {
5042
+ context.addIssue({
5043
+ code: external_exports.ZodIssueCode.custom,
5044
+ path: ["recovery_confirmed"],
5045
+ message: "recovery_confirmed is required with recovery_of"
4959
5046
  });
4960
- this.stagingName = options.stagingName ?? (() => `live.staging-${randomBytes(16).toString("hex")}`);
4961
5047
  }
4962
- get size() {
4963
- return this.packets.size;
5048
+ if (invite.state === "receipt_pending" && invite.recovery_confirmed !== false) {
5049
+ context.addIssue({
5050
+ code: external_exports.ZodIssueCode.custom,
5051
+ path: ["recovery_confirmed"],
5052
+ message: "receipt_pending recovery lineage must be unconfirmed"
5053
+ });
4964
5054
  }
4965
- get(roomId) {
4966
- return this.packets.get(roomId);
5055
+ if (invite.recovery_of !== void 0 && (invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required") && invite.recovery_confirmed !== true) {
5056
+ context.addIssue({
5057
+ code: external_exports.ZodIssueCode.custom,
5058
+ path: ["recovery_confirmed"],
5059
+ message: "live, consumed, and replacement_required recovery lineage must be confirmed"
5060
+ });
4967
5061
  }
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);
5062
+ if (invite.state === "receipt_pending" && invite.accepted_cids.length > 0) {
5063
+ context.addIssue({
5064
+ code: external_exports.ZodIssueCode.custom,
5065
+ path: ["accepted_cids"],
5066
+ message: "receipt_pending invites cannot have accepted CIDs"
5067
+ });
5068
+ }
5069
+ });
5070
+ MissionV1Schema = external_exports.object({
5071
+ goal: MissionTextSchema,
5072
+ briefing: MissionTextSchema
5073
+ }).strict();
5074
+ MissionSchema = external_exports.object({
5075
+ goal: MissionTextSchema,
5076
+ briefing: MissionTextSchema,
5077
+ briefing_version: PositiveSafeIntegerSchema
5078
+ }).strict();
5079
+ RoleBriefingSchema = external_exports.object({
5080
+ text: MissionTextSchema,
5081
+ version: PositiveSafeIntegerSchema,
5082
+ updated_at: Rfc3339Schema
5083
+ }).strict();
5084
+ RoomCommonShape = {
5085
+ room_id: LowerCrockfordUlidSchema,
5086
+ identity_name: NonEmptyStringSchema,
5087
+ identity_cid: external_exports.string(),
5088
+ state: RoomStateSchema,
5089
+ status: NonEmptyStringSchema.optional(),
5090
+ invites: external_exports.array(RoomInviteSchema),
5091
+ created_at: Rfc3339Schema,
5092
+ activated_at: Rfc3339Schema.optional(),
5093
+ closed_at: Rfc3339Schema.optional()
5094
+ };
5095
+ RoomV1Schema = external_exports.object({
5096
+ ...RoomCommonShape,
5097
+ version: external_exports.literal(1),
5098
+ mission: MissionV1Schema,
5099
+ seats: external_exports.array(SeatV1Schema)
5100
+ }).strict().superRefine(refineRoomLineage);
5101
+ RoomSchema = external_exports.object({
5102
+ ...RoomCommonShape,
5103
+ version: external_exports.literal(2),
5104
+ mission: MissionSchema,
5105
+ role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
5106
+ anonymous: external_exports.boolean(),
5107
+ quiet_membership: external_exports.boolean(),
5108
+ membership_epoch: external_exports.number().int().nonnegative().safe(),
5109
+ seats: external_exports.array(SeatSchema)
5110
+ }).strict().superRefine((room, context) => {
5111
+ refineRoomLineage(room, context);
5112
+ const byParticipant = /* @__PURE__ */ new Map();
5113
+ const activeAliases = /* @__PURE__ */ new Set();
5114
+ for (const [index, seat] of room.seats.entries()) {
5115
+ if (byParticipant.has(seat.participant_id)) {
5116
+ context.addIssue({
5117
+ code: external_exports.ZodIssueCode.custom,
5118
+ path: ["seats", index, "participant_id"],
5119
+ message: "participant_id must be unique within the room"
5120
+ });
5121
+ }
5122
+ byParticipant.set(seat.participant_id, seat);
5123
+ if (room.anonymous && seat.alias === void 0) {
5124
+ context.addIssue({
5125
+ code: external_exports.ZodIssueCode.custom,
5126
+ path: ["seats", index, "alias"],
5127
+ message: "anonymous rooms require an alias on every seat"
5128
+ });
5129
+ }
5130
+ if (!room.anonymous && seat.alias !== void 0) {
5131
+ context.addIssue({
5132
+ code: external_exports.ZodIssueCode.custom,
5133
+ path: ["seats", index, "alias"],
5134
+ message: "aliases are reserved for anonymous rooms"
5135
+ });
5136
+ }
5137
+ if (seat.state === "active" && seat.alias !== void 0) {
5138
+ if (activeAliases.has(seat.alias)) {
5139
+ context.addIssue({
5140
+ code: external_exports.ZodIssueCode.custom,
5141
+ path: ["seats", index, "alias"],
5142
+ message: "active seats must hold distinct aliases"
5143
+ });
4977
5144
  }
5145
+ activeAliases.add(seat.alias);
4978
5146
  }
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
- }
5011
- }
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);
5147
+ if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
5148
+ context.addIssue({
5149
+ code: external_exports.ZodIssueCode.custom,
5150
+ path: ["seats", index, "removed_epoch"],
5151
+ message: "removed_epoch cannot exceed the room membership_epoch"
5051
5152
  });
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
5153
  }
5074
5154
  }
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);
5155
+ for (const [index, seat] of room.seats.entries()) {
5156
+ if (seat.replaces_seat === void 0) continue;
5157
+ const predecessor = byParticipant.get(seat.replaces_seat);
5158
+ if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
5159
+ context.addIssue({
5160
+ code: external_exports.ZodIssueCode.custom,
5161
+ path: ["seats", index, "replaces_seat"],
5162
+ message: "replaces_seat must reference a removed seat in this room"
5163
+ });
5164
+ continue;
5081
5165
  }
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;
5166
+ if (predecessor.role !== seat.role) {
5167
+ context.addIssue({
5168
+ code: external_exports.ZodIssueCode.custom,
5169
+ path: ["seats", index, "role"],
5170
+ message: "a replacement seat must inherit the predecessor role"
5171
+ });
5091
5172
  }
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
- );
5173
+ if (room.anonymous && seat.alias !== predecessor.alias) {
5174
+ context.addIssue({
5175
+ code: external_exports.ZodIssueCode.custom,
5176
+ path: ["seats", index, "alias"],
5177
+ message: "an anonymous replacement seat must inherit the predecessor alias"
5178
+ });
5098
5179
  }
5099
- return residue;
5100
5180
  }
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);
5181
+ });
5182
+ CreateRoomInputSchema = external_exports.object({
5183
+ goal: MissionTextSchema,
5184
+ briefing: MissionTextSchema,
5185
+ anonymous: external_exports.boolean().optional(),
5186
+ quiet_membership: external_exports.boolean().optional()
5187
+ }).strict();
5188
+ UpdateRoomInputSchema = external_exports.object({
5189
+ goal: MissionTextSchema.optional(),
5190
+ briefing: MissionTextSchema.optional(),
5191
+ status: NonEmptyStringSchema.optional(),
5192
+ quiet_membership: external_exports.boolean().optional()
5193
+ }).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
5194
+ RoleBriefingSetInputSchema = external_exports.object({
5195
+ role: RoleSchema,
5196
+ text: MissionTextSchema
5197
+ }).strict();
5198
+ RoleBriefingDeleteInputSchema = external_exports.object({
5199
+ role: RoleSchema
5200
+ }).strict();
5201
+ PostMessageInputSchema = external_exports.object({
5202
+ text: MessageTextSchema
5203
+ }).strict();
5204
+ AuthorSnapshotSchema = external_exports.object({
5205
+ identity: NonEmptyStringSchema,
5206
+ display_name: NonEmptyStringSchema,
5207
+ role: RoleSchema
5208
+ }).strict();
5209
+ RecordCommonShape = {
5210
+ version: external_exports.literal(1),
5211
+ room_id: LowerCrockfordUlidSchema,
5212
+ seq: PositiveSafeIntegerSchema,
5213
+ record_id: NonEmptyStringSchema,
5214
+ at: Rfc3339Schema
5215
+ };
5216
+ AppendCommonShape = {
5217
+ version: external_exports.literal(1),
5218
+ room_id: LowerCrockfordUlidSchema,
5219
+ at: Rfc3339Schema
5220
+ };
5221
+ MembershipNoticeSchema = external_exports.object({
5222
+ action: external_exports.enum(["remove"]),
5223
+ alias: NonEmptyStringSchema.optional(),
5224
+ role: RoleSchema.optional(),
5225
+ epoch: external_exports.number().int().nonnegative().safe()
5226
+ }).strict();
5227
+ AuthorAliasSchema = external_exports.object({
5228
+ participant_id: LowerCrockfordUlidSchema,
5229
+ alias: NonEmptyStringSchema
5230
+ }).strict();
5231
+ MessageShape = {
5232
+ kind: external_exports.literal("message"),
5233
+ message_id: LowerCrockfordUlidSchema,
5234
+ author: AuthorSnapshotSchema,
5235
+ author_alias: AuthorAliasSchema.optional(),
5236
+ category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
5237
+ briefing_role: RoleSchema.optional(),
5238
+ briefing_version: PositiveSafeIntegerSchema.optional(),
5239
+ membership: MembershipNoticeSchema.optional(),
5240
+ text: MessageTextSchema,
5241
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5242
+ const seen = /* @__PURE__ */ new Set();
5243
+ for (const [index, identity] of identities.entries()) {
5244
+ if (seen.has(identity)) {
5245
+ context.addIssue({
5246
+ code: external_exports.ZodIssueCode.custom,
5247
+ path: [index],
5248
+ message: "recipient identities must be unique"
5249
+ });
5110
5250
  }
5251
+ seen.add(identity);
5111
5252
  }
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;
5253
+ }),
5254
+ source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5255
+ source_wire_id: NonEmptyStringSchema.optional()
5256
+ };
5257
+ RelayIntentShape = {
5258
+ kind: external_exports.literal("relay_intent"),
5259
+ message_id: LowerCrockfordUlidSchema.optional(),
5260
+ file_id: LowerCrockfordUlidSchema.optional(),
5261
+ recipient_identity: NonEmptyStringSchema
5262
+ };
5263
+ RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
5264
+ RelayResultShape = {
5265
+ kind: external_exports.literal("relay_result"),
5266
+ intent_record_id: NonEmptyStringSchema,
5267
+ message_id: LowerCrockfordUlidSchema.optional(),
5268
+ file_id: LowerCrockfordUlidSchema.optional(),
5269
+ recipient_identity: NonEmptyStringSchema,
5270
+ status: RelayResultStatusSchema,
5271
+ wire_id: NonEmptyStringSchema.optional(),
5272
+ metadata_wire_id: NonEmptyStringSchema.optional()
5273
+ };
5274
+ FileShape = {
5275
+ kind: external_exports.literal("file"),
5276
+ file_id: LowerCrockfordUlidSchema,
5277
+ author: AuthorSnapshotSchema,
5278
+ author_alias: AuthorAliasSchema.optional(),
5279
+ filename: FileNameSchema,
5280
+ mime: FileMimeSchema,
5281
+ size: external_exports.number().int().nonnegative().max(MAX_FILE_BYTES),
5282
+ sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
5283
+ data_base64: external_exports.string(),
5284
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5285
+ const seen = /* @__PURE__ */ new Set();
5286
+ for (const [index, identity] of identities.entries()) {
5287
+ if (seen.has(identity)) {
5288
+ context.addIssue({
5289
+ code: external_exports.ZodIssueCode.custom,
5290
+ path: [index],
5291
+ message: "recipient identities must be unique"
5292
+ });
5293
+ }
5294
+ seen.add(identity);
5131
5295
  }
5296
+ }),
5297
+ source_file_id: external_exports.number().int().nonnegative().safe(),
5298
+ source_wire_id: NonEmptyStringSchema.optional()
5299
+ };
5300
+ MembershipIntentShape = {
5301
+ kind: external_exports.literal("membership_intent"),
5302
+ action: external_exports.enum(["remove"]),
5303
+ participant_id: LowerCrockfordUlidSchema,
5304
+ recipient_identity: NonEmptyStringSchema,
5305
+ role: RoleSchema,
5306
+ alias: NonEmptyStringSchema.optional(),
5307
+ epoch: PositiveSafeIntegerSchema,
5308
+ notify: external_exports.boolean()
5309
+ };
5310
+ MembershipResultShape = {
5311
+ kind: external_exports.literal("membership_result"),
5312
+ intent_record_id: NonEmptyStringSchema,
5313
+ participant_id: LowerCrockfordUlidSchema,
5314
+ status: RelayStatusSchema,
5315
+ notified: external_exports.boolean(),
5316
+ key_material_retained: external_exports.literal(true),
5317
+ uncertain_after_restart: external_exports.literal(true).optional()
5318
+ };
5319
+ CloseNoticeIntentShape = {
5320
+ kind: external_exports.literal("close_notice_intent"),
5321
+ recipient_identity: NonEmptyStringSchema
5322
+ };
5323
+ CloseNoticeResultShape = {
5324
+ kind: external_exports.literal("close_notice_result"),
5325
+ intent_record_id: NonEmptyStringSchema,
5326
+ recipient_identity: NonEmptyStringSchema,
5327
+ status: RelayStatusSchema,
5328
+ notified: external_exports.boolean(),
5329
+ key_material_retained: external_exports.literal(true),
5330
+ uncertain_after_restart: external_exports.literal(true).optional()
5331
+ };
5332
+ MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
5333
+ FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
5334
+ RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5335
+ RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5336
+ MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
5337
+ MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
5338
+ CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
5339
+ CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
5340
+ RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5341
+ MessageRecordSchema,
5342
+ FileRecordSchema,
5343
+ RelayIntentRecordSchema,
5344
+ RelayResultRecordSchema,
5345
+ MembershipIntentRecordSchema,
5346
+ MembershipResultRecordSchema,
5347
+ CloseNoticeIntentRecordSchema,
5348
+ CloseNoticeResultRecordSchema
5349
+ ]);
5350
+ CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5351
+ if (record.record_id !== `${record.room_id}:${record.seq}`) {
5352
+ context.addIssue({
5353
+ code: external_exports.ZodIssueCode.custom,
5354
+ path: ["record_id"],
5355
+ message: 'record_id must equal room_id + ":" + seq'
5356
+ });
5132
5357
  }
5133
- packetName(roomId) {
5134
- return `cowork-room-${roomId}`;
5358
+ if (record.kind === "message") refineMessageCategory(record, context);
5359
+ refineRelaySubject(record, context);
5360
+ refineFileRecord(record, context);
5361
+ });
5362
+ AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5363
+ external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
5364
+ external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
5365
+ external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5366
+ external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5367
+ external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
5368
+ external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
5369
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
5370
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
5371
+ ]).superRefine((record, context) => {
5372
+ if (record.kind === "message") refineMessageCategory(record, context);
5373
+ refineRelaySubject(record, context);
5374
+ refineFileRecord(record, context);
5375
+ });
5376
+ }
5377
+ });
5378
+
5379
+ // src/packets.ts
5380
+ import { randomBytes } from "node:crypto";
5381
+ import * as nodeFs2 from "node:fs";
5382
+ import { dirname as dirname3, join as join3 } from "node:path";
5383
+ function atomicWriteFileSync(target, bytes, ops = nodeFs2) {
5384
+ const temp = `${target}.tmp-${process.pid}-${temporarySequence++}`;
5385
+ let fileFd;
5386
+ let directoryFd;
5387
+ try {
5388
+ fileFd = ops.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_TRUNC | nodeFs2.constants.O_WRONLY, 384);
5389
+ ops.fchmodSync(fileFd, 384);
5390
+ let offset = 0;
5391
+ while (offset < bytes.byteLength) {
5392
+ const written = ops.writeSync(fileFd, bytes, offset, bytes.byteLength - offset, null);
5393
+ if (written <= 0) throw new Error(`short write while persisting ${target}`);
5394
+ offset += written;
5395
+ }
5396
+ ops.fsyncSync(fileFd);
5397
+ ops.closeSync(fileFd);
5398
+ fileFd = void 0;
5399
+ ops.renameSync(temp, target);
5400
+ ops.chmodSync(target, 384);
5401
+ directoryFd = ops.openSync(dirname3(target), nodeFs2.constants.O_RDONLY);
5402
+ ops.fsyncSync(directoryFd);
5403
+ ops.closeSync(directoryFd);
5404
+ directoryFd = void 0;
5405
+ } catch (error) {
5406
+ if (fileFd !== void 0) {
5407
+ try {
5408
+ ops.closeSync(fileFd);
5409
+ } catch {
5135
5410
  }
5136
- roomDir(roomId) {
5137
- return join3(this.stateDir, "rooms", roomId);
5411
+ }
5412
+ if (directoryFd !== void 0) {
5413
+ try {
5414
+ ops.closeSync(directoryFd);
5415
+ } catch {
5138
5416
  }
5139
- liveDir(roomId) {
5140
- return join3(this.roomDir(roomId), "live");
5417
+ }
5418
+ try {
5419
+ ops.rmSync(temp, { force: true });
5420
+ } catch {
5421
+ }
5422
+ throw new PacketPersistenceError(`failed to durably persist ${target}`, { cause: error });
5423
+ }
5424
+ }
5425
+ function renderInbox(value) {
5426
+ const output = [];
5427
+ if (value.IsNil()) return output;
5428
+ for (let index = 0; ; index += 1) {
5429
+ const message = value.Reduce(index);
5430
+ if (message.IsNil()) break;
5431
+ output.push({
5432
+ msg_id: Number(message.Reduce("msg_id").Visualize()),
5433
+ sender_id: message.Reduce("sender_id").Visualize(),
5434
+ sender_name: message.Reduce("sender_name").Visualize(),
5435
+ text: message.Reduce("text").Visualize(),
5436
+ date: adaptTimeToRfc3339(message.Reduce("date").Visualize()),
5437
+ status: message.Reduce("status").Visualize(),
5438
+ wire_id: message.Reduce("wire_id").Visualize()
5439
+ });
5440
+ }
5441
+ return output;
5442
+ }
5443
+ function renderFileInbox(value) {
5444
+ const output = [];
5445
+ if (value.IsNil()) return output;
5446
+ for (let index = 0; ; index += 1) {
5447
+ const file = value.Reduce(index);
5448
+ if (file.IsNil()) break;
5449
+ output.push({
5450
+ file_id: Number(file.Reduce("file_id").Visualize()),
5451
+ sender_id: file.Reduce("sender_id").Visualize(),
5452
+ sender_name: file.Reduce("sender_name").Visualize(),
5453
+ filename: file.Reduce("filename").Visualize(),
5454
+ mime: file.Reduce("mime").Visualize(),
5455
+ data: Buffer.from(file.Reduce("data").GetBinary()),
5456
+ date: adaptTimeToRfc3339(file.Reduce("date").Visualize()),
5457
+ status: file.Reduce("status").Visualize(),
5458
+ wire_id: file.Reduce("wire_id").Visualize()
5459
+ });
5460
+ }
5461
+ return output;
5462
+ }
5463
+ function renderIntegerArray(value) {
5464
+ const output = [];
5465
+ if (value.IsNil()) return output;
5466
+ for (let index = 0; ; index += 1) {
5467
+ const item = value.Reduce(index);
5468
+ if (item.IsNil()) break;
5469
+ output.push(Number(item.Visualize()));
5470
+ }
5471
+ return output;
5472
+ }
5473
+ function dictionaryEntries(value) {
5474
+ if (value.IsNil()) return [];
5475
+ return value.GetKeys().map((key) => [key.Visualize(), value.Reduce(key)]);
5476
+ }
5477
+ function booleanValue(value) {
5478
+ if (value.IsNil()) return false;
5479
+ try {
5480
+ return value.GetBoolean();
5481
+ } catch {
5482
+ return /true/i.test(value.Visualize());
5483
+ }
5484
+ }
5485
+ function strictBooleanValue(value, label) {
5486
+ if (value.IsNil()) throw new Error(`${label} must be a boolean`);
5487
+ let decoded;
5488
+ try {
5489
+ decoded = value.GetBoolean();
5490
+ } catch (error) {
5491
+ throw new Error(`${label} must be a boolean`, { cause: error });
5492
+ }
5493
+ if (typeof decoded !== "boolean") throw new Error(`${label} must be a boolean`);
5494
+ return decoded;
5495
+ }
5496
+ function nilString(value) {
5497
+ return value.IsNil() ? "" : value.Visualize();
5498
+ }
5499
+ function adaptTimeToRfc3339(value) {
5500
+ 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);
5501
+ if (rfc3339) {
5502
+ const [, year2, month2, day2, hour2, minute2, second2, fraction2 = "", sign2, offsetHour2 = "0", offsetMinute = "0"] = rfc3339;
5503
+ if (sign2 === "-" && offsetHour2 === "00" && offsetMinute === "00") return invalidAdaptTime(value);
5504
+ return canonicalUtcTime(value, year2, month2, day2, hour2, minute2, second2, fraction2, sign2, offsetHour2, offsetMinute);
5505
+ }
5506
+ 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);
5507
+ if (!native) return invalidAdaptTime(value);
5508
+ const [, year, month, day, hour, minute, second, fraction = "", sign, offsetHour = "0"] = native;
5509
+ if (sign === "-" && offsetHour === "0") return invalidAdaptTime(value);
5510
+ return canonicalUtcTime(value, year, month, day, hour, minute, second, fraction, sign, offsetHour, "0");
5511
+ }
5512
+ function canonicalUtcTime(source, yearText, monthText, dayText, hourText, minuteText, secondText, fraction, offsetSign, offsetHourText, offsetMinuteText) {
5513
+ const year = Number(yearText);
5514
+ const month = Number(monthText);
5515
+ const day = Number(dayText);
5516
+ const hour = Number(hourText);
5517
+ const minute = Number(minuteText);
5518
+ const second = Number(secondText);
5519
+ const offsetHour = Number(offsetHourText);
5520
+ const offsetMinute = Number(offsetMinuteText);
5521
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
5522
+ const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
5523
+ if (month < 1 || month > 12 || day < 1 || day > monthDays[month - 1] || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) return invalidAdaptTime(source);
5524
+ const milliseconds = Number(fraction.slice(0, 3).padEnd(3, "0"));
5525
+ const local = /* @__PURE__ */ new Date(0);
5526
+ local.setUTCFullYear(year, month - 1, day);
5527
+ local.setUTCHours(hour, minute, second, milliseconds);
5528
+ if (local.getUTCFullYear() !== year || local.getUTCMonth() !== month - 1 || local.getUTCDate() !== day || local.getUTCHours() !== hour || local.getUTCMinutes() !== minute || local.getUTCSeconds() !== second) {
5529
+ return invalidAdaptTime(source);
5530
+ }
5531
+ const direction = offsetSign === "-" ? -1 : 1;
5532
+ const offsetMilliseconds = direction * (offsetHour * 60 + offsetMinute) * 6e4;
5533
+ const canonical = new Date(local.getTime() - offsetMilliseconds).toISOString();
5534
+ if (!/^\d{4}-/.test(canonical)) return invalidAdaptTime(source);
5535
+ return canonical;
5536
+ }
5537
+ function invalidAdaptTime(value) {
5538
+ throw new Error(`unexpected ADAPT time visualization: ${value}`);
5539
+ }
5540
+ function inviteMode(value) {
5541
+ const normalized = value.replace(/^\$/, "");
5542
+ if (normalized === "one_time" || normalized === "public") return normalized;
5543
+ throw new Error(`unexpected invite mode: ${value}`);
5544
+ }
5545
+ function exportSigningSecret(packet) {
5546
+ return withScope((lifetime) => Buffer.from(packet.readonlyTx("::actor::export_signing_secret", lifetime).Serialize()).toString("hex"));
5547
+ }
5548
+ function validateRoomId(roomId) {
5549
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(roomId)) throw new Error(`invalid room id: ${roomId}`);
5550
+ }
5551
+ var PacketPersistenceError, temporarySequence, PacketRegistry, HostedRoomPacket;
5552
+ var init_packets = __esm({
5553
+ async "src/packets.ts"() {
5554
+ "use strict";
5555
+ await init_adapt();
5556
+ init_contracts();
5557
+ PacketPersistenceError = class extends Error {
5558
+ constructor(message, options) {
5559
+ super(message, options);
5560
+ this.name = "PacketPersistenceError";
5561
+ }
5562
+ };
5563
+ temporarySequence = 0;
5564
+ PacketRegistry = class {
5565
+ packets = /* @__PURE__ */ new Map();
5566
+ host;
5567
+ stateDir;
5568
+ fs;
5569
+ persistence;
5570
+ log;
5571
+ seed;
5572
+ beforeExpose;
5573
+ onNotify;
5574
+ provisioningCheckpoint;
5575
+ stagingName;
5576
+ constructor(host, stateDir, options = {}) {
5577
+ this.host = host;
5578
+ this.stateDir = stateDir;
5579
+ this.fs = options.fs ?? nodeFs2;
5580
+ this.persistence = options.persistence ?? this.fs;
5581
+ this.log = options.log ?? (() => {
5582
+ });
5583
+ this.seed = options.seed ?? (() => randomBytes(24).toString("hex"));
5584
+ this.beforeExpose = options.beforeExpose ?? (() => {
5585
+ });
5586
+ this.onNotify = options.onNotify ?? (() => {
5587
+ });
5588
+ this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
5589
+ });
5590
+ this.stagingName = options.stagingName ?? (() => `live.staging-${randomBytes(16).toString("hex")}`);
5591
+ }
5592
+ get size() {
5593
+ return this.packets.size;
5594
+ }
5595
+ get(roomId) {
5596
+ return this.packets.get(roomId);
5597
+ }
5598
+ async create(roomId, identityName = `cowork-room-${roomId}`, bio = `ours-cowork mission room ${roomId}`) {
5599
+ validateRoomId(roomId);
5600
+ if (this.packets.has(roomId)) throw new Error(`room packet "${roomId}" is already hosted`);
5601
+ const liveDir = this.liveDir(roomId);
5602
+ if (this.hasRestorableState(roomId)) {
5603
+ try {
5604
+ return await this.restore(roomId, void 0, identityName, bio);
5605
+ } catch (error) {
5606
+ this.log(`[${this.packetName(roomId)}] pending packet restore failed; reprovisioning:`, error);
5607
+ }
5608
+ }
5609
+ this.prepareProvisioningDirectory(roomId);
5610
+ let native;
5611
+ try {
5612
+ native = await this.host.createPacket(this.packetName(roomId), this.seed());
5613
+ let room;
5614
+ room = new HostedRoomPacket(
5615
+ native,
5616
+ () => this.saveState(native, liveDir),
5617
+ this.log,
5618
+ (event) => this.onNotify(roomId, event),
5619
+ () => {
5620
+ if (this.packets.get(roomId) === room) this.packets.delete(roomId);
5621
+ }
5622
+ );
5623
+ atomicWriteFileSync(this.identityPath(roomId), Buffer.from(exportSigningSecret(native), "utf8"), this.persistence);
5624
+ this.provisioningCheckpoint("identity");
5625
+ this.saveState(native, liveDir);
5626
+ this.provisioningCheckpoint("state");
5627
+ this.packets.set(roomId, room);
5628
+ await room.setIdentity(identityName, bio);
5629
+ this.provisioningCheckpoint("identity_applied");
5630
+ return room;
5631
+ } catch (error) {
5632
+ if (native) {
5633
+ try {
5634
+ this.host.removePacket(native.cid);
5635
+ } catch {
5636
+ }
5637
+ }
5638
+ this.packets.delete(roomId);
5639
+ throw error;
5640
+ }
5641
+ }
5642
+ async restore(roomId, expectedCid, identityName, bio) {
5643
+ validateRoomId(roomId);
5644
+ if (this.packets.has(roomId)) throw new Error(`room packet "${roomId}" is already hosted`);
5645
+ const liveDir = this.liveDir(roomId);
5646
+ const secret = this.fs.readFileSync(this.identityPath(roomId), "utf8").trim();
5647
+ if (!/^[0-9a-f]+$/i.test(secret) || secret.length % 2 !== 0) {
5648
+ throw new Error(`invalid signing secret for room "${roomId}"`);
5649
+ }
5650
+ const stateBytes = this.fs.readFileSync(this.statePath(roomId));
5651
+ if (stateBytes.length === 0) throw new Error(`empty packet state for room "${roomId}"`);
5652
+ const native = await this.host.createPacket(
5653
+ this.packetName(roomId),
5654
+ this.seed(),
5655
+ secret,
5656
+ { deferredExposure: true }
5657
+ );
5658
+ if (expectedCid !== void 0 && native.cid !== expectedCid) {
5659
+ try {
5660
+ this.host.removePacket(native.cid);
5661
+ } catch {
5662
+ }
5663
+ throw new Error(
5664
+ `restored room packet CID mismatch for "${roomId}": expected "${expectedCid}", found "${native.cid}"`
5665
+ );
5666
+ }
5667
+ let room;
5668
+ room = new HostedRoomPacket(
5669
+ native,
5670
+ () => this.saveState(native, liveDir),
5671
+ this.log,
5672
+ (event) => this.onNotify(roomId, event),
5673
+ () => {
5674
+ if (this.packets.get(roomId) === room) this.packets.delete(roomId);
5675
+ }
5676
+ );
5677
+ try {
5678
+ await withScopeAsync(async (lifetime) => {
5679
+ const state = native.pw.packet.ParseValue(new Uint8Array(stateBytes)).Attach(lifetime);
5680
+ await native.mutatingTx("::actor::import_state", state, lifetime);
5681
+ });
5682
+ native.pw.refresh_identity_proof_document();
5683
+ if (identityName !== void 0 && bio !== void 0) {
5684
+ await room.setIdentity(identityName, bio);
5685
+ this.provisioningCheckpoint("identity_applied");
5686
+ }
5687
+ atomicWriteFileSync(
5688
+ this.ownershipPath(roomId),
5689
+ Buffer.from(`${roomId}
5690
+ `, "utf8"),
5691
+ this.persistence
5692
+ );
5693
+ await this.beforeExpose(room);
5694
+ this.host.exposePacket(native.cid);
5695
+ this.packets.set(roomId, room);
5696
+ return room;
5697
+ } catch (error) {
5698
+ try {
5699
+ this.host.removePacket(native.cid);
5700
+ } catch {
5701
+ }
5702
+ throw error;
5703
+ }
5704
+ }
5705
+ async destroy(roomId) {
5706
+ validateRoomId(roomId);
5707
+ const room = this.packets.get(roomId);
5708
+ if (room) {
5709
+ this.host.removePacket(room.cid);
5710
+ this.packets.delete(roomId);
5711
+ }
5712
+ const liveDir = this.liveDir(roomId);
5713
+ let removalFailure;
5714
+ try {
5715
+ this.assertSafeRoomDirectory(roomId);
5716
+ this.fs.rmSync(liveDir, { recursive: true, force: true });
5717
+ this.fsyncDirectory(this.roomDir(roomId));
5718
+ } catch (error) {
5719
+ this.log(`[${room?.name ?? this.packetName(roomId)}] live-state removal failed:`, error);
5720
+ removalFailure = error;
5721
+ }
5722
+ const residue = this.residue(roomId);
5723
+ if (removalFailure !== void 0 && residue.length === 0) {
5724
+ throw new PacketPersistenceError(
5725
+ `live-state removal durability is uncertain for room "${roomId}"`,
5726
+ { cause: removalFailure }
5727
+ );
5728
+ }
5729
+ return residue;
5730
+ }
5731
+ /** Unhost runtime packets while retaining every byte required for restart. */
5732
+ async unhostAll() {
5733
+ const errors = [];
5734
+ for (const [roomId, room] of [...this.packets]) {
5735
+ try {
5736
+ this.host.removePacket(room.cid, new Error("cowork daemon is shutting down"));
5737
+ this.packets.delete(roomId);
5738
+ } catch (error) {
5739
+ errors.push(error);
5740
+ }
5741
+ }
5742
+ if (errors.length > 0) throw new AggregateError(errors, "failed to unhost room packets");
5743
+ }
5744
+ saveState(packet, liveDir) {
5745
+ try {
5746
+ const bytes = withScope((lifetime) => Buffer.from(packet.readonlyTx("::actor::export_state", lifetime).Serialize()));
5747
+ atomicWriteFileSync(join3(liveDir, "state_data.bin"), bytes, this.persistence);
5748
+ } catch (error) {
5749
+ if (error instanceof PacketPersistenceError) throw error;
5750
+ throw new PacketPersistenceError(`failed to export state for packet "${packet.name}"`, { cause: error });
5751
+ }
5752
+ }
5753
+ residue(roomId) {
5754
+ const liveDir = this.liveDir(roomId);
5755
+ try {
5756
+ this.fs.lstatSync(liveDir);
5757
+ return [liveDir];
5758
+ } catch (error) {
5759
+ if (error.code === "ENOENT") return [];
5760
+ throw error;
5761
+ }
5762
+ }
5763
+ packetName(roomId) {
5764
+ return `cowork-room-${roomId}`;
5765
+ }
5766
+ roomDir(roomId) {
5767
+ return join3(this.stateDir, "rooms", roomId);
5768
+ }
5769
+ liveDir(roomId) {
5770
+ return join3(this.roomDir(roomId), "live");
5141
5771
  }
5142
5772
  identityPath(roomId) {
5143
5773
  return join3(this.liveDir(roomId), "identity.key");
@@ -5327,681 +5957,184 @@ var init_packets = __esm({
5327
5957
  }
5328
5958
  }
5329
5959
  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);
5343
- }
5344
- }
5345
- };
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");
5464
- });
5465
- }
5466
- };
5467
- }
5468
- });
5469
-
5470
- // src/contracts.ts
5471
- function utf8Bounded(label, maximumBytes) {
5472
- return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
5473
- (value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
5474
- `${label} must be at most ${maximumBytes} UTF-8 bytes`
5475
- );
5476
- }
5477
- function isStrictRfc3339(value) {
5478
- const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
5479
- if (!match) return false;
5480
- const [, yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText] = match;
5481
- const year = Number(yearText);
5482
- const month = Number(monthText);
5483
- const day = Number(dayText);
5484
- const hour = Number(hourText);
5485
- const minute = Number(minuteText);
5486
- const second = Number(secondText);
5487
- const offsetHour = offsetHourText === void 0 ? 0 : Number(offsetHourText);
5488
- const offsetMinute = offsetMinuteText === void 0 ? 0 : Number(offsetMinuteText);
5489
- if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
5490
- if (offsetHour > 23 || offsetMinute > 59) return false;
5491
- const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
5492
- const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
5493
- return day >= 1 && day <= days[month - 1];
5494
- }
5495
- 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"
5503
- });
5504
- }
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"
5510
- });
5511
- }
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
- }
5542
- }
5543
- }
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
- });
5559
- }
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
- });
5585
- }
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
- }
5594
- }
5595
- }
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"() {
5599
- "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
- }
5960
+ const roomDir = this.roomDir(roomId);
5961
+ const stat = this.fs.lstatSync(roomDir);
5962
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
5963
+ throw new Error(`room directory for "${roomId}" is not a safe directory`);
5670
5964
  }
5671
5965
  }
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"
5966
+ fsyncDirectory(path) {
5967
+ let fd;
5968
+ try {
5969
+ fd = this.fs.openSync(path, nodeFs2.constants.O_RDONLY | (nodeFs2.constants.O_NOFOLLOW ?? 0));
5970
+ this.fs.fsyncSync(fd);
5971
+ } finally {
5972
+ if (fd !== void 0) this.fs.closeSync(fd);
5973
+ }
5974
+ }
5975
+ };
5976
+ HostedRoomPacket = class {
5977
+ name;
5978
+ cid;
5979
+ packet;
5980
+ constructor(packet, saveState, log, onNotify = () => {
5981
+ }, onTerminal = () => {
5982
+ }) {
5983
+ this.packet = packet;
5984
+ this.name = packet.name;
5985
+ this.cid = packet.cid;
5986
+ wireHandlers(packet, { onSaveState: saveState, onNotify: (event) => onNotify(event) }, log);
5987
+ packet.onTerminalClose?.(onTerminal);
5988
+ }
5989
+ async setIdentity(identityName, bio) {
5990
+ await withScopeAsync((lifetime) => this.packet.mutatingTx("::a2a_messaging::set_my_name", { name: identityName }, lifetime));
5991
+ await withScopeAsync((lifetime) => this.packet.mutatingTx("::a2a_messaging::set_my_bio", { bio }, lifetime));
5992
+ }
5993
+ async mintInvite(mode) {
5994
+ return withScopeAsync(async (lifetime) => {
5995
+ const result = await this.packet.mutatingTx("::a2a_messaging::generate_invite", { mode }, lifetime);
5996
+ return {
5997
+ blob: packInvite(Buffer.from(result.Reduce("invite").GetBinary())),
5998
+ invite_id: result.Reduce("invite_id").Visualize(),
5999
+ reusable: booleanValue(result.Reduce("reusable"))
6000
+ };
5690
6001
  });
5691
6002
  }
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"
6003
+ async revokeInvite(inviteId) {
6004
+ return withScopeAsync(async (lifetime) => {
6005
+ const result = await this.packet.mutatingTx("::a2a_messaging::revoke_invite", { invite_id: inviteId }, lifetime);
6006
+ return { revoked: booleanValue(result.Reduce("revoked")) };
5697
6007
  });
5698
6008
  }
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"
6009
+ listInvites() {
6010
+ return withScope((lifetime) => {
6011
+ const value = this.packet.readonlyTx("::a2a_messaging::list_invites", lifetime);
6012
+ return dictionaryEntries(value).map(([inviteId, invite]) => ({
6013
+ invite_id: inviteId,
6014
+ mode: inviteMode(invite.Reduce("mode").Visualize())
6015
+ }));
5704
6016
  });
5705
6017
  }
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"
6018
+ listContacts() {
6019
+ return withScope((lifetime) => {
6020
+ const value = this.packet.readonlyTx("::a2a_messaging::list_contacts", lifetime);
6021
+ return dictionaryEntries(value).map(([, contact]) => ({
6022
+ name: contact.Reduce("name").Visualize(),
6023
+ container_id: contact.Reduce("container_id").Visualize()
6024
+ }));
5711
6025
  });
5712
6026
  }
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"
6027
+ listContactOrigins() {
6028
+ return withScope((lifetime) => {
6029
+ const value = this.packet.readonlyTx("::a2a_messaging::list_contact_origins", lifetime);
6030
+ return Object.fromEntries(dictionaryEntries(value).map(([cid, origin]) => [cid, {
6031
+ via: origin.Reduce("via").Visualize(),
6032
+ invite_id: nilString(origin.Reduce("invite_id")),
6033
+ at: adaptTimeToRfc3339(origin.Reduce("at").Visualize())
6034
+ }]));
5718
6035
  });
5719
6036
  }
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"
6037
+ peekInbox() {
6038
+ return withScope((lifetime) => renderInbox(this.packet.readonlyTx("::actor::list_incoming_messages", lifetime)).filter((message) => message.status === "unread").map(({ status: _status, ...message }) => message));
6039
+ }
6040
+ async consumeInbox(expectedIds) {
6041
+ return withScopeAsync(async (lifetime) => {
6042
+ const result = await this.packet.mutatingTx(
6043
+ "::actor::consume_messages",
6044
+ { expected_ids: expectedIds },
6045
+ lifetime
6046
+ );
6047
+ return {
6048
+ consumed: renderIntegerArray(result.Reduce("consumed")),
6049
+ deferred: renderIntegerArray(result.Reduce("deferred"))
6050
+ };
5725
6051
  });
5726
6052
  }
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"
6053
+ peekFileInbox() {
6054
+ return withScope((lifetime) => renderFileInbox(
6055
+ this.packet.readonlyTx("::actor::list_incoming_files", lifetime)
6056
+ ).filter((file) => file.status === "unread").map(({ status: _status, ...file }) => file));
6057
+ }
6058
+ async consumeFileInbox(expectedIds) {
6059
+ return withScopeAsync(async (lifetime) => {
6060
+ const result = await this.packet.mutatingTx(
6061
+ "::actor::consume_files",
6062
+ { expected_ids: expectedIds },
6063
+ lifetime
6064
+ );
6065
+ return {
6066
+ consumed: renderIntegerArray(result.Reduce("consumed")),
6067
+ deferred: renderIntegerArray(result.Reduce("deferred"))
6068
+ };
5732
6069
  });
5733
6070
  }
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
- }
6071
+ async send(contactCid, body) {
6072
+ return withScopeAsync(async (lifetime) => {
6073
+ const result = await this.packet.mutatingTx(
6074
+ "::a2a_messaging::send_message",
6075
+ { contact: contactCid, text: body },
6076
+ lifetime
6077
+ );
6078
+ const refused = !result.Reduce("downgrade_refused").IsNil();
6079
+ return refused ? { status: "send_failed" } : { status: "queued", wire_id: nilString(result.Reduce("wire_id")) || void 0 };
6080
+ });
5819
6081
  }
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
- });
6082
+ async sendFile(contactCid, filename, mime, data) {
6083
+ const validName = FileNameSchema.parse(filename);
6084
+ const validMime = FileMimeSchema.parse(mime);
6085
+ if (data.length > MAX_FILE_BYTES) {
6086
+ throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
5844
6087
  }
6088
+ return withScopeAsync(async (lifetime) => {
6089
+ const result = await this.packet.mutatingTx(
6090
+ "::a2a_messaging::send_file",
6091
+ {
6092
+ contact: contactCid,
6093
+ filename: validName,
6094
+ mime: validMime,
6095
+ // The core contract takes bytes. A filesystem path here would make
6096
+ // recovery depend on staging ownership and is deliberately forbidden.
6097
+ data: this.packet.newBinary(data, lifetime)
6098
+ },
6099
+ lifetime
6100
+ );
6101
+ const refused = !result.Reduce("downgrade_refused").IsNil() || !result.Reduce("migrating").IsNil();
6102
+ return refused ? { status: "send_failed" } : { status: "queued", wire_id: nilString(result.Reduce("wire_id")) || void 0 };
6103
+ });
5845
6104
  }
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
- });
6105
+ async removeContact(contactCid) {
6106
+ return withScopeAsync(async (lifetime) => {
6107
+ const result = await this.packet.mutatingTx(
6108
+ "::a2a_messaging::remove_contact",
6109
+ { contact: contactCid },
6110
+ lifetime
6111
+ );
6112
+ const notified = strictBooleanValue(result.Reduce("notified"), "remove_contact notified");
6113
+ const keyMaterialRetained = strictBooleanValue(
6114
+ result.Reduce("key_material_retained"),
6115
+ "remove_contact key_material_retained"
6116
+ );
6117
+ if (!keyMaterialRetained) {
6118
+ throw new Error("remove_contact key_material_retained must be true");
5915
6119
  }
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'
6120
+ return {
6121
+ status: notified ? "queued" : "send_failed",
6122
+ notified,
6123
+ key_material_retained: true
6124
+ };
5990
6125
  });
5991
6126
  }
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
- });
6127
+ async sign(canonicalJson2) {
6128
+ return withScopeAsync(async (lifetime) => {
6129
+ const result = await this.packet.mutatingTx(
6130
+ "::actor::sign_app_envelope",
6131
+ { canonical_json: canonicalJson2 },
6132
+ lifetime
6133
+ );
6134
+ return Buffer.from(result.Reduce("signature").GetBinary()).toString("base64url");
6135
+ });
6136
+ }
6137
+ };
6005
6138
  }
6006
6139
  });
6007
6140
 
@@ -6037,11 +6170,16 @@ var init_ulid = __esm({
6037
6170
  });
6038
6171
 
6039
6172
  // src/intake.ts
6173
+ import { createHash as createHash2 } from "node:crypto";
6040
6174
  function canonicalJson(value) {
6041
6175
  const encoded = JSON.stringify(canonicalValue(value));
6042
6176
  if (encoded === void 0) throw new TypeError("canonical JSON value is not serializable");
6043
6177
  return encoded;
6044
6178
  }
6179
+ async function sendSignedBody(packet, recipientIdentity, unsigned) {
6180
+ const signature = await packet.sign(canonicalJson(unsigned));
6181
+ return packet.send(recipientIdentity, canonicalJson({ ...unsigned, signature }));
6182
+ }
6045
6183
  function canonicalValue(value) {
6046
6184
  if (Array.isArray(value)) return value.map(canonicalValue);
6047
6185
  if (value !== null && typeof value === "object") {
@@ -6155,9 +6293,58 @@ var init_intake = __esm({
6155
6293
  async processAndRelayUnlocked(roomId, packet) {
6156
6294
  const snapshot = packet.peekInbox();
6157
6295
  for (const item of snapshot) await this.processInboxItem(roomId, packet, item);
6296
+ const fileSnapshot = packet.peekFileInbox();
6297
+ for (const item of fileSnapshot) await this.processFileInboxItem(roomId, packet, item);
6158
6298
  await this.completeSnapshotIntents(roomId);
6159
6299
  await this.relayPendingUnlocked(roomId, packet);
6160
6300
  }
6301
+ async processFileInboxItem(roomId, packet, item) {
6302
+ const parsedName = FileNameSchema.safeParse(item.filename);
6303
+ const parsedMime = FileMimeSchema.safeParse(item.mime);
6304
+ if (!parsedName.success || !parsedMime.success) {
6305
+ await packet.consumeFileInbox([item.file_id]);
6306
+ return;
6307
+ }
6308
+ if (item.data.length > MAX_FILE_BYTES) {
6309
+ throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
6310
+ }
6311
+ const room = await this.store.load(roomId);
6312
+ const seat = room.seats.find(
6313
+ (candidate) => candidate.identity === item.sender_id && candidate.state === "active"
6314
+ );
6315
+ if (room.state !== "active" || !seat) {
6316
+ if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
6317
+ await packet.consumeFileInbox([item.file_id]);
6318
+ return;
6319
+ }
6320
+ const records = await this.store.read(roomId);
6321
+ let file = this.findSourceFile(records, item);
6322
+ if (!file) {
6323
+ const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
6324
+ const bytes = Buffer.from(item.data);
6325
+ const appended = await this.store.append(roomId, {
6326
+ version: 1,
6327
+ kind: "file",
6328
+ room_id: roomId,
6329
+ at: Rfc3339Schema.parse(item.date),
6330
+ file_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
6331
+ author: { identity: seat.identity, display_name: seat.display_name, role: seat.role },
6332
+ ...room.anonymous && seat.alias !== void 0 ? { author_alias: { participant_id: seat.participant_id, alias: seat.alias } } : {},
6333
+ filename: parsedName.data,
6334
+ mime: parsedMime.data,
6335
+ size: bytes.length,
6336
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
6337
+ data_base64: bytes.toString("base64"),
6338
+ recipient_identities: recipientIdentities,
6339
+ source_file_id: item.file_id,
6340
+ ...item.wire_id === "" ? {} : { source_wire_id: item.wire_id }
6341
+ });
6342
+ if (appended.kind !== "file") throw new Error("storage returned the wrong participant file kind");
6343
+ file = appended;
6344
+ }
6345
+ await this.completeFileIntents(roomId, file);
6346
+ await packet.consumeFileInbox([item.file_id]);
6347
+ }
6161
6348
  async processInboxItem(roomId, packet, item) {
6162
6349
  const room = await this.store.load(roomId);
6163
6350
  const seat = room.seats.find(
@@ -6215,8 +6402,7 @@ var init_intake = __esm({
6215
6402
  await this.store.save(RoomSchema.parse({ ...room, seats }));
6216
6403
  try {
6217
6404
  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 }));
6405
+ await sendSignedBody(packet, item.sender_id, unsigned);
6220
6406
  } catch {
6221
6407
  }
6222
6408
  }
@@ -6225,6 +6411,25 @@ var init_intake = __esm({
6225
6411
  for (const message of records.filter(
6226
6412
  (record) => record.kind === "message"
6227
6413
  )) await this.completeMessageIntents(roomId, message);
6414
+ for (const file of records.filter(
6415
+ (record) => record.kind === "file"
6416
+ )) await this.completeFileIntents(roomId, file);
6417
+ }
6418
+ async completeFileIntents(roomId, file) {
6419
+ const records = await this.store.read(roomId);
6420
+ const intended = new Set(records.filter((record) => record.kind === "relay_intent" && record.file_id === file.file_id).map((intent) => intent.recipient_identity));
6421
+ for (const recipientIdentity of file.recipient_identities) {
6422
+ if (intended.has(recipientIdentity)) continue;
6423
+ await this.store.append(roomId, {
6424
+ version: 1,
6425
+ kind: "relay_intent",
6426
+ room_id: roomId,
6427
+ at: this.now(),
6428
+ file_id: file.file_id,
6429
+ recipient_identity: recipientIdentity
6430
+ });
6431
+ intended.add(recipientIdentity);
6432
+ }
6228
6433
  }
6229
6434
  async completeMessageIntents(roomId, message) {
6230
6435
  const records = await this.store.read(roomId);
@@ -6248,14 +6453,17 @@ var init_intake = __esm({
6248
6453
  const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
6249
6454
  const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
6250
6455
  const messages = new Map(records.filter((record) => record.kind === "message").map((message) => [message.message_id, message]));
6456
+ const files = new Map(records.filter((record) => record.kind === "file").map((file) => [file.file_id, file]));
6251
6457
  const completed = new Set(records.filter((record) => record.kind === "relay_result").map((result) => result.kind === "relay_result" ? result.intent_record_id : ""));
6252
6458
  for (const intent of records.filter(
6253
6459
  (record) => record.kind === "relay_intent"
6254
6460
  )) {
6255
6461
  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;
6462
+ const message = intent.message_id === void 0 ? void 0 : messages.get(intent.message_id);
6463
+ const file = intent.file_id === void 0 ? void 0 : files.get(intent.file_id);
6464
+ if (message === void 0 === (file === void 0)) continue;
6465
+ const recipients = message?.recipient_identities ?? file.recipient_identities;
6466
+ if (!recipients.includes(intent.recipient_identity)) continue;
6259
6467
  if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
6260
6468
  const skipped = await this.store.append(roomId, {
6261
6469
  version: 1,
@@ -6263,7 +6471,8 @@ var init_intake = __esm({
6263
6471
  room_id: roomId,
6264
6472
  at: this.now(),
6265
6473
  intent_record_id: intent.record_id,
6266
- message_id: intent.message_id,
6474
+ ...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
6475
+ ...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
6267
6476
  recipient_identity: intent.recipient_identity,
6268
6477
  status: "skipped_removed"
6269
6478
  });
@@ -6271,6 +6480,61 @@ var init_intake = __esm({
6271
6480
  completed.add(intent.record_id);
6272
6481
  continue;
6273
6482
  }
6483
+ if (file !== void 0) {
6484
+ const author = file.author_alias === void 0 ? file.author : {
6485
+ identity: file.author_alias.participant_id,
6486
+ display_name: file.author_alias.alias,
6487
+ role: file.author.role
6488
+ };
6489
+ const metadata = await sendSignedBody(packet, intent.recipient_identity, {
6490
+ version: 1,
6491
+ kind: "room_file",
6492
+ room_id: roomId,
6493
+ file_id: file.file_id,
6494
+ author,
6495
+ filename: file.filename,
6496
+ mime: file.mime,
6497
+ size: file.size,
6498
+ sha256: file.sha256,
6499
+ at: file.at
6500
+ });
6501
+ if (metadata.status === "send_failed") {
6502
+ const failed = await this.store.append(roomId, {
6503
+ version: 1,
6504
+ kind: "relay_result",
6505
+ room_id: roomId,
6506
+ at: this.now(),
6507
+ intent_record_id: intent.record_id,
6508
+ file_id: file.file_id,
6509
+ recipient_identity: intent.recipient_identity,
6510
+ status: "send_failed"
6511
+ });
6512
+ if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6513
+ completed.add(intent.record_id);
6514
+ continue;
6515
+ }
6516
+ const outcome2 = await packet.sendFile(
6517
+ intent.recipient_identity,
6518
+ file.filename,
6519
+ file.mime,
6520
+ Buffer.from(file.data_base64, "base64")
6521
+ );
6522
+ const appended2 = await this.store.append(roomId, {
6523
+ version: 1,
6524
+ kind: "relay_result",
6525
+ room_id: roomId,
6526
+ at: this.now(),
6527
+ intent_record_id: intent.record_id,
6528
+ file_id: file.file_id,
6529
+ recipient_identity: intent.recipient_identity,
6530
+ status: outcome2.status,
6531
+ ...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
6532
+ ...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
6533
+ });
6534
+ if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6535
+ completed.add(intent.record_id);
6536
+ continue;
6537
+ }
6274
6538
  const unsigned = {
6275
6539
  version: 1,
6276
6540
  kind: wireKind(message.category),
@@ -6288,9 +6552,7 @@ var init_intake = __esm({
6288
6552
  ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
6289
6553
  ...message.membership === void 0 ? {} : { membership: message.membership }
6290
6554
  };
6291
- const signature = await packet.sign(canonicalJson(unsigned));
6292
- const body = canonicalJson({ ...unsigned, signature });
6293
- const outcome = await packet.send(intent.recipient_identity, body);
6555
+ const outcome = await sendSignedBody(packet, intent.recipient_identity, unsigned);
6294
6556
  const appended = await this.store.append(roomId, {
6295
6557
  version: 1,
6296
6558
  kind: "relay_result",
@@ -6315,6 +6577,16 @@ var init_intake = __esm({
6315
6577
  }
6316
6578
  return message;
6317
6579
  }
6580
+ findSourceFile(records, item) {
6581
+ const file = records.find((record) => record.kind === "file" && record.source_file_id === item.file_id);
6582
+ if (!file) return void 0;
6583
+ const observedWireId = item.wire_id === "" ? void 0 : item.wire_id;
6584
+ const bytes = Buffer.from(item.data);
6585
+ if (file.source_wire_id !== observedWireId || file.author.identity !== item.sender_id || file.filename !== item.filename || file.mime !== item.mime || file.at !== item.date || file.size !== bytes.length || file.data_base64 !== bytes.toString("base64")) {
6586
+ throw new Error(`file inbox source ${item.file_id} does not match its durable room file`);
6587
+ }
6588
+ return file;
6589
+ }
6318
6590
  lock(roomId, work) {
6319
6591
  return this.store.mutex(roomId).runExclusive(work);
6320
6592
  }
@@ -6331,6 +6603,23 @@ var init_intake = __esm({
6331
6603
  });
6332
6604
 
6333
6605
  // src/service.ts
6606
+ function byteBoundedHistoryPage(records) {
6607
+ const page = [];
6608
+ let bytes = 2;
6609
+ for (const record of records) {
6610
+ const encoded = JSON.stringify(record);
6611
+ const nextBytes = Buffer.byteLength(encoded, "utf8") + (page.length === 0 ? 0 : 1);
6612
+ if (bytes + nextBytes > MAX_HISTORY_PAGE_BYTES) {
6613
+ if (page.length === 0) {
6614
+ throw new RangeError(`one history record exceeds the ${MAX_HISTORY_PAGE_BYTES}-byte page contract`);
6615
+ }
6616
+ break;
6617
+ }
6618
+ page.push(record);
6619
+ bytes += nextBytes;
6620
+ }
6621
+ return page;
6622
+ }
6334
6623
  function activeSeats(room) {
6335
6624
  return room.seats.filter((seat) => seat.state === "active");
6336
6625
  }
@@ -6982,9 +7271,9 @@ var init_service = __esm({
6982
7271
  async history(roomId, options = {}) {
6983
7272
  const id = LowerCrockfordUlidSchema.parse(roomId);
6984
7273
  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) => {
7274
+ const records = await this.store.read(id, view === "participant" ? { after: page.after } : page);
7275
+ if (view !== "participant") return byteBoundedHistoryPage(records);
7276
+ const projected = records.filter((record) => record.kind === "message").map((record) => {
6988
7277
  const {
6989
7278
  author_alias,
6990
7279
  recipient_identities: _recipients,
@@ -7001,6 +7290,7 @@ var init_service = __esm({
7001
7290
  }
7002
7291
  };
7003
7292
  });
7293
+ return byteBoundedHistoryPage(projected.slice(0, page.limit ?? Number.MAX_SAFE_INTEGER));
7004
7294
  }
7005
7295
  /**
7006
7296
  * Forward-only close. Every external contact mutation is preceded by a
@@ -7304,7 +7594,7 @@ var init_service = __esm({
7304
7594
  const matching = records.filter((record) => record.kind === "message" && record.category === briefing.category && record.briefing_role === briefing.briefing_role && (record.briefing_version ?? 1) === briefing.briefing_version);
7305
7595
  const intentsByMessage = /* @__PURE__ */ new Map();
7306
7596
  for (const record of records) {
7307
- if (record.kind !== "relay_intent") continue;
7597
+ if (record.kind !== "relay_intent" || record.message_id === void 0) continue;
7308
7598
  const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
7309
7599
  intents.add(record.recipient_identity);
7310
7600
  intentsByMessage.set(record.message_id, intents);
@@ -8915,6 +9205,7 @@ __export(daemon_runtime_exports, {
8915
9205
  DaemonShutdownError: () => DaemonShutdownError,
8916
9206
  acquireDaemonLock: () => acquireDaemonLock,
8917
9207
  createDaemonControlRoutes: () => createDaemonControlRoutes,
9208
+ isIntakeNotification: () => isIntakeNotification,
8918
9209
  loadConfig: () => loadConfig,
8919
9210
  removeDaemonPid: () => removeDaemonPid,
8920
9211
  writeDaemonPid: () => writeDaemonPid
@@ -8922,6 +9213,9 @@ __export(daemon_runtime_exports, {
8922
9213
  import * as nodeFs6 from "node:fs";
8923
9214
  import { join as join7 } from "node:path";
8924
9215
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9216
+ function isIntakeNotification(event) {
9217
+ return event === "message_received" || event === "file_received" || event === "contact_accepted";
9218
+ }
8925
9219
  function createDaemonControlRoutes(control) {
8926
9220
  if (!/^[0-9a-f]{32}$/.test(control.session)) throw new TypeError("invalid daemon control session");
8927
9221
  const requireExact = (params, keys) => {
@@ -9204,7 +9498,7 @@ var init_daemon_runtime = __esm({
9204
9498
  {
9205
9499
  log: this.options.log,
9206
9500
  onNotify: (roomId, event) => {
9207
- if (event === "message_received" || event === "contact_accepted") {
9501
+ if (isIntakeNotification(event)) {
9208
9502
  this.handleNotification(roomId, event, serviceRef);
9209
9503
  }
9210
9504
  }