@ours.network/cowork 0.3.4 → 0.3.6

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
@@ -107,6 +107,15 @@ function packInvite(raw) {
107
107
  }
108
108
  }).toString("base64url");
109
109
  }
110
+ function unpackInvite(encoded, maximumBytes) {
111
+ const normalized = encoded.replace(/\s+/g, "");
112
+ if (maximumBytes !== void 0 && Buffer.byteLength(encoded, "utf8") > maximumBytes || normalized.length === 0 || !/^[A-Za-z0-9_-]+$/.test(normalized)) {
113
+ throw new Error("the invite blob is empty, oversized, or invalid base64url");
114
+ }
115
+ const compressed = Buffer.from(normalized, "base64url");
116
+ if (compressed.length === 0) throw new Error("the invite blob is empty or invalid base64url");
117
+ return Buffer.from(maximumBytes === void 0 ? brotliDecompressSync(compressed) : brotliDecompressSync(compressed, { maxOutputLength: maximumBytes }));
118
+ }
110
119
  var commonSdk, executableSdk, wrapperSdk, wrappersSdk, AdaptObjectLifetime, adapt_wrapper, object_to_adapt_value, PacketWrapperConfigurator, LATE_RESULT_DRAIN_MS, Packet, AdaptHostShutdownError, AdaptHost;
