@ours.network/cowork 1.0.5 → 1.0.7

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
@@ -4540,8 +4540,27 @@ function isStrictRfc3339(value) {
4540
4540
  function normalizeRoomName(value) {
4541
4541
  return value.trim().normalize("NFC");
4542
4542
  }
4543
+ function sdkIdentityNameError(identityName) {
4544
+ const length = Array.from(identityName).length;
4545
+ if (length < 1 || length > MAX_ROOM_IDENTITY_NAME_CHARACTERS) {
4546
+ return `name must be 1-${MAX_ROOM_IDENTITY_NAME_CHARACTERS} Unicode characters`;
4547
+ }
4548
+ if (identityName !== identityName.normalize("NFC")) return "name must use Unicode NFC normalization";
4549
+ if (SDK_IDENTITY_NAME_FORBIDDEN.test(identityName)) {
4550
+ return "name must not contain control, format, surrogate, line-separator, or path-separator characters";
4551
+ }
4552
+ if (SDK_IDENTITY_NAME_RESERVED.has(identityName)) return `name ${JSON.stringify(identityName)} is reserved`;
4553
+ return void 0;
4554
+ }
4543
4555
  function roomIdentityName(roomName) {
4544
- return `${ROOM_IDENTITY_PREFIX}${RoomNameSchema.parse(roomName)}`;
4556
+ const normalized = RoomNameSchema.parse(roomName);
4557
+ const boundedTitle = Array.from(normalized).slice(0, MAX_ROOM_IDENTITY_TITLE_CHARACTERS).join("");
4558
+ const identityName = `${ROOM_IDENTITY_PREFIX}${boundedTitle}`;
4559
+ const detail = sdkIdentityNameError(identityName);
4560
+ if (detail !== void 0) {
4561
+ throw new CoworkIdentityNameError(`generated room identity name is invalid: ${detail}`);
4562
+ }
4563
+ return identityName;
4545
4564
  }
4546
4565
  function isPersistedRoomIdentityName(roomId, identityName) {
4547
4566
  if (!LowerCrockfordUlidSchema.safeParse(roomId).success || !identityName.startsWith(ROOM_IDENTITY_PREFIX)) {
@@ -4551,7 +4570,7 @@ function isPersistedRoomIdentityName(roomId, identityName) {
4551
4570
  return parsed.success && identityName === `${ROOM_IDENTITY_PREFIX}${parsed.data}`;
4552
4571
  }
4553
4572
  function isStandardRoomIdentityName(roomId, identityName) {
4554
- return isPersistedRoomIdentityName(roomId, identityName);
4573
+ return isPersistedRoomIdentityName(roomId, identityName) && sdkIdentityNameError(identityName) === void 0;
4555
4574
  }
4556
4575
  function refineRoomLineage(room, context) {
4557
4576
  const exactPacketPending = room.state === "provisioning" && room.status === "packet_pending" && isPersistedRoomIdentityName(room.room_id, room.identity_name) && room.invites.length === 0 && room.seats.length === 0 && room.activated_at === void 0 && room.closed_at === void 0;
@@ -4681,7 +4700,7 @@ function refineMessageCategory(message, context) {
4681
4700
  }
4682
4701
  }
4683
4702
  }
4684
- 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, ROOM_ROLE, 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, PostAsRoleInputSchema, RestRoleInputSchema, ContainerIdSchema, AcceptExternalInviteInputSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, ReplyReferenceSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
4703
+ 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, MAX_ROOM_IDENTITY_NAME_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoomNameSchema, ROOM_IDENTITY_PREFIX, MAX_ROOM_IDENTITY_TITLE_CHARACTERS, SDK_IDENTITY_NAME_FORBIDDEN, SDK_IDENTITY_NAME_RESERVED, CoworkIdentityNameError, RoleSchema, ROOM_ROLE, 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, PostAsRoleInputSchema, RestRoleInputSchema, ContainerIdSchema, AcceptExternalInviteInputSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, ReplyReferenceSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
4685
4704
  var init_contracts = __esm({
4686
4705
  "src/contracts.ts"() {
4687
4706
  "use strict";
@@ -4695,6 +4714,7 @@ var init_contracts = __esm({
4695
4714
  MAX_MIME_BYTES = 255;
4696
4715
  MAX_ROLE_BYTES = 256;
4697
4716
  MAX_ROOM_NAME_CHARACTERS = 64;
4717
+ MAX_ROOM_IDENTITY_NAME_CHARACTERS = 64;
4698
4718
  NonEmptyStringSchema = external_exports.string().min(1);
4699
4719
  PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
4700
4720
  LowerCrockfordUlidSchema = external_exports.string().regex(
@@ -4715,6 +4735,16 @@ var init_contracts = __esm({
4715
4735
  }
4716
4736
  });
4717
4737
  ROOM_IDENTITY_PREFIX = "ours-cowork:";
4738
+ MAX_ROOM_IDENTITY_TITLE_CHARACTERS = MAX_ROOM_IDENTITY_NAME_CHARACTERS - Array.from(ROOM_IDENTITY_PREFIX).length;
4739
+ SDK_IDENTITY_NAME_FORBIDDEN = /[\\/\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
4740
+ SDK_IDENTITY_NAME_RESERVED = /* @__PURE__ */ new Set([".", "..", "contact-book", "root.json", "bindings.json"]);
4741
+ CoworkIdentityNameError = class extends Error {
4742
+ code = "NAME_INVALID";
4743
+ constructor(message) {
4744
+ super(message);
4745
+ this.name = "CoworkIdentityNameError";
4746
+ }
4747
+ };
4718
4748
  RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4719
4749
  ROOM_ROLE = "room";
4720
4750
  MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
@@ -5267,6 +5297,24 @@ import { createHash as createHash2 } from "node:crypto";
5267
5297
  import * as fs from "node:fs";
5268
5298
  import { join as join2 } from "node:path";
5269
5299
  import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from "node:zlib";
5300
+ async function sdkNameError(code, message) {
5301
+ const { OursError } = await import("@ours.network/sdk");
5302
+ return new OursError(code, message);
5303
+ }
5304
+ async function isTypedNameRefusal(error) {
5305
+ const { OursError } = await import("@ours.network/sdk");
5306
+ if (!(error instanceof OursError)) return false;
5307
+ const code = error.code;
5308
+ return code === "NAME_INVALID" || code === "NAME_TAKEN";
5309
+ }
5310
+ function sdkErrorCode(error) {
5311
+ if (error === null || typeof error !== "object" || !("code" in error)) return void 0;
5312
+ return typeof error.code === "string" ? error.code : void 0;
5313
+ }
5314
+ function isTransientTransportError(error) {
5315
+ if (error === null || typeof error !== "object" || !("code" in error)) return false;
5316
+ return (/* @__PURE__ */ new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT"])).has(String(error.code));
5317
+ }
5270
5318
  function sendResult(result) {
5271
5319
  if (result.kind === "refused" || result.kind === "migrating") {
5272
5320
  return { status: "send_failed" };
@@ -5386,7 +5434,7 @@ function unpackInvite(encoded, maximumBytes) {
5386
5434
  if (compressed.length === 0) throw new Error("the invite blob is empty or invalid base64url");
5387
5435
  return Buffer.from(maximumBytes === void 0 ? brotliDecompressSync(compressed) : brotliDecompressSync(compressed, { maxOutputLength: maximumBytes }));
5388
5436
  }
5389
- var LegacyCoworkStateError, PacketRegistry, SdkRoomPacket;
5437
+ var LegacyCoworkStateError, RoomIdentityMismatchError, PacketRegistry, SdkRoomPacket;
5390
5438
  var init_packets = __esm({
5391
5439
  "src/packets.ts"() {
5392
5440
  "use strict";
@@ -5399,6 +5447,12 @@ var init_packets = __esm({
5399
5447
  this.name = "LegacyCoworkStateError";
5400
5448
  }
5401
5449
  };
5450
+ RoomIdentityMismatchError = class extends Error {
5451
+ constructor(expected, found) {
5452
+ super(`room identity CID mismatch during rebind: expected "${expected}", found "${found}"`);
5453
+ this.name = "RoomIdentityMismatchError";
5454
+ }
5455
+ };
5402
5456
  PacketRegistry = class {
5403
5457
  packets = /* @__PURE__ */ new Map();
5404
5458
  trackers = /* @__PURE__ */ new Map();
@@ -5407,6 +5461,8 @@ var init_packets = __esm({
5407
5461
  fs;
5408
5462
  log;
5409
5463
  onNotify;
5464
+ rebindSleep;
5465
+ rebindRandom;
5410
5466
  unsubscribe;
5411
5467
  constructor(host, stateDir, options = {}) {
5412
5468
  this.host = host;
@@ -5416,6 +5472,8 @@ var init_packets = __esm({
5416
5472
  });
5417
5473
  this.onNotify = options.onNotify ?? (() => {
5418
5474
  });
5475
+ this.rebindSleep = options.rebindSleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
5476
+ this.rebindRandom = options.rebindRandom ?? Math.random;
5419
5477
  this.unsubscribe = host.onIdentityNotify((name) => {
5420
5478
  const found = [...this.packets.entries()].find(([, packet2]) => packet2.name === name);
5421
5479
  if (!found) return;
@@ -5432,18 +5490,22 @@ var init_packets = __esm({
5432
5490
  get(roomId) {
5433
5491
  return this.packets.get(roomId);
5434
5492
  }
5435
- async create(roomId, identityName, bio = `ours-cowork mission room ${roomId}`) {
5493
+ async preflightCreate(roomId, identityName) {
5436
5494
  validateRoomId(roomId);
5437
- this.assertStandardIdentity(roomId, identityName);
5495
+ await this.assertStandardIdentity(roomId, identityName);
5438
5496
  this.assertNoLegacyState(roomId);
5439
5497
  if (this.packets.has(roomId)) throw new Error(`room identity "${roomId}" is already hosted`);
5440
5498
  const localNames = /* @__PURE__ */ new Set([identityName]);
5441
5499
  const available = await this.host.listIdentityNames(localNames);
5442
5500
  if (available.has(identityName)) {
5443
- throw new Error(
5501
+ throw await sdkNameError(
5502
+ "NAME_TAKEN",
5444
5503
  `shared ours daemon already contains unproven room identity "${identityName}"; refusing to adopt it without a durably recorded CID`
5445
5504
  );
5446
5505
  }
5506
+ }
5507
+ async create(roomId, identityName, bio = `ours-cowork mission room ${roomId}`) {
5508
+ await this.preflightCreate(roomId, identityName);
5447
5509
  const client = await this.host.createClient();
5448
5510
  try {
5449
5511
  const created = await client.createIdentity({
@@ -5453,7 +5515,12 @@ var init_packets = __esm({
5453
5515
  localAutoAccept: true
5454
5516
  });
5455
5517
  const cid = created.info.cid;
5456
- const packet = new SdkRoomPacket(identityName, cid, client);
5518
+ const packet = new SdkRoomPacket(identityName, cid, client, {
5519
+ roomId,
5520
+ log: this.log,
5521
+ sleep: this.rebindSleep,
5522
+ random: this.rebindRandom
5523
+ });
5457
5524
  await packet.refresh();
5458
5525
  this.packets.set(roomId, packet);
5459
5526
  this.track(roomId, identityName);
@@ -5461,12 +5528,15 @@ var init_packets = __esm({
5461
5528
  } catch (error) {
5462
5529
  await client.releaseLease().catch(() => {
5463
5530
  });
5531
+ if (await isTypedNameRefusal(error)) {
5532
+ throw error;
5533
+ }
5464
5534
  throw new Error(`failed to provision standard SDK identity for room "${roomId}"`, { cause: error });
5465
5535
  }
5466
5536
  }
5467
5537
  async restore(roomId, expectedCid, identityName) {
5468
5538
  validateRoomId(roomId);
5469
- this.assertStandardIdentity(roomId, identityName);
5539
+ await this.assertStandardIdentity(roomId, identityName);
5470
5540
  this.assertNoLegacyState(roomId);
5471
5541
  if (expectedCid === void 0 || expectedCid.length === 0) {
5472
5542
  throw new Error(`refusing to restore room identity "${identityName}" without a durably recorded expected CID`);
@@ -5482,7 +5552,12 @@ var init_packets = __esm({
5482
5552
  if (expectedCid !== void 0 && bound.cid !== expectedCid) {
5483
5553
  throw new Error(`restored room identity CID mismatch: expected "${expectedCid}", found "${bound.cid}"`);
5484
5554
  }
5485
- const packet = new SdkRoomPacket(identityName, bound.cid, client);
5555
+ const packet = new SdkRoomPacket(identityName, bound.cid, client, {
5556
+ roomId,
5557
+ log: this.log,
5558
+ sleep: this.rebindSleep,
5559
+ random: this.rebindRandom
5560
+ });
5486
5561
  await packet.refresh();
5487
5562
  this.packets.set(roomId, packet);
5488
5563
  this.track(roomId, identityName);
@@ -5493,6 +5568,21 @@ var init_packets = __esm({
5493
5568
  throw error;
5494
5569
  }
5495
5570
  }
5571
+ async rebind(roomId) {
5572
+ validateRoomId(roomId);
5573
+ const packet = this.packets.get(roomId);
5574
+ if (!packet) throw new Error(`room packet "${roomId}" is not hosted`);
5575
+ return packet.rebind();
5576
+ }
5577
+ /** Release one local SDK lease without deleting the persisted daemon identity. */
5578
+ async unhost(roomId) {
5579
+ validateRoomId(roomId);
5580
+ const packet = this.packets.get(roomId);
5581
+ if (!packet) return;
5582
+ this.untrack(roomId);
5583
+ this.packets.delete(roomId);
5584
+ await packet.close();
5585
+ }
5496
5586
  async destroy(roomId) {
5497
5587
  validateRoomId(roomId);
5498
5588
  const packet = this.packets.get(roomId);
@@ -5541,7 +5631,9 @@ var init_packets = __esm({
5541
5631
  throw new LegacyCoworkStateError(roomId);
5542
5632
  }
5543
5633
  }
5544
- assertStandardIdentity(roomId, identityName) {
5634
+ async assertStandardIdentity(roomId, identityName) {
5635
+ const detail = sdkIdentityNameError(identityName);
5636
+ if (detail !== void 0) throw await sdkNameError("NAME_INVALID", `generated room identity name is invalid: ${detail}`);
5545
5637
  if (!isStandardRoomIdentityName(roomId, identityName)) throw new LegacyCoworkStateError(roomId);
5546
5638
  }
5547
5639
  };
@@ -5549,28 +5641,87 @@ var init_packets = __esm({
5549
5641
  name;
5550
5642
  cid;
5551
5643
  client;
5644
+ roomId;
5645
+ log;
5646
+ rebindSleep;
5647
+ rebindRandom;
5648
+ rebindWork;
5552
5649
  contacts = [];
5553
5650
  hasInviteProvenance = false;
5554
5651
  invites = [];
5555
5652
  refreshWork;
5556
5653
  contactRefreshWork;
5557
- constructor(name, cid, client) {
5654
+ constructor(name, cid, client, recovery = {}) {
5655
+ this.roomId = recovery.roomId ?? name;
5558
5656
  this.name = name;
5559
5657
  this.cid = cid;
5560
5658
  this.client = client;
5659
+ this.log = recovery.log ?? (() => {
5660
+ });
5661
+ this.rebindSleep = recovery.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
5662
+ this.rebindRandom = recovery.random ?? Math.random;
5663
+ }
5664
+ rebind() {
5665
+ this.rebindWork ??= this.rebindUnlocked().finally(() => {
5666
+ this.rebindWork = void 0;
5667
+ });
5668
+ return this.rebindWork;
5669
+ }
5670
+ async rebindUnlocked() {
5671
+ this.observe("identity_rebind_detected");
5672
+ let lastError;
5673
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
5674
+ this.observe("identity_rebind_attempt", { attempt });
5675
+ try {
5676
+ const bound = await this.client.chooseIdentity({ name: this.name, force: false });
5677
+ if (bound.cid !== this.cid) {
5678
+ throw new RoomIdentityMismatchError(this.cid, bound.cid);
5679
+ }
5680
+ await this.refreshUnlocked();
5681
+ this.observe("identity_rebind_succeeded", { attempt });
5682
+ return { name: this.name, cid: this.cid, status: "rebound" };
5683
+ } catch (error) {
5684
+ lastError = error;
5685
+ const code = sdkErrorCode(error);
5686
+ if (error instanceof RoomIdentityMismatchError || !isTransientTransportError(error) || attempt === 3) {
5687
+ this.observe("identity_rebind_failed", {
5688
+ attempt,
5689
+ code: code ?? (error instanceof RoomIdentityMismatchError ? "CID_MISMATCH" : "RECOVERY_FAILED")
5690
+ });
5691
+ throw error;
5692
+ }
5693
+ const delay_ms = Math.round(100 * 2 ** (attempt - 1) * (0.75 + this.rebindRandom() * 0.5));
5694
+ this.observe("identity_rebind_retry", { attempt, delay_ms });
5695
+ await this.rebindSleep(delay_ms);
5696
+ }
5697
+ }
5698
+ throw lastError;
5699
+ }
5700
+ async runBound(operation) {
5701
+ try {
5702
+ return await operation();
5703
+ } catch (error) {
5704
+ const code = sdkErrorCode(error);
5705
+ if (code !== "NOT_BOUND" && code !== "BINDING_REASSIGNED") throw error;
5706
+ await this.rebind();
5707
+ return operation();
5708
+ }
5709
+ }
5710
+ observe(event, detail = {}) {
5711
+ this.log(JSON.stringify({ event, room_id: this.roomId, identity_name: this.name, identity_cid: this.cid, ...detail }));
5561
5712
  }
5562
5713
  refresh() {
5563
- this.refreshWork ??= this.refreshUnlocked().finally(() => {
5714
+ this.refreshWork ??= this.runBound(() => this.refreshUnlocked()).finally(() => {
5564
5715
  this.refreshWork = void 0;
5565
5716
  });
5566
5717
  return this.refreshWork;
5567
5718
  }
5568
5719
  async refreshUnlocked() {
5569
- await this.refreshContacts();
5720
+ await this.refreshContactsUnlocked();
5570
5721
  this.invites = (await this.client.listInvites()).flatMap((invite) => invite.mode === "one_time" || invite.mode === "public" ? [{ invite_id: invite.invite_id, mode: invite.mode }] : []);
5571
5722
  }
5572
5723
  refreshContacts() {
5573
- this.contactRefreshWork ??= this.refreshContactsUnlocked().finally(() => {
5724
+ this.contactRefreshWork ??= this.runBound(() => this.refreshContactsUnlocked()).finally(() => {
5574
5725
  this.contactRefreshWork = void 0;
5575
5726
  });
5576
5727
  return this.contactRefreshWork;
@@ -5584,13 +5735,13 @@ var init_packets = __esm({
5584
5735
  }));
5585
5736
  }
5586
5737
  async mintInvite(mode) {
5587
- const result = await this.client.generateInvite({ mode });
5738
+ const result = await this.runBound(() => this.client.generateInvite({ mode }));
5588
5739
  await this.refresh();
5589
5740
  return { blob: result.blob, invite_id: result.inviteId, reusable: mode === "public" };
5590
5741
  }
5591
5742
  async addContact(invite) {
5592
5743
  const decoded = unpackInvite(invite, MAX_EXTERNAL_INVITE_BYTES);
5593
- const result = await this.client.addContact({ invite });
5744
+ const result = await this.runBound(() => this.client.addContact({ invite }));
5594
5745
  await this.refresh();
5595
5746
  return {
5596
5747
  invite_id: createHash2("sha256").update(decoded).digest("hex"),
@@ -5600,7 +5751,7 @@ var init_packets = __esm({
5600
5751
  };
5601
5752
  }
5602
5753
  async revokeInvite(inviteId) {
5603
- const result = await this.client.revokeInvite({ invite_id: inviteId });
5754
+ const result = await this.runBound(() => this.client.revokeInvite({ invite_id: inviteId }));
5604
5755
  await this.refresh();
5605
5756
  return { revoked: result.revoked };
5606
5757
  }
@@ -5615,9 +5766,9 @@ var init_packets = __esm({
5615
5766
  }
5616
5767
  async listUnreadMessages(limit) {
5617
5768
  validateBatchLimit(limit);
5618
- const metadata = (await this.client.listIncomingMessages()).filter((message) => message.status === "unread").sort((left, right) => left.seq - right.seq).slice(0, limit);
5769
+ const metadata = (await this.runBound(() => this.client.listIncomingMessages())).filter((message) => message.status === "unread").sort((left, right) => left.seq - right.seq).slice(0, limit);
5619
5770
  return Promise.all(metadata.map(async (listed) => {
5620
- const history = await this.client.getHistoryItem({ wire_id: listed.wire_id });
5771
+ const history = await this.runBound(() => this.client.getHistoryItem({ wire_id: listed.wire_id }));
5621
5772
  if (history === null) throw new Error(`SDK history is missing unread message ${listed.wire_id}`);
5622
5773
  assertListedMessage(listed, history);
5623
5774
  return messageItem(history);
@@ -5625,7 +5776,7 @@ var init_packets = __esm({
5625
5776
  }
5626
5777
  async acknowledgeMessage(expected, onUnexpected) {
5627
5778
  for (; ; ) {
5628
- const pulled = await this.client.getMessages({ limit: 1 });
5779
+ const pulled = await this.runBound(() => this.client.getMessages({ limit: 1 }));
5629
5780
  if (pulled.messages.length > 1) throw new Error("SDK returned more than one message for limit 1");
5630
5781
  const [history] = pulled.messages;
5631
5782
  if (history === void 0) return;
@@ -5639,31 +5790,31 @@ var init_packets = __esm({
5639
5790
  }
5640
5791
  async listUnreadFiles(limit) {
5641
5792
  validateBatchLimit(limit);
5642
- const unread = (await this.client.listIncomingFiles()).filter((file) => file.status === "unread").sort((left, right) => left.seq - right.seq).slice(0, limit);
5643
- return Promise.all(unread.map(async (file) => fileItem(file, await this.client.fetchFile(file.wire_id))));
5793
+ const unread = (await this.runBound(() => this.client.listIncomingFiles())).filter((file) => file.status === "unread").sort((left, right) => left.seq - right.seq).slice(0, limit);
5794
+ return Promise.all(unread.map(async (file) => fileItem(file, await this.runBound(() => this.client.fetchFile(file.wire_id)))));
5644
5795
  }
5645
5796
  async acknowledgeFile(expected) {
5646
- const pulled = await this.client.getFiles({ wire_ids: [expected.wire_id] });
5797
+ const pulled = await this.runBound(() => this.client.getFiles({ wire_ids: [expected.wire_id] }));
5647
5798
  if (pulled.files.length !== 1) {
5648
5799
  throw new Error(`SDK did not acknowledge selected file ${expected.wire_id}`);
5649
5800
  }
5650
5801
  assertReceivedFile(expected, pulled.files[0]);
5651
5802
  }
5652
5803
  async send(contactCid, body, replyTo) {
5653
- return sendResult(await this.client.sendMessage({
5804
+ return sendResult(await this.runBound(() => this.client.sendMessage({
5654
5805
  contact: contactCid,
5655
5806
  text: body,
5656
5807
  ...replyTo === void 0 ? {} : {
5657
5808
  reply_to_wire_id: replyTo.wire_id,
5658
5809
  ...replyTo.sentence === void 0 ? {} : { reply_to_sentence: replyTo.sentence }
5659
5810
  }
5660
- }));
5811
+ })));
5661
5812
  }
5662
5813
  async sendFile(contactCid, filename, mime, data, replyTo) {
5663
5814
  const validName = FileNameSchema.parse(filename);
5664
5815
  const validMime = FileMimeSchema.parse(mime);
5665
5816
  if (data.length > MAX_FILE_BYTES) throw new RangeError(`room files must be at most ${MAX_FILE_BYTES} bytes (2 MiB)`);
5666
- return sendResult(await this.client.sendFile({
5817
+ return sendResult(await this.runBound(() => this.client.sendFile({
5667
5818
  contact: contactCid,
5668
5819
  data_base64: data.toString("base64"),
5669
5820
  filename: validName,
@@ -5672,10 +5823,10 @@ var init_packets = __esm({
5672
5823
  reply_to_wire_id: replyTo.wire_id,
5673
5824
  ...replyTo.sentence === void 0 ? {} : { reply_to_sentence: replyTo.sentence }
5674
5825
  }
5675
- }));
5826
+ })));
5676
5827
  }
5677
5828
  async removeContact(contactCid) {
5678
- const result = await this.client.removeContact({ contact: contactCid });
5829
+ const result = await this.runBound(() => this.client.removeContact({ contact: contactCid }));
5679
5830
  const notified = result.notified === true;
5680
5831
  this.contacts = this.contacts.filter((contact) => contact.container_id !== contactCid);
5681
5832
  return { status: notified ? "queued" : "send_failed", notified, key_material_retained: true };
@@ -6243,6 +6394,12 @@ function byteBoundedHistoryPage(records) {
6243
6394
  }
6244
6395
  return page;
6245
6396
  }
6397
+ async function isCleanIdentityRefusal(error) {
6398
+ const { OursError } = await import("@ours.network/sdk");
6399
+ if (!(error instanceof OursError)) return false;
6400
+ const code = error.code;
6401
+ return code === "NAME_INVALID" || code === "NAME_TAKEN";
6402
+ }
6246
6403
  function activeSeats(room) {
6247
6404
  return room.seats.filter((seat) => seat.state === "active");
6248
6405
  }
@@ -6328,6 +6485,7 @@ var init_service = __esm({
6328
6485
  nextMessageId;
6329
6486
  intake;
6330
6487
  provisioningCheckpoint;
6488
+ identityNameTails = /* @__PURE__ */ new Map();
6331
6489
  constructor(store, packets, options = {}) {
6332
6490
  this.store = store;
6333
6491
  this.packets = packets;
@@ -6346,7 +6504,8 @@ var init_service = __esm({
6346
6504
  const roomId = LowerCrockfordUlidSchema.parse(this.nextRoomId());
6347
6505
  const roomName = settings.name ?? defaultRoomName(roomId);
6348
6506
  const identityName = roomIdentityName(roomName);
6349
- return this.lock(roomId, async () => {
6507
+ return this.lockIdentityName(identityName, () => this.lock(roomId, async () => {
6508
+ await this.packets.preflightCreate(roomId, identityName);
6350
6509
  const provisional = RoomSchema.parse({
6351
6510
  version: 2,
6352
6511
  room_id: roomId,
@@ -6369,15 +6528,30 @@ var init_service = __esm({
6369
6528
  created_at: this.now()
6370
6529
  });
6371
6530
  await this.store.create(provisional);
6372
- const packet = await this.packets.create(
6373
- roomId,
6374
- identityName,
6375
- `ours-cowork mission room ${roomId}`
6376
- );
6531
+ let packet;
6532
+ try {
6533
+ packet = await this.packets.create(
6534
+ roomId,
6535
+ identityName,
6536
+ `ours-cowork mission room ${roomId}`
6537
+ );
6538
+ } catch (error) {
6539
+ if (await isCleanIdentityRefusal(error)) {
6540
+ try {
6541
+ await this.store.discardPendingProvisioning(roomId, identityName);
6542
+ } catch (rollbackError) {
6543
+ throw new AggregateError(
6544
+ [error, rollbackError],
6545
+ `room "${roomId}" identity provisioning was refused and its fresh sentinel could not be discarded`
6546
+ );
6547
+ }
6548
+ }
6549
+ throw error;
6550
+ }
6377
6551
  this.provisioningCheckpoint("metadata");
6378
6552
  const { status: _packetPending, ...created } = provisional;
6379
6553
  return this.store.save(RoomSchema.parse({ ...created, identity_cid: packet.cid }));
6380
- });
6554
+ }));
6381
6555
  }
6382
6556
  async recoverRoom(roomId) {
6383
6557
  const id = LowerCrockfordUlidSchema.parse(roomId);
@@ -6392,10 +6566,39 @@ var init_service = __esm({
6392
6566
  const id = LowerCrockfordUlidSchema.parse(roomId);
6393
6567
  return this.lock(id, async () => this.recoverPacketUnlocked(id, await this.store.load(id)));
6394
6568
  }
6569
+ /** Canonical operator recovery for an established room identity lease. */
6570
+ async rebindIdentity(roomId) {
6571
+ const id = LowerCrockfordUlidSchema.parse(roomId);
6572
+ let room = await this.store.load(id);
6573
+ if (room.state === "closed" || room.state === "closing") {
6574
+ throw new RoomServiceError(`cannot rebind room "${id}" while it is ${room.state}`);
6575
+ }
6576
+ if (this.isPacketPending(room) || !this.packets.get(id)) {
6577
+ room = await this.recoverPacket(id);
6578
+ } else {
6579
+ if (!this.packets.rebind) throw new RoomServiceError("room packet registry does not support explicit rebind");
6580
+ await this.packets.rebind(id);
6581
+ }
6582
+ await this.reconcileRoom(id);
6583
+ await this.resumePending(id);
6584
+ return {
6585
+ room_id: id,
6586
+ identity_name: room.identity_name,
6587
+ identity_cid: room.identity_cid,
6588
+ status: "rebound",
6589
+ fanout: "resumed"
6590
+ };
6591
+ }
6395
6592
  async recoverPacketUnlocked(id, initial) {
6396
6593
  let room = initial;
6397
6594
  if (room.state === "closed") return room;
6398
- const packetPending = this.isPacketPending(room);
6595
+ let packetPending = this.isPacketPending(room);
6596
+ if (packetPending && isPersistedRoomIdentityName(id, room.identity_name) && !isStandardRoomIdentityName(id, room.identity_name)) {
6597
+ const storedCreationName = room.identity_name.slice(ROOM_IDENTITY_PREFIX.length);
6598
+ const correctedIdentityName = roomIdentityName(storedCreationName);
6599
+ room = await this.store.save(RoomSchema.parse({ ...room, identity_name: correctedIdentityName }));
6600
+ packetPending = this.isPacketPending(room);
6601
+ }
6399
6602
  let packet = this.packets.get(id);
6400
6603
  if (packet) {
6401
6604
  if (!packetPending && packet.cid !== room.identity_cid) {
@@ -7074,6 +7277,11 @@ var init_service = __esm({
7074
7277
  if (room.state === "closed") {
7075
7278
  return this.store.save(room);
7076
7279
  }
7280
+ if (this.isPacketPending(room)) {
7281
+ throw new RoomServiceError(
7282
+ `cannot close room "${id}" while its identity CID is unproven; run room rebind first, or verify and remove a colliding orphan identity before retrying recovery`
7283
+ );
7284
+ }
7077
7285
  if (room.state === "closing") {
7078
7286
  room = await this.store.save(room);
7079
7287
  } else {
@@ -7549,6 +7757,17 @@ var init_service = __esm({
7549
7757
  lock(roomId, work) {
7550
7758
  return this.store.mutex(roomId).runExclusive(work);
7551
7759
  }
7760
+ async lockIdentityName(identityName, work) {
7761
+ const previous = this.identityNameTails.get(identityName) ?? Promise.resolve();
7762
+ const result = previous.then(work, work);
7763
+ const tail = result.then(() => void 0, () => void 0);
7764
+ this.identityNameTails.set(identityName, tail);
7765
+ try {
7766
+ return await result;
7767
+ } finally {
7768
+ if (this.identityNameTails.get(identityName) === tail) this.identityNameTails.delete(identityName);
7769
+ }
7770
+ }
7552
7771
  packet(roomId) {
7553
7772
  const packet = this.packets.get(roomId);
7554
7773
  if (!packet) throw new RoomServiceError(`room packet "${roomId}" is not hosted`);
@@ -7895,6 +8114,45 @@ var init_storage = __esm({
7895
8114
  synchronous: db.pragma("synchronous", { simple: true })
7896
8115
  })));
7897
8116
  }
8117
+ /** Roll back only a fresh, empty provisioning sentinel after a proven no-side-effect SDK refusal. */
8118
+ async discardPendingProvisioning(roomId, expectedIdentityName) {
8119
+ const id = this.roomId(roomId);
8120
+ await this.mutex(id, () => {
8121
+ this.ensureBaseDirectories();
8122
+ const roomDir = this.roomDirectory(id);
8123
+ this.ensurePrivateDirectory(roomDir, false, `room "${id}" directory`);
8124
+ const room = this.loadUnlocked(id);
8125
+ if (room.state !== "provisioning" || room.status !== "packet_pending" || room.identity_cid !== "" || room.identity_name !== expectedIdentityName || room.invites.length !== 0 || room.seats.length !== 0) {
8126
+ throw new CoworkStorageError(`room "${id}" is not the exact empty provisioning sentinel`);
8127
+ }
8128
+ const blobs = this.blobsDirectory(id);
8129
+ this.ensurePrivateDirectory(blobs, false, `room "${id}" blobs directory`);
8130
+ if (this.fs.readdirSync(blobs).length !== 0) {
8131
+ throw new CoworkStorageError(`room "${id}" provisioning sentinel has file blobs`);
8132
+ }
8133
+ const recordCount = this.withDatabase(id, (db) => db.prepare(
8134
+ "SELECT COUNT(*) AS count FROM records"
8135
+ ).get().count);
8136
+ if (recordCount !== 0) throw new CoworkStorageError(`room "${id}" provisioning sentinel has archive records`);
8137
+ const expected = /* @__PURE__ */ new Set(["archive.sqlite3", "archive.sqlite3-wal", "archive.sqlite3-shm", "blobs", "room.json"]);
8138
+ const unexpected = this.fs.readdirSync(roomDir).filter((name) => !expected.has(name));
8139
+ if (unexpected.length !== 0) {
8140
+ throw new CoworkStorageError(`room "${id}" provisioning sentinel has unexpected residue: ${unexpected.join(", ")}`);
8141
+ }
8142
+ for (const name of ["archive.sqlite3-wal", "archive.sqlite3-shm", "archive.sqlite3", "room.json"]) {
8143
+ const path = join3(roomDir, name);
8144
+ if (this.lstatIfPresent(path)) {
8145
+ this.assertRegularFile(path, name);
8146
+ this.fs.unlinkSync(path);
8147
+ }
8148
+ }
8149
+ this.fs.rmdirSync(blobs);
8150
+ this.fsyncDirectory(roomDir);
8151
+ this.fs.rmdirSync(roomDir);
8152
+ this.fsyncDirectory(this.roomsDirectory());
8153
+ this.reconciledBlobRooms.delete(id);
8154
+ });
8155
+ }
7898
8156
  async delete(roomId) {
7899
8157
  const id = this.roomId(roomId);
7900
8158
  await this.mutex(id, () => {
@@ -8765,6 +9023,14 @@ var init_openapi = __esm({
8765
9023
  result: "The updated room record with the confirmed recovery.",
8766
9024
  example: { room_id: EXAMPLE_ROOM_ID, recovery_of: "inv-01", invite_id: "inv-02" }
8767
9025
  },
9026
+ {
9027
+ method: "room.rebind",
9028
+ summary: "Rebind an established room identity",
9029
+ description: "Safely restores the exact persisted room identity with a non-force lease claim and CID proof, then resumes durable room fanout. It never creates or renames an identity.",
9030
+ params: params({ room_id: roomIdProperty }, ["room_id"]),
9031
+ result: "A structured rebind receipt after identity proof, reconciliation, and pending fanout resumption.",
9032
+ example: { room_id: EXAMPLE_ROOM_ID }
9033
+ },
8768
9034
  {
8769
9035
  method: "room.list",
8770
9036
  summary: "List rooms",
@@ -9298,6 +9564,7 @@ function createServiceRoutes(service) {
9298
9564
  const value = RecoverConfirmParams.parse(params2);
9299
9565
  return service.confirmRecoveredInvite(value.room_id, value.recovery_of, value.invite_id);
9300
9566
  } },
9567
+ "room.rebind": { auth: true, run: (params2) => service.rebindIdentity(RoomIdParams.parse(params2).room_id) },
9301
9568
  "room.list": { auth: true, run: (params2) => {
9302
9569
  external_exports.object({}).strict().parse(params2);
9303
9570
  return service.listRooms();
@@ -10330,21 +10597,43 @@ var init_daemon_runtime = __esm({
10330
10597
  const rooms = await this.store.list();
10331
10598
  this.checkpoint();
10332
10599
  const recoverable = rooms.filter((room) => room.state !== "closed");
10600
+ const healthy = new Set(recoverable.map((room) => room.room_id));
10601
+ const recoverPhase = async (roomId, phase, work) => {
10602
+ if (!healthy.has(roomId)) return;
10603
+ try {
10604
+ await work();
10605
+ this.checkpoint();
10606
+ } catch (error) {
10607
+ healthy.delete(roomId);
10608
+ const unhost = this.registry?.unhost;
10609
+ if (unhost) {
10610
+ await unhost.call(this.registry, roomId).catch((unhostError) => {
10611
+ this.options.log?.(JSON.stringify({
10612
+ event: "startup_room_unhost_failed",
10613
+ room_id: roomId,
10614
+ error: unhostError instanceof Error ? unhostError.message : String(unhostError)
10615
+ }));
10616
+ });
10617
+ }
10618
+ this.options.log?.(JSON.stringify({
10619
+ event: "startup_room_recovery_failed",
10620
+ room_id: roomId,
10621
+ phase,
10622
+ error: error instanceof Error ? error.message : String(error)
10623
+ }));
10624
+ }
10625
+ };
10333
10626
  for (const room of recoverable) {
10334
- await this.service.recoverPacket(room.room_id);
10335
- this.checkpoint();
10627
+ await recoverPhase(room.room_id, "restore", () => this.service.recoverPacket(room.room_id));
10336
10628
  }
10337
10629
  for (const room of recoverable.filter((candidate) => candidate.state !== "closing")) {
10338
- await this.service.reconcileRoom(room.room_id);
10339
- this.checkpoint();
10630
+ await recoverPhase(room.room_id, "reconcile", () => this.service.reconcileRoom(room.room_id));
10340
10631
  }
10341
10632
  for (const room of recoverable.filter((candidate) => candidate.state === "closing")) {
10342
- await this.service.closeRoom(room.room_id);
10343
- this.checkpoint();
10633
+ await recoverPhase(room.room_id, "close", () => this.service.closeRoom(room.room_id));
10344
10634
  }
10345
10635
  for (const room of recoverable.filter((candidate) => candidate.state !== "closing")) {
10346
- await this.service.resumePending(room.room_id);
10347
- this.checkpoint();
10636
+ await recoverPhase(room.room_id, "fanout", () => this.service.resumePending(room.room_id));
10348
10637
  }
10349
10638
  const realService = this.service;
10350
10639
  const serviceRoutes = createServiceRoutes(realService);