@tinycloud/cli 1.3.0-beta.50 → 1.3.0-beta.52

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
@@ -242,6 +242,17 @@ record (for example, restored from an older copy) refuses logout; switch the
242
242
  default profile if needed, then run `tc profile delete <name>` and sign in
243
243
  again.
244
244
 
245
+ `auth logout`, `auth rotate`, and replica-enabled `auth login` that replaces the
246
+ profile's share-history key check the sender share history first. If it records
247
+ unexpired shares, or cannot be read, the command refuses with exit 5
248
+ (`SHARE_HISTORY_UNEXPIRED` or `SHARE_HISTORY_UNAVAILABLE`) and changes nothing.
249
+ Pass `--force` to proceed anyway; `auth logout --revoke` also skips the check. A
250
+ plain re-login that keeps the key is not checked, and local-owner profiles are not
251
+ checked. `tc share history recover` moves an unreadable history file aside to
252
+ `share-history-v2.json.unreadable-<UTC timestamp>` and reports the new path. It
253
+ never deletes or overwrites a file. If the file exists but cannot be read,
254
+ recover exits 1 with `SHARE_HISTORY_RECOVER_FAILED` and moves nothing.
255
+
245
256
  `tc auth grants list` labels the signed-in delegation CID `(current session)`.
246
257
  `tc delegation revoke <cid>` still accepts any eligible exact CID, but when
247
258
  `<cid>` is the current session it requires `--yes` or an interactive confirmation
