@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/coverage.json +1 -0
- package/dist/artifacts.cjs +115 -54
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +30 -30
- package/dist/artifacts.d.ts +30 -30
- package/dist/artifacts.js +119 -54
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +117 -56
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +121 -56
- package/dist/cli-runtime.js.map +1 -1
- package/dist/index.cjs +117 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +121 -56
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +2 -1
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +6 -1
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +105 -42
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +8 -0
- package/dist/state.d.ts +8 -0
- package/dist/state.js +109 -42
- package/dist/state.js.map +1 -1
- package/package.json +3 -3
package/dist/cli-runtime.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
|
-
|
|
7233
|
-
|
|
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
|
|
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
|
|
7259
|
+
await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7265
7260
|
} catch (error) {
|
|
7266
|
-
if (isErrno(error, "
|
|
7261
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7267
7262
|
throw error;
|
|
7268
7263
|
}
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
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
|
-
|
|
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
|
|
7343
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7297
7344
|
const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
|
|
7298
7345
|
try {
|
|
7299
|
-
await
|
|
7300
|
-
} catch
|
|
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
|
-
|
|
7307
|
-
|
|
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
|
|
7370
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7315
7371
|
if (process.env.NODE_ENV !== "test") return;
|
|
7316
|
-
const barrierDirectory = process.env[
|
|
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
|
|