@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/README.md +3 -2
- package/dist/cli.js +9 -0
- package/dist/daemon.js +334 -45
- package/dist/web/assets/app.js +1 -1
- package/docs/03-configuration.md +2 -2
- package/docs/05-room-workflow.md +6 -2
- package/docs/08-backup-restore.md +1 -1
- package/docs/10-limitations.md +1 -1
- package/docs/11-web-console.md +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
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
|
-
|
|
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);
|