@ours.network/cowork 1.0.1 → 1.0.3

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
@@ -4116,1181 +4116,1149 @@ var init_zod = __esm({
4116
4116
  }
4117
4117
  });
4118
4118
 
4119
- // src/contracts.ts
4120
- import { createHash } from "node:crypto";
4121
- function utf8Bounded(label, maximumBytes) {
4122
- return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
4123
- (value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
4124
- `${label} must be at most ${maximumBytes} UTF-8 bytes`
4125
- );
4126
- }
4127
- function isStrictRfc3339(value) {
4128
- const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
4129
- if (!match) return false;
4130
- const [, yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText] = match;
4131
- const year = Number(yearText);
4132
- const month = Number(monthText);
4133
- const day = Number(dayText);
4134
- const hour = Number(hourText);
4135
- const minute = Number(minuteText);
4136
- const second = Number(secondText);
4137
- const offsetHour = offsetHourText === void 0 ? 0 : Number(offsetHourText);
4138
- const offsetMinute = offsetMinuteText === void 0 ? 0 : Number(offsetMinuteText);
4139
- if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
4140
- if (offsetHour > 23 || offsetMinute > 59) return false;
4141
- const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
4142
- const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
4143
- return day >= 1 && day <= days[month - 1];
4119
+ // src/config.ts
4120
+ import * as nodeFs from "node:fs";
4121
+ import { homedir } from "node:os";
4122
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
4123
+ function defaultConfig(home = homedir()) {
4124
+ return {
4125
+ version: 1,
4126
+ stateDir: resolve(home, ".ours-cowork"),
4127
+ rest: { enabled: true, port: 3052 }
4128
+ };
4144
4129
  }
4145
- function normalizeRoomName(value) {
4146
- return value.trim().normalize("NFC");
4130
+ function loadConfig(env = process.env, io = {}) {
4131
+ rejectRemovedEnvironment(env);
4132
+ const fs2 = io.fs ?? nodeFs;
4133
+ const defaults = defaultConfig(io.home);
4134
+ const configPath = resolve(env.OURS_COWORK_CONFIG ?? join(io.home ?? homedir(), ".ours-cowork", "config.json"));
4135
+ let file = defaults;
4136
+ const stat = lstatIfPresent(fs2, configPath);
4137
+ if (stat) {
4138
+ assertSecureFile(fs2, configPath, "config file");
4139
+ let parsed;
4140
+ try {
4141
+ parsed = JSON.parse(readSecureFile(fs2, configPath, "config file").toString("utf8"));
4142
+ } catch (error) {
4143
+ throw new CoworkConfigError(`malformed cowork config at ${configPath}`, { cause: error });
4144
+ }
4145
+ rejectRemovedConfig(parsed, configPath);
4146
+ try {
4147
+ file = CoworkConfigSchema.parse(parsed);
4148
+ } catch (error) {
4149
+ throw new CoworkConfigError(`invalid cowork config at ${configPath}`, { cause: error });
4150
+ }
4151
+ } else if (env.OURS_COWORK_CONFIG !== void 0) {
4152
+ throw new CoworkConfigError(`configured cowork config does not exist: ${configPath}`);
4153
+ }
4154
+ const restPort = env.OURS_COWORK_REST_PORT === void 0 ? void 0 : parsePort(env.OURS_COWORK_REST_PORT);
4155
+ try {
4156
+ return CoworkConfigSchema.parse({
4157
+ version: 1,
4158
+ stateDir: resolve(env.OURS_COWORK_STATE_DIR ?? file.stateDir),
4159
+ rest: {
4160
+ enabled: restPort === void 0 ? file.rest.enabled : true,
4161
+ port: restPort ?? file.rest.port
4162
+ }
4163
+ });
4164
+ } catch (error) {
4165
+ throw new CoworkConfigError("invalid effective cowork config", { cause: error });
4166
+ }
4147
4167
  }
4148
- function roomIdentityName(roomId) {
4149
- return `${ROOM_IDENTITY_PREFIX}${LowerCrockfordUlidSchema.parse(roomId)}`;
4168
+ function rejectRemovedEnvironment(env) {
4169
+ const removed = [
4170
+ "OURS_COWORK_BROKER_URL",
4171
+ "OURS_COWORK_DAEMON_MODE",
4172
+ "OURS_COWORK_DAEMON_ENDPOINT",
4173
+ "OURS_COWORK_DAEMON_STATE_DIR"
4174
+ ].filter((name) => env[name] !== void 0);
4175
+ if (removed.length === 0) return;
4176
+ throw new CoworkConfigError(
4177
+ `${removed.join(", ")} ${removed.length === 1 ? "was" : "were"} removed: ours-cowork now attaches only to the shared ours daemon. Configure that daemon with @ours.network/cli and select it through the standard OURS_CONFIG, OURS_PORT, and OURS_STATE_DIR inputs.`
4178
+ );
4150
4179
  }
4151
- function roomIdentitySlug(roomName) {
4152
- const normalized = RoomNameSchema.parse(roomName).normalize("NFKD").replace(new RegExp("\\p{M}+", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4153
- const bounded = normalized.slice(0, MAX_FRIENDLY_IDENTITY_SLUG_CHARACTERS).replace(/-+$/g, "");
4154
- return bounded || "room";
4180
+ function rejectRemovedConfig(value, path) {
4181
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
4182
+ const removed = ["brokerUrl", "daemon"].filter((key) => Object.hasOwn(value, key));
4183
+ if (removed.length === 0) return;
4184
+ throw new CoworkConfigError(
4185
+ `cowork config at ${path} contains removed ${removed.join(" and ")} ${removed.length === 1 ? "key" : "keys"}: ours-cowork now attaches only to the shared ours daemon. Remove those keys and configure the daemon with @ours.network/cli.`
4186
+ );
4155
4187
  }
4156
- function friendlyRoomIdentityName(roomId, roomName) {
4157
- const id = LowerCrockfordUlidSchema.parse(roomId);
4158
- const name = `${ROOM_IDENTITY_PREFIX}${roomIdentitySlug(roomName)}-${id}`;
4159
- if (name.length > MAX_ROOM_IDENTITY_NAME_CHARACTERS || !SDK_IDENTITY_NAME_PATTERN.test(name)) {
4160
- throw new TypeError("generated room identity name violates the shared ours daemon contract");
4188
+ function ensureRuntimeState(config, io = {}) {
4189
+ const parsed = CoworkConfigSchema.parse(config);
4190
+ const fs2 = io.fs ?? nodeFs;
4191
+ const stateDir = resolve(parsed.stateDir);
4192
+ assertSecureAncestors(fs2, stateDir, "state directory");
4193
+ const existing = lstatIfPresent(fs2, stateDir);
4194
+ if (!existing) {
4195
+ createSecureDirectoryTree(fs2, stateDir, "state directory");
4161
4196
  }
4162
- return name;
4163
- }
4164
- function configuredRoomIdentityName(roomId, roomName, mode) {
4165
- return mode === "friendly" ? friendlyRoomIdentityName(roomId, roomName) : roomIdentityName(roomId);
4197
+ assertSecureDirectory(fs2, stateDir, "state directory");
4198
+ const roomsPath = join(stateDir, "rooms");
4199
+ const rooms = lstatIfPresent(fs2, roomsPath);
4200
+ if (!rooms) {
4201
+ fs2.mkdirSync(roomsPath, { mode: DIRECTORY_MODE });
4202
+ secureOpenedDirectory(fs2, roomsPath, "rooms directory");
4203
+ fsyncDirectory(fs2, stateDir);
4204
+ }
4205
+ assertSecureDirectory(fs2, roomsPath, "rooms directory");
4206
+ return {
4207
+ socketPath: join(stateDir, "management.sock"),
4208
+ pidPath: join(stateDir, "daemon.pid"),
4209
+ lockPath: join(stateDir, "daemon.lock")
4210
+ };
4166
4211
  }
4167
- function legacyRoomIdentityName(roomId) {
4168
- return `cowork-room-${LowerCrockfordUlidSchema.parse(roomId)}`;
4212
+ function parsePort(value) {
4213
+ if (!/^[1-9][0-9]{0,4}$/.test(value)) {
4214
+ throw new CoworkConfigError("OURS_COWORK_REST_PORT must be a decimal port from 1 to 65535");
4215
+ }
4216
+ const port = Number(value);
4217
+ if (port > 65535) throw new CoworkConfigError("OURS_COWORK_REST_PORT must be from 1 to 65535");
4218
+ return port;
4169
4219
  }
4170
- function isPersistedRoomIdentityName(roomId, identityName) {
4171
- const id = LowerCrockfordUlidSchema.safeParse(roomId);
4172
- if (!id.success) return false;
4173
- if (identityName === roomIdentityName(id.data) || identityName === legacyRoomIdentityName(id.data)) return true;
4174
- const suffix = `-${id.data}`;
4175
- if (!identityName.startsWith(ROOM_IDENTITY_PREFIX) || !identityName.endsWith(suffix)) return false;
4176
- const slug = identityName.slice(ROOM_IDENTITY_PREFIX.length, -suffix.length);
4177
- return slug.length >= 1 && slug.length <= MAX_FRIENDLY_IDENTITY_SLUG_CHARACTERS && FRIENDLY_SLUG_PATTERN.test(slug) && identityName.length <= MAX_ROOM_IDENTITY_NAME_CHARACTERS && SDK_IDENTITY_NAME_PATTERN.test(identityName);
4220
+ function assertSecureAncestors(fs2, path, label) {
4221
+ const absolute = isAbsolute(path) ? path : resolve(path);
4222
+ const root = parse(absolute).root;
4223
+ const rootOwner = fs2.lstatSync(root).uid;
4224
+ let cursor = root;
4225
+ const components = absolute.slice(root.length).split("/").filter(Boolean);
4226
+ for (const [index, component] of components.entries()) {
4227
+ cursor = join(cursor, component);
4228
+ const stat = lstatIfPresent(fs2, cursor);
4229
+ if (stat?.isSymbolicLink()) throw new CoworkConfigError(`${label} must not traverse a symbolic link (symlink): ${cursor}`);
4230
+ if (!stat) break;
4231
+ if (!stat.isDirectory()) {
4232
+ if (index === components.length - 1) return;
4233
+ throw new CoworkConfigError(`${label} ancestor is not a directory: ${cursor}`);
4234
+ }
4235
+ if (index < components.length - 1) assertTrustedAncestor(stat, rootOwner, cursor, label);
4236
+ }
4178
4237
  }
4179
- function isStandardRoomIdentityName(roomId, identityName) {
4180
- return isPersistedRoomIdentityName(roomId, identityName) && identityName !== legacyRoomIdentityName(roomId);
4238
+ function assertTrustedAncestor(stat, rootOwner, path, label) {
4239
+ const uid = typeof process.getuid === "function" ? process.getuid() : stat.uid;
4240
+ const trustedStickyDirectory = (stat.mode & 512) !== 0;
4241
+ const writableByOthers = (stat.mode & 18) !== 0;
4242
+ const trustedOwner = stat.uid === uid || stat.uid === 0 || stat.uid === rootOwner;
4243
+ if (writableByOthers && (!trustedStickyDirectory || !trustedOwner)) {
4244
+ throw new CoworkConfigError(`${label} has an unsafe writable ancestor: ${path}`);
4245
+ }
4181
4246
  }
4182
- function refineRoomLineage(room, context) {
4183
- 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;
4184
- if (room.identity_cid === "" && !exactPacketPending) {
4185
- context.addIssue({
4186
- code: external_exports.ZodIssueCode.custom,
4187
- path: ["identity_cid"],
4188
- message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
4189
- });
4247
+ function createSecureDirectoryTree(fs2, path, label) {
4248
+ const missing = [];
4249
+ let cursor = path;
4250
+ while (!lstatIfPresent(fs2, cursor)) {
4251
+ missing.push(cursor);
4252
+ const parent = dirname(cursor);
4253
+ if (parent === cursor) throw new CoworkConfigError(`cannot locate an existing ancestor for ${label}`);
4254
+ cursor = parent;
4190
4255
  }
4191
- if (room.identity_cid !== "" && room.status === "packet_pending") {
4192
- context.addIssue({
4193
- code: external_exports.ZodIssueCode.custom,
4194
- path: ["status"],
4195
- message: "packet_pending status requires an empty identity_cid"
4196
- });
4256
+ assertSecureAncestors(fs2, path, label);
4257
+ for (const directory of missing.reverse()) {
4258
+ fs2.mkdirSync(directory, { mode: DIRECTORY_MODE });
4259
+ secureOpenedDirectory(fs2, directory, label);
4260
+ fsyncDirectory(fs2, dirname(directory));
4197
4261
  }
4198
- const pendingByRecovery = /* @__PURE__ */ new Map();
4199
- for (const [index, invite] of room.invites.entries()) {
4200
- if (invite.recovery_of === void 0) continue;
4201
- const recoveryOf = invite.recovery_of;
4202
- const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
4203
- const validSourceState = invite.state === "receipt_pending" ? source?.state === "replacement_required" : invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required" ? source?.state === "revoked" : invite.state === "revoked" ? invite.recovery_confirmed === true ? source?.state === "revoked" : source?.state === "replacement_required" || source?.state === "revoked" : false;
4204
- if (!source || source.invite_id === invite.invite_id || !validSourceState) {
4205
- context.addIssue({
4206
- code: external_exports.ZodIssueCode.custom,
4207
- path: ["invites", index, "recovery_of"],
4208
- message: "recovery_of must point to a source invite in the state required by this recovery lineage"
4209
- });
4210
- } else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
4211
- context.addIssue({
4212
- code: external_exports.ZodIssueCode.custom,
4213
- path: ["invites", index],
4214
- message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
4215
- });
4216
- }
4217
- if (invite.state === "receipt_pending") {
4218
- const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
4219
- pendingByRecovery.set(recoveryOf, count);
4220
- if (count > 1) {
4221
- context.addIssue({
4222
- code: external_exports.ZodIssueCode.custom,
4223
- path: ["invites", index, "recovery_of"],
4224
- message: "only one receipt_pending invite may exist per recovery_of pointer"
4225
- });
4226
- }
4262
+ }
4263
+ function secureOpenedDirectory(fs2, path, label) {
4264
+ let fd;
4265
+ try {
4266
+ fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
4267
+ const opened = fs2.fstatSync(fd);
4268
+ const current = fs2.lstatSync(path);
4269
+ if (!opened.isDirectory() || current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino) {
4270
+ throw new CoworkConfigError(`${label} changed while opening`);
4227
4271
  }
4272
+ fs2.fchmodSync(fd, DIRECTORY_MODE);
4273
+ fs2.fsyncSync(fd);
4274
+ } finally {
4275
+ if (fd !== void 0) fs2.closeSync(fd);
4228
4276
  }
4229
4277
  }
4230
- function defaultRoomName(roomId) {
4231
- return `Room ${LowerCrockfordUlidSchema.parse(roomId).slice(0, 8)}`;
4232
- }
4233
- function migrateRoomV1(room, mintParticipantId) {
4234
- return RoomSchema.parse({
4235
- ...room,
4236
- version: 2,
4237
- mission: { ...room.mission, briefing_version: 1 },
4238
- role_briefings: {},
4239
- rest_roles: [],
4240
- anonymous: false,
4241
- quiet_membership: false,
4242
- membership_epoch: 0,
4243
- seats: room.seats.map((seat) => ({
4244
- ...seat,
4245
- participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
4246
- state: "active"
4247
- }))
4248
- });
4249
- }
4250
- function refineRelaySubject(record, context) {
4251
- if (record.kind !== "relay_intent" && record.kind !== "relay_result") return;
4252
- if (record.message_id === void 0 === (record.file_id === void 0)) {
4253
- context.addIssue({
4254
- code: external_exports.ZodIssueCode.custom,
4255
- path: ["message_id"],
4256
- message: "relay records require exactly one of message_id or file_id"
4257
- });
4278
+ function assertSecureDirectory(fs2, path, label) {
4279
+ const stat = fs2.lstatSync(path);
4280
+ if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
4281
+ if (!stat.isDirectory()) throw new CoworkConfigError(`${label} must be a directory`);
4282
+ if ((stat.mode & 511) !== DIRECTORY_MODE) {
4283
+ throw new CoworkConfigError(`${label} mode must be 0700`);
4258
4284
  }
4285
+ assertOwner(stat, label);
4259
4286
  }
4260
- function refineFileRecord(record, context) {
4261
- if (record.kind !== "file" || record.data_base64 === void 0) return;
4262
- const bytes = Buffer.from(record.data_base64, "base64");
4263
- if (bytes.toString("base64") !== record.data_base64) {
4264
- context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["data_base64"], message: "file bytes must use canonical base64" });
4265
- }
4266
- if (bytes.length !== record.size) {
4267
- context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["size"], message: "file size must match decoded bytes" });
4268
- }
4269
- const digest = createHash("sha256").update(bytes).digest("hex");
4270
- if (digest !== record.sha256) {
4271
- context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["sha256"], message: "file sha256 must match decoded bytes" });
4287
+ function assertSecureFile(fs2, path, label) {
4288
+ const stat = fs2.lstatSync(path);
4289
+ if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
4290
+ if (!stat.isFile() || stat.nlink !== 1) throw new CoworkConfigError(`${label} must be a single-link regular file`);
4291
+ if ((stat.mode & 511) !== FILE_MODE) throw new CoworkConfigError(`${label} mode must be 0600`);
4292
+ assertOwner(stat, label);
4293
+ }
4294
+ function assertOwner(stat, label) {
4295
+ if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
4296
+ throw new CoworkConfigError(`${label} must be owned by the current user`);
4272
4297
  }
4273
4298
  }
4274
- function refineMessageCategory(message, context) {
4275
- const requires = (field, present) => {
4276
- if (present && message[field] === void 0) {
4277
- context.addIssue({
4278
- code: external_exports.ZodIssueCode.custom,
4279
- path: [field],
4280
- message: `${message.category} messages require ${field}`
4281
- });
4282
- }
4283
- if (!present && message[field] !== void 0) {
4284
- context.addIssue({
4285
- code: external_exports.ZodIssueCode.custom,
4286
- path: [field],
4287
- message: `${field} is forbidden on ${message.category} messages`
4288
- });
4299
+ function readSecureFile(fs2, path, label) {
4300
+ let fd;
4301
+ try {
4302
+ fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
4303
+ const opened = fs2.fstatSync(fd);
4304
+ const current = fs2.lstatSync(path);
4305
+ if (!opened.isFile() || opened.nlink !== 1 || (opened.mode & 511) !== FILE_MODE || typeof process.getuid === "function" && opened.uid !== process.getuid() || current.isSymbolicLink() || current.dev !== opened.dev || current.ino !== opened.ino) {
4306
+ throw new CoworkConfigError(`${label} changed while opening`);
4289
4307
  }
4290
- };
4291
- requires("briefing_role", message.category === "role_briefing");
4292
- requires("membership", message.category === "membership");
4293
- if (message.category === "role_briefing" && message.briefing_version === void 0) {
4294
- context.addIssue({
4295
- code: external_exports.ZodIssueCode.custom,
4296
- path: ["briefing_version"],
4297
- message: "role_briefing messages require briefing_version"
4298
- });
4308
+ return fs2.readFileSync(fd);
4309
+ } finally {
4310
+ if (fd !== void 0) fs2.closeSync(fd);
4299
4311
  }
4300
- if (message.category === "chat" || message.category === "membership") {
4301
- if (message.briefing_version !== void 0) {
4302
- context.addIssue({
4303
- code: external_exports.ZodIssueCode.custom,
4304
- path: ["briefing_version"],
4305
- message: `briefing_version is forbidden on ${message.category} messages`
4306
- });
4307
- }
4312
+ }
4313
+ function fsyncDirectory(fs2, path) {
4314
+ let fd;
4315
+ try {
4316
+ fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
4317
+ fs2.fsyncSync(fd);
4318
+ } finally {
4319
+ if (fd !== void 0) fs2.closeSync(fd);
4308
4320
  }
4309
4321
  }
4310
- 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, MAX_FRIENDLY_IDENTITY_SLUG_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, Rfc3339Schema, RoomNameSchema, RoomIdentityNameModeSchema, ROOM_IDENTITY_PREFIX, SDK_IDENTITY_NAME_PATTERN, FRIENDLY_SLUG_PATTERN, 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;
4311
- var init_contracts = __esm({
4312
- "src/contracts.ts"() {
4322
+ function lstatIfPresent(fs2, path) {
4323
+ try {
4324
+ return fs2.lstatSync(path);
4325
+ } catch (error) {
4326
+ if (error.code === "ENOENT") return void 0;
4327
+ throw error;
4328
+ }
4329
+ }
4330
+ var DIRECTORY_MODE, FILE_MODE, NO_FOLLOW, CoworkConfigSchema, CoworkConfigError;
4331
+ var init_config = __esm({
4332
+ "src/config.ts"() {
4313
4333
  "use strict";
4314
4334
  init_zod();
4315
- MAX_TEXT_BYTES = 262144;
4316
- MAX_FILE_BYTES = 2 * 1024 * 1024;
4317
- MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
4318
- MAX_MANAGEMENT_RESPONSE_BYTES = MAX_HISTORY_PAGE_BYTES + 1024 * 1024;
4319
- MAX_EXTERNAL_INVITE_BYTES = 48 * 1024;
4320
- MAX_FILE_NAME_BYTES = 255;
4321
- MAX_MIME_BYTES = 255;
4322
- MAX_ROLE_BYTES = 256;
4323
- MAX_ROOM_NAME_CHARACTERS = 64;
4324
- MAX_ROOM_IDENTITY_NAME_CHARACTERS = 64;
4325
- MAX_FRIENDLY_IDENTITY_SLUG_CHARACTERS = 25;
4326
- NonEmptyStringSchema = external_exports.string().min(1);
4327
- PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
4328
- LowerCrockfordUlidSchema = external_exports.string().regex(
4329
- /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4330
- "must be a 26-character lowercase Crockford ULID"
4331
- );
4332
- Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
4333
- RoomNameSchema = external_exports.string().refine(
4334
- (value) => !/[\p{Cc}\p{Cf}]/u.test(value),
4335
- "room name must not contain Unicode control or format characters"
4336
- ).transform(normalizeRoomName).superRefine((value, context) => {
4337
- const length = Array.from(value).length;
4338
- if (length < 1 || length > MAX_ROOM_NAME_CHARACTERS) {
4339
- context.addIssue({
4340
- code: external_exports.ZodIssueCode.custom,
4341
- message: `room name must contain 1-${MAX_ROOM_NAME_CHARACTERS} Unicode characters after normalization`
4342
- });
4343
- }
4344
- });
4345
- RoomIdentityNameModeSchema = external_exports.enum(["stable_id", "friendly"]);
4346
- ROOM_IDENTITY_PREFIX = "ours-cowork-";
4347
- SDK_IDENTITY_NAME_PATTERN = /^[A-Za-z0-9 _.@-]{1,64}$/;
4348
- FRIENDLY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4349
- RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4350
- ROOM_ROLE = "room";
4351
- MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
4352
- MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
4353
- FileNameSchema = utf8Bounded("file name", MAX_FILE_NAME_BYTES).refine((value) => value !== "." && value !== "..", "file name must not be a relative path token").refine((value) => !/[\x00/\\]/.test(value), "file name must be a single path-free name");
4354
- FileMimeSchema = external_exports.string().refine(
4355
- (value) => Buffer.byteLength(value, "utf8") <= MAX_MIME_BYTES,
4356
- `file MIME metadata must be at most ${MAX_MIME_BYTES} UTF-8 bytes`
4357
- );
4358
- RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
4359
- SeatStateSchema = external_exports.enum(["pending", "active", "removed"]);
4360
- InviteModeSchema = external_exports.enum(["one_time", "public"]);
4361
- DEFAULT_ROLE = "Participant";
4362
- InviteStateSchema = external_exports.enum([
4363
- "live",
4364
- "consumed",
4365
- "revoked",
4366
- "replacement_required",
4367
- "receipt_pending"
4368
- ]);
4369
- RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
4370
- SeatV1Schema = external_exports.object({
4371
- identity: NonEmptyStringSchema,
4372
- display_name: NonEmptyStringSchema,
4373
- role: RoleSchema,
4374
- invite_id: NonEmptyStringSchema,
4375
- accepted_at: Rfc3339Schema
4335
+ DIRECTORY_MODE = 448;
4336
+ FILE_MODE = 384;
4337
+ NO_FOLLOW = nodeFs.constants.O_NOFOLLOW ?? 0;
4338
+ CoworkConfigSchema = external_exports.object({
4339
+ version: external_exports.literal(1),
4340
+ stateDir: external_exports.string().min(1),
4341
+ rest: external_exports.object({
4342
+ enabled: external_exports.boolean(),
4343
+ port: external_exports.number().int().min(1).max(65535)
4344
+ }).strict()
4376
4345
  }).strict();
4377
- SeatSchema = external_exports.object({
4378
- identity: NonEmptyStringSchema,
4379
- display_name: NonEmptyStringSchema,
4380
- role: RoleSchema,
4381
- invite_id: NonEmptyStringSchema,
4382
- accepted_at: Rfc3339Schema.optional(),
4383
- requested_at: Rfc3339Schema.optional(),
4384
- invite_sha256: external_exports.string().regex(/^[0-9a-f]{64}$/).optional(),
4385
- participant_id: LowerCrockfordUlidSchema,
4386
- state: SeatStateSchema,
4387
- alias: NonEmptyStringSchema.optional(),
4388
- removed_at: Rfc3339Schema.optional(),
4389
- removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
4390
- replaces_seat: LowerCrockfordUlidSchema.optional(),
4391
- bounced_at: Rfc3339Schema.optional()
4392
- }).strict().superRefine((seat, context) => {
4393
- if (seat.state === "pending") {
4394
- for (const field of ["requested_at", "invite_sha256"]) {
4395
- if (seat[field] === void 0) {
4396
- context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `pending seats require ${field}` });
4397
- }
4398
- }
4399
- for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at"]) {
4400
- if (seat[field] !== void 0) {
4401
- context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `${field} is forbidden on pending seats` });
4402
- }
4403
- }
4404
- } else if (seat.state === "removed") {
4405
- if (seat.accepted_at === void 0 && seat.requested_at === void 0) {
4406
- context.addIssue({
4407
- code: external_exports.ZodIssueCode.custom,
4408
- path: ["accepted_at"],
4409
- message: "removed seats require accepted_at unless they are cancelled external admissions"
4410
- });
4411
- }
4412
- if (seat.removed_at === void 0) {
4413
- context.addIssue({
4414
- code: external_exports.ZodIssueCode.custom,
4415
- path: ["removed_at"],
4416
- message: "removed seats require removed_at"
4417
- });
4418
- }
4419
- if (seat.removed_epoch === void 0) {
4420
- context.addIssue({
4421
- code: external_exports.ZodIssueCode.custom,
4422
- path: ["removed_epoch"],
4423
- message: "removed seats require removed_epoch"
4424
- });
4425
- }
4426
- } else {
4427
- if (seat.accepted_at === void 0) {
4428
- context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["accepted_at"], message: "active seats require accepted_at" });
4429
- }
4430
- for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
4431
- if (seat[field] !== void 0) {
4432
- context.addIssue({
4433
- code: external_exports.ZodIssueCode.custom,
4434
- path: [field],
4435
- message: `${field} is reserved for removed seats`
4436
- });
4437
- }
4438
- }
4439
- }
4440
- if (seat.requested_at === void 0 !== (seat.invite_sha256 === void 0)) {
4441
- context.addIssue({
4442
- code: external_exports.ZodIssueCode.custom,
4443
- path: ["invite_sha256"],
4444
- message: "external admission metadata requires both requested_at and invite_sha256"
4445
- });
4446
- }
4447
- });
4448
- RoomInviteSchema = external_exports.object({
4449
- invite_id: NonEmptyStringSchema,
4450
- mode: InviteModeSchema,
4451
- role: RoleSchema,
4452
- min_accepts: PositiveSafeIntegerSchema,
4453
- accepted_cids: external_exports.array(NonEmptyStringSchema),
4454
- state: InviteStateSchema,
4455
- recovery_of: NonEmptyStringSchema.optional(),
4456
- recovery_confirmed: external_exports.boolean().optional(),
4457
- created_at: Rfc3339Schema,
4458
- replaces_seat: LowerCrockfordUlidSchema.optional()
4459
- }).strict().superRefine((invite, context) => {
4460
- if (invite.mode === "one_time" && invite.min_accepts !== 1) {
4461
- context.addIssue({
4462
- code: external_exports.ZodIssueCode.custom,
4463
- path: ["min_accepts"],
4464
- message: "one_time invites require min_accepts === 1"
4465
- });
4466
- }
4467
- if (invite.state === "receipt_pending" && invite.recovery_of === void 0) {
4468
- context.addIssue({
4469
- code: external_exports.ZodIssueCode.custom,
4470
- path: ["recovery_of"],
4471
- message: "receipt_pending invites require recovery_of"
4472
- });
4473
- }
4474
- if (invite.recovery_of === void 0 && invite.recovery_confirmed !== void 0) {
4475
- context.addIssue({
4476
- code: external_exports.ZodIssueCode.custom,
4477
- path: ["recovery_confirmed"],
4478
- message: "recovery_confirmed is forbidden without recovery_of"
4479
- });
4346
+ CoworkConfigError = class extends Error {
4347
+ constructor(message, options) {
4348
+ super(message, options);
4349
+ this.name = "CoworkConfigError";
4480
4350
  }
4481
- if (invite.recovery_of !== void 0 && invite.recovery_confirmed === void 0) {
4482
- context.addIssue({
4483
- code: external_exports.ZodIssueCode.custom,
4484
- path: ["recovery_confirmed"],
4485
- message: "recovery_confirmed is required with recovery_of"
4486
- });
4351
+ };
4352
+ }
4353
+ });
4354
+
4355
+ // src/ours-runtime.ts
4356
+ import { randomBytes } from "node:crypto";
4357
+ function createOursHost(_config, log = () => {
4358
+ }) {
4359
+ return new SharedOursHost(log);
4360
+ }
4361
+ function sleep(ms, signal) {
4362
+ return new Promise((resolveSleep) => {
4363
+ const timer = setTimeout(finish, ms);
4364
+ signal.addEventListener("abort", finish, { once: true });
4365
+ function finish() {
4366
+ clearTimeout(timer);
4367
+ signal.removeEventListener("abort", finish);
4368
+ resolveSleep();
4369
+ }
4370
+ });
4371
+ }
4372
+ var WATCH_RETRY_MIN_MS, WATCH_RETRY_MAX_MS, STATE_RESYNC_INTERVAL_MS, attachSharedClient, SharedOursHost;
4373
+ var init_ours_runtime = __esm({
4374
+ "src/ours-runtime.ts"() {
4375
+ "use strict";
4376
+ WATCH_RETRY_MIN_MS = 500;
4377
+ WATCH_RETRY_MAX_MS = 3e4;
4378
+ STATE_RESYNC_INTERVAL_MS = 2e3;
4379
+ attachSharedClient = async (options) => {
4380
+ const { attachOursClient } = await import("@ours.network/sdk");
4381
+ return attachOursClient(options);
4382
+ };
4383
+ SharedOursHost = class {
4384
+ log;
4385
+ attach;
4386
+ listeners = /* @__PURE__ */ new Set();
4387
+ watchers = /* @__PURE__ */ new Map();
4388
+ watchLeaseToken = `cowork-watch-${randomBytes(16).toString("hex")}`;
4389
+ watchClient;
4390
+ resyncTimer;
4391
+ closed = false;
4392
+ constructor(log = () => {
4393
+ }, attach = attachSharedClient) {
4394
+ this.log = log;
4395
+ this.attach = attach;
4487
4396
  }
4488
- if (invite.state === "receipt_pending" && invite.recovery_confirmed !== false) {
4489
- context.addIssue({
4490
- code: external_exports.ZodIssueCode.custom,
4491
- path: ["recovery_confirmed"],
4492
- message: "receipt_pending recovery lineage must be unconfirmed"
4493
- });
4397
+ async boot() {
4398
+ if (this.watchClient) return;
4399
+ if (this.closed) throw new Error("shared ours daemon host cannot restart in the same process");
4400
+ this.watchClient = await this.attach({ leaseToken: this.watchLeaseToken });
4494
4401
  }
4495
- if (invite.recovery_of !== void 0 && (invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required") && invite.recovery_confirmed !== true) {
4496
- context.addIssue({
4497
- code: external_exports.ZodIssueCode.custom,
4498
- path: ["recovery_confirmed"],
4499
- message: "live, consumed, and replacement_required recovery lineage must be confirmed"
4500
- });
4402
+ async createClient(leaseToken = `cowork-${randomBytes(16).toString("hex")}`) {
4403
+ if (!this.watchClient) throw new Error("shared ours daemon host is not booted");
4404
+ return this.attach({ leaseToken });
4501
4405
  }
4502
- if (invite.state === "receipt_pending" && invite.accepted_cids.length > 0) {
4503
- context.addIssue({
4504
- code: external_exports.ZodIssueCode.custom,
4505
- path: ["accepted_cids"],
4506
- message: "receipt_pending invites cannot have accepted CIDs"
4507
- });
4406
+ async listIdentityNames(localNames) {
4407
+ if (!this.watchClient) throw new Error("shared ours daemon host is not booted");
4408
+ const rows = await this.watchClient.identities();
4409
+ return new Set(rows.flatMap((row) => localNames.has(row.name) ? [row.name] : []));
4410
+ }
4411
+ onIdentityNotify(listener) {
4412
+ this.listeners.add(listener);
4413
+ return () => this.listeners.delete(listener);
4414
+ }
4415
+ trackIdentity(identityName) {
4416
+ if (!this.watchClient) throw new Error("shared ours daemon host is not booted");
4417
+ const existing = this.watchers.get(identityName);
4418
+ if (existing) return () => this.untrack(identityName, existing);
4419
+ const controller = new AbortController();
4420
+ const watcher = { controller, work: Promise.resolve() };
4421
+ watcher.work = this.follow(identityName, controller.signal);
4422
+ this.watchers.set(identityName, watcher);
4423
+ this.announce(identityName);
4424
+ this.ensureStateResync();
4425
+ return () => this.untrack(identityName, watcher);
4426
+ }
4427
+ close() {
4428
+ }
4429
+ async shutdown() {
4430
+ if (this.closed) return { requiresProcessExit: false };
4431
+ this.closed = true;
4432
+ this.listeners.clear();
4433
+ this.stopStateResync();
4434
+ const watchers = [...this.watchers.values()];
4435
+ this.watchers.clear();
4436
+ for (const watcher of watchers) watcher.controller.abort();
4437
+ await Promise.allSettled(watchers.map((watcher) => watcher.work));
4438
+ const client = this.watchClient;
4439
+ this.watchClient = void 0;
4440
+ if (client) await client.releaseLease();
4441
+ return { requiresProcessExit: false };
4442
+ }
4443
+ untrack(identityName, watcher) {
4444
+ if (this.watchers.get(identityName) !== watcher) return;
4445
+ this.watchers.delete(identityName);
4446
+ watcher.controller.abort();
4447
+ if (this.watchers.size === 0) this.stopStateResync();
4448
+ }
4449
+ ensureStateResync() {
4450
+ if (this.resyncTimer) return;
4451
+ this.resyncTimer = setInterval(() => {
4452
+ for (const identityName of this.watchers.keys()) this.announce(identityName);
4453
+ }, STATE_RESYNC_INTERVAL_MS);
4454
+ this.resyncTimer.unref();
4455
+ }
4456
+ stopStateResync() {
4457
+ if (!this.resyncTimer) return;
4458
+ clearInterval(this.resyncTimer);
4459
+ this.resyncTimer = void 0;
4508
4460
  }
4509
- });
4510
- MissionV1Schema = external_exports.object({
4511
- goal: MissionTextSchema,
4512
- briefing: MissionTextSchema
4513
- }).strict();
4514
- MissionSchema = external_exports.object({
4515
- goal: MissionTextSchema,
4516
- briefing: MissionTextSchema,
4517
- briefing_version: PositiveSafeIntegerSchema
4518
- }).strict();
4519
- RoleBriefingSchema = external_exports.object({
4520
- text: MissionTextSchema,
4521
- version: PositiveSafeIntegerSchema,
4522
- updated_at: Rfc3339Schema
4523
- }).strict();
4524
- RoomCommonShape = {
4525
- room_id: LowerCrockfordUlidSchema,
4526
- identity_name: NonEmptyStringSchema,
4527
- identity_cid: external_exports.string(),
4528
- state: RoomStateSchema,
4529
- status: NonEmptyStringSchema.optional(),
4530
- invites: external_exports.array(RoomInviteSchema),
4531
- created_at: Rfc3339Schema,
4532
- activated_at: Rfc3339Schema.optional(),
4533
- closed_at: Rfc3339Schema.optional()
4534
- };
4535
- RoomV1Schema = external_exports.object({
4536
- ...RoomCommonShape,
4537
- version: external_exports.literal(1),
4538
- mission: MissionV1Schema,
4539
- seats: external_exports.array(SeatV1Schema)
4540
- }).strict().superRefine(refineRoomLineage);
4541
- CurrentRoomSchema = external_exports.object({
4542
- ...RoomCommonShape,
4543
- room_name: RoomNameSchema,
4544
- version: external_exports.literal(2),
4545
- mission: MissionSchema,
4546
- role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
4547
4461
  /**
4548
- * Roles a REST caller may author under. A plain array of names:
4549
- * the role name IS the identifier, so there is nothing per-role to store.
4550
- * Not a seat, not a membership: seat and membership invariants do not apply.
4551
- * The registry itself is an exact-name set, and reserves `room` for the
4552
- * room's own voice.
4462
+ * Long-poll one identity forever, reconnecting with bounded backoff.
4463
+ *
4464
+ * A replacement stream primes at the daemon tip, so each reconnection also
4465
+ * requests a full state resync. The periodic resync above covers the small
4466
+ * request-prime race and daemon transitions that are not in the structured
4467
+ * notification log; reconciliation is idempotent and self-coalescing.
4553
4468
  */
4554
- rest_roles: external_exports.array(RoleSchema).superRefine((roles, context) => {
4555
- const seen = /* @__PURE__ */ new Set();
4556
- for (const [index, role] of roles.entries()) {
4557
- if (role === ROOM_ROLE) {
4558
- context.addIssue({
4559
- code: external_exports.ZodIssueCode.custom,
4560
- path: [index],
4561
- message: `role "${ROOM_ROLE}" is reserved for the room's own voice`
4562
- });
4563
- }
4564
- if (seen.has(role)) {
4565
- context.addIssue({
4566
- code: external_exports.ZodIssueCode.custom,
4567
- path: [index],
4568
- message: "REST role names must be unique within the room"
4569
- });
4469
+ async follow(identityName, signal) {
4470
+ let backoffMs = WATCH_RETRY_MIN_MS;
4471
+ let resyncPending = false;
4472
+ while (!signal.aborted) {
4473
+ const client = this.watchClient;
4474
+ if (!client) return;
4475
+ try {
4476
+ const stream = client.watchNotifications(identityName, { signal });
4477
+ let step = stream.next();
4478
+ if (resyncPending) {
4479
+ resyncPending = false;
4480
+ this.announce(identityName);
4481
+ }
4482
+ for (let settled = await step; !settled.done; settled = await step) {
4483
+ backoffMs = WATCH_RETRY_MIN_MS;
4484
+ this.announce(identityName);
4485
+ step = stream.next();
4486
+ }
4487
+ if (!signal.aborted) {
4488
+ resyncPending = true;
4489
+ this.log(`[${identityName}] shared ours daemon notification watch ended; reconnecting`);
4490
+ }
4491
+ } catch (error) {
4492
+ if (signal.aborted) return;
4493
+ resyncPending = true;
4494
+ this.log(`[${identityName}] shared ours daemon notification watch failed:`, error);
4570
4495
  }
4571
- seen.add(role);
4496
+ if (signal.aborted) return;
4497
+ await sleep(backoffMs, signal);
4498
+ backoffMs = Math.min(backoffMs * 2, WATCH_RETRY_MAX_MS);
4572
4499
  }
4573
- }),
4574
- anonymous: external_exports.boolean(),
4575
- quiet_membership: external_exports.boolean(),
4576
- membership_epoch: external_exports.number().int().nonnegative().safe(),
4577
- seats: external_exports.array(SeatSchema)
4578
- }).strict().superRefine((room, context) => {
4579
- refineRoomLineage(room, context);
4580
- const byParticipant = /* @__PURE__ */ new Map();
4581
- const activeAliases = /* @__PURE__ */ new Set();
4582
- const authorizedCids = /* @__PURE__ */ new Set();
4583
- for (const [index, seat] of room.seats.entries()) {
4584
- if (byParticipant.has(seat.participant_id)) {
4585
- context.addIssue({
4586
- code: external_exports.ZodIssueCode.custom,
4587
- path: ["seats", index, "participant_id"],
4588
- message: "participant_id must be unique within the room"
4589
- });
4590
- }
4591
- byParticipant.set(seat.participant_id, seat);
4592
- if (seat.state === "pending" || seat.state === "active") {
4593
- if (authorizedCids.has(seat.identity)) {
4594
- context.addIssue({
4595
- code: external_exports.ZodIssueCode.custom,
4596
- path: ["seats", index, "identity"],
4597
- message: "at most one pending or active seat may exist per CID"
4598
- });
4599
- }
4600
- authorizedCids.add(seat.identity);
4601
- }
4602
- if (room.anonymous && seat.alias === void 0) {
4603
- context.addIssue({
4604
- code: external_exports.ZodIssueCode.custom,
4605
- path: ["seats", index, "alias"],
4606
- message: "anonymous rooms require an alias on every seat"
4607
- });
4608
- }
4609
- if (!room.anonymous && seat.alias !== void 0) {
4610
- context.addIssue({
4611
- code: external_exports.ZodIssueCode.custom,
4612
- path: ["seats", index, "alias"],
4613
- message: "aliases are reserved for anonymous rooms"
4614
- });
4615
- }
4616
- if (seat.state === "active" && seat.alias !== void 0) {
4617
- if (activeAliases.has(seat.alias)) {
4618
- context.addIssue({
4619
- code: external_exports.ZodIssueCode.custom,
4620
- path: ["seats", index, "alias"],
4621
- message: "active seats must hold distinct aliases"
4622
- });
4623
- }
4624
- activeAliases.add(seat.alias);
4625
- }
4626
- if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
4627
- context.addIssue({
4628
- code: external_exports.ZodIssueCode.custom,
4629
- path: ["seats", index, "removed_epoch"],
4630
- message: "removed_epoch cannot exceed the room membership_epoch"
4631
- });
4500
+ }
4501
+ announce(identityName) {
4502
+ for (const listener of this.listeners) {
4503
+ try {
4504
+ listener(identityName);
4505
+ } catch (error) {
4506
+ this.log(`cowork SDK notification listener failed for ${identityName}:`, error);
4507
+ }
4632
4508
  }
4633
4509
  }
4634
- for (const [index, seat] of room.seats.entries()) {
4635
- if (seat.replaces_seat === void 0) continue;
4636
- const predecessor = byParticipant.get(seat.replaces_seat);
4637
- if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
4638
- context.addIssue({
4639
- code: external_exports.ZodIssueCode.custom,
4640
- path: ["seats", index, "replaces_seat"],
4641
- message: "replaces_seat must reference a removed seat in this room"
4642
- });
4643
- continue;
4644
- }
4645
- if (predecessor.role !== seat.role) {
4646
- context.addIssue({
4647
- code: external_exports.ZodIssueCode.custom,
4648
- path: ["seats", index, "role"],
4649
- message: "a replacement seat must inherit the predecessor role"
4650
- });
4651
- }
4652
- if (room.anonymous && seat.alias !== predecessor.alias) {
4653
- context.addIssue({
4654
- code: external_exports.ZodIssueCode.custom,
4655
- path: ["seats", index, "alias"],
4656
- message: "an anonymous replacement seat must inherit the predecessor alias"
4657
- });
4658
- }
4659
- }
4660
- });
4661
- RoomSchema = external_exports.preprocess((value) => {
4662
- if (typeof value !== "object" || value === null) return value;
4663
- const patch = {};
4664
- if (!Object.hasOwn(value, "room_name")) {
4665
- const roomId = value.room_id;
4666
- if (typeof roomId === "string" && LowerCrockfordUlidSchema.safeParse(roomId).success) {
4667
- patch.room_name = defaultRoomName(roomId);
4668
- }
4669
- }
4670
- if (!Object.hasOwn(value, "rest_roles")) patch.rest_roles = [];
4671
- if (Object.keys(patch).length === 0) return value;
4672
- return { ...value, ...patch };
4673
- }, CurrentRoomSchema);
4674
- CreateRoomInputSchema = external_exports.object({
4675
- name: RoomNameSchema.optional(),
4676
- goal: MissionTextSchema,
4677
- briefing: MissionTextSchema,
4678
- anonymous: external_exports.boolean().optional(),
4679
- quiet_membership: external_exports.boolean().optional()
4680
- }).strict();
4681
- UpdateRoomInputSchema = external_exports.object({
4682
- name: RoomNameSchema.optional(),
4683
- goal: MissionTextSchema.optional(),
4684
- briefing: MissionTextSchema.optional(),
4685
- status: NonEmptyStringSchema.optional(),
4686
- quiet_membership: external_exports.boolean().optional()
4687
- }).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
4688
- RoleBriefingSetInputSchema = external_exports.object({
4689
- role: RoleSchema,
4690
- text: MissionTextSchema
4691
- }).strict();
4692
- RoleBriefingDeleteInputSchema = external_exports.object({
4693
- role: RoleSchema
4694
- }).strict();
4695
- PostMessageInputSchema = external_exports.object({
4696
- text: MessageTextSchema
4697
- }).strict();
4698
- PostAsRoleInputSchema = external_exports.object({
4699
- role: RoleSchema,
4700
- text: MessageTextSchema
4701
- }).strict();
4702
- RestRoleInputSchema = external_exports.object({
4703
- role: RoleSchema
4704
- }).strict();
4705
- ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4706
- AcceptExternalInviteInputSchema = external_exports.object({
4707
- role: RoleSchema,
4708
- invite: external_exports.string().refine(
4709
- (value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
4710
- `invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
4711
- ),
4712
- expected_cid: ContainerIdSchema.optional(),
4713
- replaces_seat: LowerCrockfordUlidSchema.optional()
4714
- }).strict();
4715
- AuthorSnapshotSchema = external_exports.object({
4716
- identity: NonEmptyStringSchema,
4717
- display_name: NonEmptyStringSchema,
4718
- role: RoleSchema
4719
- }).strict();
4720
- RecordCommonShape = {
4721
- version: external_exports.literal(1),
4722
- room_id: LowerCrockfordUlidSchema,
4723
- seq: PositiveSafeIntegerSchema,
4724
- record_id: NonEmptyStringSchema,
4725
- at: Rfc3339Schema
4726
- };
4727
- AppendCommonShape = {
4728
- version: external_exports.literal(1),
4729
- room_id: LowerCrockfordUlidSchema,
4730
- at: Rfc3339Schema
4731
- };
4732
- MembershipNoticeSchema = external_exports.object({
4733
- action: external_exports.enum(["remove"]),
4734
- alias: NonEmptyStringSchema.optional(),
4735
- role: RoleSchema.optional(),
4736
- epoch: external_exports.number().int().nonnegative().safe()
4737
- }).strict();
4738
- AuthorAliasSchema = external_exports.object({
4739
- participant_id: LowerCrockfordUlidSchema,
4740
- alias: NonEmptyStringSchema
4741
- }).strict();
4742
- ReplyReferenceSchema = external_exports.object({
4743
- wire_id: NonEmptyStringSchema,
4744
- sentence: PositiveSafeIntegerSchema.optional()
4745
- }).strict();
4746
- MessageShape = {
4747
- kind: external_exports.literal("message"),
4748
- message_id: LowerCrockfordUlidSchema,
4749
- author: AuthorSnapshotSchema,
4750
- author_alias: AuthorAliasSchema.optional(),
4751
- category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
4752
- briefing_role: RoleSchema.optional(),
4753
- briefing_version: PositiveSafeIntegerSchema.optional(),
4754
- membership: MembershipNoticeSchema.optional(),
4755
- text: MessageTextSchema,
4756
- recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
4757
- const seen = /* @__PURE__ */ new Set();
4758
- for (const [index, identity] of identities.entries()) {
4759
- if (seen.has(identity)) {
4760
- context.addIssue({
4761
- code: external_exports.ZodIssueCode.custom,
4762
- path: [index],
4763
- message: "recipient identities must be unique"
4764
- });
4765
- }
4766
- seen.add(identity);
4767
- }
4768
- }),
4769
- source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
4770
- source_wire_id: NonEmptyStringSchema.optional(),
4771
- source_reply_to: ReplyReferenceSchema.optional()
4772
- };
4773
- RelayIntentShape = {
4774
- kind: external_exports.literal("relay_intent"),
4775
- message_id: LowerCrockfordUlidSchema.optional(),
4776
- file_id: LowerCrockfordUlidSchema.optional(),
4777
- recipient_identity: NonEmptyStringSchema
4778
- };
4779
- RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
4780
- RelayResultShape = {
4781
- kind: external_exports.literal("relay_result"),
4782
- intent_record_id: NonEmptyStringSchema,
4783
- message_id: LowerCrockfordUlidSchema.optional(),
4784
- file_id: LowerCrockfordUlidSchema.optional(),
4785
- recipient_identity: NonEmptyStringSchema,
4786
- status: RelayResultStatusSchema,
4787
- wire_id: NonEmptyStringSchema.optional(),
4788
- metadata_wire_id: NonEmptyStringSchema.optional()
4789
4510
  };
4790
- FileShape = {
4791
- kind: external_exports.literal("file"),
4792
- file_id: LowerCrockfordUlidSchema,
4793
- author: AuthorSnapshotSchema,
4794
- author_alias: AuthorAliasSchema.optional(),
4795
- filename: FileNameSchema,
4796
- mime: FileMimeSchema,
4797
- size: external_exports.number().int().nonnegative().max(MAX_FILE_BYTES),
4798
- sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
4799
- data_base64: external_exports.string(),
4800
- recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
4801
- const seen = /* @__PURE__ */ new Set();
4802
- for (const [index, identity] of identities.entries()) {
4803
- if (seen.has(identity)) {
4804
- context.addIssue({
4805
- code: external_exports.ZodIssueCode.custom,
4806
- path: [index],
4807
- message: "recipient identities must be unique"
4808
- });
4809
- }
4810
- seen.add(identity);
4811
- }
4812
- }),
4813
- source_file_id: external_exports.number().int().nonnegative().safe(),
4814
- source_wire_id: NonEmptyStringSchema.optional(),
4815
- source_reply_to: ReplyReferenceSchema.optional()
4816
- };
4817
- MembershipIntentShape = {
4818
- kind: external_exports.literal("membership_intent"),
4819
- action: external_exports.enum(["remove"]),
4820
- participant_id: LowerCrockfordUlidSchema,
4821
- recipient_identity: NonEmptyStringSchema,
4822
- role: RoleSchema,
4823
- alias: NonEmptyStringSchema.optional(),
4824
- epoch: PositiveSafeIntegerSchema,
4825
- notify: external_exports.boolean()
4826
- };
4827
- MembershipResultShape = {
4828
- kind: external_exports.literal("membership_result"),
4829
- intent_record_id: NonEmptyStringSchema,
4830
- participant_id: LowerCrockfordUlidSchema,
4831
- status: RelayStatusSchema,
4832
- notified: external_exports.boolean(),
4833
- key_material_retained: external_exports.literal(true),
4834
- uncertain_after_restart: external_exports.literal(true).optional()
4835
- };
4836
- CloseNoticeIntentShape = {
4837
- kind: external_exports.literal("close_notice_intent"),
4838
- recipient_identity: NonEmptyStringSchema
4839
- };
4840
- CloseNoticeResultShape = {
4841
- kind: external_exports.literal("close_notice_result"),
4842
- intent_record_id: NonEmptyStringSchema,
4843
- recipient_identity: NonEmptyStringSchema,
4844
- status: RelayStatusSchema,
4845
- notified: external_exports.boolean(),
4846
- key_material_retained: external_exports.literal(true),
4847
- uncertain_after_restart: external_exports.literal(true).optional()
4848
- };
4849
- MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
4850
- FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
4851
- RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
4852
- RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
4853
- MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
4854
- MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
4855
- CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
4856
- CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
4857
- RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
4858
- MessageRecordSchema,
4859
- FileRecordSchema,
4860
- RelayIntentRecordSchema,
4861
- RelayResultRecordSchema,
4862
- MembershipIntentRecordSchema,
4863
- MembershipResultRecordSchema,
4864
- CloseNoticeIntentRecordSchema,
4865
- CloseNoticeResultRecordSchema
4866
- ]);
4867
- CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
4868
- if (record.record_id !== `${record.room_id}:${record.seq}`) {
4869
- context.addIssue({
4870
- code: external_exports.ZodIssueCode.custom,
4871
- path: ["record_id"],
4872
- message: 'record_id must equal room_id + ":" + seq'
4873
- });
4874
- }
4875
- if (record.kind === "message") refineMessageCategory(record, context);
4876
- refineRelaySubject(record, context);
4877
- refineFileRecord(record, context);
4878
- });
4879
- AppendRecordSchema = external_exports.discriminatedUnion("kind", [
4880
- external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
4881
- external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
4882
- external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
4883
- external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
4884
- external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
4885
- external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
4886
- external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
4887
- external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
4888
- ]).superRefine((record, context) => {
4889
- if (record.kind === "message") refineMessageCategory(record, context);
4890
- refineRelaySubject(record, context);
4891
- refineFileRecord(record, context);
4892
- });
4893
4511
  }
4894
4512
  });
4895
4513
 
4896
- // src/config.ts
4897
- import * as nodeFs from "node:fs";
4898
- import { homedir } from "node:os";
4899
- import { dirname, isAbsolute, join, parse, resolve } from "node:path";
4900
- function defaultConfig(home = homedir()) {
4901
- return {
4902
- version: 1,
4903
- stateDir: resolve(home, ".ours-cowork"),
4904
- roomIdentity: { nameMode: "stable_id" },
4905
- rest: { enabled: true, port: 3052 }
4906
- };
4907
- }
4908
- function loadConfig(env = process.env, io = {}) {
4909
- rejectRemovedEnvironment(env);
4910
- const fs2 = io.fs ?? nodeFs;
4911
- const defaults = defaultConfig(io.home);
4912
- const configPath = resolve(env.OURS_COWORK_CONFIG ?? join(io.home ?? homedir(), ".ours-cowork", "config.json"));
4913
- let file = defaults;
4914
- const stat = lstatIfPresent(fs2, configPath);
4915
- if (stat) {
4916
- assertSecureFile(fs2, configPath, "config file");
4917
- let parsed;
4918
- try {
4919
- parsed = JSON.parse(readSecureFile(fs2, configPath, "config file").toString("utf8"));
4920
- } catch (error) {
4921
- throw new CoworkConfigError(`malformed cowork config at ${configPath}`, { cause: error });
4922
- }
4923
- rejectRemovedConfig(parsed, configPath);
4924
- try {
4925
- file = CoworkConfigSchema.parse(parsed);
4926
- } catch (error) {
4927
- throw new CoworkConfigError(`invalid cowork config at ${configPath}`, { cause: error });
4928
- }
4929
- } else if (env.OURS_COWORK_CONFIG !== void 0) {
4930
- throw new CoworkConfigError(`configured cowork config does not exist: ${configPath}`);
4931
- }
4932
- const restPort = env.OURS_COWORK_REST_PORT === void 0 ? void 0 : parsePort(env.OURS_COWORK_REST_PORT);
4933
- try {
4934
- return CoworkConfigSchema.parse({
4935
- version: 1,
4936
- stateDir: resolve(env.OURS_COWORK_STATE_DIR ?? file.stateDir),
4937
- roomIdentity: file.roomIdentity,
4938
- rest: {
4939
- enabled: restPort === void 0 ? file.rest.enabled : true,
4940
- port: restPort ?? file.rest.port
4941
- }
4942
- });
4943
- } catch (error) {
4944
- throw new CoworkConfigError("invalid effective cowork config", { cause: error });
4945
- }
4946
- }
4947
- function rejectRemovedEnvironment(env) {
4948
- const removed = [
4949
- "OURS_COWORK_BROKER_URL",
4950
- "OURS_COWORK_DAEMON_MODE",
4951
- "OURS_COWORK_DAEMON_ENDPOINT",
4952
- "OURS_COWORK_DAEMON_STATE_DIR"
4953
- ].filter((name) => env[name] !== void 0);
4954
- if (removed.length === 0) return;
4955
- throw new CoworkConfigError(
4956
- `${removed.join(", ")} ${removed.length === 1 ? "was" : "were"} removed: ours-cowork now attaches only to the shared ours daemon. Configure that daemon with @ours.network/cli and select it through the standard OURS_CONFIG, OURS_PORT, and OURS_STATE_DIR inputs.`
4514
+ // src/contracts.ts
4515
+ import { createHash } from "node:crypto";
4516
+ function utf8Bounded(label, maximumBytes) {
4517
+ return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
4518
+ (value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
4519
+ `${label} must be at most ${maximumBytes} UTF-8 bytes`
4957
4520
  );
4958
4521
  }
4959
- function rejectRemovedConfig(value, path) {
4960
- if (value === null || typeof value !== "object" || Array.isArray(value)) return;
4961
- const removed = ["brokerUrl", "daemon"].filter((key) => Object.hasOwn(value, key));
4962
- if (removed.length === 0) return;
4963
- throw new CoworkConfigError(
4964
- `cowork config at ${path} contains removed ${removed.join(" and ")} ${removed.length === 1 ? "key" : "keys"}: ours-cowork now attaches only to the shared ours daemon. Remove those keys and configure the daemon with @ours.network/cli.`
4965
- );
4522
+ function isStrictRfc3339(value) {
4523
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
4524
+ if (!match) return false;
4525
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText] = match;
4526
+ const year = Number(yearText);
4527
+ const month = Number(monthText);
4528
+ const day = Number(dayText);
4529
+ const hour = Number(hourText);
4530
+ const minute = Number(minuteText);
4531
+ const second = Number(secondText);
4532
+ const offsetHour = offsetHourText === void 0 ? 0 : Number(offsetHourText);
4533
+ const offsetMinute = offsetMinuteText === void 0 ? 0 : Number(offsetMinuteText);
4534
+ if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
4535
+ if (offsetHour > 23 || offsetMinute > 59) return false;
4536
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
4537
+ const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
4538
+ return day >= 1 && day <= days[month - 1];
4966
4539
  }
4967
- function ensureRuntimeState(config, io = {}) {
4968
- const parsed = CoworkConfigSchema.parse(config);
4969
- const fs2 = io.fs ?? nodeFs;
4970
- const stateDir = resolve(parsed.stateDir);
4971
- assertSecureAncestors(fs2, stateDir, "state directory");
4972
- const existing = lstatIfPresent(fs2, stateDir);
4973
- if (!existing) {
4974
- createSecureDirectoryTree(fs2, stateDir, "state directory");
4975
- }
4976
- assertSecureDirectory(fs2, stateDir, "state directory");
4977
- const roomsPath = join(stateDir, "rooms");
4978
- const rooms = lstatIfPresent(fs2, roomsPath);
4979
- if (!rooms) {
4980
- fs2.mkdirSync(roomsPath, { mode: DIRECTORY_MODE });
4981
- secureOpenedDirectory(fs2, roomsPath, "rooms directory");
4982
- fsyncDirectory(fs2, stateDir);
4983
- }
4984
- assertSecureDirectory(fs2, roomsPath, "rooms directory");
4985
- return {
4986
- socketPath: join(stateDir, "management.sock"),
4987
- pidPath: join(stateDir, "daemon.pid"),
4988
- lockPath: join(stateDir, "daemon.lock")
4989
- };
4540
+ function normalizeRoomName(value) {
4541
+ return value.trim().normalize("NFC");
4990
4542
  }
4991
- function parsePort(value) {
4992
- if (!/^[1-9][0-9]{0,4}$/.test(value)) {
4993
- throw new CoworkConfigError("OURS_COWORK_REST_PORT must be a decimal port from 1 to 65535");
4994
- }
4995
- const port = Number(value);
4996
- if (port > 65535) throw new CoworkConfigError("OURS_COWORK_REST_PORT must be from 1 to 65535");
4997
- return port;
4543
+ function roomIdentityName(roomName) {
4544
+ return `${ROOM_IDENTITY_PREFIX}${RoomNameSchema.parse(roomName)}`;
4998
4545
  }
4999
- function assertSecureAncestors(fs2, path, label) {
5000
- const absolute = isAbsolute(path) ? path : resolve(path);
5001
- const root = parse(absolute).root;
5002
- const rootOwner = fs2.lstatSync(root).uid;
5003
- let cursor = root;
5004
- const components = absolute.slice(root.length).split("/").filter(Boolean);
5005
- for (const [index, component] of components.entries()) {
5006
- cursor = join(cursor, component);
5007
- const stat = lstatIfPresent(fs2, cursor);
5008
- if (stat?.isSymbolicLink()) throw new CoworkConfigError(`${label} must not traverse a symbolic link (symlink): ${cursor}`);
5009
- if (!stat) break;
5010
- if (!stat.isDirectory()) {
5011
- if (index === components.length - 1) return;
5012
- throw new CoworkConfigError(`${label} ancestor is not a directory: ${cursor}`);
5013
- }
5014
- if (index < components.length - 1) assertTrustedAncestor(stat, rootOwner, cursor, label);
4546
+ function isPersistedRoomIdentityName(roomId, identityName) {
4547
+ if (!LowerCrockfordUlidSchema.safeParse(roomId).success || !identityName.startsWith(ROOM_IDENTITY_PREFIX)) {
4548
+ return false;
5015
4549
  }
4550
+ const parsed = RoomNameSchema.safeParse(identityName.slice(ROOM_IDENTITY_PREFIX.length));
4551
+ return parsed.success && identityName === `${ROOM_IDENTITY_PREFIX}${parsed.data}`;
5016
4552
  }
5017
- function assertTrustedAncestor(stat, rootOwner, path, label) {
5018
- const uid = typeof process.getuid === "function" ? process.getuid() : stat.uid;
5019
- const trustedStickyDirectory = (stat.mode & 512) !== 0;
5020
- const writableByOthers = (stat.mode & 18) !== 0;
5021
- const trustedOwner = stat.uid === uid || stat.uid === 0 || stat.uid === rootOwner;
5022
- if (writableByOthers && (!trustedStickyDirectory || !trustedOwner)) {
5023
- throw new CoworkConfigError(`${label} has an unsafe writable ancestor: ${path}`);
5024
- }
4553
+ function isStandardRoomIdentityName(roomId, identityName) {
4554
+ return isPersistedRoomIdentityName(roomId, identityName);
5025
4555
  }
5026
- function createSecureDirectoryTree(fs2, path, label) {
5027
- const missing = [];
5028
- let cursor = path;
5029
- while (!lstatIfPresent(fs2, cursor)) {
5030
- missing.push(cursor);
5031
- const parent = dirname(cursor);
5032
- if (parent === cursor) throw new CoworkConfigError(`cannot locate an existing ancestor for ${label}`);
5033
- cursor = parent;
4556
+ function refineRoomLineage(room, context) {
4557
+ 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;
4558
+ if (room.identity_cid === "" && !exactPacketPending) {
4559
+ context.addIssue({
4560
+ code: external_exports.ZodIssueCode.custom,
4561
+ path: ["identity_cid"],
4562
+ message: "empty identity_cid is reserved for the exact packet_pending provisioning sentinel"
4563
+ });
5034
4564
  }
5035
- assertSecureAncestors(fs2, path, label);
5036
- for (const directory of missing.reverse()) {
5037
- fs2.mkdirSync(directory, { mode: DIRECTORY_MODE });
5038
- secureOpenedDirectory(fs2, directory, label);
5039
- fsyncDirectory(fs2, dirname(directory));
4565
+ if (room.identity_cid !== "" && room.status === "packet_pending") {
4566
+ context.addIssue({
4567
+ code: external_exports.ZodIssueCode.custom,
4568
+ path: ["status"],
4569
+ message: "packet_pending status requires an empty identity_cid"
4570
+ });
5040
4571
  }
5041
- }
5042
- function secureOpenedDirectory(fs2, path, label) {
5043
- let fd;
5044
- try {
5045
- fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
5046
- const opened = fs2.fstatSync(fd);
5047
- const current = fs2.lstatSync(path);
5048
- if (!opened.isDirectory() || current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino) {
5049
- throw new CoworkConfigError(`${label} changed while opening`);
4572
+ const pendingByRecovery = /* @__PURE__ */ new Map();
4573
+ for (const [index, invite] of room.invites.entries()) {
4574
+ if (invite.recovery_of === void 0) continue;
4575
+ const recoveryOf = invite.recovery_of;
4576
+ const source = room.invites.find((candidate) => candidate.invite_id === recoveryOf);
4577
+ const validSourceState = invite.state === "receipt_pending" ? source?.state === "replacement_required" : invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required" ? source?.state === "revoked" : invite.state === "revoked" ? invite.recovery_confirmed === true ? source?.state === "revoked" : source?.state === "replacement_required" || source?.state === "revoked" : false;
4578
+ if (!source || source.invite_id === invite.invite_id || !validSourceState) {
4579
+ context.addIssue({
4580
+ code: external_exports.ZodIssueCode.custom,
4581
+ path: ["invites", index, "recovery_of"],
4582
+ message: "recovery_of must point to a source invite in the state required by this recovery lineage"
4583
+ });
4584
+ } else if (invite.mode !== source.mode || invite.role !== source.role || invite.min_accepts !== source.min_accepts) {
4585
+ context.addIssue({
4586
+ code: external_exports.ZodIssueCode.custom,
4587
+ path: ["invites", index],
4588
+ message: "receipt_pending descriptor must copy source mode, role, and min_accepts"
4589
+ });
4590
+ }
4591
+ if (invite.state === "receipt_pending") {
4592
+ const count = (pendingByRecovery.get(recoveryOf) ?? 0) + 1;
4593
+ pendingByRecovery.set(recoveryOf, count);
4594
+ if (count > 1) {
4595
+ context.addIssue({
4596
+ code: external_exports.ZodIssueCode.custom,
4597
+ path: ["invites", index, "recovery_of"],
4598
+ message: "only one receipt_pending invite may exist per recovery_of pointer"
4599
+ });
4600
+ }
5050
4601
  }
5051
- fs2.fchmodSync(fd, DIRECTORY_MODE);
5052
- fs2.fsyncSync(fd);
5053
- } finally {
5054
- if (fd !== void 0) fs2.closeSync(fd);
5055
4602
  }
5056
4603
  }
5057
- function assertSecureDirectory(fs2, path, label) {
5058
- const stat = fs2.lstatSync(path);
5059
- if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
5060
- if (!stat.isDirectory()) throw new CoworkConfigError(`${label} must be a directory`);
5061
- if ((stat.mode & 511) !== DIRECTORY_MODE) {
5062
- throw new CoworkConfigError(`${label} mode must be 0700`);
5063
- }
5064
- assertOwner(stat, label);
4604
+ function defaultRoomName(roomId) {
4605
+ return `Room ${LowerCrockfordUlidSchema.parse(roomId).slice(0, 8)}`;
5065
4606
  }
5066
- function assertSecureFile(fs2, path, label) {
5067
- const stat = fs2.lstatSync(path);
5068
- if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
5069
- if (!stat.isFile() || stat.nlink !== 1) throw new CoworkConfigError(`${label} must be a single-link regular file`);
5070
- if ((stat.mode & 511) !== FILE_MODE) throw new CoworkConfigError(`${label} mode must be 0600`);
5071
- assertOwner(stat, label);
4607
+ function migrateRoomV1(room, mintParticipantId) {
4608
+ return RoomSchema.parse({
4609
+ ...room,
4610
+ version: 2,
4611
+ mission: { ...room.mission, briefing_version: 1 },
4612
+ role_briefings: {},
4613
+ rest_roles: [],
4614
+ anonymous: false,
4615
+ quiet_membership: false,
4616
+ membership_epoch: 0,
4617
+ seats: room.seats.map((seat) => ({
4618
+ ...seat,
4619
+ participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
4620
+ state: "active"
4621
+ }))
4622
+ });
5072
4623
  }
5073
- function assertOwner(stat, label) {
5074
- if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
5075
- throw new CoworkConfigError(`${label} must be owned by the current user`);
4624
+ function refineRelaySubject(record, context) {
4625
+ if (record.kind !== "relay_intent" && record.kind !== "relay_result") return;
4626
+ if (record.message_id === void 0 === (record.file_id === void 0)) {
4627
+ context.addIssue({
4628
+ code: external_exports.ZodIssueCode.custom,
4629
+ path: ["message_id"],
4630
+ message: "relay records require exactly one of message_id or file_id"
4631
+ });
5076
4632
  }
5077
4633
  }
5078
- function readSecureFile(fs2, path, label) {
5079
- let fd;
5080
- try {
5081
- fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
5082
- const opened = fs2.fstatSync(fd);
5083
- const current = fs2.lstatSync(path);
5084
- if (!opened.isFile() || opened.nlink !== 1 || (opened.mode & 511) !== FILE_MODE || typeof process.getuid === "function" && opened.uid !== process.getuid() || current.isSymbolicLink() || current.dev !== opened.dev || current.ino !== opened.ino) {
5085
- throw new CoworkConfigError(`${label} changed while opening`);
5086
- }
5087
- return fs2.readFileSync(fd);
5088
- } finally {
5089
- if (fd !== void 0) fs2.closeSync(fd);
4634
+ function refineFileRecord(record, context) {
4635
+ if (record.kind !== "file" || record.data_base64 === void 0) return;
4636
+ const bytes = Buffer.from(record.data_base64, "base64");
4637
+ if (bytes.toString("base64") !== record.data_base64) {
4638
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["data_base64"], message: "file bytes must use canonical base64" });
5090
4639
  }
5091
- }
5092
- function fsyncDirectory(fs2, path) {
5093
- let fd;
5094
- try {
5095
- fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
5096
- fs2.fsyncSync(fd);
5097
- } finally {
5098
- if (fd !== void 0) fs2.closeSync(fd);
4640
+ if (bytes.length !== record.size) {
4641
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["size"], message: "file size must match decoded bytes" });
5099
4642
  }
5100
- }
5101
- function lstatIfPresent(fs2, path) {
5102
- try {
5103
- return fs2.lstatSync(path);
5104
- } catch (error) {
5105
- if (error.code === "ENOENT") return void 0;
5106
- throw error;
4643
+ const digest = createHash("sha256").update(bytes).digest("hex");
4644
+ if (digest !== record.sha256) {
4645
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["sha256"], message: "file sha256 must match decoded bytes" });
5107
4646
  }
5108
4647
  }
5109
- var DIRECTORY_MODE, FILE_MODE, NO_FOLLOW, CoworkConfigSchema, CoworkConfigError;
5110
- var init_config = __esm({
5111
- "src/config.ts"() {
5112
- "use strict";
5113
- init_zod();
5114
- init_contracts();
5115
- DIRECTORY_MODE = 448;
5116
- FILE_MODE = 384;
5117
- NO_FOLLOW = nodeFs.constants.O_NOFOLLOW ?? 0;
5118
- CoworkConfigSchema = external_exports.object({
5119
- version: external_exports.literal(1),
5120
- stateDir: external_exports.string().min(1),
5121
- roomIdentity: external_exports.object({
5122
- nameMode: RoomIdentityNameModeSchema
5123
- }).strict().default({ nameMode: "stable_id" }),
5124
- rest: external_exports.object({
5125
- enabled: external_exports.boolean(),
5126
- port: external_exports.number().int().min(1).max(65535)
5127
- }).strict()
5128
- }).strict();
5129
- CoworkConfigError = class extends Error {
5130
- constructor(message, options) {
5131
- super(message, options);
5132
- this.name = "CoworkConfigError";
5133
- }
5134
- };
4648
+ function refineMessageCategory(message, context) {
4649
+ const requires = (field, present) => {
4650
+ if (present && message[field] === void 0) {
4651
+ context.addIssue({
4652
+ code: external_exports.ZodIssueCode.custom,
4653
+ path: [field],
4654
+ message: `${message.category} messages require ${field}`
4655
+ });
4656
+ }
4657
+ if (!present && message[field] !== void 0) {
4658
+ context.addIssue({
4659
+ code: external_exports.ZodIssueCode.custom,
4660
+ path: [field],
4661
+ message: `${field} is forbidden on ${message.category} messages`
4662
+ });
4663
+ }
4664
+ };
4665
+ requires("briefing_role", message.category === "role_briefing");
4666
+ requires("membership", message.category === "membership");
4667
+ if (message.category === "role_briefing" && message.briefing_version === void 0) {
4668
+ context.addIssue({
4669
+ code: external_exports.ZodIssueCode.custom,
4670
+ path: ["briefing_version"],
4671
+ message: "role_briefing messages require briefing_version"
4672
+ });
5135
4673
  }
5136
- });
5137
-
5138
- // src/ours-runtime.ts
5139
- import { randomBytes } from "node:crypto";
5140
- function createOursHost(_config, log = () => {
5141
- }) {
5142
- return new SharedOursHost(log);
5143
- }
5144
- function sleep(ms, signal) {
5145
- return new Promise((resolveSleep) => {
5146
- const timer = setTimeout(finish, ms);
5147
- signal.addEventListener("abort", finish, { once: true });
5148
- function finish() {
5149
- clearTimeout(timer);
5150
- signal.removeEventListener("abort", finish);
5151
- resolveSleep();
4674
+ if (message.category === "chat" || message.category === "membership") {
4675
+ if (message.briefing_version !== void 0) {
4676
+ context.addIssue({
4677
+ code: external_exports.ZodIssueCode.custom,
4678
+ path: ["briefing_version"],
4679
+ message: `briefing_version is forbidden on ${message.category} messages`
4680
+ });
5152
4681
  }
5153
- });
4682
+ }
5154
4683
  }
5155
- var WATCH_RETRY_MIN_MS, WATCH_RETRY_MAX_MS, STATE_RESYNC_INTERVAL_MS, attachSharedClient, SharedOursHost;
5156
- var init_ours_runtime = __esm({
5157
- "src/ours-runtime.ts"() {
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;
4685
+ var init_contracts = __esm({
4686
+ "src/contracts.ts"() {
5158
4687
  "use strict";
5159
- WATCH_RETRY_MIN_MS = 500;
5160
- WATCH_RETRY_MAX_MS = 3e4;
5161
- STATE_RESYNC_INTERVAL_MS = 2e3;
5162
- attachSharedClient = async (options) => {
5163
- const { attachOursClient } = await import("@ours.network/sdk");
5164
- return attachOursClient(options);
5165
- };
5166
- SharedOursHost = class {
5167
- log;
5168
- attach;
5169
- listeners = /* @__PURE__ */ new Set();
5170
- watchers = /* @__PURE__ */ new Map();
5171
- watchLeaseToken = `cowork-watch-${randomBytes(16).toString("hex")}`;
5172
- watchClient;
5173
- resyncTimer;
5174
- closed = false;
5175
- constructor(log = () => {
5176
- }, attach = attachSharedClient) {
5177
- this.log = log;
5178
- this.attach = attach;
5179
- }
5180
- async boot() {
5181
- if (this.watchClient) return;
5182
- if (this.closed) throw new Error("shared ours daemon host cannot restart in the same process");
5183
- this.watchClient = await this.attach({ leaseToken: this.watchLeaseToken });
4688
+ init_zod();
4689
+ MAX_TEXT_BYTES = 262144;
4690
+ MAX_FILE_BYTES = 2 * 1024 * 1024;
4691
+ MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
4692
+ MAX_MANAGEMENT_RESPONSE_BYTES = MAX_HISTORY_PAGE_BYTES + 1024 * 1024;
4693
+ MAX_EXTERNAL_INVITE_BYTES = 48 * 1024;
4694
+ MAX_FILE_NAME_BYTES = 255;
4695
+ MAX_MIME_BYTES = 255;
4696
+ MAX_ROLE_BYTES = 256;
4697
+ MAX_ROOM_NAME_CHARACTERS = 64;
4698
+ NonEmptyStringSchema = external_exports.string().min(1);
4699
+ PositiveSafeIntegerSchema = external_exports.number().int().positive().safe();
4700
+ LowerCrockfordUlidSchema = external_exports.string().regex(
4701
+ /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4702
+ "must be a 26-character lowercase Crockford ULID"
4703
+ );
4704
+ Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
4705
+ RoomNameSchema = external_exports.string().refine(
4706
+ (value) => !/[\p{Cc}\p{Cf}]/u.test(value),
4707
+ "room name must not contain Unicode control or format characters"
4708
+ ).transform(normalizeRoomName).superRefine((value, context) => {
4709
+ const length = Array.from(value).length;
4710
+ if (length < 1 || length > MAX_ROOM_NAME_CHARACTERS) {
4711
+ context.addIssue({
4712
+ code: external_exports.ZodIssueCode.custom,
4713
+ message: `room name must contain 1-${MAX_ROOM_NAME_CHARACTERS} Unicode characters after normalization`
4714
+ });
5184
4715
  }
5185
- async createClient(leaseToken = `cowork-${randomBytes(16).toString("hex")}`) {
5186
- if (!this.watchClient) throw new Error("shared ours daemon host is not booted");
5187
- return this.attach({ leaseToken });
4716
+ });
4717
+ ROOM_IDENTITY_PREFIX = "ours-cowork:";
4718
+ RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4719
+ ROOM_ROLE = "room";
4720
+ MissionTextSchema = utf8Bounded("mission text", MAX_TEXT_BYTES);
4721
+ MessageTextSchema = utf8Bounded("message text", MAX_TEXT_BYTES);
4722
+ FileNameSchema = utf8Bounded("file name", MAX_FILE_NAME_BYTES).refine((value) => value !== "." && value !== "..", "file name must not be a relative path token").refine((value) => !/[\x00/\\]/.test(value), "file name must be a single path-free name");
4723
+ FileMimeSchema = external_exports.string().refine(
4724
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_MIME_BYTES,
4725
+ `file MIME metadata must be at most ${MAX_MIME_BYTES} UTF-8 bytes`
4726
+ );
4727
+ RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
4728
+ SeatStateSchema = external_exports.enum(["pending", "active", "removed"]);
4729
+ InviteModeSchema = external_exports.enum(["one_time", "public"]);
4730
+ DEFAULT_ROLE = "Participant";
4731
+ InviteStateSchema = external_exports.enum([
4732
+ "live",
4733
+ "consumed",
4734
+ "revoked",
4735
+ "replacement_required",
4736
+ "receipt_pending"
4737
+ ]);
4738
+ RelayStatusSchema = external_exports.enum(["queued", "send_failed"]);
4739
+ SeatV1Schema = external_exports.object({
4740
+ identity: NonEmptyStringSchema,
4741
+ display_name: NonEmptyStringSchema,
4742
+ role: RoleSchema,
4743
+ invite_id: NonEmptyStringSchema,
4744
+ accepted_at: Rfc3339Schema
4745
+ }).strict();
4746
+ SeatSchema = external_exports.object({
4747
+ identity: NonEmptyStringSchema,
4748
+ display_name: NonEmptyStringSchema,
4749
+ role: RoleSchema,
4750
+ invite_id: NonEmptyStringSchema,
4751
+ accepted_at: Rfc3339Schema.optional(),
4752
+ requested_at: Rfc3339Schema.optional(),
4753
+ invite_sha256: external_exports.string().regex(/^[0-9a-f]{64}$/).optional(),
4754
+ participant_id: LowerCrockfordUlidSchema,
4755
+ state: SeatStateSchema,
4756
+ alias: NonEmptyStringSchema.optional(),
4757
+ removed_at: Rfc3339Schema.optional(),
4758
+ removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
4759
+ replaces_seat: LowerCrockfordUlidSchema.optional(),
4760
+ bounced_at: Rfc3339Schema.optional()
4761
+ }).strict().superRefine((seat, context) => {
4762
+ if (seat.state === "pending") {
4763
+ for (const field of ["requested_at", "invite_sha256"]) {
4764
+ if (seat[field] === void 0) {
4765
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `pending seats require ${field}` });
4766
+ }
4767
+ }
4768
+ for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at"]) {
4769
+ if (seat[field] !== void 0) {
4770
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `${field} is forbidden on pending seats` });
4771
+ }
4772
+ }
4773
+ } else if (seat.state === "removed") {
4774
+ if (seat.accepted_at === void 0 && seat.requested_at === void 0) {
4775
+ context.addIssue({
4776
+ code: external_exports.ZodIssueCode.custom,
4777
+ path: ["accepted_at"],
4778
+ message: "removed seats require accepted_at unless they are cancelled external admissions"
4779
+ });
4780
+ }
4781
+ if (seat.removed_at === void 0) {
4782
+ context.addIssue({
4783
+ code: external_exports.ZodIssueCode.custom,
4784
+ path: ["removed_at"],
4785
+ message: "removed seats require removed_at"
4786
+ });
4787
+ }
4788
+ if (seat.removed_epoch === void 0) {
4789
+ context.addIssue({
4790
+ code: external_exports.ZodIssueCode.custom,
4791
+ path: ["removed_epoch"],
4792
+ message: "removed seats require removed_epoch"
4793
+ });
4794
+ }
4795
+ } else {
4796
+ if (seat.accepted_at === void 0) {
4797
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["accepted_at"], message: "active seats require accepted_at" });
4798
+ }
4799
+ for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
4800
+ if (seat[field] !== void 0) {
4801
+ context.addIssue({
4802
+ code: external_exports.ZodIssueCode.custom,
4803
+ path: [field],
4804
+ message: `${field} is reserved for removed seats`
4805
+ });
4806
+ }
4807
+ }
5188
4808
  }
5189
- async listIdentityNames(localNames) {
5190
- if (!this.watchClient) throw new Error("shared ours daemon host is not booted");
5191
- const rows = await this.watchClient.identities();
5192
- return new Set(rows.flatMap((row) => localNames.has(row.name) ? [row.name] : []));
4809
+ if (seat.requested_at === void 0 !== (seat.invite_sha256 === void 0)) {
4810
+ context.addIssue({
4811
+ code: external_exports.ZodIssueCode.custom,
4812
+ path: ["invite_sha256"],
4813
+ message: "external admission metadata requires both requested_at and invite_sha256"
4814
+ });
5193
4815
  }
5194
- onIdentityNotify(listener) {
5195
- this.listeners.add(listener);
5196
- return () => this.listeners.delete(listener);
4816
+ });
4817
+ RoomInviteSchema = external_exports.object({
4818
+ invite_id: NonEmptyStringSchema,
4819
+ mode: InviteModeSchema,
4820
+ role: RoleSchema,
4821
+ min_accepts: PositiveSafeIntegerSchema,
4822
+ accepted_cids: external_exports.array(NonEmptyStringSchema),
4823
+ state: InviteStateSchema,
4824
+ recovery_of: NonEmptyStringSchema.optional(),
4825
+ recovery_confirmed: external_exports.boolean().optional(),
4826
+ created_at: Rfc3339Schema,
4827
+ replaces_seat: LowerCrockfordUlidSchema.optional()
4828
+ }).strict().superRefine((invite, context) => {
4829
+ if (invite.mode === "one_time" && invite.min_accepts !== 1) {
4830
+ context.addIssue({
4831
+ code: external_exports.ZodIssueCode.custom,
4832
+ path: ["min_accepts"],
4833
+ message: "one_time invites require min_accepts === 1"
4834
+ });
5197
4835
  }
5198
- trackIdentity(identityName) {
5199
- if (!this.watchClient) throw new Error("shared ours daemon host is not booted");
5200
- const existing = this.watchers.get(identityName);
5201
- if (existing) return () => this.untrack(identityName, existing);
5202
- const controller = new AbortController();
5203
- const watcher = { controller, work: Promise.resolve() };
5204
- watcher.work = this.follow(identityName, controller.signal);
5205
- this.watchers.set(identityName, watcher);
5206
- this.announce(identityName);
5207
- this.ensureStateResync();
5208
- return () => this.untrack(identityName, watcher);
4836
+ if (invite.state === "receipt_pending" && invite.recovery_of === void 0) {
4837
+ context.addIssue({
4838
+ code: external_exports.ZodIssueCode.custom,
4839
+ path: ["recovery_of"],
4840
+ message: "receipt_pending invites require recovery_of"
4841
+ });
5209
4842
  }
5210
- close() {
4843
+ if (invite.recovery_of === void 0 && invite.recovery_confirmed !== void 0) {
4844
+ context.addIssue({
4845
+ code: external_exports.ZodIssueCode.custom,
4846
+ path: ["recovery_confirmed"],
4847
+ message: "recovery_confirmed is forbidden without recovery_of"
4848
+ });
5211
4849
  }
5212
- async shutdown() {
5213
- if (this.closed) return { requiresProcessExit: false };
5214
- this.closed = true;
5215
- this.listeners.clear();
5216
- this.stopStateResync();
5217
- const watchers = [...this.watchers.values()];
5218
- this.watchers.clear();
5219
- for (const watcher of watchers) watcher.controller.abort();
5220
- await Promise.allSettled(watchers.map((watcher) => watcher.work));
5221
- const client = this.watchClient;
5222
- this.watchClient = void 0;
5223
- if (client) await client.releaseLease();
5224
- return { requiresProcessExit: false };
4850
+ if (invite.recovery_of !== void 0 && invite.recovery_confirmed === void 0) {
4851
+ context.addIssue({
4852
+ code: external_exports.ZodIssueCode.custom,
4853
+ path: ["recovery_confirmed"],
4854
+ message: "recovery_confirmed is required with recovery_of"
4855
+ });
5225
4856
  }
5226
- untrack(identityName, watcher) {
5227
- if (this.watchers.get(identityName) !== watcher) return;
5228
- this.watchers.delete(identityName);
5229
- watcher.controller.abort();
5230
- if (this.watchers.size === 0) this.stopStateResync();
4857
+ if (invite.state === "receipt_pending" && invite.recovery_confirmed !== false) {
4858
+ context.addIssue({
4859
+ code: external_exports.ZodIssueCode.custom,
4860
+ path: ["recovery_confirmed"],
4861
+ message: "receipt_pending recovery lineage must be unconfirmed"
4862
+ });
5231
4863
  }
5232
- ensureStateResync() {
5233
- if (this.resyncTimer) return;
5234
- this.resyncTimer = setInterval(() => {
5235
- for (const identityName of this.watchers.keys()) this.announce(identityName);
5236
- }, STATE_RESYNC_INTERVAL_MS);
5237
- this.resyncTimer.unref();
4864
+ if (invite.recovery_of !== void 0 && (invite.state === "live" || invite.state === "consumed" || invite.state === "replacement_required") && invite.recovery_confirmed !== true) {
4865
+ context.addIssue({
4866
+ code: external_exports.ZodIssueCode.custom,
4867
+ path: ["recovery_confirmed"],
4868
+ message: "live, consumed, and replacement_required recovery lineage must be confirmed"
4869
+ });
5238
4870
  }
5239
- stopStateResync() {
5240
- if (!this.resyncTimer) return;
5241
- clearInterval(this.resyncTimer);
5242
- this.resyncTimer = void 0;
4871
+ if (invite.state === "receipt_pending" && invite.accepted_cids.length > 0) {
4872
+ context.addIssue({
4873
+ code: external_exports.ZodIssueCode.custom,
4874
+ path: ["accepted_cids"],
4875
+ message: "receipt_pending invites cannot have accepted CIDs"
4876
+ });
5243
4877
  }
4878
+ });
4879
+ MissionV1Schema = external_exports.object({
4880
+ goal: MissionTextSchema,
4881
+ briefing: MissionTextSchema
4882
+ }).strict();
4883
+ MissionSchema = external_exports.object({
4884
+ goal: MissionTextSchema,
4885
+ briefing: MissionTextSchema,
4886
+ briefing_version: PositiveSafeIntegerSchema
4887
+ }).strict();
4888
+ RoleBriefingSchema = external_exports.object({
4889
+ text: MissionTextSchema,
4890
+ version: PositiveSafeIntegerSchema,
4891
+ updated_at: Rfc3339Schema
4892
+ }).strict();
4893
+ RoomCommonShape = {
4894
+ room_id: LowerCrockfordUlidSchema,
4895
+ identity_name: NonEmptyStringSchema,
4896
+ identity_cid: external_exports.string(),
4897
+ state: RoomStateSchema,
4898
+ status: NonEmptyStringSchema.optional(),
4899
+ invites: external_exports.array(RoomInviteSchema),
4900
+ created_at: Rfc3339Schema,
4901
+ activated_at: Rfc3339Schema.optional(),
4902
+ closed_at: Rfc3339Schema.optional()
4903
+ };
4904
+ RoomV1Schema = external_exports.object({
4905
+ ...RoomCommonShape,
4906
+ version: external_exports.literal(1),
4907
+ mission: MissionV1Schema,
4908
+ seats: external_exports.array(SeatV1Schema)
4909
+ }).strict().superRefine(refineRoomLineage);
4910
+ CurrentRoomSchema = external_exports.object({
4911
+ ...RoomCommonShape,
4912
+ room_name: RoomNameSchema,
4913
+ version: external_exports.literal(2),
4914
+ mission: MissionSchema,
4915
+ role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
5244
4916
  /**
5245
- * Long-poll one identity forever, reconnecting with bounded backoff.
5246
- *
5247
- * A replacement stream primes at the daemon tip, so each reconnection also
5248
- * requests a full state resync. The periodic resync above covers the small
5249
- * request-prime race and daemon transitions that are not in the structured
5250
- * notification log; reconciliation is idempotent and self-coalescing.
4917
+ * Roles a REST caller may author under. A plain array of names:
4918
+ * the role name IS the identifier, so there is nothing per-role to store.
4919
+ * Not a seat, not a membership: seat and membership invariants do not apply.
4920
+ * The registry itself is an exact-name set, and reserves `room` for the
4921
+ * room's own voice.
5251
4922
  */
5252
- async follow(identityName, signal) {
5253
- let backoffMs = WATCH_RETRY_MIN_MS;
5254
- let resyncPending = false;
5255
- while (!signal.aborted) {
5256
- const client = this.watchClient;
5257
- if (!client) return;
5258
- try {
5259
- const stream = client.watchNotifications(identityName, { signal });
5260
- let step = stream.next();
5261
- if (resyncPending) {
5262
- resyncPending = false;
5263
- this.announce(identityName);
5264
- }
5265
- for (let settled = await step; !settled.done; settled = await step) {
5266
- backoffMs = WATCH_RETRY_MIN_MS;
5267
- this.announce(identityName);
5268
- step = stream.next();
5269
- }
5270
- if (!signal.aborted) {
5271
- resyncPending = true;
5272
- this.log(`[${identityName}] shared ours daemon notification watch ended; reconnecting`);
5273
- }
5274
- } catch (error) {
5275
- if (signal.aborted) return;
5276
- resyncPending = true;
5277
- this.log(`[${identityName}] shared ours daemon notification watch failed:`, error);
4923
+ rest_roles: external_exports.array(RoleSchema).superRefine((roles, context) => {
4924
+ const seen = /* @__PURE__ */ new Set();
4925
+ for (const [index, role] of roles.entries()) {
4926
+ if (role === ROOM_ROLE) {
4927
+ context.addIssue({
4928
+ code: external_exports.ZodIssueCode.custom,
4929
+ path: [index],
4930
+ message: `role "${ROOM_ROLE}" is reserved for the room's own voice`
4931
+ });
5278
4932
  }
5279
- if (signal.aborted) return;
5280
- await sleep(backoffMs, signal);
5281
- backoffMs = Math.min(backoffMs * 2, WATCH_RETRY_MAX_MS);
4933
+ if (seen.has(role)) {
4934
+ context.addIssue({
4935
+ code: external_exports.ZodIssueCode.custom,
4936
+ path: [index],
4937
+ message: "REST role names must be unique within the room"
4938
+ });
4939
+ }
4940
+ seen.add(role);
4941
+ }
4942
+ }),
4943
+ anonymous: external_exports.boolean(),
4944
+ quiet_membership: external_exports.boolean(),
4945
+ membership_epoch: external_exports.number().int().nonnegative().safe(),
4946
+ seats: external_exports.array(SeatSchema)
4947
+ }).strict().superRefine((room, context) => {
4948
+ refineRoomLineage(room, context);
4949
+ const byParticipant = /* @__PURE__ */ new Map();
4950
+ const activeAliases = /* @__PURE__ */ new Set();
4951
+ const authorizedCids = /* @__PURE__ */ new Set();
4952
+ for (const [index, seat] of room.seats.entries()) {
4953
+ if (byParticipant.has(seat.participant_id)) {
4954
+ context.addIssue({
4955
+ code: external_exports.ZodIssueCode.custom,
4956
+ path: ["seats", index, "participant_id"],
4957
+ message: "participant_id must be unique within the room"
4958
+ });
4959
+ }
4960
+ byParticipant.set(seat.participant_id, seat);
4961
+ if (seat.state === "pending" || seat.state === "active") {
4962
+ if (authorizedCids.has(seat.identity)) {
4963
+ context.addIssue({
4964
+ code: external_exports.ZodIssueCode.custom,
4965
+ path: ["seats", index, "identity"],
4966
+ message: "at most one pending or active seat may exist per CID"
4967
+ });
4968
+ }
4969
+ authorizedCids.add(seat.identity);
4970
+ }
4971
+ if (room.anonymous && seat.alias === void 0) {
4972
+ context.addIssue({
4973
+ code: external_exports.ZodIssueCode.custom,
4974
+ path: ["seats", index, "alias"],
4975
+ message: "anonymous rooms require an alias on every seat"
4976
+ });
4977
+ }
4978
+ if (!room.anonymous && seat.alias !== void 0) {
4979
+ context.addIssue({
4980
+ code: external_exports.ZodIssueCode.custom,
4981
+ path: ["seats", index, "alias"],
4982
+ message: "aliases are reserved for anonymous rooms"
4983
+ });
4984
+ }
4985
+ if (seat.state === "active" && seat.alias !== void 0) {
4986
+ if (activeAliases.has(seat.alias)) {
4987
+ context.addIssue({
4988
+ code: external_exports.ZodIssueCode.custom,
4989
+ path: ["seats", index, "alias"],
4990
+ message: "active seats must hold distinct aliases"
4991
+ });
4992
+ }
4993
+ activeAliases.add(seat.alias);
4994
+ }
4995
+ if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
4996
+ context.addIssue({
4997
+ code: external_exports.ZodIssueCode.custom,
4998
+ path: ["seats", index, "removed_epoch"],
4999
+ message: "removed_epoch cannot exceed the room membership_epoch"
5000
+ });
5001
+ }
5002
+ }
5003
+ for (const [index, seat] of room.seats.entries()) {
5004
+ if (seat.replaces_seat === void 0) continue;
5005
+ const predecessor = byParticipant.get(seat.replaces_seat);
5006
+ if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
5007
+ context.addIssue({
5008
+ code: external_exports.ZodIssueCode.custom,
5009
+ path: ["seats", index, "replaces_seat"],
5010
+ message: "replaces_seat must reference a removed seat in this room"
5011
+ });
5012
+ continue;
5013
+ }
5014
+ if (predecessor.role !== seat.role) {
5015
+ context.addIssue({
5016
+ code: external_exports.ZodIssueCode.custom,
5017
+ path: ["seats", index, "role"],
5018
+ message: "a replacement seat must inherit the predecessor role"
5019
+ });
5020
+ }
5021
+ if (room.anonymous && seat.alias !== predecessor.alias) {
5022
+ context.addIssue({
5023
+ code: external_exports.ZodIssueCode.custom,
5024
+ path: ["seats", index, "alias"],
5025
+ message: "an anonymous replacement seat must inherit the predecessor alias"
5026
+ });
5027
+ }
5028
+ }
5029
+ });
5030
+ RoomSchema = external_exports.preprocess((value) => {
5031
+ if (typeof value !== "object" || value === null) return value;
5032
+ const patch = {};
5033
+ if (!Object.hasOwn(value, "room_name")) {
5034
+ const roomId = value.room_id;
5035
+ if (typeof roomId === "string" && LowerCrockfordUlidSchema.safeParse(roomId).success) {
5036
+ patch.room_name = defaultRoomName(roomId);
5037
+ }
5038
+ }
5039
+ if (!Object.hasOwn(value, "rest_roles")) patch.rest_roles = [];
5040
+ if (Object.keys(patch).length === 0) return value;
5041
+ return { ...value, ...patch };
5042
+ }, CurrentRoomSchema);
5043
+ CreateRoomInputSchema = external_exports.object({
5044
+ name: RoomNameSchema.optional(),
5045
+ goal: MissionTextSchema,
5046
+ briefing: MissionTextSchema,
5047
+ anonymous: external_exports.boolean().optional(),
5048
+ quiet_membership: external_exports.boolean().optional()
5049
+ }).strict();
5050
+ UpdateRoomInputSchema = external_exports.object({
5051
+ name: RoomNameSchema.optional(),
5052
+ goal: MissionTextSchema.optional(),
5053
+ briefing: MissionTextSchema.optional(),
5054
+ status: NonEmptyStringSchema.optional(),
5055
+ quiet_membership: external_exports.boolean().optional()
5056
+ }).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
5057
+ RoleBriefingSetInputSchema = external_exports.object({
5058
+ role: RoleSchema,
5059
+ text: MissionTextSchema
5060
+ }).strict();
5061
+ RoleBriefingDeleteInputSchema = external_exports.object({
5062
+ role: RoleSchema
5063
+ }).strict();
5064
+ PostMessageInputSchema = external_exports.object({
5065
+ text: MessageTextSchema
5066
+ }).strict();
5067
+ PostAsRoleInputSchema = external_exports.object({
5068
+ role: RoleSchema,
5069
+ text: MessageTextSchema
5070
+ }).strict();
5071
+ RestRoleInputSchema = external_exports.object({
5072
+ role: RoleSchema
5073
+ }).strict();
5074
+ ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
5075
+ AcceptExternalInviteInputSchema = external_exports.object({
5076
+ role: RoleSchema,
5077
+ invite: external_exports.string().refine(
5078
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
5079
+ `invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
5080
+ ),
5081
+ expected_cid: ContainerIdSchema.optional(),
5082
+ replaces_seat: LowerCrockfordUlidSchema.optional()
5083
+ }).strict();
5084
+ AuthorSnapshotSchema = external_exports.object({
5085
+ identity: NonEmptyStringSchema,
5086
+ display_name: NonEmptyStringSchema,
5087
+ role: RoleSchema
5088
+ }).strict();
5089
+ RecordCommonShape = {
5090
+ version: external_exports.literal(1),
5091
+ room_id: LowerCrockfordUlidSchema,
5092
+ seq: PositiveSafeIntegerSchema,
5093
+ record_id: NonEmptyStringSchema,
5094
+ at: Rfc3339Schema
5095
+ };
5096
+ AppendCommonShape = {
5097
+ version: external_exports.literal(1),
5098
+ room_id: LowerCrockfordUlidSchema,
5099
+ at: Rfc3339Schema
5100
+ };
5101
+ MembershipNoticeSchema = external_exports.object({
5102
+ action: external_exports.enum(["remove"]),
5103
+ alias: NonEmptyStringSchema.optional(),
5104
+ role: RoleSchema.optional(),
5105
+ epoch: external_exports.number().int().nonnegative().safe()
5106
+ }).strict();
5107
+ AuthorAliasSchema = external_exports.object({
5108
+ participant_id: LowerCrockfordUlidSchema,
5109
+ alias: NonEmptyStringSchema
5110
+ }).strict();
5111
+ ReplyReferenceSchema = external_exports.object({
5112
+ wire_id: NonEmptyStringSchema,
5113
+ sentence: PositiveSafeIntegerSchema.optional()
5114
+ }).strict();
5115
+ MessageShape = {
5116
+ kind: external_exports.literal("message"),
5117
+ message_id: LowerCrockfordUlidSchema,
5118
+ author: AuthorSnapshotSchema,
5119
+ author_alias: AuthorAliasSchema.optional(),
5120
+ category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
5121
+ briefing_role: RoleSchema.optional(),
5122
+ briefing_version: PositiveSafeIntegerSchema.optional(),
5123
+ membership: MembershipNoticeSchema.optional(),
5124
+ text: MessageTextSchema,
5125
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5126
+ const seen = /* @__PURE__ */ new Set();
5127
+ for (const [index, identity] of identities.entries()) {
5128
+ if (seen.has(identity)) {
5129
+ context.addIssue({
5130
+ code: external_exports.ZodIssueCode.custom,
5131
+ path: [index],
5132
+ message: "recipient identities must be unique"
5133
+ });
5134
+ }
5135
+ seen.add(identity);
5282
5136
  }
5283
- }
5284
- announce(identityName) {
5285
- for (const listener of this.listeners) {
5286
- try {
5287
- listener(identityName);
5288
- } catch (error) {
5289
- this.log(`cowork SDK notification listener failed for ${identityName}:`, error);
5137
+ }),
5138
+ source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5139
+ source_wire_id: NonEmptyStringSchema.optional(),
5140
+ source_reply_to: ReplyReferenceSchema.optional()
5141
+ };
5142
+ RelayIntentShape = {
5143
+ kind: external_exports.literal("relay_intent"),
5144
+ message_id: LowerCrockfordUlidSchema.optional(),
5145
+ file_id: LowerCrockfordUlidSchema.optional(),
5146
+ recipient_identity: NonEmptyStringSchema
5147
+ };
5148
+ RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
5149
+ RelayResultShape = {
5150
+ kind: external_exports.literal("relay_result"),
5151
+ intent_record_id: NonEmptyStringSchema,
5152
+ message_id: LowerCrockfordUlidSchema.optional(),
5153
+ file_id: LowerCrockfordUlidSchema.optional(),
5154
+ recipient_identity: NonEmptyStringSchema,
5155
+ status: RelayResultStatusSchema,
5156
+ wire_id: NonEmptyStringSchema.optional(),
5157
+ metadata_wire_id: NonEmptyStringSchema.optional()
5158
+ };
5159
+ FileShape = {
5160
+ kind: external_exports.literal("file"),
5161
+ file_id: LowerCrockfordUlidSchema,
5162
+ author: AuthorSnapshotSchema,
5163
+ author_alias: AuthorAliasSchema.optional(),
5164
+ filename: FileNameSchema,
5165
+ mime: FileMimeSchema,
5166
+ size: external_exports.number().int().nonnegative().max(MAX_FILE_BYTES),
5167
+ sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
5168
+ data_base64: external_exports.string(),
5169
+ recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
5170
+ const seen = /* @__PURE__ */ new Set();
5171
+ for (const [index, identity] of identities.entries()) {
5172
+ if (seen.has(identity)) {
5173
+ context.addIssue({
5174
+ code: external_exports.ZodIssueCode.custom,
5175
+ path: [index],
5176
+ message: "recipient identities must be unique"
5177
+ });
5290
5178
  }
5179
+ seen.add(identity);
5291
5180
  }
5292
- }
5181
+ }),
5182
+ source_file_id: external_exports.number().int().nonnegative().safe(),
5183
+ source_wire_id: NonEmptyStringSchema.optional(),
5184
+ source_reply_to: ReplyReferenceSchema.optional()
5185
+ };
5186
+ MembershipIntentShape = {
5187
+ kind: external_exports.literal("membership_intent"),
5188
+ action: external_exports.enum(["remove"]),
5189
+ participant_id: LowerCrockfordUlidSchema,
5190
+ recipient_identity: NonEmptyStringSchema,
5191
+ role: RoleSchema,
5192
+ alias: NonEmptyStringSchema.optional(),
5193
+ epoch: PositiveSafeIntegerSchema,
5194
+ notify: external_exports.boolean()
5195
+ };
5196
+ MembershipResultShape = {
5197
+ kind: external_exports.literal("membership_result"),
5198
+ intent_record_id: NonEmptyStringSchema,
5199
+ participant_id: LowerCrockfordUlidSchema,
5200
+ status: RelayStatusSchema,
5201
+ notified: external_exports.boolean(),
5202
+ key_material_retained: external_exports.literal(true),
5203
+ uncertain_after_restart: external_exports.literal(true).optional()
5204
+ };
5205
+ CloseNoticeIntentShape = {
5206
+ kind: external_exports.literal("close_notice_intent"),
5207
+ recipient_identity: NonEmptyStringSchema
5293
5208
  };
5209
+ CloseNoticeResultShape = {
5210
+ kind: external_exports.literal("close_notice_result"),
5211
+ intent_record_id: NonEmptyStringSchema,
5212
+ recipient_identity: NonEmptyStringSchema,
5213
+ status: RelayStatusSchema,
5214
+ notified: external_exports.boolean(),
5215
+ key_material_retained: external_exports.literal(true),
5216
+ uncertain_after_restart: external_exports.literal(true).optional()
5217
+ };
5218
+ MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
5219
+ FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
5220
+ RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5221
+ RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5222
+ MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
5223
+ MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
5224
+ CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
5225
+ CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
5226
+ RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5227
+ MessageRecordSchema,
5228
+ FileRecordSchema,
5229
+ RelayIntentRecordSchema,
5230
+ RelayResultRecordSchema,
5231
+ MembershipIntentRecordSchema,
5232
+ MembershipResultRecordSchema,
5233
+ CloseNoticeIntentRecordSchema,
5234
+ CloseNoticeResultRecordSchema
5235
+ ]);
5236
+ CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5237
+ if (record.record_id !== `${record.room_id}:${record.seq}`) {
5238
+ context.addIssue({
5239
+ code: external_exports.ZodIssueCode.custom,
5240
+ path: ["record_id"],
5241
+ message: 'record_id must equal room_id + ":" + seq'
5242
+ });
5243
+ }
5244
+ if (record.kind === "message") refineMessageCategory(record, context);
5245
+ refineRelaySubject(record, context);
5246
+ refineFileRecord(record, context);
5247
+ });
5248
+ AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5249
+ external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
5250
+ external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
5251
+ external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5252
+ external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5253
+ external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
5254
+ external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
5255
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
5256
+ external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
5257
+ ]).superRefine((record, context) => {
5258
+ if (record.kind === "message") refineMessageCategory(record, context);
5259
+ refineRelaySubject(record, context);
5260
+ refineFileRecord(record, context);
5261
+ });
5294
5262
  }
5295
5263
  });
5296
5264
 
@@ -5464,7 +5432,7 @@ var init_packets = __esm({
5464
5432
  get(roomId) {
5465
5433
  return this.packets.get(roomId);
5466
5434
  }
5467
- async create(roomId, identityName = `ours-cowork-${roomId}`, bio = `ours-cowork mission room ${roomId}`) {
5435
+ async create(roomId, identityName, bio = `ours-cowork mission room ${roomId}`) {
5468
5436
  validateRoomId(roomId);
5469
5437
  this.assertStandardIdentity(roomId, identityName);
5470
5438
  this.assertNoLegacyState(roomId);
@@ -5496,7 +5464,7 @@ var init_packets = __esm({
5496
5464
  throw new Error(`failed to provision standard SDK identity for room "${roomId}"`, { cause: error });
5497
5465
  }
5498
5466
  }
5499
- async restore(roomId, expectedCid, identityName = `ours-cowork-${roomId}`) {
5467
+ async restore(roomId, expectedCid, identityName) {
5500
5468
  validateRoomId(roomId);
5501
5469
  this.assertStandardIdentity(roomId, identityName);
5502
5470
  this.assertNoLegacyState(roomId);
@@ -5768,6 +5736,20 @@ function sameReply2(stored, observed) {
5768
5736
  if (stored === void 0 || observed == null) return stored === void 0 && observed == null;
5769
5737
  return stored.wire_id === observed.wire_id && stored.sentence === observed.sentence;
5770
5738
  }
5739
+ async function queryStore(store, roomId, options) {
5740
+ if (store.query) return store.query(roomId, options);
5741
+ let records = await store.read(roomId);
5742
+ records = records.filter((record) => {
5743
+ const value = record;
5744
+ return (options.kind === void 0 || record.kind === options.kind) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.sourceMsgId === void 0 || value.source_msg_id === options.sourceMsgId) && (options.sourceFileId === void 0 || value.source_file_id === options.sourceFileId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity);
5745
+ });
5746
+ if (options.unresolvedResultKind) {
5747
+ const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
5748
+ records = records.filter((record) => !completed.has(record.record_id));
5749
+ }
5750
+ if (options.descending) records.reverse();
5751
+ return records.slice(0, options.limit);
5752
+ }
5771
5753
  function canonicalValue(value) {
5772
5754
  if (Array.isArray(value)) return value.map(canonicalValue);
5773
5755
  if (value !== null && typeof value === "object") {
@@ -5795,13 +5777,14 @@ function wireKind(category) {
5795
5777
  return "room_msg";
5796
5778
  }
5797
5779
  }
5798
- var INTAKE_BATCH_SIZE, IntakePump;
5780
+ var JOURNAL_WORK_BATCH_SIZE, INTAKE_BATCH_SIZE, IntakePump;
5799
5781
  var init_intake = __esm({
5800
5782
  "src/intake.ts"() {
5801
5783
  "use strict";
5802
5784
  init_zod();
5803
5785
  init_contracts();
5804
5786
  init_ulid();
5787
+ JOURNAL_WORK_BATCH_SIZE = 64;
5805
5788
  INTAKE_BATCH_SIZE = 32;
5806
5789
  IntakePump = class {
5807
5790
  store;
@@ -5909,8 +5892,8 @@ var init_intake = __esm({
5909
5892
  await packet.acknowledgeFile(item);
5910
5893
  return;
5911
5894
  }
5912
- const records = await this.store.read(roomId);
5913
- let file = this.findSourceFile(records, item);
5895
+ const [storedFile] = await queryStore(this.store, roomId, { kind: "file", sourceFileId: item.file_id, limit: 1 });
5896
+ let file = this.findSourceFile(storedFile === void 0 ? [] : [storedFile], item);
5914
5897
  if (!file) {
5915
5898
  const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
5916
5899
  const bytes = Buffer.from(item.data);
@@ -5948,8 +5931,8 @@ var init_intake = __esm({
5948
5931
  if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
5949
5932
  return;
5950
5933
  }
5951
- const before = await this.store.read(roomId);
5952
- let message = this.findSourceMessage(before, item);
5934
+ const [storedMessage] = await queryStore(this.store, roomId, { kind: "message", sourceMsgId: item.msg_id, limit: 1 });
5935
+ let message = this.findSourceMessage(storedMessage === void 0 ? [] : [storedMessage], item);
5953
5936
  if (!message) {
5954
5937
  const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
5955
5938
  const appended = await this.store.append(roomId, {
@@ -6012,100 +5995,153 @@ var init_intake = __esm({
6012
5995
  }
6013
5996
  }
6014
5997
  async completeSnapshotIntents(roomId) {
6015
- const records = await this.store.read(roomId);
6016
- for (const message of records.filter(
6017
- (record) => record.kind === "message"
6018
- )) await this.completeMessageIntents(roomId, message);
6019
- for (const file of records.filter(
6020
- (record) => record.kind === "file"
6021
- )) await this.completeFileIntents(roomId, file);
5998
+ if (!this.store.recordsNeedingRelayIntents) {
5999
+ const records = await this.store.read(roomId);
6000
+ for (const message of records.filter(
6001
+ (record) => record.kind === "message"
6002
+ )) await this.completeMessageIntents(roomId, message);
6003
+ for (const file of records.filter(
6004
+ (record) => record.kind === "file"
6005
+ )) await this.completeFileIntents(roomId, file);
6006
+ return;
6007
+ }
6008
+ for (; ; ) {
6009
+ const records = await this.store.recordsNeedingRelayIntents(
6010
+ roomId,
6011
+ { limit: JOURNAL_WORK_BATCH_SIZE }
6012
+ );
6013
+ if (records.length === 0) return;
6014
+ for (const record of records) {
6015
+ if (record.kind === "message") await this.completeMessageIntents(roomId, record);
6016
+ else if (record.kind === "file") await this.completeFileIntents(roomId, record);
6017
+ }
6018
+ }
6022
6019
  }
6023
6020
  async completeFileIntents(roomId, file) {
6024
- const records = await this.store.read(roomId);
6025
- const intended = new Set(records.filter((record) => record.kind === "relay_intent" && record.file_id === file.file_id).map((intent) => intent.recipient_identity));
6021
+ if (this.store.relayRecipientsNeedingIntent) {
6022
+ for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, file.seq)) {
6023
+ await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
6024
+ }
6025
+ return;
6026
+ }
6027
+ const records = await queryStore(this.store, roomId, { kind: "relay_intent", fileId: file.file_id });
6028
+ const intended = new Set(records.map((record) => record.recipient_identity));
6026
6029
  for (const recipientIdentity of file.recipient_identities) {
6027
6030
  if (intended.has(recipientIdentity)) continue;
6028
- await this.store.append(roomId, {
6029
- version: 1,
6030
- kind: "relay_intent",
6031
- room_id: roomId,
6032
- at: this.now(),
6033
- file_id: file.file_id,
6034
- recipient_identity: recipientIdentity
6035
- });
6031
+ await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
6036
6032
  intended.add(recipientIdentity);
6037
6033
  }
6038
6034
  }
6035
+ async appendFileIntent(roomId, fileId, recipientIdentity) {
6036
+ await this.store.append(roomId, {
6037
+ version: 1,
6038
+ kind: "relay_intent",
6039
+ room_id: roomId,
6040
+ at: this.now(),
6041
+ file_id: fileId,
6042
+ recipient_identity: recipientIdentity
6043
+ });
6044
+ }
6039
6045
  async completeMessageIntents(roomId, message) {
6040
- const records = await this.store.read(roomId);
6041
- const intended = new Set(records.filter((record) => record.kind === "relay_intent" && record.message_id === message.message_id).map((intent) => intent.recipient_identity));
6046
+ if (this.store.relayRecipientsNeedingIntent) {
6047
+ for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, message.seq)) {
6048
+ await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
6049
+ }
6050
+ return;
6051
+ }
6052
+ const records = await queryStore(this.store, roomId, { kind: "relay_intent", messageId: message.message_id });
6053
+ const intended = new Set(records.map((record) => record.recipient_identity));
6042
6054
  for (const recipientIdentity of message.recipient_identities) {
6043
6055
  if (intended.has(recipientIdentity)) continue;
6044
- await this.store.append(roomId, {
6045
- version: 1,
6046
- kind: "relay_intent",
6047
- room_id: roomId,
6048
- at: this.now(),
6049
- message_id: message.message_id,
6050
- recipient_identity: recipientIdentity
6051
- });
6056
+ await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
6052
6057
  intended.add(recipientIdentity);
6053
6058
  }
6054
6059
  }
6060
+ async appendMessageIntent(roomId, messageId, recipientIdentity) {
6061
+ await this.store.append(roomId, {
6062
+ version: 1,
6063
+ kind: "relay_intent",
6064
+ room_id: roomId,
6065
+ at: this.now(),
6066
+ message_id: messageId,
6067
+ recipient_identity: recipientIdentity
6068
+ });
6069
+ }
6055
6070
  async relayPendingUnlocked(roomId, packet) {
6056
- const records = await this.store.read(roomId);
6057
6071
  const room = await this.store.load(roomId);
6058
6072
  const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
6059
6073
  const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
6060
- const messages = new Map(records.filter((record) => record.kind === "message").map((message) => [message.message_id, message]));
6061
- const files = new Map(records.filter((record) => record.kind === "file").map((file) => [file.file_id, file]));
6062
- const completed = new Set(records.filter((record) => record.kind === "relay_result").map((result) => result.kind === "relay_result" ? result.intent_record_id : ""));
6063
- for (const intent of records.filter(
6064
- (record) => record.kind === "relay_intent"
6065
- )) {
6066
- if (completed.has(intent.record_id)) continue;
6067
- const message = intent.message_id === void 0 ? void 0 : messages.get(intent.message_id);
6068
- const file = intent.file_id === void 0 ? void 0 : files.get(intent.file_id);
6069
- if (message === void 0 === (file === void 0)) continue;
6070
- const recipients = message?.recipient_identities ?? file.recipient_identities;
6071
- if (!recipients.includes(intent.recipient_identity)) continue;
6072
- if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
6073
- const skipped = await this.store.append(roomId, {
6074
- version: 1,
6075
- kind: "relay_result",
6076
- room_id: roomId,
6077
- at: this.now(),
6078
- intent_record_id: intent.record_id,
6079
- ...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
6080
- ...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
6081
- recipient_identity: intent.recipient_identity,
6082
- status: "skipped_removed"
6083
- });
6084
- if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6085
- completed.add(intent.record_id);
6086
- continue;
6087
- }
6088
- if (file !== void 0) {
6089
- const author = file.author_alias === void 0 ? file.author : {
6090
- identity: file.author_alias.participant_id,
6091
- display_name: file.author_alias.alias,
6092
- role: file.author.role
6093
- };
6094
- const metadata = await sendRoomBody(packet, intent.recipient_identity, {
6095
- version: 1,
6096
- kind: "room_file",
6097
- room_id: roomId,
6098
- room_name: room.room_name,
6099
- file_id: file.file_id,
6100
- author,
6101
- filename: file.filename,
6102
- mime: file.mime,
6103
- size: file.size,
6104
- sha256: file.sha256,
6105
- at: file.at
6106
- });
6107
- if (metadata.status === "send_failed") {
6108
- const failed = await this.store.append(roomId, {
6074
+ let after = 0;
6075
+ for (; ; ) {
6076
+ const pending = await queryStore(this.store, roomId, {
6077
+ kind: "relay_intent",
6078
+ unresolvedResultKind: "relay_result",
6079
+ after,
6080
+ limit: JOURNAL_WORK_BATCH_SIZE
6081
+ });
6082
+ if (pending.length === 0) return;
6083
+ for (const intent of pending) {
6084
+ after = intent.seq;
6085
+ const [message] = intent.message_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "message", messageId: intent.message_id, limit: 1 });
6086
+ const [file] = intent.file_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "file", fileId: intent.file_id, limit: 1 });
6087
+ if (message === void 0 === (file === void 0)) continue;
6088
+ const recipients = message?.recipient_identities ?? file.recipient_identities;
6089
+ if (!recipients.includes(intent.recipient_identity)) continue;
6090
+ if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
6091
+ const skipped = await this.store.append(roomId, {
6092
+ version: 1,
6093
+ kind: "relay_result",
6094
+ room_id: roomId,
6095
+ at: this.now(),
6096
+ intent_record_id: intent.record_id,
6097
+ ...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
6098
+ ...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
6099
+ recipient_identity: intent.recipient_identity,
6100
+ status: "skipped_removed"
6101
+ });
6102
+ if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6103
+ continue;
6104
+ }
6105
+ if (file !== void 0) {
6106
+ const author = file.author_alias === void 0 ? file.author : {
6107
+ identity: file.author_alias.participant_id,
6108
+ display_name: file.author_alias.alias,
6109
+ role: file.author.role
6110
+ };
6111
+ const metadata = await sendRoomBody(packet, intent.recipient_identity, {
6112
+ version: 1,
6113
+ kind: "room_file",
6114
+ room_id: roomId,
6115
+ room_name: room.room_name,
6116
+ file_id: file.file_id,
6117
+ author,
6118
+ filename: file.filename,
6119
+ mime: file.mime,
6120
+ size: file.size,
6121
+ sha256: file.sha256,
6122
+ at: file.at
6123
+ });
6124
+ if (metadata.status === "send_failed") {
6125
+ const failed = await this.store.append(roomId, {
6126
+ version: 1,
6127
+ kind: "relay_result",
6128
+ room_id: roomId,
6129
+ at: this.now(),
6130
+ intent_record_id: intent.record_id,
6131
+ file_id: file.file_id,
6132
+ recipient_identity: intent.recipient_identity,
6133
+ status: "send_failed"
6134
+ });
6135
+ if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6136
+ continue;
6137
+ }
6138
+ const outcome2 = await packet.sendFile(
6139
+ intent.recipient_identity,
6140
+ file.filename,
6141
+ file.mime,
6142
+ Buffer.from(file.data_base64, "base64")
6143
+ );
6144
+ const appended2 = await this.store.append(roomId, {
6109
6145
  version: 1,
6110
6146
  kind: "relay_result",
6111
6147
  room_id: roomId,
@@ -6113,66 +6149,45 @@ var init_intake = __esm({
6113
6149
  intent_record_id: intent.record_id,
6114
6150
  file_id: file.file_id,
6115
6151
  recipient_identity: intent.recipient_identity,
6116
- status: "send_failed"
6152
+ status: outcome2.status,
6153
+ ...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
6154
+ ...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
6117
6155
  });
6118
- if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6119
- completed.add(intent.record_id);
6156
+ if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6120
6157
  continue;
6121
6158
  }
6122
- const outcome2 = await packet.sendFile(
6123
- intent.recipient_identity,
6124
- file.filename,
6125
- file.mime,
6126
- Buffer.from(file.data_base64, "base64")
6127
- );
6128
- const appended2 = await this.store.append(roomId, {
6159
+ const unsigned = {
6160
+ version: 1,
6161
+ kind: wireKind(message.category),
6162
+ room_id: roomId,
6163
+ room_name: room.room_name,
6164
+ message_id: message.message_id,
6165
+ // An anonymous author leaves the archive only in alias form.
6166
+ author: message.author_alias === void 0 ? message.author : {
6167
+ identity: message.author_alias.participant_id,
6168
+ display_name: message.author_alias.alias,
6169
+ role: message.author.role
6170
+ },
6171
+ text: message.text,
6172
+ at: message.at,
6173
+ ...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
6174
+ ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
6175
+ ...message.membership === void 0 ? {} : { membership: message.membership }
6176
+ };
6177
+ const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
6178
+ const appended = await this.store.append(roomId, {
6129
6179
  version: 1,
6130
6180
  kind: "relay_result",
6131
6181
  room_id: roomId,
6132
6182
  at: this.now(),
6133
6183
  intent_record_id: intent.record_id,
6134
- file_id: file.file_id,
6184
+ message_id: intent.message_id,
6135
6185
  recipient_identity: intent.recipient_identity,
6136
- status: outcome2.status,
6137
- ...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
6138
- ...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
6186
+ status: outcome.status,
6187
+ ...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
6139
6188
  });
6140
- if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6141
- completed.add(intent.record_id);
6142
- continue;
6189
+ if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6143
6190
  }
6144
- const unsigned = {
6145
- version: 1,
6146
- kind: wireKind(message.category),
6147
- room_id: roomId,
6148
- room_name: room.room_name,
6149
- message_id: message.message_id,
6150
- // An anonymous author leaves the archive only in alias form.
6151
- author: message.author_alias === void 0 ? message.author : {
6152
- identity: message.author_alias.participant_id,
6153
- display_name: message.author_alias.alias,
6154
- role: message.author.role
6155
- },
6156
- text: message.text,
6157
- at: message.at,
6158
- ...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
6159
- ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
6160
- ...message.membership === void 0 ? {} : { membership: message.membership }
6161
- };
6162
- const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
6163
- const appended = await this.store.append(roomId, {
6164
- version: 1,
6165
- kind: "relay_result",
6166
- room_id: roomId,
6167
- at: this.now(),
6168
- intent_record_id: intent.record_id,
6169
- message_id: intent.message_id,
6170
- recipient_identity: intent.recipient_identity,
6171
- status: outcome.status,
6172
- ...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
6173
- });
6174
- if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6175
- completed.add(intent.record_id);
6176
6191
  }
6177
6192
  }
6178
6193
  findSourceMessage(records, item) {
@@ -6231,6 +6246,21 @@ function byteBoundedHistoryPage(records) {
6231
6246
  function activeSeats(room) {
6232
6247
  return room.seats.filter((seat) => seat.state === "active");
6233
6248
  }
6249
+ async function queryStore2(store, roomId, options) {
6250
+ if (store.query) return store.query(roomId, options);
6251
+ let records = await store.read(roomId);
6252
+ records = records.filter((record) => {
6253
+ const value = record;
6254
+ const membership = value.membership;
6255
+ return (options.kind === void 0 || record.kind === options.kind) && (options.after === void 0 || record.seq > options.after) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.intentRecordId === void 0 || value.intent_record_id === options.intentRecordId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity) && (options.category === void 0 || value.category === options.category) && (options.membershipEpoch === void 0 || membership?.epoch === options.membershipEpoch);
6256
+ });
6257
+ if (options.unresolvedResultKind) {
6258
+ const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
6259
+ records = records.filter((record) => !completed.has(record.record_id));
6260
+ }
6261
+ if (options.descending) records.reverse();
6262
+ return records.slice(0, options.limit ?? Number.MAX_SAFE_INTEGER);
6263
+ }
6234
6264
  function isCancelledExternalSeat(seat) {
6235
6265
  return seat.state === "removed" && seat.accepted_at === void 0 && seat.requested_at !== void 0 && seat.invite_sha256 !== void 0;
6236
6266
  }
@@ -6243,7 +6273,7 @@ function uniqueIdentities(identities) {
6243
6273
  function currentContactIdentities(packet) {
6244
6274
  return new Set(packet.listContacts().map((contact) => contact.container_id));
6245
6275
  }
6246
- var CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
6276
+ var CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
6247
6277
  var init_service = __esm({
6248
6278
  "src/service.ts"() {
6249
6279
  "use strict";
@@ -6270,6 +6300,7 @@ var init_service = __esm({
6270
6300
  limit: external_exports.number().int().positive().safe().optional(),
6271
6301
  view: external_exports.enum(["operator", "participant"]).optional()
6272
6302
  }).strict();
6303
+ JOURNAL_WORK_BATCH_SIZE2 = 64;
6273
6304
  DeleteRoomInputSchema = external_exports.object({
6274
6305
  confirm: external_exports.literal(true)
6275
6306
  }).strict();
@@ -6297,7 +6328,6 @@ var init_service = __esm({
6297
6328
  nextMessageId;
6298
6329
  intake;
6299
6330
  provisioningCheckpoint;
6300
- identityNameMode;
6301
6331
  constructor(store, packets, options = {}) {
6302
6332
  this.store = store;
6303
6333
  this.packets = packets;
@@ -6306,7 +6336,6 @@ var init_service = __esm({
6306
6336
  this.nextMessageId = options.messageId ?? generateUlid;
6307
6337
  this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
6308
6338
  });
6309
- this.identityNameMode = options.identityNameMode ?? "stable_id";
6310
6339
  this.intake = new IntakePump(store, packets, {
6311
6340
  now: this.nowValue,
6312
6341
  messageId: this.nextMessageId
@@ -6316,7 +6345,7 @@ var init_service = __esm({
6316
6345
  const settings = CreateRoomInputSchema.parse(input);
6317
6346
  const roomId = LowerCrockfordUlidSchema.parse(this.nextRoomId());
6318
6347
  const roomName = settings.name ?? defaultRoomName(roomId);
6319
- const identityName = configuredRoomIdentityName(roomId, roomName, this.identityNameMode);
6348
+ const identityName = roomIdentityName(roomName);
6320
6349
  return this.lock(roomId, async () => {
6321
6350
  const provisional = RoomSchema.parse({
6322
6351
  version: 2,
@@ -6754,8 +6783,11 @@ var init_service = __esm({
6754
6783
  membership_epoch: Math.max(current.membership_epoch, intent.epoch)
6755
6784
  }));
6756
6785
  }
6757
- const records = await this.store.read(current.room_id);
6758
- const existing = records.find((record) => record.kind === "membership_result" && record.intent_record_id === intent.record_id);
6786
+ const [existing] = await queryStore2(this.store, current.room_id, {
6787
+ kind: "membership_result",
6788
+ intentRecordId: intent.record_id,
6789
+ limit: 1
6790
+ });
6759
6791
  let outcome;
6760
6792
  if (existing !== void 0 && existing.kind === "membership_result") {
6761
6793
  outcome = {
@@ -6794,8 +6826,13 @@ var init_service = __esm({
6794
6826
  };
6795
6827
  }
6796
6828
  async ensureMembershipNotice(room, intent) {
6797
- const records = await this.store.read(room.room_id);
6798
- const already = records.some((record) => record.kind === "message" && record.category === "membership" && record.membership?.action === "remove" && record.membership.epoch === intent.epoch);
6829
+ const records = await queryStore2(this.store, room.room_id, {
6830
+ kind: "message",
6831
+ category: "membership",
6832
+ membershipEpoch: intent.epoch,
6833
+ limit: 1
6834
+ });
6835
+ const already = records.length > 0;
6799
6836
  if (already) return;
6800
6837
  const remaining = activeSeats(room);
6801
6838
  if (remaining.length === 0) return;
@@ -7005,7 +7042,7 @@ var init_service = __esm({
7005
7042
  async history(roomId, options = {}) {
7006
7043
  const id = LowerCrockfordUlidSchema.parse(roomId);
7007
7044
  const { view, ...page } = HistoryOptionsSchema.parse(options);
7008
- const records = await this.store.read(id, view === "participant" ? { after: page.after } : page);
7045
+ const records = view === "participant" ? await queryStore2(this.store, id, { kind: "message", after: page.after, limit: page.limit }) : await this.store.read(id, page);
7009
7046
  if (view !== "participant") return byteBoundedHistoryPage(records);
7010
7047
  const projected = records.filter((record) => record.kind === "message").map((record) => {
7011
7048
  const {
@@ -7272,13 +7309,19 @@ var init_service = __esm({
7272
7309
  invites,
7273
7310
  membership_epoch: room.membership_epoch + activatedPending.length + newSeats.length
7274
7311
  });
7275
- const journal = await this.store.read(next.room_id);
7276
- const completedIntents = new Set(journal.filter((record) => record.kind === "membership_result").map((record) => record.kind === "membership_result" ? record.intent_record_id : ""));
7277
- for (const intent of journal.filter(
7278
- (record) => record.kind === "membership_intent"
7279
- )) {
7280
- if (completedIntents.has(intent.record_id)) continue;
7281
- ({ room: next } = await this.completeRemovalUnlocked(next, intent));
7312
+ let membershipAfter = 0;
7313
+ for (; ; ) {
7314
+ const journal = await queryStore2(this.store, next.room_id, {
7315
+ kind: "membership_intent",
7316
+ unresolvedResultKind: "membership_result",
7317
+ after: membershipAfter,
7318
+ limit: JOURNAL_WORK_BATCH_SIZE2
7319
+ });
7320
+ if (journal.length === 0) break;
7321
+ for (const intent of journal) {
7322
+ membershipAfter = intent.seq;
7323
+ ({ room: next } = await this.completeRemovalUnlocked(next, intent));
7324
+ }
7282
7325
  }
7283
7326
  const requirementsMet = invites.filter((invite) => invite.state !== "revoked").every((invite) => invite.accepted_cids.length >= invite.min_accepts);
7284
7327
  const admitted = [...activatedPending, ...newSeats];
@@ -7295,23 +7338,31 @@ var init_service = __esm({
7295
7338
  const packet = this.packets.get(roomId);
7296
7339
  if (packet) {
7297
7340
  await packet.refreshContacts();
7298
- let records = await this.store.read(roomId);
7299
7341
  let contacts = currentContactIdentities(packet);
7300
- const completed = new Set(records.filter((record) => record.kind === "close_notice_result").map((record) => record.kind === "close_notice_result" ? record.intent_record_id : ""));
7301
- const pending = records.filter(
7302
- (record) => record.kind === "close_notice_intent" && !completed.has(record.record_id)
7303
- );
7304
- for (const intent of pending) {
7305
- if (contacts.has(intent.recipient_identity)) continue;
7306
- await this.appendUncertainCloseResult(roomId, intent);
7307
- completed.add(intent.record_id);
7342
+ let closeAfter = 0;
7343
+ for (; ; ) {
7344
+ const pending = await queryStore2(this.store, roomId, {
7345
+ kind: "close_notice_intent",
7346
+ unresolvedResultKind: "close_notice_result",
7347
+ after: closeAfter,
7348
+ limit: JOURNAL_WORK_BATCH_SIZE2
7349
+ });
7350
+ if (pending.length === 0) break;
7351
+ for (const intent of pending) {
7352
+ closeAfter = intent.seq;
7353
+ if (contacts.has(intent.recipient_identity)) continue;
7354
+ await this.appendUncertainCloseResult(roomId, intent);
7355
+ }
7308
7356
  }
7309
7357
  for (const recipientIdentity of contacts) {
7310
- records = await this.store.read(roomId);
7311
- const currentCompleted = new Set(records.filter((record) => record.kind === "close_notice_result").map((record) => record.kind === "close_notice_result" ? record.intent_record_id : ""));
7312
- let intent = [...records].reverse().find(
7313
- (record) => record.kind === "close_notice_intent" && record.recipient_identity === recipientIdentity && !currentCompleted.has(record.record_id)
7314
- );
7358
+ const [existingIntent] = await queryStore2(this.store, roomId, {
7359
+ kind: "close_notice_intent",
7360
+ recipientIdentity,
7361
+ unresolvedResultKind: "close_notice_result",
7362
+ descending: true,
7363
+ limit: 1
7364
+ });
7365
+ let intent = existingIntent;
7315
7366
  if (!intent) {
7316
7367
  const appended2 = await this.store.append(roomId, {
7317
7368
  version: 1,
@@ -7354,13 +7405,19 @@ var init_service = __esm({
7354
7405
  `room "${roomId}" live-state purge left residue: ${residue.join(", ") || "packet registry entry"}`
7355
7406
  );
7356
7407
  }
7357
- const afterPurge = await this.store.read(roomId);
7358
- const completedAfterPurge = new Set(afterPurge.filter((record) => record.kind === "close_notice_result").map((record) => record.kind === "close_notice_result" ? record.intent_record_id : ""));
7359
- for (const intent of afterPurge.filter(
7360
- (record) => record.kind === "close_notice_intent" && !completedAfterPurge.has(record.record_id)
7361
- )) {
7362
- await this.appendUncertainCloseResult(roomId, intent);
7363
- completedAfterPurge.add(intent.record_id);
7408
+ let purgeAfter = 0;
7409
+ for (; ; ) {
7410
+ const afterPurge = await queryStore2(this.store, roomId, {
7411
+ kind: "close_notice_intent",
7412
+ unresolvedResultKind: "close_notice_result",
7413
+ after: purgeAfter,
7414
+ limit: JOURNAL_WORK_BATCH_SIZE2
7415
+ });
7416
+ if (afterPurge.length === 0) break;
7417
+ for (const intent of afterPurge) {
7418
+ purgeAfter = intent.seq;
7419
+ await this.appendUncertainCloseResult(roomId, intent);
7420
+ }
7364
7421
  }
7365
7422
  return this.store.save(RoomSchema.parse({ ...room, state: "closed", closed_at: this.now() }));
7366
7423
  }
@@ -7420,18 +7477,29 @@ var init_service = __esm({
7420
7477
  });
7421
7478
  }
7422
7479
  async ensureBriefingKind(room, recipients, briefing) {
7423
- const records = await this.store.read(room.room_id);
7480
+ if (this.store.briefingDeliveryTimes) {
7481
+ const recipientIdentities = uniqueIdentities(recipients.map((seat) => seat.identity));
7482
+ const deliveries = await this.store.briefingDeliveryTimes(room.room_id, {
7483
+ category: briefing.category,
7484
+ briefingRole: briefing.briefing_role,
7485
+ briefingVersion: briefing.briefing_version
7486
+ }, recipientIdentities);
7487
+ const missing2 = recipients.filter((seat) => !deliveries.has(seat.identity));
7488
+ const appendedAt2 = await this.appendBriefingForMissing(room, missing2, briefing);
7489
+ return [...deliveries.values(), ...appendedAt2 === void 0 ? [] : [appendedAt2]].sort()[0] ?? this.now();
7490
+ }
7491
+ const records = await queryStore2(this.store, room.room_id, {
7492
+ kind: "message",
7493
+ category: briefing.category
7494
+ });
7424
7495
  const matching = records.filter((record) => record.kind === "message" && record.category === briefing.category && record.briefing_role === briefing.briefing_role && (record.briefing_version ?? 1) === briefing.briefing_version);
7425
- const intentsByMessage = /* @__PURE__ */ new Map();
7426
- for (const record of records) {
7427
- if (record.kind !== "relay_intent" || record.message_id === void 0) continue;
7428
- const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
7429
- intents.add(record.recipient_identity);
7430
- intentsByMessage.set(record.message_id, intents);
7431
- }
7432
7496
  const covered = /* @__PURE__ */ new Set();
7433
7497
  for (const message of matching) {
7434
- const intents = intentsByMessage.get(message.message_id) ?? /* @__PURE__ */ new Set();
7498
+ const intentRecords = await queryStore2(this.store, room.room_id, {
7499
+ kind: "relay_intent",
7500
+ messageId: message.message_id
7501
+ });
7502
+ const intents = new Set(intentRecords.map((record) => record.recipient_identity));
7435
7503
  for (const recipientIdentity of message.recipient_identities) {
7436
7504
  covered.add(recipientIdentity);
7437
7505
  if (!intents.has(recipientIdentity)) {
@@ -7447,35 +7515,36 @@ var init_service = __esm({
7447
7515
  }
7448
7516
  }
7449
7517
  const missing = recipients.filter((seat) => !covered.has(seat.identity));
7450
- let appendedAt;
7451
- if (missing.length > 0) {
7452
- const appended = await this.store.append(room.room_id, {
7518
+ const appendedAt = await this.appendBriefingForMissing(room, missing, briefing);
7519
+ return matching[0]?.at ?? appendedAt ?? this.now();
7520
+ }
7521
+ async appendBriefingForMissing(room, missing, briefing) {
7522
+ if (missing.length === 0) return void 0;
7523
+ const appended = await this.store.append(room.room_id, {
7524
+ version: 1,
7525
+ kind: "message",
7526
+ room_id: room.room_id,
7527
+ at: this.now(),
7528
+ message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
7529
+ author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
7530
+ category: briefing.category,
7531
+ ...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
7532
+ briefing_version: briefing.briefing_version,
7533
+ text: briefing.text,
7534
+ recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
7535
+ });
7536
+ if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
7537
+ for (const recipientIdentity of appended.recipient_identities) {
7538
+ await this.store.append(room.room_id, {
7453
7539
  version: 1,
7454
- kind: "message",
7540
+ kind: "relay_intent",
7455
7541
  room_id: room.room_id,
7456
7542
  at: this.now(),
7457
- message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
7458
- author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
7459
- category: briefing.category,
7460
- ...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
7461
- briefing_version: briefing.briefing_version,
7462
- text: briefing.text,
7463
- recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
7543
+ message_id: appended.message_id,
7544
+ recipient_identity: recipientIdentity
7464
7545
  });
7465
- if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
7466
- appendedAt = appended.at;
7467
- for (const recipientIdentity of appended.recipient_identities) {
7468
- await this.store.append(room.room_id, {
7469
- version: 1,
7470
- kind: "relay_intent",
7471
- room_id: room.room_id,
7472
- at: this.now(),
7473
- message_id: appended.message_id,
7474
- recipient_identity: recipientIdentity
7475
- });
7476
- }
7477
7546
  }
7478
- return matching[0]?.at ?? appendedAt ?? this.now();
7547
+ return appended.at;
7479
7548
  }
7480
7549
  lock(roomId, work) {
7481
7550
  return this.store.mutex(roomId).runExclusive(work);
@@ -7509,8 +7578,9 @@ var init_service = __esm({
7509
7578
  import { randomBytes as randomBytes3 } from "node:crypto";
7510
7579
  import * as nodeFs2 from "node:fs";
7511
7580
  import { AsyncLocalStorage } from "node:async_hooks";
7512
- import { dirname as dirname2, join as join3 } from "node:path";
7513
- var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
7581
+ import { basename, dirname as dirname2, join as join3 } from "node:path";
7582
+ import Database from "better-sqlite3";
7583
+ var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
7514
7584
  var init_storage = __esm({
7515
7585
  "src/storage.ts"() {
7516
7586
  "use strict";
@@ -7519,6 +7589,8 @@ var init_storage = __esm({
7519
7589
  DIRECTORY_MODE2 = 448;
7520
7590
  FILE_MODE2 = 384;
7521
7591
  NO_FOLLOW2 = nodeFs2.constants.O_NOFOLLOW ?? 0;
7592
+ SQLITE_SCHEMA_VERSION = 1;
7593
+ DEFAULT_WORK_BATCH_SIZE = 64;
7522
7594
  utf8Decoder = new TextDecoder("utf-8", { fatal: true });
7523
7595
  CoworkStorageError = class extends Error {
7524
7596
  constructor(message, options) {
@@ -7546,20 +7618,20 @@ var init_storage = __esm({
7546
7618
  CoworkStore = class {
7547
7619
  stateDir;
7548
7620
  fs;
7549
- // The standalone daemon is the sole writer. This is intentionally an
7550
- // in-process room FIFO, not an on-disk lock with stale-owner recovery.
7621
+ beforeRecordCommit;
7551
7622
  roomMutexes = /* @__PURE__ */ new Map();
7552
7623
  lockOwnership = new AsyncLocalStorage();
7553
- nextSequences = /* @__PURE__ */ new Map();
7624
+ reconciledBlobRooms = /* @__PURE__ */ new Set();
7554
7625
  constructor(stateDir, options = {}) {
7555
7626
  if (!stateDir) throw new CoworkStorageError("state directory is required");
7556
7627
  this.stateDir = stateDir;
7557
7628
  this.fs = options.fs ?? nodeFs2;
7629
+ this.beforeRecordCommit = options.beforeRecordCommit;
7558
7630
  }
7559
7631
  mutex(roomId, work) {
7560
- const validRoomId = this.roomId(roomId);
7561
- if (work) return this.withRoomMutex(validRoomId, work);
7562
- return { runExclusive: (nested) => this.withRoomMutex(validRoomId, nested) };
7632
+ const id = this.roomId(roomId);
7633
+ if (work) return this.withRoomMutex(id, work);
7634
+ return { runExclusive: (nested) => this.withRoomMutex(id, nested) };
7563
7635
  }
7564
7636
  withRoomMutex(roomId, work) {
7565
7637
  const inherited = this.lockOwnership.getStore();
@@ -7567,15 +7639,10 @@ var init_storage = __esm({
7567
7639
  if (ownership?.active) {
7568
7640
  const nested = Promise.resolve().then(work);
7569
7641
  ownership.pending.add(nested);
7570
- void nested.then(
7571
- () => {
7572
- ownership.pending.delete(nested);
7573
- },
7574
- (error) => {
7575
- ownership.pending.delete(nested);
7576
- ownership.failures.push(error);
7577
- }
7578
- );
7642
+ void nested.then(() => ownership.pending.delete(nested), (error) => {
7643
+ ownership.pending.delete(nested);
7644
+ ownership.failures.push(error);
7645
+ });
7579
7646
  return nested;
7580
7647
  }
7581
7648
  let queue = this.roomMutexes.get(roomId);
@@ -7597,9 +7664,7 @@ var init_storage = __esm({
7597
7664
  rootFailed = true;
7598
7665
  rootFailure = error;
7599
7666
  }
7600
- while (acquired.pending.size > 0) {
7601
- await Promise.allSettled([...acquired.pending]);
7602
- }
7667
+ while (acquired.pending.size > 0) await Promise.allSettled([...acquired.pending]);
7603
7668
  } finally {
7604
7669
  acquired.active = false;
7605
7670
  }
@@ -7617,20 +7682,20 @@ var init_storage = __esm({
7617
7682
  this.rejectSymlink(roomDir, "room directory");
7618
7683
  throw new CoworkStorageError(`room "${room.room_id}" already exists`);
7619
7684
  }
7620
- let roomCreated = false;
7685
+ let created = false;
7621
7686
  try {
7622
7687
  this.fs.mkdirSync(roomDir, { mode: DIRECTORY_MODE2 });
7623
- roomCreated = true;
7688
+ created = true;
7624
7689
  this.fs.chmodSync(roomDir, DIRECTORY_MODE2);
7690
+ this.fs.mkdirSync(this.blobsDirectory(room.room_id), { mode: DIRECTORY_MODE2 });
7691
+ this.fs.chmodSync(this.blobsDirectory(room.room_id), DIRECTORY_MODE2);
7625
7692
  this.fsyncDirectory(roomDir);
7626
7693
  this.fsyncDirectory(this.roomsDirectory());
7627
- this.createArchive(room.room_id);
7694
+ this.withDatabase(room.room_id, () => void 0, true);
7628
7695
  this.atomicMetadataWrite(this.metadataPath(room.room_id), room);
7629
- this.nextSequences.set(room.room_id, 1);
7630
7696
  return room;
7631
7697
  } catch (error) {
7632
- this.nextSequences.delete(room.room_id);
7633
- if (roomCreated) {
7698
+ if (created) {
7634
7699
  try {
7635
7700
  this.fs.rmSync(roomDir, { recursive: true, force: true });
7636
7701
  this.fsyncDirectory(this.roomsDirectory());
@@ -7642,8 +7707,8 @@ var init_storage = __esm({
7642
7707
  });
7643
7708
  }
7644
7709
  async load(roomId) {
7645
- const validRoomId = this.roomId(roomId);
7646
- return this.mutex(validRoomId, () => this.loadUnlocked(validRoomId));
7710
+ const id = this.roomId(roomId);
7711
+ return this.mutex(id, () => this.loadUnlocked(id));
7647
7712
  }
7648
7713
  async save(input) {
7649
7714
  const room = RoomSchema.parse(input);
@@ -7655,176 +7720,435 @@ var init_storage = __esm({
7655
7720
  }
7656
7721
  async list() {
7657
7722
  this.ensureBaseDirectories();
7658
- const roomIds = this.fs.readdirSync(this.roomsDirectory(), { withFileTypes: true }).filter((entry) => LowerCrockfordUlidSchema.safeParse(entry.name).success).map((entry) => entry.name).sort();
7723
+ const ids = this.fs.readdirSync(this.roomsDirectory(), { withFileTypes: true }).filter((entry) => entry.isDirectory() && LowerCrockfordUlidSchema.safeParse(entry.name).success).map((entry) => entry.name).sort();
7659
7724
  const rooms = [];
7660
- for (const roomId of roomIds) rooms.push(await this.load(roomId));
7725
+ for (const id of ids) rooms.push(await this.load(id));
7661
7726
  return rooms;
7662
7727
  }
7663
7728
  async append(roomId, input) {
7664
- const validRoomId = this.roomId(roomId);
7729
+ const id = this.roomId(roomId);
7665
7730
  const draft = AppendRecordSchema.parse(input);
7666
- if (draft.room_id !== validRoomId) {
7667
- throw new CoworkStorageError(`record room_id "${draft.room_id}" does not match room "${validRoomId}"`);
7668
- }
7669
- return this.mutex(validRoomId, () => {
7670
- this.assertRoomDirectory(validRoomId);
7671
- const archivePath = this.archivePath(validRoomId);
7672
- this.assertRegularFile(archivePath, "room archive");
7673
- let nextSequence = this.nextSequences.get(validRoomId);
7674
- if (nextSequence === void 0) {
7675
- nextSequence = this.scanArchive(validRoomId).nextSequence;
7676
- }
7677
- const record = CommunicationRecordSchema.parse({
7678
- ...draft,
7679
- seq: nextSequence,
7680
- record_id: `${validRoomId}:${nextSequence}`
7681
- });
7682
- const bytes = Buffer.from(`${JSON.stringify(record)}
7683
- `, "utf8");
7684
- let fd;
7685
- let originalSize;
7686
- let writeStarted = false;
7731
+ if (draft.room_id !== id) throw new CoworkStorageError(`record room_id "${draft.room_id}" does not match room "${id}"`);
7732
+ return this.mutex(id, () => {
7733
+ this.assertRoomDirectory(id);
7734
+ if (!this.reconciledBlobRooms.has(id)) this.withDatabase(id, () => void 0);
7735
+ let blob;
7736
+ let storedDraft = draft;
7737
+ if (draft.kind === "file") {
7738
+ const bytes = Buffer.from(draft.data_base64, "base64");
7739
+ blob = this.persistBlob(id, draft.sha256, bytes);
7740
+ const { data_base64: _bytes, ...withoutBytes } = draft;
7741
+ storedDraft = withoutBytes;
7742
+ }
7687
7743
  try {
7688
- fd = this.fs.openSync(
7689
- archivePath,
7690
- nodeFs2.constants.O_WRONLY | nodeFs2.constants.O_APPEND | NO_FOLLOW2
7691
- );
7692
- const opened = this.validateOpenPath(fd, archivePath, "room archive", "file", true);
7693
- originalSize = opened.size;
7694
- this.fs.fchmodSync(fd, FILE_MODE2);
7695
- writeStarted = true;
7696
- this.writeAll(fd, bytes);
7697
- this.fs.fsyncSync(fd);
7698
- this.nextSequences.set(validRoomId, nextSequence + 1);
7699
- return record;
7744
+ return this.withDatabase(id, (db) => {
7745
+ const transaction = db.transaction(() => {
7746
+ const next = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM records").get().next;
7747
+ const record = { ...draft, seq: next, record_id: `${id}:${next}` };
7748
+ const stored = { ...storedDraft, seq: next, record_id: record.record_id };
7749
+ const values = this.indexValues(record);
7750
+ db.prepare(`INSERT INTO records
7751
+ (seq, record_id, kind, at, payload_json, blob_path, message_id, file_id, intent_record_id,
7752
+ recipient_identity, source_msg_id, source_file_id, category, briefing_role, briefing_version, membership_epoch)
7753
+ VALUES (@seq,@record_id,@kind,@at,@payload_json,@blob_path,@message_id,@file_id,@intent_record_id,
7754
+ @recipient_identity,@source_msg_id,@source_file_id,@category,@briefing_role,@briefing_version,@membership_epoch)`).run({ ...values, payload_json: JSON.stringify(stored), blob_path: blob?.path ?? null });
7755
+ if (record.kind === "message" || record.kind === "file") {
7756
+ const insert = db.prepare(`INSERT INTO record_recipients
7757
+ (record_seq, recipient_identity, category, briefing_role, briefing_version)
7758
+ VALUES (?, ?, ?, ?, ?)`);
7759
+ const enqueue = db.prepare("INSERT INTO relay_intent_work(record_seq, recipient_identity) VALUES (?, ?)");
7760
+ for (const recipient of record.recipient_identities) {
7761
+ insert.run(
7762
+ record.seq,
7763
+ recipient,
7764
+ record.kind === "message" ? record.category : null,
7765
+ record.kind === "message" ? record.briefing_role ?? null : null,
7766
+ record.kind === "message" ? record.briefing_version ?? 1 : null
7767
+ );
7768
+ enqueue.run(record.seq, recipient);
7769
+ }
7770
+ } else if (record.kind === "relay_intent") {
7771
+ const sourceColumn = record.message_id === void 0 ? "file_id" : "message_id";
7772
+ const sourceId = record.message_id ?? record.file_id;
7773
+ db.prepare(`DELETE FROM relay_intent_work
7774
+ WHERE recipient_identity = ? AND record_seq IN (
7775
+ SELECT seq FROM records WHERE kind = ? AND ${sourceColumn} = ?
7776
+ )`).run(record.recipient_identity, record.message_id === void 0 ? "file" : "message", sourceId);
7777
+ }
7778
+ this.beforeRecordCommit?.();
7779
+ return record;
7780
+ });
7781
+ return transaction.immediate();
7782
+ });
7700
7783
  } catch (error) {
7701
- this.nextSequences.delete(validRoomId);
7702
- if (fd !== void 0 && originalSize !== void 0 && writeStarted) {
7703
- try {
7704
- this.fs.ftruncateSync(fd, originalSize);
7705
- this.fs.fsyncSync(fd);
7706
- } catch {
7707
- }
7708
- }
7709
- throw this.wrap(`failed to append room "${validRoomId}" archive`, error);
7710
- } finally {
7711
- if (fd !== void 0) {
7712
- try {
7713
- this.fs.closeSync(fd);
7714
- } catch {
7715
- }
7716
- }
7784
+ if (blob?.created) this.removeUnreferencedBlob(id, blob.path);
7785
+ throw this.wrap(`failed to append room "${id}" archive`, error);
7717
7786
  }
7718
7787
  });
7719
7788
  }
7720
7789
  async read(roomId, options = {}) {
7721
- const validRoomId = this.roomId(roomId);
7790
+ const id = this.roomId(roomId);
7722
7791
  const after = options.after ?? 0;
7723
7792
  const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
7724
- if (!Number.isSafeInteger(after) || after < 0) throw new CoworkStorageError("after must be a non-negative safe integer");
7793
+ this.validatePage(after, limit);
7794
+ return this.mutex(id, () => {
7795
+ this.assertRoomDirectory(id);
7796
+ return this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(
7797
+ "SELECT seq,payload_json,blob_path FROM records WHERE seq > ? ORDER BY seq ASC LIMIT ?"
7798
+ ).all(after, limit)));
7799
+ });
7800
+ }
7801
+ async query(roomId, options) {
7802
+ const id = this.roomId(roomId);
7803
+ const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
7725
7804
  if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
7726
- return this.mutex(validRoomId, () => {
7727
- this.assertRoomDirectory(validRoomId);
7728
- const scan = this.scanArchive(validRoomId);
7729
- this.nextSequences.set(validRoomId, scan.nextSequence);
7730
- return scan.records.filter((record) => record.seq > after).slice(0, limit);
7805
+ return this.mutex(id, () => {
7806
+ this.assertRoomDirectory(id);
7807
+ return this.withDatabase(id, (db) => {
7808
+ const clauses = [];
7809
+ const values = [];
7810
+ const add = (column, value) => {
7811
+ if (value !== void 0) {
7812
+ clauses.push(`r.${column} = ?`);
7813
+ values.push(value);
7814
+ }
7815
+ };
7816
+ add("kind", options.kind);
7817
+ add("message_id", options.messageId);
7818
+ add("file_id", options.fileId);
7819
+ add("source_msg_id", options.sourceMsgId);
7820
+ add("source_file_id", options.sourceFileId);
7821
+ add("intent_record_id", options.intentRecordId);
7822
+ add("recipient_identity", options.recipientIdentity);
7823
+ add("category", options.category);
7824
+ add("membership_epoch", options.membershipEpoch);
7825
+ if (options.after !== void 0) {
7826
+ clauses.push("r.seq > ?");
7827
+ values.push(options.after);
7828
+ }
7829
+ if (options.unresolvedResultKind) {
7830
+ clauses.push("NOT EXISTS (SELECT 1 FROM records result WHERE result.kind = ? AND result.intent_record_id = r.record_id)");
7831
+ values.push(options.unresolvedResultKind);
7832
+ }
7833
+ values.push(limit);
7834
+ const sql = `SELECT r.seq,r.payload_json,r.blob_path FROM records r${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY r.seq ${options.descending ? "DESC" : "ASC"} LIMIT ?`;
7835
+ return this.decodeRows(id, db.prepare(sql).all(...values));
7836
+ });
7731
7837
  });
7732
7838
  }
7839
+ async recipients(roomId, recordSeq) {
7840
+ const id = this.roomId(roomId);
7841
+ return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
7842
+ "SELECT recipient_identity FROM record_recipients WHERE record_seq = ? ORDER BY recipient_identity"
7843
+ ).all(recordSeq).map((row) => row.recipient_identity)));
7844
+ }
7845
+ async recordsNeedingRelayIntents(roomId, options = {}) {
7846
+ const id = this.roomId(roomId);
7847
+ const after = options.after ?? 0;
7848
+ const limit = options.limit ?? DEFAULT_WORK_BATCH_SIZE;
7849
+ this.validatePage(after, limit);
7850
+ return this.mutex(id, () => this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(`
7851
+ SELECT source.seq,source.payload_json,source.blob_path
7852
+ FROM relay_intent_work work INDEXED BY relay_work_source
7853
+ JOIN records source ON source.seq = work.record_seq
7854
+ WHERE work.record_seq > ?
7855
+ GROUP BY source.seq
7856
+ ORDER BY source.seq ASC
7857
+ LIMIT ?
7858
+ `).all(after, limit))));
7859
+ }
7860
+ async relayRecipientsNeedingIntent(roomId, recordSeq, limit = DEFAULT_WORK_BATCH_SIZE) {
7861
+ const id = this.roomId(roomId);
7862
+ if (!Number.isSafeInteger(recordSeq) || recordSeq < 1) throw new CoworkStorageError("record sequence must be a positive safe integer");
7863
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
7864
+ return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
7865
+ "SELECT recipient_identity FROM relay_intent_work WHERE record_seq = ? ORDER BY recipient_identity LIMIT ?"
7866
+ ).all(recordSeq, limit).map((row) => row.recipient_identity)));
7867
+ }
7868
+ async briefingDeliveryTimes(roomId, key, recipientIdentities) {
7869
+ const id = this.roomId(roomId);
7870
+ if (recipientIdentities.length === 0) return /* @__PURE__ */ new Map();
7871
+ return this.mutex(id, () => this.withDatabase(id, (db) => {
7872
+ const lookup = db.prepare(`SELECT records.at FROM record_recipients recipients
7873
+ INDEXED BY recipients_briefing_delivery
7874
+ JOIN records ON records.seq = recipients.record_seq
7875
+ WHERE recipients.recipient_identity = ? AND recipients.category = ?
7876
+ AND recipients.briefing_role IS ? AND recipients.briefing_version = ?
7877
+ ORDER BY recipients.record_seq ASC LIMIT 1`);
7878
+ const deliveries = /* @__PURE__ */ new Map();
7879
+ for (const recipient of recipientIdentities) {
7880
+ const row = lookup.get(
7881
+ recipient,
7882
+ key.category,
7883
+ key.briefingRole ?? null,
7884
+ key.briefingVersion
7885
+ );
7886
+ if (row) deliveries.set(recipient, row.at);
7887
+ }
7888
+ return deliveries;
7889
+ }));
7890
+ }
7891
+ async durability(roomId) {
7892
+ const id = this.roomId(roomId);
7893
+ return this.mutex(id, () => this.withDatabase(id, (db) => ({
7894
+ journalMode: db.pragma("journal_mode", { simple: true }),
7895
+ synchronous: db.pragma("synchronous", { simple: true })
7896
+ })));
7897
+ }
7733
7898
  async delete(roomId) {
7734
- const validRoomId = this.roomId(roomId);
7735
- await this.mutex(validRoomId, () => {
7899
+ const id = this.roomId(roomId);
7900
+ await this.mutex(id, () => {
7736
7901
  this.ensureBaseDirectories();
7737
- const roomDir = this.roomDirectory(validRoomId);
7902
+ const roomDir = this.roomDirectory(id);
7738
7903
  if (!this.lstatIfPresent(roomDir)) {
7739
7904
  this.fsyncDirectory(this.roomsDirectory());
7740
- this.nextSequences.delete(validRoomId);
7741
7905
  return;
7742
7906
  }
7743
- this.ensurePrivateDirectory(roomDir, false, `room "${validRoomId}" directory`);
7744
- const archivePath = this.archivePath(validRoomId);
7745
- const metadataPath = this.metadataPath(validRoomId);
7746
- const archivePresent = this.lstatIfPresent(archivePath) !== void 0;
7747
- const metadataPresent = this.lstatIfPresent(metadataPath) !== void 0;
7907
+ this.ensurePrivateDirectory(roomDir, false, `room "${id}" directory`);
7908
+ const metadata = this.metadataPath(id);
7909
+ const metadataPresent = this.lstatIfPresent(metadata) !== void 0;
7910
+ const archivePresent = this.lstatIfPresent(this.archivePath(id)) !== void 0;
7911
+ const blobsPresent = this.lstatIfPresent(this.blobsDirectory(id)) !== void 0;
7748
7912
  if (metadataPresent) {
7749
- const room = this.loadUnlocked(validRoomId);
7750
- if (room.state !== "closed") {
7751
- throw new CoworkStorageError(`room "${validRoomId}" must be closed before deletion`);
7752
- }
7913
+ const room = this.loadUnlocked(id);
7914
+ if (room.state !== "closed") throw new CoworkStorageError(`room "${id}" must be closed before deletion`);
7753
7915
  this.removeProvisioningArtifacts(roomDir);
7754
- } else if (archivePresent) {
7755
- throw new CoworkStorageError(
7756
- `room "${validRoomId}" has archive residue without deletion metadata`
7757
- );
7916
+ } else if (archivePresent || blobsPresent) {
7917
+ throw new CoworkStorageError(`room "${id}" has archive residue without deletion metadata`);
7758
7918
  }
7759
- const expected = /* @__PURE__ */ new Set(["archive.jsonl", "room.json", "room.json.v1.bak"]);
7919
+ const expected = /* @__PURE__ */ new Set(["archive.sqlite3", "archive.sqlite3-wal", "archive.sqlite3-shm", "blobs", "room.json", "room.json.v1.bak"]);
7760
7920
  const unexpected = this.fs.readdirSync(roomDir).filter((name) => !expected.has(name));
7761
- if (unexpected.length > 0) {
7762
- throw new CoworkStorageError(`room "${validRoomId}" contains live or unexpected residue: ${unexpected.join(", ")}`);
7921
+ if (unexpected.length) throw new CoworkStorageError(`room "${id}" contains live or unexpected residue: ${unexpected.join(", ")}`);
7922
+ for (const name of ["archive.sqlite3-wal", "archive.sqlite3-shm", "archive.sqlite3", "room.json.v1.bak", "room.json"]) {
7923
+ const path = join3(roomDir, name);
7924
+ if (this.lstatIfPresent(path)) {
7925
+ this.assertRegularFile(path, name);
7926
+ this.fs.unlinkSync(path);
7927
+ }
7763
7928
  }
7764
- const backupPath = `${metadataPath}.v1.bak`;
7765
- if (this.lstatIfPresent(backupPath)) {
7766
- this.assertRegularFile(backupPath, "room metadata v1 backup");
7767
- this.fs.unlinkSync(backupPath);
7768
- this.fsyncDirectory(roomDir);
7929
+ const blobs = this.blobsDirectory(id);
7930
+ if (this.lstatIfPresent(blobs)) this.fs.rmSync(blobs, { recursive: true, force: true });
7931
+ this.fsyncDirectory(roomDir);
7932
+ this.fs.rmdirSync(roomDir);
7933
+ this.fsyncDirectory(this.roomsDirectory());
7934
+ });
7935
+ }
7936
+ withDatabase(roomId, work, create = false) {
7937
+ const path = this.archivePath(roomId);
7938
+ let guardFd;
7939
+ if (!create) {
7940
+ this.assertRegularFile(path, "room archive database");
7941
+ guardFd = this.fs.openSync(path, nodeFs2.constants.O_RDONLY | NO_FOLLOW2);
7942
+ this.validateOpenPath(guardFd, path, "room archive database", "file", true);
7943
+ }
7944
+ let db;
7945
+ try {
7946
+ this.secureSqliteFiles(path);
7947
+ db = new Database(path, { fileMustExist: !create });
7948
+ if (guardFd !== void 0) this.validateOpenPath(guardFd, path, "room archive database", "file", true);
7949
+ this.fs.chmodSync(path, FILE_MODE2);
7950
+ db.pragma("journal_mode = WAL");
7951
+ db.pragma("synchronous = FULL");
7952
+ db.pragma("foreign_keys = ON");
7953
+ db.pragma("busy_timeout = 5000");
7954
+ if (create) {
7955
+ db.exec(`CREATE TABLE records (
7956
+ seq INTEGER PRIMARY KEY, record_id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, at TEXT NOT NULL,
7957
+ payload_json TEXT NOT NULL, blob_path TEXT, message_id TEXT, file_id TEXT, intent_record_id TEXT,
7958
+ recipient_identity TEXT, source_msg_id INTEGER, source_file_id INTEGER, category TEXT,
7959
+ briefing_role TEXT, briefing_version INTEGER, membership_epoch INTEGER
7960
+ );
7961
+ CREATE TABLE record_recipients (
7962
+ record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
7963
+ recipient_identity TEXT NOT NULL, category TEXT, briefing_role TEXT,
7964
+ briefing_version INTEGER, PRIMARY KEY(record_seq, recipient_identity)
7965
+ );
7966
+ CREATE TABLE relay_intent_work (
7967
+ record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
7968
+ recipient_identity TEXT NOT NULL, PRIMARY KEY(record_seq, recipient_identity)
7969
+ );
7970
+ CREATE INDEX relay_work_source ON relay_intent_work(record_seq, recipient_identity);
7971
+ CREATE INDEX records_kind_seq ON records(kind, seq);
7972
+ CREATE INDEX records_message ON records(message_id, kind, seq);
7973
+ CREATE INDEX records_file ON records(file_id, kind, seq);
7974
+ CREATE INDEX records_intent_result ON records(intent_record_id, kind);
7975
+ CREATE INDEX records_relay_recipient ON records(kind, recipient_identity, seq);
7976
+ CREATE UNIQUE INDEX records_source_message ON records(source_msg_id) WHERE kind='message' AND source_msg_id IS NOT NULL;
7977
+ CREATE UNIQUE INDEX records_source_file ON records(source_file_id) WHERE kind='file' AND source_file_id IS NOT NULL;
7978
+ CREATE INDEX records_briefing ON records(category, briefing_role, briefing_version, seq);
7979
+ CREATE INDEX records_membership_epoch ON records(category, membership_epoch);
7980
+ CREATE INDEX recipients_identity ON record_recipients(recipient_identity, record_seq);
7981
+ CREATE INDEX recipients_briefing_delivery ON record_recipients
7982
+ (recipient_identity, category, briefing_role, briefing_version, record_seq);`);
7983
+ db.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
7984
+ this.reconciledBlobRooms.add(roomId);
7985
+ } else {
7986
+ const version = db.pragma("user_version", { simple: true });
7987
+ if (version !== SQLITE_SCHEMA_VERSION) {
7988
+ throw new CoworkStorageError(`unsupported room archive schema version ${version}`);
7989
+ }
7990
+ if (!this.reconciledBlobRooms.has(roomId)) {
7991
+ this.reconcileBlobDirectory(roomId, db);
7992
+ this.reconciledBlobRooms.add(roomId);
7993
+ }
7769
7994
  }
7770
- if (archivePresent) {
7771
- this.assertRegularFile(archivePath, "room archive");
7772
- this.fs.unlinkSync(archivePath);
7773
- this.fsyncDirectory(roomDir);
7995
+ this.secureSqliteFiles(path);
7996
+ const result = work(db);
7997
+ this.secureSqliteFiles(path);
7998
+ return result;
7999
+ } catch (error) {
8000
+ throw this.wrap(`failed to access room "${roomId}" SQLite archive`, error);
8001
+ } finally {
8002
+ try {
8003
+ db?.close();
8004
+ } catch {
7774
8005
  }
7775
- if (metadataPresent) {
7776
- this.assertRegularFile(metadataPath, "room metadata");
7777
- this.fs.unlinkSync(metadataPath);
7778
- this.fsyncDirectory(roomDir);
8006
+ if (guardFd !== void 0) try {
8007
+ this.fs.closeSync(guardFd);
8008
+ } catch {
8009
+ }
8010
+ this.secureSqliteFiles(path);
8011
+ }
8012
+ }
8013
+ indexValues(record) {
8014
+ const subject = record;
8015
+ return {
8016
+ seq: record.seq,
8017
+ record_id: record.record_id,
8018
+ kind: record.kind,
8019
+ at: record.at,
8020
+ message_id: subject.message_id ?? null,
8021
+ file_id: subject.file_id ?? null,
8022
+ intent_record_id: subject.intent_record_id ?? null,
8023
+ recipient_identity: subject.recipient_identity ?? null,
8024
+ source_msg_id: subject.source_msg_id ?? null,
8025
+ source_file_id: subject.source_file_id ?? null,
8026
+ category: subject.category ?? null,
8027
+ briefing_role: subject.briefing_role ?? null,
8028
+ briefing_version: subject.briefing_version ?? null,
8029
+ membership_epoch: typeof subject.membership === "object" && subject.membership !== null ? subject.membership.epoch ?? null : null
8030
+ };
8031
+ }
8032
+ decodeRows(roomId, rows) {
8033
+ return rows.map((row) => {
8034
+ let decoded;
8035
+ try {
8036
+ decoded = JSON.parse(row.payload_json);
8037
+ } catch (error) {
8038
+ throw new CoworkStorageError(`malformed JSON in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
8039
+ }
8040
+ if (row.blob_path !== null) {
8041
+ const subject = decoded;
8042
+ const expectedPath = subject.kind === "file" && typeof subject.sha256 === "string" && /^[0-9a-f]{64}$/.test(subject.sha256) ? join3("blobs", subject.sha256) : void 0;
8043
+ if (expectedPath === void 0 || row.blob_path !== expectedPath) {
8044
+ throw new CoworkStorageError(`invalid blob reference in room "${roomId}" archive at sequence ${row.seq}`);
8045
+ }
8046
+ const bytes = this.readFileNoFollow(join3(this.roomDirectory(roomId), expectedPath), "room file blob");
8047
+ decoded = { ...decoded, data_base64: bytes.toString("base64") };
8048
+ }
8049
+ try {
8050
+ const record = CommunicationRecordSchema.parse(decoded);
8051
+ if (record.room_id !== roomId || record.seq !== row.seq) throw new Error("indexed identity mismatch");
8052
+ return record;
8053
+ } catch (error) {
8054
+ throw new CoworkStorageError(`invalid record in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
7779
8055
  }
7780
- this.fs.rmdirSync(roomDir);
7781
- this.fsyncDirectory(this.roomsDirectory());
7782
- this.nextSequences.delete(validRoomId);
7783
8056
  });
7784
8057
  }
7785
- removeProvisioningArtifacts(roomDir) {
7786
- const journalPath = join3(roomDir, ".cowork-provisioning-stage");
7787
- const targets = [join3(roomDir, "live"), join3(roomDir, "provisioning-residue")];
7788
- if (this.lstatIfPresent(journalPath)) {
7789
- this.assertRegularFile(journalPath, "provisioning staging journal");
7790
- const stagingName = this.fs.readFileSync(journalPath, "utf8").trim();
7791
- if (!/^live\.staging-[0-9a-f]{32}$/.test(stagingName)) {
7792
- throw new CoworkStorageError("invalid provisioning staging journal");
8058
+ persistBlob(roomId, digest, bytes) {
8059
+ const directory = this.blobsDirectory(roomId);
8060
+ this.ensurePrivateDirectory(directory, true, "room blobs directory");
8061
+ const final = join3(directory, digest);
8062
+ if (this.lstatIfPresent(final)) {
8063
+ this.assertRegularFile(final, "room file blob");
8064
+ if (this.readFileNoFollow(final, "room file blob").equals(bytes)) return { path: join3("blobs", digest), created: false };
8065
+ throw new CoworkStorageError(`immutable blob collision for ${digest}`);
8066
+ }
8067
+ const temp = join3(directory, `.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
8068
+ let fd;
8069
+ try {
8070
+ fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
8071
+ this.writeAll(fd, bytes);
8072
+ this.fs.fsyncSync(fd);
8073
+ this.fs.closeSync(fd);
8074
+ fd = void 0;
8075
+ this.fs.renameSync(temp, final);
8076
+ this.fsyncDirectory(directory);
8077
+ this.fs.chmodSync(final, FILE_MODE2);
8078
+ return { path: join3("blobs", digest), created: true };
8079
+ } finally {
8080
+ if (fd !== void 0) try {
8081
+ this.fs.closeSync(fd);
8082
+ } catch {
8083
+ }
8084
+ if (this.lstatIfPresent(temp)) try {
8085
+ this.fs.unlinkSync(temp);
8086
+ } catch {
7793
8087
  }
7794
- targets.push(join3(roomDir, stagingName));
7795
8088
  }
7796
- for (const target of targets) {
7797
- const stat = this.lstatIfPresent(target);
7798
- if (!stat) continue;
7799
- if (stat.isSymbolicLink() || !stat.isDirectory()) this.fs.unlinkSync(target);
7800
- else this.fs.rmSync(target, { recursive: true, force: true });
7801
- this.fsyncDirectory(roomDir);
8089
+ }
8090
+ removeUnreferencedBlob(roomId, relativePath) {
8091
+ const absolute = join3(this.roomDirectory(roomId), relativePath);
8092
+ try {
8093
+ const referenced = this.withDatabase(roomId, (db) => db.prepare(
8094
+ "SELECT EXISTS(SELECT 1 FROM records WHERE blob_path = ?) AS found"
8095
+ ).get(relativePath).found !== 0);
8096
+ if (referenced || !this.lstatIfPresent(absolute)) return;
8097
+ this.assertRegularFile(absolute, "unreferenced room file blob");
8098
+ this.fs.unlinkSync(absolute);
8099
+ this.fsyncDirectory(dirname2(absolute));
8100
+ } catch {
7802
8101
  }
7803
- if (this.lstatIfPresent(journalPath)) {
7804
- this.fs.unlinkSync(journalPath);
7805
- this.fsyncDirectory(roomDir);
8102
+ }
8103
+ reconcileBlobDirectory(roomId, db) {
8104
+ const directory = this.blobsDirectory(roomId);
8105
+ this.ensurePrivateDirectory(directory, true, "room blobs directory");
8106
+ const referenced = new Set(db.prepare(
8107
+ "SELECT blob_path FROM records WHERE blob_path IS NOT NULL"
8108
+ ).all().map((row) => basename(row.blob_path)));
8109
+ let changed = false;
8110
+ for (const entry of this.fs.readdirSync(directory, { withFileTypes: true })) {
8111
+ const path = join3(directory, entry.name);
8112
+ if (/^\.tmp-[0-9]+-[0-9a-f]{16}$/.test(entry.name)) {
8113
+ this.assertRegularFile(path, "crash-left room blob temporary file");
8114
+ this.fs.unlinkSync(path);
8115
+ changed = true;
8116
+ continue;
8117
+ }
8118
+ if (!/^[0-9a-f]{64}$/.test(entry.name)) throw new CoworkStorageError(`unexpected room blob residue: ${entry.name}`);
8119
+ this.assertRegularFile(path, referenced.has(entry.name) ? "room file blob" : "unreferenced room file blob");
8120
+ this.fs.chmodSync(path, FILE_MODE2);
8121
+ if (referenced.has(entry.name)) continue;
8122
+ this.fs.unlinkSync(path);
8123
+ changed = true;
8124
+ }
8125
+ if (changed) this.fsyncDirectory(directory);
8126
+ }
8127
+ secureSqliteFiles(path) {
8128
+ for (const candidate of [path, `${path}-wal`, `${path}-shm`]) {
8129
+ if (!this.lstatIfPresent(candidate)) continue;
8130
+ this.assertRegularFile(candidate, `SQLite file ${basename(candidate)}`);
8131
+ this.fs.chmodSync(candidate, FILE_MODE2);
7806
8132
  }
7807
8133
  }
8134
+ validatePage(after, limit) {
8135
+ if (!Number.isSafeInteger(after) || after < 0) throw new CoworkStorageError("after must be a non-negative safe integer");
8136
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
8137
+ }
7808
8138
  loadUnlocked(roomId) {
7809
8139
  this.assertRoomDirectory(roomId);
7810
8140
  const path = this.metadataPath(roomId);
7811
8141
  this.assertRegularFile(path, "room metadata");
8142
+ let decoded;
7812
8143
  let bytes;
7813
8144
  try {
7814
8145
  bytes = this.readFileNoFollow(path, "room metadata");
7815
- } catch (error) {
7816
- throw this.wrap(`failed to read room "${roomId}" metadata`, error);
7817
- }
7818
- let decoded;
7819
- try {
7820
8146
  decoded = JSON.parse(utf8Decoder.decode(bytes));
7821
8147
  } catch (error) {
7822
8148
  throw this.wrap(`malformed metadata for room "${roomId}"`, error);
7823
8149
  }
7824
8150
  const room = this.isVersion1(decoded) ? this.migrateUnlocked(roomId, decoded, bytes) : RoomSchema.parse(decoded);
7825
- if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name) {
7826
- this.atomicMetadataWrite(this.metadataPath(roomId), room);
7827
- }
8151
+ if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name) this.atomicMetadataWrite(path, room);
7828
8152
  if (room.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
7829
8153
  return room;
7830
8154
  }
@@ -7834,79 +8158,54 @@ var init_storage = __esm({
7834
8158
  persistedRoomName(decoded) {
7835
8159
  return typeof decoded === "object" && decoded !== null ? decoded.room_name : void 0;
7836
8160
  }
7837
- /**
7838
- * Lazy additive v1 → v2 migration: preserve the exact pre-migration
7839
- * bytes once as room.json.v1.bak, then atomically persist the v2 metadata.
7840
- *
7841
- * THE BACKUP IS WRITTEN TEMP → FSYNC → RENAME, not opened in place, and the
7842
- * reason is a crash window that an existence check cannot see. The earlier
7843
- * version guarded with `if (!lstatIfPresent(backupPath))` and wrote straight
7844
- * into the final path under O_CREAT|O_EXCL. A crash inside that write leaves a
7845
- * PARTIAL file that nonetheless EXISTS, so the next load's existence check
7846
- * skips the backup, writes v2, and the pre-migration bytes are gone — no
7847
- * error, no warning, and the one artefact that exists to undo a bad migration
7848
- * is a truncated fragment. Measured before the fix: a 40-byte prefix of a
7849
- * 604-byte room, JSON.parse false, room.json already v2.
7850
- *
7851
- * A rename is atomic, so the final path now only ever appears complete. The
7852
- * temp file is created with O_EXCL under a pid+random name and removed on
7853
- * failure, so a crashed attempt leaves at most an orphan temp, never a
7854
- * plausible-looking backup.
7855
- *
7856
- * AND AN EXISTING BACKUP IS PARSED BEFORE IT IS TRUSTED, which repairs the
7857
- * case where a partial file is ALREADY on disk from a build without this fix —
7858
- * exactly the state a host that ran the previous code could be in right now.
7859
- * A backup that does not parse as v1 is replaced by the bytes we hold, because
7860
- * those are the real pre-migration bytes and the fragment is worthless.
7861
- *
7862
- * WHAT THE BACKUP IS NOT: restoring it is NOT a rollback. See the note on
7863
- * `restoreV1Backup` — re-migrating mints fresh participant ids.
7864
- */
7865
- migrateUnlocked(roomId, decoded, originalBytes) {
8161
+ migrateUnlocked(roomId, decoded, original) {
7866
8162
  const v1 = RoomV1Schema.parse(decoded);
7867
8163
  if (v1.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
7868
8164
  const migrated = migrateRoomV1(v1, generateUlid);
7869
- const backupPath = `${this.metadataPath(roomId)}.v1.bak`;
7870
- if (!this.hasIntactV1Backup(backupPath)) {
7871
- this.atomicBytesWrite(backupPath, originalBytes, "room metadata v1 backup");
7872
- }
8165
+ const backup = `${this.metadataPath(roomId)}.v1.bak`;
8166
+ if (!this.hasIntactV1Backup(backup)) this.atomicBytesWrite(backup, original, "room metadata v1 backup");
7873
8167
  this.atomicMetadataWrite(this.metadataPath(roomId), migrated);
7874
8168
  return migrated;
7875
8169
  }
7876
- /**
7877
- * Is there already a backup we would be willing to hand back to an operator?
7878
- *
7879
- * Existence is not the question — a partial file exists. It must parse and
7880
- * still claim to be the v1 metadata for this room; anything else is a fragment
7881
- * and is better overwritten with the bytes we are holding right now.
7882
- */
7883
- hasIntactV1Backup(backupPath) {
7884
- if (!this.lstatIfPresent(backupPath)) return false;
8170
+ hasIntactV1Backup(path) {
8171
+ if (!this.lstatIfPresent(path)) return false;
7885
8172
  try {
7886
- const decoded = JSON.parse(utf8Decoder.decode(this.readFileNoFollow(backupPath, "room metadata v1 backup")));
7887
- return this.isVersion1(decoded);
8173
+ return this.isVersion1(JSON.parse(utf8Decoder.decode(this.readFileNoFollow(path, "room metadata v1 backup"))));
7888
8174
  } catch {
7889
8175
  return false;
7890
8176
  }
7891
8177
  }
7892
- /**
7893
- * Durably place exact bytes at `path`: temp under O_EXCL, fsync, rename, fsync
7894
- * the directory. The same dance as atomicMetadataWrite, which serialises a Room
7895
- * rather than taking bytes verbatim — and taking them verbatim is the whole
7896
- * point for a backup, whose value is being byte-identical to what was there.
7897
- */
8178
+ removeProvisioningArtifacts(roomDir) {
8179
+ const journal = join3(roomDir, ".cowork-provisioning-stage");
8180
+ const targets = [join3(roomDir, "live"), join3(roomDir, "provisioning-residue")];
8181
+ if (this.lstatIfPresent(journal)) {
8182
+ this.assertRegularFile(journal, "provisioning staging journal");
8183
+ const name = this.fs.readFileSync(journal, "utf8").trim();
8184
+ if (!/^live\.staging-[0-9a-f]{32}$/.test(name)) throw new CoworkStorageError("invalid provisioning staging journal");
8185
+ targets.push(join3(roomDir, name));
8186
+ }
8187
+ for (const target of targets) {
8188
+ const stat = this.lstatIfPresent(target);
8189
+ if (!stat) continue;
8190
+ if (stat.isSymbolicLink() || !stat.isDirectory()) this.fs.unlinkSync(target);
8191
+ else this.fs.rmSync(target, { recursive: true, force: true });
8192
+ this.fsyncDirectory(roomDir);
8193
+ }
8194
+ if (this.lstatIfPresent(journal)) {
8195
+ this.fs.unlinkSync(journal);
8196
+ this.fsyncDirectory(roomDir);
8197
+ }
8198
+ }
8199
+ atomicMetadataWrite(path, room) {
8200
+ this.atomicBytesWrite(path, Buffer.from(`${JSON.stringify(room)}
8201
+ `), "room metadata");
8202
+ }
7898
8203
  atomicBytesWrite(path, bytes, label) {
7899
8204
  if (this.lstatIfPresent(path)) this.assertRegularFile(path, label);
7900
8205
  const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
7901
8206
  let fd;
7902
8207
  try {
7903
- fd = this.fs.openSync(
7904
- temp,
7905
- nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
7906
- FILE_MODE2
7907
- );
7908
- this.validateOpenPath(fd, temp, `temporary ${label}`, "file", true);
7909
- this.fs.fchmodSync(fd, FILE_MODE2);
8208
+ fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
7910
8209
  this.writeAll(fd, bytes);
7911
8210
  this.fs.fsyncSync(fd);
7912
8211
  this.fs.closeSync(fd);
@@ -7914,11 +8213,9 @@ var init_storage = __esm({
7914
8213
  this.fs.renameSync(temp, path);
7915
8214
  this.fsyncDirectory(dirname2(path));
7916
8215
  } catch (error) {
7917
- if (fd !== void 0) {
7918
- try {
7919
- this.fs.closeSync(fd);
7920
- } catch {
7921
- }
8216
+ if (fd !== void 0) try {
8217
+ this.fs.closeSync(fd);
8218
+ } catch {
7922
8219
  }
7923
8220
  try {
7924
8221
  this.fs.rmSync(temp, { force: true });
@@ -7927,84 +8224,31 @@ var init_storage = __esm({
7927
8224
  throw this.wrap(`failed to write ${label} at ${path}`, error);
7928
8225
  }
7929
8226
  }
7930
- scanArchive(roomId) {
7931
- const path = this.archivePath(roomId);
7932
- this.assertRegularFile(path, "room archive");
7933
- let bytes;
7934
- try {
7935
- bytes = this.readFileNoFollow(path, "room archive");
7936
- } catch (error) {
7937
- throw this.wrap(`failed to read room "${roomId}" archive`, error);
7938
- }
7939
- const records = [];
7940
- let byteOffset = 0;
7941
- let expectedSequence = 1;
7942
- while (byteOffset < bytes.byteLength) {
7943
- const newline = bytes.indexOf(10, byteOffset);
7944
- if (newline === -1) {
7945
- throw new CoworkStorageError(`partial JSON record in room "${roomId}" archive at byte offset ${byteOffset}`);
7946
- }
7947
- const line = bytes.subarray(byteOffset, newline);
7948
- let decoded;
7949
- try {
7950
- decoded = JSON.parse(utf8Decoder.decode(line));
7951
- } catch (error) {
7952
- throw new CoworkStorageError(
7953
- `malformed JSON in room "${roomId}" archive at byte offset ${byteOffset}`,
7954
- { cause: error }
7955
- );
7956
- }
7957
- const observedSequence = typeof decoded === "object" && decoded !== null && "seq" in decoded ? decoded.seq : void 0;
7958
- if (observedSequence !== expectedSequence) {
7959
- throw new CoworkStorageError(
7960
- `non-monotonic sequence in room "${roomId}" archive at byte offset ${byteOffset}: expected ${expectedSequence}, found ${String(observedSequence)}`
7961
- );
7962
- }
7963
- let record;
7964
- try {
7965
- record = CommunicationRecordSchema.parse(decoded);
7966
- } catch (error) {
7967
- throw new CoworkStorageError(
7968
- `invalid record in room "${roomId}" archive at byte offset ${byteOffset}`,
7969
- { cause: error }
7970
- );
7971
- }
7972
- if (record.room_id !== roomId) {
7973
- throw new CoworkStorageError(
7974
- `record room_id mismatch in room "${roomId}" archive at byte offset ${byteOffset}`
7975
- );
7976
- }
7977
- records.push(record);
7978
- expectedSequence += 1;
7979
- byteOffset = newline + 1;
7980
- }
7981
- return { records, nextSequence: expectedSequence };
7982
- }
7983
8227
  ensureBaseDirectories() {
7984
8228
  this.ensurePrivateDirectory(this.stateDir, true, "state directory");
7985
8229
  this.ensurePrivateDirectory(this.roomsDirectory(), true, "rooms directory");
7986
8230
  }
8231
+ assertRoomDirectory(roomId) {
8232
+ this.ensureBaseDirectories();
8233
+ this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
8234
+ }
7987
8235
  ensurePrivateDirectory(path, create, label) {
7988
8236
  let stat = this.lstatIfPresent(path);
7989
- const created = stat === void 0;
7990
- if (created) {
8237
+ if (!stat) {
7991
8238
  if (!create) throw new CoworkStorageError(`${label} does not exist`);
7992
8239
  this.createPrivateDirectoryTree(path, label);
7993
8240
  stat = this.fs.lstatSync(path);
7994
8241
  }
7995
- const current = stat;
7996
- if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
7997
- if (!current.isDirectory()) throw new CoworkStorageError(`${label} is not a directory`);
8242
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} is not a private directory`);
7998
8243
  this.fs.chmodSync(path, DIRECTORY_MODE2);
7999
8244
  }
8000
8245
  createPrivateDirectoryTree(path, label) {
8001
8246
  const missing = [];
8002
8247
  let cursor = path;
8003
8248
  for (; ; ) {
8004
- const existing = this.lstatIfPresent(cursor);
8005
- if (existing) {
8006
- if (existing.isSymbolicLink()) throw new CoworkStorageError(`${label} parent must not be a symbolic link (symlink)`);
8007
- if (!existing.isDirectory()) throw new CoworkStorageError(`${label} parent is not a directory`);
8249
+ const stat = this.lstatIfPresent(cursor);
8250
+ if (stat) {
8251
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} parent is unsafe`);
8008
8252
  break;
8009
8253
  }
8010
8254
  missing.push(cursor);
@@ -8012,120 +8256,45 @@ var init_storage = __esm({
8012
8256
  if (parent === cursor) throw new CoworkStorageError(`cannot find existing parent for ${label}`);
8013
8257
  cursor = parent;
8014
8258
  }
8015
- const created = [];
8016
- try {
8017
- for (const directory of missing.reverse()) {
8018
- this.fs.mkdirSync(directory, { mode: DIRECTORY_MODE2 });
8019
- created.push(directory);
8020
- this.fs.chmodSync(directory, DIRECTORY_MODE2);
8021
- this.fsyncDirectory(directory);
8022
- this.fsyncDirectory(dirname2(directory));
8023
- }
8024
- } catch (error) {
8025
- for (const directory of created.reverse()) {
8026
- try {
8027
- this.fs.rmdirSync(directory);
8028
- this.fsyncDirectory(dirname2(directory));
8029
- } catch {
8030
- }
8031
- }
8032
- throw error;
8259
+ for (const directory of missing.reverse()) {
8260
+ this.fs.mkdirSync(directory, { mode: DIRECTORY_MODE2 });
8261
+ this.fs.chmodSync(directory, DIRECTORY_MODE2);
8262
+ this.fsyncDirectory(directory);
8263
+ this.fsyncDirectory(dirname2(directory));
8033
8264
  }
8034
8265
  }
8035
- assertRoomDirectory(roomId) {
8036
- this.ensureBaseDirectories();
8037
- this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
8038
- }
8039
8266
  rejectSymlink(path, label) {
8040
- if (this.fs.lstatSync(path).isSymbolicLink()) {
8041
- throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
8042
- }
8267
+ if (this.fs.lstatSync(path).isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
8043
8268
  }
8044
8269
  assertRegularFile(path, label) {
8045
8270
  this.rejectSymlink(path, label);
8046
8271
  const stat = this.fs.lstatSync(path);
8047
- if (!stat.isFile()) throw new CoworkStorageError(`${label} is not a regular file`);
8048
- if (stat.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${stat.nlink}`);
8049
- }
8050
- lstatIfPresent(path) {
8051
- try {
8052
- return this.fs.lstatSync(path);
8053
- } catch (error) {
8054
- if (error.code === "ENOENT") return void 0;
8055
- throw error;
8056
- }
8272
+ if (!stat.isFile() || stat.nlink !== 1) throw new CoworkStorageError(`${label} is not a safe regular file`);
8057
8273
  }
8058
8274
  validateOpenPath(fd, path, label, kind, requireSingleLink) {
8059
8275
  const opened = this.fs.fstatSync(fd);
8060
8276
  const validKind = kind === "file" ? opened.isFile() : opened.isDirectory();
8061
8277
  if (!validKind) throw new CoworkStorageError(`${label} open descriptor is not a regular ${kind}`);
8062
- if (requireSingleLink && opened.nlink !== 1) {
8063
- throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
8064
- }
8278
+ if (requireSingleLink && opened.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
8065
8279
  const current = this.fs.lstatSync(path);
8066
8280
  if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
8067
- if (current.dev !== opened.dev || current.ino !== opened.ino) {
8068
- throw new CoworkStorageError(`${label} inode changed during open`);
8069
- }
8281
+ if (current.dev !== opened.dev || current.ino !== opened.ino) throw new CoworkStorageError(`${label} inode changed during open`);
8070
8282
  return opened;
8071
8283
  }
8072
- createArchive(roomId) {
8073
- const path = this.archivePath(roomId);
8074
- let fd;
8075
- try {
8076
- fd = this.fs.openSync(
8077
- path,
8078
- nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
8079
- FILE_MODE2
8080
- );
8081
- this.validateOpenPath(fd, path, "room archive", "file", true);
8082
- this.fs.fchmodSync(fd, FILE_MODE2);
8083
- this.fs.fsyncSync(fd);
8084
- } finally {
8085
- if (fd !== void 0) this.fs.closeSync(fd);
8086
- }
8087
- this.fsyncDirectory(dirname2(path));
8088
- }
8089
- atomicMetadataWrite(path, room) {
8090
- if (this.lstatIfPresent(path)) this.assertRegularFile(path, "room metadata");
8091
- const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
8092
- const bytes = Buffer.from(`${JSON.stringify(room)}
8093
- `, "utf8");
8094
- let fd;
8284
+ lstatIfPresent(path) {
8095
8285
  try {
8096
- fd = this.fs.openSync(
8097
- temp,
8098
- nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
8099
- FILE_MODE2
8100
- );
8101
- this.validateOpenPath(fd, temp, "temporary room metadata", "file", true);
8102
- this.fs.fchmodSync(fd, FILE_MODE2);
8103
- this.writeAll(fd, bytes);
8104
- this.fs.fsyncSync(fd);
8105
- this.fs.closeSync(fd);
8106
- fd = void 0;
8107
- this.fs.renameSync(temp, path);
8108
- this.fsyncDirectory(dirname2(path));
8286
+ return this.fs.lstatSync(path);
8109
8287
  } catch (error) {
8110
- if (fd !== void 0) {
8111
- try {
8112
- this.fs.closeSync(fd);
8113
- } catch {
8114
- }
8115
- }
8116
- try {
8117
- this.fs.rmSync(temp, { force: true });
8118
- } catch {
8119
- }
8120
- throw this.wrap(`failed to atomically persist ${path}`, error);
8288
+ if (error.code === "ENOENT") return void 0;
8289
+ throw error;
8121
8290
  }
8122
8291
  }
8123
8292
  writeAll(fd, bytes) {
8124
8293
  let offset = 0;
8125
- while (offset < bytes.byteLength) {
8126
- const written = this.fs.writeSync(fd, bytes, offset, bytes.byteLength - offset, null);
8127
- if (written <= 0) throw new CoworkStorageError("write made no progress");
8128
- offset += written;
8294
+ while (offset < bytes.length) {
8295
+ const n = this.fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
8296
+ if (n <= 0) throw new CoworkStorageError("write made no progress");
8297
+ offset += n;
8129
8298
  }
8130
8299
  }
8131
8300
  readFileNoFollow(path, label) {
@@ -8163,10 +8332,13 @@ var init_storage = __esm({
8163
8332
  return join3(this.roomDirectory(roomId), "room.json");
8164
8333
  }
8165
8334
  archivePath(roomId) {
8166
- return join3(this.roomDirectory(roomId), "archive.jsonl");
8335
+ return join3(this.roomDirectory(roomId), "archive.sqlite3");
8336
+ }
8337
+ blobsDirectory(roomId) {
8338
+ return join3(this.roomDirectory(roomId), "blobs");
8167
8339
  }
8168
8340
  wrap(message, error) {
8169
- return error instanceof CoworkStorageError ? new CoworkStorageError(`${message}: ${error.message}`, { cause: error }) : new CoworkStorageError(`${message}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
8341
+ return new CoworkStorageError(`${message}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
8170
8342
  }
8171
8343
  };
8172
8344
  }
@@ -9077,7 +9249,7 @@ import { randomBytes as randomBytes4 } from "node:crypto";
9077
9249
  import * as http from "node:http";
9078
9250
  import * as net from "node:net";
9079
9251
  import * as nodeFs4 from "node:fs";
9080
- import { basename, dirname as dirname3, join as join5 } from "node:path";
9252
+ import { basename as basename2, dirname as dirname3, join as join5 } from "node:path";
9081
9253
  function createServiceRoutes(service) {
9082
9254
  return {
9083
9255
  "room.create": { auth: true, run: (params2) => service.createRoom(params2) },
@@ -9743,7 +9915,7 @@ var init_transports = __esm({
9743
9915
  }
9744
9916
  async cleanupStalePrivateSockets() {
9745
9917
  const directory = dirname3(this.options.socketPath);
9746
- const prefix = `${basename(this.options.socketPath)}.private-`;
9918
+ const prefix = `${basename2(this.options.socketPath)}.private-`;
9747
9919
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
9748
9920
  const candidates = this.fs.readdirSync(directory).filter((name) => name.startsWith(prefix)).sort().slice(0, MAX_STALE_PRIVATE_SOCKET_CLEANUP);
9749
9921
  for (const name of candidates) {
@@ -10146,8 +10318,7 @@ var init_daemon_runtime = __esm({
10146
10318
  );
10147
10319
  this.service = this.options.service ?? new RoomService(
10148
10320
  this.store,
10149
- this.registry,
10150
- { identityNameMode: config.roomIdentity?.nameMode ?? "stable_id" }
10321
+ this.registry
10151
10322
  );
10152
10323
  serviceRef = this.service;
10153
10324
  this.hostStartAttempted = true;