@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/dist/artifacts.js CHANGED
@@ -2177,16 +2177,17 @@ var KVService = class extends BaseService {
2177
2177
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
2178
2178
  }
2179
2179
  );
2180
+ if (response.status === 401 || response.status === 403) {
2181
+ const errorText = await response.text();
2182
+ const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
2183
+ const { resource, action: requiredAction } = parseAuthError(errorText);
2184
+ return err(authUnauthorizedError("kv", message, {
2185
+ status: response.status,
2186
+ ...requiredAction && { requiredAction },
2187
+ ...resource && { resource }
2188
+ }));
2189
+ }
2180
2190
  if (!response.ok) {
2181
- if (response.status === 401) {
2182
- const errorText2 = await response.text();
2183
- const { resource, action } = parseAuthError(errorText2);
2184
- return err(authUnauthorizedError("kv", errorText2, {
2185
- status: response.status,
2186
- ...action && { requiredAction: action },
2187
- ...resource && { resource }
2188
- }));
2189
- }
2190
2191
  const errorText = await response.text();
2191
2192
  if (response.status === 412) {
2192
2193
  return err(serviceError(
@@ -6927,11 +6928,15 @@ function canonicalizeCaveats(caveats) {
6927
6928
  import { randomUUID } from "crypto";
6928
6929
  import { AsyncLocalStorage } from "async_hooks";
6929
6930
  import {
6931
+ chmod,
6932
+ link,
6930
6933
  mkdir,
6931
6934
  readFile,
6935
+ readdir,
6932
6936
  rename,
6933
6937
  rmdir,
6934
6938
  rm,
6939
+ stat,
6935
6940
  writeFile
6936
6941
  } from "fs/promises";
6937
6942
  import { homedir } from "os";
@@ -6940,8 +6945,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
6940
6945
  var DEFAULT_LOCK_RETRY_MS = 25;
6941
6946
  var DEFAULT_STALE_LOCK_MS = 3e4;
6942
6947
  var TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
6948
+ var PRIVATE_FILE_MODE = 384;
6949
+ var PRIVATE_DIR_MODE = 448;
6943
6950
  var TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
6951
+ var TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
6952
+ var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
6953
+ var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
6954
+ var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
6955
+ var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
6956
+ var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
6957
+ var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
6944
6958
  var invocationStateRoot = new AsyncLocalStorage();
6959
+ var heldProfileLocks = new AsyncLocalStorage();
6945
6960
  var ProfileLockTimeoutError = class extends Error {
6946
6961
  constructor(profile, timeoutMs) {
6947
6962
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -6968,9 +6983,6 @@ function profileStoreMetadataPath(profile, store) {
6968
6983
  function profileLockPath(profile) {
6969
6984
  return join(profilePath(profile), ".lock");
6970
6985
  }
6971
- function profileLockMetadataPath(profile) {
6972
- return join(profileLockPath(profile), "owner.json");
6973
- }
6974
6986
  async function readJson(filePath) {
6975
6987
  try {
6976
6988
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -6987,9 +6999,9 @@ async function writeJsonAtomic(filePath, value) {
6987
6999
  );
6988
7000
  const contents = `${JSON.stringify(value, null, 2)}
6989
7001
  `;
6990
- await mkdir(directory, { recursive: true });
7002
+ await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
6991
7003
  try {
6992
- await writeFile(temporaryPath, contents, "utf8");
7004
+ await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
6993
7005
  await rename(temporaryPath, filePath);
6994
7006
  } catch (error) {
6995
7007
  await rm(temporaryPath, { force: true }).catch(() => void 0);
@@ -7019,10 +7031,15 @@ async function readProfileStore(profile, store) {
7019
7031
  }
7020
7032
  async function withProfileLock(profile, action, options = {}) {
7021
7033
  const normalizedProfile = validateProfileName(profile);
7034
+ const lockPath = profileLockPath(normalizedProfile);
7035
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
7036
+ if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
7022
7037
  const release = await acquireProfileLock(normalizedProfile, options);
7038
+ const ownership = { lockPath, active: true };
7023
7039
  try {
7024
- return await action();
7040
+ return await heldProfileLocks.run([...held, ownership], action);
7025
7041
  } finally {
7042
+ ownership.active = false;
7026
7043
  await release();
7027
7044
  }
7028
7045
  }
@@ -7049,28 +7066,18 @@ async function acquireProfileLock(profile, options) {
7049
7066
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7050
7067
  const startedAt = Date.now();
7051
7068
  const lockPath = profileLockPath(profile);
7052
- await mkdir(profilePath(profile), { recursive: true });
7069
+ await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
7070
+ for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
7071
+ await chmod(directory, PRIVATE_DIR_MODE);
7072
+ }
7053
7073
  while (true) {
7054
- try {
7055
- await mkdir(lockPath);
7056
- const token = randomUUID();
7057
- try {
7058
- await writeJsonAtomic(profileLockMetadataPath(profile), {
7059
- pid: process.pid,
7060
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7061
- token
7062
- });
7063
- } catch (error) {
7064
- await rmdir(lockPath).catch(() => void 0);
7065
- throw error;
7066
- }
7074
+ const token = randomUUID();
7075
+ if (await publishProfileLock(profile, lockPath, token)) {
7067
7076
  return async () => {
7068
- await releaseProfileLock(lockPath, token);
7077
+ await releaseProfileLock(profile, lockPath, token);
7069
7078
  };
7070
- } catch (error) {
7071
- if (!isLockAlreadyHeld(error)) throw error;
7072
- await signalTestLockContention(profile);
7073
7079
  }
7080
+ await signalTestLockContention(profile);
7074
7081
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7075
7082
  const elapsedMs = Date.now() - startedAt;
7076
7083
  if (elapsedMs >= timeoutMs) {
@@ -7079,21 +7086,35 @@ async function acquireProfileLock(profile, options) {
7079
7086
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7080
7087
  }
7081
7088
  }
7082
- async function releaseProfileLock(lockPath, token) {
7083
- const ownerPath = join(lockPath, "owner.json");
7084
- const claimPath = join(lockPath, `.release-${token}.json`);
7089
+ async function publishProfileLock(profile, lockPath, token) {
7085
7090
  try {
7086
- await rename(ownerPath, claimPath);
7091
+ await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
7087
7092
  } catch (error) {
7088
- if (isErrno(error, "ENOENT")) return;
7093
+ if (isErrno(error, "EEXIST")) return false;
7089
7094
  throw error;
7090
7095
  }
7091
- const claimed = await readJson(claimPath).catch(() => null);
7092
- if (claimed?.token !== token) {
7093
- await rename(claimPath, ownerPath).catch(() => void 0);
7094
- return;
7096
+ await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
7097
+ const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
7098
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
7099
+ try {
7100
+ await writeFile(staged, `${JSON.stringify(owner, null, 2)}
7101
+ `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7102
+ await link(staged, join(lockPath, "owner.json"));
7103
+ return true;
7104
+ } catch (error) {
7105
+ if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
7106
+ await rmdir(lockPath).catch(() => void 0);
7107
+ throw error;
7108
+ } finally {
7109
+ await rm(staged, { force: true }).catch(() => void 0);
7095
7110
  }
7096
- await rm(claimPath, { force: true });
7111
+ }
7112
+ async function releaseProfileLock(profile, lockPath, token) {
7113
+ const ownerPath = join(lockPath, "owner.json");
7114
+ const owner = await readJson(ownerPath).catch(() => null);
7115
+ if (owner?.token !== token) return;
7116
+ await rm(ownerPath, { force: true });
7117
+ await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
7097
7118
  await rmdir(lockPath).catch(() => void 0);
7098
7119
  }
7099
7120
  async function signalTestLockContention(profile) {
@@ -7110,32 +7131,79 @@ async function signalTestLockContention(profile) {
7110
7131
  await writeFile(signalPath, `${profile}
7111
7132
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7112
7133
  }
7134
+ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7135
+ try {
7136
+ const { mtimeMs } = await stat(lockPath);
7137
+ if (Date.now() - mtimeMs < staleAfterMs) return false;
7138
+ } catch {
7139
+ return false;
7140
+ }
7141
+ const checkedAt = performance.now();
7142
+ await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
7143
+ try {
7144
+ const entries = await readdir(lockPath);
7145
+ if (entries.includes("owner.json")) return false;
7146
+ if (recoveryFenced(checkedAt, staleAfterMs)) return false;
7147
+ for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
7148
+ await rm(join(lockPath, name), { force: true });
7149
+ }
7150
+ await rmdir(lockPath);
7151
+ } catch {
7152
+ return false;
7153
+ }
7154
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7155
+ return true;
7156
+ }
7157
+ function recoveryFenced(since, staleAfterMs) {
7158
+ return performance.now() - since >= staleAfterMs / 2;
7159
+ }
7160
+ var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7161
+ var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7162
+ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
7163
+ const directory = dirname(lockPath);
7164
+ const names = await readdir(directory).catch(() => []);
7165
+ for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
7166
+ const path = join(directory, name);
7167
+ const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
7168
+ if (aged) await rm(path, { force: true }).catch(() => void 0);
7169
+ }
7170
+ }
7113
7171
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7114
7172
  const ownerPath = join(lockPath, "owner.json");
7115
7173
  const owner = await readJson(ownerPath).catch(() => null);
7174
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7116
7175
  if (!isStaleOwner(owner, staleAfterMs)) return false;
7117
7176
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7118
- await waitForTestStaleRecoveryBarrier(profile);
7177
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7119
7178
  const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
7120
7179
  try {
7121
- await rename(ownerPath, claimPath);
7122
- } catch (error) {
7123
- if (isErrno(error, "ENOENT")) return false;
7180
+ await link(ownerPath, claimPath);
7181
+ } catch {
7124
7182
  return false;
7125
7183
  }
7184
+ const claimedAt = performance.now();
7185
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7126
7186
  const claimed = await readJson(claimPath).catch(() => null);
7127
7187
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7128
- if (!sameInstance) {
7129
- await rename(claimPath, ownerPath).catch(() => void 0);
7188
+ await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
7189
+ if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
7190
+ await rm(claimPath, { force: true });
7191
+ await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
7130
7192
  return false;
7131
7193
  }
7194
+ if (sameInstance) {
7195
+ await rm(ownerPath, { force: true });
7196
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7197
+ }
7132
7198
  await rm(claimPath, { force: true });
7133
7199
  await rmdir(lockPath).catch(() => void 0);
7200
+ if (!sameInstance) return false;
7201
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7134
7202
  return true;
7135
7203
  }
7136
- async function waitForTestStaleRecoveryBarrier(profile) {
7204
+ async function waitForTestBarrier(environmentName, profile) {
7137
7205
  if (process.env.NODE_ENV !== "test") return;
7138
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
7206
+ const barrierDirectory = process.env[environmentName];
7139
7207
  if (!barrierDirectory) return;
7140
7208
  await mkdir(barrierDirectory, { recursive: true });
7141
7209
  const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -7159,9 +7227,6 @@ function isStaleOwner(owner, staleAfterMs) {
7159
7227
  }
7160
7228
  return !isProcessAlive(owner.pid);
7161
7229
  }
7162
- function isLockAlreadyHeld(error) {
7163
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
7164
- }
7165
7230
  function isProcessAlive(pid) {
7166
7231
  try {
7167
7232
  process.kill(pid, 0);