@ours.network/cowork 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,9 +7,9 @@ ours-cowork web
7
7
  ours-cowork docs
8
8
  ```
9
9
 
10
- `ours-cowork web` starts the daemon if it is absent, waits for the console, and opens `http://127.0.0.1:3052/`. Create a room with a name first, then add each invitation requirement from its Invite panel. Names are trimmed, normalized to Unicode NFC, and may contain 1–64 Unicode characters excluding control and format characters. A normalized name must be unique among identities in the shared daemon. The Communication view contains the human-readable room chat; operational records remain in Events and the complete ordered stream remains in Archive.
10
+ `ours-cowork web` starts the daemon if it is absent, waits for the console, and opens `http://127.0.0.1:3052/`. Create a room with a name first, then add each invitation requirement from its Invite panel. Names are trimmed, normalized to Unicode NFC, and may contain 1–64 Unicode characters excluding control and format characters. The bounded identity name must not collide with an identity in the shared daemon. The Communication view contains the human-readable room chat; operational records remain in Events and the complete ordered stream remains in Archive.
11
11
 
12
- Each room identity is exactly `ours-cowork:<room name>`, preserving the normalized name supplied at creation so Messenger can display it directly without separate `room_name` metadata. The authenticated identity name is frozen at creation; later room settings may change `room_name` but do not rename the identity, CID, contacts, or history. Identity CIDs, not names, remain the authorization and routing keys. Because identity names are daemon-global, creating another room with the same normalized name fails closed. Earlier unreleased ID- and slug-based identity formats are unsupported; no migration is provided for this unreleased major.
12
+ Each room identity is `ours-cowork:<bounded room name>`. Cowork NFC-normalizes the creation name and retains its first 52 Unicode code points so the complete SDK identity stays within 64 code points. The authenticated identity name is frozen at creation; later room settings may change `room_name` but do not rename the identity, CID, contacts, or history. Identity CIDs, not names, remain the authorization and routing keys. Because identity names are daemon-global, equal names—including distinct long titles with the same retained prefix—collide cleanly. Earlier unreleased ID- and slug-based identity formats are unsupported; no migration is provided for this unreleased major.
13
13
 
14
14
  The localhost HTTP console has no authentication. Keep it bound to `127.0.0.1`; do not proxy, forward, or expose the port to other hosts. Room state is refreshed by periodic polling, not pushed to the browser.
15
15
 
package/dist/cli.js CHANGED
@@ -4298,6 +4298,7 @@ var MAX_FILE_NAME_BYTES = 255;
4298
4298
  var MAX_MIME_BYTES = 255;
4299
4299
  var MAX_ROLE_BYTES = 256;
4300
4300
  var MAX_ROOM_NAME_CHARACTERS = 64;
4301
+ var MAX_ROOM_IDENTITY_NAME_CHARACTERS = 64;
4301
4302
  function utf8Bounded(label, maximumBytes) {
4302
4303
  return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
4303
4304
  (value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
@@ -4345,6 +4346,7 @@ var RoomNameSchema = external_exports.string().refine(
4345
4346
  }
4346
4347
  });
4347
4348
  var ROOM_IDENTITY_PREFIX = "ours-cowork:";
