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