@ours.network/cowork 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4277,6 +4277,7 @@ var MAX_TEXT_BYTES = 262144;
4277
4277
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
4278
4278
  var MAX_HISTORY_PAGE_BYTES = 3 * 1024 * 1024;
4279
4279
  var MAX_MANAGEMENT_RESPONSE_BYTES = MAX_HISTORY_PAGE_BYTES + 1024 * 1024;
4280
+ var MAX_EXTERNAL_INVITE_BYTES = 48 * 1024;
4280
4281
  var MAX_FILE_NAME_BYTES = 255;
4281
4282
  var MAX_MIME_BYTES = 255;
4282
4283
  var MAX_ROLE_BYTES = 256;
@@ -4343,7 +4344,7 @@ var FileMimeSchema = external_exports.string().refine(
4343
4344
  `file MIME metadata must be at most ${MAX_MIME_BYTES} UTF-8 bytes`
4344
4345
  );
4345
4346
  var RoomStateSchema = external_exports.enum(["provisioning", "active", "closing", "closed"]);
4346
- var SeatStateSchema = external_exports.enum(["active", "removed"]);
4347
+ var SeatStateSchema = external_exports.enum(["pending", "active", "removed"]);
4347
4348
  var InviteModeSchema = external_exports.enum(["one_time", "public"]);