4349
+ var MAX_ROOM_IDENTITY_TITLE_CHARACTERS = MAX_ROOM_IDENTITY_NAME_CHARACTERS - Array.from(ROOM_IDENTITY_PREFIX).length;
4348
4350
  function isPersistedRoomIdentityName(roomId, identityName) {
4349
4351
  if (!LowerCrockfordUlidSchema.safeParse(roomId).success || !identityName.startsWith(ROOM_IDENTITY_PREFIX)) {
4350
4352
  return false;
@@ -5717,7 +5719,11 @@ function serviceEnvironment(config) {
5717
5719
  };
5718
5720
  }
5719
5721
  function validateServiceConfiguration(config) {
5720
- const values = { cowork_cli: SELF, ...serviceEnvironment(config) };
5722
+ const values = {
5723
+ node_executable: process.execPath,
5724
+ cowork_cli: SELF,
5725
+ ...serviceEnvironment(config)
5726
+ };
5721
5727
  for (const [label, value] of Object.entries(values)) {
5722
5728
  if (/[\x00-\x1f\x7f-\x9f]/.test(value)) {
5723
5729
  throw new CliError(EXIT.invalidState, "invalid_state", `${label} contains a control character unsafe for a service definition`);
@@ -5729,7 +5735,7 @@ function systemdValueQuote(value) {
5729
5735
  }
5730
5736
  function systemdExecutableQuote(value) {
5731
5737
  if (value.includes('"') || /[\0\n\r]/.test(value)) {
5732
- throw new CliError(EXIT.invalidState, "invalid_state", "cowork CLI path contains a character unsupported by systemd ExecStart");
5738
+ throw new CliError(EXIT.invalidState, "invalid_state", "node_executable contains a character unsupported by systemd ExecStart");
5733
5739
  }
5734
5740
  return `"${value.replaceAll("\\", "\\\\").replaceAll("%", "%%")}"`;
5735
5741
  }
@@ -5759,7 +5765,7 @@ StartLimitIntervalSec=0
5759
5765
 
5760
5766
  [Service]
5761
5767
  Type=simple
5762
- ExecStart=${systemdExecutableQuote(SELF)} serve
5768
+ ExecStart=${systemdExecutableQuote(process.execPath)} ${systemdValueQuote(SELF)} serve
5763
5769
  ${environment}
5764
5770
  Restart=on-failure
5765
5771
  RestartSec=5
@@ -5796,7 +5802,7 @@ function installLaunchd(config) {
5796
5802
  <plist version="1.0"><dict>
5797
5803
  <key>Label</key><string>${LAUNCHD_LABEL}</string>
5798
5804
  <key>ProgramArguments</key><array>
5799
- <string>${xml(SELF)}</string><string>serve</string>
5805
+ <string>${xml(process.execPath)}</string><string>${xml(SELF)}</string><string>serve</string>
5800
5806
  </array>
5801
5807
  <key>EnvironmentVariables</key><dict>
5802
5808
  ${environment}
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,16 @@ 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
+ }
5270
5310
  function sdkErrorCode(error) {
5271
5311
  if (error === null || typeof error !== "object" || !("code" in error)) return void 0;
5272
5312
  return typeof error.code === "string" ? error.code : void 0;
@@ -5450,18 +5490,22 @@ var init_packets = __esm({
5450
5490
  get(roomId) {
5451
5491
  return this.packets.get(roomId);
5452
5492
  }
5453
- async create(roomId, identityName, bio = `ours-cowork mission room ${roomId}`) {
5493
+ async preflightCreate(roomId, identityName) {
5454
5494
  validateRoomId(roomId);
5455
- this.assertStandardIdentity(roomId, identityName);
5495
+ await this.assertStandardIdentity(roomId, identityName);
5456
5496
  this.assertNoLegacyState(roomId);
5457
5497
  if (this.packets.has(roomId)) throw new Error(`room identity "${roomId}" is already hosted`);
5458
5498
  const localNames = /* @__PURE__ */ new Set([identityName]);
5459
5499
  const available = await this.host.listIdentityNames(localNames);
5460
5500
  if (available.has(identityName)) {
5461
- throw new Error(
5501
+ throw await sdkNameError(
5502
+ "NAME_TAKEN",
5462
5503
  `shared ours daemon already contains unproven room identity "${identityName}"; refusing to adopt it without a durably recorded CID`
5463
5504
  );
5464
5505
  }
5506
+ }
5507
+ async create(roomId, identityName, bio = `ours-cowork mission room ${roomId}`) {
5508
+ await this.preflightCreate(roomId, identityName);
5465
5509
  const client = await this.host.createClient();
5466
5510
  try {
5467
5511
  const created = await client.createIdentity({
@@ -5484,12 +5528,15 @@ var init_packets = __esm({
5484
5528
  } catch (error) {
5485
5529
  await client.releaseLease().catch(() => {
5486
5530
  });
5531
+ if (await isTypedNameRefusal(error)) {
5532
+ throw error;
5533
+ }
5487
5534
  throw new Error(`failed to provision standard SDK identity for room "${roomId}"`, { cause: error });
5488
5535
  }
5489
5536
  }
5490
5537
  async restore(roomId, expectedCid, identityName) {
5491
5538
  validateRoomId(roomId);
5492
- this.assertStandardIdentity(roomId, identityName);
5539
+ await this.assertStandardIdentity(roomId, identityName);
5493
5540
  this.assertNoLegacyState(roomId);
5494
5541
  if (expectedCid === void 0 || expectedCid.length === 0) {
5495
5542
  throw new Error(`refusing to restore room identity "${identityName}" without a durably recorded expected CID`);
@@ -5584,7 +5631,9 @@ var init_packets = __esm({
5584
5631
  throw new LegacyCoworkStateError(roomId);
5585
5632
  }
5586
5633
  }
5587
- 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}`);
5588
5637
  if (!isStandardRoomIdentityName(roomId, identityName)) throw new LegacyCoworkStateError(roomId);
5589
5638
  }
5590
5639
  };
@@ -6345,6 +6394,12 @@ function byteBoundedHistoryPage(records) {
6345
6394
  }
6346
6395
  return page;
6347
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
+ }
6348
6403
  function activeSeats(room) {
6349
6404
  return room.seats.filter((seat) => seat.state === "active");
6350
6405
  }
@@ -6430,6 +6485,7 @@ var init_service = __esm({
6430
6485
  nextMessageId;
6431
6486
  intake;
6432
6487
  provisioningCheckpoint;
6488
+ identityNameTails = /* @__PURE__ */ new Map();
6433
6489
  constructor(store, packets, options = {}) {
6434
6490
  this.store = store;
6435
6491
  this.packets = packets;
@@ -6448,7 +6504,8 @@ var init_service = __esm({
6448
6504
  const roomId = LowerCrockfordUlidSchema.parse(this.nextRoomId());
6449
6505
  const roomName = settings.name ?? defaultRoomName(roomId);
6450
6506
  const identityName = roomIdentityName(roomName);
6451
- return this.lock(roomId, async () => {
6507
+ return this.lockIdentityName(identityName, () => this.lock(roomId, async () => {
6508
+ await this.packets.preflightCreate(roomId, identityName);
6452
6509
  const provisional = RoomSchema.parse({
6453
6510
  version: 2,
6454
6511
  room_id: roomId,
@@ -6471,15 +6528,30 @@ var init_service = __esm({
6471
6528
  created_at: this.now()
6472
6529
  });
6473
6530
  await this.store.create(provisional);
6474
- const packet = await this.packets.create(
6475
- roomId,
6476
- identityName,
6477
- `ours-cowork mission room ${roomId}`
6478
- );
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
+ }
6479
6551
  this.provisioningCheckpoint("metadata");
6480
6552
  const { status: _packetPending, ...created } = provisional;
6481
6553
  return this.store.save(RoomSchema.parse({ ...created, identity_cid: packet.cid }));
6482
- });
6554
+ }));
6483
6555
  }
6484
6556
  async recoverRoom(roomId) {
6485
6557
  const id = LowerCrockfordUlidSchema.parse(roomId);
@@ -6520,7 +6592,13 @@ var init_service = __esm({
6520
6592
  async recoverPacketUnlocked(id, initial) {
6521
6593
  let room = initial;
6522
6594
  if (room.state === "closed") return room;
6523
- 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
+ }
6524
6602
  let packet = this.packets.get(id);
6525
6603
  if (packet) {
6526
6604
  if (!packetPending && packet.cid !== room.identity_cid) {
@@ -7679,6 +7757,17 @@ var init_service = __esm({
7679
7757
  lock(roomId, work) {
7680
7758
  return this.store.mutex(roomId).runExclusive(work);
7681
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
+ }
7682
7771
  packet(roomId) {
7683
7772
  const packet = this.packets.get(roomId);
7684
7773
  if (!packet) throw new RoomServiceError(`room packet "${roomId}" is not hosted`);
@@ -8025,6 +8114,45 @@ var init_storage = __esm({
8025
8114
  synchronous: db.pragma("synchronous", { simple: true })
8026
8115
  })));
8027
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
+ }
8028
8156
  async delete(roomId) {
8029
8157
  const id = this.roomId(roomId);
8030
8158
  await this.mutex(id, () => {