@ours.network/cowork 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.js +232 -250
- package/dist/daemon.js +1838 -1667
- 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 +5 -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
|
-
});
|
|
4590
|
-
}
|
|
4591
|
-
byParticipant.set(seat.participant_id, seat);
|
|
4592
|
-
if (seat.state === "pending" || seat.state === "active") {
|
|
4593
|
-
if (authorizedCids.has(seat.identity)) {
|
|
4594
|
-
context.addIssue({
|
|
4595
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4596
|
-
path: ["seats", index, "identity"],
|
|
4597
|
-
message: "at most one pending or active seat may exist per CID"
|
|
4598
|
-
});
|
|
4599
|
-
}
|
|
4600
|
-
authorizedCids.add(seat.identity);
|
|
4601
|
-
}
|
|
4602
|
-
if (room.anonymous && seat.alias === void 0) {
|
|
4603
|
-
context.addIssue({
|
|
4604
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4605
|
-
path: ["seats", index, "alias"],
|
|
4606
|
-
message: "anonymous rooms require an alias on every seat"
|
|
4607
|
-
});
|
|
4608
|
-
}
|
|
4609
|
-
if (!room.anonymous && seat.alias !== void 0) {
|
|
4610
|
-
context.addIssue({
|
|
4611
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4612
|
-
path: ["seats", index, "alias"],
|
|
4613
|
-
message: "aliases are reserved for anonymous rooms"
|
|
4614
|
-
});
|
|
4615
|
-
}
|
|
4616
|
-
if (seat.state === "active" && seat.alias !== void 0) {
|
|
4617
|
-
if (activeAliases.has(seat.alias)) {
|
|
4618
|
-
context.addIssue({
|
|
4619
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4620
|
-
path: ["seats", index, "alias"],
|
|
4621
|
-
message: "active seats must hold distinct aliases"
|
|
4622
|
-
});
|
|
4623
|
-
}
|
|
4624
|
-
activeAliases.add(seat.alias);
|
|
4625
|
-
}
|
|
4626
|
-
if (seat.removed_epoch !== void 0 && seat.removed_epoch > room.membership_epoch) {
|
|
4627
|
-
context.addIssue({
|
|
4628
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4629
|
-
path: ["seats", index, "removed_epoch"],
|
|
4630
|
-
message: "removed_epoch cannot exceed the room membership_epoch"
|
|
4631
|
-
});
|
|
4500
|
+
}
|
|
4501
|
+
announce(identityName) {
|
|
4502
|
+
for (const listener of this.listeners) {
|
|
4503
|
+
try {
|
|
4504
|
+
listener(identityName);
|
|
4505
|
+
} catch (error) {
|
|
4506
|
+
this.log(`cowork SDK notification listener failed for ${identityName}:`, error);
|
|
4507
|
+
}
|
|
4632
4508
|
}
|
|
4633
4509
|
}
|
|
4634
|
-
for (const [index, seat] of room.seats.entries()) {
|
|
4635
|
-
if (seat.replaces_seat === void 0) continue;
|
|
4636
|
-
const predecessor = byParticipant.get(seat.replaces_seat);
|
|
4637
|
-
if (!predecessor || predecessor === seat || predecessor.state !== "removed") {
|
|
4638
|
-
context.addIssue({
|
|
4639
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4640
|
-
path: ["seats", index, "replaces_seat"],
|
|
4641
|
-
message: "replaces_seat must reference a removed seat in this room"
|
|
4642
|
-
});
|
|
4643
|
-
continue;
|
|
4644
|
-
}
|
|
4645
|
-
if (predecessor.role !== seat.role) {
|
|
4646
|
-
context.addIssue({
|
|
4647
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4648
|
-
path: ["seats", index, "role"],
|
|
4649
|
-
message: "a replacement seat must inherit the predecessor role"
|
|
4650
|
-
});
|
|
4651
|
-
}
|
|
4652
|
-
if (room.anonymous && seat.alias !== predecessor.alias) {
|
|
4653
|
-
context.addIssue({
|
|
4654
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4655
|
-
path: ["seats", index, "alias"],
|
|
4656
|
-
message: "an anonymous replacement seat must inherit the predecessor alias"
|
|
4657
|
-
});
|
|
4658
|
-
}
|
|
4659
|
-
}
|
|
4660
|
-
});
|
|
4661
|
-
RoomSchema = external_exports.preprocess((value) => {
|
|
4662
|
-
if (typeof value !== "object" || value === null) return value;
|
|
4663
|
-
const patch = {};
|
|
4664
|
-
if (!Object.hasOwn(value, "room_name")) {
|
|
4665
|
-
const roomId = value.room_id;
|
|
4666
|
-
if (typeof roomId === "string" && LowerCrockfordUlidSchema.safeParse(roomId).success) {
|
|
4667
|
-
patch.room_name = defaultRoomName(roomId);
|
|
4668
|
-
}
|
|
4669
|
-
}
|
|
4670
|
-
if (!Object.hasOwn(value, "rest_roles")) patch.rest_roles = [];
|
|
4671
|
-
if (Object.keys(patch).length === 0) return value;
|
|
4672
|
-
return { ...value, ...patch };
|
|
4673
|
-
}, CurrentRoomSchema);
|
|
4674
|
-
CreateRoomInputSchema = external_exports.object({
|
|
4675
|
-
name: RoomNameSchema.optional(),
|
|
4676
|
-
goal: MissionTextSchema,
|
|
4677
|
-
briefing: MissionTextSchema,
|
|
4678
|
-
anonymous: external_exports.boolean().optional(),
|
|
4679
|
-
quiet_membership: external_exports.boolean().optional()
|
|
4680
|
-
}).strict();
|
|
4681
|
-
UpdateRoomInputSchema = external_exports.object({
|
|
4682
|
-
name: RoomNameSchema.optional(),
|
|
4683
|
-
goal: MissionTextSchema.optional(),
|
|
4684
|
-
briefing: MissionTextSchema.optional(),
|
|
4685
|
-
status: NonEmptyStringSchema.optional(),
|
|
4686
|
-
quiet_membership: external_exports.boolean().optional()
|
|
4687
|
-
}).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
|
|
4688
|
-
RoleBriefingSetInputSchema = external_exports.object({
|
|
4689
|
-
role: RoleSchema,
|
|
4690
|
-
text: MissionTextSchema
|
|
4691
|
-
}).strict();
|
|
4692
|
-
RoleBriefingDeleteInputSchema = external_exports.object({
|
|
4693
|
-
role: RoleSchema
|
|
4694
|
-
}).strict();
|
|
4695
|
-
PostMessageInputSchema = external_exports.object({
|
|
4696
|
-
text: MessageTextSchema
|
|
4697
|
-
}).strict();
|
|
4698
|
-
PostAsRoleInputSchema = external_exports.object({
|
|
4699
|
-
role: RoleSchema,
|
|
4700
|
-
text: MessageTextSchema
|
|
4701
|
-
}).strict();
|
|
4702
|
-
RestRoleInputSchema = external_exports.object({
|
|
4703
|
-
role: RoleSchema
|
|
4704
|
-
}).strict();
|
|
4705
|
-
ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
|
|
4706
|
-
AcceptExternalInviteInputSchema = external_exports.object({
|
|
4707
|
-
role: RoleSchema,
|
|
4708
|
-
invite: external_exports.string().refine(
|
|
4709
|
-
(value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
|
|
4710
|
-
`invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
|
|
4711
|
-
),
|
|
4712
|
-
expected_cid: ContainerIdSchema.optional(),
|
|
4713
|
-
replaces_seat: LowerCrockfordUlidSchema.optional()
|
|
4714
|
-
}).strict();
|
|
4715
|
-
AuthorSnapshotSchema = external_exports.object({
|
|
4716
|
-
identity: NonEmptyStringSchema,
|
|
4717
|
-
display_name: NonEmptyStringSchema,
|
|
4718
|
-
role: RoleSchema
|
|
4719
|
-
}).strict();
|
|
4720
|
-
RecordCommonShape = {
|
|
4721
|
-
version: external_exports.literal(1),
|
|
4722
|
-
room_id: LowerCrockfordUlidSchema,
|
|
4723
|
-
seq: PositiveSafeIntegerSchema,
|
|
4724
|
-
record_id: NonEmptyStringSchema,
|
|
4725
|
-
at: Rfc3339Schema
|
|
4726
|
-
};
|
|
4727
|
-
AppendCommonShape = {
|
|
4728
|
-
version: external_exports.literal(1),
|
|
4729
|
-
room_id: LowerCrockfordUlidSchema,
|
|
4730
|
-
at: Rfc3339Schema
|
|
4731
|
-
};
|
|
4732
|
-
MembershipNoticeSchema = external_exports.object({
|
|
4733
|
-
action: external_exports.enum(["remove"]),
|
|
4734
|
-
alias: NonEmptyStringSchema.optional(),
|
|
4735
|
-
role: RoleSchema.optional(),
|
|
4736
|
-
epoch: external_exports.number().int().nonnegative().safe()
|
|
4737
|
-
}).strict();
|
|
4738
|
-
AuthorAliasSchema = external_exports.object({
|
|
4739
|
-
participant_id: LowerCrockfordUlidSchema,
|
|
4740
|
-
alias: NonEmptyStringSchema
|
|
4741
|
-
}).strict();
|
|
4742
|
-
ReplyReferenceSchema = external_exports.object({
|
|
4743
|
-
wire_id: NonEmptyStringSchema,
|
|
4744
|
-
sentence: PositiveSafeIntegerSchema.optional()
|
|
4745
|
-
}).strict();
|
|
4746
|
-
MessageShape = {
|
|
4747
|
-
kind: external_exports.literal("message"),
|
|
4748
|
-
message_id: LowerCrockfordUlidSchema,
|
|
4749
|
-
author: AuthorSnapshotSchema,
|
|
4750
|
-
author_alias: AuthorAliasSchema.optional(),
|
|
4751
|
-
category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
|
|
4752
|
-
briefing_role: RoleSchema.optional(),
|
|
4753
|
-
briefing_version: PositiveSafeIntegerSchema.optional(),
|
|
4754
|
-
membership: MembershipNoticeSchema.optional(),
|
|
4755
|
-
text: MessageTextSchema,
|
|
4756
|
-
recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
|
|
4757
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4758
|
-
for (const [index, identity] of identities.entries()) {
|
|
4759
|
-
if (seen.has(identity)) {
|
|
4760
|
-
context.addIssue({
|
|
4761
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4762
|
-
path: [index],
|
|
4763
|
-
message: "recipient identities must be unique"
|
|
4764
|
-
});
|
|
4765
|
-
}
|
|
4766
|
-
seen.add(identity);
|
|
4767
|
-
}
|
|
4768
|
-
}),
|
|
4769
|
-
source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
4770
|
-
source_wire_id: NonEmptyStringSchema.optional(),
|
|
4771
|
-
source_reply_to: ReplyReferenceSchema.optional()
|
|
4772
|
-
};
|
|
4773
|
-
RelayIntentShape = {
|
|
4774
|
-
kind: external_exports.literal("relay_intent"),
|
|
4775
|
-
message_id: LowerCrockfordUlidSchema.optional(),
|
|
4776
|
-
file_id: LowerCrockfordUlidSchema.optional(),
|
|
4777
|
-
recipient_identity: NonEmptyStringSchema
|
|
4778
|
-
};
|
|
4779
|
-
RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
|
|
4780
|
-
RelayResultShape = {
|
|
4781
|
-
kind: external_exports.literal("relay_result"),
|
|
4782
|
-
intent_record_id: NonEmptyStringSchema,
|
|
4783
|
-
message_id: LowerCrockfordUlidSchema.optional(),
|
|
4784
|
-
file_id: LowerCrockfordUlidSchema.optional(),
|
|
4785
|
-
recipient_identity: NonEmptyStringSchema,
|
|
4786
|
-
status: RelayResultStatusSchema,
|
|
4787
|
-
wire_id: NonEmptyStringSchema.optional(),
|
|
4788
|
-
metadata_wire_id: NonEmptyStringSchema.optional()
|
|
4789
4510
|
};
|
|
4790
|
-
FileShape = {
|
|
4791
|
-
kind: external_exports.literal("file"),
|
|
4792
|
-
file_id: LowerCrockfordUlidSchema,
|
|
4793
|
-
author: AuthorSnapshotSchema,
|
|
4794
|
-
author_alias: AuthorAliasSchema.optional(),
|
|
4795
|
-
filename: FileNameSchema,
|
|
4796
|
-
mime: FileMimeSchema,
|
|
4797
|
-
size: external_exports.number().int().nonnegative().max(MAX_FILE_BYTES),
|
|
4798
|
-
sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
4799
|
-
data_base64: external_exports.string(),
|
|
4800
|
-
recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
|
|
4801
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4802
|
-
for (const [index, identity] of identities.entries()) {
|
|
4803
|
-
if (seen.has(identity)) {
|
|
4804
|
-
context.addIssue({
|
|
4805
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4806
|
-
path: [index],
|
|
4807
|
-
message: "recipient identities must be unique"
|
|
4808
|
-
});
|
|
4809
|
-
}
|
|
4810
|
-
seen.add(identity);
|
|
4811
|
-
}
|
|
4812
|
-
}),
|
|
4813
|
-
source_file_id: external_exports.number().int().nonnegative().safe(),
|
|
4814
|
-
source_wire_id: NonEmptyStringSchema.optional(),
|
|
4815
|
-
source_reply_to: ReplyReferenceSchema.optional()
|
|
4816
|
-
};
|
|
4817
|
-
MembershipIntentShape = {
|
|
4818
|
-
kind: external_exports.literal("membership_intent"),
|
|
4819
|
-
action: external_exports.enum(["remove"]),
|
|
4820
|
-
participant_id: LowerCrockfordUlidSchema,
|
|
4821
|
-
recipient_identity: NonEmptyStringSchema,
|
|
4822
|
-
role: RoleSchema,
|
|
4823
|
-
alias: NonEmptyStringSchema.optional(),
|
|
4824
|
-
epoch: PositiveSafeIntegerSchema,
|
|
4825
|
-
notify: external_exports.boolean()
|
|
4826
|
-
};
|
|
4827
|
-
MembershipResultShape = {
|
|
4828
|
-
kind: external_exports.literal("membership_result"),
|
|
4829
|
-
intent_record_id: NonEmptyStringSchema,
|
|
4830
|
-
participant_id: LowerCrockfordUlidSchema,
|
|
4831
|
-
status: RelayStatusSchema,
|
|
4832
|
-
notified: external_exports.boolean(),
|
|
4833
|
-
key_material_retained: external_exports.literal(true),
|
|
4834
|
-
uncertain_after_restart: external_exports.literal(true).optional()
|
|
4835
|
-
};
|
|
4836
|
-
CloseNoticeIntentShape = {
|
|
4837
|
-
kind: external_exports.literal("close_notice_intent"),
|
|
4838
|
-
recipient_identity: NonEmptyStringSchema
|
|
4839
|
-
};
|
|
4840
|
-
CloseNoticeResultShape = {
|
|
4841
|
-
kind: external_exports.literal("close_notice_result"),
|
|
4842
|
-
intent_record_id: NonEmptyStringSchema,
|
|
4843
|
-
recipient_identity: NonEmptyStringSchema,
|
|
4844
|
-
status: RelayStatusSchema,
|
|
4845
|
-
notified: external_exports.boolean(),
|
|
4846
|
-
key_material_retained: external_exports.literal(true),
|
|
4847
|
-
uncertain_after_restart: external_exports.literal(true).optional()
|
|
4848
|
-
};
|
|
4849
|
-
MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
|
|
4850
|
-
FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
|
|
4851
|
-
RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
|
|
4852
|
-
RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
|
|
4853
|
-
MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
|
|
4854
|
-
MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
|
|
4855
|
-
CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
|
|
4856
|
-
CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
|
|
4857
|
-
RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
4858
|
-
MessageRecordSchema,
|
|
4859
|
-
FileRecordSchema,
|
|
4860
|
-
RelayIntentRecordSchema,
|
|
4861
|
-
RelayResultRecordSchema,
|
|
4862
|
-
MembershipIntentRecordSchema,
|
|
4863
|
-
MembershipResultRecordSchema,
|
|
4864
|
-
CloseNoticeIntentRecordSchema,
|
|
4865
|
-
CloseNoticeResultRecordSchema
|
|
4866
|
-
]);
|
|
4867
|
-
CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
|
|
4868
|
-
if (record.record_id !== `${record.room_id}:${record.seq}`) {
|
|
4869
|
-
context.addIssue({
|
|
4870
|
-
code: external_exports.ZodIssueCode.custom,
|
|
4871
|
-
path: ["record_id"],
|
|
4872
|
-
message: 'record_id must equal room_id + ":" + seq'
|
|
4873
|
-
});
|
|
4874
|
-
}
|
|
4875
|
-
if (record.kind === "message") refineMessageCategory(record, context);
|
|
4876
|
-
refineRelaySubject(record, context);
|
|
4877
|
-
refineFileRecord(record, context);
|
|
4878
|
-
});
|
|
4879
|
-
AppendRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
4880
|
-
external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
|
|
4881
|
-
external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
|
|
4882
|
-
external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
|
|
4883
|
-
external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
|
|
4884
|
-
external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
|
|
4885
|
-
external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
|
|
4886
|
-
external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
|
|
4887
|
-
external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
|
|
4888
|
-
]).superRefine((record, context) => {
|
|
4889
|
-
if (record.kind === "message") refineMessageCategory(record, context);
|
|
4890
|
-
refineRelaySubject(record, context);
|
|
4891
|
-
refineFileRecord(record, context);
|
|
4892
|
-
});
|
|
4893
4511
|
}
|
|
4894
4512
|
});
|
|
4895
4513
|
|
|
4896
|
-
// src/
|
|
4897
|
-
import
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
version: 1,
|
|
4903
|
-
stateDir: resolve(home, ".ours-cowork"),
|
|
4904
|
-
roomIdentity: { nameMode: "stable_id" },
|
|
4905
|
-
rest: { enabled: true, port: 3052 }
|
|
4906
|
-
};
|
|
4907
|
-
}
|
|
4908
|
-
function loadConfig(env = process.env, io = {}) {
|
|
4909
|
-
rejectRemovedEnvironment(env);
|
|
4910
|
-
const fs2 = io.fs ?? nodeFs;
|
|
4911
|
-
const defaults = defaultConfig(io.home);
|
|
4912
|
-
const configPath = resolve(env.OURS_COWORK_CONFIG ?? join(io.home ?? homedir(), ".ours-cowork", "config.json"));
|
|
4913
|
-
let file = defaults;
|
|
4914
|
-
const stat = lstatIfPresent(fs2, configPath);
|
|
4915
|
-
if (stat) {
|
|
4916
|
-
assertSecureFile(fs2, configPath, "config file");
|
|
4917
|
-
let parsed;
|
|
4918
|
-
try {
|
|
4919
|
-
parsed = JSON.parse(readSecureFile(fs2, configPath, "config file").toString("utf8"));
|
|
4920
|
-
} catch (error) {
|
|
4921
|
-
throw new CoworkConfigError(`malformed cowork config at ${configPath}`, { cause: error });
|
|
4922
|
-
}
|
|
4923
|
-
rejectRemovedConfig(parsed, configPath);
|
|
4924
|
-
try {
|
|
4925
|
-
file = CoworkConfigSchema.parse(parsed);
|
|
4926
|
-
} catch (error) {
|
|
4927
|
-
throw new CoworkConfigError(`invalid cowork config at ${configPath}`, { cause: error });
|
|
4928
|
-
}
|
|
4929
|
-
} else if (env.OURS_COWORK_CONFIG !== void 0) {
|
|
4930
|
-
throw new CoworkConfigError(`configured cowork config does not exist: ${configPath}`);
|
|
4931
|
-
}
|
|
4932
|
-
const restPort = env.OURS_COWORK_REST_PORT === void 0 ? void 0 : parsePort(env.OURS_COWORK_REST_PORT);
|
|
4933
|
-
try {
|
|
4934
|
-
return CoworkConfigSchema.parse({
|
|
4935
|
-
version: 1,
|
|
4936
|
-
stateDir: resolve(env.OURS_COWORK_STATE_DIR ?? file.stateDir),
|
|
4937
|
-
roomIdentity: file.roomIdentity,
|
|
4938
|
-
rest: {
|
|
4939
|
-
enabled: restPort === void 0 ? file.rest.enabled : true,
|
|
4940
|
-
port: restPort ?? file.rest.port
|
|
4941
|
-
}
|
|
4942
|
-
});
|
|
4943
|
-
} catch (error) {
|
|
4944
|
-
throw new CoworkConfigError("invalid effective cowork config", { cause: error });
|
|
4945
|
-
}
|
|
4946
|
-
}
|
|
4947
|
-
function rejectRemovedEnvironment(env) {
|
|
4948
|
-
const removed = [
|
|
4949
|
-
"OURS_COWORK_BROKER_URL",
|
|
4950
|
-
"OURS_COWORK_DAEMON_MODE",
|
|
4951
|
-
"OURS_COWORK_DAEMON_ENDPOINT",
|
|
4952
|
-
"OURS_COWORK_DAEMON_STATE_DIR"
|
|
4953
|
-
].filter((name) => env[name] !== void 0);
|
|
4954
|
-
if (removed.length === 0) return;
|
|
4955
|
-
throw new CoworkConfigError(
|
|
4956
|
-
`${removed.join(", ")} ${removed.length === 1 ? "was" : "were"} removed: ours-cowork now attaches only to the shared ours daemon. Configure that daemon with @ours.network/cli and select it through the standard OURS_CONFIG, OURS_PORT, and OURS_STATE_DIR inputs.`
|
|
4514
|
+
// src/contracts.ts
|
|
4515
|
+
import { createHash } from "node:crypto";
|
|
4516
|
+
function utf8Bounded(label, maximumBytes) {
|
|
4517
|
+
return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
|
|
4518
|
+
(value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
|
|
4519
|
+
`${label} must be at most ${maximumBytes} UTF-8 bytes`
|
|
4957
4520
|
);
|
|
4958
4521
|
}
|
|
4959
|
-
function
|
|
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
4602
|
}
|
|
5056
4603
|
}
|
|
5057
|
-
function
|
|
5058
|
-
|
|
5059
|
-
if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
|
|
5060
|
-
if (!stat.isDirectory()) throw new CoworkConfigError(`${label} must be a directory`);
|
|
5061
|
-
if ((stat.mode & 511) !== DIRECTORY_MODE) {
|
|
5062
|
-
throw new CoworkConfigError(`${label} mode must be 0700`);
|
|
5063
|
-
}
|
|
5064
|
-
assertOwner(stat, label);
|
|
4604
|
+
function defaultRoomName(roomId) {
|
|
4605
|
+
return `Room ${LowerCrockfordUlidSchema.parse(roomId).slice(0, 8)}`;
|
|
5065
4606
|
}
|
|
5066
|
-
function
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
4607
|
+
function migrateRoomV1(room, mintParticipantId) {
|
|
4608
|
+
return RoomSchema.parse({
|
|
4609
|
+
...room,
|
|
4610
|
+
version: 2,
|
|
4611
|
+
mission: { ...room.mission, briefing_version: 1 },
|
|
4612
|
+
role_briefings: {},
|
|
4613
|
+
rest_roles: [],
|
|
4614
|
+
anonymous: false,
|
|
4615
|
+
quiet_membership: false,
|
|
4616
|
+
membership_epoch: 0,
|
|
4617
|
+
seats: room.seats.map((seat) => ({
|
|
4618
|
+
...seat,
|
|
4619
|
+
participant_id: LowerCrockfordUlidSchema.parse(mintParticipantId()),
|
|
4620
|
+
state: "active"
|
|
4621
|
+
}))
|
|
4622
|
+
});
|
|
5072
4623
|
}
|
|
5073
|
-
function
|
|
5074
|
-
if (
|
|
5075
|
-
|
|
4624
|
+
function refineRelaySubject(record, context) {
|
|
4625
|
+
if (record.kind !== "relay_intent" && record.kind !== "relay_result") return;
|
|
4626
|
+
if (record.message_id === void 0 === (record.file_id === void 0)) {
|
|
4627
|
+
context.addIssue({
|
|
4628
|
+
code: external_exports.ZodIssueCode.custom,
|
|
4629
|
+
path: ["message_id"],
|
|
4630
|
+
message: "relay records require exactly one of message_id or file_id"
|
|
4631
|
+
});
|
|
5076
4632
|
}
|
|
5077
4633
|
}
|
|
5078
|
-
function
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
const current = fs2.lstatSync(path);
|
|
5084
|
-
if (!opened.isFile() || opened.nlink !== 1 || (opened.mode & 511) !== FILE_MODE || typeof process.getuid === "function" && opened.uid !== process.getuid() || current.isSymbolicLink() || current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
5085
|
-
throw new CoworkConfigError(`${label} changed while opening`);
|
|
5086
|
-
}
|
|
5087
|
-
return fs2.readFileSync(fd);
|
|
5088
|
-
} finally {
|
|
5089
|
-
if (fd !== void 0) fs2.closeSync(fd);
|
|
4634
|
+
function refineFileRecord(record, context) {
|
|
4635
|
+
if (record.kind !== "file" || record.data_base64 === void 0) return;
|
|
4636
|
+
const bytes = Buffer.from(record.data_base64, "base64");
|
|
4637
|
+
if (bytes.toString("base64") !== record.data_base64) {
|
|
4638
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["data_base64"], message: "file bytes must use canonical base64" });
|
|
5090
4639
|
}
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
let fd;
|
|
5094
|
-
try {
|
|
5095
|
-
fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
|
|
5096
|
-
fs2.fsyncSync(fd);
|
|
5097
|
-
} finally {
|
|
5098
|
-
if (fd !== void 0) fs2.closeSync(fd);
|
|
4640
|
+
if (bytes.length !== record.size) {
|
|
4641
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["size"], message: "file size must match decoded bytes" });
|
|
5099
4642
|
}
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
return fs2.lstatSync(path);
|
|
5104
|
-
} catch (error) {
|
|
5105
|
-
if (error.code === "ENOENT") return void 0;
|
|
5106
|
-
throw error;
|
|
4643
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
4644
|
+
if (digest !== record.sha256) {
|
|
4645
|
+
context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["sha256"], message: "file sha256 must match decoded bytes" });
|
|
5107
4646
|
}
|
|
5108
4647
|
}
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
})
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
};
|
|
4648
|
+
function refineMessageCategory(message, context) {
|
|
4649
|
+
const requires = (field, present) => {
|
|
4650
|
+
if (present && message[field] === void 0) {
|
|
4651
|
+
context.addIssue({
|
|
4652
|
+
code: external_exports.ZodIssueCode.custom,
|
|
4653
|
+
path: [field],
|
|
4654
|
+
message: `${message.category} messages require ${field}`
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4657
|
+
if (!present && message[field] !== void 0) {
|
|
4658
|
+
context.addIssue({
|
|
4659
|
+
code: external_exports.ZodIssueCode.custom,
|
|
4660
|
+
path: [field],
|
|
4661
|
+
message: `${field} is forbidden on ${message.category} messages`
|
|
4662
|
+
});
|
|
4663
|
+
}
|
|
4664
|
+
};
|
|
4665
|
+
requires("briefing_role", message.category === "role_briefing");
|
|
4666
|
+
requires("membership", message.category === "membership");
|
|
4667
|
+
if (message.category === "role_briefing" && message.briefing_version === void 0) {
|
|
4668
|
+
context.addIssue({
|
|
4669
|
+
code: external_exports.ZodIssueCode.custom,
|
|
4670
|
+
path: ["briefing_version"],
|
|
4671
|
+
message: "role_briefing messages require briefing_version"
|
|
4672
|
+
});
|
|
5135
4673
|
}
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
}
|
|
5142
|
-
|
|
5143
|
-
}
|
|
5144
|
-
function sleep(ms, signal) {
|
|
5145
|
-
return new Promise((resolveSleep) => {
|
|
5146
|
-
const timer = setTimeout(finish, ms);
|
|
5147
|
-
signal.addEventListener("abort", finish, { once: true });
|
|
5148
|
-
function finish() {
|
|
5149
|
-
clearTimeout(timer);
|
|
5150
|
-
signal.removeEventListener("abort", finish);
|
|
5151
|
-
resolveSleep();
|
|
4674
|
+
if (message.category === "chat" || message.category === "membership") {
|
|
4675
|
+
if (message.briefing_version !== void 0) {
|
|
4676
|
+
context.addIssue({
|
|
4677
|
+
code: external_exports.ZodIssueCode.custom,
|
|
4678
|
+
path: ["briefing_version"],
|
|
4679
|
+
message: `briefing_version is forbidden on ${message.category} messages`
|
|
4680
|
+
});
|
|
5152
4681
|
}
|
|
5153
|
-
}
|
|
4682
|
+
}
|
|
5154
4683
|
}
|
|
5155
|
-
var
|
|
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
|
+
});
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
});
|
|
5030
|
+
RoomSchema = external_exports.preprocess((value) => {
|
|
5031
|
+
if (typeof value !== "object" || value === null) return value;
|
|
5032
|
+
const patch = {};
|
|
5033
|
+
if (!Object.hasOwn(value, "room_name")) {
|
|
5034
|
+
const roomId = value.room_id;
|
|
5035
|
+
if (typeof roomId === "string" && LowerCrockfordUlidSchema.safeParse(roomId).success) {
|
|
5036
|
+
patch.room_name = defaultRoomName(roomId);
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
if (!Object.hasOwn(value, "rest_roles")) patch.rest_roles = [];
|
|
5040
|
+
if (Object.keys(patch).length === 0) return value;
|
|
5041
|
+
return { ...value, ...patch };
|
|
5042
|
+
}, CurrentRoomSchema);
|
|
5043
|
+
CreateRoomInputSchema = external_exports.object({
|
|
5044
|
+
name: RoomNameSchema.optional(),
|
|
5045
|
+
goal: MissionTextSchema,
|
|
5046
|
+
briefing: MissionTextSchema,
|
|
5047
|
+
anonymous: external_exports.boolean().optional(),
|
|
5048
|
+
quiet_membership: external_exports.boolean().optional()
|
|
5049
|
+
}).strict();
|
|
5050
|
+
UpdateRoomInputSchema = external_exports.object({
|
|
5051
|
+
name: RoomNameSchema.optional(),
|
|
5052
|
+
goal: MissionTextSchema.optional(),
|
|
5053
|
+
briefing: MissionTextSchema.optional(),
|
|
5054
|
+
status: NonEmptyStringSchema.optional(),
|
|
5055
|
+
quiet_membership: external_exports.boolean().optional()
|
|
5056
|
+
}).strict().refine((input) => Object.keys(input).length > 0, "at least one setting is required");
|
|
5057
|
+
RoleBriefingSetInputSchema = external_exports.object({
|
|
5058
|
+
role: RoleSchema,
|
|
5059
|
+
text: MissionTextSchema
|
|
5060
|
+
}).strict();
|
|
5061
|
+
RoleBriefingDeleteInputSchema = external_exports.object({
|
|
5062
|
+
role: RoleSchema
|
|
5063
|
+
}).strict();
|
|
5064
|
+
PostMessageInputSchema = external_exports.object({
|
|
5065
|
+
text: MessageTextSchema
|
|
5066
|
+
}).strict();
|
|
5067
|
+
PostAsRoleInputSchema = external_exports.object({
|
|
5068
|
+
role: RoleSchema,
|
|
5069
|
+
text: MessageTextSchema
|
|
5070
|
+
}).strict();
|
|
5071
|
+
RestRoleInputSchema = external_exports.object({
|
|
5072
|
+
role: RoleSchema
|
|
5073
|
+
}).strict();
|
|
5074
|
+
ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
|
|
5075
|
+
AcceptExternalInviteInputSchema = external_exports.object({
|
|
5076
|
+
role: RoleSchema,
|
|
5077
|
+
invite: external_exports.string().refine(
|
|
5078
|
+
(value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
|
|
5079
|
+
`invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
|
|
5080
|
+
),
|
|
5081
|
+
expected_cid: ContainerIdSchema.optional(),
|
|
5082
|
+
replaces_seat: LowerCrockfordUlidSchema.optional()
|
|
5083
|
+
}).strict();
|
|
5084
|
+
AuthorSnapshotSchema = external_exports.object({
|
|
5085
|
+
identity: NonEmptyStringSchema,
|
|
5086
|
+
display_name: NonEmptyStringSchema,
|
|
5087
|
+
role: RoleSchema
|
|
5088
|
+
}).strict();
|
|
5089
|
+
RecordCommonShape = {
|
|
5090
|
+
version: external_exports.literal(1),
|
|
5091
|
+
room_id: LowerCrockfordUlidSchema,
|
|
5092
|
+
seq: PositiveSafeIntegerSchema,
|
|
5093
|
+
record_id: NonEmptyStringSchema,
|
|
5094
|
+
at: Rfc3339Schema
|
|
5095
|
+
};
|
|
5096
|
+
AppendCommonShape = {
|
|
5097
|
+
version: external_exports.literal(1),
|
|
5098
|
+
room_id: LowerCrockfordUlidSchema,
|
|
5099
|
+
at: Rfc3339Schema
|
|
5100
|
+
};
|
|
5101
|
+
MembershipNoticeSchema = external_exports.object({
|
|
5102
|
+
action: external_exports.enum(["remove"]),
|
|
5103
|
+
alias: NonEmptyStringSchema.optional(),
|
|
5104
|
+
role: RoleSchema.optional(),
|
|
5105
|
+
epoch: external_exports.number().int().nonnegative().safe()
|
|
5106
|
+
}).strict();
|
|
5107
|
+
AuthorAliasSchema = external_exports.object({
|
|
5108
|
+
participant_id: LowerCrockfordUlidSchema,
|
|
5109
|
+
alias: NonEmptyStringSchema
|
|
5110
|
+
}).strict();
|
|
5111
|
+
ReplyReferenceSchema = external_exports.object({
|
|
5112
|
+
wire_id: NonEmptyStringSchema,
|
|
5113
|
+
sentence: PositiveSafeIntegerSchema.optional()
|
|
5114
|
+
}).strict();
|
|
5115
|
+
MessageShape = {
|
|
5116
|
+
kind: external_exports.literal("message"),
|
|
5117
|
+
message_id: LowerCrockfordUlidSchema,
|
|
5118
|
+
author: AuthorSnapshotSchema,
|
|
5119
|
+
author_alias: AuthorAliasSchema.optional(),
|
|
5120
|
+
category: external_exports.enum(["briefing", "role_briefing", "chat", "membership"]),
|
|
5121
|
+
briefing_role: RoleSchema.optional(),
|
|
5122
|
+
briefing_version: PositiveSafeIntegerSchema.optional(),
|
|
5123
|
+
membership: MembershipNoticeSchema.optional(),
|
|
5124
|
+
text: MessageTextSchema,
|
|
5125
|
+
recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
|
|
5126
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5127
|
+
for (const [index, identity] of identities.entries()) {
|
|
5128
|
+
if (seen.has(identity)) {
|
|
5129
|
+
context.addIssue({
|
|
5130
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5131
|
+
path: [index],
|
|
5132
|
+
message: "recipient identities must be unique"
|
|
5133
|
+
});
|
|
5134
|
+
}
|
|
5135
|
+
seen.add(identity);
|
|
5282
5136
|
}
|
|
5283
|
-
}
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5137
|
+
}),
|
|
5138
|
+
source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
5139
|
+
source_wire_id: NonEmptyStringSchema.optional(),
|
|
5140
|
+
source_reply_to: ReplyReferenceSchema.optional()
|
|
5141
|
+
};
|
|
5142
|
+
RelayIntentShape = {
|
|
5143
|
+
kind: external_exports.literal("relay_intent"),
|
|
5144
|
+
message_id: LowerCrockfordUlidSchema.optional(),
|
|
5145
|
+
file_id: LowerCrockfordUlidSchema.optional(),
|
|
5146
|
+
recipient_identity: NonEmptyStringSchema
|
|
5147
|
+
};
|
|
5148
|
+
RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
|
|
5149
|
+
RelayResultShape = {
|
|
5150
|
+
kind: external_exports.literal("relay_result"),
|
|
5151
|
+
intent_record_id: NonEmptyStringSchema,
|
|
5152
|
+
message_id: LowerCrockfordUlidSchema.optional(),
|
|
5153
|
+
file_id: LowerCrockfordUlidSchema.optional(),
|
|
5154
|
+
recipient_identity: NonEmptyStringSchema,
|
|
5155
|
+
status: RelayResultStatusSchema,
|
|
5156
|
+
wire_id: NonEmptyStringSchema.optional(),
|
|
5157
|
+
metadata_wire_id: NonEmptyStringSchema.optional()
|
|
5158
|
+
};
|
|
5159
|
+
FileShape = {
|
|
5160
|
+
kind: external_exports.literal("file"),
|
|
5161
|
+
file_id: LowerCrockfordUlidSchema,
|
|
5162
|
+
author: AuthorSnapshotSchema,
|
|
5163
|
+
author_alias: AuthorAliasSchema.optional(),
|
|
5164
|
+
filename: FileNameSchema,
|
|
5165
|
+
mime: FileMimeSchema,
|
|
5166
|
+
size: external_exports.number().int().nonnegative().max(MAX_FILE_BYTES),
|
|
5167
|
+
sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
5168
|
+
data_base64: external_exports.string(),
|
|
5169
|
+
recipient_identities: external_exports.array(NonEmptyStringSchema).superRefine((identities, context) => {
|
|
5170
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5171
|
+
for (const [index, identity] of identities.entries()) {
|
|
5172
|
+
if (seen.has(identity)) {
|
|
5173
|
+
context.addIssue({
|
|
5174
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5175
|
+
path: [index],
|
|
5176
|
+
message: "recipient identities must be unique"
|
|
5177
|
+
});
|
|
5290
5178
|
}
|
|
5179
|
+
seen.add(identity);
|
|
5291
5180
|
}
|
|
5292
|
-
}
|
|
5181
|
+
}),
|
|
5182
|
+
source_file_id: external_exports.number().int().nonnegative().safe(),
|
|
5183
|
+
source_wire_id: NonEmptyStringSchema.optional(),
|
|
5184
|
+
source_reply_to: ReplyReferenceSchema.optional()
|
|
5185
|
+
};
|
|
5186
|
+
MembershipIntentShape = {
|
|
5187
|
+
kind: external_exports.literal("membership_intent"),
|
|
5188
|
+
action: external_exports.enum(["remove"]),
|
|
5189
|
+
participant_id: LowerCrockfordUlidSchema,
|
|
5190
|
+
recipient_identity: NonEmptyStringSchema,
|
|
5191
|
+
role: RoleSchema,
|
|
5192
|
+
alias: NonEmptyStringSchema.optional(),
|
|
5193
|
+
epoch: PositiveSafeIntegerSchema,
|
|
5194
|
+
notify: external_exports.boolean()
|
|
5195
|
+
};
|
|
5196
|
+
MembershipResultShape = {
|
|
5197
|
+
kind: external_exports.literal("membership_result"),
|
|
5198
|
+
intent_record_id: NonEmptyStringSchema,
|
|
5199
|
+
participant_id: LowerCrockfordUlidSchema,
|
|
5200
|
+
status: RelayStatusSchema,
|
|
5201
|
+
notified: external_exports.boolean(),
|
|
5202
|
+
key_material_retained: external_exports.literal(true),
|
|
5203
|
+
uncertain_after_restart: external_exports.literal(true).optional()
|
|
5204
|
+
};
|
|
5205
|
+
CloseNoticeIntentShape = {
|
|
5206
|
+
kind: external_exports.literal("close_notice_intent"),
|
|
5207
|
+
recipient_identity: NonEmptyStringSchema
|
|
5293
5208
|
};
|
|
5209
|
+
CloseNoticeResultShape = {
|
|
5210
|
+
kind: external_exports.literal("close_notice_result"),
|
|
5211
|
+
intent_record_id: NonEmptyStringSchema,
|
|
5212
|
+
recipient_identity: NonEmptyStringSchema,
|
|
5213
|
+
status: RelayStatusSchema,
|
|
5214
|
+
notified: external_exports.boolean(),
|
|
5215
|
+
key_material_retained: external_exports.literal(true),
|
|
5216
|
+
uncertain_after_restart: external_exports.literal(true).optional()
|
|
5217
|
+
};
|
|
5218
|
+
MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...MessageShape }).strict();
|
|
5219
|
+
FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
|
|
5220
|
+
RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
|
|
5221
|
+
RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
|
|
5222
|
+
MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
|
|
5223
|
+
MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
|
|
5224
|
+
CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
|
|
5225
|
+
CloseNoticeResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeResultShape }).strict();
|
|
5226
|
+
RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
5227
|
+
MessageRecordSchema,
|
|
5228
|
+
FileRecordSchema,
|
|
5229
|
+
RelayIntentRecordSchema,
|
|
5230
|
+
RelayResultRecordSchema,
|
|
5231
|
+
MembershipIntentRecordSchema,
|
|
5232
|
+
MembershipResultRecordSchema,
|
|
5233
|
+
CloseNoticeIntentRecordSchema,
|
|
5234
|
+
CloseNoticeResultRecordSchema
|
|
5235
|
+
]);
|
|
5236
|
+
CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
|
|
5237
|
+
if (record.record_id !== `${record.room_id}:${record.seq}`) {
|
|
5238
|
+
context.addIssue({
|
|
5239
|
+
code: external_exports.ZodIssueCode.custom,
|
|
5240
|
+
path: ["record_id"],
|
|
5241
|
+
message: 'record_id must equal room_id + ":" + seq'
|
|
5242
|
+
});
|
|
5243
|
+
}
|
|
5244
|
+
if (record.kind === "message") refineMessageCategory(record, context);
|
|
5245
|
+
refineRelaySubject(record, context);
|
|
5246
|
+
refineFileRecord(record, context);
|
|
5247
|
+
});
|
|
5248
|
+
AppendRecordSchema = external_exports.discriminatedUnion("kind", [
|
|
5249
|
+
external_exports.object({ ...AppendCommonShape, ...MessageShape }).strict(),
|
|
5250
|
+
external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
|
|
5251
|
+
external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
|
|
5252
|
+
external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
|
|
5253
|
+
external_exports.object({ ...AppendCommonShape, ...MembershipIntentShape }).strict(),
|
|
5254
|
+
external_exports.object({ ...AppendCommonShape, ...MembershipResultShape }).strict(),
|
|
5255
|
+
external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
|
|
5256
|
+
external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
|
|
5257
|
+
]).superRefine((record, context) => {
|
|
5258
|
+
if (record.kind === "message") refineMessageCategory(record, context);
|
|
5259
|
+
refineRelaySubject(record, context);
|
|
5260
|
+
refineFileRecord(record, context);
|
|
5261
|
+
});
|
|
5294
5262
|
}
|
|
5295
5263
|
});
|
|
5296
5264
|
|
|
@@ -5464,7 +5432,7 @@ var init_packets = __esm({
|
|
|
5464
5432
|
get(roomId) {
|
|
5465
5433
|
return this.packets.get(roomId);
|
|
5466
5434
|
}
|
|
5467
|
-
async create(roomId, identityName
|
|
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);
|
|
@@ -5768,6 +5736,20 @@ function sameReply2(stored, observed) {
|
|
|
5768
5736
|
if (stored === void 0 || observed == null) return stored === void 0 && observed == null;
|
|
5769
5737
|
return stored.wire_id === observed.wire_id && stored.sentence === observed.sentence;
|
|
5770
5738
|
}
|
|
5739
|
+
async function queryStore(store, roomId, options) {
|
|
5740
|
+
if (store.query) return store.query(roomId, options);
|
|
5741
|
+
let records = await store.read(roomId);
|
|
5742
|
+
records = records.filter((record) => {
|
|
5743
|
+
const value = record;
|
|
5744
|
+
return (options.kind === void 0 || record.kind === options.kind) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.sourceMsgId === void 0 || value.source_msg_id === options.sourceMsgId) && (options.sourceFileId === void 0 || value.source_file_id === options.sourceFileId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity);
|
|
5745
|
+
});
|
|
5746
|
+
if (options.unresolvedResultKind) {
|
|
5747
|
+
const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
|
|
5748
|
+
records = records.filter((record) => !completed.has(record.record_id));
|
|
5749
|
+
}
|
|
5750
|
+
if (options.descending) records.reverse();
|
|
5751
|
+
return records.slice(0, options.limit);
|
|
5752
|
+
}
|
|
5771
5753
|
function canonicalValue(value) {
|
|
5772
5754
|
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
5773
5755
|
if (value !== null && typeof value === "object") {
|
|
@@ -5795,13 +5777,14 @@ function wireKind(category) {
|
|
|
5795
5777
|
return "room_msg";
|
|
5796
5778
|
}
|
|
5797
5779
|
}
|
|
5798
|
-
var INTAKE_BATCH_SIZE, IntakePump;
|
|
5780
|
+
var JOURNAL_WORK_BATCH_SIZE, INTAKE_BATCH_SIZE, IntakePump;
|
|
5799
5781
|
var init_intake = __esm({
|
|
5800
5782
|
"src/intake.ts"() {
|
|
5801
5783
|
"use strict";
|
|
5802
5784
|
init_zod();
|
|
5803
5785
|
init_contracts();
|
|
5804
5786
|
init_ulid();
|
|
5787
|
+
JOURNAL_WORK_BATCH_SIZE = 64;
|
|
5805
5788
|
INTAKE_BATCH_SIZE = 32;
|
|
5806
5789
|
IntakePump = class {
|
|
5807
5790
|
store;
|
|
@@ -5909,8 +5892,8 @@ var init_intake = __esm({
|
|
|
5909
5892
|
await packet.acknowledgeFile(item);
|
|
5910
5893
|
return;
|
|
5911
5894
|
}
|
|
5912
|
-
const
|
|
5913
|
-
let file = this.findSourceFile(
|
|
5895
|
+
const [storedFile] = await queryStore(this.store, roomId, { kind: "file", sourceFileId: item.file_id, limit: 1 });
|
|
5896
|
+
let file = this.findSourceFile(storedFile === void 0 ? [] : [storedFile], item);
|
|
5914
5897
|
if (!file) {
|
|
5915
5898
|
const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
|
|
5916
5899
|
const bytes = Buffer.from(item.data);
|
|
@@ -5948,8 +5931,8 @@ var init_intake = __esm({
|
|
|
5948
5931
|
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
5949
5932
|
return;
|
|
5950
5933
|
}
|
|
5951
|
-
const
|
|
5952
|
-
let message = this.findSourceMessage(
|
|
5934
|
+
const [storedMessage] = await queryStore(this.store, roomId, { kind: "message", sourceMsgId: item.msg_id, limit: 1 });
|
|
5935
|
+
let message = this.findSourceMessage(storedMessage === void 0 ? [] : [storedMessage], item);
|
|
5953
5936
|
if (!message) {
|
|
5954
5937
|
const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
|
|
5955
5938
|
const appended = await this.store.append(roomId, {
|
|
@@ -6012,100 +5995,153 @@ var init_intake = __esm({
|
|
|
6012
5995
|
}
|
|
6013
5996
|
}
|
|
6014
5997
|
async completeSnapshotIntents(roomId) {
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
(
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
(
|
|
6021
|
-
|
|
5998
|
+
if (!this.store.recordsNeedingRelayIntents) {
|
|
5999
|
+
const records = await this.store.read(roomId);
|
|
6000
|
+
for (const message of records.filter(
|
|
6001
|
+
(record) => record.kind === "message"
|
|
6002
|
+
)) await this.completeMessageIntents(roomId, message);
|
|
6003
|
+
for (const file of records.filter(
|
|
6004
|
+
(record) => record.kind === "file"
|
|
6005
|
+
)) await this.completeFileIntents(roomId, file);
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
6008
|
+
for (; ; ) {
|
|
6009
|
+
const records = await this.store.recordsNeedingRelayIntents(
|
|
6010
|
+
roomId,
|
|
6011
|
+
{ limit: JOURNAL_WORK_BATCH_SIZE }
|
|
6012
|
+
);
|
|
6013
|
+
if (records.length === 0) return;
|
|
6014
|
+
for (const record of records) {
|
|
6015
|
+
if (record.kind === "message") await this.completeMessageIntents(roomId, record);
|
|
6016
|
+
else if (record.kind === "file") await this.completeFileIntents(roomId, record);
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6022
6019
|
}
|
|
6023
6020
|
async completeFileIntents(roomId, file) {
|
|
6024
|
-
|
|
6025
|
-
|
|
6021
|
+
if (this.store.relayRecipientsNeedingIntent) {
|
|
6022
|
+
for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, file.seq)) {
|
|
6023
|
+
await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
|
|
6024
|
+
}
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
const records = await queryStore(this.store, roomId, { kind: "relay_intent", fileId: file.file_id });
|
|
6028
|
+
const intended = new Set(records.map((record) => record.recipient_identity));
|
|
6026
6029
|
for (const recipientIdentity of file.recipient_identities) {
|
|
6027
6030
|
if (intended.has(recipientIdentity)) continue;
|
|
6028
|
-
await this.
|
|
6029
|
-
version: 1,
|
|
6030
|
-
kind: "relay_intent",
|
|
6031
|
-
room_id: roomId,
|
|
6032
|
-
at: this.now(),
|
|
6033
|
-
file_id: file.file_id,
|
|
6034
|
-
recipient_identity: recipientIdentity
|
|
6035
|
-
});
|
|
6031
|
+
await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
|
|
6036
6032
|
intended.add(recipientIdentity);
|
|
6037
6033
|
}
|
|
6038
6034
|
}
|
|
6035
|
+
async appendFileIntent(roomId, fileId, recipientIdentity) {
|
|
6036
|
+
await this.store.append(roomId, {
|
|
6037
|
+
version: 1,
|
|
6038
|
+
kind: "relay_intent",
|
|
6039
|
+
room_id: roomId,
|
|
6040
|
+
at: this.now(),
|
|
6041
|
+
file_id: fileId,
|
|
6042
|
+
recipient_identity: recipientIdentity
|
|
6043
|
+
});
|
|
6044
|
+
}
|
|
6039
6045
|
async completeMessageIntents(roomId, message) {
|
|
6040
|
-
|
|
6041
|
-
|
|
6046
|
+
if (this.store.relayRecipientsNeedingIntent) {
|
|
6047
|
+
for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, message.seq)) {
|
|
6048
|
+
await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
|
|
6049
|
+
}
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
const records = await queryStore(this.store, roomId, { kind: "relay_intent", messageId: message.message_id });
|
|
6053
|
+
const intended = new Set(records.map((record) => record.recipient_identity));
|
|
6042
6054
|
for (const recipientIdentity of message.recipient_identities) {
|
|
6043
6055
|
if (intended.has(recipientIdentity)) continue;
|
|
6044
|
-
await this.
|
|
6045
|
-
version: 1,
|
|
6046
|
-
kind: "relay_intent",
|
|
6047
|
-
room_id: roomId,
|
|
6048
|
-
at: this.now(),
|
|
6049
|
-
message_id: message.message_id,
|
|
6050
|
-
recipient_identity: recipientIdentity
|
|
6051
|
-
});
|
|
6056
|
+
await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
|
|
6052
6057
|
intended.add(recipientIdentity);
|
|
6053
6058
|
}
|
|
6054
6059
|
}
|
|
6060
|
+
async appendMessageIntent(roomId, messageId, recipientIdentity) {
|
|
6061
|
+
await this.store.append(roomId, {
|
|
6062
|
+
version: 1,
|
|
6063
|
+
kind: "relay_intent",
|
|
6064
|
+
room_id: roomId,
|
|
6065
|
+
at: this.now(),
|
|
6066
|
+
message_id: messageId,
|
|
6067
|
+
recipient_identity: recipientIdentity
|
|
6068
|
+
});
|
|
6069
|
+
}
|
|
6055
6070
|
async relayPendingUnlocked(roomId, packet) {
|
|
6056
|
-
const records = await this.store.read(roomId);
|
|
6057
6071
|
const room = await this.store.load(roomId);
|
|
6058
6072
|
const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
|
|
6059
6073
|
const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6074
|
+
let after = 0;
|
|
6075
|
+
for (; ; ) {
|
|
6076
|
+
const pending = await queryStore(this.store, roomId, {
|
|
6077
|
+
kind: "relay_intent",
|
|
6078
|
+
unresolvedResultKind: "relay_result",
|
|
6079
|
+
after,
|
|
6080
|
+
limit: JOURNAL_WORK_BATCH_SIZE
|
|
6081
|
+
});
|
|
6082
|
+
if (pending.length === 0) return;
|
|
6083
|
+
for (const intent of pending) {
|
|
6084
|
+
after = intent.seq;
|
|
6085
|
+
const [message] = intent.message_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "message", messageId: intent.message_id, limit: 1 });
|
|
6086
|
+
const [file] = intent.file_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "file", fileId: intent.file_id, limit: 1 });
|
|
6087
|
+
if (message === void 0 === (file === void 0)) continue;
|
|
6088
|
+
const recipients = message?.recipient_identities ?? file.recipient_identities;
|
|
6089
|
+
if (!recipients.includes(intent.recipient_identity)) continue;
|
|
6090
|
+
if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
|
|
6091
|
+
const skipped = await this.store.append(roomId, {
|
|
6092
|
+
version: 1,
|
|
6093
|
+
kind: "relay_result",
|
|
6094
|
+
room_id: roomId,
|
|
6095
|
+
at: this.now(),
|
|
6096
|
+
intent_record_id: intent.record_id,
|
|
6097
|
+
...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
|
|
6098
|
+
...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
|
|
6099
|
+
recipient_identity: intent.recipient_identity,
|
|
6100
|
+
status: "skipped_removed"
|
|
6101
|
+
});
|
|
6102
|
+
if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6103
|
+
continue;
|
|
6104
|
+
}
|
|
6105
|
+
if (file !== void 0) {
|
|
6106
|
+
const author = file.author_alias === void 0 ? file.author : {
|
|
6107
|
+
identity: file.author_alias.participant_id,
|
|
6108
|
+
display_name: file.author_alias.alias,
|
|
6109
|
+
role: file.author.role
|
|
6110
|
+
};
|
|
6111
|
+
const metadata = await sendRoomBody(packet, intent.recipient_identity, {
|
|
6112
|
+
version: 1,
|
|
6113
|
+
kind: "room_file",
|
|
6114
|
+
room_id: roomId,
|
|
6115
|
+
room_name: room.room_name,
|
|
6116
|
+
file_id: file.file_id,
|
|
6117
|
+
author,
|
|
6118
|
+
filename: file.filename,
|
|
6119
|
+
mime: file.mime,
|
|
6120
|
+
size: file.size,
|
|
6121
|
+
sha256: file.sha256,
|
|
6122
|
+
at: file.at
|
|
6123
|
+
});
|
|
6124
|
+
if (metadata.status === "send_failed") {
|
|
6125
|
+
const failed = await this.store.append(roomId, {
|
|
6126
|
+
version: 1,
|
|
6127
|
+
kind: "relay_result",
|
|
6128
|
+
room_id: roomId,
|
|
6129
|
+
at: this.now(),
|
|
6130
|
+
intent_record_id: intent.record_id,
|
|
6131
|
+
file_id: file.file_id,
|
|
6132
|
+
recipient_identity: intent.recipient_identity,
|
|
6133
|
+
status: "send_failed"
|
|
6134
|
+
});
|
|
6135
|
+
if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6136
|
+
continue;
|
|
6137
|
+
}
|
|
6138
|
+
const outcome2 = await packet.sendFile(
|
|
6139
|
+
intent.recipient_identity,
|
|
6140
|
+
file.filename,
|
|
6141
|
+
file.mime,
|
|
6142
|
+
Buffer.from(file.data_base64, "base64")
|
|
6143
|
+
);
|
|
6144
|
+
const appended2 = await this.store.append(roomId, {
|
|
6109
6145
|
version: 1,
|
|
6110
6146
|
kind: "relay_result",
|
|
6111
6147
|
room_id: roomId,
|
|
@@ -6113,66 +6149,45 @@ var init_intake = __esm({
|
|
|
6113
6149
|
intent_record_id: intent.record_id,
|
|
6114
6150
|
file_id: file.file_id,
|
|
6115
6151
|
recipient_identity: intent.recipient_identity,
|
|
6116
|
-
status:
|
|
6152
|
+
status: outcome2.status,
|
|
6153
|
+
...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
|
|
6154
|
+
...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
|
|
6117
6155
|
});
|
|
6118
|
-
if (
|
|
6119
|
-
completed.add(intent.record_id);
|
|
6156
|
+
if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6120
6157
|
continue;
|
|
6121
6158
|
}
|
|
6122
|
-
const
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6159
|
+
const unsigned = {
|
|
6160
|
+
version: 1,
|
|
6161
|
+
kind: wireKind(message.category),
|
|
6162
|
+
room_id: roomId,
|
|
6163
|
+
room_name: room.room_name,
|
|
6164
|
+
message_id: message.message_id,
|
|
6165
|
+
// An anonymous author leaves the archive only in alias form.
|
|
6166
|
+
author: message.author_alias === void 0 ? message.author : {
|
|
6167
|
+
identity: message.author_alias.participant_id,
|
|
6168
|
+
display_name: message.author_alias.alias,
|
|
6169
|
+
role: message.author.role
|
|
6170
|
+
},
|
|
6171
|
+
text: message.text,
|
|
6172
|
+
at: message.at,
|
|
6173
|
+
...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
|
|
6174
|
+
...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
|
|
6175
|
+
...message.membership === void 0 ? {} : { membership: message.membership }
|
|
6176
|
+
};
|
|
6177
|
+
const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
|
|
6178
|
+
const appended = await this.store.append(roomId, {
|
|
6129
6179
|
version: 1,
|
|
6130
6180
|
kind: "relay_result",
|
|
6131
6181
|
room_id: roomId,
|
|
6132
6182
|
at: this.now(),
|
|
6133
6183
|
intent_record_id: intent.record_id,
|
|
6134
|
-
|
|
6184
|
+
message_id: intent.message_id,
|
|
6135
6185
|
recipient_identity: intent.recipient_identity,
|
|
6136
|
-
status:
|
|
6137
|
-
...
|
|
6138
|
-
...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
|
|
6186
|
+
status: outcome.status,
|
|
6187
|
+
...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
|
|
6139
6188
|
});
|
|
6140
|
-
if (
|
|
6141
|
-
completed.add(intent.record_id);
|
|
6142
|
-
continue;
|
|
6189
|
+
if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6143
6190
|
}
|
|
6144
|
-
const unsigned = {
|
|
6145
|
-
version: 1,
|
|
6146
|
-
kind: wireKind(message.category),
|
|
6147
|
-
room_id: roomId,
|
|
6148
|
-
room_name: room.room_name,
|
|
6149
|
-
message_id: message.message_id,
|
|
6150
|
-
// An anonymous author leaves the archive only in alias form.
|
|
6151
|
-
author: message.author_alias === void 0 ? message.author : {
|
|
6152
|
-
identity: message.author_alias.participant_id,
|
|
6153
|
-
display_name: message.author_alias.alias,
|
|
6154
|
-
role: message.author.role
|
|
6155
|
-
},
|
|
6156
|
-
text: message.text,
|
|
6157
|
-
at: message.at,
|
|
6158
|
-
...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
|
|
6159
|
-
...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
|
|
6160
|
-
...message.membership === void 0 ? {} : { membership: message.membership }
|
|
6161
|
-
};
|
|
6162
|
-
const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
|
|
6163
|
-
const appended = await this.store.append(roomId, {
|
|
6164
|
-
version: 1,
|
|
6165
|
-
kind: "relay_result",
|
|
6166
|
-
room_id: roomId,
|
|
6167
|
-
at: this.now(),
|
|
6168
|
-
intent_record_id: intent.record_id,
|
|
6169
|
-
message_id: intent.message_id,
|
|
6170
|
-
recipient_identity: intent.recipient_identity,
|
|
6171
|
-
status: outcome.status,
|
|
6172
|
-
...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
|
|
6173
|
-
});
|
|
6174
|
-
if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6175
|
-
completed.add(intent.record_id);
|
|
6176
6191
|
}
|
|
6177
6192
|
}
|
|
6178
6193
|
findSourceMessage(records, item) {
|
|
@@ -6231,6 +6246,21 @@ function byteBoundedHistoryPage(records) {
|
|
|
6231
6246
|
function activeSeats(room) {
|
|
6232
6247
|
return room.seats.filter((seat) => seat.state === "active");
|
|
6233
6248
|
}
|
|
6249
|
+
async function queryStore2(store, roomId, options) {
|
|
6250
|
+
if (store.query) return store.query(roomId, options);
|
|
6251
|
+
let records = await store.read(roomId);
|
|
6252
|
+
records = records.filter((record) => {
|
|
6253
|
+
const value = record;
|
|
6254
|
+
const membership = value.membership;
|
|
6255
|
+
return (options.kind === void 0 || record.kind === options.kind) && (options.after === void 0 || record.seq > options.after) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.intentRecordId === void 0 || value.intent_record_id === options.intentRecordId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity) && (options.category === void 0 || value.category === options.category) && (options.membershipEpoch === void 0 || membership?.epoch === options.membershipEpoch);
|
|
6256
|
+
});
|
|
6257
|
+
if (options.unresolvedResultKind) {
|
|
6258
|
+
const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
|
|
6259
|
+
records = records.filter((record) => !completed.has(record.record_id));
|
|
6260
|
+
}
|
|
6261
|
+
if (options.descending) records.reverse();
|
|
6262
|
+
return records.slice(0, options.limit ?? Number.MAX_SAFE_INTEGER);
|
|
6263
|
+
}
|
|
6234
6264
|
function isCancelledExternalSeat(seat) {
|
|
6235
6265
|
return seat.state === "removed" && seat.accepted_at === void 0 && seat.requested_at !== void 0 && seat.invite_sha256 !== void 0;
|
|
6236
6266
|
}
|
|
@@ -6243,7 +6273,7 @@ function uniqueIdentities(identities) {
|
|
|
6243
6273
|
function currentContactIdentities(packet) {
|
|
6244
6274
|
return new Set(packet.listContacts().map((contact) => contact.container_id));
|
|
6245
6275
|
}
|
|
6246
|
-
var CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
|
|
6276
|
+
var CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
|
|
6247
6277
|
var init_service = __esm({
|
|
6248
6278
|
"src/service.ts"() {
|
|
6249
6279
|
"use strict";
|
|
@@ -6270,6 +6300,7 @@ var init_service = __esm({
|
|
|
6270
6300
|
limit: external_exports.number().int().positive().safe().optional(),
|
|
6271
6301
|
view: external_exports.enum(["operator", "participant"]).optional()
|
|
6272
6302
|
}).strict();
|
|
6303
|
+
JOURNAL_WORK_BATCH_SIZE2 = 64;
|
|
6273
6304
|
DeleteRoomInputSchema = external_exports.object({
|
|
6274
6305
|
confirm: external_exports.literal(true)
|
|
6275
6306
|
}).strict();
|
|
@@ -6297,7 +6328,6 @@ var init_service = __esm({
|
|
|
6297
6328
|
nextMessageId;
|
|
6298
6329
|
intake;
|
|
6299
6330
|
provisioningCheckpoint;
|
|
6300
|
-
identityNameMode;
|
|
6301
6331
|
constructor(store, packets, options = {}) {
|
|
6302
6332
|
this.store = store;
|
|
6303
6333
|
this.packets = packets;
|
|
@@ -6306,7 +6336,6 @@ var init_service = __esm({
|
|
|
6306
6336
|
this.nextMessageId = options.messageId ?? generateUlid;
|
|
6307
6337
|
this.provisioningCheckpoint = options.provisioningCheckpoint ?? (() => {
|
|
6308
6338
|
});
|
|
6309
|
-
this.identityNameMode = options.identityNameMode ?? "stable_id";
|
|
6310
6339
|
this.intake = new IntakePump(store, packets, {
|
|
6311
6340
|
now: this.nowValue,
|
|
6312
6341
|
messageId: this.nextMessageId
|
|
@@ -6316,7 +6345,7 @@ var init_service = __esm({
|
|
|
6316
6345
|
const settings = CreateRoomInputSchema.parse(input);
|
|
6317
6346
|
const roomId = LowerCrockfordUlidSchema.parse(this.nextRoomId());
|
|
6318
6347
|
const roomName = settings.name ?? defaultRoomName(roomId);
|
|
6319
|
-
const identityName =
|
|
6348
|
+
const identityName = roomIdentityName(roomName);
|
|
6320
6349
|
return this.lock(roomId, async () => {
|
|
6321
6350
|
const provisional = RoomSchema.parse({
|
|
6322
6351
|
version: 2,
|
|
@@ -6754,8 +6783,11 @@ var init_service = __esm({
|
|
|
6754
6783
|
membership_epoch: Math.max(current.membership_epoch, intent.epoch)
|
|
6755
6784
|
}));
|
|
6756
6785
|
}
|
|
6757
|
-
const
|
|
6758
|
-
|
|
6786
|
+
const [existing] = await queryStore2(this.store, current.room_id, {
|
|
6787
|
+
kind: "membership_result",
|
|
6788
|
+
intentRecordId: intent.record_id,
|
|
6789
|
+
limit: 1
|
|
6790
|
+
});
|
|
6759
6791
|
let outcome;
|
|
6760
6792
|
if (existing !== void 0 && existing.kind === "membership_result") {
|
|
6761
6793
|
outcome = {
|
|
@@ -6794,8 +6826,13 @@ var init_service = __esm({
|
|
|
6794
6826
|
};
|
|
6795
6827
|
}
|
|
6796
6828
|
async ensureMembershipNotice(room, intent) {
|
|
6797
|
-
const records = await this.store
|
|
6798
|
-
|
|
6829
|
+
const records = await queryStore2(this.store, room.room_id, {
|
|
6830
|
+
kind: "message",
|
|
6831
|
+
category: "membership",
|
|
6832
|
+
membershipEpoch: intent.epoch,
|
|
6833
|
+
limit: 1
|
|
6834
|
+
});
|
|
6835
|
+
const already = records.length > 0;
|
|
6799
6836
|
if (already) return;
|
|
6800
6837
|
const remaining = activeSeats(room);
|
|
6801
6838
|
if (remaining.length === 0) return;
|
|
@@ -7005,7 +7042,7 @@ var init_service = __esm({
|
|
|
7005
7042
|
async history(roomId, options = {}) {
|
|
7006
7043
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
7007
7044
|
const { view, ...page } = HistoryOptionsSchema.parse(options);
|
|
7008
|
-
const records = await this.store
|
|
7045
|
+
const records = view === "participant" ? await queryStore2(this.store, id, { kind: "message", after: page.after, limit: page.limit }) : await this.store.read(id, page);
|
|
7009
7046
|
if (view !== "participant") return byteBoundedHistoryPage(records);
|
|
7010
7047
|
const projected = records.filter((record) => record.kind === "message").map((record) => {
|
|
7011
7048
|
const {
|
|
@@ -7272,13 +7309,19 @@ var init_service = __esm({
|
|
|
7272
7309
|
invites,
|
|
7273
7310
|
membership_epoch: room.membership_epoch + activatedPending.length + newSeats.length
|
|
7274
7311
|
});
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7312
|
+
let membershipAfter = 0;
|
|
7313
|
+
for (; ; ) {
|
|
7314
|
+
const journal = await queryStore2(this.store, next.room_id, {
|
|
7315
|
+
kind: "membership_intent",
|
|
7316
|
+
unresolvedResultKind: "membership_result",
|
|
7317
|
+
after: membershipAfter,
|
|
7318
|
+
limit: JOURNAL_WORK_BATCH_SIZE2
|
|
7319
|
+
});
|
|
7320
|
+
if (journal.length === 0) break;
|
|
7321
|
+
for (const intent of journal) {
|
|
7322
|
+
membershipAfter = intent.seq;
|
|
7323
|
+
({ room: next } = await this.completeRemovalUnlocked(next, intent));
|
|
7324
|
+
}
|
|
7282
7325
|
}
|
|
7283
7326
|
const requirementsMet = invites.filter((invite) => invite.state !== "revoked").every((invite) => invite.accepted_cids.length >= invite.min_accepts);
|
|
7284
7327
|
const admitted = [...activatedPending, ...newSeats];
|
|
@@ -7295,23 +7338,31 @@ var init_service = __esm({
|
|
|
7295
7338
|
const packet = this.packets.get(roomId);
|
|
7296
7339
|
if (packet) {
|
|
7297
7340
|
await packet.refreshContacts();
|
|
7298
|
-
let records = await this.store.read(roomId);
|
|
7299
7341
|
let contacts = currentContactIdentities(packet);
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7342
|
+
let closeAfter = 0;
|
|
7343
|
+
for (; ; ) {
|
|
7344
|
+
const pending = await queryStore2(this.store, roomId, {
|
|
7345
|
+
kind: "close_notice_intent",
|
|
7346
|
+
unresolvedResultKind: "close_notice_result",
|
|
7347
|
+
after: closeAfter,
|
|
7348
|
+
limit: JOURNAL_WORK_BATCH_SIZE2
|
|
7349
|
+
});
|
|
7350
|
+
if (pending.length === 0) break;
|
|
7351
|
+
for (const intent of pending) {
|
|
7352
|
+
closeAfter = intent.seq;
|
|
7353
|
+
if (contacts.has(intent.recipient_identity)) continue;
|
|
7354
|
+
await this.appendUncertainCloseResult(roomId, intent);
|
|
7355
|
+
}
|
|
7308
7356
|
}
|
|
7309
7357
|
for (const recipientIdentity of contacts) {
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7358
|
+
const [existingIntent] = await queryStore2(this.store, roomId, {
|
|
7359
|
+
kind: "close_notice_intent",
|
|
7360
|
+
recipientIdentity,
|
|
7361
|
+
unresolvedResultKind: "close_notice_result",
|
|
7362
|
+
descending: true,
|
|
7363
|
+
limit: 1
|
|
7364
|
+
});
|
|
7365
|
+
let intent = existingIntent;
|
|
7315
7366
|
if (!intent) {
|
|
7316
7367
|
const appended2 = await this.store.append(roomId, {
|
|
7317
7368
|
version: 1,
|
|
@@ -7354,13 +7405,19 @@ var init_service = __esm({
|
|
|
7354
7405
|
`room "${roomId}" live-state purge left residue: ${residue.join(", ") || "packet registry entry"}`
|
|
7355
7406
|
);
|
|
7356
7407
|
}
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7408
|
+
let purgeAfter = 0;
|
|
7409
|
+
for (; ; ) {
|
|
7410
|
+
const afterPurge = await queryStore2(this.store, roomId, {
|
|
7411
|
+
kind: "close_notice_intent",
|
|
7412
|
+
unresolvedResultKind: "close_notice_result",
|
|
7413
|
+
after: purgeAfter,
|
|
7414
|
+
limit: JOURNAL_WORK_BATCH_SIZE2
|
|
7415
|
+
});
|
|
7416
|
+
if (afterPurge.length === 0) break;
|
|
7417
|
+
for (const intent of afterPurge) {
|
|
7418
|
+
purgeAfter = intent.seq;
|
|
7419
|
+
await this.appendUncertainCloseResult(roomId, intent);
|
|
7420
|
+
}
|
|
7364
7421
|
}
|
|
7365
7422
|
return this.store.save(RoomSchema.parse({ ...room, state: "closed", closed_at: this.now() }));
|
|
7366
7423
|
}
|
|
@@ -7420,18 +7477,29 @@ var init_service = __esm({
|
|
|
7420
7477
|
});
|
|
7421
7478
|
}
|
|
7422
7479
|
async ensureBriefingKind(room, recipients, briefing) {
|
|
7423
|
-
|
|
7480
|
+
if (this.store.briefingDeliveryTimes) {
|
|
7481
|
+
const recipientIdentities = uniqueIdentities(recipients.map((seat) => seat.identity));
|
|
7482
|
+
const deliveries = await this.store.briefingDeliveryTimes(room.room_id, {
|
|
7483
|
+
category: briefing.category,
|
|
7484
|
+
briefingRole: briefing.briefing_role,
|
|
7485
|
+
briefingVersion: briefing.briefing_version
|
|
7486
|
+
}, recipientIdentities);
|
|
7487
|
+
const missing2 = recipients.filter((seat) => !deliveries.has(seat.identity));
|
|
7488
|
+
const appendedAt2 = await this.appendBriefingForMissing(room, missing2, briefing);
|
|
7489
|
+
return [...deliveries.values(), ...appendedAt2 === void 0 ? [] : [appendedAt2]].sort()[0] ?? this.now();
|
|
7490
|
+
}
|
|
7491
|
+
const records = await queryStore2(this.store, room.room_id, {
|
|
7492
|
+
kind: "message",
|
|
7493
|
+
category: briefing.category
|
|
7494
|
+
});
|
|
7424
7495
|
const matching = records.filter((record) => record.kind === "message" && record.category === briefing.category && record.briefing_role === briefing.briefing_role && (record.briefing_version ?? 1) === briefing.briefing_version);
|
|
7425
|
-
const intentsByMessage = /* @__PURE__ */ new Map();
|
|
7426
|
-
for (const record of records) {
|
|
7427
|
-
if (record.kind !== "relay_intent" || record.message_id === void 0) continue;
|
|
7428
|
-
const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
|
|
7429
|
-
intents.add(record.recipient_identity);
|
|
7430
|
-
intentsByMessage.set(record.message_id, intents);
|
|
7431
|
-
}
|
|
7432
7496
|
const covered = /* @__PURE__ */ new Set();
|
|
7433
7497
|
for (const message of matching) {
|
|
7434
|
-
const
|
|
7498
|
+
const intentRecords = await queryStore2(this.store, room.room_id, {
|
|
7499
|
+
kind: "relay_intent",
|
|
7500
|
+
messageId: message.message_id
|
|
7501
|
+
});
|
|
7502
|
+
const intents = new Set(intentRecords.map((record) => record.recipient_identity));
|
|
7435
7503
|
for (const recipientIdentity of message.recipient_identities) {
|
|
7436
7504
|
covered.add(recipientIdentity);
|
|
7437
7505
|
if (!intents.has(recipientIdentity)) {
|
|
@@ -7447,35 +7515,36 @@ var init_service = __esm({
|
|
|
7447
7515
|
}
|
|
7448
7516
|
}
|
|
7449
7517
|
const missing = recipients.filter((seat) => !covered.has(seat.identity));
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7518
|
+
const appendedAt = await this.appendBriefingForMissing(room, missing, briefing);
|
|
7519
|
+
return matching[0]?.at ?? appendedAt ?? this.now();
|
|
7520
|
+
}
|
|
7521
|
+
async appendBriefingForMissing(room, missing, briefing) {
|
|
7522
|
+
if (missing.length === 0) return void 0;
|
|
7523
|
+
const appended = await this.store.append(room.room_id, {
|
|
7524
|
+
version: 1,
|
|
7525
|
+
kind: "message",
|
|
7526
|
+
room_id: room.room_id,
|
|
7527
|
+
at: this.now(),
|
|
7528
|
+
message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
|
|
7529
|
+
author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
|
|
7530
|
+
category: briefing.category,
|
|
7531
|
+
...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
|
|
7532
|
+
briefing_version: briefing.briefing_version,
|
|
7533
|
+
text: briefing.text,
|
|
7534
|
+
recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
|
|
7535
|
+
});
|
|
7536
|
+
if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
|
|
7537
|
+
for (const recipientIdentity of appended.recipient_identities) {
|
|
7538
|
+
await this.store.append(room.room_id, {
|
|
7453
7539
|
version: 1,
|
|
7454
|
-
kind: "
|
|
7540
|
+
kind: "relay_intent",
|
|
7455
7541
|
room_id: room.room_id,
|
|
7456
7542
|
at: this.now(),
|
|
7457
|
-
message_id:
|
|
7458
|
-
|
|
7459
|
-
category: briefing.category,
|
|
7460
|
-
...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
|
|
7461
|
-
briefing_version: briefing.briefing_version,
|
|
7462
|
-
text: briefing.text,
|
|
7463
|
-
recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
|
|
7543
|
+
message_id: appended.message_id,
|
|
7544
|
+
recipient_identity: recipientIdentity
|
|
7464
7545
|
});
|
|
7465
|
-
if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
|
|
7466
|
-
appendedAt = appended.at;
|
|
7467
|
-
for (const recipientIdentity of appended.recipient_identities) {
|
|
7468
|
-
await this.store.append(room.room_id, {
|
|
7469
|
-
version: 1,
|
|
7470
|
-
kind: "relay_intent",
|
|
7471
|
-
room_id: room.room_id,
|
|
7472
|
-
at: this.now(),
|
|
7473
|
-
message_id: appended.message_id,
|
|
7474
|
-
recipient_identity: recipientIdentity
|
|
7475
|
-
});
|
|
7476
|
-
}
|
|
7477
7546
|
}
|
|
7478
|
-
return
|
|
7547
|
+
return appended.at;
|
|
7479
7548
|
}
|
|
7480
7549
|
lock(roomId, work) {
|
|
7481
7550
|
return this.store.mutex(roomId).runExclusive(work);
|
|
@@ -7509,8 +7578,9 @@ var init_service = __esm({
|
|
|
7509
7578
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
7510
7579
|
import * as nodeFs2 from "node:fs";
|
|
7511
7580
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7512
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
7513
|
-
|
|
7581
|
+
import { basename, dirname as dirname2, join as join3 } from "node:path";
|
|
7582
|
+
import Database from "better-sqlite3";
|
|
7583
|
+
var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
|
|
7514
7584
|
var init_storage = __esm({
|
|
7515
7585
|
"src/storage.ts"() {
|
|
7516
7586
|
"use strict";
|
|
@@ -7519,6 +7589,8 @@ var init_storage = __esm({
|
|
|
7519
7589
|
DIRECTORY_MODE2 = 448;
|
|
7520
7590
|
FILE_MODE2 = 384;
|
|
7521
7591
|
NO_FOLLOW2 = nodeFs2.constants.O_NOFOLLOW ?? 0;
|
|
7592
|
+
SQLITE_SCHEMA_VERSION = 1;
|
|
7593
|
+
DEFAULT_WORK_BATCH_SIZE = 64;
|
|
7522
7594
|
utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
7523
7595
|
CoworkStorageError = class extends Error {
|
|
7524
7596
|
constructor(message, options) {
|
|
@@ -7546,20 +7618,20 @@ var init_storage = __esm({
|
|
|
7546
7618
|
CoworkStore = class {
|
|
7547
7619
|
stateDir;
|
|
7548
7620
|
fs;
|
|
7549
|
-
|
|
7550
|
-
// in-process room FIFO, not an on-disk lock with stale-owner recovery.
|
|
7621
|
+
beforeRecordCommit;
|
|
7551
7622
|
roomMutexes = /* @__PURE__ */ new Map();
|
|
7552
7623
|
lockOwnership = new AsyncLocalStorage();
|
|
7553
|
-
|
|
7624
|
+
reconciledBlobRooms = /* @__PURE__ */ new Set();
|
|
7554
7625
|
constructor(stateDir, options = {}) {
|
|
7555
7626
|
if (!stateDir) throw new CoworkStorageError("state directory is required");
|
|
7556
7627
|
this.stateDir = stateDir;
|
|
7557
7628
|
this.fs = options.fs ?? nodeFs2;
|
|
7629
|
+
this.beforeRecordCommit = options.beforeRecordCommit;
|
|
7558
7630
|
}
|
|
7559
7631
|
mutex(roomId, work) {
|
|
7560
|
-
const
|
|
7561
|
-
if (work) return this.withRoomMutex(
|
|
7562
|
-
return { runExclusive: (nested) => this.withRoomMutex(
|
|
7632
|
+
const id = this.roomId(roomId);
|
|
7633
|
+
if (work) return this.withRoomMutex(id, work);
|
|
7634
|
+
return { runExclusive: (nested) => this.withRoomMutex(id, nested) };
|
|
7563
7635
|
}
|
|
7564
7636
|
withRoomMutex(roomId, work) {
|
|
7565
7637
|
const inherited = this.lockOwnership.getStore();
|
|
@@ -7567,15 +7639,10 @@ var init_storage = __esm({
|
|
|
7567
7639
|
if (ownership?.active) {
|
|
7568
7640
|
const nested = Promise.resolve().then(work);
|
|
7569
7641
|
ownership.pending.add(nested);
|
|
7570
|
-
void nested.then(
|
|
7571
|
-
()
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
(error) => {
|
|
7575
|
-
ownership.pending.delete(nested);
|
|
7576
|
-
ownership.failures.push(error);
|
|
7577
|
-
}
|
|
7578
|
-
);
|
|
7642
|
+
void nested.then(() => ownership.pending.delete(nested), (error) => {
|
|
7643
|
+
ownership.pending.delete(nested);
|
|
7644
|
+
ownership.failures.push(error);
|
|
7645
|
+
});
|
|
7579
7646
|
return nested;
|
|
7580
7647
|
}
|
|
7581
7648
|
let queue = this.roomMutexes.get(roomId);
|
|
@@ -7597,9 +7664,7 @@ var init_storage = __esm({
|
|
|
7597
7664
|
rootFailed = true;
|
|
7598
7665
|
rootFailure = error;
|
|
7599
7666
|
}
|
|
7600
|
-
while (acquired.pending.size > 0)
|
|
7601
|
-
await Promise.allSettled([...acquired.pending]);
|
|
7602
|
-
}
|
|
7667
|
+
while (acquired.pending.size > 0) await Promise.allSettled([...acquired.pending]);
|
|
7603
7668
|
} finally {
|
|
7604
7669
|
acquired.active = false;
|
|
7605
7670
|
}
|
|
@@ -7617,20 +7682,20 @@ var init_storage = __esm({
|
|
|
7617
7682
|
this.rejectSymlink(roomDir, "room directory");
|
|
7618
7683
|
throw new CoworkStorageError(`room "${room.room_id}" already exists`);
|
|
7619
7684
|
}
|
|
7620
|
-
let
|
|
7685
|
+
let created = false;
|
|
7621
7686
|
try {
|
|
7622
7687
|
this.fs.mkdirSync(roomDir, { mode: DIRECTORY_MODE2 });
|
|
7623
|
-
|
|
7688
|
+
created = true;
|
|
7624
7689
|
this.fs.chmodSync(roomDir, DIRECTORY_MODE2);
|
|
7690
|
+
this.fs.mkdirSync(this.blobsDirectory(room.room_id), { mode: DIRECTORY_MODE2 });
|
|
7691
|
+
this.fs.chmodSync(this.blobsDirectory(room.room_id), DIRECTORY_MODE2);
|
|
7625
7692
|
this.fsyncDirectory(roomDir);
|
|
7626
7693
|
this.fsyncDirectory(this.roomsDirectory());
|
|
7627
|
-
this.
|
|
7694
|
+
this.withDatabase(room.room_id, () => void 0, true);
|
|
7628
7695
|
this.atomicMetadataWrite(this.metadataPath(room.room_id), room);
|
|
7629
|
-
this.nextSequences.set(room.room_id, 1);
|
|
7630
7696
|
return room;
|
|
7631
7697
|
} catch (error) {
|
|
7632
|
-
|
|
7633
|
-
if (roomCreated) {
|
|
7698
|
+
if (created) {
|
|
7634
7699
|
try {
|
|
7635
7700
|
this.fs.rmSync(roomDir, { recursive: true, force: true });
|
|
7636
7701
|
this.fsyncDirectory(this.roomsDirectory());
|
|
@@ -7642,8 +7707,8 @@ var init_storage = __esm({
|
|
|
7642
7707
|
});
|
|
7643
7708
|
}
|
|
7644
7709
|
async load(roomId) {
|
|
7645
|
-
const
|
|
7646
|
-
return this.mutex(
|
|
7710
|
+
const id = this.roomId(roomId);
|
|
7711
|
+
return this.mutex(id, () => this.loadUnlocked(id));
|
|
7647
7712
|
}
|
|
7648
7713
|
async save(input) {
|
|
7649
7714
|
const room = RoomSchema.parse(input);
|
|
@@ -7655,176 +7720,435 @@ var init_storage = __esm({
|
|
|
7655
7720
|
}
|
|
7656
7721
|
async list() {
|
|
7657
7722
|
this.ensureBaseDirectories();
|
|
7658
|
-
const
|
|
7723
|
+
const ids = this.fs.readdirSync(this.roomsDirectory(), { withFileTypes: true }).filter((entry) => entry.isDirectory() && LowerCrockfordUlidSchema.safeParse(entry.name).success).map((entry) => entry.name).sort();
|
|
7659
7724
|
const rooms = [];
|
|
7660
|
-
for (const
|
|
7725
|
+
for (const id of ids) rooms.push(await this.load(id));
|
|
7661
7726
|
return rooms;
|
|
7662
7727
|
}
|
|
7663
7728
|
async append(roomId, input) {
|
|
7664
|
-
const
|
|
7729
|
+
const id = this.roomId(roomId);
|
|
7665
7730
|
const draft = AppendRecordSchema.parse(input);
|
|
7666
|
-
if (draft.room_id !==
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
...draft,
|
|
7679
|
-
seq: nextSequence,
|
|
7680
|
-
record_id: `${validRoomId}:${nextSequence}`
|
|
7681
|
-
});
|
|
7682
|
-
const bytes = Buffer.from(`${JSON.stringify(record)}
|
|
7683
|
-
`, "utf8");
|
|
7684
|
-
let fd;
|
|
7685
|
-
let originalSize;
|
|
7686
|
-
let writeStarted = false;
|
|
7731
|
+
if (draft.room_id !== id) throw new CoworkStorageError(`record room_id "${draft.room_id}" does not match room "${id}"`);
|
|
7732
|
+
return this.mutex(id, () => {
|
|
7733
|
+
this.assertRoomDirectory(id);
|
|
7734
|
+
if (!this.reconciledBlobRooms.has(id)) this.withDatabase(id, () => void 0);
|
|
7735
|
+
let blob;
|
|
7736
|
+
let storedDraft = draft;
|
|
7737
|
+
if (draft.kind === "file") {
|
|
7738
|
+
const bytes = Buffer.from(draft.data_base64, "base64");
|
|
7739
|
+
blob = this.persistBlob(id, draft.sha256, bytes);
|
|
7740
|
+
const { data_base64: _bytes, ...withoutBytes } = draft;
|
|
7741
|
+
storedDraft = withoutBytes;
|
|
7742
|
+
}
|
|
7687
7743
|
try {
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7744
|
+
return this.withDatabase(id, (db) => {
|
|
7745
|
+
const transaction = db.transaction(() => {
|
|
7746
|
+
const next = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM records").get().next;
|
|
7747
|
+
const record = { ...draft, seq: next, record_id: `${id}:${next}` };
|
|
7748
|
+
const stored = { ...storedDraft, seq: next, record_id: record.record_id };
|
|
7749
|
+
const values = this.indexValues(record);
|
|
7750
|
+
db.prepare(`INSERT INTO records
|
|
7751
|
+
(seq, record_id, kind, at, payload_json, blob_path, message_id, file_id, intent_record_id,
|
|
7752
|
+
recipient_identity, source_msg_id, source_file_id, category, briefing_role, briefing_version, membership_epoch)
|
|
7753
|
+
VALUES (@seq,@record_id,@kind,@at,@payload_json,@blob_path,@message_id,@file_id,@intent_record_id,
|
|
7754
|
+
@recipient_identity,@source_msg_id,@source_file_id,@category,@briefing_role,@briefing_version,@membership_epoch)`).run({ ...values, payload_json: JSON.stringify(stored), blob_path: blob?.path ?? null });
|
|
7755
|
+
if (record.kind === "message" || record.kind === "file") {
|
|
7756
|
+
const insert = db.prepare(`INSERT INTO record_recipients
|
|
7757
|
+
(record_seq, recipient_identity, category, briefing_role, briefing_version)
|
|
7758
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
7759
|
+
const enqueue = db.prepare("INSERT INTO relay_intent_work(record_seq, recipient_identity) VALUES (?, ?)");
|
|
7760
|
+
for (const recipient of record.recipient_identities) {
|
|
7761
|
+
insert.run(
|
|
7762
|
+
record.seq,
|
|
7763
|
+
recipient,
|
|
7764
|
+
record.kind === "message" ? record.category : null,
|
|
7765
|
+
record.kind === "message" ? record.briefing_role ?? null : null,
|
|
7766
|
+
record.kind === "message" ? record.briefing_version ?? 1 : null
|
|
7767
|
+
);
|
|
7768
|
+
enqueue.run(record.seq, recipient);
|
|
7769
|
+
}
|
|
7770
|
+
} else if (record.kind === "relay_intent") {
|
|
7771
|
+
const sourceColumn = record.message_id === void 0 ? "file_id" : "message_id";
|
|
7772
|
+
const sourceId = record.message_id ?? record.file_id;
|
|
7773
|
+
db.prepare(`DELETE FROM relay_intent_work
|
|
7774
|
+
WHERE recipient_identity = ? AND record_seq IN (
|
|
7775
|
+
SELECT seq FROM records WHERE kind = ? AND ${sourceColumn} = ?
|
|
7776
|
+
)`).run(record.recipient_identity, record.message_id === void 0 ? "file" : "message", sourceId);
|
|
7777
|
+
}
|
|
7778
|
+
this.beforeRecordCommit?.();
|
|
7779
|
+
return record;
|
|
7780
|
+
});
|
|
7781
|
+
return transaction.immediate();
|
|
7782
|
+
});
|
|
7700
7783
|
} catch (error) {
|
|
7701
|
-
this.
|
|
7702
|
-
|
|
7703
|
-
try {
|
|
7704
|
-
this.fs.ftruncateSync(fd, originalSize);
|
|
7705
|
-
this.fs.fsyncSync(fd);
|
|
7706
|
-
} catch {
|
|
7707
|
-
}
|
|
7708
|
-
}
|
|
7709
|
-
throw this.wrap(`failed to append room "${validRoomId}" archive`, error);
|
|
7710
|
-
} finally {
|
|
7711
|
-
if (fd !== void 0) {
|
|
7712
|
-
try {
|
|
7713
|
-
this.fs.closeSync(fd);
|
|
7714
|
-
} catch {
|
|
7715
|
-
}
|
|
7716
|
-
}
|
|
7784
|
+
if (blob?.created) this.removeUnreferencedBlob(id, blob.path);
|
|
7785
|
+
throw this.wrap(`failed to append room "${id}" archive`, error);
|
|
7717
7786
|
}
|
|
7718
7787
|
});
|
|
7719
7788
|
}
|
|
7720
7789
|
async read(roomId, options = {}) {
|
|
7721
|
-
const
|
|
7790
|
+
const id = this.roomId(roomId);
|
|
7722
7791
|
const after = options.after ?? 0;
|
|
7723
7792
|
const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
|
|
7724
|
-
|
|
7793
|
+
this.validatePage(after, limit);
|
|
7794
|
+
return this.mutex(id, () => {
|
|
7795
|
+
this.assertRoomDirectory(id);
|
|
7796
|
+
return this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(
|
|
7797
|
+
"SELECT seq,payload_json,blob_path FROM records WHERE seq > ? ORDER BY seq ASC LIMIT ?"
|
|
7798
|
+
).all(after, limit)));
|
|
7799
|
+
});
|
|
7800
|
+
}
|
|
7801
|
+
async query(roomId, options) {
|
|
7802
|
+
const id = this.roomId(roomId);
|
|
7803
|
+
const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
|
|
7725
7804
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
|
|
7726
|
-
return this.mutex(
|
|
7727
|
-
this.assertRoomDirectory(
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7805
|
+
return this.mutex(id, () => {
|
|
7806
|
+
this.assertRoomDirectory(id);
|
|
7807
|
+
return this.withDatabase(id, (db) => {
|
|
7808
|
+
const clauses = [];
|
|
7809
|
+
const values = [];
|
|
7810
|
+
const add = (column, value) => {
|
|
7811
|
+
if (value !== void 0) {
|
|
7812
|
+
clauses.push(`r.${column} = ?`);
|
|
7813
|
+
values.push(value);
|
|
7814
|
+
}
|
|
7815
|
+
};
|
|
7816
|
+
add("kind", options.kind);
|
|
7817
|
+
add("message_id", options.messageId);
|
|
7818
|
+
add("file_id", options.fileId);
|
|
7819
|
+
add("source_msg_id", options.sourceMsgId);
|
|
7820
|
+
add("source_file_id", options.sourceFileId);
|
|
7821
|
+
add("intent_record_id", options.intentRecordId);
|
|
7822
|
+
add("recipient_identity", options.recipientIdentity);
|
|
7823
|
+
add("category", options.category);
|
|
7824
|
+
add("membership_epoch", options.membershipEpoch);
|
|
7825
|
+
if (options.after !== void 0) {
|
|
7826
|
+
clauses.push("r.seq > ?");
|
|
7827
|
+
values.push(options.after);
|
|
7828
|
+
}
|
|
7829
|
+
if (options.unresolvedResultKind) {
|
|
7830
|
+
clauses.push("NOT EXISTS (SELECT 1 FROM records result WHERE result.kind = ? AND result.intent_record_id = r.record_id)");
|
|
7831
|
+
values.push(options.unresolvedResultKind);
|
|
7832
|
+
}
|
|
7833
|
+
values.push(limit);
|
|
7834
|
+
const sql = `SELECT r.seq,r.payload_json,r.blob_path FROM records r${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY r.seq ${options.descending ? "DESC" : "ASC"} LIMIT ?`;
|
|
7835
|
+
return this.decodeRows(id, db.prepare(sql).all(...values));
|
|
7836
|
+
});
|
|
7731
7837
|
});
|
|
7732
7838
|
}
|
|
7839
|
+
async recipients(roomId, recordSeq) {
|
|
7840
|
+
const id = this.roomId(roomId);
|
|
7841
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
|
|
7842
|
+
"SELECT recipient_identity FROM record_recipients WHERE record_seq = ? ORDER BY recipient_identity"
|
|
7843
|
+
).all(recordSeq).map((row) => row.recipient_identity)));
|
|
7844
|
+
}
|
|
7845
|
+
async recordsNeedingRelayIntents(roomId, options = {}) {
|
|
7846
|
+
const id = this.roomId(roomId);
|
|
7847
|
+
const after = options.after ?? 0;
|
|
7848
|
+
const limit = options.limit ?? DEFAULT_WORK_BATCH_SIZE;
|
|
7849
|
+
this.validatePage(after, limit);
|
|
7850
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(`
|
|
7851
|
+
SELECT source.seq,source.payload_json,source.blob_path
|
|
7852
|
+
FROM relay_intent_work work INDEXED BY relay_work_source
|
|
7853
|
+
JOIN records source ON source.seq = work.record_seq
|
|
7854
|
+
WHERE work.record_seq > ?
|
|
7855
|
+
GROUP BY source.seq
|
|
7856
|
+
ORDER BY source.seq ASC
|
|
7857
|
+
LIMIT ?
|
|
7858
|
+
`).all(after, limit))));
|
|
7859
|
+
}
|
|
7860
|
+
async relayRecipientsNeedingIntent(roomId, recordSeq, limit = DEFAULT_WORK_BATCH_SIZE) {
|
|
7861
|
+
const id = this.roomId(roomId);
|
|
7862
|
+
if (!Number.isSafeInteger(recordSeq) || recordSeq < 1) throw new CoworkStorageError("record sequence must be a positive safe integer");
|
|
7863
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
|
|
7864
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
|
|
7865
|
+
"SELECT recipient_identity FROM relay_intent_work WHERE record_seq = ? ORDER BY recipient_identity LIMIT ?"
|
|
7866
|
+
).all(recordSeq, limit).map((row) => row.recipient_identity)));
|
|
7867
|
+
}
|
|
7868
|
+
async briefingDeliveryTimes(roomId, key, recipientIdentities) {
|
|
7869
|
+
const id = this.roomId(roomId);
|
|
7870
|
+
if (recipientIdentities.length === 0) return /* @__PURE__ */ new Map();
|
|
7871
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => {
|
|
7872
|
+
const lookup = db.prepare(`SELECT records.at FROM record_recipients recipients
|
|
7873
|
+
INDEXED BY recipients_briefing_delivery
|
|
7874
|
+
JOIN records ON records.seq = recipients.record_seq
|
|
7875
|
+
WHERE recipients.recipient_identity = ? AND recipients.category = ?
|
|
7876
|
+
AND recipients.briefing_role IS ? AND recipients.briefing_version = ?
|
|
7877
|
+
ORDER BY recipients.record_seq ASC LIMIT 1`);
|
|
7878
|
+
const deliveries = /* @__PURE__ */ new Map();
|
|
7879
|
+
for (const recipient of recipientIdentities) {
|
|
7880
|
+
const row = lookup.get(
|
|
7881
|
+
recipient,
|
|
7882
|
+
key.category,
|
|
7883
|
+
key.briefingRole ?? null,
|
|
7884
|
+
key.briefingVersion
|
|
7885
|
+
);
|
|
7886
|
+
if (row) deliveries.set(recipient, row.at);
|
|
7887
|
+
}
|
|
7888
|
+
return deliveries;
|
|
7889
|
+
}));
|
|
7890
|
+
}
|
|
7891
|
+
async durability(roomId) {
|
|
7892
|
+
const id = this.roomId(roomId);
|
|
7893
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => ({
|
|
7894
|
+
journalMode: db.pragma("journal_mode", { simple: true }),
|
|
7895
|
+
synchronous: db.pragma("synchronous", { simple: true })
|
|
7896
|
+
})));
|
|
7897
|
+
}
|
|
7733
7898
|
async delete(roomId) {
|
|
7734
|
-
const
|
|
7735
|
-
await this.mutex(
|
|
7899
|
+
const id = this.roomId(roomId);
|
|
7900
|
+
await this.mutex(id, () => {
|
|
7736
7901
|
this.ensureBaseDirectories();
|
|
7737
|
-
const roomDir = this.roomDirectory(
|
|
7902
|
+
const roomDir = this.roomDirectory(id);
|
|
7738
7903
|
if (!this.lstatIfPresent(roomDir)) {
|
|
7739
7904
|
this.fsyncDirectory(this.roomsDirectory());
|
|
7740
|
-
this.nextSequences.delete(validRoomId);
|
|
7741
7905
|
return;
|
|
7742
7906
|
}
|
|
7743
|
-
this.ensurePrivateDirectory(roomDir, false, `room "${
|
|
7744
|
-
const
|
|
7745
|
-
const
|
|
7746
|
-
const archivePresent = this.lstatIfPresent(archivePath) !== void 0;
|
|
7747
|
-
const
|
|
7907
|
+
this.ensurePrivateDirectory(roomDir, false, `room "${id}" directory`);
|
|
7908
|
+
const metadata = this.metadataPath(id);
|
|
7909
|
+
const metadataPresent = this.lstatIfPresent(metadata) !== void 0;
|
|
7910
|
+
const archivePresent = this.lstatIfPresent(this.archivePath(id)) !== void 0;
|
|
7911
|
+
const blobsPresent = this.lstatIfPresent(this.blobsDirectory(id)) !== void 0;
|
|
7748
7912
|
if (metadataPresent) {
|
|
7749
|
-
const room = this.loadUnlocked(
|
|
7750
|
-
if (room.state !== "closed") {
|
|
7751
|
-
throw new CoworkStorageError(`room "${validRoomId}" must be closed before deletion`);
|
|
7752
|
-
}
|
|
7913
|
+
const room = this.loadUnlocked(id);
|
|
7914
|
+
if (room.state !== "closed") throw new CoworkStorageError(`room "${id}" must be closed before deletion`);
|
|
7753
7915
|
this.removeProvisioningArtifacts(roomDir);
|
|
7754
|
-
} else if (archivePresent) {
|
|
7755
|
-
throw new CoworkStorageError(
|
|
7756
|
-
`room "${validRoomId}" has archive residue without deletion metadata`
|
|
7757
|
-
);
|
|
7916
|
+
} else if (archivePresent || blobsPresent) {
|
|
7917
|
+
throw new CoworkStorageError(`room "${id}" has archive residue without deletion metadata`);
|
|
7758
7918
|
}
|
|
7759
|
-
const expected = /* @__PURE__ */ new Set(["archive.
|
|
7919
|
+
const expected = /* @__PURE__ */ new Set(["archive.sqlite3", "archive.sqlite3-wal", "archive.sqlite3-shm", "blobs", "room.json", "room.json.v1.bak"]);
|
|
7760
7920
|
const unexpected = this.fs.readdirSync(roomDir).filter((name) => !expected.has(name));
|
|
7761
|
-
if (unexpected.length
|
|
7762
|
-
|
|
7921
|
+
if (unexpected.length) throw new CoworkStorageError(`room "${id}" contains live or unexpected residue: ${unexpected.join(", ")}`);
|
|
7922
|
+
for (const name of ["archive.sqlite3-wal", "archive.sqlite3-shm", "archive.sqlite3", "room.json.v1.bak", "room.json"]) {
|
|
7923
|
+
const path = join3(roomDir, name);
|
|
7924
|
+
if (this.lstatIfPresent(path)) {
|
|
7925
|
+
this.assertRegularFile(path, name);
|
|
7926
|
+
this.fs.unlinkSync(path);
|
|
7927
|
+
}
|
|
7763
7928
|
}
|
|
7764
|
-
const
|
|
7765
|
-
if (this.lstatIfPresent(
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7929
|
+
const blobs = this.blobsDirectory(id);
|
|
7930
|
+
if (this.lstatIfPresent(blobs)) this.fs.rmSync(blobs, { recursive: true, force: true });
|
|
7931
|
+
this.fsyncDirectory(roomDir);
|
|
7932
|
+
this.fs.rmdirSync(roomDir);
|
|
7933
|
+
this.fsyncDirectory(this.roomsDirectory());
|
|
7934
|
+
});
|
|
7935
|
+
}
|
|
7936
|
+
withDatabase(roomId, work, create = false) {
|
|
7937
|
+
const path = this.archivePath(roomId);
|
|
7938
|
+
let guardFd;
|
|
7939
|
+
if (!create) {
|
|
7940
|
+
this.assertRegularFile(path, "room archive database");
|
|
7941
|
+
guardFd = this.fs.openSync(path, nodeFs2.constants.O_RDONLY | NO_FOLLOW2);
|
|
7942
|
+
this.validateOpenPath(guardFd, path, "room archive database", "file", true);
|
|
7943
|
+
}
|
|
7944
|
+
let db;
|
|
7945
|
+
try {
|
|
7946
|
+
this.secureSqliteFiles(path);
|
|
7947
|
+
db = new Database(path, { fileMustExist: !create });
|
|
7948
|
+
if (guardFd !== void 0) this.validateOpenPath(guardFd, path, "room archive database", "file", true);
|
|
7949
|
+
this.fs.chmodSync(path, FILE_MODE2);
|
|
7950
|
+
db.pragma("journal_mode = WAL");
|
|
7951
|
+
db.pragma("synchronous = FULL");
|
|
7952
|
+
db.pragma("foreign_keys = ON");
|
|
7953
|
+
db.pragma("busy_timeout = 5000");
|
|
7954
|
+
if (create) {
|
|
7955
|
+
db.exec(`CREATE TABLE records (
|
|
7956
|
+
seq INTEGER PRIMARY KEY, record_id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, at TEXT NOT NULL,
|
|
7957
|
+
payload_json TEXT NOT NULL, blob_path TEXT, message_id TEXT, file_id TEXT, intent_record_id TEXT,
|
|
7958
|
+
recipient_identity TEXT, source_msg_id INTEGER, source_file_id INTEGER, category TEXT,
|
|
7959
|
+
briefing_role TEXT, briefing_version INTEGER, membership_epoch INTEGER
|
|
7960
|
+
);
|
|
7961
|
+
CREATE TABLE record_recipients (
|
|
7962
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
7963
|
+
recipient_identity TEXT NOT NULL, category TEXT, briefing_role TEXT,
|
|
7964
|
+
briefing_version INTEGER, PRIMARY KEY(record_seq, recipient_identity)
|
|
7965
|
+
);
|
|
7966
|
+
CREATE TABLE relay_intent_work (
|
|
7967
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
7968
|
+
recipient_identity TEXT NOT NULL, PRIMARY KEY(record_seq, recipient_identity)
|
|
7969
|
+
);
|
|
7970
|
+
CREATE INDEX relay_work_source ON relay_intent_work(record_seq, recipient_identity);
|
|
7971
|
+
CREATE INDEX records_kind_seq ON records(kind, seq);
|
|
7972
|
+
CREATE INDEX records_message ON records(message_id, kind, seq);
|
|
7973
|
+
CREATE INDEX records_file ON records(file_id, kind, seq);
|
|
7974
|
+
CREATE INDEX records_intent_result ON records(intent_record_id, kind);
|
|
7975
|
+
CREATE INDEX records_relay_recipient ON records(kind, recipient_identity, seq);
|
|
7976
|
+
CREATE UNIQUE INDEX records_source_message ON records(source_msg_id) WHERE kind='message' AND source_msg_id IS NOT NULL;
|
|
7977
|
+
CREATE UNIQUE INDEX records_source_file ON records(source_file_id) WHERE kind='file' AND source_file_id IS NOT NULL;
|
|
7978
|
+
CREATE INDEX records_briefing ON records(category, briefing_role, briefing_version, seq);
|
|
7979
|
+
CREATE INDEX records_membership_epoch ON records(category, membership_epoch);
|
|
7980
|
+
CREATE INDEX recipients_identity ON record_recipients(recipient_identity, record_seq);
|
|
7981
|
+
CREATE INDEX recipients_briefing_delivery ON record_recipients
|
|
7982
|
+
(recipient_identity, category, briefing_role, briefing_version, record_seq);`);
|
|
7983
|
+
db.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
7984
|
+
this.reconciledBlobRooms.add(roomId);
|
|
7985
|
+
} else {
|
|
7986
|
+
const version = db.pragma("user_version", { simple: true });
|
|
7987
|
+
if (version !== SQLITE_SCHEMA_VERSION) {
|
|
7988
|
+
throw new CoworkStorageError(`unsupported room archive schema version ${version}`);
|
|
7989
|
+
}
|
|
7990
|
+
if (!this.reconciledBlobRooms.has(roomId)) {
|
|
7991
|
+
this.reconcileBlobDirectory(roomId, db);
|
|
7992
|
+
this.reconciledBlobRooms.add(roomId);
|
|
7993
|
+
}
|
|
7769
7994
|
}
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7995
|
+
this.secureSqliteFiles(path);
|
|
7996
|
+
const result = work(db);
|
|
7997
|
+
this.secureSqliteFiles(path);
|
|
7998
|
+
return result;
|
|
7999
|
+
} catch (error) {
|
|
8000
|
+
throw this.wrap(`failed to access room "${roomId}" SQLite archive`, error);
|
|
8001
|
+
} finally {
|
|
8002
|
+
try {
|
|
8003
|
+
db?.close();
|
|
8004
|
+
} catch {
|
|
7774
8005
|
}
|
|
7775
|
-
if (
|
|
7776
|
-
this.
|
|
7777
|
-
|
|
7778
|
-
|
|
8006
|
+
if (guardFd !== void 0) try {
|
|
8007
|
+
this.fs.closeSync(guardFd);
|
|
8008
|
+
} catch {
|
|
8009
|
+
}
|
|
8010
|
+
this.secureSqliteFiles(path);
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
indexValues(record) {
|
|
8014
|
+
const subject = record;
|
|
8015
|
+
return {
|
|
8016
|
+
seq: record.seq,
|
|
8017
|
+
record_id: record.record_id,
|
|
8018
|
+
kind: record.kind,
|
|
8019
|
+
at: record.at,
|
|
8020
|
+
message_id: subject.message_id ?? null,
|
|
8021
|
+
file_id: subject.file_id ?? null,
|
|
8022
|
+
intent_record_id: subject.intent_record_id ?? null,
|
|
8023
|
+
recipient_identity: subject.recipient_identity ?? null,
|
|
8024
|
+
source_msg_id: subject.source_msg_id ?? null,
|
|
8025
|
+
source_file_id: subject.source_file_id ?? null,
|
|
8026
|
+
category: subject.category ?? null,
|
|
8027
|
+
briefing_role: subject.briefing_role ?? null,
|
|
8028
|
+
briefing_version: subject.briefing_version ?? null,
|
|
8029
|
+
membership_epoch: typeof subject.membership === "object" && subject.membership !== null ? subject.membership.epoch ?? null : null
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
8032
|
+
decodeRows(roomId, rows) {
|
|
8033
|
+
return rows.map((row) => {
|
|
8034
|
+
let decoded;
|
|
8035
|
+
try {
|
|
8036
|
+
decoded = JSON.parse(row.payload_json);
|
|
8037
|
+
} catch (error) {
|
|
8038
|
+
throw new CoworkStorageError(`malformed JSON in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
|
|
8039
|
+
}
|
|
8040
|
+
if (row.blob_path !== null) {
|
|
8041
|
+
const subject = decoded;
|
|
8042
|
+
const expectedPath = subject.kind === "file" && typeof subject.sha256 === "string" && /^[0-9a-f]{64}$/.test(subject.sha256) ? join3("blobs", subject.sha256) : void 0;
|
|
8043
|
+
if (expectedPath === void 0 || row.blob_path !== expectedPath) {
|
|
8044
|
+
throw new CoworkStorageError(`invalid blob reference in room "${roomId}" archive at sequence ${row.seq}`);
|
|
8045
|
+
}
|
|
8046
|
+
const bytes = this.readFileNoFollow(join3(this.roomDirectory(roomId), expectedPath), "room file blob");
|
|
8047
|
+
decoded = { ...decoded, data_base64: bytes.toString("base64") };
|
|
8048
|
+
}
|
|
8049
|
+
try {
|
|
8050
|
+
const record = CommunicationRecordSchema.parse(decoded);
|
|
8051
|
+
if (record.room_id !== roomId || record.seq !== row.seq) throw new Error("indexed identity mismatch");
|
|
8052
|
+
return record;
|
|
8053
|
+
} catch (error) {
|
|
8054
|
+
throw new CoworkStorageError(`invalid record in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
|
|
7779
8055
|
}
|
|
7780
|
-
this.fs.rmdirSync(roomDir);
|
|
7781
|
-
this.fsyncDirectory(this.roomsDirectory());
|
|
7782
|
-
this.nextSequences.delete(validRoomId);
|
|
7783
8056
|
});
|
|
7784
8057
|
}
|
|
7785
|
-
|
|
7786
|
-
const
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
if (
|
|
7792
|
-
|
|
8058
|
+
persistBlob(roomId, digest, bytes) {
|
|
8059
|
+
const directory = this.blobsDirectory(roomId);
|
|
8060
|
+
this.ensurePrivateDirectory(directory, true, "room blobs directory");
|
|
8061
|
+
const final = join3(directory, digest);
|
|
8062
|
+
if (this.lstatIfPresent(final)) {
|
|
8063
|
+
this.assertRegularFile(final, "room file blob");
|
|
8064
|
+
if (this.readFileNoFollow(final, "room file blob").equals(bytes)) return { path: join3("blobs", digest), created: false };
|
|
8065
|
+
throw new CoworkStorageError(`immutable blob collision for ${digest}`);
|
|
8066
|
+
}
|
|
8067
|
+
const temp = join3(directory, `.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
|
|
8068
|
+
let fd;
|
|
8069
|
+
try {
|
|
8070
|
+
fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
|
|
8071
|
+
this.writeAll(fd, bytes);
|
|
8072
|
+
this.fs.fsyncSync(fd);
|
|
8073
|
+
this.fs.closeSync(fd);
|
|
8074
|
+
fd = void 0;
|
|
8075
|
+
this.fs.renameSync(temp, final);
|
|
8076
|
+
this.fsyncDirectory(directory);
|
|
8077
|
+
this.fs.chmodSync(final, FILE_MODE2);
|
|
8078
|
+
return { path: join3("blobs", digest), created: true };
|
|
8079
|
+
} finally {
|
|
8080
|
+
if (fd !== void 0) try {
|
|
8081
|
+
this.fs.closeSync(fd);
|
|
8082
|
+
} catch {
|
|
8083
|
+
}
|
|
8084
|
+
if (this.lstatIfPresent(temp)) try {
|
|
8085
|
+
this.fs.unlinkSync(temp);
|
|
8086
|
+
} catch {
|
|
7793
8087
|
}
|
|
7794
|
-
targets.push(join3(roomDir, stagingName));
|
|
7795
8088
|
}
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
8089
|
+
}
|
|
8090
|
+
removeUnreferencedBlob(roomId, relativePath) {
|
|
8091
|
+
const absolute = join3(this.roomDirectory(roomId), relativePath);
|
|
8092
|
+
try {
|
|
8093
|
+
const referenced = this.withDatabase(roomId, (db) => db.prepare(
|
|
8094
|
+
"SELECT EXISTS(SELECT 1 FROM records WHERE blob_path = ?) AS found"
|
|
8095
|
+
).get(relativePath).found !== 0);
|
|
8096
|
+
if (referenced || !this.lstatIfPresent(absolute)) return;
|
|
8097
|
+
this.assertRegularFile(absolute, "unreferenced room file blob");
|
|
8098
|
+
this.fs.unlinkSync(absolute);
|
|
8099
|
+
this.fsyncDirectory(dirname2(absolute));
|
|
8100
|
+
} catch {
|
|
7802
8101
|
}
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
8102
|
+
}
|
|
8103
|
+
reconcileBlobDirectory(roomId, db) {
|
|
8104
|
+
const directory = this.blobsDirectory(roomId);
|
|
8105
|
+
this.ensurePrivateDirectory(directory, true, "room blobs directory");
|
|
8106
|
+
const referenced = new Set(db.prepare(
|
|
8107
|
+
"SELECT blob_path FROM records WHERE blob_path IS NOT NULL"
|
|
8108
|
+
).all().map((row) => basename(row.blob_path)));
|
|
8109
|
+
let changed = false;
|
|
8110
|
+
for (const entry of this.fs.readdirSync(directory, { withFileTypes: true })) {
|
|
8111
|
+
const path = join3(directory, entry.name);
|
|
8112
|
+
if (/^\.tmp-[0-9]+-[0-9a-f]{16}$/.test(entry.name)) {
|
|
8113
|
+
this.assertRegularFile(path, "crash-left room blob temporary file");
|
|
8114
|
+
this.fs.unlinkSync(path);
|
|
8115
|
+
changed = true;
|
|
8116
|
+
continue;
|
|
8117
|
+
}
|
|
8118
|
+
if (!/^[0-9a-f]{64}$/.test(entry.name)) throw new CoworkStorageError(`unexpected room blob residue: ${entry.name}`);
|
|
8119
|
+
this.assertRegularFile(path, referenced.has(entry.name) ? "room file blob" : "unreferenced room file blob");
|
|
8120
|
+
this.fs.chmodSync(path, FILE_MODE2);
|
|
8121
|
+
if (referenced.has(entry.name)) continue;
|
|
8122
|
+
this.fs.unlinkSync(path);
|
|
8123
|
+
changed = true;
|
|
8124
|
+
}
|
|
8125
|
+
if (changed) this.fsyncDirectory(directory);
|
|
8126
|
+
}
|
|
8127
|
+
secureSqliteFiles(path) {
|
|
8128
|
+
for (const candidate of [path, `${path}-wal`, `${path}-shm`]) {
|
|
8129
|
+
if (!this.lstatIfPresent(candidate)) continue;
|
|
8130
|
+
this.assertRegularFile(candidate, `SQLite file ${basename(candidate)}`);
|
|
8131
|
+
this.fs.chmodSync(candidate, FILE_MODE2);
|
|
7806
8132
|
}
|
|
7807
8133
|
}
|
|
8134
|
+
validatePage(after, limit) {
|
|
8135
|
+
if (!Number.isSafeInteger(after) || after < 0) throw new CoworkStorageError("after must be a non-negative safe integer");
|
|
8136
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
|
|
8137
|
+
}
|
|
7808
8138
|
loadUnlocked(roomId) {
|
|
7809
8139
|
this.assertRoomDirectory(roomId);
|
|
7810
8140
|
const path = this.metadataPath(roomId);
|
|
7811
8141
|
this.assertRegularFile(path, "room metadata");
|
|
8142
|
+
let decoded;
|
|
7812
8143
|
let bytes;
|
|
7813
8144
|
try {
|
|
7814
8145
|
bytes = this.readFileNoFollow(path, "room metadata");
|
|
7815
|
-
} catch (error) {
|
|
7816
|
-
throw this.wrap(`failed to read room "${roomId}" metadata`, error);
|
|
7817
|
-
}
|
|
7818
|
-
let decoded;
|
|
7819
|
-
try {
|
|
7820
8146
|
decoded = JSON.parse(utf8Decoder.decode(bytes));
|
|
7821
8147
|
} catch (error) {
|
|
7822
8148
|
throw this.wrap(`malformed metadata for room "${roomId}"`, error);
|
|
7823
8149
|
}
|
|
7824
8150
|
const room = this.isVersion1(decoded) ? this.migrateUnlocked(roomId, decoded, bytes) : RoomSchema.parse(decoded);
|
|
7825
|
-
if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name)
|
|
7826
|
-
this.atomicMetadataWrite(this.metadataPath(roomId), room);
|
|
7827
|
-
}
|
|
8151
|
+
if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name) this.atomicMetadataWrite(path, room);
|
|
7828
8152
|
if (room.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
|
|
7829
8153
|
return room;
|
|
7830
8154
|
}
|
|
@@ -7834,79 +8158,54 @@ var init_storage = __esm({
|
|
|
7834
8158
|
persistedRoomName(decoded) {
|
|
7835
8159
|
return typeof decoded === "object" && decoded !== null ? decoded.room_name : void 0;
|
|
7836
8160
|
}
|
|
7837
|
-
|
|
7838
|
-
* Lazy additive v1 → v2 migration: preserve the exact pre-migration
|
|
7839
|
-
* bytes once as room.json.v1.bak, then atomically persist the v2 metadata.
|
|
7840
|
-
*
|
|
7841
|
-
* THE BACKUP IS WRITTEN TEMP → FSYNC → RENAME, not opened in place, and the
|
|
7842
|
-
* reason is a crash window that an existence check cannot see. The earlier
|
|
7843
|
-
* version guarded with `if (!lstatIfPresent(backupPath))` and wrote straight
|
|
7844
|
-
* into the final path under O_CREAT|O_EXCL. A crash inside that write leaves a
|
|
7845
|
-
* PARTIAL file that nonetheless EXISTS, so the next load's existence check
|
|
7846
|
-
* skips the backup, writes v2, and the pre-migration bytes are gone — no
|
|
7847
|
-
* error, no warning, and the one artefact that exists to undo a bad migration
|
|
7848
|
-
* is a truncated fragment. Measured before the fix: a 40-byte prefix of a
|
|
7849
|
-
* 604-byte room, JSON.parse false, room.json already v2.
|
|
7850
|
-
*
|
|
7851
|
-
* A rename is atomic, so the final path now only ever appears complete. The
|
|
7852
|
-
* temp file is created with O_EXCL under a pid+random name and removed on
|
|
7853
|
-
* failure, so a crashed attempt leaves at most an orphan temp, never a
|
|
7854
|
-
* plausible-looking backup.
|
|
7855
|
-
*
|
|
7856
|
-
* AND AN EXISTING BACKUP IS PARSED BEFORE IT IS TRUSTED, which repairs the
|
|
7857
|
-
* case where a partial file is ALREADY on disk from a build without this fix —
|
|
7858
|
-
* exactly the state a host that ran the previous code could be in right now.
|
|
7859
|
-
* A backup that does not parse as v1 is replaced by the bytes we hold, because
|
|
7860
|
-
* those are the real pre-migration bytes and the fragment is worthless.
|
|
7861
|
-
*
|
|
7862
|
-
* WHAT THE BACKUP IS NOT: restoring it is NOT a rollback. See the note on
|
|
7863
|
-
* `restoreV1Backup` — re-migrating mints fresh participant ids.
|
|
7864
|
-
*/
|
|
7865
|
-
migrateUnlocked(roomId, decoded, originalBytes) {
|
|
8161
|
+
migrateUnlocked(roomId, decoded, original) {
|
|
7866
8162
|
const v1 = RoomV1Schema.parse(decoded);
|
|
7867
8163
|
if (v1.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
|
|
7868
8164
|
const migrated = migrateRoomV1(v1, generateUlid);
|
|
7869
|
-
const
|
|
7870
|
-
if (!this.hasIntactV1Backup(
|
|
7871
|
-
this.atomicBytesWrite(backupPath, originalBytes, "room metadata v1 backup");
|
|
7872
|
-
}
|
|
8165
|
+
const backup = `${this.metadataPath(roomId)}.v1.bak`;
|
|
8166
|
+
if (!this.hasIntactV1Backup(backup)) this.atomicBytesWrite(backup, original, "room metadata v1 backup");
|
|
7873
8167
|
this.atomicMetadataWrite(this.metadataPath(roomId), migrated);
|
|
7874
8168
|
return migrated;
|
|
7875
8169
|
}
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
*
|
|
7879
|
-
* Existence is not the question — a partial file exists. It must parse and
|
|
7880
|
-
* still claim to be the v1 metadata for this room; anything else is a fragment
|
|
7881
|
-
* and is better overwritten with the bytes we are holding right now.
|
|
7882
|
-
*/
|
|
7883
|
-
hasIntactV1Backup(backupPath) {
|
|
7884
|
-
if (!this.lstatIfPresent(backupPath)) return false;
|
|
8170
|
+
hasIntactV1Backup(path) {
|
|
8171
|
+
if (!this.lstatIfPresent(path)) return false;
|
|
7885
8172
|
try {
|
|
7886
|
-
|
|
7887
|
-
return this.isVersion1(decoded);
|
|
8173
|
+
return this.isVersion1(JSON.parse(utf8Decoder.decode(this.readFileNoFollow(path, "room metadata v1 backup"))));
|
|
7888
8174
|
} catch {
|
|
7889
8175
|
return false;
|
|
7890
8176
|
}
|
|
7891
8177
|
}
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
8178
|
+
removeProvisioningArtifacts(roomDir) {
|
|
8179
|
+
const journal = join3(roomDir, ".cowork-provisioning-stage");
|
|
8180
|
+
const targets = [join3(roomDir, "live"), join3(roomDir, "provisioning-residue")];
|
|
8181
|
+
if (this.lstatIfPresent(journal)) {
|
|
8182
|
+
this.assertRegularFile(journal, "provisioning staging journal");
|
|
8183
|
+
const name = this.fs.readFileSync(journal, "utf8").trim();
|
|
8184
|
+
if (!/^live\.staging-[0-9a-f]{32}$/.test(name)) throw new CoworkStorageError("invalid provisioning staging journal");
|
|
8185
|
+
targets.push(join3(roomDir, name));
|
|
8186
|
+
}
|
|
8187
|
+
for (const target of targets) {
|
|
8188
|
+
const stat = this.lstatIfPresent(target);
|
|
8189
|
+
if (!stat) continue;
|
|
8190
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) this.fs.unlinkSync(target);
|
|
8191
|
+
else this.fs.rmSync(target, { recursive: true, force: true });
|
|
8192
|
+
this.fsyncDirectory(roomDir);
|
|
8193
|
+
}
|
|
8194
|
+
if (this.lstatIfPresent(journal)) {
|
|
8195
|
+
this.fs.unlinkSync(journal);
|
|
8196
|
+
this.fsyncDirectory(roomDir);
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
atomicMetadataWrite(path, room) {
|
|
8200
|
+
this.atomicBytesWrite(path, Buffer.from(`${JSON.stringify(room)}
|
|
8201
|
+
`), "room metadata");
|
|
8202
|
+
}
|
|
7898
8203
|
atomicBytesWrite(path, bytes, label) {
|
|
7899
8204
|
if (this.lstatIfPresent(path)) this.assertRegularFile(path, label);
|
|
7900
8205
|
const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
|
|
7901
8206
|
let fd;
|
|
7902
8207
|
try {
|
|
7903
|
-
fd = this.fs.openSync(
|
|
7904
|
-
temp,
|
|
7905
|
-
nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
|
|
7906
|
-
FILE_MODE2
|
|
7907
|
-
);
|
|
7908
|
-
this.validateOpenPath(fd, temp, `temporary ${label}`, "file", true);
|
|
7909
|
-
this.fs.fchmodSync(fd, FILE_MODE2);
|
|
8208
|
+
fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
|
|
7910
8209
|
this.writeAll(fd, bytes);
|
|
7911
8210
|
this.fs.fsyncSync(fd);
|
|
7912
8211
|
this.fs.closeSync(fd);
|
|
@@ -7914,11 +8213,9 @@ var init_storage = __esm({
|
|
|
7914
8213
|
this.fs.renameSync(temp, path);
|
|
7915
8214
|
this.fsyncDirectory(dirname2(path));
|
|
7916
8215
|
} catch (error) {
|
|
7917
|
-
if (fd !== void 0) {
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
} catch {
|
|
7921
|
-
}
|
|
8216
|
+
if (fd !== void 0) try {
|
|
8217
|
+
this.fs.closeSync(fd);
|
|
8218
|
+
} catch {
|
|
7922
8219
|
}
|
|
7923
8220
|
try {
|
|
7924
8221
|
this.fs.rmSync(temp, { force: true });
|
|
@@ -7927,84 +8224,31 @@ var init_storage = __esm({
|
|
|
7927
8224
|
throw this.wrap(`failed to write ${label} at ${path}`, error);
|
|
7928
8225
|
}
|
|
7929
8226
|
}
|
|
7930
|
-
scanArchive(roomId) {
|
|
7931
|
-
const path = this.archivePath(roomId);
|
|
7932
|
-
this.assertRegularFile(path, "room archive");
|
|
7933
|
-
let bytes;
|
|
7934
|
-
try {
|
|
7935
|
-
bytes = this.readFileNoFollow(path, "room archive");
|
|
7936
|
-
} catch (error) {
|
|
7937
|
-
throw this.wrap(`failed to read room "${roomId}" archive`, error);
|
|
7938
|
-
}
|
|
7939
|
-
const records = [];
|
|
7940
|
-
let byteOffset = 0;
|
|
7941
|
-
let expectedSequence = 1;
|
|
7942
|
-
while (byteOffset < bytes.byteLength) {
|
|
7943
|
-
const newline = bytes.indexOf(10, byteOffset);
|
|
7944
|
-
if (newline === -1) {
|
|
7945
|
-
throw new CoworkStorageError(`partial JSON record in room "${roomId}" archive at byte offset ${byteOffset}`);
|
|
7946
|
-
}
|
|
7947
|
-
const line = bytes.subarray(byteOffset, newline);
|
|
7948
|
-
let decoded;
|
|
7949
|
-
try {
|
|
7950
|
-
decoded = JSON.parse(utf8Decoder.decode(line));
|
|
7951
|
-
} catch (error) {
|
|
7952
|
-
throw new CoworkStorageError(
|
|
7953
|
-
`malformed JSON in room "${roomId}" archive at byte offset ${byteOffset}`,
|
|
7954
|
-
{ cause: error }
|
|
7955
|
-
);
|
|
7956
|
-
}
|
|
7957
|
-
const observedSequence = typeof decoded === "object" && decoded !== null && "seq" in decoded ? decoded.seq : void 0;
|
|
7958
|
-
if (observedSequence !== expectedSequence) {
|
|
7959
|
-
throw new CoworkStorageError(
|
|
7960
|
-
`non-monotonic sequence in room "${roomId}" archive at byte offset ${byteOffset}: expected ${expectedSequence}, found ${String(observedSequence)}`
|
|
7961
|
-
);
|
|
7962
|
-
}
|
|
7963
|
-
let record;
|
|
7964
|
-
try {
|
|
7965
|
-
record = CommunicationRecordSchema.parse(decoded);
|
|
7966
|
-
} catch (error) {
|
|
7967
|
-
throw new CoworkStorageError(
|
|
7968
|
-
`invalid record in room "${roomId}" archive at byte offset ${byteOffset}`,
|
|
7969
|
-
{ cause: error }
|
|
7970
|
-
);
|
|
7971
|
-
}
|
|
7972
|
-
if (record.room_id !== roomId) {
|
|
7973
|
-
throw new CoworkStorageError(
|
|
7974
|
-
`record room_id mismatch in room "${roomId}" archive at byte offset ${byteOffset}`
|
|
7975
|
-
);
|
|
7976
|
-
}
|
|
7977
|
-
records.push(record);
|
|
7978
|
-
expectedSequence += 1;
|
|
7979
|
-
byteOffset = newline + 1;
|
|
7980
|
-
}
|
|
7981
|
-
return { records, nextSequence: expectedSequence };
|
|
7982
|
-
}
|
|
7983
8227
|
ensureBaseDirectories() {
|
|
7984
8228
|
this.ensurePrivateDirectory(this.stateDir, true, "state directory");
|
|
7985
8229
|
this.ensurePrivateDirectory(this.roomsDirectory(), true, "rooms directory");
|
|
7986
8230
|
}
|
|
8231
|
+
assertRoomDirectory(roomId) {
|
|
8232
|
+
this.ensureBaseDirectories();
|
|
8233
|
+
this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
|
|
8234
|
+
}
|
|
7987
8235
|
ensurePrivateDirectory(path, create, label) {
|
|
7988
8236
|
let stat = this.lstatIfPresent(path);
|
|
7989
|
-
|
|
7990
|
-
if (created) {
|
|
8237
|
+
if (!stat) {
|
|
7991
8238
|
if (!create) throw new CoworkStorageError(`${label} does not exist`);
|
|
7992
8239
|
this.createPrivateDirectoryTree(path, label);
|
|
7993
8240
|
stat = this.fs.lstatSync(path);
|
|
7994
8241
|
}
|
|
7995
|
-
|
|
7996
|
-
if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
7997
|
-
if (!current.isDirectory()) throw new CoworkStorageError(`${label} is not a directory`);
|
|
8242
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} is not a private directory`);
|
|
7998
8243
|
this.fs.chmodSync(path, DIRECTORY_MODE2);
|
|
7999
8244
|
}
|
|
8000
8245
|
createPrivateDirectoryTree(path, label) {
|
|
8001
8246
|
const missing = [];
|
|
8002
8247
|
let cursor = path;
|
|
8003
8248
|
for (; ; ) {
|
|
8004
|
-
const
|
|
8005
|
-
if (
|
|
8006
|
-
if (
|
|
8007
|
-
if (!existing.isDirectory()) throw new CoworkStorageError(`${label} parent is not a directory`);
|
|
8249
|
+
const stat = this.lstatIfPresent(cursor);
|
|
8250
|
+
if (stat) {
|
|
8251
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} parent is unsafe`);
|
|
8008
8252
|
break;
|
|
8009
8253
|
}
|
|
8010
8254
|
missing.push(cursor);
|
|
@@ -8012,120 +8256,45 @@ var init_storage = __esm({
|
|
|
8012
8256
|
if (parent === cursor) throw new CoworkStorageError(`cannot find existing parent for ${label}`);
|
|
8013
8257
|
cursor = parent;
|
|
8014
8258
|
}
|
|
8015
|
-
const
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
this.fs.chmodSync(directory, DIRECTORY_MODE2);
|
|
8021
|
-
this.fsyncDirectory(directory);
|
|
8022
|
-
this.fsyncDirectory(dirname2(directory));
|
|
8023
|
-
}
|
|
8024
|
-
} catch (error) {
|
|
8025
|
-
for (const directory of created.reverse()) {
|
|
8026
|
-
try {
|
|
8027
|
-
this.fs.rmdirSync(directory);
|
|
8028
|
-
this.fsyncDirectory(dirname2(directory));
|
|
8029
|
-
} catch {
|
|
8030
|
-
}
|
|
8031
|
-
}
|
|
8032
|
-
throw error;
|
|
8259
|
+
for (const directory of missing.reverse()) {
|
|
8260
|
+
this.fs.mkdirSync(directory, { mode: DIRECTORY_MODE2 });
|
|
8261
|
+
this.fs.chmodSync(directory, DIRECTORY_MODE2);
|
|
8262
|
+
this.fsyncDirectory(directory);
|
|
8263
|
+
this.fsyncDirectory(dirname2(directory));
|
|
8033
8264
|
}
|
|
8034
8265
|
}
|
|
8035
|
-
assertRoomDirectory(roomId) {
|
|
8036
|
-
this.ensureBaseDirectories();
|
|
8037
|
-
this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
|
|
8038
|
-
}
|
|
8039
8266
|
rejectSymlink(path, label) {
|
|
8040
|
-
if (this.fs.lstatSync(path).isSymbolicLink()) {
|
|
8041
|
-
throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
8042
|
-
}
|
|
8267
|
+
if (this.fs.lstatSync(path).isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
8043
8268
|
}
|
|
8044
8269
|
assertRegularFile(path, label) {
|
|
8045
8270
|
this.rejectSymlink(path, label);
|
|
8046
8271
|
const stat = this.fs.lstatSync(path);
|
|
8047
|
-
if (!stat.isFile()) throw new CoworkStorageError(`${label} is not a regular file`);
|
|
8048
|
-
if (stat.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${stat.nlink}`);
|
|
8049
|
-
}
|
|
8050
|
-
lstatIfPresent(path) {
|
|
8051
|
-
try {
|
|
8052
|
-
return this.fs.lstatSync(path);
|
|
8053
|
-
} catch (error) {
|
|
8054
|
-
if (error.code === "ENOENT") return void 0;
|
|
8055
|
-
throw error;
|
|
8056
|
-
}
|
|
8272
|
+
if (!stat.isFile() || stat.nlink !== 1) throw new CoworkStorageError(`${label} is not a safe regular file`);
|
|
8057
8273
|
}
|
|
8058
8274
|
validateOpenPath(fd, path, label, kind, requireSingleLink) {
|
|
8059
8275
|
const opened = this.fs.fstatSync(fd);
|
|
8060
8276
|
const validKind = kind === "file" ? opened.isFile() : opened.isDirectory();
|
|
8061
8277
|
if (!validKind) throw new CoworkStorageError(`${label} open descriptor is not a regular ${kind}`);
|
|
8062
|
-
if (requireSingleLink && opened.nlink !== 1) {
|
|
8063
|
-
throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
|
|
8064
|
-
}
|
|
8278
|
+
if (requireSingleLink && opened.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
|
|
8065
8279
|
const current = this.fs.lstatSync(path);
|
|
8066
8280
|
if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
8067
|
-
if (current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
8068
|
-
throw new CoworkStorageError(`${label} inode changed during open`);
|
|
8069
|
-
}
|
|
8281
|
+
if (current.dev !== opened.dev || current.ino !== opened.ino) throw new CoworkStorageError(`${label} inode changed during open`);
|
|
8070
8282
|
return opened;
|
|
8071
8283
|
}
|
|
8072
|
-
|
|
8073
|
-
const path = this.archivePath(roomId);
|
|
8074
|
-
let fd;
|
|
8075
|
-
try {
|
|
8076
|
-
fd = this.fs.openSync(
|
|
8077
|
-
path,
|
|
8078
|
-
nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
|
|
8079
|
-
FILE_MODE2
|
|
8080
|
-
);
|
|
8081
|
-
this.validateOpenPath(fd, path, "room archive", "file", true);
|
|
8082
|
-
this.fs.fchmodSync(fd, FILE_MODE2);
|
|
8083
|
-
this.fs.fsyncSync(fd);
|
|
8084
|
-
} finally {
|
|
8085
|
-
if (fd !== void 0) this.fs.closeSync(fd);
|
|
8086
|
-
}
|
|
8087
|
-
this.fsyncDirectory(dirname2(path));
|
|
8088
|
-
}
|
|
8089
|
-
atomicMetadataWrite(path, room) {
|
|
8090
|
-
if (this.lstatIfPresent(path)) this.assertRegularFile(path, "room metadata");
|
|
8091
|
-
const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
|
|
8092
|
-
const bytes = Buffer.from(`${JSON.stringify(room)}
|
|
8093
|
-
`, "utf8");
|
|
8094
|
-
let fd;
|
|
8284
|
+
lstatIfPresent(path) {
|
|
8095
8285
|
try {
|
|
8096
|
-
|
|
8097
|
-
temp,
|
|
8098
|
-
nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
|
|
8099
|
-
FILE_MODE2
|
|
8100
|
-
);
|
|
8101
|
-
this.validateOpenPath(fd, temp, "temporary room metadata", "file", true);
|
|
8102
|
-
this.fs.fchmodSync(fd, FILE_MODE2);
|
|
8103
|
-
this.writeAll(fd, bytes);
|
|
8104
|
-
this.fs.fsyncSync(fd);
|
|
8105
|
-
this.fs.closeSync(fd);
|
|
8106
|
-
fd = void 0;
|
|
8107
|
-
this.fs.renameSync(temp, path);
|
|
8108
|
-
this.fsyncDirectory(dirname2(path));
|
|
8286
|
+
return this.fs.lstatSync(path);
|
|
8109
8287
|
} catch (error) {
|
|
8110
|
-
if (
|
|
8111
|
-
|
|
8112
|
-
this.fs.closeSync(fd);
|
|
8113
|
-
} catch {
|
|
8114
|
-
}
|
|
8115
|
-
}
|
|
8116
|
-
try {
|
|
8117
|
-
this.fs.rmSync(temp, { force: true });
|
|
8118
|
-
} catch {
|
|
8119
|
-
}
|
|
8120
|
-
throw this.wrap(`failed to atomically persist ${path}`, error);
|
|
8288
|
+
if (error.code === "ENOENT") return void 0;
|
|
8289
|
+
throw error;
|
|
8121
8290
|
}
|
|
8122
8291
|
}
|
|
8123
8292
|
writeAll(fd, bytes) {
|
|
8124
8293
|
let offset = 0;
|
|
8125
|
-
while (offset < bytes.
|
|
8126
|
-
const
|
|
8127
|
-
if (
|
|
8128
|
-
offset +=
|
|
8294
|
+
while (offset < bytes.length) {
|
|
8295
|
+
const n = this.fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
|
|
8296
|
+
if (n <= 0) throw new CoworkStorageError("write made no progress");
|
|
8297
|
+
offset += n;
|
|
8129
8298
|
}
|
|
8130
8299
|
}
|
|
8131
8300
|
readFileNoFollow(path, label) {
|
|
@@ -8163,10 +8332,13 @@ var init_storage = __esm({
|
|
|
8163
8332
|
return join3(this.roomDirectory(roomId), "room.json");
|
|
8164
8333
|
}
|
|
8165
8334
|
archivePath(roomId) {
|
|
8166
|
-
return join3(this.roomDirectory(roomId), "archive.
|
|
8335
|
+
return join3(this.roomDirectory(roomId), "archive.sqlite3");
|
|
8336
|
+
}
|
|
8337
|
+
blobsDirectory(roomId) {
|
|
8338
|
+
return join3(this.roomDirectory(roomId), "blobs");
|
|
8167
8339
|
}
|
|
8168
8340
|
wrap(message, error) {
|
|
8169
|
-
return
|
|
8341
|
+
return new CoworkStorageError(`${message}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
8170
8342
|
}
|
|
8171
8343
|
};
|
|
8172
8344
|
}
|
|
@@ -9077,7 +9249,7 @@ import { randomBytes as randomBytes4 } from "node:crypto";
|
|
|
9077
9249
|
import * as http from "node:http";
|
|
9078
9250
|
import * as net from "node:net";
|
|
9079
9251
|
import * as nodeFs4 from "node:fs";
|
|
9080
|
-
import { basename, dirname as dirname3, join as join5 } from "node:path";
|
|
9252
|
+
import { basename as basename2, dirname as dirname3, join as join5 } from "node:path";
|
|
9081
9253
|
function createServiceRoutes(service) {
|
|
9082
9254
|
return {
|
|
9083
9255
|
"room.create": { auth: true, run: (params2) => service.createRoom(params2) },
|
|
@@ -9743,7 +9915,7 @@ var init_transports = __esm({
|
|
|
9743
9915
|
}
|
|
9744
9916
|
async cleanupStalePrivateSockets() {
|
|
9745
9917
|
const directory = dirname3(this.options.socketPath);
|
|
9746
|
-
const prefix = `${
|
|
9918
|
+
const prefix = `${basename2(this.options.socketPath)}.private-`;
|
|
9747
9919
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
9748
9920
|
const candidates = this.fs.readdirSync(directory).filter((name) => name.startsWith(prefix)).sort().slice(0, MAX_STALE_PRIVATE_SOCKET_CLEANUP);
|
|
9749
9921
|
for (const name of candidates) {
|
|
@@ -10146,8 +10318,7 @@ var init_daemon_runtime = __esm({
|
|
|
10146
10318
|
);
|
|
10147
10319
|
this.service = this.options.service ?? new RoomService(
|
|
10148
10320
|
this.store,
|
|
10149
|
-
this.registry
|
|
10150
|
-
{ identityNameMode: config.roomIdentity?.nameMode ?? "stable_id" }
|
|
10321
|
+
this.registry
|
|
10151
10322
|
);
|
|
10152
10323
|
serviceRef = this.service;
|
|
10153
10324
|
this.hostStartAttempted = true;
|