@ours.network/cowork 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,15 +7,15 @@ ours-cowork web
7
7
  ours-cowork docs
8
8
  ```
9
9
 
10
- `ours-cowork web` starts the daemon if it is absent, waits for the console, and opens `http://127.0.0.1:3052/`. Create a room with a friendly display name first, then add each invitation requirement from its Invite panel. Names are trimmed, normalized to Unicode NFC, and may contain 1–64 Unicode characters excluding control and format characters. Duplicate names are allowed. The Communication view contains the human-readable room chat; operational records remain in Events and the complete ordered stream remains in Archive.
10
+ `ours-cowork web` starts the daemon if it is absent, waits for the console, and opens `http://127.0.0.1:3052/`. Create a room with a name first, then add each invitation requirement from its Invite panel. Names are trimmed, normalized to Unicode NFC, and may contain 1–64 Unicode characters excluding control and format characters. A normalized name must be unique among identities in the shared daemon. The Communication view contains the human-readable room chat; operational records remain in Events and the complete ordered stream remains in Archive.
11
11
 
12
- The friendly `room_name` is presentation metadata. By default, new rooms use the globally unique SDK identity `ours-cowork-<room_id>`. Operators may configure `roomIdentity.nameMode` as `friendly`; new rooms then use `ours-cowork-<bounded-ascii-slug>-<room_id>`. The full room ID preserves uniqueness, and the exact authenticated name is frozen at creation independently of later display-name or configuration changes. Identity CIDs, not names, remain the authorization and routing keys. Pre-1.0 rooms using `cowork-room-<room_id>` or `ours-cowork-room:<name>` custom packet state are detected and refused: back them up with the old release, recreate them, and re-invite their participants.
12
+ Each room identity is exactly `ours-cowork:<room name>`, preserving the normalized name supplied at creation so Messenger can display it directly without separate `room_name` metadata. The authenticated identity name is frozen at creation; later room settings may change `room_name` but do not rename the identity, CID, contacts, or history. Identity CIDs, not names, remain the authorization and routing keys. Because identity names are daemon-global, creating another room with the same normalized name fails closed. Earlier unreleased ID- and slug-based identity formats are unsupported; no migration is provided for this unreleased major.
13
13
 
14
14
  The localhost HTTP console has no authentication. Keep it bound to `127.0.0.1`; do not proxy, forward, or expose the port to other hosts. Room state is refreshed by periodic polling, not pushed to the browser.
15
15
 
16
16
  The same listener describes its own room management REST API: `http://127.0.0.1:3052/openapi.json` is the OpenAPI 3.1 document and `http://127.0.0.1:3052/docs` is the browser UI for it. Both are read-only, load no remote assets, and follow the console's loopback-only exposure rules.
17
17
 
18
- Start or install the shared daemon with `@ours.network/cli` 2.0.1 before starting cowork. Cowork never embeds, starts, stops, or silently substitutes an ours daemon. It uses the SDK-standard shared selection (the default `~/.ours` daemon, `OURS_CONFIG`, or a coherent `OURS_PORT` plus `OURS_STATE_DIR` selection) and fails clearly when that daemon is unavailable or mismatched.
18
+ Start or install the shared daemon with `@ours.network/cli` 2.5.0 before starting cowork. Cowork never embeds, starts, stops, or silently substitutes an ours daemon. It uses the SDK-standard shared selection (the default `~/.ours` daemon, `OURS_CONFIG`, or a coherent `OURS_PORT` plus `OURS_STATE_DIR` selection) and fails clearly when that daemon is unavailable or mismatched.
19
19
 
20
20
  The shared daemon retains application payload history outside its protocol packets: each identity has a `history.sqlite3` database and immutable content-addressed file blobs. Cowork authorizes unread metadata by authenticated CID, reads the corresponding persistent history or blob, durably archives the room item and its complete fan-out, and only then advances that exact SDK unread item. There is no packet-inbox fallback, defer queue, host outbox, or cross-store transaction.
21
21
 
package/dist/cli.js CHANGED
@@ -4061,6 +4061,232 @@ var coerce = {
4061
4061
  };
4062
4062
  var NEVER = INVALID;
4063
4063
 