4348
4349
  var InviteStateSchema = external_exports.enum([
4349
4350
  "live",
@@ -4365,7 +4366,9 @@ var SeatSchema = external_exports.object({
4365
4366
  display_name: NonEmptyStringSchema,
4366
4367
  role: RoleSchema,
4367
4368
  invite_id: NonEmptyStringSchema,
4368
- accepted_at: Rfc3339Schema,
4369
+ accepted_at: Rfc3339Schema.optional(),
4370
+ requested_at: Rfc3339Schema.optional(),
4371
+ invite_sha256: external_exports.string().regex(/^[0-9a-f]{64}$/).optional(),
4369
4372
  participant_id: LowerCrockfordUlidSchema,
4370
4373
  state: SeatStateSchema,
4371
4374
  alias: NonEmptyStringSchema.optional(),
@@ -4374,7 +4377,25 @@ var SeatSchema = external_exports.object({
4374
4377
  replaces_seat: LowerCrockfordUlidSchema.optional(),
4375
4378
  bounced_at: Rfc3339Schema.optional()
4376
4379
  }).strict().superRefine((seat, context) => {
4377
- if (seat.state === "removed") {
4380
+ if (seat.state === "pending") {
4381
+ for (const field of ["requested_at", "invite_sha256"]) {
4382
+ if (seat[field] === void 0) {
4383
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `pending seats require ${field}` });
4384
+ }
4385
+ }
4386
+ for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at"]) {
4387
+ if (seat[field] !== void 0) {
4388
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `${field} is forbidden on pending seats` });
4389
+ }
4390
+ }
4391
+ } else if (seat.state === "removed") {
4392
+ if (seat.accepted_at === void 0 && seat.requested_at === void 0) {
4393
+ context.addIssue({
4394
+ code: external_exports.ZodIssueCode.custom,
4395
+ path: ["accepted_at"],
4396
+ message: "removed seats require accepted_at unless they are cancelled external admissions"
4397
+ });
4398
+ }
4378
4399
  if (seat.removed_at === void 0) {
4379
4400
  context.addIssue({
4380
4401
  code: external_exports.ZodIssueCode.custom,
@@ -4390,6 +4411,9 @@ var SeatSchema = external_exports.object({
4390
4411
  });
4391
4412
  }
4392
4413
  } else {
4414
+ if (seat.accepted_at === void 0) {
4415
+ context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["accepted_at"], message: "active seats require accepted_at" });
4416
+ }
4393
4417
  for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
4394
4418
  if (seat[field] !== void 0) {
4395
4419
  context.addIssue({
@@ -4400,6 +4424,13 @@ var SeatSchema = external_exports.object({
4400
4424
  }
4401
4425
  }
4402
4426
  }
4427
+ if (seat.requested_at === void 0 !== (seat.invite_sha256 === void 0)) {
4428
+ context.addIssue({
4429
+ code: external_exports.ZodIssueCode.custom,
4430
+ path: ["invite_sha256"],
4431
+ message: "external admission metadata requires both requested_at and invite_sha256"
4432
+ });
4433
+ }
4403
4434
  });
4404
4435
  var RoomInviteSchema = external_exports.object({
4405
4436
  invite_id: NonEmptyStringSchema,
@@ -4558,6 +4589,7 @@ var CurrentRoomSchema = external_exports.object({
4558
4589
  refineRoomLineage(room, context);
4559
4590
  const byParticipant = /* @__PURE__ */ new Map();
4560
4591
  const activeAliases = /* @__PURE__ */ new Set();
4592
+ const authorizedCids = /* @__PURE__ */ new Set();
4561
4593
  for (const [index, seat] of room.seats.entries()) {
4562
4594
  if (byParticipant.has(seat.participant_id)) {
4563
4595
  context.addIssue({
@@ -4567,6 +4599,16 @@ var CurrentRoomSchema = external_exports.object({
4567
4599
  });
4568
4600
  }
4569
4601
  byParticipant.set(seat.participant_id, seat);
4602
+ if (seat.state === "pending" || seat.state === "active") {
4603
+ if (authorizedCids.has(seat.identity)) {
4604
+ context.addIssue({
4605
+ code: external_exports.ZodIssueCode.custom,
4606
+ path: ["seats", index, "identity"],
4607
+ message: "at most one pending or active seat may exist per CID"
4608
+ });
4609
+ }
4610
+ authorizedCids.add(seat.identity);
4611
+ }
4570
4612
  if (room.anonymous && seat.alias === void 0) {
4571
4613
  context.addIssue({
4572
4614
  code: external_exports.ZodIssueCode.custom,
@@ -4659,6 +4701,16 @@ var RoleBriefingDeleteInputSchema = external_exports.object({
4659
4701
  var PostMessageInputSchema = external_exports.object({
4660
4702
  text: MessageTextSchema
4661
4703
  }).strict();
4704
+ var ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4705
+ var AcceptExternalInviteInputSchema = external_exports.object({
4706
+ role: RoleSchema,
4707
+ invite: external_exports.string().refine(
4708
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
4709
+ `invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
4710
+ ),
4711
+ expected_cid: ContainerIdSchema.optional(),
4712
+ replaces_seat: LowerCrockfordUlidSchema.optional()
4713
+ }).strict();
4662
4714
  var AuthorSnapshotSchema = external_exports.object({
4663
4715
  identity: NonEmptyStringSchema,
4664
4716
  display_name: NonEmptyStringSchema,
@@ -4932,6 +4984,7 @@ var RpcTransportError = class extends CliError {
4932
4984
  var NATIVE_MUTATION_METHODS = /* @__PURE__ */ new Set([
4933
4985
  "room.create",
4934
4986
  "room.invite",
4987
+ "room.accept",
4935
4988
  "room.revoke",
4936
4989
  "room.message",
4937
4990
  "room.close",
@@ -4975,6 +5028,7 @@ Room commands:
4975
5028
  settings <room-id> [--name <display-name>] [--goal <text>] [--briefing <text>] [--status <text>] [--quiet-membership true|false]
4976
5029
  role-briefing <room-id> --role <label> (--text <text> | --delete)
4977
5030
  invite <room-id> [--role <label>] [--mode one_time|public] [--min-accepts <n>]
5031
+ accept <room-id> --role <label> (--invite-file <path> | --invite-stdin) [--expected-cid <64-hex>] [--replaces <participant-id>]
4978
5032
  revoke <room-id> <invite-id>
4979
5033
  remove <room-id> <participant> [--silent]
4980
5034
  replace <room-id> <participant> [--mode one_time|public] [--min-accepts <n>] [--silent]
@@ -5057,7 +5111,7 @@ function loadCliConfig() {
5057
5111
  throw new CliError(EXIT.internal, "internal", error instanceof Error ? error.message : "failed to load config", { cause: error });
5058
5112
  }
5059
5113
  }
5060
- function roomRequest(command, args) {
5114
+ async function roomRequest(command, args) {
5061
5115
  if (!command) usageError("room requires a command");
5062
5116
  switch (command) {
5063
5117
  case "create": {
@@ -5115,6 +5169,27 @@ function roomRequest(command, args) {
5115
5169
  min_accepts: minimum
5116
5170
  } };
5117
5171
  }
5172
+ case "accept": {
5173
+ const parsed = parseOptions(
5174
+ args,
5175
+ ["--role", "--invite-file", "--expected-cid", "--replaces"],
5176
+ ["--invite-stdin"]
5177
+ );
5178
+ const [roomId] = exactPositionals(parsed, 1, "room accept");
5179
+ const inviteFile = parsed.values["--invite-file"];
5180
+ const inviteStdin = parsed.booleans.has("--invite-stdin");
5181
+ if ((inviteFile === void 0 ? 0 : 1) + (inviteStdin ? 1 : 0) !== 1) {
5182
+ usageError("room accept requires exactly one of --invite-file or --invite-stdin");
5183
+ }
5184
+ const invite = inviteFile === void 0 ? await readBoundedStdin() : readSecureInviteFile(inviteFile);
5185
+ return { method: "room.accept", params: {
5186
+ room_id: roomId,
5187
+ role: requiredFlag(parsed, "--role", "room accept"),
5188
+ invite,
5189
+ ...parsed.values["--expected-cid"] === void 0 ? {} : { expected_cid: parsed.values["--expected-cid"] },
5190
+ ...parsed.values["--replaces"] === void 0 ? {} : { replaces_seat: parsed.values["--replaces"] }
5191
+ } };
5192
+ }
5118
5193
  case "remove": {
5119
5194
  const parsed = parseOptions(args, [], ["--silent"]);
5120
5195
  const [roomId, participant] = exactPositionals(parsed, 2, "room remove");
@@ -5197,6 +5272,75 @@ function roomRequest(command, args) {
5197
5272
  usageError(`unknown room command: ${command}`);
5198
5273
  }
5199
5274
  }
5275
+ function readSecureInviteFile(path) {
5276
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
5277
+ let fd;
5278
+ try {
5279
+ const observed = fs.lstatSync(path);
5280
+ if (observed.isSymbolicLink() || !observed.isFile() || observed.nlink !== 1 || (observed.mode & 511) !== 384 || typeof process.getuid === "function" && observed.uid !== process.getuid()) {
5281
+ throw new Error("invite file must be an owned 0600 single-link regular file");
5282
+ }
5283
+ if (observed.size > MAX_EXTERNAL_INVITE_BYTES) throw new Error("invite input exceeds 48 KiB");
5284
+ fd = fs.openSync(path, fs.constants.O_RDONLY | noFollow);
5285
+ const opened = fs.fstatSync(fd);
5286
+ const current = fs.lstatSync(path);
5287
+ if (!opened.isFile() || opened.nlink !== 1 || (opened.mode & 511) !== 384 || typeof process.getuid === "function" && opened.uid !== process.getuid() || current.isSymbolicLink() || current.dev !== opened.dev || current.ino !== opened.ino) {
5288
+ throw new Error("invite file changed while opening");
5289
+ }
5290
+ const bytes = fs.readFileSync(fd);
5291
+ if (bytes.length > MAX_EXTERNAL_INVITE_BYTES) throw new Error("invite input exceeds 48 KiB");
5292
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
5293
+ } catch (error) {
5294
+ throw new CliError(
5295
+ EXIT.invalidState,
5296
+ "invalid_params",
5297
+ error instanceof Error && /48 KiB/.test(error.message) ? "invite input exceeds 48 KiB" : "invite file is unavailable or insecure"
5298
+ );
5299
+ } finally {
5300
+ if (fd !== void 0) fs.closeSync(fd);
5301
+ }
5302
+ }
5303
+ function readBoundedStdin() {
5304
+ return new Promise((resolveInput, rejectInput) => {
5305
+ const chunks = [];
5306
+ let size = 0;
5307
+ const fail = () => {
5308
+ cleanup();
5309
+ process.stdin.pause();
5310
+ rejectInput(new CliError(EXIT.invalidState, "invalid_params", "invite input exceeds 48 KiB"));
5311
+ };
5312
+ const cleanup = () => {
5313
+ process.stdin.off("data", onData);
5314
+ process.stdin.off("end", onEnd);
5315
+ process.stdin.off("error", onError);
5316
+ };
5317
+ const onData = (chunk) => {
5318
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5319
+ size += bytes.length;
5320
+ if (size > MAX_EXTERNAL_INVITE_BYTES) {
5321
+ fail();
5322
+ return;
5323
+ }
5324
+ chunks.push(bytes);
5325
+ };
5326
+ const onEnd = () => {
5327
+ cleanup();
5328
+ try {
5329
+ resolveInput(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
5330
+ } catch {
5331
+ rejectInput(new CliError(EXIT.invalidState, "invalid_params", "invite input is not valid UTF-8"));
5332
+ }
5333
+ };
5334
+ const onError = () => {
5335
+ cleanup();
5336
+ rejectInput(new CliError(EXIT.invalidState, "invalid_params", "could not read invite input"));
5337
+ };
5338
+ process.stdin.on("data", onData);
5339
+ process.stdin.once("end", onEnd);
5340
+ process.stdin.once("error", onError);
5341
+ process.stdin.resume();
5342
+ });
5343
+ }
5200
5344
  function rpcCall(socketPath, method, params, timeoutMs = RPC_TIMEOUT_MS) {
5201
5345
  const id = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
5202
5346
  const request = `${JSON.stringify({ version: 1, id, method, params })}
@@ -5656,7 +5800,7 @@ async function execute(args, output) {
5656
5800
  return;
5657
5801
  }
5658
5802
  if (command === "room") {
5659
- const request = roomRequest(args[1], args.slice(2));
5803
+ const request = await roomRequest(args[1], args.slice(2));
5660
5804
  const config2 = loadCliConfig();
5661
5805
  const socketPath = join2(config2.stateDir, "management.sock");
5662
5806
  const result = request.method === "room.history" ? await readHistoryPages(socketPath, request.params) : await rpcCall(