111
120
  var init_adapt = __esm({
112
121
  async "src/adapt.ts"() {
@@ -4931,7 +4940,7 @@ function refineMessageCategory(message, context) {
4931
4940
  }
4932
4941
  }
4933
4942
  }
4934
- var MAX_TEXT_BYTES, MAX_FILE_BYTES, MAX_HISTORY_PAGE_BYTES, MAX_MANAGEMENT_RESPONSE_BYTES, MAX_FILE_NAME_BYTES, MAX_MIME_BYTES, MAX_ROLE_BYTES, MAX_ROOM_NAME_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoomNameSchema, ROOM_IDENTITY_PREFIX, RoleSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, CurrentRoomSchema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
4943
+ var MAX_TEXT_BYTES, MAX_FILE_BYTES, MAX_HISTORY_PAGE_BYTES, MAX_MANAGEMENT_RESPONSE_BYTES, MAX_EXTERNAL_INVITE_BYTES, MAX_FILE_NAME_BYTES, MAX_MIME_BYTES, MAX_ROLE_BYTES, MAX_ROOM_NAME_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoomNameSchema, ROOM_IDENTITY_PREFIX, RoleSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, CurrentRoomSchema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, ContainerIdSchema, AcceptExternalInviteInputSchema, 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;
4935
4944
  var init_contracts = __esm({
4936
4945
  "src/contracts.ts"() {
4937
4946
  "use strict";
@@ -4940,6 +4949,7 @@ var init_contracts = __esm({
4940
4949
  MAX_FILE_BYTES = 2 * 1024 * 1024;
4941
4950
  MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
4942
4951
  MAX_MANAGEMENT_RESPONSE_BYTES = MAX_HISTORY_PAGE_BYTES + 1024 * 1024;
4952
+ MAX_EXTERNAL_INVITE_BYTES = 48 * 1024;
4943
4953
  MAX_FILE_NAME_BYTES = 255;
4944
4954
  MAX_MIME_BYTES = 255;
4945
4955
  MAX_ROLE_BYTES = 256;
@@ -4973,7 +4983,7 @@ var init_contracts = __esm({
4973
4983
  `file MIME metadata must be at most ${MAX_MIME_BYTES} UTF-8 bytes`
4974
4984
  );
4975
4985
  RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
4976
- SeatStateSchema = external_exports.enum(["active", "removed"]);
4986
+ SeatStateSchema = external_exports.enum(["pending", "active", "removed"]);
4977
4987
  InviteModeSchema = external_exports.enum(["one_time", "public"]);
4978
4988
  DEFAULT_ROLE = "Participant";
4979
4989
  InviteStateSchema = external_exports.enum([
@@ -4996,7 +5006,9 @@ var init_contracts = __esm({
4996
5006
  display_name: NonEmptyStringSchema,
4997
5007
  role: RoleSchema,
4998
5008
  invite_id: NonEmptyStringSchema,
4999
- accepted_at: Rfc3339Schema,
5009
+ accepted_at: Rfc3339Schema.optional(),
5010
+ requested_at: Rfc3339Schema.optional(),
5011
+ invite_sha256: external_exports.string().regex(/^[0-9a-f]{64}$/).optional(),
5000
5012
  participant_id: LowerCrockfordUlidSchema,
5001
5013
  state: SeatStateSchema,
5002
5014
  alias: NonEmptyStringSchema.optional(),
@@ -5005,7 +5017,25 @@ var init_contracts = __esm({
5005
5017
  replaces_seat: LowerCrockfordUlidSchema.optional(),
5006
5018
  bounced_at: Rfc3339Schema.optional()
5007
5019
  }).strict().superRefine((seat, context) => {
5008
- if (seat.state === "removed") {
5020
+ if (seat.state === "pending") {
5021
+ for (const field of ["requested_at", "invite_sha256"]) {
5022
+ if (seat[field] === void 0) {
5023
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `pending seats require ${field}` });
5024
+ }
5025
+ }
5026
+ for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at"]) {
5027
+ if (seat[field] !== void 0) {
5028
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `${field} is forbidden on pending seats` });
5029
+ }
5030
+ }
5031
+ } else if (seat.state === "removed") {
5032
+ if (seat.accepted_at === void 0 && seat.requested_at === void 0) {
5033
+ context.addIssue({
5034
+ code: external_exports.ZodIssueCode.custom,
5035
+ path: ["accepted_at"],
5036
+ message: "removed seats require accepted_at unless they are cancelled external admissions"
5037
+ });
5038
+ }
5009
5039
  if (seat.removed_at === void 0) {
5010
5040
  context.addIssue({
5011
5041
  code: external_exports.ZodIssueCode.custom,
@@ -5021,6 +5051,9 @@ var init_contracts = __esm({
5021
5051
  });
5022
5052
  }
5023
5053
  } else {
5054
+ if (seat.accepted_at === void 0) {
5055
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["accepted_at"], message: "active seats require accepted_at" });
5056
+ }
5024
5057
  for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
5025
5058
  if (seat[field] !== void 0) {
5026
5059
  context.addIssue({
@@ -5031,6 +5064,13 @@ var init_contracts = __esm({
5031
5064
  }
5032
5065
  }
5033
5066
  }
5067
+ if (seat.requested_at === void 0 !== (seat.invite_sha256 === void 0)) {
5068
+ context.addIssue({
5069
+ code: external_exports.ZodIssueCode.custom,
5070
+ path: ["invite_sha256"],
5071
+ message: "external admission metadata requires both requested_at and invite_sha256"
5072
+ });
5073
+ }
5034
5074
  });
5035
5075
  RoomInviteSchema = external_exports.object({
5036
5076
  invite_id: NonEmptyStringSchema,
@@ -5139,6 +5179,7 @@ var init_contracts = __esm({
5139
5179
  refineRoomLineage(room, context);
5140
5180
  const byParticipant = /* @__PURE__ */ new Map();
5141
5181
  const activeAliases = /* @__PURE__ */ new Set();
5182
+ const authorizedCids = /* @__PURE__ */ new Set();
5142
5183
  for (const [index, seat] of room.seats.entries()) {
5143
5184
  if (byParticipant.has(seat.participant_id)) {
5144
5185
  context.addIssue({
@@ -5148,6 +5189,16 @@ var init_contracts = __esm({
5148
5189
  });
5149
5190
  }
5150
5191
  byParticipant.set(seat.participant_id, seat);
5192
+ if (seat.state === "pending" || seat.state === "active") {
5193
+ if (authorizedCids.has(seat.identity)) {
5194
+ context.addIssue({
5195
+ code: external_exports.ZodIssueCode.custom,
5196
+ path: ["seats", index, "identity"],
5197
+ message: "at most one pending or active seat may exist per CID"
5198
+ });
5199
+ }
5200
+ authorizedCids.add(seat.identity);
5201
+ }
5151
5202
  if (room.anonymous && seat.alias === void 0) {
5152
5203
  context.addIssue({
5153
5204
  code: external_exports.ZodIssueCode.custom,
@@ -5237,6 +5288,16 @@ var init_contracts = __esm({
5237
5288
  PostMessageInputSchema = external_exports.object({
5238
5289
  text: MessageTextSchema
5239
5290
  }).strict();
5291
+ ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
5292
+ AcceptExternalInviteInputSchema = external_exports.object({
5293
+ role: RoleSchema,
5294
+ invite: external_exports.string().refine(
5295
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
5296
+ `invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
5297
+ ),
5298
+ expected_cid: ContainerIdSchema.optional(),
5299
+ replaces_seat: LowerCrockfordUlidSchema.optional()
5300
+ }).strict();
5240
5301
  AuthorSnapshotSchema = external_exports.object({
5241
5302
  identity: NonEmptyStringSchema,
5242
5303
  display_name: NonEmptyStringSchema,
@@ -6036,6 +6097,25 @@ var init_packets = __esm({
6036
6097
  };
6037
6098
  });
6038
6099
  }
6100
+ async addContact(invite) {
6101
+ let decoded;
6102
+ try {
6103
+ decoded = unpackInvite(invite, MAX_EXTERNAL_INVITE_BYTES);
6104
+ } catch (error) {
6105
+ throw new Error("external invite could not be decoded within the 48 KiB limit", { cause: error });
6106
+ }
6107
+ return withScopeAsync(async (lifetime) => {
6108
+ const result = await this.packet.mutatingTx("::a2a_messaging::add_contact", {
6109
+ invite: this.packet.newBinary(decoded, lifetime)
6110
+ }, lifetime);
6111
+ return {
6112
+ invite_id: result.Reduce("invite_id").Visualize(),
6113
+ container_id: result.Reduce("container_id").Visualize(),
6114
+ inviter_name: result.Reduce("inviter_name").Visualize(),
6115
+ pending_name: result.Reduce("pending").Visualize()
6116
+ };
6117
+ });
6118
+ }
6039
6119
  async revokeInvite(inviteId) {
6040
6120
  return withScopeAsync(async (lifetime) => {
6041
6121
  const result = await this.packet.mutatingTx("::a2a_messaging::revoke_invite", { invite_id: inviteId }, lifetime);
@@ -6639,6 +6719,7 @@ var init_intake = __esm({
6639
6719
  });
6640
6720
 
6641
6721
  // src/service.ts
6722
+ import { createHash as createHash3 } from "node:crypto";
6642
6723
  function byteBoundedHistoryPage(records) {
6643
6724
  const page = [];
6644
6725
  let bytes = 2;
@@ -6659,6 +6740,9 @@ function byteBoundedHistoryPage(records) {
6659
6740
  function activeSeats(room) {
6660
6741
  return room.seats.filter((seat) => seat.state === "active");
6661
6742
  }
6743
+ function isCancelledExternalSeat(seat) {
6744
+ return seat.state === "removed" && seat.accepted_at === void 0 && seat.requested_at !== void 0 && seat.invite_sha256 !== void 0;
6745
+ }
6662
6746
  function mintAlias(seated, role) {
6663
6747
  return `${role} #${seated.filter((seat) => seat.role === role).length + 1}`;
6664
6748
  }
@@ -6670,10 +6754,11 @@ function currentContactIdentities(packet) {
6670
6754
  }
6671
6755
  var ROOM_ROLE, CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
6672
6756
  var init_service = __esm({
6673
- "src/service.ts"() {
6757
+ async "src/service.ts"() {
6674
6758
  "use strict";
6675
6759
  init_zod();
6676
6760
  init_contracts();
6761
+ await init_adapt();
6677
6762
  init_intake();
6678
6763
  init_ulid();
6679
6764
  ROOM_ROLE = "room";
@@ -6928,6 +7013,81 @@ var init_service = __esm({
6928
7013
  });
6929
7014
  });
6930
7015
  }
7016
+ /** Redeem an external invite without retaining its secret-bearing bytes. */
7017
+ async acceptExternalInvite(roomId, input) {
7018
+ const id = LowerCrockfordUlidSchema.parse(roomId);
7019
+ const request = AcceptExternalInviteInputSchema.parse(input);
7020
+ let decoded;
7021
+ try {
7022
+ decoded = unpackInvite(request.invite, MAX_EXTERNAL_INVITE_BYTES);
7023
+ } catch {
7024
+ throw new RoomServiceError("external invite is invalid or exceeds the 48 KiB decoded limit");
7025
+ }
7026
+ const digest = createHash3("sha256").update(decoded).digest("hex");
7027
+ const receipt = await this.lock(id, async () => {
7028
+ const room = await this.store.load(id);
7029
+ this.assertMutable(room, "accept an external invite for");
7030
+ const contactsBeforeRedemption = new Set(
7031
+ this.packet(id).listContacts().map((contact) => contact.container_id)
7032
+ );
7033
+ const predecessor = request.replaces_seat === void 0 ? void 0 : room.seats.find((seat2) => seat2.participant_id === request.replaces_seat);
7034
+ if (request.replaces_seat !== void 0 && (!predecessor || predecessor.state !== "removed" || predecessor.role !== request.role)) {
7035
+ throw new RoomServiceError("external replacement must reference a removed seat with the configured role");
7036
+ }
7037
+ if (predecessor && room.seats.some((seat2) => seat2.replaces_seat === predecessor.participant_id && (seat2.state === "pending" || seat2.state === "active"))) {
7038
+ throw new RoomServiceError("external replacement seat already has a pending or active successor");
7039
+ }
7040
+ let added;
7041
+ try {
7042
+ added = await this.packet(id).addContact(request.invite);
7043
+ } catch {
7044
+ throw new RoomServiceError("external invite was rejected");
7045
+ }
7046
+ const cid = ContainerIdSchema.parse(added.container_id);
7047
+ if (request.expected_cid !== void 0 && cid !== request.expected_cid) {
7048
+ throw new RoomServiceError("external invite inviter CID did not match --expected-cid");
7049
+ }
7050
+ if (contactsBeforeRedemption.has(cid)) {
7051
+ throw new RoomServiceError("external invite inviter is already a contact; redemption provenance is ambiguous");
7052
+ }
7053
+ if (room.seats.some((seat2) => seat2.identity === cid && (seat2.state === "pending" || seat2.state === "active"))) {
7054
+ throw new RoomServiceError("external inviter already has a pending or active seat in this room");
7055
+ }
7056
+ const requestedAt = this.now();
7057
+ const participantId = LowerCrockfordUlidSchema.parse(generateUlid());
7058
+ const seated = room.seats;
7059
+ const pending = {
7060
+ identity: cid,
7061
+ display_name: added.inviter_name,
7062
+ role: request.role,
7063
+ invite_id: added.invite_id,
7064
+ requested_at: requestedAt,
7065
+ invite_sha256: digest,
7066
+ participant_id: participantId,
7067
+ state: "pending",
7068
+ ...predecessor === void 0 ? {} : { replaces_seat: predecessor.participant_id },
7069
+ ...room.anonymous ? { alias: predecessor?.alias ?? mintAlias(seated, request.role) } : {}
7070
+ };
7071
+ await this.store.save(RoomSchema.parse({ ...room, seats: [...room.seats, pending] }));
7072
+ const reconciled = await this.reconcileUnlocked(await this.store.load(id), this.packet(id));
7073
+ const seat = reconciled.seats.find((candidate) => candidate.participant_id === participantId);
7074
+ return {
7075
+ room_id: id,
7076
+ participant_id: participantId,
7077
+ state: seat.state,
7078
+ role: seat.role,
7079
+ identity: seat.identity,
7080
+ invite_id: seat.invite_id,
7081
+ inviter_name: added.inviter_name,
7082
+ pending_name: added.pending_name,
7083
+ requested_at: requestedAt,
7084
+ ...seat.accepted_at === void 0 ? {} : { accepted_at: seat.accepted_at },
7085
+ ...seat.replaces_seat === void 0 ? {} : { replaces_seat: seat.replaces_seat }
7086
+ };
7087
+ });
7088
+ await this.intake.resumePending(id);
7089
+ return receipt;
7090
+ }
6931
7091
  async mintInviteUnlocked(room, request) {
6932
7092
  const packet = this.packet(room.room_id);
6933
7093
  const minted = await packet.mintInvite(request.mode);
@@ -6968,7 +7128,7 @@ var init_service = __esm({
6968
7128
  const receipt = await this.lock(id, async () => {
6969
7129
  const room = await this.store.load(id);
6970
7130
  this.assertMutable(room, "remove a participant from");
6971
- const seat = this.findActiveSeat(room, request.participant);
7131
+ const seat = this.findSeatForRemoval(room, request.participant);
6972
7132
  const notify = (request.notify ?? true) && !room.quiet_membership;
6973
7133
  return this.beginRemovalUnlocked(room, seat, notify);
6974
7134
  });
@@ -7001,6 +7161,16 @@ var init_service = __esm({
7001
7161
  await this.intake.resumePending(id);
7002
7162
  return receipt;
7003
7163
  }
7164
+ findSeatForRemoval(room, participant) {
7165
+ const matches = (candidate) => candidate.identity === participant || candidate.participant_id === participant;
7166
+ const seat = room.seats.find((candidate) => candidate.state !== "removed" && matches(candidate)) ?? room.seats.find((candidate) => isCancelledExternalSeat(candidate) && matches(candidate));
7167
+ if (!seat) {
7168
+ throw new RoomServiceError(
7169
+ `"${participant}" is not a pending, active, or recoverable cancelled participant of room "${room.room_id}"`
7170
+ );
7171
+ }
7172
+ return seat;
7173
+ }
7004
7174
  findActiveSeat(room, participant) {
7005
7175
  const seat = room.seats.find((candidate) => candidate.state === "active" && (candidate.identity === participant || candidate.participant_id === participant));
7006
7176
  if (!seat) {
@@ -7009,6 +7179,40 @@ var init_service = __esm({
7009
7179
  return seat;
7010
7180
  }
7011
7181
  async beginRemovalUnlocked(room, seat, notify) {
7182
+ if (seat.state === "pending" || isCancelledExternalSeat(seat)) {
7183
+ let cancelled = seat;
7184
+ if (seat.state === "pending") {
7185
+ const removedAt = this.now();
7186
+ cancelled = {
7187
+ ...seat,
7188
+ state: "removed",
7189
+ removed_at: removedAt,
7190
+ removed_epoch: room.membership_epoch
7191
+ };
7192
+ }
7193
+ const seats = room.seats.map((candidate) => candidate.participant_id === seat.participant_id ? cancelled : candidate);
7194
+ if (seat.state === "pending") await this.store.save(RoomSchema.parse({ ...room, seats }));
7195
+ const established = this.packet(room.room_id).listContacts().some((contact) => contact.container_id === seat.identity);
7196
+ if (established) {
7197
+ const outcome = await this.packet(room.room_id).removeContact(seat.identity);
7198
+ return {
7199
+ room_id: room.room_id,
7200
+ participant_id: seat.participant_id,
7201
+ epoch: room.membership_epoch,
7202
+ status: outcome.status,
7203
+ notified: outcome.notified,
7204
+ key_material_retained: true
7205
+ };
7206
+ }
7207
+ return {
7208
+ room_id: room.room_id,
7209
+ participant_id: seat.participant_id,
7210
+ epoch: room.membership_epoch,
7211
+ status: "cancelled_pending",
7212
+ notified: false,
7213
+ key_material_retained: true
7214
+ };
7215
+ }
7012
7216
  const intent = await this.store.append(room.room_id, {
7013
7217
  version: 1,
7014
7218
  kind: "membership_intent",
@@ -7419,7 +7623,7 @@ var init_service = __esm({
7419
7623
  }
7420
7624
  const origins = packet.listContactOrigins();
7421
7625
  const inviteById = new Map(room.invites.map((invite) => [invite.invite_id, invite]));
7422
- const existingCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
7626
+ const existingCids = new Set(room.seats.filter((seat) => seat.state === "active" || seat.state === "pending").map((seat) => seat.identity));
7423
7627
  const lastRemovedAt = /* @__PURE__ */ new Map();
7424
7628
  for (const seat of room.seats) {
7425
7629
  if (seat.state !== "removed" || seat.removed_at === void 0) continue;
@@ -7428,6 +7632,35 @@ var init_service = __esm({
7428
7632
  lastRemovedAt.set(seat.identity, seat.removed_at);
7429
7633
  }
7430
7634
  }
7635
+ const activatedPending = [];
7636
+ const seatsWithActivations = [...room.seats];
7637
+ for (const [seatIndex, seat] of room.seats.entries()) {
7638
+ if (seat.state !== "pending") continue;
7639
+ const displayName = contactsByCid.get(seat.identity);
7640
+ const origin = origins[seat.identity];
7641
+ if (displayName === void 0 || origin === void 0) continue;
7642
+ const completedExternalRedemption = origin.via === "invite_redeemed" && origin.invite_id === seat.invite_id;
7643
+ const fallbackInvite = origin.via === "invite_one_time" || origin.via === "invite_public" ? inviteById.get(origin.invite_id) : void 0;
7644
+ const confirmedFallback = fallbackInvite !== void 0 && fallbackInvite.state !== "receipt_pending" && (fallbackInvite.recovery_of === void 0 || fallbackInvite.recovery_confirmed === true);
7645
+ if (!completedExternalRedemption && !confirmedFallback) continue;
7646
+ const removedAt = lastRemovedAt.get(seat.identity);
7647
+ if (fallbackInvite !== void 0 && removedAt !== void 0 && fallbackInvite.created_at <= removedAt) continue;
7648
+ const predecessor = fallbackInvite?.replaces_seat === void 0 ? void 0 : room.seats.find((candidate) => candidate.participant_id === fallbackInvite.replaces_seat && candidate.state === "removed");
7649
+ if (fallbackInvite?.replaces_seat !== void 0 && predecessor === void 0) continue;
7650
+ const { alias: _alias, replaces_seat: _replacesSeat, ...base } = seat;
7651
+ const activated = {
7652
+ ...base,
7653
+ state: "active",
7654
+ display_name: displayName,
7655
+ accepted_at: origin.at,
7656
+ role: fallbackInvite?.role ?? seat.role,
7657
+ invite_id: fallbackInvite?.invite_id ?? seat.invite_id,
7658
+ ...fallbackInvite === void 0 ? _replacesSeat === void 0 ? {} : { replaces_seat: _replacesSeat } : predecessor === void 0 ? {} : { replaces_seat: predecessor.participant_id },
7659
+ ...room.anonymous ? { alias: fallbackInvite === void 0 ? _alias : predecessor?.alias ?? (fallbackInvite.role === seat.role ? _alias : mintAlias(seatsWithActivations, fallbackInvite.role)) } : {}
7660
+ };
7661
+ activatedPending.push(activated);
7662
+ seatsWithActivations[seatIndex] = activated;
7663
+ }
7431
7664
  const newSeats = [];
7432
7665
  for (const [cid, displayName] of contactsByCid) {
7433
7666
  if (existingCids.has(cid)) continue;
@@ -7437,7 +7670,7 @@ var init_service = __esm({
7437
7670
  if (!invite || invite.state === "receipt_pending" || invite.recovery_of !== void 0 && invite.recovery_confirmed !== true) continue;
7438
7671
  const removedAt = lastRemovedAt.get(cid);
7439
7672
  if (removedAt !== void 0 && invite.created_at <= removedAt) continue;
7440
- const seated = [...room.seats, ...newSeats];
7673
+ const seated = [...seatsWithActivations, ...newSeats];
7441
7674
  const predecessor = invite.replaces_seat === void 0 ? void 0 : seated.find((seat) => seat.participant_id === invite.replaces_seat && seat.state === "removed");
7442
7675
  if (invite.replaces_seat !== void 0 && !predecessor) continue;
7443
7676
  newSeats.push({
@@ -7455,9 +7688,10 @@ var init_service = __esm({
7455
7688
  });
7456
7689
  existingCids.add(cid);
7457
7690
  }
7458
- const seats = [...room.seats, ...newSeats];
7691
+ const seats = [...seatsWithActivations, ...newSeats];
7459
7692
  const acceptedByInvite = /* @__PURE__ */ new Map();
7460
7693
  for (const seat of seats) {
7694
+ if (seat.state === "pending" || !inviteById.has(seat.invite_id)) continue;
7461
7695
  const accepted = acceptedByInvite.get(seat.invite_id) ?? [];
7462
7696
  if (!accepted.includes(seat.identity)) accepted.push(seat.identity);
7463
7697
  acceptedByInvite.set(seat.invite_id, accepted);
@@ -7481,7 +7715,7 @@ var init_service = __esm({
7481
7715
  ...room,
7482
7716
  seats,
7483
7717
  invites,
7484
- membership_epoch: room.membership_epoch + newSeats.length
7718
+ membership_epoch: room.membership_epoch + activatedPending.length + newSeats.length
7485
7719
  });
7486
7720
  const journal = await this.store.read(next.room_id);
7487
7721
  const completedIntents = new Set(journal.filter((record) => record.kind === "membership_result").map((record) => record.kind === "membership_result" ? record.intent_record_id : ""));
@@ -7492,11 +7726,12 @@ var init_service = __esm({
7492
7726
  ({ room: next } = await this.completeRemovalUnlocked(next, intent));
7493
7727
  }
7494
7728
  const requirementsMet = invites.filter((invite) => invite.state !== "revoked").every((invite) => invite.accepted_cids.length >= invite.min_accepts);
7495
- if (next.state === "provisioning" && seats.length > 0 && requirementsMet) {
7729
+ const admitted = [...activatedPending, ...newSeats];
7730
+ if (next.state === "provisioning" && activeSeats(next).length > 0 && requirementsMet) {
7496
7731
  const activationAt = await this.ensureActivationBriefing(next, activeSeats(next));
7497
7732
  next = RoomSchema.parse({ ...next, state: "active", activated_at: activationAt });
7498
- } else if (next.state === "active" && newSeats.length > 0) {
7499
- await this.ensureActivationBriefing(next, newSeats);
7733
+ } else if (next.state === "active" && admitted.length > 0) {
7734
+ await this.ensureActivationBriefing(next, admitted);
7500
7735
  }
7501
7736
  return this.store.save(next);
7502
7737
  }
@@ -8609,6 +8844,14 @@ function createServiceRoutes(service) {
8609
8844
  } }
8610
8845
  };
8611
8846
  }
8847
+ function createPrivateServiceRoutes(service) {
8848
+ return {
8849
+ "room.accept": { auth: true, run: (params) => {
8850
+ const { room_id, ...input } = ExternalInviteAcceptParams.parse(params);
8851
+ return service.acceptExternalInvite(room_id, input);
8852
+ } }
8853
+ };
8854
+ }
8612
8855
  function errorResponse(id, code, message) {
8613
8856
  return { version: 1, id, error: { code, message } };
8614
8857
  }
@@ -8736,7 +8979,7 @@ function responseFinished2(response) {
8736
8979
  response.once("error", rejectResponse);
8737
8980
  });
8738
8981
  }
8739
- 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;
8982
+ 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, ExternalInviteAcceptParams, TransportServer;
8740
8983
  var init_transports = __esm({
8741
8984
  "src/transports.ts"() {
8742
8985
  "use strict";
@@ -8835,6 +9078,13 @@ var init_transports = __esm({
8835
9078
  mode: external_exports.enum(["one_time", "public"]).optional(),
8836
9079
  min_accepts: external_exports.number().optional()
8837
9080
  }).strict();
9081
+ ExternalInviteAcceptParams = external_exports.object({
9082
+ room_id: external_exports.string(),
9083
+ role: external_exports.string(),
9084
+ invite: external_exports.string(),
9085
+ expected_cid: external_exports.string().optional(),
9086
+ replaces_seat: external_exports.string().optional()
9087
+ }).strict();
8838
9088
  TransportServer = class {
8839
9089
  options;
8840
9090
  fs;
@@ -9260,7 +9510,7 @@ import * as nodeFs6 from "node:fs";
9260
9510
  import { join as join7 } from "node:path";
9261
9511
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9262
9512
  function isIntakeNotification(event) {
9263
- return event === "message_received" || event === "file_received" || event === "contact_accepted";
9513
+ return event === "message_received" || event === "file_received" || event === "contact_accepted" || event === "contact_added";
9264
9514
  }
9265
9515
  function createDaemonControlRoutes(control) {
9266
9516
  if (!/^[0-9a-f]{32}$/.test(control.session)) throw new TypeError("invalid daemon control session");
@@ -9478,7 +9728,7 @@ var init_daemon_runtime = __esm({
9478
9728
  init_config();
9479
9729
  init_config();
9480
9730
  await init_packets();
9481
- init_service();
9731
+ await init_service();
9482
9732
  init_storage();
9483
9733
  init_transports();
9484
9734
  init_web();
@@ -9582,7 +9832,7 @@ var init_daemon_runtime = __esm({
9582
9832
  }
9583
9833
  const realService = this.service;
9584
9834
  const serviceRoutes = createServiceRoutes(realService);
9585
- const unixRoutes = this.options.control ? { ...serviceRoutes, ...createDaemonControlRoutes(this.options.control) } : serviceRoutes;
9835
+ const unixRoutes = this.options.control ? { ...serviceRoutes, ...createPrivateServiceRoutes(realService), ...createDaemonControlRoutes(this.options.control) } : { ...serviceRoutes, ...createPrivateServiceRoutes(realService) };
9586
9836
  const unixDispatcher = new RpcDispatcher(unixRoutes);
9587
9837
  const restDispatcher = new RpcDispatcher(serviceRoutes);
9588
9838
  const staticHandler = createStaticWebHandler(loadWebAssets(