@@ -9070,8 +9070,9 @@ function loadPeers(path21, ownerUid2 = process.getuid?.()) {
9070
9070
  const stat = fs14.fstatSync(fd);
9071
9071
  if (!stat.isFile())
9072
9072
  throw new CourierError("insecure-peers", `${path21} is not a regular file.`);
9073
- if (ownerUid2 !== void 0 && stat.uid !== ownerUid2)
9074
- throw new CourierError("insecure-peers", `${path21} is not owned by the courier user.`);
9073
+ if (ownerUid2 !== void 0 && stat.uid !== ownerUid2) {
9074
+ throw new CourierError("insecure-peers", `${path21} is not owned by the peers owner (uid ${ownerUid2}); pass --peers-owner or chown the file.`);
9075
+ }
9075
9076
  if ((stat.mode & 18) !== 0) {
9076
9077
  throw new CourierError("insecure-peers", `${path21} is writable by group or others; restrict it to the owner (chmod 600).`);
9077
9078
  }
@@ -9093,6 +9094,53 @@ function loadPeers(path21, ownerUid2 = process.getuid?.()) {
9093
9094
  function policyOf(peers, peerId) {
9094
9095
  return Object.hasOwn(peers, peerId) ? peers[peerId] : { ...ABSENT };
9095
9096
  }
9097
+ var UID_MAX = 4294967294;
9098
+ var PASSWD = "/etc/passwd";
9099
+ function uidOfName(name2, passwdPath) {
9100
+ let text;
9101
+ try {
9102
+ text = fs14.readFileSync(passwdPath, "utf8");
9103
+ } catch {
9104
+ throw new CourierError("usage", `cannot read ${passwdPath} to resolve "${name2}"; pass a numeric uid instead.`);
9105
+ }
9106
+ const uids = /* @__PURE__ */ new Set();
9107
+ for (const line of text.split("\n")) {
9108
+ if (line === "" || line.startsWith("#") || line.startsWith("+") || line.startsWith("-"))
9109
+ continue;
9110
+ const fields = line.split(":");
9111
+ if (fields.length !== 7 || !/^(0|[1-9]\d*)$/.test(fields[2]) || Number(fields[2]) > UID_MAX) {
9112
+ throw new CourierError("usage", `${passwdPath} has a malformed line; fix it or pass a numeric uid.`);
9113
+ }
9114
+ if (fields[0] === name2)
9115
+ uids.add(Number(fields[2]));
9116
+ }
9117
+ if (uids.size === 0)
9118
+ throw new CourierError("usage", `no account named "${name2}"; pass a numeric uid instead.`);
9119
+ if (uids.size > 1)
9120
+ throw new CourierError("usage", `"${name2}" maps to more than one uid in ${passwdPath}; pass a numeric uid.`);
9121
+ return [...uids][0];
9122
+ }
9123
+ function resolvePeersOwner(flag, env, passwdPath = PASSWD) {
9124
+ const value2 = flag ?? env;
9125
+ if (value2 === void 0) {
9126
+ const uid = process.getuid?.();
9127
+ if (uid === void 0)
9128
+ throw new CourierError("unsupported", "peers ownership needs a POSIX platform.");
9129
+ return uid;
9130
+ }
9131
+ if (value2 === "true")
9132
+ throw new CourierError("usage", "--peers-owner needs a user name or numeric uid.");
9133
+ if (/^(0|[1-9]\d*)$/.test(value2)) {
9134
+ const uid = Number(value2);
9135
+ if (uid > UID_MAX)
9136
+ throw new CourierError("usage", `peers owner uid ${value2} is out of range.`);
9137
+ return uid;
9138
+ }
9139
+ if (!/^[A-Za-z_][A-Za-z0-9._-]{0,31}$/.test(value2)) {
9140
+ throw new CourierError("usage", `peers owner "${value2}" is neither a user name nor a numeric uid.`);
9141
+ }
9142
+ return uidOfName(value2, passwdPath);
9143
+ }
9096
9144
 
9097
9145
  // ../courier/dist/intake/queue.js
9098
9146
  import fs15 from "fs";
@@ -9801,13 +9849,29 @@ function intervalSeconds(raw) {
9801
9849
  throw new CourierError("usage", "--interval must be an integer from 5 to 3600");
9802
9850
  return n;
9803
9851
  }
9804
- async function cmdIntake(args) {
9852
+ function reloadPeers(peersFile, peersOwner, last, log) {
9853
+ try {
9854
+ return loadPeers(peersFile, peersOwner);
9855
+ } catch (error) {
9856
+ log("intake-error", { code: errInfo(error).code ?? "bad-peers" });
9857
+ return last;
9858
+ }
9859
+ }
9860
+ var defaultLoopDeps = {
9861
+ loadProfile,
9862
+ courierNode,
9863
+ reloadPeers,
9864
+ intakeCycle,
9865
+ sleep: (ms2) => new Promise((resolve) => setTimeout(resolve, ms2))
9866
+ };
9867
+ async function cmdIntake(args, deps = defaultLoopDeps) {
9805
9868
  const base = path18.join(profileDir(), "intake");
9806
9869
  const queueRoot = args.get("queue-dir") ?? path18.join(base, "queue");
9807
9870
  const doneRoot = args.get("done-dir") ?? path18.join(base, "done");
9808
9871
  const overflowRoot = args.get("overflow-dir") ?? path18.join(base, "overflow");
9809
9872
  const stateDir = args.get("state-dir") ?? path18.join(base, "state");
9810
9873
  const peersFile = args.get("peers-file") ?? path18.join(base, "peers.json");
9874
+ const peersOwner = resolvePeersOwner(args.get("peers-owner"), process.env.COURIER_PEERS_OWNER);
9811
9875
  const once = args.has("once");
9812
9876
  const intervalMs = intervalSeconds(args.get("interval")) * 1e3;
9813
9877
  const log = jsonLineLog();
@@ -9824,18 +9888,14 @@ async function cmdIntake(args) {
9824
9888
  return;
9825
9889
  }
9826
9890
  const state = readState(path18.join(stateDir, STATE_FILE));
9827
- let peers = loadPeers(peersFile);
9891
+ let peers = loadPeers(peersFile, peersOwner);
9828
9892
  log("intake-start", { once, intervalSeconds: intervalMs / 1e3 });
9829
9893
  for (; ; ) {
9830
9894
  try {
9831
- const profile = loadProfile();
9832
- const node = await courierNode(profile);
9833
- try {
9834
- peers = loadPeers(peersFile);
9835
- } catch (error) {
9836
- log("intake-error", { code: errInfo(error).code ?? "bad-peers" });
9837
- }
9838
- const queued = await intakeCycle({
9895
+ const profile = deps.loadProfile();
9896
+ const node = await deps.courierNode(profile);
9897
+ peers = deps.reloadPeers(peersFile, peersOwner, peers, log);
9898
+ const queued = await deps.intakeCycle({
9839
9899
  kv: node.kv,
9840
9900
  profile,
9841
9901
  peers,
@@ -9855,9 +9915,7 @@ async function cmdIntake(args) {
9855
9915
  }
9856
9916
  if (once)
9857
9917
  return;
9858
- await new Promise((resolve) => {
9859
- setTimeout(resolve, intervalMs);
9860
- });
9918
+ await deps.sleep(intervalMs);
9861
9919
  }
9862
9920
  } finally {
9863
9921
  release();
@@ -9896,7 +9954,7 @@ var Refusal = class extends Error {
9896
9954
  }
9897
9955
  };
9898
9956
  var ADD_USAGE = `Usage: ${cmd("outbox add <peer> (--thread <id> | --new <subject>) [--text t | --file f | stdin] [--outbox-dir d]")}`;
9899
- var SERVE_USAGE = `Usage: ${cmd("outbox serve --pipe <socket> [--outbox-dir d] [--socket-group g] [--peers-file f] [--sent-dir d] [--once] [--dry-run]")}`;
9957
+ var SERVE_USAGE = `Usage: ${cmd("outbox serve --pipe <socket> [--outbox-dir d] [--socket-group g] [--peers-file f] [--peers-owner u] [--sent-dir d] [--once] [--dry-run]")}`;
9900
9958
  var OUTBOX_USAGE = `Usage: ${cmd("outbox add <peer> ... | outbox serve ...")}`;
9901
9959
  function lstatOrUndefined(file) {
9902
9960
  try {
@@ -9928,6 +9986,7 @@ var LOGGABLE_CODES = /* @__PURE__ */ new Set([
9928
9986
  "ECONNRESET",
9929
9987
  "bad-marker",
9930
9988
  "bad-peers",
9989
+ "insecure-peers",
9931
9990
  "socket-group-denied",
9932
9991
  "unknown-group",
9933
9992
  "unsafe-dir",
@@ -10230,7 +10289,7 @@ async function startOutbox(opts) {
10230
10289
  let peersNow;
10231
10290
  function currentPeers() {
10232
10291
  try {
10233
- peersNow = loadPeers(opts.peersFile);
10292
+ peersNow = loadPeers(opts.peersFile, opts.peersOwnerUid);
10234
10293
  } catch (error) {
10235
10294
  limited("outbox.peers-invalid", { code: safeCode(error) });
10236
10295
  }
@@ -10396,6 +10455,7 @@ async function cmdOutboxServe(args) {
10396
10455
  pipe: configValue(args, "pipe", "COURIER_OUTBOX_PIPE", SERVE_USAGE),
10397
10456
  socketGroup: configValue(args, "socket-group", "COURIER_OUTBOX_SOCKET_GROUP", SERVE_USAGE, DEFAULT_SOCKET_GROUP),
10398
10457
  peersFile: args.get("peers-file") ?? path19.join(base, "intake", "peers.json"),
10458
+ peersOwnerUid: resolvePeersOwner(args.get("peers-owner"), process.env.COURIER_PEERS_OWNER),
10399
10459
  sentDir: args.get("sent-dir") ?? path19.join(base, "outbox-sent"),
10400
10460
  once: args.has("once"),
10401
10461
  dryRun: args.has("dry-run")
@@ -10995,14 +11055,14 @@ Delivery (one watcher per person per machine; never calls a model):
10995
11055
  watch \u2026 --install-launchd | --install-systemd [--dry-run] [--name n]
10996
11056
  Install the same watch command as a macOS launchd agent or a Linux
10997
11057
  systemd --user unit. --dry-run prints the exact file and commands.
10998
- intake [--queue-dir d] [--peers-file f] [--state-dir d] [--interval 60] [--once]
11058
+ intake [--queue-dir d] [--peers-file f] [--peers-owner u] [--state-dir d] [--interval 60] [--once]
10999
11059
  Intake reader: copy paired peers' mirrored messages into the local queue
11000
11060
  (one <msgId>.json per message) with cheap checks. Never calls a model.
11001
11061
  outbox add <peer> (--thread <id> | --new <subject>) [--text t | --file f | stdin] [--outbox-dir d]
11002
11062
  Queue one message for a paired peer as a single-use outbox item. Text limit:
11003
11063
  2000 characters (8 KiB UTF-8); subject limit: 200 characters.
11004
11064
  outbox serve --pipe <socket> [--outbox-dir d] [--socket-group courier-sender] [--peers-file f]
11005
- [--sent-dir d] [--once] [--dry-run]
11065
+ [--peers-owner u] [--sent-dir d] [--once] [--dry-run]
11006
11066
  Sender: accept one-line item requests over a Unix socket and send the
11007
11067
  named outbox item. Replies are ok or refused:<code>.
11008
11068