@tinycloud/operations 0.3.3-beta.9 → 0.3.4-beta.0

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/index.js CHANGED
@@ -3618,16 +3618,17 @@ var init_dist2 = __esm({
3618
3618
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
3619
3619
  }
3620
3620
  );
3621
+ if (response.status === 401 || response.status === 403) {
3622
+ const errorText = await response.text();
3623
+ const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
3624
+ const { resource, action: requiredAction } = parseAuthError(errorText);
3625
+ return err(authUnauthorizedError("kv", message, {
3626
+ status: response.status,
3627
+ ...requiredAction && { requiredAction },
3628
+ ...resource && { resource }
3629
+ }));
3630
+ }
3621
3631
  if (!response.ok) {
3622
- if (response.status === 401) {
3623
- const errorText2 = await response.text();
3624
- const { resource, action } = parseAuthError(errorText2);
3625
- return err(authUnauthorizedError("kv", errorText2, {
3626
- status: response.status,
3627
- ...action && { requiredAction: action },
3628
- ...resource && { resource }
3629
- }));
3630
- }
3631
3632
  const errorText = await response.text();
3632
3633
  if (response.status === 412) {
3633
3634
  return err(serviceError(
@@ -7089,11 +7090,15 @@ var init_authority = __esm({
7089
7090
  import { randomUUID } from "crypto";
7090
7091
  import { AsyncLocalStorage } from "async_hooks";
7091
7092
  import {
7093
+ chmod,
7094
+ link,
7092
7095
  mkdir,
7093
7096
  readFile,
7097
+ readdir,
7094
7098
  rename,
7095
7099
  rmdir,
7096
7100
  rm,
7101
+ stat,
7097
7102
  writeFile
7098
7103
  } from "fs/promises";
7099
7104
  import { homedir } from "os";
@@ -7136,9 +7141,6 @@ function profileStoreMetadataPath(profile, store) {
7136
7141
  function profileLockPath(profile) {
7137
7142
  return join(profilePath(profile), ".lock");
7138
7143
  }
7139
- function profileLockMetadataPath(profile) {
7140
- return join(profileLockPath(profile), "owner.json");
7141
- }
7142
7144
  async function readJson(filePath) {
7143
7145
  try {
7144
7146
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -7155,9 +7157,9 @@ async function writeJsonAtomic(filePath, value) {
7155
7157
  );
7156
7158
  const contents = `${JSON.stringify(value, null, 2)}
7157
7159
  `;
7158
- await mkdir(directory, { recursive: true });
7160
+ await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
7159
7161
  try {
7160
- await writeFile(temporaryPath, contents, "utf8");
7162
+ await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
7161
7163
  await rename(temporaryPath, filePath);
7162
7164
  } catch (error) {
7163
7165
  await rm(temporaryPath, { force: true }).catch(() => void 0);
@@ -7197,10 +7199,15 @@ async function readProfileStore(profile, store) {
7197
7199
  }
7198
7200
  async function withProfileLock(profile, action, options = {}) {
7199
7201
  const normalizedProfile = validateProfileName(profile);
7202
+ const lockPath = profileLockPath(normalizedProfile);
7203
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
7204
+ if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
7200
7205
  const release = await acquireProfileLock(normalizedProfile, options);
7206
+ const ownership = { lockPath, active: true };
7201
7207
  try {
7202
- return await action();
7208
+ return await heldProfileLocks.run([...held, ownership], action);
7203
7209
  } finally {
7210
+ ownership.active = false;
7204
7211
  await release();
7205
7212
  }
7206
7213
  }
@@ -7227,28 +7234,18 @@ async function acquireProfileLock(profile, options) {
7227
7234
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7228
7235
  const startedAt = Date.now();
7229
7236
  const lockPath = profileLockPath(profile);
7230
- await mkdir(profilePath(profile), { recursive: true });
7237
+ await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
7238
+ for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
7239
+ await chmod(directory, PRIVATE_DIR_MODE);
7240
+ }
7231
7241
  while (true) {
7232
- try {
7233
- await mkdir(lockPath);
7234
- const token = randomUUID();
7235
- try {
7236
- await writeJsonAtomic(profileLockMetadataPath(profile), {
7237
- pid: process.pid,
7238
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7239
- token
7240
- });
7241
- } catch (error) {
7242
- await rmdir(lockPath).catch(() => void 0);
7243
- throw error;
7244
- }
7242
+ const token = randomUUID();
7243
+ if (await publishProfileLock(profile, lockPath, token)) {
7245
7244
  return async () => {
7246
- await releaseProfileLock(lockPath, token);
7245
+ await releaseProfileLock(profile, lockPath, token);
7247
7246
  };
7248
- } catch (error) {
7249
- if (!isLockAlreadyHeld(error)) throw error;
7250
- await signalTestLockContention(profile);
7251
7247
  }
7248
+ await signalTestLockContention(profile);
7252
7249
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7253
7250
  const elapsedMs = Date.now() - startedAt;
7254
7251
  if (elapsedMs >= timeoutMs) {
@@ -7257,21 +7254,35 @@ async function acquireProfileLock(profile, options) {
7257
7254
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7258
7255
  }
7259
7256
  }
7260
- async function releaseProfileLock(lockPath, token) {
7261
- const ownerPath = join(lockPath, "owner.json");
7262
- const claimPath = join(lockPath, `.release-${token}.json`);
7257
+ async function publishProfileLock(profile, lockPath, token) {
7263
7258
  try {
7264
- await rename(ownerPath, claimPath);
7259
+ await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
7265
7260
  } catch (error) {
7266
- if (isErrno(error, "ENOENT")) return;
7261
+ if (isErrno(error, "EEXIST")) return false;
7267
7262
  throw error;
7268
7263
  }
7269
- const claimed = await readJson(claimPath).catch(() => null);
7270
- if (claimed?.token !== token) {
7271
- await rename(claimPath, ownerPath).catch(() => void 0);
7272
- return;
7264
+ await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
7265
+ const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
7266
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
7267
+ try {
7268
+ await writeFile(staged, `${JSON.stringify(owner, null, 2)}
7269
+ `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7270
+ await link(staged, join(lockPath, "owner.json"));
7271
+ return true;
7272
+ } catch (error) {
7273
+ if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
7274
+ await rmdir(lockPath).catch(() => void 0);
7275
+ throw error;
7276
+ } finally {
7277
+ await rm(staged, { force: true }).catch(() => void 0);
7273
7278
  }
7274
- await rm(claimPath, { force: true });
7279
+ }
7280
+ async function releaseProfileLock(profile, lockPath, token) {
7281
+ const ownerPath = join(lockPath, "owner.json");
7282
+ const owner = await readJson(ownerPath).catch(() => null);
7283
+ if (owner?.token !== token) return;
7284
+ await rm(ownerPath, { force: true });
7285
+ await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
7275
7286
  await rmdir(lockPath).catch(() => void 0);
7276
7287
  }
7277
7288
  async function signalTestLockContention(profile) {
@@ -7288,32 +7299,77 @@ async function signalTestLockContention(profile) {
7288
7299
  await writeFile(signalPath, `${profile}
7289
7300
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7290
7301
  }
7302
+ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7303
+ try {
7304
+ const { mtimeMs } = await stat(lockPath);
7305
+ if (Date.now() - mtimeMs < staleAfterMs) return false;
7306
+ } catch {
7307
+ return false;
7308
+ }
7309
+ const checkedAt = performance.now();
7310
+ await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
7311
+ try {
7312
+ const entries = await readdir(lockPath);
7313
+ if (entries.includes("owner.json")) return false;
7314
+ if (recoveryFenced(checkedAt, staleAfterMs)) return false;
7315
+ for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
7316
+ await rm(join(lockPath, name), { force: true });
7317
+ }
7318
+ await rmdir(lockPath);
7319
+ } catch {
7320
+ return false;
7321
+ }
7322
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7323
+ return true;
7324
+ }
7325
+ function recoveryFenced(since, staleAfterMs) {
7326
+ return performance.now() - since >= staleAfterMs / 2;
7327
+ }
7328
+ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
7329
+ const directory = dirname(lockPath);
7330
+ const names = await readdir(directory).catch(() => []);
7331
+ for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
7332
+ const path = join(directory, name);
7333
+ const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
7334
+ if (aged) await rm(path, { force: true }).catch(() => void 0);
7335
+ }
7336
+ }
7291
7337
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7292
7338
  const ownerPath = join(lockPath, "owner.json");
7293
7339
  const owner = await readJson(ownerPath).catch(() => null);
7340
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7294
7341
  if (!isStaleOwner(owner, staleAfterMs)) return false;
7295
7342
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7296
- await waitForTestStaleRecoveryBarrier(profile);
7343
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7297
7344
  const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
7298
7345
  try {
7299
- await rename(ownerPath, claimPath);
7300
- } catch (error) {
7301
- if (isErrno(error, "ENOENT")) return false;
7346
+ await link(ownerPath, claimPath);
7347
+ } catch {
7302
7348
  return false;
7303
7349
  }
7350
+ const claimedAt = performance.now();
7351
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7304
7352
  const claimed = await readJson(claimPath).catch(() => null);
7305
7353
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7306
- if (!sameInstance) {
7307
- await rename(claimPath, ownerPath).catch(() => void 0);
7354
+ await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
7355
+ if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
7356
+ await rm(claimPath, { force: true });
7357
+ await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
7308
7358
  return false;
7309
7359
  }
7360
+ if (sameInstance) {
7361
+ await rm(ownerPath, { force: true });
7362
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7363
+ }
7310
7364
  await rm(claimPath, { force: true });
7311
7365
  await rmdir(lockPath).catch(() => void 0);
7366
+ if (!sameInstance) return false;
7367
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7312
7368
  return true;
7313
7369
  }
7314
- async function waitForTestStaleRecoveryBarrier(profile) {
7370
+ async function waitForTestBarrier(environmentName, profile) {
7315
7371
  if (process.env.NODE_ENV !== "test") return;
7316
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
7372
+ const barrierDirectory = process.env[environmentName];
7317
7373
  if (!barrierDirectory) return;
7318
7374
  await mkdir(barrierDirectory, { recursive: true });
7319
7375
  const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -7337,9 +7393,6 @@ function isStaleOwner(owner, staleAfterMs) {
7337
7393
  }
7338
7394
  return !isProcessAlive(owner.pid);
7339
7395
  }
7340
- function isLockAlreadyHeld(error) {
7341
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
7342
- }
7343
7396
  function isProcessAlive(pid) {
7344
7397
  try {
7345
7398
  process.kill(pid, 0);
@@ -7360,7 +7413,7 @@ function isErrno(error, code) {
7360
7413
  function sleep(milliseconds) {
7361
7414
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
7362
7415
  }
7363
- var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, TEST_LOCK_RECOVERY_BARRIER_DIR, invocationStateRoot, ProfileLockTimeoutError;
7416
+ var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_FENCED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, invocationStateRoot, heldProfileLocks, ProfileLockTimeoutError, ABANDONED_CLAIM, STAGED_OWNER;
7364
7417
  var init_state = __esm({
7365
7418
  "src/state.ts"() {
7366
7419
  "use strict";
@@ -7369,8 +7422,18 @@ var init_state = __esm({
7369
7422
  DEFAULT_LOCK_RETRY_MS = 25;
7370
7423
  DEFAULT_STALE_LOCK_MS = 3e4;
7371
7424
  TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
7425
+ PRIVATE_FILE_MODE = 384;
7426
+ PRIVATE_DIR_MODE = 448;
7372
7427
  TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
7428
+ TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
7429
+ TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
7430
+ TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
7431
+ TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
7432
+ TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
7433
+ TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
7434
+ TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
7373
7435
  invocationStateRoot = new AsyncLocalStorage();
7436
+ heldProfileLocks = new AsyncLocalStorage();
7374
7437
  ProfileLockTimeoutError = class extends Error {
7375
7438
  constructor(profile, timeoutMs) {
7376
7439
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -7378,6 +7441,8 @@ var init_state = __esm({
7378
7441
  this.name = "ProfileLockTimeoutError";
7379
7442
  }
7380
7443
  };
7444
+ ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7445
+ STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7381
7446
  }
7382
7447
  });
7383
7448
 
@@ -10060,7 +10125,7 @@ var init_profile = __esm({
10060
10125
  "src/profile.ts"() {
10061
10126
  "use strict";
10062
10127
  init_state();
10063
- DEFAULT_HOST = "https://node.tinycloud.xyz";
10128
+ DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
10064
10129
  }
10065
10130
  });
10066
10131