4064
+ // src/config.ts
4065
+ var DIRECTORY_MODE = 448;
4066
+ var FILE_MODE = 384;
4067
+ var NO_FOLLOW = nodeFs.constants.O_NOFOLLOW ?? 0;
4068
+ var CoworkConfigSchema = external_exports.object({
4069
+ version: external_exports.literal(1),
4070
+ stateDir: external_exports.string().min(1),
4071
+ rest: external_exports.object({
4072
+ enabled: external_exports.boolean(),
4073
+ port: external_exports.number().int().min(1).max(65535)
4074
+ }).strict()
4075
+ }).strict();
4076
+ var CoworkConfigError = class extends Error {
4077
+ constructor(message, options) {
4078
+ super(message, options);
4079
+ this.name = "CoworkConfigError";
4080
+ }
4081
+ };
4082
+ function defaultConfig(home = homedir()) {
4083
+ return {
4084
+ version: 1,
4085
+ stateDir: resolve(home, ".ours-cowork"),
4086
+ rest: { enabled: true, port: 3052 }
4087
+ };
4088
+ }
4089
+ function loadConfig(env = process.env, io = {}) {
4090
+ rejectRemovedEnvironment(env);
4091
+ const fs2 = io.fs ?? nodeFs;
4092
+ const defaults = defaultConfig(io.home);
4093
+ const configPath = resolve(env.OURS_COWORK_CONFIG ?? join(io.home ?? homedir(), ".ours-cowork", "config.json"));
4094
+ let file = defaults;
4095
+ const stat = lstatIfPresent(fs2, configPath);
4096
+ if (stat) {
4097
+ assertSecureFile(fs2, configPath, "config file");
4098
+ let parsed;
4099
+ try {
4100
+ parsed = JSON.parse(readSecureFile(fs2, configPath, "config file").toString("utf8"));
4101
+ } catch (error) {
4102
+ throw new CoworkConfigError(`malformed cowork config at ${configPath}`, { cause: error });
4103
+ }
4104
+ rejectRemovedConfig(parsed, configPath);
4105
+ try {
4106
+ file = CoworkConfigSchema.parse(parsed);
4107
+ } catch (error) {
4108
+ throw new CoworkConfigError(`invalid cowork config at ${configPath}`, { cause: error });
4109
+ }
4110
+ } else if (env.OURS_COWORK_CONFIG !== void 0) {
4111
+ throw new CoworkConfigError(`configured cowork config does not exist: ${configPath}`);
4112
+ }
4113
+ const restPort = env.OURS_COWORK_REST_PORT === void 0 ? void 0 : parsePort(env.OURS_COWORK_REST_PORT);
4114
+ try {
4115
+ return CoworkConfigSchema.parse({
4116
+ version: 1,
4117
+ stateDir: resolve(env.OURS_COWORK_STATE_DIR ?? file.stateDir),
4118
+ rest: {
4119
+ enabled: restPort === void 0 ? file.rest.enabled : true,
4120
+ port: restPort ?? file.rest.port
4121
+ }
4122
+ });
4123
+ } catch (error) {
4124
+ throw new CoworkConfigError("invalid effective cowork config", { cause: error });
4125
+ }
4126
+ }
4127
+ function rejectRemovedEnvironment(env) {
4128
+ const removed = [
4129
+ "OURS_COWORK_BROKER_URL",
4130
+ "OURS_COWORK_DAEMON_MODE",
4131
+ "OURS_COWORK_DAEMON_ENDPOINT",
4132
+ "OURS_COWORK_DAEMON_STATE_DIR"
4133
+ ].filter((name) => env[name] !== void 0);
4134
+ if (removed.length === 0) return;
4135
+ throw new CoworkConfigError(
4136
+ `${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.`
4137
+ );
4138
+ }
4139
+ function rejectRemovedConfig(value, path) {
4140
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
4141
+ const removed = ["brokerUrl", "daemon"].filter((key) => Object.hasOwn(value, key));
4142
+ if (removed.length === 0) return;
4143
+ throw new CoworkConfigError(
4144
+ `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.`
4145
+ );
4146
+ }
4147
+ function ensureRuntimeState(config, io = {}) {
4148
+ const parsed = CoworkConfigSchema.parse(config);
4149
+ const fs2 = io.fs ?? nodeFs;
4150
+ const stateDir = resolve(parsed.stateDir);
4151
+ assertSecureAncestors(fs2, stateDir, "state directory");
4152
+ const existing = lstatIfPresent(fs2, stateDir);
4153
+ if (!existing) {
4154
+ createSecureDirectoryTree(fs2, stateDir, "state directory");
4155
+ }
4156
+ assertSecureDirectory(fs2, stateDir, "state directory");
4157
+ const roomsPath = join(stateDir, "rooms");
4158
+ const rooms = lstatIfPresent(fs2, roomsPath);
4159
+ if (!rooms) {
4160
+ fs2.mkdirSync(roomsPath, { mode: DIRECTORY_MODE });
4161
+ secureOpenedDirectory(fs2, roomsPath, "rooms directory");
4162
+ fsyncDirectory(fs2, stateDir);
4163
+ }
4164
+ assertSecureDirectory(fs2, roomsPath, "rooms directory");
4165
+ return {
4166
+ socketPath: join(stateDir, "management.sock"),
4167
+ pidPath: join(stateDir, "daemon.pid"),
4168
+ lockPath: join(stateDir, "daemon.lock")
4169
+ };
4170
+ }
4171
+ function parsePort(value) {
4172
+ if (!/^[1-9][0-9]{0,4}$/.test(value)) {
4173
+ throw new CoworkConfigError("OURS_COWORK_REST_PORT must be a decimal port from 1 to 65535");
4174
+ }
4175
+ const port = Number(value);
4176
+ if (port > 65535) throw new CoworkConfigError("OURS_COWORK_REST_PORT must be from 1 to 65535");
4177
+ return port;
4178
+ }
4179
+ function assertSecureAncestors(fs2, path, label) {
4180
+ const absolute = isAbsolute(path) ? path : resolve(path);
4181
+ const root = parse(absolute).root;
4182
+ const rootOwner = fs2.lstatSync(root).uid;
4183
+ let cursor = root;
4184
+ const components = absolute.slice(root.length).split("/").filter(Boolean);
4185
+ for (const [index, component] of components.entries()) {
4186
+ cursor = join(cursor, component);
4187
+ const stat = lstatIfPresent(fs2, cursor);
4188
+ if (stat?.isSymbolicLink()) throw new CoworkConfigError(`${label} must not traverse a symbolic link (symlink): ${cursor}`);
4189
+ if (!stat) break;
4190
+ if (!stat.isDirectory()) {
4191
+ if (index === components.length - 1) return;
4192
+ throw new CoworkConfigError(`${label} ancestor is not a directory: ${cursor}`);
4193
+ }
4194
+ if (index < components.length - 1) assertTrustedAncestor(stat, rootOwner, cursor, label);
4195
+ }
4196
+ }
4197
+ function assertTrustedAncestor(stat, rootOwner, path, label) {
4198
+ const uid = typeof process.getuid === "function" ? process.getuid() : stat.uid;
4199
+ const trustedStickyDirectory = (stat.mode & 512) !== 0;
4200
+ const writableByOthers = (stat.mode & 18) !== 0;
4201
+ const trustedOwner = stat.uid === uid || stat.uid === 0 || stat.uid === rootOwner;
4202
+ if (writableByOthers && (!trustedStickyDirectory || !trustedOwner)) {
4203
+ throw new CoworkConfigError(`${label} has an unsafe writable ancestor: ${path}`);
4204
+ }
4205
+ }
4206
+ function createSecureDirectoryTree(fs2, path, label) {
4207
+ const missing = [];
4208
+ let cursor = path;
4209
+ while (!lstatIfPresent(fs2, cursor)) {
4210
+ missing.push(cursor);
4211
+ const parent = dirname(cursor);
4212
+ if (parent === cursor) throw new CoworkConfigError(`cannot locate an existing ancestor for ${label}`);
4213
+ cursor = parent;
4214
+ }
4215
+ assertSecureAncestors(fs2, path, label);
4216
+ for (const directory of missing.reverse()) {
4217
+ fs2.mkdirSync(directory, { mode: DIRECTORY_MODE });
4218
+ secureOpenedDirectory(fs2, directory, label);
4219
+ fsyncDirectory(fs2, dirname(directory));
4220
+ }
4221
+ }
4222
+ function secureOpenedDirectory(fs2, path, label) {
4223
+ let fd;
4224
+ try {
4225
+ fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
4226
+ const opened = fs2.fstatSync(fd);
4227
+ const current = fs2.lstatSync(path);
4228
+ if (!opened.isDirectory() || current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino) {
4229
+ throw new CoworkConfigError(`${label} changed while opening`);
4230
+ }
4231
+ fs2.fchmodSync(fd, DIRECTORY_MODE);
4232
+ fs2.fsyncSync(fd);
4233
+ } finally {
4234
+ if (fd !== void 0) fs2.closeSync(fd);
4235
+ }
4236
+ }
4237
+ function assertSecureDirectory(fs2, path, label) {
4238
+ const stat = fs2.lstatSync(path);
4239
+ if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
4240
+ if (!stat.isDirectory()) throw new CoworkConfigError(`${label} must be a directory`);
4241
+ if ((stat.mode & 511) !== DIRECTORY_MODE) {
4242
+ throw new CoworkConfigError(`${label} mode must be 0700`);
4243
+ }
4244
+ assertOwner(stat, label);
4245
+ }
4246
+ function assertSecureFile(fs2, path, label) {
4247
+ const stat = fs2.lstatSync(path);
4248
+ if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
4249
+ if (!stat.isFile() || stat.nlink !== 1) throw new CoworkConfigError(`${label} must be a single-link regular file`);
4250
+ if ((stat.mode & 511) !== FILE_MODE) throw new CoworkConfigError(`${label} mode must be 0600`);
4251
+ assertOwner(stat, label);
4252
+ }
4253
+ function assertOwner(stat, label) {
4254
+ if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
4255
+ throw new CoworkConfigError(`${label} must be owned by the current user`);
4256
+ }
4257
+ }
4258
+ function readSecureFile(fs2, path, label) {
4259
+ let fd;
4260
+ try {
4261
+ fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
4262
+ const opened = fs2.fstatSync(fd);
4263
+ const current = fs2.lstatSync(path);
4264
+ 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) {
4265
+ throw new CoworkConfigError(`${label} changed while opening`);
4266
+ }
4267
+ return fs2.readFileSync(fd);
4268
+ } finally {
4269
+ if (fd !== void 0) fs2.closeSync(fd);
4270
+ }
4271
+ }
4272
+ function fsyncDirectory(fs2, path) {
4273
+ let fd;
4274
+ try {
4275
+ fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
4276
+ fs2.fsyncSync(fd);
4277
+ } finally {
4278
+ if (fd !== void 0) fs2.closeSync(fd);
4279
+ }
4280
+ }
4281
+ function lstatIfPresent(fs2, path) {
4282
+ try {
4283
+ return fs2.lstatSync(path);
4284
+ } catch (error) {
4285
+ if (error.code === "ENOENT") return void 0;
4286
+ throw error;
4287
+ }
4288
+ }
4289
+
4064
4290
  // src/contracts.ts
