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