@tinycloud/operations 0.3.3-beta.9 → 0.3.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/coverage.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "source": "packages/cli/src/command-registry.ts",
4
4
  "commands": [
5
5
  { "command": "init", "status": "legacy" },
6
+ { "command": "context", "status": "excluded", "reason": "Read-only local CLI context diagnostics; performs no storage operation." },
6
7
  { "command": "enable", "status": "legacy" },
7
8
  { "command": "enable share", "status": "legacy" },
8
9
  { "command": "auth", "status": "legacy" },
@@ -2201,16 +2201,17 @@ var KVService = class extends BaseService {
2201
2201
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
2202
2202
  }
2203
2203
  );
2204
+ if (response.status === 401 || response.status === 403) {
2205
+ const errorText = await response.text();
2206
+ const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
2207
+ const { resource, action: requiredAction } = parseAuthError(errorText);
2208
+ return err(authUnauthorizedError("kv", message, {
2209
+ status: response.status,
2210
+ ...requiredAction && { requiredAction },
2211
+ ...resource && { resource }
2212
+ }));
2213
+ }
2204
2214
  if (!response.ok) {
2205
- if (response.status === 401) {
2206
- const errorText2 = await response.text();
2207
- const { resource, action } = parseAuthError(errorText2);
2208
- return err(authUnauthorizedError("kv", errorText2, {
2209
- status: response.status,
2210
- ...action && { requiredAction: action },
2211
- ...resource && { resource }
2212
- }));
2213
- }
2214
2215
  const errorText = await response.text();
2215
2216
  if (response.status === 412) {
2216
2217
  return err(serviceError(
@@ -6957,8 +6958,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
6957
6958
  var DEFAULT_LOCK_RETRY_MS = 25;
6958
6959
  var DEFAULT_STALE_LOCK_MS = 3e4;
6959
6960
  var TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
6961
+ var PRIVATE_FILE_MODE = 384;
6962
+ var PRIVATE_DIR_MODE = 448;
6960
6963
  var TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
6964
+ var TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
6965
+ var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
6966
+ var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
6967
+ var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
6968
+ var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
6969
+ var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
6970
+ var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
6961
6971
  var invocationStateRoot = new import_node_async_hooks.AsyncLocalStorage();
6972
+ var heldProfileLocks = new import_node_async_hooks.AsyncLocalStorage();
6962
6973
  var ProfileLockTimeoutError = class extends Error {
6963
6974
  constructor(profile, timeoutMs) {
6964
6975
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -6985,9 +6996,6 @@ function profileStoreMetadataPath(profile, store) {
6985
6996
  function profileLockPath(profile) {
6986
6997
  return (0, import_node_path.join)(profilePath(profile), ".lock");
6987
6998
  }
6988
- function profileLockMetadataPath(profile) {
6989
- return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
6990
- }
6991
6999
  async function readJson(filePath) {
6992
7000
  try {
6993
7001
  return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
@@ -7004,9 +7012,9 @@ async function writeJsonAtomic(filePath, value) {
7004
7012
  );
7005
7013
  const contents = `${JSON.stringify(value, null, 2)}
7006
7014
  `;
7007
- await (0, import_promises.mkdir)(directory, { recursive: true });
7015
+ await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
7008
7016
  try {
7009
- await (0, import_promises.writeFile)(temporaryPath, contents, "utf8");
7017
+ await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
7010
7018
  await (0, import_promises.rename)(temporaryPath, filePath);
7011
7019
  } catch (error) {
7012
7020
  await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
@@ -7036,10 +7044,15 @@ async function readProfileStore(profile, store) {
7036
7044
  }
7037
7045
  async function withProfileLock(profile, action, options = {}) {
7038
7046
  const normalizedProfile = validateProfileName(profile);
7047
+ const lockPath = profileLockPath(normalizedProfile);
7048
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
7049
+ if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
7039
7050
  const release = await acquireProfileLock(normalizedProfile, options);
7051
+ const ownership = { lockPath, active: true };
7040
7052
  try {
7041
- return await action();
7053
+ return await heldProfileLocks.run([...held, ownership], action);
7042
7054
  } finally {
7055
+ ownership.active = false;
7043
7056
  await release();
7044
7057
  }
7045
7058
  }
@@ -7066,28 +7079,18 @@ async function acquireProfileLock(profile, options) {
7066
7079
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7067
7080
  const startedAt = Date.now();
7068
7081
  const lockPath = profileLockPath(profile);
7069
- await (0, import_promises.mkdir)(profilePath(profile), { recursive: true });
7082
+ await (0, import_promises.mkdir)(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
7083
+ for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
7084
+ await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
7085
+ }
7070
7086
  while (true) {
7071
- try {
7072
- await (0, import_promises.mkdir)(lockPath);
7073
- const token = (0, import_node_crypto.randomUUID)();
7074
- try {
7075
- await writeJsonAtomic(profileLockMetadataPath(profile), {
7076
- pid: process.pid,
7077
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7078
- token
7079
- });
7080
- } catch (error) {
7081
- await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7082
- throw error;
7083
- }
7087
+ const token = (0, import_node_crypto.randomUUID)();
7088
+ if (await publishProfileLock(profile, lockPath, token)) {
7084
7089
  return async () => {
7085
- await releaseProfileLock(lockPath, token);
7090
+ await releaseProfileLock(profile, lockPath, token);
7086
7091
  };
7087
- } catch (error) {
7088
- if (!isLockAlreadyHeld(error)) throw error;
7089
- await signalTestLockContention(profile);
7090
7092
  }
7093
+ await signalTestLockContention(profile);
7091
7094
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7092
7095
  const elapsedMs = Date.now() - startedAt;
7093
7096
  if (elapsedMs >= timeoutMs) {
@@ -7096,21 +7099,35 @@ async function acquireProfileLock(profile, options) {
7096
7099
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7097
7100
  }
7098
7101
  }
7099
- async function releaseProfileLock(lockPath, token) {
7100
- const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7101
- const claimPath = (0, import_node_path.join)(lockPath, `.release-${token}.json`);
7102
+ async function publishProfileLock(profile, lockPath, token) {
7102
7103
  try {
7103
- await (0, import_promises.rename)(ownerPath, claimPath);
7104
+ await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
7104
7105
  } catch (error) {
7105
- if (isErrno(error, "ENOENT")) return;
7106
+ if (isErrno(error, "EEXIST")) return false;
7106
7107
  throw error;
7107
7108
  }
7108
- const claimed = await readJson(claimPath).catch(() => null);
7109
- if (claimed?.token !== token) {
7110
- await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
7111
- return;
7109
+ await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
7110
+ const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
7111
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
7112
+ try {
7113
+ await (0, import_promises.writeFile)(staged, `${JSON.stringify(owner, null, 2)}
7114
+ `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7115
+ await (0, import_promises.link)(staged, (0, import_node_path.join)(lockPath, "owner.json"));
7116
+ return true;
7117
+ } catch (error) {
7118
+ if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
7119
+ await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7120
+ throw error;
7121
+ } finally {
7122
+ await (0, import_promises.rm)(staged, { force: true }).catch(() => void 0);
7112
7123
  }
7113
- await (0, import_promises.rm)(claimPath, { force: true });
7124
+ }
7125
+ async function releaseProfileLock(profile, lockPath, token) {
7126
+ const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7127
+ const owner = await readJson(ownerPath).catch(() => null);
7128
+ if (owner?.token !== token) return;
7129
+ await (0, import_promises.rm)(ownerPath, { force: true });
7130
+ await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
7114
7131
  await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7115
7132
  }
7116
7133
  async function signalTestLockContention(profile) {
@@ -7127,32 +7144,79 @@ async function signalTestLockContention(profile) {
7127
7144
  await (0, import_promises.writeFile)(signalPath, `${profile}
7128
7145
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7129
7146
  }
7147
+ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7148
+ try {
7149
+ const { mtimeMs } = await (0, import_promises.stat)(lockPath);
7150
+ if (Date.now() - mtimeMs < staleAfterMs) return false;
7151
+ } catch {
7152
+ return false;
7153
+ }
7154
+ const checkedAt = performance.now();
7155
+ await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
7156
+ try {
7157
+ const entries = await (0, import_promises.readdir)(lockPath);
7158
+ if (entries.includes("owner.json")) return false;
7159
+ if (recoveryFenced(checkedAt, staleAfterMs)) return false;
7160
+ for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
7161
+ await (0, import_promises.rm)((0, import_node_path.join)(lockPath, name), { force: true });
7162
+ }
7163
+ await (0, import_promises.rmdir)(lockPath);
7164
+ } catch {
7165
+ return false;
7166
+ }
7167
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7168
+ return true;
7169
+ }
7170
+ function recoveryFenced(since, staleAfterMs) {
7171
+ return performance.now() - since >= staleAfterMs / 2;
7172
+ }
7173
+ var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7174
+ var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7175
+ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
7176
+ const directory = (0, import_node_path.dirname)(lockPath);
7177
+ const names = await (0, import_promises.readdir)(directory).catch(() => []);
7178
+ for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
7179
+ const path = (0, import_node_path.join)(directory, name);
7180
+ const aged = await (0, import_promises.stat)(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
7181
+ if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
7182
+ }
7183
+ }
7130
7184
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7131
7185
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7132
7186
  const owner = await readJson(ownerPath).catch(() => null);
7187
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7133
7188
  if (!isStaleOwner(owner, staleAfterMs)) return false;
7134
7189
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7135
- await waitForTestStaleRecoveryBarrier(profile);
7190
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7136
7191
  const claimPath = (0, import_node_path.join)(lockPath, `.stale-${(0, import_node_crypto.randomUUID)()}.json`);
7137
7192
  try {
7138
- await (0, import_promises.rename)(ownerPath, claimPath);
7139
- } catch (error) {
7140
- if (isErrno(error, "ENOENT")) return false;
7193
+ await (0, import_promises.link)(ownerPath, claimPath);
7194
+ } catch {
7141
7195
  return false;
7142
7196
  }
7197
+ const claimedAt = performance.now();
7198
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7143
7199
  const claimed = await readJson(claimPath).catch(() => null);
7144
7200
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7145
- if (!sameInstance) {
7146
- await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
7201
+ await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
7202
+ if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
7203
+ await (0, import_promises.rm)(claimPath, { force: true });
7204
+ await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
7147
7205
  return false;
7148
7206
  }
7207
+ if (sameInstance) {
7208
+ await (0, import_promises.rm)(ownerPath, { force: true });
7209
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7210
+ }
7149
7211
  await (0, import_promises.rm)(claimPath, { force: true });
7150
7212
  await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7213
+ if (!sameInstance) return false;
7214
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7151
7215
  return true;
7152
7216
  }
7153
- async function waitForTestStaleRecoveryBarrier(profile) {
7217
+ async function waitForTestBarrier(environmentName, profile) {
7154
7218
  if (process.env.NODE_ENV !== "test") return;
7155
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
7219
+ const barrierDirectory = process.env[environmentName];
7156
7220
  if (!barrierDirectory) return;
7157
7221
  await (0, import_promises.mkdir)(barrierDirectory, { recursive: true });
7158
7222
  const readyPath = (0, import_node_path.join)(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -7176,9 +7240,6 @@ function isStaleOwner(owner, staleAfterMs) {
7176
7240
  }
7177
7241
  return !isProcessAlive(owner.pid);
7178
7242
  }
7179
- function isLockAlreadyHeld(error) {
7180
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
7181
- }
7182
7243
  function isProcessAlive(pid) {
7183
7244
  try {
7184
7245
  process.kill(pid, 0);