@ours.network/cowork 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.js
CHANGED
|
@@ -5492,7 +5492,108 @@ function isStrictRfc3339(value) {
|
|
|
5492
5492
|
const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
5493
5493
|
return day >= 1 && day <= days[month - 1];
|
|
5494
5494
|
}
|
|
5495
|
-
|
|
5495
|
+
function refineRoomLineage(room, context) {
|
|
5496
|
+
const pendingIdentityName = `cowork-room-${room.room_id}`;
|
|
5497
|
+
const exactPacketPending = room.state === "provisioning" && room.status === "packet_pending" && room.identity_name === pendingIdentityName && room.invites.length === 0 && room.seats.length === 0 && room.activated_at === void 0 && room.closed_at === void 0;
|
|
5498
|
+
if (room.identity_cid === "" && !exactPacketPending) {
|
|
5499
|
+
context.addIssue({
|
|
5500
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5501
|
+
path: ["identity_cid"],
|
|
5502
|
+
message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
|
|
5503
|
+
});
|
|
5504
|
+
}
|
|
5505
|
+
if (room.identity_cid !== "" && room.status === "packet_pending") {
|
|
5506
|
+
context.addIssue({
|
|
5507
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5508
|
+
path: ["status"],
|
|
5509
|
+
message: "packet_pending status requires an empty identity_cid"
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
const pendingByRecovery = /* @__PURE__ */ new Map();
|
|
5513
|
+
for (const [index, invite] of room.invites.entries()) {
|
|
5514
|
+
if (invite.recovery_of === void 0) continue;
|
|
5515
|
+
const recoveryOf = invite.recovery_of;
|
|
5516
|
+
const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
|
|
5517
|
+
const validSourceState = invite.state === "receipt_pending" ? source?.state === "replacement_required" : invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required" ? source?.state === "revoked" : invite.state === "revoked" ? invite.recovery_confirmed === true ? source?.state === "revoked" : source?.state === "replacement_required" || source?.state === "revoked" : false;
|
|
5518
|
+
if (!source || source.invite_id === invite.invite_id || !validSourceState) {
|
|
5519
|
+
context.addIssue({
|
|
5520
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5521
|
+
path: ["invites", index, "recovery_of"],
|
|
5522
|
+
message: "recovery_of must point to a source invite in the state required by this recovery lineage"
|
|
5523
|
+
});
|
|
5524
|
+
} else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
|
|
5525
|
+
context.addIssue({
|
|
5526
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5527
|
+
path: ["invites", index],
|
|
5528
|
+
message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
|
|
5529
|
+
});
|
|
5530
|
+
}
|
|
5531
|
+
if (invite.state === "receipt_pending") {
|
|
5532
|
+
const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
|
|
5533
|
+
pendingByRecovery.set(recoveryOf, count);
|
|
5534
|
+
if (count > 1) {
|
|
5535
|
+
context.addIssue({
|
|
5536
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5537
|
+
path: ["invites", index, "recovery_of"],
|
|
5538
|
+
message: "only one receipt_pending invite may exist per recovery_of pointer"
|
|
5539
|
+
});
|
|
5540
|
+
}
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
function migrateRoomV1(room, mintParticipantId) {
|
|
5545
|
+
return RoomSchema.parse({
|
|
5546
|
+
...room,
|
|
5547
|
+
version: 2,
|
|
5548
|
+
mission: { ...room.mission, briefing_version: 1 },
|
|
5549
|
+
role_briefings: {},
|
|
5550
|
+
anonymous: false,
|
|
5551
|
+
quiet_membership: false,
|
|
5552
|
+
membership_epoch: 0,
|
|
5553
|
+
seats: room.seats.map((seat) => ({
|
|
5554
|
+
...seat,
|
|
5555
|
+
participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
|
|
5556
|
+
state: "active"
|
|
5557
|
+
}))
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5560
|
+
function refineMessageCategory(message, context) {
|
|
5561
|
+
const requires = (field, present) => {
|
|
5562
|
+
if (present && message[field] === void 0) {
|
|
5563
|
+
context.addIssue({
|
|
5564
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5565
|
+
path: [field],
|
|
5566
|
+
message: `${message.category} messages require ${field}`
|
|
5567
|
+
});
|
|
5568
|
+
}
|
|
5569
|
+
if (!present && message[field] !== void 0) {
|
|
5570
|
+
context.addIssue({
|
|
5571
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5572
|
+
path: [field],
|
|
5573
|
+
message: `${field} is forbidden on ${message.category} messages`
|
|
5574
|
+
});
|
|
5575
|
+
}
|
|
5576
|
+
};
|
|
5577
|
+
requires("briefing_role", message.category === "role_briefing");
|
|
5578
|
+
requires("membership", message.category === "membership");
|
|
5579
|
+
if (message.category === "role_briefing" && message.briefing_version === void 0) {
|
|
5580
|
+
context.addIssue({
|
|
5581
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5582
|
+
path: ["briefing_version"],
|
|
5583
|
+
message: "role_briefing messages require briefing_version"
|
|
5584
|
+
});
|
|
5585
|
+
}
|
|
5586
|
+
if (message.category === "chat" || message.category === "membership") {
|
|
5587
|
+
if (message.briefing_version !== void 0) {
|
|
5588
|
+
context.addIssue({
|
|
5589
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5590
|
+
path: ["briefing_version"],
|
|
5591
|
+
message: `briefing_version is forbidden on ${message.category} messages`
|
|
5592
|
+
});
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
var MAX_TEXT_BYTES, MAX_ROLE_BYTES, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoleSchema, MissionTextSchema, MessageTextSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
|
|
5496
5597
|
var init_contracts = __esm({
|
|
5497
5598
|
"src/contracts.ts"() {
|
|
5498
5599
|
"use strict";
|
|
@@ -5510,7 +5611,9 @@ var init_contracts = __esm({
|
|
|
5510
5611
|
MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
|
|
5511
5612
|
MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
|
|
5512
5613
|
RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
|
|
5614
|
+
SeatStateSchema = external_exports.enum(["active", "removed"]);
|
|
5513
5615
|
InviteModeSchema = external_exports.enum(["one_time", "public"]);
|
|
5616
|
+
DEFAULT_ROLE = "Participant";
|
|
5514
5617
|
InviteStateSchema = external_exports.enum([
|
|
5515
5618
|
"live",
|
|
5516
5619
|
"consumed",
|
|
@@ -5519,13 +5622,54 @@ var init_contracts = __esm({
|
|
|
5519
5622
|
"receipt_pending"
|
|
5520
5623
|
]);
|
|
5521
5624
|
RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
|
|
5522
|
-
|
|
5625
|
+
SeatV1Schema = external_exports.object({
|
|
5523
5626
|
identity: NonEmptyStringSchema,
|
|
5524
5627
|
display_name: NonEmptyStringSchema,
|
|
5525
5628
|
role: RoleSchema,
|
|
5526
5629
|
invite_id: NonEmptyStringSchema,
|
|
5527
5630
|
accepted_at: Rfc3339Schema
|
|
5528
5631
|
}).strict();
|
|
5632
|
+
SeatSchema = external_exports.object({
|
|
5633
|
+
identity: NonEmptyStringSchema,
|
|
5634
|
+
display_name: NonEmptyStringSchema,
|
|
5635
|
+
role: RoleSchema,
|
|
5636
|
+
invite_id: NonEmptyStringSchema,
|
|
5637
|
+
accepted_at: Rfc3339Schema,
|
|
5638
|
+
participant_id: LowerCrockfordUlidSchema,
|
|
5639
|
+
state: SeatStateSchema,
|
|
5640
|
+
alias: NonEmptyStringSchema.optional(),
|
|
5641
|
+
removed_at: Rfc3339Schema.optional(),
|
|
5642
|
+
removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
|
|
5643
|
+
replaces_seat: LowerCrockfordUlidSchema.optional(),
|
|
5644
|
+
bounced_at: Rfc3339Schema.optional()
|
|
5645
|
+
}).strict().superRefine((seat, context) => {
|
|
5646
|
+
if (seat.state === "removed") {
|
|
5647
|
+
if (seat.removed_at === void 0) {
|
|
5648
|
+
context.addIssue({
|
|
5649
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5650
|
+
path: ["removed_at"],
|
|
5651
|
+
message: "removed seats require removed_at"
|
|
5652
|
+
});
|
|
5653
|
+
}
|
|
5654
|
+
if (seat.removed_epoch === void 0) {
|
|
5655
|
+
context.addIssue({
|
|
5656
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5657
|
+
path: ["removed_epoch"],
|
|
5658
|
+
message: "removed seats require removed_epoch"
|
|
5659
|
+
});
|
|
5660
|
+
}
|
|
5661
|
+
} else {
|
|
5662
|
+
for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
|
|
5663
|
+
if (seat[field] !== void 0) {
|
|
5664
|
+
context.addIssue({
|
|
5665
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5666
|
+
path: [field],
|
|
5667
|
+
message: `${field} is reserved for removed seats`
|
|
5668
|
+
});
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
});
|
|
5529
5673
|
RoomInviteSchema = external_exports.object({
|
|
5530
5674
|
invite_id: NonEmptyStringSchema,
|
|
5531
5675
|
mode: InviteModeSchema,
|
|
@@ -5535,7 +5679,8 @@ var init_contracts = __esm({
|
|
|
5535
5679
|
state: InviteStateSchema,
|
|
5536
5680
|
recovery_of: NonEmptyStringSchema.optional(),
|
|
5537
5681
|
recovery_confirmed: external_exports.boolean().optional(),
|
|
5538
|
-
created_at: Rfc3339Schema
|
|
5682
|
+
created_at: Rfc3339Schema,
|
|
5683
|
+
replaces_seat: LowerCrockfordUlidSchema.optional()
|
|
5539
5684
|
}).strict().superRefine((invite, context) => {
|
|
5540
5685
|
if (invite.mode === "one_time" && invite.min_accepts !== 1) {
|
|
5541
5686
|
context.addIssue({
|
|
@@ -5587,81 +5732,137 @@ var init_contracts = __esm({
|
|
|
5587
5732
|
});
|
|
5588
5733
|
}
|
|
5589
5734
|
});
|
|
5590
|
-
|
|
5735
|
+
MissionV1Schema = external_exports.object({
|
|
5591
5736
|
goal: MissionTextSchema,
|
|
5592
5737
|
briefing: MissionTextSchema
|
|
5593
5738
|
}).strict();
|
|
5594
|
-
|
|
5595
|
-
|
|
5739
|
+
MissionSchema = external_exports.object({
|
|
5740
|
+
goal: MissionTextSchema,
|
|
5741
|
+
briefing: MissionTextSchema,
|
|
5742
|
+
briefing_version: PositiveSafeIntegerSchema
|
|
5743
|
+
}).strict();
|
|
5744
|
+
RoleBriefingSchema = external_exports.object({
|
|
5745
|
+
text: MissionTextSchema,
|
|
5746
|
+
version: PositiveSafeIntegerSchema,
|
|
5747
|
+
updated_at: Rfc3339Schema
|
|
5748
|
+
}).strict();
|
|
5749
|
+
RoomCommonShape = {
|
|
5596
5750
|
room_id: LowerCrockfordUlidSchema,
|
|
5597
5751
|
identity_name: NonEmptyStringSchema,
|
|
5598
5752
|
identity_cid: external_exports.string(),
|
|
5599
|
-
mission: MissionSchema,
|
|
5600
5753
|
state: RoomStateSchema,
|
|
5601
5754
|
status: NonEmptyStringSchema.optional(),
|
|
5602
5755
|
invites: external_exports.array(RoomInviteSchema),
|
|
5603
|
-
seats: external_exports.array(SeatSchema),
|
|
5604
5756
|
created_at: Rfc3339Schema,
|
|
5605
5757
|
activated_at: Rfc3339Schema.optional(),
|
|
5606
5758
|
closed_at: Rfc3339Schema.optional()
|
|
5759
|
+
};
|
|
5760
|
+
RoomV1Schema = external_exports.object({
|
|
5761
|
+
...RoomCommonShape,
|
|
5762
|
+
version: external_exports.literal(1),
|
|
5763
|
+
mission: MissionV1Schema,
|
|
5764
|
+
seats: external_exports.array(SeatV1Schema)
|
|
5765
|
+
}).strict().superRefine(refineRoomLineage);
|
|
5766
|
+
RoomSchema = external_exports.object({
|
|
5767
|
+
...RoomCommonShape,
|
|
5768
|
+
version: external_exports.literal(2),
|
|
5769
|
+
mission: MissionSchema,
|
|
5770
|
+
role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
|
|
5771
|
+
anonymous: external_exports.boolean(),
|
|
5772
|
+
quiet_membership: external_exports.boolean(),
|
|
5773
|
+
membership_epoch: external_exports.number().int().nonnegative().safe(),
|
|
5774
|
+
seats: external_exports.array(SeatSchema)
|
|
5607
5775
|
}).strict().superRefine((room, context) => {
|
|
5608
|
-
|
|
5609
|
-
const
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
path: ["identity_cid"],
|
|
5614
|
-
message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
|
|
5615
|
-
});
|
|
5616
|
-
}
|
|
5617
|
-
if (room.identity_cid !== "" && room.status === "packet_pending") {
|
|
5618
|
-
context.addIssue({
|
|
5619
|
-
code: external_exports.ZodIssueCode.custom,
|
|
5620
|
-
path: ["status"],
|
|
5621
|
-
message: "packet_pending status requires an empty identity_cid"
|
|
5622
|
-
});
|
|
5623
|
-
}
|
|
5624
|
-
const pendingByRecovery = /* @__PURE__ */ new Map();
|
|
5625
|
-
for (const [index, invite] of room.invites.entries()) {
|
|
5626
|
-
if (invite.recovery_of === void 0) continue;
|
|
5627
|
-
const recoveryOf = invite.recovery_of;
|
|
5628
|
-
const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
|
|
5629
|
-
const validSourceState = invite.state === "receipt_pending" ? source?.state === "replacement_required" : invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required" ? source?.state === "revoked" : invite.state === "revoked" ? invite.recovery_confirmed === true ? source?.state === "revoked" : source?.state === "replacement_required" || source?.state === "revoked" : false;
|
|
5630
|
-
if (!source || source.invite_id === invite.invite_id || !validSourceState) {
|
|
5776
|
+
refineRoomLineage(room, context);
|
|
5777
|
+
const byParticipant = /* @__PURE__ */ new Map();
|
|
5778
|
+
const activeAliases = /* @__PURE__ */ new Set();
|
|
5779
|
+
for (const [index, seat] of room.seats.entries()) {
|
|
5780
|
+
if (byParticipant.has(seat.participant_id)) {
|
|
5631
5781
|
context.addIssue({
|
|
5632
5782
|
code: external_exports.ZodIssueCode.custom,
|
|
5633
|
-
path: ["
|
|
5634
|
-
message: "
|
|
5783
|
+
path: ["seats", index, "participant_id"],
|
|
5784
|
+
message: "participant_id must be unique within the room"
|
|
5635
5785
|
});
|
|
5636
|
-
}
|
|
5786
|
+
}
|
|
5787
|
+
byParticipant.set(seat.participant_id, seat);
|
|
5788
|
+
if (room.anonymous && seat.alias === void 0) {
|
|
5637
5789
|
context.addIssue({
|
|
5638
5790
|
code: external_exports.ZodIssueCode.custom,
|
|
5639
|
-
path: ["
|
|
5640
|
-
message: "
|
|
5791
|
+
path: ["seats", index, "alias"],
|
|
5792
|
+
message: "anonymous rooms require an alias on every seat"
|
|
5641
5793
|
});
|
|
5642
5794
|
}
|
|
5643
|
-
if (
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5795
|
+
if (!room.anonymous && seat.alias !== void 0) {
|
|
5796
|
+
context.addIssue({
|
|
5797
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5798
|
+
path: ["seats", index, "alias"],
|
|
5799
|
+
message: "aliases are reserved for anonymous rooms"
|
|
5800
|
+
});
|
|
5801
|
+
}
|
|
5802
|
+
if (seat.state === "active" && seat.alias !== void 0) {
|
|
5803
|
+
if (activeAliases.has(seat.alias)) {
|
|
5647
5804
|
context.addIssue({
|
|
5648
5805
|
code: external_exports.ZodIssueCode.custom,
|
|
5649
|
-
path: ["
|
|
5650
|
-
message: "
|
|
5806
|
+
path: ["seats", index, "alias"],
|
|
5807
|
+
message: "active seats must hold distinct aliases"
|
|
5651
5808
|
});
|
|
5652
5809
|
}
|
|
5810
|
+
activeAliases.add(seat.alias);
|
|
5811
|
+
}
|
|
5812
|
+
if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
|
|
5813
|
+
context.addIssue({
|
|
5814
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5815
|
+
path: ["seats", index, "removed_epoch"],
|
|
5816
|
+
message: "removed_epoch cannot exceed the room membership_epoch"
|
|
5817
|
+
});
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
for (const [index, seat] of room.seats.entries()) {
|
|
5821
|
+
if (seat.replaces_seat === void 0) continue;
|
|
5822
|
+
const predecessor = byParticipant.get(seat.replaces_seat);
|
|
5823
|
+
if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
|
|
5824
|
+
context.addIssue({
|
|
5825
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5826
|
+
path: ["seats", index, "replaces_seat"],
|
|
5827
|
+
message: "replaces_seat must reference a removed seat in this room"
|
|
5828
|
+
});
|
|
5829
|
+
continue;
|
|
5830
|
+
}
|
|
5831
|
+
if (predecessor.role !== seat.role) {
|
|
5832
|
+
context.addIssue({
|
|
5833
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5834
|
+
path: ["seats", index, "role"],
|
|
5835
|
+
message: "a replacement seat must inherit the predecessor role"
|
|
5836
|
+
});
|
|
5837
|
+
}
|
|
5838
|
+
if (room.anonymous && seat.alias !== predecessor.alias) {
|
|
5839
|
+
context.addIssue({
|
|
5840
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5841
|
+
path: ["seats", index, "alias"],
|
|
5842
|
+
message: "an anonymous replacement seat must inherit the predecessor alias"
|
|
5843
|
+
});
|
|
5653
5844
|
}
|
|
5654
5845
|
}
|
|
5655
5846
|
});
|
|
5656
5847
|
CreateRoomInputSchema = external_exports.object({
|
|
5657
5848
|
goal: MissionTextSchema,
|
|
5658
|
-
briefing: MissionTextSchema
|
|
5849
|
+
briefing: MissionTextSchema,
|
|
5850
|
+
anonymous: external_exports.boolean().optional(),
|
|
5851
|
+
quiet_membership: external_exports.boolean().optional()
|
|
5659
5852
|
}).strict();
|
|
5660
5853
|
UpdateRoomInputSchema = external_exports.object({
|
|
5661
5854
|
goal: MissionTextSchema.optional(),
|
|
5662
5855
|
briefing: MissionTextSchema.optional(),
|
|
5663
|
-
status: NonEmptyStringSchema.optional()
|
|
5856
|
+
status: NonEmptyStringSchema.optional(),
|
|
5857
|
+
quiet_membership: external_exports.boolean().optional()
|
|
5664
5858
|
}).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
|
|
5859
|
+
RoleBriefingSetInputSchema = external_exports.object({
|
|
5860
|
+
role: RoleSchema,
|
|
5861
|
+
text: MissionTextSchema
|
|
5862
|
+
}).strict();
|
|
5863
|
+
RoleBriefingDeleteInputSchema = external_exports.object({
|
|
5864
|
+
role: RoleSchema
|
|
5865
|
+
}).strict();
|
|
5665
5866
|
PostMessageInputSchema = external_exports.object({
|
|
5666
5867
|
text: MessageTextSchema
|
|
5667
5868
|
}).strict();
|
|
@@ -5682,11 +5883,25 @@ var init_contracts = __esm({
|
|
|
5682
5883
|
room_id: LowerCrockfordUlidSchema,
|
|
5683
5884
|
at: Rfc3339Schema
|
|
5684
5885
|
};
|
|
5886
|
+
MembershipNoticeSchema = external_exports.object({
|
|
5887
|
+
action: external_exports.enum(["remove"]),
|
|
5888
|
+
alias: NonEmptyStringSchema.optional(),
|
|
5889
|
+
role: RoleSchema.optional(),
|
|
5890
|
+
epoch: external_exports.number().int().nonnegative().safe()
|
|
5891
|
+
}).strict();
|
|
5892
|
+
AuthorAliasSchema = external_exports.object({
|
|
5893
|
+
participant_id: LowerCrockfordUlidSchema,
|
|
5894
|
+
alias: NonEmptyStringSchema
|
|
5895
|
+
}).strict();
|
|
5685
5896
|
MessageShape = {
|
|
5686
5897
|
kind: external_exports.literal("message"),
|
|
5687
5898
|
message_id: LowerCrockfordUlidSchema,
|
|
5688
5899
|
author: AuthorSnapshotSchema,
|
|
5689
|
-
|
|
5900
|
+
author_alias: AuthorAliasSchema.optional(),
|
|
5901
|
+
category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
|
|
5902
|
+
briefing_role: RoleSchema.optional(),
|
|
5903
|
+
briefing_version: PositiveSafeIntegerSchema.optional(),
|
|
5904
|
+
membership: MembershipNoticeSchema.optional(),
|
|
5690
5905
|
text: MessageTextSchema,
|
|
5691
5906
|
recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
|
|
5692
5907
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5709,14 +5924,34 @@ var init_contracts = __esm({
|
|
|
5709
5924
|
message_id: LowerCrockfordUlidSchema,
|
|
5710
5925
|
recipient_identity: NonEmptyStringSchema
|
|
5711
5926
|
};
|
|
5927
|
+
RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
|
|
5712
5928
|
RelayResultShape = {
|
|
5713
5929
|
kind: external_exports.literal("relay_result"),
|
|
5714
5930
|
intent_record_id: NonEmptyStringSchema,
|
|
5715
5931
|
message_id: LowerCrockfordUlidSchema,
|
|
5716
5932
|
recipient_identity: NonEmptyStringSchema,
|
|
5717
|
-
status:
|
|
5933
|
+
status: RelayResultStatusSchema,
|
|
5718
5934
|
wire_id: NonEmptyStringSchema.optional()
|
|
5719
5935
|
};
|
|
5936
|
+
MembershipIntentShape = {
|
|
5937
|
+
kind: external_exports.literal("membership_intent"),
|
|
5938
|
+
action: external_exports.enum(["remove"]),
|
|
5939
|
+
participant_id: LowerCrockfordUlidSchema,
|
|
5940
|
+
recipient_identity: NonEmptyStringSchema,
|
|
5941
|
+
role: RoleSchema,
|
|
5942
|
+
alias: NonEmptyStringSchema.optional(),
|
|
5943
|
+
epoch: PositiveSafeIntegerSchema,
|
|
5944
|
+
notify: external_exports.boolean()
|
|
5945
|
+
};
|
|
5946
|
+
MembershipResultShape = {
|
|
5947
|
+
kind: external_exports.literal("membership_result"),
|
|
5948
|
+
intent_record_id: NonEmptyStringSchema,
|
|
5949
|
+
participant_id: LowerCrockfordUlidSchema,
|
|
5950
|
+
status: RelayStatusSchema,
|
|
5951
|
+
notified: external_exports.boolean(),
|
|
5952
|
+
key_material_retained: external_exports.literal(true),
|
|
5953
|
+
uncertain_after_restart: external_exports.literal(true).optional()
|
|
5954
|
+
};
|
|
5720
5955
|
CloseNoticeIntentShape = {
|
|
5721
5956
|
kind: external_exports.literal("close_notice_intent"),
|
|
5722
5957
|
recipient_identity: NonEmptyStringSchema
|
|
@@ -5733,12 +5968,16 @@ var init_contracts = __esm({
|
|
|
5733
5968
|
MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
|
|
5734
5969
|
RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
|
|
5735
5970
|
RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
|
|
5971
|
+
MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
|
|
5972
|
+
MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
|
|
5736
5973
|
CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
|
|
5737
5974
|
CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
|
|
5738
5975
|
RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
5739
5976
|
MessageRecordSchema,
|
|
5740
5977
|
RelayIntentRecordSchema,
|
|
5741
5978
|
RelayResultRecordSchema,
|
|
5979
|
+
MembershipIntentRecordSchema,
|
|
5980
|
+
MembershipResultRecordSchema,
|
|
5742
5981
|
CloseNoticeIntentRecordSchema,
|
|
5743
5982
|
CloseNoticeResultRecordSchema
|
|
5744
5983
|
]);
|
|
@@ -5750,19 +5989,54 @@ var init_contracts = __esm({
|
|
|
5750
5989
|
message: 'record_id must equal room_id + ":" + seq'
|
|
5751
5990
|
});
|
|
5752
5991
|
}
|
|
5992
|
+
if (record.kind === "message") refineMessageCategory(record, context);
|
|
5753
5993
|
});
|
|
5754
5994
|
AppendRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
5755
5995
|
external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
|
|
5756
5996
|
external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
|
|
5757
5997
|
external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
|
|
5998
|
+
external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
|
|
5999
|
+
external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
|
|
5758
6000
|
external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
|
|
5759
6001
|
external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
|
|
5760
|
-
])
|
|
6002
|
+
]).superRefine((record, context) => {
|
|
6003
|
+
if (record.kind === "message") refineMessageCategory(record, context);
|
|
6004
|
+
});
|
|
5761
6005
|
}
|
|
5762
6006
|
});
|
|
5763
6007
|
|
|
5764
|
-
// src/
|
|
6008
|
+
// src/ulid.ts
|
|
5765
6009
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6010
|
+
function generateUlid() {
|
|
6011
|
+
let time = Date.now();
|
|
6012
|
+
const output = new Array(26);
|
|
6013
|
+
for (let index = 9; index >= 0; index -= 1) {
|
|
6014
|
+
output[index] = CROCKFORD[time % 32];
|
|
6015
|
+
time = Math.floor(time / 32);
|
|
6016
|
+
}
|
|
6017
|
+
const entropy = randomBytes2(10);
|
|
6018
|
+
let bits = 0;
|
|
6019
|
+
let value = 0;
|
|
6020
|
+
let byteIndex = 0;
|
|
6021
|
+
for (let index = 10; index < 26; index += 1) {
|
|
6022
|
+
while (bits < 5) {
|
|
6023
|
+
value = value << 8 | entropy[byteIndex++];
|
|
6024
|
+
bits += 8;
|
|
6025
|
+
}
|
|
6026
|
+
bits -= 5;
|
|
6027
|
+
output[index] = CROCKFORD[value >>> bits & 31];
|
|
6028
|
+
}
|
|
6029
|
+
return output.join("");
|
|
6030
|
+
}
|
|
6031
|
+
var CROCKFORD;
|
|
6032
|
+
var init_ulid = __esm({
|
|
6033
|
+
"src/ulid.ts"() {
|
|
6034
|
+
"use strict";
|
|
6035
|
+
CROCKFORD = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
6036
|
+
}
|
|
6037
|
+
});
|
|
6038
|
+
|
|
6039
|
+
// src/intake.ts
|
|
5766
6040
|
function canonicalJson(value) {
|
|
5767
6041
|
const encoded = JSON.stringify(canonicalValue(value));
|
|
5768
6042
|
if (encoded === void 0) throw new TypeError("canonical JSON value is not serializable");
|
|
@@ -5783,34 +6057,25 @@ function canonicalValue(value) {
|
|
|
5783
6057
|
function unique(values) {
|
|
5784
6058
|
return [...new Set(values)];
|
|
5785
6059
|
}
|
|
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];
|
|
6060
|
+
function wireKind(category) {
|
|
6061
|
+
switch (category) {
|
|
6062
|
+
case "briefing":
|
|
6063
|
+
return "room_briefing";
|
|
6064
|
+
case "role_briefing":
|
|
6065
|
+
return "room_role_briefing";
|
|
6066
|
+
case "membership":
|
|
6067
|
+
return "room_membership";
|
|
6068
|
+
default:
|
|
6069
|
+
return "room_msg";
|
|
5804
6070
|
}
|
|
5805
|
-
return output.join("");
|
|
5806
6071
|
}
|
|
5807
|
-
var
|
|
6072
|
+
var IntakePump;
|
|
5808
6073
|
var init_intake = __esm({
|
|
5809
6074
|
"src/intake.ts"() {
|
|
5810
6075
|
"use strict";
|
|
5811
6076
|
init_zod();
|
|
5812
6077
|
init_contracts();
|
|
5813
|
-
|
|
6078
|
+
init_ulid();
|
|
5814
6079
|
IntakePump = class {
|
|
5815
6080
|
store;
|
|
5816
6081
|
packets;
|
|
@@ -5895,15 +6160,18 @@ var init_intake = __esm({
|
|
|
5895
6160
|
}
|
|
5896
6161
|
async processInboxItem(roomId, packet, item) {
|
|
5897
6162
|
const room = await this.store.load(roomId);
|
|
5898
|
-
const seat = room.seats.find(
|
|
6163
|
+
const seat = room.seats.find(
|
|
6164
|
+
(candidate) => candidate.identity === item.sender_id && candidate.state === "active"
|
|
6165
|
+
);
|
|
5899
6166
|
if (room.state !== "active" || !seat) {
|
|
6167
|
+
if (room.state === "active") await this.bounceRemovedSender(roomId, room, packet, item);
|
|
5900
6168
|
await packet.consumeInbox([item.msg_id]);
|
|
5901
6169
|
return;
|
|
5902
6170
|
}
|
|
5903
6171
|
const before = await this.store.read(roomId);
|
|
5904
6172
|
let message = this.findSourceMessage(before, item);
|
|
5905
6173
|
if (!message) {
|
|
5906
|
-
const recipientIdentities = unique(room.seats.map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
|
|
6174
|
+
const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
|
|
5907
6175
|
const appended = await this.store.append(roomId, {
|
|
5908
6176
|
version: 1,
|
|
5909
6177
|
kind: "message",
|
|
@@ -5915,6 +6183,9 @@ var init_intake = __esm({
|
|
|
5915
6183
|
display_name: seat.display_name,
|
|
5916
6184
|
role: seat.role
|
|
5917
6185
|
},
|
|
6186
|
+
// In an anonymous room the archive keeps both identities (INV-R4);
|
|
6187
|
+
// the relay pump substitutes the alias into every outbound body.
|
|
6188
|
+
...room.anonymous && seat.alias !== void 0 ? { author_alias: { participant_id: seat.participant_id, alias: seat.alias } } : {},
|
|
5918
6189
|
category: "chat",
|
|
5919
6190
|
text: item.text,
|
|
5920
6191
|
recipient_identities: recipientIdentities,
|
|
@@ -5927,6 +6198,28 @@ var init_intake = __esm({
|
|
|
5927
6198
|
await this.completeMessageIntents(roomId, message);
|
|
5928
6199
|
await packet.consumeInbox([item.msg_id]);
|
|
5929
6200
|
}
|
|
6201
|
+
/**
|
|
6202
|
+
* One content-free self-assertion per removed seat (spec §5.2, OC-8), so a
|
|
6203
|
+
* healthy ex-client stops sending. The durable bounced_at mark precedes the
|
|
6204
|
+
* best-effort send: at-most-once, and a hostile peer gets nothing further.
|
|
6205
|
+
*/
|
|
6206
|
+
async bounceRemovedSender(roomId, room, packet, item) {
|
|
6207
|
+
const removed = room.seats.find(
|
|
6208
|
+
(candidate) => candidate.identity === item.sender_id && candidate.state === "removed"
|
|
6209
|
+
);
|
|
6210
|
+
if (!removed || removed.bounced_at !== void 0) return;
|
|
6211
|
+
if (room.seats.some(
|
|
6212
|
+
(candidate) => candidate.identity === item.sender_id && candidate.state === "active"
|
|
6213
|
+
)) return;
|
|
6214
|
+
const seats = room.seats.map((candidate) => candidate.participant_id === removed.participant_id ? { ...candidate, bounced_at: this.now() } : candidate);
|
|
6215
|
+
await this.store.save(RoomSchema.parse({ ...room, seats }));
|
|
6216
|
+
try {
|
|
6217
|
+
const unsigned = { version: 1, kind: "room_not_member", room_id: roomId };
|
|
6218
|
+
const signature = await packet.sign(canonicalJson(unsigned));
|
|
6219
|
+
await packet.send(item.sender_id, canonicalJson({ ...unsigned, signature }));
|
|
6220
|
+
} catch {
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
5930
6223
|
async completeSnapshotIntents(roomId) {
|
|
5931
6224
|
const records = await this.store.read(roomId);
|
|
5932
6225
|
for (const message of records.filter(
|
|
@@ -5951,6 +6244,9 @@ var init_intake = __esm({
|
|
|
5951
6244
|
}
|
|
5952
6245
|
async relayPendingUnlocked(roomId, packet) {
|
|
5953
6246
|
const records = await this.store.read(roomId);
|
|
6247
|
+
const room = await this.store.load(roomId);
|
|
6248
|
+
const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
|
|
6249
|
+
const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
|
|
5954
6250
|
const messages = new Map(records.filter((record) => record.kind === "message").map((message) => [message.message_id, message]));
|
|
5955
6251
|
const completed = new Set(records.filter((record) => record.kind === "relay_result").map((result) => result.kind === "relay_result" ? result.intent_record_id : ""));
|
|
5956
6252
|
for (const intent of records.filter(
|
|
@@ -5960,14 +6256,37 @@ var init_intake = __esm({
|
|
|
5960
6256
|
const message = messages.get(intent.message_id);
|
|
5961
6257
|
if (!message) continue;
|
|
5962
6258
|
if (!message.recipient_identities.includes(intent.recipient_identity)) continue;
|
|
6259
|
+
if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
|
|
6260
|
+
const skipped = await this.store.append(roomId, {
|
|
6261
|
+
version: 1,
|
|
6262
|
+
kind: "relay_result",
|
|
6263
|
+
room_id: roomId,
|
|
6264
|
+
at: this.now(),
|
|
6265
|
+
intent_record_id: intent.record_id,
|
|
6266
|
+
message_id: intent.message_id,
|
|
6267
|
+
recipient_identity: intent.recipient_identity,
|
|
6268
|
+
status: "skipped_removed"
|
|
6269
|
+
});
|
|
6270
|
+
if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6271
|
+
completed.add(intent.record_id);
|
|
6272
|
+
continue;
|
|
6273
|
+
}
|
|
5963
6274
|
const unsigned = {
|
|
5964
6275
|
version: 1,
|
|
5965
|
-
kind: message.category
|
|
6276
|
+
kind: wireKind(message.category),
|
|
5966
6277
|
room_id: roomId,
|
|
5967
6278
|
message_id: message.message_id,
|
|
5968
|
-
|
|
6279
|
+
// INV-R3: an anonymous author leaves the archive only in alias form.
|
|
6280
|
+
author: message.author_alias === void 0 ? message.author : {
|
|
6281
|
+
identity: message.author_alias.participant_id,
|
|
6282
|
+
display_name: message.author_alias.alias,
|
|
6283
|
+
role: message.author.role
|
|
6284
|
+
},
|
|
5969
6285
|
text: message.text,
|
|
5970
|
-
at: message.at
|
|
6286
|
+
at: message.at,
|
|
6287
|
+
...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
|
|
6288
|
+
...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
|
|
6289
|
+
...message.membership === void 0 ? {} : { membership: message.membership }
|
|
5971
6290
|
};
|
|
5972
6291
|
const signature = await packet.sign(canonicalJson(unsigned));
|
|
5973
6292
|
const body = canonicalJson({ ...unsigned, signature });
|
|
@@ -6012,27 +6331,11 @@ var init_intake = __esm({
|
|
|
6012
6331
|
});
|
|
6013
6332
|
|
|
6014
6333
|
// src/service.ts
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
output[index] = CROCKFORD2[time % 32];
|
|
6021
|
-
time = Math.floor(time / 32);
|
|
6022
|
-
}
|
|
6023
|
-
const entropy = randomBytes3(10);
|
|
6024
|
-
let bits = 0;
|
|
6025
|
-
let value = 0;
|
|
6026
|
-
let byteIndex = 0;
|
|
6027
|
-
for (let index = 10; index < 26; index += 1) {
|
|
6028
|
-
while (bits < 5) {
|
|
6029
|
-
value = value << 8 | entropy[byteIndex++];
|
|
6030
|
-
bits += 8;
|
|
6031
|
-
}
|
|
6032
|
-
bits -= 5;
|
|
6033
|
-
output[index] = CROCKFORD2[value >>> bits & 31];
|
|
6034
|
-
}
|
|
6035
|
-
return output.join("");
|
|
6334
|
+
function activeSeats(room) {
|
|
6335
|
+
return room.seats.filter((seat) => seat.state === "active");
|
|
6336
|
+
}
|
|
6337
|
+
function mintAlias(seated, role) {
|
|
6338
|
+
return `${role} #${seated.filter((seat) => seat.role === role).length + 1}`;
|
|
6036
6339
|
}
|
|
6037
6340
|
function uniqueIdentities(identities) {
|
|
6038
6341
|
return [...new Set(identities)];
|
|
@@ -6040,18 +6343,18 @@ function uniqueIdentities(identities) {
|
|
|
6040
6343
|
function currentContactIdentities(packet) {
|
|
6041
6344
|
return new Set(packet.listContacts().map((contact) => contact.container_id));
|
|
6042
6345
|
}
|
|
6043
|
-
var
|
|
6346
|
+
var ROOM_ROLE, CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
|
|
6044
6347
|
var init_service = __esm({
|
|
6045
6348
|
"src/service.ts"() {
|
|
6046
6349
|
"use strict";
|
|
6047
6350
|
init_zod();
|
|
6048
6351
|
init_contracts();
|
|
6049
6352
|
init_intake();
|
|
6050
|
-
|
|
6353
|
+
init_ulid();
|
|
6051
6354
|
ROOM_ROLE = "room";
|
|
6052
6355
|
CreateInviteInputSchema = external_exports.object({
|
|
6053
6356
|
mode: InviteModeSchema,
|
|
6054
|
-
role: RoleSchema,
|
|
6357
|
+
role: RoleSchema.optional(),
|
|
6055
6358
|
min_accepts: external_exports.number().int().positive().safe()
|
|
6056
6359
|
}).strict().superRefine((input, context) => {
|
|
6057
6360
|
if (input.mode === "one_time" && input.min_accepts !== 1) {
|
|
@@ -6064,11 +6367,22 @@ var init_service = __esm({
|
|
|
6064
6367
|
});
|
|
6065
6368
|
HistoryOptionsSchema = external_exports.object({
|
|
6066
6369
|
after: external_exports.number().int().nonnegative().safe().optional(),
|
|
6067
|
-
limit: external_exports.number().int().positive().safe().optional()
|
|
6370
|
+
limit: external_exports.number().int().positive().safe().optional(),
|
|
6371
|
+
view: external_exports.enum(["operator", "participant"]).optional()
|
|
6068
6372
|
}).strict();
|
|
6069
6373
|
DeleteRoomInputSchema = external_exports.object({
|
|
6070
6374
|
confirm: external_exports.literal(true)
|
|
6071
6375
|
}).strict();
|
|
6376
|
+
RemoveParticipantInputSchema = external_exports.object({
|
|
6377
|
+
participant: external_exports.string().min(1),
|
|
6378
|
+
notify: external_exports.boolean().optional()
|
|
6379
|
+
}).strict();
|
|
6380
|
+
ReplaceParticipantInputSchema = external_exports.object({
|
|
6381
|
+
participant: external_exports.string().min(1),
|
|
6382
|
+
notify: external_exports.boolean().optional(),
|
|
6383
|
+
mode: InviteModeSchema.optional(),
|
|
6384
|
+
min_accepts: external_exports.number().int().positive().safe().optional()
|
|
6385
|
+
}).strict();
|
|
6072
6386
|
RoomServiceError = class extends Error {
|
|
6073
6387
|
constructor(message, options) {
|
|
6074
6388
|
super(message, options);
|
|
@@ -6087,8 +6401,8 @@ var init_service = __esm({
|
|
|
6087
6401
|
this.store = store;
|
|
6088
6402
|
this.packets = packets;
|
|
6089
6403
|
this.nowValue = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
6090
|
-
this.nextRoomId = options.roomId ??
|
|
6091
|
-
this.nextMessageId = options.messageId ??
|
|
6404
|
+
this.nextRoomId = options.roomId ?? generateUlid;
|
|
6405
|
+
this.nextMessageId = options.messageId ?? generateUlid;
|
|
6092
6406
|
this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
|
|
6093
6407
|
});
|
|
6094
6408
|
this.intake = new IntakePump(store, packets, {
|
|
@@ -6102,14 +6416,18 @@ var init_service = __esm({
|
|
|
6102
6416
|
const identityName = `cowork-room-${roomId}`;
|
|
6103
6417
|
return this.lock(roomId, async () => {
|
|
6104
6418
|
const provisional = RoomSchema.parse({
|
|
6105
|
-
version:
|
|
6419
|
+
version: 2,
|
|
6106
6420
|
room_id: roomId,
|
|
6107
6421
|
identity_name: identityName,
|
|
6108
6422
|
// PacketRegistry needs the durable room directory to exist first. A
|
|
6109
6423
|
// valid, explicitly provisional value lets startup resume this exact
|
|
6110
6424
|
// two-resource boundary without claiming a packet CID yet.
|
|
6111
6425
|
identity_cid: "",
|
|
6112
|
-
mission: { goal: settings.goal, briefing: settings.briefing },
|
|
6426
|
+
mission: { goal: settings.goal, briefing: settings.briefing, briefing_version: 1 },
|
|
6427
|
+
role_briefings: {},
|
|
6428
|
+
anonymous: settings.anonymous ?? false,
|
|
6429
|
+
quiet_membership: settings.quiet_membership ?? false,
|
|
6430
|
+
membership_epoch: 0,
|
|
6113
6431
|
state: "provisioning",
|
|
6114
6432
|
status: "packet_pending",
|
|
6115
6433
|
invites: [],
|
|
@@ -6200,18 +6518,73 @@ var init_service = __esm({
|
|
|
6200
6518
|
async updateRoom(roomId, input) {
|
|
6201
6519
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
6202
6520
|
const settings = UpdateRoomInputSchema.parse(input);
|
|
6203
|
-
|
|
6521
|
+
const { room: updated, redelivered } = await this.lock(id, async () => {
|
|
6204
6522
|
const room = await this.store.load(id);
|
|
6205
6523
|
this.assertMutable(room, "update");
|
|
6524
|
+
const briefingChanged = settings.briefing !== void 0 && settings.briefing !== room.mission.briefing;
|
|
6206
6525
|
const mission = {
|
|
6207
6526
|
goal: settings.goal ?? room.mission.goal,
|
|
6208
|
-
briefing: settings.briefing ?? room.mission.briefing
|
|
6527
|
+
briefing: settings.briefing ?? room.mission.briefing,
|
|
6528
|
+
briefing_version: briefingChanged ? room.mission.briefing_version + 1 : room.mission.briefing_version
|
|
6209
6529
|
};
|
|
6210
|
-
|
|
6530
|
+
const next = await this.store.save(RoomSchema.parse({
|
|
6211
6531
|
...room,
|
|
6212
6532
|
mission,
|
|
6533
|
+
...settings.quiet_membership === void 0 ? {} : { quiet_membership: settings.quiet_membership },
|
|
6213
6534
|
...settings.status === void 0 ? {} : { status: settings.status }
|
|
6214
6535
|
}));
|
|
6536
|
+
if (briefingChanged && next.state === "active") {
|
|
6537
|
+
await this.redeliverCommonBriefing(next);
|
|
6538
|
+
return { room: next, redelivered: true };
|
|
6539
|
+
}
|
|
6540
|
+
return { room: next, redelivered: false };
|
|
6541
|
+
});
|
|
6542
|
+
if (redelivered) await this.intake.resumePending(id);
|
|
6543
|
+
return updated;
|
|
6544
|
+
}
|
|
6545
|
+
/** Author or edit one role's briefing; an edit bumps its version and re-delivers to seats of that role only (spec §3.3). */
|
|
6546
|
+
async setRoleBriefing(roomId, input) {
|
|
6547
|
+
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
6548
|
+
const request = RoleBriefingSetInputSchema.parse(input);
|
|
6549
|
+
const { room: updated, redelivered } = await this.lock(id, async () => {
|
|
6550
|
+
const room = await this.store.load(id);
|
|
6551
|
+
this.assertMutable(room, "set a role briefing for");
|
|
6552
|
+
const existing = room.role_briefings[request.role];
|
|
6553
|
+
if (existing && existing.text === request.text) return { room, redelivered: false };
|
|
6554
|
+
const briefing = {
|
|
6555
|
+
text: request.text,
|
|
6556
|
+
version: existing === void 0 ? 1 : existing.version + 1,
|
|
6557
|
+
updated_at: this.now()
|
|
6558
|
+
};
|
|
6559
|
+
const next = await this.store.save(RoomSchema.parse({
|
|
6560
|
+
...room,
|
|
6561
|
+
role_briefings: { ...room.role_briefings, [request.role]: briefing }
|
|
6562
|
+
}));
|
|
6563
|
+
if (next.state !== "active") return { room: next, redelivered: false };
|
|
6564
|
+
const holders = activeSeats(next).filter((seat) => seat.role === request.role);
|
|
6565
|
+
if (holders.length === 0) return { room: next, redelivered: false };
|
|
6566
|
+
await this.ensureBriefingKind(next, holders, {
|
|
6567
|
+
category: "role_briefing",
|
|
6568
|
+
briefing_role: request.role,
|
|
6569
|
+
text: briefing.text,
|
|
6570
|
+
briefing_version: briefing.version
|
|
6571
|
+
});
|
|
6572
|
+
return { room: next, redelivered: true };
|
|
6573
|
+
});
|
|
6574
|
+
if (redelivered) await this.intake.resumePending(id);
|
|
6575
|
+
return updated;
|
|
6576
|
+
}
|
|
6577
|
+
async deleteRoleBriefing(roomId, input) {
|
|
6578
|
+
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
6579
|
+
const request = RoleBriefingDeleteInputSchema.parse(input);
|
|
6580
|
+
return this.lock(id, async () => {
|
|
6581
|
+
const room = await this.store.load(id);
|
|
6582
|
+
this.assertMutable(room, "delete a role briefing for");
|
|
6583
|
+
if (room.role_briefings[request.role] === void 0) {
|
|
6584
|
+
throw new RoomServiceError(`no role briefing exists for "${request.role}" in room "${id}"`);
|
|
6585
|
+
}
|
|
6586
|
+
const { [request.role]: _removed, ...rest } = room.role_briefings;
|
|
6587
|
+
return this.store.save(RoomSchema.parse({ ...room, role_briefings: rest }));
|
|
6215
6588
|
});
|
|
6216
6589
|
}
|
|
6217
6590
|
async createInvite(roomId, input) {
|
|
@@ -6220,33 +6593,215 @@ var init_service = __esm({
|
|
|
6220
6593
|
return this.lock(id, async () => {
|
|
6221
6594
|
const room = await this.store.load(id);
|
|
6222
6595
|
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,
|
|
6596
|
+
return this.mintInviteUnlocked(room, {
|
|
6227
6597
|
mode: request.mode,
|
|
6228
|
-
role: request.role,
|
|
6229
|
-
min_accepts: request.min_accepts
|
|
6230
|
-
accepted_cids: [],
|
|
6231
|
-
state: "live",
|
|
6232
|
-
created_at: this.now()
|
|
6598
|
+
role: request.role ?? DEFAULT_ROLE,
|
|
6599
|
+
min_accepts: request.min_accepts
|
|
6233
6600
|
});
|
|
6601
|
+
});
|
|
6602
|
+
}
|
|
6603
|
+
async mintInviteUnlocked(room, request) {
|
|
6604
|
+
const packet = this.packet(room.room_id);
|
|
6605
|
+
const minted = await packet.mintInvite(request.mode);
|
|
6606
|
+
const invite = RoomInviteSchema.parse({
|
|
6607
|
+
invite_id: minted.invite_id,
|
|
6608
|
+
mode: request.mode,
|
|
6609
|
+
role: request.role,
|
|
6610
|
+
min_accepts: request.min_accepts,
|
|
6611
|
+
accepted_cids: [],
|
|
6612
|
+
state: "live",
|
|
6613
|
+
created_at: this.now(),
|
|
6614
|
+
...request.replaces_seat === void 0 ? {} : { replaces_seat: request.replaces_seat }
|
|
6615
|
+
});
|
|
6616
|
+
try {
|
|
6617
|
+
await this.store.save(RoomSchema.parse({ ...room, invites: [...room.invites, invite] }));
|
|
6618
|
+
} catch (error) {
|
|
6234
6619
|
try {
|
|
6235
|
-
await
|
|
6236
|
-
} catch
|
|
6237
|
-
try {
|
|
6238
|
-
await packet.revokeInvite(minted.invite_id);
|
|
6239
|
-
} catch {
|
|
6240
|
-
}
|
|
6241
|
-
throw error;
|
|
6620
|
+
await packet.revokeInvite(minted.invite_id);
|
|
6621
|
+
} catch {
|
|
6242
6622
|
}
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6623
|
+
throw error;
|
|
6624
|
+
}
|
|
6625
|
+
return {
|
|
6626
|
+
room_id: room.room_id,
|
|
6627
|
+
invite,
|
|
6628
|
+
blob: minted.blob,
|
|
6629
|
+
reusable: minted.reusable
|
|
6630
|
+
};
|
|
6631
|
+
}
|
|
6632
|
+
/**
|
|
6633
|
+
* Operator-only removal (spec §5.2): archive-before-act membership intent,
|
|
6634
|
+
* seat state flip + epoch bump, core 0.13 bilateral sever with an honest
|
|
6635
|
+
* receipt, and an alias-form announcement unless the room or call is quiet.
|
|
6636
|
+
*/
|
|
6637
|
+
async removeParticipant(roomId, input) {
|
|
6638
|
+
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
6639
|
+
const request = RemoveParticipantInputSchema.parse(input);
|
|
6640
|
+
const receipt = await this.lock(id, async () => {
|
|
6641
|
+
const room = await this.store.load(id);
|
|
6642
|
+
this.assertMutable(room, "remove a participant from");
|
|
6643
|
+
const seat = this.findActiveSeat(room, request.participant);
|
|
6644
|
+
const notify = (request.notify ?? true) && !room.quiet_membership;
|
|
6645
|
+
return this.beginRemovalUnlocked(room, seat, notify);
|
|
6646
|
+
});
|
|
6647
|
+
await this.intake.resumePending(id);
|
|
6648
|
+
return receipt;
|
|
6649
|
+
}
|
|
6650
|
+
/**
|
|
6651
|
+
* Removal plus a same-role invite stamped with the seat lineage (spec §5.3).
|
|
6652
|
+
* Owner override OC-2/OC-6: in an anonymous room the flow is unconditionally
|
|
6653
|
+
* silent — the successor inherits the alias and other members see nothing.
|
|
6654
|
+
*/
|
|
6655
|
+
async replaceParticipant(roomId, input) {
|
|
6656
|
+
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
6657
|
+
const request = ReplaceParticipantInputSchema.parse(input);
|
|
6658
|
+
const receipt = await this.lock(id, async () => {
|
|
6659
|
+
const room = await this.store.load(id);
|
|
6660
|
+
this.assertMutable(room, "replace a participant in");
|
|
6661
|
+
const seat = this.findActiveSeat(room, request.participant);
|
|
6662
|
+
const notify = room.anonymous ? false : (request.notify ?? true) && !room.quiet_membership;
|
|
6663
|
+
const removal = await this.beginRemovalUnlocked(room, seat, notify);
|
|
6664
|
+
const current = await this.store.load(id);
|
|
6665
|
+
const invite = await this.mintInviteUnlocked(current, {
|
|
6666
|
+
mode: request.mode ?? "one_time",
|
|
6667
|
+
role: seat.role,
|
|
6668
|
+
min_accepts: request.min_accepts ?? 1,
|
|
6669
|
+
replaces_seat: seat.participant_id
|
|
6670
|
+
});
|
|
6671
|
+
return { ...invite, removal };
|
|
6672
|
+
});
|
|
6673
|
+
await this.intake.resumePending(id);
|
|
6674
|
+
return receipt;
|
|
6675
|
+
}
|
|
6676
|
+
findActiveSeat(room, participant) {
|
|
6677
|
+
const seat = room.seats.find((candidate) => candidate.state === "active" && (candidate.identity === participant || candidate.participant_id === participant));
|
|
6678
|
+
if (!seat) {
|
|
6679
|
+
throw new RoomServiceError(`"${participant}" is not an active participant of room "${room.room_id}"`);
|
|
6680
|
+
}
|
|
6681
|
+
return seat;
|
|
6682
|
+
}
|
|
6683
|
+
async beginRemovalUnlocked(room, seat, notify) {
|
|
6684
|
+
const intent = await this.store.append(room.room_id, {
|
|
6685
|
+
version: 1,
|
|
6686
|
+
kind: "membership_intent",
|
|
6687
|
+
room_id: room.room_id,
|
|
6688
|
+
at: this.now(),
|
|
6689
|
+
action: "remove",
|
|
6690
|
+
participant_id: seat.participant_id,
|
|
6691
|
+
recipient_identity: seat.identity,
|
|
6692
|
+
role: seat.role,
|
|
6693
|
+
// The participant-visible label: the alias in anonymous rooms, the
|
|
6694
|
+
// contact display name otherwise (INV-R3 holds either way).
|
|
6695
|
+
alias: seat.alias ?? seat.display_name,
|
|
6696
|
+
epoch: room.membership_epoch + 1,
|
|
6697
|
+
notify
|
|
6698
|
+
});
|
|
6699
|
+
if (intent.kind !== "membership_intent") {
|
|
6700
|
+
throw new RoomServiceError("storage returned the wrong membership intent kind");
|
|
6701
|
+
}
|
|
6702
|
+
const { receipt } = await this.completeRemovalUnlocked(room, intent);
|
|
6703
|
+
return receipt;
|
|
6704
|
+
}
|
|
6705
|
+
/**
|
|
6706
|
+
* Idempotent completion of a durable removal intent: each step re-checks the
|
|
6707
|
+
* archive/state it would produce, so a crash anywhere re-drives cleanly
|
|
6708
|
+
* (INV-R5; the 0.13 sever is replay-safe by design).
|
|
6709
|
+
*/
|
|
6710
|
+
async completeRemovalUnlocked(room, intent) {
|
|
6711
|
+
let current = room;
|
|
6712
|
+
const index = current.seats.findIndex(
|
|
6713
|
+
(candidate) => candidate.participant_id === intent.participant_id
|
|
6714
|
+
);
|
|
6715
|
+
if (index < 0) {
|
|
6716
|
+
throw new RoomServiceError(
|
|
6717
|
+
`membership intent ${intent.record_id} references an unknown seat in room "${current.room_id}"`
|
|
6718
|
+
);
|
|
6719
|
+
}
|
|
6720
|
+
if (current.seats[index].state !== "removed") {
|
|
6721
|
+
const seats = [...current.seats];
|
|
6722
|
+
seats[index] = {
|
|
6723
|
+
...seats[index],
|
|
6724
|
+
state: "removed",
|
|
6725
|
+
removed_at: intent.at,
|
|
6726
|
+
removed_epoch: intent.epoch
|
|
6248
6727
|
};
|
|
6728
|
+
current = await this.store.save(RoomSchema.parse({
|
|
6729
|
+
...current,
|
|
6730
|
+
seats,
|
|
6731
|
+
membership_epoch: Math.max(current.membership_epoch, intent.epoch)
|
|
6732
|
+
}));
|
|
6733
|
+
}
|
|
6734
|
+
const records = await this.store.read(current.room_id);
|
|
6735
|
+
const existing = records.find((record) => record.kind === "membership_result" && record.intent_record_id === intent.record_id);
|
|
6736
|
+
let outcome;
|
|
6737
|
+
if (existing !== void 0 && existing.kind === "membership_result") {
|
|
6738
|
+
outcome = {
|
|
6739
|
+
status: existing.status,
|
|
6740
|
+
notified: existing.notified,
|
|
6741
|
+
key_material_retained: true
|
|
6742
|
+
};
|
|
6743
|
+
} else {
|
|
6744
|
+
outcome = await this.packet(current.room_id).removeContact(intent.recipient_identity);
|
|
6745
|
+
const result = await this.store.append(current.room_id, {
|
|
6746
|
+
version: 1,
|
|
6747
|
+
kind: "membership_result",
|
|
6748
|
+
room_id: current.room_id,
|
|
6749
|
+
at: this.now(),
|
|
6750
|
+
intent_record_id: intent.record_id,
|
|
6751
|
+
participant_id: intent.participant_id,
|
|
6752
|
+
status: outcome.status,
|
|
6753
|
+
notified: outcome.notified,
|
|
6754
|
+
key_material_retained: true
|
|
6755
|
+
});
|
|
6756
|
+
if (result.kind !== "membership_result") {
|
|
6757
|
+
throw new RoomServiceError("storage returned the wrong membership result kind");
|
|
6758
|
+
}
|
|
6759
|
+
}
|
|
6760
|
+
if (intent.notify) await this.ensureMembershipNotice(current, intent);
|
|
6761
|
+
return {
|
|
6762
|
+
room: current,
|
|
6763
|
+
receipt: {
|
|
6764
|
+
room_id: current.room_id,
|
|
6765
|
+
participant_id: intent.participant_id,
|
|
6766
|
+
epoch: intent.epoch,
|
|
6767
|
+
status: outcome.status,
|
|
6768
|
+
notified: outcome.notified,
|
|
6769
|
+
key_material_retained: true
|
|
6770
|
+
}
|
|
6771
|
+
};
|
|
6772
|
+
}
|
|
6773
|
+
async ensureMembershipNotice(room, intent) {
|
|
6774
|
+
const records = await this.store.read(room.room_id);
|
|
6775
|
+
const already = records.some((record) => record.kind === "message" && record.category === "membership" && record.membership?.action === "remove" && record.membership.epoch === intent.epoch);
|
|
6776
|
+
if (already) return;
|
|
6777
|
+
const remaining = activeSeats(room);
|
|
6778
|
+
if (remaining.length === 0) return;
|
|
6779
|
+
const label = intent.alias ?? intent.role;
|
|
6780
|
+
const appended = await this.store.append(room.room_id, {
|
|
6781
|
+
version: 1,
|
|
6782
|
+
kind: "message",
|
|
6783
|
+
room_id: room.room_id,
|
|
6784
|
+
at: this.now(),
|
|
6785
|
+
message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
|
|
6786
|
+
author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
|
|
6787
|
+
category: "membership",
|
|
6788
|
+
text: `${label} left the room \xB7 epoch ${intent.epoch}`,
|
|
6789
|
+
membership: { action: "remove", alias: label, role: intent.role, epoch: intent.epoch },
|
|
6790
|
+
recipient_identities: uniqueIdentities(remaining.map((seat) => seat.identity))
|
|
6249
6791
|
});
|
|
6792
|
+
if (appended.kind !== "message") {
|
|
6793
|
+
throw new RoomServiceError("storage returned the wrong membership notice kind");
|
|
6794
|
+
}
|
|
6795
|
+
for (const recipientIdentity of appended.recipient_identities) {
|
|
6796
|
+
await this.store.append(room.room_id, {
|
|
6797
|
+
version: 1,
|
|
6798
|
+
kind: "relay_intent",
|
|
6799
|
+
room_id: room.room_id,
|
|
6800
|
+
at: this.now(),
|
|
6801
|
+
message_id: appended.message_id,
|
|
6802
|
+
recipient_identity: recipientIdentity
|
|
6803
|
+
});
|
|
6804
|
+
}
|
|
6250
6805
|
}
|
|
6251
6806
|
async revokeInvite(roomId, inviteId) {
|
|
6252
6807
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
@@ -6426,8 +6981,26 @@ var init_service = __esm({
|
|
|
6426
6981
|
}
|
|
6427
6982
|
async history(roomId, options = {}) {
|
|
6428
6983
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
6429
|
-
const page = HistoryOptionsSchema.parse(options);
|
|
6430
|
-
|
|
6984
|
+
const { view, ...page } = HistoryOptionsSchema.parse(options);
|
|
6985
|
+
const records = await this.store.read(id, page);
|
|
6986
|
+
if (view !== "participant") return records;
|
|
6987
|
+
return records.filter((record) => record.kind === "message").map((record) => {
|
|
6988
|
+
const {
|
|
6989
|
+
author_alias,
|
|
6990
|
+
recipient_identities: _recipients,
|
|
6991
|
+
source_msg_id: _sourceMsg,
|
|
6992
|
+
source_wire_id: _sourceWire,
|
|
6993
|
+
...rest
|
|
6994
|
+
} = record;
|
|
6995
|
+
return {
|
|
6996
|
+
...rest,
|
|
6997
|
+
author: author_alias === void 0 ? record.author : {
|
|
6998
|
+
identity: author_alias.participant_id,
|
|
6999
|
+
display_name: author_alias.alias,
|
|
7000
|
+
role: record.author.role
|
|
7001
|
+
}
|
|
7002
|
+
};
|
|
7003
|
+
});
|
|
6431
7004
|
}
|
|
6432
7005
|
/**
|
|
6433
7006
|
* Forward-only close. Every external contact mutation is preceded by a
|
|
@@ -6492,7 +7065,7 @@ var init_service = __esm({
|
|
|
6492
7065
|
},
|
|
6493
7066
|
category: "chat",
|
|
6494
7067
|
text: request.text,
|
|
6495
|
-
recipient_identities: uniqueIdentities(room.
|
|
7068
|
+
recipient_identities: uniqueIdentities(activeSeats(room).map((seat) => seat.identity))
|
|
6496
7069
|
});
|
|
6497
7070
|
if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong room message kind");
|
|
6498
7071
|
for (const recipientIdentity of appended.recipient_identities) {
|
|
@@ -6517,7 +7090,15 @@ var init_service = __esm({
|
|
|
6517
7090
|
}
|
|
6518
7091
|
const origins = packet.listContactOrigins();
|
|
6519
7092
|
const inviteById = new Map(room.invites.map((invite) => [invite.invite_id, invite]));
|
|
6520
|
-
const existingCids = new Set(room.seats.map((seat) => seat.identity));
|
|
7093
|
+
const existingCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
|
|
7094
|
+
const lastRemovedAt = /* @__PURE__ */ new Map();
|
|
7095
|
+
for (const seat of room.seats) {
|
|
7096
|
+
if (seat.state !== "removed" || seat.removed_at === void 0) continue;
|
|
7097
|
+
const previous = lastRemovedAt.get(seat.identity);
|
|
7098
|
+
if (previous === void 0 || seat.removed_at > previous) {
|
|
7099
|
+
lastRemovedAt.set(seat.identity, seat.removed_at);
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
6521
7102
|
const newSeats = [];
|
|
6522
7103
|
for (const [cid, displayName] of contactsByCid) {
|
|
6523
7104
|
if (existingCids.has(cid)) continue;
|
|
@@ -6525,12 +7106,23 @@ var init_service = __esm({
|
|
|
6525
7106
|
if (!origin || origin.via !== "invite_one_time" && origin.via !== "invite_public") continue;
|
|
6526
7107
|
const invite = inviteById.get(origin.invite_id);
|
|
6527
7108
|
if (!invite || invite.state === "receipt_pending" || invite.recovery_of !== void 0 && invite.recovery_confirmed !== true) continue;
|
|
7109
|
+
const removedAt = lastRemovedAt.get(cid);
|
|
7110
|
+
if (removedAt !== void 0 && invite.created_at <= removedAt) continue;
|
|
7111
|
+
const seated = [...room.seats, ...newSeats];
|
|
7112
|
+
const predecessor = invite.replaces_seat === void 0 ? void 0 : seated.find((seat) => seat.participant_id === invite.replaces_seat && seat.state === "removed");
|
|
7113
|
+
if (invite.replaces_seat !== void 0 && !predecessor) continue;
|
|
6528
7114
|
newSeats.push({
|
|
6529
7115
|
identity: cid,
|
|
6530
7116
|
display_name: displayName,
|
|
6531
7117
|
role: invite.role,
|
|
6532
7118
|
invite_id: invite.invite_id,
|
|
6533
|
-
accepted_at: origin.at
|
|
7119
|
+
accepted_at: origin.at,
|
|
7120
|
+
participant_id: LowerCrockfordUlidSchema.parse(generateUlid()),
|
|
7121
|
+
state: "active",
|
|
7122
|
+
...predecessor === void 0 ? {} : { replaces_seat: predecessor.participant_id },
|
|
7123
|
+
// OC-6 (owner override): a replacement into a role inherits the
|
|
7124
|
+
// predecessor's alias — the alias binds to the seat/role lineage.
|
|
7125
|
+
...room.anonymous ? { alias: predecessor?.alias ?? mintAlias(seated, invite.role) } : {}
|
|
6534
7126
|
});
|
|
6535
7127
|
existingCids.add(cid);
|
|
6536
7128
|
}
|
|
@@ -6556,13 +7148,26 @@ var init_service = __esm({
|
|
|
6556
7148
|
}
|
|
6557
7149
|
return { ...invite, accepted_cids, state };
|
|
6558
7150
|
});
|
|
6559
|
-
let next = RoomSchema.parse({
|
|
7151
|
+
let next = RoomSchema.parse({
|
|
7152
|
+
...room,
|
|
7153
|
+
seats,
|
|
7154
|
+
invites,
|
|
7155
|
+
membership_epoch: room.membership_epoch + newSeats.length
|
|
7156
|
+
});
|
|
7157
|
+
const journal = await this.store.read(next.room_id);
|
|
7158
|
+
const completedIntents = new Set(journal.filter((record) => record.kind === "membership_result").map((record) => record.kind === "membership_result" ? record.intent_record_id : ""));
|
|
7159
|
+
for (const intent of journal.filter(
|
|
7160
|
+
(record) => record.kind === "membership_intent"
|
|
7161
|
+
)) {
|
|
7162
|
+
if (completedIntents.has(intent.record_id)) continue;
|
|
7163
|
+
({ room: next } = await this.completeRemovalUnlocked(next, intent));
|
|
7164
|
+
}
|
|
6560
7165
|
const requirementsMet = invites.filter((invite) => invite.state !== "revoked").every((invite) => invite.accepted_cids.length >= invite.min_accepts);
|
|
6561
7166
|
if (next.state === "provisioning" && seats.length > 0 && requirementsMet) {
|
|
6562
|
-
const activationAt = await this.ensureActivationBriefing(next,
|
|
7167
|
+
const activationAt = await this.ensureActivationBriefing(next, activeSeats(next));
|
|
6563
7168
|
next = RoomSchema.parse({ ...next, state: "active", activated_at: activationAt });
|
|
6564
|
-
} else if (next.state === "active") {
|
|
6565
|
-
|
|
7169
|
+
} else if (next.state === "active" && newSeats.length > 0) {
|
|
7170
|
+
await this.ensureActivationBriefing(next, newSeats);
|
|
6566
7171
|
}
|
|
6567
7172
|
return this.store.save(next);
|
|
6568
7173
|
}
|
|
@@ -6659,62 +7264,98 @@ var init_service = __esm({
|
|
|
6659
7264
|
deleteReceipt(roomId) {
|
|
6660
7265
|
return { version: 1, room_id: roomId, deleted: true, scope: "this_host" };
|
|
6661
7266
|
}
|
|
7267
|
+
/**
|
|
7268
|
+
* Deliver the common briefing followed by the seat's role briefing (spec
|
|
7269
|
+
* §3.3): exactly once per (seat, briefing kind, version) via the message +
|
|
7270
|
+
* relay-intent ledger. Returns the timestamp of the common briefing message
|
|
7271
|
+
* that admitted the earliest of the given recipients (activation time).
|
|
7272
|
+
*/
|
|
6662
7273
|
async ensureActivationBriefing(room, recipients) {
|
|
7274
|
+
const at = await this.ensureBriefingKind(room, recipients, {
|
|
7275
|
+
category: "briefing",
|
|
7276
|
+
text: room.mission.briefing,
|
|
7277
|
+
briefing_version: room.mission.briefing_version
|
|
7278
|
+
});
|
|
7279
|
+
await this.ensureRoleBriefings(room, recipients);
|
|
7280
|
+
return at;
|
|
7281
|
+
}
|
|
7282
|
+
async ensureRoleBriefings(room, recipients) {
|
|
7283
|
+
for (const seat of recipients) {
|
|
7284
|
+
const briefing = room.role_briefings[seat.role];
|
|
7285
|
+
if (!briefing) continue;
|
|
7286
|
+
await this.ensureBriefingKind(room, [seat], {
|
|
7287
|
+
category: "role_briefing",
|
|
7288
|
+
briefing_role: seat.role,
|
|
7289
|
+
text: briefing.text,
|
|
7290
|
+
briefing_version: briefing.version
|
|
7291
|
+
});
|
|
7292
|
+
}
|
|
7293
|
+
}
|
|
7294
|
+
/** Re-deliver the (just bumped) common briefing to every active seat. */
|
|
7295
|
+
async redeliverCommonBriefing(room) {
|
|
7296
|
+
await this.ensureBriefingKind(room, activeSeats(room), {
|
|
7297
|
+
category: "briefing",
|
|
7298
|
+
text: room.mission.briefing,
|
|
7299
|
+
briefing_version: room.mission.briefing_version
|
|
7300
|
+
});
|
|
7301
|
+
}
|
|
7302
|
+
async ensureBriefingKind(room, recipients, briefing) {
|
|
6663
7303
|
const records = await this.store.read(room.room_id);
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
)
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
7304
|
+
const matching = records.filter((record) => record.kind === "message" && record.category === briefing.category && record.briefing_role === briefing.briefing_role && (record.briefing_version ?? 1) === briefing.briefing_version);
|
|
7305
|
+
const intentsByMessage = /* @__PURE__ */ new Map();
|
|
7306
|
+
for (const record of records) {
|
|
7307
|
+
if (record.kind !== "relay_intent") continue;
|
|
7308
|
+
const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
|
|
7309
|
+
intents.add(record.recipient_identity);
|
|
7310
|
+
intentsByMessage.set(record.message_id, intents);
|
|
7311
|
+
}
|
|
7312
|
+
const covered = /* @__PURE__ */ new Set();
|
|
7313
|
+
for (const message of matching) {
|
|
7314
|
+
const intents = intentsByMessage.get(message.message_id) ?? /* @__PURE__ */ new Set();
|
|
7315
|
+
for (const recipientIdentity of message.recipient_identities) {
|
|
7316
|
+
covered.add(recipientIdentity);
|
|
7317
|
+
if (!intents.has(recipientIdentity)) {
|
|
7318
|
+
await this.store.append(room.room_id, {
|
|
7319
|
+
version: 1,
|
|
7320
|
+
kind: "relay_intent",
|
|
7321
|
+
room_id: room.room_id,
|
|
7322
|
+
at: this.now(),
|
|
7323
|
+
message_id: message.message_id,
|
|
7324
|
+
recipient_identity: recipientIdentity
|
|
7325
|
+
});
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
const missing = recipients.filter((seat) => !covered.has(seat.identity));
|
|
7330
|
+
let appendedAt;
|
|
7331
|
+
if (missing.length > 0) {
|
|
7332
|
+
const appended = await this.store.append(room.room_id, {
|
|
7333
|
+
version: 1,
|
|
7334
|
+
kind: "message",
|
|
7335
|
+
room_id: room.room_id,
|
|
7336
|
+
at: this.now(),
|
|
7337
|
+
message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
|
|
7338
|
+
author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
|
|
7339
|
+
category: briefing.category,
|
|
7340
|
+
...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
|
|
7341
|
+
briefing_version: briefing.briefing_version,
|
|
7342
|
+
text: briefing.text,
|
|
7343
|
+
recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
|
|
7344
|
+
});
|
|
7345
|
+
if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
|
|
7346
|
+
appendedAt = appended.at;
|
|
7347
|
+
for (const recipientIdentity of appended.recipient_identities) {
|
|
6671
7348
|
await this.store.append(room.room_id, {
|
|
6672
7349
|
version: 1,
|
|
6673
7350
|
kind: "relay_intent",
|
|
6674
7351
|
room_id: room.room_id,
|
|
6675
7352
|
at: this.now(),
|
|
6676
|
-
message_id:
|
|
7353
|
+
message_id: appended.message_id,
|
|
6677
7354
|
recipient_identity: recipientIdentity
|
|
6678
7355
|
});
|
|
6679
7356
|
}
|
|
6680
7357
|
}
|
|
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;
|
|
7358
|
+
return matching[0]?.at ?? appendedAt ?? this.now();
|
|
6718
7359
|
}
|
|
6719
7360
|
lock(roomId, work) {
|
|
6720
7361
|
return this.store.mutex(roomId).runExclusive(work);
|
|
@@ -6740,7 +7381,7 @@ var init_service = __esm({
|
|
|
6740
7381
|
});
|
|
6741
7382
|
|
|
6742
7383
|
// src/storage.ts
|
|
6743
|
-
import { randomBytes as
|
|
7384
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
6744
7385
|
import * as nodeFs3 from "node:fs";
|
|
6745
7386
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6746
7387
|
import { dirname as dirname4, join as join4 } from "node:path";
|
|
@@ -6749,6 +7390,7 @@ var init_storage = __esm({
|
|
|
6749
7390
|
"src/storage.ts"() {
|
|
6750
7391
|
"use strict";
|
|
6751
7392
|
init_contracts();
|
|
7393
|
+
init_ulid();
|
|
6752
7394
|
DIRECTORY_MODE2 = 448;
|
|
6753
7395
|
FILE_MODE2 = 384;
|
|
6754
7396
|
NO_FOLLOW2 = nodeFs3.constants.O_NOFOLLOW ?? 0;
|
|
@@ -6989,11 +7631,17 @@ var init_storage = __esm({
|
|
|
6989
7631
|
`room "${validRoomId}" has archive residue without deletion metadata`
|
|
6990
7632
|
);
|
|
6991
7633
|
}
|
|
6992
|
-
const expected = /* @__PURE__ */ new Set(["archive.jsonl", "room.json"]);
|
|
7634
|
+
const expected = /* @__PURE__ */ new Set(["archive.jsonl", "room.json", "room.json.v1.bak"]);
|
|
6993
7635
|
const unexpected = this.fs.readdirSync(roomDir).filter((name) => !expected.has(name));
|
|
6994
7636
|
if (unexpected.length > 0) {
|
|
6995
7637
|
throw new CoworkStorageError(`room "${validRoomId}" contains live or unexpected residue: ${unexpected.join(", ")}`);
|
|
6996
7638
|
}
|
|
7639
|
+
const backupPath = `${metadataPath}.v1.bak`;
|
|
7640
|
+
if (this.lstatIfPresent(backupPath)) {
|
|
7641
|
+
this.assertRegularFile(backupPath, "room metadata v1 backup");
|
|
7642
|
+
this.fs.unlinkSync(backupPath);
|
|
7643
|
+
this.fsyncDirectory(roomDir);
|
|
7644
|
+
}
|
|
6997
7645
|
if (archivePresent) {
|
|
6998
7646
|
this.assertRegularFile(archivePath, "room archive");
|
|
6999
7647
|
this.fs.unlinkSync(archivePath);
|
|
@@ -7048,10 +7696,106 @@ var init_storage = __esm({
|
|
|
7048
7696
|
} catch (error) {
|
|
7049
7697
|
throw this.wrap(`malformed metadata for room "${roomId}"`, error);
|
|
7050
7698
|
}
|
|
7051
|
-
const room = RoomSchema.parse(decoded);
|
|
7699
|
+
const room = this.isVersion1(decoded) ? this.migrateUnlocked(roomId, decoded, bytes) : RoomSchema.parse(decoded);
|
|
7052
7700
|
if (room.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
|
|
7053
7701
|
return room;
|
|
7054
7702
|
}
|
|
7703
|
+
isVersion1(decoded) {
|
|
7704
|
+
return typeof decoded === "object" && decoded !== null && decoded.version === 1;
|
|
7705
|
+
}
|
|
7706
|
+
/**
|
|
7707
|
+
* Lazy additive v1 → v2 migration (spec §7): preserve the exact pre-migration
|
|
7708
|
+
* bytes once as room.json.v1.bak, then atomically persist the v2 metadata.
|
|
7709
|
+
*
|
|
7710
|
+
* THE BACKUP IS WRITTEN TEMP → FSYNC → RENAME, not opened in place, and the
|
|
7711
|
+
* reason is a crash window that an existence check cannot see. The earlier
|
|
7712
|
+
* version guarded with `if (!lstatIfPresent(backupPath))` and wrote straight
|
|
7713
|
+
* into the final path under O_CREAT|O_EXCL. A crash inside that write leaves a
|
|
7714
|
+
* PARTIAL file that nonetheless EXISTS, so the next load's existence check
|
|
7715
|
+
* skips the backup, writes v2, and the pre-migration bytes are gone — no
|
|
7716
|
+
* error, no warning, and the one artefact that exists to undo a bad migration
|
|
7717
|
+
* is a truncated fragment. Measured before the fix: a 40-byte prefix of a
|
|
7718
|
+
* 604-byte room, JSON.parse false, room.json already v2.
|
|
7719
|
+
*
|
|
7720
|
+
* A rename is atomic, so the final path now only ever appears complete. The
|
|
7721
|
+
* temp file is created with O_EXCL under a pid+random name and removed on
|
|
7722
|
+
* failure, so a crashed attempt leaves at most an orphan temp, never a
|
|
7723
|
+
* plausible-looking backup.
|
|
7724
|
+
*
|
|
7725
|
+
* AND AN EXISTING BACKUP IS PARSED BEFORE IT IS TRUSTED, which repairs the
|
|
7726
|
+
* case where a partial file is ALREADY on disk from a build without this fix —
|
|
7727
|
+
* exactly the state a host that ran the previous code could be in right now.
|
|
7728
|
+
* A backup that does not parse as v1 is replaced by the bytes we hold, because
|
|
7729
|
+
* those are the real pre-migration bytes and the fragment is worthless.
|
|
7730
|
+
*
|
|
7731
|
+
* WHAT THE BACKUP IS NOT: restoring it is NOT a rollback. See the note on
|
|
7732
|
+
* `restoreV1Backup` — re-migrating mints fresh participant ids.
|
|
7733
|
+
*/
|
|
7734
|
+
migrateUnlocked(roomId, decoded, originalBytes) {
|
|
7735
|
+
const v1 = RoomV1Schema.parse(decoded);
|
|
7736
|
+
if (v1.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
|
|
7737
|
+
const migrated = migrateRoomV1(v1, generateUlid);
|
|
7738
|
+
const backupPath = `${this.metadataPath(roomId)}.v1.bak`;
|
|
7739
|
+
if (!this.hasIntactV1Backup(backupPath)) {
|
|
7740
|
+
this.atomicBytesWrite(backupPath, originalBytes, "room metadata v1 backup");
|
|
7741
|
+
}
|
|
7742
|
+
this.atomicMetadataWrite(this.metadataPath(roomId), migrated);
|
|
7743
|
+
return migrated;
|
|
7744
|
+
}
|
|
7745
|
+
/**
|
|
7746
|
+
* Is there already a backup we would be willing to hand back to an operator?
|
|
7747
|
+
*
|
|
7748
|
+
* Existence is not the question — a partial file exists. It must parse and
|
|
7749
|
+
* still claim to be the v1 metadata for this room; anything else is a fragment
|
|
7750
|
+
* and is better overwritten with the bytes we are holding right now.
|
|
7751
|
+
*/
|
|
7752
|
+
hasIntactV1Backup(backupPath) {
|
|
7753
|
+
if (!this.lstatIfPresent(backupPath)) return false;
|
|
7754
|
+
try {
|
|
7755
|
+
const decoded = JSON.parse(utf8Decoder.decode(this.readFileNoFollow(backupPath, "room metadata v1 backup")));
|
|
7756
|
+
return this.isVersion1(decoded);
|
|
7757
|
+
} catch {
|
|
7758
|
+
return false;
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7761
|
+
/**
|
|
7762
|
+
* Durably place exact bytes at `path`: temp under O_EXCL, fsync, rename, fsync
|
|
7763
|
+
* the directory. The same dance as atomicMetadataWrite, which serialises a Room
|
|
7764
|
+
* rather than taking bytes verbatim — and taking them verbatim is the whole
|
|
7765
|
+
* point for a backup, whose value is being byte-identical to what was there.
|
|
7766
|
+
*/
|
|
7767
|
+
atomicBytesWrite(path, bytes, label) {
|
|
7768
|
+
if (this.lstatIfPresent(path)) this.assertRegularFile(path, label);
|
|
7769
|
+
const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
|
|
7770
|
+
let fd;
|
|
7771
|
+
try {
|
|
7772
|
+
fd = this.fs.openSync(
|
|
7773
|
+
temp,
|
|
7774
|
+
nodeFs3.constants.O_CREAT | nodeFs3.constants.O_EXCL | nodeFs3.constants.O_WRONLY | NO_FOLLOW2,
|
|
7775
|
+
FILE_MODE2
|
|
7776
|
+
);
|
|
7777
|
+
this.validateOpenPath(fd, temp, `temporary ${label}`, "file", true);
|
|
7778
|
+
this.fs.fchmodSync(fd, FILE_MODE2);
|
|
7779
|
+
this.writeAll(fd, bytes);
|
|
7780
|
+
this.fs.fsyncSync(fd);
|
|
7781
|
+
this.fs.closeSync(fd);
|
|
7782
|
+
fd = void 0;
|
|
7783
|
+
this.fs.renameSync(temp, path);
|
|
7784
|
+
this.fsyncDirectory(dirname4(path));
|
|
7785
|
+
} catch (error) {
|
|
7786
|
+
if (fd !== void 0) {
|
|
7787
|
+
try {
|
|
7788
|
+
this.fs.closeSync(fd);
|
|
7789
|
+
} catch {
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
try {
|
|
7793
|
+
this.fs.rmSync(temp, { force: true });
|
|
7794
|
+
} catch {
|
|
7795
|
+
}
|
|
7796
|
+
throw this.wrap(`failed to write ${label} at ${path}`, error);
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7055
7799
|
scanArchive(roomId) {
|
|
7056
7800
|
const path = this.archivePath(roomId);
|
|
7057
7801
|
this.assertRegularFile(path, "room archive");
|
|
@@ -7213,7 +7957,7 @@ var init_storage = __esm({
|
|
|
7213
7957
|
}
|
|
7214
7958
|
atomicMetadataWrite(path, room) {
|
|
7215
7959
|
if (this.lstatIfPresent(path)) this.assertRegularFile(path, "room metadata");
|
|
7216
|
-
const temp = `${path}.tmp-${process.pid}-${
|
|
7960
|
+
const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
|
|
7217
7961
|
const bytes = Buffer.from(`${JSON.stringify(room)}
|
|
7218
7962
|
`, "utf8");
|
|
7219
7963
|
let fd;
|
|
@@ -7456,7 +8200,7 @@ var init_web = __esm({
|
|
|
7456
8200
|
});
|
|
7457
8201
|
|
|
7458
8202
|
// src/transports.ts
|
|
7459
|
-
import { randomBytes as
|
|
8203
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
7460
8204
|
import * as http from "node:http";
|
|
7461
8205
|
import * as net from "node:net";
|
|
7462
8206
|
import * as nodeFs5 from "node:fs";
|
|
@@ -7469,19 +8213,36 @@ function createServiceRoutes(service) {
|
|
|
7469
8213
|
room_id: external_exports.string(),
|
|
7470
8214
|
goal: external_exports.unknown().optional(),
|
|
7471
8215
|
briefing: external_exports.unknown().optional(),
|
|
7472
|
-
status: external_exports.unknown().optional()
|
|
8216
|
+
status: external_exports.unknown().optional(),
|
|
8217
|
+
quiet_membership: external_exports.unknown().optional()
|
|
7473
8218
|
}).strict().parse(params);
|
|
7474
8219
|
return service.updateRoom(room_id, input);
|
|
7475
8220
|
} },
|
|
8221
|
+
"room.briefing.role.set": { auth: true, run: (params) => {
|
|
8222
|
+
const { room_id, ...input } = RoleBriefingSetParams.parse(params);
|
|
8223
|
+
return service.setRoleBriefing(room_id, input);
|
|
8224
|
+
} },
|
|
8225
|
+
"room.briefing.role.delete": { auth: true, run: (params) => {
|
|
8226
|
+
const { room_id, ...input } = RoleBriefingDeleteParams.parse(params);
|
|
8227
|
+
return service.deleteRoleBriefing(room_id, input);
|
|
8228
|
+
} },
|
|
7476
8229
|
"room.invite": { auth: true, run: (params) => {
|
|
7477
8230
|
const { room_id, ...input } = external_exports.object({
|
|
7478
8231
|
room_id: external_exports.string(),
|
|
7479
8232
|
mode: external_exports.unknown(),
|
|
7480
|
-
role: external_exports.unknown(),
|
|
8233
|
+
role: external_exports.unknown().optional(),
|
|
7481
8234
|
min_accepts: external_exports.unknown()
|
|
7482
8235
|
}).strict().parse(params);
|
|
7483
8236
|
return service.createInvite(room_id, input);
|
|
7484
8237
|
} },
|
|
8238
|
+
"room.participant.remove": { auth: true, run: (params) => {
|
|
8239
|
+
const { room_id, ...input } = ParticipantRemoveParams.parse(params);
|
|
8240
|
+
return service.removeParticipant(room_id, input);
|
|
8241
|
+
} },
|
|
8242
|
+
"room.participant.replace": { auth: true, run: (params) => {
|
|
8243
|
+
const { room_id, ...input } = ParticipantReplaceParams.parse(params);
|
|
8244
|
+
return service.replaceParticipant(room_id, input);
|
|
8245
|
+
} },
|
|
7485
8246
|
"room.revoke": { auth: true, run: (params) => {
|
|
7486
8247
|
const value = InviteRevokeParams.parse(params);
|
|
7487
8248
|
return service.revokeInvite(value.room_id, value.invite_id);
|
|
@@ -7639,7 +8400,7 @@ function responseFinished2(response) {
|
|
|
7639
8400
|
response.once("error", rejectResponse);
|
|
7640
8401
|
});
|
|
7641
8402
|
}
|
|
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;
|
|
8403
|
+
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
8404
|
var init_transports = __esm({
|
|
7644
8405
|
"src/transports.ts"() {
|
|
7645
8406
|
"use strict";
|
|
@@ -7714,7 +8475,29 @@ var init_transports = __esm({
|
|
|
7714
8475
|
HistoryParams = external_exports.object({
|
|
7715
8476
|
room_id: external_exports.string(),
|
|
7716
8477
|
after: external_exports.number().optional(),
|
|
7717
|
-
limit: external_exports.number().optional()
|
|
8478
|
+
limit: external_exports.number().optional(),
|
|
8479
|
+
view: external_exports.enum(["operator", "participant"]).optional()
|
|
8480
|
+
}).strict();
|
|
8481
|
+
RoleBriefingSetParams = external_exports.object({
|
|
8482
|
+
room_id: external_exports.string(),
|
|
8483
|
+
role: external_exports.string(),
|
|
8484
|
+
text: external_exports.string()
|
|
8485
|
+
}).strict();
|
|
8486
|
+
RoleBriefingDeleteParams = external_exports.object({
|
|
8487
|
+
room_id: external_exports.string(),
|
|
8488
|
+
role: external_exports.string()
|
|
8489
|
+
}).strict();
|
|
8490
|
+
ParticipantRemoveParams = external_exports.object({
|
|
8491
|
+
room_id: external_exports.string(),
|
|
8492
|
+
participant: external_exports.string(),
|
|
8493
|
+
notify: external_exports.boolean().optional()
|
|
8494
|
+
}).strict();
|
|
8495
|
+
ParticipantReplaceParams = external_exports.object({
|
|
8496
|
+
room_id: external_exports.string(),
|
|
8497
|
+
participant: external_exports.string(),
|
|
8498
|
+
notify: external_exports.boolean().optional(),
|
|
8499
|
+
mode: external_exports.enum(["one_time", "public"]).optional(),
|
|
8500
|
+
min_accepts: external_exports.number().optional()
|
|
7718
8501
|
}).strict();
|
|
7719
8502
|
TransportServer = class {
|
|
7720
8503
|
options;
|
|
@@ -7751,7 +8534,7 @@ var init_transports = __esm({
|
|
|
7751
8534
|
await this.cleanupStalePrivateSockets();
|
|
7752
8535
|
const unix = net.createServer((socket) => this.handleUnix(socket));
|
|
7753
8536
|
this.unixServer = unix;
|
|
7754
|
-
const privateSocketPath = `${this.options.socketPath}.private-${process.pid}-${
|
|
8537
|
+
const privateSocketPath = `${this.options.socketPath}.private-${process.pid}-${randomBytes4(6).toString("hex")}`;
|
|
7755
8538
|
this.privateSocketPath = privateSocketPath;
|
|
7756
8539
|
try {
|
|
7757
8540
|
await listen(unix, privateSocketPath);
|
|
@@ -8060,7 +8843,7 @@ var init_transports = __esm({
|
|
|
8060
8843
|
}
|
|
8061
8844
|
}
|
|
8062
8845
|
quarantinePath(target, label) {
|
|
8063
|
-
const path = `${this.options.socketPath}.safe-residue-${label}-${process.pid}-${
|
|
8846
|
+
const path = `${this.options.socketPath}.safe-residue-${label}-${process.pid}-${randomBytes4(6).toString("hex")}`;
|
|
8064
8847
|
this.fs.renameSync(target, path);
|
|
8065
8848
|
const moved = this.fs.lstatSync(path);
|
|
8066
8849
|
return { target, path, dev: moved.dev, ino: moved.ino };
|
|
@@ -8084,7 +8867,7 @@ var init_transports = __esm({
|
|
|
8084
8867
|
}
|
|
8085
8868
|
}
|
|
8086
8869
|
quarantinePublicPath() {
|
|
8087
|
-
const quarantinePath = `${this.options.socketPath}.replacement-${process.pid}-${
|
|
8870
|
+
const quarantinePath = `${this.options.socketPath}.replacement-${process.pid}-${randomBytes4(6).toString("hex")}`;
|
|
8088
8871
|
try {
|
|
8089
8872
|
this.fs.renameSync(this.options.socketPath, quarantinePath);
|
|
8090
8873
|
} catch (error) {
|
|
@@ -8608,7 +9391,7 @@ var daemon_process_exports = {};
|
|
|
8608
9391
|
__export(daemon_process_exports, {
|
|
8609
9392
|
runDaemonProcess: () => runDaemonProcess
|
|
8610
9393
|
});
|
|
8611
|
-
import { randomBytes as
|
|
9394
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
8612
9395
|
async function runDaemonProcess() {
|
|
8613
9396
|
try {
|
|
8614
9397
|
const code = process.env.OURS_COWORK_DAEMON_WORKER === "1" ? await runWorker() : await runSupervisor();
|
|
@@ -8721,7 +9504,7 @@ async function runWorker() {
|
|
|
8721
9504
|
},
|
|
8722
9505
|
control: {
|
|
8723
9506
|
// Created only after the supervisor capability handshake completed.
|
|
8724
|
-
session:
|
|
9507
|
+
session: randomBytes5(16).toString("hex"),
|
|
8725
9508
|
async requestSupervisorShutdown() {
|
|
8726
9509
|
if (!capability || disconnected || process.connected === false) return false;
|
|
8727
9510
|
return sendIpc({ type: "shutdown_request", capability });
|
|
@@ -8799,7 +9582,7 @@ var init_daemon_process = __esm({
|
|
|
8799
9582
|
|
|
8800
9583
|
// src/daemon.ts
|
|
8801
9584
|
import { fork } from "node:child_process";
|
|
8802
|
-
import { randomBytes as
|
|
9585
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
8803
9586
|
import { resolve as resolve3 } from "node:path";
|
|
8804
9587
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
8805
9588
|
async function runSupervisor(options = {}) {
|
|
@@ -8906,7 +9689,7 @@ var init_daemon = __esm({
|
|
|
8906
9689
|
this.signals = options.signals ?? process;
|
|
8907
9690
|
this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? DAEMON_SHUTDOWN_TIMEOUT_MS;
|
|
8908
9691
|
this.onStageCallback = options.onStage;
|
|
8909
|
-
this.capability = options.capability ??
|
|
9692
|
+
this.capability = options.capability ?? randomBytes6(32).toString("hex");
|
|
8910
9693
|
if (!/^[0-9a-f]{64}$/.test(this.capability)) throw new Error("invalid daemon worker capability");
|
|
8911
9694
|
this.done = new Promise((resolveDone) => {
|
|
8912
9695
|
this.resolveDone = resolveDone;
|