@ours.network/cowork 1.0.1 → 1.0.2

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
- });
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
+ }
4590
4508
  }
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
- });
4632
- }
4633
- }
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
- };
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
- }
4894
- });
4895
-
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
4509
  }
4942
- });
4943
- } catch (error) {
4944
- throw new CoworkConfigError("invalid effective cowork config", { cause: error });
4510
+ };
4945
4511
  }
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.`
4512
+ });
4513
+
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
- }
5056
- }
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
4602
  }
5064
- assertOwner(stat, label);
5065
- }
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);
5072
4603
  }
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`);
5076
- }
4604
+ function defaultRoomName(roomId) {
4605
+ return `Room ${LowerCrockfordUlidSchema.parse(roomId).slice(0, 8)}`;
5077
4606
  }
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);
5090
- }
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
+ });
5091
4623
  }
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);
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
+ });
5099
4632
  }
5100
4633
  }
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;
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" });
5107
4639
  }
5108
- }
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
- };
4640
+ if (bytes.length !== record.size) {
4641
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["size"], message: "file size must match decoded bytes" });
4642
+ }
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" });
5135
4646
  }
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
4647
  }
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();
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
+ });
5152
4656
  }
5153
- });
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
+ });
4673
+ }
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
+ });
4681
+ }
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
+ });
5282
5027
  }
5283
5028
  }
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);
5290
- }
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);
5291
5037
  }
5292
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);
5136
+ }
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
+ });
5178
+ }
5179
+ seen.add(identity);
5180
+ }
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
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()
5293
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);
@@ -6297,7 +6265,6 @@ var init_service = __esm({
6297
6265
  nextMessageId;
6298
6266
  intake;
6299
6267
  provisioningCheckpoint;
6300
- identityNameMode;
6301
6268
  constructor(store, packets, options = {}) {
6302
6269
  this.store = store;
6303
6270
  this.packets = packets;
@@ -6306,7 +6273,6 @@ var init_service = __esm({
6306
6273
  this.nextMessageId = options.messageId ?? generateUlid;
6307
6274
  this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
6308
6275
  });
6309
- this.identityNameMode = options.identityNameMode ?? "stable_id";
6310
6276
  this.intake = new IntakePump(store, packets, {
6311
6277
  now: this.nowValue,
6312
6278
  messageId: this.nextMessageId
@@ -6316,7 +6282,7 @@ var init_service = __esm({
6316
6282
  const settings = CreateRoomInputSchema.parse(input);
6317
6283
  const roomId = LowerCrockfordUlidSchema.parse(this.nextRoomId());
6318
6284
  const roomName = settings.name ?? defaultRoomName(roomId);
6319
- const identityName = configuredRoomIdentityName(roomId, roomName, this.identityNameMode);
6285
+ const identityName = roomIdentityName(roomName);
6320
6286
  return this.lock(roomId, async () => {
6321
6287
  const provisional = RoomSchema.parse({
6322
6288
  version: 2,
@@ -10146,8 +10112,7 @@ var init_daemon_runtime = __esm({
10146
10112
  );
10147
10113
  this.service = this.options.service ?? new RoomService(
10148
10114
  this.store,
10149
- this.registry,
10150
- { identityNameMode: config.roomIdentity?.nameMode ?? "stable_id" }
10115
+ this.registry
10151
10116
  );
10152
10117
  serviceRef = this.service;
10153
10118
  this.hostStartAttempted = true;