@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/README.md +3 -3
- package/dist/cli.js +232 -250
- package/dist/daemon.js +1069 -1104
- package/dist/web/assets/app.js +1 -1
- package/docs/01-prerequisites.md +1 -1
- package/docs/02-installation.md +1 -1
- package/docs/03-configuration.md +1 -4
- package/docs/05-room-workflow.md +2 -2
- package/docs/08-backup-restore.md +2 -2
- package/docs/10-limitations.md +1 -1
- package/docs/11-web-console.md +1 -1
- package/package.json +3 -3
package/dist/daemon.js
CHANGED
|
@@ -4116,1181 +4116,1149 @@ var init_zod = __esm({
|
|
|
4116
4116
|
}
|
|
4117
4117
|
});
|
|
4118
4118
|
|
|
4119
|
-
// src/
|
|
4120
|
-
import
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
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
|
|
4146
|
-
|
|
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
|
|
4149
|
-
|
|
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
|
|
4152
|
-
|
|
4153
|
-
const
|
|
4154
|
-
|
|
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
|
|
4157
|
-
const
|
|
4158
|
-
const
|
|
4159
|
-
|
|
4160
|
-
|
|
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
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
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
|
|
4168
|
-
|
|
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
|
|
4171
|
-
const
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
const
|
|
4177
|
-
|
|
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
|
|
4180
|
-
|
|
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
|
|
4183
|
-
const
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
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
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
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
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
const
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
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
|
|
4231
|
-
|
|
4232
|
-
}
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
if (
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
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
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
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
|
-
|
|
4292
|
-
|
|
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
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
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
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
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
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
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
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
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
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
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
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
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
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
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
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
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
|
-
*
|
|
4549
|
-
*
|
|
4550
|
-
*
|
|
4551
|
-
*
|
|
4552
|
-
*
|
|
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
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
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
|
-
|
|
4496
|
+
if (signal.aborted) return;
|
|
4497
|
+
await sleep(backoffMs, signal);
|
|
4498
|
+
backoffMs = Math.min(backoffMs * 2, WATCH_RETRY_MAX_MS);
|
|
4572
4499
|
}
|
|
4573
|
-
}
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
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
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
"
|
|
4953
|
-
|
|
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
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
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
|
|
4968
|
-
|
|
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
|
|
4992
|
-
|
|
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
|
|
5000
|
-
|
|
5001
|
-
|
|
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
|
|
5018
|
-
|
|
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
|
|
5027
|
-
const
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
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
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
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
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
const
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
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
|
|
5074
|
-
|
|
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
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
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
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
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
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
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
|
-
|
|
5110
|
-
|
|
5111
|
-
"
|
|
5112
|
-
|
|
5113
|
-
|
|
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
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
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
|
|
5156
|
-
var
|
|
5157
|
-
"src/
|
|
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
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
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
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
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
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
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
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
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
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
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
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
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
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
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
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
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
|
-
*
|
|
5246
|
-
*
|
|
5247
|
-
*
|
|
5248
|
-
*
|
|
5249
|
-
*
|
|
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
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
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 (
|
|
5280
|
-
|
|
5281
|
-
|
|
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
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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;
|