4065
4291
  import { createHash } from "node:crypto";
4066
4292
  var MAX_TEXT_BYTES = 262144;
@@ -4072,8 +4298,6 @@ var MAX_FILE_NAME_BYTES = 255;
4072
4298
  var MAX_MIME_BYTES = 255;
4073
4299
  var MAX_ROLE_BYTES = 256;
4074
4300
  var MAX_ROOM_NAME_CHARACTERS = 64;
4075
- var MAX_ROOM_IDENTITY_NAME_CHARACTERS = 64;
4076
- var MAX_FRIENDLY_IDENTITY_SLUG_CHARACTERS = 25;
4077
4301
  function utf8Bounded(label, maximumBytes) {
4078
4302
  return external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") >= 1, `${label} must be at least 1 UTF-8 byte`).refine(
4079
4303
  (value) => Buffer.byteLength(value, "utf8") <= maximumBytes,
@@ -4120,24 +4344,13 @@ var RoomNameSchema = external_exports.string().refine(
4120
4344
  });
4121
4345
  }
4122
4346
  });
4123
- var RoomIdentityNameModeSchema = external_exports.enum(["stable_id", "friendly"]);
4124
- var ROOM_IDENTITY_PREFIX = "ours-cowork-";
4125
- var SDK_IDENTITY_NAME_PATTERN = /^[A-Za-z0-9 _.@-]{1,64}$/;
4126
- var FRIENDLY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4127
- function roomIdentityName(roomId) {
4128
- return `${ROOM_IDENTITY_PREFIX}${LowerCrockfordUlidSchema.parse(roomId)}`;
4129
- }
4130
- function legacyRoomIdentityName(roomId) {
4131
- return `cowork-room-${LowerCrockfordUlidSchema.parse(roomId)}`;
4132
- }
4347
+ var ROOM_IDENTITY_PREFIX = "ours-cowork:";
4133
4348
  function isPersistedRoomIdentityName(roomId, identityName) {
4134
- const id = LowerCrockfordUlidSchema.safeParse(roomId);
4135
- if (!id.success) return false;
4136
- if (identityName === roomIdentityName(id.data) || identityName === legacyRoomIdentityName(id.data)) return true;
4137
- const suffix = `-${id.data}`;
4138
- if (!identityName.startsWith(ROOM_IDENTITY_PREFIX) || !identityName.endsWith(suffix)) return false;
4139
- const slug = identityName.slice(ROOM_IDENTITY_PREFIX.length, -suffix.length);
4140
- return slug.length >= 1 && slug.length <= MAX_FRIENDLY_IDENTITY_SLUG_CHARACTERS && FRIENDLY_SLUG_PATTERN.test(slug) && identityName.length <= MAX_ROOM_IDENTITY_NAME_CHARACTERS && SDK_IDENTITY_NAME_PATTERN.test(identityName);
4349
+ if (!LowerCrockfordUlidSchema.safeParse(roomId).success || !identityName.startsWith(ROOM_IDENTITY_PREFIX)) {
4350
+ return false;
4351
+ }
4352
+ const parsed = RoomNameSchema.safeParse(identityName.slice(ROOM_IDENTITY_PREFIX.length));
4353
+ return parsed.success && identityName === `${ROOM_IDENTITY_PREFIX}${parsed.data}`;
4141
4354
  }
4142
4355
  var RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4143
4356
  var ROOM_ROLE = "room";
@@ -4794,237 +5007,6 @@ var AppendRecordSchema = external_exports.discriminatedUnion("kind", [
4794
5007
  refineFileRecord(record, context);
4795
5008
  });
4796
5009
 
4797
- // src/config.ts
4798
- var DIRECTORY_MODE = 448;
4799
- var FILE_MODE = 384;
4800
- var NO_FOLLOW = nodeFs.constants.O_NOFOLLOW ?? 0;
4801
- var CoworkConfigSchema = external_exports.object({
4802
- version: external_exports.literal(1),
4803
- stateDir: external_exports.string().min(1),
4804
- roomIdentity: external_exports.object({
4805
- nameMode: RoomIdentityNameModeSchema
4806
- }).strict().default({ nameMode: "stable_id" }),
4807
- rest: external_exports.object({
4808
- enabled: external_exports.boolean(),
4809
- port: external_exports.number().int().min(1).max(65535)
4810
- }).strict()
4811
- }).strict();
4812
- var CoworkConfigError = class extends Error {
4813
- constructor(message, options) {
4814
- super(message, options);
4815
- this.name = "CoworkConfigError";
4816
- }
4817
- };
4818
- function defaultConfig(home = homedir()) {
4819
- return {
4820
- version: 1,
4821
- stateDir: resolve(home, ".ours-cowork"),
4822
- roomIdentity: { nameMode: "stable_id" },
4823
- rest: { enabled: true, port: 3052 }
4824
- };
4825
- }
4826
- function loadConfig(env = process.env, io = {}) {
4827
- rejectRemovedEnvironment(env);
4828
- const fs2 = io.fs ?? nodeFs;
4829
- const defaults = defaultConfig(io.home);
4830
- const configPath = resolve(env.OURS_COWORK_CONFIG ?? join(io.home ?? homedir(), ".ours-cowork", "config.json"));
4831
- let file = defaults;
4832
- const stat = lstatIfPresent(fs2, configPath);
4833
- if (stat) {
4834
- assertSecureFile(fs2, configPath, "config file");
4835
- let parsed;
4836
- try {
4837
- parsed = JSON.parse(readSecureFile(fs2, configPath, "config file").toString("utf8"));
4838
- } catch (error) {
4839
- throw new CoworkConfigError(`malformed cowork config at ${configPath}`, { cause: error });
4840
- }
4841
- rejectRemovedConfig(parsed, configPath);
4842
- try {
4843
- file = CoworkConfigSchema.parse(parsed);
4844
- } catch (error) {
4845
- throw new CoworkConfigError(`invalid cowork config at ${configPath}`, { cause: error });
4846
- }
4847
- } else if (env.OURS_COWORK_CONFIG !== void 0) {
4848
- throw new CoworkConfigError(`configured cowork config does not exist: ${configPath}`);
4849
- }
4850
- const restPort = env.OURS_COWORK_REST_PORT === void 0 ? void 0 : parsePort(env.OURS_COWORK_REST_PORT);
4851
- try {
4852
- return CoworkConfigSchema.parse({
4853
- version: 1,
4854
- stateDir: resolve(env.OURS_COWORK_STATE_DIR ?? file.stateDir),
4855
- roomIdentity: file.roomIdentity,
4856
- rest: {
4857
- enabled: restPort === void 0 ? file.rest.enabled : true,
4858
- port: restPort ?? file.rest.port
4859
- }
4860
- });
4861
- } catch (error) {
4862
- throw new CoworkConfigError("invalid effective cowork config", { cause: error });
4863
- }
4864
- }
4865
- function rejectRemovedEnvironment(env) {
4866
- const removed = [
4867
- "OURS_COWORK_BROKER_URL",
4868
- "OURS_COWORK_DAEMON_MODE",
4869
- "OURS_COWORK_DAEMON_ENDPOINT",
4870
- "OURS_COWORK_DAEMON_STATE_DIR"
4871
- ].filter((name) => env[name] !== void 0);
4872
- if (removed.length === 0) return;
4873
- throw new CoworkConfigError(
4874
- `${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.`
4875
- );
4876
- }
4877
- function rejectRemovedConfig(value, path) {
4878
- if (value === null || typeof value !== "object" || Array.isArray(value)) return;
4879
- const removed = ["brokerUrl", "daemon"].filter((key) => Object.hasOwn(value, key));
4880
- if (removed.length === 0) return;
4881
- throw new CoworkConfigError(
4882
- `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.`
4883
- );
4884
- }
4885
- function ensureRuntimeState(config, io = {}) {
4886
- const parsed = CoworkConfigSchema.parse(config);
4887
- const fs2 = io.fs ?? nodeFs;
4888
- const stateDir = resolve(parsed.stateDir);
4889
- assertSecureAncestors(fs2, stateDir, "state directory");
4890
- const existing = lstatIfPresent(fs2, stateDir);
4891
- if (!existing) {
4892
- createSecureDirectoryTree(fs2, stateDir, "state directory");
4893
- }
4894
- assertSecureDirectory(fs2, stateDir, "state directory");
4895
- const roomsPath = join(stateDir, "rooms");
4896
- const rooms = lstatIfPresent(fs2, roomsPath);
4897
- if (!rooms) {
4898
- fs2.mkdirSync(roomsPath, { mode: DIRECTORY_MODE });
4899
- secureOpenedDirectory(fs2, roomsPath, "rooms directory");
4900
- fsyncDirectory(fs2, stateDir);
4901
- }
4902
- assertSecureDirectory(fs2, roomsPath, "rooms directory");
4903
- return {
4904
- socketPath: join(stateDir, "management.sock"),
4905
- pidPath: join(stateDir, "daemon.pid"),
4906
- lockPath: join(stateDir, "daemon.lock")
4907
- };
4908
- }
4909
- function parsePort(value) {
4910
- if (!/^[1-9][0-9]{0,4}$/.test(value)) {
4911
- throw new CoworkConfigError("OURS_COWORK_REST_PORT must be a decimal port from 1 to 65535");
4912
- }
4913
- const port = Number(value);
4914
- if (port > 65535) throw new CoworkConfigError("OURS_COWORK_REST_PORT must be from 1 to 65535");
4915
- return port;
4916
- }
4917
- function assertSecureAncestors(fs2, path, label) {
4918
- const absolute = isAbsolute(path) ? path : resolve(path);
4919
- const root = parse(absolute).root;
4920
- const rootOwner = fs2.lstatSync(root).uid;
4921
- let cursor = root;
4922
- const components = absolute.slice(root.length).split("/").filter(Boolean);
4923
- for (const [index, component] of components.entries()) {
4924
- cursor = join(cursor, component);
4925
- const stat = lstatIfPresent(fs2, cursor);
4926
- if (stat?.isSymbolicLink()) throw new CoworkConfigError(`${label} must not traverse a symbolic link (symlink): ${cursor}`);
4927
- if (!stat) break;
4928
- if (!stat.isDirectory()) {
4929
- if (index === components.length - 1) return;
4930
- throw new CoworkConfigError(`${label} ancestor is not a directory: ${cursor}`);
4931
- }
4932
- if (index < components.length - 1) assertTrustedAncestor(stat, rootOwner, cursor, label);
4933
- }
4934
- }
4935
- function assertTrustedAncestor(stat, rootOwner, path, label) {
4936
- const uid = typeof process.getuid === "function" ? process.getuid() : stat.uid;
4937
- const trustedStickyDirectory = (stat.mode & 512) !== 0;
4938
- const writableByOthers = (stat.mode & 18) !== 0;
4939
- const trustedOwner = stat.uid === uid || stat.uid === 0 || stat.uid === rootOwner;
4940
- if (writableByOthers && (!trustedStickyDirectory || !trustedOwner)) {
4941
- throw new CoworkConfigError(`${label} has an unsafe writable ancestor: ${path}`);
4942
- }
4943
- }
4944
- function createSecureDirectoryTree(fs2, path, label) {
4945
- const missing = [];
4946
- let cursor = path;
4947
- while (!lstatIfPresent(fs2, cursor)) {
4948
- missing.push(cursor);
4949
- const parent = dirname(cursor);
4950
- if (parent === cursor) throw new CoworkConfigError(`cannot locate an existing ancestor for ${label}`);
4951
- cursor = parent;
4952
- }
4953
- assertSecureAncestors(fs2, path, label);
4954
- for (const directory of missing.reverse()) {
4955
- fs2.mkdirSync(directory, { mode: DIRECTORY_MODE });
4956
- secureOpenedDirectory(fs2, directory, label);
4957
- fsyncDirectory(fs2, dirname(directory));
4958
- }
4959
- }
4960
- function secureOpenedDirectory(fs2, path, label) {
4961
- let fd;
4962
- try {
4963
- fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
4964
- const opened = fs2.fstatSync(fd);
4965
- const current = fs2.lstatSync(path);
4966
- if (!opened.isDirectory() || current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino) {
4967
- throw new CoworkConfigError(`${label} changed while opening`);
4968
- }
4969
- fs2.fchmodSync(fd, DIRECTORY_MODE);
4970
- fs2.fsyncSync(fd);
4971
- } finally {
4972
- if (fd !== void 0) fs2.closeSync(fd);
4973
- }
4974
- }
4975
- function assertSecureDirectory(fs2, path, label) {
4976
- const stat = fs2.lstatSync(path);
4977
- if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
4978
- if (!stat.isDirectory()) throw new CoworkConfigError(`${label} must be a directory`);
4979
- if ((stat.mode & 511) !== DIRECTORY_MODE) {
4980
- throw new CoworkConfigError(`${label} mode must be 0700`);
4981
- }
4982
- assertOwner(stat, label);
4983
- }
4984
- function assertSecureFile(fs2, path, label) {
4985
- const stat = fs2.lstatSync(path);
4986
- if (stat.isSymbolicLink()) throw new CoworkConfigError(`${label} must not be a symbolic link (symlink)`);
4987
- if (!stat.isFile() || stat.nlink !== 1) throw new CoworkConfigError(`${label} must be a single-link regular file`);
4988
- if ((stat.mode & 511) !== FILE_MODE) throw new CoworkConfigError(`${label} mode must be 0600`);
4989
- assertOwner(stat, label);
4990
- }
4991
- function assertOwner(stat, label) {
4992
- if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
4993
- throw new CoworkConfigError(`${label} must be owned by the current user`);
4994
- }
4995
- }
4996
- function readSecureFile(fs2, path, label) {
4997
- let fd;
4998
- try {
4999
- fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
5000
- const opened = fs2.fstatSync(fd);
5001
- const current = fs2.lstatSync(path);
5002
- 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) {
5003
- throw new CoworkConfigError(`${label} changed while opening`);
5004
- }
5005
- return fs2.readFileSync(fd);
5006
- } finally {
5007
- if (fd !== void 0) fs2.closeSync(fd);
5008
- }
5009
- }
5010
- function fsyncDirectory(fs2, path) {
5011
- let fd;
5012
- try {
5013
- fd = fs2.openSync(path, nodeFs.constants.O_RDONLY | NO_FOLLOW);
5014
- fs2.fsyncSync(fd);
5015
- } finally {
5016
- if (fd !== void 0) fs2.closeSync(fd);
5017
- }
5018
- }
5019
- function lstatIfPresent(fs2, path) {
5020
- try {
5021
- return fs2.lstatSync(path);
5022
- } catch (error) {
5023
- if (error.code === "ENOENT") return void 0;
5024
- throw error;
5025
- }
5026
- }
5027
-
5028
5010
  // src/cli.ts
5029
5011
  var EXIT = {
5030
5012
  success: 0,